//! Unified result JSON — mirrors ds2sf's agent-to-agent contract.
//!
//! Exit codes:
//!   0 → status=golden                (all sources agree exactly)
//!   0 → status=golden_with_variants  (all disagreements are known-benign patterns
//!                                     enumerated in `variants[]` — caller can
//!                                     ship, just remember the variants)
//!   2 → status=divergent             (recoverable: chip-fetcher should re-fetch
//!                                     a lib or have the user intervene)
//!   3 → status=unrecoverable         (parser failure / missing inputs)

use anyhow::{Context as _, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Status {
    Golden,
    GoldenWithVariants,
    Divergent,
    Unrecoverable,
}

impl Status {
    pub fn exit_code(self) -> i32 {
        match self {
            Status::Golden => 0,
            Status::GoldenWithVariants => 0,
            Status::Divergent => 2,
            Status::Unrecoverable => 3,
        }
    }
    pub fn is_acceptable(self) -> bool {
        matches!(self, Status::Golden | Status::GoldenWithVariants)
    }
}

/// A variant explains a disagreement as known-benign rather than as a real
/// divergence. When every disagreeing axis is covered by a variant, status
/// becomes `golden_with_variants` (exit 0) instead of `divergent` (exit 2).
///
/// The `covers` method declares which axes a variant explains — used by the
/// status-determination logic in cli.rs to decide whether to ship or block.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Variant {
    /// One source counts an exposed/thermal pad as pin N+1 while another doesn't.
    /// Pad-count diff is exactly 1 across sources. EE practice varies on whether
    /// to expose the EP as a symbol pin; either is acceptable.
    ExposedPadVariant {
        sources_with_ep: Vec<String>,
        sources_without_ep: Vec<String>,
    },
    /// A module's KiCad/EAGLE footprint includes mounting/shield/test pads
    /// beyond the user-facing pinout (the module datasheet's N pads). Caller
    /// can use whichever set fits their assembly process.
    ModuleExtraPads {
        sources_with_extras: Vec<String>,
        sources_without_extras: Vec<String>,
        extra_count: u32,
    },
    /// Package-family names disagree (different prefixes / library naming
    /// conventions, e.g. TI's `SOT_` prefix on a VSSOP footprint) BUT the
    /// underlying geometry — pad count, pad numbers, pad pitch — agrees.
    /// Cosmetic only; downstream tooling should look at geometry, not name.
    LabelOnlyMismatch {
        notes: String,
    },
    /// One source's symbol uses pin numbers as labels ("1","2","3"…) instead
    /// of real pin names (VDD, SCL, …). Symbol is unusable for schematic
    /// capture as-is, but a richer source (ds2sf or EAGLE) can supply names.
    StubSymbolPinNames {
        stub_sources: Vec<String>,
        canonical_sources: Vec<String>,
    },
    /// Body-dim x or y disagrees by >0.5 mm but pad geometry agrees. Body dims
    /// are courtyard-only — they don't affect placement. The disagreement is
    /// usually a measurement-method artifact (lead-tip span vs body D dim, etc).
    BodyDimMeasurementArtifact {
        notes: String,
    },
    /// Pin names disagree but every disagreeing pin pair is a benign naming-
    /// convention difference, not a real mismatch. Two cases handled:
    ///   - Channel-digit position swaps: `OUT1` ↔ `1OUT`, `IN1+` ↔ `1IN+`
    ///     (op-amps, dual DACs, multi-rail regulators)
    ///   - Power-pin aliases: `V+` ↔ `VCC` ↔ `VDD`, `V-` ↔ `GND` ↔ `VSS`
    ///     (chip-vendor convention vs library convention)
    NamingConventionMismatch {
        examples: Vec<String>,
    },
    /// One source represents a 4-terminal Kelvin sense resistor (Vishay WSL,
    /// Bourns CSL, Susumu PCS) with 4 pads (I+, V+, V-, I- — separate force
    /// and sense terminals for precision current measurement) while another
    /// source simplifies to 2 pads. Both are valid representations: 4-pad
    /// preserves the Kelvin connection benefit (no IR drop in the sense
    /// path) while 2-pad is fine for non-precision routing. Downstream
    /// tooling should prefer the 4-pad representation when low-µΩ accuracy
    /// matters for the design.
    KelvinSenseRepresentation {
        sources_with_kelvin: Vec<String>,
        sources_without_kelvin: Vec<String>,
        kelvin_pin_names: Vec<String>,
    },
}

