//! Custom `tutdock` URI scheme protocol for serving video, subtitles, and fonts.
//!
//! On Windows the actual URL seen by the handler is:
//! `http://tutdock.localhost/<path>` (Tauri v2 Windows behaviour).
//! On macOS / Linux it would be `tutdock://localhost/<path>`.
//!
//! Routes:
//!   /video/{index}          — video file with Range support
//!   /sub/{libid}/{fid}      — stored VTT subtitle
//!   /fonts.css              — cached Google Fonts CSS
//!   /fonts/{filename}       — cached font file
//!   /fa.css                 — cached Font Awesome CSS
//!   /fa/webfonts/{filename} — cached FA webfont

use std::fs::{self, File};
use std::io::{Read, Seek, SeekFrom};
use std::path::PathBuf;
use std::sync::Mutex;

use tauri::http::{self, header, StatusCode};
use tauri::Manager;

use crate::library::Library;
use crate::AppPaths;

/// Maximum chunk size for full-file responses (4 MB).
const MAX_CHUNK: u64 = 4 * 1024 * 1024;

/// CORS header name.
const CORS: &str = "Access-Control-Allow-Origin";

/// Register the `tutdock` custom protocol on the builder.
pub fn register_protocol(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
    builder.register_uri_scheme_protocol("tutdock", move |ctx, request| {
        let app = ctx.app_handle();
        handle_request(app, &request)
    })
}

// ---------------------------------------------------------------------------
// Router
// ---------------------------------------------------------------------------

fn handle_request(
    app: &tauri::AppHandle,
    request: &http::Request<Vec<u8>>,
) -> http::Response<Vec<u8>> {
    let uri = request.uri();
    let path = uri.path();

    // Strip leading slash.
    let path = path.strip_prefix('/').unwrap_or(path);

    // Route.
    if let Some(rest) = path.strip_prefix("video/") {
        return handle_video(app, request, rest);
    }
    if let Some(rest) = path.strip_prefix("sub/") {
        return handle_subtitle(app, rest);
    }
    if path == "fonts.css" {
        return handle_fonts_css(app);
    }
    if let Some(rest) = path.strip_prefix("fonts/") {
        return handle_font_file(app, rest);
    }
    if path == "fa.css" {
        return handle_fa_css(app);
    }
    if let Some(rest) = path.strip_prefix("fa/webfonts/") {
        return handle_fa_webfont(app, rest);
    }

    not_found()
}

// ---------------------------------------------------------------------------
// Video — with Range request support
// ---------------------------------------------------------------------------

fn handle_video(
    app: &tauri::AppHandle,
    request: &http::Request<Vec<u8>>,
    index_str: &str,
) -> http::Response<Vec<u8>> {
    let index: usize = match index_str.parse() {
        Ok(i) => i,
        Err(_) => return bad_request("invalid index"),
    };

    let state = app.state::<Mutex<Library>>();
    let file_path = {
        let lib = match state.lock() {
            Ok(l) => l,
            Err(_) => return internal_error("lock error"),
        };
        match lib.get_video_path(index) {
            Ok(p) => p,
            Err(_) => return not_found(),
        }
    };

    let metadata = match fs::metadata(&file_path) {
        Ok(m) => m,
        Err(_) => return not_found(),
    };
    let file_size = metadata.len();

    let ext = file_path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("");
    let mime = mime_for_video(ext);

    // Check for Range header.
    let range_header = request
        .headers()
        .get(header::RANGE)
        .and_then(|v| v.to_str().ok())
        .map(|s| s.to_string());

    match range_header {
        Some(range_str) => serve_range(&file_path, file_size, &range_str, mime),
        None => serve_full(&file_path, file_size, mime),
    }
}

/// Serve the full file (or first MAX_CHUNK bytes).
fn serve_full(path: &PathBuf, file_size: u64, mime: &str) -> http::Response<Vec<u8>> {
    let read_len = file_size.min(MAX_CHUNK) as usize;
    let mut buf = vec![0u8; read_len];
    let mut file = match File::open(path) {
        Ok(f) => f,
        Err(_) => return internal_error("cannot open file"),
    };
    if file.read_exact(&mut buf).is_err() {
        return internal_error("read error");
    }

    http::Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, mime)
        .header(header::CONTENT_LENGTH, file_size.to_string())
        .header(header::ACCEPT_RANGES, "bytes")
        .header(header::CACHE_CONTROL, "no-store")
        .header(CORS, "*")
        .body(buf)
        .unwrap_or_else(|_| internal_error("response build error"))
}

