#!/usr/bin/env python3
"""
Plugin Discovery and Loading System

Automatically discovers and loads plugins from the plugins/ directory.
Each plugin must be in a subdirectory with a plugin.py file containing a *Plugin class.
"""

import importlib.util
import inspect
import sys
from pathlib import Path
from typing import Dict, Any, Optional
from dataclasses import dataclass


@dataclass
class PluginParameter:
    """Defines a parameter for the plugin, used for both validation and UI generation."""
    name: str                      # Parameter name in request body
    type: str                      # 'text', 'url', 'number', 'checkbox', 'textarea', 'select'
    label: str                     # Display label for UI
    required: bool = False         # Whether parameter is required
    default: Any = None            # Default value
    placeholder: str = ""          # Placeholder text for inputs
    options: list[str] = None      # Options for 'select' type
    help_text: str = ""            # Help text shown in UI
    
    # Constraint fields (optional)
    min: float = None              # Minimum value (number) or minimum length (string)
    max: float = None              # Maximum value (number) or maximum length (string)
    min_length: int = None         # Minimum length for strings
    max_length: int = None         # Maximum length for strings
    pattern: str = None            # Regex pattern for string validation
    pattern_description: str = None # Human-readable pattern description
    
    def __post_init__(self):
        if self.options is None and self.type == 'select':
            self.options = []


@dataclass
class PluginMetadata:
    """Plugin metadata describing the plugin's interface and behavior."""
    name: str                      # URL-safe name: "wikipedia-search"
    description: str               # What the plugin does (shown as button label)
    methods: list[str] = None      # HTTP methods, defaults to ["POST"]
    parameters: list[PluginParameter] = None  # Parameter schema for validation and UI
    bg_color: list[str] = None     # Optional background color(s) - single color or list for gradient (left-to-right)
    
    def __post_init__(self):
        if self.methods is None:
            self.methods = ["POST"]
        if self.parameters is None:
            self.parameters = []


