use anyhow::{Context as _, Result};
use serde_json::{json, Map, Value};
use std::fs;

use crate::context::Context;
use crate::extract::{FootprintPass, SymbolPass};
use crate::normalize::Resolved;

pub fn write_all(
    ctx: &Context,
    sym: &SymbolPass,
    fp: Option<&FootprintPass>,
    norm: Option<&Resolved>,
    model: &str,
) -> Result<()> {
    write_symbol_json(ctx, sym)?;
    if let Some(f) = fp {
        write_footprint_json(ctx, sym, f, norm)?;
    }
    write_provenance_json(ctx, sym, fp, norm, model)?;
    Ok(())
}

fn write_symbol_json(ctx: &Context, sym: &SymbolPass) -> Result<()> {
    // Strip provenance fields from pins for the sym_create-shaped output —
    // those fields belong only in the provenance JSON, not the input to sym_create.
    let pins: Vec<Value> = sym
        .pins
        .iter()
        .map(|p| {
            let mut o = Map::new();
            o.insert("number".into(), Value::String(p.number.clone()));
            o.insert("name".into(), Value::String(p.name.clone()));
            o.insert("type".into(), Value::String(p.kind.clone()));
            if !p.group.is_empty() {
                o.insert("group".into(), Value::String(p.group.clone()));
            }
            if !p.description.is_empty() {
                o.insert("description".into(), Value::String(p.description.clone()));
            }
            Value::Object(o)
        })
        .collect();

    let mut out = Map::new();
    out.insert("symbolName".into(), Value::String(sym.symbol_name.clone()));
    if let Some(m) = sym.manufacturer.clone() {
        out.insert("manufacturer".into(), Value::String(m));
    }
    if let Some(p) = sym.package.clone() {
        out.insert("package".into(), Value::String(p));
    }
    if let Some(d) = sym.description.clone() {
        out.insert("description".into(), Value::String(d));
    }
    if let Some(u) = ctx.datasheet_url.clone() {
        out.insert("datasheetUrl".into(), Value::String(u));
    }
    out.insert("category".into(), Value::String(sym.category.clone()));
    out.insert("referencePrefix".into(), Value::String(sym.reference_prefix.clone()));
    if let Some(db) = sym.discrete_baseline.as_ref() {
        let mut baseline = Map::new();
        baseline.insert("library".into(), Value::String(db.library.clone()));
        baseline.insert("symbol".into(), Value::String(db.symbol.clone()));
        out.insert("discreteBaseline".into(), Value::Object(baseline));
    }
    out.insert("pins".into(), Value::Array(pins));
    out.insert(
        "groups".into(),
        Value::Array(
            sym.groups
                .iter()
                .map(|g| {
                    let mut o = Map::new();
                    o.insert("name".into(), Value::String(g.name.clone()));
                    if !g.description.is_empty() {
                        o.insert("description".into(), Value::String(g.description.clone()));
                    }
                    Value::Object(o)
                })
                .collect(),
        ),
    );

    if !sym.variants.is_empty() {
        out.insert("variants".into(), Value::Array(sym.variants.clone()));
    }

    let path = ctx.symbol_out_path();
    fs::write(&path, serde_json::to_string_pretty(&out)? + "\n")
        .with_context(|| format!("writing {}", path.display()))?;
    Ok(())
}

