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

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Context {
    pub mpn: String,
    pub manufacturer: Option<String>,
    pub package_hint: Option<String>,
    pub description_hint: Option<String>,
    pub family_hint: Option<String>,
    pub datasheet_path: PathBuf,
    pub datasheet_url: Option<String>,
    pub datasheet_source_tier: Option<String>,
    pub datasheet_source_host: Option<String>,
    pub datasheet_sha256: String,
    pub fetched_at: Option<String>,
    pub extracted_at: String,
    pub chip_dir: PathBuf,
    pub out_dir: PathBuf,
    pub page_count: Option<u32>,
}

#[derive(Debug, Deserialize)]
struct InfoJson {
    mpn: Option<String>,
    manufacturer: Option<String>,
    package: Option<String>,
    description: Option<String>,
    family: Option<String>,
    datasheet_url: Option<String>,
    datasheet_source: Option<String>,
    /// Accept either a string ("2026-05-03") or a number (Unix timestamp
    /// 1778065044). chip-fetcher writes both forms across vendors.
    #[serde(default, deserialize_with = "de_string_or_number")]
    fetched_at: Option<String>,
}

fn de_string_or_number<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
    let v = serde_json::Value::deserialize(d)?;
    Ok(match v {
        serde_json::Value::Null => None,
        serde_json::Value::String(s) => Some(s),
        serde_json::Value::Number(n) => Some(n.to_string()),
        _ => None,
    })
}

impl Context {
    pub fn ingest(
        chip_dir: &Path,
        pdf_override: Option<&Path>,
        mpn_override: Option<&str>,
        manuf_override: Option<&str>,
        url_override: Option<&str>,
        source_tier_override: Option<&str>,
        out_dir_override: Option<&Path>,
    ) -> Result<Self> {
        let chip_dir = chip_dir.canonicalize().with_context(|| {
            format!("chip dir not found: {}", chip_dir.display())
        })?;
        let basename = chip_dir
            .file_name()
            .and_then(|s| s.to_str())
            .ok_or_else(|| anyhow!("chip dir has no basename"))?
            .to_string();

        let info_path = chip_dir.join("info.json");
        let info: Option<InfoJson> = if info_path.exists() {
            let raw = fs::read_to_string(&info_path).context("reading info.json")?;
            Some(serde_json::from_str(&raw).context("parsing info.json")?)
        } else {
            None
        };

        let mpn = mpn_override
            .map(|s| s.to_string())
            .or_else(|| info.as_ref().and_then(|i| i.mpn.clone()))
            .unwrap_or(basename.clone());

        let datasheet_path = pdf_override
            .map(|p| p.to_path_buf())
            .unwrap_or_else(|| chip_dir.join(format!("{basename}.pdf")));
        if !datasheet_path.exists() {
            return Err(anyhow!(
                "datasheet PDF not found at {} — pass --pdf to override",
                datasheet_path.display()
            ));
        }

        let datasheet_sha256 = sha256_file(&datasheet_path)?;
        let page_count = pdf_page_count(&datasheet_path);

        let manufacturer = manuf_override
            .map(|s| s.to_string())
            .or_else(|| info.as_ref().and_then(|i| i.manufacturer.clone()));
        let package_hint = info.as_ref().and_then(|i| i.package.clone());
        let description_hint = info.as_ref().and_then(|i| i.description.clone());
        let family_hint = info.as_ref().and_then(|i| i.family.clone());
        let datasheet_url = url_override
            .map(|s| s.to_string())
            .or_else(|| info.as_ref().and_then(|i| i.datasheet_url.clone()));
        let datasheet_source_host = info.as_ref().and_then(|i| i.datasheet_source.clone());
        let datasheet_source_tier = source_tier_override
            .map(|s| s.to_string())
            .or_else(|| infer_source_tier(datasheet_source_host.as_deref()));
        let fetched_at = info.as_ref().and_then(|i| i.fetched_at.clone());

        let out_dir = out_dir_override
            .map(|p| p.to_path_buf())
            .unwrap_or_else(|| chip_dir.clone());

        let extracted_at = now_iso8601();

        Ok(Context {
            mpn,
            manufacturer,
            package_hint,
            description_hint,
            family_hint,
            datasheet_path,
            datasheet_url,
            datasheet_source_tier,
            datasheet_source_host,
            datasheet_sha256,
            fetched_at,
            extracted_at,
            chip_dir,
            out_dir,
            page_count,
        })
    }

