"""Serialize a :class:`ParseResult999` to a complete X12 999 ACK text.

Emits the envelope layers (ISA / GS / ST / SE / GE / IEA) and the
body segments (AK1, AK2 / AK3 / AK4 / AK5 per set, AK9). Uses the
standard X12 delimiters (``*`` element separator, ``~`` segment
terminator, ``:`` component separator, ``^`` repetition separator).

This is the mirror of :mod:`cyclone.parsers.writer` and
:mod:`cyclone.parsers.writer_835`, but for 999: those modules write
parsed claim/remit rows to disk as JSON files, while this one writes
an X12 document string. The output round-trips through
:func:`cyclone.parsers.parse_999.parse_999_text` — see the tests in
``backend/tests/test_serialize_999.py``.

The serializer does NOT recompute the AK9 / AK5 counts: it relies on
the caller's ``FunctionalGroupAck`` counts. The auto-ACK builder
(T15) is responsible for setting those counts correctly from the
per-set ``SetAcceptReject`` codes.
"""
from __future__ import annotations

from datetime import date

from cyclone.parsers.models_999 import ParseResult999

_SEG = "~"
_ELEM = "*"


def _today_yymmdd() -> str:
    """Return today's date as YYMMDD for ISA09 / GS04."""
    t = date.today()
    return f"{t.year % 100:02d}{t.month:02d}{t.day:02d}"


def _today_yyyymmdd() -> str:
    """Return today's date as CCYYMMDD for GS04."""
    t = date.today()
    return f"{t.year:04d}{t.month:02d}{t.day:02d}"


def _pad(s: str, width: int) -> str:
    """Pad a string to ``width`` characters; truncate if longer."""
    return (s or "").ljust(width)[:width]


def _build_isa(
    sender_id: str,
    receiver_id: str,
    interchange_control_number: str,
) -> str:
    """Build the ISA envelope segment (fixed 106-char layout).

    Per the X12 spec the ISA is a fixed-width 16-element record.
    Element lengths:
      ISA01 (auth qualifier)         2
      ISA02 (auth info)              10
      ISA03 (security qualifier)     2
      ISA04 (security info)          10
      ISA05 (sender id qualifier)    2
      ISA06 (sender id)              15
      ISA07 (receiver id qualifier)  2
      ISA08 (receiver id)            15
      ISA09 (interchange date)       6  (YYMMDD)
      ISA10 (interchange time)       4  (HHMM)
      ISA11 (repetition separator)   1
      ISA12 (control version)        5
      ISA13 (interchange ctrl num)   9
      ISA14 (ack requested)          1
      ISA15 (usage indicator)        1
      ISA16 (component separator)    1
    """
    parts = [
        "ISA",            # 0
        "00",             # 1
        _pad("", 10),     # 2
        "00",             # 3
        _pad("", 10),     # 4
        "ZZ",             # 5
        _pad(sender_id, 15),  # 6
        "ZZ",             # 7
        _pad(receiver_id, 15),  # 8
        _today_yymmdd(),  # 9
        "0000",           # 10
        "^",              # 11 (repetition separator)
        "00501",          # 12
        _pad(interchange_control_number, 9),  # 13
        "0",              # 14 (no TA1 ack requested)
        "P",              # 15 (production)
        ":",              # 16 (component separator)
    ]
    isa = _ELEM.join(parts)
    # ISA must be exactly 105 characters (excluding the segment terminator).
    # The tokenize() function reads the 4 delimiters from fixed positions:
    #   3rd char (0-indexed) = element separator
    #   82nd char (0-indexed) = repetition separator
    #   104th char (0-indexed) = component separator
    #   105th char (0-indexed) = segment terminator
    # Our parts above sum to 105 chars; the trailing segment terminator
    # is appended separately and is the 106th byte.
    return isa + _SEG


def _build_gs(
    sender_id: str,
    receiver_id: str,
    functional_id_code: str,
    group_control_number: str,
) -> str:
    """Build a GS (Functional Group Header) segment.

    GS01 = functional_id_code (e.g. "HC")
    GS02 = sender id (application code)
    GS03 = receiver id
    GS04 = transaction date (CCYYMMDD)
    GS05 = transaction time (HHMM)
    GS06 = group control number
    GS07 = responsible agency code ("X")
    GS08 = version / release / industry id (e.g. "005010X231A1")
    """
    return _ELEM.join([
        "GS",
        functional_id_code or "HC",
        _pad(sender_id, 15),
        _pad(receiver_id, 15),
        _today_yyyymmdd(),
        "0000",
        _pad(group_control_number or "1", 9),
        "X",
        "005010X231A1",
    ]) + _SEG


def _build_ak1(ak1) -> str:
    """Build an AK1 segment: AK1*functional_id_code*group_control_number~"""
    return _ELEM.join(["AK1", ak1.functional_id_code, ak1.group_control_number]) + _SEG


