"""Plugin lifecycle manager.

Provides :class:`PluginManager` for managing plugin discovery,
activation, deactivation, and config-driven registration.  Thread-safe
via ``threading.RLock``.

Based on issue #585.
"""

from __future__ import annotations

import threading
from typing import Any

import structlog

from cleveragents.infrastructure.plugins.exceptions import (
    PluginError,
    PluginLoadError,
    PluginNotFoundError,
    ProtocolMismatchError,
)
from cleveragents.infrastructure.plugins.loader import PluginLoader
from cleveragents.infrastructure.plugins.types import (
    ExtensionPoint,
    PluginDescriptor,
    PluginState,
)

logger = structlog.get_logger(__name__)


class PluginManager:
    """Manages the plugin lifecycle: discover, activate, deactivate.

    The manager is the single entry point for plugin operations.  It
    maintains a thread-safe registry of :class:`PluginDescriptor`
    entries and their resolved class objects.

    Config-driven registration reads ``custom_module``,
    ``custom_class``, and ``custom_options`` from configuration dicts
    to register plugins dynamically.

    Thread-safety: all public methods are guarded by ``threading.RLock``.

    Example::

        manager = PluginManager()
        descriptor = PluginDescriptor(
            name="my-plugin",
            module_path="cleveragents.my_module",
            class_name="MyPlugin",
        )
        manager.register_plugin(descriptor)
        manager.activate_plugin("my-plugin")
        info = manager.get_plugin("my-plugin")
        manager.deactivate_plugin("my-plugin")

    Args:
        loader: Optional :class:`PluginLoader` instance.  If ``None``,
            a default loader is created.
        allowed_prefixes: Module prefix allowlist for the loader.
            Ignored if *loader* is provided.
    """

    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._plugins: dict[str, PluginDescriptor] = {}
        self._classes: dict[str, type[Any]] = {}
        self._instances: dict[str, object] = {}
        self._extension_points: dict[str, ExtensionPoint] = {}
        self._logger = logger.bind(service="plugin_manager")

    # ------------------------------------------------------------------
    # Extension point registration
    # ------------------------------------------------------------------

    def register_extension_point(self, extension_point: ExtensionPoint) -> None:
        """Register an extension point the manager knows about.

        Args:
            extension_point: The extension point metadata to register.
        """
        with self._lock:
            self._extension_points[extension_point.name] = extension_point
            self._logger.debug(
                "plugin_manager.extension_point_registered",
                name=extension_point.name,
            )

    def list_extension_points(self) -> list[ExtensionPoint]:
        """Return all registered extension points."""
        with self._lock:
            return list(self._extension_points.values())

    # ------------------------------------------------------------------
    # Plugin registration
    # ------------------------------------------------------------------

    def register_plugin(self, descriptor: PluginDescriptor) -> None:
        """Register a plugin descriptor in the manager.

        The plugin is placed in ``DISCOVERED`` state.  No import or
        validation is performed until :meth:`activate_plugin` is called.

        Args:
            descriptor: The plugin descriptor to register.

        Raises:
            PluginError: If a plugin with the same name is already
                registered.
        """
        with self._lock:
            if descriptor.name in self._plugins:
                msg = f"Plugin '{descriptor.name}' is already registered"
                raise PluginError(msg)

            descriptor.state = PluginState.DISCOVERED
            self._plugins[descriptor.name] = descriptor
            self._logger.info(
                "plugin_manager.registered",
                name=descriptor.name,
                module_path=descriptor.module_path,
                class_name=descriptor.class_name,
            )

    # ------------------------------------------------------------------
    # Plugin queries
    # ------------------------------------------------------------------

    def get_plugin(self, name: str) -> PluginDescriptor:
        """Return the descriptor for a registered plugin.

        Args:
            name: Plugin name.

        Returns:
            The :class:`PluginDescriptor` for the named plugin.

        Raises:
            PluginNotFoundError: If the plugin is not registered.
        """
        with self._lock:
            if name not in self._plugins:
                msg = (
                    f"Plugin '{name}' not found in registry. "
                    f"Available: {sorted(self._plugins)}"
                )
                raise PluginNotFoundError(msg)
            return self._plugins[name]

    def list_plugins(self) -> list[PluginDescriptor]:
        """Return all registered plugin descriptors."""
        with self._lock:
            return list(self._plugins.values())

    def get_plugin_class(self, name: str) -> type[Any] | None:
        """Return the loaded class for a plugin, or ``None``.

        Args:
            name: Plugin name.

        Returns:
            The loaded class, or ``None`` if not yet activated.
        """
        with self._lock:
            return self._classes.get(name)

    def get_plugin_instance(self, name: str) -> object | None:
        """Return the instantiated plugin object, or ``None``.

        Args:
            name: Plugin name.

        Returns:
            The plugin instance, or ``None`` if not yet activated.
        """
        with self._lock:
            return self._instances.get(name)

    # ------------------------------------------------------------------
    # Lifecycle: discover
    # ------------------------------------------------------------------

    def discover(
        self,
        group: str = "cleveragents.plugins",
    ) -> list[PluginDescriptor]:
        """Discover plugins from entry points and register them.

        Args:
            group: Entry-point group to scan.

        Returns:
            List of newly discovered :class:`PluginDescriptor` instances.
        """
        with self._lock:
            descriptors = self._loader.load_from_entry_points(group=group)
            newly_registered: list[PluginDescriptor] = []

            for desc in descriptors:
                if desc.name not in self._plugins:
                    self._plugins[desc.name] = desc
                    newly_registered.append(desc)
                    self._logger.info(
                        "plugin_manager.discovered",
                        name=desc.name,
                    )

            return newly_registered

    # ------------------------------------------------------------------
    # Lifecycle: activate
    # ------------------------------------------------------------------

    def activate_plugin(self, name: str) -> None:
        """Activate a registered plugin by loading its class.

        Imports the module, loads the class, creates an instance, and
        transitions the plugin to ``ACTIVATED`` state.

        Args:
            name: Plugin name.

        Raises:
            PluginNotFoundError: If the plugin is not registered.
            PluginLoadError: If the class cannot be imported.
            PluginError: If the plugin is already activated.
        """
        with self._lock:
            descriptor = self.get_plugin(name)

            if descriptor.state == PluginState.ACTIVATED:
                msg = f"Plugin '{name}' is already activated"
                raise PluginError(msg)

            if not descriptor.module_path or not descriptor.class_name:
                msg = (
                    f"Plugin '{name}' has no module_path or class_name. "
                    f"Cannot activate."
                )
                raise PluginLoadError(msg)

            try:
                cls = self._loader.load_class(
                    descriptor.module_path,
                    descriptor.class_name,
                )
                instance = cls()

                self._classes[name] = cls
                self._instances[name] = instance
                descriptor.state = PluginState.ACTIVATED

                self._logger.info(
                    "plugin_manager.activated",
                    name=name,
                    module_path=descriptor.module_path,
                    class_name=descriptor.class_name,
                )
            except (PluginLoadError, ProtocolMismatchError):
                descriptor.state = PluginState.ERRORED
                raise
            except Exception as exc:
                descriptor.state = PluginState.ERRORED
                msg = f"Failed to activate plugin '{name}': {exc}"
                raise PluginError(msg) from exc

    # ------------------------------------------------------------------
    # Lifecycle: deactivate
    # ------------------------------------------------------------------

    def deactivate_plugin(self, name: str) -> None:
        """Deactivate a plugin, removing its class and instance.

        Args:
            name: Plugin name.

        Raises:
            PluginNotFoundError: If the plugin is not registered.
            PluginError: If the plugin is not in an activatable state.
        """
        with self._lock:
            descriptor = self.get_plugin(name)

            if descriptor.state not in (
                PluginState.ACTIVATED,
                PluginState.ERRORED,
            ):
                msg = (
                    f"Plugin '{name}' is in state '{descriptor.state}' "
                    f"and cannot be deactivated"
                )
                raise PluginError(msg)

            self._classes.pop(name, None)
            self._instances.pop(name, None)
            descriptor.state = PluginState.DEACTIVATED

            self._logger.info(
                "plugin_manager.deactivated",
                name=name,
            )

    # ------------------------------------------------------------------
    # Config-driven registration
    # ------------------------------------------------------------------

    def register_from_config(
        self,
        config: dict[str, Any],
    ) -> PluginDescriptor | None:
        """Register a plugin from a configuration dictionary.

        Reads ``custom_module``, ``custom_class``, and optionally
        ``custom_options`` from the config dict.

        Args:
            config: Configuration dictionary with keys:
                - ``custom_module`` (str): Module path.
                - ``custom_class`` (str): Class name.
                - ``custom_options`` (dict, optional): Extra options.
                - ``name`` (str, optional): Plugin name.

        Returns:
            The registered :class:`PluginDescriptor`, or ``None`` if
            the config doesn't contain plugin keys.
        """
        custom_module = config.get("custom_module", "")
        custom_class = config.get("custom_class", "")

        if not custom_module or not custom_class:
            return None

        name = config.get("name", f"{custom_module}:{custom_class}")
        custom_options = config.get("custom_options", {})

        descriptor = PluginDescriptor(
            name=name,
            module_path=custom_module,
            class_name=custom_class,
            description=str(custom_options.get("description", "")),
            version=str(custom_options.get("version", "0.0.0")),
        )

        with self._lock:
            if name in self._plugins:
                self._logger.debug(
                    "plugin_manager.config_already_registered",
                    name=name,
                )
                return self._plugins[name]

            self._plugins[name] = descriptor
            self._logger.info(
                "plugin_manager.registered_from_config",
                name=name,
                module_path=custom_module,
                class_name=custom_class,
            )
            return descriptor

    # ------------------------------------------------------------------
    # Batch config registration
    # ------------------------------------------------------------------

    def register_all_from_config(
        self,
        configs: list[dict[str, Any]],
    ) -> list[PluginDescriptor]:
        """Register multiple plugins from a list of config dicts.

        Args:
            configs: List of configuration dictionaries.

        Returns:
            List of successfully registered descriptors.
        """
        registered: list[PluginDescriptor] = []
        for cfg in configs:
            result = self.register_from_config(cfg)
            if result is not None:
                registered.append(result)
        return registered

    # ------------------------------------------------------------------
    # Cleanup
    # ------------------------------------------------------------------

    def clear(self) -> None:
        """Remove all plugins and reset state."""
        with self._lock:
            self._plugins.clear()
            self._classes.clear()
            self._instances.clear()
            self._extension_points.clear()
            self._logger.debug("plugin_manager.cleared")
