"""Real-time filesystem event bridge for the inbox watcher.

Wraps `watchdog` to give the watcher near-instant wake-ups on file changes,
with a polling interval (60s default) acting as a safety net for:
  - Cloud sync folders (OneDrive, Dropbox, iCloud) that emit fake events
  - Network/mapped drives where `ReadDirectoryChangesW` silently fails
  - Kernel event-queue overflow (watchdog drops events under burst load)
  - Offline changes (events the kernel never saw because the server was down)

The event handler is *only* a wake-up signal — it sets an `asyncio.Event`
and lets the existing poll loop do the actual work. That way debouncing,
batching, settle-time, mundane-file filtering, and quarantine all keep
working through one code path.

Design rules:
  1. Watchdog is optional. If the import fails, the watcher falls back to
     pure polling without any user-visible change.
  2. Events never do work directly. They only `event.set()` the drain event.
     The poll loop handles the "what changed" logic via its own scan.
  3. A single event triggers a cycle. 100 events in 5ms also trigger one
     cycle — the event is a boolean, not a queue.
  4. Observer errors are caught and logged; the poll loop is the fallback,
     so observer failure degrades gracefully.
"""

from __future__ import annotations

import asyncio
import logging
import threading
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from agntspace.automation.raw_watcher import RawWatcher

log = logging.getLogger(__name__)

try:
    from watchdog.events import FileSystemEventHandler
    from watchdog.observers import Observer
    _WATCHDOG_AVAILABLE = True
except ImportError:
    _WATCHDOG_AVAILABLE = False
    FileSystemEventHandler = object  # type: ignore[misc,assignment]
    Observer = None  # type: ignore[assignment]


# Only these extensions trigger a wake-up. Everything else (OneDrive
# metadata files, .DS_Store, lockfiles, etc.) is ignored at the event
# layer so we don't thrash the poll loop.
_SUPPORTED_EXTS = {  # arch:deterministic — file extensions the ingest pipeline can technically handle (capability surface, not user preference); mirrors raw_watcher._SUPPORTED_EXTS
    # Text + web
    ".md", ".txt", ".html", ".htm", ".rst",
    # Documents (Phase 3: docx, epub)
    ".pdf", ".docx", ".epub",
    # Images
    ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp",
    # Tabular / structured
    ".csv", ".tsv", ".yaml", ".yml", ".json", ".jsonl",
    # Transcripts / subtitles
    ".vtt", ".srt",
    # Code
    ".py", ".js", ".ts",
    # Audio (Phase 3)
    ".mp3", ".wav", ".m4a", ".flac", ".ogg", ".opus",
    # Video (Phase 3)
    ".mp4", ".webm", ".mov", ".mkv",
}

# Directories to ignore. Matches raw_watcher._SKIP_DIRS so the event layer
# agrees with the poll layer on what counts as "agent internals".
_SKIP_DIR_NAMES = {  # arch:deterministic — structural vault internals and universal noise dirs (.git, node_modules, OS caches); cross-platform fact, not a policy the user chooses
    "agntspace", "agntspace-inbox", "agntspace-journal",
    "agntspace-knowledge", "agntspace-projects", "agntspace-goals",
    ".obsidian", ".git", ".trash",
    "node_modules", "__pycache__", ".venv", "venv",
    "AppData", "Application Data", ".cache", ".local", ".config", ".vscode",
    "Library", ".Trash",
    "OneDrive", "Dropbox", "Google Drive", "iCloud Drive",
    "Downloads", ".npm", ".nuget", ".cargo", ".rustup",
}