/// Serve a byte range (206 Partial Content).
fn serve_range(
    path: &PathBuf,
    file_size: u64,
    range_str: &str,
    mime: &str,
) -> http::Response<Vec<u8>> {
    let (start, end) = match parse_range(range_str, file_size) {
        Some(r) => r,
        None => return range_not_satisfiable(file_size),
    };

    let length = end - start + 1;
    let mut buf = vec![0u8; length as usize];

    let mut file = match File::open(path) {
        Ok(f) => f,
        Err(_) => return internal_error("cannot open file"),
    };
    if file.seek(SeekFrom::Start(start)).is_err() {
        return internal_error("seek error");
    }
    if file.read_exact(&mut buf).is_err() {
        return internal_error("read error");
    }

    http::Response::builder()
        .status(StatusCode::PARTIAL_CONTENT)
        .header(header::CONTENT_TYPE, mime)
        .header(
            header::CONTENT_RANGE,
            format!("bytes {}-{}/{}", start, end, file_size),
        )
        .header(header::CONTENT_LENGTH, length.to_string())
        .header(header::ACCEPT_RANGES, "bytes")
        .header(header::CACHE_CONTROL, "no-store")
        .header(CORS, "*")
        .body(buf)
        .unwrap_or_else(|_| internal_error("response build error"))
}

/// Parse `Range: bytes=START-END` header.
fn parse_range(header: &str, file_size: u64) -> Option<(u64, u64)> {
    let s = header.strip_prefix("bytes=")?;
    let (start_s, end_s) = s.split_once('-')?;

    let start = if start_s.is_empty() {
        let suffix: u64 = end_s.parse().ok()?;
        file_size.checked_sub(suffix)?
    } else {
        start_s.parse().ok()?
    };

    let end = if end_s.is_empty() {
        file_size - 1
    } else {
        let e: u64 = end_s.parse().ok()?;
        e.min(file_size - 1)
    };

    if start > end || start >= file_size {
        return None;
    }

    Some((start, end))
}

// ---------------------------------------------------------------------------
// Subtitles
// ---------------------------------------------------------------------------

fn handle_subtitle(app: &tauri::AppHandle, rest: &str) -> http::Response<Vec<u8>> {
    // rest is a VTT filename like "{fid}_{name}.vtt"
    let filename = rest;

    // Validate: no path traversal
    if filename.contains("..") || filename.contains('/') || filename.contains('\\') || filename.is_empty() {
        return not_found();
    }

    let paths = app.state::<AppPaths>();
    let vtt_path = paths.subs_dir.join(filename);

    // Ensure resolved path is still inside subs_dir
    if let (Ok(base), Ok(resolved)) = (paths.subs_dir.canonicalize(), vtt_path.canonicalize()) {
        if !resolved.starts_with(&base) {
            return not_found();
        }
    } else {
        return not_found();
    }

    let data = match fs::read(&vtt_path) {
        Ok(d) => d,
        Err(_) => return not_found(),
    };

    http::Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, "text/vtt; charset=utf-8")
        .header(header::CACHE_CONTROL, "no-store")
        .header(CORS, "*")
        .body(data)
        .unwrap_or_else(|_| internal_error("response build error"))
}

// ---------------------------------------------------------------------------
// Fonts
// ---------------------------------------------------------------------------

fn handle_fonts_css(app: &tauri::AppHandle) -> http::Response<Vec<u8>> {
    let paths = app.state::<AppPaths>();
    let css_path = paths.fonts_dir.join("fonts.css");

    let data = if css_path.exists() {
        fs::read(&css_path).unwrap_or_default()
    } else {
        Vec::new()
    };

    http::Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, "text/css; charset=utf-8")
        .header(header::CACHE_CONTROL, "no-store")
        .body(data)
        .unwrap_or_else(|_| internal_error("response build error"))
}

fn handle_font_file(app: &tauri::AppHandle, filename: &str) -> http::Response<Vec<u8>> {
    let paths = app.state::<AppPaths>();
    serve_static_file(&paths.fonts_dir, filename)
}

