"""Read-only activity-core ops_run projection (STATE-WP-0078)."""

from __future__ import annotations

import asyncio
import time
from datetime import datetime, timedelta, timezone
from typing import Any

import httpx

from api.config import settings
from api.schemas.ops_run import OpsRunProjection, OpsRunProjectionItem


_cache: OpsRunProjection | None = None
_cache_at: float = 0.0
_lock = asyncio.Lock()


def reset_ops_run_projection_cache() -> None:
    global _cache, _cache_at
    _cache = None
    _cache_at = 0.0


def _parse_datetime(value: Any) -> datetime | None:
    if not value or not isinstance(value, str):
        return None
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        return None
    return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)


async def _fetch_ops_runs() -> dict[str, Any]:
    if not settings.activity_core_url:
        raise RuntimeError("ACTIVITY_CORE_URL is not configured")
    headers = {}
    if settings.activity_core_worker_token:
        headers["X-Worker-Token"] = settings.activity_core_worker_token
    async with httpx.AsyncClient(
        base_url=settings.activity_core_url.rstrip("/"),
        timeout=10.0,
        follow_redirects=True,
        trust_env=False,
        headers=headers,
    ) as client:
        response = await client.get("/ops-runs", params={"limit": 200})
        response.raise_for_status()
        return response.json()


def _project(payload: dict[str, Any], now: datetime) -> OpsRunProjection:
    raw_items = payload.get("items") or []
    counts = payload.get("counts") or {}
    failed_cutoff = now - timedelta(hours=24)
    stuck_cutoff = now - timedelta(hours=max(0.1, settings.ops_run_sla_hours))
    failed_24h = 0
    stuck = 0
    items: list[OpsRunProjectionItem] = []

    for raw in raw_items:
        state = str(raw.get("state") or "unknown")
        updated_at = _parse_datetime(raw.get("updated_at"))
        created_at = _parse_datetime(raw.get("created_at"))
        if state == "failed" and updated_at and updated_at >= failed_cutoff:
            failed_24h += 1
        if state in {"open", "claimed"} and (created_at or updated_at):
            if (created_at or updated_at) < stuck_cutoff:
                stuck += 1

        result = raw.get("result") if isinstance(raw.get("result"), dict) else {}
        last_error = result.get("error") or raw.get("last_error")
        if state in {"open", "claimed", "failed"}:
            items.append(OpsRunProjectionItem(
                id=str(raw.get("id")),
                definition=str(raw.get("activity_definition_id")) if raw.get("activity_definition_id") else None,
                target_repo=raw.get("target_repo"),
                state=state,
                lease={
                    "owner": raw.get("claim_owner"),
                    "until": raw.get("lease_until"),
                    "attempt": raw.get("attempt", 0),
                } if raw.get("claim_owner") or raw.get("lease_until") else None,
                updated_at=updated_at,
                last_error=str(last_error) if last_error else None,
            ))

    items.sort(key=lambda item: item.updated_at or datetime.min.replace(tzinfo=timezone.utc), reverse=True)
    return OpsRunProjection(
        available=True,
        generated_at=now,
        open=int(counts.get("open", sum(item.state == "open" for item in items))),
        claimed=int(counts.get("claimed", sum(item.state == "claimed" for item in items))),
        failed_24h=failed_24h,
        stuck_open_or_claimed=stuck,
        items=items[:50],
    )


async def get_ops_run_projection(*, refresh: bool = False) -> OpsRunProjection:
    global _cache, _cache_at
    now_mono = time.monotonic()
    ttl = max(1.0, settings.ops_run_projection_ttl_seconds)
    if not refresh and _cache is not None and now_mono - _cache_at < ttl:
        return _cache

    async with _lock:
        now_mono = time.monotonic()
        if not refresh and _cache is not None and now_mono - _cache_at < ttl:
            return _cache
        now = datetime.now(timezone.utc)
        try:
            projection = _project(await _fetch_ops_runs(), now)
        except Exception as exc:
            if _cache is not None and _cache.available:
                projection = _cache.model_copy(update={
                    "stale": True,
                    "error": f"activity-core refresh failed: {exc}",
                })
            else:
                projection = OpsRunProjection(
                    generated_at=now,
                    error=f"activity-core unavailable: {exc}",
                )
        _cache = projection
        _cache_at = now_mono
        return projection
