"""HCPF X12 File Naming Standards helpers.

SP9. Source-of-truth spec:
https://hcpf.colorado.gov/tp-x12-filenaming (HCPF X12 File Naming Standards Quick Guide)

Outbound (we send):
    tp{tpid}-{transaction_type}-{yyyymmddhhmmssSSS_MT}-1of1.{ext}
    Example: tp11525703-837P-20260620132243505-1of1.x12

Inbound (HPE sends to our FromHPE):
    [Tt][Pp]{tpid}-{orig_tx}_M{tracking}-{ts}-1of1_{file_type}.x12
    Example: tp11525703-837P_M019048402-20260520231513488-1of1_999.x12
    (legacy / prodfiles may use uppercase TP; the regex is case-insensitive
    on the prefix to accept both — Gainwell's filer has used both
    casings over time.)

Both use Mountain Time (MT) timestamps with 17-digit millisecond precision
(yyyymmddhhmmssSSS = 4+2+2+2+2+2+3 = 17 digits). Sequence is always "1of1"
(the only accepted value per HCPF).
"""

from __future__ import annotations

import re
from datetime import datetime
from zoneinfo import ZoneInfo

from cyclone.providers import InboundFilename

# ---------------------------------------------------------------------------
# Regexes
# ---------------------------------------------------------------------------

# Outbound: tp11525703-837P-20260620132243505-1of1.x12
#   - tp:     literal "tp" prefix
#   - tpid:   1+ digits
#   - tx:     1+ alnum
#   - ts:     17 digits (yyyymmddhhmmssSSS)
#   - seq:    literal "1of1"
#   - ext:    1+ alnum
OUTBOUND_RE = re.compile(
    r"^tp(?P<tpid>\d+)-(?P<tx>[A-Z0-9]+)-(?P<ts>\d{17})-1of1\.(?P<ext>[A-Za-z0-9]+)$"
)

# Inbound: [Tt][Pp]11525703-837P_M019048402-20260520231513488-1of1_999.x12
#   - prefix:  literal "TP" or "tp" (case-insensitive — Gainwell's
#     production filer has used both)
#   - tpid:    1+ digits (inside TP<...>)
#   - orig_tx: 1+ uppercase alnum
#   - track:   M + 1+ uppercase alnum (e.g. M019048402) — the M is
#     part of the tracking value, not a separator.
#   - ts:      17 digits
#   - seq:     literal "1of1"
#   - ft:      1+ uppercase alnum (e.g. 999, TA1, 271, 277, 277CA,
#              820, 834, 835, ENCR)
#
# Case insensitivity is scoped to the ``TP`` prefix via ``(?i:TP)``
# — the rest of the pattern is case-sensitive so we still reject a
# stray ``837p`` or ``m019048402`` (they'd be invalid HCPF).
INBOUND_RE = re.compile(
    r"^(?i:TP)(?P<tpid>\d+)-(?P<orig_tx>[A-Z0-9]+)_(?P<tracking>M[A-Z0-9]+)"
    r"-(?P<ts>\d{17})-1of1_(?P<file_type>[A-Z0-9]+)\.(?P<ext>x12)$"
)

# Inbound suffix-less form (SP27 Task 7): tp11525703-835_M019110219-20260525001606050-1of1.x12
# Gainwell's production filer has shipped this shorter form in addition
# to the spec form above — the 6/15-6/19 835 batch arrived this way and
# was silently dropped by the strict INBOUND_RE. The loose form omits
# the `_{file_type}.x12` suffix; the token between `-` and `_M` doubles
# as both `orig_tx` and `file_type` (since there's no separate suffix
# to disambiguate). Allowed values still must be in ALLOWED_FILE_TYPES
# — the suffix is optional, the type-set is not.
#   - prefix:    literal "TP" or "tp" (case-insensitive — same as INBOUND_RE)
#   - tpid:      1+ digits
#   - file_type: 3-5 char alphanumeric (covers 999, TA1, 835, 277CA, ENCR;
#                capped at 5 to avoid swallowing the next `_M` token)
#   - tracking:  M + 1+ uppercase alnum
#   - ts:        17 digits
#   - seq:       literal "1of1"
#   - ext:       literal "x12" (relaxing this would also relax the strict
#                form's contract; out of scope for SP27 Task 7)
INBOUND_RE_LOOSE = re.compile(
    r"^(?i:TP)(?P<tpid>\d+)-(?P<file_type>[A-Z0-9]{3,5})"
    r"_(?P<tracking>M[A-Z0-9]+)-(?P<ts>\d{17})-1of1\.(?P<ext>x12)$"
)

ALLOWED_FILE_TYPES = frozenset({
    "999", "TA1", "270", "271", "276", "277", "277CA", "278",
    "820", "834", "835", "ENCR",
})

# ---------------------------------------------------------------------------
# Outbound
# ---------------------------------------------------------------------------


