"""
File Watching & Auto-Reload System for AIIQ
Monitors file changes and automatically reloads configuration, code, and resources
"""
import asyncio
import logging
from datetime import datetime
from pathlib import Path
from typing import Dict, Set, Callable, Optional, Any
from dataclasses import dataclass
from enum import Enum
import json
import hashlib

logger = logging.getLogger(__name__)


class ChangeType(Enum):
    """Types of file changes"""
    CREATED = "created"
    MODIFIED = "modified"
    DELETED = "deleted"
    MOVED = "moved"


@dataclass
class FileChangeEvent:
    """A file system change event"""
    path: str
    change_type: ChangeType
    timestamp: str
    old_path: Optional[str] = None  # For moved files
    checksum: Optional[str] = None


class FileWatcher:
    """
    Watches files and directories for changes.
    Supports debouncing, filtering, and hot-reload.
    """
    
    def __init__(
        self,
        watch_paths: list[str],
        debounce_seconds: float = 0.5,
        ignore_patterns: Optional[set[str]] = None
    ):
        """
        Args:
            watch_paths: Paths to watch
            debounce_seconds: Delay before processing changes (debouncing)
            ignore_patterns: File patterns to ignore (e.g., "*.pyc", "__pycache__")
        """
        self.watch_paths = [Path(p) for p in watch_paths]
        self.debounce_seconds = debounce_seconds
        self.ignore_patterns = ignore_patterns or {
            "*.pyc", "__pycache__", ".git", "node_modules", "*.log"
        }
        
        # File state tracking
        self._file_checksums: Dict[str, str] = {}
        self._pending_changes: Dict[str, FileChangeEvent] = {}
        self._subscribers: Dict[str, Set[Callable]] = {}
        
        # Watch state
        self._watching = False
        self._poll_task: Optional[asyncio.Task] = None
        
        # Initialize checksums
        self._initialize_state()
        
        logger.info("[File Watcher] Watching %d paths", len(watch_paths))
    
    def _initialize_state(self) -> None:
        """Initialize file checksums for watched paths"""
        for watch_path in self.watch_paths:
            if watch_path.is_dir():
                for file_path in watch_path.rglob("*"):
                    if file_path.is_file() and not self._should_ignore(file_path):
                        self._file_checksums[str(file_path)] = self._checksum(file_path)
            elif watch_path.is_file():
                self._file_checksums[str(watch_path)] = self._checksum(watch_path)
        
        logger.info(
            "[File Watcher] Initialized %d files",
            len(self._file_checksums)
        )
    
    def _should_ignore(self, path: Path) -> bool:
        """Check if path should be ignored"""
        path_str = str(path)
        
        for pattern in self.ignore_patterns:
            if path.match(pattern):
                return True
            
            # Check parent directories
            for parent in path.parents:
                if parent.match(pattern):
                    return True
        
        return False
    
    @staticmethod
    def _checksum(path: Path) -> str:
        """Calculate file checksum"""
        try:
            with open(path, 'rb') as f:
                return hashlib.md5(f.read()).hexdigest()
        except:
            return ""
    
    def subscribe(
        self,
        event_type: str,
        callback: Callable[[FileChangeEvent], Any]
    ) -> None:
        """
        Subscribe to file change events.
        
        Args:
            event_type: Event type (e.g., "file_changed", "config_changed")
            callback: Async callback function
        """
        if event_type not in self._subscribers:
            self._subscribers[event_type] = set()
        
        self._subscribers[event_type].add(callback)
        
        logger.debug("[File Watcher] Subscribed to %s: %s", event_type, callback.__name__)
    
    async def start_watching(self) -> None:
        """Start watching for file changes"""
        if self._watching:
            return
        
        self._watching = True
        self._poll_task = asyncio.create_task(self._poll_loop())
        
        logger.info("[File Watcher] Started watching")
    
    async def stop_watching(self) -> None:
        """Stop watching for file changes"""
        self._watching = False
        
        if self._poll_task:
            self._poll_task.cancel()
            try:
                await self._poll_task
            except asyncio.CancelledError:
                pass
        
        logger.info("[File Watcher] Stopped watching")
    
    async def _poll_loop(self) -> None:
        """Main polling loop"""
        while self._watching:
            try:
                await self._check_for_changes()
                await self._process_pending_changes()
                
            except Exception as e:
                logger.error("[File Watcher] Error in poll loop: %s", e)
            
            await asyncio.sleep(1)  # Poll every second
    
    async def _check_for_changes(self) -> None:
        """Check watched paths for changes"""
        current_files: set[str] = set()
        
        for watch_path in self.watch_paths:
            if not watch_path.exists():
                continue
            
            if watch_path.is_dir():
                for file_path in watch_path.rglob("*"):
                    if file_path.is_file() and not self._should_ignore(file_path):
                        path_str = str(file_path)
                        current_files.add(path_str)
                        
                        # Check for modifications
                        current_checksum = self._checksum(file_path)
                        old_checksum = self._file_checksums.get(path_str)
                        
                        if old_checksum and current_checksum != old_checksum:
                            self._queue_change(FileChangeEvent(
                                path=path_str,
                                change_type=ChangeType.MODIFIED,
                                timestamp=datetime.now().isoformat(),
                                checksum=current_checksum
                            ))
                        
                        # Update checksum
                        self._file_checksums[path_str] = current_checksum
            
            elif watch_path.is_file():
                path_str = str(watch_path)
                current_files.add(path_str)
                
                current_checksum = self._checksum(watch_path)
                old_checksum = self._file_checksums.get(path_str)
                
                if old_checksum and current_checksum != old_checksum:
                    self._queue_change(FileChangeEvent(
                        path=path_str,
                        change_type=ChangeType.MODIFIED,
                        timestamp=datetime.now().isoformat(),
                        checksum=current_checksum
                    ))
                
                self._file_checksums[path_str] = current_checksum
        
        # Check for deleted files
        deleted_files = set(self._file_checksums.keys()) - current_files
        for deleted_path in deleted_files:
            self._queue_change(FileChangeEvent(
                path=deleted_path,
                change_type=ChangeType.DELETED,
                timestamp=datetime.now().isoformat()
            ))
            
            # Remove from checksums
            del self._file_checksums[deleted_path]
    
    def _queue_change(self, event: FileChangeEvent) -> None:
        """Queue a change event (with debouncing)"""
        self._pending_changes[event.path] = event
        
        logger.debug(
            "[File Watcher] Detected %s: %s",
            event.change_type.value,
            event.path
        )
    
    async def _process_pending_changes(self) -> None:
        """Process pending changes after debounce delay"""
        if not self._pending_changes:
            return
        
        # Wait for debounce period
        await asyncio.sleep(self.debounce_seconds)
        
        # Process changes
        events = list(self._pending_changes.values())
        self._pending_changes.clear()
        
        for event in events:
            await self._notify_subscribers(event)
    
    async def _notify_subscribers(self, event: FileChangeEvent) -> None:
        """Notify subscribers of file change"""
        # Determine event type
        event_type = "file_changed"
        
        if event.path.endswith((".json", ".yaml", ".yml", ".toml", ".ini", ".env")):
            event_type = "config_changed"
        elif event.path.endswith((".py", ".js", ".ts")):
            event_type = "code_changed"
        
        # Notify subscribers
        if event_type in self._subscribers:
            for callback in self._subscribers[event_type]:
                try:
                    result = callback(event)
                    if asyncio.iscoroutine(result):
                        await result
                except Exception as e:
                    logger.error(
                        "[File Watcher] Error in callback %s: %s",
                        callback.__name__, e
                    )


