"""Minimal HL7 v2.x parsing and ACK building (no external deps).

Segments are separated by <CR> (sometimes <LF>); fields by '|'; components by
'^'. MSH is special: MSH-1 is the field separator itself, so field indexing for
MSH is offset by one.
"""

from __future__ import annotations

import time
from typing import List, Optional


class Segment:
    """A single HL7 segment (e.g. MSH, PID, OBR, OBX)."""

    def __init__(self, raw: str) -> None:
        self.raw = raw
        self.parts = raw.split("|")
        self.name = self.parts[0] if self.parts else ""

    def field(self, n: int) -> str:
        """Return the n-th field (1-based), accounting for the MSH offset."""
        if self.name == "MSH":
            if n == 1:
                return "|"
            idx = n - 1
        else:
            idx = n
        return self.parts[idx] if 0 <= idx < len(self.parts) else ""

    def component(self, n: int, c: int) -> str:
        """Return component c (1-based) of field n (1-based)."""
        comps = self.field(n).split("^")
        return comps[c - 1] if 0 <= c - 1 < len(comps) else ""


class Message:
    """A parsed HL7 message — a list of segments with convenience lookups."""

    def __init__(self, raw: str) -> None:
        self.raw = raw
        # Normalise line endings: HL7 uses CR, but tolerate CRLF/LF too.
        lines = raw.replace("\r\n", "\r").replace("\n", "\r").split("\r")
        self.segments: List[Segment] = [Segment(ln) for ln in lines if ln.strip()]

    def first(self, name: str) -> Optional[Segment]:
        for seg in self.segments:
            if seg.name == name:
                return seg
        return None

    def all(self, name: str) -> List[Segment]:
        return [seg for seg in self.segments if seg.name == name]

    @property
    def message_type(self) -> str:
        """MSH-9, e.g. 'ORU^R01' → returns 'ORU'."""
        msh = self.first("MSH")
        return msh.component(9, 1) if msh else ""

    @property
    def control_id(self) -> str:
        """MSH-10 — the message control id (echoed back in ACK)."""
        msh = self.first("MSH")
        return msh.field(10) if msh else ""


def get_path(msg: Message, path: str) -> str:
    """Resolve a 'SEG-N' or 'SEG-N.C' path to a value, first matching segment."""
    seg_name, _, rest = path.partition("-")
    field_part, _, comp_part = rest.partition(".")
    try:
        n = int(field_part)
    except ValueError:
        return ""
    seg = msg.first(seg_name)
    if not seg:
        return ""
    if comp_part:
        try:
            return seg.component(n, int(comp_part))
        except ValueError:
            return ""
    value = seg.field(n)
    # Sample/order ids occasionally carry components — take the first.
    return value.split("^")[0] if value else ""


def build_ack(msg: Message, code: str = "AA") -> str:
    """Build an HL7 ACK that echoes the inbound message's control id.

    Sender/receiver are swapped relative to the inbound MSH.
    """
    msh = msg.first("MSH")
    sending_app = msh.field(3) if msh else "LIS"
    sending_fac = msh.field(4) if msh else "LAB"
    recv_app = msh.field(5) if msh else "ANALYZER"
    recv_fac = msh.field(6) if msh else "LAB"
    ts = time.strftime("%Y%m%d%H%M%S")
    control = msg.control_id or ts
    # Swap sender/receiver: we answer the analyzer.
    ack_msh = (
        f"MSH|^~\\&|{recv_app}|{recv_fac}|{sending_app}|{sending_fac}|{ts}||"
        f"ACK|{control}|P|2.3.1"
    )
    msa = f"MSA|{code}|{control}"
    return ack_msh + "\r" + msa
