"""
Plugin Loaders

Provides mechanisms to load plugins from:
- Local Python files/packages
- NPM packages
"""

import importlib.util
import json
import logging
import subprocess
import sys
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Optional

from groknroll.plugins.base import Plugin, PluginConfig, PluginStatus

logger = logging.getLogger(__name__)


class PluginLoader(ABC):
    """Base class for plugin loaders"""

    @abstractmethod
    def can_load(self, source: str) -> bool:
        """Check if this loader can handle the source"""
        pass

    @abstractmethod
    def load(self, source: str, config: Optional[PluginConfig] = None) -> Optional[Plugin]:
        """Load a plugin from the source"""
        pass


class LocalPluginLoader(PluginLoader):
    """
    Load plugins from local Python files or packages

    Supports:
    - Single .py files with Plugin subclass
    - Python packages with plugin.py entry point
    """

    def can_load(self, source: str) -> bool:
        """Check if source is a local path"""
        path = Path(source)
        return path.exists() and (path.suffix == ".py" or path.is_dir())

    def load(self, source: str, config: Optional[PluginConfig] = None) -> Optional[Plugin]:
        """Load a plugin from local path"""
        path = Path(source).resolve()

        try:
            if path.is_file() and path.suffix == ".py":
                return self._load_from_file(path)
            elif path.is_dir():
                return self._load_from_package(path)
            else:
                logger.error(f"Invalid plugin source: {source}")
                return None
        except Exception as e:
            logger.error(f"Failed to load plugin from {source}: {e}")
            return None

    def _load_from_file(self, path: Path) -> Optional[Plugin]:
        """Load plugin from a single Python file"""
        spec = importlib.util.spec_from_file_location(path.stem, path)
        if spec is None or spec.loader is None:
            return None

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

        # Find Plugin subclass
        for attr_name in dir(module):
            attr = getattr(module, attr_name)
            if isinstance(attr, type) and issubclass(attr, Plugin) and attr is not Plugin:
                return attr()

        logger.warning(f"No Plugin subclass found in {path}")
        return None

    def _load_from_package(self, path: Path) -> Optional[Plugin]:
        """Load plugin from a Python package"""
        plugin_file = path / "plugin.py"
        if not plugin_file.exists():
            plugin_file = path / "__init__.py"

        if not plugin_file.exists():
            logger.error(f"No plugin.py or __init__.py found in {path}")
            return None

        return self._load_from_file(plugin_file)


class NPMPluginLoader(PluginLoader):
    """
    Load plugins from NPM packages

    NPM plugins must have a groknroll.json manifest with:
    - name: Plugin name
    - version: Plugin version
    - main: Entry point (Python file)
    """

    def __init__(self, npm_prefix: Optional[Path] = None):
        self.npm_prefix = npm_prefix or Path.home() / ".groknroll" / "plugins"

    def can_load(self, source: str) -> bool:
        """Check if source looks like an NPM package"""
        return source.startswith("npm:") or "@" in source or "/" not in source

    def load(self, source: str, config: Optional[PluginConfig] = None) -> Optional[Plugin]:
        """Load plugin from NPM"""
        package_name = source.replace("npm:", "")

        try:
            # Install package if not already installed
            package_path = self._get_package_path(package_name)
            if not package_path.exists():
                self._install_package(package_name)
                package_path = self._get_package_path(package_name)

            if not package_path.exists():
                logger.error(f"Package not found after install: {package_name}")
                return None

            # Load manifest
            manifest = self._load_manifest(package_path)
            if not manifest:
                return None

            # Load plugin
            entry_point = package_path / manifest.get("main", "plugin.py")
            if not entry_point.exists():
                logger.error(f"Entry point not found: {entry_point}")
                return None

            loader = LocalPluginLoader()
            return loader.load(str(entry_point), config)

        except Exception as e:
            logger.error(f"Failed to load NPM plugin {package_name}: {e}")
            return None

    def _get_package_path(self, package_name: str) -> Path:
        """Get local path for an NPM package"""
        return self.npm_prefix / "node_modules" / package_name

    def _install_package(self, package_name: str) -> None:
        """Install an NPM package"""
        self.npm_prefix.mkdir(parents=True, exist_ok=True)

        # Initialize package.json if needed
        package_json = self.npm_prefix / "package.json"
        if not package_json.exists():
            package_json.write_text(json.dumps({"name": "groknroll-plugins", "private": True}))

        # Install package
        subprocess.run(
            ["npm", "install", package_name],
            cwd=self.npm_prefix,
            capture_output=True,
            check=True,
        )

    def _load_manifest(self, package_path: Path) -> Optional[dict]:
        """Load groknroll.json manifest"""
        manifest_file = package_path / "groknroll.json"
        if manifest_file.exists():
            return json.loads(manifest_file.read_text())

        # Fall back to package.json
        package_json = package_path / "package.json"
        if package_json.exists():
            return json.loads(package_json.read_text())

        return None


def load_plugin(source: str, config: Optional[PluginConfig] = None) -> Optional[Plugin]:
    """
    Load a plugin from any supported source

    Args:
        source: Plugin source (local path or NPM package)
        config: Optional plugin configuration

    Returns:
        Loaded Plugin or None if loading failed
    """
    loaders = [LocalPluginLoader(), NPMPluginLoader()]

    for loader in loaders:
        if loader.can_load(source):
            plugin = loader.load(source, config)
            if plugin:
                plugin._status = PluginStatus.LOADED
                return plugin

    logger.error(f"No loader could handle source: {source}")
    return None
