// Thumbnail generation by shelling to the canonical `chip-thumbnailer` CLI.
//
// chip-thumbnailer is the single source of truth for rendering the three
// per-chip artifacts under library/<MPN>/ — keeping the rendering logic
// (KiCad-CLI invocations, SVG dark theming, OCCT iso-pose render) in one
// place across every Adom tool that needs them.
//
// Output paths (canonical):
//   <MPN>-symbol.svg
//   <MPN>-footprint.svg
//   <MPN>-3d-iso-sm.png   (160×120)
//   <MPN>-3d-iso.png      (320×240) — primary, used by the dashboard
//   <MPN>-3d-iso-lg.png   (800×600) — used for full-screen pup tab on click
//
// chip-thumbnailer is idempotent: it skips artifacts whose source hasn't
// changed since the last render.

use anyhow::{Context, Result};
use std::process::Command;

use crate::library;

const CHIP_THUMBNAILER_BIN: &str = "adom-chip-thumbnailer";
const PENDING_MARKER: &str = ".thumb-pending";

pub struct GenReport {
    pub mpn: String,
    pub generated: Vec<&'static str>,
    pub skipped: Vec<&'static str>,
    pub errors: Vec<(String, String)>,
}

/// Render thumbnails for a single chip. `force = true` touches the
/// pending-marker first so chip-thumbnailer re-renders even if the output
/// is newer than the source.
pub fn generate_for(mpn: &str, force: bool) -> Result<GenReport> {
    let dir = library::root().join(mpn);
    if !dir.exists() {
        anyhow::bail!("library/{mpn}/ does not exist");
    }
    if force {
        let _ = std::fs::write(dir.join(PENDING_MARKER), b"");
    }

    let out = Command::new(CHIP_THUMBNAILER_BIN)
        .arg("once")
        .arg(mpn)
        .output()
        .context("running chip-thumbnailer once — install with `chip-thumbnailer install`")?;

    let mut report = GenReport {
        mpn: mpn.to_string(),
        generated: Vec::new(),
        skipped: Vec::new(),
        errors: Vec::new(),
    };

    let stdout = String::from_utf8_lossy(&out.stdout);
    let stderr = String::from_utf8_lossy(&out.stderr);

    // Parse chip-thumbnailer's `OK: sym → ...` / `ERROR: 3d → ...` lines so
    // chip-fetcher can show the same per-kind summary it always has.
    for line in stdout.lines().chain(stderr.lines()) {
        let l = strip_ansi(line);
        if let Some(rest) = l.strip_prefix("OK: ") {
            if rest.starts_with("sym ") || rest.starts_with("sym\t") || rest.starts_with("sym→") || rest.starts_with("sym ") {
                report.generated.push("symbol");
            } else if rest.starts_with("fp ") || rest.starts_with("fp\t") || rest.starts_with("fp→") || rest.starts_with("fp ") {
                report.generated.push("footprint");
            } else if rest.starts_with("3d ") || rest.starts_with("3d\t") || rest.starts_with("3d→") || rest.starts_with("3d ") {
                report.generated.push("chip3d");
            }
        } else if let Some(rest) = l.strip_prefix("ERROR: ") {
            // Best-effort kind parsing
            let kind = if rest.starts_with("sym") { "symbol" }
                       else if rest.starts_with("fp") { "footprint" }
                       else if rest.starts_with("3d") { "chip3d" }
                       else { "render" };
            report.errors.push((kind.into(), rest.trim().to_string()));
        }
    }

    if !out.status.success() && report.errors.is_empty() {
        report.errors.push(("setup".into(), format!("chip-thumbnailer exited {:?}: {}", out.status.code(), stderr.trim())));
    }

    // Dedup the kinds in case parsing matched twice
    report.generated.sort_unstable();
    report.generated.dedup();

    // Cross-app: also render the ds2sf source-variant thumbnails (best-effort).
    generate_ds2sf_variants(&dir, mpn);

    Ok(report)
}

