//! `adom-parts-search search <keyword>` — unified JSON on stdout.
//! `adom-parts-search show  <keyword>` — same, but also pushes results into
//! the running Hydrogen webview app so the user sees the 3-column grid.

use crate::vendors;
use crate::{ShowArgs, SearchArgs};

pub fn run_search(args: SearchArgs) -> Result<(), String> {
    let agg = vendors::search_all(
        &args.keyword,
        args.limit,
        args.in_stock_only,
        args.mouser_api.as_deref(),
        args.digikey_api.as_deref(),
        args.jlcpcb_api.as_deref(),
    );
    // OK: line to stderr so stdout stays pure JSON for jq.
    let ok_line = format!(
        "Mouser: {} hit(s)  |  DigiKey: {} hit(s)  |  JLCPCB: {} hit(s)  |  recommendation: {}",
        agg.mouser.hits.len(),
        agg.digikey.hits.len(),
        agg.jlcpcb.hits.len(),
        agg.recommendation.as_ref()
            .map(|r| format!("{} {}", r.winner.display(), r.mpn))
            .unwrap_or_else(|| "none".into()),
    );
    if crate::stderr_is_tty() {
        eprintln!("{}{}OK:{} {}", crate::GREEN, crate::BOLD, crate::RESET, ok_line);
    } else {
        eprintln!("OK: {}", ok_line);
    }
    println!("{}", serde_json::to_string_pretty(&agg).unwrap_or_default());
    Ok(())
}

/// `show` — run the search *through* a running app server, so the same
/// results the AI prints to stdout are the ones the user sees in the
/// webview tab. Auto-starts the app in the background if it isn't up.
pub fn run_show(args: ShowArgs) -> Result<(), String> {
    let base = format!("http://127.0.0.1:{}", args.port);

    // 1. Ensure the app server is up. If not, spawn it detached and wait.
    if !ping(&base) {
        crate::note!("app server not running on :{} — starting it...", args.port);
        spawn_app_detached(args.port, &args.tab_name)?;
        wait_until_up(&base, std::time::Duration::from_secs(10))?;
        crate::ok(format!("app server up on :{}", args.port));
    } else {
        crate::note!("app server already up on :{}", args.port);
    }

    // 2. Ensure the Hydrogen tab exists (if the app was already up without
    //    a tab we still want the user to see results).
    ensure_tab(&args.tab_name, args.port);

    // 3. POST the search through the app — server updates its state, UI
    //    poller renders the 3 columns, and we get the full JSON back to
    //    print to stdout.
    let body = serde_json::json!({
        "keyword": args.keyword,
        "limit": args.limit,
        "inStockOnly": args.in_stock_only,
    });
    let resp = post_json(&format!("{}/search", base), body)
        .map_err(|e| format!("POST /search failed: {}", e))?;

    // 4. Human-readable summary to stdout (AI-friendly).
    render_summary_to_stdout(&args.keyword, &resp);

    // 5. Full JSON so downstream jq / AI has structured access to the data.
    println!("{}", serde_json::to_string_pretty(&resp).unwrap_or_default());

    Ok(())
}

fn ping(base: &str) -> bool {
    let agent = ureq::AgentBuilder::new()
        .timeout(std::time::Duration::from_millis(400))
        .build();
    agent.get(&format!("{}/state", base)).call().is_ok()
}

fn wait_until_up(base: &str, total: std::time::Duration) -> Result<(), String> {
    let start = std::time::Instant::now();
    while start.elapsed() < total {
        if ping(base) { return Ok(()); }
        std::thread::sleep(std::time::Duration::from_millis(150));
    }
    Err(format!("app server on {} did not come up within {:?}", base, total))
}

fn spawn_app_detached(port: u16, tab_name: &str) -> Result<(), String> {
    use std::process::{Command, Stdio};
    let exe = std::env::current_exe().map_err(|e| format!("current_exe: {}", e))?;
    Command::new(exe)
        .args([
            "app",
            "--port", &port.to_string(),
            "--tab-name", tab_name,
            "--no-tab",
        ])
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .map(|_| ())
        .map_err(|e| format!("spawn: {}", e))
}

fn ensure_tab(tab_name: &str, port: u16) {
    use std::process::Command;
    // `find-tab` takes the name as a positional arg. Exit-code isn't
    // a reliable signal (the CLI returns 0 in both found and not-found
    // cases), so parse stdout for an object with `panelId`.
    let already = Command::new("adom-cli")
        .args(["hydrogen", "workspace", "find-tab", tab_name])
        .output()
        .ok()
        .and_then(|o| serde_json::from_slice::<serde_json::Value>(&o.stdout).ok())
        .map(|v| v.get("panelId").is_some())
        .unwrap_or(false);
    if already {
        let _ = Command::new("adom-cli")
            .args(["hydrogen", "workspace", "active-tab", "--name", tab_name])
            .output();
        return;
    }
    let url = crate::app::tab::proxy_url(port);
    let icon = include_str!("../docs/icon.svg");
    if let Err(e) = crate::app::tab::add_webview_tab(tab_name, &url, icon) {
        crate::warn(format!("couldn't open tab: {}. Manually visit: {}", e, url));
    }
}