fn handle_fa_css(app: &tauri::AppHandle) -> http::Response<Vec<u8>> {
    let paths = app.state::<AppPaths>();
    let css_path = paths.fa_dir.join("fa.css");

    let data = if css_path.exists() {
        fs::read(&css_path).unwrap_or_default()
    } else {
        Vec::new()
    };

    http::Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, "text/css; charset=utf-8")
        .header(header::CACHE_CONTROL, "no-store")
        .body(data)
        .unwrap_or_else(|_| internal_error("response build error"))
}

fn handle_fa_webfont(app: &tauri::AppHandle, filename: &str) -> http::Response<Vec<u8>> {
    let paths = app.state::<AppPaths>();
    let webfonts_dir = paths.fa_dir.join("webfonts");
    serve_static_file(&webfonts_dir, filename)
}

// ---------------------------------------------------------------------------
// Static file helper
// ---------------------------------------------------------------------------

fn serve_static_file(base_dir: &PathBuf, filename: &str) -> http::Response<Vec<u8>> {
    if filename.contains("..") || filename.contains('\\') || filename.contains('/') {
        return not_found();
    }

    let file_path = base_dir.join(filename);

    let canonical_base = match base_dir.canonicalize() {
        Ok(p) => p,
        Err(_) => return not_found(),
    };
    let canonical_file = match file_path.canonicalize() {
        Ok(p) => p,
        Err(_) => return not_found(),
    };
    if !canonical_file.starts_with(&canonical_base) {
        return not_found();
    }

    let data = match fs::read(&file_path) {
        Ok(d) => d,
        Err(_) => return not_found(),
    };

    let mime = guess_mime(filename);

    http::Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, mime)
        .header(header::CACHE_CONTROL, "no-store")
        .header(CORS, "*")
        .body(data)
        .unwrap_or_else(|_| internal_error("response build error"))
}

// ---------------------------------------------------------------------------
// MIME helpers
// ---------------------------------------------------------------------------

fn mime_for_video(ext: &str) -> &'static str {
    match ext.to_ascii_lowercase().as_str() {
        "mp4" | "m4v" => "video/mp4",
        "webm" => "video/webm",
        "ogv" => "video/ogg",
        "mov" => "video/quicktime",
        "mkv" => "video/x-matroska",
        "avi" => "video/x-msvideo",
        "mpeg" | "mpg" => "video/mpeg",
        "m2ts" | "mts" => "video/mp2t",
        _ => "application/octet-stream",
    }
}

fn guess_mime(filename: &str) -> &'static str {
    let ext = filename.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
    match ext.as_str() {
        "css" => "text/css; charset=utf-8",
        "woff" => "font/woff",
        "woff2" => "font/woff2",
        "ttf" => "font/ttf",
        "otf" => "font/otf",
        "eot" => "application/vnd.ms-fontobject",
        "svg" => "image/svg+xml",
        _ => "application/octet-stream",
    }
}

// ---------------------------------------------------------------------------
// Error responses
// ---------------------------------------------------------------------------

fn not_found() -> http::Response<Vec<u8>> {
    http::Response::builder()
        .status(StatusCode::NOT_FOUND)
        .header(header::CONTENT_TYPE, "text/plain")
        .body(b"Not Found".to_vec())
        .unwrap()
}

fn bad_request(msg: &str) -> http::Response<Vec<u8>> {
    http::Response::builder()
        .status(StatusCode::BAD_REQUEST)
        .header(header::CONTENT_TYPE, "text/plain")
        .body(msg.as_bytes().to_vec())
        .unwrap()
}

fn internal_error(msg: &str) -> http::Response<Vec<u8>> {
    http::Response::builder()
        .status(StatusCode::INTERNAL_SERVER_ERROR)
        .header(header::CONTENT_TYPE, "text/plain")
        .body(msg.as_bytes().to_vec())
        .unwrap()
}

fn range_not_satisfiable(file_size: u64) -> http::Response<Vec<u8>> {
    http::Response::builder()
        .status(StatusCode::RANGE_NOT_SATISFIABLE)
        .header(header::CONTENT_RANGE, format!("bytes */{}", file_size))
        .body(Vec::new())
        .unwrap()
}
