"""ORM row builders — convert parser output into SQLAlchemy ORM rows.

Each function returns an unsaved ORM object (or inserts related rows
within an existing session). They never commit or close the session —
the caller (``write.add_record``, ``acks.add_ack``, etc.) owns the
transaction boundary.
"""

from __future__ import annotations

import json
from decimal import Decimal

from cyclone import db
from cyclone.db import Claim, ClaimState, Remittance
from cyclone.parsers.models import ClaimOutput
from cyclone.parsers.models_835 import ClaimPayment

from . import utcnow


def _service_dates_from_claim(claim: ClaimOutput) -> tuple[date | None, date | None]:
    """Extract (service_date_from, service_date_to) from a ClaimOutput.

    The 837P model has ``service_lines[*].service_date`` (one per SV1).
    We use the earliest as ``from`` and the latest as ``to``; if there
    are no service lines, both are ``None``.
    """
    dates: list[date] = []
    for sl in claim.service_lines:
        if sl.service_date is not None:
            dates.append(sl.service_date)
    if not dates:
        return None, None
    return min(dates), max(dates)


def _claim_837_row(claim: ClaimOutput, batch_id: str) -> Claim:
    """Build a Claim ORM row from a ClaimOutput. NOT yet persisted."""
    d_from, d_to = _service_dates_from_claim(claim)
    return Claim(
        id=claim.claim_id,
        batch_id=batch_id,
        # SP27 Task 17: Claim.patient_control_number must hold the CLM01
        # claim_submittr's_identifier the 837 sent — that's the value the
        # 835 echoes in CLP01, which the reconcile matcher joins on
        # (reconcile.py:by_pcn), and what 999 / 277CA ACK lookups also
        # use to cross-reference the original claim. Storing
        # subscriber.member_id here (the 2010BA NM109) silently broke
        # every auto-match in production.
        patient_control_number=claim.claim_id or "",
        service_date_from=d_from,
        service_date_to=d_to,
        charge_amount=Decimal(claim.claim.total_charge or 0),
        provider_npi=claim.billing_provider.npi,
        payer_id=claim.payer.id,
        state=ClaimState.SUBMITTED,
        raw_json=json.loads(claim.model_dump_json()),
    )


def _remittance_835_row(cp: ClaimPayment, batch_id: str) -> Remittance:
    """Build a Remittance ORM row from a ClaimPayment. NOT yet persisted."""
    received_at = utcnow()
    # Adjustment amount: sum the CAS rows for the first service line.
    # NOTE: This is a best-effort placeholder used until the reconciliation
    # pass (T10) overwrites it from the persisted CasAdjustment rows. The
    # authoritative value comes from `reconcile.run()`, which sums
    # ``CasAdjustment.amount`` per ``remittance_id`` and writes the result
    # back to ``Remittance.adjustment_amount``. We keep this stub so the
    # row has a sane value if reconciliation is disabled or fails.
    adjustment = Decimal("0")
    if cp.service_payments:
        sp = cp.service_payments[0]
        for adj in sp.adjustments:
            adjustment += adj.amount
    # Use the first service line's service_date as the remit service_date.
    service_date: date | None = None
    if cp.service_payments and cp.service_payments[0].service_date is not None:
        service_date = cp.service_payments[0].service_date
    return Remittance(
        id=cp.payer_claim_control_number,
        batch_id=batch_id,
        payer_claim_control_number=cp.payer_claim_control_number,
        claim_id=None,
        status_code=cp.status_code,
        status_label=cp.status_label,
        total_charge=Decimal(cp.total_charge or 0),
        total_paid=Decimal(cp.total_paid or 0),
        patient_responsibility=cp.patient_responsibility,
        adjustment_amount=adjustment,
        received_at=received_at,
        service_date=service_date,
        is_reversal=cp.status_code in ("21", "22"),
        raw_json=json.loads(cp.model_dump_json()),
    )


def _persist_835_remit(session, cp: "ClaimPayment", remittance_id: str) -> None:
    """SP7: persist ServiceLinePayment + CAS rows for one CLP composite.

    For each 835 SVC composite in ``cp.service_payments``:
      - insert a ServiceLinePayment row (line_number, procedure, modifiers,
        charge, payment, units, service_date).
      - flush to populate slp.id.
      - insert each per-SVC CAS adjustment with ``service_line_payment_id``
        set to slp.id.

    For CLP-level CAS adjustments (``cp.claim_adjustments``, a future
    extension; not produced by today's 835 parser but allowed by the spec):
      - insert CAS rows with ``service_line_payment_id IS NULL``.

    The caller controls the transaction; this function does not commit.
    The 835 ingest site calls this after ``_remittance_835_row`` is
    flushed so the FK target is populated.
    """
    import json as _json
    from cyclone.db import ServiceLinePayment, CasAdjustment

    for svc in cp.service_payments:
        slp = ServiceLinePayment(
            remittance_id=remittance_id,
            line_number=svc.line_number,
            procedure_qualifier=svc.procedure_qualifier,
            procedure_code=svc.procedure_code,
            modifiers_json=_json.dumps(svc.modifiers or []),
            charge=Decimal(str(svc.charge)),
            payment=Decimal(str(svc.payment)),
            units=Decimal(str(svc.units)) if svc.units is not None else None,
            unit_type=svc.unit_type,
            service_date=svc.service_date,
            ref_benefit_plan=svc.ref_benefit_plan,
        )
        session.add(slp)
        session.flush()  # populate slp.id for the FK below

        for adj in svc.adjustments:
            quantity = getattr(adj, "quantity", None)
            session.add(CasAdjustment(
                remittance_id=remittance_id,
                group_code=adj.group_code,
                reason_code=adj.reason_code,
                amount=Decimal(str(adj.amount)),
                quantity=Decimal(str(quantity)) if quantity is not None else None,
                service_line_payment_id=slp.id,
            ))

    # CLP-level CAS (no SVC composite to attach to). Today's parser does
    # not produce these; the branch is forward-compatible.
    for adj in getattr(cp, "claim_adjustments", []) or []:
        quantity = getattr(adj, "quantity", None)
        session.add(CasAdjustment(
            remittance_id=remittance_id,
            group_code=adj.group_code,
            reason_code=adj.reason_code,
            amount=Decimal(str(adj.amount)),
            quantity=Decimal(str(quantity)) if quantity is not None else None,
            service_line_payment_id=None,
        ))


def _claim_status_from_validation(claim: ClaimOutput) -> str:
    """Re-implement the in-memory status rules (sub-project 1 §6.2)."""
    v = claim.validation
    if not v.passed:
        has_r050 = any(e.rule == "R050_diagnosis_present" for e in v.errors)
        return "draft" if has_r050 else "denied"
    if claim.claim.frequency_code == "1":
        return "submitted"
    if v.warnings:
        return "pending"
    return "draft"