//! Backend HTTP proxy for the Mouser API. Replaces
//! gallia/services/mouser/server.js.
//!
//! Endpoints:
//!   GET  /health
//!   GET  /search?keyword=...&limit=10&inStockOnly=false
//!   GET  /part?partNumber=...
//!   POST /                         — action-based: {"action":"search", ...}
//!   GET  /                         — landing page (text/html)
//!
//! Env: MOUSER_API_KEY is REQUIRED.
//! Port: 8775 by default (configurable via --port / MOUSER_PORT).

use crate::mouser::{self, SearchResult};
use crate::{note, ok, warn, ServeArgs, CYAN, RESET};
use serde::Serialize;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tiny_http::{Header, Method, Response, Server};

const LANDING_HTML: &str = include_str!("service_landing.html");
const CACHE_TTL: Duration = Duration::from_secs(10 * 60);
const CACHE_MAX: usize = 200;

#[derive(Clone)]
struct CacheEntry {
    data: Value,
    inserted: Instant,
}

fn cache_get(cache: &Arc<Mutex<HashMap<String, CacheEntry>>>, key: &str) -> Option<Value> {
    let mut m = cache.lock().ok()?;
    if let Some(e) = m.get(key) {
        if e.inserted.elapsed() < CACHE_TTL {
            return Some(e.data.clone());
        }
        m.remove(key);
    }
    None
}

fn cache_set(cache: &Arc<Mutex<HashMap<String, CacheEntry>>>, key: String, data: Value) {
    if let Ok(mut m) = cache.lock() {
        if m.len() >= CACHE_MAX {
            // Evict the oldest entry.
            if let Some((k, _)) = m
                .iter()
                .min_by_key(|(_, v)| v.inserted)
                .map(|(k, v)| (k.clone(), v.clone()))
            {
                m.remove(&k);
            }
        }
        m.insert(key, CacheEntry { data, inserted: Instant::now() });
    }
}

#[derive(Serialize)]
struct HealthResponse<'a> {
    ok: bool,
    version: &'static str,
    cache_size: usize,
    cache_max: usize,
    cache_ttl_ms: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<&'a str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    api_reachable: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    api_error: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    test_results: Option<u64>,
    /// Daily upstream-API-quota telemetry — see `quota` module.
    quota: crate::quota::QuotaState,
    /// True iff a `KEL_ALERT_WEBHOOK` env var is configured.
    kel_alert_configured: bool,
}

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("Access-Control-Allow-Origin", "*").unwrap())
        .with_header(Header::from_bytes("Access-Control-Allow-Methods", "GET,POST,OPTIONS").unwrap())
        .with_header(Header::from_bytes("Access-Control-Allow-Headers", "Content-Type").unwrap())
}

fn html_resp(html: String) -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_string(html)
        .with_status_code(200)
        .with_header(Header::from_bytes("Content-Type", "text/html; charset=utf-8").unwrap())
}

fn parse_query(url: &str) -> HashMap<String, String> {
    let mut out = HashMap::new();
    if let Some(q) = url.split_once('?').map(|(_, q)| q) {
        for pair in q.split('&') {
            if let Some((k, v)) = pair.split_once('=') {
                out.insert(
                    urldecode(k),
                    urldecode(v),
                );
            }
        }
    }
    out
}

fn urldecode(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    let bytes = s.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        let b = bytes[i];
        if b == b'+' {
            out.push(' ');
            i += 1;
        } else if b == b'%' && i + 2 < bytes.len() {
            if let Ok(n) = u8::from_str_radix(&s[i + 1..i + 3], 16) {
                out.push(n as char);
                i += 3;
                continue;
            }
            out.push(b as char);
            i += 1;
        } else {
            out.push(b as char);
            i += 1;
        }
    }
    out
}

fn path_of(url: &str) -> &str {
    url.split_once('?').map(|(p, _)| p).unwrap_or(url)
}

