"""Bounded FIFO buffer for notifications during WebSocket disconnects."""

import time
from dataclasses import dataclass, field
from typing import List

# Only buffer notifications that carry execution outputs or status changes.
# Other RPCs (file ops, session management, etc.) are request/response and
# don't need buffering — the frontend retries them on reconnect.
BUFFERED_METHODS = {
    "kernel.output", "kernel.statusChanged", "kernel.died",
    # Mutation notifications (AI agent-initiated)
    "notebook.cellUpdated", "notebook.cellAdded", "notebook.cellDeleted",
    "file.written", "file.created", "file.deleted", "file.renamed", "file.moved",
}


@dataclass
class BufferedNotification:
    """A single buffered JSON-RPC notification."""

    method: str
    params: dict
    timestamp: float = field(default_factory=lambda: time.monotonic())


class OutputBuffer:
    """
    Bounded FIFO buffer that captures notifications while the WebSocket is down.

    Usage:
        buffer = OutputBuffer()
        buffer.set_connected(False)      # WebSocket disconnected
        buffer.add("kernel.output", {...})  # Buffered instead of dropped
        buffer.set_connected(True)       # WebSocket reconnected
        missed = buffer.drain()          # Returns buffered notifications
    """

    def __init__(self, max_size: int = 10_000, ttl_seconds: float = 300.0):
        self._max_size = max_size
        self._ttl_seconds = ttl_seconds
        self._connected = True  # Starts connected (no buffering)
        self._buffer: List[BufferedNotification] = []

    def set_connected(self, connected: bool) -> None:
        """Toggle buffering state. When disconnected, add() stores notifications."""
        self._connected = connected

    def should_buffer(self) -> bool:
        """Return True if notifications should be buffered (i.e., not connected)."""
        return not self._connected

    def add(self, method: str, params: dict) -> None:
        """
        Buffer a notification if the method is in BUFFERED_METHODS.

        Prunes stale entries, appends the new notification, and FIFO-evicts
        if the buffer exceeds max_size.
        """
        if method not in BUFFERED_METHODS:
            return

        self._prune_stale()
        self._buffer.append(BufferedNotification(method=method, params=params))

        # FIFO eviction: drop oldest entries if over max
        while len(self._buffer) > self._max_size:
            self._buffer.pop(0)

    def drain(self) -> List[dict]:
        """
        Return all buffered notifications as dicts and clear the buffer.

        Prunes stale entries first, then returns [{method, params}, ...].
        """
        self._prune_stale()
        result = [
            {"method": n.method, "params": n.params}
            for n in self._buffer
        ]
        self._buffer.clear()
        return result

    def clear(self) -> None:
        """Empty the buffer."""
        self._buffer.clear()

    def __len__(self) -> int:
        """Return the number of buffered notifications (for debugging)."""
        return len(self._buffer)

    def _prune_stale(self) -> None:
        """Remove entries older than ttl_seconds."""
        cutoff = time.monotonic() - self._ttl_seconds
        self._buffer = [n for n in self._buffer if n.timestamp > cutoff]
