"""Parse an X12 TA1 (Interchange Acknowledgment) file.

TA1 is the lowest-level ack in the X12 envelope stack. A TA1 file has
the shape::

    ISA*…~
    TA1*<icn>*<date>*<time>*<ack_code>*<note_code>*<ack_date>~
    IEA*1*<icn>~

This module is intentionally simple — the spec allows for multiple TA1
segments in a single interchange (one per included 837 batch), but in
practice Colorado Medicaid sends exactly one TA1 per file. We accept both
shapes and return the first TA1 we see (the rest are surfaced via the
``raw_segments`` field on the envelope in future versions).

Whole-document problems (missing ISA, no TA1) raise
:class:`CycloneParseError`.
"""
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_ta1 import ParseResultTa1, Ta1Ack
from cyclone.parsers.segments import tokenize

log = logging.getLogger(__name__)


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


def _parse_yyyymmdd(s: str) -> date | None:
    """Parse an X12 date string. Returns None on bad input.

    Accepts:
      * CCYYMMDD (8 digits) — the spec shape for TA102 / TA106.
      * YYMMDD (6 digits) — what Colorado Medicaid sends in practice.
        The century is inferred as 2000 + YY (TA1s older than ~2030
        are out of scope; if that assumption changes, switch to a
        rolling-window strategy).
    """
    if not s or not s.isdigit():
        return None
    if len(s) == 8:
        try:
            return date(int(s[0:4]), int(s[4:6]), int(s[6:8]))
        except ValueError:
            return None
    if len(s) == 6:
        try:
            return date(2000 + int(s[0:2]), int(s[2:4]), int(s[4:6]))
        except ValueError:
            return None
    return None


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


def _build_envelope(segments: list[list[str]], input_file: str) -> Envelope:
    """Build the envelope from ISA. TA1 has no GS/ST — just ISA → TA1 → IEA.

    ``transaction_date`` falls back to a placeholder if ISA09 is missing
    or unparseable; the authoritative date is the one inside the TA1
    segment itself (TA102).
    """
    envelope: Envelope | 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),
                    implementation_guide=None,
                )
            except (IndexError, ValueError) as exc:
                raise CycloneParseError(f"Bad ISA: {exc}") from exc
            break
    if envelope is None:
        raise CycloneParseError("No ISA envelope found")
    return envelope


# --------------------------------------------------------------------------- #
# TA1 segment consumer
# --------------------------------------------------------------------------- #


def _consume_ta1(segments: list[list[str]], idx: int) -> tuple[Ta1Ack, int]:
    """Read a TA1 segment and return a :class:`Ta1Ack`.

    Per X12 005010X231A1:
      TA101 = Interchange Control Number (15/9 digits)
      TA102 = Interchange Date (CCYYMMDD)
      TA103 = Interchange Time (HHMM)
      TA104 = Acknowledgment Code (A/E/R)
      TA105 = Interchange Note Code (3-digit numeric; optional but commonly present)
      TA106 = Interchange Date — date the ack was generated (optional)
    """
    ta1 = segments[idx]
    if ta1[0] != "TA1":
        raise CycloneParseError(f"Expected TA1, got {ta1[0]!r}")
    ctrl = ta1[1] if len(ta1) > 1 else ""
    date_s = ta1[2] if len(ta1) > 2 else ""
    time_s = ta1[3] if len(ta1) > 3 else ""
    code = (ta1[4] if len(ta1) > 4 else "R").strip() or "R"
    note = ta1[5] if len(ta1) > 5 and ta1[5] else None
    ack_date_s = ta1[6] if len(ta1) > 6 and ta1[6] else ""

    # Narrow to the literal type — anything outside the canonical set
    # surfaces as the raw string instead of failing parse, so unknown
    # codes from new payers don't break ingest.
    if code not in ("A", "E", "R"):
        log.warning("Unexpected TA1 ack code %r on icn=%s", code, ctrl)

    return (
        Ta1Ack(
            control_number=ctrl,
            interchange_date=_parse_yyyymmdd(date_s),
            interchange_time=time_s or None,
            ack_code=code,  # type: ignore[arg-type]
            note_code=note,
            ack_generated_date=_parse_yyyymmdd(ack_date_s),
        ),
        idx + 1,
    )


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


def parse_ta1_text(text: str, *, input_file: str = "") -> ParseResultTa1:
    """Parse a complete TA1 document and return a :class:`ParseResultTa1`.

    Permissive: if the file contains multiple TA1 segments, we return the
    first one and warn. The spec allows this for interchanges that wrap
    multiple 837 batches but no real-world Colorado file uses it today.
    """
    segments = tokenize(text)
    envelope = _build_envelope(segments, input_file=input_file)

    ta1_idx: int | None = None
    for i, seg in enumerate(segments):
        if seg[0] == "TA1":
            ta1_idx = i
            break
    if ta1_idx is None:
        raise CycloneParseError("No TA1 segment found")

    ta1, _ = _consume_ta1(segments, ta1_idx)

    # TA102 is the authoritative interchange date — overwrite the placeholder.
    if ta1.interchange_date is not None:
        envelope = envelope.model_copy(update={"transaction_date": ta1.interchange_date})

    # Stable FK target. Mirrors the 999 path: ``<kind>-<ISA13>``.
    source_batch_id = f"TA1-{(envelope.control_number or '').strip() or '000000001'}"

    summary = BatchSummary(
        input_file=input_file,
        control_number=envelope.control_number,
        transaction_date=envelope.transaction_date,
        total_claims=1,    # one TA1 == one interchange acknowledged
        passed=1 if ta1.ack_code == "A" else 0,
        failed=0 if ta1.ack_code == "A" else 1,
    )

    return ParseResultTa1(
        envelope=envelope,
        ta1=ta1,
        summary=summary,
        source_batch_id=source_batch_id,
    )


__all__ = ["parse_ta1_text"]