class AutoReloader:
    """
    Automatic code/configuration reloading.
    Responds to file changes by reloading modules, configs, etc.
    """
    
    def __init__(self):
        self._watchers: Dict[str, FileWatcher] = {}
        self._reload_hooks: Dict[str, Callable] = {}
        
        logger.info("[Auto Reload] Initialized")
    
    def watch_directory(
        self,
        path: str,
        reload_hook: Optional[Callable] = None
    ) -> FileWatcher:
        """
        Watch a directory for changes.
        
        Args:
            path: Directory path to watch
            reload_hook: Optional callback for reload
            
        Returns:
            FileWatcher instance
        """
        watcher = FileWatcher([path])
        
        # Subscribe to changes
        if reload_hook:
            watcher.subscribe("file_changed", reload_hook)
            watcher.subscribe("config_changed", reload_hook)
            watcher.subscribe("code_changed", reload_hook)
        
        # Store watcher
        self._watchers[path] = watcher
        
        # Start watching
        asyncio.create_task(watcher.start_watching())
        
        return watcher
    
    def register_reload_hook(
        self,
        name: str,
        hook: Callable
    ) -> None:
        """
        Register a reload hook.
        
        Args:
            name: Hook name
            hook: Reload callback
        """
        self._reload_hooks[name] = hook
        logger.info("[Auto Reload] Registered hook: %s", name)
    
    async def trigger_reload(self, module_name: str) -> bool:
        """
        Manually trigger a module reload.
        
        Args:
            module_name: Python module path (e.g., "ai_orchestrator.services.model_router")
            
        Returns:
            Success status
        """
        try:
            import importlib
            import sys
            
            if module_name in sys.modules:
                logger.info("[Auto Reload] Reloading module: %s", module_name)
                
                # Reload the module
                module = sys.modules[module_name]
                importlib.reload(module)
                
                # Call reload hooks
                for hook_name, hook in self._reload_hooks.items():
                    try:
                        result = hook(module_name, "module_reloaded")
                        if asyncio.iscoroutine(result):
                            await result
                    except Exception as e:
                        logger.warning(
                            "[Auto Reload] Hook %s failed: %s",
                            hook_name, e
                        )
                
                return True
            
        except Exception as e:
            logger.error("[Auto Reload] Error reloading %s: %s", module_name, e)
        
        return False
    
    async def reload_config(self, config_path: str) -> Dict[str, Any]:
        """
        Reload configuration from file.
        
        Args:
            config_path: Path to config file
            
        Returns:
            Loaded configuration
        """
        config_file = Path(config_path)
        
        if not config_file.exists():
            logger.error("[Auto Reload] Config not found: %s", config_path)
            return {}
        
        try:
            # Parse based on extension
            if config_file.suffix == ".json":
                with open(config_file, 'r') as f:
                    config = json.load(f)
            
            elif config_file.suffix in [".yaml", ".yml"]:
                import yaml
                with open(config_file, 'r') as f:
                    config = yaml.safe_load(f)
            
            elif config_file.suffix in [".toml"]:
                import toml
                with open(config_file, 'r') as f:
                    config = toml.load(f)
            
            elif config_file.name == ".env":
                from dotenv import load_dotenv
                load_dotenv(config_file)
                config = dict(os.environ)
            
            else:
                logger.warning(
                    "[Auto Reload] Unsupported config type: %s",
                    config_file.suffix
                )
                return {}
            
            # Notify subscribers
            for hook in self._reload_hooks.values():
                try:
                    result = hook("config", config)
                    if asyncio.iscoroutine(result):
                        await result
                except:
                    pass
            
            logger.info("[Auto Reload] Reloaded config: %s", config_path)
            
            return config
            
        except Exception as e:
            logger.error("[Auto Reload] Error reloading config: %s", e)
            return {}
    
    async def stop_all(self) -> None:
        """Stop all file watchers"""
        for watcher in self._watchers.values():
            await watcher.stop_watching()
        
        self._watchers.clear()
        
        logger.info("[Auto Reload] Stopped all watchers")


