"""
Python runtime executor.

Executes Python code directly in-process with sandboxing.
"""

import importlib
from typing import Any, Callable

from mcard import MCard

from .base import RUNTIME_CONFIG, RuntimeExecutor

# Module URI prefix for dynamic module imports
MODULE_URI_PREFIX = "module://"

# Import sandbox configuration
try:
    from mcard.config.settings import settings as _settings

    ALLOWED_IMPORTS = _settings.ptr.allowed_imports
    SAFE_BUILTINS = _settings.ptr.safe_builtins
except ImportError:
    ALLOWED_IMPORTS = {
        "math": "math",
        "json": "json",
        "yaml": "yaml",
        "pathlib": "pathlib",
        "typing": "typing",
        "hashlib": "hashlib",
        "mcard": "mcard",
        "os": "os",
        "time": "time",
        "random": "random",
        "logging": "logging",
    }
    SAFE_BUILTINS = {
        "int",
        "float",
        "str",
        "len",
        "range",
        "list",
        "dict",
        "tuple",
        "set",
        "abs",
        "round",
        "max",
        "min",
        "sum",
        "sorted",
        "enumerate",
        "zip",
        "ValueError",
        "Exception",
        "__build_class__",
        "super",
        "open",
        "all",
        "any",
        "bool",
        "isinstance",
        "bytes",
        "print",
        "globals",
    }


