#!/usr/bin/env python3
"""Cross-check the routed PCB pad-net map against the LMR51450 schematic."""
from __future__ import annotations

import hashlib
import json
import math
import re
import xml.etree.ElementTree as ET
from collections import Counter
from pathlib import Path

ROOT = Path(__file__).resolve().parent
BOARD = ROOT / "lamp_buck_lmr51450_a1.kicad_pcb"
NETLIST = ROOT / "lamp_buck_lmr51450_a1.net.xml"
DRC = ROOT / "lmr51450_refined_drc.rpt"
OMITTED_DNP = {"D1", "R5", "C9", "C10"}


def sexpr_blocks(text: str, token: str):
    pos = 0
    while True:
        start = text.find(token, pos)
        if start < 0:
            return
        depth = 0
        quoted = False
        escaped = False
        for i in range(start, len(text)):
            ch = text[i]
            if quoted:
                if escaped:
                    escaped = False
                elif ch == "\\":
                    escaped = True
                elif ch == '"':
                    quoted = False
            elif ch == '"':
                quoted = True
            elif ch == "(":
                depth += 1
            elif ch == ")":
                depth -= 1
                if depth == 0:
                    yield text[start:i + 1]
                    pos = i + 1
                    break
        else:
            raise RuntimeError(f"unbalanced S-expression at {token}")


def norm(net: str | None) -> str:
    if not net or net.startswith("unconnected-("):
        return ""
    return net.removeprefix("/")


root = ET.parse(NETLIST).getroot()
expected: dict[tuple[str, str], str] = {}
for net in root.findall("./nets/net"):
    name = net.attrib["name"]
    for node in net.findall("node"):
        expected[(node.attrib["ref"], node.attrib["pin"])] = norm(name)

board_text = BOARD.read_text(encoding="utf-8")
actual: dict[tuple[str, str], str] = {}
board_components: dict[str, dict[str, str]] = {}
for footprint in sexpr_blocks(board_text, '(footprint "'):
    ref_match = re.search(r'\(property "Reference" "([^"]+)"', footprint)
    if not ref_match:
        continue
    ref = ref_match.group(1)
    value_match = re.search(r'\(property "Value" "((?:[^"\\]|\\.)*)"', footprint)
    lib_match = re.match(r'\(footprint "([^"]+)"', footprint)
    board_components[ref] = {
        "value": value_match.group(1) if value_match else "",
        "footprint": lib_match.group(1) if lib_match else "",
    }
    for pad in sexpr_blocks(footprint, '(pad "'):
        number_match = re.match(r'\(pad "([^"]+)"', pad)
        if not number_match:
            continue
        net_match = re.search(r'\(net "([^"]+)"\)', pad)
        actual[(ref, number_match.group(1))] = norm(net_match.group(1) if net_match else "")

present_refs = set(board_components)
expected_refs = {ref for ref, _ in expected}
component_ref_mismatch = {
    "missing_installed_footprints": sorted(expected_refs - present_refs - OMITTED_DNP),
    "unexpected_footprints": sorted(present_refs - expected_refs),
    "expected_dnp_omissions": sorted(OMITTED_DNP & expected_refs - present_refs),
}
pad_mismatches = []
for key, expected_net in sorted(expected.items()):
    if key[0] in OMITTED_DNP:
        continue
    actual_net = actual.get(key)
    if actual_net is None or actual_net != expected_net:
        pad_mismatches.append({
            "ref": key[0], "pad": key[1],
            "expected": expected_net, "actual": actual_net,
        })
extra_board_pads = [
    {"ref": ref, "pad": pin, "actual": net}
    for (ref, pin), net in sorted(actual.items())
    if ref not in OMITTED_DNP and (ref, pin) not in expected
]

