//! Render a `.kicad_mod` to a themed, cropped footprint SVG via **service-kicad**
//! (no local KiCad, no gallia). Wraps the footprint in a minimal one-footprint
//! board, POSTs to `/kicad/pcb/export/svg`, crops the page to the footprint
//! bounding box, and applies the Adom footprint theme.

use anyhow::{anyhow, Context, Result};
use regex::Regex;

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

fn service_url() -> String {
    std::env::var("KICAD_SERVICE_API")
        .unwrap_or_else(|_| KICAD_SERVICE_DEFAULT.to_string())
        .trim_end_matches('/')
        .to_string()
}

// Where the footprint is placed on the wrapper board (page center, fully on-page).
const OX: f64 = 148.5;
const OY: f64 = 105.0;
const MARGIN: f64 = 1.0;

/// Render the footprint to a themed SVG, cropped to `bbox` (footprint-local mm).
/// CLEAN by default — NO InstaPCB paste-dot overlay. John's standing rule: the
/// default footprint render (cards, thumbnails, preview, the live app's initial
/// view) must show the bare footprint; the InstaPCB paste layer is opt-in. Use
/// `render_footprint_svg_with_paste` only where InstaPCB is the point (`embed`).
pub fn render_footprint_svg(kicad_mod: &str, _bbox: (f64, f64, f64, f64)) -> Result<String> {
    let normalized = normalize_legacy(kicad_mod);
    let fp = crate::fp_render::parse(&normalized);
    let (svg, _ox, _oy) = crate::fp_render::render(&fp);
    Ok(svg)
}

/// Same render, PLUS the InstaPCB paste apertures + 300 µm jetted-dot overlay.
/// Only the `embed` flow (the interactive InstaPCB layer panel) uses this.
pub fn render_footprint_svg_with_paste(kicad_mod: &str, _bbox: (f64, f64, f64, f64)) -> Result<String> {
    let normalized = normalize_legacy(kicad_mod);
    let fp = crate::fp_render::parse(&normalized);
    let (svg, ox, oy) = crate::fp_render::render(&fp);
    let aps = crate::paste::paste_apertures(&normalized);
    let (apert_g, dots_g, _n) = crate::paste::paste_overlay_svg(&aps, ox, oy);
    let out = if let Some(idx) = svg.rfind("</svg>") {
        let mut s = String::with_capacity(svg.len() + apert_g.len() + dots_g.len());
        s.push_str(&svg[..idx]);
        s.push_str(&apert_g);
        s.push_str(&dots_g);
        s.push_str(&svg[idx..]);
        s
    } else {
        svg
    };
    Ok(out)
}

