"""File watcher for MCP Security Audit — polling-based watch mode.

Monitors source files for changes and triggers incremental re-scans.
Uses os.stat polling (no external dependencies like watchdog).
"""

from __future__ import annotations

import os
import signal
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Callable, Optional

from .formatters import format_text
from .models import ScanResult
from .scanner import MCPSecurityScanner, ALL_EXTENSIONS


class FileWatcher:
    """Polls files for modifications and triggers callbacks.

    Uses os.stat() to detect mtime changes — no external deps.
    Only watches files with extensions in ALL_EXTENSIONS (.py, .ts, .js, etc.).
    """

    def __init__(
        self,
        target: Path,
        scanner: MCPSecurityScanner,
        *,
        interval: float = 2.0,
        on_change: Optional[Callable[[list[ScanResult]], None]] = None,
    ) -> None:
        """Initialize watcher.

        Args:
            target: File or directory to watch.
            scanner: Scanner instance to use for re-scans.
            interval: Polling interval in seconds.
            on_change: Optional callback receiving scan results on change.
        """
        self.target = target
        self.scanner = scanner
        self.interval = max(0.5, interval)
        self.on_change = on_change or _default_on_change
        self._running = False
        self._mtimes: dict[str, float] = {}

    def _collect_files(self) -> list[Path]:
        """Collect all watchable source files under target."""
        if self.target.is_file():
            if self.target.suffix.lower() in ALL_EXTENSIONS:
                return [self.target]
            return []
        files: list[Path] = []
        for ext in ALL_EXTENSIONS:
            for p in self.target.rglob(f"*{ext}"):
                parts = p.parts
                if "node_modules" in parts or "dist" in parts or "__pycache__" in parts:
                    continue
                files.append(p)
        return sorted(files)

    def _snapshot_mtimes(self, files: list[Path]) -> dict[str, float]:
        """Get modification times for a list of files."""
        mtimes: dict[str, float] = {}
        for f in files:
            try:
                mtimes[str(f)] = os.stat(f).st_mtime
            except OSError:
                continue
        return mtimes

    def _detect_changes(self, new_mtimes: dict[str, float]) -> list[str]:
        """Compare mtimes and return paths of changed/new files."""
        changed: list[str] = []
        for path, mtime in new_mtimes.items():
            old_mtime = self._mtimes.get(path)
            if old_mtime is None or mtime > old_mtime:
                changed.append(path)
        return changed

    def run(self) -> None:
        """Start the watch loop. Blocks until Ctrl+C or stop().

        Performs an initial full scan, then polls for changes.
        """
        self._running = True
        _install_signal_handler(self)

        _print_header(self.target, self.interval)

        # Initial full scan
        files = self._collect_files()
        self._mtimes = self._snapshot_mtimes(files)

        _print_status(f"Watching {len(files)} files... (Ctrl+C to stop)")

        summary = self.scanner.scan(self.target)
        if summary.results:
            self.on_change(summary.results)

        # Poll loop
        while self._running:
            try:
                time.sleep(self.interval)
            except KeyboardInterrupt:
                break

            if not self._running:
                break

            files = self._collect_files()
            new_mtimes = self._snapshot_mtimes(files)
            changed = self._detect_changes(new_mtimes)
            self._mtimes = new_mtimes

            if not changed:
                continue

            # Re-scan only changed files
            results: list[ScanResult] = []
            for path_str in changed:
                path = Path(path_str)
                if path.exists():
                    result = self.scanner.scan_file(path)
                    results.append(result)

            if results:
                _print_status(f"{len(changed)} file(s) changed")
                self.on_change(results)

        _print_status("Watch mode stopped.")

    def stop(self) -> None:
        """Signal the watch loop to stop."""
        self._running = False


def _install_signal_handler(watcher: FileWatcher) -> None:
    """Install SIGINT handler for clean shutdown."""
    def _handler(sig: int, frame: object) -> None:
        watcher.stop()
    try:
        signal.signal(signal.SIGINT, _handler)
    except (OSError, ValueError):
        pass  # Not all platforms support signal handling in all contexts


def _print_header(target: Path, interval: float) -> None:
    """Print watch mode header."""
    print(f"\n{'=' * 60}")
    print("  MCP Security Audit — Watch Mode")
    print(f"{'=' * 60}")
    print(f"  Target: {target}")
    print(f"  Polling interval: {interval}s")
    print(f"{'=' * 60}\n")


def _print_status(message: str) -> None:
    """Print a timestamped status message."""
    ts = datetime.now().strftime("%H:%M:%S")
    print(f"  [{ts}] {message}")


def _default_on_change(results: list[ScanResult]) -> None:
    """Default callback: print text-formatted results."""
    ts = datetime.now().strftime("%H:%M:%S")
    total_findings = sum(len(r.findings) for r in results)
    files_str = ", ".join(Path(r.file).name for r in results)

    print(f"\n  [{ts}] Scan results for: {files_str}")
    if total_findings == 0:
        print(f"  [{ts}] No findings detected.\n")
        return

    output = format_text(results)
    print(output)
