"""
Plugin registry — extensible hook system for parsers, LLM providers,
renderers, and post-processors.

Usage:
    from hld_generator.plugins import registry

    # Register a custom parser
    @registry.register_parser("swift")
    class SwiftParser:
        def parse_file(self, file_path, language):
            ...

    # Register a custom LLM provider
    @registry.register_llm_provider("ollama")
    class OllamaProvider:
        def call(self, context: str, system_prompt: str) -> str:
            ...

    # Register a custom renderer
    @registry.register_renderer("html")
    class HTMLRenderer:
        def render(self, analysis, code_graph, output_dir) -> list[Path]:
            ...

    # Register a post-processor hook
    @registry.register_hook("post_parse")
    def enrich_parsed_files(parsed_files):
        ...
        return parsed_files

    # Register custom endpoint patterns
    registry.register_endpoint_pattern(
        r'@MyFramework\\.route\\("([^"]+)"\\)',
        name="MyFramework"
    )
"""

from __future__ import annotations

import importlib.util
import logging
import re
from pathlib import Path
from typing import Any, Callable, Protocol, runtime_checkable

from hld_generator.parsers.base import ParsedFile

logger = logging.getLogger(__name__)


# ── Abstract interfaces for plugins ────────────────────────────────────────────


@runtime_checkable
class ParserPlugin(Protocol):
    """Interface for custom language parsers."""

    def parse_file(self, file_path: Path, language: str) -> ParsedFile:
        ...


@runtime_checkable
class LLMProviderPlugin(Protocol):
    """Interface for custom LLM providers."""

    def call(self, context: str, system_prompt: str) -> str:
        """Send context to the LLM and return raw text response."""
        ...


@runtime_checkable
class RendererPlugin(Protocol):
    """Interface for custom output renderers."""

    def render(self, analysis: Any, code_graph: Any, output_dir: Path) -> list[Path]:
        """Render analysis results and return list of output file paths."""
        ...


# Hook function type
HookFn = Callable[..., Any]

# Valid hook points in the pipeline
HOOK_POINTS = {
    "pre_scan",       # (config) → config
    "post_scan",      # (scanned_files) → scanned_files
    "pre_parse",      # (scanned_files) → scanned_files
    "post_parse",     # (parsed_files) → parsed_files
    "pre_graph",      # (parsed_files) → parsed_files
    "post_graph",     # (code_graph) → code_graph
    "pre_llm",        # (code_graph) → code_graph
    "post_llm",       # (analysis) → analysis
    "pre_render",     # (analysis, code_graph) → (analysis, code_graph)
    "post_render",    # (output_files) → output_files
}


