//! `adom-parts-search app` — Hydrogen webview showing unified parts search
//! across all three vendors in a 3-column grid with product photos.
//!
//! The app owns its own HTTP server (tiny_http). When the UI POSTs to
//! /search, the server delegates to vendors::search_all which fires
//! three parallel HTTP calls to the existing vendor backends.

pub mod tab;

use crate::vendors;
use crate::{note, ok, warn, AppArgs};
use serde_json::{json, Value};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use tiny_http::{Header, Method, Response, Server};

const UI_HTML: &str = include_str!("ui.html");
const ICON_SVG: &str = include_str!("../../docs/icon.svg");
const CONSOLE_MAX: usize = 500;

struct AppState {
    mouser_api: Option<String>,
    digikey_api: Option<String>,
    jlcpcb_api: Option<String>,
    dev: bool,
    console: Mutex<VecDeque<Value>>,
    last_query: Mutex<Option<String>>,
    last_results: Mutex<Option<Value>>,
    last_query_ts: Mutex<u128>,
    pending_eval: Mutex<Option<(String, String)>>,
    eval_results: Mutex<std::collections::HashMap<String, Value>>,
}

fn json_resp(code: u16, body: Value) -> Response<std::io::Cursor<Vec<u8>>> {
    // Every response — HTML, JSON, SVG — carries the embed-friendly CSP
    // so Hydrogen's "Some sites block embedding" warning doesn't trip on
    // any request it sniffs (it probes more than just /).
    Response::from_string(body.to_string())
        .with_status_code(code)
        .with_header(Header::from_bytes("Content-Type", "application/json").unwrap())
        .with_header(Header::from_bytes("Cache-Control", "no-cache").unwrap())
        .with_header(Header::from_bytes("Content-Security-Policy", "frame-ancestors *").unwrap())
}

fn read_body(req: &mut tiny_http::Request) -> Result<Value, String> {
    use std::io::Read;
    let mut body = String::new();
    req.as_reader().read_to_string(&mut body).map_err(|e| format!("read body: {}", e))?;
    if body.is_empty() { return Ok(Value::Null); }
    serde_json::from_str(&body).map_err(|e| format!("parse JSON: {}", e))
}