fn write_footprint_json(
    ctx: &Context,
    sym: &SymbolPass,
    fp: &FootprintPass,
    norm: Option<&Resolved>,
) -> Result<()> {
    let mut body = Map::new();
    body.insert("x".into(), json!(fp.body_dimensions.x));
    body.insert("y".into(), json!(fp.body_dimensions.y));
    if let Some(t) = fp.body_dimensions.thickness_max {
        body.insert("thickness_max".into(), json!(t));
    }

    // Strip provenance keys from leadDimensions so the output is consumer-clean.
    let mut lead_clean = Map::new();
    if let Some(obj) = fp.lead_dimensions.as_object() {
        for (k, v) in obj {
            if matches!(k.as_str(), "source_page" | "figure_label") {
                continue;
            }
            lead_clean.insert(k.clone(), v.clone());
        }
    }

    let mut out = Map::new();
    out.insert("footprintName".into(), Value::String(sym.symbol_name.clone()));
    if let Some(m) = sym.manufacturer.clone() {
        out.insert("manufacturer".into(), Value::String(m));
    }
    out.insert("package".into(), Value::String(fp.package_name.clone()));
    if let Some(d) = sym.description.clone() {
        out.insert("description".into(), Value::String(d));
    }
    if let Some(u) = ctx.datasheet_url.clone() {
        out.insert("datasheetUrl".into(), Value::String(u));
    }
    out.insert("padCount".into(), json!(fp.pad_count));
    out.insert("bodyDimensions".into(), Value::Object(body));
    out.insert("leadDimensions".into(), Value::Object(lead_clean));
    out.insert("padDescriptions".into(), Value::Object(fp.pad_descriptions.clone()));
    out.insert("symbolPinMap".into(), Value::Object(fp.symbol_pin_map.clone()));

    // Mechanical section — JEDEC dimensions, lead shape, pin-1 indicator,
    // extracted directly from the datasheet mechanical drawing by Claude.
    if let Some(mech) = &fp.mechanical {
        out.insert("mechanical".into(), mech.clone());
    }

    // Per-pad position array for chipsmith cross-validation.
    let computed = crate::fpsvg::compute_pads(fp);
    if !computed.is_empty() {
        out.insert("pads".into(), serde_json::to_value(&computed)?);
    }

    // 3D chip geometry — enough for chipsmith/OCCT to construct the chip
    // model from scratch without a manufacturer STEP file. When mechanical.jedec_dims
    // is present, chip_3d uses measured values; otherwise heuristic defaults.
    let chip_3d = crate::fpsvg::compute_chip_3d(fp);
    out.insert("chip_3d".into(), serde_json::to_value(&chip_3d)?);

    // Lead annotations — per-pin metadata ordered for service-step2glb /create-chip.
    // Embeds signal names into the STEP file as XCAF annotations.
    let annotations = crate::fpsvg::compute_lead_annotations(fp);
    if !annotations.is_empty() {
        out.insert("lead_annotations".into(), serde_json::to_value(&annotations)?);
    }

    if let Some(n) = norm {
        if let Some(b) = n.kicad_baseline.as_ref() {
            out.insert(
                "kicad_baseline".into(),
                json!({ "library": b.library, "name": b.name, "source": b.source }),
            );
            out.insert(
                "package_outline_source".into(),
                Value::String("service-kicad-stdlib".into()),
            );
        } else {
            out.insert("kicad_baseline".into(), Value::Null);
            out.insert(
                "package_outline_source".into(),
                Value::String("datasheet-extracted".into()),
            );
        }
    }

    let path = ctx.footprint_out_path();
    fs::write(&path, serde_json::to_string_pretty(&out)? + "\n")
        .with_context(|| format!("writing {}", path.display()))?;
    Ok(())
}

