//! adom-layout-viewer live app server — the AI-drivable surface + the embeddable live PCB
//! view (same app-creator contract as adom-footprint/adom-symbol). Routes:
//! GET / (UI), /viewer.js, /favicon.svg, /state (poller), POST /load (push a
//! .kicad_pcb), GET /enrich (component sidecar), /eval + /console, /shutdown.

use anyhow::Result;
use serde_json::{json, Value};
use std::collections::{HashMap, VecDeque};
use std::io::Read;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use tiny_http::{Header, Method, Response, Server};

#[derive(Default, Clone)]
pub struct AppState {
    pub name: String,
    pub svg: String,
    pub meta_json: String, // serialized RenderMeta
    pub kicad_pcb: String, // retained for re-render / delivery
}

static STATE: Mutex<Option<AppState>> = Mutex::new(None);
static EVAL_PENDING: Mutex<VecDeque<(u64, String)>> = Mutex::new(VecDeque::new());
static EVAL_RESULTS: Mutex<Option<HashMap<u64, Value>>> = Mutex::new(None);
static CONSOLE: Mutex<Option<Vec<String>>> = Mutex::new(None);
static ENRICH_CACHE: Mutex<Option<HashMap<String, String>>> = Mutex::new(None);
static ENRICH_PENDING: Mutex<Option<std::collections::HashSet<String>>> = Mutex::new(None);
static EVAL_ID: AtomicU64 = AtomicU64::new(1);
// AI channel: narrate (POST /ui/toast) + drive (POST /ui/cmd); the UI drains both
// via GET /ui/pull. (Complements the JS /eval channel with a user-visible one.)
static TOASTS: Mutex<Vec<Value>> = Mutex::new(Vec::new());
static CMDS: Mutex<Vec<Value>> = Mutex::new(Vec::new());

const APP_HTML: &str = include_str!("app.html");
const VIEWER_JS: &str = include_str!("viewer.js");
const FAVICON: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#00e6dc" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8" cy="8" r="1.4"/><circle cx="16" cy="16" r="1.4"/><path d="M8 9.5v3l4 3.5"/></svg>"##;

pub fn set_state(s: AppState) { *STATE.lock().unwrap() = Some(s); }

pub fn load_pcb(name: &str, kicad_pcb: &str) -> Result<()> {
    let (svg, meta) = crate::pcb_render::render_board(kicad_pcb);
    // pre-warm enrichment for every component that carries an MPN, so hover is
    // instant. Sequential in ONE background thread — vendor APIs are slow and we
    // don't want to fan out N live requests at once.
    let mpns: Vec<(String, String)> = meta.components.iter()
        .filter(|c| !c.mpn.is_empty())
        .map(|c| (c.mpn.clone(), c.lcsc.clone())).collect();
    set_state(AppState {
        name: name.to_string(), svg,
        meta_json: serde_json::to_string(&meta)?,
        kicad_pcb: kicad_pcb.to_string(),
    });
    if !mpns.is_empty() {
        std::thread::spawn(move || {
            for (mpn, lcsc) in mpns { let _ = enrich_fill(&mpn, &lcsc); }
        });
    }
    Ok(())
}

/// Compute (blocking) and cache one enrichment record. Returns the JSON.
fn enrich_fill(mpn: &str, lcsc: &str) -> String {
    let key = format!("{mpn}|{lcsc}");
    let out = crate::enrich::lookup(mpn, lcsc);
    if let Some(m) = ENRICH_CACHE.lock().unwrap().as_mut() { m.insert(key.clone(), out.clone()); }
    if let Some(p) = ENRICH_PENDING.lock().unwrap().as_mut() { p.remove(&key); }
    out
}

fn state_json() -> String {
    match &*STATE.lock().unwrap() {
        Some(s) => format!(r#"{{"name":{},"svg":{},"meta":{}}}"#,
            serde_json::to_string(&s.name).unwrap(),
            serde_json::to_string(&s.svg).unwrap(),
            if s.meta_json.is_empty() { "{}" } else { &s.meta_json }),
        None => "{}".to_string(),
    }
}

pub fn run(port: u16) -> Result<()> {
    *EVAL_RESULTS.lock().unwrap() = Some(HashMap::new());
    *CONSOLE.lock().unwrap() = Some(Vec::new());
    *ENRICH_CACHE.lock().unwrap() = Some(HashMap::new());
    *ENRICH_PENDING.lock().unwrap() = Some(std::collections::HashSet::new());
    let server = Server::http(format!("0.0.0.0:{port}")).map_err(|e| anyhow::anyhow!("bind {port}: {e}"))?;
    eprintln!("[adom-layout-viewer] serving on http://127.0.0.1:{port}");
    register_port(port, "adom-layout-viewer", "Interactive PCB layout view (AI-drivable)");
    for mut req in server.incoming_requests() {
        let raw = req.url().to_string();
        let url = raw.split('?').next().unwrap_or("/").to_string();
        let query = raw.splitn(2, '?').nth(1).unwrap_or("").to_string();
        let method = req.method().clone();
        let mut body = String::new();
        if matches!(method, Method::Post) { let _ = req.as_reader().read_to_string(&mut body); }
        let resp = route(&method, &url, &query, &body);
        let _ = req.respond(resp);
        if url == "/shutdown" && matches!(method, Method::Post) { break; }
    }
    unregister_port(port);
    Ok(())
}

