import builtins
from enum import Enum
from logging import config
from logging.handlers import TimedRotatingFileHandler
import os
from queue import Queue
import re
import socket
import logging
from sqlite3 import Date
import threading
import time
import datetime
import traceback
import serial # type: ignore

from flask import Flask, jsonify, request # type: ignore
from sqlalchemy import create_engine, Column, Integer, String, Boolean, Text # type: ignore
from sqlalchemy import DateTime as SqDateTime # type: ignore
from sqlalchemy import Date as SqDate # type: ignore
from sqlalchemy.orm import declarative_base, sessionmaker # type: ignore
# Logging Setup
# Konfigurasi logging per hari
log_handler = TimedRotatingFileHandler(
    filename="geneexpert.log",
    when="midnight",
    interval=1,
    backupCount=7,
    encoding="utf-8"
)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(threadName)s - %(message)s')
log_handler.setFormatter(formatter)
logging.basicConfig(level=logging.INFO, handlers=[log_handler])

THREAD_LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "thread_logs")
thread_log_lock = threading.Lock()

def _sanitize_thread_log_name(thread_name):
    safe_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", str(thread_name or "main").strip())
    return safe_name or "main"

def _write_thread_log(message):
    try:
        os.makedirs(THREAD_LOG_DIR, exist_ok=True)
        thread_name = threading.current_thread().name
        safe_thread_name = _sanitize_thread_log_name(thread_name)
        log_path = os.path.join(THREAD_LOG_DIR, f"{safe_thread_name}.log")
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        with thread_log_lock:
            with open(log_path, "a", encoding="utf-8") as fh:
                fh.write(f"{timestamp} | {message}\n")
    except Exception:
        pass

def print(*args, **kwargs):
    sep = kwargs.get("sep", " ")
    end = kwargs.get("end", "\n")
    message = sep.join(str(arg) for arg in args)
    _write_thread_log(message)
    return builtins.print(*args, **kwargs)

def _visible_bytes(data: bytes) -> str:
    mapping = {
        0x02: "<STX>",
        0x03: "<ETX>",
        0x04: "<EOT>",
        0x05: "<ENQ>",
        0x06: "<ACK>",
        0x0D: "<CR>",
        0x0A: "<LF>",
        0x17: "<ETB>",
        0x15: "<NAK>",
        0x1C: "<FS>",
        0x0B: "<VT>",
    }
    parts = []
    for b in data:
        if b in mapping:
            parts.append(mapping[b])
        elif 32 <= b <= 126:
            parts.append(chr(b))
        else:
            parts.append(f"<0x{b:02X}>")
    return "".join(parts)

def _hex_bytes(data: bytes, limit: int = 160) -> str:
    clipped = data[:limit]
    text = clipped.hex().upper()
    return text + ("..." if len(data) > limit else "")

# ==========================================
# 1. KONFIGURASI SISTEM
# ==========================================
# Global Variables
app = Flask(__name__)
active_genexpert_connections = {}
connection_lock = threading.Lock()
pending_result_queries = {}
pending_query_lock = threading.Lock()
scheduled_result_queries = {}
scheduled_result_query_lock = threading.Lock()
genexpert_query_inflight_by_ip = {}
genexpert_query_inflight_lock = threading.Lock()
# Network Configuration
TCP_LISTENER_PORT = 6001         # PC GeneXpert set ke mode Client, konek ke IP:PORT ini
SERVER_HOST = '0.0.0.0'          # Listen di semua interface
HTTP_API_PORT = 6002             # Endpoint trigger dari Laravel -> Python
GENEXPERT_RESULT_QUERY_INITIAL_DELAY_SECONDS = 60
GENEXPERT_RESULT_QUERY_INTERVAL_SECONDS = 120
GENEXPERT_RESULT_QUERY_MAX_DURATION_SECONDS = 21600
GENEXPERT_RESULT_QUERY_INFLIGHT_TIMEOUT_SECONDS = 45
GENEXPERT_ENABLE_RESULT_QUERY_SCHEDULER = False
GENEXPERT_RESPONSE_MODE_DEFAULT = "astm_active"
GENEXPERT_RESPONSE_MODE_BY_IP = {
    # "10.10.120.75": "hl7_passive",
}
GENEXPERT_HOST_APPLICATION_DEFAULT = "DE002"
GENEXPERT_HOST_APPLICATION_BY_IP = {
    "10.10.120.75": "GE01", # GenExpert Kecil
    "10.10.120.108": "DE002", # GenExpert Tempat Lama
    "10.10.120.73": "GE01", # GenExpert Besa
}
# Mapping Flag ke IP Address GeneXpert
# Pastikan IP ini SESUAI dengan settingan "Server IP" di masing-masing alat (Client Mode)
TARGET_MAPPING = {
    'flg_gxp1': '10.10.120.73',
    'flg_gxp2': '10.10.120.108',
    'flg_gxp3': '10.10.120.75'
}
# GeneXpert Configuration
# ==========================================
# KONFIGURASI MAPPING TES (DATABASE -> GENEXPERT)
# ==========================================
# Kiri: Nama di kolom 'tes' database Anda
# Kanan: 'Host Test Code' dari Dokumen Word Anda
GENEXPERT_TEST_MAPPING = {
    # Mapping untuk IP 10.10.120.75 (Multi-Assay)
    "HIV": "HIV1-VL",
    "TCM TB": "MTBRIF",
    "TCM TB ULTRA": "MTBRIF",
    "TCM TB XDR": "MTB-XDR 2",
    "HCV VL": "HCV",
    "COVID-19": "SARSCOV2FLURSV",
    "17.3.1 TCM COVID-19": "SARSCOV2FLURSV",
    "17.3.2 PCR COVID-19": "SARSCOV2FLURSV",
    "E.2.5 HCV TCM": "HCV",
    "18.1.1 TCM HCV": "HCV",
    "18.1.2 TCM HIV VIRAL LOAD": "HIV1-VL",
    "18.1.4 TCM HPV": "HCV",
    "7.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF",
    "5.3.8 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF",
    "5.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
    "3.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL) ": "MTB-XDR 2",
    "2.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
    "1.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
    "1.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
    "15.2.1 KULTUR TB MEDIA LJ": "MTB-XDR 2",
    "8.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
    "9.3.5 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
    "9.3.6 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
    "12.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
    "H.2.5 PEMERIKSAAN KULTUR MYCROBACTERIUM TBC": "MTB-XDR 2",
    "12.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
    "11.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
    "10.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
    "10.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
    "3.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
    "15.2.2 KULTUR TB MEDIA MGIT (AUTOMATIC)": "MTB-XDR 2",
    "11.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
    "8.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
    "7.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
    "2.3.10 TCM CLAMIDIA TRACHOMATIS / NEISSERIA GONORRHOE": "MTBRIF",
    "12.3.8 TCM TB (GENE EXPERT)": "MTBRIF",
    "15.2.3 TCM TB (GENE EXPERT)": "MTBRIF",
    "2.3.9 TCM GENE EXPERT": "MTBRIF",
    "3.3.8 TCM GENE EXPERT": "MTBRIF",
    "3.3.9 TCM MYCOBACTERIUM TUBERCULOSIS": "MTBRIF",
    "5.3.9 TCM TBC (GENE EXPERT)": "MTBRIF",
    "7.3.8 TCM TBC (GENE EXPERT)": "MTBRIF",
    "8.3.8 TCM TBC (GENE EXPERT)": "MTBRIF",
    "10.3.8 TCM TBC (GENE EXPERT)": "MTBRIF",
    "11.3.9 TCM TB (GENE EXPERT)": "MTBRIF",
    
}
GENEXPERT_IP_CAPABILITIES = {
    "10.10.120.75": ["MTBRIF", "HBVVL", "HIV1-VL", "MTB-XDR 2", "HCV", "SARSCOV2FLURSV"],
    "10.10.120.73": ["MTBRIF", "HBVVL", "HIV1-VL", "HCV", "SARSCOV2FLURSV"],
    "10.10.120.108": ["HCV", "HIV1-VL", "HBVVL"],
}
DEFAULT_GXP_CODE = "MTBRIF"

DEVICE_CONFIGS = [
    {
        'port': 'COM6', 'baud_rate': 9600, 'device_type': 'vitek', 'alat_name': 'Vitek 1',
        'protocol': 'serial', 'flag_column': 'flg_vitek1'
    },
    {
        'port': 'COM4', 'baud_rate': 9600, 'device_type': 'vitek', 'alat_name': 'Vitek 2',
        'protocol': 'serial', 'flag_column': 'flg_vitek2'
    },
    {
        'port': 'COM5', 'baud_rate': 9600, 'device_type': 'bd', 'alat_name': 'BACTEC',
        'protocol': 'serial', 'flag_column': 'flg_bd1'
    },
    #BD_MGIT yang di dalam ruangan isolasi
    #{
    #    'port': 'COM4', 'baud_rate': 19200, 'device_type': 'bd', 'alat_name': 'MGIT',
    #    'protocol': 'serial', 'flag_column': 'flg_bd2'
    #},
]
MYLA_HOST = '10.10.120.89'
MYLA_PORT = 8000
MYLA_INBOUND_HOST = '0.0.0.0'
MYLA_INBOUND_PORT = 8000
MYLA_POLL_INTERVAL_SECONDS = 1
MYLA_CONNECT_RETRY_SECONDS = 5
MYLA_CONTROL_TIMEOUT_SECONDS = 6
MYLA_ACK_TIMEOUT_SECONDS = 8
MYLA_IDLE_LOG_INTERVAL_SECONDS = 30

# Karakter kontrol standar
STX, ETX, ACK, NAK, EOT, ENQ = b'\x02', b'\x03', b'\x06', b'\x15', b'\x04', b'\x05'
RS, GS = b'\x1e', b'\x1d'
ports_lock = threading.Lock()
active_serial_ports = {}

order_queues = {config['port']: Queue() for config in DEVICE_CONFIGS if config['protocol'] == 'serial'}

# ==========================================
# 2. DATABASE MODEL
# ==========================================
DATABASE_URL = "postgresql://lismikro:lismikro@10.10.123.193:5002/lismikro"
engine = create_engine(DATABASE_URL, pool_recycle=3600)
SessionLocal = sessionmaker(bind=engine)
Base = declarative_base()

class Sample(Base):
    __tablename__ = 'samples'
    id = Column(Integer, primary_key=True)
    patient_name = Column(String(100))
    patient_id = Column(String(50))
    sample_id = Column(String(50), unique=True)
    test_type = Column(String(100))
    result = Column(Text)
    raw_message = Column(Text)
    created_at = Column(SqDateTime, default=datetime.datetime.now)

class SerialOrderQueue(Base):
    __tablename__ = 'serial_order_queue'
    id = Column(Integer, primary_key=True)
    target_port = Column(String(50), nullable=False, index=True)
    message_to_send = Column(Text, nullable=False)
    status = Column(String(20), default='pending', index=True)
    created_at = Column(SqDateTime, default=datetime.datetime.now)
    updated_at = Column(SqDateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)

class LisPhoenix(Base):
    __tablename__ = 'lis_phoenix'
    id = Column(Integer, primary_key=True)
    no_id = Column(String(50)) # Patient ID
    seq_no = Column(String(50)) # Isolate ID / Sample ID
    rnmpas = Column(String(100)) # Patient Name
    tgl_data = Column(SqDate)
    rawdt = Column(Text)
    organisme = Column(String(100))
    kd_orgm = Column(String(50))
    alat = Column(String(50))
    processed = Column(String(50), nullable=True)

class LisPhoenixDtl(Base):
    __tablename__ = 'lis_phoenix_dtl'
    id = Column(Integer, primary_key=True)
    seq_no = Column(String(50))
    kd_antibiotik = Column(String(50))
    nm_antibiotik = Column(String(100))
    keterangan = Column(String(50))
    interpretasi = Column(String(10)) # S, I, R
    no = Column(Integer)

class PaslabOrder(Base):
    __tablename__ = 'paslab'
    urut = Column(Integer, primary_key=True)
    rnoreg = Column(String)
    nama = Column(String)
    norm = Column(String)
    rjenis = Column(String)
    rtglast = Column(SqDateTime)
    alamat = Column(String)
    umur = Column(String)
    namadok = Column(String)
    ruangan = Column(String)
    tes = Column(String)
    alat = Column(String)
    kd_spesimen = Column(String)
    nm_spesimen = Column(String)
    tgllahir = Column(SqDate)
    flg_vitek1 = Column(Boolean, default=False)
    flg_vitek2 = Column(Boolean, default=False)
    flg_bd1 = Column(Boolean, default=False)
    flg_bd2 = Column(Boolean, default=False)
    flg_gxp1 = Column(Boolean, default=False)
    flg_gxp2 = Column(Boolean, default=False)
    flg_gxp3 = Column(Boolean, default=False)
    flg_vitek3 = Column(Boolean, default=False)
    created_at = Column(SqDateTime, default=datetime.datetime.now)
    updated_at = Column(SqDateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)

Base.metadata.create_all(bind=engine)

# ==========================================
# 3. HL7 HELPER FUNCTIONS
# ==========================================
def get_flag_by_device(ip_addr):
    for flag, ip in TARGET_MAPPING.items():
        if ip == ip_addr:
            return flag
    return None

def get_pending_orders(ip_addr):
    flag = get_flag_by_device(ip_addr)
    if not flag:
        return []

    session = SessionLocal()
    try:
        q = session.query(PaslabOrder).filter(getattr(PaslabOrder, flag) == False)
        return q.all()
    finally:
        session.close()

def parse_hl7_segments(hl7_message):
    return [segment for segment in str(hl7_message or "").split('\r') if segment]

def extract_segment(hl7_message, segment_name):
    prefix = f"{segment_name}|"
    for segment in parse_hl7_segments(hl7_message):
        if segment.startswith(prefix):
            return segment
    return ""

def parse_genexpert_qpd(qpd_segment):
    fields = str(qpd_segment or "").split('|')
    query_name = fields[1] if len(fields) > 1 else ""
    query_tag = fields[2] if len(fields) > 2 else ""
    param_1 = fields[3] if len(fields) > 3 else ""
    param_2 = fields[4] if len(fields) > 4 else ""
    return {
        "query_name": query_name,
        "query_tag": query_tag,
        "param_1": param_1,
        "param_2": param_2,
    }

def parse_astm_records(message_text):
    records = []
    for rec in str(message_text or "").split("\r"):
        rec = rec.strip()
        if not rec:
            continue
        if rec and rec[0].isdigit():
            rec = rec[1:]
        records.append(rec)
    return records

def parse_genexpert_astm_query(message_text):
    records = parse_astm_records(message_text)
    query = {
        "query_sample_id": "",
        "query_tag": "",
        "raw_records": records,
    }
    for rec in records:
        fields = rec.split("|")
        if not fields:
            continue
        if fields[0] == "H":
            query["query_tag"] = fields[4] if len(fields) > 4 else ""
        elif fields[0] == "Q":
            query["query_sample_id"] = fields[2] if len(fields) > 2 else ""
    return query

def detect_genexpert_message_framing(message_bytes):
    raw = message_bytes or b""
    if b"\x1c" in raw or raw.startswith(b"\x0b"):
        return "mllp"
    # [PERBAIKAN KRITIS]: Harus mendeteksi ETX (\x03) ATAU ETB (\x17)
    if b"\x03" in raw or b"\x17" in raw:
        return "astm"
    return "plain"

def extract_astm_frame_text(frame_bytes):
    raw = frame_bytes or b""
    if not raw.startswith(b"\x02"): # Jika tidak diawali STX, kosongkan
        return ""
        
    etx_pos = raw.find(b"\x03")
    etb_pos = raw.find(b"\x17")
    
    # Logika aman jika kebetulan ada dua-duanya di dalam buffer
    if etx_pos != -1 and etb_pos != -1:
        end_pos = min(etx_pos, etb_pos)
    else:
        end_pos = max(etx_pos, etb_pos)
        
    if end_pos == -1 or end_pos < 2:
        return ""
        
    # Ambil mulai dari index ke-2 (mengabaikan STX dan Frame Number)
    text_bytes = raw[2:end_pos]
    return text_bytes.decode("latin-1", errors="ignore")

def resolve_genexpert_assay(order, ip_addr=None):
    assay_name = str(getattr(order, "tes", "") or "").strip()
    specimen_code = str(getattr(order, "kd_spesimen", "") or "").strip()
    assay_code = GENEXPERT_TEST_MAPPING.get(assay_name)
    assay_source = "mapping:tes"

    if not assay_code and specimen_code:
        assay_code = specimen_code
        assay_source = "fallback:kd_spesimen"

    supported_codes = GENEXPERT_IP_CAPABILITIES.get(str(ip_addr or "").strip(), []) if ip_addr else []
    capability_match = True if not supported_codes else assay_code in supported_codes

    if not assay_code:
        print(
            f"[GENEXPERT-DEBUG] rnoreg={getattr(order, 'rnoreg', '')}, ip={ip_addr}, "
            f"tes='{assay_name}', kd_spesimen='{specimen_code}', assay_code=EMPTY"
        )
        return None, "none", capability_match

    print(
        f"[GENEXPERT-DEBUG] rnoreg={getattr(order, 'rnoreg', '')}, ip={ip_addr}, "
        f"tes='{assay_name}', kd_spesimen='{specimen_code}', assay_code='{assay_code}', "
        f"assay_source={assay_source}, capability_match={capability_match}"
    )
    return assay_code, assay_source, capability_match

def get_genexpert_query_orders(ip_addr, hl7_msg):
    flag = get_flag_by_device(ip_addr)
    if not flag:
        return []

    qpd_segment = extract_segment(hl7_msg, "QPD")
    qpd = parse_genexpert_qpd(qpd_segment)
    param_1 = str(qpd.get("param_1") or "").strip()
    param_2 = str(qpd.get("param_2") or "").strip()

    session = SessionLocal()
    try:
        flag_attr = getattr(PaslabOrder, flag, None)
        if flag_attr is None:
            return []

        base_orders = session.query(PaslabOrder).filter(
            (flag_attr == False) | (flag_attr == None)
        ).order_by(PaslabOrder.urut.asc()).all()

        requested_sample_id = (param_2 or param_1).strip() if (param_2 or param_1) else ""
        if requested_sample_id and requested_sample_id.upper() != "ALL":
            for order in base_orders:
                if str(order.rnoreg or "").strip() != requested_sample_id:
                    continue
                assay_code, _, _ = resolve_genexpert_assay(order, ip_addr)
                if not assay_code:
                    return []
                return [order]
            print(f"[GENEXPERT] Tidak ada order untuk sample_id={requested_sample_id} di {ip_addr}")
            return []

        for order in base_orders:
            assay_code, _, _ = resolve_genexpert_assay(order, ip_addr)
            if assay_code:
                return [order]
        print(f"[GENEXPERT] Tidak ada order dengan assay valid untuk IP {ip_addr}")
        return []
    finally:
        session.close()

def get_genexpert_host_application(ip_addr):
    ip_addr = str(ip_addr or "").strip()
    host_app = GENEXPERT_HOST_APPLICATION_BY_IP.get(ip_addr, GENEXPERT_HOST_APPLICATION_DEFAULT)
    host_app = str(host_app or "").strip()
    return host_app or GENEXPERT_HOST_APPLICATION_DEFAULT

def build_genexpert_response_msh(message_code, incoming_hl7, resp_control_id, ip_addr=None):
    timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
    msh_fields = extract_segment(incoming_hl7, "MSH").split('|')
    sender_app = msh_fields[2] if len(msh_fields) > 2 else "GeneXpert"
    sender_fac = msh_fields[3] if len(msh_fields) > 3 else ""
    host_app = get_genexpert_host_application(ip_addr)
    return f"MSH|^~\\&|{host_app}||{sender_app}|{sender_fac}|{timestamp}||{message_code}|{resp_control_id}|P|2.5|||NE|NE"

