//! Fetch a board's AUTHENTIC per-layer SVG from service-kicad (KiCad 10 headless).
//!
//! This is the visual base of the viewer: each layer rendered exactly as the EDA
//! draws it (real Newstroke silk, knockout, theme colours). We request one layer
//! at a time (`?layers=<L>`) so each comes back cleanly in its own theme colour
//! and we can wrap it in our own toggle/flip/dim group. All layers share the
//! board's mm coordinate system, so they align with each other AND with our
//! parsed interaction overlay (same viewBox).

use anyhow::{bail, Result};
use std::time::Duration;

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

pub struct KLayer {
    pub group: String, // our layer group class suffix
    pub side: char,    // 'F' | 'B' | 'I' (inner) | 'X' (shared)
    pub inner: String, // the KiCad SVG content (between <svg> and </svg>)
}

/// (KiCad layer name, our group class, side). Order here is z-order bottom→top;
/// the frontend re-orders F/B when flipping sides.
// Mask sits BELOW copper (so pads read as copper, not mask-purple).
const LAYERS: &[(&str, &str, char)] = &[
    ("B.Fab", "fabB", 'B'),
    ("B.Mask", "maskB", 'B'),
    ("B.Cu", "copperB", 'B'),
    ("B.SilkS", "silkB", 'B'),
    ("F.Mask", "maskF", 'F'),
    ("F.Cu", "copperF", 'F'),
    ("F.SilkS", "silkF", 'F'),
    ("F.Fab", "fabF", 'F'),
    ("B.CrtYd", "crtydB", 'B'),
    ("F.CrtYd", "crtydF", 'F'),
    ("Edge.Cuts", "edge", 'X'),
];