/// Render the ds2sf datasheet-extracted symbol/footprint to their own variant
/// thumbnails (`<mpn>-symbol.ds2sf.svg`, `<mpn>-footprint.ds2sf.svg`) so the
/// provenance carat shows a ds2sf option distinct from the vendor download.
/// Best-effort — skipped silently if the tools or the ds2sf extraction are absent.
fn generate_ds2sf_variants(dir: &std::path::Path, mpn: &str) {
    // The canonical SYM/FP tile must show the ACTUAL vendor download (the source
    // the carat labels "SnapEDA"/etc.), not the ds2sf datasheet synthesis.
    // chip-thumbnailer prefers the ds2sf "authoritative" SVG for the footprint
    // (it predates a working service-kicad path), which made the vendor tile show
    // "ds2sf SYNTHESIZED". Re-render the canonical tiles from the real vendor
    // files via adom-symbol/adom-footprint (service-kicad), overwriting that.
    let canon_mod = dir.join(format!("{mpn}.kicad_mod"));
    if canon_mod.is_file() {
        let _ = Command::new("adom-footprint")
            .args(["render", "--file"]).arg(&canon_mod)
            .arg("--out").arg(dir.join(format!("{mpn}-footprint.svg")))
            .output();
    }
    let sym_json = dir.join(format!("{mpn}-symbol.extracted.json"));
    if sym_json.is_file() {
        let out = dir.join(format!("{mpn}-symbol.ds2sf.svg"));
        let _ = Command::new("adom-symbol")
            .args(["render-ds2sf", "--file"])
            .arg(&sym_json).arg("--out").arg(&out)
            .output();
    }
    let fp_json = dir.join(format!("{mpn}-footprint.extracted.json"));
    if fp_json.is_file() {
        let out = dir.join(format!("{mpn}-footprint.ds2sf.svg"));
        let _ = Command::new("adom-footprint")
            .args(["render-ds2sf", "--file"])
            .arg(&fp_json).arg("--out").arg(&out)
            .output();
    }
    // Black-&-white vector OUTLINE icons (OCCT HLR) at the chosen orientation.
    let step = dir.join(format!("{mpn}.step"));
    if step.is_file() {
        let axis = std::fs::read_to_string(dir.join("info.json"))
            .ok()
            .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
            .and_then(|v| v.get("chosen_up_axis").and_then(|a| a.as_str()).map(String::from))
            .unwrap_or_else(|| "z".to_string());
        let _ = Command::new("python3")
            .arg("/home/adom/project/chip-fetcher/scripts/occt-outline.py")
            .arg(&step).arg(dir).arg(mpn).arg(&axis)
            .output();
        // Transparent-background iso icon (chroma-key the shaded iso for the
        // chosen axis) so the symbol-centre overlay has no ugly box.
        let iso_in = match axis.as_str() {
            "y" | "yup" => format!("{mpn}-3d-iso-yup.png"),
            "x" | "xup" => format!("{mpn}-3d-iso-xup.png"),
            "-x" | "xdown" => format!("{mpn}-3d-iso-xdown.png"),
            "-y" | "ydown" => format!("{mpn}-3d-iso-ydown.png"),
            "-z" | "zdown" => format!("{mpn}-3d-iso-zdown.png"),
            _ => format!("{mpn}-3d-iso.png"),
        };
        let iso_path = dir.join(&iso_in);
        if iso_path.is_file() {
            let _ = Command::new("python3")
                .arg("/home/adom/project/chip-fetcher/scripts/occt-iso.py")
                .arg(&iso_path)
                .arg(dir.join(format!("{mpn}-3d-iso-icon.png")))
                .output();
        }
        // Name-marked 3D: emboss the part number on the chip's top face (real
        // STEP geometry) and render the iso + flat-top outline variants, so the
        // "laser the name on the chip" mode has perspective-correct assets that
        // match what ships to KiCad. Best-effort; skipped if OCCT isn't present.
        generate_named_assets(dir, mpn, &axis);
    }

    // Canonical SYMBOL render — done LAST, AFTER the transparent iso icon exists.
    // John's rule: show adom-symbol's DEFAULT symbol with the iso 3D chip
    // composited into the body. The order matters: rendering the symbol BEFORE
    // the `-3d-iso-icon.png` (transparent) is generated bakes in the shaded
    // `-3d-iso.png` instead — a gray box behind the chip. Generating the icon
    // first (above) guarantees the symbol gets the clean transparent overlay.
    let canon_sym = dir.join(format!("{mpn}.kicad_sym"));
    if canon_sym.is_file() {
        let mut cmd = Command::new("adom-symbol");
        cmd.args(["render", "--file"]).arg(&canon_sym)
            .arg("--out").arg(dir.join(format!("{mpn}-symbol.svg")));
        let iso_icon = dir.join(format!("{mpn}-3d-iso-icon.png"));
        let iso_plain = dir.join(format!("{mpn}-3d-iso.png"));
        if iso_icon.is_file() {
            cmd.arg("--iso").arg(&iso_icon);
        } else if iso_plain.is_file() {
            cmd.arg("--iso").arg(&iso_plain);
        }
        let _ = cmd.output();
    }

    // Foreign-format libraries (EAGLE/Fusion .lbr) → convert to KiCad sym+mod via
    // adom-sfconvert so each format renders its OWN geometry in the carat instead
    // of falling back to the canonical KiCad render. Altium .SchLib/.PcbLib are
    // OLE binary — adom-sfconvert errors honestly there, so no Altium thumb yet.
    convert_foreign_libraries(dir, mpn);

    // Per-source vendor variants kept by import (<mpn>.<method>.kicad_sym/.kicad_mod)
    // → render each to its own thumbnail so the carat shows a preview per source.
    if let Ok(rd) = std::fs::read_dir(dir) {
        for e in rd.flatten() {
            let name = e.file_name().to_string_lossy().to_string();
            for (ext, bin, kind) in [("kicad_sym", "adom-symbol", "symbol"), ("kicad_mod", "adom-footprint", "footprint")] {
                let suffix = format!(".{ext}");
                let prefix = format!("{mpn}.");
                if name == format!("{mpn}.{ext}") || !name.ends_with(&suffix) || !name.starts_with(&prefix) {
                    continue;
                }
                let method = &name[prefix.len()..name.len() - suffix.len()];
                if method.is_empty() || method.contains('.') {
                    continue;
                }
                let out = dir.join(format!("{mpn}-{kind}.{method}.svg"));
                if !out.is_file() {
                    let _ = Command::new(bin).args(["render", "--file"]).arg(e.path()).arg("--out").arg(&out).output();
                }
            }
        }
    }
}

