"""Base ASTM E1394 driver: parse H/P/O/Q/R/L records and build order replies.

Shared by the per-device ASTM drivers (Maglumi, Mispa…). The wire framing is
handled by `astm.py`; this layer understands the record structure:

  H = header     P = patient    O = order/specimen    Q = host query
  R = result     L = terminator

Bidirectional flow (the common case):
  analyzer →  Q record with the sample barcode   ("what tests for this tube?")
  LIS      →  H,P,O(tests),L                      (the order)
  analyzer →  H,P,O,R(results),L                  (the measured values)
"""

from __future__ import annotations

from typing import Any, Dict, List

FS = "|"   # field
CS = "^"   # component
RS = "\\"  # repeat


def _fields(record: str) -> List[str]:
    return record.split(FS)


def _comp(field: str, idx: int) -> str:
    parts = field.split(CS)
    return parts[idx] if idx < len(parts) else ""


class AstmDriver:
    """Generic bidirectional ASTM driver. Subclass per device if a model needs
    quirks (test-code casing, value scaling…); the protocol itself is shared."""

    name = "astm_generic"

    # ----- inbound: parse a fully-assembled ASTM message -----------------
    def parse(self, message: str, device_cfg: Any = None) -> Dict[str, Any]:
        records = [r for r in message.replace("\n", "\r").split("\r") if r.strip()]
        barcode = ""
        query_barcode = ""
        results: List[Dict[str, Any]] = []
        msg_type = "other"

        for rec in records:
            rtype = rec[:1].upper()
            f = _fields(rec)
            if rtype == "Q":
                msg_type = "query"
                # Q-3 starting range: ^barcode^... (the tube the analyzer asks about)
                rng = f[2] if len(f) > 2 else ""
                query_barcode = _comp(rng, 1) or rng.strip(CS) or rng
            elif rtype == "O":
                # O-3 specimen id (barcode); fallback O-2.
                spec = (f[2] if len(f) > 2 else "") or (f[1] if len(f) > 1 else "")
                if spec:
                    barcode = _comp(spec, 0) or spec
            elif rtype == "R":
                msg_type = "result"
                # R-3 universal test id: ^^^CODE^name ; R-4 value ; R-5 unit ; R-7 flag
                test = f[2] if len(f) > 2 else ""
                parts = test.split(CS)
                code = ""
                for p in parts:  # the code is the first non-empty component
                    if p.strip():
                        code = p.strip()
                        break
                value = (f[3] if len(f) > 3 else "").strip()
                unit = (f[4] if len(f) > 4 else "").strip()
                flag = (f[6] if len(f) > 6 else "").strip()
                if code and value != "":
                    results.append({
                        "test_code": code,
                        "raw_result": value,
                        "unit": unit or None,
                        "flag": flag or None,
                        "value_type": "NM",
                        "raw_obx": rec,
                    })

        return {
            "type": msg_type,
            "barcode": barcode,
            "query_barcode": query_barcode,
            "results": results,
        }

    # ----- outbound: build the order reply to a host query ---------------
    def build_order(self, barcode: str, test_codes: List[str]) -> str:
        """Return the ASTM message body (records joined by CR) ordering the
        given analyzer test codes for a sample barcode."""
        tests = RS.join(f"^^^{c}" for c in test_codes) if test_codes else ""
        records = [
            "H|\\^&|||MoonLIS|||||||P|1",
            "P|1",
            f"O|1|{barcode}||{tests}|R||||||N||||1",
            "L|1|N",
        ]
        return "\r".join(records)

    def build_no_order(self, barcode: str) -> str:
        """Reply that there are no orders for this tube (empty order + 'X')."""
        records = ["H|\\^&|||MoonLIS|||||||P|1", "P|1", f"O|1|{barcode}|||C||||||X", "L|1|N"]
        return "\r".join(records)
