//! Component enrichment: MPN → { price, stock, tier, popularity, wiki_url }.
//!
//! Sources (sibling Adom CLIs, shelled with a hard timeout so a slow/absent CLI
//! never hangs the UI):
//!   • popularity + wiki page — `adom-wiki discover search --query <mpn> --json`
//!     (vouch_count / star_count → popularity; owner/slug → wiki_url)
//!   • price + stock — `adom-parts-search search <mpn>` (max stock across
//!     vendors; first real unit price; JLCPCB tier from flags)
//!
//! Because the vendor lookup is slow (~30 s, live APIs), the server PRE-WARMS the
//! cache for every board MPN in a background thread at load time; hover then hits
//! the cache. `lookup()` is the (slow) cache-fill; `/enrich` reads the result.

use serde_json::{json, Value};
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::Duration;

/// Run a command, capping wall-clock at `secs`. Returns stdout on success.
fn run_capped(bin: &str, args: &[&str], secs: u64) -> Option<String> {
    let bin = bin.to_string();
    let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
    let (tx, rx) = mpsc::channel();
    std::thread::spawn(move || {
        let out = Command::new(&bin).args(&args).stdin(Stdio::null()).output();
        let _ = tx.send(out.ok().filter(|o| o.status.success()).map(|o| String::from_utf8_lossy(&o.stdout).into_owned()));
    });
    rx.recv_timeout(Duration::from_secs(secs)).ok().flatten()
}

/// Strip any leading non-JSON preamble (parts-search prints an "OK: …" summary
/// line before its JSON) and parse.
fn parse_json_tail(s: &str) -> Option<Value> {
    let start = s.find(['{', '['])?;
    serde_json::from_str(&s[start..]).ok()
}

/// The slow path: hit both CLIs and assemble the enrichment record as a JSON
/// string. Safe to call off-thread; degrades field-by-field.
pub fn lookup(mpn: &str, _lcsc: &str) -> String {
    let mut out = json!({});
    let o = out.as_object_mut().unwrap();

    // ── wiki page + popularity (vouches/score) ──
    if let Some(txt) = run_capped("adom-wiki", &["discover", "search", "--query", mpn, "--limit", "5", "--json"], 12) {
        if let Some(v) = parse_json_tail(&txt) {
            if let Some(results) = v.get("data").and_then(|d| d.get("results")).and_then(|r| r.as_array()) {
                // prefer an exact (case-insensitive) title/slug match, else the first component
                let pick = results.iter().find(|r| r.get("title").and_then(|t| t.as_str()).map(|t| t.eq_ignore_ascii_case(mpn)).unwrap_or(false))
                    .or_else(|| results.iter().find(|r| r.get("type").and_then(|t| t.as_str()) == Some("component")))
                    .or_else(|| results.first());
                if let Some(r) = pick {
                    let owner = r.get("owner").and_then(|x| x.as_str()).unwrap_or("");
                    let slug = r.get("slug").and_then(|x| x.as_str()).unwrap_or("");
                    if !slug.is_empty() {
                        o.insert("wiki_url".into(), json!(format!("https://wiki.adom.inc/{owner}/{slug}")));
                    }
                    let vouch = r.get("vouch_count").and_then(|x| x.as_i64()).unwrap_or(0);
                    let stars = r.get("star_count").and_then(|x| x.as_i64()).unwrap_or(0);
                    let installs = r.get("installs_30d").and_then(|x| x.as_i64()).unwrap_or(0);
                    o.insert("popularity".into(), json!(vouch.max(stars).max(installs)));
                }
            }
        }
    }

    // ── price + stock (parts-search across vendors) ──
    if let Some(txt) = run_capped("adom-parts-search", &["search", mpn, "--limit", "3"], 40) {
        if let Some(v) = parse_json_tail(&txt) {
            let mut best_stock: i64 = 0;
            let mut price: Option<String> = None;
            let mut tier: Option<String> = None;
            for vendor in ["mouser", "digikey", "jlcpcb"] {
                if let Some(hits) = v.get(vendor).and_then(|m| m.get("hits")).and_then(|h| h.as_array()) {
                    for h in hits {
                        best_stock = best_stock.max(h.get("stock").and_then(|s| s.as_i64()).unwrap_or(0));
                        if price.is_none() {
                            let up = h.get("unit_price").and_then(|p| p.as_f64())
                                .or_else(|| h.get("price_100").and_then(|p| p.as_f64()));
                            if let Some(p) = up { price = Some(format!("${:.3}", p)); }
                        }
                        if vendor == "jlcpcb" && tier.is_none() {
                            if let Some(flags) = h.get("flags").and_then(|f| f.as_array()) {
                                for f in flags {
                                    if let Some(fs) = f.as_str() {
                                        if ["Basic", "Preferred", "Extended"].contains(&fs) { tier = Some(fs.to_string()); }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            o.insert("stock".into(), json!(best_stock));
            if let Some(p) = price { o.insert("price".into(), json!(p)); }
            if let Some(t) = tier { o.insert("tier".into(), json!(t)); }
        }
    }

    if o.is_empty() { return "{\"error\":\"no data\"}".into(); }
    out.to_string()
}