//! Custom `brittle://` URI scheme handler.
//!
//! Routes:
//!   `brittle://app/viewer?ref_id=<uuid>`   — the PDF viewer HTML page
//!   `brittle://app/pdfjs/<path>`            — pdfjs-dist static assets
//!   `brittle://app/pdf?ref_id=<uuid>`       — raw PDF bytes from the repository
//!
//! The pure routing and path-resolution logic lives in the `routing` sub-module
//! so it can be unit-tested without a running Tauri application.

use std::path::PathBuf;
use tauri::{
    http::{header, Request, Response, StatusCode},
    AppHandle, Manager, Runtime,
};

use crate::state::AppState;

// ── Embedded assets ───────────────────────────────────────────────────────────

static VIEWER_HTML: &[u8] = include_bytes!("../assets/viewer/index.html");
static PDFJS_MIN_JS: &[u8] = include_bytes!(
    "../assets/viewer/pdfjs/pdf.min.js"
);
static PDFJS_WORKER_JS: &[u8] = include_bytes!(
    "../assets/viewer/pdfjs/pdf.worker.min.js"
);

// Viewer JS modules (served at brittle://app/viewer/<file>)
// Note: We now serve the compiled JavaScript files from TypeScript
static VIEWER_JS: &[u8]            = include_bytes!("../assets/viewer/viewer.js");
static VIEWPORT_TRACKER_JS: &[u8]  = include_bytes!("../assets/viewer/viewport-tracker.js");
static PAGE_MANAGER_ENHANCED_JS: &[u8] = include_bytes!("../assets/viewer/page-manager-enhanced.js");
static ZOOM_CONTROLLER_ENHANCED_JS: &[u8] = include_bytes!("../assets/viewer/zoom-controller-enhanced.js");
static RENDER_WORKER_JS: &[u8]     = include_bytes!("../assets/viewer/render-worker.js");
static RENDER_SYSTEM_JS: &[u8]     = include_bytes!("../assets/viewer/render-system.js");
static PERFORMANCE_MANAGER_JS: &[u8] = include_bytes!("../assets/viewer/performance-manager.js");
static VISIBILITY_MANAGER_JS: &[u8] = include_bytes!("../assets/viewer/visibility-manager.js");
static MESSAGE_BRIDGE_JS: &[u8]    = include_bytes!("../assets/viewer/message-bridge.js");
static ANNOTATION_LAYER_JS: &[u8]  = include_bytes!("../assets/viewer/annotation-layer.js");

// ── Public API ────────────────────────────────────────────────────────────────

/// Entry point registered with `tauri::Builder::register_uri_scheme_protocol`.
///
/// Generic over the Tauri runtime so the function can be used from a closure
/// in the builder without knowing the concrete runtime at compile time.
pub fn handle<R: Runtime>(app: &AppHandle<R>, req: &Request<Vec<u8>>) -> Response<Vec<u8>> {
    let uri = req.uri();

    match routing::classify(uri.path(), uri.query()) {
        routing::Route::Viewer { ref_id } => serve_viewer(&ref_id),
        routing::Route::ViewerAsset { file_name } => serve_viewer_asset(&file_name),
        routing::Route::PdfjsAsset { rel_path } => {
            if rel_path.contains("..") {
                return response_403();
            }
            serve_pdfjs_file(&rel_path)
        }
        routing::Route::Pdf { ref_id } => serve_pdf(app, &ref_id),
        routing::Route::NotFound => response_404(),
    }
}

// ── Route handlers ────────────────────────────────────────────────────────────

fn serve_viewer(_ref_id: &str) -> Response<Vec<u8>> {
    // The viewer reads `ref_id` from its own URL query string via JavaScript
    // (`new URLSearchParams(location.search).get("ref_id")`), so no substitution
    // into the HTML template is needed.
    response_ok(VIEWER_HTML.to_vec(), "text/html; charset=utf-8")
}

