"""Compute the Inbox lanes from the DB on read.

SP6 T6.

Lanes:
  - rejected:        claims whose 999 AK5 set-level response was R/E
  - payer_rejected:  claims whose 277CA STC category is A4/A6/A7
                     (SP10: payer-side rejection, distinct from 999
                     envelope rejection)
  - candidates:      remits without a matched claim, with scoreable claims
  - unmatched:       claims still SUBMITTED with no remittance in flight
  - done_today:      claims that reached a terminal state in the last 24h
"""
from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import Iterable

from sqlalchemy.orm import Session

from cyclone.db import Claim, ClaimState, Remittance
from cyclone.scoring import score_pair, ScoreBreakdown


@dataclass
class Lanes:
    rejected: list[dict] = field(default_factory=list)
    payer_rejected: list[dict] = field(default_factory=list)
    candidates: list[dict] = field(default_factory=list)
    unmatched: list[dict] = field(default_factory=list)
    done_today: list[dict] = field(default_factory=list)


def _isoformat(value) -> str | None:
    if value is None:
        return None
    if hasattr(value, "isoformat"):
        return value.isoformat()
    return str(value)


def _claim_to_row(
    c: Claim,
    *,
    kind: str,
    score: ScoreBreakdown | None = None,
    matched_remittance: dict | None = None,
) -> dict:
    return {
        "id": c.id,
        "kind": kind,
        "patient_control_number": c.patient_control_number,
        "charge_amount": float(c.charge_amount) if c.charge_amount is not None else None,
        "payer_id": c.payer_id,
        "provider_npi": c.provider_npi,
        "state": c.state.value if hasattr(c.state, "value") else str(c.state),
        "rejection_reason": c.rejection_reason,
        "rejected_at": _isoformat(c.rejected_at),
        "service_date_from": _isoformat(c.service_date_from),
        "score": score.total if score else None,
        "score_tier": score.tier if score else None,
        "score_breakdown": {
            "patient": score.patient,
            "date": score.date,
            "amount": score.amount,
            "provider": score.provider,
        } if score else None,
        "matched_remittance": matched_remittance,
        # SP10: payer-side rejection fields. Populated when a 277CA
        # acknowledged the claim with STC A4/A6/A7.
        "payer_rejected_at": _isoformat(c.payer_rejected_at),
        "payer_rejected_reason": c.payer_rejected_reason,
        "payer_rejected_status_code": c.payer_rejected_status_code,
        "payer_rejected_by_277ca_id": c.payer_rejected_by_277ca_id,
        # SP14: acknowledgment tracking. Always null on the lane
        # (we filter acknowledged claims out) but exposed for
        # forward-compat if we later add a "Recently acknowledged"
        # inspector view.
        "payer_rejected_acknowledged_at": _isoformat(c.payer_rejected_acknowledged_at),
        "payer_rejected_acknowledged_actor": c.payer_rejected_acknowledged_actor,
    }


def _remit_to_row(r: Remittance, *, candidates: list[tuple[Claim, ScoreBreakdown]]) -> dict:
    raw = r.raw_json or {}
    return {
        "id": r.id,
        "kind": "remit",
        "payer_claim_control_number": r.payer_claim_control_number,
        "charge_amount": float(r.total_charge) if r.total_charge is not None else None,
        "payer_id": raw.get("payer_id"),
        "rendering_provider_npi": raw.get("rendering_provider_npi"),
        "service_date": _isoformat(r.service_date),
        "candidates": [
            {
                "claim_id": c.id,
                "score": s.total,
                "tier": s.tier,
                "breakdown": {
                    "patient": s.patient, "date": s.date,
                    "amount": s.amount, "provider": s.provider,
                },
            }
            for c, s in candidates[:5]
        ],
    }


class _RemitScoringShim:
    """Adapt a Remittance to the duck-typed protocol score_pair expects.

    The 835 parser stores service_date on the ORM column. PCN, NPI, payer_id
    aren't on Remittance; we read them from raw_json.
    """
    def __init__(self, r: Remittance):
        raw = r.raw_json or {}
        self.patient_control_number = (
            raw.get("patient_control_number") or r.payer_claim_control_number
        )
        self.service_date = r.service_date
        self.charge_amount = r.total_charge
        self.rendering_provider_npi = raw.get("rendering_provider_npi")


def _matched_remittance_block(
    session: Session,
    claim: Claim,
    matched_counts: dict,
    total_lines_by_claim: dict,
) -> dict | None:
    """Build the ``matched_remittance`` block for a paired claim, or None.

    SP7 §5.3: carries the matched remittance id plus per-line counts so
    the ``MatchedRemitCard`` can show a ``3/4 matched lines`` badge.
    """
    if claim.matched_remittance_id is None:
        return None
    return {
        "id": claim.matched_remittance_id,
        "matched_lines": int(matched_counts.get(claim.id, 0)),
        "total_lines": int(total_lines_by_claim.get(claim.id, 0)),
    }


