"""Stubbed FreeCAD executor for deterministic testing without FreeCAD imports.

Provides ``StubFreecadExecutor`` which implements the same async interface as
``FreecadExecutor`` but returns deterministic results derived from a feature
graph.  No FreeCAD imports or runtime dependencies.

Rules:
- Intercepts Python code generated by ``_execute_set_parameter`` and returns
  ``old_value`` / ``new_value`` pairs looked up from the feature graph.
- Returns canned responses for STEP and FCStd export helpers.
- Never imports FreeCAD.
"""

from __future__ import annotations

import ast
import re
from typing import Any

from freecad_mcp.bridge.executor import FreecadExecutor


# ------------------------------------------------------------------
# Regex patterns for the code templates generated by tools_cad
# ------------------------------------------------------------------

_GETOBJECT_RE = re.compile(r"doc\.getObject\((['\"])([^'\"]*?)\1\)")
_GETATTR_RE = re.compile(r"getattr\(obj,\s*(['\"])([^'\"]*?)\1\)")
_SETATTR_RE = re.compile(r"setattr\(obj,\s*(['\"])([^'\"]*?)\1,\s*(.+)\)\s*$", re.MULTILINE)
_EXPORT_STEP_RE = re.compile(r"shape\.exportStep\((['\"])([^'\"]*?)\1\)")
_SAVEAS_RE = re.compile(r"doc\.saveAs\((['\"])([^'\"]*?)\1\)")


class StubFreecadExecutor(FreecadExecutor):
    """Drop-in executor substitute that simulates FreeCAD without importing it.

    Args:
        feature_graph: Optional feature graph dict used to derive ``old_value``
            for parameter changes.  Parameters with a ``current_value`` field
            are read during initialisation.
        initial_state: Optional flat dict ``{object_name: {param: value}}``
            that seeds the stub's internal state.  Overrides values from
            ``feature_graph``.
    """

    def __init__(
        self,
        feature_graph: dict[str, Any] | None = None,
        initial_state: dict[str, dict[str, Any]] | None = None,
    ) -> None:
        # Deliberately skip FreecadExecutor.__init__ to avoid settings / import side effects
        self.calls: list[str] = []
        self._state: dict[str, dict[str, Any]] = {}
        self._init_state_from_feature_graph(feature_graph or {})
        if initial_state:
            for obj_name, params in initial_state.items():
                self._state.setdefault(obj_name, {}).update(params)

    # ------------------------------------------------------------------
    # State helpers
    # ------------------------------------------------------------------

    def _init_state_from_feature_graph(self, feature_graph: dict[str, Any]) -> None:
        features = feature_graph.get("features", {})
        if not isinstance(features, dict):
            return
        for _feature_id, feature in features.items():
            if not isinstance(feature, dict):
                continue
            obj_name = feature.get("freecad_object_name") or _feature_id
            for param in feature.get("parameters", []):
                if not isinstance(param, dict):
                    continue
                param_name = param.get("freecad_parameter_name") or param.get("name")
                current_value = param.get("current_value")
                if param_name is not None and current_value is not None:
                    self._state.setdefault(str(obj_name), {})[str(param_name)] = current_value

    def get_state(self) -> dict[str, dict[str, Any]]:
        """Return the current internal parameter state."""
        return {k: dict(v) for k, v in self._state.items()}

    # ------------------------------------------------------------------
    # Async interface (same shape as FreecadExecutor)
    # ------------------------------------------------------------------

    async def execute_async(self, code: str) -> dict[str, Any]:
        self.calls.append(code)

        result = self._try_parameter_set(code)
        if result is not None:
            return {"success": True, "result": result}

        result = self._try_export_step(code)
        if result is not None:
            return {"success": True, "result": result}

        result = self._try_export_fcstd(code)
        if result is not None:
            return {"success": True, "result": result}

        # Fallback for unrecognised code patterns
        return {"success": True, "result": {}}

    async def get_version_async(self) -> dict[str, Any]:
        self.calls.append("get_version")
        return {
            "version": "0.21.0-stub",
            "revision": "stub",
            "python_version": "3.11-stub",
            "gui_available": False,
        }

    # ------------------------------------------------------------------
    # Code-pattern matchers
    # ------------------------------------------------------------------

    def _try_parameter_set(self, code: str) -> dict[str, Any] | None:
        """Match code from ``_execute_set_parameter`` and return simulated result."""
        obj_match = _GETOBJECT_RE.search(code)
        if not obj_match:
            return None

        getattr_match = _GETATTR_RE.search(code)
        if not getattr_match:
            return None

        setattr_match = _SETATTR_RE.search(code)
        if not setattr_match:
            return None

        obj_name = obj_match.group(2)
        param_name = getattr_match.group(2)

        try:
            raw_value = setattr_match.group(3).strip()
            new_value = ast.literal_eval(raw_value)
        except (ValueError, SyntaxError):
            return None

        # Look up old value from internal state
        old_value = self._state.get(obj_name, {}).get(param_name)

        # Update internal state so subsequent calls see the new value
        self._state.setdefault(obj_name, {})[param_name] = new_value

        return {
            "object_name": obj_name,
            "parameter_name": param_name,
            "old_value": old_value,
            "new_value": new_value,
        }

    def _try_export_step(self, code: str) -> dict[str, Any] | None:
        """Match code from ``_execute_export_step`` and return simulated result."""
        match = _EXPORT_STEP_RE.search(code)
        if not match:
            return None
        file_path = match.group(2)
        return {"file_path": file_path, "object_count": 1}

    def _try_export_fcstd(self, code: str) -> dict[str, Any] | None:
        """Match code from ``_execute_export_fcstd`` and return simulated result."""
        match = _SAVEAS_RE.search(code)
        if not match:
            return None
        file_path = match.group(2)
        return {"file_path": file_path, "document": "Unnamed"}
