use serde::Serialize;
use std::path::Path;

#[derive(Serialize)]
pub struct Chip {
    pub mpn: String,
    pub vendor: String,
    pub package: String,
    pub family: String,
    pub fully_bundled: bool,
    pub has_step: bool,
    pub has_sym: bool,           // KiCad .kicad_sym
    pub has_mod: bool,           // KiCad .kicad_mod
    pub has_altium_sch: bool,    // Altium .SchLib
    pub has_altium_pcb: bool,    // Altium .PcbLib
    pub has_altium_intlib: bool, // Altium .IntLib (compiled — SchLib+PcbLib in one)
    pub has_altium: bool,        // (sch && pcb) || intlib — derived for the UI
    pub altium_not_available: bool, // info.json says "not_available" (exhausted all sources)
    pub has_fusion_lbr: bool,    // Fusion 360 .lbr
    pub fusion_not_available: bool, // info.json says "not_available" (exhausted all sources)
    pub has_pdf: bool,
    pub pdf_count: usize,
    pub has_info: bool,
    pub has_thumb_symbol: bool,
    pub sym_rev: u64,                   // mtime(ms) of <MPN>-symbol.svg — bumps when adom-symbol re-renders, so the dashboard reloads the thumbnail instantly
    pub has_thumb_footprint: bool,
    pub has_thumb_chip3d: bool,         // Z-up iso (canonical, <MPN>-3d-iso.png)
    pub has_thumb_chip3d_yup: bool,     // Y-up iso (<MPN>-3d-iso-yup.png) — ME / Y-up source convention
    /// chip-thumbnailer's auto-inferred source-up axis: "z" or "y" (or "?"
    /// when the manifest is missing). Read from <MPN>-thumbs.json's
    /// thumbnails.threeD.inferredSourceUpAxis.
    pub inferred_up_axis: String,
    /// The orientation the user has chosen for this chip ("z" / "y"). Falls
    /// back to the inferred axis when the user hasn't overridden. Stored in
    /// info.json's chosen_up_axis field so downstream tools (adom-step,
    /// adom-chiplinter, etc.) can pick up the right orientation.
    pub chosen_up_axis: String,
    pub step_source: String,
    pub size_bytes: u64,
    pub files: Vec<String>,
    pub info: serde_json::Value,
    pub stock: serde_json::Value,
    /// ds2sf v0.5.1+ outputs: pixel-trustworthy footprint SVG that
    /// downstream consumers (chipsmith visual diff, sym_create previews,
    /// wiki page hero) use as ground truth. Path is relative to the
    /// chip directory; source slug is "service-kicad-stdlib" (canonical
    /// KiCad render) or "ds2sf-synthesized" (best-effort from extracted
    /// dims; layout reference only). Read from
    /// `<MPN>-extraction.result.json`'s `outputs.footprint_svg` field.
    pub authoritative_footprint_svg: Option<String>,
    pub authoritative_footprint_svg_source: Option<String>,
    /// concur v0.x+ persisted state: golden / golden_with_variants /
    /// divergent / unrecoverable. Read from `<MPN>-validation-status.json`
    /// (which our validate.rs writes after every concur run). Surfaces on
    /// the dashboard so the user sees the gold-dataset state per card.
    pub validation_status: Option<String>,
    pub validation_variants: Vec<String>,
    /// Pin-1 sign-off from chipsmith (`<MPN>.chipsmith.json`). `pin1_signed` = the
    /// user lined up the 3D chip's pin-1 to the footprint's pin-1 in chipsmith and
    /// signed off; `pin1_corner` is which corner pin-1 sits in (NW/NE/SW/SE). The
    /// baked `<MPN>.chipsmith.step` is the orientation-corrected canonical model
    /// downstream tools consume. None/false until signed.
    pub pin1_signed: bool,
    pub pin1_corner: Option<String>,
    pub pin1_signed_at: Option<String>,
    pub pin1_measure_ok: Option<bool>,
    /// Which corner the FOOTPRINT's pin-1 sits in (NW/NE/SW/SE), derived from the
    /// `.kicad_mod` (pad "1" vs the pad centroid). This is the TARGET the 3D chip's
    /// pin-1 must line up with — shown as a hint on unsigned cards, and compared
    /// against `pin1_corner` to flag a chip-vs-footprint mismatch once signed.
    pub fp_pin1_corner: Option<String>,
    /// Per-file provenance: which ladder source + URL each library file
    /// came from. Chipsmith reads this for tooltips. Written by
    /// import::import_path_with_provenance at import time.
    pub file_provenance: serde_json::Value,
    /// Provenance for the symbol/footprint thumbnail carat pulldowns: which
    /// method(s) produced an available variant (vendor source + ds2sf).
    pub symbol_sources: Vec<SourceTag>,
    pub footprint_sources: Vec<SourceTag>,
    /// chip-thumbnailer's per-kind failure reasons, read from
    /// `.thumb-errors.json` (e.g. {"sym":"...","fp":"...","3d":"..."}).
    /// Lets the dashboard render a "render failed" tile state with the
    /// reason, instead of a blank tile that's indistinguishable from
    /// "source file absent". Null when no errors file is present.
    pub thumb_errors: serde_json::Value,
}

