"""277CA Claim Acknowledgment → claim payer-rejection state transitions.

SP10 T2.

For each ``ClaimStatus`` in a parsed 277CA whose ``classification``
is ``"rejected"`` (STC category codes A4 / A6 / A7), look up the
matching Cyclone claim row by ``payer_claim_control_number`` and stamp
``payer_rejected_at`` + ``payer_rejected_reason`` + the STC category
code + the originating 277CA row id.

Distinct from :func:`cyclone.inbox_state.apply_999_rejections` (which
handles envelope-level 999 AK5 R/E rejections). A claim can be:
  * rejected at the envelope level only (999 R, no 277CA yet)
  * payer-rejected only (999 A, then 277CA STC A6 — file was fine,
    claim was denied)
  * both (rare — envelope retry after payer rejection)

We never overwrite a previous ``payer_rejected_at`` with ``NULL``
(empty STC list) — that prevents a later, looser 277CA from
accidentally clearing the rejection flag.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Callable

from sqlalchemy.orm import Session

from cyclone.db import Claim


@dataclass
class Apply277CAResult:
    matched: list[str] = field(default_factory=list)
    orphans: list[str] = field(default_factory=list)
    already_rejected: list[str] = field(default_factory=list)


def _build_reason(status_code: str, status_description: str | None) -> str:
    parts = [f"277CA STC {status_code}"]
    if status_description:
        parts.append(f"({status_description})")
    return " ".join(parts)


def apply_277ca_rejections(
    session: Session,
    parsed_277ca,
    *,
    claim_lookup: Callable[[str], Claim | None],
    two77ca_id: str,
) -> Apply277CAResult:
    """For each rejected ``ClaimStatus``, look up the matching claim and stamp it.

    Args:
        session: SQLAlchemy session.
        parsed_277ca: a :class:`cyclone.parsers.models_277ca.ParseResult277CA`.
        claim_lookup: callable from ``payer_claim_control_number`` →
            ``Claim`` or ``None``. Use REF*1K (the payer claim control
            number) — that's the canonical cross-reference.
        two77ca_id: the id of the persisted 277CA row that produced
            this parsed result. Stored on each claim so an operator
            can trace the rejection back to the source file.

    Returns:
        Apply277CAResult with matched/orphan/already-rejected lists.
    """
    result = Apply277CAResult()
    now = datetime.now(timezone.utc)

    for status in parsed_277ca.claim_statuses:
        if status.classification != "rejected":
            continue
        pcn = status.payer_claim_control_number
        if not pcn:
            # No REF*1K — we can't tie this rejection to a Cyclone claim.
            # Surface it as an orphan so the operator knows we saw it.
            result.orphans.append(status.status_code or "")
            continue

        claim = claim_lookup(pcn)
        if claim is None:
            result.orphans.append(pcn)
            continue

        if claim.payer_rejected_at is not None:
            # Idempotent — already stamped. Update only if this is a
            # newer status code (e.g. A6 → A7 escalation).
            if (claim.payer_rejected_status_code or "") == status.status_code:
                result.already_rejected.append(claim.id)
                continue

        claim.payer_rejected_at = now
        claim.payer_rejected_reason = _build_reason(
            status.status_code, status.status_description,
        )
        claim.payer_rejected_status_code = status.status_code
        claim.payer_rejected_by_277ca_id = two77ca_id
        result.matched.append(claim.id)

    if result.matched or result.already_rejected:
        session.commit()

    return result


__all__ = ["Apply277CAResult", "apply_277ca_rejections"]