fn route(method: &Method, url: &str, query: &str, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    match (method, url) {
        (Method::Get, "/") | (Method::Get, "/index.html") => resp(APP_HTML, "text/html; charset=utf-8"),
        (Method::Get, "/viewer.js") => resp(VIEWER_JS, "application/javascript"),
        (Method::Get, "/favicon.svg") => resp(FAVICON, "image/svg+xml"),
        (Method::Get, "/state") => json_resp(&state_json()),
        (Method::Get, "/version") => json_resp(&json!({ "version": env!("CARGO_PKG_VERSION"), "name": "adom-layout-viewer" }).to_string()),
        (Method::Post, "/ui/toast") => {
            if let Ok(v) = serde_json::from_str::<Value>(body) {
                let msg = v.get("message").and_then(|x| x.as_str()).unwrap_or("");
                let ty = v.get("type").and_then(|x| x.as_str()).unwrap_or("info");
                if !msg.is_empty() { TOASTS.lock().unwrap().push(json!({ "message": msg, "type": ty })); }
            }
            json_resp("{\"ok\":true}")
        }
        (Method::Post, "/ui/cmd") => {
            if let Ok(v) = serde_json::from_str::<Value>(body) {
                if v.get("action").and_then(|x| x.as_str()).map(|s| !s.is_empty()).unwrap_or(false) { CMDS.lock().unwrap().push(v); }
            }
            json_resp("{\"ok\":true}")
        }
        (Method::Get, "/ui/pull") => {
            let toasts: Vec<Value> = std::mem::take(&mut *TOASTS.lock().unwrap());
            let cmds: Vec<Value> = std::mem::take(&mut *CMDS.lock().unwrap());
            json_resp(&json!({ "toasts": toasts, "cmds": cmds }).to_string())
        }
        (Method::Get, "/enrich") => json_resp(&enrich(query)),
        (Method::Post, "/load") => {
            if let Ok(v) = serde_json::from_str::<Value>(body) {
                if let Some(pcb) = v.get("kicadPcb").and_then(|x| x.as_str()) {
                    let name = v.get("name").and_then(|x| x.as_str()).unwrap_or("board").to_string();
                    match load_pcb(&name, pcb) {
                        Ok(()) => return json_resp("{\"ok\":true}"),
                        Err(e) => return json_err(422, &e.to_string()),
                    }
                }
                set_state(AppState {
                    name: v.get("name").and_then(|x| x.as_str()).unwrap_or("").to_string(),
                    svg: v.get("svg").and_then(|x| x.as_str()).unwrap_or("").to_string(),
                    meta_json: v.get("meta").map(|m| m.to_string()).unwrap_or_else(|| "{}".into()),
                    kicad_pcb: String::new(),
                });
            }
            json_resp("{\"ok\":true}")
        }
        (Method::Post, "/eval") => {
            let code = serde_json::from_str::<Value>(body).ok()
                .and_then(|v| v.get("code").and_then(|c| c.as_str()).map(String::from)).unwrap_or_default();
            let id = EVAL_ID.fetch_add(1, Ordering::SeqCst);
            EVAL_PENDING.lock().unwrap().push_back((id, code));
            json_resp(&json!({ "id": id.to_string() }).to_string())
        }
        (Method::Get, "/eval/pending") => match EVAL_PENDING.lock().unwrap().pop_front() {
            Some((id, code)) => json_resp(&json!({ "id": id.to_string(), "code": code }).to_string()),
            None => json_resp("{}"),
        },
        (Method::Get, "/console") => {
            let log = CONSOLE.lock().unwrap().clone().unwrap_or_default();
            json_resp(&json!({ "lines": log }).to_string())
        }
        (Method::Post, "/console") => {
            if let Ok(v) = serde_json::from_str::<Value>(body) {
                let line = format!("[{}] {}", v.get("level").and_then(|l| l.as_str()).unwrap_or("log"), v.get("msg").and_then(|m| m.as_str()).unwrap_or(""));
                if let Some(buf) = CONSOLE.lock().unwrap().as_mut() { buf.push(line); let n = buf.len(); if n > 500 { buf.drain(0..n - 500); } }
            }
            json_resp("{\"ok\":true}")
        }
        (Method::Post, "/shutdown") => json_resp("{\"ok\":true}"),
        _ if url.starts_with("/eval/") => {
            let rest = &url["/eval/".len()..];
            if let Some(id_str) = rest.strip_suffix("/result") {
                if matches!(method, Method::Post) {
                    if let (Ok(id), Ok(v)) = (id_str.parse::<u64>(), serde_json::from_str::<Value>(body)) {
                        if let Some(map) = EVAL_RESULTS.lock().unwrap().as_mut() { map.insert(id, v); }
                    }
                    return json_resp("{\"ok\":true}");
                }
            }
            if let Ok(id) = rest.parse::<u64>() {
                let map = EVAL_RESULTS.lock().unwrap();
                if let Some(r) = map.as_ref().and_then(|m| m.get(&id)) {
                    return json_resp(&json!({ "done": true, "result": r }).to_string());
                }
                return json_resp("{\"done\":false}");
            }
            not_found()
        }
        _ => not_found(),
    }
}

