"""
RBSP Plugin Architecture.

Provides base classes for custom resolvers, indexers, and rankers, along
with a discovery mechanism that loads plugins from ``.rbsp/plugins/``.

Hook points in the RBSP pipeline:
    - ``pre_index``   : called before a file is indexed
    - ``post_index``  : called after a file is indexed
    - ``pre_query``   : called before query execution
    - ``post_query``  : called after results are gathered
    - ``custom_ranker``: called to re-rank result candidates
"""

from __future__ import annotations

import importlib.util
import os
import sys
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Sequence, Type


# ---------------------------------------------------------------------------
# Result type used across plugins (lightweight, no external deps)
# ---------------------------------------------------------------------------

@dataclass
class PluginResult:
    """A single search/resolve result as seen by the plugin layer."""

    path: str
    score: float
    snippet: str = ""
    line: int = 0
    metadata: Dict[str, Any] = field(default_factory=dict)


# ---------------------------------------------------------------------------
# Abstract base classes for plugins
# ---------------------------------------------------------------------------

class ResolverPlugin(ABC):
    """Base class for custom resolver plugins.

    A resolver takes a query string and returns a list of results.
    """

    name: str = "unnamed_resolver"
    priority: int = 100  # lower = runs first

    @abstractmethod
    def resolve(self, query: str, scope: str, **kwargs: Any) -> List[PluginResult]:
        """Execute the resolver against *query* within *scope*.

        Returns a list of :class:`PluginResult` instances.
        """

    def can_handle(self, query: str) -> bool:
        """Return True if this resolver should handle *query*.

        Defaults to True (resolver is always eligible).
        """
        return True


class IndexerPlugin(ABC):
    """Base class for custom indexer plugins.

    An indexer processes a file during the indexing phase and may augment
    the index with additional information.
    """

    name: str = "unnamed_indexer"
    extensions: List[str] = []  # file extensions this indexer handles

    @abstractmethod
    def index_file(self, path: str, content: str) -> Dict[str, Any]:
        """Process a file and return metadata to merge into the index entry."""

    def can_handle(self, path: str) -> bool:
        """Return True if this indexer should process *path*."""
        if not self.extensions:
            return True
        return any(path.endswith(ext) for ext in self.extensions)


class RankerPlugin(ABC):
    """Base class for custom ranker plugins.

    A ranker re-scores and reorders a set of candidate results.
    """

    name: str = "unnamed_ranker"

    @abstractmethod
    def rank(
        self,
        query: str,
        candidates: List[PluginResult],
    ) -> List[PluginResult]:
        """Re-rank *candidates* for the given *query*. Return the reordered list."""


# ---------------------------------------------------------------------------
# Hook system
# ---------------------------------------------------------------------------

HookCallback = Callable[..., Any]

_VALID_HOOKS = frozenset({
    "pre_index",
    "post_index",
    "pre_query",
    "post_query",
    "custom_ranker",
})


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

