#!/usr/bin/env python3
"""
GENESIS PLUGIN SYSTEM
======================
Dynamic plugin loading and lifecycle management.

Features:
    - Dynamic plugin discovery
    - Lifecycle hooks (load, start, stop, unload)
    - Dependency resolution
    - Plugin configuration
    - Hot reload support
    - Event hooks

Usage:
    manager = PluginManager()
    manager.discover("plugins/")
    manager.load("my_plugin")
    manager.start_all()
"""

import importlib
import importlib.util
import inspect
import json
import sys
import threading
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Any, Optional, Callable, Type, Set
from enum import Enum


class PluginState(Enum):
    """Plugin lifecycle states."""
    UNLOADED = "unloaded"
    LOADED = "loaded"
    STARTED = "started"
    STOPPED = "stopped"
    ERROR = "error"


@dataclass
class PluginManifest:
    """Plugin metadata and configuration."""
    name: str
    version: str
    description: str = ""
    author: str = ""
    dependencies: List[str] = field(default_factory=list)
    entry_point: str = "plugin"  # Module containing Plugin class
    config_schema: Dict[str, Any] = field(default_factory=dict)
    hooks: List[str] = field(default_factory=list)
    min_genesis_version: str = "0.1.0"

    @classmethod
    def from_dict(cls, data: Dict) -> 'PluginManifest':
        return cls(
            name=data.get("name", "unknown"),
            version=data.get("version", "0.0.0"),
            description=data.get("description", ""),
            author=data.get("author", ""),
            dependencies=data.get("dependencies", []),
            entry_point=data.get("entry_point", "plugin"),
            config_schema=data.get("config_schema", {}),
            hooks=data.get("hooks", []),
            min_genesis_version=data.get("min_genesis_version", "0.1.0")
        )

    def to_dict(self) -> Dict:
        return {
            "name": self.name,
            "version": self.version,
            "description": self.description,
            "author": self.author,
            "dependencies": self.dependencies,
            "entry_point": self.entry_point,
            "config_schema": self.config_schema,
            "hooks": self.hooks,
            "min_genesis_version": self.min_genesis_version
        }


class Plugin(ABC):
    """
    Base class for all plugins.
    Plugins must inherit from this class.
    """

    def __init__(self, manager: 'PluginManager', config: Dict[str, Any] = None):
        self.manager = manager
        self.config = config or {}
        self._state = PluginState.UNLOADED

    @property
    def name(self) -> str:
        """Plugin name."""
        return self.__class__.__name__

    @property
    def state(self) -> PluginState:
        return self._state

    def on_load(self):
        """Called when plugin is loaded."""
        pass

    def on_start(self):
        """Called when plugin is started."""
        pass

    def on_stop(self):
        """Called when plugin is stopped."""
        pass

    def on_unload(self):
        """Called when plugin is unloaded."""
        pass

    def on_config_change(self, old_config: Dict, new_config: Dict):
        """Called when plugin configuration changes."""
        self.config = new_config

    def get_commands(self) -> Dict[str, Callable]:
        """Return commands this plugin provides."""
        return {}

    def get_event_handlers(self) -> Dict[str, Callable]:
        """Return event handlers this plugin provides."""
        return {}


@dataclass
class LoadedPlugin:
    """A loaded plugin instance."""
    manifest: PluginManifest
    instance: Plugin
    module: Any
    path: Path
    state: PluginState = PluginState.UNLOADED
    loaded_at: str = field(default_factory=lambda: datetime.now().isoformat())
    error: Optional[str] = None

    def to_dict(self) -> Dict:
        return {
            "name": self.manifest.name,
            "version": self.manifest.version,
            "state": self.state.value,
            "path": str(self.path),
            "loaded_at": self.loaded_at,
            "error": self.error
        }