def create_genexpert_ack_j01_response(incoming_hl7, ip_addr=None):
    incoming_control_id = extract_msg_control_id(incoming_hl7) or "UNKNOWN"
    resp_control_id = f"ACK{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
    msh = build_genexpert_response_msh("ACK^J01", incoming_hl7, resp_control_id, ip_addr=ip_addr)
    msa = f"MSA|CA|{incoming_control_id}"
    return f"{msh}\r{msa}\r"

def create_genexpert_ack_r01_response(incoming_hl7, ip_addr=None):
    incoming_control_id = extract_msg_control_id(incoming_hl7) or "UNKNOWN"
    resp_control_id = f"ACK{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
    msh = build_genexpert_response_msh("ACK^R01", incoming_hl7, resp_control_id, ip_addr=ip_addr)
    msa = f"MSA|CA|{incoming_control_id}"
    return f"{msh}\r{msa}\r"

def format_hl7_date(value):
    if not value:
        return ""
    if isinstance(value, datetime.datetime):
        return value.strftime("%Y%m%d")
    if isinstance(value, datetime.date):
        return value.strftime("%Y%m%d")
    text = str(value).strip()
    digits = re.sub(r"[^0-9]", "", text)
    return digits[:8] if len(digits) >= 8 else ""

def map_hl7_sex(value):
    text = str(value or "").strip().upper()
    if not text:
        return ""
    if text.startswith("L") or "LAKI" in text or text == "M":
        return "M"
    if text.startswith("P") or "PEREM" in text or text == "F":
        return "F"
    return ""

def create_genexpert_rsp_z02_response(orders, incoming_hl7, ip_addr=None):
    qpd_segment = extract_segment(incoming_hl7, "QPD")
    qpd = parse_genexpert_qpd(qpd_segment)
    query_tag = qpd.get("query_tag") or (extract_msg_control_id(incoming_hl7) or "UNKNOWN")
    incoming_message_type = extract_message_type(incoming_hl7)
    query_name = qpd.get("query_name") or "Z03^HOST QUERY"
    resp_control_id = f"RSP{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"

    if str(incoming_message_type or "").startswith("QBP^Z03"):
        query_name = "Z03^HOST QUERY"

    msh = build_genexpert_response_msh("RSP^Z02", incoming_hl7, resp_control_id, ip_addr=ip_addr)
    msa = f"MSA|AA|{query_tag}"
    qak = f"QAK|{query_tag}|OK|{query_name}"
    segments = [msh, msa, qak]
    if str(incoming_message_type or "").startswith("QBP^Z03"):
        selected_patient_id = ""
        selected_sample_id = ""
        if orders:
            selected_patient_id = sanitize_astm_field(orders[0].norm or "", max_len=50)
            selected_sample_id = sanitize_astm_field(orders[0].rnoreg or "", max_len=50)
        segments.append(f"QPD|{query_name}|{query_tag}|{selected_patient_id}|{selected_sample_id}")
    elif qpd_segment:
        segments.append(qpd_segment)

    for patient_idx, order in enumerate(orders, start=1):
        patient_id = sanitize_astm_field(order.norm or order.rnoreg, max_len=50)
        sample_id = sanitize_astm_field(order.rnoreg, max_len=50)
        order_ts = (
            getattr(order, "rtglast", None).strftime('%Y%m%d%H%M%S')
            if getattr(order, "rtglast", None)
            else datetime.datetime.now().strftime('%Y%m%d%H%M%S')
        )
        assay_code, assay_source, capability_match = resolve_genexpert_assay(order, ip_addr)
        if not assay_code:
            print(f"[GENEXPERT] Payload order dilewati rnoreg={sample_id} karena assay kosong.")
            continue

        print(
            f"[GENEXPERT-DEBUG] Build RSP rnoreg={sample_id}, ip={ip_addr}, "
            f"patient_id={patient_id}, assay_code={assay_code}, assay_source={assay_source}, "
            f"capability_match={capability_match}, query_name='{query_name}', query_tag='{query_tag}', "
            "profile='minimal-rsp-z02'"
        )

        segments.append(f"PID|{patient_idx}||{patient_id}")
        segments.append(f"ORC|NW|1|||||||{order_ts}")
        segments.append(f"OBR|1|||{assay_code}|||||||A")
        segments.append("TQ1|||||||||R")
        segments.append(f"SPM|1|{sample_id}^||ORH|||||||P")

    return "\r".join(segments) + "\r"

def create_genexpert_astm_order_message(orders, ip_addr=None, query_tag=""):
    timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    records = [
        f"H|\\^&|||{sanitize_astm_field(get_genexpert_host_application(ip_addr), max_len=20)}|||||GeneXpert Host||P|1394-97|{timestamp}"
    ]

    for index, order in enumerate(orders, start=1):
        patient_id = sanitize_astm_field(order.norm or order.rnoreg, max_len=32)
        sample_id = sanitize_astm_field(order.rnoreg, max_len=32)
        assay_code, assay_source, capability_match = resolve_genexpert_assay(order, ip_addr)
        if not assay_code:
            print(f"[GENEXPERT] ASTM payload order dilewati rnoreg={sample_id} karena assay kosong.")
            continue

        first_name, last_name = split_patient_name(sanitize_astm_field(order.nama, max_len=80))
        first_name = sanitize_astm_field(first_name, uppercase=True, max_len=20)
        last_name = sanitize_astm_field(last_name, uppercase=True, max_len=20)
        patient_name = f"{last_name}^{first_name}".strip("^")
        sex_raw = sanitize_astm_field(order.rjenis, uppercase=True, max_len=10)
        sex = "M" if sex_raw.startswith("L") else ("F" if sex_raw else "")
        order_ts = (
            getattr(order, "rtglast", None).strftime('%Y%m%d%H%M%S')
            if getattr(order, "rtglast", None)
            else timestamp
        )

        print(
            f"[GENEXPERT-ASTM-ORDER] rnoreg={sample_id}, ip={ip_addr}, patient_id={patient_id}, "
            f"assay_code={assay_code}, assay_source={assay_source}, capability_match={capability_match}"
        )

        records.append(f"P|{index}|{patient_name}||{patient_id}|{patient_name}|||{sex}")
        records.append(f"O|1|{sample_id}||^^^{assay_code}|R|{order_ts}|||||||||ORH||||||||||A")

    records.append("L|1|N")
    message = "\r".join(records) + "\r"
    print(f"[GENEXPERT-ASTM-ORDER] ip={ip_addr}, query_tag={query_tag}, records={len(records)}, visible={_visible_bytes(message.encode('latin-1'))}")
    return message

def send_all_orders(conn, ip_addr, hl7_msg, msg_id, response_framing="mllp"):
    orders = get_genexpert_query_orders(ip_addr, hl7_msg)
    scheduled_orders = []
    if not orders:
        print(f"[GENEXPERT] Tidak ada order pending untuk {ip_addr}")
        rsp = create_genexpert_rsp_z02_response([], hl7_msg, ip_addr=ip_addr)
        send_genexpert_response(conn, ip_addr, rsp, response_framing, label="qbp-empty")
        return

    qpd_segment = extract_segment(hl7_msg, "QPD")
    print(
        f"[GENEXPERT-DEBUG] QBP diproses untuk ip={ip_addr}, msg_id={msg_id}, "
        f"qpd='{qpd_segment}', selected_rnoreg={[str(order.rnoreg or '').strip() for order in orders]}"
    )
    print(f"[GENEXPERT] Mengirim {len(orders)} order ke {ip_addr}")
    rsp = create_genexpert_rsp_z02_response(orders, hl7_msg, ip_addr=ip_addr)
    first_accnumber = str(orders[0].rnoreg or "").strip() if orders else ""
    debug_genexpert_order_message(rsp, ip_addr=ip_addr)
    send_genexpert_response(conn, ip_addr, rsp, response_framing, label=f"qbp-order:{first_accnumber}")

    for order in orders:
        print(f"[GENEXPERT] Order ditawarkan ke {ip_addr}: {order.rnoreg}")
        scheduled_orders.append({
            "accnumber": str(order.rnoreg or "").strip(),
            "register_no": str(order.rnoreg or "").strip(),
            "target_ip": ip_addr,
        })

    for scheduled_order in scheduled_orders:
        schedule_result_query_for_order(
            accnumber=scheduled_order["accnumber"],
            register_no=scheduled_order["register_no"],
            target_ip=scheduled_order["target_ip"],
        )

def send_all_orders_astm(conn, ip_addr, astm_msg, response_framing="astm"):
    query = parse_genexpert_astm_query(astm_msg)
    requested_sample_id = str(query.get("query_sample_id") or "").strip()
    query_tag = str(query.get("query_tag") or "").strip()
    flag = get_flag_by_device(ip_addr)
    if not flag:
        print(f"[GENEXPERT] ASTM query diabaikan, flag untuk {ip_addr} tidak ditemukan.")
        return

    session = SessionLocal()
    try:
        flag_attr = getattr(PaslabOrder, flag, None)
        if flag_attr is None:
            print(f"[GENEXPERT] ASTM query diabaikan, atribut flag {flag} tidak ada.")
            return

        base_orders = session.query(PaslabOrder).filter(
            (flag_attr == False) | (flag_attr == None)
        ).order_by(PaslabOrder.urut.asc()).all()

        selected_orders = []
        if requested_sample_id and requested_sample_id.upper() != "ALL":
            for order in base_orders:
                if str(order.rnoreg or "").strip() != requested_sample_id:
                    continue
                assay_code, _, _ = resolve_genexpert_assay(order, ip_addr)
                if assay_code:
                    selected_orders = [order]
                    break
        else:
            for order in base_orders:
                assay_code, _, _ = resolve_genexpert_assay(order, ip_addr)
                if assay_code:
                    selected_orders = [order]
                    break

        print(
            f"[GENEXPERT-ASTM-QUERY] ip={ip_addr}, query_tag={query_tag}, requested_sample_id='{requested_sample_id}', "
            f"selected_rnoreg={[str(order.rnoreg or '').strip() for order in selected_orders]}"
        )

        if not selected_orders:
            reply = f"H|\\^&|||{sanitize_astm_field(get_genexpert_host_application(ip_addr), max_len=20)}|||||GeneXpert Host||P|1394-97|{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}\rL|1|N\r"
            send_genexpert_response(conn, ip_addr, reply, response_framing, label="astm-q-empty")
            return

        reply = create_genexpert_astm_order_message(selected_orders, ip_addr=ip_addr, query_tag=query_tag)
        send_genexpert_response(conn, ip_addr, reply, response_framing, label=f"astm-q-order:{selected_orders[0].rnoreg}")
        for order in selected_orders:
            print(f"[GENEXPERT] Order ASTM ditawarkan ke {ip_addr}: {order.rnoreg}")
    finally:
        session.close()

def extract_msg_control_id(hl7_message):
    try:
        segments = hl7_message.split('\r')
        msh = segments[0].split('|')
        if len(msh) > 9:
            return msh[9].strip()
        return None
    except:
        return None

def extract_message_type(hl7_message):
    try:
        segments = hl7_message.split('\r')
        msh = segments[0].split('|')
        if len(msh) > 8:
            return msh[8].strip()
        return ""
    except:
        return ""

def build_hl7_preview(hl7_message, max_segments=4):
    try:
        segments = [segment.strip() for segment in str(hl7_message or "").split('\r') if segment.strip()]
        preview = " | ".join(segments[:max_segments])
        return preview[:800]
    except Exception:
        return str(hl7_message or "")[:800]

def log_genexpert_hl7(direction, ip_addr, hl7_message, label=""):
    message_type = extract_message_type(hl7_message) or "UNKNOWN"
    control_id = extract_msg_control_id(hl7_message) or "UNKNOWN"
    suffix = f", label={label}" if label else ""
    preview = build_hl7_preview(hl7_message)
    logging.info(
        f"[GENEXPERT-HL7-{direction}] ip={ip_addr}, type={message_type}, control_id={control_id}{suffix}, payload={preview}"
    )
    print(
        f"[GENEXPERT-HL7-{direction}] ip={ip_addr}, type={message_type}, control_id={control_id}{suffix}, payload={preview}"
    )

def log_genexpert_hl7_full(direction, ip_addr, hl7_message, label=""):
    message_type = extract_message_type(hl7_message) or "UNKNOWN"
    control_id = extract_msg_control_id(hl7_message) or "UNKNOWN"
    suffix = f", label={label}" if label else ""
    payload = str(hl7_message or "").replace("\r", "\\r\n")
    logging.info(
        f"[GENEXPERT-HL7-{direction}-FULL] ip={ip_addr}, type={message_type}, control_id={control_id}{suffix}, payload={payload}"
    )
    print(
        f"[GENEXPERT-HL7-{direction}-FULL] ip={ip_addr}, type={message_type}, control_id={control_id}{suffix}, payload={payload}"
    )

def frame_genexpert_response(hl7_message, framing):
    message = str(hl7_message or "")
    if framing == "astm":
        frame_body = f"1{message}\x03"
        chk = calculate_astm_checksum(frame_body)
        return f"\x02{frame_body}{chk}\r\n".encode("latin-1")
    if framing == "mllp":
        return f"\x0b{message}\x1c\r".encode("utf-8")
    return message.encode("utf-8")

def build_genexpert_astm_frames(hl7_message, max_text_bytes=240):
    message = str(hl7_message or "")
    text_bytes = message.encode("latin-1")
    chunks = [text_bytes[i:i + max_text_bytes] for i in range(0, len(text_bytes), max_text_bytes)] or [b""]
    frames = []
    frame_number = 1

    for index, chunk in enumerate(chunks):
        is_last = index == len(chunks) - 1
        terminator = b"\x03" if is_last else b"\x17"
        frame_no_byte = str(frame_number).encode("ascii")
        frame_core_bytes = frame_no_byte + chunk + terminator
        checksum = calculate_astm_checksum(frame_core_bytes.decode("latin-1", errors="ignore")).encode("ascii")
        full_frame = b"\x02" + frame_core_bytes + checksum + b"\x0D\x0A"
        frames.append({
            "frame_number": frame_number,
            "is_last": is_last,
            "chunk_len": len(chunk),
            "payload": full_frame,
            "checksum": checksum.decode("ascii", errors="ignore"),
        })
        frame_number = (frame_number + 1) % 8
    return frames

def recv_genexpert_control_char(conn, timeout_seconds=5):
    previous_timeout = conn.gettimeout()
    try:
        conn.settimeout(timeout_seconds)
        return conn.recv(1)
    finally:
        conn.settimeout(previous_timeout)

def send_genexpert_astm_frame(conn, ip_addr, hl7_message, label=""):
    try:
        frames = build_genexpert_astm_frames(hl7_message)
        print(f"[GENEXPERT-ASTM-TX] ip={ip_addr}, label={label}, total_frames={len(frames)}, mode=astm_active")
        conn.sendall(b"\x05")
        log_genexpert_handshake(ip_addr, "ENQ-TX", detail=f"label={label}")

        ctrl = recv_genexpert_control_char(conn, timeout_seconds=5)
        if ctrl == b"\x06":
            log_genexpert_handshake(ip_addr, "ACK-RX", detail=f"phase=pre-frame,label={label}")
        elif ctrl == b"\x15":
            log_genexpert_handshake(ip_addr, "NAK-RX", detail=f"phase=pre-frame,label={label}")
            return False
        else:
            log_genexpert_handshake(ip_addr, "CTRL-RX", detail=f"phase=pre-frame,label={label},hex={ctrl.hex() if ctrl else 'timeout'}")
            return False

        for frame in frames:
            payload = frame["payload"]
            debug_genexpert_astm_frame(ip_addr, payload, direction="TX", label=f"{label}:frame{frame['frame_number']}")
            conn.sendall(payload)
            log_genexpert_handshake(
                ip_addr,
                "FRAME-TX",
                detail=(
                    f"label={label},frame_no={frame['frame_number']},bytes={len(payload)},"
                    f"chunk_len={frame['chunk_len']},last={frame['is_last']}"
                ),
            )

            ctrl = recv_genexpert_control_char(conn, timeout_seconds=15)
            if ctrl == b"\x06":
                log_genexpert_handshake(ip_addr, "ACK-RX", detail=f"phase=post-frame,label={label},frame_no={frame['frame_number']}")
                continue
            if ctrl == b"\x15":
                log_genexpert_handshake(ip_addr, "NAK-RX", detail=f"phase=post-frame,label={label},frame_no={frame['frame_number']}")
                conn.sendall(b"\x04")
                log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after=nak,frame_no={frame['frame_number']}")
                return False
            if ctrl == b"\x04":
                log_genexpert_handshake(ip_addr, "EOT-RX", detail=f"phase=post-frame,label={label},frame_no={frame['frame_number']}")
                conn.sendall(b"\x04")
                log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after-peer-eot,frame_no={frame['frame_number']}")
                return False

            log_genexpert_handshake(
                ip_addr,
                "CTRL-RX",
                detail=f"phase=post-frame,label={label},frame_no={frame['frame_number']},hex={ctrl.hex() if ctrl else 'timeout'}",
            )
            conn.sendall(b"\x04")
            log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after=unexpected,frame_no={frame['frame_number']}")
            return False

        conn.sendall(b"\x04")
        log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label}")
        return True
    except Exception as exc:
        log_genexpert_handshake(ip_addr, "ASTM-SEND-ERROR", detail=f"label={label},error={exc}")
        return False

def get_genexpert_response_mode(ip_addr):
    ip_addr = str(ip_addr or "").strip()
    mode = GENEXPERT_RESPONSE_MODE_BY_IP.get(ip_addr, GENEXPERT_RESPONSE_MODE_DEFAULT)
    if mode not in {"hl7_passive", "astm_active"}:
        mode = GENEXPERT_RESPONSE_MODE_DEFAULT
    return mode

def send_genexpert_response(conn, ip_addr, hl7_message, framing, label=""):
    log_genexpert_hl7("OUT", ip_addr, hl7_message, label=label)
    log_genexpert_hl7_full("OUT", ip_addr, hl7_message, label=label)
    response_mode = get_genexpert_response_mode(ip_addr)
    if framing == "astm" and response_mode == "astm_active":
        print(
            f"[GENEXPERT-DEBUG] Send response ip={ip_addr}, framing={framing}, "
            f"response_mode={response_mode}, label={label}, bytes=multi-frame"
        )
        send_genexpert_astm_frame(conn, ip_addr, hl7_message, label=label)
        return

    payload = frame_genexpert_response(hl7_message, framing)
    if framing == "astm":
        debug_genexpert_astm_frame(ip_addr, payload, direction="TX", label=label)
    else:
        print(
            f"[GENEXPERT-FRAME-TX] ip={ip_addr}, label={label}, framing={framing}, "
            f"hex={_hex_bytes(payload)}, visible={_visible_bytes(payload)}"
        )
    print(
        f"[GENEXPERT-DEBUG] Send response ip={ip_addr}, framing={framing}, "
        f"response_mode={response_mode}, label={label}, bytes={len(payload)}"
    )
    conn.sendall(payload)

def send_genexpert_transport_ack(conn, ip_addr, framing, reason="frame-received"):
    if framing != "astm":
        return
    try:
        conn.sendall(b"\x06")
        print(f"[GENEXPERT-DEBUG] Transport ACK sent ip={ip_addr}, framing={framing}, reason={reason}")
    except Exception as exc:
        print(f"[GENEXPERT-DEBUG] Transport ACK gagal ip={ip_addr}, framing={framing}, reason={reason}, error={exc}")

