//! Unified result JSON — the agent-to-agent contract.
//!
//! Every `ds2sf extract` invocation writes `<MPN>-extraction.result.json` containing
//! a single object whose shape is stable. Upstream callers (chip-fetcher, automation)
//! switch on `status` + `exit_code` and walk `hints[]` to decide what to do next.
//!
//! Exit codes:
//!   0 → status=ok (extraction completed; review_required may still have entries)
//!   2 → status=needs_better_pdf (recoverable: try a different datasheet PDF)
//!   3 → status=unrecoverable (give up; surface to a human)

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

use crate::context::Context;

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

impl Status {
    pub fn exit_code(self) -> i32 {
        match self {
            Status::Ok => 0,
            Status::NeedsBetterPdf => 2,
            Status::Unrecoverable => 3,
        }
    }
}

/// Canonical `review_required.reason` strings. Kept in sync with what we instruct
/// the model to emit. Documented in SKILL.md so callers can switch on them.
pub mod reasons {
    /// Symbol-pass returned empty pins because the supplied PDF was the internal
    /// SoC datasheet, not the module's external castellated-pad datasheet.
    pub const MODULE_EXTERNAL_PINOUT_MISSING: &str = "module_external_pinout_missing";

    /// info.json's `package` hint disagreed with the datasheet's actual package.
    /// Caller should update info.json's package field to match.
    pub const PACKAGE_MISMATCH_IN_HINT: &str = "package_mismatch_in_hint";

    /// Datasheet covers many variants; the right one couldn't be picked from
    /// info.json + MPN alone. Caller should pass --variant or a more-specific PDF.
    #[allow(dead_code)]
    pub const VARIANT_DISAMBIGUATION_NEEDED: &str = "variant_disambiguation_needed";

    /// Claude's response failed schema validation twice in a row. Inspect
    /// `.ds2sf-cache/claude-symbol-raw*.txt` for the raw response.
    pub const UNRECOVERABLE_EXTRACTION_ERROR: &str = "unrecoverable_extraction_error";