class PluginManager:
    """
    Manages plugin lifecycle.
    """

    def __init__(self, plugins_dir: Path = None):
        self.plugins_dir = plugins_dir or Path(__file__).parent.parent / "plugins"
        self.plugins_dir.mkdir(parents=True, exist_ok=True)

        self._plugins: Dict[str, LoadedPlugin] = {}
        self._hooks: Dict[str, List[Callable]] = {}
        self._lock = threading.RLock()

        # Event callbacks
        self._on_load: List[Callable[[str], None]] = []
        self._on_unload: List[Callable[[str], None]] = []
        self._on_error: List[Callable[[str, Exception], None]] = []

    def discover(self, path: Path = None) -> List[PluginManifest]:
        """Discover plugins in a directory."""
        search_path = Path(path) if path else self.plugins_dir
        manifests = []

        if not search_path.exists():
            return manifests

        for item in search_path.iterdir():
            if item.is_dir():
                manifest_path = item / "manifest.json"
                if manifest_path.exists():
                    try:
                        with open(manifest_path, 'r') as f:
                            data = json.load(f)
                        manifest = PluginManifest.from_dict(data)
                        manifests.append(manifest)
                    except Exception:
                        pass

            elif item.suffix == '.py' and not item.name.startswith('_'):
                # Single-file plugin
                manifest = PluginManifest(
                    name=item.stem,
                    version="0.0.0",
                    description=f"Plugin from {item.name}"
                )
                manifests.append(manifest)

        return manifests

    def load(self, plugin_name: str, config: Dict = None) -> bool:
        """Load a plugin by name."""
        with self._lock:
            if plugin_name in self._plugins:
                return True  # Already loaded

            # Find plugin
            plugin_path = self._find_plugin_path(plugin_name)
            if not plugin_path:
                self._notify_error(plugin_name, Exception(f"Plugin not found: {plugin_name}"))
                return False

            try:
                # Load manifest
                manifest = self._load_manifest(plugin_path)

                # Check dependencies
                for dep in manifest.dependencies:
                    if dep not in self._plugins:
                        if not self.load(dep):
                            raise Exception(f"Failed to load dependency: {dep}")

                # Load module
                if plugin_path.is_dir():
                    module_path = plugin_path / f"{manifest.entry_point}.py"
                else:
                    module_path = plugin_path

                spec = importlib.util.spec_from_file_location(plugin_name, module_path)
                if not spec or not spec.loader:
                    raise Exception(f"Cannot load module: {module_path}")

                module = importlib.util.module_from_spec(spec)
                sys.modules[plugin_name] = module
                spec.loader.exec_module(module)

                # Find Plugin class
                plugin_class = None
                for name, obj in inspect.getmembers(module):
                    if inspect.isclass(obj) and issubclass(obj, Plugin) and obj is not Plugin:
                        plugin_class = obj
                        break

                if not plugin_class:
                    raise Exception(f"No Plugin class found in {plugin_name}")

                # Create instance
                instance = plugin_class(self, config or {})
                instance.on_load()
                instance._state = PluginState.LOADED

                loaded = LoadedPlugin(
                    manifest=manifest,
                    instance=instance,
                    module=module,
                    path=plugin_path,
                    state=PluginState.LOADED
                )

                self._plugins[plugin_name] = loaded

                # Register hooks
                for hook_name in manifest.hooks:
                    handler = getattr(instance, f"on_{hook_name}", None)
                    if handler:
                        self.register_hook(hook_name, handler)

                # Notify
                for callback in self._on_load:
                    try:
                        callback(plugin_name)
                    except Exception:
                        pass

                return True

            except Exception as e:
                self._notify_error(plugin_name, e)
                return False

    def _find_plugin_path(self, name: str) -> Optional[Path]:
        """Find plugin path by name."""
        # Check directory plugin
        dir_path = self.plugins_dir / name
        if dir_path.is_dir() and (dir_path / "manifest.json").exists():
            return dir_path

        # Check single file plugin
        file_path = self.plugins_dir / f"{name}.py"
        if file_path.exists():
            return file_path

        return None

    def _load_manifest(self, plugin_path: Path) -> PluginManifest:
        """Load or create manifest."""
        if plugin_path.is_dir():
            manifest_path = plugin_path / "manifest.json"
            if manifest_path.exists():
                with open(manifest_path, 'r') as f:
                    return PluginManifest.from_dict(json.load(f))

        # Create default manifest
        return PluginManifest(
            name=plugin_path.stem if plugin_path.is_file() else plugin_path.name,
            version="0.0.0"
        )

    def unload(self, plugin_name: str) -> bool:
        """Unload a plugin."""
        with self._lock:
            if plugin_name not in self._plugins:
                return True

            loaded = self._plugins[plugin_name]

            try:
                # Stop first if running
                if loaded.state == PluginState.STARTED:
                    self.stop(plugin_name)

                # Call unload hook
                loaded.instance.on_unload()
                loaded.instance._state = PluginState.UNLOADED

                # Remove from sys.modules
                if plugin_name in sys.modules:
                    del sys.modules[plugin_name]

                # Remove from plugins
                del self._plugins[plugin_name]

                # Notify
                for callback in self._on_unload:
                    try:
                        callback(plugin_name)
                    except Exception:
                        pass

                return True

            except Exception as e:
                self._notify_error(plugin_name, e)
                return False

    def start(self, plugin_name: str) -> bool:
        """Start a plugin."""
        with self._lock:
            if plugin_name not in self._plugins:
                return False

            loaded = self._plugins[plugin_name]
            if loaded.state == PluginState.STARTED:
                return True

            try:
                loaded.instance.on_start()
                loaded.instance._state = PluginState.STARTED
                loaded.state = PluginState.STARTED
                return True

            except Exception as e:
                loaded.state = PluginState.ERROR
                loaded.error = str(e)
                self._notify_error(plugin_name, e)
                return False

    def stop(self, plugin_name: str) -> bool:
        """Stop a plugin."""
        with self._lock:
            if plugin_name not in self._plugins:
                return False

            loaded = self._plugins[plugin_name]
            if loaded.state != PluginState.STARTED:
                return True

            try:
                loaded.instance.on_stop()
                loaded.instance._state = PluginState.STOPPED
                loaded.state = PluginState.STOPPED
                return True

            except Exception as e:
                self._notify_error(plugin_name, e)
                return False

    def reload(self, plugin_name: str) -> bool:
        """Reload a plugin (hot reload)."""
        with self._lock:
            if plugin_name not in self._plugins:
                return self.load(plugin_name)

            old_loaded = self._plugins[plugin_name]
            old_config = old_loaded.instance.config

            # Unload
            self.unload(plugin_name)

            # Reload
            return self.load(plugin_name, old_config)

    def start_all(self):
        """Start all loaded plugins."""
        for name in list(self._plugins.keys()):
            self.start(name)

    def stop_all(self):
        """Stop all running plugins."""
        for name in list(self._plugins.keys()):
            self.stop(name)

    def unload_all(self):
        """Unload all plugins."""
        for name in list(self._plugins.keys()):
            self.unload(name)

    def register_hook(self, hook_name: str, handler: Callable):
        """Register a hook handler."""
        if hook_name not in self._hooks:
            self._hooks[hook_name] = []
        self._hooks[hook_name].append(handler)

    def trigger_hook(self, hook_name: str, *args, **kwargs) -> List[Any]:
        """Trigger a hook and return all results."""
        results = []
        for handler in self._hooks.get(hook_name, []):
            try:
                result = handler(*args, **kwargs)
                results.append(result)
            except Exception:
                pass
        return results

    def get_plugin(self, name: str) -> Optional[Plugin]:
        """Get a plugin instance by name."""
        loaded = self._plugins.get(name)
        return loaded.instance if loaded else None

    def get_all_plugins(self) -> Dict[str, LoadedPlugin]:
        """Get all loaded plugins."""
        return self._plugins.copy()

    def get_commands(self) -> Dict[str, Callable]:
        """Get all commands from all plugins."""
        commands = {}
        for loaded in self._plugins.values():
            if loaded.state == PluginState.STARTED:
                commands.update(loaded.instance.get_commands())
        return commands

    def get_status(self) -> Dict:
        """Get plugin system status."""
        return {
            "plugins_dir": str(self.plugins_dir),
            "loaded_count": len(self._plugins),
            "plugins": {
                name: loaded.to_dict()
                for name, loaded in self._plugins.items()
            },
            "hooks": list(self._hooks.keys())
        }

    def on_load(self, callback: Callable[[str], None]):
        """Register callback for plugin load."""
        self._on_load.append(callback)

    def on_unload(self, callback: Callable[[str], None]):
        """Register callback for plugin unload."""
        self._on_unload.append(callback)

    def on_error(self, callback: Callable[[str, Exception], None]):
        """Register callback for plugin errors."""
        self._on_error.append(callback)

    def _notify_error(self, plugin_name: str, error: Exception):
        """Notify error callbacks."""
        for callback in self._on_error:
            try:
                callback(plugin_name, error)
            except Exception:
                pass


