//! `adom-usb app` — the Hydrogen webview USB device dashboard.
//!
//! One table of every USB device across the Adom ecosystem, with Local/Cloud
//! filters and per-device actions (mount/unmount/reset/test/force-class/sniff).
//! Server state is the single source of truth; the UI polls `GET /state` and
//! every button POSTs to an endpoint the AI can also drive with curl.
//!
//! M1 scope: live inventory + a mount-state overlay that records intent and
//! emits the exact agent command each action will run. The real agent calls
//! (usbipd-win over the relay, `usbip attach` in the target, usbmon, etc.) slot
//! into `agent_command()` / the action handlers as they are wired — the HTTP
//! contract here does not change when they land.

pub mod tab;

use crate::devices;
use crate::{note, ok, warn, AppArgs};
use serde_json::{json, Value};
use std::collections::{HashMap, 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;
const ACTION_LOG_MAX: usize = 100;

struct AppState {
    demo: bool,
    dev: bool,
    /// device_id → Some(host) mounted / None unmounted (live overlay).
    mounts: Mutex<HashMap<String, Option<String>>>,
    actions: Mutex<VecDeque<Value>>,
    console: Mutex<VecDeque<Value>>,
    state_ts: Mutex<u128>,
    pending_eval: Mutex<Option<(String, String)>>,
    eval_results: Mutex<HashMap<String, Value>>,
}

fn now_ms() -> u128 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis())
        .unwrap_or(0)
}

fn json_resp(code: u16, body: Value) -> Response<std::io::Cursor<Vec<u8>>> {
    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> {
    let mut body = String::new();
    std::io::Read::read_to_string(req.as_reader(), &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))
}

fn snapshot(state: &AppState) -> Value {
    let overlay = state.mounts.lock().unwrap().clone();
    devices::snapshot(state.demo, &overlay)
}

fn find_device(snap: &Value, id: &str) -> Option<Value> {
    snap.get("devices")?
        .as_array()?
        .iter()
        .find(|d| d.get("id").and_then(|v| v.as_str()) == Some(id))
        .cloned()
}

fn log_action(state: &AppState, entry: Value) {
    let mut a = state.actions.lock().unwrap();
    if a.len() >= ACTION_LOG_MAX { a.pop_front(); }
    a.push_back(entry);
    *state.state_ts.lock().unwrap() = now_ms();
}

/// The concrete command a live agent will run for an op. Surfaced in the action
/// log so a human (and the AI) can see exactly what the mount will execute, and
/// so it is trivially diffable against the eventual real implementation.
fn agent_command(op: &str, dev: &Value, target: &str, mechanism: &str) -> String {
    let bus = dev.get("bus_id").and_then(|v| v.as_str()).unwrap_or("?");
    let src = dev.get("source_id").and_then(|v| v.as_str()).unwrap_or("?");
    let vid = dev.get("vid_hex").and_then(|v| v.as_str()).unwrap_or("????");
    let pid = dev.get("pid_hex").and_then(|v| v.as_str()).unwrap_or("????");
    match (op, mechanism) {
        ("mount", "usbipd-attach-wsl") =>
            format!("[{src}] usbipd bind --busid {bus} && usbipd attach --wsl --busid {bus}"),
        ("mount", "reverse-tunnel") =>
            format!("[{src}] usbipd bind --busid {bus} ; adom-desktop tunnel --reverse --expose {src}:3240 ; [{target}] usbip attach -r 127.0.0.1 -b {bus}"),
        ("mount", "direct-tcp") =>
            format!("[{target}] usbip attach -r {src} -b {bus}"),
        ("mount", "native") =>
            format!("[{target}] (native) re-bind {vid}:{pid} to the host driver"),
        ("unmount", _) =>
            format!("[{target}] usbip detach (device {vid}:{pid}) ; [{src}] usbipd unbind --busid {bus}"),
        ("reset", _) => format!("[{target}] usb reset ioctl on {vid}:{pid} (detach+reattach)"),
        ("test", _) => format!("[{target}] lsusb -v -d {vid}:{pid} + enumerate/echo health check"),
        ("class_override", _) => format!("[{target}] echo <driver> > driver_override ; rebind {vid}:{pid}"),
        ("sniff", _) => format!("[{target}] modprobe usbmon ; capture usbmonX for {vid}:{pid} → pcap"),
        _ => format!("[{target}] {op} {vid}:{pid}"),
    }
}