fn post_json(url: &str, body: serde_json::Value) -> Result<serde_json::Value, String> {
    let agent = ureq::AgentBuilder::new()
        .timeout(std::time::Duration::from_secs(60))
        .build();
    match agent.post(url).set("Content-Type", "application/json").send_json(body) {
        Ok(r) => r.into_json().map_err(|e| format!("decode: {}", e)),
        Err(ureq::Error::Status(code, r)) => {
            let body: serde_json::Value = r.into_json().unwrap_or(serde_json::json!({}));
            Err(format!("HTTP {}: {}", code, body))
        }
        Err(e) => Err(e.to_string()),
    }
}

fn render_summary_to_stdout(query: &str, resp: &serde_json::Value) {
    use crate::{BOLD, CYAN, DIM, GREEN, RED, RESET, YELLOW, stdout_is_tty};

    let color = |code: &str, s: &str| -> String {
        if stdout_is_tty() { format!("{}{}{}", code, s, RESET) } else { s.to_string() }
    };
    println!();
    println!("{}  keyword: \"{}\"", color(BOLD, "adom-parts-search"), query);

    for vendor in ["mouser", "digikey", "jlcpcb"] {
        let v = &resp[vendor];
        let color_code = match vendor {
            "mouser"  => "\x1b[38;5;39m",   // Mouser blue
            "digikey" => "\x1b[38;5;203m",  // DigiKey red
            _         => "\x1b[38;5;107m",  // JLCPCB green
        };
        let header = color(color_code, &format!("{:<8}", vendor.to_uppercase()));
        let ok_flag = v.get("ok").and_then(|b| b.as_bool()).unwrap_or(false);
        let hits: Vec<&serde_json::Value> = v.get("hits").and_then(|h| h.as_array()).map(|a| a.iter().collect()).unwrap_or_default();
        let total = v.get("total").and_then(|n| n.as_u64()).unwrap_or(0);
        let elapsed = v.get("elapsed_ms").and_then(|n| n.as_u64()).unwrap_or(0);
        let status = if ok_flag { color(GREEN, "ok") } else {
            let e = v.get("error").and_then(|e| e.as_str()).unwrap_or("");
            color(RED, &format!("err: {}", truncate(e, 60)))
        };
        println!();
        println!("{}  total={:>3}  hits={:>2}  {}ms  [{}]",
                 header, total, hits.len(), elapsed, status);
        for h in hits.iter().take(5) {
            let mpn = h.get("mpn").and_then(|v| v.as_str()).unwrap_or("—");
            let manuf = h.get("manufacturer").and_then(|v| v.as_str()).unwrap_or("");
            let price1 = h.get("price_1").and_then(|v| v.as_str()).unwrap_or("—");
            let stock = h.get("stock").and_then(|v| v.as_u64()).unwrap_or(0);
            let lead = h.get("lead_time").and_then(|v| v.as_str()).unwrap_or("");
            println!("  {} {} {} stock {} {}",
                     color(BOLD, &format!("{:<28}", truncate(mpn, 28))),
                     color(DIM, &format!("{:<16}", truncate(manuf, 16))),
                     color(YELLOW, &format!("{:>9}", price1)),
                     format_stock(stock),
                     color(DIM, lead));
        }
    }

    // Recommendation OR empty-state explainer
    if let Some(rec) = resp.get("recommendation").filter(|v| !v.is_null()) {
        let winner = rec.get("winner").and_then(|v| v.as_str()).unwrap_or("—");
        let mpn = rec.get("mpn").and_then(|v| v.as_str()).unwrap_or("—");
        let price = rec.get("unit_price").and_then(|v| v.as_str()).unwrap_or("—");
        let stock = rec.get("stock").and_then(|v| v.as_u64()).unwrap_or(0);
        let reason = rec.get("reason").and_then(|v| v.as_str()).unwrap_or("");
        println!();
        println!("{}  {}  {}  {}  stock {}",
                 color(BOLD, "RECOMMENDATION →"),
                 color(CYAN, winner),
                 mpn,
                 color(YELLOW, price),
                 stock);
        println!("                  {}", color(DIM, reason));
    } else if let Some(es) = resp.get("empty_state").filter(|v| !v.is_null()) {
        let title = es.get("title").and_then(|v| v.as_str()).unwrap_or("No results.");
        let expl  = es.get("explanation").and_then(|v| v.as_str()).unwrap_or("");
        let hint  = es.get("hint").and_then(|v| v.as_str()).unwrap_or("");
        println!();
        println!("{}  {}", color(BOLD, "NO MATCH —"), color(YELLOW, title));
        if !expl.is_empty() { println!("            {}", color(DIM, expl)); }
        if !hint.is_empty() { println!("            {} {}", color(CYAN, "→"), hint); }
    }
    println!();
}

fn truncate(s: &str, n: usize) -> String {
    if s.chars().count() <= n { s.to_string() } else {
        let mut out: String = s.chars().take(n.saturating_sub(1)).collect();
        out.push('…'); out
    }
}

fn format_stock(n: u64) -> String {
    if n == 0 { "0".to_string() }
    else if n < 10 { format!("{}", n) }
    else if n < 1000 { format!("{}", n) }
    else { format!("{}k", n / 1000) }
}