# Example plugin for testing
class ExamplePlugin(Plugin):
    """Example plugin for testing."""

    def on_load(self):
        print(f"[{self.name}] Loaded")

    def on_start(self):
        print(f"[{self.name}] Started")

    def on_stop(self):
        print(f"[{self.name}] Stopped")

    def on_unload(self):
        print(f"[{self.name}] Unloaded")

    def get_commands(self) -> Dict[str, Callable]:
        return {
            "example.hello": lambda: "Hello from ExamplePlugin!"
        }


def main():
    """CLI and demo for plugin system."""
    import argparse
    parser = argparse.ArgumentParser(description="Genesis Plugin System")
    parser.add_argument("command", choices=["discover", "status", "demo"])
    parser.add_argument("--dir", help="Plugins directory")
    args = parser.parse_args()

    manager = PluginManager(Path(args.dir) if args.dir else None)

    if args.command == "discover":
        manifests = manager.discover()
        print(f"Discovered {len(manifests)} plugins:")
        for m in manifests:
            print(f"  - {m.name} v{m.version}: {m.description}")

    elif args.command == "status":
        status = manager.get_status()
        print(json.dumps(status, indent=2))

    elif args.command == "demo":
        print("Plugin System Demo")
        print("=" * 40)

        # Create test plugin file with proper import
        test_plugin_path = manager.plugins_dir / "test_plugin.py"
        core_path = Path(__file__).parent
        test_plugin_code = f'''
import sys
sys.path.insert(0, "{core_path}")
from plugin_system import Plugin

class TestPlugin(Plugin):
    def on_load(self):
        print(f"[{{self.name}}] Loaded with config: {{self.config}}")

    def on_start(self):
        print(f"[{{self.name}}] Started!")

    def on_stop(self):
        print(f"[{{self.name}}] Stopped")

    def get_commands(self):
        return {{"test.greet": lambda name="World": f"Hello, {{name}}!"}}
'''
        with open(test_plugin_path, 'w') as f:
            f.write(test_plugin_code)

        # Set up callbacks
        manager.on_load(lambda name: print(f">> Plugin loaded: {name}"))
        manager.on_error(lambda name, e: print(f">> Plugin error in {name}: {e}"))

        # Load and start
        print("\n1. Loading plugin...")
        manager.load("test_plugin", {"debug": True})

        print("\n2. Starting plugin...")
        manager.start("test_plugin")

        print("\n3. Getting commands...")
        commands = manager.get_commands()
        for cmd_name, cmd_fn in commands.items():
            print(f"  - {cmd_name}: {cmd_fn()}")

        print("\n4. Stopping plugin...")
        manager.stop("test_plugin")

        print("\n5. Unloading plugin...")
        manager.unload("test_plugin")

        print("\nStatus:")
        print(json.dumps(manager.get_status(), indent=2))

        # Cleanup
        test_plugin_path.unlink()


if __name__ == "__main__":
    main()
