"""``/api/providers`` and ``/api/config/providers*`` — read views over providers.

Three endpoints across two URL prefixes, all gated by ``matrix_gate``:

- ``GET /api/providers``                  — distinct provider list
  derived from the Claims population (``store.distinct_providers()``),
  with ``npi`` / ``state`` filters, pagination, and an NDJSON variant.
- ``GET /api/config/providers``           — the configured provider
  rows (3 NPIs for SP9), filtered by ``is_active``.
- ``GET /api/config/providers/{npi}``     — one configured provider
  plus a small drill-down block: ``recent_claims`` (top-10 by
  ``submissionDate``) and ``recent_activity`` (top-10, joined via
  ``Claim.id`` because ``ActivityEvent`` has no ``provider_npi``
  column; the outer-join via ``Remittance.claim_id`` surfaces the
  orphan ``remit_received`` events that were recorded pre-match).

SP36 Task 12: this block moved here from ``api.py:2448`` (the
``/api/providers`` list, the ``/api/config/providers`` list, and
the ``/api/config/providers/{npi}`` detail) — three URL prefixes,
one router per spec.
"""
from __future__ import annotations

import json
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import StreamingResponse
from sqlalchemy import desc, or_

from cyclone import db
from cyclone.api_helpers import (
    ndjson_stream_list as _ndjson_stream_list,
    wants_ndjson as _wants_ndjson,
)
from cyclone.auth.deps import matrix_gate
from cyclone.db import Claim, Remittance
from cyclone.store import store

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


@router.get("/api/providers")
def list_providers(
    request: Request,
    npi: str | None = Query(None),
    state: str | None = Query(None),
    limit: int = Query(100, ge=1, le=1000),
    offset: int = Query(0, ge=0),
) -> Any:
    items = store.distinct_providers()
    if npi is not None:
        items = [p for p in items if p["npi"] == npi]
    if state is not None:
        items = [p for p in items if p.get("state") == state]
    paged = items[offset:offset + limit]
    total = len(items)
    returned = len(paged)
    has_more = total > offset + returned
    if _wants_ndjson(request):
        return StreamingResponse(
            _ndjson_stream_list(paged, total, returned, has_more),
            media_type="application/x-ndjson",
        )
    return {
        "items": paged,
        "total": total,
        "returned": returned,
        "has_more": has_more,
    }


@router.get("/api/config/providers")
def list_configured_providers(is_active: bool | None = Query(default=True)):
    """List the configured provider rows (3 NPIs for SP9)."""
    return [json.loads(p.model_dump_json()) for p in store.list_providers(is_active=is_active)]


@router.get("/api/config/providers/{npi}")
def get_configured_provider(npi: str):
    p = store.get_provider(npi)
    if p is None:
        raise HTTPException(status_code=404, detail=f"provider {npi!r} not found")
    provider_dict = json.loads(p.model_dump_json())

    # SP21 Task 1.6: extend the response with two top-N arrays that the
    # drill-down peek panel hangs off. ``recent_claims`` reuses the
    # existing store projection (already returns UI-shaped dicts with
    # ``submissionDate``); ``recent_activity`` is a direct ORM join
    # because ``ActivityEvent`` has no ``provider_npi`` column — the
    # filter has to hop through ``Claim.id``.
    recent_claims = sorted(
        store.iter_claims(provider_npi=npi),
        key=lambda c: c.get("submissionDate") or "",
        reverse=True,
    )[:10]

    # Activity filter has TWO join paths back to a Claim for this
    # provider:
    #   1. ``ActivityEvent.claim_id IN (claim_ids)`` — events that were
    #      recorded with a claim FK already set (claim_submitted,
    #      manual_match, claim_paid, etc.).
    #   2. ``Remittance.claim_id IN (claim_ids)`` — the original
    #      ``remit_received`` event recorded at 835 ingest time
    #      (``store.add`` lines 999-1003) is inserted with
    #      ``claim_id=None`` because the remittance hasn't been matched
    #      to a claim yet. The auto-reconcile pass later sets
    #      ``Remittance.claim_id`` (``reconcile.run`` lines 289-293),
    #      but the *original* ActivityEvent row stays orphaned. The
    #      outer-join-then-OR lets us surface both shapes. Without
    #      path 2, a provider's activity feed looks frozen the moment
    #      an 835 lands — the most common activity, invisible.
    with db.SessionLocal()() as s:
        claim_ids = [
            cid
            for (cid,) in s.query(Claim.id)
            .filter(Claim.provider_npi == npi)
            .all()
        ]
        activity_rows = []
        if claim_ids:
            activity_rows = (
                s.query(db.ActivityEvent)
                .outerjoin(
                    Remittance,
                    db.ActivityEvent.remittance_id == Remittance.id,
                )
                .filter(or_(
                    db.ActivityEvent.claim_id.in_(claim_ids),
                    Remittance.claim_id.in_(claim_ids),
                ))
                .order_by(desc(db.ActivityEvent.ts))
                .limit(10)
                .all()
            )

    def _activity_to_ui(a):
        return {
            "id": a.id,
            "ts": a.ts.isoformat().replace("+00:00", "Z") if a.ts else "",
            "kind": a.kind,
            "batchId": a.batch_id,
            "claimId": a.claim_id,
            "remittanceId": a.remittance_id,
            "payload": a.payload_json or {},
        }

    provider_dict["recent_claims"] = recent_claims
    provider_dict["recent_activity"] = [_activity_to_ui(a) for a in activity_rows]
    return provider_dict
