"""
loader.py — STEP file loading with build123d primary and FreeCAD fallback.

Returns a StepModel dataclass used by all other modules.
FreeCAD fallback invokes the signed app bundle Python to avoid
Gatekeeper issues on macOS 15 Sequoia.
"""

import json
import logging
import subprocess
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional

logger = logging.getLogger("step_processor.loader")

FREECAD_PYTHON = "/Applications/FreeCAD.app/Contents/Resources/bin/python"
FREECAD_LIB    = "/Applications/FreeCAD.app/Contents/Resources/lib"
FREECAD_CMD    = "/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd"


@dataclass
class StepModel:
    """Unified model object returned by load_step(). Used by all modules."""
    shape: Any
    backend: str            # "build123d" | "freecad"
    path: Path
    parts: list = field(default_factory=list)
    face_count: int = 0
    metadata: dict = field(default_factory=dict)


def load_step(filepath) -> Optional["StepModel"]:
    """Load a STEP file. Tries build123d first; falls back to FreeCAD."""
    step_path = Path(filepath).expanduser().resolve()
    if not step_path.exists():
        logger.error(f"File not found: {step_path}")
        return None
    try:
        return _load_via_build123d(step_path)
    except ImportError:
        logger.warning("build123d not available — falling back to FreeCAD")
        return _load_via_freecad(step_path)
    except Exception as e:
        logger.warning(f"build123d failed ({type(e).__name__}: {e}) — falling back to FreeCAD")
        return _load_via_freecad(step_path)


def _load_via_build123d(step_path: Path) -> "StepModel":
    """Load using build123d. Raises on failure."""
    from build123d import import_step
    logger.info(f"[build123d] Loading: {step_path.name}")
    shape = import_step(str(step_path))
    face_count = 0
    try:
        face_count = sum(1 for _ in shape.faces())
    except Exception:
        pass
    parts = _extract_parts_build123d(shape)
    logger.info(f"[build123d] Loaded: {step_path.name} | {face_count} faces | {len(parts)} parts")
    return StepModel(shape=shape, backend="build123d", path=step_path,
                     parts=parts, face_count=face_count)


def _fix_gbk_mojibake(s: str) -> str:
    """
    Recover Chinese text stored as mojibake in STEP part labels.

    STEP files from Chinese CAD tools (SolidWorks CN, etc.) embed raw GBK bytes
    in PRODUCT name strings. OpenCASCADE reads STEP strings as latin-1, which
    re-interprets those GBK bytes as latin-1 code points — classic mojibake.

    Fix: re-encode the string to latin-1 (restoring the original GBK byte
    sequence) then decode as GBK to get correct Unicode Chinese characters.

    If the string is pure ASCII, or the round-trip fails (already valid Unicode
    or a non-GBK extended char), returns the original string unchanged.
    """
    if not s or all(ord(c) < 128 for c in s):
        return s  # pure ASCII: nothing to fix
    try:
        return s.encode('latin-1').decode('gbk')
    except (UnicodeDecodeError, UnicodeEncodeError):
        return s  # not GBK mojibake — leave original


def _extract_parts_build123d(shape) -> list:
    """Walk build123d compound tree and extract named parts."""
    parts = []
    def _walk(compound, level=0, parent_name=""):
        children = []
        try:
            children = compound.children if hasattr(compound, "children") else []
        except Exception:
            pass
        if children:
            for child in children:
                raw = (getattr(child, "label", "") or
                       getattr(child, "name", "") or f"Part_{level}")
                name = _fix_gbk_mojibake(raw)
                parts.append({"name": name, "level": level, "parent": parent_name})
                _walk(child, level + 1, name)
        else:
            raw = (getattr(compound, "label", "") or
                   getattr(compound, "name", "") or "")
            if raw:
                name = _fix_gbk_mojibake(raw)
                parts.append({"name": name, "level": level, "parent": parent_name})
    _walk(shape)
    return parts


def _load_via_freecad(step_path: Path) -> Optional["StepModel"]:
    """Load using FreeCAD app bundle Python via subprocess."""
    if not Path(FREECAD_PYTHON).exists():
        logger.error(f"FreeCAD Python not found at {FREECAD_PYTHON}. Install FreeCAD.app.")
        return None
    logger.info(f"[FreeCAD] Loading: {step_path.name}")
    script = f"""
import sys, json
sys.path.insert(0, {repr(FREECAD_LIB)})
import FreeCAD, Part
try:
    shape = Part.read({repr(str(step_path))})
    bb = shape.BoundBox
    sub = shape.SubShapes if hasattr(shape, 'SubShapes') else []
    parts = [{{"name": f"Part_{{i}}", "level": 1, "parent": "root"}}
             for i in range(len(sub))]
    print(json.dumps({{"ok": True, "face_count": len(shape.Faces),
        "parts": parts,
        "bbox": {{"XMin": bb.XMin, "XMax": bb.XMax,
                 "YMin": bb.YMin, "YMax": bb.YMax,
                 "ZMin": bb.ZMin, "ZMax": bb.ZMax}}}}))
except Exception as e:
    print(json.dumps({{"ok": False, "error": str(e)}}))
"""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
        f.write(script)
        script_path = f.name


    try:
        proc = subprocess.run([FREECAD_PYTHON, script_path],
                              capture_output=True, text=True, timeout=120)
        json_line = next((l.strip() for l in proc.stdout.splitlines()
                          if l.strip().startswith("{")), None)
        if not json_line:
            logger.error(f"[FreeCAD] No JSON output. stderr: {proc.stderr[:300]}")
            return None
        data = json.loads(json_line)
        if not data.get("ok"):
            logger.error(f"[FreeCAD] Load failed: {data.get('error')}")
            return None
        proxy = _FreeCADShapeProxy(data["bbox"], data["face_count"])
        logger.info(f"[FreeCAD] Loaded: {step_path.name} | {data['face_count']} faces")
        return StepModel(shape=proxy, backend="freecad", path=step_path,
                         parts=data.get("parts", []),
                         face_count=data["face_count"],
                         metadata={"bbox": data["bbox"]})
    except subprocess.TimeoutExpired:
        logger.error("[FreeCAD] Load timed out after 120s")
        return None
    except Exception as e:
        logger.error(f"[FreeCAD] Unexpected error: {e}")
        return None
    finally:
        Path(script_path).unlink(missing_ok=True)


class _FreeCADShapeProxy:
    """Proxy carrying FreeCAD geometry data extracted via subprocess."""
    def __init__(self, bbox_dict: dict, face_count: int):
        self.BoundBox = _BoundBox(bbox_dict)
        self.face_count = face_count
        self.Faces = [None] * face_count
    def faces(self):
        for _ in range(self.face_count):
            yield object()


class _BoundBox:
    def __init__(self, d: dict):
        self.XMin = d.get("XMin", 0); self.XMax = d.get("XMax", 0)
        self.YMin = d.get("YMin", 0); self.YMax = d.get("YMax", 0)
        self.ZMin = d.get("ZMin", 0); self.ZMax = d.get("ZMax", 0)
