"""Per-connection asyncio.Queue manager for SSE fan-out.

Each connected client gets its own queue. The ConnectionManager handles:
- Registering new connections (with restaurant_id scope)
- Enqueueing events to matching connections
- Cleaning up on disconnect

Queue size is capped to prevent slow clients from consuming unbounded memory.
"""

import asyncio
import contextlib
import uuid
from dataclasses import dataclass, field

import logfire

from app.realtime.events import DomainEvent

MAX_QUEUE_SIZE = 256


@dataclass
class Connection:
    """A single SSE client connection."""

    id: str
    restaurant_id: str
    queue: asyncio.Queue[DomainEvent | None] = field(
        default_factory=lambda: asyncio.Queue(maxsize=MAX_QUEUE_SIZE)
    )


class ConnectionManager:
    """Manages per-connection queues for SSE event delivery."""

    def __init__(self) -> None:
        self._connections: dict[str, Connection] = {}

    def register(self, restaurant_id: str) -> Connection:
        """Create a new connection and return it for streaming."""
        conn = Connection(id=str(uuid.uuid4()), restaurant_id=restaurant_id)
        self._connections[conn.id] = conn
        logfire.info(
            "sse_connection_registered",
            connection_id=conn.id,
            restaurant_id=restaurant_id,
            active_connections=len(self._connections),
        )
        return conn

    def disconnect(self, connection_id: str) -> None:
        """Remove a connection and signal its queue to stop."""
        conn = self._connections.pop(connection_id, None)
        if conn is not None:
            # Put sentinel to unblock any waiting get()
            with contextlib.suppress(asyncio.QueueFull):
                conn.queue.put_nowait(None)
            logfire.info(
                "sse_connection_disconnected",
                connection_id=connection_id,
                active_connections=len(self._connections),
            )

    async def enqueue(self, event: DomainEvent) -> int:
        """Push an event to all connections matching the event's restaurant_id.

        Returns the number of connections that received the event.
        Drops the event for connections whose queues are full (slow client).
        """
        delivered = 0
        stale: list[str] = []

        for conn_id, conn in self._connections.items():
            if conn.restaurant_id != event.restaurant_id:
                continue
            try:
                conn.queue.put_nowait(event)
                delivered += 1
            except asyncio.QueueFull:
                stale.append(conn_id)
                logfire.warning(
                    "sse_queue_full_dropping_client",
                    connection_id=conn_id,
                    restaurant_id=conn.restaurant_id,
                )

        # Disconnect stale clients whose queues overflowed
        for conn_id in stale:
            self.disconnect(conn_id)

        return delivered

    @property
    def active_count(self) -> int:
        return len(self._connections)


# Singleton shared across the app lifetime.
manager = ConnectionManager()
