r"""Orchestrate parsing an X12 999 (Implementation Acknowledgment) file.

Single-pass walker over the tokenized segment list:

- ISA / GS / ST — envelope
- AK1 (Functional Group Response Header) — starts a new functional group
- AK2 (Transaction Set Response Header) — starts a new set
- AK3 (Segment Context) + AK4 (Element Context) — optional per-segment errors
- AK5 (Transaction Set Response Status) — per-set accept/reject
- AK9 (Functional Group Response Status) — per-group counts + ack code
- SE / GE / IEA

Errors at the file level raise :class:`CycloneParseError`. The parser
is intentionally lenient: per-set error segments are surfaced via the
``SetFunctionalGroupResponse.segment_errors`` field, not raised.
"""
from __future__ import annotations

import logging
from datetime import date

from cyclone.parsers.exceptions import CycloneParseError
from cyclone.parsers.models import BatchSummary, Envelope
from cyclone.parsers.models_999 import (
    AcknowledgmentHeader,
    FunctionalGroupAck,
    ParseResult999,
    SegmentContext,
    SegmentError,
    SetAcceptReject,
    SetFunctionalGroupResponse,
)
from cyclone.parsers.segments import tokenize

log = logging.getLogger(__name__)


# --------------------------------------------------------------------------- #
# Date parsing
# --------------------------------------------------------------------------- #


def _parse_yyyymmdd(s: str) -> date | None:
    """Parse an 8-digit CCYYMMDD string. Returns None on bad input."""
    if not s or len(s) != 8 or not s.isdigit():
        return None
    try:
        return date(int(s[0:4]), int(s[4:6]), int(s[6:8]))
    except ValueError:
        return None


# --------------------------------------------------------------------------- #
# Envelope
# --------------------------------------------------------------------------- #


def _build_envelope(segments: list[list[str]], input_file: str) -> tuple[Envelope, date | None]:
    """Build the 999 envelope from ISA/GS/ST. Returns (envelope, txn_date).

    ``txn_date`` is sourced from the GS04 (functional group creation
    date, CCYYMMDD). Falls back to a placeholder date if GS04 is
    missing or unparseable.
    """
    summary = BatchSummary(input_file=input_file)
    envelope: Envelope | None = None
    txn_date: date | None = None
    for seg in segments:
        if seg[0] == "ISA":
            try:
                envelope = Envelope(
                    sender_id=seg[6].strip(),
                    receiver_id=seg[8].strip(),
                    control_number=seg[13].strip(),
                    transaction_date=date(2024, 1, 1),  # overwritten by GS04 below
                    implementation_guide=None,
                )
            except (IndexError, ValueError) as exc:
                raise CycloneParseError(f"Bad ISA: {exc}") from exc
        elif seg[0] == "GS" and envelope is not None:
            if len(seg) > 4:
                txn_date = _parse_yyyymmdd(seg[3]) or txn_date
        elif seg[0] == "ST" and envelope is not None:
            if len(seg) > 3:
                envelope = envelope.model_copy(update={"implementation_guide": seg[3]})
    if envelope is None:
        raise CycloneParseError("No ISA envelope found")
    if txn_date is not None:
        envelope = envelope.model_copy(update={"transaction_date": txn_date})
    return envelope, txn_date


# --------------------------------------------------------------------------- #
# Per-set / per-group response
# --------------------------------------------------------------------------- #


def _consume_ak3_ak4(segments: list[list[str]], idx: int) -> tuple[list[SegmentError], int]:
    """Read consecutive AK3 (and optional AK4 children) segments and return them.

    AK3 carries the segment context (AK3-1 segment id, AK3-2 position,
    AK3-3 loop id, AK3-4 implementation convention reference). AK4
    attaches to the immediately preceding AK3 and carries the element
    position / reference / error code. Per the X12 999 spec a given
    AK3 may have one or more AK4 children, or none at all.
    """
    errors: list[SegmentError] = []
    while idx < len(segments) and segments[idx][0] == "AK3":
        ak3 = segments[idx]
        idx += 1
        seg_id = ak3[1] if len(ak3) > 1 else ""
        seg_pos = int(ak3[2]) if len(ak3) > 2 and ak3[2].isdigit() else 0
        loop_id = ak3[3] if len(ak3) > 3 and ak3[3] else None
        impl_ref = ak3[4] if len(ak3) > 4 and ak3[4] else None
        ctx = SegmentContext(
            segment_id=seg_id,
            segment_position=seg_pos,
            loop_id=loop_id,
            implementation_convention_ref=impl_ref,
        )
        # Consume any following AK4 segments as children of this AK3.
        # Per X12 005010X231A1:
        #   AK4-1 = Element Position in Segment (integer)
        #   AK4-2 = Component Data Element Position in Composite (integer, optional)
        #   AK4-3 = Data Element Reference Number (integer, optional)
        #   AK4-4 = Data Element Syntax Error Code (code, optional)
        while idx < len(segments) and segments[idx][0] == "AK4":
            ak4 = segments[idx]
            idx += 1
            el_pos = (
                int(ak4[1]) if len(ak4) > 1 and ak4[1].isdigit() else None
            )
            el_ref = (
                int(ak4[3]) if len(ak4) > 3 and ak4[3].isdigit() else None
            )
            err_code = ak4[4] if len(ak4) > 4 and ak4[4] else ""
            errors.append(SegmentError(
                context=ctx,
                error_code=err_code,
                element_position=el_pos,
                element_reference=el_ref,
            ))
    return errors, idx