def build_outbound_filename(
    tpid: str,
    tx: str,
    *,
    ext: str = "x12",
    now_mt: datetime | None = None,
) -> str:
    """Build an outbound HCPF filename.

    Args:
        tpid: Trading Partner ID (e.g. "11525703"). Must be digits.
        tx: Transaction type (e.g. "837P", "835").
        ext: File extension (default "x12").
        now_mt: Override for the timestamp (must be timezone-aware, MT or
            anything ZoneInfo can normalize to MT). When None, the current
            time in ``America/Denver`` is used.

    Returns:
        Filename like "tp11525703-837P-20260620132243505-1of1.x12"
        (note the ``tp`` prefix per HCPF outbound spec).

    Raises:
        ValueError: If tpid is non-numeric, tx contains invalid chars, or
            now_mt is naive.
    """
    if not tpid.isdigit():
        raise ValueError(f"tpid must be digits, got {tpid!r}")
    if not re.match(r"^[A-Z0-9]+$", tx):
        raise ValueError(f"tx must be uppercase alnum, got {tx!r}")
    if now_mt is None:
        now_mt = datetime.now(ZoneInfo("America/Denver"))
    elif now_mt.tzinfo is None:
        raise ValueError("now_mt must be timezone-aware")
    else:
        now_mt = now_mt.astimezone(ZoneInfo("America/Denver"))

    # Format: yyyymmddhhmmssSSS — 17 digits total
    ts = now_mt.strftime("%Y%m%d%H%M%S") + f"{now_mt.microsecond // 1000:03d}"
    assert len(ts) == 17
    # Per HCPF outbound spec, prefix is "tp" + tpid. Matches the format
    # we receive from HPE inbound (which uses uppercase TP) and the
    # historical outbound prodfile naming (e.g. tp11525703-837P-...).
    return f"tp{tpid}-{tx}-{ts}-1of1.{ext}"


# ---------------------------------------------------------------------------
# Inbound
# ---------------------------------------------------------------------------


def parse_inbound_filename(name: str) -> InboundFilename:
    """Parse an inbound HCPF filename.

    Accepts both forms (Gainwell ships both):
      * Spec form with ``_{file_type}.x12`` suffix:
        ``tp11525703-837P_M019048402-20260520231513488-1of1_999.x12``
      * Suffix-less form (SP27 Task 7): the token between ``-`` and
        ``_M`` doubles as both ``orig_tx`` and ``file_type``:
        ``tp11525703-835_M019110219-20260525001606050-1of1.x12``

    The strict form is tried first (preserves historical behavior for
    every existing caller); the loose form is the fallback. The
    ``.x12`` extension and ``ALLOWED_FILE_TYPES`` set are enforced in
    both forms.

    Args:
        name: Filename like ``tp11525703-837P_M019048402-20260520231513488-1of1_999.x12``
            (case-insensitive on the ``TP`` prefix; both ``TP`` and
            ``tp`` are accepted.)

    Returns:
        InboundFilename with tpid, orig_tx, tracking, ts, file_type, ext.

    Raises:
        ValueError: If the filename doesn't match either HCPF inbound
            form, or if the derived file_type isn't in
            ALLOWED_FILE_TYPES.
    """
    m = INBOUND_RE.match(name)
    if m:
        file_type = m.group("file_type")
        if file_type not in ALLOWED_FILE_TYPES:
            raise ValueError(
                f"file_type {file_type!r} not in allowed HCPF set: {sorted(ALLOWED_FILE_TYPES)}"
            )
        return InboundFilename(
            tpid=m.group("tpid"),
            orig_tx=m.group("orig_tx"),
            tracking=m.group("tracking"),
            ts=m.group("ts"),
            file_type=file_type,
            ext=m.group("ext"),
        )

    # Fall back to the suffix-less form.
    m = INBOUND_RE_LOOSE.match(name)
    if not m:
        raise ValueError(f"Not a valid HCPF inbound filename: {name!r}")
    file_type = m.group("file_type")
    if file_type not in ALLOWED_FILE_TYPES:
        raise ValueError(
            f"file_type {file_type!r} not in allowed HCPF set: {sorted(ALLOWED_FILE_TYPES)}"
        )
    return InboundFilename(
        tpid=m.group("tpid"),
        orig_tx=m.group("file_type"),  # preserve the historical shape
        tracking=m.group("tracking"),
        ts=m.group("ts"),
        file_type=file_type,
        ext=m.group("ext"),
    )


# ---------------------------------------------------------------------------
# Validation helpers
# ---------------------------------------------------------------------------


def is_outbound_filename(name: str) -> bool:
    """True if the given string matches the HCPF outbound filename regex."""
    return OUTBOUND_RE.match(name) is not None


def is_inbound_filename(name: str) -> bool:
    """True if the given string matches either HCPF inbound form.

    Accepts both the spec form (with ``_{file_type}.x12`` suffix) and
    the suffix-less form (SP27 Task 7). Cheap fast-path check used by
    callers that want to pre-filter a directory listing before invoking
    :func:`parse_inbound_filename`; mirrors the parser's own fallback
    so the two never disagree on what counts as an HCPF inbound file.
    """
    return INBOUND_RE.match(name) is not None or INBOUND_RE_LOOSE.match(name) is not None