def log_genexpert_handshake(ip_addr, event, detail=""):
    suffix = f", detail={detail}" if detail else ""
    print(f"[GENEXPERT-HANDSHAKE] ip={ip_addr}, event={event}{suffix}")

def debug_genexpert_astm_frame(ip_addr, frame_bytes, direction="RX", label=""):
    raw = frame_bytes or b""
    suffix = f", label={label}" if label else ""
    if not raw:
        print(f"[GENEXPERT-ASTM-{direction}] ip={ip_addr}{suffix}, empty-frame")
        return

    detail_parts = [
        f"len={len(raw)}",
        f"hex={_hex_bytes(raw)}",
        f"visible={_visible_bytes(raw)}",
    ]

    if raw.startswith(b"\x02") and len(raw) >= 6:
        frame_no = raw[1:2]
        etx_pos = raw.find(b"\x03")
        etb_pos = raw.find(b"\x17")
        end_pos = etx_pos if etx_pos != -1 else etb_pos
        end_name = "ETX" if etx_pos != -1 else ("ETB" if etb_pos != -1 else "NONE")
        if end_pos != -1 and len(raw) >= end_pos + 5:
            checksum_rx = raw[end_pos + 1:end_pos + 3]
            trailer = raw[end_pos + 3:end_pos + 5]
            checksum_basis = raw[1:end_pos + 1].decode("latin-1", errors="ignore")
            checksum_calc = calculate_astm_checksum(checksum_basis).encode("ascii")
            checksum_ok = checksum_rx.upper() == checksum_calc.upper()
            trailer_ok = trailer == b"\r\n"
            detail_parts.extend([
                f"frame_no={frame_no.decode('ascii', errors='ignore')}",
                f"terminator=<{end_name}>",
                f"checksum_rx={checksum_rx.decode('ascii', errors='ignore')}",
                f"checksum_calc={checksum_calc.decode('ascii', errors='ignore')}",
                f"checksum_ok={checksum_ok}",
                f"trailer={_visible_bytes(trailer)}",
                f"trailer_ok={trailer_ok}",
            ])
        else:
            detail_parts.append(f"terminator=<{end_name}>")

    print(f"[GENEXPERT-ASTM-{direction}] ip={ip_addr}{suffix}, " + ", ".join(detail_parts))

def debug_genexpert_order_message(hl7_message, ip_addr=None):
    segments = parse_hl7_segments(hl7_message)
    current_pid = ""
    current_patient_name = ""
    order_index = 0

    for segment in segments:
        fields = segment.split("|")
        seg_type = fields[0] if fields else ""

        if seg_type == "PID":
            current_pid = fields[3] if len(fields) > 3 else ""
            current_patient_name = fields[5] if len(fields) > 5 else ""
            dob = fields[7] if len(fields) > 7 else ""
            sex = fields[8] if len(fields) > 8 else ""
            address = fields[11] if len(fields) > 11 else ""
            print(
                f"[GENEXPERT-ORDER-DEBUG] ip={ip_addr}, seg=PID, patient_id='{current_pid}', "
                f"patient_name='{current_patient_name}', dob='{dob}', sex='{sex}', "
                f"address='{address}', mode='minimal', raw='{segment}'"
            )
        elif seg_type == "ORC":
            order_index = fields[2] if len(fields) > 2 else ""
            order_time = fields[9] if len(fields) > 9 else ""
            print(
                f"[GENEXPERT-ORDER-DEBUG] ip={ip_addr}, seg=ORC, placer_order='{order_index}', "
                f"order_time='{order_time}', patient_id='{current_pid}', "
                f"patient_name='{current_patient_name}', raw='{segment}'"
            )
        elif seg_type == "OBR":
            assay_code = fields[4] if len(fields) > 4 else ""
            result_status = fields[11] if len(fields) > 11 else ""
            assay_parts = assay_code.split("^")
            assay_id = assay_parts[0] if len(assay_parts) > 0 else ""
            assay_name = assay_parts[1] if len(assay_parts) > 1 else ""
            print(
                f"[GENEXPERT-ORDER-DEBUG] ip={ip_addr}, seg=OBR, placer_order='{order_index}', "
                f"assay_code='{assay_id}', assay_name='{assay_name}', result_status='{result_status}', "
                f"patient_id='{current_pid}', patient_name='{current_patient_name}', raw='{segment}'"
            )
        elif seg_type == "TQ1":
            priority = fields[9] if len(fields) > 9 else ""
            print(
                f"[GENEXPERT-ORDER-DEBUG] ip={ip_addr}, seg=TQ1, placer_order='{order_index}', "
                f"priority='{priority}', patient_id='{current_pid}', "
                f"patient_name='{current_patient_name}', raw='{segment}'"
            )
        elif seg_type == "SPM":
            specimen_id = fields[2] if len(fields) > 2 else ""
            specimen_type = fields[4] if len(fields) > 4 else ""
            print(
                f"[GENEXPERT-ORDER-DEBUG] ip={ip_addr}, seg=SPM, placer_order='{order_index}', "
                f"specimen_id='{specimen_id}', specimen_type='{specimen_type}', patient_id='{current_pid}', "
                f"patient_name='{current_patient_name}', raw='{segment}'"
            )

def build_genexpert_result_query(accnumber, msg_control_id):
    ts = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
    # Query hasil berbasis accession number di QRD-8.
    query_msg = (
        f"MSH|^~\\&|LIS|LAB|GeneXpert|Cepheid|{ts}||QRY^Q02|{msg_control_id}|P|2.5\r"
        f"QRD|{ts}|R|I|{msg_control_id}|||1^RD|{accnumber}|OTH|||T\r"
    )
    return query_msg

def get_active_genexpert_ips():
    with connection_lock:
        return list(active_genexpert_connections.keys())

def clear_genexpert_inflight_for_ip(ip_addr, reason="cleared"):
    ip_addr = str(ip_addr or "").strip()
    if not ip_addr:
        return False

    with genexpert_query_inflight_lock:
        state = genexpert_query_inflight_by_ip.pop(ip_addr, None)

    if state:
        logging.info(
            f"[GENEXPERT-QUERY] Clear inflight ip={ip_addr}, reason={reason}, accnumber={state.get('accnumber')}"
        )
        print(
            f"[GENEXPERT-QUERY] Clear inflight ip={ip_addr}, reason={reason}, accnumber={state.get('accnumber')}"
        )
        return True
    return False

def stop_all_scheduled_result_queries(reason="no-active-genexpert"):
    with scheduled_result_query_lock:
        if not scheduled_result_queries:
            return 0

        stopped_accnumbers = list(scheduled_result_queries.keys())
        for accnumber in stopped_accnumbers:
            state = scheduled_result_queries.get(accnumber)
            if not state:
                continue
            state["status"] = reason
            stop_event = state.get("stop_event")
            if stop_event:
                stop_event.set()

        scheduled_result_queries.clear()

    logging.info(
        f"[GENEXPERT-SCHEDULER] Stop semua jadwal, reason={reason}, total={len(stopped_accnumbers)}"
    )
    print(
        f"[GENEXPERT-SCHEDULER] Stop semua jadwal, reason={reason}, total={len(stopped_accnumbers)}"
    )
    with genexpert_query_inflight_lock:
        genexpert_query_inflight_by_ip.clear()
    return len(stopped_accnumbers)

def stop_scheduled_result_query(accnumber, reason="completed"):
    accnumber = str(accnumber or "").strip()
    if not accnumber:
        return False

    with scheduled_result_query_lock:
        state = scheduled_result_queries.get(accnumber)
        if not state:
            return False
        state["status"] = reason
        stop_event = state.get("stop_event")
        if stop_event:
            stop_event.set()
        scheduled_result_queries.pop(accnumber, None)

    logging.info(f"[GENEXPERT-SCHEDULER] Stop accnumber={accnumber}, reason={reason}")
    print(f"[GENEXPERT-SCHEDULER] Stop accnumber={accnumber}, reason={reason}")
    return True

def scheduled_result_query_worker(accnumber):
    if not GENEXPERT_ENABLE_RESULT_QUERY_SCHEDULER:
        stop_scheduled_result_query(accnumber, reason="disabled")
        return

    while True:
        if not get_active_genexpert_ips():
            stop_all_scheduled_result_queries(reason="no-active-genexpert")
            return

        with scheduled_result_query_lock:
            state = scheduled_result_queries.get(accnumber)
            if not state:
                return
            stop_event = state["stop_event"]
            target_ip = state.get("target_ip")
            register_no = state.get("register_no") or accnumber
            attempt = int(state.get("attempt", 0))
            created_at = state.get("created_at") or datetime.datetime.now()
            max_duration_seconds = max(
                int(state.get("max_duration_seconds", GENEXPERT_RESULT_QUERY_MAX_DURATION_SECONDS)),
                1
            )
            age_seconds = max(int((datetime.datetime.now() - created_at).total_seconds()), 0)
            if age_seconds >= max_duration_seconds:
                state["status"] = "expired"
                state["expired_at"] = datetime.datetime.now()
                state["age_seconds"] = age_seconds
                stop_event.set()
                scheduled_result_queries.pop(accnumber, None)
                logging.info(
                    f"[GENEXPERT-SCHEDULER] Expired accnumber={accnumber}, "
                    f"age={age_seconds}s, max_duration={max_duration_seconds}s"
                )
                print(
                    f"[GENEXPERT-SCHEDULER] Expired accnumber={accnumber}, "
                    f"age={age_seconds}s, max_duration={max_duration_seconds}s"
                )
                return
            next_delay = max(
                int(
                    state.get(
                        "initial_delay_seconds" if attempt == 0 else "interval_seconds",
                        GENEXPERT_RESULT_QUERY_INITIAL_DELAY_SECONDS if attempt == 0 else GENEXPERT_RESULT_QUERY_INTERVAL_SECONDS
                    )
                ),
                1
            )

        if stop_event.wait(next_delay):
            return

        with scheduled_result_query_lock:
            state = scheduled_result_queries.get(accnumber)
            if not state:
                return
            state["attempt"] = int(state.get("attempt", 0)) + 1
            state["last_requested_at"] = datetime.datetime.now()
            attempt_no = state["attempt"]

        print(
            f"[GENEXPERT-SCHEDULER] Trigger query hasil accnumber={accnumber}, "
            f"register_no={register_no}, target_ip={target_ip}, attempt={attempt_no}"
        )

        result = trigger_result_query_to_genexpert(
            accnumber=accnumber,
            register_no=register_no,
            target_ip=target_ip,
            wait_seconds=0,
        )

        with scheduled_result_query_lock:
            state = scheduled_result_queries.get(accnumber)
            if not state:
                return
            state["last_result"] = result
            if result.get("ok"):
                state["last_successful_query_at"] = datetime.datetime.now()
            else:
                state["last_error"] = result.get("message")

def schedule_result_query_for_order(
    accnumber,
    register_no,
    target_ip=None,
    initial_delay_seconds=GENEXPERT_RESULT_QUERY_INITIAL_DELAY_SECONDS,
    interval_seconds=GENEXPERT_RESULT_QUERY_INTERVAL_SECONDS,
    max_duration_seconds=GENEXPERT_RESULT_QUERY_MAX_DURATION_SECONDS,
):
    if not GENEXPERT_ENABLE_RESULT_QUERY_SCHEDULER:
        print(
            f"[GENEXPERT-SCHEDULER] Nonaktif. Jadwal query hasil dilewati untuk accnumber={accnumber}"
        )
        return False

    accnumber = str(accnumber or "").strip()
    register_no = str(register_no or accnumber).strip()
    target_ip = str(target_ip or "").strip() or None

    if not accnumber:
        print("[GENEXPERT-SCHEDULER] Jadwal query hasil dilewati karena accnumber kosong.")
        return False

    with scheduled_result_query_lock:
        existing = scheduled_result_queries.get(accnumber)
        if existing:
            existing["register_no"] = register_no
            existing["target_ip"] = target_ip
            existing["initial_delay_seconds"] = initial_delay_seconds
            existing["interval_seconds"] = interval_seconds
            existing["max_duration_seconds"] = max_duration_seconds
            existing["status"] = "active"
            print(f"[GENEXPERT-SCHEDULER] Jadwal sudah aktif untuk accnumber={accnumber}")
            return True

        stop_event = threading.Event()
        worker = threading.Thread(
            target=scheduled_result_query_worker,
            args=(accnumber,),
            name=f"GeneXpertResultQuery-{accnumber}",
            daemon=True,
        )
        scheduled_result_queries[accnumber] = {
            "register_no": register_no,
            "target_ip": target_ip,
            "initial_delay_seconds": initial_delay_seconds,
            "interval_seconds": interval_seconds,
            "max_duration_seconds": max_duration_seconds,
            "attempt": 0,
            "status": "active",
            "created_at": datetime.datetime.now(),
            "stop_event": stop_event,
            "thread_name": worker.name,
        }

    worker.start()
    print(
        f"[GENEXPERT-SCHEDULER] Jadwal dibuat accnumber={accnumber}, "
        f"register_no={register_no}, target_ip={target_ip}, "
        f"initial_delay={initial_delay_seconds}s, interval={interval_seconds}s, "
        f"max_duration={max_duration_seconds}s"
    )
    return True

def trigger_result_query_to_genexpert(accnumber, register_no, target_ip=None, wait_seconds=20):
    if not GENEXPERT_ENABLE_RESULT_QUERY_SCHEDULER:
        return {
            "ok": False,
            "message": "Mode GeneXpert pasif aktif. Host hanya menjawab request dari alat.",
        }

    active_ips = get_active_genexpert_ips()
    if not active_ips:
        return {"ok": False, "message": "Tidak ada koneksi GeneXpert aktif."}

    if target_ip:
        target_ips = [target_ip] if target_ip in active_ips else []
        if not target_ips:
            return {"ok": False, "message": f"Koneksi GeneXpert {target_ip} tidak aktif."}
    else:
        target_ips = active_ips

    ts = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
    msg_control_id = f"LISQRY{ts}{int(time.time() * 1000) % 1000:03d}"
    query_message = build_genexpert_result_query(accnumber, msg_control_id)
    mllp_payload = f"\x0b{query_message}\x1c\r".encode("utf-8")

    pending_event = threading.Event()
    with pending_query_lock:
        pending_result_queries[accnumber] = {
            "register_no": register_no,
            "target_ips": list(target_ips),
            "msg_control_id": msg_control_id,
            "requested_at": datetime.datetime.now(),
            "event": pending_event,
            "status": "requested",
        }

    sent_ips = []
    failed_ips = []
    for ip in target_ips:
        now = datetime.datetime.now()
        with genexpert_query_inflight_lock:
            inflight = genexpert_query_inflight_by_ip.get(ip)
            if inflight:
                inflight_age = max(
                    (now - inflight.get("requested_at", now)).total_seconds(),
                    0
                )
                if inflight_age < GENEXPERT_RESULT_QUERY_INFLIGHT_TIMEOUT_SECONDS:
                    failed_ips.append({
                        "ip": ip,
                        "error": f"query-inflight:{inflight.get('accnumber')}",
                    })
                    print(
                        f"[GENEXPERT-QUERY] Skip query accnumber={accnumber} ke {ip} "
                        f"karena masih menunggu accnumber={inflight.get('accnumber')} "
                        f"({int(inflight_age)}s)"
                    )
                    continue
                genexpert_query_inflight_by_ip.pop(ip, None)

        with connection_lock:
            conn = active_genexpert_connections.get(ip)
        if not conn:
            failed_ips.append({"ip": ip, "error": "connection-not-active"})
            continue
        try:
            log_genexpert_hl7("OUT", ip, query_message, label=f"result-query:{accnumber}")
            log_genexpert_hl7_full("OUT", ip, query_message, label=f"result-query:{accnumber}")
            conn.sendall(mllp_payload)
            with genexpert_query_inflight_lock:
                genexpert_query_inflight_by_ip[ip] = {
                    "accnumber": accnumber,
                    "register_no": register_no,
                    "requested_at": now,
                    "msg_control_id": msg_control_id,
                }
            sent_ips.append(ip)
            print(f"[GENEXPERT-QUERY] Kirim query hasil accnumber={accnumber} ke {ip}")
        except Exception as e:
            failed_ips.append({"ip": ip, "error": str(e)})
            clear_genexpert_inflight_for_ip(ip, reason="send-failed")

    if not sent_ips:
        with pending_query_lock:
            pending_result_queries.pop(accnumber, None)
        return {"ok": False, "message": "Gagal kirim query ke semua GeneXpert aktif.", "failures": failed_ips}

    if wait_seconds and wait_seconds > 0:
        pending_event.wait(wait_seconds)
        with pending_query_lock:
            state = pending_result_queries.get(accnumber)
            if state and state.get("status") == "found":
                pending_result_queries.pop(accnumber, None)
                return {
                    "ok": True,
                    "message": "Hasil ditemukan dan disimpan ke LisPhoenix.",
                    "target_ips": sent_ips,
                    "accnumber": accnumber,
                    "source_ip": state.get("source_ip"),
                }
            # Timeout/hasil belum masuk, biarkan state tetap ada agar response telat tetap bisa diproses.
        return {
            "ok": True,
            "message": "Query terkirim. Menunggu hasil dari GeneXpert.",
            "target_ips": sent_ips,
            "accnumber": accnumber,
            "failures": failed_ips,
        }

    return {
        "ok": True,
        "message": "Query terkirim.",
        "target_ips": sent_ips,
        "accnumber": accnumber,
        "failures": failed_ips,
    }

@app.route("/api/genexpert/query-result", methods=["POST"])
def api_query_genexpert_result():
    if not GENEXPERT_ENABLE_RESULT_QUERY_SCHEDULER:
        return jsonify({
            "ok": False,
            "message": "Mode GeneXpert pasif aktif. Host hanya menjawab request dari alat.",
        }), 409

    payload = request.get_json(silent=True) or {}
    accnumber = str(payload.get("accnumber") or "").strip()
    register_no = str(payload.get("register_no") or payload.get("nomor_register") or "").strip()
    target_ip = str(payload.get("target_ip") or "").strip() or None

    try:
        wait_seconds = int(payload.get("wait_seconds", 20))
    except Exception:
        wait_seconds = 20

    if not accnumber:
        return jsonify({"ok": False, "message": "Field 'accnumber' wajib diisi."}), 400
    if not register_no:
        return jsonify({"ok": False, "message": "Field 'register_no' (nomor register pasien) wajib diisi."}), 400

    result = trigger_result_query_to_genexpert(
        accnumber=accnumber,
        register_no=register_no,
        target_ip=target_ip,
        wait_seconds=wait_seconds,
    )
    status_code = 200 if result.get("ok") else 409
    return jsonify(result), status_code
    
