//! adom-lbr live app server — the AI-drivable surface (app-creator §7/§7b/§8).
//! Same shell as adom-symbol/adom-footprint: GET / (UI), GET /state (poller),
//! POST /load (push an .lbr — raw XML, re-linted server-side), POST /eval +
//! GET /eval/pending + POST /eval/:id/result + GET /eval/:id, POST/GET /console,
//! POST /shutdown, GET /favicon.svg. No gallia, no Node.

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

#[derive(Default, Clone)]
pub struct AppState {
    pub library_name: String,
    pub has_content: bool,
    pub lint_passed: bool,
    pub errors: Vec<(String, String)>,   // (code, message)
    pub warnings: Vec<(String, String)>, // (code, message)
    pub symbol_count: usize,
    pub package_count: usize,
    pub deviceset_count: usize,
    pub pin_count: usize,
    pub pad_count: usize,
    /// Raw `.lbr` XML retained for "Send to" delivery.
    pub lbr: String,
}

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 EVAL_ID: AtomicU64 = AtomicU64::new(1);

const APP_HTML: &str = include_str!("app.html");
const FAVICON: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5.5A1.5 1.5 0 0 1 5.5 4H9a1.5 1.5 0 0 1 1.5 1.5V20a1.5 1.5 0 0 0-1.5-1.5H5.5A1.5 1.5 0 0 1 4 17z"/><path d="M20 5.5A1.5 1.5 0 0 0 18.5 4H15a1.5 1.5 0 0 0-1.5 1.5V20a1.5 1.5 0 0 1 1.5-1.5h3.5a1.5 1.5 0 0 0 1.5-1.5z"/></svg>"##;

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

/// Parse + lint an `.lbr` (EAGLE XML) into the displayable app state.
pub fn analyze_lbr(content: &str) -> AppState {
    let lint = crate::lint_lbr(content);
    let lib = extract_attr(content, "deviceset")
        .or_else(|| extract_attr(content, "symbol"))
        .or_else(|| extract_attr(content, "package"))
        .unwrap_or_else(|| "library".to_string());
    AppState {
        library_name: lib,
        has_content: true,
        lint_passed: lint.passed,
        errors: lint.errors.iter().map(|e| (e.code.clone(), e.message.clone())).collect(),
        warnings: lint.warnings.iter().map(|w| (w.code.clone(), w.message.clone())).collect(),
        symbol_count: content.matches("<symbol ").count(),
        package_count: content.matches("<package ").count(),
        deviceset_count: content.matches("<deviceset ").count(),
        pin_count: content.matches("<pin ").count(),
        pad_count: content.matches("<smd ").count() + content.matches("<pad ").count(),
        lbr: content.to_string(),
    }
}

/// First `name="…"` attribute of the first `<tag …>` element.
fn extract_attr(content: &str, tag: &str) -> Option<String> {
    let needle = format!("<{tag} ");
    let i = content.find(&needle)? + needle.len();
    let rest = &content[i..];
    let k = rest.find("name=\"")? + "name=\"".len();
    let j = rest[k..].find('"')? + k;
    Some(rest[k..j].to_string())
}

fn pairs_json(v: &[(String, String)]) -> Value {
    Value::Array(v.iter().map(|(c, m)| json!({ "code": c, "message": m })).collect())
}

fn state_json() -> String {
    match &*STATE.lock().unwrap() {
        Some(s) => json!({
            "libraryName": s.library_name,
            "hasContent": s.has_content,
            "lintPassed": s.lint_passed,
            "errors": pairs_json(&s.errors),
            "warnings": pairs_json(&s.warnings),
            "symbolCount": s.symbol_count,
            "packageCount": s.package_count,
            "devicesetCount": s.deviceset_count,
            "pinCount": s.pin_count,
            "padCount": s.pad_count,
        })
        .to_string(),
        None => "{}".to_string(),
    }
}

