"""Build a 999 ACK from a persisted 837P batch.

Used by the ``?ack=true`` path on ``POST /api/parse-837`` (SP3 P3 T15).
The builder reads the batch's persisted claim rows + their validation
reports, then constructs a :class:`ParseResult999` whose per-set AK5
codes reflect each claim's validation outcome:

- "A" (accepted) when ``claim.validation.passed`` is True
- "R" (rejected) when the claim has any validation errors
- "E" (accepted with errors) is reserved for the warnings-only case,
  but the current 837P validator promotes all warnings to errors in
  ``strict`` mode, so v1 keeps the binary A/R split. The split is
  documented inline below.

The :class:`FunctionalGroupAck` is derived from the per-set codes
(received = total sets, accepted = #A, rejected = #R, ack_code =
"A" / "R" / "P" for partial). The envelope is sourced from the
batch's stored ``raw_result_json.envelope`` — the receiver is
swapped with the sender (the 999 is sent back the other direction).
"""
from __future__ import annotations

import json
from datetime import date, datetime
from typing import Any

from sqlalchemy import select

from cyclone import db
from cyclone.db import Batch, Claim
from cyclone.parsers.models import BatchSummary, Envelope
from cyclone.parsers.models_999 import (
    AcknowledgmentHeader,
    FunctionalGroupAck,
    ParseResult999,
    SetAcceptReject,
    SetFunctionalGroupResponse,
)


def _claim_set_code(claim_validation: dict) -> str:
    """Return the AK5 code for a single claim based on its validation report.

    Auto-ACK semantics (SP3 P3 T15):

      - "A" = accepted (validation.passed is True; no errors and no
        warnings to report)
      - "R" = rejected (validation.passed is False; errors present)
      - "E" = accepted with errors (warnings present but no errors) —
        currently unreachable from the 837P validator (warnings are
        always paired with errors in practice); reserved for forward
        compatibility.

    The "rejected" signal uses the same ``validation.passed`` flag the
    837P summary uses, so the 999 always agrees with the API response
    (no second opinion).
    """
    passed = bool(claim_validation.get("passed"))
    if passed:
        return "A"
    return "R"


def _claim_set_code_from_orm(claim: Claim) -> str:
    """Return the AK5 code for one Claim ORM row, reading its raw_json."""
    raw = claim.raw_json or {}
    validation = raw.get("validation") or {}
    return _claim_set_code(validation)


def _parse_envelope_date(value: Any) -> date | None:
    """Coerce an envelope.transaction_date value to a date.

    The batch's raw_result_json round-trips through Pydantic so the
    stored value may be a date, an ISO string, or a datetime. Try the
    cheap paths first; fall back to None on failure so the builder
    never raises on a bad envelope.
    """
    if isinstance(value, datetime):
        return value.date()
    if isinstance(value, date):
        return value
    if isinstance(value, str) and value:
        try:
            return date.fromisoformat(value[:10])
        except ValueError:
            return None
    return None


