//! The unified USB device inventory — the model behind the dashboard.
//!
//! A `Device` is one physical USB device, wherever it is physically plugged
//! in across the whole Adom ecosystem. Every device knows:
//!   - its identity (vid/pid/serial/class) — the `BoundDevice` shape that the
//!     workcell USB/IP feature already uses (hydrogen-desktop workcell.ts),
//!   - its `source` host (where it's physically plugged in),
//!   - where it can be mounted (`targets`) given the v1 routing matrix,
//!   - where it is *currently* mounted (`mounted_on`), and
//!   - which per-device actions are available (reset/test/class/sniff are
//!     Linux-target-only — greyed out for Windows-native devices).
//!
//! For M1 the live inventory comes from (a) a real `lsusb` enumeration of THIS
//! container and (b) any registered agents (laptop/Azure/workcell) once wired.
//! Until the agents land, `--demo` seeds a representative cross-ecosystem set
//! so the dashboard is fully demonstrable. Demo devices are flagged `demo:true`
//! and never silently masquerade as real hardware.

use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::process::Command;

/// A host in the mesh — a place a device can be plugged into and/or mounted.
#[derive(Clone, Serialize)]
pub struct Host {
    pub id: String,
    pub label: String,
    /// laptop | azure-vm | wsl2 | container | workcell
    pub kind: String,
    pub os: String,
    /// Filter bucket shown in the UI: "local" (user's own machine) or "cloud".
    pub location: String,
    /// Can this host *consume* a remote USB device as a mount target?
    pub can_consume: bool,
    /// Is this host a Linux target (so reset/test/class/sniff are possible)?
    pub linux: bool,
    /// Is the host's agent live, or is this a placeholder until wired?
    pub agent_online: bool,
}

/// A mount target option offered for a given device.
#[derive(Clone, Serialize)]
pub struct Target {
    pub host_id: String,
    pub label: String,
    /// usbipd-attach-wsl | reverse-tunnel | native | direct-tcp
    pub mechanism: String,
    /// Empty when available; a human reason when not (e.g. deferred).
    pub blocked_reason: String,
}

#[derive(Clone, Serialize, Deserialize)]
pub struct Device {
    pub id: String,
    pub name: String,
    pub vid: u16,
    pub pid: u16,
    pub serial: Option<String>,
    pub class: String,
    pub bus_id: Option<String>,
    /// host id where it is physically plugged in
    pub source_id: String,
    pub source_label: String,
    /// "local" | "cloud" — mirrors the source host's bucket
    pub location: String,
    /// host id it is currently mounted on, or None
    pub mounted_on: Option<String>,
    pub demo: bool,
}

/// The full inventory snapshot the UI renders from. `overlay` carries live
/// mount-state changes made this session (device_id → Some(host) mounted /
/// None unmounted), applied on top of the freshly enumerated inventory.
pub fn snapshot(demo: bool, overlay: &std::collections::HashMap<String, Option<String>>) -> Value {
    let hosts = hosts(demo);
    let mut devices = enumerate_local();
    if demo {
        devices.extend(demo_devices());
    }
    for d in devices.iter_mut() {
        if let Some(m) = overlay.get(&d.id) {
            d.mounted_on = m.clone();
        }
    }

    let devices_json: Vec<Value> = devices
        .iter()
        .map(|d| {
            let targets = targets_for(d, &hosts);
            // Exportable = can be slingshotted to a host other than its own source.
            let exportable = targets.iter().any(|t| t.host_id != d.source_id);
            let caps = capabilities_for(d, &hosts, exportable);
            let mut v = serde_json::to_value(d).unwrap_or(json!({}));
            v["exportable"] = json!(exportable);
            v["vid_hex"] = json!(format!("{:04X}", d.vid));
            v["pid_hex"] = json!(format!("{:04X}", d.pid));
            v["targets"] = serde_json::to_value(&targets).unwrap_or(json!([]));
            v["capabilities"] = caps;
            v
        })
        .collect();

    json!({
        "hosts": serde_json::to_value(&hosts).unwrap_or(json!([])),
        "devices": devices_json,
        "demo": demo,
        "counts": {
            "total": devices.len(),
            "local": devices.iter().filter(|d| d.location == "local").count(),
            "cloud": devices.iter().filter(|d| d.location == "cloud").count(),
            "mounted": devices.iter().filter(|d| d.mounted_on.is_some()).count(),
        }
    })
}