def parse_hl7_result(conn, msg_id, hl7_message, device_name="GeneXpert"):
    session = SessionLocal()
    clean_hl7 = hl7_message
    try:
        # 1. Bersihkan dan Split Pesan
        # HL7 dipisahkan oleh \r (Carriage Return)
        if "MSH|" not in hl7_message:
            logging.warning("[HL7] Data tidak mengandung segmen MSH valid.")
            print("[HL7] Data tidak mengandung segmen MSH valid.")
            return
        start_idx = hl7_message.find("MSH|")
        hl7_message = hl7_message[start_idx:]

        segments = hl7_message.strip().split('\r')
        
        # Variabel penampung
        sample_id = ""      # no_id
        patient_id = ""     # seq_no
        patient_name = ""   # rnmpas
        result_date = None  # tgl_data
        results_list = []   # untuk organisme
        message_type = ""
        
        # Waktu default jika tidak ada di pesan
        result_date = datetime.datetime.now()

        # 2. Loop setiap segmen
        for segment in segments:
            fields = segment.split('|')
            if not fields: continue
            
            seg_type = fields[0]

            # --- MSH (Header) ---
            if seg_type == 'MSH':
                if len(fields) > 8 and fields[8]:
                    message_type = fields[8].strip()
                # Ambil tanggal pesan (Field 7) Format: YYYYMMDDHHMMSS
                if len(fields) > 6 and fields[6]:
                    try:
                        raw_date = fields[6][:14] # Ambil 14 digit pertama
                        result_date = datetime.datetime.strptime(raw_date, "%Y%m%d%H%M%S")
                    except ValueError:
                        pass # Gunakan default datetime.now() jika format salah

            # --- PID (Patient ID) ---
            elif seg_type == 'PID':
                # PID|3 = Patient ID (seq_no)
                if len(fields) > 3:
                    patient_id = fields[3].replace('^', '')
                
                # PID|5 = Patient Name (rnmpas)
                if len(fields) > 5:
                    # Ganti caret ^ dengan spasi (Family^Name -> Family Name)
                    patient_name = fields[5].replace('^', ' ').strip()

            # --- OBR (Observation Request - Info Sample) ---
            elif seg_type == 'OBR':
                # OBR|2 atau OBR|3 biasanya berisi Sample ID / Accession No
                # Kita coba ambil field 3 (Filler Order Number) dulu, kalau kosong field 2
                if len(fields) > 3 and fields[3]:
                    sample_id = fields[3].replace('^', '')
                elif len(fields) > 2 and fields[2]:
                    sample_id = fields[2].replace('^', '')

            # --- OBX (Observation Result - Hasil Tes) ---
            elif seg_type == 'OBX':
                # OBX|3 = Test Name/Code (Misal: MTB, RIF)
                # OBX|5 = Result Value (Misal: DETECTED, NOT DETECTED)
                if len(fields) > 5:
                    test_name = fields[3].split('^')[1] if '^' in fields[3] else fields[3]
                    test_val = fields[5].replace('^', ' ')
                    
                    # Gabungkan nama tes dan hasil
                    # Contoh: "MTB: DETECTED"
                    results_list.append(f"{test_name}: {test_val}")

        # 3. Gabungkan semua hasil OBX menjadi satu string
        if not results_list:
            final_result = "No Result Found"
        else:
            final_result = "; ".join(results_list)

        # 4. Validasi Data Penting
        if not sample_id:
            if str(message_type).startswith("ORU^"):
                source_ip = ""
                try:
                    source_ip = str(conn.getpeername()[0]).strip()
                except Exception:
                    source_ip = str(device_name).replace("GeneXpert-", "").replace(",", " ").strip()

                if source_ip:
                    logging.info(f"[HL7 Parser] ORU tanpa sample_id dari {source_ip}. Anggap sebagai request order.")
                    print(f"[HL7 Parser] ORU tanpa sample_id dari {source_ip}. Anggap sebagai request order.")
                    send_all_orders(conn, source_ip, clean_hl7, msg_id)
                else:
                    logging.warning("[HL7 Parser] ORU tanpa sample_id diterima, tetapi IP sumber tidak dapat diidentifikasi.")
                    print("[HL7 Parser] ORU tanpa sample_id diterima, tetapi IP sumber tidak dapat diidentifikasi.")
            else:
                logging.info(f"[HL7 Parser] Pesan {message_type or 'UNKNOWN'} tanpa sample_id diabaikan.")
                print(f"[HL7 Parser] Pesan {message_type or 'UNKNOWN'} tanpa sample_id diabaikan.")
            return

        mapped_no_id = sample_id
        mapped_seq_no = patient_id
        mapped_alat = device_name
        pending_event = None

        with pending_query_lock:
            pending = pending_result_queries.get(sample_id)
            if pending:
                source_ip = str(device_name).replace("GeneXpert-", "").strip()
                mapped_no_id = pending.get("register_no") or sample_id
                mapped_seq_no = sample_id
                mapped_alat = f"GeneXpert-{source_ip}" if source_ip else device_name
                pending["status"] = "found"
                pending["source_ip"] = source_ip
                pending["response_at"] = datetime.datetime.now()
                pending_event = pending.get("event")
                clear_genexpert_inflight_for_ip(source_ip, reason="result-received")

        source_ip = ""
        try:
            source_ip = str(conn.getpeername()[0]).strip()
        except Exception:
            source_ip = str(device_name).replace("GeneXpert-", "").replace(",", " ").strip()

        if source_ip:
            flag_name = get_flag_by_device(source_ip)
            if flag_name:
                order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == sample_id).first()
                if order:
                    setattr(order, flag_name, True)
                    print(f"[GENEXPERT] Hasil diterima, set {flag_name}=TRUE untuk {sample_id}")

        print(f"[HL7 Parser] Menyimpan hasil untuk Sample: {sample_id}")

        # 5. Simpan ke Database
        new_data = LisPhoenix(
            no_id=mapped_no_id,
            seq_no=mapped_seq_no,
            rnmpas=patient_name,
            tgl_data=result_date,
            rawdt=hl7_message,
            organisme=final_result,
            alat=mapped_alat
        )
        
        session.add(new_data)
        session.commit()
        print(f"[DB] Berhasil simpan ke LisPhoenix: {sample_id}")
        stop_scheduled_result_query(sample_id, reason="result-received")
        if pending_event:
            pending_event.set()

    except Exception as e:
        logging.error(f"[HL7 Parser] Error menyimpan data: {e}")
        print(f"[HL7 Parser] Error menyimpan data: {e}")
        session.rollback()
    finally:
        session.close()

def create_hl7_dsr_response(order, msg_control_id, qrd_segment):
    """
    Membuat pesan balasan DSR^Q03 (Data Response) untuk GeneXpert.
    """
    timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
    
    # --- 1. HEADER (MSH) ---
    # Field 9 (Message Type) adalah DSR^Q03
    # Field 10 (Control ID) kita generate baru
    # Field 6 (Receiving Fac) harusnya GeneXpert
    resp_control_id = f"RESP{timestamp}"
    msh = f"MSH|^~\\&|LIS|LAB|GeneXpert|Cepheid|{timestamp}||DSR^Q03|{resp_control_id}|P|2.5"
    
    # --- 2. ACKNOWLEDGEMENT (MSA) ---
    # AA = Application Accept (Kita mengerti pertanyaannya)
    # msg_control_id = ID dari pesan QRY yang dikirim GeneXpert (Supaya dia tahu ini jawaban untuk pertanyaan yg mana)
    msa = f"MSA|AA|{msg_control_id}"
    
    # --- 3. QUERY DEFINITION (QRD) ---
    # Kita kembalikan segmen QRD yang dikirim alat (Echo back)
    # qrd_segment harus string raw dari pesan masuk
    qrd = qrd_segment

    # --- 4. QUERY RESPONSE STATUS (QRF) ---
    # Opsional, tapi baik untuk konfirmasi
    qrf = f"QRF|LIS|{timestamp}||||"

    # --- 5. DATA PASIEN & ORDER (Jika Order Ditemukan) ---
    if order:
        # Mapping Data
        sample_id = str(order.rnoreg)
        pid_norm = order.norm if order.norm else ""
        first_name, last_name = split_patient_name(order.nama)
        pid_nama = f"{last_name}^{first_name}"
        room = str(getattr(order, "ruangan", "") or "RSSA MALANG").strip().upper()
        room = room.replace("|", " ").replace("^", " ")[:30]
        
        # Gender
        raw_gender = str(order.rjenis).upper()
        pid_gender = 'M' if 'LAKI' in raw_gender or raw_gender == 'L' else 'F'
        
        # Test Code (Pakai Mapping yang tadi kita buat)
        nama_tes_db = str(order.tes).strip() if order.tes else ""
        test_code = GENEXPERT_TEST_MAPPING.get(nama_tes_db, DEFAULT_GXP_CODE) # Default: MTBRIF
        
        # Segmen Data
        # DSP/PID/ORC/OBR tergantung setting alat. 
        # GeneXpert standar biasanya terima format ORM di dalam DSR atau sequence PID-ORC-OBR
        
        pid = f"PID|1||{pid_norm}||{pid_nama}|||{pid_gender}"
        pv1 = f"PV1|1|I|{room}"
        orc = f"ORC|NW|{sample_id}"
        obr = f"OBR|1|{sample_id}||{test_code}^{nama_tes_db}^L|||{timestamp}"
        
        # Gabungkan
        return f"{msh}\r{msa}\r{qrd}\r{qrf}\r{pid}\r{pv1}\r{orc}\r{obr}\r"
    
    else:
        # Jika TIDAK ADA ORDER (Not Found)
        # Kita kirim QAK (Query Acknowledge) dengan status NF (Not Found) di QRF/QAK
        # Atau cukup kirim MSA|AA tapi tanpa segmen Order
        return f"{msh}\r{msa}\r{qrd}\r{qrf}\r"

def parse_myla_result(hl7_message, device_name="MYLA"):
    """
    Parser khusus untuk HL7 dari bioMérieux MYLA (Kalibrasi V4.9).
    Menangani hasil BACT/ALERT (Kultur Darah) dan VITEK (Identifikasi & AST).
    """
    session = SessionLocal()
    try:
        segments = hl7_message.strip().split('\r')
        
        sample_id = None
        patient_id = ""
        patient_name = ""
        result_date = datetime.datetime.now()
        
        kultur_id = []
        ast_results = []
        
        for segment in segments:
            fields = segment.split('|')
            if not fields: continue
            seg_type = fields[0]

            if seg_type == 'MSH':
                if len(fields) > 6 and fields[6]:
                    try:
                        result_date = datetime.datetime.strptime(fields[6][:14], "%Y%m%d%H%M%S")
                    except: pass

            elif seg_type == 'PID':
                if len(fields) > 3: patient_id = fields[3].replace('^', '')
                if len(fields) > 5: patient_name = fields[5].replace('^', ' ').strip()

            elif seg_type == 'OBR':
                if len(fields) > 3 and fields[3]:
                    sample_id = fields[3].replace('^', '')
                elif len(fields) > 2 and fields[2]:
                    sample_id = fields[2].replace('^', '')

            elif seg_type == 'OBX':
                if len(fields) > 5:
                    test_param = fields[3].split('^')[1] if '^' in fields[3] else fields[3]
                    
                    if "Time To Detection" in test_param:
                        continue

                    raw_val = fields[5]
                    val_parts = raw_val.split('^')
                    
                    if len(val_parts) > 1:
                        if val_parts[0] in ['<', '<=', '>', '>=', '=']:
                            result_val = f"{val_parts[0]} {val_parts[1]}"
                        else:
                            result_val = val_parts[1]
                    else:
                        result_val = raw_val.strip()
                    
                    interpretation = ""
                    if len(fields) > 8 and fields[8]:
                        interpretation = fields[8].split('^')[0].strip().upper()
                    
                    if interpretation in ['S', 'I', 'R', 'NS']:
                        ast_results.append(f"{test_param}: {result_val} ({interpretation})")
                    else:
                        kultur_id.append(f"{test_param}: {result_val}")

        final_results = []
        if kultur_id:
            final_results.append("ID: " + ", ".join(kultur_id))
        if ast_results:
            final_results.append("AST: " + "; ".join(ast_results))
            
        final_result_str = " | ".join(final_results) if final_results else "No Data"

        if not sample_id:
            sample_id = f"ERR_MYLA_{datetime.datetime.now().strftime('%H%M%S')}"
            final_result_str = f"[NO_ID] {final_result_str}"

        
        new_entry = LisPhoenix(
            no_id=sample_id,
            seq_no=patient_id,
            rnmpas=patient_name,
            tgl_data=result_date,
            rawdt=hl7_message,
            organisme=final_result_str[:255],
            alat=device_name
        )
        session.add(new_entry)
        session.commit()

    except Exception as e:
        logging.error(f"[MYLA Parser] Error: {e}")
        session.rollback()
    finally:
        session.close()

def get_pending_myla_orders(limit=10):
    session = SessionLocal()
    try:
        return session.query(PaslabOrder).filter(
            (PaslabOrder.flg_vitek3 == False) | (PaslabOrder.flg_vitek3 == None)
        ).order_by(PaslabOrder.urut.asc()).limit(limit).all()
    finally:
        session.close()

def mark_myla_order_sent(order_id):
    session = SessionLocal()
    try:
        order = session.query(PaslabOrder).filter(PaslabOrder.urut == order_id).first()
        if order:
            order.flg_vitek3 = True
            session.commit()
            return True
        return False
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()

def create_myla_astm_order_message(order):
    """
    ASTM E1394/LIS2-A2 style:
    H, P, C, O, R, L
    """
    pid = sanitize_astm_field(order.norm, max_len=32)
    sid = sanitize_astm_field(order.rnoreg, max_len=32)

    first_name, last_name = split_patient_name(sanitize_astm_field(order.nama, max_len=80))
    first_name = sanitize_astm_field(first_name, uppercase=True, max_len=20)
    last_name = sanitize_astm_field(last_name, uppercase=True, max_len=20)
    p_name = f"{last_name}^{first_name}"

    sex_raw = sanitize_astm_field(order.rjenis, uppercase=True, max_len=10)
    sex = "M" if sex_raw.startswith("L") else "F"

    raw_location = sanitize_astm_field(getattr(order, 'ruangan', "UT"), uppercase=True, max_len=40)
    location = re.sub(r"[^A-Z0-9]", "", raw_location)[:10] or "UT"

    diagnosis = sanitize_astm_field(getattr(order, 'diagnosa', "Unspecified"), max_len=60) or "Unspecified"
    specimen_type = sanitize_astm_field(order.kd_spesimen, uppercase=True, max_len=20) if order.kd_spesimen else "BLOOD"
    specimen_field = f"{specimen_type}^VENA^^BAIK"
    test_code = sanitize_astm_field(order.tes, uppercase=True, max_len=40) or "GENERAL"

    head = r"H|\^&|||LIS|||||||||P|1"

    p_rec = [""] * 36
    p_rec[0] = "P"
    p_rec[1] = "1"
    p_rec[2] = pid
    p_rec[3] = pid
    p_rec[5] = p_name
    p_rec[8] = sex
    p_rec[25] = location
    pat_str = "|".join(p_rec[:36])

    com_str = f"C|1|L|{diagnosis}|G"

    o_rec = [""] * 32
    o_rec[0] = "O"
    o_rec[1] = "1"
    o_rec[2] = sid
    o_rec[4] = f"^^^{test_code}"
    o_rec[5] = "R"
    o_rec[11] = "A"
    o_rec[15] = specimen_field
    ord_str = "|".join(o_rec[:32])

    # Placeholder R record agar struktur record lengkap H,P,C,O,R,L.
    rcd_str = "R|1|^^^ORDER_STATUS|PENDING|||N|||||F"
    term = "L|1|N"

    message_content = f"{head}\r{pat_str}\r{com_str}\r{ord_str}\r{rcd_str}\r{term}\r"
    seq = "1"
    frame_body = f"{seq}{message_content}\x03"
    chk = calculate_astm_checksum(frame_body)
    full_frame = f"\x02{frame_body}{chk}\r\n"
    return [full_frame.encode("latin-1")]

def parse_myla_astm_records(raw_message, device_name="MYLA"):
    session = SessionLocal()
    try:
        sample_id = ""
        patient_id = ""
        patient_name = ""
        results = []

        records = [r for r in raw_message.split('\r') if r.strip()]
        for rec in records:
            row = rec[1:] if rec and rec[0].isdigit() else rec
            fields = row.split('|')
            if not fields:
                continue

            rtype = fields[0]
            if rtype == "P":
                if len(fields) > 3:
                    patient_id = fields[3].strip()
                if len(fields) > 5:
                    patient_name = fields[5].replace("^", " ").strip()
            elif rtype == "O":
                if len(fields) > 2:
                    sample_id = fields[2].replace("^", "").strip()
            elif rtype == "R":
                test_name = fields[2].strip() if len(fields) > 2 else ""
                test_value = fields[3].strip() if len(fields) > 3 else ""
                if test_name or test_value:
                    results.append(f"{test_name}: {test_value}".strip(": "))

        if sample_id and results:
            final_result_str = " | ".join(results)[:255]
            new_entry = LisPhoenix(
                no_id=sample_id,
                seq_no=patient_id,
                rnmpas=patient_name,
                tgl_data=datetime.datetime.now(),
                rawdt=raw_message,
                organisme=final_result_str,
                alat=device_name
            )
            session.add(new_entry)
            session.commit()
        else:
            print("[MYLA-ASTM] ASTM message diterima (tanpa hasil R untuk disimpan).")
    except Exception as e:
        logging.error(f"[MYLA-ASTM] Error parse/simpan: {e}")
        session.rollback()
    finally:
        session.close()

def _read_until_lf(conn, timeout_seconds=5):
    deadline = time.time() + timeout_seconds
    payload = b""
    while time.time() < deadline:
        conn.settimeout(max(0.1, deadline - time.time()))
        chunk = conn.recv(1)
        if not chunk:
            break
        payload += chunk
        if payload.endswith(b"\n"):
            break
    return payload

def receive_myla_astm_transmission(conn, peer_ip, first_control=ENQ):
    """
    Menerima transmisi ASTM dari server:
    ENQ -> ACK, lalu STX frame(s) -> ACK/NAK tiap frame, diakhiri EOT.
    """
    if first_control == ENQ:
        conn.sendall(ACK)

    assembled = []
    pending_ctrl = first_control if first_control == STX else None
    while True:
        if pending_ctrl is not None:
            ctrl = pending_ctrl
            pending_ctrl = None
        else:
            conn.settimeout(5)
            ctrl = conn.recv(1)
        if not ctrl:
            return
        if ctrl == EOT:
            break
        if ctrl == ENQ:
            conn.sendall(ACK)
            continue
        if ctrl != STX:
            continue

        payload = _read_until_lf(conn, timeout_seconds=5)
        if not payload:
            conn.sendall(NAK)
            continue

        frame = ctrl + payload
        try:
            frame_text = frame.decode("latin-1", errors="ignore")
            body_start = frame_text.find("\x02")
            etx_pos = frame_text.find("\x03")
            if body_start == -1 or etx_pos == -1 or etx_pos <= body_start:
                conn.sendall(NAK)
                continue

            frame_body = frame_text[body_start + 1:etx_pos + 1]
            recv_chk = frame_text[etx_pos + 1:etx_pos + 3].upper()
            calc_chk = calculate_astm_checksum(frame_body).upper()
            if recv_chk != calc_chk:
                logging.warning(f"[MYLA-ASTM] Checksum mismatch dari {peer_ip}: recv={recv_chk}, calc={calc_chk}")
                print(f"[MYLA-ASTM] Checksum mismatch dari {peer_ip}: recv={recv_chk}, calc={calc_chk}")
                conn.sendall(NAK)
                continue

            # Drop seq(1 char) dan ETX
            data_part = frame_body[1:-1]
            assembled.append(data_part)
            conn.sendall(ACK)
        except Exception as e:
            logging.error(f"[MYLA-ASTM] Gagal parse frame dari {peer_ip}: {e}")
            print(f"[MYLA-ASTM] Gagal parse frame dari {peer_ip}: {e}")
            conn.sendall(NAK)

    if assembled:
        raw_message = "".join(assembled)
        parse_myla_astm_records(raw_message, device_name=f"MYLA-{peer_ip}")

