"""Printability validation v3 — deterministic, no judge needed.

Primary gates (fused body, via the design's gen_metal()):
1. the metal fuses into exactly ONE valid solid (watertight castable body)
2. mass sanity: volume x 18k density within 0.8-25 g
3. every stone is held: gap from each stone to the metal body stays under the
   seat clearance (stones sit in cut seats after v2)

  .cad-venv/bin/python ringcli/validate.py designs/<name>.py
Exit 0 = printable; nonzero with JSON errors on stdout.
"""

from __future__ import annotations

import importlib.util
import json
import math
import sys
from pathlib import Path

TOUCH_MM = 0.25          # stone-to-seat max gap (seat_clearance leaves ~0.1)
DENSITY_18K = 0.0155     # g/mm^3


def _load(gen_path: Path):
    spec = importlib.util.spec_from_file_location("gen_mod", gen_path)
    mod = importlib.util.module_from_spec(spec)
    sys.path.insert(0, str(gen_path.parent))
    spec.loader.exec_module(mod)
    return mod


def validate(gen_path: Path) -> dict:
    mod = _load(gen_path)
    errors = []
    if not hasattr(mod, "gen_metal"):
        return {"generator": str(gen_path), "ok": False,
                "errors": ["no gen_metal() — regenerate the design with ringcli v2"]}

    spec = getattr(mod, "SPEC", {})
    production = bool(spec.get("production")) or "stone_mm" in spec
    from ringcli.alloys import DENSITY, alloy_from_metal
    alloy = alloy_from_metal(spec.get("metal", "white"), spec.get("casting"))
    density = DENSITY.get(alloy, DENSITY_18K)

    metal = mod.gen_metal()
    solids = metal.solids()
    if len(solids) != 1:
        errors.append(f"metal fused into {len(solids)} solids, expected 1")
    iv = metal.is_valid
    if not (iv() if callable(iv) else iv):
        errors.append("fused metal body failed OCC validity check")
    mass = metal.volume * density
    hi = 35.0 if "plat" in alloy else 25.0
    if not (0.8 <= mass <= hi):
        errors.append(f"implausible mass {mass:.1f} g ({alloy}) — "
                      f"volume {metal.volume:.0f} mm3")

    loose = []
    seat_errs = []
    if hasattr(mod, "gen_parts"):
        from ringcli.parts import is_metal
        for i, p in enumerate(mod.gen_parts()):
            if is_metal(p):
                continue
            try:
                d = p.distance_to(metal)
            except Exception:
                continue
            bb = p.bounding_box()
            precision = max(bb.size.X, bb.size.Y) > 2.0   # center/side stones;
            if production and precision:                  # melee are bench-set
                if not (0.03 <= d <= 0.15):
                    seat_errs.append(f"stone {i} bearing gap {d:.3f}mm "
                                     "outside 0.03-0.15 (too tight = seat cut "
                                     "fell back; too loose = stone rattles)")
            elif d > TOUCH_MM:
                loose.append(i)
    if loose:
        errors.append(f"{len(loose)} stone(s) not held by the metal body: {loose}")
    errors += seat_errs

    shank_type = (spec.get("shank") or {}).get("type", "band")
    if production and shank_type in ("band", "split"):
        # ID probes assume metal hugs the finger circle at every lower-arc
        # angle — true for band/split; helix strands ride the wave (their
        # clearance is constructional: mid_r = inner + amp + prof/2) and
        # path shanks are blocked from production upstream
        from build123d import Vertex
        from ringcli.parts import inner_radius
        r_nom = inner_radius(float(spec.get("size", 6.5)))
        # sample away from the head arc (top of the ring): the seat pocket
        # legitimately opens toward the finger there in open-basket designs
        for k in range(8):
            a = math.tau * (0.42 + 0.66 * k / 8)   # 151..389 deg (bottom arc)
            x, z = r_nom * math.cos(a), r_nom * math.sin(a)
            inside = metal.distance_to(Vertex(1.05 * x, 0, 1.05 * z))
            hole = metal.distance_to(Vertex(0.95 * x, 0, 0.95 * z))
            if inside > 0.2 or hole < 0.02:
                errors.append(f"ring ID off nominal on the lower band arc "
                              f"(size {spec.get('size')})")
                break

    return {"generator": str(gen_path), "ok": not errors, "errors": errors,
            "alloy": alloy, "mass_g": round(mass, 2),
            "production": production}


def main() -> int:
    r = validate(Path(sys.argv[1]))
    print(json.dumps(r))
    return 0 if r["ok"] else 1


if __name__ == "__main__":
    raise SystemExit(main())