fn write_provenance_json(
    ctx: &Context,
    sym: &SymbolPass,
    fp: Option<&FootprintPass>,
    norm: Option<&Resolved>,
    model: &str,
) -> Result<()> {
    let pin_provenance: Vec<Value> = sym
        .pins
        .iter()
        .map(|p| {
            let mut o = Map::new();
            o.insert("pin_number".into(), Value::String(p.number.clone()));
            o.insert("pin_name".into(), Value::String(p.name.clone()));
            o.insert("page".into(), json!(p.source_page));
            o.insert(
                "table_caption".into(),
                Value::String(p.source_table_caption.clone()),
            );
            o.insert("confidence".into(), Value::String(p.confidence.clone()));
            if !p.notes.is_empty() {
                o.insert("notes".into(), Value::String(p.notes.clone()));
            }
            Value::Object(o)
        })
        .collect();

    let symbol_block = json!({
        "pin_provenance": pin_provenance,
        "groups_emitted": sym.groups.iter().map(|g| g.name.clone()).collect::<Vec<_>>(),
    });

    let footprint_block = match fp {
        Some(f) => {
            let mut dim_prov: Vec<Value> = Vec::new();
            dim_prov.push(json!({
                "field": "bodyDimensions.x",
                "value": f.body_dimensions.x,
                "page": f.body_dimensions.source_page,
                "figure_label": f.body_dimensions.figure_label,
            }));
            dim_prov.push(json!({
                "field": "bodyDimensions.y",
                "value": f.body_dimensions.y,
                "page": f.body_dimensions.source_page,
                "figure_label": f.body_dimensions.figure_label,
            }));
            if let Some(t) = f.body_dimensions.thickness_max {
                dim_prov.push(json!({
                    "field": "bodyDimensions.thickness_max",
                    "value": t,
                    "page": f.body_dimensions.source_page,
                    "figure_label": f.body_dimensions.figure_label,
                }));
            }
            if let Some(obj) = f.lead_dimensions.as_object() {
                let lead_page = obj.get("source_page").and_then(|v| v.as_u64()).unwrap_or(0);
                let lead_label = obj.get("figure_label").and_then(|v| v.as_str()).unwrap_or("").to_string();
                for (k, v) in obj {
                    if matches!(k.as_str(), "source_page" | "figure_label") {
                        continue;
                    }
                    dim_prov.push(json!({
                        "field": format!("leadDimensions.{k}"),
                        "value": v,
                        "page": lead_page,
                        "figure_label": lead_label,
                    }));
                }
            }

            let kicad_baseline = norm
                .and_then(|n| n.kicad_baseline.as_ref())
                .map(|b| json!({ "library": b.library, "name": b.name }))
                .unwrap_or(Value::Null);

            let attempts = norm
                .map(|n| {
                    n.search_attempts
                        .iter()
                        .map(|a| {
                            json!({ "library": a.library, "name": a.name, "result": a.result })
                        })
                        .collect::<Vec<_>>()
                })
                .unwrap_or_default();

            json!({
                "package_name_provenance": {
                    "value": f.package_name,
                    "page": f.package_name_source_page,
                    "info_json_value": ctx.package_hint,
                    "info_json_used": ctx.package_hint.is_some(),
                    "is_standard_jedec": f.is_standard_jedec
                },
                "kicad_baseline": kicad_baseline,
                "kicad_baseline_search_attempts": attempts,
                "dimension_provenance": dim_prov
            })
        }
        None => Value::Null,
    };

    let merged_review = crate::extract::merge_review_required(sym, fp);

    let prov = json!({
        "schema_version": "1",
        "mpn": ctx.mpn,
        "manufacturer": ctx.manufacturer,
        "datasheet": {
            "path": ctx.datasheet_path,
            "url": ctx.datasheet_url,
            "sha256": ctx.datasheet_sha256,
            "source_tier": ctx.datasheet_source_tier,
            "source_host": ctx.datasheet_source_host,
            "fetched_at": ctx.fetched_at,
            "page_count": ctx.page_count
        },
        "tooling": {
            "extractor": "ds2sf",
            "extractor_version": env!("CARGO_PKG_VERSION"),
            "extracted_at": ctx.extracted_at,
            "claude_model": model,
            "service_kicad_used": norm.is_some(),
            "service_kicad_baseline_match":
                norm.map(|n| n.kicad_baseline.is_some()).unwrap_or(false)
        },
        "symbol": symbol_block,
        "footprint": footprint_block,
        "review_required": merged_review
    });

    let path = ctx.provenance_path();
    fs::write(&path, serde_json::to_string_pretty(&prov)? + "\n")
        .with_context(|| format!("writing {}", path.display()))?;
    Ok(())
}

pub fn summary_line(
    ctx: &Context,
    sym: &SymbolPass,
    fp: Option<&FootprintPass>,
    norm: Option<&Resolved>,
) -> String {
    let baseline = match norm.and_then(|n| n.kicad_baseline.as_ref()) {
        Some(b) => format!("{}:{}", b.library, b.name),
        None => "no stdlib match".to_string(),
    };
    let groups: Vec<&str> = sym.groups.iter().map(|g| g.name.as_str()).collect();
    let groups_str = if groups.is_empty() {
        "(no groups)".to_string()
    } else {
        groups.join(", ")
    };
    let pkg = fp.map(|f| f.package_name.as_str()).unwrap_or("(no footprint pass)");
    let pad_count = fp.map(|f| f.pad_count).unwrap_or(0);
    let review = crate::extract::merge_review_required(sym, fp).len();
    format!(
        "{} — {} pins, package {} ({} pads), {}, groups: {}, review_required: {}",
        ctx.mpn,
        sym.pins.len(),
        pkg,
        pad_count,
        baseline,
        groups_str,
        review,
    )
}