class PluginManager:
    """Discovers, loads, and manages RBSP plugins.

    Plugins are Python files placed in the ``<root>/.rbsp/plugins/``
    directory.  Each plugin file may define:

    * Subclasses of :class:`ResolverPlugin`, :class:`IndexerPlugin`, or
      :class:`RankerPlugin`.
    * A module-level ``register(manager)`` function that is called after
      the module is imported, allowing imperative plugin registration.
    """

    def __init__(self) -> None:
        self._resolvers: List[ResolverPlugin] = []
        self._indexers: List[IndexerPlugin] = []
        self._rankers: List[RankerPlugin] = []
        self._hooks: Dict[str, List[HookCallback]] = {h: [] for h in _VALID_HOOKS}
        self._loaded_paths: List[str] = []

    # --- Registration API --------------------------------------------------

    def register_resolver(self, resolver: ResolverPlugin) -> None:
        """Register a custom resolver plugin."""
        self._resolvers.append(resolver)
        self._resolvers.sort(key=lambda r: r.priority)

    def register_indexer(self, indexer: IndexerPlugin) -> None:
        """Register a custom indexer plugin."""
        self._indexers.append(indexer)

    def register_ranker(self, ranker: RankerPlugin) -> None:
        """Register a custom ranker plugin."""
        self._rankers.append(ranker)

    def register_hook(self, hook_name: str, callback: HookCallback) -> None:
        """Register a callback for one of the supported hook points.

        Raises ``ValueError`` if *hook_name* is not a recognized hook.
        """
        if hook_name not in _VALID_HOOKS:
            raise ValueError(
                f"Unknown hook {hook_name!r}. Valid hooks: {sorted(_VALID_HOOKS)}"
            )
        self._hooks[hook_name].append(callback)

    # --- Accessors ---------------------------------------------------------

    @property
    def resolvers(self) -> List[ResolverPlugin]:
        """Return all registered resolver plugins (sorted by priority)."""
        return list(self._resolvers)

    @property
    def indexers(self) -> List[IndexerPlugin]:
        """Return all registered indexer plugins."""
        return list(self._indexers)

    @property
    def rankers(self) -> List[RankerPlugin]:
        """Return all registered ranker plugins."""
        return list(self._rankers)

    # --- Hook execution ----------------------------------------------------

    def run_hook(self, hook_name: str, **kwargs: Any) -> List[Any]:
        """Execute all callbacks registered for *hook_name*.

        Returns a list of return values (one per callback).
        """
        results: List[Any] = []
        for cb in self._hooks.get(hook_name, []):
            try:
                results.append(cb(**kwargs))
            except Exception:
                # Plugins must not crash the host.  Silently skip failures.
                pass
        return results

    # --- Discovery and loading ---------------------------------------------

    def discover(self, plugins_dir: str | Path) -> int:
        """Scan *plugins_dir* for ``.py`` files and load them as plugins.

        Returns the number of plugin files loaded.
        """
        plugins_path = Path(plugins_dir)
        if not plugins_path.is_dir():
            return 0

        count = 0
        for entry in sorted(plugins_path.iterdir()):
            if entry.suffix == ".py" and entry.is_file() and not entry.name.startswith("_"):
                if self._load_plugin_file(entry):
                    count += 1
        return count

    def _load_plugin_file(self, path: Path) -> bool:
        """Import a single plugin file and auto-register its contents."""
        module_name = f"rbsp_plugin_{path.stem}"
        try:
            spec = importlib.util.spec_from_file_location(module_name, str(path))
            if spec is None or spec.loader is None:
                return False
            module = importlib.util.module_from_spec(spec)
            sys.modules[module_name] = module
            spec.loader.exec_module(module)  # type: ignore[union-attr]
        except Exception:
            return False

        self._loaded_paths.append(str(path))

        # Auto-discover subclasses defined in the module
        for attr_name in dir(module):
            obj = getattr(module, attr_name)
            if not isinstance(obj, type):
                continue
            if issubclass(obj, ResolverPlugin) and obj is not ResolverPlugin:
                try:
                    self.register_resolver(obj())
                except Exception:
                    pass
            elif issubclass(obj, IndexerPlugin) and obj is not IndexerPlugin:
                try:
                    self.register_indexer(obj())
                except Exception:
                    pass
            elif issubclass(obj, RankerPlugin) and obj is not RankerPlugin:
                try:
                    self.register_ranker(obj())
                except Exception:
                    pass

        # Call register(manager) if present
        register_fn = getattr(module, "register", None)
        if callable(register_fn):
            try:
                register_fn(self)
            except Exception:
                pass

        return True

    # --- Info --------------------------------------------------------------

    def summary(self) -> Dict[str, Any]:
        """Return a summary of loaded plugins."""
        return {
            "resolvers": [r.name for r in self._resolvers],
            "indexers": [i.name for i in self._indexers],
            "rankers": [r.name for r in self._rankers],
            "hooks": {k: len(v) for k, v in self._hooks.items() if v},
            "loaded_files": list(self._loaded_paths),
        }