fn serve_viewer_asset(file_name: &str) -> Response<Vec<u8>> {
    let bytes: &[u8] = match file_name {
        "viewer.js"           => VIEWER_JS,
        "viewport-tracker.js" => VIEWPORT_TRACKER_JS,
        "page-manager-enhanced.js" => PAGE_MANAGER_ENHANCED_JS,
        "zoom-controller-enhanced.js" => ZOOM_CONTROLLER_ENHANCED_JS,
        "render-worker.js"      => RENDER_WORKER_JS,
        "render-system.js"      => RENDER_SYSTEM_JS,
        "performance-manager.js" => PERFORMANCE_MANAGER_JS,
        "visibility-manager.js" => VISIBILITY_MANAGER_JS,
        "message-bridge.js"     => MESSAGE_BRIDGE_JS,
        "annotation-layer.js"   => ANNOTATION_LAYER_JS,
        _                       => return response_404(),
    };
    response_ok(bytes.to_vec(), "application/javascript; charset=utf-8")
}

fn serve_pdfjs_file(rel_path: &str) -> Response<Vec<u8>> {
    let bytes: &[u8] = match rel_path {
        "build/pdf.min.js"        => PDFJS_MIN_JS,
        "build/pdf.worker.min.js" => PDFJS_WORKER_JS,
        _                         => return response_404(),
    };
    let mut resp = response_ok(bytes.to_vec(), "application/javascript; charset=utf-8");
    resp.headers_mut().insert(
        header::CACHE_CONTROL,
        "public, max-age=3600".parse().unwrap(),
    );
    resp
}

fn serve_pdf<R: Runtime>(app: &AppHandle<R>, ref_id: &str) -> Response<Vec<u8>> {
    use brittle_core::{model::ids::ReferenceId, store::FsStore, Brittle};
    use uuid::Uuid;

    let uuid = match Uuid::parse_str(ref_id) {
        Ok(u) => u,
        Err(_) => return response_400("invalid ref_id: not a valid UUID"),
    };
    let rid = ReferenceId::from(uuid);

    let state = app.state::<AppState>();
    let pdf_path: Result<PathBuf, String> =
        state.with_repo_read(|b: &Brittle<FsStore>| b.get_pdf_path(rid));

    match pdf_path {
        Err(e) => response_404_msg(&e),
        Ok(path) => match std::fs::read(&path) {
            Ok(bytes) => {
                let mut resp = response_ok(bytes, "application/pdf");
                resp.headers_mut()
                    .insert(header::CACHE_CONTROL, "no-store".parse().unwrap());
                resp
            }
            Err(e) => response_500(&e.to_string()),
        },
    }
}

// ── Response builders ─────────────────────────────────────────────────────────

fn response_ok(body: Vec<u8>, content_type: &str) -> Response<Vec<u8>> {
    Response::builder()
        .header(header::CONTENT_TYPE, content_type)
        .header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*")
        .status(StatusCode::OK)
        .body(body)
        .unwrap()
}

fn response_404() -> Response<Vec<u8>> {
    Response::builder()
        .status(StatusCode::NOT_FOUND)
        .body(b"Not Found".to_vec())
        .unwrap()
}

fn response_404_msg(msg: &str) -> Response<Vec<u8>> {
    Response::builder()
        .status(StatusCode::NOT_FOUND)
        .body(msg.as_bytes().to_vec())
        .unwrap()
}

fn response_400(msg: &str) -> Response<Vec<u8>> {
    Response::builder()
        .status(StatusCode::BAD_REQUEST)
        .body(msg.as_bytes().to_vec())
        .unwrap()
}

fn response_403() -> Response<Vec<u8>> {
    Response::builder()
        .status(StatusCode::FORBIDDEN)
        .body(b"Forbidden".to_vec())
        .unwrap()
}

fn response_500(msg: &str) -> Response<Vec<u8>> {
    Response::builder()
        .status(StatusCode::INTERNAL_SERVER_ERROR)
        .body(msg.as_bytes().to_vec())
        .unwrap()
}

// ── Pure routing logic (unit-testable) ───────────────────────────────────────

pub mod routing {

    #[derive(Debug, PartialEq)]
    pub enum Route {
        Viewer { ref_id: String },
        ViewerAsset { file_name: String },
        PdfjsAsset { rel_path: String },
        Pdf { ref_id: String },
        NotFound,
    }