    /// Datasheet PDF wasn't found at the auto-detected or --pdf-supplied path.
    #[allow(dead_code)]
    pub const MISSING_DATASHEET_PDF: &str = "missing_datasheet_pdf";
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResultDoc {
    pub schema_version: String,
    pub status: Status,
    pub exit_code: i32,
    pub mpn: String,
    pub manufacturer: Option<String>,
    pub datasheet_used: PathBuf,
    pub datasheet_sha256: String,
    pub summary: String,
    /// Merged review_required from symbol + footprint passes (whichever ran).
    pub review_required: Vec<Value>,
    /// Actionable next-steps for an agent caller.
    pub hints: Vec<Hint>,
    /// Paths to the three output JSONs. Null when that file wasn't written.
    pub outputs: Outputs,
    pub extractor_version: String,
    pub claude_model: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Outputs {
    pub symbol_json: Option<PathBuf>,
    pub footprint_json: Option<PathBuf>,
    pub provenance_json: Option<PathBuf>,
    /// ds2sf's own SVG — synthesized purely from the manufacturer's datasheet
    /// mechanical drawing. This is the independent "manufacturer's voice" in
    /// chipsmith's cross-validation and the primary visual output of ds2sf.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub footprint_svg_ds2sf: Option<PathBuf>,
    /// KiCad pcbnew render of the stdlib footprint (when a match exists).
    /// A third-party interpretation for visual comparison — NOT the same
    /// data source as ds2sf's extraction. chipsmith diffs these two.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub footprint_svg_pcbnew: Option<PathBuf>,
    /// Legacy field (pre-v0.7). Points to the ds2sf SVG for backwards compat.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub footprint_svg: Option<PathBuf>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub footprint_svg_source: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum Hint {
    /// Try a different PDF in the same chip-dir. `candidates` lists plausible
    /// alternates ds2sf already located on disk.
    TryAlternatePdf {
        reason: String,
        candidates: Vec<PathBuf>,
    },
    /// Update an upstream metadata file (e.g. chip-fetcher's info.json) and
    /// re-run. `key` + `current` + `extracted` describe the disagreement.
    UpdateUpstreamMetadata {
        file: PathBuf,
        key: String,
        current: String,
        extracted: String,
    },
    /// The output is good but a human should inspect the listed pins/fields
    /// (muxed protocols, thermal pads, power sequencing, etc.).
    HumanReview {
        reason: String,
        notes: String,
    },
    /// The package isn't in service-kicad's standard library; downstream
    /// footprint-authoring should use the datasheet-extracted dimensions.
    UseDatasheetExtractedFootprint {
        package: String,
    },
}

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

pub fn result_path(ctx: &Context) -> PathBuf {
    ctx.out_dir
        .join(format!("{}-extraction.result.json", ctx.mpn))
}

/// Scan the chip-dir for alternate PDFs that might be the right datasheet.
/// Returns paths relative to absolute, ordered by likelihood (most-specific first).
pub fn find_alternate_pdfs(ctx: &Context) -> Vec<PathBuf> {
    let mut out = Vec::new();
    let entries = match fs::read_dir(&ctx.chip_dir) {
        Ok(e) => e,
        Err(_) => return out,
    };

    let used = ctx.datasheet_path.clone();
    let mut all_pdfs: Vec<PathBuf> = entries
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .filter(|p| {
            p.extension().and_then(|s| s.to_str()) == Some("pdf")
                && p != &used
        })
        .collect();

    // Sort by relevance score (lower = better). The most-canonical-looking
    // datasheet filenames win. `<MPN>-data-sheet.pdf` and
    // `<MPN>-datasheet.pdf` outrank module-guideline / hardware-guideline
    // PDFs (which themselves outrank application notes / shield guides /
    // terms-of-use / etc.).
    //
    // Real-world catch (PCA9685, 2026-05-06): chip-fetcher pulled the
    // Adafruit shield user guide as the auto-detected `<MPN>.pdf`. The
    // canonical NXP datasheet was sitting next to it as
    // `PCA9685-data-sheet.pdf` but ranked too low, so the alt-PDF loop
    // suggested the LED-driver-selector-guide first. v0.3 ranks the actual
    // datasheet ahead of the family-specific guides.
    all_pdfs.sort_by_key(|p| {
        let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("").to_lowercase();
        let mut score: i32 = 0;
        // Strongest positive signal: filename literally contains "data-sheet"
        // or "datasheet" — these are the canonical IC datasheets.
        if name.contains("data-sheet") || name.contains("datasheet") {
            score -= 200;
        }
        // Per-package pinout supplements. Many vendors (notably Lattice for
        // iCE40) split the per-package pin-number assignments out of the
        // main datasheet into a separate "Pinout Files" / "Package Diagrams"
        // / "Hardware Checklist" PDF. When the main datasheet refuses with
        // a `pinout_supplement_required` review_required, this is the path
        // chip-fetcher should walk.
        if name.contains("package-diagrams") || name.contains("package-diagram")
            || name.contains("pinout") || name.contains("pin-out")
            || name.contains("hardware-checklist") || name.contains("hwchecklist")
            || name.contains("package-information") || name.contains("pkg-info")
        {
            score -= 150;
        }
        // Module-specific datasheets (ESP32 case) — second-best when looking
        // at a module product.
        if name.contains("hardware") || name.contains("guideline") { score -= 100; }
        if name.contains("original") { score -= 50; }
        if name.contains("module") { score -= 40; }
        if name.contains("wroom") || name.contains("wrover") { score -= 30; }
        // Negative signals: marketing / non-datasheet documents.
        if name.contains("brief") || name.contains("ti-applies") || name.contains("terms-of-use") {
            score += 100; // these are NOT what we want
        }
        if name.contains("application-note") || name.contains("an-")
            || name.contains("errata") || name.contains("trm")
            || name.contains("reference-manual") || name.contains("user-guide")
            || name.contains("user-manual")
            || name.contains("selector-guide") || name.contains("shield")
            || name.contains("flat-panel") || name.contains("white-good")
            || name.contains("application-guide")
            || name.contains("led-controllers") || name.contains("pwm-click")
            || name.contains("reel-pack") || name.contains("get-diagram")
            || name.contains("check-to-add")
        {
            score += 50;
        }
        score
    });

    // Cap to top 5 candidates so the JSON stays digestible.
    out.extend(all_pdfs.into_iter().take(5));
    out
}