/// Inner copper layers (`In1.Cu`, `In2.Cu`, …) declared in the board's stackup,
/// ordered as they sit physically from the FRONT downwards. A 4-layer board gives
/// `["In1.Cu", "In2.Cu"]`. Returns empty for a plain 2-layer board.
///
/// These are NOT in `LAYERS` because the set is board-specific — a 2/4/6-layer
/// board must not request layers it doesn't have (and a 4-layer board whose pours
/// live only on In1/In2, like the USB3-to-Ethernet molecule, would otherwise show
/// no copper fills at all).
pub fn inner_copper_layers(board: &str) -> Vec<String> {
    let re = regex::Regex::new(r#"\((\d+)\s+"(In\d+\.Cu)"#).unwrap();
    let mut v: Vec<(u32, String)> = re
        .captures_iter(board)
        .filter_map(|c| c[1].parse::<u32>().ok().map(|n| (n, c[2].to_string())))
        .collect();
    v.sort_by_key(|(_, name)| {
        name.trim_start_matches("In").trim_end_matches(".Cu").parse::<u32>().unwrap_or(0)
    });
    v.dedup_by(|a, b| a.1 == b.1);
    v.into_iter().map(|(_, name)| name).collect()
}

/// Our group-class suffix for an inner copper layer: `In1.Cu` -> `copperIn1`.
pub fn inner_group(layer: &str) -> String {
    format!("copper{}", layer.trim_end_matches(".Cu"))
}

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

/// Fetch every board layer. Errors (or an empty result) let the caller fall back
/// to the self-render path.
pub fn fetch_all(board: &str) -> Result<Vec<KLayer>> {
    let base = base();
    let mut out = Vec::new();

    // Base layers, plus this board's inner copper layers. Inner layers are inserted
    // directly after B.Cu in z-order (bottom->top when viewed from the top), i.e.
    // B.Cu, In(n) .. In1, F.Cu — deepest first. The frontend re-orders on flip.
    let inners = inner_copper_layers(board);
    let mut plan: Vec<(String, String, char)> =
        LAYERS.iter().map(|(l, g, s)| (l.to_string(), g.to_string(), *s)).collect();
    if let Some(at) = plan.iter().position(|(l, _, _)| l == "B.Cu") {
        // reverse: the layer nearest the BACK goes lowest in the stack
        for layer in inners.iter().rev() {
            plan.insert(at + 1, (layer.clone(), inner_group(layer), 'I'));
        }
    }

    for (layer, group, side) in &plan {
        let url = format!("{base}/kicad/pcb/export/svg?layers={layer}");
        match ureq::post(&url)
            .set("Content-Type", "application/octet-stream")
            .timeout(Duration::from_secs(30))
            .send_bytes(board.as_bytes())
        {
            Ok(resp) => {
                if let Ok(svg) = resp.into_string() {
                    let mut inner = extract_inner(&svg);
                    // On copper: make the ZONE POUR translucent (traces/pads stay solid),
                    // and recolor KiCad's WHITE drill holes (via/pad barrels) to the dark
                    // board colour so via insides read black, not white.
                    if group.starts_with("copper") {
                        inner = dim_pours(&inner);
                        inner = inner.replace("fill:#FFFFFF", "fill:#0b0e13").replace("fill:#ffffff", "fill:#0b0e13");
                    }
                    if inner.contains("<path") || inner.contains("<circle") {
                        out.push(KLayer { group: group.clone(), side: *side, inner });
                    }
                }
            }
            Err(e) => eprintln!("[kicad] layer {layer}: {e}"),
        }
    }
    if out.is_empty() {
        bail!("service-kicad returned no usable layers");
    }
    Ok(out)
}

/// Make copper zone POURS translucent while leaving traces (stroked, no fill) and
/// pads (small filled paths) solid. Heuristic: a filled path with many `L`
/// segments is a pour; give it a low fill-opacity via an inline style.
fn dim_pours(inner: &str) -> String {
    // KiCad draws each shape as its own `<path style="fill:#…; fill-opacity:1.0000; …">`
    // with the polygon as `M x,y\n x,y … Z` (one comma per vertex). A pour has many
    // vertices; pads/traces have few. For big FILLED paths, knock the fill down.
    //
    // THRESHOLD: measured on the USB3-to-Ethernet molecule (KiCad 10) — the largest
    // PAD on F.Cu is 28 vertices (U3, a SOT-223 tab), while the In1.Cu GND pour is
    // 4182. The old cutoff of 20 sat BELOW the biggest pads, so it dimmed U3's four
    // pads to 28% and the magenta F.Mask beneath showed through: they read as
    // translucent pink instead of copper red. 200 sits ~7x above the largest pad and
    // ~20x below a real pour.
    //
    // Deliberately NOT keyed on `fill-rule:evenodd` — 152 F.Cu paths carry it on a
    // board with zero front pours, so it does not discriminate.
    //
    // Known limit: a very small copper island zone (< ~200 verts) renders solid
    // rather than translucent. That is a cosmetic miss, and far preferable to
    // dimming real pads.
    const POUR_MIN_VERTS: usize = 200;
    let re = regex::Regex::new(r#"<path\b[^>]*>"#).unwrap();
    re.replace_all(inner, |c: &regex::Captures| {
        let tag = &c[0];
        let is_fill = tag.contains("fill:#");
        let verts = tag.matches(',').count(); // ≈ polygon vertices (style has no commas)
        if is_fill && verts > POUR_MIN_VERTS {
            tag.replacen("fill-opacity:1.0000", "fill-opacity:0.28", 1)
        } else {
            tag.to_string()
        }
    })
    .into_owned()
}

/// Keep the content between the opening `<svg …>` and the closing `</svg>`.
fn extract_inner(svg: &str) -> String {
    let open_end = match svg.find("<svg").and_then(|s| svg[s..].find('>').map(|e| s + e + 1)) {
        Some(i) => i,
        None => return String::new(),
    };
    let close = svg.rfind("</svg>").unwrap_or(svg.len());
    svg[open_end..close].to_string()
}