    pub fn cache_dir(&self) -> PathBuf {
        self.out_dir.join(".ds2sf-cache")
    }
    pub fn cache_dir_create(&self) -> Result<()> {
        fs::create_dir_all(self.cache_dir()).context("creating cache dir")
    }
    pub fn write_pdf_sha_marker(&self) -> Result<()> {
        let p = self.cache_dir().join("pdf-sha256.txt");
        fs::write(&p, &self.datasheet_sha256).context("writing pdf-sha256.txt")
    }

    pub fn symbol_out_path(&self) -> PathBuf {
        self.out_dir.join(format!("{}-symbol.extracted.json", self.mpn))
    }
    pub fn footprint_out_path(&self) -> PathBuf {
        self.out_dir.join(format!("{}-footprint.extracted.json", self.mpn))
    }
    pub fn provenance_path(&self) -> PathBuf {
        self.out_dir
            .join(format!("{}-extraction.provenance.json", self.mpn))
    }
    pub fn footprint_svg_path(&self) -> PathBuf {
        self.out_dir
            .join(format!("{}-footprint.authoritative.svg", self.mpn))
    }
    pub fn symbol_cache_path(&self) -> PathBuf {
        self.cache_dir().join("claude-symbol-response.json")
    }
    pub fn footprint_cache_path(&self) -> PathBuf {
        self.cache_dir().join("claude-footprint-response.json")
    }

    pub fn outputs_exist(&self) -> Result<bool> {
        Ok(self.symbol_out_path().exists()
            && self.footprint_out_path().exists()
            && self.provenance_path().exists())
    }
}

fn sha256_file(path: &Path) -> Result<String> {
    let bytes = fs::read(path).with_context(|| format!("reading {}", path.display()))?;
    let mut h = Sha256::new();
    h.update(&bytes);
    Ok(hex(&h.finalize()))
}

fn hex(bytes: &[u8]) -> String {
    let mut s = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        use std::fmt::Write;
        let _ = write!(s, "{b:02x}");
    }
    s
}

fn pdf_page_count(path: &Path) -> Option<u32> {
    // Use `pdfinfo` if available (poppler-utils). Fail-soft: None.
    let out = std::process::Command::new("pdfinfo")
        .arg(path)
        .output()
        .ok()?;
    if !out.status.success() {
        return None;
    }
    for line in String::from_utf8_lossy(&out.stdout).lines() {
        if let Some(rest) = line.strip_prefix("Pages:") {
            return rest.trim().parse().ok();
        }
    }
    None
}

fn now_iso8601() -> String {
    // Avoid pulling in chrono — the date-string format is informational.
    // Use SystemTime with a tiny formatter.
    let secs = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs() as i64)
        .unwrap_or(0);
    // Convert to UTC components without chrono.
    format_utc(secs)
}

fn format_utc(secs: i64) -> String {
    // Days from 1970-01-01.
    let days = secs.div_euclid(86_400);
    let secs_of_day = secs.rem_euclid(86_400);
    let h = secs_of_day / 3600;
    let m = (secs_of_day % 3600) / 60;
    let s = secs_of_day % 60;
    let (y, mo, d) = days_to_ymd(days);
    format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
}

fn days_to_ymd(days: i64) -> (i32, u32, u32) {
    // Public-domain algorithm from Howard Hinnant.
    let z = days + 719_468;
    let era = z.div_euclid(146_097);
    let doe = (z - era * 146_097) as u64;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 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;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = if m <= 2 { y + 1 } else { y };
    (y as i32, m as u32, d as u32)
}

fn infer_source_tier(host: Option<&str>) -> Option<String> {
    let h = host?.to_lowercase();
    let mfr_hosts = [
        "ti.com", "ti.com/lit", "st.com", "nxp.com", "espressif.com",
        "raspberrypi.com", "diodes.com", "renesas.com", "infineon.com",
        "rohm.com", "onsemi.com", "microchip.com", "analog.com",
        "vishay.com", "skyworksinc.com", "silabs.com",
    ];
    if mfr_hosts.iter().any(|m| h.contains(m)) || h.contains("manufacturer_direct") {
        return Some("mfr".to_string());
    }
    if h.contains("snapeda") || h.contains("snapmagic") {
        return Some("snapmagic".to_string());
    }
    if h.contains("mouser") {
        return Some("mouser".to_string());
    }
    if h.contains("digikey") {
        return Some("digikey".to_string());
    }
    if h.contains("arrow") {
        return Some("arrow".to_string());
    }
    if h.contains("componentsearchengine") || h.contains("ultralibrarian") {
        return Some("cse".to_string());
    }
    if h.contains("lcsc") {
        return Some("lcsc".to_string());
    }
    None
}