def _consume_ak2(segments: list[list[str]], idx: int) -> SetFunctionalGroupResponse | None:
    """Read an AK2 + its child AK3*/AK4* + AK5 segments, return the SetResponse.

    Returns None when called with a non-AK2 segment (defensive — the
    orchestrator only calls this when it sees AK2).
    """
    ak2 = segments[idx]
    if ak2[0] != "AK2":
        return None
    txn_set_id = ak2[1] if len(ak2) > 1 else ""
    set_ctrl = ak2[2] if len(ak2) > 2 else ""
    ak2_header = AcknowledgmentHeader(
        functional_id_code=txn_set_id,
        group_control_number=set_ctrl,
    )
    idx += 1
    # Optional AK3/AK4 cluster
    seg_errors: list[SegmentError] = []
    if idx < len(segments) and segments[idx][0] == "AK3":
        seg_errors, idx = _consume_ak3_ak4(segments, idx)
    # AK5 (set accept/reject) — required by the spec; default to "R" if missing.
    accept_code = "R"
    if idx < len(segments) and segments[idx][0] == "AK5":
        ak5 = segments[idx]
        if len(ak5) > 1 and ak5[1]:
            accept_code = ak5[1]
        idx += 1
    return SetFunctionalGroupResponse(
        ak2=ak2_header,
        set_control_number=set_ctrl,
        transaction_set_identifier=txn_set_id,
        segment_errors=seg_errors,
        set_accept_reject=SetAcceptReject(code=accept_code),  # type: ignore[arg-type]
    )


def _consume_ak9(segments: list[list[str]], idx: int) -> tuple[FunctionalGroupAck, int]:
    """Read the AK9 segment and return a FunctionalGroupAck.

    AK9-1 = ack_code, AK9-2 = received_count, AK9-3 = accepted_count,
    AK9-4 = rejected_count. All four are required by the spec.
    """
    ak9 = segments[idx]
    code = ak9[1] if len(ak9) > 1 else "R"
    received = int(ak9[2]) if len(ak9) > 2 and ak9[2].isdigit() else 0
    accepted = int(ak9[3]) if len(ak9) > 3 and ak9[3].isdigit() else 0
    rejected = int(ak9[4]) if len(ak9) > 4 and ak9[4].isdigit() else 0
    idx += 1
    return (
        FunctionalGroupAck(
            ak1=current_ak1_header if current_ak1_header is not None else AcknowledgmentHeader(
                functional_id_code="", group_control_number="",
            ),
            received_count=received,
            accepted_count=accepted,
            rejected_count=rejected,
            ack_code=code,  # type: ignore[arg-type]
        ),
        idx,
    )


# Module-level slot for the most recently seen AK1 header. The parser
# uses a single AK1 per functional group, and the AK9 consumer needs
# to attach the AK1 to the FunctionalGroupAck. We keep this in a small
# holder so the orchestrator can update it as it walks.
current_ak1_header: AcknowledgmentHeader | None = None


# --------------------------------------------------------------------------- #
# Top-level orchestrator
# --------------------------------------------------------------------------- #


def parse_999_text(text: str, *, input_file: str = "") -> ParseResult999:
    """Parse a complete 999 ACK document and return a :class:`ParseResult999`.

    The function is intentionally permissive: per-segment AK3/AK4
    errors are surfaced on the response (never raised) so the API
    layer can pass them through to the UI. Whole-document problems
    (missing ISA, no functional group) still raise
    :class:`CycloneParseError`.
    """
    global current_ak1_header
    current_ak1_header = None

    segments = tokenize(text)
    envelope, txn_date = _build_envelope(segments, input_file=input_file)

    functional_group_acks: list[FunctionalGroupAck] = []
    set_responses: list[SetFunctionalGroupResponse] = []

    i = 0
    while i < len(segments):
        seg = segments[i]
        if seg[0] in {"ISA", "GS", "ST", "SE", "GE", "IEA"}:
            i += 1
            continue
        if seg[0] == "AK1":
            func_id = seg[1] if len(seg) > 1 else ""
            grp_ctrl = seg[2] if len(seg) > 2 else ""
            current_ak1_header = AcknowledgmentHeader(
                functional_id_code=func_id,
                group_control_number=grp_ctrl,
            )
            i += 1
            continue
        if seg[0] == "AK2":
            sr = _consume_ak2(segments, i)
            if sr is not None:
                set_responses.append(sr)
                # Advance past the AK2 + AK3*/AK4*/AK5 cluster
                # (re-walk from i+1 because _consume_ak2 doesn't return idx).
                i += 1
                while i < len(segments) and segments[i][0] in {"AK3", "AK4", "AK5"}:
                    i += 1
            else:
                i += 1
            continue
        if seg[0] == "AK9":
            fg, i = _consume_ak9(segments, i)
            functional_group_acks.append(fg)
            continue
        # Unknown segment inside a 999 (rare in practice); skip it.
        i += 1

    if not functional_group_acks:
        raise CycloneParseError("No AK9 (Functional Group Response Status) segment found")

    # Summary counts: total sets = total responses; pass/fail from AK5 codes.
    total_sets = len(set_responses)
    accepted = sum(
        1 for s in set_responses if s.set_accept_reject.code == "A"
    )
    failed = total_sets - accepted
    summary = BatchSummary(
        input_file=input_file,
        control_number=envelope.control_number,
        transaction_date=envelope.transaction_date,
        total_claims=total_sets,
        passed=accepted,
        failed=failed,
    )

    return ParseResult999(
        envelope=envelope,
        functional_group_acks=functional_group_acks,
        set_responses=set_responses,
        summary=summary,
    )


__all__ = ["parse_999_text"]
