"""
Base classes for runtime executors.

This module provides the core abstractions for runtime execution:
- RuntimeType: Enum of supported runtimes
- BoundaryType: Enum for intrinsic/extrinsic execution
- RuntimeExecutor: Abstract base class for all runtimes
- SubprocessRuntime: Base class for runtimes that execute via subprocess
"""

import logging
import subprocess
from abc import ABC, abstractmethod
from enum import Enum
from typing import Any

from mcard import MCard

# ─────────────────────────────────────────────────────────────────────────────
# Configuration Constants
# ─────────────────────────────────────────────────────────────────────────────

try:
    from mcard.config.settings import settings as _settings

    DEFAULT_TIMEOUT = _settings.ptr.default_timeout
    LEAN_TIMEOUT = _settings.ptr.lean_timeout
    JULIA_TIMEOUT = _settings.ptr.julia_timeout
    R_TIMEOUT = _settings.ptr.r_timeout
    RUNTIME_CONFIG = _settings.ptr.runtime_config
except ImportError:
    DEFAULT_TIMEOUT = 5
    LEAN_TIMEOUT = 30
    JULIA_TIMEOUT = 15
    R_TIMEOUT = 10
    RUNTIME_CONFIG = {
        "python": {"command": "python3", "version_flag": "--version"},
        "javascript": {
            "command": ["npx", "tsx"],
            "version_flag": "--version",
            "eval_flag": "--eval",
        },
        "lean": {"command": "lean", "version_flag": "--version", "run_flag": "--run"},
        "r": {"command": "Rscript", "version_flag": "--version"},
        "julia": {"command": "julia", "version_flag": "--version"},
        "rust": {"command": None},
        "c": {"command": None},
        "wasm": {"command": None},
    }


# ─────────────────────────────────────────────────────────────────────────────
# Runtime Types (shared source of truth: schema/runtime_types.json)
# ─────────────────────────────────────────────────────────────────────────────


def _load_shared_runtime_types() -> tuple[list[str], dict[str, str]]:
    """Load runtime type IDs and aliases from the shared JSON registry.

    Returns (runtime_ids, aliases) tuple.
    Falls back to empty lists if file is not found.
    """
    import json
    from pathlib import Path

    module_dir = Path(__file__).parent
    candidates = [
        module_dir.parent.parent.parent.parent / "schema" / "runtime_types.json",
        Path.cwd() / "schema" / "runtime_types.json",
    ]
    for candidate in candidates:
        if candidate.exists():
            try:
                with open(candidate, encoding="utf-8") as f:
                    data = json.load(f)
                ids = [r["id"] for r in data.get("runtimes", [])]
                aliases = data.get("aliases", {})
                return ids, aliases
            except Exception as e:
                logging.warning("Failed to load runtime_types.json: %s", e)
    return [], {}


# Load shared data at import time
_SHARED_RUNTIME_IDS, SHARED_ALIASES = _load_shared_runtime_types()


class RuntimeType(Enum):
    """Supported runtime types.

    IDs are aligned with schema/runtime_types.json (single source of truth).
    """

    PYTHON = "python"
    JAVASCRIPT = "javascript"
    RUST = "rust"
    C = "c"
    WASM = "wasm"
    LEAN = "lean"
    R = "r"
    JULIA = "julia"
    LLM = "llm"
    LAMBDA = "lambda"
    NETWORK = "network"
    LOADER = "loader"
    COLLECTION_LOADER = "collection_loader"


def resolve_runtime_alias(name: str) -> str:
    """Resolve a runtime name through shared aliases.

    E.g., 'node' -> 'javascript', 'py' -> 'python', 'js' -> 'javascript'.
    Returns the canonical runtime ID.
    """
    return SHARED_ALIASES.get(name.lower(), name.lower())


