"""Server-Sent Events transport — framework-agnostic generator.

Plumb into any framework's streaming response by yielding bytes from
sse_stream() into the framework's write method. Works with:
- http.server.BaseHTTPRequestHandler.wfile.write()
- Flask Response(stream, mimetype="text/event-stream")
- FastAPI/Starlette StreamingResponse(stream, media_type="text/event-stream")

Reconnect semantics:
- Browser EventSource automatically sends Last-Event-ID on reconnect.
- Server passes it to sse_stream(broker, last_event_id=<id>).
- If the id is in the ring buffer, replay from that point.
- If the id is not (cursor gap), the ring buffer has rolled past — replay
  returns empty, client code MUST force-refresh via /api/asset/* calls.
"""

from __future__ import annotations

import queue
from typing import Generator, Optional

from ..events import FsEvent
from ..pubsub import EventBroker


def format_sse_frame(ev: FsEvent) -> bytes:
    """Format a single FsEvent as an SSE frame.

    Frame format:
        id: <event_id>\n
        event: <event_type>\n
        data: <json>\n
        \n
    """
    return (
        f"id: {ev.event_id}\n"
        f"event: {ev.event_type.value}\n"
        f"data: {ev.to_json()}\n"
        f"\n"
    ).encode("utf-8")


CURSOR_GAP_FRAME: bytes = b"event: cursor_gap\ndata: {}\n\n"


def sse_stream(
    broker: EventBroker,
    last_event_id: Optional[str] = None,
) -> Generator[bytes, None, None]:
    """Generator yielding SSE-formatted bytes from broker events.

    Behavior:
    - If last_event_id is None (initial connect), DO NOT replay history.
      The browser's first connect doesn't send Last-Event-ID, so replaying
      would dump up to 500 events on every page load and cause a render
      storm. The client is expected to call _refresh() once on activation
      to load initial state, then rely on live events from this point.
    - If last_event_id is provided (reconnect), replay events after the
      cursor from the ring buffer.
    - If last_event_id is provided but the cursor has rolled out of the
      ring (replay_since returns empty), yield a synthetic `cursor_gap`
      event so the client can force-refresh its state.
    - Then subscribe for live events via the broker queue.
    - If the queue yields None (poison pill from slow-consumer drop),
      break and exit cleanly so the HTTP connection closes and the
      browser's EventSource auto-reconnects with Last-Event-ID.
    - Unsubscribes cleanly in the finally block when the caller stops
      iterating or calls .close().

    To use in http.server.BaseHTTPRequestHandler.do_GET:

        self.send_response(200)
        self.send_header("Content-Type", "text/event-stream")
        self.send_header("Cache-Control", "no-cache")
        self.send_header("Connection", "keep-alive")
        self.end_headers()
        try:
            for frame in sse_stream(broker, last_event_id):
                self.wfile.write(frame)
                self.wfile.flush()
        except (BrokenPipeError, ConnectionResetError):
            pass  # client disconnected
    """
    # Skip history entirely on initial connect (last_event_id is None).
    # Call subscribe() alone — no replay needed.
    if last_event_id is None:
        sid, sub_queue = broker.subscribe()
        replayed_frames: list[bytes] = []
    else:
        # Atomic subscribe + replay closes the race window that would
        # otherwise drop events published between replay_since() and
        # subscribe(). See EventBroker.subscribe_with_replay docstring.
        sid, sub_queue, replayed = broker.subscribe_with_replay(last_event_id)
        if replayed is None:
            # True cursor gap — client's cursor has rolled out of the ring.
            # Emit a synthetic cursor_gap event so the client can
            # force-refresh its entire state.
            replayed_frames = [CURSOR_GAP_FRAME]
        else:
            # Cursor was found. Replay any events after it. An empty list
            # here means the client is up-to-date — no frames to yield,
            # just fall through to the live-event subscription below.
            replayed_frames = [format_sse_frame(ev) for ev in replayed]

    # Yield history frames (if any) now that the subscription is already
    # installed — any event published after subscribe_with_replay returned
    # is guaranteed to be in sub_queue, so there's no lost-events window.
    try:
        for frame in replayed_frames:
            yield frame
        while True:
            try:
                ev = sub_queue.get(timeout=30)
            except queue.Empty:
                continue
            if ev is None:
                # Poison pill — we were dropped as a slow consumer.
                # Break to close the HTTP connection; the browser's
                # EventSource will auto-reconnect with Last-Event-ID
                # and resume from the ring buffer.
                break
            yield format_sse_frame(ev)
    finally:
        broker.unsubscribe(sid)
