"""Config-driven custom sandbox strategy registry.

Provides :class:`SandboxStrategyRegistry` for registering, validating,
and resolving custom sandbox strategies.  Strategies are registered
either programmatically or via configuration keys::

    sandbox.custom_strategies.<name>.module = "my_package.my_module"
    sandbox.custom_strategies.<name>.class  = "MySandboxClass"

All custom strategies are validated against the
:class:`SandboxStrategyProtocol` at registration time using
``@runtime_checkable`` Protocol checks.

Thread-safe via ``threading.RLock``.

Based on issue #586.
"""

from __future__ import annotations

import logging
import threading
from typing import Any

from cleveragents.infrastructure.plugins.exceptions import (
    PluginLoadError,
    ProtocolMismatchError,
)
from cleveragents.infrastructure.plugins.loader import PluginLoader

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Custom strategy configuration model
# ---------------------------------------------------------------------------


class CustomStrategyConfig:
    """Configuration for a single custom sandbox strategy.

    Attributes:
        name: Strategy name (used as the key in lookups).
        module: Python module path containing the strategy class.
        class_name: Name of the class within the module.
        options: Optional strategy-specific configuration.
    """

    __slots__ = ("class_name", "module", "name", "options")

    def __init__(
        self,
        name: str,
        module: str,
        class_name: str,
        options: dict[str, Any] | None = None,
    ) -> None:
        if not name:
            raise ValueError("Strategy name cannot be empty")
        if not module:
            raise ValueError("Strategy module cannot be empty")
        if not class_name:
            raise ValueError("Strategy class_name cannot be empty")

        self.name = name
        self.module = module
        self.class_name = class_name
        self.options = options or {}


# ---------------------------------------------------------------------------
# SandboxStrategyRegistry
# ---------------------------------------------------------------------------


class SandboxStrategyRegistry:
    """Registry for custom sandbox strategies with Protocol validation.

    Maintains a thread-safe mapping of strategy names to their
    implementing classes.  Strategies are validated against the
    :class:`SandboxStrategyProtocol` at registration time.

    Built-in strategies (``none``, ``git_worktree``, ``copy_on_write``,
    ``transaction_rollback``) are not managed by this registry; they
    are handled by :class:`SandboxFactory`.

    Usage::

        registry = SandboxStrategyRegistry()
        registry.register(
            "my_strategy",
            "cleveragents.my_module",
            "MySandbox",
        )
        cls = registry.get("my_strategy")

    Args:
        loader: Optional :class:`PluginLoader` for dynamic imports.
        allowed_prefixes: Module prefix allowlist for the loader.
    """

    def __init__(
        self,
        *,
        loader: PluginLoader | None = None,
        allowed_prefixes: tuple[str, ...] | None = None,
    ) -> None:
        self._lock = threading.RLock()
        self._loader = loader or PluginLoader(allowed_prefixes=allowed_prefixes)
        self._strategies: dict[str, type[Any]] = {}

    # ------------------------------------------------------------------
    # Registration
    # ------------------------------------------------------------------

    def register(
        self,
        name: str,
        module_path: str,
        class_name: str,
    ) -> type[Any]:
        """Register a custom sandbox strategy.

        Loads the class via the plugin loader, validates it against
        :class:`SandboxStrategyProtocol`, and stores it.

        Args:
            name: Strategy name for lookup.
            module_path: Fully-qualified module path.
            class_name: Class name within the module.

        Returns:
            The loaded and validated class.

        Raises:
            ValueError: If *name* is empty.
            PluginLoadError: If the class cannot be imported.
            ProtocolMismatchError: If the class does not satisfy
                the SandboxStrategyProtocol.
        """
        if not name:
            raise ValueError("Strategy name cannot be empty")

        cls = self._loader.load_class(module_path, class_name)
        self._validate_protocol(cls)

        with self._lock:
            self._strategies[name] = cls
            logger.info(
                "Registered custom sandbox strategy: name=%s class=%s.%s",
                name,
                module_path,
                class_name,
            )

        return cls

    def register_from_config(self, config: CustomStrategyConfig) -> type[Any]:
        """Register a strategy from a :class:`CustomStrategyConfig`.

        Args:
            config: Configuration describing the strategy.

        Returns:
            The loaded and validated class.
        """
        return self.register(config.name, config.module, config.class_name)

    def register_all_from_config(
        self,
        configs: dict[str, dict[str, Any]],
    ) -> list[str]:
        """Register multiple strategies from a config dictionary.

        The dictionary maps strategy names to dicts with ``module``
        and ``class`` keys::

            {
                "my_strategy": {
                    "module": "my_package.module",
                    "class": "MyStrategy",
                },
            }

        Args:
            configs: Strategy configuration dictionary.

        Returns:
            List of successfully registered strategy names.
        """
        registered: list[str] = []
        for name, cfg in configs.items():
            module = cfg.get("module", "")
            class_name = cfg.get("class", "")
            if not module or not class_name:
                logger.warning(
                    "Skipping custom strategy '%s': missing module or class",
                    name,
                )
                continue

            try:
                self.register(name, module, class_name)
                registered.append(name)
            except (PluginLoadError, ProtocolMismatchError, ValueError) as exc:
                logger.error(
                    "Failed to register custom strategy '%s': %s",
                    name,
                    exc,
                )

        return registered

    # ------------------------------------------------------------------
    # Lookup
    # ------------------------------------------------------------------

    def get(self, name: str) -> type[Any] | None:
        """Look up a registered custom strategy class.

        Args:
            name: Strategy name.

        Returns:
            The class, or ``None`` if not registered.
        """
        with self._lock:
            return self._strategies.get(name)

    def has(self, name: str) -> bool:
        """Check whether a strategy is registered.

        Args:
            name: Strategy name.

        Returns:
            ``True`` if the strategy is registered.
        """
        with self._lock:
            return name in self._strategies

    def list_strategies(self) -> list[str]:
        """Return names of all registered custom strategies.

        Returns:
            Sorted list of strategy names.
        """
        with self._lock:
            return sorted(self._strategies.keys())

    def clear(self) -> None:
        """Remove all registered custom strategies."""
        with self._lock:
            self._strategies.clear()
            logger.debug("Custom sandbox strategy registry cleared")

    # ------------------------------------------------------------------
    # Validation
    # ------------------------------------------------------------------

    @staticmethod
    def _validate_protocol(klass: type[Any]) -> None:
        """Validate that a class satisfies :class:`SandboxStrategyProtocol`.

        Uses structural subtyping: checks that all 9 required methods
        are present as callable attributes on the class.

        Args:
            klass: The class to validate.

        Raises:
            ProtocolMismatchError: If the class is missing required methods.
        """
        required_methods = (
            "create",
            "read",
            "write",
            "diff",
            "commit",
            "rollback",
            "checkpoint",
            "restore_checkpoint",
            "cleanup",
        )

        missing = [
            method
            for method in required_methods
            if not callable(getattr(klass, method, None))
        ]

        if missing:
            msg = (
                f"Class '{klass.__name__}' does not satisfy "
                f"SandboxStrategyProtocol. Missing methods: "
                f"{', '.join(missing)}"
            )
            raise ProtocolMismatchError(msg)
