use anyhow::Result;
use serde::{Deserialize, Serialize};

const KICAD_BASE: &str = "https://kicad-rk5ue5pcfemi.adom.cloud";

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resolved {
    pub input: String,
    pub kicad_baseline: Option<KicadBaseline>,
    pub search_attempts: Vec<SearchAttempt>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KicadBaseline {
    pub library: String,
    pub name: String,
    pub source: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchAttempt {
    pub library: String,
    pub name: String,
    pub result: String,
}

/// Resolve a free-form package string against the service-kicad standard library.
/// Tries a hand-curated normalize map first, then probes service-kicad for each candidate.
pub fn resolve(package: &str, info_json_hint: Option<&str>) -> Result<Resolved> {
    let combined = match info_json_hint {
        Some(h) => format!("{package} | {h}"),
        None => package.to_string(),
    };

    let mut candidates = candidate_map(&combined);
    // Dedup while preserving order.
    let mut seen = std::collections::HashSet::new();
    candidates.retain(|c| seen.insert(format!("{}:{}", c.0, c.1)));

    let mut attempts = Vec::new();
    for (lib, name) in &candidates {
        let result = probe_kicad(lib, name);
        attempts.push(SearchAttempt {
            library: lib.clone(),
            name: name.clone(),
            result: result.clone(),
        });
        if result == "200" {
            return Ok(Resolved {
                input: package.to_string(),
                kicad_baseline: Some(KicadBaseline {
                    library: lib.clone(),
                    name: name.clone(),
                    source: "service-kicad-stdlib".to_string(),
                }),
                search_attempts: attempts,
            });
        }
    }

    Ok(Resolved {
        input: package.to_string(),
        kicad_baseline: None,
        search_attempts: attempts,
    })
}

fn probe_kicad(library: &str, name: &str) -> String {
    let url = format!("{KICAD_BASE}/fp/export/svg/{library}/{name}");
    match ureq::get(&url)
        .timeout(std::time::Duration::from_secs(4))
        .call()
    {
        Ok(r) if r.status() == 200 => "200".to_string(),
        Ok(r) => r.status().to_string(),
        Err(ureq::Error::Status(code, _)) => code.to_string(),
        Err(e) => format!("err:{e}"),
    }
}

/// Generate plausible (library, name) candidates for a free-form package string.
/// Hand-curated heuristics; expand as we observe misses on real datasheets.
fn candidate_map(s: &str) -> Vec<(String, String)> {
    let upper = s.to_uppercase();
    let mut out: Vec<(String, String)> = Vec::new();

    // VSSOP / MSOP small-outline.
    if upper.contains("VSSOP-10") || upper.contains("VSSOP10") || upper.contains("MSOP-10") {
        out.push(("Package_SO".into(), "VSSOP-10_3x3mm_P0.5mm".into()));
        out.push(("Package_SO".into(), "MSOP-10_3x3mm_P0.5mm".into()));
    }
    if upper.contains("VSSOP-8") || upper.contains("MSOP-8") {
        out.push(("Package_SO".into(), "VSSOP-8_3.0x3.0mm_P0.65mm".into()));
        out.push(("Package_SO".into(), "MSOP-8_3x3mm_P0.65mm".into()));
    }

    // SOIC narrow / wide.
    if upper.contains("SOIC-8") || upper.contains("SO-8") || upper.contains("SOIC8") {
        if upper.contains("WIDE") || upper.contains("208") || upper.contains("5.3X5.3") {
            out.push(("Package_SO".into(), "SOIC-8W_5.3x5.3mm_P1.27mm".into()));
        } else {
            out.push(("Package_SO".into(), "SOIC-8_3.9x4.9mm_P1.27mm".into()));
        }
    }
    if upper.contains("SOIC-14") {
        out.push(("Package_SO".into(), "SOIC-14_3.9x8.7mm_P1.27mm".into()));
    }
    if upper.contains("SOIC-16") {
        if upper.contains("WIDE") || upper.contains("7.5") {
            out.push(("Package_SO".into(), "SOIC-16W_7.5x10.3mm_P1.27mm".into()));
        } else {
            out.push(("Package_SO".into(), "SOIC-16_3.9x9.9mm_P1.27mm".into()));
        }
    }

    // SOT-23 family.
    if upper.contains("SOT-23-5") || upper.contains("SOT23-5") || upper.contains("SOT-25") {
        out.push(("Package_TO_SOT_SMD".into(), "SOT-23-5".into()));
    }
    if upper.contains("SOT-23-6") || upper.contains("SOT23-6") {
        out.push(("Package_TO_SOT_SMD".into(), "SOT-23-6".into()));
    }
    if upper.contains("SOT-23") && !upper.contains("-5") && !upper.contains("-6") {
        out.push(("Package_TO_SOT_SMD".into(), "SOT-23".into()));
    }
    if upper.contains("SOT-563") {
        out.push(("Package_TO_SOT_SMD".into(), "SOT-563".into()));
    }
    if upper.contains("SOT-25") {
        out.push(("Package_TO_SOT_SMD".into(), "SOT-25".into()));
    }

    // QFN / VQFN / DFN.
    if let Some((pin_count, body)) = parse_qfn(&upper) {
        // Try with EP (most QFNs have one), then without.
        out.push((
            "Package_DFN_QFN".into(),
            format!("QFN-{pin_count}-1EP_{body}_P0.5mm_EP3.2x3.2mm"),
        ));
        out.push((
            "Package_DFN_QFN".into(),
            format!("QFN-{pin_count}-1EP_{body}_P0.4mm_EP3.2x3.2mm"),
        ));
        out.push((
            "Package_DFN_QFN".into(),
            format!("QFN-{pin_count}_{body}_P0.5mm"),
        ));
    }

    // LQFP / TQFP.
    if let Some((pin_count, body)) = parse_lqfp(&upper) {
        out.push((
            "Package_LQFP".into(),
            format!("LQFP-{pin_count}_{body}_P0.5mm"),
        ));
        out.push((
            "Package_QFP".into(),
            format!("TQFP-{pin_count}_{body}_P0.5mm"),
        ));
    }

    // BGA — very lookup-heavy; just register the search attempt for now.
    if upper.contains("BGA") {
        if let Some(p) = parse_bga(&upper) {
            out.push(("Package_BGA".into(), p));
        }
    }

    // DIP / PDIP.
    if upper.contains("DIP-8") || upper.contains("PDIP-8") {
        out.push(("Package_DIP".into(), "DIP-8_W7.62mm".into()));
    }
    if upper.contains("DIP-14") || upper.contains("PDIP-14") {
        out.push(("Package_DIP".into(), "DIP-14_W7.62mm".into()));
    }
    if upper.contains("DIP-16") || upper.contains("PDIP-16") {
        out.push(("Package_DIP".into(), "DIP-16_W7.62mm".into()));
    }

    // OLGA / non-standard LGA — not in stdlib; leave blank so caller falls through.
    // (Empty candidate list means kicad_baseline will be None.)

    out
}

fn parse_qfn(upper: &str) -> Option<(u32, String)> {
    // Match patterns like "QFN-32 5x5", "VQFN-32 (5×5)", "VQFN32-5X5", "QFN 32 5X5MM"
    let pin_count = find_after(upper, "QFN").or_else(|| find_after(upper, "QFN-"))?;
    let body = parse_body_dims(upper)?;
    Some((pin_count, body))
}

fn parse_lqfp(upper: &str) -> Option<(u32, String)> {
    let key = if upper.contains("LQFP") {
        "LQFP"
    } else if upper.contains("TQFP") {
        "TQFP"
    } else {
        return None;
    };
    let pin_count = find_after(upper, key)?;
    let body = parse_body_dims(upper)?;
    Some((pin_count, body))
}

fn parse_bga(upper: &str) -> Option<String> {
    let pin_count = find_after(upper, "BGA")?;
    Some(format!("BGA-{pin_count}"))
}

fn find_after(s: &str, prefix: &str) -> Option<u32> {
    let idx = s.find(prefix)?;
    let rest = &s[idx + prefix.len()..];
    let rest = rest.trim_start_matches(['-', ' ']);
    let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
    if digits.is_empty() {
        None
    } else {
        digits.parse().ok()
    }
}

fn parse_body_dims(upper: &str) -> Option<String> {
    // Find patterns like "5X5", "5x5", "7X7", "5×5", "10×10".
    // Boundary rule: the char before the first digit MUST be non-digit
    // (so "VQFN-32 5X5" → "5x5", not "325x5"; "32" gets boundary-rejected).
    let s = upper.replace('×', "X");
    let bytes = s.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        // Boundary check before reading a number.
        let prev_ok = i == 0
            || !(bytes[i - 1].is_ascii_digit() || bytes[i - 1] == b'.');
        if prev_ok && bytes[i].is_ascii_digit() {
            let n1_start = i;
            while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
                i += 1;
            }
            let n1 = &s[n1_start..i];
            if i < bytes.len() && bytes[i] == b'X' {
                i += 1;
                let n2_start = i;
                while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
                    i += 1;
                }
                if i > n2_start {
                    let n2 = &s[n2_start..i];
                    if let (Ok(a), Ok(b)) = (n1.parse::<f64>(), n2.parse::<f64>()) {
                        // Sanity: package body dims are typically 1–30 mm.
                        if (0.5..=50.0).contains(&a) && (0.5..=50.0).contains(&b) {
                            return Some(format!("{n1}x{n2}mm"));
                        }
                    }
                }
            }
        } else {
            i += 1;
        }
    }
    None
}