class BoundaryType(Enum):
    """CLM execution boundary type.

    Determines how a CLM is executed relative to the caller:

    INTRINSIC: Process runs within the same execution context (same process).
               Best for lightweight, trusted operations. No serialization overhead.

    EXTRINSIC: Process runs in a separate process/container/network.
               Required for untrusted code, language isolation, or distributed execution.
               Requires serialization/deserialization of inputs and outputs.
    """

    INTRINSIC = "intrinsic"
    EXTRINSIC = "extrinsic"


# ─────────────────────────────────────────────────────────────────────────────
# Base Executor Classes
# ─────────────────────────────────────────────────────────────────────────────


class RuntimeExecutor(ABC):
    """Abstract base class for runtime executors.

    Each executor manages a specific runtime environment (Python, JavaScript, etc.)
    and can operate in either intrinsic or extrinsic boundary mode.
    """

    # Default boundary type for this runtime
    boundary_type: BoundaryType = BoundaryType.INTRINSIC

    def __init__(self):
        self.logger = logging.getLogger(self.__class__.__name__)

    @abstractmethod
    def execute(
        self, concrete_impl: dict[str, Any], target: MCard, context: dict[str, Any]
    ) -> Any:
        """Execute the operation in the runtime environment."""
        pass

    @abstractmethod
    def validate_environment(self) -> bool:
        """Check if runtime environment is available."""
        pass

    def get_boundary_type(self) -> BoundaryType:
        """Get the boundary type for this runtime.

        Override in subclasses to provide dynamic boundary determination.
        """
        return self.boundary_type

    def is_intrinsic(self) -> bool:
        """Check if this runtime operates within the same process."""
        return self.get_boundary_type() == BoundaryType.INTRINSIC

    def is_extrinsic(self) -> bool:
        """Check if this runtime operates in a separate process."""
        return self.get_boundary_type() == BoundaryType.EXTRINSIC

    def get_runtime_status(self) -> dict[str, Any]:
        """Get detailed status information about this runtime."""
        return {
            "available": self.validate_environment(),
            "version": None,
            "command": self.__class__.__name__.replace("Runtime", "").lower(),
            "details": "",
        }


class SubprocessRuntime(RuntimeExecutor):
    """
    Base class for runtimes that execute via subprocess.
    Reduces duplication across Rust/C/Lean/R/Julia.

    Note: Subprocess runtimes are EXTRINSIC - they run in separate processes.
    """

    # Subprocess runtimes are extrinsic by default
    boundary_type: BoundaryType = BoundaryType.EXTRINSIC

    runtime_name: str = ""
    command: str | None = None
    version_flag: str = "--version"
    timeout: int = DEFAULT_TIMEOUT

    def _run_subprocess(
        self,
        cmd: list[str],
        target: MCard,
        context: dict,
        timeout: int | None = None,
    ) -> str:
        """Execute subprocess and return output or error string."""
        try:
            result = subprocess.run(
                cmd,
                input=target.get_content(),
                capture_output=True,
                timeout=timeout or self.timeout,
            )
            if result.returncode != 0:
                return f"Error: {self.runtime_name} execution failed: {result.stderr.decode()}"
            return result.stdout.decode("utf-8")
        except subprocess.TimeoutExpired:
            return f"Error: {self.runtime_name} execution timed out"
        except (OSError, subprocess.SubprocessError) as e:
            return f"Error executing {self.runtime_name}: {e}"

    def _check_command(self, cmd: Any, flag: str, timeout: int = 10) -> bool:
        """Check if a command is available."""
        try:
            if isinstance(cmd, list):
                args = cmd + [flag]
            else:
                args = [cmd, flag]

            result = subprocess.run(args, capture_output=True, timeout=timeout)
            if result.returncode != 0:
                self.logger.debug(
                    f"Command check failed for {args}: {result.stderr.decode()}"
                )
                return False
            return True
        except (OSError, subprocess.SubprocessError) as e:
            self.logger.debug(f"Exception checking command {cmd}: {e}")
            return False

    def validate_environment(self) -> bool:
        if not self.command:
            return True  # No command needed (e.g., compiled binaries)
        return self._check_command(self.command, self.version_flag)