fn do_search(
    api_key: &str,
    cache: &Arc<Mutex<HashMap<String, CacheEntry>>>,
    keyword: &str,
    limit: u32,
    in_stock_only: bool,
) -> Result<SearchResult, String> {
    let cache_key = format!("s:{}:{}:{}", keyword, limit, in_stock_only);
    if let Some(cached) = cache_get(cache, &cache_key) {
        // Cache hit: don't count toward upstream quota (no API call
        // was made). Do count the results returned toward the "usage
        // today" figure so the human-facing stat reflects our reach.
        if let Some(n) = cached.get("components").and_then(|v| v.as_array()).map(|a| a.len() as u64) {
            // (We increment a "served from cache" counter by reusing
            // results_returned; searches is not incremented because
            // that metric is about upstream quota use.)
            let _ = n; // placeholder — not tracked yet; upstream-quota tracking is the priority.
        }
        return serde_json::from_value(cached).map_err(|e| e.to_string());
    }
    match mouser::search(api_key, keyword, limit, in_stock_only) {
        Ok(result) => {
            let returned = result.components.len() as u64;
            crate::quota::record_success(returned);
            let v = serde_json::to_value(&result).map_err(|e| e.to_string())?;
            cache_set(cache, cache_key, v);
            Ok(result)
        }
        Err(e) => {
            // Detect quota-exhausted response in the 403-body path too.
            let lower = e.to_lowercase();
            if lower.contains("maximum calls per day") || lower.contains("toomanyrequests") {
                if crate::quota::record_quota_hit() {
                    let q = crate::quota::get();
                    crate::quota::post_kel_alert(&format!(
                        "⚠️ *Mouser daily API quota exhausted.*\n\
                         Key used up for {} — resets at midnight CT.\n\
                         Usage today: {} searches, {} results returned.\n\
                         (DigiKey + JLCPCB are unaffected.)",
                        q.date_ct, q.searches, q.results_returned
                    ));
                }
            }
            Err(e)
        }
    }
}

fn do_part(
    api_key: &str,
    cache: &Arc<Mutex<HashMap<String, CacheEntry>>>,
    part_number: &str,
) -> Result<SearchResult, String> {
    let cache_key = format!("p:{}", part_number);
    if let Some(cached) = cache_get(cache, &cache_key) {
        return serde_json::from_value(cached).map_err(|e| e.to_string());
    }
    let result = mouser::part(api_key, part_number)?;
    let v = serde_json::to_value(&result).map_err(|e| e.to_string())?;
    cache_set(cache, cache_key, v);
    Ok(result)
}

fn health(
    api_key: &str,
    cache: &Arc<Mutex<HashMap<String, CacheEntry>>>,
    live: bool,
) -> (u16, Value) {
    let has_key = !api_key.is_empty();
    let cache_size = cache.lock().map(|m| m.len()).unwrap_or(0);
    let mut hr = HealthResponse {
        ok: has_key,
        version: env!("CARGO_PKG_VERSION"),
        cache_size,
        cache_max: CACHE_MAX,
        cache_ttl_ms: CACHE_TTL.as_millis() as u64,
        error: if has_key { None } else { Some("MOUSER_API_KEY not set") },
        api_reachable: None,
        api_error: None,
        test_results: None,
        quota: crate::quota::get(),
        kel_alert_configured: std::env::var("KEL_ALERT_WEBHOOK").map(|s| !s.is_empty()).unwrap_or(false),
    };
    if has_key && live {
        match mouser::ping(api_key) {
            Ok(n) => {
                hr.api_reachable = Some(true);
                hr.test_results = Some(n);
            }
            Err(e) => {
                hr.api_reachable = Some(false);
                hr.api_error = Some(e);
            }
        }
    }
    let code = if hr.ok { 200 } else { 500 };
    (code, serde_json::to_value(&hr).unwrap_or_else(|_| json!({"ok": false})))
}

