//! Locate chip-fetcher library entries on disk and inspect their files.
//!
//! chip-fetcher stores each chip at:
//!   /home/adom/project/chip-fetcher/library/<MPN>/
//! with files named <MPN>.{step,kicad_sym,kicad_mod,pdf,...}.
//!
//! This module finds the library root and surfaces per-chip metadata
//! about which source files exist (so the renderers can decide which
//! thumbnails are even possible).

use anyhow::{anyhow, Result};
use std::path::{Path, PathBuf};

/// Marker file written by chip-fetcher after a successful import. Presence
/// means "thumbnails are out of date; re-render." chip-thumbnailer deletes
/// the marker after a successful render.
pub const THUMB_PENDING_MARKER: &str = ".thumb-pending";

/// Errors-from-last-render sidecar so chip-thumbnailer can resume failed
/// renders without re-running the whole pipeline.
pub const THUMB_ERRORS_FILE: &str = ".thumb-errors.json";

pub fn root() -> PathBuf {
    if let Ok(p) = std::env::var("CHIP_FETCHER_LIBRARY") {
        return PathBuf::from(p);
    }
    PathBuf::from("/home/adom/project/chip-fetcher/library")
}

#[derive(Debug, Clone)]
pub struct ChipPaths {
    pub mpn: String,
    pub dir: PathBuf,
    pub kicad_sym: Option<PathBuf>,
    pub kicad_mod: Option<PathBuf>,
    pub step: Option<PathBuf>,
    pub thumb_pending: bool,
}

impl ChipPaths {
    pub fn for_mpn(mpn: &str) -> Result<Self> {
        let dir = root().join(mpn);
        if !dir.is_dir() {
            return Err(anyhow!("chip not in library: {}", dir.display()));
        }
        let pick = |ext: &str| {
            let p = dir.join(format!("{mpn}.{ext}"));
            if p.is_file() {
                Some(p)
            } else {
                None
            }
        };
        Ok(Self {
            mpn: mpn.to_string(),
            kicad_sym: pick("kicad_sym"),
            kicad_mod: pick("kicad_mod"),
            step: pick("step").or_else(|| pick("stp")),
            thumb_pending: dir.join(THUMB_PENDING_MARKER).exists(),
            dir,
        })
    }

    /// Build a ChipPaths for an arbitrary working directory (NOT under the
    /// chip-fetcher library root). Used by `chip-thumbnailer serve` when a
    /// user uploads a lone STEP file: we stage it in a per-session scratch
    /// dir as `<name>.step` and render every 3D orientation into that dir.
    /// Symbol / footprint stay `None` unless a matching `.kicad_sym` /
    /// `.kicad_mod` was staged alongside it.
    pub fn for_dir(dir: PathBuf, mpn: &str) -> Self {
        let pick = |ext: &str| {
            let p = dir.join(format!("{mpn}.{ext}"));
            if p.is_file() {
                Some(p)
            } else {
                None
            }
        };
        Self {
            mpn: mpn.to_string(),
            kicad_sym: pick("kicad_sym"),
            kicad_mod: pick("kicad_mod"),
            step: pick("step").or_else(|| pick("stp")),
            thumb_pending: false,
            dir,
        }
    }

    pub fn symbol_thumb(&self) -> PathBuf {
        self.dir.join(format!("{}-symbol.svg", self.mpn))
    }
    pub fn footprint_thumb(&self) -> PathBuf {
        self.dir.join(format!("{}-footprint.svg", self.mpn))
    }
    /// Canonical 3D thumbnail (as-is orientation, medium / 320×240).
    /// Kept un-suffixed so back-compat consumers still find it.
    pub fn three_d_thumb(&self) -> PathBuf {
        self.three_d_thumb_path(ThreeDOrientation::AsIs, ThreeDSize::Md)
    }
    /// 3D thumbnail at a specific size, as-is orientation.
    pub fn three_d_thumb_size(&self, size: ThreeDSize) -> PathBuf {
        self.three_d_thumb_path(ThreeDOrientation::AsIs, size)
    }
    /// 3D thumbnail at a specific (orientation, size).
    /// As-is keeps the original un-suffixed name; yUp/zUp get a token.
    pub fn three_d_thumb_path(&self, orient: ThreeDOrientation, size: ThreeDSize) -> PathBuf {
        let osuffix = orient.file_suffix();
        let ssuffix = size.suffix();
        self.dir
            .join(format!("{}-3d-iso{osuffix}{ssuffix}.png", self.mpn))
    }
    /// Transparent-background icon for one orientation (Md size, alpha-keyed
    /// from the shaded iso). asIs keeps the canonical `<MPN>-3d-iso-icon.png`
    /// name adom-symbol reads first; others get the orientation token.
    pub fn three_d_icon_path(&self, orient: ThreeDOrientation) -> PathBuf {
        let osuffix = orient.file_suffix();
        self.dir
            .join(format!("{}-3d-iso{osuffix}-icon.png", self.mpn))
    }

