"""ringcli — the deterministic ring-design CLI.

The model's ONLY job is a spec; templates + parts kit do the geometry, so there
is nothing free-form to screw up. Emits a tiny committed generator (spec + one
call), builds STEP/GLB through the standard pipeline, and validates the result.

  # build one ring from flags
  .cad-venv/bin/python harness/ringcli.py build --name my-ring \
      --cut round --carat 1.5 --size 6.5 --archetype solitaire --metal yellow

  # regenerate every design of a benchmark round from its specs
  .cad-venv/bin/python harness/ringcli.py bench eval/rounds/r47.json

  # style overrides (tunable knobs live in authored/_templates.py STYLE)
  ... build ... --style '{"claw_w": 0.34, "dominance": 1.1}'

Validation: STEP exists and is non-trivial, GLB sidecar exists, bounding box is
ring-sized. Failures exit nonzero and say why.
"""

from __future__ import annotations

import argparse
import json
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
PYBIN = str(ROOT / ".cad-venv" / "bin" / "python")
STEP = str(ROOT / ".agents/skills/cad/scripts/step")
DESIGNS = ROOT / "designs"

GENERATOR = '''"""{name}: generated by ringcli — DO NOT hand-edit geometry.
To change this ring, change SPEC (or STYLE in ringcli/templates.py) and rerun:
  .cad-venv/bin/python -m ringcli.cli build --spec-json '{spec_json}'
"""
import sys
from pathlib import Path

for _anc in Path(__file__).resolve().parents:
    if (_anc / "ringcli").is_dir():
        sys.path.insert(0, str(_anc))
        break
from ringcli.templates import build_ring  # noqa: E402

SPEC = {spec!r}


def gen_step():
    return build_ring(SPEC)


def gen_parts():
    """Raw colored parts list for the renderer (Compound drops colors)."""
    return build_ring(SPEC, parts_only=True)


def gen_metal():
    """The single fused metal Solid — the printable/castable body."""
    return build_ring(SPEC, fused=True)


def gen_supports():
    """Green casting sprue + feeder stems (render/print aid, never cast)."""
    from ringcli.templates import supports_for
    return supports_for(SPEC)
'''


VALID_CUTS = {"round", "oval", "cushion", "elongated_cushion", "princess",
              "emerald", "radiant", "asscher", "pear", "marquise"}
ARCH_WORDS = ("bezel", "halo", "three", "pav", "solitaire")


def check_spec(spec: dict) -> list[str]:
    """RingSpec validation — delegates to ringcli.spec (errors are written
    for LLM self-correction: field, value, allowed range, concrete fix)."""
    from ringcli.spec import validate
    return validate(spec)


def write_generator(spec: dict) -> Path:
    gen = DESIGNS / f"{spec['name']}.py"
    gen.write_text(GENERATOR.format(name=spec["name"], spec=spec,
                                    spec_json=json.dumps(spec)))
    return gen


def build(spec: dict, outdir: Path | None = None, timeout: int = 240) -> dict:
    """Emit generator, build STEP via the standard pipeline, validate."""
    spec_errs = check_spec(spec)
    if spec_errs:
        return {"name": spec.get("name", "?"), "ok": False,
                "errors": ["bad spec: " + "; ".join(spec_errs)]}
    gen = write_generator(spec)
    outdir = outdir or (ROOT / "artifacts" / "cli" / spec["name"])
    outdir.mkdir(parents=True, exist_ok=True)
    local = outdir / gen.name
    local.write_text(gen.read_text())
    p = subprocess.run([PYBIN, STEP, str(local)], cwd=ROOT,
                       capture_output=True, text=True, timeout=timeout)
    step = outdir / f"{spec['name']}.step"
    glb = outdir / f".{spec['name']}.step.glb"
    stl = outdir / f"{spec['name']}-cast.stl"
    print_stl = outdir / f"{spec['name']}-print.stl"
    errors = []
    if p.returncode != 0:
        errors.append("build failed: " + (p.stdout + p.stderr).strip()[-400:])
    if not errors and (not step.exists() or step.stat().st_size < 50_000):
        errors.append("STEP missing or trivially small")
    if not errors and not glb.exists():
        errors.append("GLB sidecar missing")
    if not errors:
        # printable export: fused metal-only body -> STL (the file the jeweler
        # prints for lost-wax casting; stones are set after casting)
        try:
            r = subprocess.run([PYBIN, "-c",
                                f"import sys; sys.path.insert(0, r'{ROOT}'); "
                                f"from ringcli.cast import export_cast; "
                                f"export_cast(r'{local}', r'{stl}', r'{print_stl}')"],
                               cwd=ROOT, capture_output=True, text=True,
                               timeout=max(timeout, 480))
            if r.returncode != 0 or not stl.exists():
                errors.append("cast STL export failed: "
                              + (r.stdout + r.stderr).strip()[-300:])
        except subprocess.TimeoutExpired:
            errors.append("cast STL export timed out")
        else:
            for line in reversed((r.stdout or "").strip().splitlines()):
                if line.endswith(".stl"):        # shrink-tagged final path
                    print_stl = Path(line)
                    break
    if not errors:
        v = subprocess.run([PYBIN, str(ROOT / "ringcli" / "validate.py"), str(local)],
                           cwd=ROOT, capture_output=True, text=True, timeout=timeout)
        try:
            verdict = json.loads(v.stdout.strip().splitlines()[-1])
            if not verdict.get("ok"):
                errors.append("printability: " + "; ".join(verdict.get("errors", ["failed"])))
        except Exception:
            errors.append("printability validation failed to run")
    result = {"name": spec["name"], "ok": not errors, "errors": errors,
              "step": str(step), "glb": str(glb), "cast_stl": str(stl),
              "print_stl": str(print_stl), "generator": str(gen)}
    production = bool(spec.get("production")) or "stone_mm" in spec
    if production and not errors:
        coupon = outdir / f"{spec['name']}-coupon.stl"
        try:
            r = subprocess.run([PYBIN, "-c",
                                f"import sys; sys.path.insert(0, r'{ROOT}'); "
                                f"from ringcli.coupon import export_coupon; "
                                f"export_coupon(r'{local}', r'{coupon}')"],
                               cwd=ROOT, capture_output=True, text=True,
                               timeout=max(timeout, 480))
            if r.returncode != 0:
                errors.append("coupon export failed: "
                              + (r.stdout + r.stderr).strip()[-250:])
        except subprocess.TimeoutExpired:
            errors.append("coupon export timed out")
        r = subprocess.run([PYBIN, "-c",
                            f"import sys; sys.path.insert(0, r'{ROOT}'); "
                            f"from ringcli.workorder import write_workorder; "
                            f"print(write_workorder(r'{local}', r'{outdir}'))"],
                           cwd=ROOT, capture_output=True, text=True, timeout=timeout)
        if r.returncode != 0:
            errors.append("work order failed: " + (r.stdout + r.stderr).strip()[-250:])
        result.update(ok=not errors, errors=errors, coupon_stl=str(coupon),
                      workorder_html=str(outdir / f"{spec['name']}-workorder.html"))
    return result


