//! Per-chip thumbnail manifest. Written as `<MPN>-thumbs.json` next to
//! the source files after every render. Self-describing — downstream
//! apps and AI callers should be able to consume the manifest alone
//! to know:
//!
//!   1. What thumbnail files exist + their dimensions / sizes
//!   2. What palette colors are baked in (so the host UI can match)
//!   3. Which "full-fidelity" app to launch when the user wants more
//!      than the thumbnail (adom-step for 3D, SymView for symbol,
//!      FpView for footprint)
//!   4. Inline hints — short imperative sentences the AI can echo to
//!      the user without re-deriving them

use crate::library::{ChipPaths, ThreeDOrientation, ThreeDSize};
use anyhow::Result;
use serde::Serialize;
use std::path::PathBuf;

const VERSION: &str = env!("CARGO_PKG_VERSION");

// All manifest types serialize to camelCase JSON. This matches the
// rest of the Adom CLI ecosystem (step2glb's `sizeBytes` /
// `durationMs` / `srcKind`, chip-fetcher's `infoJson`, etc.) so
// downstream apps can use one casing convention everywhere. Rust
// fields stay snake_case (the language norm); the wire format is
// camelCase via serde rename.

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Manifest {
    pub mpn: String,
    pub generated_by: String,
    pub generated_by_version: &'static str,
    pub generated_at_iso: String,
    pub palette: Palette,
    pub thumbnails: Thumbnails,
    pub hints: Vec<String>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Palette {
    // Symbol (gallia/symbol-creator/lib/svg-theme.js THEME_COLORS)
    pub canvas_bg: &'static str,
    pub body_fill: &'static str,
    pub body_fill_grad: &'static str,
    pub body_stroke: &'static str,
    pub pin_wire: &'static str,
    pub pin_name: &'static str,
    pub pin_number: &'static str,
    pub group_label: &'static str,
    pub group_label_hover: &'static str,
    // Adom brand accent (gallia/viewer/adom-theme.js THEME)
    pub accent_teal: &'static str,
    pub accent_teal_bright: &'static str,
    // Footprint (gallia/viewer/kicad-footprint-viewer.js)
    pub pad_fill: &'static str,
    pub pad_stroke: &'static str,
    pub pad_number: &'static str,
    pub courtyard: &'static str,
    pub silkscreen: &'static str,
    pub fab_outline: &'static str,
    // Canvas atmosphere (baked into themed SVGs by chip-thumbnailer)
    pub grid_line: &'static str,
    pub glow: &'static str,
}

impl Palette {
    pub fn canonical() -> Self {
        Self {
            canvas_bg: "#0d1117",
            body_fill: "#1a2332",
            body_fill_grad: "#141c27",
            body_stroke: "#30363d",
            pin_wire: "#4a5568",
            pin_name: "#ffffff",
            pin_number: "#8b949e",
            group_label: "#E6B450",
            group_label_hover: "#FFD180",
            accent_teal: "#00b8b0",
            accent_teal_bright: "#00e6dc",
            pad_fill: "#C83434",
            pad_stroke: "#ef5350",
            pad_number: "#ffffff",
            courtyard: "#FF26E2",
            silkscreen: "#00cccc",
            fab_outline: "#6e7681",
            grid_line: "rgba(255,255,255,0.025)",
            glow: "rgba(0,184,176,0.06)",
        }
    }
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Thumbnails {
    pub symbol: Option<SvgEntry>,
    pub footprint: Option<SvgEntry>,
    #[serde(rename = "threeD")]
    pub three_d: Option<ThreeD>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SvgEntry {
    pub format: &'static str, // "svg"
    pub path: String,         // relative to chip dir
    pub size_bytes: u64,
    pub full_fidelity: FullFidelity,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreeD {
    pub format: &'static str, // "png"
    pub pose: &'static str,   // "iso" (default; iso2/top/front/side available)
    /// Back-compat alias: the `asIs` orientation's PNG sizes. Older
    /// consumers (chip-thumbnailer 0.2.x) that look up
    /// `thumbnails.threeD.iso.md.path` keep working.
    pub iso: PngSizes,
    /// All six face-up orientation variants of the same chip. chip-
    /// thumbnailer is intentionally neutral about which is "correct" —
    /// every variant gets rendered, and downstream tooling
    /// (chipsmith, chipfit, etc.) picks the right one per chip.
    /// See gallia/skills/up-axis-conventions for the math.
    pub orientations: Orientations,
    /// Source-STEP bounding box in millimeters (canonical Adom unit per
    /// gallia/skills/electrical-engineering §1a). `bbox_mils` carries the
    /// same value pre-converted so downstream UIs can render mm + mils
    /// side-by-side without recomputing (the human-ui-patterns §4
    /// multi-unit rule).
    pub bbox_mm: Option<[[f64; 2]; 3]>,
    pub bbox_mils: Option<[[f64; 2]; 3]>,
    pub full_fidelity: FullFidelity,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Orientations {
    /// Identity. Source +Z up. STEP / KiCad / Solidworks canonical.
    pub as_is: OrientationVariant,
    /// `R_x(π)`. Source -Z up.
    pub z_down: OrientationVariant,
    /// `R_x(+π/2)`. Source +Y up. Canonical Y-up (glTF / three.js /
    /// Blender) — Bosch sensors, Espressif modules, Nordic WLCSPs.
    pub y_up: OrientationVariant,
    /// `R_x(-π/2)`. Source -Y up. Inverted Y-up — WAGO connectors.
    pub y_down: OrientationVariant,
    /// `R_y(-π/2)`. Source +X up.
    pub x_up: OrientationVariant,
    /// `R_y(+π/2)`. Source -X up.
    pub x_down: OrientationVariant,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrientationVariant {
    /// Human-readable label; UIs should use this in radio button text.
    pub label: &'static str,
    /// Math notation describing the rotation applied (or "identity").
    pub rotation: &'static str,
    /// PNG sizes — sm, md, lg.
    pub sizes: PngSizes,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PngSizes {
    pub sm: Option<PngEntry>,
    pub md: Option<PngEntry>,
    pub lg: Option<PngEntry>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PngEntry {
    pub path: String,
    pub width: u32,
    pub height: u32,
    pub size_bytes: u64,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FullFidelity {
    /// Adom tool that renders this kind interactively at full fidelity.
    pub tool: &'static str,
    /// Concrete command the user (or an AI agent) can run.
    pub command: String,
    /// One-line description of what extra functionality the full-fidelity
    /// viewer provides over the thumbnail.
    pub description: &'static str,
}

pub fn build(chip: &ChipPaths) -> Manifest {
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| iso8601(d.as_secs()))
        .unwrap_or_else(|_| "1970-01-01T00:00:00Z".into());

    Manifest {
        mpn: chip.mpn.clone(),
        generated_by: "chip-thumbnailer".into(),
        generated_by_version: VERSION,
        generated_at_iso: now,
        palette: Palette::canonical(),
        thumbnails: Thumbnails {
            symbol: svg_entry(&chip.symbol_thumb(), &chip.dir, FullFidelity {
                tool: "symbol-creator (SymView)",
                command: format!(
                    "POST http://127.0.0.1:8781/sym/create with this chip's pin metadata, then open <MPN>-viewer.html. Or for an existing .kicad_sym, run the gallia generateBrandedViewer pipeline (gallia/symbol-creator/lib/viewer-gen.js)."
                ),
                description: "Interactive SymView — pin hover tooltips, group highlights, click-to-pin sticky info, datasheet link, zoom/fit controls.",
            }),
            footprint: svg_entry(&chip.footprint_thumb(), &chip.dir, FullFidelity {
                tool: "FpView (gallia/viewer/kicad-footprint-viewer.js)",
                command: "Run generateFootprintViewer(<MPN>.kicad_mod) to produce <MPN>-fpview.html — wraps the SVG with toolbar, layer toggle, measure tool, pad tooltips, solder-blob preview.".into(),
                description: "Interactive FpView — pad hover tooltips with signal + dimensions, layer toggle (silk / fab / courtyard / solder), Fusion-style measure tool, solder-blob jet-placement preview.",
            }),
            three_d: three_d_entry(chip),
        },
        hints: build_hints(chip),
    }
}

fn svg_entry(path: &PathBuf, _chip_dir: &PathBuf, full_fidelity: FullFidelity) -> Option<SvgEntry> {
    if !path.is_file() {
        return None;
    }
    let size_bytes = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
    Some(SvgEntry {
        format: "svg",
        path: path
            .file_name()
            .map(|n| n.to_string_lossy().to_string())
            .unwrap_or_default(),
        size_bytes,
        full_fidelity,
    })
}

fn three_d_entry(chip: &ChipPaths) -> Option<ThreeD> {
    // Walk every (orientation, size) and collect what landed on disk.
    let as_is  = collect_sizes_for(chip, ThreeDOrientation::AsIs);
    let z_down = collect_sizes_for(chip, ThreeDOrientation::ZDown);
    let y_up   = collect_sizes_for(chip, ThreeDOrientation::YUp);
    let y_down = collect_sizes_for(chip, ThreeDOrientation::YDown);
    let x_up   = collect_sizes_for(chip, ThreeDOrientation::XUp);
    let x_down = collect_sizes_for(chip, ThreeDOrientation::XDown);
    let any = as_is.has_any() || z_down.has_any() || y_up.has_any()
           || y_down.has_any() || x_up.has_any() || x_down.has_any();
    if !any {
        return None;
    }

    // Pull bbox from the sidecar features.json (auto-populated by
    // render_3d::render_orient_size on first render).
    let (bbox_mm, bbox_mils) = read_bbox_for(chip);

    Some(ThreeD {
        format: "png",
        pose: "iso",
        iso: as_is.clone(), // back-compat alias for 0.2.x consumers
        orientations: Orientations {
            as_is:  OrientationVariant { label: ThreeDOrientation::AsIs.label(),  rotation: "identity", sizes: as_is },
            z_down: OrientationVariant { label: ThreeDOrientation::ZDown.label(), rotation: "R_x(π)",   sizes: z_down },
            y_up:   OrientationVariant { label: ThreeDOrientation::YUp.label(),   rotation: "R_x(+π/2)", sizes: y_up },
            y_down: OrientationVariant { label: ThreeDOrientation::YDown.label(), rotation: "R_x(-π/2)", sizes: y_down },
            x_up:   OrientationVariant { label: ThreeDOrientation::XUp.label(),   rotation: "R_y(-π/2)", sizes: x_up },
            x_down: OrientationVariant { label: ThreeDOrientation::XDown.label(), rotation: "R_y(+π/2)", sizes: x_down },
        },
        bbox_mm,
        bbox_mils,
        full_fidelity: FullFidelity {
            tool: "adom-step",
            command: format!("adom-step view library/{}/{}.step", chip.mpn, chip.mpn),
            description: "Interactive STEP viewer — Fusion 360-feel components browser, smart-pick measure tool (mm + mils + angle), hover-inspect, geometry-based pin/contact detection (LGA, BGA, QFN, SOIC, through-hole). Backed by service-step2glb's same OCCT pipeline.",
        },
    })
}

fn collect_sizes_for(chip: &ChipPaths, orient: ThreeDOrientation) -> PngSizes {
    let mut s = PngSizes { sm: None, md: None, lg: None };
    for size in ThreeDSize::all() {
        let path = chip.three_d_thumb_path(orient, size);
        if !path.is_file() {
            continue;
        }
        let (w, h) = size.dims();
        let entry = Some(PngEntry {
            path: path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(),
            width: w,
            height: h,
            size_bytes: std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0),
        });
        match size {
            ThreeDSize::Sm => s.sm = entry,
            ThreeDSize::Md => s.md = entry,
            ThreeDSize::Lg => s.lg = entry,
        }
    }
    s
}

impl PngSizes {
    fn has_any(&self) -> bool {
        self.sm.is_some() || self.md.is_some() || self.lg.is_some()
    }
}

impl Clone for PngSizes {
    fn clone(&self) -> Self {
        PngSizes {
            sm: self.sm.clone(),
            md: self.md.clone(),
            lg: self.lg.clone(),
        }
    }
}

impl Clone for PngEntry {
    fn clone(&self) -> Self {
        PngEntry {
            path: self.path.clone(),
            width: self.width,
            height: self.height,
            size_bytes: self.size_bytes,
        }
    }
}


/// Look for a `<MPN>-features.json` sidecar (left by `step2glb features`)
/// and pull the chip's bbox in mm. Convert to mils for the multi-unit
/// display rule (1 mm = 39.3700787 mils, per electrical-engineering §1a).
fn read_bbox_for(chip: &ChipPaths) -> (Option<[[f64; 2]; 3]>, Option<[[f64; 2]; 3]>) {
    let path = chip.dir.join(format!("{}-features.json", chip.mpn));
    let bytes = match std::fs::read(&path) {
        Ok(b) => b,
        Err(_) => return (None, None),
    };
    let v: serde_json::Value = match serde_json::from_slice(&bytes) {
        Ok(v) => v,
        Err(_) => return (None, None),
    };
    let bb = match v.get("bbox_mm") {
        Some(b) => b,
        None => return (None, None),
    };
    let pair = |key: &str| -> Option<[f64; 2]> {
        let arr = bb.get(key)?.as_array()?;
        Some([arr.get(0)?.as_f64()?, arr.get(1)?.as_f64()?])
    };
    let (Some(x), Some(y), Some(z)) = (pair("x"), pair("y"), pair("z")) else {
        return (None, None);
    };
    let mm = [x, y, z];
    let to_mils = |v: f64| (v * 39.370_078_7 * 100.0).round() / 100.0;
    let mils = [
        [to_mils(x[0]), to_mils(x[1])],
        [to_mils(y[0]), to_mils(y[1])],
        [to_mils(z[0]), to_mils(z[1])],
    ];
    (Some(mm), Some(mils))
}

fn build_hints(chip: &ChipPaths) -> Vec<String> {
    let mut hints = vec![
        "These thumbnails are sized for index / card / tooltip views. When the user clicks on one to see the full version, hand off to the matching full-fidelity tool listed in `thumbnails.<kind>.full_fidelity`.".into(),
        "Vector thumbnails (symbol, footprint) scale infinitely — render at any size with no quality loss. The 3D PNG comes in three preset sizes (sm/md/lg); pick by the consuming UI's container width.".into(),
        "All thumbnails share the canonical Adom dark palette declared in `palette` — match your surrounding UI to it (e.g. card bg `palette.canvas_bg`, accent links `palette.accent_teal_bright`) so thumbnails don't visually clash.".into(),
        "The dark canvas + 2.54 mm PCB grid + faint teal radial glow are baked into the SVGs (so they look correct standalone). If your container already provides those (e.g. SymView), don't double up.".into(),
    ];
    if chip.step.is_some() {
        hints.push(format!(
            "For 3D inspection: `adom-step view library/{}/{}.step` opens the source STEP in an interactive viewer with measure tool + components browser.",
            chip.mpn, chip.mpn
        ));
    }
    if chip.kicad_sym.is_some() {
        hints.push(format!(
            "For the interactive symbol with pin tooltips and group highlights, point a SymView at library/{}/{}.kicad_sym.",
            chip.mpn, chip.mpn
        ));
    }
    if chip.kicad_mod.is_some() {
        hints.push(format!(
            "For the FpView footprint viewer (measure, pad tooltips, solder-blob jet preview), point at library/{}/{}.kicad_mod.",
            chip.mpn, chip.mpn
        ));
    }
    hints.push(
        "If a thumbnail entry is `null` in this manifest, the source file (.kicad_sym / .kicad_mod / .step) wasn't present in the chip dir at render time. chip-fetcher should populate it.".into(),
    );
    hints
}

pub fn write(chip: &ChipPaths) -> Result<PathBuf> {
    let manifest = build(chip);
    let path = chip.dir.join(format!("{}-thumbs.json", chip.mpn));
    let pretty = serde_json::to_vec_pretty(&manifest)?;
    std::fs::write(&path, pretty)?;
    Ok(path)
}

fn iso8601(secs_since_epoch: u64) -> String {
    // Lightweight UTC ISO-8601 formatter (no chrono dep). Enough precision
    // for "when was this rendered".
    let s = secs_since_epoch;
    let days = (s / 86400) as i64;
    let mut secs_of_day = (s % 86400) as i64;
    let hour = secs_of_day / 3600;
    secs_of_day -= hour * 3600;
    let minute = secs_of_day / 60;
    let second = secs_of_day - minute * 60;
    let (year, month, day) = days_to_ymd(days + 719468); // shift epoch
    format!(
        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
        year, month, day, hour, minute, second
    )
}

fn days_to_ymd(z: i64) -> (i64, u32, u32) {
    // Howard Hinnant's date algorithm. Input: days since 0000-03-01.
    let era = if z >= 0 { z / 146097 } else { (z - 146096) / 146097 };
    let doe = (z - era * 146097) as u64;
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
    let y = yoe as i64 + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
    let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
    let y = if m <= 2 { y + 1 } else { y };
    (y, m, d)
}