"""Plugin system — extensibility via protocol-based plugin architecture.

Provides a plugin protocol, registry, lifecycle hooks, and dynamic
loading. Plugins can extend causal algorithms, data connectors,
visualization skins, and CLI commands. Stdlib-only (ADR-007).
"""

from __future__ import annotations

import importlib
import importlib.util
import json
from dataclasses import dataclass
from enum import Enum
from pathlib import Path


class PluginType(Enum):
    """Categories of plugins."""

    ALGORITHM = "algorithm"
    CONNECTOR = "connector"
    SKIN = "skin"
    COMMAND = "command"
    PROVIDER = "provider"
    VALIDATOR = "validator"


class HookPoint(Enum):
    """Lifecycle hook points."""

    BEFORE_DISCOVER = "before_discover"
    AFTER_DISCOVER = "after_discover"
    BEFORE_SIMULATE = "before_simulate"
    AFTER_SIMULATE = "after_simulate"
    BEFORE_VALIDATE = "before_validate"
    AFTER_VALIDATE = "after_validate"
    ON_DAG_CHANGE = "on_dag_change"
    ON_ENGINE_START = "on_engine_start"


@dataclass(frozen=True)
class PluginMeta:
    """Metadata for a registered plugin."""

    name: str
    version: str
    plugin_type: PluginType
    description: str = ""
    author: str = ""
    hooks: tuple[HookPoint, ...] = ()

    def to_dict(self) -> dict:
        return {
            "name": self.name,
            "version": self.version,
            "type": self.plugin_type.value,
            "description": self.description,
            "author": self.author,
            "hooks": [h.value for h in self.hooks],
        }


class OrxaqPlugin:
    """Base class for Orxaq plugins.

    Subclass this and implement the desired methods.
    """

    meta: PluginMeta

    def activate(self, context: dict) -> None:
        """Called when plugin is activated."""

    def deactivate(self) -> None:
        """Called when plugin is deactivated."""

    def on_hook(self, hook: HookPoint, data: dict) -> dict:
        """Called when a lifecycle hook fires. Returns modified data."""
        return data


@dataclass
class PluginEntry:
    """Internal registry entry for a loaded plugin."""

    meta: PluginMeta
    instance: OrxaqPlugin
    active: bool = True
    load_order: int = 0


class PluginRegistry:
    """Central plugin registry with lifecycle management."""

    def __init__(self) -> None:
        self._plugins: dict[str, PluginEntry] = {}
        self._hook_subscribers: dict[HookPoint, list[str]] = {hook: [] for hook in HookPoint}
        self._load_counter = 0

    def register(self, plugin: OrxaqPlugin) -> None:
        """Register a plugin instance."""
        meta = plugin.meta
        if meta.name in self._plugins:
            msg = f"Plugin '{meta.name}' already registered"
            raise ValueError(msg)

        self._load_counter += 1
        entry = PluginEntry(
            meta=meta,
            instance=plugin,
            active=True,
            load_order=self._load_counter,
        )
        self._plugins[meta.name] = entry

        for hook in meta.hooks:
            self._hook_subscribers[hook].append(meta.name)

    def unregister(self, name: str) -> bool:
        """Unregister a plugin. Returns True if it existed."""
        entry = self._plugins.pop(name, None)
        if entry is None:
            return False

        entry.instance.deactivate()
        for hook in entry.meta.hooks:
            if name in self._hook_subscribers.get(hook, []):
                self._hook_subscribers[hook].remove(name)

        return True

    def get(self, name: str) -> OrxaqPlugin | None:
        """Get a plugin instance by name."""
        entry = self._plugins.get(name)
        return entry.instance if entry else None

    def list_plugins(self) -> list[PluginMeta]:
        """List all registered plugins."""
        entries = sorted(self._plugins.values(), key=lambda e: e.load_order)
        return [e.meta for e in entries]

    def list_by_type(self, plugin_type: PluginType) -> list[PluginMeta]:
        """List plugins of a specific type."""
        return [m for m in self.list_plugins() if m.plugin_type == plugin_type]

    def activate(self, name: str, context: dict | None = None) -> bool:
        """Activate a plugin."""
        entry = self._plugins.get(name)
        if entry is None:
            return False
        entry.instance.activate(context or {})
        entry.active = True
        return True

    def deactivate(self, name: str) -> bool:
        """Deactivate a plugin."""
        entry = self._plugins.get(name)
        if entry is None:
            return False
        entry.instance.deactivate()
        entry.active = False
        return True

    def fire_hook(self, hook: HookPoint, data: dict) -> dict:
        """Fire a lifecycle hook, passing data through all subscribers."""
        result = dict(data)
        for name in self._hook_subscribers.get(hook, []):
            entry = self._plugins.get(name)
            if entry and entry.active:
                result = entry.instance.on_hook(hook, result)
        return result

    def health_check(self) -> dict:
        """Report plugin system health."""
        plugins = []
        for name, entry in sorted(self._plugins.items()):
            plugins.append(
                {
                    "name": name,
                    "version": entry.meta.version,
                    "type": entry.meta.plugin_type.value,
                    "active": entry.active,
                }
            )
        return {
            "total_plugins": len(self._plugins),
            "active_plugins": sum(1 for e in self._plugins.values() if e.active),
            "plugins": plugins,
        }

    def __len__(self) -> int:
        return len(self._plugins)

    def __contains__(self, name: str) -> bool:
        return name in self._plugins


class PluginLoader:
    """Load plugins from Python modules or plugin directories."""

    def __init__(self, registry: PluginRegistry) -> None:
        self._registry = registry

    def load_module(self, module_path: str) -> list[str]:
        """Load plugins from a Python module path (e.g., 'my_plugin.main').

        The module should define a `register(registry)` function or
        have module-level `PLUGIN` attribute.
        """
        loaded: list[str] = []
        mod = importlib.import_module(module_path)

        if hasattr(mod, "register"):
            mod.register(self._registry)
            loaded.append(module_path)
        elif hasattr(mod, "PLUGIN"):
            plugin = mod.PLUGIN
            self._registry.register(plugin)
            loaded.append(plugin.meta.name)

        return loaded

    def load_file(self, file_path: str) -> list[str]:
        """Load plugins from a Python file."""
        loaded: list[str] = []
        path = Path(file_path)
        if not path.exists():
            return loaded

        spec = importlib.util.spec_from_file_location(path.stem, path)
        if spec is None or spec.loader is None:
            return loaded

        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)

        if hasattr(mod, "register"):
            mod.register(self._registry)
            loaded.append(str(path))
        elif hasattr(mod, "PLUGIN"):
            plugin = mod.PLUGIN
            self._registry.register(plugin)
            loaded.append(plugin.meta.name)

        return loaded

    def load_directory(self, dir_path: str) -> list[str]:
        """Load all .py plugin files from a directory."""
        loaded: list[str] = []
        d = Path(dir_path)
        if not d.is_dir():
            return loaded

        for py_file in sorted(d.glob("*.py")):
            if py_file.name.startswith("_"):
                continue
            loaded.extend(self.load_file(str(py_file)))

        return loaded

    def load_from_config(self, config_path: str) -> list[str]:
        """Load plugins specified in a JSON config file.

        Config format: {"plugins": [{"module": "...", "enabled": true}]}
        """
        loaded: list[str] = []
        path = Path(config_path)
        if not path.exists():
            return loaded

        with open(path) as f:
            config = json.loads(f.read())

        for entry in config.get("plugins", []):
            if not entry.get("enabled", True):
                continue
            module = entry.get("module", "")
            if module:
                loaded.extend(self.load_module(module))

        return loaded