def wait_for_astm_control(conn, expected_controls, peer_ip, timeout_seconds=MYLA_CONTROL_TIMEOUT_SECONDS):
    deadline = time.time() + timeout_seconds
    while time.time() < deadline:
        try:
            conn.settimeout(max(0.1, deadline - time.time()))
            b = conn.recv(1)
            if not b:
                return None
            if b in expected_controls:
                return b
            if b == ENQ:
                receive_myla_astm_transmission(conn, peer_ip, first_control=ENQ)
            elif b == STX:
                receive_myla_astm_transmission(conn, peer_ip, first_control=STX)
        except socket.timeout:
            continue
        except Exception as e:
            logging.error(f"[MYLA-ASTM] Error wait control: {e}")
            print(f"[MYLA-ASTM] Error wait control: {e}")
            return None
    return None

def send_order_to_myla_astm(conn, order, peer_ip):
    frames = create_myla_astm_order_message(order)
    conn.sendall(ENQ)
    hs = wait_for_astm_control(conn, {ACK}, peer_ip, timeout_seconds=MYLA_CONTROL_TIMEOUT_SECONDS)
    if hs != ACK:
        logging.warning(f"[MYLA-ASTM] Handshake gagal untuk rnoreg={order.rnoreg}, respon={hs}")
        print(f"[MYLA-ASTM] Handshake gagal untuk rnoreg={order.rnoreg}, respon={hs}")
        return False

    for i, frame in enumerate(frames, start=1):
        frame_ok = False
        for attempt in range(1, 4):
            conn.sendall(frame)
            resp = wait_for_astm_control(conn, {ACK, NAK}, peer_ip, timeout_seconds=MYLA_CONTROL_TIMEOUT_SECONDS)
            if resp == ACK:
                frame_ok = True
                break
            if resp == NAK:
                logging.warning(f"[MYLA-ASTM] Frame {i} NAK rnoreg={order.rnoreg}, retry={attempt}")
                print(f"[MYLA-ASTM] Frame {i} NAK rnoreg={order.rnoreg}, retry={attempt}")
                time.sleep(0.5)
                continue
            logging.warning(f"[MYLA-ASTM] Frame {i} timeout rnoreg={order.rnoreg}, retry={attempt}")
            print(f"[MYLA-ASTM] Frame {i} timeout rnoreg={order.rnoreg}, retry={attempt}")
        if not frame_ok:
            conn.sendall(EOT)
            return False

    conn.sendall(EOT)
    return True

def create_myla_hl7_order_message(order, msg_control_id):
    timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
    sample_id = sanitize_astm_field(order.rnoreg, uppercase=True, max_len=32) or "000000"
    pid_norm = sanitize_astm_field(order.norm, uppercase=True, max_len=32) or f"PID{sample_id}"
    test_code = sanitize_astm_field(order.tes, uppercase=True, max_len=40) or "ID"
    specimen_code = sanitize_astm_field(order.kd_spesimen, uppercase=True, max_len=20) or "URC"

    # Sesuaikan pattern ID seperti contoh vendor (SPE/AWOS + accession).
    compact_id = re.sub(r"[^A-Z0-9]", "", sample_id) or "000000"
    spm_id = f"SPE{compact_id}"[:30]
    obr_order_id = f"AWOS{compact_id}"[:30]

    msh = (
        f"MSH|^~\\&|LIS|LAB|MYLA|BMX|{timestamp}||OML^O33^OML_O33|{msg_control_id}|P|2.5.1"
        f"|||NE|AL||UNICODE UTF-8"
    )
    pid = f"PID|||{pid_norm}"
    pv1 = "PV1||O"
    spm = f"SPM|1|{spm_id}||{specimen_code}^{specimen_code}^99BMx|||||||P^^HL70369||||||{timestamp}"
    sac = f"SAC|||{spm_id}"
    # ORC.9 wajib ada; ikuti contoh MyLA: ORC|NW||||||||<datetime>
    orc = f"ORC|NW||||||||{timestamp}"
    tq1 = "TQ1|||||||||R^^HL7048"
    obr = f"OBR|1|{obr_order_id}||{test_code}^{test_code}^99BMx"
    return f"{msh}\r{pid}\r{pv1}\r{spm}\r{sac}\r{orc}\r{tq1}\r{obr}\r"

def process_myla_hl7_message(conn, hl7_str, peer_ip):
    incoming_control_id = ""
    message_type = ""
    ack_code = ""
    ack_for_control_id = ""
    err_text = ""
    orc_control = ""
    orc_status = ""
    orc_ref = ""

    try:
        msh_fields = hl7_str.split('\r')[0].split('|')
        if len(msh_fields) > 8:
            message_type = msh_fields[8].strip().upper()
        if len(msh_fields) > 9:
            incoming_control_id = msh_fields[9].strip()
    except Exception:
        pass

    if "ORU^" in message_type or message_type.startswith("OUL^R22"):
        print(f"[MYLA-HL7] Menerima hasil dari {peer_ip} (type={message_type}, ID: {incoming_control_id})")
        parse_myla_result(hl7_str, device_name=f"MYLA-{peer_ip}")
        if incoming_control_id:
            try:
                ack_time = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
                ack_msg = (
                    f"MSH|^~\\&|LIS|LAB|MYLA|bioMerieux|{ack_time}||ACK|{incoming_control_id}|P|2.5\r"
                    f"MSA|AA|{incoming_control_id}\r"
                )
                conn.sendall(f"\x0b{ack_msg}\x1c\r".encode("utf-8"))
            except Exception as e:
                logging.warning(f"[MYLA-HL7] Gagal kirim ACK hasil {incoming_control_id}: {e}")
    elif message_type.startswith("ACK") or message_type.startswith("ORL^O34"):
        for seg in hl7_str.split('\r'):
            if seg.startswith("MSA|"):
                parts = seg.split('|')
                if len(parts) > 1:
                    ack_code = parts[1].strip().upper()
                if len(parts) > 2:
                    ack_for_control_id = parts[2].strip()
                break
        for seg in hl7_str.split('\r'):
            if seg.startswith("ERR|"):
                err_parts = seg.split('|')
                if len(err_parts) > 2:
                    err_text = err_parts[2].strip()
                if len(err_parts) > 3 and err_parts[3]:
                    err_text = f"{err_text} {err_parts[3].strip()}".strip()
                break
        for seg in hl7_str.split('\r'):
            if seg.startswith("ORC|"):
                o = seg.split('|')
                if len(o) > 1:
                    orc_control = o[1].strip().upper()
                if len(o) > 5:
                    orc_status = o[5].strip().upper()
                if len(o) > 6:
                    orc_ref = o[6].strip()
                break

    return {
        "message_type": message_type,
        "control_id": incoming_control_id,
        "ack_code": ack_code,
        "ack_for_control_id": ack_for_control_id,
        "err_text": err_text,
        "orc_control": orc_control,
        "orc_status": orc_status,
        "orc_ref": orc_ref,
    }

def wait_for_myla_hl7_ack(conn, expected_control_id, peer_ip, timeout_seconds=MYLA_ACK_TIMEOUT_SECONDS):
    deadline = time.time() + timeout_seconds
    buffer = b""

    while time.time() < deadline:
        try:
            conn.settimeout(max(0.1, deadline - time.time()))
            data = conn.recv(4096)
            if not data:
                return False
            buffer += data

            if b"\x1c\r" not in buffer:
                continue

            chunks = buffer.split(b"\x1c\r")
            for raw_msg in chunks[:-1]:
                clean_msg = raw_msg.replace(b"\x0b", b"")
                hl7_str = clean_msg.decode("latin-1", errors="ignore")
                if "MSH|" not in hl7_str:
                    continue

                hl7_str = hl7_str[hl7_str.find("MSH|"):]
                parsed = process_myla_hl7_message(conn, hl7_str, peer_ip)
                if parsed["message_type"].startswith("ACK") or parsed["message_type"].startswith("ORL^O34"):
                    ack_for = parsed.get("ack_for_control_id", "")
                    ack_code = parsed.get("ack_code", "")
                    err_text = parsed.get("err_text", "")
                    orc_control = parsed.get("orc_control", "")
                    orc_status = parsed.get("orc_status", "")
                    orc_ref = parsed.get("orc_ref", "")
                    if ack_for == expected_control_id:
                        logging.info(
                            f"[MYLA-HL7] Response diterima untuk {expected_control_id} "
                            f"(type={parsed.get('message_type')}, code={ack_code or '-'}, "
                            f"orc1={orc_control or '-'}, orc5={orc_status or '-'}, orc6={orc_ref or '-'}, "
                            f"err={err_text or '-'})"
                        )
                        app_ok = True
                        if parsed["message_type"].startswith("ORL^O34"):
                            app_ok = (orc_control == "OK")
                        return (ack_code in ("AA", "CA")) and app_ok
                    logging.info(
                        f"[MYLA-HL7] Response bukan untuk pesan ini "
                        f"(expected={expected_control_id}, msa2={ack_for or '-'}, "
                        f"type={parsed.get('message_type')}, code={ack_code or '-'})"
                    )

            buffer = chunks[-1]
        except socket.timeout:
            continue
        except Exception as e:
            logging.error(f"[MYLA-HL7] Error wait ACK: {e}")
            print(f"[MYLA-HL7] Error wait ACK: {e}")
            return False

    return False

def pump_myla_incoming(conn, peer_ip, buffer, wait_seconds=0.2):
    """
    Proses pesan masuk dari MyLA saat idle (mis. ORU hasil) pada koneksi client yang sama.
    """
    deadline = time.time() + max(0.05, wait_seconds)
    while time.time() < deadline:
        try:
            conn.settimeout(max(0.05, deadline - time.time()))
            data = conn.recv(4096)
            if not data:
                raise ConnectionError("Koneksi ditutup oleh MyLA")
            buffer += data

            if b"\x1c\r" not in buffer:
                continue

            chunks = buffer.split(b"\x1c\r")
            for raw_msg in chunks[:-1]:
                clean_msg = raw_msg.replace(b"\x0b", b"")
                hl7_str = clean_msg.decode("latin-1", errors="ignore")
                if "MSH|" not in hl7_str:
                    continue
                hl7_str = hl7_str[hl7_str.find("MSH|"):]
                process_myla_hl7_message(conn, hl7_str, peer_ip)

            buffer = chunks[-1]
        except socket.timeout:
            break

    return buffer

def send_order_to_myla_hl7(conn, order, peer_ip):
    msg_control_id = f"MYLA{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}{order.urut}"
    hl7_message = create_myla_hl7_order_message(order, msg_control_id)
    mllp_payload = f"\x0b{hl7_message}\x1c\r".encode("utf-8")
    msh_line = hl7_message.split('\r')[0]
    print(f"[MYLA-HL7] Kirim rnoreg={order.rnoreg}, MSH={msh_line}")

    conn.sendall(mllp_payload)
    ack_ok = wait_for_myla_hl7_ack(
        conn,
        expected_control_id=msg_control_id,
        peer_ip=peer_ip,
        timeout_seconds=MYLA_ACK_TIMEOUT_SECONDS
    )
    return ack_ok
# ==========================================
# 4. NETWORK & COMMUNICATION LOGIC
# ==========================================
def handle_myla_client(conn, addr):
    """
    TCP Handler untuk bioMérieux MYLA.
    Hanya menerima HL7 berbungkus MLLP (\x0b ... \x1c\r).
    """
    print(f"[MYLA-TCP] Koneksi baru dari {addr}")
    buffer = b""
    
    try:
        while True:
            data = conn.recv(4096)
            if not data: break
            
            buffer += data
            
            # Cek penutup MLLP (\x1c\r)
            if b'\x1c\r' in buffer:
                # Pisahkan pesan jika ada beberapa pesan yang nempel
                messages = buffer.split(b'\x1c\r')
                
                for msg in messages[:-1]: # Abaikan yang terakhir (karena string kosong atau pesan belum selesai)
                    # Hapus pembuka MLLP (\x0b)
                    clean_msg_bytes = msg.replace(b'\x0b', b'')
                    hl7_str = clean_msg_bytes.decode('latin-1', errors='ignore')
                    
                    if "MSH|" in hl7_str:
                        # Potong tepat dari MSH
                        hl7_str = hl7_str[hl7_str.find("MSH|"):]
                        
                        # Ambil Control ID untuk ACK
                        incoming_control_id = ""
                        try:
                            msh_fields = hl7_str.split('\r')[0].split('|')
                            if len(msh_fields) > 9:
                                incoming_control_id = msh_fields[9]
                        except: pass

                        # --- PROSES HASIL (ORU / OUL^R22) ---
                        if "ORU^" in hl7_str or "OUL^R22" in hl7_str:
                            print(f"[MYLA] Menerima hasil (ORU/OUL) ID: {incoming_control_id}")
                            parse_myla_result(hl7_str, device_name=f"MYLA-{addr[0]}")
                        
                        # --- PROSES QUERY (QRY/QBP) - JIKA MYLA BERTANYA ORDER ---
                        elif "QRY^" in hl7_str or "QBP^" in hl7_str:
                            client_ip = addr[0]
                            print(f"[MYLA] Menerima Query dari {client_ip} (Control ID: {incoming_control_id})")
                            # Khusus MyLA gunakan flag VITEK3.
                            target_flag_col = "flg_vitek3"

                            lines = hl7_str.split('\r')
                            search_sample_id = None
                            qrd_line = ""

                            for line in lines:
                                if line.startswith("QRD|"):
                                    qrd_line = line
                                    fields = line.split('|')
                                    if len(fields) > 8 and fields[8]:
                                        search_sample_id = fields[8].strip()
                                    break

                            if not search_sample_id:
                                for line in lines:
                                    if line.startswith("QPD|"):
                                        qpd_fields = line.split('|')
                                        for candidate in qpd_fields[3:]:
                                            value = candidate.strip()
                                            if value:
                                                search_sample_id = value.split('^')[0].strip()
                                                break
                                        break

                            session = SessionLocal()
                            try:
                                flag_attr = getattr(PaslabOrder, target_flag_col, None)
                                if flag_attr is None:
                                    raise Exception(f"Kolom flag tidak valid: {target_flag_col}")

                                order = None
                                if search_sample_id:
                                    order = session.query(PaslabOrder).filter(
                                        PaslabOrder.rnoreg == search_sample_id,
                                        (flag_attr == False) | (flag_attr == None)
                                    ).first()
                                else:
                                    # Fallback: jika query tidak membawa sample ID, kirim order pending paling awal.
                                    order = session.query(PaslabOrder).filter(
                                        (flag_attr == False) | (flag_attr == None)
                                    ).order_by(PaslabOrder.urut.asc()).first()

                                if order:
                                    reply_msg = create_hl7_dsr_response(order, incoming_control_id, qrd_line)
                                    conn.sendall(f"\x0b{reply_msg}\x1c\r".encode('utf-8'))

                                    setattr(order, target_flag_col, True)
                                    session.commit()
                                    print(f"[MYLA] Order terkirim rnoreg={order.rnoreg}, set {target_flag_col}=TRUE")
                                else:
                                    reply_msg = create_hl7_dsr_response(None, incoming_control_id, qrd_line)
                                    conn.sendall(f"\x0b{reply_msg}\x1c\r".encode('utf-8'))
                                    print(f"[MYLA] Tidak ada order pending untuk sample={search_sample_id or '-'}")

                            except Exception as db_err:
                                session.rollback()
                                logging.error(f"[MYLA] Gagal proses query: {db_err}")
                                print(f"[MYLA] Gagal proses query: {db_err}")
                                reply_msg = create_hl7_dsr_response(None, incoming_control_id, qrd_line)
                                conn.sendall(f"\x0b{reply_msg}\x1c\r".encode('utf-8'))
                            finally:
                                session.close()
                            continue

                        # --- KIRIM ACK ---
                        if incoming_control_id:
                            ack_time = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
                            ack_msg = f"MSH|^~\\&|LIS|LAB|MYLA|bioMerieux|{ack_time}||ACK|{incoming_control_id}|P|2.5\rMSA|AA|{incoming_control_id}\r"
                            full_ack = f"\x0b{ack_msg}\x1c\r"
                            conn.sendall(full_ack.encode('utf-8'))
                            print(f"[MYLA ACK] Terkirim untuk ID {incoming_control_id}")

                # Sisakan bagian terakhir di buffer (kalau ada pesan yang terpotong)
                buffer = messages[-1]

    except Exception as e:
        logging.error(f"[MYLA-TCP] Error koneksi {addr}: {e}")
        print(f"[MYLA-TCP] Error koneksi {addr}: {e}")
    finally:
        conn.close()
        logging.info(f"[MYLA-TCP] Koneksi {addr} ditutup.")
        print(f"[MYLA-TCP] Koneksi {addr} ditutup.")

def start_myla_server(host, port):
    """
    Listener berperan sebagai TCP client:
    - Konek ke MYLA server
    - Poll order yang belum terkirim (flg_vitek3 = False/NULL)
    - Kirim order via HL7 MLLP
    - Tandai terkirim jika ACK diterima
    """
    while True:
        conn = None
        incoming_buffer = b""
        last_idle_log_at = 0.0
        try:
            print(f"[MYLA-CLIENT] Mencoba konek ke MYLA {host}:{port} ...")
            conn = socket.create_connection((host, port), timeout=10)
            conn.settimeout(1.0)
            print(f"[MYLA-CLIENT] Terhubung ke MYLA {host}:{port}")

            while True:
                incoming_buffer = pump_myla_incoming(conn, host, incoming_buffer, wait_seconds=0.15)
                pending_orders = get_pending_myla_orders(limit=10)
                if not pending_orders:
                    now_ts = time.time()
                    if (now_ts - last_idle_log_at) >= MYLA_IDLE_LOG_INTERVAL_SECONDS:
                        print(
                            "[MYLA-CLIENT] Polling aktif: tidak ada order pending "
                            "(flg_vitek3 = FALSE/NULL)."
                        )
                        last_idle_log_at = now_ts
                    time.sleep(MYLA_POLL_INTERVAL_SECONDS)
                    continue

                print(f"[MYLA-CLIENT] Ditemukan {len(pending_orders)} order pending untuk MYLA")
                last_idle_log_at = 0.0

                for order in pending_orders:
                    send_ok = send_order_to_myla_hl7(conn, order, host)
                    if send_ok:
                        mark_myla_order_sent(order.urut)
                        print(f"[MYLA-HL7] Order sukses, set flg_vitek3=TRUE rnoreg={order.rnoreg}")
                    else:
                        logging.warning(
                            f"[MYLA-HL7] Pengiriman gagal/ACK timeout untuk rnoreg={order.rnoreg}, "
                            f"akan dicoba ulang di polling berikutnya."
                        )
                        print(
                            f"[MYLA-HL7] Pengiriman gagal/ACK timeout untuk rnoreg={order.rnoreg}, "
                            f"akan dicoba ulang di polling berikutnya."
                        )
                        break

                time.sleep(1)

        except Exception as e:
            logging.error(f"[MYLA-CLIENT] Koneksi/pengiriman error ke {host}:{port}: {e}")
            print(f"[MYLA-CLIENT] Koneksi/pengiriman error ke {host}:{port}: {e}")
        finally:
            if conn:
                try:
                    conn.close()
                except Exception:
                    pass

        time.sleep(MYLA_CONNECT_RETRY_SECONDS)

def start_myla_inbound_server(host, port):
    """
    Listener TCP inbound untuk menerima hasil dari connector AI_to_LIS_MyLis (BCI/MyLA).
    """
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    try:
        server.bind((host, port))
        server.listen(5)
        print(f"[MYLA-INBOUND] Listening di {host}:{port}")

        while True:
            client_socket, addr = server.accept()
            client_thread = threading.Thread(
                target=handle_myla_client,
                args=(client_socket, addr),
                daemon=True
            )
            client_thread.start()
    except Exception as e:
        logging.critical(f"[MYLA-INBOUND] Gagal start listener {host}:{port}: {e}")
        print(f"[MYLA-INBOUND] Gagal start listener {host}:{port}: {e}")
    finally:
        try:
            server.close()
        except Exception:
            pass

