//! `adom-mouser app` — opens a Hydrogen webview tab with a searchable
//! Mouser UI. The server here is a thin per-session wrapper that proxies
//! the real work to the shared backend (`adom-mouser serve` on 8775, or
//! a remote MOUSER_API URL).
//!
//! Frontend conventions (per app-creator skill):
//!   - brand fonts/colors
//!   - monochrome white icons inside the UI
//!   - Adom teal accent for primary actions
//!   - 2-way HTTP comms (every UI action has a server endpoint)
//!   - console forwarding + optional eval channel for dev
//!   - relative URLs for every fetch/asset (proxy-safe)
//!   - custom SVG favicon served at /favicon.svg

mod tab;

use crate::mouser::SearchResult;
use crate::{note, ok, warn, AppArgs};
use serde_json::{json, Value};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::Duration;
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;

struct AppState {
    api: String,
    dev: bool,
    console: Mutex<VecDeque<Value>>,
    last_query: Mutex<Option<String>>,
    last_results: Mutex<Option<SearchResult>>,
    pending_eval: Mutex<Option<(String, String)>>, // (id, code)
    eval_results: Mutex<std::collections::HashMap<String, Value>>,
}

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())
}

fn read_body(req: &mut tiny_http::Request) -> Result<Value, String> {
    use std::io::Read;
    let mut body = String::new();
    req.as_reader()
        .read_to_string(&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 backend_post(api: &str, body: Value) -> Result<Value, String> {
    let url = if api.ends_with('/') { api.to_string() } else { format!("{}/", api) };
    let agent = ureq::AgentBuilder::new()
        .timeout(Duration::from_secs(30))
        .build();
    let res = match agent.post(&url).set("Content-Type", "application/json").send_json(body) {
        Ok(r) => r,
        // ureq treats 4xx/5xx as errors; unwrap the response so we can
        // forward the JSON body ({"error":"..."}) to the caller.
        Err(ureq::Error::Status(_code, r)) => r,
        Err(e) => return Err(format!("backend POST failed: {}", e)),
    };
    res.into_json().map_err(|e| format!("backend JSON parse failed: {}", e))
}

fn backend_get(url: &str) -> Result<Value, String> {
    let agent = ureq::AgentBuilder::new()
        .timeout(Duration::from_secs(10))
        .build();
    let res = match agent.get(url).call() {
        Ok(r) => r,
        Err(ureq::Error::Status(_code, r)) => r,
        Err(e) => return Err(format!("backend GET failed: {}", e)),
    };
    res.into_json().map_err(|e| format!("backend JSON parse failed: {}", e))
}

fn resolve_backend(explicit: Option<String>) -> String {
    if let Some(s) = explicit.filter(|s| !s.is_empty()) {
        return s;
    }
    "http://127.0.0.1:8775".into()
}

pub fn run(args: AppArgs) -> Result<(), String> {
    let api = resolve_backend(args.api.clone());
    ok(format!("Mouser app starting on port {} (backend: {})", args.port, api));

    let state = Arc::new(AppState {
        api: api.clone(),
        dev: args.dev,
        console: Mutex::new(VecDeque::with_capacity(CONSOLE_MAX)),
        last_query: Mutex::new(None),
        last_results: Mutex::new(None),
        pending_eval: Mutex::new(None),
        eval_results: Mutex::new(std::collections::HashMap::new()),
    });

    let addr = format!("0.0.0.0:{}", args.port);
    let server = Server::http(&addr).map_err(|e| format!("bind {}: {}", addr, e))?;

    // Verify backend reachable up-front; non-fatal, UI shows the error.
    match backend_get(&format!("{}/health", api)) {
        Ok(v) => note!("  backend health: {}", v),
        Err(e) => warn(format!("backend unreachable at {}: {}", api, e)),
    }

    // Open the Hydrogen tab unless --no-tab
    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 via adom-cli: {}. You can still open {} manually.", e, proxy_url)),
        }
    }

    // Ctrl-c cleanup is best-effort via the /shutdown endpoint.
    // If the process is killed abruptly, the user can run:
    //   adom-cli hydrogen workspace remove-tab --name "Mouser Search"
    note!("  {}Press ctrl-c or curl 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 state_ref = Arc::clone(&state);

        let result: (u16, Value, bool) = match (method.clone(), path.as_str()) {
            // Static
            (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()),
                );
                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("Cache-Control", "max-age=86400").unwrap()),
                );
                continue;
            }

            // State + control
            (Method::Get, "/state") => {
                let q = state_ref.last_query.lock().unwrap().clone();
                let r = state_ref.last_results.lock().unwrap().clone();
                (200, json!({
                    "backend": state_ref.api,
                    "dev": state_ref.dev,
                    "last_query": q,
                    "last_results": r,
                }), false)
            }

            (Method::Post, "/search") => match read_body(&mut req) {
                Ok(body) => {
                    let keyword = body.get("keyword").and_then(|v| v.as_str()).unwrap_or("").to_string();
                    let limit = body.get("limit").and_then(|v| v.as_u64()).unwrap_or(20);
                    let in_stock = body.get("inStockOnly").and_then(|v| v.as_bool()).unwrap_or(false);
                    if keyword.trim().is_empty() {
                        (400, json!({"error": "keyword is required"}), false)
                    } else {
                        match backend_post(
                            &state_ref.api,
                            json!({"action":"search","keyword":keyword,"limit":limit,"inStockOnly":in_stock}),
                        ) {
                            Ok(v) => {
                                if let Some(e) = v.get("error").and_then(|v| v.as_str()) {
                                    (502, json!({"error": e}), false)
                                } else {
                                    *state_ref.last_query.lock().unwrap() = Some(keyword);
                                    if let Ok(sr) = serde_json::from_value::<SearchResult>(v.clone()) {
                                        *state_ref.last_results.lock().unwrap() = Some(sr);
                                    }
                                    (200, v, false)
                                }
                            }
                            Err(e) => (502, json!({"error": e}), false),
                        }
                    }
                }
                Err(e) => (400, json!({"error": e}), false),
            },

            (Method::Post, "/part") => match read_body(&mut req) {
                Ok(body) => {
                    let pn = body.get("partNumber").and_then(|v| v.as_str()).unwrap_or("").to_string();
                    if pn.trim().is_empty() {
                        (400, json!({"error": "partNumber is required"}), false)
                    } else {
                        match backend_post(&state_ref.api, json!({"action":"part","partNumber":pn})) {
                            Ok(v) => {
                                if let Some(e) = v.get("error").and_then(|v| v.as_str()) {
                                    (502, json!({"error": e}), false)
                                } else {
                                    (200, v, false)
                                }
                            }
                            Err(e) => (502, json!({"error": e}), false),
                        }
                    }
                }
                Err(e) => (400, json!({"error": e}), false),
            },

            (Method::Get, "/backend-health") => {
                match backend_get(&format!("{}/health", state_ref.api)) {
                    Ok(v) => (200, v, false),
                    Err(e) => (502, json!({"error": e, "backend": state_ref.api}), false),
                }
            }

            // Console forwarding (UI → server)
            (Method::Post, "/console") => match read_body(&mut req) {
                Ok(v) => {
                    let mut cons = state_ref.console.lock().unwrap();
                    if cons.len() >= CONSOLE_MAX {
                        cons.pop_front();
                    }
                    cons.push_back(v);
                    (200, json!({"ok": true}), false)
                }
                Err(e) => (400, json!({"error": e}), false),
            },
            (Method::Get, "/console") => {
                let cons: Vec<Value> = state_ref.console.lock().unwrap().iter().cloned().collect();
                (200, json!({"messages": cons}), false)
            }
            (Method::Delete, "/console") => {
                state_ref.console.lock().unwrap().clear();
                (200, json!({"ok": true}), false)
            }

            // Eval channel (dev only — gated on --dev flag)
            (Method::Post, "/eval") if state_ref.dev => match read_body(&mut req) {
                Ok(v) => {
                    let code = v.get("code").and_then(|v| v.as_str()).unwrap_or("").to_string();
                    if code.is_empty() {
                        (400, json!({"error": "code is required"}), false)
                    } else {
                        let id = format!("ev{}", rand_suffix());
                        *state_ref.pending_eval.lock().unwrap() = Some((id.clone(), code));
                        (200, json!({"id": id}), false)
                    }
                }
                Err(e) => (400, json!({"error": e}), false),
            },
            (Method::Get, "/eval/pending") if state_ref.dev => {
                let pending = state_ref.pending_eval.lock().unwrap().take();
                match pending {
                    Some((id, code)) => (200, json!({"id": id, "code": code}), false),
                    None => (200, json!({}), false),
                }
            }
            (Method::Post, path) if state_ref.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 result = v.get("result").cloned().unwrap_or(Value::Null);
                        state_ref.eval_results.lock().unwrap().insert(id.clone(), result);
                        (200, json!({"ok": true}), false)
                    }
                    Err(e) => (400, json!({"error": e}), false),
                }
            }
            (Method::Get, path) if state_ref.dev && path.starts_with("/eval/") => {
                let id = path.trim_start_matches("/eval/").to_string();
                let r = state_ref.eval_results.lock().unwrap().get(&id).cloned();
                match r {
                    Some(v) => (200, json!({"result": v, "done": true}), false),
                    None => (200, json!({"done": false}), false),
                }
            }

            // Shutdown
            (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)}), false),
        };

        let _ = req.respond(json_resp(result.0, result.1));
    }
    Ok(())
}

fn rand_suffix() -> String {
    // Tiny rand using system time — good enough for per-request ids.
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    format!("{:x}", now)
}