"""Cast export — the printable file.

Loads a generated design module, builds the FUSED metal-only body, verifies it
is one valid solid, and writes the STL the jeweler resin-prints for lost-wax
casting (stones are set after casting and are never part of the print).
"""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path

from build123d import export_stl


def export_cast(gen_path: str, out_stl: str, out_print_stl: str | None = None) -> None:
    gen = Path(gen_path)
    spec = importlib.util.spec_from_file_location("gen_mod", gen)
    mod = importlib.util.module_from_spec(spec)
    sys.path.insert(0, str(gen.parent))
    spec.loader.exec_module(mod)
    shrink = float(mod.SPEC.get("casting", {}).get("shrink_scale", 1.0)) \
        if hasattr(mod, "SPEC") else 1.0
    metal = mod.gen_metal()
    solids = metal.solids()
    if len(solids) != 1:
        raise RuntimeError(f"metal body is {len(solids)} solids, expected 1")
    iv = metal.is_valid
    if not (iv() if callable(iv) else iv):
        raise RuntimeError("fused metal body failed OCC validity")
    # 10um tessellation: far finer than 25-50um printer resolution, ~10x
    # smaller files than the 1um default (a halo hit 33MB and repair crawled)
    export_stl(metal, out_stl, tolerance=0.01, angular_tolerance=0.3)
    _make_watertight(out_stl, expected_vol=metal.volume)
    if out_print_stl and hasattr(mod, "gen_supports"):
        from ringcli.parts import fuse_all
        printable = fuse_all([metal] + mod.gen_supports())
        if shrink != 1.0:
            # pre-compensate casting shrinkage in the PRINT file only; STEP +
            # cast.stl stay nominal (the design record). Factor is in the
            # filename so the slicer is never double-compensated.
            out_print_stl = str(Path(out_print_stl).with_name(
                Path(out_print_stl).stem.replace("-print", "")
                + f"-print-s{shrink:.4f}.stl"))
        export_stl(printable, out_print_stl, tolerance=0.01, angular_tolerance=0.3)
        _make_watertight(out_print_stl, expected_vol=printable.volume)
        if shrink != 1.0:
            import trimesh
            m = trimesh.load(out_print_stl)
            m.apply_scale(shrink)          # uniform scale keeps watertightness
            m.export(out_print_stl)
        print(out_print_stl)               # cli reads the final path from stdout


def _make_watertight(path, expected_vol: float | None = None) -> None:
    """OCC tessellation leaves cracked/non-manifold edges at tangencies; print
    houses bounce those. Repair in place and verify against the OCC solid's
    exact volume — a NON-watertight mesh has no meaningful volume, so the old
    repaired-vs-broken comparison misfired on heavily-segmented geometry."""
    import numpy as np
    import trimesh
    m = trimesh.load(path)
    if m.is_watertight:
        return
    import pymeshfix
    v, f = pymeshfix.clean_from_arrays(np.asarray(m.vertices), np.asarray(m.faces),
                                   joincomp=True,
                                   remove_smallest_components=False)
    m2 = trimesh.Trimesh(v, f)
    if not m2.is_watertight:
        raise RuntimeError(f"mesh repair failed: {path} still not watertight")
    ref = expected_vol if expected_vol else m.volume
    if abs(m2.volume - ref) > 0.08 * max(ref, 1.0):
        raise RuntimeError(f"mesh repair volume {m2.volume:.1f} deviates >8% "
                           f"from solid volume {ref:.1f} ({path})")
    m2.export(path)


if __name__ == "__main__":
    export_cast(sys.argv[1], sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else None)
    print(f"cast STL: {sys.argv[2]}")
