"""
Brynq File Watcher — Agents watch local files and auto-react to changes.

Register glob patterns on directories. When files matching the pattern change,
the watcher posts a notification to a broker channel. Agents can then react
without polling.

Storage:
  state/broker/watches/{watch_id}.json — watch registration
  state/broker/watches/.snapshots/{watch_id}.json — file state snapshots

No external dependencies — uses stdlib os.stat for change detection.
"""

import json
import os
import time
import uuid
from datetime import datetime, timezone
from fnmatch import fnmatch
from pathlib import Path
from typing import Optional

from brynq.server import (
    BROKER_DIR,
    CHANNELS_DIR,
    SESSION_ID,
    SESSION_NAME,
    PLATFORM,
    _append_jsonl_sync,
    _write_json_sync,
)

WATCHES_DIR = BROKER_DIR / "watches"
SNAPSHOTS_DIR = WATCHES_DIR / ".snapshots"

for d in (WATCHES_DIR, SNAPSHOTS_DIR):
    d.mkdir(parents=True, exist_ok=True)


def register_watch(
    directory: str,
    pattern: str = "*",
    channel: str = "file-events",
    recursive: bool = True,
) -> dict:
    """Register a file watch. Returns the watch metadata."""
    watch_id = uuid.uuid4().hex[:10]
    watch = {
        "watch_id": watch_id,
        "directory": str(Path(directory).resolve()),
        "pattern": pattern,
        "channel": channel,
        "recursive": recursive,
        "session_id": SESSION_ID,
        "session_name": SESSION_NAME,
        "platform": PLATFORM,
        "created": datetime.now(timezone.utc).isoformat(),
        "active": True,
    }
    _write_json_sync(WATCHES_DIR / f"{watch_id}.json", watch)
    # Take initial snapshot
    _take_snapshot(watch)
    return watch


def remove_watch(watch_id: str) -> bool:
    """Remove a file watch by ID."""
    path = WATCHES_DIR / f"{watch_id}.json"
    if not path.exists():
        return False
    path.unlink(missing_ok=True)
    snap = SNAPSHOTS_DIR / f"{watch_id}.json"
    snap.unlink(missing_ok=True)
    return True


def list_watches() -> list[dict]:
    """List all active file watches."""
    watches = []
    for f in sorted(WATCHES_DIR.glob("*.json")):
        try:
            data = json.loads(f.read_text("utf-8"))
            if data.get("active", True):
                watches.append(data)
        except (json.JSONDecodeError, OSError):
            continue
    return watches


def _scan_files(directory: str, pattern: str, recursive: bool) -> dict[str, float]:
    """Scan directory and return {relative_path: mtime} for matching files."""
    root = Path(directory)
    if not root.exists():
        return {}
    files = {}
    glob_pattern = f"**/{pattern}" if recursive else pattern
    for p in root.glob(glob_pattern):
        if p.is_file():
            try:
                rel = str(p.relative_to(root))
                files[rel] = p.stat().st_mtime
            except (OSError, ValueError):
                continue
    return files


def _take_snapshot(watch: dict) -> dict[str, float]:
    """Take a snapshot of current file state for a watch."""
    files = _scan_files(watch["directory"], watch["pattern"], watch["recursive"])
    snap_path = SNAPSHOTS_DIR / f"{watch['watch_id']}.json"
    _write_json_sync(snap_path, {"files": files, "taken": datetime.now(timezone.utc).isoformat()})
    return files


def _load_snapshot(watch_id: str) -> dict[str, float]:
    """Load the previous snapshot for a watch."""
    snap_path = SNAPSHOTS_DIR / f"{watch_id}.json"
    try:
        data = json.loads(snap_path.read_text("utf-8"))
        return data.get("files", {})
    except (json.JSONDecodeError, FileNotFoundError, OSError):
        return {}


def check_watch(watch: dict) -> dict:
    """Check a single watch for changes. Returns diff and posts to channel if changes found."""
    old_files = _load_snapshot(watch["watch_id"])
    new_files = _scan_files(watch["directory"], watch["pattern"], watch["recursive"])

    old_set = set(old_files.keys())
    new_set = set(new_files.keys())

    created = new_set - old_set
    deleted = old_set - new_set
    modified = {
        f for f in old_set & new_set
        if new_files[f] != old_files[f]
    }

    changes = {
        "created": sorted(created),
        "modified": sorted(modified),
        "deleted": sorted(deleted),
    }

    total = len(created) + len(modified) + len(deleted)

    if total > 0:
        # Update snapshot
        _take_snapshot(watch)

        # Build change summary
        parts = []
        if created:
            parts.append(f"+{len(created)} new")
        if modified:
            parts.append(f"~{len(modified)} changed")
        if deleted:
            parts.append(f"-{len(deleted)} deleted")
        summary = ", ".join(parts)

        # List first few files
        file_list = []
        for f in list(created)[:5]:
            file_list.append(f"  + {f}")
        for f in list(modified)[:5]:
            file_list.append(f"  ~ {f}")
        for f in list(deleted)[:5]:
            file_list.append(f"  - {f}")
        if total > 15:
            file_list.append(f"  ... and {total - 15} more")

        detail = "\n".join(file_list)
        message = f"[file-watch:{watch['watch_id']}] {summary} in {watch['directory']} ({watch['pattern']})\n{detail}"

        entry = {
            "id": uuid.uuid4().hex[:12],
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "session_id": watch.get("session_id", SESSION_ID),
            "session_name": "brynq-watcher",
            "platform": "brynq",
            "channel": watch["channel"],
            "msg_type": "file_ready",
            "message": message,
            "watch_id": watch["watch_id"],
            "changes": changes,
        }
        _append_jsonl_sync(CHANNELS_DIR / f"{watch['channel']}.jsonl", entry)

    return {"total_changes": total, **changes}


def check_all_watches() -> list[dict]:
    """Check all active watches for changes. Returns list of results."""
    results = []
    for watch in list_watches():
        result = check_watch(watch)
        result["watch_id"] = watch["watch_id"]
        result["directory"] = watch["directory"]
        results.append(result)
    return results