class _WakeupHandler(FileSystemEventHandler):
    """Translate filesystem events into wake-up signals on the main loop."""

    def __init__(
        self,
        loop: asyncio.AbstractEventLoop,
        drain_event: asyncio.Event,
        stats: dict,
    ) -> None:
        super().__init__()
        self._loop = loop
        self._drain_event = drain_event
        self._stats = stats

    def _should_wake(self, src_path: str) -> bool:
        """Filter out events we know are noise."""
        if not src_path:
            return False
        p = Path(src_path)
        # Skip events inside known agent/OS/system dirs
        for part in p.parts:
            if part in _SKIP_DIR_NAMES or part.startswith("."):
                return False
        # Only react to supported content extensions
        if p.suffix and p.suffix.lower() not in _SUPPORTED_EXTS:
            return False
        return True

    def _wake(self) -> None:
        """Set the drain event on the main asyncio loop (thread-safe)."""
        try:
            self._loop.call_soon_threadsafe(self._drain_event.set)
            self._stats["wakes"] = self._stats.get("wakes", 0) + 1
        except RuntimeError:
            # Loop is closed (server shutting down)
            pass

    # Event methods run on the watchdog observer thread.
    def on_created(self, event) -> None:  # type: ignore[no-untyped-def]
        if event.is_directory:
            return
        if self._should_wake(event.src_path):
            self._stats["created"] = self._stats.get("created", 0) + 1
            self._wake()

    def on_modified(self, event) -> None:  # type: ignore[no-untyped-def]
        if event.is_directory:
            return
        if self._should_wake(event.src_path):
            self._stats["modified"] = self._stats.get("modified", 0) + 1
            self._wake()

    def on_moved(self, event) -> None:  # type: ignore[no-untyped-def]
        if event.is_directory:
            return
        dst = getattr(event, "dest_path", "")
        if self._should_wake(dst) or self._should_wake(event.src_path):
            self._stats["moved"] = self._stats.get("moved", 0) + 1
            self._wake()

    def on_deleted(self, event) -> None:  # type: ignore[no-untyped-def]
        if event.is_directory:
            return
        if self._should_wake(event.src_path):
            self._stats["deleted"] = self._stats.get("deleted", 0) + 1
            self._wake()


class FileSystemEventBridge:
    """Owns a watchdog Observer that wakes the watcher on file changes.

    Lifecycle:
        bridge = FileSystemEventBridge(watcher, root_dir)
        bridge.start()   # starts observer thread; no-op if watchdog missing
        ...
        bridge.stop()    # clean shutdown on server exit

    Status:
        bridge.status() → dict with available/running/wakes/errors fields
    """

    def __init__(self, watcher: RawWatcher, root_dir: Path) -> None:
        self._watcher = watcher
        self._root_dir = root_dir
        self._observer = None
        self._handler: _WakeupHandler | None = None
        self._lock = threading.Lock()
        self._stats: dict = {
            "wakes": 0,
            "created": 0,
            "modified": 0,
            "moved": 0,
            "deleted": 0,
            "last_error": "",
        }

    @property
    def available(self) -> bool:
        return _WATCHDOG_AVAILABLE

    @property
    def running(self) -> bool:
        with self._lock:
            return self._observer is not None and self._observer.is_alive()

    def start(self) -> bool:
        """Start the observer. Returns True if watchdog is active, False if
        not (missing dependency, root dir unavailable, or start failed)."""
        if not _WATCHDOG_AVAILABLE:
            log.info("watchdog not installed — falling back to poll-only watcher")
            return False

        if not self._root_dir or not self._root_dir.is_dir():
            log.warning("fs_events: root_dir %s is not a directory", self._root_dir)
            return False

        with self._lock:
            if self._observer is not None:
                return True  # already running

            try:
                loop = asyncio.get_running_loop()
            except RuntimeError:
                log.error("fs_events: no running event loop — cannot start")
                return False

            # Use the existing drain_event if the watcher has one; otherwise
            # create a new event that the watcher is already listening on.
            drain_event = getattr(self._watcher, "_event_drain", None)
            if drain_event is None:
                # Will be wired up by raw_watcher.
                log.error("fs_events: watcher has no _event_drain — cannot wire up")
                return False

            self._handler = _WakeupHandler(loop, drain_event, self._stats)
            try:
                self._observer = Observer()
                self._observer.schedule(
                    self._handler, str(self._root_dir), recursive=True
                )
                self._observer.start()
                log.info(
                    "fs_events: watchdog observer started on %s",
                    self._root_dir,
                )
                return True
            except Exception as exc:
                self._stats["last_error"] = f"{type(exc).__name__}: {exc}"
                log.exception("fs_events: failed to start observer")
                self._observer = None
                return False

    def stop(self) -> None:
        """Stop the observer. Safe to call multiple times."""
        with self._lock:
            if self._observer is None:
                return
            try:
                self._observer.stop()
                self._observer.join(timeout=2)
            except Exception:
                log.exception("fs_events: observer stop failed")
            self._observer = None
            self._handler = None

    def status(self) -> dict:
        """Return observer state for /api/watcher/status."""
        return {
            "available": self.available,
            "running": self.running,
            "root_dir": str(self._root_dir) if self._root_dir else "",
            **self._stats,
        }
