"""LLMHosts plugin system -- discover, load, and manage plugins.

Plugins are discovered from two sources:

1. **Directory-based** -- Python files (``*.py``) in each path listed in
   ``PluginConfig.plugin_dirs`` (default: ``~/.llmhosts/plugins/``).
   Each file must expose a top-level ``register(app, config)`` callable
   **or** a class that inherits :class:`~llmhost.plugins.base.LLMHostPlugin`.

2. **Entry-point-based** -- any installed package that advertises the
   ``llmhost.plugins`` entry-point group.  The entry-point value must
   resolve to a :class:`~llmhost.plugins.base.LLMHostPlugin` subclass.

Plugin errors are logged but **never** crash the server.
"""

from __future__ import annotations

import importlib
import importlib.util
import logging
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any

from pydantic import BaseModel

from llmhosts.config import PluginConfig
from llmhosts.plugins.base import (
    BackendPlugin,
    HookPlugin,
    LLMHostPlugin,
    MiddlewarePlugin,
    PluginType,
)

if TYPE_CHECKING:
    from fastapi import FastAPI

    from llmhosts.config import LLMHostsConfig

logger = logging.getLogger(__name__)

__all__ = [
    "BackendPlugin",
    "HookPlugin",
    "LLMHostPlugin",
    "MiddlewarePlugin",
    "PluginConfig",
    "PluginInfo",
    "PluginManager",
    "PluginType",
]


# ---------------------------------------------------------------------------
# Plugin info model
# ---------------------------------------------------------------------------


class PluginInfo(BaseModel):
    """Serialisable summary of a loaded plugin."""

    name: str
    version: str
    description: str
    author: str = ""
    plugin_type: str = "hook"


# ---------------------------------------------------------------------------
# Plugin manager
# ---------------------------------------------------------------------------