def spec_from_bench(b: dict) -> dict:
    keys = ("name", "cut", "carat", "standard", "size", "archetype", "metal",
            "band_width", "band_thickness", "shank", "head", "accents",
            "production", "stone_mm", "stone", "casting")
    spec = {k: b[k] for k in keys if k in b}
    if "shank" in spec:                 # v3 composition wins; archetype stays
        spec.pop("archetype", None)     # in the bench entry for display only
    if "style" in b:
        spec["style"] = b["style"]
    return spec


def main() -> int:
    p = argparse.ArgumentParser()
    sub = p.add_subparsers(dest="cmd", required=True)
    pb = sub.add_parser("build")
    pb.add_argument("--spec-json", help="full spec as one JSON object")
    pb.add_argument("--name")
    pb.add_argument("--cut")
    pb.add_argument("--carat", type=float)
    pb.add_argument("--size", type=float)
    pb.add_argument("--archetype")
    pb.add_argument("--metal", default="white")
    pb.add_argument("--style", help="JSON overriding STYLE keys")
    pn = sub.add_parser("bench")
    pn.add_argument("round_file")
    pc = sub.add_parser("calibrate", help="emit sized test bands to dial in "
                        "your print+cast shrink factor")
    pc.add_argument("--size", type=float, default=7.0)
    pc.add_argument("--scales", type=float, nargs="+",
                    default=[1.00, 1.02, 1.03])
    args = p.parse_args()

    if args.cmd == "calibrate":
        import trimesh
        from build123d import export_stl
        from ringcli.parts import band
        outdir = ROOT / "artifacts" / "cli" / "calibration"
        outdir.mkdir(parents=True, exist_ok=True)
        b, inner_r, _ = band(args.size, 1.2, 2.0, "white")
        base = outdir / "cal-base.stl"
        export_stl(b, str(base), tolerance=0.01, angular_tolerance=0.3)
        for f in args.scales:
            m = trimesh.load(base)
            m.apply_scale(f)
            out = outdir / f"cal-s{args.size:g}-{f:.3f}.stl"
            m.export(out)
            print(out)
        base.unlink()
        print(f"\nPrint each band in your castable resin, cast in your alloy, "
              f"measure the finished inner diameter (target "
              f"{2*inner_r:.2f} mm for size {args.size:g}).\n"
              f"Your shrink_scale = target_ID / measured_ID of the closest "
              f"band; put it in the spec's casting.shrink_scale.")
        return 0

    if args.cmd == "build":
        if args.spec_json:
            spec = json.loads(args.spec_json)
        else:
            if not all((args.name, args.cut, args.carat, args.size, args.archetype)):
                p.error("--name --cut --carat --size --archetype required (or --spec-json)")
            spec = {"name": args.name, "cut": args.cut, "carat": args.carat,
                    "standard": "US", "size": args.size,
                    "archetype": args.archetype, "metal": args.metal}
            if args.style:
                spec["style"] = json.loads(args.style)
        r = build(spec)
        print(json.dumps(r, indent=2))
        return 0 if r["ok"] else 1

    bench = json.loads((ROOT / args.round_file).read_text())
    results = [build(spec_from_bench(b)) for b in bench]
    for r in results:
        print(("OK   " if r["ok"] else "FAIL ") + r["name"]
              + ("" if r["ok"] else "  " + "; ".join(r["errors"])))
    print(json.dumps({"built": sum(r["ok"] for r in results),
                      "total": len(results)}))
    return 0 if all(r["ok"] for r in results) else 1


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