"""``GET /api/payers/{payer_id}/summary`` — payer-level rollup for the drill-down panel.

Returns billed/received totals, denial rate, and the top providers for
a given ``payer_id`` (the X12 NM1*PR*PI qualifier, e.g. ``SKCO0``).
Cached in-process for ``_SUMMARY_TTL_S`` seconds via a per-payer_id
memo. The drill-down UI hammers this on every hover; the underlying
query is O(claims) per payer, so the TTL keeps the panel responsive.

Pubsub invalidation is intentionally NOT wired (see the long comment
in the body). The 60s TTL keeps staleness bounded; a follow-up can
wire targeted invalidation if the UI proves TTL-bounded staleness is
unacceptable.

404 when no claims AND no remits reference this payer_id — the UI
uses that to distinguish a typo from a payer with zero traffic (the
latter would still return a valid 200 with zeroed totals).

SP36 Task 6: this block moved here from ``api.py:3188`` (the
``SP21 Task 1.5: payer-level summary`` divider).
"""
from __future__ import annotations

import logging
from time import monotonic

from fastapi import APIRouter, Depends, HTTPException

from cyclone import db
from cyclone.auth.deps import matrix_gate
from cyclone.db import Claim, ClaimState, Remittance

log = logging.getLogger(__name__)

router = APIRouter(dependencies=[Depends(matrix_gate)])

# Per-payer_id memoization for /api/payers/{payer_id}/summary. The drill-
# down UI hammers this on every hover; the underlying query is O(claims)
# per payer so a 60s TTL keeps the panel responsive. Process-local on
# purpose — invalidation is driven by the TTL alone for now (see note
# below).
_SUMMARY_TTL_S = 60.0
_summary_cache: dict[str, tuple[float, dict]] = {}


def _clear_summary_cache() -> None:
    """Test hook: wipe the process-local cache.

    The 60s TTL means tests that want to assert on a recomputed payload
    must clear the cache between requests. Not used by production code.
    """
    _summary_cache.clear()


# Pubsub invalidation is intentionally NOT wired. The ``claim_written``
# and ``remittance_written`` payloads emitted by ``store.add`` are the
# UI-shaped dicts from ``to_ui_claim_from_orm`` / ``to_ui_remittance_from_orm``,
# neither of which carries the X12 ``payer_id`` (NM1*PR*PI qualifier).
# They carry ``payerName`` only, which is the human-readable name and not
# the URL key we cache on. Wiring a subscriber here would either need a
# DB lookup per event (re-deriving payer_id from Claim.id) or a different
# cache key — both are out of scope for this task. The 60s TTL keeps
# staleness bounded; a follow-up can wire targeted invalidation if the
# UI proves TTL-bounded staleness is unacceptable.


@router.get("/api/payers/{payer_id}/summary")
def get_payer_summary(payer_id: str):
    """Payer-level rollup for the drill-down panel.

    Returns billed/received totals, denial rate, and top providers for
    the given ``payer_id`` (the X12 NM1*PR*PI qualifier, e.g. ``SKCO0``).
    Cached in-process for ``_SUMMARY_TTL_S`` seconds.

    404 when no claims AND no remits reference this payer_id — the UI
    uses that to distinguish a typo from a payer with zero traffic
    (the latter would still return a valid 200 with zeroed totals).
    """
    now = monotonic()
    cached = _summary_cache.get(payer_id)
    if cached and (now - cached[0]) < _SUMMARY_TTL_S:
        return cached[1]

    log.debug("payer summary cache miss", extra={"payer_id": payer_id})

    # Query claims + remits scoped to this payer_id. We bypass
    # ``store.iter_claims`` because that helper filters by payer NAME
    # substring, not by the X12 PI qualifier we use as the URL key.
    with db.SessionLocal()() as s:
        claim_rows: list[Claim] = (
            s.query(Claim).filter(Claim.payer_id == payer_id).all()
        )
        claim_ids = [c.id for c in claim_rows]
        remit_rows: list[Remittance] = []
        if claim_ids:
            remit_rows = (
                s.query(Remittance)
                .filter(Remittance.claim_id.in_(claim_ids))
                .all()
            )

        if not claim_rows and not remit_rows:
            raise HTTPException(
                status_code=404,
                detail=f"Payer {payer_id!r} not found",
            )

        billed_total = sum(float(c.charge_amount or 0) for c in claim_rows)
        received_total = sum(float(r.total_paid or 0) for r in remit_rows)
        denied = sum(
            1 for c in claim_rows if c.state == ClaimState.DENIED
        )
        claim_count = len(claim_rows)
        denial_rate = (denied / claim_count) if claim_count else 0.0

        provider_counts: dict[str, int] = {}
        for c in claim_rows:
            npi = c.provider_npi
            if not npi:
                continue
            provider_counts[npi] = provider_counts.get(npi, 0) + 1
        top_providers = [
            {"npi": npi, "count": count}
            for npi, count in sorted(
                provider_counts.items(), key=lambda kv: -kv[1]
            )[:5]
        ]

        # Best-effort payer display name from the first claim's raw
        # payer object (NM1*PR name element, e.g. "COHCPF"). Falls
        # back to the id when no parsed envelope is available.
        payer_name = payer_id
        for c in claim_rows:
            raw = c.raw_json or {}
            p = raw.get("payer") if isinstance(raw, dict) else None
            if isinstance(p, dict) and p.get("name"):
                payer_name = p["name"]
                break

    payload = {
        "payer_id": payer_id,
        "name": payer_name,
        "claim_count": claim_count,
        "billed_total": billed_total,
        "received_total": received_total,
        "denial_rate": denial_rate,
        "top_providers": top_providers,
    }
    _summary_cache[payer_id] = (now, payload)
    return payload
