#!/usr/bin/env python3
"""
GENESIS SKILL HOT-RELOADER
==========================
Enables live skill updates without restarting the system.

Features:
    - File watching for skill changes
    - Safe module reloading
    - Skill validation before loading
    - Rollback on failure
    - Version tracking

Usage:
    reloader = SkillReloader()
    reloader.watch("/path/to/skills")
    reloader.reload_skill("my_skill")
"""

import importlib
import importlib.util
import json
import os
import sys
import time
import hashlib
import threading
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Any, Optional, Callable
import traceback


@dataclass
class SkillInfo:
    """Information about a loaded skill."""
    name: str
    path: str
    module: Any
    version: str
    loaded_at: str
    file_hash: str
    dependencies: List[str] = field(default_factory=list)
    last_error: Optional[str] = None


@dataclass
class ReloadResult:
    """Result of a skill reload operation."""
    success: bool
    skill_name: str
    message: str
    old_version: Optional[str] = None
    new_version: Optional[str] = None
    rollback_available: bool = False


class SkillValidator:
    """Validates skills before loading."""

    required_attributes = [
        "SKILL_NAME",
        "SKILL_VERSION",
    ]

    def validate(self, module: Any) -> List[str]:
        """Validate a skill module. Returns list of issues."""
        issues = []

        # Check required attributes
        for attr in self.required_attributes:
            if not hasattr(module, attr):
                issues.append(f"Missing required attribute: {attr}")

        # Check for main entry point
        if not (hasattr(module, 'execute') or hasattr(module, 'run') or hasattr(module, 'main')):
            issues.append("Missing entry point (execute, run, or main)")

        return issues


class FileWatcher:
    """Watches skill files for changes."""

    def __init__(self, callback: Callable[[Path], None]):
        self.callback = callback
        self.watching: Dict[Path, float] = {}
        self.running = False
        self.thread: Optional[threading.Thread] = None
        self.interval = 1.0  # Check every second

    def start(self, paths: List[Path]):
        """Start watching paths."""
        for path in paths:
            if path.is_file():
                self.watching[path] = path.stat().st_mtime
            elif path.is_dir():
                for p in path.glob("*.py"):
                    self.watching[p] = p.stat().st_mtime

        self.running = True
        self.thread = threading.Thread(target=self._watch_loop, daemon=True)
        self.thread.start()

    def stop(self):
        """Stop watching."""
        self.running = False
        if self.thread:
            self.thread.join(timeout=2)

    def _watch_loop(self):
        """Main watch loop."""
        while self.running:
            for path, last_mtime in list(self.watching.items()):
                try:
                    current_mtime = path.stat().st_mtime
                    if current_mtime > last_mtime:
                        self.watching[path] = current_mtime
                        self.callback(path)
                except FileNotFoundError:
                    del self.watching[path]
                except Exception:
                    pass

            time.sleep(self.interval)


