"""
Incremental Memory System — Phase 4 of Kodara pipeline.

Watches the filesystem for changes and automatically re-indexes
only the changed files. Uses watchdog for cross-platform file events.

Design decision: Poll-based fallback (2s interval) is used when watchdog
native backends are unavailable, ensuring reliability on all platforms.
"""

from __future__ import annotations

import hashlib
import time
from pathlib import Path
from threading import Thread
from typing import Callable


SKIP_DIRS = {
    "node_modules", ".git", "__pycache__", "venv", ".venv",
    "build", "dist", "target", ".next", "out", "tmp",
}


class FileWatcher:
    """
    Monitors a directory for file changes using polling.

    Calls `on_change(changed=[...], deleted=[...])` when files are
    added, modified, or removed.
    """

    def __init__(
        self,
        root: str | Path,
        on_change: Callable[[list[str], list[str]], None],
        poll_interval: float = 2.0,
    ):
        self.root = Path(root)
        self.on_change = on_change
        self.poll_interval = poll_interval
        self._running = False
        self._thread: Thread | None = None
        self._hashes: dict[str, str] = {}

    def start(self) -> None:
        """Start watching in a background thread."""
        self._running = True
        self._hashes = self._snapshot()
        self._thread = Thread(target=self._loop, daemon=True)
        self._thread.start()

    def stop(self) -> None:
        """Stop the watcher thread."""
        self._running = False
        if self._thread:
            self._thread.join(timeout=5)

    def _loop(self) -> None:
        while self._running:
            time.sleep(self.poll_interval)
            try:
                self._check()
            except Exception:
                pass  # Never crash the watcher thread

    def _check(self) -> None:
        current = self._snapshot()

        changed = [
            p for p, h in current.items()
            if p not in self._hashes or self._hashes[p] != h
        ]
        deleted = [p for p in self._hashes if p not in current]

        if changed or deleted:
            self._hashes = current
            self.on_change(changed, deleted)

    def _snapshot(self) -> dict[str, str]:
        """Hash all tracked files in the directory."""
        hashes: dict[str, str] = {}
        for path in self.root.rglob("*"):
            if not path.is_file():
                continue
            rel_parts = path.relative_to(self.root).parts
            if any(p in SKIP_DIRS or p.startswith(".") for p in rel_parts[:-1]):
                continue
            try:
                content = path.read_bytes()
                hashes[str(path)] = hashlib.sha256(content).hexdigest()
            except OSError:
                pass
        return hashes