/// Convert foreign-format vendor libraries into KiCad sym+mod so each format
/// renders its OWN geometry. Writes `<mpn>.<method>.kicad_sym/.kicad_mod`, which
/// the per-source variant loop above then renders to `<mpn>-<kind>.<method>.svg`.
/// Idempotent + best-effort: if the converted files already exist, or the
/// converter can't handle the format (Altium OLE binary), we leave it alone.
fn convert_foreign_libraries(dir: &std::path::Path, mpn: &str) {
    // EAGLE / Fusion 360 .lbr → NATIVE via adom-sfconvert (eagle.rs, pure Rust —
    // no service round-trip). Emits export .kicad_sym/.kicad_mod AND the native
    // SVG thumbnails (<mpn>.fusion-symbol.svg / <mpn>.fusion-footprint.svg),
    // which we move to the canonical per-source names so the variant loop skips
    // the service render.
    let lbr = dir.join(format!("{mpn}.lbr"));
    if lbr.is_file() && !dir.join(format!("{mpn}-symbol.fusion.svg")).is_file() {
        let _ = Command::new("adom-sfconvert")
            .arg("convert").arg(&lbr)
            .arg("--out-dir").arg(dir)
            .arg("--name").arg(format!("{mpn}.fusion"))
            .output();
        let _ = std::fs::rename(
            dir.join(format!("{mpn}.fusion-symbol.svg")),
            dir.join(format!("{mpn}-symbol.fusion.svg")),
        );
        let _ = std::fs::rename(
            dir.join(format!("{mpn}.fusion-footprint.svg")),
            dir.join(format!("{mpn}-footprint.fusion.svg")),
        );
    }
    // Altium .SchLib (symbol) + .PcbLib (footprint) → NATIVE via adom-sfconvert
    // (altium-schlib / altium-pcblib, pure Rust — no service round-trip). It
    // emits the export .kicad_sym/.kicad_mod AND the native SVG thumbnail
    // (<mpn>.altium-symbol.svg / <mpn>.altium-footprint.svg). We move those SVGs
    // to chip-fetcher's canonical per-source thumbnail names so the variant
    // render loop below finds them already present and skips the service render.
    let schlib = dir.join(format!("{mpn}.SchLib"));
    if schlib.is_file() && !dir.join(format!("{mpn}-symbol.altium.svg")).is_file() {
        let _ = Command::new("adom-sfconvert")
            .arg("convert").arg(&schlib)
            .arg("--out-dir").arg(dir)
            .arg("--name").arg(format!("{mpn}.altium"))
            .output();
        let _ = std::fs::rename(
            dir.join(format!("{mpn}.altium-symbol.svg")),
            dir.join(format!("{mpn}-symbol.altium.svg")),
        );
    }
    let pcblib = dir.join(format!("{mpn}.PcbLib"));
    if pcblib.is_file() && !dir.join(format!("{mpn}-footprint.altium.svg")).is_file() {
        let _ = Command::new("adom-sfconvert")
            .arg("convert").arg(&pcblib)
            .arg("--out-dir").arg(dir)
            .arg("--name").arg(format!("{mpn}.altium"))
            .output();
        let _ = std::fs::rename(
            dir.join(format!("{mpn}.altium-footprint.svg")),
            dir.join(format!("{mpn}-footprint.altium.svg")),
        );
    }
}