pub fn run(args: AppArgs) -> Result<(), String> {
    ok(format!("adom-parts-search app starting on port {}", args.port));
    note!("  backends:");
    note!("    mouser:  {}", args.mouser_api.clone().unwrap_or(vendors::DEFAULT_MOUSER.into()));
    note!("    digikey: {}", args.digikey_api.clone().unwrap_or(vendors::DEFAULT_DIGIKEY.into()));
    note!("    jlcpcb:  {}", args.jlcpcb_api.clone().unwrap_or(vendors::DEFAULT_JLCPCB.into()));

    let state = Arc::new(AppState {
        mouser_api: args.mouser_api.clone(),
        digikey_api: args.digikey_api.clone(),
        jlcpcb_api: args.jlcpcb_api.clone(),
        dev: args.dev,
        console: Mutex::new(VecDeque::with_capacity(CONSOLE_MAX)),
        last_query: Mutex::new(None),
        last_results: Mutex::new(None),
        last_query_ts: Mutex::new(0),
        pending_eval: Mutex::new(None),
        eval_results: Mutex::new(std::collections::HashMap::new()),
    });

    let addr = format!("0.0.0.0:{}", args.port);
    let server = Server::http(&addr).map_err(|e| format!("bind {}: {}", addr, e))?;

    if !args.no_tab {
        let proxy_url = tab::proxy_url(args.port);
        match tab::add_webview_tab(&args.tab_name, &proxy_url, ICON_SVG) {
            Ok(_) => ok(format!("opened Hydrogen tab '{}' → {}", args.tab_name, proxy_url)),
            Err(e) => warn(format!("couldn't open tab: {}. Manually: {}", e, proxy_url)),
        }
    }
    note!("  {}Press ctrl-c or POST /shutdown to stop.{}", crate::DIM, crate::RESET);

    for mut req in server.incoming_requests() {
        let url = req.url().to_string();
        let path = url.split_once('?').map(|(p, _)| p).unwrap_or(&url).to_string();
        let method = req.method().clone();
        let state_ref = Arc::clone(&state);

        let result: (u16, Value) = match (method.clone(), path.as_str()) {
            (Method::Get, "/") | (Method::Get, "/index.html") => {
                // Explicitly declare we are embed-safe so Hydrogen's webview
                // doesn't show its "Some sites block embedding" banner. We
                // allow framing from any origin (the container's Coder proxy
                // hostname varies per user) but require same-origin for
                // everything else. No `X-Frame-Options` — modern browsers
                // prefer `frame-ancestors` and emitting both can contradict.
                let _ = req.respond(
                    Response::from_string(UI_HTML).with_status_code(200)
                        .with_header(Header::from_bytes("Content-Type", "text/html; charset=utf-8").unwrap())
                        .with_header(Header::from_bytes("Cache-Control", "no-cache").unwrap())
                        .with_header(Header::from_bytes("Content-Security-Policy", "frame-ancestors *").unwrap()),
                );
                continue;
            }
            (Method::Get, "/favicon.svg") => {
                let _ = req.respond(
                    Response::from_string(ICON_SVG).with_status_code(200)
                        .with_header(Header::from_bytes("Content-Type", "image/svg+xml").unwrap())
                        .with_header(Header::from_bytes("Content-Security-Policy", "frame-ancestors *").unwrap()),
                );
                continue;
            }
            (Method::Get, "/state") => {
                let q = state_ref.last_query.lock().unwrap().clone();
                let r = state_ref.last_results.lock().unwrap().clone();
                let ts = *state_ref.last_query_ts.lock().unwrap();
                (200, json!({
                    "dev": state_ref.dev,
                    "last_query": q,
                    "last_query_ts": ts as u64,
                    "last_results": r,
                    "backends": {
                        "mouser": state_ref.mouser_api.clone().unwrap_or(vendors::DEFAULT_MOUSER.into()),
                        "digikey": state_ref.digikey_api.clone().unwrap_or(vendors::DEFAULT_DIGIKEY.into()),
                        "jlcpcb": state_ref.jlcpcb_api.clone().unwrap_or(vendors::DEFAULT_JLCPCB.into()),
                    }
                }))
            }
            (Method::Post, "/search") => match read_body(&mut req) {
                Ok(body) => {
                    let keyword = body.get("keyword").and_then(|v| v.as_str()).unwrap_or("").to_string();
                    let limit = body.get("limit").and_then(|v| v.as_u64()).unwrap_or(5) as u32;
                    let in_stock = body.get("inStockOnly").and_then(|v| v.as_bool()).unwrap_or(false);
                    if keyword.trim().is_empty() {
                        (400, json!({"error": "keyword required"}))
                    } else {
                        let agg = vendors::search_all(
                            &keyword, limit, in_stock,
                            state_ref.mouser_api.as_deref(),
                            state_ref.digikey_api.as_deref(),
                            state_ref.jlcpcb_api.as_deref(),
                        );
                        let v = serde_json::to_value(&agg).unwrap_or(json!({}));
                        *state_ref.last_query.lock().unwrap() = Some(keyword);
                        *state_ref.last_results.lock().unwrap() = Some(v.clone());
                        *state_ref.last_query_ts.lock().unwrap() = std::time::SystemTime::now()
                            .duration_since(std::time::UNIX_EPOCH)
                            .map(|d| d.as_millis()).unwrap_or(0);
                        (200, v)
                    }
                }
                Err(e) => (400, json!({"error": e})),
            },
            (Method::Post, "/console") => match read_body(&mut req) {
                Ok(v) => {
                    let mut c = state_ref.console.lock().unwrap();
                    if c.len() >= CONSOLE_MAX { c.pop_front(); }
                    c.push_back(v);
                    (200, json!({"ok": true}))
                }
                Err(e) => (400, json!({"error": e})),
            },
            (Method::Get, "/console") => {
                let c: Vec<Value> = state_ref.console.lock().unwrap().iter().cloned().collect();
                (200, json!({"messages": c}))
            }
            (Method::Post, "/eval") if state_ref.dev => match read_body(&mut req) {
                Ok(v) => {
                    let code = v.get("code").and_then(|v| v.as_str()).unwrap_or("").to_string();
                    let id = format!("ev{:x}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos());
                    *state_ref.pending_eval.lock().unwrap() = Some((id.clone(), code));
                    (200, json!({"id": id}))
                }
                Err(e) => (400, json!({"error": e})),
            },
            (Method::Get, "/eval/pending") if state_ref.dev => {
                let p = state_ref.pending_eval.lock().unwrap().take();
                match p {
                    Some((id, code)) => (200, json!({"id": id, "code": code})),
                    None => (200, json!({})),
                }
            }
            (Method::Post, path) if state_ref.dev && path.starts_with("/eval/") && path.ends_with("/result") => {
                let id = path.trim_start_matches("/eval/").trim_end_matches("/result").to_string();
                match read_body(&mut req) {
                    Ok(v) => {
                        let r = v.get("result").cloned().unwrap_or(Value::Null);
                        state_ref.eval_results.lock().unwrap().insert(id, r);
                        (200, json!({"ok": true}))
                    }
                    Err(e) => (400, json!({"error": e})),
                }
            }
            (Method::Post, "/shutdown") => {
                if !args.no_tab { let _ = tab::remove_webview_tab(&args.tab_name); }
                let _ = req.respond(json_resp(200, json!({"ok": true, "shutdown": true})));
                ok("shutdown requested, exiting");
                std::process::exit(0);
            }
            _ => (404, json!({"error": format!("unknown endpoint: {} {}", method, path)})),
        };
        let _ = req.respond(json_resp(result.0, result.1));
    }
    Ok(())
}