# ==========================================
# HL7 TCP LISTENER FOR GENEXPERT
# ==========================================
def parse_genexpert_astm_records(astm_string, device_name):
    """
    Parser khusus untuk membaca hasil ASTM dari instrumen GeneXpert 
    dan menyimpannya ke tabel LisPhoenix dengan aman (mencegah VARCHAR limit error).
    """
    try:
        records = astm_string.split('\r')
        no_id = ""
        rnmpas = ""
        seq_no = ""
        hasil_list = []

        for rec in records:
            # 1. Ambil Data Pasien (P Record)
            if rec.startswith("P|"):
                parts = rec.split('|')
                if len(parts) > 3:
                    # Ambil Patient ID (Bisa di index 3 atau 4 tergantung setting alat)
                    no_id = parts[3].strip() or (parts[4].strip() if len(parts) > 4 else "")
                if len(parts) > 5:
                    # Ambil Nama Pasien, ganti ^ dengan spasi
                    rnmpas = parts[5].replace('^', ' ').strip()
                    
            # 2. Ambil Nomor Order / Registrasi (O Record)
            elif rec.startswith("O|"):
                parts = rec.split('|')
                if len(parts) > 2:
                    seq_no = parts[2].strip()

            # 3. Ambil Hasil Tes (R Record)
            elif rec.startswith("R|"):
                parts = rec.split('|')
                if len(parts) > 3:
                    test_info = parts[2] # Contoh: ^^^MTB-RIF_ULTRA 2^^^MTB^
                    result_val = parts[3].replace('^', '').strip() # Contoh: DETECTED atau INVALID

                    # [KUNCI]: Abaikan kurva analitik agar teks tidak kepanjangan
                    if "Ct|" not in rec and "EndPt|" not in rec and result_val:
                        # Ekstrak nama targetnya saja (misal "MTB" atau "RIF Resistance")
                        target_match = test_info.split('^^^')
                        target_name = target_match[-1].strip('^') if len(target_match) > 1 else ""

                        if target_name and result_val:
                            hasil_list.append(f"{target_name}: {result_val}")

        # Gabungkan hasil-hasil penting menjadi 1 string
        kesimpulan = " | ".join(hasil_list)

        # [PATCH KRITIS]: Potong string agar TIDAK CRASH di database
        no_id_safe = no_id[:50]
        seq_no_safe = seq_no[:50]
        rnmpas_safe = rnmpas[:100]
        kesimpulan_safe = kesimpulan[:100] # Membatasi maksimal 100 karakter untuk kolom 'organisme'

        if not seq_no_safe:
            print("[GENEXPERT-PARSER] Warning: seq_no (No Order) tidak ditemukan dalam pesan hasil.")
            return

        # Simpan ke Database
        with SessionLocal() as session:
            new_result = LisPhoenix(
                no_id=no_id_safe,
                seq_no=seq_no_safe,
                rnmpas=rnmpas_safe,
                tgl_data=datetime.datetime.now().date(),
                rawdt=astm_string, # Simpan data mentahnya (utuh) ke TEXT untuk jaga-jaga/bisa dibaca ulang
                organisme=kesimpulan_safe, # Hasil yang sudah bersih dan muat
                alat=device_name,
            )
            session.add(new_result)
            session.commit()
            print(f"[GENEXPERT-DB-SUCCESS] Hasil lab untuk Order {seq_no_safe} berhasil disimpan ke LisPhoenix!")

    except Exception as e:
        print(f"[GENEXPERT-PARSER-ERROR] Gagal memparsing/menyimpan hasil: {e}")
        import traceback
        traceback.print_exc()

def manage_tcp_server():
    """Thread Server Utama untuk GeneXpert"""
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # Allow reuse address agar tidak error 'Address already in use' saat restart
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    try:
        server.bind((SERVER_HOST, TCP_LISTENER_PORT))
        server.listen(5) # Bisa antri 5 koneksi
        print(f"[TCP-SERVER] Listening GeneXpert di port {TCP_LISTENER_PORT}...")
        while True:
            # Accept koneksi baru (Blocking, tapi aman karena di thread sendiri)
            client_sock, addr = server.accept()
            
            # Buat thread kecil untuk handle client tersebut (agar server bisa terima client lain)
            client_thread = threading.Thread(
                target=handle_genexpert_client,
                args=(client_sock, addr),
                daemon=True
            )
            client_thread.start()
            
    except Exception as e:
        logging.critical(f"[TCP-SERVER] Gagal Start: {e}")
        print(f"[TCP-SERVER] Gagal Start: {e}")

def run_http_api_server():
    print(f"[HTTP-API] Listening di port {HTTP_API_PORT}...")
    app.run(host=SERVER_HOST, port=HTTP_API_PORT, debug=False, use_reloader=False, threaded=True)

def process_genexpert_hl7_message(conn, ip_addr, clean_hl7, response_framing):
    # ==========================================================
    # 1. BLOK PENANGANAN ASTM (Karena tidak diawali "MSH|")
    # ==========================================================
    if not str(clean_hl7 or "").startswith("MSH|"):
        records = parse_astm_records(clean_hl7)
        record_types = [rec.split("|", 1)[0] for rec in records if rec]
        print(f"[GENEXPERT-ASTM] ip={ip_addr}, record_types={record_types}")

        # A. Cek Jika Alat Meminta Order (Query)
        if any(rec.startswith("Q|") for rec in records):
            print(f"[GENEXPERT-ASTM] Alat meminta ORDER (Q Record)")
            send_all_orders_astm(conn, ip_addr, clean_hl7, response_framing="astm")
            return

        # B. Cek Jika Alat Mengirim Hasil Lab (Result)
        if any(rec.startswith("R|") for rec in records):
            print(f"[RESULT] Menerima Hasil Lab ASTM dari {ip_addr}.")
            # Memanggil fungsi parser Anda untuk menyimpan hasil ke DB
            parse_genexpert_astm_records(clean_hl7, device_name=f"GeneXpert-{ip_addr}")
            return

        # C. [PERBAIKAN] Cek Jika Alat Mengirim Komentar/Penolakan (Comment)
        if any(rec.startswith("C|") for rec in records):
            # 1. Ekstrak teks komentar untuk ditampilkan di log
            comments = [rec for rec in records if rec.startswith("C|")]
            for c in comments:
                parts = c.split('|')
                comment_text = parts[3] if len(parts) > 3 else c
                print(f"[GENEXPERT-ASTM-INFO] Komentar dari Alat: {comment_text}")
            
            # 2. Ekstrak NoReg (Nomor Order) dari record 'O'
            rnoreg = None
            for rec in records:
                if rec.startswith("O|"):
                    o_parts = rec.split('|')
                    if len(o_parts) > 2:
                        rnoreg = o_parts[2].strip()
                        break
            
            # 3. Update Database PaslabOrder
            if rnoreg:
                # Cari kolom flag yang cocok dengan IP yang sedang terkoneksi
                target_flag_col = None
                for flag_col, mapped_ip in TARGET_MAPPING.items():
                    if mapped_ip == ip_addr:
                        target_flag_col = flag_col
                        break
                
                if target_flag_col:
                    try:
                        # Buka sesi database dan update
                        with SessionLocal() as session:
                            order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == rnoreg).first()
                            if order:
                                # Set flag mesin tersebut menjadi True agar tidak dikirim ulang
                                setattr(order, target_flag_col, True)
                                session.commit()
                                print(f"[GENEXPERT-DB] Order {rnoreg} ditolak alat. Flag {target_flag_col} di-set True (Selesai).")
                            else:
                                print(f"[GENEXPERT-DB] Order {rnoreg} tidak ditemukan di database saat memproses penolakan.")
                    except Exception as e:
                        print(f"[GENEXPERT-DB-ERROR] Gagal update order duplikat {rnoreg}: {e}")

            print(f"[GENEXPERT-ASTM] Transaksi penolakan order selesai diproses.")
            return

        # D. Jika hanya berisi H dan L tanpa ada transaksi berarti (Status Echo)
        if set(record_types).issubset({'H', 'L'}):
            print(f"[GENEXPERT-ASTM] Menerima Heartbeat / Sesi Kosong dari alat.")
            return

        print(f"[GENEXPERT-ASTM] Pesan ASTM tidak dikenali dari {ip_addr}. Isi: {clean_hl7[:50]}")
        return

    # ==========================================================
    # 2. BLOK PENANGANAN HL7 (Fallback / Cadangan)
    # ==========================================================
    log_genexpert_hl7("IN", ip_addr, clean_hl7)
    lines = clean_hl7.split('\r')
    msh_fields = lines[0].split('|')
    incoming_control_id = msh_fields[9] if len(msh_fields) > 9 else "UNKNOWN"
    msg_id = incoming_control_id

    if "QRY^" in clean_hl7 or "QRY|" in clean_hl7:
        print(
            f"[GENEXPERT] Legacy QRY diterima dari {ip_addr} tetapi diabaikan. "
            "Host hanya mendukung QBP^Z01/QBP^Z03 untuk order query."
        )
        return

    if "ORU^" in clean_hl7:
        # --- [PATCH] CEK APAKAH INI PESAN ERROR / PENOLAKAN ---
        if "|Error^" in clean_hl7 or "|X\r" in clean_hl7 or "\rTE|" in clean_hl7:
            print(f"[GENEXPERT-ERROR] Mesin {ip_addr} menolak tes (Test Unknown/Disabled).")
            
            # Coba ekstrak NoReg dari segmen SPM agar kita bisa mengunci ordernya (set Flag = True)
            rnoreg_error = None
            for line in clean_hl7.split('\r'):
                if line.startswith('SPM|'):
                    parts = line.split('|')
                    if len(parts) > 2:
                        rnoreg_error = parts[2].replace('^', '').strip()
                        break
            
            if rnoreg_error:
                print(f"[GENEXPERT-ERROR] Mengunci/Membatalkan Order {rnoreg_error} agar tidak terjadi Infinite Loop.")
                # (OPSIONAL: Jalankan fungsi update ke DB untuk mengubah flag PaslabOrder menjadi True di sini)
                # ...
                
            # Kirim ACK agar alat berhenti mengirim error
            ack_msg = create_genexpert_ack_r01_response(clean_hl7, ip_addr=ip_addr)
            send_genexpert_response(conn, ip_addr, ack_msg, response_framing, label="oru-ack")
            return # BERHENTI DI SINI. Jangan lanjut ke parse_hl7_result!
        # -----------------------------------------------------

        print(f"[RESULT] Menerima Hasil Lab.")
        parse_hl7_result(conn, msg_id, clean_hl7, device_name=f"GeneXpert-{ip_addr}")
        ack_msg = create_genexpert_ack_r01_response(clean_hl7, ip_addr=ip_addr)
        send_genexpert_response(conn, ip_addr, ack_msg, response_framing, label="oru-ack")
        print(f"[ACK SENT] Untuk hasil ID {incoming_control_id}")
        return

    if "QBP^Z01" in clean_hl7 or "QBP^Z03" in clean_hl7:
        print("[GENEXPERT] Alat meminta ORDER")
        msg_id = extract_msg_control_id(clean_hl7)
        send_all_orders(conn, ip_addr, clean_hl7, msg_id, response_framing=response_framing)
        return

    if "QCN^J01" in clean_hl7:
        clear_genexpert_inflight_for_ip(ip_addr, reason="query-confirmation")
        ack_msg = create_genexpert_ack_j01_response(clean_hl7, ip_addr=ip_addr)
        send_genexpert_response(conn, ip_addr, ack_msg, response_framing, label="qcn-ack")
        print(f"[GENEXPERT] Menerima konfirmasi query dari {ip_addr}.")
        return

    print(f"[GenExpert_TCP] Pesan Lengkap Diterima: {clean_hl7[:50]}...")
    parse_hl7_result(conn, msg_id, clean_hl7, device_name=f"GeneXpert-{ip_addr}, ")

    try:
        if len(msh_fields) > 9:
            msg_control_id = msh_fields[9]
            ack_time = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
            ack_msg = f"MSH|^~\\&|LIS|LAB|GeneXpert|Cepheid|{ack_time}||ACK|{msg_control_id}|P|2.5\rMSA|AA|{msg_control_id}\r"
            full_ack = f"\x0b{ack_msg}\x1c\r"
            log_genexpert_hl7("OUT", ip_addr, ack_msg, label="generic-ack")
            conn.sendall(full_ack.encode('utf-8'))
            print(f"[ACK] Terkirim untuk ID {msg_control_id}")
    except Exception as e:
        print(f"Gagal kirim ACK: {e}")

def handle_genexpert_client(conn, addr):
    print(f"[GenExpert_TCP] Koneksi baru dari {addr}")
    buffer = b""
    pending_astm_hl7 = None
    pending_astm_framing = None
    conn.settimeout(60)
    client_ip = addr[0]
    with connection_lock:
        active_genexpert_connections[client_ip] = conn
    print(f"[GenExpert_TCP] Register koneksi aktif {client_ip}")
    
    try:
        while True:
            try:
                data = conn.recv(4096)
                if not data:
                    if pending_astm_hl7:
                        log_genexpert_handshake(addr[0], "ASTM-MSG-PROCESS", detail="reason=connection-close")
                        process_genexpert_hl7_message(conn, addr[0], pending_astm_hl7, pending_astm_framing or "astm")
                        pending_astm_hl7 = None
                        pending_astm_framing = None
                    print(f"[GenExpert_TCP] Client {addr} menutup koneksi.")
                    break
                
                buffer += data
                if b"\x02" in data:
                    log_genexpert_handshake(addr[0], "STX-RX", detail=f"bytes={len(data)}")
                if b"\x03" in data:
                    log_genexpert_handshake(addr[0], "ETX-RX", detail=f"bytes={len(data)}")
                if b"\x04" in data:
                    log_genexpert_handshake(addr[0], "EOT-RX", detail=f"bytes={len(data)}")
                if b"\x06" in data:
                    log_genexpert_handshake(addr[0], "ACK-RX", detail=f"bytes={len(data)}")
                if b"\x15" in data:
                    log_genexpert_handshake(addr[0], "NAK-RX", detail=f"bytes={len(data)}")
                
                # --- 1. HANDLE HANDSHAKE (ENQ) ---
                # Jika alat kirim ENQ (\x05/♣), langsung balas ACK (\x06)
                if b'\x05' in buffer:
                    log_genexpert_handshake(addr[0], "ENQ-RX", detail=f"buffer_len={len(buffer)}")
                    conn.sendall(b'\x06')
                    log_genexpert_handshake(addr[0], "ACK-TX", detail="reason=enq")
                    
                    # [PERBAIKAN KURSIS 2]: KOSONGKAN TOTAL BUFFER SAAT ENQ!
                    # Alat meminta sesi baru, pastikan tidak ada sisa pesan lama yang nyangkut
                    buffer = b""
                    pending_astm_hl7 = "" 
                    continue # Langsung lanjut ke recv() berikutnya
                if b'\x15' in buffer:
                    log_genexpert_handshake(addr[0], "NAK-BUFFER-CLEAR", detail=f"buffer_len={len(buffer)}")
                    buffer = buffer.replace(b'\x15', b'')
                if b'\x06' in buffer:
                    log_genexpert_handshake(addr[0], "ACK-BUFFER-CLEAR", detail=f"buffer_len={len(buffer)}")
                    buffer = buffer.replace(b'\x06', b'')
                
                # --- 2. CEK APAKAH PESAN SUDAH LENGKAP? ---
                # Kita cari tanda akhir pesan umum:
                # - \x1c (End Block MLLP)
                # - \x03 (ETX - End Text ASTM)
                # - \x04 (EOT - End Transmission ASTM)
                
                msg_complete = False
                end_marker_pos = -1
                
                if b'\x1c' in buffer: # Pola MLLP Standard
                    end_marker_pos = buffer.find(b'\x1c')
                    msg_complete = True
                elif b'\x03' in buffer or b'\x17' in buffer: 
                    # Cari di index mana letak ETX atau ETB
                    pos_etx = buffer.find(b'\x03')
                    pos_etb = buffer.find(b'\x17')
                    
                    # Tentukan mana yang muncul lebih dulu di buffer
                    pos = -1
                    if pos_etx != -1 and pos_etb != -1:
                        pos = min(pos_etx, pos_etb)
                    else:
                        pos = max(pos_etx, pos_etb)
                    
                    # Pastikan kita menerima 5 bytes penuh (ETB/ETX + C1 + C2 + CR + LF)
                    if pos != -1 and len(buffer) >= pos + 5:
                        end_marker_pos = pos + 5
                        msg_complete = True
                        
                elif b'\x04' in buffer: # Pola EOT (Putus Koneksi/Selesai)
                    end_marker_pos = buffer.find(b'\x04')
                    msg_complete = True

                # --- 3. PROSES JIKA LENGKAP ---
                if msg_complete:
                    if end_marker_pos == 0 and buffer[:1] == b'\x04':
                        log_genexpert_handshake(addr[0], "EOT-CLEAR", detail="standalone-eot")
                        buffer = buffer[1:].lstrip(b'\r').lstrip(b'\n')
                        if pending_astm_hl7:
                            log_genexpert_handshake(addr[0], "ASTM-MSG-PROCESS", detail=f"framing={pending_astm_framing}")
                            process_genexpert_hl7_message(conn, addr[0], pending_astm_hl7, pending_astm_framing or "astm")
                            pending_astm_hl7 = None
                            pending_astm_framing = None
                        continue

                    # Ambil pesan dari awal sampai marker
                    # (Gunakan slice sampai end_marker_pos+1 agar karakter penutup ikut terambil/dibuang)
                    if end_marker_pos == -1: end_marker_pos = len(buffer)
                    
                    full_message_bytes = buffer[:end_marker_pos]
                    response_framing = detect_genexpert_message_framing(full_message_bytes)
                    if response_framing == "astm":
                        debug_genexpert_astm_frame(addr[0], full_message_bytes, direction="RX")
                    log_genexpert_handshake(
                        addr[0],
                        "FRAME-COMPLETE",
                        detail=f"framing={response_framing}, frame_len={len(full_message_bytes)}"
                    )

                    send_genexpert_transport_ack(
                        conn,
                        addr[0],
                        response_framing,
                        reason="incoming-frame-complete"
                    )
                    
                    # Sisa buffer (jika ada paket nempel di belakangnya) disimpan untuk loop berikutnya
                    buffer = buffer[end_marker_pos:]
                    
                    # Jangan buang EOT di sini; jika EOT datang menempel setelah frame ASTM,
                    # ia harus diproses pada iterasi berikutnya agar pending ASTM message dijalankan.
                    buffer = buffer.lstrip(b'\r').lstrip(b'\n')

                    # Decode ke string
                    temp_str = full_message_bytes.decode('latin-1', errors='ignore')
                    astm_text = extract_astm_frame_text(full_message_bytes) if response_framing == "astm" else ""

                    # --- SANITIZING (PEMBERSIHAN) ---
                    # Cari MSH pertama
                    if "MSH|" in temp_str:
                        msh_index = temp_str.find("MSH|")
                        clean_hl7 = temp_str[msh_index:]
                        if response_framing == "astm":
                            pending_astm_hl7 = clean_hl7
                            pending_astm_framing = response_framing
                            log_genexpert_handshake(addr[0], "ASTM-MSG-STORED", detail=f"len={len(clean_hl7)}")
                            continue
                        process_genexpert_hl7_message(conn, addr[0], clean_hl7, response_framing)
                    elif response_framing == "astm" and astm_text:
                        pending_astm_hl7 = (pending_astm_hl7 or "") + astm_text
                        pending_astm_framing = response_framing
                        log_genexpert_handshake(addr[0], "ASTM-MSG-STORED", detail=f"len={len(pending_astm_hl7)}, mode=records")
                        continue
                    else:
                        # Jika pesan lengkap tapi tidak ada MSH (misal cuma EOT doang)
                        pass
                else:
                    if buffer:
                        head_hex = buffer[:12].hex()
                        log_genexpert_handshake(
                            addr[0],
                            "BUFFER-WAIT",
                            detail=f"buffer_len={len(buffer)}, head_hex={head_hex}"
                        )

            except ConnectionResetError:
                logging.warning(f"[GenExpert_TCP] Connection reset by peer: {addr}")
                break
            except OSError as e:
                if getattr(e, "winerror", None) == 10054:
                    logging.warning(f"[GenExpert_TCP] WinError 10054 dari {addr}")
                    break
                raise
            except socket.timeout:
                continue
            except Exception as e:
                print(f"[Loop Error] {e}")
                logging.exception(f"[Loop Error] Unexpected error from {addr}: {e}")
                break

    except Exception as e:
        logging.error(f"[GenExpert_TCP Error] Koneksi {addr} terputus: {e}")
    finally:
        with connection_lock:
            if active_genexpert_connections.get(client_ip) is conn:
                del active_genexpert_connections[client_ip]
            remaining_connections = len(active_genexpert_connections)
        clear_genexpert_inflight_for_ip(client_ip, reason="connection-closed")
        if remaining_connections == 0:
            stop_all_scheduled_result_queries(reason="no-active-genexpert")
        try:
            conn.close()
        except Exception:
            pass
        logging.info(f"[GenExpert_TCP] Koneksi {addr} ditutup.")

