"""Pydantic v2 models for parsed 835 ERA (Health Care Claim Payment/Advice) files.

Models mirror the X12 835 005010X221A1 structure:

- ``Envelope`` (ISA/GS/ST) — reused from :mod:`cyclone.parsers.models`
- ``FinancialInfo`` (BPR)
- ``ReassociationTrace`` (TRN)
- ``Payer835`` / ``Payee835`` (Loop 1000A / 1000B)
- ``ClaimPayment`` (Loop 2100) with embedded ``ServicePayment`` (Loop 2110)
- ``ParseResult835`` (batch)

We copy the ``_Base`` model from :mod:`cyclone.parsers.models` because it
carries the custom ``@model_serializer`` that converts ``date`` to ``str``
on JSON output — a behavior the 835 models must match so the FastAPI
output is consistent across transactions.
"""

from __future__ import annotations

from datetime import date
from decimal import Decimal
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, model_serializer, model_validator

from cyclone.parsers.models import Address, BatchSummary, Envelope, ValidationIssue, ValidationReport


# --------------------------------------------------------------------------- #
# Shared base
# --------------------------------------------------------------------------- #


class _Base(BaseModel):
    """Shared Pydantic base; matches the 837P models for JSON consistency."""

    model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)

    @model_serializer(mode="wrap")
    def _serialize(self, handler):  # type: ignore[no-untyped-def]
        data = handler(self)
        for key, value in data.items():
            if isinstance(value, date):
                data[key] = value.isoformat()
        return data


# --------------------------------------------------------------------------- #
# CLP02 Claim Status Code → human-readable label
# --------------------------------------------------------------------------- #

# Source: X12 005010X221A1 element 1029 (Claim Status Code).
# In v1 we keep the subset a 835 ERA actually carries; status_code "25"
# (predetermination pricing only — no payment) is included per the spec.
_CLP_STATUS_LABELS: dict[str, str] = {
    "1": "Processed as Primary",
    "2": "Processed as Secondary",
    "3": "Pending",
    "4": "Denied",
    "19": "Processed Primary, Forwarded",
    "20": "Processed Secondary, Forwarded",
    "21": "Reversal of Previous Payment",
    "22": "Reversal of Previous",
    "23": "Not Our Claim, Forwarded",
    "25": "Predetermination Pricing Only",
}


def claim_status_label(code: str) -> str | None:
    """Return a human-readable label for a CLP02 code, or ``None`` if unknown."""
    return _CLP_STATUS_LABELS.get(code)


# --------------------------------------------------------------------------- #
# Header
# --------------------------------------------------------------------------- #


class FinancialInfo(_Base):
    """BPR — Financial Information."""

    handling_code: str
    paid_amount: Decimal
    credit_debit_flag: str
    payment_method: str | None = None
    payer_tax_id: str | None = None
    payment_date: date | None = None


class ReassociationTrace(_Base):
    """TRN — Reassociation Trace Number."""

    trace_type_code: str
    trace_number: str
    originating_company_id: str
    payer_id: str | None = None


class Payer835(_Base):
    """Loop 1000A — Payer Identification (NM1*PR)."""

    name: str
    id: str | None = None
    address: Address | None = None
    contact_url: str | None = None  # PER04 per the CO Medicaid guide


class Payee835(_Base):
    """Loop 1000B — Payee Identification (NM1*PE)."""

    name: str
    npi: str
    address: Address | None = None


# --------------------------------------------------------------------------- #
# Claim / Service payment
# --------------------------------------------------------------------------- #


class ClaimAdjustment(_Base):
    """CAS — Claim Adjustment (one row per reason/amount pair)."""

    group_code: str
    reason_code: str
    amount: Decimal
    quantity: Decimal | None = None


class ServicePayment(_Base):
    """Loop 2110 — Service Payment Information (SVC composite)."""

    line_number: int
    procedure_qualifier: str
    procedure_code: str
    modifiers: list[str] = Field(default_factory=list)
    charge: Decimal
    payment: Decimal
    units: Decimal | None = None
    unit_type: str | None = None
    service_date: date | None = None
    adjustments: list[ClaimAdjustment] = Field(default_factory=list)
    ref_benefit_plan: str | None = None  # REF*0K per the CO guide


class ClaimPayment(_Base):
    """Loop 2100 — Claim Payment Information (CLP composite)."""

    payer_claim_control_number: str
    status_code: str
    status_label: str | None = None
    total_charge: Decimal
    total_paid: Decimal
    patient_responsibility: Decimal | None = None
    claim_filing_indicator: str | None = None  # CLP06 (e.g. "MC" for Medicaid)
    original_claim_id: str | None = None  # CLP07 — original 837P CLM01
    facility_type: str | None = None  # CLP08 — original 837P CLM05-1
    frequency_code: str | None = None  # CLP09 — original 837P CLM05-3
    per_diem_covered_days: Decimal | None = None  # QTY*CA per the CO guide
    ref_benefit_plan: str | None = None  # REF*CE per the CO guide
    service_payments: list[ServicePayment] = Field(default_factory=list)
    raw_segments: list[list[str]] = Field(default_factory=list)

    @model_validator(mode="before")
    @classmethod
    def _decode_status_label(cls, data: Any) -> Any:
        """Populate ``status_label`` from ``status_code`` when caller omitted it.

        Callers that explicitly set ``status_label`` (e.g. round-tripping from
        JSON) keep their value.
        """
        if isinstance(data, dict) and "status_label" not in data and "status_code" in data:
            data = {**data, "status_label": claim_status_label(str(data["status_code"]))}
        return data


# --------------------------------------------------------------------------- #
# Top-level result
# --------------------------------------------------------------------------- #


class ParseResult835(_Base):
    envelope: Envelope
    financial_info: FinancialInfo
    trace: ReassociationTrace
    payer: Payer835
    payee: Payee835
    claims: list[ClaimPayment] = Field(default_factory=list)
    summary: BatchSummary
    validation: ValidationReport | None = None  # populated by the orchestrator
    # ``True`` when the file contains more than one BPR segment. X12 835
    # allows only one BPR per transaction set, but some payers (notably
    # CO Medicaid) send a "split payment" with several BPRs whose amounts
    # sum to the total paid. The parser accumulates them into a single
    # ``FinancialInfo``; the validator emits a warning so operators see it.
    multi_bpr: bool = False


# --------------------------------------------------------------------------- #
# Re-exports
# --------------------------------------------------------------------------- #

__all__ = [
    "Address",
    "BatchSummary",
    "ClaimAdjustment",
    "ClaimPayment",
    "Envelope",
    "FinancialInfo",
    "ParseResult835",
    "Payer835",
    "Payee835",
    "ReassociationTrace",
    "ServicePayment",
    "ValidationIssue",
    "ValidationReport",
    "claim_status_label",
]


# Silence the "imported but unused" complaint on a few names that exist for
# parity with the 837P module's public surface but are not yet referenced
# in this file's implementation.
_ = (Literal, BaseModel)
