"""Handle a 277CA Claim Acknowledgment file (SP27 Task 4).

Lifted verbatim from ``scheduler.py:_handle_277ca``. The handler owns
its own DB session, dispatches to ``parse_277ca_text``, persists the
277CA ack row, applies 277CA rejections to matched claims via
``inbox_state.apply_277ca_rejections``, and emits
``claim.payer_rejected`` audit events for each newly-stamped claim.

The actor tag (``"277ca-parser-scheduler"``) is preserved so the
audit log keeps tracing back to the same source after extraction.
Both the FastAPI endpoint and the scheduler path (re)use this module
— the API migration drops the inline copy in Task 6.

``claim.rejected_after_remit`` audit emission (when a 277CA rejects
a claim that already has ``matched_remittance_id`` set) is deferred
to SP27 Task 13. Today the handler only emits ``claim.payer_rejected``.
"""
from __future__ import annotations

import json
import logging

from cyclone import db
from cyclone.audit_log import AuditEvent, append_event
from cyclone.claim_acks import apply_277ca_acks
from cyclone.handlers._ack_id import two77ca_synthetic_source_batch_id
from cyclone.inbox_state_277ca import apply_277ca_rejections
from cyclone.parsers.exceptions import CycloneParseError
from cyclone.parsers.parse_277ca import parse_277ca_text
from cyclone.store import store as cycl_store

log = logging.getLogger(__name__)


def handle(
    text: str,
    source_file: str,
) -> tuple[str, int]:
    """Parse a 277CA, persist ack + stamp payer-rejected claims.

    Args:
        text: Raw 277CA document bytes (decoded).
        source_file: Filename the 277CA came from.

    Returns:
        ``(parser_used, claim_count)`` tuple where ``claim_count`` is
        the number of STC statuses in the file (one per claim).

    Raises:
        ValueError: on parser-level failure (wraps CycloneParseError).

    SP25: the store (``cyclone.store.acks.add_277ca_ack``) owns the
    publish-from-store contract; the handler no longer needs an
    ``event_bus`` kwarg.
    """
    try:
        result = parse_277ca_text(text, input_file=source_file)
    except CycloneParseError as exc:
        raise ValueError(f"277CA parse error: {exc}") from exc

    icn = result.envelope.control_number
    synthetic_id = two77ca_synthetic_source_batch_id(icn)
    accepted = sum(
        1 for s in result.claim_statuses if s.classification == "accepted"
    )
    paid = sum(
        1 for s in result.claim_statuses if s.classification == "paid"
    )
    rejected = sum(
        1 for s in result.claim_statuses if s.classification == "rejected"
    )
    pended = sum(
        1 for s in result.claim_statuses if s.classification == "pended"
    )

    # Build the batch envelope index BEFORE opening the work session
    # — cycl_store.batch_envelope_index opens its own short-lived
    # session, and SQLite + concurrent sessions causes "database is
    # locked" errors.
    batch_index = cycl_store.batch_envelope_index()

    with db.SessionLocal()() as session:
        row = cycl_store.add_277ca_ack(
            source_batch_id=synthetic_id,
            control_number=icn,
            accepted_count=accepted,
            rejected_count=rejected,
            paid_count=paid,
            pended_count=pended,
            raw_json=json.loads(result.model_dump_json()),
        )
        def _lookup(pcn: str):
            return (
                session.query(db.Claim)
                .filter(db.Claim.patient_control_number == pcn)
                .first()
            )
        apply_result = apply_277ca_rejections(
            session, result, claim_lookup=_lookup, two77ca_id=row.id,
        )
        if apply_result.matched:
            for cid in apply_result.matched:
                append_event(session, AuditEvent(
                    event_type="claim.payer_rejected",
                    entity_type="claim",
                    entity_id=cid,
                    payload={"source_batch_id": synthetic_id, "277ca_id": row.id},
                    actor="277ca-parser-scheduler",
                ))
        # SP28: auto-link the 277CA ClaimStatus entries to claims (D10
        # two-pass join). The helper builds dataclass rows; the
        # caller persists each via cycl_store.add_claim_ack so the
        # publish-from-store contract owns the live-tail event.
        def _pcn_lookup(pcn: str):
            return (
                session.query(db.Claim)
                .filter(db.Claim.patient_control_number == pcn)
                .first()
            )

        link_result = apply_277ca_acks(
            session, result, ack_id=row.id,
            batch_envelope_index=batch_index,
            pc_claim_lookup=_pcn_lookup,
        )
        # Snapshot the rows before closing the work session — SQLite
        # + concurrent sessions from the same thread cause "database
        # is locked" errors when add_claim_ack opens its own session
        # while this one is still open.
        link_rows = list(link_result.linked)
        orphans = list(link_result.orphans)
        session.commit()

    for link_row in link_rows:
        cycl_store.add_claim_ack(
            claim_id=link_row.claim_id,
            batch_id=link_row.batch_id,
            ack_id=row.id,
            ack_kind="277ca",
            ak2_index=link_row.ak2_index,
            set_control_number=link_row.set_control_number,
            set_accept_reject_code=link_row.set_accept_reject_code,
            linked_by="auto",
        )
    if orphans:
        log.warning(
            "277CA had %d orphan status entries (no matching claim): %s",
            len(orphans),
            orphans[:5],
        )

    return ("parse_277ca", len(result.claim_statuses))