def build_ack_for_batch(
    batch_id: str,
    *,
    interchange_control_number: str = "000000001",
) -> ParseResult999:
    """Read the batch's claims + validation reports and build a 999 ACK.

    For each persisted claim:
      - ``claim.validation.passed`` is True  -> AK5 code = "A"
      - else                                  -> AK5 code = "R"

    The envelope's sender/receiver are swapped vs. the source 837
    (the 999 is the destination's response, so the destination is
    the 999's sender and vice versa). The transaction date is the
    current date — the 999 is generated in real-time at the 837
    ingest boundary.

    Returns a fully-formed :class:`ParseResult999`. No side effects
    (no DB writes) — the caller persists the result via
    ``store.add_ack(...)``.
    """
    with db.SessionLocal()() as s:
        batch_row = s.get(Batch, batch_id)
        if batch_row is None:
            raise LookupError(f"batch {batch_id} not found")
        # Read the source envelope (sender/receiver/control number) from
        # the batch's stored raw_result_json.
        raw_result = batch_row.raw_result_json or {}
        env_data = raw_result.get("envelope") or {}
        # We deliberately read the envelope fields via Pydantic so
        # date/Decimal/None coercion is consistent with the rest of
        # the parser surface.
        try:
            src_envelope = Envelope.model_validate(env_data)
        except Exception:
            src_envelope = None
        claims = (
            s.execute(
                select(Claim)
                .where(Claim.batch_id == batch_id)
                .order_by(Claim.id.asc())
            )
            .scalars()
            .all()
        )
        # Determine the control number for the source ST. We don't
        # have a separate ST table; fall back to the batch id when
        # the envelope didn't carry one. The auto-ACK's AK2.set_control
        # is the 837's ST02, which the parser stores as envelope.control
        # for single-batch 837s. For multi-batch this is approximate.
        src_control = (
            env_data.get("control_number")
            or (src_envelope.control_number if src_envelope else None)
            or batch_id
        )
        functional_id = "HC"  # health-care claim

    # Build per-set responses. We use the 837's ST01 ("837") as the
    # AK2 functional_id_code, per X12 spec.
    set_responses: list[SetFunctionalGroupResponse] = []
    accepted_count = 0
    rejected_count = 0
    for claim in claims:
        code = _claim_set_code_from_orm(claim)
        if code == "A":
            accepted_count += 1
        else:
            rejected_count += 1
        set_responses.append(SetFunctionalGroupResponse(
            ak2=AcknowledgmentHeader(
                functional_id_code="837",
                group_control_number=claim.id,
            ),
            set_control_number=claim.id,
            transaction_set_identifier="837",
            segment_errors=[],  # v1: no per-segment error drilldown
            set_accept_reject=SetAcceptReject(code=code),  # type: ignore[arg-type]
        ))

    # If the source batch had no claims, synthesize a single
    # accepted set so the 999 is structurally valid (otherwise the
    # AK9 row would be empty / illegal).
    if not set_responses:
        set_responses.append(SetFunctionalGroupResponse(
            ak2=AcknowledgmentHeader(
                functional_id_code="837", group_control_number=src_control,
            ),
            set_control_number=str(src_control),
            transaction_set_identifier="837",
            segment_errors=[],
            set_accept_reject=SetAcceptReject(code="A"),
        ))
        accepted_count = 1

    received = len(set_responses)
    if rejected_count == 0:
        ack_code = "A"
    elif accepted_count == 0:
        ack_code = "R"
    else:
        ack_code = "P"

    fg = FunctionalGroupAck(
        ak1=AcknowledgmentHeader(
            functional_id_code=functional_id,
            group_control_number=str(src_control or "1"),
        ),
        received_count=received,
        accepted_count=accepted_count,
        rejected_count=rejected_count,
        ack_code=ack_code,  # type: ignore[arg-type]
    )

    # Build the envelope for the 999. Swap sender/receiver so the 999
    # is addressed to the source 837's sender.
    today = date.today()
    if src_envelope is not None:
        new_env = Envelope(
            sender_id=src_envelope.receiver_id,
            receiver_id=src_envelope.sender_id,
            control_number=interchange_control_number,
            transaction_date=today,
            implementation_guide="005010X231A1",
        )
    else:
        new_env = Envelope(
            sender_id="CYCLONE",
            receiver_id="UNKNOWN",
            control_number=interchange_control_number,
            transaction_date=today,
            implementation_guide="005010X231A1",
        )

    summary = BatchSummary(
        input_file=batch_id,
        control_number=interchange_control_number,
        transaction_date=today,
        total_claims=received,
        passed=accepted_count,
        failed=rejected_count,
    )

    return ParseResult999(
        envelope=new_env,
        functional_group_acks=[fg],
        set_responses=set_responses,
        summary=summary,
    )


__all__ = ["build_ack_for_batch"]