class PythonRuntime(RuntimeExecutor):
    """Python runtime executor - executes Python code directly in-process."""

    # Operation handlers mapping
    OPERATIONS: dict[str, Callable] = {}

    # Compilation cache for sandboxed execution
    _compilation_cache: dict[str, Any] = {}

    def __init__(self):
        super().__init__()
        # Import operations from the extracted operations module
        from ..operations import DEFAULT_OPERATIONS

        # Register built-in operations from module
        self.OPERATIONS = DEFAULT_OPERATIONS.copy()

        # Add custom operation handler (requires self reference for sandboxing)
        self.OPERATIONS["custom"] = self._op_custom

    def execute(
        self, concrete_impl: dict[str, Any], target: MCard, context: dict[str, Any]
    ) -> Any:
        # Check for module:// syntax first
        code_file = concrete_impl.get("code_file", "")
        if code_file.startswith(MODULE_URI_PREFIX):
            return self._execute_module_logic(code_file, concrete_impl, target, context)

        # Determine operation
        operation = concrete_impl.get(
            "process", concrete_impl.get("action", concrete_impl.get("operation"))
        )
        if not operation:
            operation = concrete_impl.get("builtin")

        if not operation:
            # Auto-detect custom operation if code is present
            if "code_file" in concrete_impl or "code" in concrete_impl:
                operation = "custom"
            else:
                operation = "identity"

        handler = self.OPERATIONS.get(operation)

        if handler:
            return handler(concrete_impl, target, context)
        return f"Executed {operation} on {target.hash}"

    # ─── Custom Operation ──────────────────────────────────────────────────────

    def _op_custom(self, impl: dict, target: MCard, ctx: dict) -> Any:
        code_file = impl.get("code_file", "")
        if code_file.startswith(MODULE_URI_PREFIX):
            return self._execute_module_logic(code_file, impl, target, ctx)

        code = impl.get("code", "")
        if not code:
            return "Error: No code provided"

        return self._exec_sandboxed(code, impl, target, ctx)

    # ─── Sandboxed Execution ───────────────────────────────────────────────────

    def _make_safe_import(self) -> Callable:
        """Create a safe import function."""

        def safe_import(name, globals=None, locals=None, fromlist=(), level=0):
            if name in ALLOWED_IMPORTS:
                return importlib.import_module(name)
            raise ImportError(f"Import of '{name}' is not allowed")

        return safe_import

    def _make_namespace(self, target: MCard, context: dict) -> dict:
        """Create a sandboxed namespace for code execution."""
        import builtins as builtins_module
        import math

        # Build safe builtins dict from the builtins module
        safe_builtins = {}
        for name in SAFE_BUILTINS:
            if hasattr(builtins_module, name):
                safe_builtins[name] = getattr(builtins_module, name)

        safe_builtins["__import__"] = self._make_safe_import()
        safe_builtins["__build_class__"] = builtins_module.__build_class__

        return {
            "__builtins__": safe_builtins,
            "target": target.get_content(),
            "context": context,
            "result": None,
            "math": math,
            "__name__": "__clm_runtime__",
        }

    def _exec_sandboxed(self, code: str, impl: dict, target: MCard, ctx: dict) -> Any:
        """Execute code in a sandboxed namespace with compilation caching."""
        namespace = self._make_namespace(target, ctx)

        try:
            # Use cached code object if available to avoid re-parsing
            if code not in self._compilation_cache:
                self._compilation_cache[code] = compile(code, "<string>", "exec")

            exec(self._compilation_cache[code], namespace)

            entry_point = impl.get("entry_point")
            if entry_point:
                return self._call_entry_point(namespace, entry_point, impl, target, ctx)
            return namespace.get("result")
        except Exception as e:
            return f"Error executing Python code: {e}"

    def _call_entry_point(
        self, ns: dict, entry: str, impl: dict, target: MCard, ctx: dict
    ) -> Any:
        """Call a function defined in namespace."""
        if entry not in ns or not callable(ns[entry]):
            return f"Error: Entry point '{entry}' not found or not callable. Keys: {list(ns.keys())}"

        func = ns[entry]
        arg = self._prepare_argument(impl, target, ctx)
        return func(arg)

    def _prepare_argument(self, impl: dict, target: MCard, ctx: dict) -> Any:
        """Prepare argument for entry point function."""
        inputs_def = impl.get("implementation", {}).get("inputs", {})

        # If context has operation or params keys, the entry point likely expects context
        if ctx.get("operation") or ctx.get("params") or ctx.get("op"):
            combined = ctx.copy()
            content = target.get_content()
            if isinstance(content, bytes):
                try:
                    content = content.decode("utf-8")
                except (UnicodeDecodeError, AttributeError):
                    pass
            combined["value"] = content
            combined["__input_content__"] = content
            return combined

        if not inputs_def:
            return target.get_content()

        input_name = list(inputs_def.keys())[0]
        input_type = inputs_def[input_name]

        if input_name == "context":
            return ctx

        raw = target.get_content()
        converters = {
            "float": lambda: float(raw.decode("utf-8")),
            "int": lambda: int(raw.decode("utf-8")),
            "str": lambda: raw.decode("utf-8"),
        }

        try:
            return converters.get(input_type, lambda: raw)()
        except Exception:
            return raw

    # ─── Module Logic ──────────────────────────────────────────────────────────

    def _execute_module_logic(
        self, uri: str, impl: dict, target: MCard, ctx: dict
    ) -> Any:
        """Execute logic imported from a Python module."""
        path = uri.replace(MODULE_URI_PREFIX, "")
        module_name, func_name = (
            path.split(":", 1)
            if ":" in path
            else (path, impl.get("entry_point", "logic"))
        )

        try:
            module = importlib.import_module(module_name)
            func = getattr(module, func_name)
            if not callable(func):
                return f"Error: {func_name} in {module_name} is not callable"

            arg = (
                target.get_content().decode("utf-8")
                if hasattr(target, "get_content")
                else target
            )
            return func(arg)
        except ImportError as e:
            return f"Error importing module {module_name}: {e}"
        except AttributeError:
            return f"Error: Function {func_name} not found in {module_name}"
        except Exception as e:
            return f"Error executing module logic: {e}"

    def validate_environment(self) -> bool:
        return True

    def get_runtime_status(self) -> dict[str, Any]:
        import sys

        return {
            "available": True,
            "version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
            "command": RUNTIME_CONFIG.get("python", {}).get("command", "python3"),
            "details": f"Python {sys.version}",
        }
