//! adom-schematic live app server — the embeddable live schematic view.
//! GET / (UI), /viewer.js, /favicon.svg, /state, POST /load (.kicad_sch),
//! GET /enrich, POST /shutdown. Same app-creator shape as the layout viewer.

use anyhow::Result;
use serde_json::{json, Value};
use std::collections::{HashMap, HashSet};
use std::io::Read;
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 }

static STATE: Mutex<Option<AppState>> = Mutex::new(None);
static ENRICH_CACHE: Mutex<Option<HashMap<String, String>>> = Mutex::new(None);
static ENRICH_PENDING: Mutex<Option<HashSet<String>>> = Mutex::new(None);
// AI-drivability: the AI narrates over the running UI (POST /ui/toast) and drives it
// (POST /ui/cmd — same paths a user's click takes); the frontend drains both via
// GET /ui/pull. CONSOLE is the observability ring buffer the UI mirrors into.
static TOASTS: Mutex<Vec<Value>> = Mutex::new(Vec::new());
static CMDS: Mutex<Vec<Value>> = Mutex::new(Vec::new());
static CONSOLE: 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="5" width="18" height="14" rx="1.5"/><path d="M7 9h3v6H7zM14 12h3M14 12l-2-2M14 12l-2 2"/></svg>"##;

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

pub fn load_sch(name: &str, sch: &str) -> Result<()> {
    let (svg, meta) = crate::kicad_sch::render_sheet(sch)?;
    let lcscs: Vec<(String, String)> = meta.components.iter()
        .filter(|c| !c.lcsc.is_empty() || !c.value.is_empty())
        .map(|c| (c.value.clone(), c.lcsc.clone())).collect();
    set_state(AppState { name: name.into(), svg, meta_json: serde_json::to_string(&meta)? });
    if !lcscs.is_empty() {
        std::thread::spawn(move || { for (v, l) in lcscs { let _ = enrich_fill(&v, &l); } });
    }
    Ok(())
}

fn enrich_fill(value: &str, lcsc: &str) -> String {
    let key = format!("{value}|{lcsc}");
    let out = crate::enrich::lookup(value, 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 => "{}".into(),
    }
}

pub fn run(port: u16) -> Result<()> {
    *ENRICH_CACHE.lock().unwrap() = Some(HashMap::new());
    *ENRICH_PENDING.lock().unwrap() = Some(HashSet::new());
    let server = Server::http(format!("0.0.0.0:{port}")).map_err(|e| anyhow::anyhow!("bind {port}: {e}"))?;
    eprintln!("[adom-schematic] serving on http://127.0.0.1:{port}");
    register_port(port);
    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-schematic" }).to_string()),
        // observability ring buffer (GET to read, POST from the UI to append)
        (Method::Get, "/console") => json_resp(&json!({ "lines": *CONSOLE.lock().unwrap() }).to_string()),
        (Method::Post, "/console") => {
            if let Ok(v) = serde_json::from_str::<Value>(body) {
                let mut g = CONSOLE.lock().unwrap(); g.push(v);
                let n = g.len(); if n > 500 { g.drain(0..n - 500); }
            }
            json_resp("{\"ok\":true}")
        }
        // AI channel: narrate (toast) + drive (cmd); the UI drains both via /ui/pull.
        (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(sch) = v.get("kicadSch").and_then(|x| x.as_str()) {
                    let name = v.get("name").and_then(|x| x.as_str()).unwrap_or("sheet").to_string();
                    return match load_sch(&name, sch) { Ok(()) => json_resp("{\"ok\":true}"), Err(e) => json_err(422, &e.to_string()) };
                }
            }
            json_resp("{\"ok\":true}")
        }
        (Method::Post, "/shutdown") => json_resp("{\"ok\":true}"),
        _ => not_found(),
    }
}

fn enrich(query: &str) -> String {
    let params = parse_query(query);
    let value = params.get("mpn").cloned().unwrap_or_default();
    let lcsc = params.get("lcsc").cloned().unwrap_or_default();
    if value.is_empty() && lcsc.is_empty() { return "{}".into(); }
    let key = format!("{value}|{lcsc}");
    if let Some(hit) = ENRICH_CACHE.lock().unwrap().as_ref().and_then(|m| m.get(&key).cloned()) { return hit; }
    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(&value, &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
}

fn register_port(port: u16) {
    let body = format!("{{\"port\":{port},\"action\":\"register\",\"name\":\"adom-schematic\",\"reason\":\"Interactive schematic view\",\"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")) }
