"""Service for watching project files and broadcasting change events via SSE."""

import asyncio
import json
import logging
from datetime import datetime, timezone
from pathlib import Path

from watchfiles import awatch, Change

logger = logging.getLogger(__name__)


class WatcherService:
    """Watches project files and notifies connected SSE clients of changes."""

    def __init__(self, project_path: Path) -> None:
        self.project_path = project_path
        self._clients: set[asyncio.Queue[str]] = set()
        self._watch_task: asyncio.Task | None = None
        self._debounce_timers: dict[str, asyncio.TimerHandle] = {}
        self._debounce_delay = 0.5  # 500ms debounce

    @property
    def client_count(self) -> int:
        return len(self._clients)

    def register_client(self) -> asyncio.Queue[str]:
        """Register a new SSE client and return its event queue."""
        queue: asyncio.Queue[str] = asyncio.Queue()
        self._clients.add(queue)
        logger.info("SSE client registered (total: %d)", len(self._clients))
        return queue

    def deregister_client(self, queue: asyncio.Queue[str]) -> None:
        """Remove an SSE client."""
        self._clients.discard(queue)
        logger.info("SSE client deregistered (total: %d)", len(self._clients))

    def _broadcast(self, event_type: str) -> None:
        """Send an event to all connected clients."""
        timestamp = datetime.now(timezone.utc).isoformat()
        data = json.dumps({"type": event_type, "timestamp": timestamp})
        message = f"event: {event_type}\ndata: {data}\n\n"

        dead_clients: list[asyncio.Queue[str]] = []
        for queue in self._clients:
            try:
                queue.put_nowait(message)
            except asyncio.QueueFull:
                dead_clients.append(queue)

        for client in dead_clients:
            self._clients.discard(client)

    def _debounced_broadcast(self, event_type: str) -> None:
        """Broadcast an event, debounced to avoid flooding."""
        # Cancel any pending timer for this event type
        if event_type in self._debounce_timers:
            self._debounce_timers[event_type].cancel()

        loop = asyncio.get_event_loop()
        handle = loop.call_later(self._debounce_delay, self._broadcast, event_type)
        self._debounce_timers[event_type] = handle

    def _classify_change(self, path: str) -> str | None:
        """Classify a file change into an event type."""
        if "issues.jsonl" in path or path.endswith(".beads"):
            return "tasks_changed"
        if "openspec/" in path:
            return "specs_changed"
        return None

    async def start(self) -> None:
        """Start watching project files in the background."""
        if self._watch_task and not self._watch_task.done():
            return

        self._watch_task = asyncio.create_task(self._watch_loop())
        logger.info("File watcher started for %s", self.project_path)

    async def stop(self) -> None:
        """Stop the file watcher."""
        if self._watch_task and not self._watch_task.done():
            self._watch_task.cancel()
            try:
                await self._watch_task
            except asyncio.CancelledError:
                pass
        self._watch_task = None

        # Cancel any pending debounce timers
        for handle in self._debounce_timers.values():
            handle.cancel()
        self._debounce_timers.clear()

        logger.info("File watcher stopped")

    async def _watch_loop(self) -> None:
        """Main watch loop — monitors project files for changes."""
        beads_path = self.project_path / ".beads"
        openspec_path = self.project_path / "openspec"

        # Build list of paths to watch (only those that exist)
        watch_paths: list[Path] = []
        if beads_path.exists():
            watch_paths.append(beads_path)
        if openspec_path.exists():
            watch_paths.append(openspec_path)

        if not watch_paths:
            logger.warning("No .beads/ or openspec/ directories found at %s", self.project_path)
            return

        try:
            async for changes in awatch(*watch_paths):
                for change_type, change_path in changes:
                    event_type = self._classify_change(change_path)
                    if event_type:
                        self._debounced_broadcast(event_type)
                        logger.debug("File change: %s %s → %s", change_type, change_path, event_type)
        except asyncio.CancelledError:
            raise
        except Exception:
            logger.exception("File watcher error")
