"""
File Watch Tool - Monitor file system changes

Provides file watching capabilities with:
- Pattern-based file filtering (glob patterns)
- Debounced change notifications
- Recursive directory watching
- Event types: created, modified, deleted, moved

Example:
    tool = WatchTool()

    # Start watching
    result = await tool.execute(
        path="/project/src",
        patterns=["*.py", "*.js"],
        recursive=True,
    )

    # The tool registers a callback for changes
"""

import asyncio
import fnmatch
import time
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, Callable, Optional


class FileChangeType(Enum):
    """Types of file changes"""

    CREATED = "created"
    MODIFIED = "modified"
    DELETED = "deleted"
    MOVED = "moved"


@dataclass
class FileChange:
    """
    Represents a file system change

    Attributes:
        path: Path to the changed file
        change_type: Type of change
        timestamp: When the change occurred
        old_path: Previous path (for MOVED events)
    """

    path: Path
    change_type: FileChangeType
    timestamp: float = field(default_factory=time.time)
    old_path: Optional[Path] = None

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary"""
        return {
            "path": str(self.path),
            "change_type": self.change_type.value,
            "timestamp": self.timestamp,
            "old_path": str(self.old_path) if self.old_path else None,
        }


@dataclass
class WatchConfig:
    """
    Configuration for file watching

    Attributes:
        patterns: Glob patterns to match (e.g., ["*.py", "*.js"])
        ignore_patterns: Patterns to ignore (e.g., ["*.pyc", "__pycache__"])
        recursive: Whether to watch subdirectories
        debounce_ms: Debounce delay in milliseconds
    """

    patterns: list[str] = field(default_factory=lambda: ["*"])
    ignore_patterns: list[str] = field(default_factory=list)
    recursive: bool = True
    debounce_ms: int = 100


@dataclass
class WatchResult:
    """
    Result of starting a file watcher

    Attributes:
        watch_id: Unique identifier for the watch
        path: Path being watched
        config: Watch configuration
        started_at: When watching started
    """

    watch_id: str
    path: Path
    config: WatchConfig
    started_at: float = field(default_factory=time.time)


# Type alias for change callback
ChangeCallback = Callable[[list[FileChange]], None]


class FileWatcher:
    """
    File system watcher with debouncing and pattern filtering

    Monitors a directory for file changes and notifies via callback.
    Supports glob patterns for filtering and debouncing to batch rapid changes.

    Example:
        watcher = FileWatcher(
            path=Path("/project/src"),
            config=WatchConfig(patterns=["*.py"]),
            callback=lambda changes: print(changes),
        )
        await watcher.start()

        # Later...
        await watcher.stop()
    """

    def __init__(
        self,
        path: Path,
        config: WatchConfig,
        callback: ChangeCallback,
    ):
        """
        Initialize FileWatcher

        Args:
            path: Directory to watch
            config: Watch configuration
            callback: Function to call when changes occur
        """
        self.path = path.resolve()
        self.config = config
        self.callback = callback
        self._running = False
        self._pending_changes: list[FileChange] = []
        self._debounce_task: Optional[asyncio.Task] = None
        self._watch_task: Optional[asyncio.Task] = None
        self._observer: Optional[Any] = None

    async def start(self) -> None:
        """
        Start watching for file changes

        Uses watchdog library if available, otherwise falls back to polling.
        """
        if self._running:
            return

        self._running = True

        try:
            # Try to use watchdog for efficient watching
            from watchdog.events import FileSystemEventHandler
            from watchdog.observers import Observer

            await self._start_watchdog()
        except ImportError:
            # Fall back to polling
            await self._start_polling()

    async def _start_watchdog(self) -> None:
        """Start watching using watchdog library"""
        from watchdog.events import FileSystemEvent, FileSystemEventHandler
        from watchdog.observers import Observer

        watcher = self

        class EventHandler(FileSystemEventHandler):
            def on_any_event(self, event: FileSystemEvent):
                if event.is_directory:
                    return

                # Map event type
                if event.event_type == "created":
                    change_type = FileChangeType.CREATED
                elif event.event_type == "modified":
                    change_type = FileChangeType.MODIFIED
                elif event.event_type == "deleted":
                    change_type = FileChangeType.DELETED
                elif event.event_type == "moved":
                    change_type = FileChangeType.MOVED
                else:
                    return

                path = Path(event.src_path)
                old_path = Path(event.dest_path) if hasattr(event, "dest_path") else None

                # Check if file matches patterns
                if not watcher._matches_patterns(path):
                    return

                change = FileChange(
                    path=path,
                    change_type=change_type,
                    old_path=old_path,
                )

                # Queue the change with debouncing
                asyncio.get_event_loop().call_soon_threadsafe(
                    lambda: asyncio.create_task(watcher._queue_change(change))
                )

        self._observer = Observer()
        self._observer.schedule(
            EventHandler(),
            str(self.path),
            recursive=self.config.recursive,
        )
        self._observer.start()

    async def _start_polling(self) -> None:
        """Start watching using polling (fallback)"""
        self._watch_task = asyncio.create_task(self._poll_loop())

    async def _poll_loop(self) -> None:
        """Polling loop for file changes"""
        # Build initial state
        file_states: dict[str, float] = {}

        def scan_directory():
            states = {}
            if self.config.recursive:
                for path in self.path.rglob("*"):
                    if path.is_file() and self._matches_patterns(path):
                        states[str(path)] = path.stat().st_mtime
            else:
                for path in self.path.glob("*"):
                    if path.is_file() and self._matches_patterns(path):
                        states[str(path)] = path.stat().st_mtime
            return states

        file_states = scan_directory()

        while self._running:
            await asyncio.sleep(1.0)  # Poll every second

            new_states = scan_directory()

            # Detect changes
            for path_str, mtime in new_states.items():
                path = Path(path_str)
                if path_str not in file_states:
                    # New file
                    await self._queue_change(
                        FileChange(
                            path=path,
                            change_type=FileChangeType.CREATED,
                        )
                    )
                elif mtime > file_states[path_str]:
                    # Modified file
                    await self._queue_change(
                        FileChange(
                            path=path,
                            change_type=FileChangeType.MODIFIED,
                        )
                    )

            # Detect deletions
            for path_str in file_states:
                if path_str not in new_states:
                    await self._queue_change(
                        FileChange(
                            path=Path(path_str),
                            change_type=FileChangeType.DELETED,
                        )
                    )

            file_states = new_states

    async def stop(self) -> None:
        """Stop watching for file changes"""
        self._running = False

        if self._observer:
            self._observer.stop()
            self._observer.join()
            self._observer = None

        if self._watch_task:
            self._watch_task.cancel()
            try:
                await self._watch_task
            except asyncio.CancelledError:
                pass
            self._watch_task = None

        if self._debounce_task:
            self._debounce_task.cancel()
            try:
                await self._debounce_task
            except asyncio.CancelledError:
                pass
            self._debounce_task = None

    def _matches_patterns(self, path: Path) -> bool:
        """
        Check if a path matches the configured patterns

        Args:
            path: Path to check

        Returns:
            True if path matches include patterns and not ignore patterns
        """
        filename = path.name
        path_str = str(path)

        # Check ignore patterns first
        for pattern in self.config.ignore_patterns:
            if fnmatch.fnmatch(filename, pattern):
                return False
            if fnmatch.fnmatch(path_str, pattern):
                return False
            # Check if any part of the path matches the ignore pattern
            for part in path.parts:
                if fnmatch.fnmatch(part, pattern):
                    return False

        # Check include patterns
        if self.config.patterns == ["*"]:
            return True

        for pattern in self.config.patterns:
            if fnmatch.fnmatch(filename, pattern):
                return True
            if fnmatch.fnmatch(path_str, pattern):
                return True

        return False

    async def _queue_change(self, change: FileChange) -> None:
        """
        Queue a file change with debouncing

        Args:
            change: The file change to queue
        """
        self._pending_changes.append(change)

        # Cancel existing debounce task
        if self._debounce_task:
            self._debounce_task.cancel()
            try:
                await self._debounce_task
            except asyncio.CancelledError:
                pass

        # Start new debounce task
        self._debounce_task = asyncio.create_task(self._debounce())

    async def _debounce(self) -> None:
        """Debounce changes before notifying callback"""
        await asyncio.sleep(self.config.debounce_ms / 1000.0)

        if self._pending_changes:
            changes = self._pending_changes.copy()
            self._pending_changes.clear()

            # Deduplicate changes (keep latest for each path)
            seen_paths: dict[str, FileChange] = {}
            for change in changes:
                path_str = str(change.path)
                if path_str not in seen_paths or change.timestamp > seen_paths[path_str].timestamp:
                    seen_paths[path_str] = change

            # Notify callback
            self.callback(list(seen_paths.values()))


class WatchManager:
    """
    Manager for multiple file watchers

    Provides a central interface for creating and managing watchers.

    Example:
        manager = WatchManager()

        watch_id = await manager.watch(
            path=Path("/project/src"),
            patterns=["*.py"],
            callback=lambda changes: print(changes),
        )

        # Later...
        await manager.unwatch(watch_id)
    """

    def __init__(self):
        """Initialize WatchManager"""
        self._watchers: dict[str, FileWatcher] = {}
        self._counter = 0

    async def watch(
        self,
        path: Path,
        callback: ChangeCallback,
        patterns: Optional[list[str]] = None,
        ignore_patterns: Optional[list[str]] = None,
        recursive: bool = True,
        debounce_ms: int = 100,
    ) -> str:
        """
        Start watching a directory

        Args:
            path: Directory to watch
            callback: Function to call when changes occur
            patterns: Glob patterns to match
            ignore_patterns: Patterns to ignore
            recursive: Whether to watch subdirectories
            debounce_ms: Debounce delay in milliseconds

        Returns:
            Watch ID for managing this watcher
        """
        self._counter += 1
        watch_id = f"watch_{self._counter}"

        config = WatchConfig(
            patterns=patterns or ["*"],
            ignore_patterns=ignore_patterns or [],
            recursive=recursive,
            debounce_ms=debounce_ms,
        )

        watcher = FileWatcher(
            path=path,
            config=config,
            callback=callback,
        )

        await watcher.start()
        self._watchers[watch_id] = watcher

        return watch_id

    async def unwatch(self, watch_id: str) -> bool:
        """
        Stop watching a directory

        Args:
            watch_id: Watch ID returned from watch()

        Returns:
            True if watcher was stopped, False if not found
        """
        if watch_id not in self._watchers:
            return False

        watcher = self._watchers.pop(watch_id)
        await watcher.stop()
        return True

    async def unwatch_all(self) -> int:
        """
        Stop all watchers

        Returns:
            Number of watchers stopped
        """
        count = len(self._watchers)
        for watcher in self._watchers.values():
            await watcher.stop()
        self._watchers.clear()
        return count

    def list_watches(self) -> list[str]:
        """List all active watch IDs"""
        return list(self._watchers.keys())


# Global manager instance
_watch_manager: Optional[WatchManager] = None


def get_watch_manager() -> WatchManager:
    """Get the global WatchManager instance"""
    global _watch_manager
    if _watch_manager is None:
        _watch_manager = WatchManager()
    return _watch_manager


def reset_watch_manager() -> None:
    """Reset the global WatchManager (for testing)"""
    global _watch_manager
    _watch_manager = None


# Import base tool for BaseTool inheritance
from groknroll.tools.base_tool import BaseTool


class WatchTool(BaseTool):
    """
    Tool for watching file system changes

    Provides file watching capabilities integrated with the tool system.

    Example:
        tool = WatchTool()
        result = await tool.execute(
            path="/project/src",
            patterns=["*.py"],
            recursive=True,
        )
    """

    @property
    def name(self) -> str:
        return "watch"

    @property
    def description(self) -> str:
        return "Watch a directory for file changes"

    async def execute(
        self,
        path: str,
        patterns: Optional[list[str]] = None,
        ignore_patterns: Optional[list[str]] = None,
        recursive: bool = True,
        debounce_ms: int = 100,
        callback: Optional[ChangeCallback] = None,
    ) -> WatchResult:
        """
        Start watching a directory

        Args:
            path: Directory to watch
            patterns: Glob patterns to match
            ignore_patterns: Patterns to ignore
            recursive: Whether to watch subdirectories
            debounce_ms: Debounce delay in milliseconds
            callback: Optional callback for changes

        Returns:
            WatchResult with watch ID and configuration
        """
        watch_path = Path(path).resolve()

        if not watch_path.exists():
            raise FileNotFoundError(f"Path does not exist: {path}")

        if not watch_path.is_dir():
            raise ValueError(f"Path is not a directory: {path}")

        # Use default callback if none provided
        if callback is None:
            callback = self._default_callback

        manager = get_watch_manager()
        watch_id = await manager.watch(
            path=watch_path,
            callback=callback,
            patterns=patterns,
            ignore_patterns=ignore_patterns,
            recursive=recursive,
            debounce_ms=debounce_ms,
        )

        config = WatchConfig(
            patterns=patterns or ["*"],
            ignore_patterns=ignore_patterns or [],
            recursive=recursive,
            debounce_ms=debounce_ms,
        )

        return WatchResult(
            watch_id=watch_id,
            path=watch_path,
            config=config,
        )

    def _default_callback(self, changes: list[FileChange]) -> None:
        """Default callback that prints changes"""
        for change in changes:
            print(f"[{change.change_type.value}] {change.path}")
