//! The shared tiny_http request loop.
//!
//! `serve` starts a thread-per-request HTTP server on `cfg.port`,
//! registers the instance in `~/.adom/adom-video-post/instances/`,
//! opens a Hydrogen webview tab pointed at the server, and spins
//! on incoming requests until a `/shutdown` request arrives or
//! the process is killed.
//!
//! Built-in routes are dispatched BEFORE delegating to the app's
//! `Routes::handle`, so apps cannot override them (nor would they
//! want to — they're app-neutral infrastructure).

use super::{console, eval, registry, tab, Routes, WebappConfig};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tiny_http::{Header, Method, Request, Response, Server};

/// The eval-client JS block that gets injected into every UI in
/// place of the `<!-- EVAL_CLIENT -->` marker at serve time.
const EVAL_CLIENT_JS: &str = include_str!("eval_client.js");

/// Blocking entrypoint. Returns on clean shutdown or on bind error.
pub fn serve(cfg: WebappConfig, routes: Arc<dyn Routes>) -> Result<(), String> {
    let addr = format!("127.0.0.1:{}", cfg.port);
    let server = Server::http(&addr).map_err(|e| format!("bind {}: {}", addr, e))?;

    // Register in the instance directory so `adom-video-post list` / ctrl
    // can find us. Any error here is non-fatal — we log and continue.
    let registry_entry = registry::InstanceEntry {
        pid: std::process::id(),
        port: cfg.port,
        subcommand: cfg.subcommand.clone(),
        title: cfg.title.clone(),
    };
    let registry_path = match registry::register(&registry_entry) {
        Ok(p) => Some(p),
        Err(e) => {
            eprintln!("warning: could not register instance: {}", e);
            None
        }
    };

    // Open the Hydrogen webview tab. If this fails, we tear down
    // the server and return — a webapp with no UI is pointless.
    //
    // Append an epoch query param so the Hydrogen webview iframe
    // doesn't reuse a cached copy of the UI HTML from a previous
    // instance that was running on this same port. Combined with
    // the Cache-Control: no-store on serve_ui, this guarantees
    // the first fetch of a freshly-restarted server lands a fresh
    // response, no reload needed.
    let base_url = tab::resolve_url(cfg.port);
    let sep = if base_url.contains('?') { '&' } else { '?' };
    let epoch = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);
    let url = format!("{}{}v={}", base_url, sep, epoch);
    println!("webapp URL: {}", url);
    if cfg.no_open {
        // --no-open: HTTP server only, no Hydrogen tab. The user is
        // driving the UI in pup or via browser_eval and explicitly
        // does NOT want a webview tab to appear.
        println!("webapp running headless on port {} (--no-open)", cfg.port);
    } else if let Err(e) = tab::open(
        &cfg.tab_name,
        &url,
        &cfg.display_icon,
        cfg.panel_id_override.as_deref(),
    ) {
        if let Some(p) = &registry_path {
            registry::unregister(p);
        }
        return Err(e);
    } else {
        println!("webapp tab '{}' open on port {}", cfg.tab_name, cfg.port);
    }

    // Each subcommand gets its own console + eval queue.
    let console = console::new();
    let eval_queue = eval::new();
    let should_exit = Arc::new(AtomicBool::new(false));

    // Each request gets its own worker thread. Without this, a
    // long-poll endpoint like /eval/pending (which waits up to 25s
    // on a Condvar) would block every other request on the single
    // request loop — and since the UI's eval_client poll loop keeps
    // a /eval/pending request open continuously, nothing else would
    // ever respond. Spawning per-request is fine because tiny_http's
    // `Request::respond` is thread-safe and the handlers are
    // self-contained.
    loop {
        if should_exit.load(Ordering::Relaxed) {
            break;
        }
        // Short timeout so the loop can notice should_exit promptly.
        let request = match server.recv_timeout(std::time::Duration::from_millis(250)) {
            Ok(Some(r)) => r,
            Ok(None) => continue,
            Err(e) => {
                eprintln!("webapp accept error: {}", e);
                continue;
            }
        };

        let routes_for_thread = Arc::clone(&routes);
        let console_for_thread = Arc::clone(&console);
        let eval_for_thread = Arc::clone(&eval_queue);
        let should_exit_for_thread = Arc::clone(&should_exit);

        std::thread::spawn(move || {
            let method = request.method().clone();
            let url_full = request.url().to_string();
            let url_path = url_full
                .split('?')
                .next()
                .unwrap_or(&url_full)
                .to_string();

            let handled = dispatch(
                request,
                &method,
                &url_path,
                &url_full,
                &routes_for_thread,
                &console_for_thread,
                &eval_for_thread,
                &should_exit_for_thread,
            );
            if let Err(e) = handled {
                eprintln!("webapp error on {} {}: {}", method, url_path, e);
            }
        });
    }

    // Clean shutdown: remove the webview tab and the registry entry.
    let _ = tab::remove(&cfg.tab_name);
    if let Some(p) = &registry_path {
        registry::unregister(p);
    }
    println!("webapp shut down");
    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn dispatch(
    request: Request,
    method: &Method,
    url_path: &str,
    url_full: &str,
    routes: &Arc<dyn Routes>,
    console: &console::SharedConsole,
    eval_queue: &eval::SharedEval,
    should_exit: &Arc<AtomicBool>,
) -> Result<(), String> {
    // --- Built-in routes, handled before the app sees anything.
    match (method, url_path) {
        (Method::Get, "/") | (Method::Get, "/index.html") => {
            return serve_ui(request, routes.ui_html());
        }
        (Method::Get, "/favicon.svg") | (Method::Get, "/favicon.ico") => {
            return serve_favicon(request, routes.favicon_svg());
        }
        (Method::Get, "/console") => {
            return console::handle_get(request, console, url_full);
        }
        (Method::Post, "/console") => {
            return console::handle_post(request, console);
        }
        (Method::Delete, "/console") => {
            return console::handle_delete(request, console);
        }
        (Method::Post, "/eval") => {
            return eval::handle_post(request, eval_queue);
        }
        (Method::Get, "/eval/pending") => {
            return eval::handle_get_pending(request, eval_queue);
        }
        (Method::Post, "/shutdown") | (Method::Get, "/shutdown") => {
            let _ = request.respond(text_response(200, "OK"));
            should_exit.store(true, Ordering::Relaxed);
            return Ok(());
        }
        _ => {}
    }

    // /eval/:id/result is dynamic — match the prefix/suffix. Both
    // GET (caller polls for a result) and POST (UI submits a result)
    // land here.
    if url_path.starts_with("/eval/") && url_path.ends_with("/result") {
        let id = &url_path["/eval/".len()..url_path.len() - "/result".len()];
        match method {
            Method::Get => return eval::handle_get_result(request, eval_queue, id),
            Method::Post => return eval::handle_post_result(request, eval_queue, id),
            _ => {}
        }
    }

    // --- Nothing matched. Delegate to the app.
    match routes.handle(request, method, url_path) {
        super::RouteOutcome::Handled(result) => result,
        super::RouteOutcome::NotHandled(req) => req
            .respond(text_response(404, "not found"))
            .map_err(|e| e.to_string()),
    }
}

