"""Serialize a :class:`ParseResult270` to a complete X12 270 text.

Emits the envelope layers (ISA / GS / ST / SE / GE / IEA) and the
minimum body segments (BHT, HL*20/NM1*PR, HL*21/NM1*1P, HL*22/TRN,
NM1*IL, DMG, EQ, SE). Mirrors the structure of
:mod:`cyclone.parsers.serialize_999` so the round-trip with
:func:`cyclone.parsers.parse_270.parse` stays stable.

The serializer is the v1 "one inquiry at a time" convention: only
``result.inquiries[0]`` is emitted. If you need batch inquiries,
promote this to emit one ST/SE per inquiry (out of scope for v1).

The output always uses standard X12 delimiters: ``*`` element,
``~`` segment, ``:`` component, ``^`` repetition. The output is
safe to feed directly to :func:`cyclone.parsers.parse_270.parse`.

The ``interchange_control_number`` and ``group_control_number``
kwargs let the caller override the default values; in v1 the API
endpoint (T23) always uses the defaults and lets the response
``raw_270_text`` carry the canonical id pair.
"""
from __future__ import annotations

from datetime import date

from cyclone.parsers.models_270 import ParseResult270

_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_hhmm() -> str:
    """Return the current time as HHMM (UTC-naive, 24-hour)."""
    from datetime import datetime

    t = datetime.now()
    return f"{t.hour:02d}{t.minute: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 _format_date_yyyymmdd(d: date | None) -> str:
    """Format ``d`` as CCYYMMDD, or return empty string when ``None``."""
    if d is None:
        return ""
    return f"{d.year:04d}{d.month:02d}{d.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).

    Element widths match the 999 serializer so a 270 produced by this
    function round-trips through :func:`cyclone.parsers.segments.tokenize`.
    """
    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
        _today_hhmm(),    # 10
        "^",              # 11
        "00501",          # 12
        _pad(interchange_control_number, 9),  # 13
        "0",              # 14
        "P",              # 15
        ":",              # 16
    ]
    return _ELEM.join(parts) + _SEG


def _build_gs(
    sender_id: str,
    receiver_id: str,
    group_control_number: str,
) -> str:
    """Build the GS (Functional Group Header) segment."""
    return _ELEM.join([
        "GS",
        "HC",
        _pad(sender_id, 15),
        _pad(receiver_id, 15),
        _today_yyyymmdd(),
        _today_hhmm(),
        _pad(group_control_number or "1", 9),
        "X",
        "005010X279A1",
    ]) + _SEG


def _build_st(control_number: str, implementation_guide: str) -> str:
    """Build the ST (Transaction Set Header) segment."""
    return _ELEM.join([
        "ST",
        "270",
        _pad(control_number, 4),
        implementation_guide or "005010X279A1",
    ]) + _SEG


def _build_bht() -> str:
    """Build the BHT (Beginning of Hierarchical Transaction) segment.

    BHT01 = "0022" (Hierarchical Structure Code — same for 270/271).
    BHT02 = "13" (Transaction Set Purpose Code — Request).
    BHT03 = a synthesized reference id (timestamp-based).
    BHT04 = today's date (CCYYMMDD).
    BHT05 = current time (HHMM).
    """
    from datetime import datetime
    t = datetime.now()
    ref = f"REF{t.year:04d}{t.month:02d}{t.day:02d}{t.hour:02d}{t.minute:02d}{t.second:02d}"
    return _ELEM.join([
        "BHT",
        "0022",
        "13",
        ref,
        _today_yyyymmdd(),
        _today_hhmm(),
    ]) + _SEG


def _build_se(count: int, control_number: str) -> str:
    """Build the SE (Transaction Set Trailer) segment."""
    return _ELEM.join(["SE", str(count), _pad(control_number, 4)]) + _SEG


def serialize_270(
    result: ParseResult270,
    *,
    interchange_control_number: str = "000000001",
    group_control_number: str = "1",
) -> str:
    """Serialize a :class:`ParseResult270` to X12 270 text.

    Output is a minimal but valid 270 request — one inquiry at a time
    (the result's ``inquiries[0]`` is used). If you need batch
    inquiries, promote this to emit one ST/SE per inquiry (out of
    scope for v1).

    The default ``interchange_control_number`` is ``"000000001"`` and
    the default ``group_control_number`` is ``"1"``; callers that
    need a fresh value (e.g. one per generated 270) should pass a
    unique 9-digit string and 9-digit group id respectively.
    """
    env = result.envelope
    sender_id = env.sender_id
    receiver_id = env.receiver_id
    impl_guide = env.implementation_guide or "005010X279A1"

    # ST control number defaults to the envelope's interchange control
    # number's last 4 digits; otherwise "0001".
    st_ctrl = (env.control_number or "0001")[-4:].rjust(4, "0")

    # The v1 convention is one inquiry at a time; in v1 callers are
    # expected to populate ``inquiries`` with at most one entry.
    inquiry = result.inquiries[0] if result.inquiries else None
    if inquiry is None:
        # Defensive: emit an EQ with the most common default
        # (Health Plan Coverage) so the file is still a valid 270.
        service_type_code = "30"
        coverage_info = ""
    else:
        service_type_code = inquiry.service_type_code
        coverage_info = inquiry.coverage_info or ""

    # Subscriber name fields. Per X12 005010X279A1, NM1*IL uses
    # entity type "1" (person).
    sub = result.subscriber
    sub_dob = _format_date_yyyymmdd(sub.dob)
    # DMG layout: DMG*D8*<CCYYMMDD>*<gender>~. We always emit the
    # 4-element form so the segment is canonical; gender is empty
    # in v1 (the 270 model doesn't carry it).
    sub_dmg_parts = ["DMG", "D8", sub_dob, ""]

    # Patient (optional).
    patient = result.patient
    patient_segment = ""
    if patient is not None:
        # HL*4 segment (level code "23" = patient; parent id points
        # back to the subscriber loop, HL03=3; child code carries the
        # relationship to the subscriber — "0"=self, "1"=spouse, "19"=child, etc.).
        rel = patient.relationship or "0"
        patient_segment += _ELEM.join(["HL", "4", "3", "23", rel]) + _SEG
        # NM1*QC for the patient. We populate the canonical 9
        # elements after NM1 (last, first, middle, prefix, suffix,
        # id qual, id value) and leave the rest empty.
        patient_segment += _ELEM.join([
            "NM1",
            "QC",
            "1",
            patient.last_name or "",
            patient.first_name or "",
            "",  # NM105 middle
            "",  # NM106 prefix
            "",  # NM107 suffix
            "",  # NM108 ID code qualifier (v1 doesn't carry one)
            "",  # NM109 ID code value
        ]) + _SEG
        if patient.dob is not None:
            patient_segment += _ELEM.join([
                "DMG",
                "D8",
                _format_date_yyyymmdd(patient.dob),
            ]) + _SEG

    # Information source (HL*20) — payer.
    src = result.information_source
    src_id_qual = "PI" if src.id else ""
    src_id_value = src.id or ""
    src_segment = (
        _ELEM.join(["HL", "1", "", "20", "1"]) + _SEG
        + _ELEM.join([
            "NM1",
            "PR",
            "2",
            src.name,
            "",  # NM104 first name (not used for non-person entity)
            "",  # NM105 middle
            "",  # NM106 prefix
            "",  # NM107 suffix
            src_id_qual,  # NM108 ID code qualifier
            src_id_value,  # NM109 ID code value
        ]) + _SEG
    )

    # Information receiver (HL*21) — provider.
    rcv = result.information_receiver
    rcv_id_qual = "XX" if rcv.npi else ""
    rcv_id_value = rcv.npi or ""
    rcv_segment = (
        _ELEM.join(["HL", "2", "1", "21", "1"]) + _SEG
        + _ELEM.join([
            "NM1",
            "1P",
            "2",
            rcv.name,
            "",  # NM104 first name (not used for non-person entity)
            "",  # NM105 middle
            "",  # NM106 prefix
            "",  # NM107 suffix
            rcv_id_qual,  # NM108 ID code qualifier
            rcv_id_value,  # NM109 ID code value
        ]) + _SEG
    )

    # Subscriber (HL*22) — TRN + NM1*IL + DMG.
    sub_id_qual = "MI" if sub.member_id else ""
    sub_id_value = sub.member_id or ""
    sub_segment = (
        _ELEM.join(["HL", "3", "2", "22", "0"]) + _SEG
        + "TRN" + _ELEM + "2" + _ELEM + "TRACE001" + _SEG
        + _ELEM.join([
            "NM1",
            "IL",
            "1",
            sub.last_name or "",
            sub.first_name or "",
            "",  # NM105 middle name / initial
            "",  # NM106 name prefix
            "",  # NM107 name suffix
            sub_id_qual,  # NM108 ID code qualifier
            sub_id_value,  # NM109 ID code value
        ]) + _SEG
        + _ELEM.join(sub_dmg_parts) + _SEG
    )

    # EQ segment.
    eq_parts = ["EQ", service_type_code]
    if coverage_info:
        eq_parts.append(coverage_info)
    eq_segment = _ELEM.join(eq_parts) + _SEG

    # Build the body in canonical order. The SE count is the number of
    # segments inside the ST/SE envelope (ST itself + body segments).
    body_parts: list[str] = [
        _build_bht(),
        src_segment,
        rcv_segment,
        sub_segment,
        patient_segment,  # may be empty when no patient
        eq_segment,
    ]
    body = "".join(body_parts)
    body_segment_count = body.count(_SEG)
    se_count = 1 + body_segment_count  # +1 for the ST itself

    parts: list[str] = [
        _build_isa(sender_id, receiver_id, interchange_control_number),
        _build_gs(sender_id, receiver_id, group_control_number),
        _build_st(st_ctrl, impl_guide),
        body,
        _build_se(se_count, st_ctrl),
        f"GE{_ELEM}1{_ELEM}{_pad(group_control_number, 9)}{_SEG}",
        f"IEA{_ELEM}1{_ELEM}{_pad(interchange_control_number, 9)}{_SEG}",
    ]
    return "".join(parts)


__all__ = ["serialize_270"]