def send_order_via_active_connection(target_ip, hl7_message):
    conn = None
    with connection_lock:
        conn = active_genexpert_connections.get(target_ip)
    
    if not conn:
        logging.warning(f"Gagal kirim Order: GeneXpert dengan IP {target_ip} BELUM TERKONEKSI ke Listener.")
        print(f"Gagal kirim Order: GeneXpert dengan IP {target_ip} BELUM TERKONEKSI ke Listener.")
        return False

    try:
        # Bungkus pesan dengan MLLP (Minimal Lower Layer Protocol) standard HL7
        # Format: <VT> message <FS><CR>
        mllp_msg = f"\x0b{hl7_message}\x1c\r"
        
        logging.info(f"Mengirim Order ke {target_ip}...")
        print(f"Mengirim Order ke {target_ip}...")
        conn.sendall(mllp_msg.encode('utf-8'))
        
        # Opsi: Jika ingin menunggu ACK balasan untuk Order
        # Namun hati-hati ini bisa blocking jika alat lambat
        ack = conn.recv(1024) 
        logging.info(f"Dapat ACK Order dari {target_ip}: {ack}")
        print(f"Dapat ACK Order dari {target_ip}: {ack}")
        return True
    except Exception as e:
        logging.error(f"Error mengirim ke {target_ip}: {e}")
        print(f"Error mengirim ke {target_ip}: {e}")
        # Jika error saat kirim, anggap koneksi rusak
        with connection_lock:
            if target_ip in active_genexpert_connections:
                del active_genexpert_connections[target_ip]
        return False
    
# ==========================================
# VITEK PARSER
# ==========================================

def calculate_vitek_checksum(data_str):
    """
    Menghitung Checksum Vitek (Sum of bytes % 256).
    """
    total = sum(ord(c) for c in data_str)
    return f"{total % 256:02X}"

def parse_and_save_vitek_result(raw_data, port_name="VITEK"):
    session = SessionLocal()
    try:
        # --- 1. CLEANING DATA ---
        # Hapus karakter kontrol STX(02), ETX(03), RS(1e/30), GS(1d/29), CR, LF
        # Perhatikan: Log Anda menunjukkan RS () muncul di tengah kata, jadi harus dihapus total.
        clean_data = raw_data.replace('\x02', '').replace('\x03', '').replace('\x1e', '').replace('\x1d', '').replace('\r', '').replace('\n', '')
        
        # Pisahkan field berdasarkan pipa '|'
        fields = clean_data.split('|')
        
        # Cek apakah ini pesan result (mtrsl)
        if not fields or fields[0] != 'mtrsl':
            return # Abaikan jika bukan result

        # --- 2. VARIABLE INIT ---
        sample_id = None    # ci (No Lab / No Container)
        patient_id = ""     # pi (No RM)
        patient_name = ""   # pn
        organism_name = ""  # o2
        card_barcode = ""  # is
        antibiotics = []    # List penampung hasil AB
        result_date = datetime.datetime.now()
        
        # Variabel sementara untuk looping antibiotik
        curr_ab_name = ""
        curr_ab_mic = ""
        curr_ab_int = ""

        # --- 3. PARSING LOOP ---
        for field in fields:
            if not field: continue
            
            # --- HEADER INFO ---
            if field.startswith("ci") and len(field) > 2:
                sample_id = field[2:].strip()
            elif field.startswith("pi") and len(field) > 2:
                patient_id = field[2:].strip()
            elif field.startswith("pn") and len(field) > 2:
                patient_name = field[2:].strip()
            elif field.startswith("is") and len(field) > 2:
                card_barcode = field[2:].strip()
            # --- ORGANISME ---
            elif field.startswith("o2") and len(field) > 2:
                organism_name = field[2:].strip()

            # --- ANTIBIOTIK BLOCK (Mulai Pesan Kedua) ---
            # Tag 'ra' adalah pemisah antar obat
            elif field == "ra":
                # Jika ada data obat sebelumnya di memori, simpan dulu
                if curr_ab_name:
                    ab_str = f"{curr_ab_name} {curr_ab_mic} ({curr_ab_int})"
                    antibiotics.append(ab_str)
                # Reset untuk obat berikutnya
                curr_ab_name = ""; curr_ab_mic = ""; curr_ab_int = ""
            
            # Detail Obat
            elif field.startswith("a2") and len(field) > 2: # Nama Obat (mis: Cefoxitin)
                curr_ab_name = field[2:].strip()
            elif field.startswith("a3") and len(field) > 2: # MIC (mis: >=4)
                curr_ab_mic = field[2:].strip()
            elif field.startswith("a4") and len(field) > 2: # Interpretasi (R/S/I)
                curr_ab_int = field[2:].strip()
            elif field.startswith("an") and len(field) > 2: # Interpretasi Alternatif
                if not curr_ab_int: curr_ab_int = field[2:].strip()

        # Jangan lupa simpan obat terakhir yang tersisa di buffer
        if curr_ab_name:
            ab_str = f"{curr_ab_name} {curr_ab_mic} ({curr_ab_int})"
            antibiotics.append(ab_str)

        # --- 4. FORMAT FINAL STRING ---
        # Format: "Staphylococcus hominis | Cefoxitin >=4 (R), Gentamicin 4 (S)..."
        final_res_string = organism_name
        #if antibiotics:
        #    final_res_string += " | " + ", ".join(antibiotics)
        
        # Fallback jika negatif (biasanya tidak ada o2, tapi ada teks neg)
        #if not final_res_string and "neg" in raw_data.lower():
        #    final_res_string = "NEGATIVE / NO GROWTH"

        # --- 5. LOGIKA DATABASE (UPSERT: UPDATE or INSERT) ---
        if sample_id:
            # Cari data berdasarkan No Lab (Sample ID)
            final_seq_no = card_barcode if card_barcode else patient_id
            existing_data = session.query(LisPhoenix).filter(
                LisPhoenix.seq_no == final_seq_no
            ).first()

            if existing_data:
                # === SKENARIO PESAN KEDUA (UPDATE) ===
                print(f"[{port_name}] UPDATE Data -> ID: {sample_id} (Hasil Lengkap)")                
                new_entry = LisPhoenix(
                    no_id=sample_id,
                    seq_no=final_seq_no,
                    rnmpas=patient_name,
                    tgl_data=result_date,
                    rawdt=raw_data, 
                    organisme=final_res_string,
                    alat=port_name
                )
                session.add(new_entry)
                
            else:
                # === SKENARIO PESAN PERTAMA (INSERT) ===
                print(f"[{port_name}] INSERT Data -> ID: {sample_id} (Identifikasi Awal)")
                
                new_entry = LisPhoenix(
                    no_id=sample_id,
                    seq_no=final_seq_no,
                    rnmpas=patient_name,
                    tgl_data=result_date,
                    rawdt=raw_data, 
                    organisme=final_res_string,
                    alat=port_name
                )
                session.add(new_entry)
            
            session.commit()
        else:
            logging.warning(f"[{port_name}] Pesan diabaikan (Tanpa Sample ID): {clean_data[:30]}...")

    except Exception as e:
        logging.error(f"Error Parsing Vitek: {e}")
        print(f"Error Parsing Vitek: {e}")
        session.rollback()
    finally:
        session.close()

def split_patient_name(full_name):
    """
    Return: last_name, first_name
    ASTM format: LAST^FIRST
    """

    if not full_name:
        return "NAME", "NO"

    raw = str(full_name).strip()

    if not raw:
        return "NAME", "NO"

    first_name = ""
    last_name = ""

    # Hapus karakter ASTM berbahaya
    raw = (
        raw.replace("\x00", "")
           .replace("\r", " ")
           .replace("\n", " ")
           .replace("|", " ")
           .replace("&", " ")
    )

    if "^" in raw:
        parts = [p.strip() for p in raw.split("^")]

        last_name = parts[0] if len(parts) > 0 and parts[0] else "NAME"
        first_name = parts[1] if len(parts) > 1 and parts[1] else "NO"

    else:
        tokens = raw.split()

        if len(tokens) >= 2:
            first_name = " ".join(tokens[:-1])
            last_name = tokens[-1]
        elif len(tokens) == 1:
            first_name = tokens[0]
            last_name = "NAME"

    first_name = first_name.upper()[:20]
    last_name = last_name.upper()[:20]

    return last_name, first_name

def sanitize_astm_field(value, *, uppercase=False, max_len=None, allow_component_sep=False):
    """
    Sanitasi field agar aman untuk ASTM:
    - Buang karakter NULL/control (termasuk CR/LF)
    - Hilangkan delimiter ASTM pada field biasa
    - Trim dan optional uppercase/truncate
    """
    if value is None:
        text = ""
    else:
        text = str(value)

    # Hilangkan NULL byte yang sering muncul dari CHAR/VARCHAR bermasalah.
    text = text.replace("\x00", "")
    # Buang karakter kontrol non-printable.
    text = re.sub(r"[\x00-\x1f\x7f]", " ", text)

    if allow_component_sep:
        text = text.replace("|", " ")
    else:
        text = text.replace("|", " ").replace("^", " ")

    text = re.sub(r"\s+", " ", text).strip()

    # ASTM payload dikirim dengan latin-1; karakter di luar rentang ini diganti aman.
    text = text.encode("latin-1", errors="replace").decode("latin-1")

    if uppercase:
        text = text.upper()
    if max_len is not None:
        text = text[:max_len]
    return text

def create_vitek_order_message(order):
    """
    Membuat Frame Order Vitek sesuai Manual Ref 514937.
    Format: <STX> <RS> [DATA] <GS> [CS] <ETX> <CR><LF>
    """
    # --- 1. ISI PESAN (CONTENT) ---
    # Field Delimiter menggunakan Pipe '|'
    pid = str(order.norm).strip() if order.norm else ""
    sid = str(order.rnoreg).strip() if order.rnoreg else ""
    first_name, last_name = split_patient_name(order.nama)
    p_name = f"{last_name}^{first_name}"
    specimen = str(order.kd_spesimen).upper() if order.kd_spesimen else "BLOOD"
    room = str(getattr(order, 'ruangan', "") or "RSSA MALANG").strip().upper()
    # VITEK: Patient Location Code menggunakan tag "pl" (maks 40 char pada spec yang dipakai)
    room = room.replace("|", " ").replace("^", " ")[:40]
    
    now = datetime.datetime.now()
    date_str = now.strftime("%m/%d/%Y") 
    time_str = now.strftime("%H:%M")    

    # Struktur mtmpr sesuai Table 1-3 & Contoh Manual
    # Penting: Tidak ada Sequence Number '1' di dalam data
    content_body = (
        f"mtmpr|pi{pid}|pn{p_name}"
        f"|si|ss{specimen}"
        f"|pl{room}"
        f"|s1{date_str}|s2{time_str}"
        f"|ci{sid}|t11|zz"
    )

    # --- 2. FRAMING & CHECKSUM (Section 2.2 & 2.5) ---
    # Frame dimulai dengan STX, lalu Record dimulai dengan RS
    STX = b'\x02'
    RS  = b'\x1e'
    GS  = b'\x1d'
    ETX = b'\x03'
    CRLF = b'\r\n'

    # Data yang dihitung Checksumnya: [RS] + [Body] + [GS]
    # Manual Hal 2-10: "calculated by adding the value of all characters beginning with the first <RS>... and ending with the <GS>"
    payload_for_checksum = RS + content_body.encode('latin-1') + GS
    
    # Hitung Checksum
    total_sum = sum(payload_for_checksum)
    chk_val = total_sum % 256
    checksum_str = f"{chk_val:02X}".encode('latin-1') # Hex 2 digit uppercase
    
    # Rakit Frame Utuh
    # <STX> [PAYLOAD+GS] [CHECKSUM] <ETX> <CR><LF>
    # Perhatikan: Payload di atas sudah mengandung RS dan GS
    full_frame = STX + payload_for_checksum + checksum_str + ETX + CRLF
    
    return [full_frame]

def manage_vitek_port(config):
    port_name   = config['port']
    flag_col    = config.get('flag_column')
    alat_name   = config.get('alat_name', 'VITEK')
    
    print(f"[{port_name}] START VITEK SERVICE (Relaxed Mode)...")
    STUCK_WINDOW_SEC = 120
    stuck_count = 0
    stuck_window_start = None
    while True: 
        try:
            with serial.Serial(
                port=port_name,
                baudrate=config['baud_rate'],
                timeout=2, 
                bytesize=serial.EIGHTBITS,
                parity=serial.PARITY_NONE,
                stopbits=serial.STOPBITS_ONE,
                xonxoff=False,
                rtscts=False,
                dsrdtr=False
            ) as ser:
                
                ser.reset_input_buffer()
                print(f"[{port_name}] Ready & Listening.")

                while True:
                    # ==========================================
                    # PHASE 1: LISTENING
                    # ==========================================
                    if ser.in_waiting > 0:
                        header = ser.read(1)
                        
                        # --- HANDSHAKE ---
                        if header == b'\x05': 
                            print(f"[{port_name}] Got ENQ -> Reply ACK")
                            ser.write(b'\x06') 
                            ser.reset_input_buffer()

                        # --- DATA FRAME ---
                        elif header == b'\x02': 
                            print(f"[{port_name}] Frame Start. Reading...")
                            original_timeout = ser.timeout
                            ser.timeout = 8
                            body = ser.read_until(b'\x03')
                            ser.timeout = original_timeout
                            
                            full_frame = header + body
                            
                            
                            is_valid = False
                            
                            if full_frame.endswith(b'\x03'):
                                is_valid = True
                            elif b'\x1d' in full_frame[-20:]: 
                                is_valid = True
                                print(f"[{port_name}] Frame tanpa ETX tapi ada Checksum. Menerima paksa...")
                            if is_valid:
                                print(f"[{port_name}] Frame OK -> ACK Sent.")
                                # 1. KIRIM ACK (WAJIB)
                                ser.write(b'\x06')
                                stuck_count = 0
                                stuck_window_start = None
                                
                                # 2. Proses Data
                                try:
                                    full_str = full_frame.decode('latin-1', errors='ignore')
                                    # Debug
                                    # print(f"[{port_name}] CONTENT: {full_str}")
                                    parse_and_save_vitek_result(full_str, alat_name)
                                except Exception as e:
                                    logging.error(f"[{port_name}] Parse Err: {e}")
                                    print(f"[{port_name}] Parse Err: {e}")
                            else:
                                logging.warning(f"[{port_name}] Frame Corrupt/Timeout: {full_frame}")
                                print(f"[{port_name}] Frame Corrupt/Timeout: {full_frame}")
                                ser.write(b'\x15') # NAK

                        # --- EOT ---
                        elif header == b'\x04':
                            logging.info(f"[{port_name}] Session End (EOT).")
                            print(f"[{port_name}] Session End (EOT).")
                            ser.reset_input_buffer()
                            stuck_count = 0
                            stuck_window_start = None
                        
                        else:
                            pass

                    # ==========================================
                    # PHASE 2: SENDING ORDER (JIKA IDLE)
                    # ==========================================
                    else:
                        # Kita masuk sini jika ser.in_waiting == 0 (Sepi)
                        # Pastikan kolom flag diset di config
                        if flag_col:
                            session = None
                            try:
                                session = SessionLocal()
                                # Cari order yang belum dikirim
                                pending_order = session.query(PaslabOrder).filter(
                                    getattr(PaslabOrder, flag_col) == False
                                ).first()
                                
                                if pending_order:
                                    print(f"[{port_name}] Ada Order: {pending_order.rnoreg}...")

                                    # --- LOGIC HANDSHAKE DENGAN RETRY ---
                                    handshake_success = False
                                    
                                    # Coba kirim ENQ max 3 kali
                                    for attempt in range(3):
                                        ser.reset_input_buffer()
                                        ser.write(b'\x05') # Kirim ENQ
                                        time.sleep(0.5)    # Tunggu balasan
                                        
                                        if ser.in_waiting:
                                            resp = ser.read(1)
                                            if resp == b'\x06': # Dapat ACK
                                                handshake_success = True
                                                break
                                            elif resp == b'\x15': # Dapat NAK
                                                time.sleep(1)
                                        else:
                                            # Timeout, alat diam saja
                                            pass
                                    
                                    if handshake_success:
                                        stuck_count = 0
                                        stuck_window_start = None
                                        # === KIRIM DATA ORDER ===
                                        print(f"[{port_name}] Handshake OK. Kirim Frames...")
                                        frames = create_vitek_order_message(pending_order)
                                        all_sent = True
                                        
                                        for frame in frames:
                                            ser.write(frame)
                                            # Tunggu ACK per frame
                                            got_ack = False
                                            wait_start = time.time()
                                            while time.time() - wait_start < 3:
                                                if ser.in_waiting:
                                                    if ser.read(1) == b'\x06':
                                                        got_ack = True
                                                        break
                                            
                                            if not got_ack:
                                                all_sent = False
                                                break
                                        
                                        # Tutup Sesi
                                        ser.write(b'\x04') # EOT
                                        
                                        if all_sent:
                                            print(f"[{port_name}] Order SELESAI Terkirim.")
                                            setattr(pending_order, flag_col, True)
                                            session.commit()
                                            stuck_count = 0
                                            stuck_window_start = None
                                        else:
                                            logging.error(f"[{port_name}] Order Gagal (No ACK).")
                                            print(f"[{port_name}] Order Gagal (No ACK).")
                                    
                                    else:
                                        # === FORCE RESET (ANTI-STUCK) ===
                                        # Jika sudah 3x ENQ tidak dibalas, anggap alat 'bengong'
                                        # Kirim EOT untuk mereset status alat
                                        print(f"[{port_name}] Alat Sibuk/Stuck. Kirim Force EOT.")
                                        ser.write(b'\x04') 
                                        time.sleep(2.0)
                                        now_ts = time.time()
                                        if not stuck_window_start or (now_ts - stuck_window_start) > STUCK_WINDOW_SEC:
                                            stuck_window_start = now_ts
                                            stuck_count = 1
                                        else:
                                            stuck_count += 1
                                        if stuck_count >= 3:
                                            # Jika sudah stuck berulang dalam window waktu, restart koneksi serial
                                            logging.critical(
                                                f"[{port_name}] Stuck berulang ({stuck_count}x/{STUCK_WINDOW_SEC}s). Restart koneksi serial."
                                            )
                                            print(
                                                f"[{port_name}] Stuck berulang ({stuck_count}x/{STUCK_WINDOW_SEC}s). Restart koneksi serial."
                                            )
                                            try:
                                                ser.reset_input_buffer()
                                                ser.reset_output_buffer()
                                            except Exception:
                                                pass
                                            raise RuntimeError("Vitek stuck berulang, restart port.")

                            except Exception as e:
                                logging.error(f"[{port_name}] Sending Error: {e}")
                                print(f"[{port_name}] Sending Error: {e}")
                            finally:
                                if session: session.close()
                        
                        # Sleep penting agar CPU tidak 100% saat idle
                        time.sleep(0.1)
                    

        except Exception as e:
            logging.critical(f"[{port_name}] Serial Crash: {e}")
            print(f"[{port_name}] Serial Crash: {e}")
            time.sleep(5)
            