pub fn run(args: AppArgs) -> Result<(), String> {
    ok(format!("adom-usb dashboard starting on port {}", args.port));
    if args.demo {
        note!("  {}demo mode: seeding a representative cross-ecosystem device set{}", crate::DIM, crate::RESET);
    } else {
        note!("  {}live mode: real lsusb on this container + registered agents only{}", crate::DIM, crate::RESET);
    }

    let state = Arc::new(AppState {
        demo: args.demo,
        dev: args.dev,
        mounts: Mutex::new(HashMap::new()),
        actions: Mutex::new(VecDeque::new()),
        console: Mutex::new(VecDeque::with_capacity(CONSOLE_MAX)),
        state_ts: Mutex::new(now_ms()),
        pending_eval: Mutex::new(None),
        eval_results: Mutex::new(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 st = Arc::clone(&state);

        let result: (u16, Value) = match (method.clone(), path.as_str()) {
            (Method::Get, "/") | (Method::Get, "/index.html") => {
                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, "/health") => {
                let snap = snapshot(&st);
                let n = snap.get("devices").and_then(|d| d.as_array()).map(|a| a.len()).unwrap_or(0);
                (200, json!({"ok": true, "demo": st.demo, "devices": n, "version": env!("CARGO_PKG_VERSION")}))
            }
            (Method::Get, "/state") => {
                let mut snap = snapshot(&st);
                snap["dev"] = json!(st.dev);
                snap["state_ts"] = json!(*st.state_ts.lock().unwrap() as u64);
                snap["actions"] = json!(st.actions.lock().unwrap().iter().rev().take(20).cloned().collect::<Vec<_>>());
                // Installed plugins + which sniffer matches each device.
                let installed = crate::plugins::discover_installed();
                snap["plugins"] = crate::plugins::to_json(&installed);
                if let Some(devs) = snap.get_mut("devices").and_then(|d| d.as_array_mut()) {
                    for d in devs.iter_mut() {
                        let class = d.get("class").and_then(|v| v.as_str()).unwrap_or("").to_string();
                        let vid = d.get("vid").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
                        let pid = d.get("pid").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
                        let class = class.as_str();
                        d["sniffer_plugin"] = json!(crate::plugins::match_for(class, vid, pid, &installed).map(|p| p.slug.clone()));
                        // Firmware device-test pack: which stub personas can we flash to this chip?
                        if let Some(fw) = crate::plugins::firmware_for(class, vid, pid, &installed) {
                            d["firmware_plugin"] = json!(fw.slug);
                            d["firmware_personas"] = fw.personas.clone();
                        } else {
                            d["firmware_plugin"] = Value::Null;
                            d["firmware_personas"] = json!([]);
                        }
                    }
                }
                (200, snap)
            }
            (Method::Get, "/plugins") => {
                let installed = crate::plugins::discover_installed();
                let available = crate::plugins::discover_registry(&installed);
                (200, json!({
                    "installed": crate::plugins::to_json(&installed),
                    "available": crate::plugins::to_json(&available),
                    "tag": crate::plugins::PLUGIN_TAG,
                    "api_version": crate::plugins::PLUGIN_API_VERSION,
                }))
            }
            (Method::Post, "/refresh") => {
                *st.state_ts.lock().unwrap() = now_ms();
                (200, snapshot(&st))
            }
            (Method::Post, "/mount") => match read_body(&mut req) {
                Ok(body) => handle_mount(&st, &body),
                Err(e) => (400, json!({"error": e})),
            },
            (Method::Post, "/unmount") => match read_body(&mut req) {
                Ok(body) => handle_unmount(&st, &body),
                Err(e) => (400, json!({"error": e})),
            },
            (Method::Post, "/flash") => match read_body(&mut req) {
                Ok(body) => handle_flash(&st, &body),
                Err(e) => (400, json!({"error": e})),
            },
            (Method::Post, "/reset") => handle_device_op(&st, &mut req, "reset"),
            (Method::Post, "/test") => handle_device_op(&st, &mut req, "test"),
            (Method::Post, "/class-override") => handle_device_op(&st, &mut req, "class_override"),
            (Method::Post, "/sniff") => handle_device_op(&st, &mut req, "sniff"),
            (Method::Post, "/console") => match read_body(&mut req) {
                Ok(v) => {
                    let mut c = st.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> = st.console.lock().unwrap().iter().cloned().collect();
                (200, json!({"messages": c}))
            }
            (Method::Post, "/eval") if st.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());
                    *st.pending_eval.lock().unwrap() = Some((id.clone(), code));
                    (200, json!({"id": id}))
                }
                Err(e) => (400, json!({"error": e})),
            },
            (Method::Get, "/eval/pending") if st.dev => {
                let p = st.pending_eval.lock().unwrap().take();
                match p {
                    Some((id, code)) => (200, json!({"id": id, "code": code})),
                    None => (200, json!({})),
                }
            }
            (Method::Post, path) if st.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);
                        st.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(())
}

fn handle_mount(st: &AppState, body: &Value) -> (u16, Value) {
    let device_id = match body.get("device_id").and_then(|v| v.as_str()) {
        Some(s) => s.to_string(),
        None => return (400, json!({"error": "device_id required"})),
    };
    let target_id = match body.get("target_id").and_then(|v| v.as_str()) {
        Some(s) => s.to_string(),
        None => return (400, json!({"error": "target_id required"})),
    };
    let snap = snapshot(st);
    let dev = match find_device(&snap, &device_id) {
        Some(d) => d,
        None => return (404, json!({"error": format!("unknown device: {}", device_id)})),
    };
    // Validate the target against the device's computed routing matrix.
    let targets = dev.get("targets").and_then(|v| v.as_array()).cloned().unwrap_or_default();
    let target = targets.iter().find(|t| t.get("host_id").and_then(|v| v.as_str()) == Some(target_id.as_str()));
    let target = match target {
        Some(t) => t.clone(),
        None => return (400, json!({"error": format!("{} cannot be mounted on {} (not in routing matrix)", device_id, target_id)})),
    };
    let blocked = target.get("blocked_reason").and_then(|v| v.as_str()).unwrap_or("");
    if !blocked.is_empty() {
        return (409, json!({"error": blocked, "blocked": true}));
    }
    let mechanism = target.get("mechanism").and_then(|v| v.as_str()).unwrap_or("native");
    let cmd = agent_command("mount", &dev, &target_id, mechanism);
    st.mounts.lock().unwrap().insert(device_id.clone(), Some(target_id.clone()));
    log_action(st, json!({
        "ts": now_ms() as u64, "op": "mount", "device_id": device_id,
        "device": dev.get("name"), "target": target_id, "mechanism": mechanism,
        "command": cmd, "status": "applied"
    }));
    (200, json!({"ok": true, "device_id": device_id, "mounted_on": target_id, "mechanism": mechanism, "command": cmd}))
}

fn handle_unmount(st: &AppState, body: &Value) -> (u16, Value) {
    let device_id = match body.get("device_id").and_then(|v| v.as_str()) {
        Some(s) => s.to_string(),
        None => return (400, json!({"error": "device_id required"})),
    };
    let snap = snapshot(st);
    let dev = match find_device(&snap, &device_id) {
        Some(d) => d,
        None => return (404, json!({"error": format!("unknown device: {}", device_id)})),
    };
    let was = dev.get("mounted_on").and_then(|v| v.as_str()).unwrap_or("").to_string();
    if was.is_empty() {
        return (409, json!({"error": "device is not mounted"}));
    }
    let cmd = agent_command("unmount", &dev, &was, "");
    st.mounts.lock().unwrap().insert(device_id.clone(), None);
    log_action(st, json!({
        "ts": now_ms() as u64, "op": "unmount", "device_id": device_id,
        "device": dev.get("name"), "target": was, "command": cmd, "status": "applied"
    }));
    (200, json!({"ok": true, "device_id": device_id, "command": cmd}))
}

/// Flash a stub-firmware persona onto a device via a matching firmware plugin
/// (e.g. make an RP2040 enumerate as a HID keyboard). Dispatches to the plugin's
/// `flash` entrypoint and records the exact command.
fn handle_flash(st: &AppState, body: &Value) -> (u16, Value) {
    let device_id = match body.get("device_id").and_then(|v| v.as_str()) {
        Some(s) => s.to_string(),
        None => return (400, json!({"error": "device_id required"})),
    };
    let persona = match body.get("persona").and_then(|v| v.as_str()) {
        Some(s) => s.to_string(),
        None => return (400, json!({"error": "persona required (see firmware_personas in /state)"})),
    };
    let snap = snapshot(st);
    let dev = match find_device(&snap, &device_id) {
        Some(d) => d,
        None => return (404, json!({"error": format!("unknown device: {}", device_id)})),
    };
    let class = dev.get("class").and_then(|v| v.as_str()).unwrap_or("");
    let vid = dev.get("vid").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
    let pid = dev.get("pid").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
    let bus = dev.get("bus_id").and_then(|v| v.as_str()).unwrap_or("?");
    let installed = crate::plugins::discover_installed();
    let fw = match crate::plugins::firmware_for(class, vid, pid, &installed) {
        Some(p) => p,
        None => return (409, json!({"error": "no firmware device-test pack installed for this chip", "blocked": true,
            "hint": "search the wiki: adom-usb plugins search"})),
    };
    // Validate the persona exists in the pack's catalog.
    let known = fw.personas.as_array().map(|a| a.iter()
        .any(|p| p.get("id").and_then(|x| x.as_str()) == Some(persona.as_str()))).unwrap_or(false);
    if !known {
        return (400, json!({"error": format!("unknown persona '{}' for {}", persona, fw.slug),
            "available": fw.personas.clone()}));
    }
    let cmd = format!("[{src}] {ep} flash --persona {persona} --vid {vid:04x} --pid {pid:04x} --busid {bus} --target {src}",
        src = dev.get("source_id").and_then(|v| v.as_str()).unwrap_or("?"),
        ep = fw.entrypoint, persona = persona, vid = vid, pid = pid, bus = bus);
    log_action(st, json!({
        "ts": now_ms() as u64, "op": "flash", "device_id": device_id,
        "device": dev.get("name"), "persona": persona, "plugin": fw.slug.clone(),
        "command": cmd, "status": "queued"
    }));
    (200, json!({"ok": true, "op": "flash", "device_id": device_id, "persona": persona,
        "plugin": fw.slug.clone(), "method": fw.flash_method.clone(), "command": cmd, "status": "queued",
        "note": "after flashing, the device re-enumerates as the persona's USB class — refresh to see it"}))
}

fn handle_device_op(st: &AppState, req: &mut tiny_http::Request, op: &str) -> (u16, Value) {
    let body = match read_body(req) {
        Ok(b) => b,
        Err(e) => return (400, json!({"error": e})),
    };
    let device_id = match body.get("device_id").and_then(|v| v.as_str()) {
        Some(s) => s.to_string(),
        None => return (400, json!({"error": "device_id required"})),
    };
    let snap = snapshot(st);
    let dev = match find_device(&snap, &device_id) {
        Some(d) => d,
        None => return (404, json!({"error": format!("unknown device: {}", device_id)})),
    };
    let caps = dev.get("capabilities").cloned().unwrap_or(json!({}));
    let allowed = caps.get(op).and_then(|v| v.as_bool()).unwrap_or(false);
    if !allowed {
        let reason = caps.get("reason").and_then(|v| v.as_str())
            .unwrap_or("not available for this device in its current state");
        return (409, json!({"error": reason, "blocked": true}));
    }
    let target = dev.get("mounted_on").and_then(|v| v.as_str()).unwrap_or("");
    // For sniff, prefer a matching installed plugin's entrypoint; otherwise the
    // built-in usbmon capture. This is the extension seam the ecosystem hooks.
    let (cmd, plugin) = if op == "sniff" {
        let class = dev.get("class").and_then(|v| v.as_str()).unwrap_or("");
        let vid = dev.get("vid").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
        let pid = dev.get("pid").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
        let bus = dev.get("bus_id").and_then(|v| v.as_str()).unwrap_or("?");
        let installed = crate::plugins::discover_installed();
        match crate::plugins::match_for(class, vid, pid, &installed) {
            Some(p) => (
                format!("[{target}] {} sniff --vid {:04x} --pid {:04x} --busid {} --target {} --out <pcap>",
                    p.entrypoint, vid, pid, bus, target),
                Some(p.slug.clone()),
            ),
            None => (agent_command(op, &dev, target, ""), None),
        }
    } else {
        (agent_command(op, &dev, target, ""), None)
    };
    log_action(st, json!({
        "ts": now_ms() as u64, "op": op, "device_id": device_id,
        "device": dev.get("name"), "target": target, "command": cmd, "plugin": plugin,
        "status": "queued"  // executes once the live agent for `target` is wired
    }));
    (200, json!({"ok": true, "op": op, "device_id": device_id, "command": cmd, "plugin": plugin, "status": "queued"}))
}