/// Component enrichment: mpn (+optional lcsc) → price/stock/wiki/popularity.
/// Delegates to sibling Adom CLIs (adom-parts-search, adom-wiki-cli); cached
/// in-process. Best-effort — never blocks the UI, returns {} on any failure.
fn enrich(query: &str) -> String {
    let params = parse_query(query);
    let mpn = match params.get("mpn") { Some(m) if !m.is_empty() => m.clone(), _ => return "{}".into() };
    let lcsc = params.get("lcsc").cloned().unwrap_or_default();
    let key = format!("{mpn}|{lcsc}");
    if let Some(hit) = ENRICH_CACHE.lock().unwrap().as_ref().and_then(|m| m.get(&key).cloned()) { return hit; }
    // not cached — kick a one-off background fill (deduped) and tell the client
    // to retry. NEVER block the single-threaded server loop on a vendor call.
    let already = { let mut g = ENRICH_PENDING.lock().unwrap(); let set = g.get_or_insert_with(Default::default); !set.insert(key.clone()) };
    if !already {
        std::thread::spawn(move || { let _ = enrich_fill(&mpn, &lcsc); });
    }
    "{\"pending\":true}".into()
}

fn parse_query(q: &str) -> HashMap<String, String> {
    q.split('&').filter_map(|kv| {
        let mut it = kv.splitn(2, '=');
        Some((it.next()?.to_string(), urldecode(it.next().unwrap_or(""))))
    }).collect()
}
fn urldecode(s: &str) -> String {
    let b = s.as_bytes(); let mut out = String::new(); let mut i = 0;
    while i < b.len() {
        match b[i] {
            b'%' if i + 2 < b.len() => { if let Ok(c) = u8::from_str_radix(&s[i + 1..i + 3], 16) { out.push(c as char); i += 3; } else { out.push('%'); i += 1; } }
            b'+' => { out.push(' '); i += 1; }
            c => { out.push(c as char); i += 1; }
        }
    }
    out
}

// ── HD port registration (best-effort; no-op outside Hydrogen Desktop) ──
fn register_port(port: u16, name: &str, reason: &str) {
    let body = format!("{{\"port\":{port},\"action\":\"register\",\"name\":\"{name}\",\"reason\":\"{reason}\",\"expose\":\"internal\"}}");
    std::thread::spawn(move || hd_post(&body));
}
fn unregister_port(port: u16) { hd_post(&format!("{{\"port\":{port},\"action\":\"unregister\"}}")); }
fn hd_post(body: &str) {
    use std::io::Write;
    use std::net::{TcpStream, ToSocketAddrs};
    use std::time::Duration;
    let addr = match ("host.docker.internal", 47084u16).to_socket_addrs().ok().and_then(|mut a| a.next()) { Some(a) => a, None => return };
    if let Ok(mut s) = TcpStream::connect_timeout(&addr, Duration::from_millis(800)) {
        let _ = s.set_write_timeout(Some(Duration::from_millis(800)));
        let req = format!("POST /port-forward HTTP/1.1\r\nHost: host.docker.internal\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body);
        let _ = s.write_all(req.as_bytes());
    }
}

fn hdr(ct: &str) -> Header { Header::from_bytes(&b"Content-Type"[..], ct.as_bytes()).unwrap() }
fn resp(s: &str, ct: &str) -> Response<std::io::Cursor<Vec<u8>>> { Response::from_string(s).with_header(hdr(ct)) }
fn json_resp(s: &str) -> Response<std::io::Cursor<Vec<u8>>> { Response::from_string(s).with_header(hdr("application/json")) }
fn not_found() -> Response<std::io::Cursor<Vec<u8>>> { Response::from_string("{\"error\":\"not found\"}").with_status_code(404).with_header(hdr("application/json")) }
fn json_err(code: u16, msg: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_string(json!({ "ok": false, "error": msg }).to_string()).with_status_code(code).with_header(hdr("application/json"))
}