const SCRIPTS: &str = "/home/adom/project/chip-fetcher/scripts";

/// Emboss the part number onto the chip's 3D STEP top face and render the
/// name-marked iso + flat-top outline variants. The mark is real geometry so it
/// renders perspective-correctly AND travels with the STEP to KiCad. The shaded
/// iso uses a shallow raised relief; the line-art outline uses flat top-face
/// geometry (top edge only — no extrusion clutter). Best-effort, idempotent.
fn generate_named_assets(dir: &std::path::Path, mpn: &str, axis_raw: &str) {
    let step = dir.join(format!("{mpn}.step"));
    if !step.is_file() {
        return;
    }
    // The emboss only knows the y-up / z-up top face; normalize, skip exotic axes.
    let up = match axis_raw.to_lowercase().as_str() {
        "y" | "yup" | "+y" => "y",
        "z" | "zup" | "+z" | "" => "z",
        _ => return,
    };
    let icon = dir.join(format!("{mpn}-3d-iso-named-icon.png"));
    if icon.is_file() {
        return; // already generated
    }
    // Optional variant second line (e.g. "QFAA-R7") from info.json, if recorded.
    let variant = std::fs::read_to_string(dir.join("info.json"))
        .ok()
        .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
        .and_then(|v| v.get("variant_marking").and_then(|a| a.as_str()).map(String::from))
        .unwrap_or_default();

    // 1. Embossed (shallow relief) STEP → for the shaded iso + KiCad delivery.
    let named_step = dir.join(format!("{mpn}-named.step"));
    let mut emboss = Command::new("python3");
    emboss.arg(format!("{SCRIPTS}/occt-name-on-chip.py")).arg(&step).arg(up).arg(mpn).arg(&named_step);
    if !variant.is_empty() { emboss.arg(&variant); }
    if !matches!(emboss.output(), Ok(o) if o.status.success()) {
        return;
    }
    // 2. Render the named STEP's iso (xvfb-backed V3d) → transparent icon.
    let tmp_iso = dir.join(format!("{mpn}-3d-iso-named.png"));
    let _ = Command::new("xvfb-run").args(["-a", "python3"])
        .arg(format!("{SCRIPTS}/render-iso.py")).arg(&named_step).arg(&tmp_iso).arg(axis_raw)
        .output();
    if tmp_iso.is_file() {
        let _ = Command::new("python3").arg(format!("{SCRIPTS}/occt-iso.py")).arg(&tmp_iso).arg(&icon).output();
    }
    // 3. Outline "-named" variants — single-stroke (centerline) part-number drawn
    //    on the top face (clean even for long names; no outlined-glyph clutter).
    let mut ol = Command::new("python3");
    ol.arg(format!("{SCRIPTS}/occt-outline.py"))
        .arg(&step).arg(dir).arg(mpn).arg(axis_raw)
        .args(["--suffix", "-named", "--name", mpn]);
    if !variant.is_empty() { ol.args(["--line2"]).arg(&variant); }
    let _ = ol.output();
}

pub fn generate_all(force: bool) -> Vec<GenReport> {
    let root = library::root();
    let mut reports = Vec::new();
    if let Ok(entries) = std::fs::read_dir(&root) {
        for entry in entries.flatten() {
            if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { continue; }
            let mpn = match entry.file_name().to_str() {
                Some(s) => s.to_string(),
                None => continue,
            };
            if mpn.starts_with('.') { continue; }
            match generate_for(&mpn, force) {
                Ok(r) => reports.push(r),
                Err(e) => reports.push(GenReport {
                    mpn,
                    generated: vec![],
                    skipped: vec![],
                    errors: vec![("setup".into(), e.to_string())],
                }),
            }
        }
    }
    reports
}

fn strip_ansi(s: &str) -> String {
    // Drop CSI escape sequences (chip-thumbnailer color-codes its output).
    let mut out = String::with_capacity(s.len());
    let bytes = s.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == 0x1b && i + 1 < bytes.len() && bytes[i+1] == b'[' {
            i += 2;
            while i < bytes.len() {
                let b = bytes[i];
                i += 1;
                if (0x40..=0x7e).contains(&b) { break; }
            }
        } else {
            out.push(bytes[i] as char);
            i += 1;
        }
    }
    out
}