#[derive(Serialize)]
pub struct Library {
    pub chips: Vec<Chip>,
    pub total_size: u64,
    pub fully_count: usize,
    pub stub_count: usize,
}

/// Which corner the footprint's pin-1 sits in (NW/NE/SW/SE), from the `.kicad_mod`.
/// We take pad "1"'s position vs the centroid of all pads. KiCad footprint space
/// is **Y-down** (negative Y = top/north). Returns None when there's no pad "1"
/// or pin-1 sits dead-centre (ambiguous). Pure string parse — no s-expr dep.
fn footprint_pin1_corner(dir: &Path, mpn: &str) -> Option<String> {
    let content = std::fs::read_to_string(dir.join(format!("{mpn}.kicad_mod"))).ok()?;
    let mut p1: Option<(f64, f64)> = None;
    let (mut sx, mut sy, mut n) = (0.0f64, 0.0f64, 0usize);
    for line in content.lines() {
        let l = line.trim_start();
        if !l.starts_with("(pad ") {
            continue;
        }
        // pad name = text between the first pair of quotes: (pad "1" smd ...
        let name = match l.split('"').nth(1) {
            Some(s) => s,
            None => continue,
        };
        // coords follow "(at ": (at X Y[ rot])
        let at = match l.find("(at ") {
            Some(i) => &l[i + 4..],
            None => continue,
        };
        let mut it = at.split_whitespace();
        let x = match it.next().and_then(|t| t.trim_end_matches(')').parse::<f64>().ok()) {
            Some(v) => v,
            None => continue,
        };
        let y = match it.next().and_then(|t| t.trim_end_matches(')').parse::<f64>().ok()) {
            Some(v) => v,
            None => continue,
        };
        sx += x;
        sy += y;
        n += 1;
        if name == "1" {
            p1 = Some((x, y));
        }
    }
    if n == 0 {
        return None;
    }
    let (x, y) = p1?;
    let (cx, cy) = (sx / n as f64, sy / n as f64);
    let ew = if x < cx - 0.01 { "W" } else if x > cx + 0.01 { "E" } else { "" };
    let ns = if y < cy - 0.01 { "N" } else if y > cy + 0.01 { "S" } else { "" }; // Y-down
    if ew.is_empty() || ns.is_empty() {
        return None; // pin-1 centred on one axis → ambiguous, no hint
    }
    Some(format!("{ns}{ew}"))
}

fn vendor_from_mpn(mpn: &str) -> &'static str {
    let m = mpn.to_uppercase();
    let prefixes = [
        ("TPS65987", "TI"), ("MCXN", "NXP"), ("ESP32", "Espressif"),
        ("ICE40", "Lattice"), ("R7FA", "Renesas"), ("DA14", "Renesas"),
        ("ATSAM", "Microchip"), ("ATME", "Microchip"), ("ATTI", "Microchip"),
        ("VL5", "ST"), ("STM", "ST"), ("LSM", "ST"),
        ("BME", "Bosch"), ("BMI", "Bosch"), ("BMP", "Bosch"),
        ("PCA9", "NXP"), ("LPC", "NXP"),
        ("AD7", "ADI"), ("AD8", "ADI"), ("LTC", "ADI"), ("MAX", "ADI"),
        ("DMG", "Diodes"), ("DMP", "Diodes"), ("DMN", "Diodes"),
        ("WAGO", "WAGO"), ("NRF", "Nordic"),
        ("TPS", "TI"), ("ADS", "TI"), ("INA", "TI"), ("LM", "TI"),
        ("DRV", "TI"), ("MCF", "TI"), ("BQ", "TI"), ("MSP", "TI"),
        ("VS", "Vishay"), ("SI", "Vishay"), ("CRCW", "Vishay"),
    ];
    for (p, v) in prefixes.iter() {
        if m.starts_with(p) { return v; }
    }
    "?"
}

