"""Plugin lifecycle management for NEUROVAULT v0.2."""

from __future__ import annotations

import importlib.util
import logging
import sys
import threading
import time
from pathlib import Path
from typing import Any

from neurovault.plugins.base import PluginSensor

logger = logging.getLogger(__name__)

try:
    import tomllib
except ImportError:  # pragma: no cover
    import tomli as tomllib  # type: ignore


class PluginEntry:
    """Runtime plugin entry state."""

    __slots__ = ("sensor", "manifest", "failures")

    def __init__(self, sensor: PluginSensor, manifest: dict[str, Any], failures: int = 0):
        self.sensor = sensor
        self.manifest = manifest
        self.failures = failures


class PluginLoader:
    """Discover, validate, and load plugin sensors."""

    def __init__(self, plugin_dir: str | Path | None = None):
        self._plugin_dir = Path(plugin_dir or (Path.home() / ".neurovault" / "plugins"))
        self._loaded: dict[str, PluginEntry] = {}

    def discover(self) -> list[dict[str, Any]]:
        """Discover all plugins in the plugins directory."""
        plugins: list[dict[str, Any]] = []
        if not self._plugin_dir.exists():
            return plugins

        for entry in sorted(self._plugin_dir.iterdir()):
            if not entry.is_dir():
                continue
            manifest_path = entry / "plugin.toml"
            if not manifest_path.exists():
                continue
            try:
                with open(manifest_path, "rb") as f:
                    manifest = tomllib.load(f)
                manifest["_path"] = str(entry)
                manifest["_id"] = entry.name
                plugins.append(manifest)
            except Exception as exc:
                logger.warning("Failed to load plugin manifest %s: %s", manifest_path, exc)

        return plugins

    def load(self, plugin_id: str, config: dict[str, Any] | None = None) -> PluginSensor:
        """Load and initialize a plugin sensor."""
        plugin_path = self._plugin_dir / plugin_id
        manifest_path = plugin_path / "plugin.toml"

        if not manifest_path.exists():
            raise FileNotFoundError(f"Plugin not found: {plugin_id}")

        with open(manifest_path, "rb") as f:
            manifest = tomllib.load(f)

        sensor_conf = manifest.get("sensor", {})
        module_name = str(sensor_conf.get("module", "sensor"))
        class_name = str(sensor_conf.get("class", "PluginSensor"))

        module_path = plugin_path / f"{module_name}.py"
        if not module_path.exists():
            raise FileNotFoundError(f"Plugin module not found: {module_path}")

        spec = importlib.util.spec_from_file_location(f"neurovault_plugin_{plugin_id}", str(module_path))
        if spec is None or spec.loader is None:
            raise ImportError(f"Could not load plugin module spec for {plugin_id}")

        module = importlib.util.module_from_spec(spec)
        sys.modules[spec.name] = module
        spec.loader.exec_module(module)

        sensor_class = getattr(module, class_name)
        sensor = sensor_class()
        sensor.setup(config or {})

        self._loaded[plugin_id] = PluginEntry(sensor=sensor, manifest=manifest, failures=0)
        logger.info("Loaded plugin: %s (%s)", plugin_id, class_name)
        return sensor

    def unload(self, plugin_id: str) -> None:
        """Unload a plugin sensor."""
        entry = self._loaded.pop(plugin_id, None)
        if entry is not None:
            try:
                entry.sensor.teardown()
            except Exception as exc:
                logger.debug("Plugin teardown failed for %s: %s", plugin_id, exc)

    def get_loaded(self) -> dict[str, PluginEntry]:
        """Return all currently loaded plugins."""
        return dict(self._loaded)


class PluginManager(PluginLoader):
    """Discover, load, and manage plugin sensor polling loops."""

    MAX_CONSECUTIVE_FAILURES = 3

    def __init__(self, plugin_dir: str | Path | None = None):
        super().__init__(plugin_dir=plugin_dir)
        self._threads: list[threading.Thread] = []
        self._running = False

    def start_all(self, submit_fn) -> None:
        """Start polling loops for all loaded plugins."""
        self._running = True
        for pid, entry in self.get_loaded().items():
            t = threading.Thread(
                target=self._poll_loop,
                args=(pid, entry, submit_fn),
                daemon=True,
                name=f"plugin-{pid}",
            )
            self._threads.append(t)
            t.start()

    def stop_all(self) -> None:
        """Stop all plugin polling loops."""
        self._running = False
        for entry in self.get_loaded().values():
            try:
                entry.sensor.teardown()
            except Exception as exc:
                logger.debug("Plugin teardown during stop failed for %s: %s", entry.sensor.name, exc)
        self._loaded.clear()

    def _poll_loop(self, plugin_id: str, entry: PluginEntry, submit_fn) -> None:
        while self._running and entry.failures < self.MAX_CONSECUTIVE_FAILURES:
            try:
                items = entry.sensor.poll()
                for content, metadata in items:
                    submit_fn(content, entry.sensor.source_tag, metadata)
                entry.failures = 0
            except Exception as exc:
                entry.failures += 1
                logger.warning(
                    "Plugin %s error (%d/%d): %s",
                    plugin_id,
                    entry.failures,
                    self.MAX_CONSECUTIVE_FAILURES,
                    exc,
                )
            time.sleep(entry.sensor.poll_interval)

        if entry.failures >= self.MAX_CONSECUTIVE_FAILURES:
            logger.error("Plugin %s disabled after repeated failures", plugin_id)

    def status(self) -> dict[str, dict[str, Any]]:
        """Return runtime status for loaded plugins."""
        return {
            pid: {
                "name": e.sensor.name,
                "active": e.failures < self.MAX_CONSECUTIVE_FAILURES,
                "failures": e.failures,
                "poll_interval": e.sensor.poll_interval,
            }
            for pid, e in self.get_loaded().items()
        }
