"""Bounded server-sent-events helper (MPS-20 / MPS-21).

We don't have Redis/pub-sub (SQLite app) and uWSGI only has a handful of
worker threads, so a truly long-lived SSE connection would starve the
pool. Instead each connection is *bounded*: it polls the row every
~`poll_interval` seconds, emits a `data:` event whenever the serialized
state changes (and the current state immediately on connect), sends a
heartbeat comment periodically, then closes after ~`hold_seconds`. The
browser's ``EventSource`` reconnects automatically, so the worker thread
is only held for that bounded window.

`fetch_state(session)` is invoked each poll with a *fresh* SQLAlchemy
session (NOT `request.dbsession` — by the time this generator runs,
pyramid_tm has already closed the request transaction) and must return
either ``(version, payload_dict)`` or ``None`` if the object is gone.
`version` is any comparable value; when it changes we emit `payload_dict`.

Timings come from settings so tests can run fast:
  app.sse.hold_seconds          (default 25)
  app.sse.poll_interval_seconds (default 1.5)
"""

import json
import time


def _float_setting(settings, key, default):
    try:
        return float(settings.get(key, default))
    except (TypeError, ValueError):
        return default


def event_stream(request, fetch_state):
    settings = request.registry.settings
    hold = _float_setting(settings, "app.sse.hold_seconds", 25.0)
    poll = _float_setting(settings, "app.sse.poll_interval_seconds", 1.5)
    heartbeat_every = max(1, int(round(15.0 / poll)) if poll else 1)

    factory = request.registry["dbsession_factory"]
    deadline = time.monotonic() + hold
    sentinel = object()
    last_version = sentinel
    i = 0
    while time.monotonic() < deadline:
        session = factory()
        try:
            result = fetch_state(session)
        finally:
            session.close()
        if result is None:
            yield b"event: gone\ndata: {}\n\n"
            return
        version, payload = result
        if version != last_version:
            last_version = version
            yield ("data: " + json.dumps(payload) + "\n\n").encode("utf-8")
        elif i % heartbeat_every == 0:
            yield b": ping\n\n"
        i += 1
        if time.monotonic() >= deadline:
            break
        time.sleep(poll)
    # Closing on purpose — EventSource will reconnect.
    yield b"event: reconnect\ndata: {}\n\n"


def sse_response(request, fetch_state):
    """Attach an `event_stream` generator to `request.response` and return it."""
    response = request.response
    response.content_type = "text/event-stream"
    response.headers["Cache-Control"] = "no-cache"
    # Hint proxies (nginx; Caddy auto-detects text/event-stream) not to buffer.
    response.headers["X-Accel-Buffering"] = "no"
    response.app_iter = event_stream(request, fetch_state)
    return response
