//! Fetch a `.kicad_sch` rendered to an authentic KiCad SVG from service-kicad,
//! and compose it (cropped to the drawing) with our component hotspot overlay.
//!
//! Like the layout viewer: the visual is the EDA's own export; we only add an
//! invisible interaction layer aligned to the same page-mm coordinate system.

use crate::sch_parse::{self, Sheet};
use anyhow::{bail, Result};
use serde::Serialize;
use std::collections::HashMap;
use std::fmt::Write as _;
use std::time::Duration;

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

#[derive(Serialize)]
pub struct RenderMeta {
    pub view_box: [f64; 4],
    pub components: Vec<sch_parse::Comp>,
    pub comp_count: usize,
}

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

/// Fetch the KiCad SVG for the whole sheet.
fn fetch_svg(sch: &str) -> Result<String> {
    let url = format!("{}/kicad/sch/export/svg", base());
    let resp = ureq::post(&url)
        .set("Content-Type", "application/octet-stream")
        .timeout(Duration::from_secs(30))
        .send_bytes(sch.as_bytes())
        .map_err(|e| anyhow::anyhow!("service-kicad sch export: {e}"))?;
    let svg = resp.into_string()?;
    if !svg.contains("<path") { bail!("service-kicad returned an empty sheet"); }
    Ok(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()
}

/// Tag each KiCad drawing element (path / circle / stroked-text group) with the
/// component whose bbox CONTAINS the element (`data-cref`), so the frontend can
/// recolour just that symbol's real lines on hover. Wires (which span outside any
/// one symbol) and the page frame are never captured.
fn tag_symbols(inner: &str, comps: &[sch_parse::Comp]) -> String {
    use regex::Regex;
    let num = Regex::new(r"-?\d+\.?\d*").unwrap();
    // bbox of a coordinate string (from a `d`/`points` attr; NOT the whole tag)
    let ebbox = |coords: &str| -> Option<(f64, f64, f64, f64)> {
        let ns: Vec<f64> = num.find_iter(coords).filter_map(|m| m.as_str().parse().ok()).collect();
        if ns.len() < 2 { return None; }
        let (mut mnx, mut mny, mut mxx, mut mxy) = (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
        for p in ns.chunks_exact(2) { mnx = mnx.min(p[0]); mxx = mxx.max(p[0]); mny = mny.min(p[1]); mxy = mxy.max(p[1]); }
        if mnx.is_finite() { Some((mnx, mny, mxx, mxy)) } else { None }
    };
    // the SMALLEST component whose bbox fully CONTAINS the element's bbox (so a
    // page-spanning frame path or a wire is never captured, and nested symbols
    // pick the tightest match).
    // smallest region (across every component's body + property-text boxes) that
    // fully CONTAINS the element — wires/frame span outside all of them.
    let find = |e: (f64, f64, f64, f64)| -> Option<&str> {
        let (ex0, ey0, ex1, ey1) = e;
        let t = 1.4; // tolerance so the symbol body outline is caught, not just internals
        let mut best: Option<(&str, f64)> = None;
        for c in comps {
            for r in &c.regions {
                let [x0, y0, x1, y1] = *r;
                if ex0 >= x0 - t && ey0 >= y0 - t && ex1 <= x1 + t && ey1 <= y1 + t {
                    let area = (x1 - x0) * (y1 - y0);
                    if best.map_or(true, |(_, ba)| area < ba) { best = Some((c.reference.as_str(), area)); }
                }
            }
        }
        best.map(|(r, _)| r)
    };

    // Injecting an attribute must PRESERVE self-closing tags: KiCad emits
    // `<path … />`, so the captured tail ends in `/`. Strip it and re-emit `/>`,
    // otherwise the element stops being self-closed and the rest of the sheet
    // nests inside it (which mangles the whole drawing).
    let split_close = |tail: &str| -> (String, &'static str) {
        let t = tail.trim_end();
        match t.strip_suffix('/') {
            Some(rest) => (rest.to_string(), "/>"),
            None => (tail.to_string(), ">"),
        }
    };

    // <path … d="…"/> and <circle … cx cy …/>
    let path_re = Regex::new(r#"<path\b([^>]*?)\bd="([^"]*)"([^>]*)>"#).unwrap();
    let out = path_re.replace_all(inner, |c: &regex::Captures| {
        let (a, d, b) = (&c[1], &c[2], &c[3]);
        let (tail, close) = split_close(b);
        match ebbox(d).and_then(find) {
            Some(r) => format!(r#"<path{} d="{}"{} data-cref="{}"{}"#, a, d, tail, r, close),
            None => format!(r#"<path{} d="{}"{}{}"#, a, d, tail, close),
        }
    }).into_owned();
    // <rect> symbol bodies (e.g. the IC outline) — tag by their x/y/w/h bbox.
    let rect_re = Regex::new(r#"<rect\b([^>]*?)x="([-\d.]+)"\s*y="([-\d.]+)"\s*width="([-\d.]+)"\s*height="([-\d.]+)"([^>]*)>"#).unwrap();
    let out = rect_re.replace_all(&out, |c: &regex::Captures| {
        let (a, x, y, w, h, b) = (&c[1], c[2].parse::<f64>().unwrap_or(0.0), c[3].parse::<f64>().unwrap_or(0.0), c[4].parse::<f64>().unwrap_or(0.0), c[5].parse::<f64>().unwrap_or(0.0), &c[6]);
        let (tail, close) = split_close(b);
        match find((x, y, x + w, y + h)) {
            Some(r) => format!(r#"<rect{}x="{}" y="{}" width="{}" height="{}"{} data-cref="{}"{}"#, a, x, y, w, h, tail, r, close),
            None => format!(r#"<rect{}x="{}" y="{}" width="{}" height="{}"{}{}"#, a, x, y, w, h, tail, close),
        }
    }).into_owned();
    // circles are rare here (junction dots live in wires); tag by centre point.
    let circ_re = Regex::new(r#"<circle\b([^>]*?)cx="([-\d.]+)"\s*cy="([-\d.]+)"([^>]*)>"#).unwrap();
    // NOTE: individual <path>s (incl. every stroked-text letter-stroke) are already
    // tagged above, so we don't need to tag the <g class="stroked-text"> wrapper —
    // that keeps the dim-everything-except-.symhl CSS simple (no group inheritance).
    circ_re.replace_all(&out, |c: &regex::Captures| {
        let (a, cx, cy, b) = (&c[1], c[2].parse::<f64>().unwrap_or(0.0), c[3].parse::<f64>().unwrap_or(0.0), &c[4]);
        let (tail, close) = split_close(b);
        match find((cx, cy, cx, cy)) {
            Some(r) => format!(r#"<circle{}cx="{}" cy="{}"{} data-cref="{}"{}"#, a, cx, cy, tail, r, close),
            None => format!(r#"<circle{}cx="{}" cy="{}"{}{}"#, a, cx, cy, tail, close),
        }
    }).into_owned()
}

fn build_meta(sheet: &Sheet) -> RenderMeta {
    let (x, y, w, h) = sheet.content_bbox;
    RenderMeta { view_box: [x, y, w, h], comp_count: sheet.comps.len(), components: sheet.comps.clone() }
}

/// Top-level render: parse identity → fetch KiCad SVG → crop to the drawing →
/// overlay component hotspots. All share the page-mm coordinate system.
pub fn render_sheet(sch: &str) -> Result<(String, RenderMeta)> {
    let sheet = sch_parse::parse(sch);
    let meta = build_meta(&sheet);
    // tag each KiCad drawing element with its component (data-cref) by coordinate
    // containment, so we can recolour that symbol's actual lines on hover.
    let inner = tag_symbols(&extract_inner(&fetch_svg(sch)?), &sheet.comps);
    let (x, y, w, h) = sheet.content_bbox;

    let mut s = String::new();
    let _ = write!(s, r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{:.3} {:.3} {:.3} {:.3}" preserveAspectRatio="xMidYMid meet">"##, x, y, w, h);
    let _ = write!(s, r##"<rect class="adom-sheet-bg" x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" fill="#fbfbf6"/>"##, x, y, w, h);
    let _ = write!(s, r##"<g class="adom-sheet">{}</g>"##, inner);
    s.push_str(&sch_parse::build_overlay(&sheet));
    s.push_str("</svg>");
    Ok((s, meta))
}

/// Meta-only (for callers that already have an SVG or want the component list).
pub fn meta_json(sch: &str) -> Result<String> {
    Ok(serde_json::to_string(&build_meta(&sch_parse::parse(sch)))?)
}

// small helper so the map type is available if needed later
#[allow(dead_code)]
type _M = HashMap<String, String>;