from __future__ import annotations

import asyncio
import json
from collections.abc import AsyncIterator

from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse

from app.core.config import JOB_ID_RE
from app.core.registry import get as registry_get

router = APIRouter(tags=["events"])

# Close SSE connections that outlive this threshold to prevent zombie
# connections from accumulating when clients disconnect without a TCP RST.
_MAX_SSE_SECONDS = 4 * 3600  # 4 hours
# Hard cap on concurrent SSE connections to prevent resource exhaustion
# from tab leaks or aggressive reconnect loops.
_MAX_SSE_CONNECTIONS = 200
# Counter is only mutated from the async event loop (no awaits between
# check+increment), so no lock is needed.
_sse_active = 0


@router.get("/jobs/{job_id}/events")
async def job_events(job_id: str) -> StreamingResponse:
    """Server-Sent Events stream of job state updates. Closes when the job
    reaches a terminal status (done, error, cancelled) or after 4 hours."""
    global _sse_active
    if not JOB_ID_RE.match(job_id):
        raise HTTPException(status_code=404, detail="job not found")
    job = registry_get(job_id)
    if job is None:
        raise HTTPException(status_code=404, detail="job not found")
    if _sse_active >= _MAX_SSE_CONNECTIONS:
        raise HTTPException(status_code=503, detail="too many concurrent streams")
    _sse_active += 1

    async def stream() -> AsyncIterator[str]:
        global _sse_active
        try:
            last = None
            keepalive_at = 0
            loop = asyncio.get_running_loop()
            deadline = loop.time() + _MAX_SSE_SECONDS
            while loop.time() < deadline:
                snapshot = job.to_state()
                serialized = json.dumps(snapshot)
                if serialized != last:
                    yield f"data: {serialized}\n\n"
                    last = serialized
                    keepalive_at = 0
                if snapshot["status"] in ("done", "error", "cancelled"):
                    return
                keepalive_at += 1
                if keepalive_at >= 75:  # ~15s
                    yield ": keepalive\n\n"
                    keepalive_at = 0
                await asyncio.sleep(0.2)
        finally:
            _sse_active -= 1

    return StreamingResponse(
        stream(),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
    )