/// The hosts in the mesh. `this-container` is always real; the rest are agent
/// endpoints that come online as adom-desktop / azure-winvm / workcell register.
fn hosts(demo: bool) -> Vec<Host> {
    let online = |id: &str| -> bool {
        // TODO(agents): replace with a live registry probe (Curium /sources or
        // a direct ping over the adom-desktop relay / azure-winvm). In demo
        // mode everything is shown online so the matrix is explorable.
        demo || id == "this-container"
    };
    vec![
        Host { id: "laptop".into(), label: "John's laptop (Windows)".into(), kind: "laptop".into(),
            os: "windows".into(), location: "local".into(), can_consume: true, linux: false, agent_online: online("laptop") },
        Host { id: "laptop-wsl2".into(), label: "Laptop WSL2 / Hydrogen Desktop".into(), kind: "wsl2".into(),
            os: "linux".into(), location: "local".into(), can_consume: true, linux: true, agent_online: online("laptop-wsl2") },
        Host { id: "this-container".into(), label: "This cloud container".into(), kind: "container".into(),
            os: "linux".into(), location: "cloud".into(), can_consume: true, linux: true, agent_online: true },
        Host { id: "azure-vm".into(), label: "Azure VM desktop (Windows)".into(), kind: "azure-vm".into(),
            os: "windows".into(), location: "cloud".into(), can_consume: false, linux: false, agent_online: online("azure-vm") },
        Host { id: "azure-wsl2".into(), label: "Azure VM WSL2 / HD-on-VM".into(), kind: "wsl2".into(),
            os: "linux".into(), location: "cloud".into(), can_consume: true, linux: true, agent_online: online("azure-wsl2") },
        Host { id: "workcell-rpi-001".into(), label: "Workcell rpi-001".into(), kind: "workcell".into(),
            os: "linux".into(), location: "cloud".into(), can_consume: false, linux: true, agent_online: online("workcell-rpi-001") },
    ]
}

/// v1 routing matrix: which hosts a given device may be mounted to.
/// Windows is source + local-only — a remote device cannot mount INTO a native
/// Windows desktop in v1 (needs the test-signed usbip-win client driver).
fn targets_for(d: &Device, hosts: &[Host]) -> Vec<Target> {
    let src = hosts.iter().find(|h| h.id == d.source_id);
    let src_machine = src.map(machine_of).unwrap_or_default();

    // Only hosts with a USB *export* agent can slingshot a device elsewhere:
    // laptop / Azure VM (usbipd-win) and workcells (usbip-host). A device whose
    // source is a cloud container or a WSL2 distro is a consumer, not a source —
    // it stays native on the host it's already on.
    let exportable = matches!(src.map(|h| h.kind.as_str()), Some("laptop") | Some("azure-vm") | Some("workcell"));
    if !exportable {
        return vec![Target {
            host_id: d.source_id.clone(),
            label: format!("{} (native)", d.source_label),
            mechanism: "native".into(),
            blocked_reason: String::new(),
        }];
    }

    hosts
        .iter()
        .filter_map(|h| {
            if h.id == d.source_id && !h.linux {
                // Native use on the same Windows box it's plugged into.
                return Some(Target { host_id: h.id.clone(), label: format!("{} (native)", h.label),
                    mechanism: "native".into(), blocked_reason: String::new() });
            }
            if !h.can_consume {
                if h.os == "windows" {
                    return Some(Target { host_id: h.id.clone(), label: h.label.clone(),
                        mechanism: "usbip-win-client".into(),
                        blocked_reason: "Deferred in v1 — mounting a remote device into native Windows needs the test-signed usbip-win client driver.".into() });
                }
                return None;
            }
            let same_machine = machine_of(h) == src_machine && !src_machine.is_empty();
            let mechanism = if h.kind == "wsl2" && same_machine {
                "usbipd-attach-wsl" // local, no relay
            } else if h.os == "linux" {
                "reverse-tunnel" // cross-machine via adom-desktop tunnel
            } else {
                "native"
            };
            Some(Target { host_id: h.id.clone(), label: h.label.clone(),
                mechanism: mechanism.into(), blocked_reason: String::new() })
        })
        .collect()
}

/// Group hosts by the physical machine they live on, so "same machine" →
/// local usbipd attach --wsl rather than a relay tunnel.
fn machine_of(h: &Host) -> String {
    match h.id.as_str() {
        "laptop" | "laptop-wsl2" => "laptop-machine".into(),
        "azure-vm" | "azure-wsl2" => "azure-machine".into(),
        other => other.into(),
    }
}

/// reset/test/class-override/sniff operate on the virtual device in a Linux
/// target. Available only when the device is currently mounted on a Linux host.
fn capabilities_for(d: &Device, hosts: &[Host], exportable: bool) -> Value {
    let on_linux = d
        .mounted_on
        .as_ref()
        .and_then(|m| hosts.iter().find(|h| &h.id == m))
        .map(|h| h.linux)
        .unwrap_or(false);
    // "Mounted remotely" = slingshotted to a host other than its own source.
    // A device sitting native on its own host is home, not unmountable.
    let mounted_remote = d.mounted_on.as_deref().map(|m| m != d.source_id).unwrap_or(false);
    json!({
        "mount": exportable && !mounted_remote,
        "unmount": mounted_remote,
        "reset": on_linux,
        "test": on_linux,
        "class_override": on_linux,
        "sniff": on_linux,
        "reason": if on_linux { Value::Null } else { json!("Mount to a Linux target to reset / test / force-class / sniff.") }
    })
}

