"""In-process pub/sub for live-tail event streaming.

``EventBus`` is a tiny, async-first fan-out broker. Publishers call
``publish(kind, payload)`` and never block; if a subscriber's per-kind
queue is full, the oldest event is dropped so the slow consumer cannot
stall the producer. Subscribers receive events through an async
iterator that yields ``{**payload, "_kind": kind}``.

The bus is intentionally not thread-safe: it is designed to run on a
single asyncio event loop, which matches the FastAPI/uvicorn
deployment model used elsewhere in this project.
"""

from __future__ import annotations

import asyncio
from collections.abc import AsyncIterator


class EventBus:
    """Per-kind fan-out bus with drop-oldest overflow per subscriber."""

    def __init__(self, max_queue_size: int = 256) -> None:
        self._max_queue_size = max_queue_size
        self._subscribers: dict[str, list[asyncio.Queue[dict]]] = {}

    async def publish(self, kind: str, payload: dict) -> None:
        """Fan ``payload`` out to every subscriber of ``kind``.

        Never blocks. If a subscriber queue is full, the oldest queued
        event is discarded to make room.
        """
        event = {**payload, "_kind": kind}
        for queue in list(self._subscribers.get(kind, ())):
            self._enqueue_or_drop_oldest(queue, event)

    def _enqueue_or_drop_oldest(
        self, queue: asyncio.Queue[dict], event: dict
    ) -> None:
        try:
            queue.put_nowait(event)
        except asyncio.QueueFull:
            # Drop the oldest event to make room, then enqueue.
            queue.get_nowait()
            queue.task_done()
            queue.put_nowait(event)

    def subscribe(self, kinds: list[str]) -> AsyncIterator[dict]:
        """Return an async iterator delivering events for any of ``kinds``."""
        queue: asyncio.Queue[dict] = asyncio.Queue(maxsize=self._max_queue_size)
        for kind in kinds:
            self._subscribers.setdefault(kind, []).append(queue)
        return self._iterator(queue)

    def subscribe_raw(
        self, kinds: list[str]
    ) -> tuple[asyncio.Queue[dict], AsyncIterator[dict]]:
        """Like :meth:`subscribe` but also returns the underlying queue.

        Most consumers should use :meth:`subscribe`. The live-tail
        endpoints use ``subscribe_raw`` so they can race
        ``queue.get()`` against a heartbeat sleep without poisoning
        the subscription iterator: ``asyncio.wait_for`` cancels the
        inner future on timeout, which terminates an async generator
        at its ``await`` point and breaks all subsequent
        ``__anext__`` calls. Polling ``queue.get()`` directly avoids
        that because ``Queue.get`` is idempotent under cancellation.

        Callers MUST pair this with :meth:`unsubscribe` in a
        ``try/finally`` to release the queue when the consumer
        disconnects — otherwise the bus leaks a queue per stream.
        """
        queue: asyncio.Queue[dict] = asyncio.Queue(maxsize=self._max_queue_size)
        for kind in kinds:
            self._subscribers.setdefault(kind, []).append(queue)
        return queue, self._iterator(queue)

    def unsubscribe(
        self, queue: asyncio.Queue[dict], kinds: list[str]
    ) -> None:
        """Remove ``queue`` from each kind's subscriber list.

        Idempotent: missing entries are silently ignored so callers
        can run the cleanup in a ``finally`` without worrying about
        double-unsubscribe on disconnect-then-GeneratorExit.
        """
        for kind in kinds:
            subs = self._subscribers.get(kind)
            if not subs:
                continue
            try:
                subs.remove(queue)
            except ValueError:
                pass
            if not subs:
                self._subscribers.pop(kind, None)

    async def _iterator(self, queue: asyncio.Queue[dict]) -> AsyncIterator[dict]:
        while True:
            yield await queue.get()
            queue.task_done()

    def stats(self) -> dict[str, int]:
        """Snapshot of subscriber counts per kind.

        Used by ``/api/health`` (SP19) and the admin diagnostics page.
        Returns ``{kind: count}`` for every kind with at least one
        subscriber; kinds with zero subscribers are omitted.
        """
        return {
            kind: len(subs)
            for kind, subs in self._subscribers.items()
            if subs
        }


def get_event_bus() -> EventBus:
    """Return the process-wide EventBus attached to the FastAPI app state.

    Late-imports ``cyclone.api`` to avoid a circular import at module
    load time (the API module imports pubsub indirectly via lifespan).
    Tests stub this via monkeypatch.
    """
    from cyclone.api import app  # late import to avoid circular

    return app.state.event_bus