impl Variant {
    /// Which `axis` names does this variant explain? Used by cli.rs to decide
    /// whether the run is `golden_with_variants` (every disagreeing axis
    /// covered) or `divergent` (something is uncovered).
    pub fn covers(&self) -> Vec<&'static str> {
        match self {
            Variant::ExposedPadVariant { .. } => vec![
                "pad_count", "pad_numbers", "symbol_pin_count", "pin_names", "pin_to_pad_map",
            ],
            Variant::ModuleExtraPads { .. } => vec![
                "pad_count", "pad_numbers", "symbol_pin_count", "pin_names", "pin_to_pad_map",
            ],
            Variant::LabelOnlyMismatch { .. } => vec!["package_family"],
            Variant::StubSymbolPinNames { .. } => vec!["pin_names", "pin_to_pad_map"],
            Variant::BodyDimMeasurementArtifact { .. } => vec!["body_dims"],
            Variant::NamingConventionMismatch { .. } => vec!["pin_names", "pin_to_pad_map"],
            Variant::KelvinSenseRepresentation { .. } => vec![
                "pad_count", "pad_numbers", "symbol_pin_count",
                "pin_names", "pin_to_pad_map", "body_dims",
            ],
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResultDoc {
    pub schema_version: String,
    pub status: Status,
    pub exit_code: i32,
    pub mpn: String,
    pub summary: String,
    pub sources_compared: Vec<String>,
    pub sources_missing: Vec<String>,
    pub axes: Vec<crate::compare::AxisOutcome>,
    /// Known-benign explanations for any disagreeing axes. When every
    /// disagreeing axis is covered by a variant, status is `golden_with_variants`.
    pub variants: Vec<Variant>,
    pub hints: Vec<Hint>,
    pub extractor_version: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum Hint {
    /// One specific library source looks suspect — chip-fetcher should re-fetch
    /// it from a different upstream (manufacturer site, alternate UL bundle, etc.).
    /// `expected_resolution` tells chip-fetcher what to verify after refetch:
    /// re-run concur and check that this specific axis now agrees.
    RefetchLibrarySource {
        /// Which source format to re-fetch — "kicad_sym" (just the symbol),
        /// "kicad_mod" (just the footprint), "kicad_both", "eagle", "altium".
        format: String,
        /// The disagreeing axis (pin_names, package_family, …) so chip-fetcher
        /// can correlate to the conflict that triggered this hint.
        suspect: String,
        notes: String,
        /// Concrete next-source recommendations, in priority order. chip-fetcher
        /// should walk these in order, re-fetch from each, and re-run concur
        /// until status flips to golden / golden_with_variants.
        suggested_sources: Vec<String>,
        /// What concur expects to see after a successful refetch — chip-fetcher
        /// uses this to confirm the refetch worked. e.g.
        /// "axis 'pin_to_pad_map' should change from disagree → agree".
        expected_resolution: String,
    },
    /// The KiCad symbol from UL has stub pin names ("1","2",…) instead of real
    /// labels (VDD,SCL,…). The footprint is fine. Best response: skip the
    /// `<MPN>.kicad_sym` from chip-fetcher entirely and feed ds2sf's symbol
    /// JSON to `sym_create` to author a fresh KiCad symbol with real labels.
    UlSymbolHasStubNames {
        source: String,
        pin_count: u32,
        suggestion: String,
        /// chip-fetcher's recommended next step: NOT a refetch — instead route
        /// downstream tooling (sym_create) to use ds2sf's pin labels.
        chip_fetcher_action: String,
    },
    /// ds2sf's output is suspect (e.g. body_dim is the lead-tip span instead of
    /// the body D dim — a classic Haiku misread that Opus usually gets right).
    /// Suggest re-running ds2sf with a stronger model.
    RerunDs2sfWithStrongerModel {
        reason: String,
        suspect_field: String,
        suggested_model: String,
    },
    /// Differences exist but are within normal tolerance. Caller can accept.
    AcceptWithCaveats {
        caveats: Vec<String>,
    },
    /// Surface to a human — automated repair isn't safe.
    HumanReview {
        reason: String,
        notes: String,
    },
    /// One or more source files weren't parseable at all.
    SourceParseFailed {
        format: String,
        path: PathBuf,
        error: String,
    },
}

pub fn write(out_path: &std::path::Path, doc: &ResultDoc) -> Result<()> {
    fs::write(out_path, serde_json::to_string_pretty(doc)? + "\n")
        .with_context(|| format!("writing {}", out_path.display()))?;
    Ok(())
}