"""
Realtime live monitor API — powered by Langfuse REST API.

Provides:
- bootstrap snapshot for initial page load
- SSE stream with rolling updates for a live operations dashboard
"""

import asyncio
import json
import logging
from datetime import UTC, datetime, timedelta
from typing import Any

import httpx
from fastapi import APIRouter, Query
from starlette.responses import StreamingResponse

logger = logging.getLogger(__name__)
router = APIRouter()

# ─── Self-hosted Langfuse config ─────────────────────────────────
LANGFUSE_BASE = "http://172.16.2.207:3009"
LANGFUSE_AUTH = (
    "pk-lf-2bb249b1-77e3-4309-8954-312b8fb2fff9",
    "sk-lf-bd0c7202-a7f4-4399-a36e-65ebe2e35104",
)


def _utc_now() -> datetime:
    return datetime.now(UTC)


def _safe_float(v: Any, default: float = 0.0) -> float:
    if v is None:
        return default
    try:
        return float(v)
    except (TypeError, ValueError):
        return default


def _safe_int(v: Any, default: int = 0) -> int:
    if v is None:
        return default
    try:
        return int(v)
    except (TypeError, ValueError):
        return default


def _severity(trace: dict) -> str:
    level = (trace.get("level") or "").upper()
    if level in ("ERROR",):
        return "error"
    latency = _safe_float(trace.get("latency"))
    if latency >= 12:
        return "warn"
    return "ok"


# ─── Langfuse REST API helpers ───────────────────────────────────

async def _langfuse_get(path: str, params: dict | None = None) -> dict:
    """Authenticated GET to self-hosted Langfuse."""
    url = f"{LANGFUSE_BASE}/api/public{path}"
    async with httpx.AsyncClient(timeout=15) as client:
        resp = await client.get(url, params=params, auth=LANGFUSE_AUTH)
        resp.raise_for_status()
        return resp.json()


async def _fetch_traces(
    from_ts: str, limit: int = 100
) -> list[dict]:
    """Fetch traces from Langfuse within the time window."""
    params = {
        "limit": limit,
        "orderBy": "timestamp.desc",
        "fromTimestamp": from_ts,
    }
    result = await _langfuse_get("/traces", params)
    return result.get("data", [])


# ─── Build snapshot from Langfuse traces ─────────────────────────

def _minute_slots(window_minutes: int) -> list[tuple[str, str]]:
    now = _utc_now().replace(second=0, microsecond=0)
    slots = []
    for offset in range(window_minutes - 1, -1, -1):
        minute = now - timedelta(minutes=offset)
        slots.append((minute.strftime("%Y-%m-%dT%H:%M"), minute.strftime("%H:%M")))
    return slots


def _build_series(traces: list[dict], window_minutes: int) -> dict[str, list[Any]]:
    """Build per-minute time series from trace list."""
    # Bucket traces by minute
    minute_buckets: dict[str, list[dict]] = {}
    for t in traces:
        ts = t.get("timestamp", "")
        if len(ts) >= 16:
            bucket = ts[:16]  # "2026-03-23T07:37"
            minute_buckets.setdefault(bucket, []).append(t)

    labels: list[str] = []
    trace_counts: list[int] = []
    costs: list[float] = []
    latencies: list[float] = []
    errors: list[int] = []

    for bucket_key, short_label in _minute_slots(window_minutes):
        bucket_traces = minute_buckets.get(bucket_key, [])
        labels.append(short_label)
        trace_counts.append(len(bucket_traces))

        bucket_cost = sum(_safe_float(t.get("totalCost")) for t in bucket_traces)
        costs.append(round(bucket_cost, 4))

        bucket_latencies = [_safe_float(t.get("latency")) for t in bucket_traces if t.get("latency") is not None]
        avg_lat = sum(bucket_latencies) / len(bucket_latencies) if bucket_latencies else 0
        latencies.append(round(avg_lat, 2))

        bucket_errors = sum(1 for t in bucket_traces if (t.get("level") or "").upper() == "ERROR")
        errors.append(bucket_errors)

    return {
        "labels": labels,
        "traces": trace_counts,
        "costs": costs,
        "latencies": latencies,
        "errors": errors,
    }