# Global instance
_auto_reloader: Optional[AutoReloader] = None


def get_auto_reloader() -> AutoReloader:
    """Get the global auto-reloader"""
    global _auto_reloader
    if _auto_reloader is None:
        _auto_reloader = AutoReloader()
    return _auto_reloader


class HotReloadMiddleware:
    """
    Middleware for hot-reload in development.
    Automatically reloads Python modules when files change.
    """
    
    def __init__(self, watch_paths: list[str]):
        self.auto_reloader = get_auto_reloader()
        
        # Watch paths
        for path in watch_paths:
            self.auto_reloader.watch_directory(
                path,
                reload_hook=self._on_file_changed
            )
        
        logger.info("[Hot Reload] Watching %d paths", len(watch_paths))
    
    async def _on_file_changed(self, event: FileChangeEvent) -> None:
        """Handle file change event"""
        if event.change_type == ChangeType.MODIFIED:
            # Check if it's a Python module
            if event.path.endswith(".py"):
                # Convert path to module name
                module_name = self._path_to_module(event.path)
                
                if module_name:
                    await self.auto_reloader.trigger_reload(module_name)
    
    def _path_to_module(self, path: str) -> Optional[str]:
        """Convert file path to Python module name"""
        # Normalize path
        path_obj = Path(path).resolve()
        
        # Check if in ai_orchestrator
        if "ai_orchestrator" in path_obj.parts:
            # Find ai_orchestrator index
            idx = path_obj.parts.index("ai_orchestrator")
            
            # Get module parts
            module_parts = list(path_obj.parts[idx:])
            
            # Convert .py to module
            module_parts[-1] = module_parts[-1].replace(".py", "")
            
            # Remove __init__
            if module_parts[-1] == "__init__":
                module_parts = module_parts[:-1]
            
            return ".".join(module_parts)
        
        return None
