//! Read a ds2sf `<mpn>-symbol.extracted.json` (datasheet-extracted pins with
//! logical groups + rich descriptions) and build an `IcInput` for our generator
//! with **balanced side assignment** and optional logical grouping. Also returns
//! the pin/group description maps for the viewer tooltips.
//!
//! ds2sf already groups pins (POWER, GROUND, GPIO, DEBUG, …); GPIO is sub-split
//! by port (P0/P1/P2) so a 31-pin GPIO group doesn't pile onto one side.

use crate::sym_gen::{IcInput, Pin};
use regex::Regex;
use serde::Deserialize;
use std::collections::HashMap;

#[derive(Deserialize, Clone)]
pub struct Ds2sfPin {
    pub name: String,
    pub number: String,
    #[serde(rename = "type", default)]
    pub ptype: String,
    #[serde(default)]
    pub group: String,
    #[serde(default)]
    pub description: String,
}

#[derive(Deserialize, Clone, Default)]
pub struct Ds2sfGroup {
    pub name: String,
    #[serde(default)]
    pub description: String,
}

#[derive(Deserialize, Default)]
pub struct Ds2sfSymbol {
    #[serde(rename = "symbolName", default)]
    pub symbol_name: String,
    #[serde(rename = "referencePrefix", default)]
    pub reference_prefix: String,
    #[serde(default)]
    pub package: String,
    #[serde(default)]
    pub manufacturer: String,
    #[serde(default)]
    pub description: String,
    #[serde(default)]
    pub pins: Vec<Ds2sfPin>,
    #[serde(default)]
    pub groups: Vec<Ds2sfGroup>,
}

pub fn parse(json: &str) -> anyhow::Result<Ds2sfSymbol> {
    Ok(serde_json::from_str(json)?)
}

pub struct LayoutOpts {
    pub pin_layout: String, // "left-right" | "all-sides"
    pub grouped: bool,
    pub ref_pos: String,
    pub value_pos: String,
}

/// Build an IcInput from ds2sf pins. Returns (input, pin name/number→desc,
/// group→desc).
pub fn build_ic_input(
    sym: &Ds2sfSymbol,
    opts: &LayoutOpts,
) -> (IcInput, HashMap<String, String>, HashMap<String, String>) {
    // Description maps for the viewer.
    let mut pin_desc: HashMap<String, String> = HashMap::new();
    for p in &sym.pins {
        if !p.description.is_empty() {
            pin_desc.insert(p.name.clone(), p.description.clone());
            pin_desc.insert(p.number.clone(), p.description.clone());
        }
    }
    let mut group_desc: HashMap<String, String> = HashMap::new();
    for g in &sym.groups {
        group_desc.insert(g.name.clone(), g.description.clone());
    }

    // Effective group label per pin: sub-split GPIO by port (P0/P1/P2).
    let port_re = Regex::new(r"^P(\d+)").unwrap();
    let eff_group = |p: &Ds2sfPin| -> String {
        if p.group.eq_ignore_ascii_case("GPIO") {
            if let Some(c) = port_re.captures(&p.name) {
                return format!("P{}", &c[1]);
            }
        }
        p.group.clone()
    };
    // Carry port sub-group descriptions from the parent GPIO group.
    if let Some(gpio) = sym.groups.iter().find(|g| g.name.eq_ignore_ascii_case("GPIO")) {
        for p in &sym.pins {
            let eg = eff_group(p);
            if eg.starts_with('P') && eg.len() <= 3 {
                group_desc.entry(eg).or_insert_with(|| gpio.description.clone());
            }
        }
    }

    // Collect groups in first-seen order with their pins.
    let mut order: Vec<String> = Vec::new();
    let mut by_group: HashMap<String, Vec<&Ds2sfPin>> = HashMap::new();
    for p in &sym.pins {
        let g = eff_group(p);
        if !by_group.contains_key(&g) {
            order.push(g.clone());
        }
        by_group.entry(g).or_default().push(p);
    }

    // Sides to balance across.
    let sides: Vec<&str> = if opts.pin_layout == "all-sides" {
        vec!["left", "top", "right", "bottom"]
    } else {
        vec!["left", "right"]
    };

    // Greedy bin-pack groups (largest first) into the least-full side.
    let mut group_side: HashMap<String, String> = HashMap::new();
    if opts.grouped {
        let mut sized: Vec<(String, usize)> =
            order.iter().map(|g| (g.clone(), by_group[g].len())).collect();
        sized.sort_by(|a, b| b.1.cmp(&a.1));
        let mut load: Vec<usize> = vec![0; sides.len()];
        for (g, n) in sized {
            let i = (0..sides.len()).min_by_key(|&i| load[i]).unwrap();
            load[i] += n;
            group_side.insert(g, sides[i].to_string());
        }
    } else {
        // Ungrouped: split the flat pin list evenly across the sides.
        let total = sym.pins.len();
        let per = (total + sides.len() - 1) / sides.len();
        for (i, p) in sym.pins.iter().enumerate() {
            group_side.insert(format!("__{}", p.number), sides[(i / per).min(sides.len() - 1)].to_string());
        }
    }

    // Emit pins ordered by side, then group order, each with explicit side.
    let mut out: Vec<Pin> = Vec::new();
    for side in &sides {
        for g in &order {
            for p in &by_group[g] {
                let pin_side = if opts.grouped {
                    group_side.get(g).cloned()
                } else {
                    group_side.get(&format!("__{}", p.number)).cloned()
                };
                if pin_side.as_deref() != Some(*side) {
                    continue;
                }
                out.push(Pin {
                    name: p.name.clone(),
                    number: p.number.clone(),
                    ptype: p.ptype.clone(),
                    group: if opts.grouped { Some(g.clone()) } else { None },
                    side: Some(side.to_string()),
                });
            }
        }
    }

    let input = IcInput {
        symbol_name: sym.symbol_name.clone(),
        manufacturer: sym.manufacturer.clone(),
        package: sym.package.clone(),
        description: sym.description.clone(),
        datasheet_url: String::new(),
        reference_prefix: if sym.reference_prefix.is_empty() { "U".into() } else { sym.reference_prefix.clone() },
        pins: out,
        pin_layout: opts.pin_layout.clone(),
        chip_name_centered: false,
        ref_pos: opts.ref_pos.clone(),
        value_pos: opts.value_pos.clone(),
        distributor_pn: Vec::new(),
    };
    (input, pin_desc, group_desc)
}