class PluginRegistry:
    """Central registry for all extension points."""

    def __init__(self) -> None:
        self._parsers: dict[str, ParserPlugin] = {}
        self._llm_providers: dict[str, LLMProviderPlugin] = {}
        self._renderers: dict[str, RendererPlugin] = {}
        self._hooks: dict[str, list[HookFn]] = {hp: [] for hp in HOOK_POINTS}
        self._endpoint_patterns: list[tuple[re.Pattern[str], str]] = []
        self._language_extensions: dict[str, str] = {}   # extra ext → lang mappings

    # ── Parser plugins ─────────────────────────────────────────────────────

    def register_parser(self, language: str):
        """Decorator to register a parser plugin for a language."""
        def decorator(cls):
            instance = cls() if isinstance(cls, type) else cls
            self._parsers[language] = instance
            logger.info("Registered parser plugin for '%s'", language)
            return cls
        return decorator

    def get_parser(self, language: str) -> ParserPlugin | None:
        return self._parsers.get(language)

    def has_parser(self, language: str) -> bool:
        return language in self._parsers

    # ── LLM provider plugins ──────────────────────────────────────────────

    def register_llm_provider(self, name: str):
        """Decorator to register an LLM provider plugin."""
        def decorator(cls):
            instance = cls() if isinstance(cls, type) else cls
            self._llm_providers[name] = instance
            logger.info("Registered LLM provider plugin '%s'", name)
            return cls
        return decorator

    def get_llm_provider(self, name: str) -> LLMProviderPlugin | None:
        return self._llm_providers.get(name)

    def has_llm_provider(self, name: str) -> bool:
        return name in self._llm_providers

    @property
    def available_llm_providers(self) -> list[str]:
        return list(self._llm_providers.keys())

    # ── Renderer plugins ──────────────────────────────────────────────────

    def register_renderer(self, format_name: str):
        """Decorator to register an output renderer plugin."""
        def decorator(cls):
            instance = cls() if isinstance(cls, type) else cls
            self._renderers[format_name] = instance
            logger.info("Registered renderer plugin '%s'", format_name)
            return cls
        return decorator

    def get_renderer(self, format_name: str) -> RendererPlugin | None:
        return self._renderers.get(format_name)

    def has_renderer(self, format_name: str) -> bool:
        return format_name in self._renderers

    @property
    def available_renderers(self) -> dict[str, RendererPlugin]:
        return dict(self._renderers)

    # ── Pipeline hooks ────────────────────────────────────────────────────

    def register_hook(self, hook_point: str):
        """Decorator to register a hook at a pipeline stage."""
        if hook_point not in HOOK_POINTS:
            raise ValueError(
                f"Unknown hook point '{hook_point}'. "
                f"Available: {', '.join(sorted(HOOK_POINTS))}"
            )

        def decorator(fn):
            self._hooks[hook_point].append(fn)
            logger.info("Registered hook at '%s': %s", hook_point, fn.__name__)
            return fn
        return decorator

    def run_hooks(self, hook_point: str, *args, **kwargs) -> Any:
        """Run all hooks registered at a given point.

        Each hook receives the output of the previous one (pipeline style).
        If a hook returns None, the original input is passed forward.
        Hook exceptions are logged as errors and re-raised to prevent
        corrupt data from flowing downstream silently.
        """
        result = args[0] if len(args) == 1 else args
        for fn in self._hooks.get(hook_point, []):
            try:
                out = fn(result, **kwargs) if len(args) == 1 else fn(*result, **kwargs)
                if out is not None:
                    result = out
            except Exception as exc:
                logger.error("Hook '%s' at '%s' failed: %s", fn.__name__, hook_point, exc)
                raise
        return result

    # ── Endpoint pattern extensions ───────────────────────────────────────

    def register_endpoint_pattern(
        self,
        pattern: str,
        name: str = "custom",
        flags: int = 0,
    ) -> None:
        """Register a custom regex pattern for API endpoint detection."""
        compiled = re.compile(pattern, flags)
        self._endpoint_patterns.append((compiled, name))
        logger.info("Registered endpoint pattern '%s'", name)

    @property
    def custom_endpoint_patterns(self) -> list[tuple[re.Pattern[str], str]]:
        return list(self._endpoint_patterns)

    # ── Language extension mappings ────────────────────────────────────────

    def register_language(self, extension: str, language: str) -> None:
        """Register a custom file extension → language mapping."""
        if not extension.startswith("."):
            extension = f".{extension}"
        self._language_extensions[extension] = language
        logger.info("Registered language mapping: %s → %s", extension, language)

    @property
    def custom_language_map(self) -> dict[str, str]:
        return dict(self._language_extensions)

    # ── Discovery ─────────────────────────────────────────────────────────

    def load_plugins_from_dir(self, plugin_dir: Path) -> int:
        """Load all Python files from a directory as plugins.

        Each file is executed in its own namespace. It should import
        `registry` from this module and use the decorators.

        Returns the number of plugins loaded.
        """
        if not plugin_dir.is_dir():
            return 0

        count = 0
        for py_file in sorted(plugin_dir.glob("*.py")):
            if py_file.name.startswith("_"):
                continue
            try:
                module_name = f"hld_plugin_{py_file.stem}"
                spec = importlib.util.spec_from_file_location(module_name, py_file)
                if spec is None or spec.loader is None:
                    logger.warning("Cannot create module spec for %s", py_file.name)
                    continue
                module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(module)
                count += 1
                logger.info("Loaded plugin: %s", py_file.name)
            except Exception as exc:
                logger.warning("Failed to load plugin %s: %s", py_file.name, exc)
        return count

    def reset(self) -> None:
        """Clear all registered plugins, hooks, and custom mappings.

        Useful for ensuring a clean state between invocations when the
        tool is used programmatically in the same process.
        """
        self._parsers.clear()
        self._llm_providers.clear()
        self._renderers.clear()
        self._hooks = {hp: [] for hp in HOOK_POINTS}
        self._endpoint_patterns.clear()
        self._language_extensions.clear()

    def summary(self) -> str:
        """Return a human-readable summary of registered plugins."""
        parts = []
        if self._parsers:
            parts.append(f"Parsers: {', '.join(self._parsers.keys())}")
        if self._llm_providers:
            parts.append(f"LLM Providers: {', '.join(self._llm_providers.keys())}")
        if self._renderers:
            parts.append(f"Renderers: {', '.join(self._renderers.keys())}")
        hook_counts = {k: len(v) for k, v in self._hooks.items() if v}
        if hook_counts:
            parts.append(f"Hooks: {hook_counts}")
        if self._endpoint_patterns:
            parts.append(f"Endpoint Patterns: {len(self._endpoint_patterns)}")
        if self._language_extensions:
            parts.append(f"Language Extensions: {self._language_extensions}")
        return "; ".join(parts) if parts else "No plugins registered"


# ── Global singleton ───────────────────────────────────────────────────────────

registry = PluginRegistry()