def _build_ak2_and_children(sr) -> str:
    """Build the AK2 + (optional AK3 + AK4 cluster) + AK5 for a single set."""
    out: list[str] = []
    # AK2: AK2*transaction_set_identifier*set_control_number~
    out.append(
        _ELEM.join([
            "AK2",
            sr.transaction_set_identifier or sr.ak2.functional_id_code,
            sr.set_control_number or sr.ak2.group_control_number,
        ]) + _SEG
    )
    # AK3 + AK4 cluster — one AK3 per SegmentError context, with its
    # own AK4 children. We iterate ``segment_errors`` and emit one AK3
    # per unique ``context`` so the output matches the parser's
    # expectation. In v1 each SegmentError carries its own context, so
    # the same number of AK3s is emitted as SegmentErrors.
    # We do NOT deduplicate identical contexts — each SegmentError is
    # an independent report and the receiver expects them in order.
    for seg_err in sr.segment_errors:
        ctx = seg_err.context
        ak3_parts = [
            "AK3",
            ctx.segment_id,
            str(ctx.segment_position),
        ]
        if ctx.loop_id:
            ak3_parts.append(ctx.loop_id)
        if ctx.implementation_convention_ref:
            ak3_parts.append(ctx.implementation_convention_ref)
        out.append(_ELEM.join(ak3_parts) + _SEG)
        # AK4 follows the AK3 it describes.
        if (
            seg_err.element_position is not None
            or seg_err.element_reference is not None
            or seg_err.error_code
        ):
            ak4_parts = ["AK4"]
            ak4_parts.append(str(seg_err.element_position) if seg_err.element_position is not None else "")
            # AK4-2 is component data element position; we leave it
            # empty because the SegmentError model doesn't carry it.
            ak4_parts.append("")
            if seg_err.element_reference is not None:
                ak4_parts.append(str(seg_err.element_reference))
            else:
                ak4_parts.append("")
            if seg_err.error_code:
                ak4_parts.append(seg_err.error_code)
            out.append(_ELEM.join(ak4_parts) + _SEG)
    # AK5: AK5*code~ (set accept/reject)
    out.append(_ELEM.join(["AK5", sr.set_accept_reject.code]) + _SEG)
    return "".join(out)


def _build_ak9(fg) -> str:
    """Build an AK9 segment: AK9*ack_code*received*accepted*rejected~"""
    return _ELEM.join([
        "AK9",
        fg.ack_code,
        str(fg.received_count),
        str(fg.accepted_count),
        str(fg.rejected_count),
    ]) + _SEG


def _build_se(count: int, control_number: str) -> str:
    """Build a SE segment: SE*segment_count*control_number~"""
    return _ELEM.join(["SE", str(count), control_number]) + _SEG


def serialize_999(
    result: ParseResult999,
    *,
    interchange_control_number: str = "000000001",
) -> str:
    """Serialize a :class:`ParseResult999` to X12 999 text.

    Each set in ``result.set_responses`` is emitted as AK2 (with
    optional AK3 + AK4 children) + AK5. AK1 and AK9 wrap the
    functional group. The envelope layers ISA / GS / ST / SE / GE / IEA
    are always emitted.

    The default ``interchange_control_number`` is ``"000000001"``;
    callers that need a fresh value (e.g. one per generated ACK) should
    pass a unique 9-digit string.

    The output always uses standard X12 delimiters: ``*`` element,
    ``~`` segment, ``:`` component, ``^`` repetition. The output is
    safe to feed directly to :func:`cyclone.parsers.parse_999.parse_999_text`.
    """
    env = result.envelope
    sender = env.sender_id
    receiver = env.receiver_id
    impl_guide = env.implementation_guide or "005010X231A1"

    # We support multiple functional groups in principle, but in v1
    # there's always exactly one. Pull the first AK1 to use for the GS
    # header's functional_id_code.
    fg0 = result.functional_group_acks[0] if result.functional_group_acks else None
    functional_id = fg0.ak1.functional_id_code if fg0 else "HC"
    group_ctrl = (fg0.ak1.group_control_number if fg0 else "1") or "1"

    # ST control number defaults to the AK1 group control number.
    st_ctrl = group_ctrl

    # Build the body segments first so we can count them for SE*.
    body_parts: list[str] = []
    body_parts.append(
        _build_ak1(fg0.ak1) if fg0 else f"AK1{_ELEM}{functional_id}{_ELEM}1{_SEG}"
    )
    for sr in result.set_responses:
        body_parts.append(_build_ak2_and_children(sr))
    # AK9 — one per functional group. In v1 there's always one, but the
    # serializer supports emitting more in the future.
    if fg0 is not None:
        body_parts.append(_build_ak9(fg0))
    body = "".join(body_parts)

    # SE segment count: ST + body segments = 1 + (body segment count).
    # Each ""-separated entry in body_parts is one or more segments
    # joined without the separator, so we count newlines (each ends in "~").
    body_segment_count = body.count(_SEG)
    se_count = 1 + body_segment_count  # +1 for the ST itself

    parts: list[str] = []
    parts.append(_build_isa(sender, receiver, interchange_control_number))
    parts.append(_build_gs(sender, receiver, functional_id, group_ctrl))
    parts.append(
        f"ST{_ELEM}999{_ELEM}{st_ctrl}{_ELEM}{impl_guide}{_SEG}"
    )
    parts.append(body)
    parts.append(_build_se(se_count, st_ctrl))
    parts.append(
        f"GE{_ELEM}1{_ELEM}{group_ctrl}{_SEG}"
    )
    parts.append(
        f"IEA{_ELEM}1{_ELEM}{interchange_control_number}{_SEG}"
    )
    return "".join(parts)


__all__ = ["serialize_999"]
