"""Real-time file watcher — detect changes and scan for secrets during sessions."""

from __future__ import annotations

import logging
import sys
import threading
import time
from pathlib import Path

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

from nestdx.policy.secrets import SecretsScanner
from nestdx.session.models import FileChange, PolicyViolation

logger = logging.getLogger(__name__)

DEFAULT_IGNORE_PATTERNS: list[str] = [
    ".git/",
    "node_modules/",
    "__pycache__/",
    ".nestdx/",
    ".pyc",
]

# Debounce window in seconds
_DEBOUNCE_SECONDS = 0.1


class _WatcherHandler(FileSystemEventHandler):
    """Watchdog event handler that records changes and scans for secrets."""

    def __init__(
        self,
        project_root: Path,
        secrets_scanner: SecretsScanner,
        ignore_patterns: list[str],
    ):
        super().__init__()
        self.project_root = project_root
        self.secrets_scanner = secrets_scanner
        self.ignore_patterns = ignore_patterns

        self._lock = threading.Lock()
        self._changes: dict[str, FileChange] = {}
        self._violations: list[PolicyViolation] = []
        self._last_event_time: dict[str, float] = {}

    def _should_ignore(self, path: str) -> bool:
        for pattern in self.ignore_patterns:
            if pattern in path:
                return True
        return False

    def _relative_path(self, abs_path: str) -> str:
        try:
            return str(Path(abs_path).relative_to(self.project_root))
        except ValueError:
            return abs_path

    def _is_debounced(self, rel_path: str) -> bool:
        now = time.monotonic()
        with self._lock:
            last = self._last_event_time.get(rel_path, 0.0)
            if now - last < _DEBOUNCE_SECONDS:
                return True
            self._last_event_time[rel_path] = now
        return False

    def _record_change(self, rel_path: str, change_type: str) -> None:
        with self._lock:
            self._changes[rel_path] = FileChange(path=rel_path, change_type=change_type)

    def _scan_and_warn(self, abs_path: str, rel_path: str) -> None:
        violations = self.secrets_scanner.scan_file(Path(abs_path))
        if violations:
            for v in violations:
                v.file = rel_path
            with self._lock:
                self._violations.extend(violations)
            # Real-time warning to stderr (non-blocking)
            for v in violations:
                print(
                    f"\033[33mWarning:\033[0m Secret detected in {rel_path}:{v.line} "
                    f"— {v.message}",
                    file=sys.stderr,
                )

    def on_modified(self, event: FileSystemEvent) -> None:
        if event.is_directory:
            return
        if self._should_ignore(event.src_path):
            return
        rel_path = self._relative_path(event.src_path)
        if self._is_debounced(rel_path):
            return
        self._record_change(rel_path, "modified")
        self._scan_and_warn(event.src_path, rel_path)

    def on_created(self, event: FileSystemEvent) -> None:
        if event.is_directory:
            return
        if self._should_ignore(event.src_path):
            return
        rel_path = self._relative_path(event.src_path)
        if self._is_debounced(rel_path):
            return
        self._record_change(rel_path, "added")
        self._scan_and_warn(event.src_path, rel_path)

    def on_deleted(self, event: FileSystemEvent) -> None:
        if event.is_directory:
            return
        if self._should_ignore(event.src_path):
            return
        rel_path = self._relative_path(event.src_path)
        self._record_change(rel_path, "deleted")

    def get_results(self) -> tuple[list[FileChange], list[PolicyViolation]]:
        with self._lock:
            changes = list(self._changes.values())
            violations = list(self._violations)
        return changes, violations


class FileWatcher:
    """Watch a project directory for file changes during a session.

    Runs a watchdog Observer in a background thread. Records all file changes
    and triggers real-time secrets scanning on created/modified files.
    """

    def __init__(
        self,
        project_root: Path,
        secrets_scanner: SecretsScanner,
        ignore_patterns: list[str] | None = None,
    ):
        self.project_root = project_root
        self._ignore = ignore_patterns or DEFAULT_IGNORE_PATTERNS
        self._handler = _WatcherHandler(project_root, secrets_scanner, self._ignore)
        self._observer = Observer()
        self._started = False

    def start(self) -> bool:
        """Start watching the project directory (non-blocking).

        Returns True if the watcher started successfully, False if it failed
        (e.g. permission error, inotify limit reached). The caller should
        fall back to git-diff-only change detection on failure.
        """
        if self._started:
            return True
        try:
            self._observer.schedule(self._handler, str(self.project_root), recursive=True)
            self._observer.start()
            self._started = True
            logger.info("File watcher started for %s", self.project_root)
            return True
        except OSError as e:
            # Covers inotify limit (Linux), permission errors, etc.
            logger.warning("File watcher failed to start: %s. Falling back to git-diff-only.", e)
            self._started = False
            return False
        except Exception as e:
            logger.warning("File watcher failed to start: %s. Falling back to git-diff-only.", e)
            self._started = False
            return False

    def stop(self) -> tuple[list[FileChange], list[PolicyViolation]]:
        """Stop watching and return accumulated changes and violations."""
        if self._started:
            try:
                self._observer.stop()
                self._observer.join(timeout=5)
            except Exception as e:
                logger.warning("Error stopping file watcher: %s", e)
            self._started = False
            logger.info("File watcher stopped.")
        return self._handler.get_results()

    @property
    def is_running(self) -> bool:
        return self._started
