"""Plugin discovery and registry for the EZ-Input backend plugin system."""

from __future__ import annotations

import importlib.util
import inspect
import logging
import sys
from pathlib import Path
from types import ModuleType
from typing import Any, Dict, List

try:
    from fastapi import FastAPI
except ImportError:  # pragma: no cover
    FastAPI = Any  # type: ignore[misc,assignment]

from plugin import Plugin

logger = logging.getLogger(__name__)


class PluginRegistry:
    """Discovers and registers plugins from a directory.

    The registry loads every ``*.py`` file in the configured ``plugins/``
    directory, finds concrete subclasses of :class:`Plugin`, instantiates them,
    resolves dependency order, and exposes them for registration with a FastAPI app.
    """

    def __init__(self, plugin_dir: Path | str | None = None) -> None:
        self.plugin_dir = Path(plugin_dir) if plugin_dir else Path(__file__).parent / "plugins"
        self._plugins: Dict[str, Plugin] = {}
        self._plugin_list: List[Plugin] | None = None
        self._context: Dict[str, Any] = {}
        self._loaded = False

    @property
    def plugins(self) -> Dict[str, Plugin]:
        """Mapping of plugin name to plugin instance."""
        return dict(self._plugins)

    def _load_module(self, path: Path) -> ModuleType | None:
        module_name = f"ez_input_plugins_backend_{path.stem}"
        try:
            spec = importlib.util.spec_from_file_location(module_name, path)
            if spec is None or spec.loader is None:
                return None
            module = importlib.util.module_from_spec(spec)
            sys.modules[module_name] = module
            spec.loader.exec_module(module)
            return module
        except Exception as exc:  # pragma: no cover - plugin import failures are logged but not fatal
            logger.warning("Failed to load plugin module %s: %s", path, exc)
            return None

    def discover(self) -> List[Plugin]:
        """Discover and instantiate plugin classes from ``plugin_dir``.

        Returns:
            A list of plugin instances. The list is cached; subsequent calls
            return the same instances unless ``refresh()`` is called.
        """
        if self._plugin_list is not None:
            return self._plugin_list

        self.load()
        self._plugin_list = list(self._plugins.values())
        return self._plugin_list

    def load(self) -> "PluginRegistry":
        """Scan ``plugin_dir`` and load all valid plugin instances."""
        if self._loaded:
            return self

        self._plugins = {}
        if not self.plugin_dir.exists():
            self._loaded = True
            return self

        discovered: List[Plugin] = []
        for file_path in sorted(self.plugin_dir.glob("*.py")):
            module = self._load_module(file_path)
            if module is None:
                continue

            for _name, obj in inspect.getmembers(module, inspect.isclass):
                if obj is Plugin or not issubclass(obj, Plugin):
                    continue
                try:
                    instance = obj().bind(self._context)
                    discovered.append(instance)
                except Exception as exc:  # pragma: no cover
                    logger.warning("Failed to instantiate plugin %s: %s", obj, exc)
                    continue

        ordered = self._resolve_dependencies(discovered)
        for plugin in ordered:
            self._plugins[plugin.name] = plugin

        self._loaded = True
        return self

    def refresh(self) -> List[Plugin]:
        """Clear the plugin cache and rediscover plugins."""
        self._loaded = False
        self._plugins = {}
        self._plugin_list = None
        return self.load().discover()

    def set_context(self, context: Dict[str, Any]) -> "PluginRegistry":
        """Set the shared context passed to every discovered plugin."""
        self._context = context
        for plugin in self._plugins.values():
            plugin.bind(context)
        return self

    def _resolve_dependencies(self, plugins: List[Plugin]) -> List[Plugin]:
        """Topologically sort plugins so dependencies are activated first."""
        by_name = {p.name: p for p in plugins}
        resolved: List[Plugin] = []
        visiting: set[str] = set()
        visited: set[str] = set()

        def visit(plugin: Plugin) -> None:
            if plugin.name in visited:
                return
            if plugin.name in visiting:
                raise ValueError(f"Circular dependency detected involving {plugin.name}")

            visiting.add(plugin.name)
            for dep in plugin.dependencies:
                dep_plugin = by_name.get(dep)
                if dep_plugin is not None:
                    visit(dep_plugin)
            visiting.remove(plugin.name)
            visited.add(plugin.name)
            resolved.append(plugin)

        for plugin in plugins:
            visit(plugin)

        return resolved

    def register_all(self, app: FastAPI) -> List[Plugin]:
        """Register every discovered plugin with the supplied FastAPI app.

        Args:
            app: The FastAPI application to extend.

        Returns:
            The list of registered plugin instances.
        """
        plugins = self.discover()
        for plugin in plugins:
            plugin.register(app)
        for plugin in plugins:
            plugin.activate()
        return plugins

    def health(self) -> Dict[str, Any]:
        """Aggregate health metadata from all discovered plugins."""
        plugins = self.discover()
        return {
            "registry": "ok",
            "plugin_count": len(plugins),
            "plugins": [plugin.health() for plugin in plugins],
        }