class PluginRegistry:
    """
    Central registry for all discovered plugins.
    
    Scans the plugins/ directory, loads valid plugin classes, and maintains
    a mapping of plugin names to plugin instances.
    """
    
    def __init__(self, plugins_dir: str = "plugins"):
        """
        Initialize the plugin registry.
        
        Args:
            plugins_dir: Path to the directory containing plugin subdirectories
        """
        self.plugins_dir = Path(plugins_dir)
        self.plugins: Dict[str, Any] = {}  # name → plugin_instance
        self._load_errors: Dict[str, str] = {}  # plugin_folder → error_message
    
    def discover_and_load(self) -> None:
        """
        Scan plugins directory and load all valid plugins.
        
        Expected structure:
            plugins/
                wikipedia_search/
                    plugin.py  # Contains *Plugin class
                form_filler/
                    plugin.py
        
        Logs success/failure for each plugin discovered.
        """
        if not self.plugins_dir.exists():
            print(f"⚠️  Plugins directory not found: {self.plugins_dir}")
            print(f"   Creating directory: {self.plugins_dir}")
            self.plugins_dir.mkdir(parents=True, exist_ok=True)
            return
        
        if not self.plugins_dir.is_dir():
            print(f"❌ Plugins path exists but is not a directory: {self.plugins_dir}")
            return
        
        # Iterate through subdirectories
        plugin_folders = [p for p in self.plugins_dir.iterdir() if p.is_dir() and not p.name.startswith('_')]
        
        if not plugin_folders:
            print(f"ℹ️  No plugin directories found in {self.plugins_dir}")
            return
        
        print(f"\n🔍 Scanning for plugins in {self.plugins_dir}...")
        
        for plugin_folder in plugin_folders:
            plugin_file = plugin_folder / "plugin.py"
            
            if not plugin_file.exists():
                self._load_errors[plugin_folder.name] = "Missing plugin.py file"
                print(f"⊘  {plugin_folder.name}: Missing plugin.py file")
                continue
            
            # Attempt to load the plugin
            self._load_plugin_from_file(plugin_folder, plugin_file)
        
        # Summary
        print(f"\n{'='*60}")
        print(f"✅ Successfully loaded {len(self.plugins)} plugin(s)")
        if self._load_errors:
            print(f"❌ Failed to load {len(self._load_errors)} plugin(s)")
        print(f"{'='*60}\n")
    
    def _load_plugin_from_file(self, plugin_folder: Path, plugin_file: Path) -> None:
        """
        Load a single plugin from a plugin.py file.
        
        Args:
            plugin_folder: The plugin's directory
            plugin_file: Path to the plugin.py file
        """
        try:
            # Dynamic import of plugin.py
            module_name = f"plugins.{plugin_folder.name}.plugin"
            spec = importlib.util.spec_from_file_location(module_name, plugin_file)
            
            if spec is None or spec.loader is None:
                raise ImportError(f"Could not load spec for {plugin_file}")
            
            module = importlib.util.module_from_spec(spec)
            sys.modules[module_name] = module  # Register in sys.modules
            spec.loader.exec_module(module)
            
            # Find all classes ending with "Plugin"
            plugin_classes = [
                (name, obj) for name, obj in inspect.getmembers(module, inspect.isclass)
                if name.endswith('Plugin') and obj.__module__ == module_name
            ]
            
            if not plugin_classes:
                self._load_errors[plugin_folder.name] = "No *Plugin class found"
                print(f"⊘  {plugin_folder.name}: No class ending with 'Plugin' found")
                return
            
            # Validate and instantiate each plugin class
            for class_name, plugin_class in plugin_classes:
                validation_error = self._validate_plugin_class(plugin_class, class_name)
                
                if validation_error:
                    self._load_errors[plugin_folder.name] = validation_error
                    print(f"❌ {plugin_folder.name}.{class_name}: {validation_error}")
                    continue
                
                # Instantiate the plugin
                try:
                    plugin_instance = plugin_class()
                    plugin_name = plugin_instance.metadata.name
                    
                    # Check for duplicate names
                    if plugin_name in self.plugins:
                        print(f"⚠️  {plugin_folder.name}.{class_name}: Duplicate plugin name '{plugin_name}' (skipping)")
                        continue
                    
                    self.plugins[plugin_name] = plugin_instance
                    print(f"✅ {plugin_folder.name}.{class_name} → '{plugin_name}'")
                    
                except Exception as e:
                    self._load_errors[plugin_folder.name] = f"Failed to instantiate: {e}"
                    print(f"❌ {plugin_folder.name}.{class_name}: Failed to instantiate: {e}")
        
        except Exception as e:
            self._load_errors[plugin_folder.name] = f"Import failed: {e}"
            print(f"❌ {plugin_folder.name}: Import failed: {e}")
    
    def _validate_plugin_class(self, plugin_class: type, class_name: str) -> Optional[str]:
        """
        Validate that a plugin class follows the required structure.
        
        Args:
            plugin_class: The class to validate
            class_name: Name of the class (for error messages)
        
        Returns:
            Error message if invalid, None if valid
        """
        # Check for metadata attribute
        if not hasattr(plugin_class, 'metadata'):
            return "Missing 'metadata' attribute"
        
        # Check for execute method
        if not hasattr(plugin_class, 'execute'):
            return "Missing 'execute' method"
        
        # Validate that execute is async
        execute_method = getattr(plugin_class, 'execute')
        if not inspect.iscoroutinefunction(execute_method):
            return "'execute' method must be async"
        
        # Try to access metadata (validates it's a PluginMetadata instance)
        try:
            metadata = plugin_class.metadata
            
            # Check required metadata fields
            if not hasattr(metadata, 'name') or not metadata.name:
                return "metadata.name is required"
            
            if not hasattr(metadata, 'description') or not metadata.description:
                return "metadata.description is required"
            
            # Validate name is URL-safe (basic check)
            if not all(c.isalnum() or c in '-_' for c in metadata.name):
                return f"metadata.name '{metadata.name}' is not URL-safe (use alphanumeric, hyphens, underscores only)"
            
        except Exception as e:
            return f"Invalid metadata: {e}"
        
        return None
    
    def get_plugin(self, name: str) -> Optional[Any]:
        """
        Get a plugin instance by name.
        
        Args:
            name: The plugin name (from metadata.name)
        
        Returns:
            Plugin instance or None if not found
        """
        return self.plugins.get(name)
    
    def list_plugins(self) -> Dict[str, Any]:
        """
        Get all loaded plugins.
        
        Returns:
            Dictionary mapping plugin names to plugin instances
        """
        return self.plugins.copy()
    
    def get_load_errors(self) -> Dict[str, str]:
        """
        Get all plugin load errors.
        
        Returns:
            Dictionary mapping plugin folder names to error messages
        """
        return self._load_errors.copy()


# Global plugin registry instance
_plugin_registry: Optional[PluginRegistry] = None


def get_plugin_registry() -> PluginRegistry:
    """
    Get the global plugin registry instance.
    
    Returns:
        The global PluginRegistry instance
    """
    global _plugin_registry
    if _plugin_registry is None:
        _plugin_registry = PluginRegistry()
    return _plugin_registry


def initialize_plugins(plugins_dir: str = "plugins") -> PluginRegistry:
    """
    Initialize the plugin system by discovering and loading all plugins.
    
    Args:
        plugins_dir: Path to the plugins directory
    
    Returns:
        The initialized PluginRegistry instance
    """
    global _plugin_registry
    _plugin_registry = PluginRegistry(plugins_dir)
    _plugin_registry.discover_and_load()
    return _plugin_registry