async def _fetch_live_snapshot(window_minutes: int = 20) -> dict[str, Any]:
    """Build a live dashboard snapshot from Langfuse API."""
    now = _utc_now()
    from_ts = (now - timedelta(minutes=window_minutes)).strftime("%Y-%m-%dT%H:%M:%S.000Z")

    degraded_reasons: list[str] = []

    # Fetch traces
    try:
        traces = await _fetch_traces(from_ts, limit=100)
    except Exception as e:
        logger.error("Langfuse traces fetch error: %s", e)
        degraded_reasons.append(f"traces: {e}")
        traces = []

    # Compute metrics
    total_traces = len(traces)
    session_ids = set(t.get("sessionId") for t in traces if t.get("sessionId"))
    active_sessions = len(session_ids)

    total_cost = sum(_safe_float(t.get("totalCost")) for t in traces)
    latencies_list = [_safe_float(t.get("latency")) for t in traces if t.get("latency") is not None]
    avg_latency = sum(latencies_list) / len(latencies_list) if latencies_list else 0

    error_count = sum(1 for t in traces if (t.get("level") or "").upper() == "ERROR")
    error_rate = (error_count * 100.0 / total_traces) if total_traces > 0 else 0

    last_event_at = traces[0].get("timestamp") if traces else None

    # Build time series
    series = _build_series(traces, window_minutes)

    # Model mix
    model_usage: dict[str, dict[str, Any]] = {}
    for t in traces:
        # Get model from observations if available, else use trace name
        model = t.get("name") or "unknown"
        if model not in model_usage:
            model_usage[model] = {"traces": 0, "total_cost_usd": 0, "latencies": []}
        model_usage[model]["traces"] += 1
        model_usage[model]["total_cost_usd"] += _safe_float(t.get("totalCost"))
        if t.get("latency") is not None:
            model_usage[model]["latencies"].append(_safe_float(t.get("latency")))

    model_total = max(sum(m["traces"] for m in model_usage.values()), 1)
    model_mix = []
    for model_name, stats in sorted(model_usage.items(), key=lambda x: x[1]["traces"], reverse=True)[:6]:
        lats = stats["latencies"]
        model_mix.append({
            "model_name": model_name,
            "traces": stats["traces"],
            "total_cost_usd": round(stats["total_cost_usd"], 4),
            "avg_latency_s": round(sum(lats) / len(lats), 2) if lats else 0,
            "share_pct": round(stats["traces"] * 100.0 / model_total, 1),
        })

    # Latest events (for activity feed)
    latest_events = []
    for t in traces[:12]:
        traced_at = t.get("timestamp")
        age_seconds = None
        if traced_at:
            try:
                traced_dt = datetime.fromisoformat(traced_at.replace("Z", "+00:00"))
                age_seconds = max(0, int(now.timestamp() - traced_dt.timestamp()))
            except Exception:
                pass

        event = {
            "trace_id": str(t.get("id", ""))[:12],
            "session_id": str(t.get("sessionId") or ""),
            "model_name": str(t.get("name") or "unknown"),
            "traced_at": traced_at,
            "trace_latency_s": round(_safe_float(t.get("latency")), 2),
            "total_cost_usd": round(_safe_float(t.get("totalCost")), 4),
            "total_obs_error": 0,
            "nb_generation_error": 0,
            "age_seconds": age_seconds,
        }
        event["severity"] = _severity(t)
        latest_events.append(event)

    # Signature for change detection
    signature = "|".join([
        last_event_at or "none",
        str(total_traces),
        str(active_sessions),
        str(series["traces"][-1] if series["traces"] else 0),
        latest_events[0]["trace_id"] if latest_events else "none",
    ])

    status = "degraded" if degraded_reasons else "live"
    return {
        "generated_at": now.isoformat(),
        "window_minutes": window_minutes,
        "status": status,
        "status_reason": "; ".join(degraded_reasons) if degraded_reasons else "",
        "signature": signature,
        "metrics": {
            "traces": total_traces,
            "active_sessions": active_sessions,
            "total_cost_usd": round(total_cost, 4),
            "avg_latency_s": round(avg_latency, 2),
            "error_rate_pct": round(error_rate, 2),
            "last_event_at": last_event_at,
            "peak_traces_per_min": max(series["traces"] or [0]),
            "peak_cost_per_min": max(series["costs"] or [0]),
            "peak_latency_s": max(series["latencies"] or [0]),
        },
        "series": series,
        "model_mix": model_mix,
        "latest_events": latest_events,
    }


def _sse(payload: dict[str, Any]) -> str:
    return f"data: {json.dumps(payload, ensure_ascii=False, default=str)}\n\n"


@router.get("/api/live-monitor/bootstrap", summary="Live dashboard bootstrap snapshot")
async def live_monitor_bootstrap(
    window_minutes: int = Query(default=20, ge=5, le=60),
):
    snapshot = await _fetch_live_snapshot(window_minutes)
    snapshot.pop("signature", None)
    return snapshot


@router.get("/api/live-monitor/stream", summary="Live dashboard SSE stream")
async def live_monitor_stream(
    window_minutes: int = Query(default=20, ge=5, le=60),
    interval_seconds: int = Query(default=4, ge=2, le=15),
):
    async def event_stream():
        last_signature = ""
        try:
            initial_snapshot = await _fetch_live_snapshot(window_minutes)
            last_signature = str(initial_snapshot.get("signature", ""))
            initial_snapshot.pop("signature", None)
            yield _sse({"type": "snapshot", "payload": initial_snapshot})

            while True:
                await asyncio.sleep(interval_seconds)
                snapshot = await _fetch_live_snapshot(window_minutes)
                signature = str(snapshot.get("signature", ""))
                snapshot.pop("signature", None)

                if signature != last_signature:
                    last_signature = signature
                    yield _sse({"type": "dashboard_update", "payload": snapshot})
                else:
                    yield _sse({
                        "type": "heartbeat",
                        "generated_at": snapshot.get("generated_at"),
                        "status": snapshot.get("status", "live"),
                    })
        except asyncio.CancelledError:
            logger.info("Live monitor stream cancelled by client")
            raise
        except Exception as e:
            logger.error("Live monitor stream error: %s", e)
            yield _sse({"type": "error", "message": str(e)[:300]})

    return StreamingResponse(event_stream(), media_type="text/event-stream")
