"""Handle a 999 Implementation Acknowledgment file (SP27 Task 2).

Lifted verbatim from ``scheduler.py:_handle_999``. The handler owns
its own DB session, dispatches to ``parse_999_text``, applies 999
rejections to any matched claims via ``inbox_state.apply_999_rejections``,
persists the ack row, and returns a ``(parser_used, claim_count)``
tuple.

The actor tag (``"999-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 —
see Task 6 for the API migration that drops the inline copy.

``claim_count`` mirrors ``parsed.received`` — the count of AK2
(set-level) responses in the 999, which is the authoritative
per-claim accept/reject signal (see ``_ack_id`` for why AK9 is
ignored).
"""
from __future__ import annotations

import json
import logging

from cyclone import db
from cyclone.audit_log import AuditEvent, append_event
from cyclone.handlers._ack_id import (
    ack_count_summary,
    ack_synthetic_source_batch_id,
)
from cyclone.inbox_state import apply_999_rejections
from cyclone.parsers.exceptions import CycloneParseError
from cyclone.parsers.parse_999 import parse_999_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 999, apply rejections, persist ack row.

    Args:
        text: Raw 999 document bytes (decoded).
        source_file: Filename the 999 came from. Used to derive a
            unique synthetic ``batches.id`` (see ``_ack_id``).

    Returns:
        ``(parser_used, claim_count)`` tuple. Matches the scheduler
        ``_download_and_parse`` destructure; the HandleResult
        migration happens in Task 7.

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

    SP25: the store (``cyclone.store.acks.add_999_ack``) now owns the
    publish-from-store contract; the handler no longer needs an
    ``event_bus`` kwarg. Both the scheduler path (no bus) and the
    FastAPI endpoint path (passes the bus to the store directly) see
    the same row, the same event, and the same payload.
    """
    try:
        result = parse_999_text(text, input_file=source_file)
    except CycloneParseError as exc:
        raise ValueError(f"999 parse error: {exc}") from exc

    received, accepted, rejected, ack_code = ack_count_summary(result)
    icn = result.envelope.control_number
    pcn = (
        result.set_responses[0].set_control_number
        if result.set_responses else None
    )
    synthetic_id = ack_synthetic_source_batch_id(
        icn, pcn=pcn, source_filename=source_file,
    )

    with db.SessionLocal()() as session:
        def _lookup(pcn: str):
            return (
                session.query(db.Claim)
                .filter_by(patient_control_number=pcn)
                .first()
            )
        rejection_result = apply_999_rejections(
            session, result, claim_lookup=_lookup,
        )
        if rejection_result.matched:
            for cid in rejection_result.matched:
                append_event(session, AuditEvent(
                    event_type="claim.rejected",
                    entity_type="claim",
                    entity_id=cid,
                    payload={"source_batch_id": synthetic_id},
                    actor="999-parser-scheduler",
                ))
        row = cycl_store.add_ack(
            source_batch_id=synthetic_id,
            accepted_count=accepted,
            rejected_count=rejected,
            received_count=received,
            ack_code=ack_code,
            raw_json=json.loads(result.model_dump_json()),
        )
        session.commit()

    return ("parse_999", received)