fn serve_ui(request: Request, ui_html: &str) -> Result<(), String> {
    let injected = ui_html.replace("<!-- EVAL_CLIENT -->", EVAL_CLIENT_JS);
    // Cache-Control: no-store is essential. Without it, restarting
    // the server on the same port causes the Hydrogen webview to
    // reuse the previously-cached HTML from the dead instance —
    // meaning code changes don't land until the user hits reload.
    // That's an explicit no-no per the "never ask the user to
    // refresh" rule. Force-fresh every fetch.
    let resp = Response::from_string(injected)
        .with_header(
            Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..]).unwrap(),
        )
        .with_header(
            Header::from_bytes(&b"Cache-Control"[..], &b"no-store, must-revalidate"[..]).unwrap(),
        );
    request.respond(resp).map_err(|e| e.to_string())
}

fn serve_favicon(request: Request, svg: &str) -> Result<(), String> {
    // Each subcommand's `Routes::favicon_svg()` returns its own SVG
    // bytes — this is what ends up in the Hydrogen tab chip after
    // page load, per the hydrogen-tab-icons skill.
    let resp = Response::from_string(svg)
        .with_header(Header::from_bytes(&b"Content-Type"[..], &b"image/svg+xml"[..]).unwrap())
        .with_header(
            Header::from_bytes(&b"Cache-Control"[..], &b"no-cache"[..]).unwrap(),
        );
    request.respond(resp).map_err(|e| e.to_string())
}

fn text_response(status: u16, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_string(body)
        .with_status_code(status)
        .with_header(Header::from_bytes(&b"Content-Type"[..], &b"text/plain"[..]).unwrap())
}