#!/usr/bin/env python3
"""Circuitcaster JSON -> KiCad project (footprints + fp-lib-table stub).

Usage:
    python cc_to_kicad.py <export.json> <output_dir>

Produces:
    <output_dir>/lib/<project>.pretty/*.kicad_mod   — one per footprint type
    <output_dir>/fp-lib-table                        — registers the library
    <output_dir>/README.md                           — next-step instructions

Source of truth for the input schema:
    https://app.circuitcaster.com/llms.txt

Human-facing walkthrough:
    https://circuitcaster.com/kicad-export/

AI-facing recipe:
    https://circuitcaster.com/kicad-export/for-ai.txt
"""
import json
import re
import sys
import uuid
from pathlib import Path


def sanitize(name: str) -> str:
    """Make a name safe for KiCad footprint filenames."""
    return re.sub(r"[^A-Za-z0-9_.-]+", "_", name).strip("_") or "unnamed"


def kicad_mod_source(fp_name: str, cc_fp: dict) -> str:
    """Emit a KiCad 6+ compatible .kicad_mod S-expression.

    cc_fp is one entry from proj['footprints'][name]. Its shape:
        {shape, pitch_mm, board_mm:{w,h},
         pins:[{n, kicad_xy_mm:[x,y], name, role, ...}]}
    """
    pins = sorted(cc_fp.get("pins", []), key=lambda p: p["n"])
    w = cc_fp["board_mm"]["w"]
    h = cc_fp["board_mm"]["h"]
    shape = cc_fp.get("shape", "rect")

    def U():
        return str(uuid.uuid4())

    out = [f'(footprint "{fp_name}"',
           f'  (version 20240108)',
           f'  (generator "circuitcaster")',
           f'  (layer "F.Cu")',
           f'  (descr "Generated from Circuitcaster export — verify against physical part before fabricating.")',
           f'  (tags "circuitcaster generated")',
           f'  (property "Reference" "REF**" (at 0 {-h/2 - 2:.3f} 0) (layer "F.SilkS") (uuid "{U()}") (effects (font (size 1 1) (thickness 0.15))))',
           f'  (property "Value" "{fp_name}" (at 0 {h/2 + 2:.3f} 0) (layer "F.Fab") (uuid "{U()}") (effects (font (size 1 1) (thickness 0.15))))',
           f'  (attr through_hole)']

    hw, hh = w / 2, h / 2
    if shape == "circle":
        r = w / 2
        out.append(f'  (fp_circle (center 0 0) (end {r:.3f} 0) (stroke (width 0.12) (type solid)) (fill none) (layer "Edge.Cuts") (uuid "{U()}"))')
    else:
        segs = [((-hw, -hh), (hw, -hh)), ((hw, -hh), (hw, hh)),
                ((hw,  hh), (-hw, hh)), ((-hw, hh), (-hw, -hh))]
        for a, b in segs:
            out.append(f'  (fp_line (start {a[0]:.3f} {a[1]:.3f}) (end {b[0]:.3f} {b[1]:.3f}) '
                       f'(stroke (width 0.12) (type solid)) (layer "Edge.Cuts") (uuid "{U()}"))')

    if pins:
        p1 = pins[0]
        px, py = p1["kicad_xy_mm"]
        out.append(f'  (fp_circle (center {px - 2.0:.3f} {py:.3f}) (end {px - 1.6:.3f} {py:.3f}) '
                   f'(stroke (width 0.2) (type solid)) (fill solid) (layer "F.SilkS") (uuid "{U()}"))')

    for p in pins:
        x, y = p["kicad_xy_mm"]
        pad_shape = "rect" if p["n"] == 1 else "circle"
        out.append(f'  (pad "{p["n"]}" thru_hole {pad_shape} (at {x:.3f} {y:.3f}) '
                   f'(size 1.6 1.6) (drill 0.8) (layers "*.Cu" "*.Mask") (uuid "{U()}"))')

    out.append(')')
    return "\n".join(out)


def emit_fp_lib_table(project_name: str) -> str:
    return (
        '(fp_lib_table\n'
        '  (version 7)\n'
        f'  (lib (name "{project_name}") (type "KiCad") '
        f'(uri "${{KIPRJMOD}}/lib/{project_name}.pretty") (options "") (descr "Circuitcaster-generated"))\n'
        ')\n'
    )