pub fn scan() -> anyhow::Result<Library> {
    let root = crate::library::root();
    let mut chips = Vec::new();
    let mut total_size = 0u64;
    if !root.exists() {
        return Ok(Library { chips, total_size: 0, fully_count: 0, stub_count: 0 });
    }
    for entry in std::fs::read_dir(&root)? {
        let entry = entry?;
        let path = entry.path();
        if !path.is_dir() { continue; }
        let mpn = entry.file_name().to_string_lossy().to_string();
        // ENFORCE provenance at the source: any file that landed without going
        // through import gets its origin recorded now (derived from info.json, or
        // flagged "unknown"). Idempotent — only writes files with no entry yet —
        // so a stray un-sourced file can never silently persist past one scan.
        let _ = crate::import::backfill_dir(&path, &mpn);
        let chip = read_chip(&path, &mpn)?;
        total_size += chip.size_bytes;
        chips.push(chip);
    }
    chips.sort_by(|a, b| a.mpn.cmp(&b.mpn));
    let fully_count = chips.iter().filter(|c| c.fully_bundled).count();
    let stub_count = chips.len() - fully_count;
    Ok(Library { chips, total_size, fully_count, stub_count })
}

fn read_chip(dir: &Path, mpn: &str) -> anyhow::Result<Chip> {
    let mut files = Vec::new();
    let mut size = 0u64;
    let mut pdf_count = 0;
    let mut has_pdf = false;
    let mut has_step = false;
    let mut has_sym = false;
    let mut has_mod = false;
    let mut has_altium_sch = false;
    let mut has_altium_pcb = false;
    let mut has_altium_intlib = false;
    let mut has_fusion_lbr = false;
    let mut has_info = false;
    let mut info_value = serde_json::Value::Null;
    let mut stock_value = serde_json::Value::Null;

    for entry in std::fs::read_dir(dir)? {
        let entry = entry?;
        let name = entry.file_name().to_string_lossy().to_string();
        if !entry.path().is_file() { continue; }
        let meta = entry.metadata()?;
        size += meta.len();
        files.push(name.clone());
        if name == format!("{mpn}.step") || name == format!("{mpn}.stp") { has_step = true; }
        if name == format!("{mpn}.kicad_sym") { has_sym = true; }
        if name == format!("{mpn}.kicad_mod") { has_mod = true; }
        if name == format!("{mpn}.SchLib") { has_altium_sch = true; }
        if name == format!("{mpn}.PcbLib") { has_altium_pcb = true; }
        if name == format!("{mpn}.IntLib") { has_altium_intlib = true; }
        if name == format!("{mpn}.lbr")    { has_fusion_lbr = true; }
        if name == format!("{mpn}.pdf") || name == format!("{mpn}-datasheet.pdf") { has_pdf = true; }
        if name.ends_with(".pdf") { pdf_count += 1; }
        if name == "info.json" {
            has_info = true;
            if let Ok(s) = std::fs::read_to_string(entry.path()) {
                if let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) {
                    info_value = v;
                }
            }
        }
        if name == "stock.json" {
            if let Ok(s) = std::fs::read_to_string(entry.path()) {
                if let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) {
                    stock_value = v;
                }
            }
        }
    }
    files.sort();

    let vendor = info_value.get("manufacturer").and_then(|v| v.as_str())
        .map(String::from)
        .unwrap_or_else(|| vendor_from_mpn(mpn).to_string());
    let package = info_value.get("package").and_then(|v| v.as_str())
        .unwrap_or("?").to_string();
    let family = info_value.get("family").and_then(|v| v.as_str())
        .unwrap_or("").to_string();
    let step_source = info_value.get("step_source").and_then(|v| v.as_str())
        .unwrap_or("—").to_string();

    // Thumbnails live alongside the source files using chip-thumbnailer's
    // canonical naming: <MPN>-symbol.svg / <MPN>-footprint.svg / <MPN>-3d-iso.png
    let has_thumb_symbol     = dir.join(format!("{mpn}-symbol.svg")).is_file();
    let sym_rev = std::fs::metadata(dir.join(format!("{mpn}-symbol.svg"))).ok()
        .and_then(|m| m.modified().ok())
        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
        .map(|d| d.as_millis() as u64).unwrap_or(0);
    let has_thumb_footprint  = dir.join(format!("{mpn}-footprint.svg")).is_file();
    let has_thumb_chip3d     = dir.join(format!("{mpn}-3d-iso.png")).is_file();
    let has_thumb_chip3d_yup = dir.join(format!("{mpn}-3d-iso-yup.png")).is_file();

    // Read chip-thumbnailer's manifest for the auto-inferred up axis. Defaults
    // to "z" (STEP canonical) when the manifest is absent.
    let inferred_up_axis = std::fs::read_to_string(dir.join(format!("{mpn}-thumbs.json")))
        .ok()
        .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
        .and_then(|v| v.get("thumbnails")
            .and_then(|t| t.get("threeD"))
            .and_then(|td| td.get("inferredSourceUpAxis"))
            .and_then(|a| a.as_str())
            .map(|s| s.to_string()))
        .unwrap_or_else(|| "z".to_string());

    // User's chosen axis (if they overrode). Falls back to inferred.
    let chosen_up_axis = info_value.get("chosen_up_axis")
        .and_then(|v| v.as_str())
        .map(String::from)
        .unwrap_or_else(|| inferred_up_axis.clone());

    // Altium — accept either the SchLib+PcbLib pair OR a compiled .IntLib (which
    // contains both internally; it's what Component Search Engine downloads ship).
    let has_altium = (has_altium_sch && has_altium_pcb) || has_altium_intlib;
    let altium_not_available = info_value.get("altium_source")
        .and_then(|v| v.as_str()) == Some("not_available");
    let fusion_not_available = info_value.get("fusion_source")
        .and_then(|v| v.as_str()) == Some("not_available");

    // "Fully bundled" = all 3 EDA tools (KiCad sym+mod, Altium, Fusion lbr) + STEP.
    // Datasheet PDF is intentionally not required — many chips ship with manufacturer
    // datasheets via separate fetch and shouldn't gate the bundled-ness signal.
    let fully_bundled = has_step
        && has_sym && has_mod
        && has_altium
        && has_fusion_lbr;

    // ds2sf v0.5.1+ outputs: pull authoritative footprint SVG path + source
    // from <MPN>-extraction.result.json's `outputs` block. Stable contract
    // fields per the v0.5.1 release. Also relativize to chip dir so the
    // dashboard can serve them directly.
    let ext_result = std::fs::read_to_string(dir.join(format!("{mpn}-extraction.result.json")))
        .ok()
        .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
    let authoritative_footprint_svg = ext_result.as_ref()
        .and_then(|v| v.get("outputs"))
        .and_then(|o| o.get("footprint_svg"))
        .and_then(|s| s.as_str())
        .map(|p| {
            // Strip the absolute prefix so the dashboard can serve it.
            std::path::Path::new(p)
                .file_name()
                .and_then(|n| n.to_str())
                .map(String::from)
                .unwrap_or_else(|| p.to_string())
        });
    let authoritative_footprint_svg_source = ext_result.as_ref()
        .and_then(|v| v.get("outputs"))
        .and_then(|o| o.get("footprint_svg_source"))
        .and_then(|s| s.as_str())
        .map(String::from);

    // concur validation status: read from <MPN>-validation-status.json
    // (which validate.rs writes after every concur run). Surfaces on the
    // dashboard so the user sees the gold-dataset state per card.
    let validation = std::fs::read_to_string(dir.join(format!("{mpn}-validation-status.json")))
        .ok()
        .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
    let validation_status = validation.as_ref()
        .and_then(|v| v.get("concur_status"))
        .and_then(|s| s.as_str())
        .map(String::from);
    let validation_variants = validation.as_ref()
        .and_then(|v| v.get("variants"))
        .and_then(|a| a.as_array())
        .map(|arr| arr.iter()
            .filter_map(|e| e.get("kind").and_then(|k| k.as_str()).map(String::from))
            .collect())
        .unwrap_or_default();

    // Pin-1 sign-off from chipsmith (<MPN>.chipsmith.json). The user lines up the
    // 3D chip's pin-1 with the footprint's pin-1 in chipsmith + signs off; we read
    // it back so pin-1 is visible on the card and the baked .chipsmith.step is the
    // canonical placement model for downstream tools. (See the chipsmith launch.)
    let chipsmith = std::fs::read_to_string(dir.join(format!("{mpn}.chipsmith.json")))
        .ok()
        .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
    let pin1_signed = chipsmith.as_ref().map(|v| v.get("signed_at").is_some()).unwrap_or(false);
    let pin1_corner = chipsmith.as_ref()
        .and_then(|v| v.get("user_placed_pin1")).and_then(|p| p.get("corner"))
        .and_then(|c| c.as_str()).map(String::from);
    let pin1_signed_at = chipsmith.as_ref()
        .and_then(|v| v.get("signed_at")).and_then(|s| s.as_str()).map(String::from);
    let pin1_measure_ok = chipsmith.as_ref()
        .and_then(|v| v.get("measurements")).and_then(|m| m.get("passes")).and_then(|b| b.as_bool());
    let fp_pin1_corner = footprint_pin1_corner(&dir, mpn);

    // Per-file provenance map — chipsmith reads this for tooltips.
    let file_provenance = std::fs::read_to_string(dir.join(format!("{mpn}-file-provenance.json")))
        .ok()
        .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
        .unwrap_or(serde_json::Value::Null);

    // chip-thumbnailer's per-kind failure reasons (.thumb-errors.json). Surfaced
    // so a failed render shows as a distinct tile state, not a silent blank.
    let thumb_errors = std::fs::read_to_string(dir.join(".thumb-errors.json"))
        .ok()
        .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
        .unwrap_or(serde_json::Value::Null);

    // Per-kind source provenance for the symbol/footprint carat pulldowns: which
    // method produced each available variant. The current (rendered) thumbnail
    // is the canonical .kicad_sym/.kicad_mod; ds2sf is an alternate from the
    // datasheet. The frontend renders these as a ▾ carat like the 3D one.
    let symbol_sources = source_tags(
        &dir,
        has_sym,
        &format!("{mpn}.kicad_sym"),
        "kicad_sym",
        &format!("{mpn}-symbol.extracted.json"),
        &file_provenance,
        &info_value,
    );
    let footprint_sources = source_tags(
        &dir,
        has_mod,
        &format!("{mpn}.kicad_mod"),
        "kicad_mod",
        &format!("{mpn}-footprint.extracted.json"),
        &file_provenance,
        &info_value,
    );

    Ok(Chip {
        mpn: mpn.to_string(),
        vendor, package, family,
        fully_bundled,
        has_step, has_sym, has_mod,
        has_altium_sch, has_altium_pcb, has_altium_intlib, has_altium, altium_not_available,
        has_fusion_lbr, fusion_not_available,
        has_pdf, pdf_count,
        has_info,
        has_thumb_symbol, sym_rev, has_thumb_footprint, has_thumb_chip3d, has_thumb_chip3d_yup,
        inferred_up_axis, chosen_up_axis,
        step_source,
        size_bytes: size,
        files,
        info: info_value,
        stock: stock_value,
        authoritative_footprint_svg,
        authoritative_footprint_svg_source,
        validation_status,
        validation_variants,
        pin1_signed, pin1_corner, pin1_signed_at, pin1_measure_ok,
        fp_pin1_corner,
        file_provenance,
        thumb_errors,
        symbol_sources,
        footprint_sources,
    })
}

