# fmatch/saas/api/progress.py
from __future__ import annotations
import asyncio
import json
import logging
from datetime import datetime, timedelta
from typing import AsyncGenerator, Optional

from fastapi import APIRouter, Depends, Request, Header, HTTPException
from fastapi.responses import StreamingResponse

# Auth (accepts "dev-token" in development)
from ..auth import get_current_account

# Async Redis helper
import redis.asyncio as async_redis
from urllib.parse import urlparse

from ..settings import REDIS_URL

log = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v1/jobs", tags=["Progress"])

# One lazy, shared connection (the rest of the app also uses localhost:6379)
_redis: Optional[async_redis.Redis] = None


def _redact_redis_url(url: str) -> str:
    """
    Hide credentials when logging Redis URLs.
    """
    try:
        parsed = urlparse(url)
        if parsed.password:
            netloc = parsed.netloc.replace(parsed.password, "*****")
            return parsed._replace(netloc=netloc).geturl()
    except Exception:
        pass
    return url

async def get_redis() -> async_redis.Redis:
    global _redis
    if _redis is None:
        log.warning(
            "progress.py initializing Redis connection; REDIS_URL=%s",
            _redact_redis_url(REDIS_URL),
        )
        try:
            _redis = async_redis.from_url(
                REDIS_URL,
                decode_responses=False,
                socket_connect_timeout=3,
            )
            await _redis.ping()
            log.info("progress.py connected to Redis at %s", _redact_redis_url(REDIS_URL))
        except Exception as e:
            log.error(
                "progress.py failed to connect to Redis %s: %s",
                _redact_redis_url(REDIS_URL),
                e,
            )
            raise HTTPException(status_code=503, detail="Cache service unavailable")
    return _redis


@router.get("/{job_id}/progress")
async def stream_progress(
    request: Request,
    job_id: str,
    last_event_id: str | None = Header(default=None, alias="Last-Event-ID"),
    account_id: str = Depends(get_current_account),
) -> StreamingResponse:
    """
    Server-Sent Events (SSE) progress stream.

    Reads the latest progress JSON stored at key: job:{job_id}:progress
    Emits heartbeats every 10s and sets `retry: 3000` for automatic client reconnects.
    """
    redis = await get_redis()

    async def event_generator() -> AsyncGenerator[str, None]:
        # A simple, monotonic id for SSE clients that honor Last-Event-ID
        next_id = (
            int(last_event_id) + 1 if (last_event_id and last_event_id.isdigit()) else 1
        )

        # Heartbeat timer
        last_beat = datetime.utcnow() - timedelta(seconds=9)

        # Remember last payload so we only emit on change
        last_payload: bytes | None = None

        # Send a retry directive once (3s backoff)
        yield "retry: 3000\n\n"

        # Loop until the client disconnects or we reach FINAL
        while True:
            # Stop if client went away
            if await request.is_disconnected():
                log.info(f"SSE client for job {job_id} disconnected")
                break

            # Heartbeat every 10s (some proxies close idle streams)
            now = datetime.utcnow()
            if (now - last_beat) >= timedelta(seconds=10):
                hb = json.dumps({"ts": now.isoformat()})
                yield f"id: {next_id}\nevent: heartbeat\ndata: {hb}\n\n"
                next_id += 1
                last_beat = now

            try:
                payload = await redis.get(f"job:{job_id}:progress")
            except Exception as e:
                err = json.dumps({"error": str(e)})
                yield f"id: {next_id}\nevent: error\ndata: {err}\n\n"
                next_id += 1
                await asyncio.sleep(1.0)
                continue

            if payload and payload != last_payload:
                # Forward as-is; jobs.py already writes JSON with 'phase' and 'percent'
                try:
                    # Decode bytes to str for SSE
                    data_str = (
                        payload.decode("utf-8")
                        if isinstance(payload, (bytes, bytearray))
                        else str(payload)
                    )
                except Exception:
                    # Fallback: repr
                    data_str = json.dumps({"raw": str(payload)})

                yield f"id: {next_id}\nevent: progress\ndata: {data_str}\n\n"
                next_id += 1
                last_payload = payload

                # If phase signals completion, end the stream
                try:
                    p = json.loads(data_str)
                    phase = (p.get("phase") or "").upper()
                    percent = int(p.get("percent") or 0)
                    if phase in ("FINAL", "FINALIZING") or percent >= 100:
                        log.info(f"SSE progress FINAL for job {job_id}; closing stream")
                        break
                except Exception:
                    # Ignore parse errors; keep streaming
                    pass

            # Gentle polling cadence; progress callback updates Redis promptly
            await asyncio.sleep(0.5)

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