def emit_readme(project_name: str, fp_names: list) -> str:
    return f"""# {project_name} — Circuitcaster → KiCad handoff

Generated by cc_to_kicad.py from a Circuitcaster JSON export.

## What's in this folder

    lib/{project_name}.pretty/    — generated .kicad_mod footprints ({len(fp_names)})
    fp-lib-table                  — registers the library with KiCad
    README.md                     — this file

Footprints generated:
{chr(10).join('    - ' + fp for fp in fp_names)}

## Next steps

1. In KiCad, File → New → Project → point at this folder. Name it whatever.
2. Author schematic symbols for each unique component type. Match pin names
   to the export's `footprints[type].pins[].name` field.
3. Tools → Assign Footprints. Pick from the "{project_name}" library.
4. Tools → Update PCB from Schematic. Footprints appear as a ratsnest.
5. Route (Freerouting or manual). Ground pour on B.Cu.
6. Inspect → Design Rules Checker. Clear errors.
7. File → Fabrication Outputs → Gerbers + Drill Files.
8. Zip. Upload to https://jlcpcb.com or https://pcbway.com.

## VERIFY BEFORE FABRICATING (required)

Print one .kicad_mod at 100% on paper. Place the physical component on
top. Every pin should land inside a pad. If not, fix in Circuitcaster
(not by hand-editing the .kicad_mod) and re-run this script.

## Full walkthrough

https://circuitcaster.com/kicad-export/
"""


def footprints_from_components(components: list) -> dict:
    """Fall back path for exports whose 'footprints{}' block is empty (common
    when all components were dropped via the Component Maker parametric
    presets rather than the Library). Group components by 'type' and
    synthesize one footprint per unique type, using pad geometry from the
    first instance of that type."""
    fps = {}
    for c in components:
        t = c.get("type") or "unnamed"
        if t in fps:
            continue
        pads = c.get("pads") or []
        # Compute a reasonable board bounding box from the pads + 1 mm margin.
        if pads:
            xs = [float(p.get("dx_mm", 0)) for p in pads]
            ys = [float(p.get("dy_mm", 0)) for p in pads]
            w = max(xs) - min(xs) + 2.0
            h = max(ys) - min(ys) + 2.0
        else:
            w = h = 5.0
        # Convert pads to the library shape (kicad_xy_mm centered).
        # Component pads are already center-relative (dx_mm / dy_mm), so
        # we can re-use them directly as kicad_xy_mm.
        norm_pins = []
        for p in pads:
            norm_pins.append({
                "n": p.get("n"),
                "kicad_xy_mm": [float(p.get("dx_mm", 0)), float(p.get("dy_mm", 0))],
                "name": p.get("name", ""),
                "role": p.get("role", 0),
            })
        fps[t] = {
            "shape": "rect",
            "pitch_mm": c.get("pitch_mm") or 2.54,
            "board_mm": {"w": round(w, 3), "h": round(h, 3)},
            "pins": norm_pins,
        }
    return fps


def main():
    if len(sys.argv) < 3:
        print("Usage: python cc_to_kicad.py <export.json> <output_dir>")
        sys.exit(2)

    src = Path(sys.argv[1])
    out = Path(sys.argv[2])
    proj = json.loads(src.read_text(encoding="utf-8"))

    project_name = sanitize(proj.get("project_name") or proj.get("name") or "cc_project")
    pretty_dir = out / "lib" / f"{project_name}.pretty"
    pretty_dir.mkdir(parents=True, exist_ok=True)

    footprints = proj.get("footprints", {})
    if not footprints:
        # Fall back to synthesizing footprints from the components[] block.
        components = proj.get("components") or proj.get("wireComponents") or []
        if components:
            footprints = footprints_from_components(components)
            print(f"(footprints{{}} was empty — synthesized {len(footprints)} footprint(s) from components[])")
    if not footprints:
        print("No footprints and no components in export. Nothing to generate.")
        sys.exit(1)

    generated = []
    for fp_name, cc_fp in footprints.items():
        safe_name = sanitize(fp_name)
        (pretty_dir / f"{safe_name}.kicad_mod").write_text(
            kicad_mod_source(safe_name, cc_fp), encoding="utf-8")
        generated.append(safe_name)
        print(f"  wrote lib/{project_name}.pretty/{safe_name}.kicad_mod")

    (out / "fp-lib-table").write_text(emit_fp_lib_table(project_name), encoding="utf-8")
    (out / "README.md").write_text(emit_readme(project_name, generated), encoding="utf-8")

    # ASCII-only print to survive Windows cp1252 default consoles.
    print(f"\nDone. Open {out} as a KiCad project (File -> Open Project).")


if __name__ == "__main__":
    main()