/// Bounding box of the real drawn geometry in a service-kicad PCB SVG, in the
/// SVG's own user units (mm). Scans `<circle>`, `<rect>`, and `<path d=…>`
/// coordinate pairs, then keeps only the points within `CLUSTER_R` of the
/// **known placement center** (`cx_seed`, `cy_seed`) — where we placed the
/// footprint on the page. This rejects the A4 page frame AND the worksheet
/// title block (a dense ~1500-point text cluster in the bottom-right corner
/// that would otherwise capture the median and blow up the crop).
fn svg_geometry_bbox(svg: &str, cx_seed: f64, cy_seed: f64) -> Option<(f64, f64, f64, f64)> {
    const CLUSTER_R: f64 = 30.0; // half-extent ceiling: parts up to ~60 mm across
    let mut xs: Vec<f64> = Vec::new();
    let mut ys: Vec<f64> = Vec::new();

    let circle = Regex::new(r#"<circle\b[^>]*?cx="(-?[\d.]+)"[^>]*?cy="(-?[\d.]+)"(?:[^>]*?r="(-?[\d.]+)")?"#).unwrap();
    for c in circle.captures_iter(svg) {
        if let (Ok(cx), Ok(cy)) = (c[1].parse::<f64>(), c[2].parse::<f64>()) {
            let r = c.get(3).and_then(|m| m.as_str().parse::<f64>().ok()).unwrap_or(0.0);
            xs.push(cx - r); xs.push(cx + r);
            ys.push(cy - r); ys.push(cy + r);
        }
    }
    let rect = Regex::new(r#"<rect\b[^>]*?x="(-?[\d.]+)"[^>]*?y="(-?[\d.]+)"[^>]*?width="(-?[\d.]+)"[^>]*?height="(-?[\d.]+)""#).unwrap();
    for c in rect.captures_iter(svg) {
        if let (Ok(x), Ok(y), Ok(w), Ok(h)) = (c[1].parse::<f64>(), c[2].parse::<f64>(), c[3].parse::<f64>(), c[4].parse::<f64>()) {
            xs.push(x); xs.push(x + w);
            ys.push(y); ys.push(y + h);
        }
    }
    let dattr = Regex::new(r#"\bd="([^"]*)""#).unwrap();
    let pair = Regex::new(r"(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)").unwrap();
    for d in dattr.captures_iter(svg) {
        for p in pair.captures_iter(&d[1]) {
            if let (Ok(x), Ok(y)) = (p[1].parse::<f64>(), p[2].parse::<f64>()) {
                xs.push(x);
                ys.push(y);
            }
        }
    }
    if xs.is_empty() {
        return None;
    }
    let (mut minx, mut miny, mut maxx, mut maxy) = (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
    for (&x, &y) in xs.iter().zip(ys.iter()) {
        if (x - cx_seed).abs() > CLUSTER_R || (y - cy_seed).abs() > CLUSTER_R {
            continue; // page frame / worksheet title block — not the footprint
        }
        minx = minx.min(x); maxx = maxx.max(x);
        miny = miny.min(y); maxy = maxy.max(y);
    }
    if minx.is_finite() && maxx > minx {
        Some((minx, miny, maxx, maxy))
    } else {
        None
    }
}

/// Wrap a `.kicad_mod` footprint into a minimal one-footprint board, placed at
/// the page center.
/// Upgrade a legacy KiCad-5 `(module …)` footprint to the modern `(footprint …)`
/// s-expression that KiCad-10's board loader accepts. Real vendor mods (Ultra
/// Librarian, older SnapEDA) still ship the old format, which the strict board
/// parser rejects token-by-token. Handles: the top keyword, the 3D model block
/// (`(at (xyz` → `(offset (xyz`, quote path), unquoted `fp_text` values, legacy
/// graphic widths (`… (width W))` → `(stroke (width W) (type solid)))`), and
/// legacy arcs (`(start=center)(end=point)(angle)` → modern `(start)(mid)(end)`).
fn normalize_legacy(m: &str) -> String {
    let mut s = m.replacen("(module ", "(footprint ", 1);
    // 3D model: quote the path, and (at (xyz → (offset (xyz.
    let model_re = Regex::new(r"(?m)\(model\s+([^\s()\n]+)").unwrap();
    s = model_re.replace_all(&s, |c: &regex::Captures| {
        let p = &c[1];
        let pq = if p.starts_with('"') { p.to_string() } else { format!("\"{p}\"") };
        format!("(model {pq}")
    }).into_owned();
    s = s.replace("(at (xyz", "(offset (xyz");
    // fp_text reference/value/user: quote an unquoted value token.
    let txt_re = Regex::new(r#"\(fp_text (reference|value|user) ([^"\s()][^\s()]*) "#).unwrap();
    s = txt_re.replace_all(&s, |c: &regex::Captures| format!("(fp_text {} \"{}\" ", &c[1], &c[2])).into_owned();
    // legacy fp_arc: start=center, end=arc start point, angle=sweep° → 3-point.
    let arc_re = Regex::new(r"\(fp_arc \(start (-?[\d.]+) (-?[\d.]+)\) \(end (-?[\d.]+) (-?[\d.]+)\) \(angle (-?[\d.]+)\) \(layer ([^)]+)\) \(width ([\d.]+)\)\)").unwrap();
    s = arc_re.replace_all(&s, |c: &regex::Captures| {
        let (cx, cy): (f64, f64) = (c[1].parse().unwrap_or(0.0), c[2].parse().unwrap_or(0.0));
        let (px, py): (f64, f64) = (c[3].parse().unwrap_or(0.0), c[4].parse().unwrap_or(0.0));
        let sweep = c[5].parse::<f64>().unwrap_or(0.0).to_radians();
        let (layer, w) = (&c[6], &c[7]);
        let r = ((px - cx).powi(2) + (py - cy).powi(2)).sqrt();
        let t0 = (py - cy).atan2(px - cx);
        let (ex, ey) = (cx + r * (t0 + sweep).cos(), cy + r * (t0 + sweep).sin());
        let (mx, my) = (cx + r * (t0 + sweep / 2.0).cos(), cy + r * (t0 + sweep / 2.0).sin());
        format!("(fp_arc (start {px:.4} {py:.4}) (mid {mx:.4} {my:.4}) (end {ex:.4} {ey:.4}) (stroke (width {w}) (type solid)) (layer {layer}))")
    }).into_owned();
    // remaining legacy graphic widths (fp_line / fp_circle / fp_poly).
    let w_re = Regex::new(r"\(width ([\d.]+)\)\)").unwrap();
    s = w_re.replace_all(&s, "(stroke (width $1) (type solid)))").into_owned();
    s
}

fn build_pcb_wrap(kicad_mod: &str) -> String {
    // Upgrade legacy KiCad-5 `(module …)` footprints so the strict board loader
    // accepts them (real vendor mods still ship the old format).
    let normalized = normalize_legacy(kicad_mod);
    let kicad_mod = normalized.as_str();
    // Insert an (at OX OY) right after the footprint's layer line so the part
    // is placed at a known page location. Handle BOTH the modern quoted
    // `(layer "F.Cu")` and the legacy unquoted `(layer F.Cu)` (KiCad 5/6 mods
    // still carry the latter) — matching only the quoted form silently left old
    // footprints at their native position and off the computed viewBox.
    let layer_re = Regex::new(r#"\(layer "?F\.Cu"?\)"#).unwrap();
    let placed = match layer_re.find(kicad_mod) {
        Some(m) => {
            let mut s = kicad_mod.trim_end().to_string();
            s.insert_str(m.end(), &format!("\n  (at {OX} {OY})"));
            s
        }
        None => kicad_mod.trim_end().to_string(),
    };
    let indented = placed.replace('\n', "\n  ");
    format!(
        "(kicad_pcb (version 20240108) (generator \"adom\")\n  \
         (general (thickness 1.6))\n  (paper \"A4\")\n  \
         (layers\n    \
         (0 \"F.Cu\" signal)\n    (31 \"B.Cu\" signal)\n    \
         (36 \"B.SilkS\" user) (37 \"F.SilkS\" user)\n    \
         (38 \"B.Mask\" user) (39 \"F.Mask\" user)\n    \
         (44 \"Edge.Cuts\" user)\n    \
         (49 \"F.Fab\" user) (51 \"B.Fab\" user)\n    \
         (46 \"B.CrtYd\" user) (47 \"F.CrtYd\" user)\n  )\n  \
         {indented}\n)\n"
    )
}