/// Real enumeration of THIS container's USB via `lsusb`. Usually empty in a
/// cloud container, which is itself informative (nothing physically attached).
fn enumerate_local() -> Vec<Device> {
    let out = match Command::new("lsusb").output() {
        Ok(o) if o.status.success() => o,
        _ => return Vec::new(),
    };
    String::from_utf8_lossy(&out.stdout)
        .lines()
        .filter_map(parse_lsusb_line)
        .collect()
}

/// `Bus 001 Device 002: ID 1d6b:0002 Linux Foundation 2.0 root hub`
fn parse_lsusb_line(line: &str) -> Option<Device> {
    let id_pos = line.find(" ID ")?;
    let rest = &line[id_pos + 4..];
    let (idpart, name) = rest.split_once(' ').unwrap_or((rest, ""));
    let (vid_s, pid_s) = idpart.split_once(':')?;
    let vid = u16::from_str_radix(vid_s.trim(), 16).ok()?;
    let pid = u16::from_str_radix(pid_s.trim(), 16).ok()?;
    // Root hubs are container plumbing, not user-actionable devices.
    if vid == 0x1d6b { return None; }
    let name = if name.trim().is_empty() { format!("{:04X}:{:04X} device", vid, pid) } else { name.trim().to_string() };
    Some(Device {
        id: format!("this-container:{:04x}:{:04x}", vid, pid),
        name,
        vid,
        pid,
        serial: None,
        class: classify(vid, pid),
        bus_id: None,
        source_id: "this-container".into(),
        source_label: "This cloud container".into(),
        location: "cloud".into(),
        mounted_on: Some("this-container".into()),
        demo: false,
    })
}

fn classify(vid: u16, _pid: u16) -> String {
    match vid {
        0x1d6b => "Hub".into(),
        _ => "USB device".into(),
    }
}

/// Representative cross-ecosystem devices for demoing the dashboard before the
/// live agents are wired. Includes the RP2040 from the canonical example.
fn demo_devices() -> Vec<Device> {
    vec![
        Device { id: "laptop:2e8a:0003".into(), name: "Raspberry Pi RP2040 (BOOTSEL)".into(),
            vid: 0x2e8a, pid: 0x0003, serial: Some("E660C0D1C7332A28".into()), class: "Mass storage (UF2)".into(),
            bus_id: Some("2-4".into()), source_id: "laptop".into(), source_label: "John's laptop (Windows)".into(),
            location: "local".into(), mounted_on: None, demo: true },
        Device { id: "laptop:0483:374b".into(), name: "ST-Link/V2.1 (STM32 debugger)".into(),
            vid: 0x0483, pid: 0x374b, serial: Some("066BFF495054".into()), class: "Vendor / CMSIS-DAP".into(),
            bus_id: Some("2-1".into()), source_id: "laptop".into(), source_label: "John's laptop (Windows)".into(),
            location: "local".into(), mounted_on: Some("laptop-wsl2".into()), demo: true },
        Device { id: "laptop:10c4:ea60".into(), name: "CP2102 USB-to-UART Bridge".into(),
            vid: 0x10c4, pid: 0xea60, serial: Some("0001".into()), class: "CDC serial".into(),
            bus_id: Some("2-3".into()), source_id: "laptop".into(), source_label: "John's laptop (Windows)".into(),
            location: "local".into(), mounted_on: None, demo: true },
        Device { id: "workcell-rpi-001:1a86:7523".into(), name: "CH340 Serial".into(),
            vid: 0x1a86, pid: 0x7523, serial: None, class: "CDC serial".into(),
            bus_id: Some("1-1".into()), source_id: "workcell-rpi-001".into(), source_label: "Workcell rpi-001".into(),
            location: "cloud".into(), mounted_on: Some("this-container".into()), demo: true },
        Device { id: "azure-vm:0403:6010".into(), name: "FTDI FT2232H (JTAG/serial)".into(),
            vid: 0x0403, pid: 0x6010, serial: Some("FT9AB2CD".into()), class: "Vendor / JTAG".into(),
            bus_id: Some("3-2".into()), source_id: "azure-vm".into(), source_label: "Azure VM desktop (Windows)".into(),
            location: "cloud".into(), mounted_on: None, demo: true },
    ]
}