tracks = list(sexpr_blocks(board_text, "(segment"))
vias = list(sexpr_blocks(board_text, "(via"))
width_counts = Counter()
net_widths: dict[str, list[float]] = {}
for track in tracks:
    width = re.search(r'\(width ([\d.]+)\)', track)
    net = re.search(r'\(net "([^"]+)"\)', track)
    if width:
        width_mm = float(width.group(1))
        width_counts[f"{width_mm:.3f}"] += 1
        if net:
            net_widths.setdefault(norm(net.group(1)), []).append(width_mm)

drc_text = DRC.read_text(encoding="utf-8")
drc_match = re.search(r"Found (\d+) DRC violations", drc_text)
unconnected_match = re.search(r"Found (\d+) unconnected (?:items|pads)", drc_text)

main_5v_segments = [
    ((60.825, 32.0), (61.83, 33.0)),
    ((61.83, 33.0), (66.92, 33.0)),
    ((66.92, 33.0), (72.025, 33.0)),
    ((72.025, 18.0), (72.025, 23.0)),
    ((72.025, 23.0), (72.025, 28.0)),
    ((72.025, 28.0), (72.025, 33.0)),
    ((77.99, 34.85), (77.99, 38.88)),
    ((77.99, 38.88), (82.61, 43.5)),
]
main_5v_widths = []
for track in tracks:
    net_match = re.search(r'\(net "([^"]+)"\)', track)
    if not net_match or norm(net_match.group(1)) != "SYS_5V0_ALWAYS_ON":
        continue
    coords = re.findall(r'\(start ([\d.]+) ([\d.]+)\)|\(end ([\d.]+) ([\d.]+)\)', track)
    points = []
    for record in coords:
        x, y = (record[0], record[1]) if record[0] else (record[2], record[3])
        points.append((float(x), float(y)))
    width = re.search(r'\(width ([\d.]+)\)', track)
    if len(points) == 2 and width and any(
        (math.dist(points[0], a) < 0.03 and math.dist(points[1], b) < 0.03)
        or (math.dist(points[0], b) < 0.03 and math.dist(points[1], a) < 0.03)
        for a, b in main_5v_segments
    ):
        main_5v_widths.append(float(width.group(1)))

ref_check_pass = (
    not component_ref_mismatch["missing_installed_footprints"]
    and not component_ref_mismatch["unexpected_footprints"]
    and component_ref_mismatch["expected_dnp_omissions"] == sorted(OMITTED_DNP)
)
result = {
    "status": "PASS" if (
        ref_check_pass and not pad_mismatches and not extra_board_pads
        and drc_match and drc_match.group(1) == "0"
        and unconnected_match and unconnected_match.group(1) == "0"
    ) else "FAIL",
    "board": BOARD.name,
    "board_sha256": hashlib.sha256(BOARD.read_bytes()).hexdigest(),
    "netlist_sha256": hashlib.sha256(NETLIST.read_bytes()).hexdigest(),
    "board_footprint_count": len(board_components),
    "board_pad_count": len(actual),
    "board_track_count": len(tracks),
    "board_via_count": len(vias),
    "segment_width_counts_mm": dict(sorted(width_counts.items())),
    "min_track_width_mm": min((float(width) for width in width_counts), default=None),
    "sys_5v_net_min_width_mm": min(net_widths.get("SYS_5V0_ALWAYS_ON", [0.0])),
    "selected_high_current_path_segment_widths_mm": sorted(main_5v_widths),
    "component_ref_check": component_ref_mismatch,
    "pad_net_mismatches": pad_mismatches,
    "extra_board_pads": extra_board_pads,
    "drc_violations": int(drc_match.group(1)) if drc_match else None,
    "unconnected_items": int(unconnected_match.group(1)) if unconnected_match else None,
    "physical_validation": "NOT_RUN_NO_SAMPLE",
    "manufacturing_status": "EVT_REVIEW_CANDIDATE_FABRICATION_HOLD",
}
(ROOT / "pcb-net-parity.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
print(json.dumps(result, ensure_ascii=False, indent=2))
if result["status"] != "PASS":
    raise SystemExit(1)