class PluginManager:
    """Discovers, loads, validates, and manages the lifecycle of plugins.

    Usage::

        mgr = PluginManager(plugin_config)
        mgr.discover()
        mgr.register_all(app, config)
        await mgr.startup()
        # ... app is running ...
        await mgr.shutdown()
    """

    def __init__(self, config: PluginConfig | None = None) -> None:
        self._config = config or PluginConfig()
        self._plugins: list[LLMHostPlugin] = []
        self._registered: bool = False

    # -- Properties ---------------------------------------------------------

    @property
    def plugins(self) -> list[LLMHostPlugin]:
        """Return a copy of the currently loaded plugin list."""
        return list(self._plugins)

    @property
    def plugin_count(self) -> int:
        """Number of loaded plugins."""
        return len(self._plugins)

    # -- Discovery ----------------------------------------------------------

    def discover(self) -> list[LLMHostPlugin]:
        """Discover plugins from directories and entry points.

        Returns the list of discovered (but not yet registered) plugins.
        Previously discovered plugins are cleared.
        """
        self._plugins.clear()
        self._registered = False

        if not self._config.enabled:
            logger.info("Plugin system disabled -- skipping discovery")
            return []

        self._discover_from_dirs()
        self._discover_from_entry_points()

        # Filter disabled plugins
        before = len(self._plugins)
        self._plugins = [p for p in self._plugins if p.name not in self._config.disabled_plugins]
        skipped = before - len(self._plugins)
        if skipped:
            logger.info("Skipped %d disabled plugin(s)", skipped)

        logger.info("Discovered %d plugin(s)", len(self._plugins))
        return list(self._plugins)

    def _discover_from_dirs(self) -> None:
        """Scan configured directories for plugin Python files."""
        for raw_dir in self._config.plugin_dirs:
            plugin_dir = Path(raw_dir).expanduser().resolve()
            if not plugin_dir.is_dir():
                logger.debug("Plugin directory does not exist: %s", plugin_dir)
                continue

            for py_file in sorted(plugin_dir.glob("*.py")):
                if py_file.name.startswith("_"):
                    continue
                try:
                    plugin = self._load_plugin_from_file(py_file)
                    if plugin is not None:
                        self._plugins.append(plugin)
                        logger.info("Loaded directory plugin: %s v%s from %s", plugin.name, plugin.version, py_file)
                except Exception:
                    logger.exception("Failed to load plugin from %s", py_file)

    def _load_plugin_from_file(self, path: Path) -> LLMHostPlugin | None:
        """Import a single Python file and extract a plugin instance.

        The file is expected to contain **either**:

        * A top-level ``register(app, config)`` callable (legacy/simple API)
          -- wrapped in a :class:`_FunctionPlugin` adapter.
        * A class attribute that is a subclass of :class:`LLMHostPlugin`.
        """
        module_name = f"llmhosts_plugin_{path.stem}"
        spec = importlib.util.spec_from_file_location(module_name, path)
        if spec is None or spec.loader is None:
            logger.warning("Cannot create module spec for %s", path)
            return None

        module = importlib.util.module_from_spec(spec)
        sys.modules[module_name] = module
        try:
            spec.loader.exec_module(module)  # type: ignore[union-attr]
        except Exception:
            sys.modules.pop(module_name, None)
            raise

        # Strategy 1: look for LLMHostPlugin subclasses
        for attr_name in dir(module):
            attr = getattr(module, attr_name, None)
            if (
                isinstance(attr, type)
                and issubclass(attr, LLMHostPlugin)
                and attr is not LLMHostPlugin
                and attr is not MiddlewarePlugin
                and attr is not BackendPlugin
                and attr is not HookPlugin
            ):
                try:
                    return attr()
                except Exception:
                    logger.exception("Failed to instantiate plugin class %s from %s", attr_name, path)

        # Strategy 2: look for a bare ``register`` function
        register_fn = getattr(module, "register", None)
        if callable(register_fn):
            return _FunctionPlugin(
                register_fn=register_fn,
                plugin_name=getattr(module, "PLUGIN_NAME", path.stem),
                plugin_version=getattr(module, "PLUGIN_VERSION", "0.0.0"),
                plugin_description=getattr(module, "PLUGIN_DESCRIPTION", f"Plugin from {path.name}"),
            )

        logger.warning("No LLMHostPlugin subclass or register() found in %s -- skipping", path)
        return None

    def _discover_from_entry_points(self) -> None:
        """Scan installed packages for ``llmhost.plugins`` entry points."""
        try:
            from importlib.metadata import entry_points

            eps = entry_points()
            # Python 3.12+ returns a SelectableGroups; older returns dict
            group_eps: list[Any] = list(
                eps.get("llmhosts.plugins", []) if isinstance(eps, dict) else eps.select(group="llmhosts.plugins")  # type: ignore[union-attr]
            )

            for ep in group_eps:
                try:
                    plugin_cls = ep.load()
                    if isinstance(plugin_cls, type) and issubclass(plugin_cls, LLMHostPlugin):
                        instance = plugin_cls()
                        self._plugins.append(instance)
                        logger.info("Loaded entry-point plugin: %s v%s", instance.name, instance.version)
                    else:
                        logger.warning(
                            "Entry point %s did not resolve to an LLMHostPlugin subclass -- skipping",
                            ep.name,
                        )
                except Exception:
                    logger.exception("Failed to load entry-point plugin: %s", ep.name)
        except Exception:
            logger.exception("Failed to scan entry points")

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

    def register_all(self, app: FastAPI, config: LLMHostsConfig) -> None:
        """Call ``register(app, config)`` on every discovered plugin."""
        for plugin in self._plugins:
            try:
                plugin.register(app, config)
                logger.info("Registered plugin: %s", plugin.name)
            except Exception:
                logger.exception("Plugin %s failed during registration", plugin.name)
        self._registered = True

    # -- Lifecycle ----------------------------------------------------------

    async def startup(self) -> None:
        """Invoke ``on_startup()`` on all registered plugins."""
        for plugin in self._plugins:
            try:
                await plugin.on_startup()
            except Exception:
                logger.exception("Plugin %s raised during startup", plugin.name)

    async def shutdown(self) -> None:
        """Invoke ``on_shutdown()`` on all registered plugins."""
        for plugin in self._plugins:
            try:
                await plugin.on_shutdown()
            except Exception:
                logger.exception("Plugin %s raised during shutdown", plugin.name)

    # -- Introspection ------------------------------------------------------

    def list_plugins(self) -> list[PluginInfo]:
        """Return serialisable summaries of all loaded plugins."""
        return [
            PluginInfo(
                name=p.name,
                version=p.version,
                description=p.description,
                plugin_type=p.plugin_type.value,
            )
            for p in self._plugins
        ]

    def get_plugin(self, name: str) -> LLMHostPlugin | None:
        """Look up a loaded plugin by name (case-sensitive)."""
        for p in self._plugins:
            if p.name == name:
                return p
        return None


# ---------------------------------------------------------------------------
# Internal adapter for simple ``register(app, config)`` functions
# ---------------------------------------------------------------------------


class _FunctionPlugin(LLMHostPlugin):
    """Wraps a bare ``register(app, config)`` callable as an LLMHostPlugin."""

    def __init__(
        self,
        register_fn: Any,
        plugin_name: str = "unnamed",
        plugin_version: str = "0.0.0",
        plugin_description: str = "",
    ) -> None:
        self._register_fn = register_fn
        self._name = plugin_name
        self._version = plugin_version
        self._description = plugin_description

    @property
    def name(self) -> str:
        return self._name

    @property
    def version(self) -> str:
        return self._version

    @property
    def description(self) -> str:
        return self._description

    def register(self, app: Any, config: Any) -> None:
        self._register_fn(app, config)
