"""File system change monitoring sensor."""

from __future__ import annotations

import difflib
import time
from pathlib import Path
from typing import Optional

try:
    from watchdog.events import FileSystemEventHandler
    from watchdog.observers import Observer

    HAS_WATCHDOG = True
except ImportError:  # pragma: no cover
    class FileSystemEventHandler:  # type: ignore[no-redef]
        """Fallback event handler when watchdog is unavailable."""

    class Observer:  # type: ignore[no-redef]
        """Fallback observer when watchdog is unavailable."""

    HAS_WATCHDOG = False

from neurovault.sensors.base import BaseSensor


class FileChangeHandler(FileSystemEventHandler):
    """Collect file modification events and extract meaningful diffs."""

    ALLOWED_EXTENSIONS = {
        ".md",
        ".txt",
        ".py",
        ".js",
        ".ts",
        ".tsx",
        ".jsx",
        ".json",
        ".yaml",
        ".yml",
        ".toml",
        ".csv",
        ".sql",
        ".html",
        ".css",
        ".sh",
        ".go",
        ".rs",
        ".java",
    }
    IGNORE_PATTERNS = {
        "node_modules",
        ".git",
        "__pycache__",
        ".next",
        "dist",
        "build",
        ".venv",
        "venv",
        ".tox",
    }
    MAX_FILE_SIZE = 500_000

    def __init__(self, path_blocklist: list[str] | None = None):
        self._changes: list[tuple[str, dict]] = []
        self._file_cache: dict[str, str] = {}
        self._last_event: dict[str, float] = {}
        self._debounce_seconds = 5.0
        self._path_blocklist = [p.replace("~", str(Path.home())) for p in (path_blocklist or [])]

    def on_modified(self, event):
        if event.is_directory:
            return

        path = Path(event.src_path)
        if path.suffix not in self.ALLOWED_EXTENSIONS:
            return

        if any(part in self.IGNORE_PATTERNS for part in path.parts):
            return
        if any(blocked and blocked in str(path) for blocked in self._path_blocklist):
            return

        try:
            if path.stat().st_size > self.MAX_FILE_SIZE:
                return
        except OSError:
            return

        now = time.time()
        last = self._last_event.get(str(path), 0.0)
        if now - last < self._debounce_seconds:
            return
        self._last_event[str(path)] = now

        try:
            current = path.read_text(errors="ignore")
        except Exception:
            return

        previous = self._file_cache.get(str(path), "")
        self._file_cache[str(path)] = current
        if not previous:
            return

        diff_text = self._extract_diff(str(path), previous, current)
        if diff_text and len(diff_text.strip()) > 20:
            self._changes.append(
                (
                    diff_text,
                    {
                        "file_path": str(path),
                        "file_name": path.name,
                        "extension": path.suffix,
                        "content_type": "code"
                        if path.suffix in {".py", ".js", ".ts", ".go", ".rs"}
                        else "text",
                    },
                )
            )

    def _extract_diff(self, filepath: str, previous: str, current: str) -> str:
        diff = difflib.unified_diff(previous.splitlines(), current.splitlines(), lineterm="", n=2)
        added = [line[1:] for line in diff if line.startswith("+") and not line.startswith("+++")]
        if not added:
            return ""
        return f"Changes in {Path(filepath).name}:\n" + "\n".join(added[:50])

    def collect(self) -> list[tuple[str, dict]]:
        changes = self._changes.copy()
        self._changes.clear()
        return changes


class FileWatcherSensor(BaseSensor):
    """Monitor file system for document changes."""

    name = "file_watcher"
    source_tag = "file"
    poll_interval = 2.0

    def __init__(self, watch_paths: list[str] | None = None, path_blocklist: list[str] | None = None):
        self._watch_paths = watch_paths or [
            str(Path.home() / "Documents"),
            str(Path.home() / "Desktop"),
            str(Path.home() / "Projects"),
        ]
        self._observer: Optional[Observer] = None
        self._handler = FileChangeHandler(path_blocklist=path_blocklist) if HAS_WATCHDOG else None

    def start(self) -> None:
        self.start_watching()

    def start_watching(self) -> None:
        if not HAS_WATCHDOG or self._handler is None:
            return
        self._observer = Observer()
        for path in self._watch_paths:
            p = Path(path).expanduser()
            if p.exists():
                self._observer.schedule(self._handler, str(p), recursive=True)
        self._observer.start()

    def poll(self) -> list[tuple[str, dict]]:
        if self._handler is None:
            return []
        return self._handler.collect()

    def stop(self) -> None:
        if self._observer is None:
            return
        self._observer.stop()
        self._observer.join(timeout=5)
        self._observer = None