pub fn run(port: u16) -> Result<()> {
    *EVAL_RESULTS.lock().unwrap() = Some(HashMap::new());
    *CONSOLE.lock().unwrap() = Some(Vec::new());
    // Loopback by default: /eval queues JS for the user's browser, /deliver
    // pushes files to their desktop, /shutdown kills the app — none of that
    // should be LAN-reachable. ADOM_LBR_BIND=0.0.0.0 opts back in.
    let bind = std::env::var("ADOM_LBR_BIND").unwrap_or_else(|_| "127.0.0.1".into());
    let server = Server::http(format!("{bind}:{port}")).map_err(|e| anyhow::anyhow!("bind {bind}:{port}: {e}"))?;
    eprintln!("[adom-lbr] serving on http://127.0.0.1:{port}");
    // app-creator §6a — best-effort HD port registration (internal/proxy-only).
    register_port(port, "adom-lbr", "EAGLE .lbr linter live app (AI-drivable)");

    // Cap POST bodies: the biggest legitimate payload is a large .lbr; anything
    // beyond this is a bug or abuse, not a library.
    const MAX_BODY: u64 = 32 * 1024 * 1024;
    for mut req in server.incoming_requests() {
        let url = req.url().split('?').next().unwrap_or("/").to_string();
        let method = req.method().clone();
        let mut body = String::new();
        if matches!(method, Method::Post) {
            use std::io::Read;
            let _ = req.as_reader().take(MAX_BODY).read_to_string(&mut body);
        }
        let resp = route(&method, &url, &body);
        let _ = req.respond(resp);
        if url == "/shutdown" && matches!(method, Method::Post) {
            break;
        }
    }
    unregister_port(port);
    Ok(())
}

/// Best-effort HD port registration (app-creator §6a / adom-port-hints).
/// Raw TCP so we add no HTTP-client dependency; never blocks serving, never
/// fails the app if HD is absent (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 route(method: &Method, url: &str, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    match (method, url) {
        (Method::Get, "/") | (Method::Get, "/index.html") => html(APP_HTML),
        (Method::Get, "/favicon.svg") => svg(FAVICON),
        (Method::Get, "/state") => json_resp(&state_json()),
        (Method::Post, "/load") => {
            if let Ok(v) = serde_json::from_str::<Value>(body) {
                // Preferred: push raw .lbr XML and let the server lint it.
                if let Some(lbr) = v.get("lbr").and_then(|x| x.as_str()) {
                    set_state(analyze_lbr(lbr));
                }
            }
            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 len = buf.len();
                    if len > 500 {
                        buf.drain(0..len - 500);
                    }
                }
            }
            json_resp("{\"ok\":true}")
        }
        (Method::Post, "/deliver") => {
            let v = serde_json::from_str::<Value>(body).unwrap_or(json!({}));
            let target = v.get("target").and_then(|x| x.as_str()).unwrap_or("kicad").to_string();
            let (name, lbr) = match &*STATE.lock().unwrap() {
                Some(s) => (s.library_name.clone(), s.lbr.clone()),
                None => (String::new(), String::new()),
            };
            json_resp(&crate::deliver::deliver(&name, &lbr, &target).to_string())
        }
        (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 mut map = EVAL_RESULTS.lock().unwrap();
                // remove, don't get: results are one-shot; leaving them in the
                // map leaks for the life of the server.
                if let Some(r) = map.as_mut().and_then(|m| m.remove(&id)) {
                    return json_resp(&json!({ "done": true, "result": r }).to_string());
                }
                return json_resp("{\"done\":false}");
            }
            not_found()
        }
        _ => not_found(),
    }
}

fn hdr(ct: &str) -> Header {
    Header::from_bytes(&b"Content-Type"[..], ct.as_bytes()).unwrap()
}
fn html(s: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_string(s).with_header(hdr("text/html; charset=utf-8"))
}
fn svg(s: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_string(s).with_header(hdr("image/svg+xml"))
}
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"))
}