/// A provenance entry for a symbol/footprint thumbnail carat.
#[derive(serde::Serialize)]
pub struct SourceTag {
    pub method: String,  // "vendor" | "ds2sf" | "manufacturer" | source slug
    pub label: String,   // human label, e.g. "SnapEDA", "ds2sf · datasheet"
    pub current: bool,   // is this the variant currently rendered as the thumbnail?
    pub thumb: bool,     // does a thumbnail exist for this variant (show it in the carat)?
}

fn source_label(slug: &str) -> String {
    match slug {
        "snapmagic" | "snapeda" => "SnapEDA",
        "ultralibrarian" | "cse_ul" | "ul" => "Ultra Librarian",
        "cse" | "componentsearch" => "Component Search",
        "manufacturer" | "mfr" => "Manufacturer",
        "mouser" => "Mouser",
        "digikey" => "DigiKey",
        "arrow" => "Arrow",
        "lcsc" => "LCSC",
        "kicad_community" => "KiCad community",
        "ds2sf" => "ds2sf · datasheet",
        other => other,
    }
    .to_string()
}

/// Build the source list for a symbol/footprint: the canonical vendor file (with
/// its provenance origin) + the ds2sf datasheet extraction when present.
fn source_tags(
    dir: &std::path::Path,
    has_canonical: bool,
    canonical_file: &str,
    sor_key: &str,
    ds2sf_file: &str,
    provenance: &serde_json::Value,
    info: &serde_json::Value,
) -> Vec<SourceTag> {
    let mpn_prefix = canonical_file.trim_end_matches(&format!(".{sor_key}"));
    let ext_suffix = format!(".{sor_key}");
    let thumb_kind = if sor_key == "kicad_sym" { "symbol" } else { "footprint" };
    let canon_thumb = dir.join(format!("{mpn_prefix}-{thumb_kind}.svg")).is_file();

    // Label by WHERE it was downloaded (discovery_source), NOT who authored it
    // (content_origin). Aggregators are never "the manufacturer"; manufacturer-
    // direct names the site. See provenance::source_label.
    let manufacturer = info.get("manufacturer").and_then(|m| m.as_str()).unwrap_or("");
    let canon_disc = provenance
        .get(canonical_file)
        .and_then(|e| e.get("discovery_source").or_else(|| e.get("content_origin")))
        .and_then(|s| s.as_str())
        .or_else(|| info.get("source_of_record").and_then(|s| s.get(sor_key)).and_then(|s| s.as_str()))
        // Don't FALSELY claim "Manufacturer (direct site)" when we never recorded
        // a source — that misleading default is what made the carat look wrong
        // ("Manufacturer direct site" on a chip we couldn't actually attribute).
        .unwrap_or("unknown")
        .to_string();

    // chip-fetcher's goal: show EVERY library + format we pulled down, named, so
    // the user can compare them and pick. One carat entry PER (source × format):
    //   "Component Search Engine - KiCad Library"
    //   "Component Search Engine - Altium Library"
    //   "Component Search Engine - Fusion 360 Library"
    //   "ds2sf Texas Instruments (ti.com)"
    let src = crate::provenance::source_label(&canon_disc, manufacturer);
    let has = |f: String| dir.join(f).is_file();
    let mut tags = Vec::new();
    // KiCad library — the canonical render shown on the tile (current source).
    // When the source IS the KiCad standard library, don't say "KiCad library,
    // from KiCad standard library" — the source already names the format.
    if has_canonical {
        let kicad_label = if src.starts_with("KiCad") { src.clone() } else { format!("KiCad library, from {src}") };
        tags.push(SourceTag { method: "kicad".into(), label: kicad_label, current: true, thumb: canon_thumb });
    }
    // Altium library (.SchLib has the symbol/device, .PcbLib the footprint).
    // adom-sfconvert turns it into KiCad sym+mod via KiCad 10 (service-kicad),
    // rendered to its own variant thumbnail — so the carat previews the Altium
    // library's OWN geometry.
    let altium = if sor_key == "kicad_sym" { format!("{mpn_prefix}.SchLib") } else { format!("{mpn_prefix}.PcbLib") };
    if has(altium) {
        let altium_thumb = dir.join(format!("{mpn_prefix}-{thumb_kind}.altium.svg")).is_file();
        tags.push(SourceTag { method: "altium".into(), label: format!("Altium library, from {src}"), current: false, thumb: altium_thumb });
    }
    // Fusion 360 / EAGLE library (.lbr holds both the device + the package).
    // adom-sfconvert turns it into KiCad sym+mod, rendered to its own variant
    // thumbnail — so the carat previews the Fusion library's OWN geometry.
    if has(format!("{mpn_prefix}.lbr")) {
        let fusion_thumb = dir.join(format!("{mpn_prefix}-{thumb_kind}.fusion.svg")).is_file();
        tags.push(SourceTag { method: "fusion".into(), label: format!("Fusion 360 library, from {src}"), current: false, thumb: fusion_thumb });
    }
    // The manufacturer DATASHEET, extracted by ds2sf — the independent check.
    let ds2sf_thumb = dir.join(format!("{mpn_prefix}-{thumb_kind}.ds2sf.svg")).is_file();
    if dir.join(ds2sf_file).is_file() && ds2sf_thumb {
        let mfr = crate::provenance::source_label("manufacturer", manufacturer);
        tags.push(SourceTag { method: "ds2sf".into(), label: format!("ds2sf {mfr}"), current: !has_canonical, thumb: true });
    }
    tags
}