class SkillReloader:
    """
    Manages hot-reloading of Genesis skills.
    """

    def __init__(self, skills_dir: Path = None):
        self.skills_dir = skills_dir or Path(__file__).parent.parent / "skills"
        self.loaded_skills: Dict[str, SkillInfo] = {}
        self.skill_backups: Dict[str, Any] = {}  # For rollback
        self.validator = SkillValidator()
        self.watcher: Optional[FileWatcher] = None
        self.on_reload_callbacks: List[Callable[[str, bool], None]] = []

    def _get_file_hash(self, path: Path) -> str:
        """Get hash of file contents."""
        return hashlib.md5(path.read_bytes()).hexdigest()

    def _extract_version(self, module: Any) -> str:
        """Extract version from module."""
        if hasattr(module, 'SKILL_VERSION'):
            return str(module.SKILL_VERSION)
        if hasattr(module, '__version__'):
            return str(module.__version__)
        return "unknown"

    def load_skill(self, skill_path: Path) -> ReloadResult:
        """Load a skill from file."""
        skill_name = skill_path.stem

        try:
            # Load module from file
            spec = importlib.util.spec_from_file_location(skill_name, skill_path)
            if spec is None or spec.loader is None:
                return ReloadResult(
                    success=False,
                    skill_name=skill_name,
                    message="Could not create module spec"
                )

            module = importlib.util.module_from_spec(spec)
            sys.modules[skill_name] = module
            spec.loader.exec_module(module)

            # Validate
            issues = self.validator.validate(module)
            if issues:
                return ReloadResult(
                    success=False,
                    skill_name=skill_name,
                    message=f"Validation failed: {'; '.join(issues)}"
                )

            # Store skill info
            version = self._extract_version(module)
            self.loaded_skills[skill_name] = SkillInfo(
                name=skill_name,
                path=str(skill_path),
                module=module,
                version=version,
                loaded_at=datetime.now().isoformat(),
                file_hash=self._get_file_hash(skill_path)
            )

            return ReloadResult(
                success=True,
                skill_name=skill_name,
                message="Skill loaded successfully",
                new_version=version
            )

        except Exception as e:
            return ReloadResult(
                success=False,
                skill_name=skill_name,
                message=f"Load error: {str(e)}"
            )

    def reload_skill(self, skill_name: str) -> ReloadResult:
        """Reload an already loaded skill."""
        if skill_name not in self.loaded_skills:
            # Try to find and load it
            skill_path = self.skills_dir / f"{skill_name}.py"
            if skill_path.exists():
                return self.load_skill(skill_path)
            return ReloadResult(
                success=False,
                skill_name=skill_name,
                message="Skill not found"
            )

        info = self.loaded_skills[skill_name]
        skill_path = Path(info.path)

        # Check if file changed
        current_hash = self._get_file_hash(skill_path)
        if current_hash == info.file_hash:
            return ReloadResult(
                success=True,
                skill_name=skill_name,
                message="No changes detected"
            )

        # Backup current module
        self.skill_backups[skill_name] = info.module
        old_version = info.version

        try:
            # Remove from sys.modules to force reload
            if skill_name in sys.modules:
                del sys.modules[skill_name]

            # Reload
            result = self.load_skill(skill_path)

            if result.success:
                result.old_version = old_version
                result.rollback_available = True

                # Notify callbacks
                for callback in self.on_reload_callbacks:
                    try:
                        callback(skill_name, True)
                    except:
                        pass

            return result

        except Exception as e:
            # Restore backup
            if skill_name in self.skill_backups:
                self.loaded_skills[skill_name].module = self.skill_backups[skill_name]
                sys.modules[skill_name] = self.skill_backups[skill_name]

            return ReloadResult(
                success=False,
                skill_name=skill_name,
                message=f"Reload failed, rolled back: {str(e)}",
                rollback_available=True
            )

    def rollback_skill(self, skill_name: str) -> ReloadResult:
        """Rollback to previous version of skill."""
        if skill_name not in self.skill_backups:
            return ReloadResult(
                success=False,
                skill_name=skill_name,
                message="No backup available"
            )

        try:
            old_module = self.skill_backups[skill_name]
            sys.modules[skill_name] = old_module

            if skill_name in self.loaded_skills:
                self.loaded_skills[skill_name].module = old_module
                self.loaded_skills[skill_name].loaded_at = datetime.now().isoformat()

            return ReloadResult(
                success=True,
                skill_name=skill_name,
                message="Rolled back to previous version"
            )

        except Exception as e:
            return ReloadResult(
                success=False,
                skill_name=skill_name,
                message=f"Rollback failed: {str(e)}"
            )

    def watch(self, additional_paths: List[Path] = None):
        """Start watching for skill changes."""
        paths = [self.skills_dir]
        if additional_paths:
            paths.extend(additional_paths)

        def on_change(path: Path):
            skill_name = path.stem
            print(f"[SkillReloader] Detected change in {path.name}")
            result = self.reload_skill(skill_name)
            print(f"[SkillReloader] {result.message}")

        self.watcher = FileWatcher(on_change)
        self.watcher.start(paths)
        print(f"[SkillReloader] Watching {len(paths)} paths for changes")

    def stop_watching(self):
        """Stop watching for changes."""
        if self.watcher:
            self.watcher.stop()
            print("[SkillReloader] Stopped watching")

    def load_all(self) -> Dict[str, ReloadResult]:
        """Load all skills from skills directory."""
        results = {}

        for skill_path in self.skills_dir.glob("*.py"):
            if skill_path.name.startswith('_'):
                continue

            result = self.load_skill(skill_path)
            results[skill_path.stem] = result

        return results

    def get_skill(self, skill_name: str) -> Optional[Any]:
        """Get a loaded skill module."""
        if skill_name in self.loaded_skills:
            return self.loaded_skills[skill_name].module
        return None

    def list_skills(self) -> List[Dict]:
        """List all loaded skills."""
        return [
            {
                "name": info.name,
                "version": info.version,
                "loaded_at": info.loaded_at,
                "path": info.path
            }
            for info in self.loaded_skills.values()
        ]

    def get_status(self) -> Dict:
        """Get reloader status."""
        return {
            "skills_loaded": len(self.loaded_skills),
            "skills_dir": str(self.skills_dir),
            "watching": self.watcher is not None and self.watcher.running,
            "skills": self.list_skills()
        }


def main():
    """CLI for skill reloader."""
    import argparse
    parser = argparse.ArgumentParser(description="Genesis Skill Reloader")
    parser.add_argument("command", nargs="?", choices=["list", "load", "reload", "watch", "status"],
                       default="status")
    parser.add_argument("--skill", help="Skill name for load/reload")
    parser.add_argument("--dir", help="Skills directory")
    args = parser.parse_args()

    skills_dir = Path(args.dir) if args.dir else None
    reloader = SkillReloader(skills_dir)

    if args.command == "status":
        print("Skill Reloader Status")
        print("=" * 40)
        status = reloader.get_status()
        print(f"Skills Dir: {status['skills_dir']}")
        print(f"Loaded: {status['skills_loaded']}")
        print(f"Watching: {status['watching']}")

    elif args.command == "list":
        results = reloader.load_all()
        print(f"Loaded {len(results)} skills:\n")
        for name, result in results.items():
            status = "✓" if result.success else "✗"
            print(f"  {status} {name}: {result.message}")

    elif args.command == "load":
        if not args.skill:
            print("--skill required")
            return
        skill_path = reloader.skills_dir / f"{args.skill}.py"
        result = reloader.load_skill(skill_path)
        print(f"{'✓' if result.success else '✗'} {result.message}")

    elif args.command == "reload":
        if not args.skill:
            print("--skill required")
            return
        result = reloader.reload_skill(args.skill)
        print(f"{'✓' if result.success else '✗'} {result.message}")

    elif args.command == "watch":
        print("Starting skill watcher (Ctrl+C to stop)...")
        reloader.load_all()
        reloader.watch()
        try:
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            reloader.stop_watching()
            print("\nStopped")


if __name__ == "__main__":
    main()