def _line_count_lookup(session: Session, claims: list[Claim]) -> tuple[dict, dict]:
    """Return ({claim_id: matched_count}, {claim_id: total_lines}) for the given claims.

    matched_count = number of LineReconciliation rows with status="matched"
                     tied to this claim.
    total_lines   = len(Claim.raw_json["service_lines"]) — 837 service-line
                     count stored inside the JSON blob.
    """
    from cyclone.db import LineReconciliation
    claim_ids = [c.id for c in claims]
    matched_counts: dict = {}
    total_lines_by_claim: dict = {}
    if not claim_ids:
        return matched_counts, total_lines_by_claim

    matched_rows = (
        session.query(LineReconciliation.claim_id)
        .filter(LineReconciliation.claim_id.in_(claim_ids))
        .filter(LineReconciliation.status == "matched")
        .all()
    )
    for (cid,) in matched_rows:
        matched_counts[cid] = matched_counts.get(cid, 0) + 1

    # total_lines: from raw_json (837 service lines are stored there).
    for c in claims:
        raw = c.raw_json or {}
        total_lines_by_claim[c.id] = len(raw.get("service_lines") or [])

    return matched_counts, total_lines_by_claim


def compute_lanes(session: Session, *, dismissed_pairs: Iterable[frozenset]) -> Lanes:
    lanes = Lanes()
    dismissed = set(dismissed_pairs)

    # --- Rejected ---
    rejected_claims = (
        session.query(Claim).filter(Claim.state == ClaimState.REJECTED).all()
    )
    matched_counts, total_lines_by_claim = _line_count_lookup(session, rejected_claims)
    for c in rejected_claims:
        lanes.rejected.append(_claim_to_row(
            c, kind="claim",
            matched_remittance=_matched_remittance_block(
                session, c, matched_counts, total_lines_by_claim,
            ),
        ))

    # --- Payer-Rejected (SP10) ---
    # Distinct from the 999 envelope "rejected" lane above. A claim
    # lands here when a 277CA STC category code is A4/A6/A7 (rejected
    # by the payer after we submitted a syntactically-valid file).
    # We don't filter by Claim.state here because the claim may still
    # be in SUBMITTED state — the payer just hasn't paid it yet.
    #
    # SP14: filter out claims the operator has already acknowledged.
    # The original payer_rejected_* fields stay intact for audit;
    # only the working surface (this lane) is filtered.
    payer_rejected_claims = (
        session.query(Claim)
        .filter(Claim.payer_rejected_at.is_not(None))
        .filter(Claim.payer_rejected_acknowledged_at.is_(None))
        .all()
    )
    pr_matched, pr_total = _line_count_lookup(session, payer_rejected_claims)
    matched_counts.update(pr_matched)
    total_lines_by_claim.update(pr_total)
    for c in payer_rejected_claims:
        lanes.payer_rejected.append(_claim_to_row(
            c, kind="claim",
            matched_remittance=_matched_remittance_block(
                session, c, matched_counts, total_lines_by_claim,
            ),
        ))

    # --- Done today ---
    cutoff = datetime.now(timezone.utc) - timedelta(hours=24)
    terminal_states = {
        ClaimState.PAID, ClaimState.PARTIAL, ClaimState.DENIED,
        ClaimState.RECONCILED, ClaimState.REVERSED,
    }
    done_claims = list(
        session.query(Claim).filter(
            Claim.state.in_(terminal_states),
            Claim.state_changed_at >= cutoff,
        ).all()
    )
    # Merge counts from the rejected query — these are different claim
    # sets so the dicts can be combined.
    if done_claims:
        dm, dt = _line_count_lookup(session, done_claims)
        matched_counts.update(dm)
        total_lines_by_claim.update(dt)
    for c in done_claims:
        lanes.done_today.append(_claim_to_row(
            c, kind="claim",
            matched_remittance=_matched_remittance_block(
                session, c, matched_counts, total_lines_by_claim,
            ),
        ))

    # --- Unmatched (claims still submitted, no remittance in flight) ---
    for c in session.query(Claim).filter(Claim.state == ClaimState.SUBMITTED).all():
        lanes.unmatched.append(_claim_to_row(c, kind="claim"))

    # --- Candidates (remits not yet matched, with scoreable claims) ---
    unmatched_remits = session.query(Remittance).filter(
        Remittance.claim_id.is_(None),
    ).all()

    submitted_claims = list(
        session.query(Claim).filter(Claim.state == ClaimState.SUBMITTED).all()
    )

    for r in unmatched_remits:
        if r.claim_id:
            continue
        candidates: list[tuple[Claim, ScoreBreakdown]] = []
        shim = _RemitScoringShim(r)
        for c in submitted_claims:
            if c.payer_id and r.raw_json and (r.raw_json.get("payer_id") != c.payer_id):
                continue
            score = score_pair(c, shim)
            if score.tier == "hidden":
                continue
            if frozenset({c.id, r.id}) in dismissed:
                continue
            candidates.append((c, score))
        candidates.sort(key=lambda cs: -cs[1].total)
        if not candidates:
            continue
        lanes.candidates.append(_remit_to_row(r, candidates=candidates))

    return lanes