# ==========================================
# BECTON DICKINSON (BD) BACTEC PARSER
# ==========================================

def parse_and_save_bd_result(raw_data, port_name="BD Bactec"):
    session = SessionLocal()
    try:
        # --- LANGKAH 1: REASSEMBLY (JAHIT FRAME) ---
        raw_frames = raw_data.split('\x02')
        full_content = ""
        
        for frame in raw_frames:
            if not frame: continue
            content_chunk = frame
            # Hapus Checksum & ETX/ETB
            if '\x03' in frame: content_chunk = frame.split('\x03')[0] 
            elif '\x17' in frame: content_chunk = frame.split('\x17')[0] 
            
            # Hapus Sequence Number di awal
            if content_chunk and content_chunk[0].isdigit():
                content_chunk = content_chunk[1:]
                
            full_content += content_chunk

        # --- LANGKAH 2: PARSING FIELD ---
        lines = full_content.split('\r')
        
        sample_id = None   
        patient_id = ""     
        patient_name = ""   
        specimen_type = ""  
        result_val = ""     
        result_date = datetime.datetime.now()
        
        for line in lines:
            line = line.strip()
            if not line: continue
            fields = line.split('|')
            record_type = fields[0]
            
            # --- PATIENT (P) ---
            if record_type == 'P':
                # P|Seq|?|PID||Name
                if len(fields) > 3: patient_id = fields[3].strip()
                if len(fields) > 5: patient_name = fields[5].replace('^', ' ').strip()

            # --- ORDER (O) ---
            elif record_type == 'O':
                # O|Seq|SampleID|...|...|...|...|...|...|...|...|...|...|...|Specimen
                if len(fields) > 2:
                    sample_id = fields[2].replace('^', '').strip()
                
                # Ambil Spesimen (Biasanya di index 15 / Field 16)
                if len(fields) > 15:
                    specimen_type = fields[15].strip()

            # --- RESULT (R) ---
            elif record_type == 'R':
                # R|Seq|Test|Result|...|...|...|...|...|...|StartDate|EndDate
                if len(fields) > 3:
                    raw_res = fields[3].strip()
                    # Bersihkan hasil
                    if "NEGATIVE" in raw_res: result_val = "NEGATIVE"
                    elif "POSITIVE" in raw_res: result_val = "POSITIVE"
                    else: result_val = raw_res.split('^')[0] # Ambil kode depan saja
                
                # Ambil Tanggal Hasil Selesai (Index 12 / Field 13)
                if len(fields) > 12 and len(fields[12]) >= 14:
                    try:
                        # Format BD: YYYYMMDDHHMMSS (20251006100257)
                        res_dt_str = fields[12][:14]
                        result_date = datetime.datetime.strptime(res_dt_str, "%Y%m%d%H%M%S")
                    except:
                        pass # Gunakan default jika gagal parse

        # --- LANGKAH 3: FORMAT FINAL & SAVE ---
        if sample_id and result_val:
            
            final_res_string = result_val
            if specimen_type:
                final_res_string += f" ({specimen_type})"

            print(f"[{port_name}] Save DB -> ID: {sample_id}, Pasien: {patient_name}, Hasil: {final_res_string}")
            new_entry = LisPhoenix(
                no_id=sample_id,
                seq_no=patient_id,
                rnmpas=patient_name,
                tgl_data=result_date,
                rawdt=raw_data, 
                organisme=final_res_string,
                alat=port_name
            )
            session.add(new_entry)
            session.commit()
        else:
            logging.warning(f"[{port_name}] Data tidak lengkap. ID: {sample_id}, Res: {result_val}")
            print(f"[{port_name}] Data tidak lengkap. ID: {sample_id}, Res: {result_val}")

    except Exception as e:
        logging.error(f"Error Parsing BD: {e}")
        print(f"Error Parsing BD: {e}")
        session.rollback()
    finally:
        session.close()

def calculate_astm_checksum(frame_content):
    data_bytes = frame_content.encode('latin-1') 
    checksum = sum(data_bytes) % 256
    return f"{checksum:02X}"

def create_astm_order_message(order):
    """
    Membuat 1 Frame ASTM Single Block (H, P, O, L) dengan Mapping Index PRESISI.
    Menghindari pergeseran kolom (shifting error).
    """
    # --- 1. PERSIAPAN DATA ---
    pid = sanitize_astm_field(order.norm, max_len=32)
    sid = sanitize_astm_field(order.rnoreg, max_len=32)
    # Nama pasien dipisah agar mengikuti format ASTM: Last^First
    first_name, last_name = split_patient_name(sanitize_astm_field(order.nama, max_len=80))
    first_name = sanitize_astm_field(first_name, uppercase=True, max_len=20)
    last_name = sanitize_astm_field(last_name, uppercase=True, max_len=20)
    p_name = f"{last_name}^{first_name}"
    sex_raw = sanitize_astm_field(order.rjenis, uppercase=True, max_len=10)
    sex = "M" if sex_raw.startswith("L") else "F"
    
    # Lokasi / Ruangan (Field 26) sebaiknya berupa kode singkat agar tidak ditrunkasi LIS.
    raw_location = sanitize_astm_field(getattr(order, 'ruangan', "UT"), uppercase=True, max_len=10)
    location = re.sub(r"[^A-Z0-9]", "", raw_location)[:10]
    if not location:
        location = "UT"
    
    # Diagnosis (Clinical Info - Field 14 di ASTM standar atau 13 di beberapa varian)
    # Kita pasang di Index 13 (Field 14) agar aman
    diagnosis = sanitize_astm_field(getattr(order, 'diagnosa', "Unspecified"), max_len=60)
    if not diagnosis: diagnosis = "Unspecified"

    # Specimen Info (Field 16)
    # Format: SpecimenType^BodySite^Container^Condition
    specimen_type = sanitize_astm_field(order.kd_spesimen, uppercase=True, max_len=20) if order.kd_spesimen else "BLOOD"
    body_site = "VENA"         # Site
    condition = "BAIK"         # Condition
    specimen_field = f"{specimen_type}^{body_site}^^{condition}"

    # --- 2. KONSTRUKSI RECORD DENGAN INDEX PASTI ---
    
    # --- RECORD HEADER (H) ---
    h_rec = [""] * 14

    h_rec[0] = "H"                                  # H,1 Record Type
    h_rec[1] = r"\^&"                               # H,2 Delimiter
    h_rec[4] = "MyLIS"                   # H,5 Sender Name
    h_rec[12] = "V1.00"                             # H,13 Version
    h_rec[13] = datetime.datetime.now().strftime("%Y%m%d%H%M%S")  # H,14 Message DateTime

    head = "|".join(h_rec)
    # --- RECORD PATIENT (P) ---
    # Kita buat array kosong sebanyak 30 kolom dulu
    p_rec = [""] * 35
    p_rec[0] = "P"          # Field 1: Record Type
    p_rec[1] = "1"          # Field 2: Sequence
    p_rec[2] = pid          # Field 3: Patient ID (Practice)
    p_rec[3] = pid          # Field 4: Lab ID (Kosong)
    p_rec[4] = ""           # Field 5: ID 3 (Kosong)
    p_rec[5] = p_name       # Field 6: Patient Name (Index 5) <--- SEBELUMNYA SALAH DISINI
    p_rec[7] = ""           # Field 8: Birthdate
    p_rec[8] = sex          # Field 9: Sex (Index 8)
    # ... Field 10-25 biarkan kosong ...
    p_rec[25] = "1"    # Field 26: Location (Index 25)
    p_rec[32] = "MIKRO"     # Hospital Service
    p_rec[33] = raw_location# Hospital Client (Raw, untuk referensi internal)
    
    # Potong array sampai index 26 saja (sisanya buang) lalu gabung
    pat_str = "|".join(p_rec[:35])
    
    # --- RECORD ORDER (O) ---
    o_rec = [""] * 30
    o_rec[0] = "O"          # Field 1
    o_rec[1] = "1"          # Field 2
    o_rec[2] = sid          # Field 3: Sample ID
    o_rec[3] = ""           # Field 4: Instrument Specimen ID
    o_rec[4] = "^^^" # Field 5: Universal Test ID
    o_rec[5] = "R"          # Field 6: Priority
    # ... Field 7-11 ...
    o_rec[11] = "A"         # Field 12: Action Code (A=Add, N=New) (Index 11)
    o_rec[12] = diagnosis   # Field 13: Clinical Info / Diagnosis (Index 12)
    # ... Field 14-15 ...
    o_rec[15] = specimen_field # Field 16: Specimen Source (Index 15)
    
    # Potong array sampai index 16 (atau lebih jika BD butuh field belakang)
    # Kita ambil aman sampai field 20
    ord_str = "|".join(o_rec[:20])
    
    # --- RECORD TERMINATOR (L) ---
    term = "L|1|N"

    # --- 3. GABUNG FRAME ---
    # Gunakan \r (Carriage Return) sebagai pemisah record
    message_content = f"{head}\r{pat_str}\r{ord_str}\r{term}"
    
    # Sequence Frame = 1
    seq = "1"
    
    # Isi Frame: [Seq] [Data] [ETX]
    frame_body = f"{seq}{message_content}\r\x03"
    
    # Hitung Checksum
    chk = calculate_astm_checksum(frame_body)
    
    # Full Frame
    full_frame = f"\x02{frame_body}{chk}\r\n"
    
    return [full_frame.encode('latin-1')]

def manage_bd_port(config):
    port_name   = config['port']
    flag_col    = config.get('flag_column')
    alat_name   = config.get('alat_name', 'BD')
    print(f"[{port_name}] Membuka port untuk alat {alat_name}...")

    # Buffer untuk menampung pecahan data
    rx_buffer = "" 

    try:
        with serial.Serial(
            port=port_name,
            baudrate=config['baud_rate'],
            timeout=1
        ) as ser:
            
            while True:
                has_activity = False # Penanda agar kita sleep kalau sepi

                # ==========================================
                # PHASE 1: LISTENING (PRIORITAS UTAMA)
                # ==========================================
                try:
                    if ser.in_waiting > 0:
                        has_activity = True
                        data_chunk = ser.read(ser.in_waiting or 1024)
                        
                        if data_chunk:
                            try:
                                # 1. Handle Handshake Awal (ENQ)
                                if b'\x05' in data_chunk:
                                    print(f"[{port_name}] Terima ENQ (Alat mau kirim data). Kirim ACK.")
                                    ser.write(b'\x06') # ACK
                                    rx_buffer = "" # Reset buffer untuk data baru
                                    data_chunk = data_chunk.replace(b'\x05', b'')

                                # 2. Simpan semua byte data yang tersisa.
                                # Chunk lanjutan frame bisa datang tanpa STX, jadi tidak boleh dibuang.
                                chunk_without_eot = data_chunk.replace(b'\x04', b'')
                                if chunk_without_eot:
                                    rx_buffer += chunk_without_eot.decode('latin-1', errors='ignore')

                                # 3. Handle Data Frame Normal (STX ... ETX/ETB)
                                # ASTM butuh ACK setiap frame baru agar alat lanjut kirim frame berikutnya.
                                if b'\x02' in data_chunk:
                                    ser.write(b'\x06') 
                                    # logging.debug(f"[{port_name}] Frame diterima, ACK dikirim.")
                                    print(f"[{port_name}] Frame diterima, ACK dikirim.")

                                # 4. Handle Akhir Transmisi (EOT)
                                if b'\x04' in data_chunk:
                                    print(f"[{port_name}] Terima EOT (Selesai). Memproses data...")
                                    parse_and_save_bd_result(rx_buffer, alat_name)
                                    rx_buffer = "" # Kosongkan buffer setelah save
                                
                            except Exception as e:
                                logging.error(f"Error decode frame: {e}")
                                print(f"Error decode frame: {e}")
                            
                            continue # Loop lagi untuk ambil sisa data

                except Exception as e:
                    logging.error(f"[{port_name}] Error Reading: {e}")
                    print(f"[{port_name}] Error Reading: {e}")
                    rx_buffer = "" # Reset jika error parah

                
                # ==========================================
                # PHASE 2: SENDING ORDER (JIKA BUFFER KOSONG)
                # ==========================================
                # Kita hanya kirim order jika sedang tidak menerima data (buffer kosong)
                if not rx_buffer and flag_col: 
                    try:
                        session = SessionLocal()
                        # Cari order yang belum dikirim
                        pending_order = session.query(PaslabOrder).filter(
                            getattr(PaslabOrder, flag_col) == False
                        ).first()
                        
                        if pending_order:
                            has_activity = True # Jangan sleep lama-lama
                            print(f"[{port_name}] Menemukan Order: {pending_order.rnoreg}. Memulai Handshake...")
                            # --- STEP 1: HANDSHAKE (ENQ) ---
                            ser.reset_input_buffer()
                            ser.write(b'\x05') 
                            time.sleep(0.5)    
                            
                            ack_response = ser.read(1)
                            
                            if ack_response == b'\x06':
                                print(f"[{port_name}] Handshake Sukses (Dapat ACK). Menunggu alat siap...")
                                # --- PERBAIKAN 1: BERI JEDA SETELAH HANDSHAKE ---
                                # Mesin butuh napas sebelum terima data panjang
                                time.sleep(1.5)  # Jeda 1.5 detik
                                
                                frames = create_astm_order_message(pending_order)
                                all_frames_sent = True
                                
                                # ---------------------------------------------------
                                # STEP 2: SEND FRAMES WITH RETRY
                                # ---------------------------------------------------
                                for i, frame in enumerate(frames):
                                    retry_count = 0
                                    max_retries = 3
                                    frame_success = False
                                    
                                    while retry_count < max_retries:
                                        ser.reset_input_buffer()
                                        
                                        print(f"[{port_name}] Kirim Frame {i+1} (Percobaan {retry_count+1})...")
                                        ser.write(frame)
                                        
                                        # Tunggu ACK
                                        original_timeout = ser.timeout
                                        ser.timeout = 3 # Beri waktu agak lama (3 detik) untuk alat memproses data
                                        frame_ack = ser.read(1)
                                        ser.timeout = original_timeout
                                        
                                        if frame_ack == b'\x06': # ACK (Sukses)
                                            frame_success = True
                                            print(f"[{port_name}] Frame {i+1} ACK diterima.")
                                            # Beri jeda dikit sebelum kirim frame berikutnya
                                            time.sleep(0.2) 
                                            break 
                                            
                                        elif frame_ack == b'\x15': # NAK (Ditolak - Checksum Salah)
                                            print(f"[{port_name}] Frame ditolak (NAK). Checksum mungkin salah.")
                                            time.sleep(2) # Tunggu 2 detik
                                            retry_count += 1
                                            
                                        elif not frame_ack: # Timeout (Sepi)
                                            # --- PERBAIKAN 2: BERI JEDA SAAT TIMEOUT ---
                                            print(f"[{port_name}] Timeout (Alat diam). Menunggu sebelum retry...")
                                            time.sleep(2) # Tunggu 2 detik agar alat recover
                                            retry_count += 1
                                            
                                        else: 
                                            logging.warning(f"[{port_name}] Respon aneh: {frame_ack}")
                                            print(f"[{port_name}] Respon aneh: {frame_ack}")
                                            time.sleep(1)
                                            retry_count += 1
                                    
                                    if not frame_success:
                                        logging.error(f"[{port_name}] Gagal kirim frame ke-{i+1}. Batal.")
                                        print(f"[{port_name}] Gagal kirim frame ke-{i+1}. Batal.")
                                        all_frames_sent = False
                                        break                                
                                # ---------------------------------------------------
                                # STEP 3: FINALIZE
                                # ---------------------------------------------------
                                if all_frames_sent:
                                    ser.write(b'\x04') # EOT (End of Transmission)
                                    print(f"[{port_name}] Order {pending_order.rnoreg} SUKSES Terkirim.")
                                    # Update Database
                                    setattr(pending_order, flag_col, True)
                                    session.commit()
                                else:
                                    ser.write(b'\x04') # EOT (Putus paksa karena error)
                                    logging.error(f"[{port_name}] Pengiriman Order GAGAL.")
                                    print(f"[{port_name}] Pengiriman Order GAGAL.")
                            else:
                                # Jika Handshake gagal (Dibalas NAK, atau Timeout)
                                logging.warning(f"[{port_name}] Handshake Gagal. Respon alat: {ack_response}")
                                print(f"[{port_name}] Handshake Gagal. Respon alat: {ack_response}")
                                # Jangan update flag DB, biarkan coba lagi nanti


                        session.close()

                    except Exception as e:
                        logging.error(f"[{port_name}] Error Sending Logic: {e}")
                        print(f"[{port_name}] Error Sending Logic: {e}")
                        if 'session' in locals(): session.close()

                # ==========================================
                # PHASE 3: IDLE MANAGEMENT
                # ==========================================
                # Jika tidak ada data masuk dan tidak ada order keluar, tidur sebentar
                # Ini penting agar CPU tidak 100% dan DB tidak jebol
                if not has_activity:
                    time.sleep(1.0) 

    except Exception as e:
        logging.critical(f"[{port_name}] Gagal connect Serial: {e}")
        print(f"[{port_name}] Gagal connect Serial: {e}")
        time.sleep(5)

# ==========================================
# 5. Serial Manager
# ==========================================

def manage_serial_port(config):
    """Fungsi router yang memilih manajer yang tepat berdasarkan tipe alat."""
    device_type = config.get('device_type')
    if device_type == 'vitek':
        manage_vitek_port(config)
    elif device_type in ['bd_mgit', 'bd_bactec', 'bd']:
        manage_bd_port(config)
    else:
        print(f"Tipe alat tidak diketahui: '{device_type}' untuk port {config.get('port')}. Thread dihentikan.")

# ==========================================
# 6. MAIN EXECUTION
# ==========================================
if __name__ == "__main__":
    print("--- MEMULAI GENE XPERT ONLY LISTENER ---")
    
    all_threads = []
    t_tcp = threading.Thread(target=manage_tcp_server, name="Manager-TCP-GeneXpert", daemon=True)
    t_tcp.start()
    all_threads.append(t_tcp)

    try:
        while True:
            print(f"--- Monitoring {len(all_threads)} Threads ---")
            alive_count = 0
            for t in all_threads:
                if t.is_alive():
                    alive_count += 1
                else:
                    print(f"!!! THREAD MATI: {t.name} !!!")
                    
            if alive_count == 0:
                print("Semua thread GeneXpert mati. System Shutdown.")
                break
                
            time.sleep(10)
            
    except KeyboardInterrupt:
        print("Mematikan GeneXpert Listener (Ctrl+C)...")