    /// Iterator over all three rendered 3D sizes (as-is orientation).
    pub fn three_d_thumbs(&self) -> Vec<(ThreeDSize, PathBuf)> {
        ThreeDSize::all()
            .into_iter()
            .map(|s| (s, self.three_d_thumb_size(s)))
            .collect()
    }
}

/// Up-axis interpretation applied to the source STEP before the 3D
/// render. Six face-up cube orientations cover every "which side is
/// up" question; the consumer (e.g. chip-fetcher's UI) picks the
/// right one for each chip.
///
/// Math + reference impls + per-format catalog:
/// `gallia/skills/up-axis-conventions/SKILL.md`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThreeDOrientation {
    /// Identity. Source +Z maps to render +Z. STEP / KiCad / Solidworks
    /// canonical — right for ~80% of chips.
    AsIs,
    /// `R_x(π)`. Source -Z maps to render +Z. Use when STEP was
    /// authored upside-down in Z (rare).
    ZDown,
    /// `R_x(+π/2)`. Source +Y maps to render +Z. Canonical Y-up,
    /// matches glTF / three.js / Blender / Adom-canonical-viewer
    /// (`rotateYUpToZUp` uses Math.PI/2). Right for chips authored
    /// Y-up — Bosch sensors, Espressif modules, Nordic WLCSPs, etc.
    YUp,
    /// `R_x(-π/2)`. Source -Y maps to render +Z. Inverted Y-up — a
    /// chip authored "Y-down" (pins along +Y instead of -Y). WAGO
    /// connectors are the canonical real-world example.
    YDown,
    /// `R_y(-π/2)`. Source +X maps to render +Z. Rare — some
    /// industrial parts.
    XUp,
    /// `R_y(+π/2)`. Source -X maps to render +Z.
    XDown,
}

impl ThreeDOrientation {
    pub fn all() -> [ThreeDOrientation; 6] {
        [
            ThreeDOrientation::AsIs,
            ThreeDOrientation::ZDown,
            ThreeDOrientation::YUp,
            ThreeDOrientation::YDown,
            ThreeDOrientation::XUp,
            ThreeDOrientation::XDown,
        ]
    }
    /// Filename token appended between `-3d-iso` and the size suffix.
    /// AsIs is empty (canonical un-suffixed name for back-compat).
    pub fn file_suffix(self) -> &'static str {
        match self {
            ThreeDOrientation::AsIs => "",
            ThreeDOrientation::ZDown => "-zdown",
            ThreeDOrientation::YUp => "-yup",
            ThreeDOrientation::YDown => "-ydown",
            ThreeDOrientation::XUp => "-xup",
            ThreeDOrientation::XDown => "-xdown",
        }
    }
    /// Label used in the JSON manifest.
    pub fn json_key(self) -> &'static str {
        match self {
            ThreeDOrientation::AsIs => "asIs",
            ThreeDOrientation::ZDown => "zDown",
            ThreeDOrientation::YUp => "yUp",
            ThreeDOrientation::YDown => "yDown",
            ThreeDOrientation::XUp => "xUp",
            ThreeDOrientation::XDown => "xDown",
        }
    }
    /// Value passed to `step2glb thumbnail --up-axis ...`.
    pub fn step2glb_arg(self) -> &'static str {
        match self {
            ThreeDOrientation::AsIs => "asIs",
            ThreeDOrientation::ZDown => "zDown",
            ThreeDOrientation::YUp => "yUp",
            ThreeDOrientation::YDown => "yDown",
            ThreeDOrientation::XUp => "xUp",
            ThreeDOrientation::XDown => "xDown",
        }
    }
    /// Human-readable label for picker UIs.
    pub fn label(self) -> &'static str {
        match self {
            ThreeDOrientation::AsIs => "as-is (Z-up canonical)",
            ThreeDOrientation::ZDown => "Z-down (R_x(π))",
            ThreeDOrientation::YUp => "Y-up (R_x(+π/2))",
            ThreeDOrientation::YDown => "Y-down (R_x(-π/2))",
            ThreeDOrientation::XUp => "X-up (R_y(-π/2))",
            ThreeDOrientation::XDown => "X-down (R_y(+π/2))",
        }
    }
}