pub fn run_serve(args: ServeArgs) -> Result<(), String> {
    let api_key = std::env::var("MOUSER_API_KEY").unwrap_or_default();
    if api_key.is_empty() {
        warn("MOUSER_API_KEY is not set — service will return errors for live queries.");
    }
    let addr = format!("0.0.0.0:{}", args.port);
    let server = Server::http(&addr).map_err(|e| format!("bind {}: {}", addr, e))?;
    let cache: Arc<Mutex<HashMap<String, CacheEntry>>> = Arc::new(Mutex::new(HashMap::new()));
    ok(format!("adom-mouser backend listening on port {}", args.port));
    note!("  {CYAN}GET  /health{RESET}  — health check + live API ping");
    note!("  {CYAN}GET  /search?keyword=...{RESET}  — keyword/MPN search");
    note!("  {CYAN}GET  /part?partNumber=...{RESET}  — part lookup");
    note!("  {CYAN}POST /{RESET}  — action-based API");

    for mut request in server.incoming_requests() {
        // CORS preflight
        if request.method() == &Method::Options {
            let _ = request.respond(json_resp(204, json!({})));
            continue;
        }
        let url = request.url().to_string();
        let method = request.method().clone();
        let path = path_of(&url).trim_end_matches('/');
        let path = if path.is_empty() { "/" } else { path };

        // Landing page on GET / with Accept: text/html
        if method == Method::Get && path == "/" {
            let wants_html = request
                .headers()
                .iter()
                .any(|h| h.field.equiv("Accept") && h.value.as_str().contains("text/html"));
            if wants_html {
                let proto = request
                    .headers()
                    .iter()
                    .find(|h| h.field.equiv("X-Forwarded-Proto"))
                    .map(|h| h.value.as_str().to_string())
                    .unwrap_or_else(|| "https".into());
                let host = request
                    .headers()
                    .iter()
                    .find(|h| h.field.equiv("X-Forwarded-Host"))
                    .or_else(|| request.headers().iter().find(|h| h.field.equiv("Host")))
                    .map(|h| h.value.as_str().to_string())
                    .unwrap_or_else(|| format!("localhost:{}", args.port));
                let base = format!("{}://{}", proto, host);
                let key_present = !api_key.is_empty();
                let html = LANDING_HTML
                    .replace("{{BASE_URL}}", &base)
                    .replace("{{STATUS_CLASS}}", if key_present { "ok" } else { "err" })
                    .replace(
                        "{{STATUS_TEXT}}",
                        if key_present {
                            "Healthy — API key configured"
                        } else {
                            "Error — MOUSER_API_KEY not set"
                        },
                    );
                let _ = request.respond(html_resp(html));
                continue;
            }
        }

        let result: (u16, Value) = match (&method, path) {
            (Method::Get, "/health") => {
                // Default: CHEAP health check — no upstream ping (every
                // ping burns a slot on Mouser's daily-quota counter).
                // Opt-in via `?live=true` when you actually want the
                // end-to-end reachability test.
                let q = parse_query(&url);
                let live = q.get("live").map(|s| s == "true").unwrap_or(false);
                health(&api_key, &cache, live)
            }
            (Method::Get, "/search") => {
                let q = parse_query(&url);
                let keyword = q.get("keyword").cloned().unwrap_or_default();
                let limit: u32 = q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(10);
                let in_stock = q.get("inStockOnly").map(|s| s == "true").unwrap_or(false);
                match do_search(&api_key, &cache, &keyword, limit, in_stock) {
                    Ok(r) => (200, serde_json::to_value(&r).unwrap()),
                    Err(e) => (400, json!({"error": e})),
                }
            }
            (Method::Get, "/part") => {
                let q = parse_query(&url);
                let pn = q.get("partNumber").cloned().unwrap_or_default();
                match do_part(&api_key, &cache, &pn) {
                    Ok(r) => (200, serde_json::to_value(&r).unwrap()),
                    Err(e) => (400, json!({"error": e})),
                }
            }
            (Method::Post, "/") => {
                let mut body = String::new();
                if std::io::Read::read_to_string(&mut request.as_reader(), &mut body).is_err() {
                    let _ = request.respond(json_resp(400, json!({"error": "read body"})));
                    continue;
                }
                let body_json: Value = match serde_json::from_str(&body) {
                    Ok(v) => v,
                    Err(_) => {
                        let _ = request.respond(json_resp(400, json!({"error": "invalid JSON"})));
                        continue;
                    }
                };
                let action = body_json.get("action").and_then(|v| v.as_str()).unwrap_or("");
                match action {
                    "search" => {
                        let keyword = body_json.get("keyword").and_then(|v| v.as_str()).unwrap_or("").to_string();
                        let limit = body_json.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as u32;
                        let in_stock = body_json.get("inStockOnly").and_then(|v| v.as_bool()).unwrap_or(false);
                        match do_search(&api_key, &cache, &keyword, limit, in_stock) {
                            Ok(r) => (200, serde_json::to_value(&r).unwrap()),
                            Err(e) => (400, json!({"error": e})),
                        }
                    }
                    "part" => {
                        let pn = body_json.get("partNumber").and_then(|v| v.as_str()).unwrap_or("").to_string();
                        match do_part(&api_key, &cache, &pn) {
                            Ok(r) => (200, serde_json::to_value(&r).unwrap()),
                            Err(e) => (400, json!({"error": e})),
                        }
                    }
                    "health" => health(&api_key, &cache, false),
                    other => (400, json!({"error": format!("unknown action: '{}'. Try: search, part, health", other)})),
                }
            }
            _ => (404, json!({"error": format!("unknown endpoint: {} {}", method, path)})),
        };

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