    /// Classify a `brittle://app/{path}?{query}` request into a `Route`.
    ///
    /// - `/viewer`          (no further path segments) → HTML shell
    /// - `/viewer/<file>`   (one path segment)         → viewer JS module
    /// - `/pdfjs/<rel>`                                → PDF.js asset
    /// - `/pdf`                                        → raw PDF bytes
    pub fn classify(path: &str, query: Option<&str>) -> Route {
        let ref_id = extract_ref_id(query);

        if path == "/viewer" {
            Route::Viewer { ref_id }
        } else if let Some(rest) = path.strip_prefix("/viewer/") {
            // Reject any path that contains directory separators or dots
            // at the start, to prevent traversal.
            if rest.contains('/') || rest.starts_with('.') {
                Route::NotFound
            } else {
                Route::ViewerAsset { file_name: rest.to_owned() }
            }
        } else if let Some(rel) = path.strip_prefix("/pdfjs/") {
            Route::PdfjsAsset {
                rel_path: rel.to_owned(),
            }
        } else if path == "/pdf" {
            Route::Pdf { ref_id }
        } else {
            Route::NotFound
        }
    }

    /// Extract the value of `ref_id=…` from a URL query string.
    pub fn extract_ref_id(query: Option<&str>) -> String {
        query
            .unwrap_or("")
            .split('&')
            .find_map(|part| part.strip_prefix("ref_id="))
            .unwrap_or("")
            .to_owned()
    }


    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn viewer_route() {
            let r = classify("/viewer", Some("ref_id=abc-123"));
            assert_eq!(
                r,
                Route::Viewer {
                    ref_id: "abc-123".into()
                }
            );
        }

        #[test]
        fn viewer_route_no_ref_id() {
            let r = classify("/viewer", None);
            assert_eq!(r, Route::Viewer { ref_id: "".into() });
        }

        #[test]
        fn pdfjs_asset_route_build_file() {
            let r = classify("/pdfjs/build/pdf.min.js", None);
            assert_eq!(
                r,
                Route::PdfjsAsset {
                    rel_path: "build/pdf.min.js".into()
                }
            );
        }

        #[test]
        fn pdfjs_asset_route_nested() {
            let r = classify("/pdfjs/web/pdf_viewer.css", None);
            assert_eq!(
                r,
                Route::PdfjsAsset {
                    rel_path: "web/pdf_viewer.css".into()
                }
            );
        }

        #[test]
        fn pdf_route() {
            let r = classify("/pdf", Some("ref_id=01234567-89ab-cdef-0123-456789abcdef"));
            assert_eq!(
                r,
                Route::Pdf {
                    ref_id: "01234567-89ab-cdef-0123-456789abcdef".into()
                }
            );
        }

        #[test]
        fn viewer_asset_route() {
            let r = classify("/viewer/viewer.js", None);
            assert_eq!(r, Route::ViewerAsset { file_name: "viewer.js".into() });
        }

        #[test]
        fn viewer_asset_route_all_modules() {
            for name in &[
                "viewer.js",
                "viewport-tracker.js",
                "page-manager-enhanced.js",
                "zoom-controller-enhanced.js",
                "render-worker.js",
                "render-system.js",
                "performance-manager.js",
                "visibility-manager.js",
                "message-bridge.js",
            ] {
                let path = format!("/viewer/{}", name);
                assert_eq!(
                    classify(&path, None),
                    Route::ViewerAsset { file_name: name.to_string() }
                );
            }
        }

        #[test]
        fn viewer_asset_traversal_blocked() {
            // Directory traversal via nested path → NotFound
            assert_eq!(classify("/viewer/../secret.js", None), Route::NotFound);
            assert_eq!(classify("/viewer/sub/file.js", None), Route::NotFound);
        }

        #[test]
        fn unknown_paths_are_not_found() {
            assert_eq!(classify("/unknown", None), Route::NotFound);
            assert_eq!(classify("/", None), Route::NotFound);
            assert_eq!(classify("/favicon.ico", None), Route::NotFound);
        }

        #[test]
        fn extract_ref_id_from_compound_query() {
            let id = extract_ref_id(Some("foo=bar&ref_id=my-id&baz=1"));
            assert_eq!(id, "my-id");
        }

        #[test]
        fn extract_ref_id_missing_returns_empty() {
            assert_eq!(extract_ref_id(None), "");
            assert_eq!(extract_ref_id(Some("foo=bar")), "");
        }

        #[test]
        fn path_traversal_rel_path_contains_dotdot() {
            // The handler rejects rel_paths containing ".."; verify the routing
            // surfaces them so the handler can block them.
            if let Route::PdfjsAsset { rel_path } = classify("/pdfjs/../secret", None) {
                assert!(rel_path.contains(".."));
            } else {
                panic!("expected PdfjsAsset route");
            }
        }
    }
}
