"""File system watcher with debounced change notifications."""

from __future__ import annotations

import fnmatch
import threading
from pathlib import Path
from typing import Callable

from watchdog.events import FileSystemEvent, FileSystemEventHandler
from watchdog.observers import Observer

# Directories to always ignore
IGNORE_DIRS = {
    "__pycache__",
    ".git",
    "node_modules",
    ".venv",
    "venv",
    ".mypy_cache",
    ".pytest_cache",
    ".ruff_cache",
    "*.egg-info",
}

# File patterns to ignore
IGNORE_PATTERNS = {"*.pyc", "*.pyo", ".DS_Store", "*.swp", "*.swo", "*~"}


def _should_ignore(path: str) -> bool:
    """Return True if *path* should be ignored based on directory and file patterns."""
    parts = Path(path).parts
    for part in parts:
        for pattern in IGNORE_DIRS:
            if fnmatch.fnmatch(part, pattern):
                return True
    filename = Path(path).name
    for pattern in IGNORE_PATTERNS:
        if fnmatch.fnmatch(filename, pattern):
            return True
    return False


class _DebouncedHandler(FileSystemEventHandler):
    """Watchdog handler that debounces rapid file system events.

    Calls *on_change* at most once per *debounce_seconds* window.
    """

    def __init__(
        self,
        on_change: Callable[[str], None],
        debounce_seconds: float = 0.3,
    ) -> None:
        super().__init__()
        self._on_change = on_change
        self._debounce_seconds = debounce_seconds
        self._timer: threading.Timer | None = None
        self._lock = threading.Lock()
        self._last_path: str = ""

    def on_any_event(self, event: FileSystemEvent) -> None:
        if event.is_directory:
            return
        if _should_ignore(event.src_path):
            return

        with self._lock:
            self._last_path = event.src_path
            if self._timer is not None:
                self._timer.cancel()
            self._timer = threading.Timer(
                self._debounce_seconds,
                self._fire,
            )
            self._timer.daemon = True
            self._timer.start()

    def _fire(self) -> None:
        with self._lock:
            path = self._last_path
            self._timer = None
        self._on_change(path)

    def cancel(self) -> None:
        """Cancel any pending debounced callback."""
        with self._lock:
            if self._timer is not None:
                self._timer.cancel()
                self._timer = None


class FileWatcher:
    """Watch directories for file changes and invoke a callback.

    Usage::

        def on_change(path: str) -> None:
            print(f"Changed: {path}")

        watcher = FileWatcher(watch_paths=["src/", "tests/"], on_change=on_change)
        watcher.start()
        # ... later ...
        watcher.stop()
    """

    def __init__(
        self,
        watch_paths: list[str | Path],
        on_change: Callable[[str], None],
        debounce_seconds: float = 0.3,
    ) -> None:
        self._watch_paths = [Path(p) for p in watch_paths]
        self._handler = _DebouncedHandler(on_change, debounce_seconds)
        self._observer = Observer()

    def start(self) -> None:
        """Start watching for file changes."""
        for path in self._watch_paths:
            if path.is_dir():
                self._observer.schedule(self._handler, str(path), recursive=True)
        self._observer.start()

    def stop(self) -> None:
        """Stop watching and clean up."""
        self._handler.cancel()
        self._observer.stop()
        self._observer.join(timeout=3.0)
