"""Phase 19 — Server-Sent Events stream over the EventBus.

  GET /api/events/stream
    headers:
      Last-Event-ID: <id>   # optional resume — replays ring entries after id
    query (Debug R2 fallback):
      ?lastEventId=<id>     # cross-page-reload resume; header takes precedence

  Each event is emitted as:
      id: <ev_id>
      event: engine_event
      data: <json EngineEvent>

The endpoint never closes from the server side — clients close by
disconnecting. ``EventSourceResponse`` from sse-starlette handles the
heartbeat / framing. Subscribers are bound to the BUS for the lifetime
of the request.

Debug R2: browsers only auto-set ``Last-Event-ID`` after the first event
fires on a given EventSource — cross-page-reload resume requires the
client to thread the id through the URL. We accept ``?lastEventId=``
as a fallback; if both header and query are present, the header wins
(that's the auto-resume path inside a single page load).
"""
from __future__ import annotations

import asyncio
import json
import logging
from typing import AsyncIterator, Optional

from fastapi import APIRouter, Header, Query
from sse_starlette.sse import EventSourceResponse

from recoil.api.eventbus import BUS

logger = logging.getLogger(__name__)


router = APIRouter()


async def _event_generator(
    last_event_id: Optional[str],
) -> AsyncIterator[dict[str, str]]:
    """Yield SSE-event dicts — one per EngineEvent."""
    try:
        async for event in BUS.subscribe(last_event_id):
            payload = event.model_dump(mode="json", by_alias=True)
            yield {
                "id": event.id,
                "event": "engine_event",
                "data": json.dumps(payload),
            }
    except asyncio.CancelledError:
        # Client disconnect — propagate so EventSourceResponse cleans up.
        raise


@router.get("/events/stream")
async def stream_events(
    last_event_id: Optional[str] = Header(default=None, alias="Last-Event-ID"),
    lastEventId: Optional[str] = Query(default=None),
) -> EventSourceResponse:
    """SSE stream of engine events.

    Pre-Phase-19, events came from the receipts log on demand
    (``/api/events`` — still served by engine_routes). The new stream is
    push-driven from the EventBus and supports resume via Last-Event-ID
    header (auto-set by EventSource) or ``?lastEventId=`` query (Debug
    R2 — for cross-page-reload resume that browsers can't auto-emit).

    Header wins when both are set — the header is the auto-resume path.

    Bug-fix (Debug R5): use ``is not None`` rather than truthiness so an
    empty ``Last-Event-ID:`` header (the documented "start fresh" signal)
    is NOT silently superseded by a stale ``?lastEventId=`` query param.
    Header precedence is absolute — if the client sent the header, the
    query is ignored, even if the header value is the empty string.
    """
    effective = last_event_id if last_event_id is not None else lastEventId
    return EventSourceResponse(_event_generator(effective))


__all__ = ["router"]