/// Standard 3D thumbnail sizes. `Md` is the canonical filename
/// (no suffix) — kept for back-compat with chip-thumbnailer 0.1.x
/// and downstream consumers that expect `<MPN>-3d-iso.png`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThreeDSize {
    Sm,
    Md,
    Lg,
}

impl ThreeDSize {
    pub fn all() -> [ThreeDSize; 3] {
        [ThreeDSize::Sm, ThreeDSize::Md, ThreeDSize::Lg]
    }
    /// (width, height)
    pub fn dims(self) -> (u32, u32) {
        match self {
            ThreeDSize::Sm => (160, 120),
            ThreeDSize::Md => (320, 240),
            ThreeDSize::Lg => (800, 600),
        }
    }
    /// Filename suffix appended after `-3d-iso` and before `.png`.
    /// Md → "" (canonical, no suffix). Sm → "-sm". Lg → "-lg".
    pub fn suffix(self) -> &'static str {
        match self {
            ThreeDSize::Sm => "-sm",
            ThreeDSize::Md => "",
            ThreeDSize::Lg => "-lg",
        }
    }
    pub fn label(self) -> &'static str {
        match self {
            ThreeDSize::Sm => "sm",
            ThreeDSize::Md => "md",
            ThreeDSize::Lg => "lg",
        }
    }
    pub fn parse(s: &str) -> Option<Self> {
        match s.to_ascii_lowercase().as_str() {
            "sm" | "small" => Some(ThreeDSize::Sm),
            "md" | "medium" => Some(ThreeDSize::Md),
            "lg" | "large" => Some(ThreeDSize::Lg),
            _ => None,
        }
    }
}

/// Walk the library and return every MPN dir.
pub fn list_mpns() -> Result<Vec<String>> {
    let root = root();
    if !root.exists() {
        return Ok(vec![]);
    }
    let mut out = Vec::new();
    for entry in std::fs::read_dir(&root)? {
        let entry = entry?;
        if entry.file_type()?.is_dir() {
            if let Some(name) = entry.file_name().to_str() {
                if !name.starts_with('.') {
                    out.push(name.to_string());
                }
            }
        }
    }
    out.sort();
    Ok(out)
}

pub fn pending_chips() -> Result<Vec<String>> {
    let mut out = Vec::new();
    for mpn in list_mpns()? {
        if root().join(&mpn).join(THUMB_PENDING_MARKER).exists() {
            out.push(mpn);
        }
    }
    Ok(out)
}

pub fn touch_pending(mpn: &str) -> Result<()> {
    let path = root().join(mpn).join(THUMB_PENDING_MARKER);
    std::fs::write(&path, b"").map_err(|e| anyhow!("touch {}: {e}", path.display()))
}

pub fn clear_pending(mpn: &str) -> Result<()> {
    let path = root().join(mpn).join(THUMB_PENDING_MARKER);
    if path.exists() {
        std::fs::remove_file(&path).map_err(|e| anyhow!("rm {}: {e}", path.display()))?;
    }
    Ok(())
}

pub fn write_errors(mpn: &str, kinds: &[(&str, String)]) -> Result<PathBuf> {
    let path = root().join(mpn).join(THUMB_ERRORS_FILE);
    let mut obj = serde_json::Map::new();
    for (k, v) in kinds {
        obj.insert((*k).to_string(), serde_json::Value::String(v.clone()));
    }
    let blob = serde_json::Value::Object(obj);
    std::fs::write(&path, serde_json::to_vec_pretty(&blob)?)?;
    Ok(path)
}

pub fn clear_errors(mpn: &str) {
    let _ = std::fs::remove_file(root().join(mpn).join(THUMB_ERRORS_FILE));
}

pub fn relative_to_workspace(p: &Path) -> String {
    p.strip_prefix("/home/adom/project")
        .map(|x| format!("/home/adom/project/{}", x.display()))
        .unwrap_or_else(|_| p.display().to_string())
}