from enum import Enum
from logging import config
from logging.handlers import TimedRotatingFileHandler
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="app.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])

# ==========================================
# 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()
# 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
# 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.123.74',
    '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": "HIV-1_VL",                # Xpert HIV-1 Viral Load XC Version 3
    "TCM TB": "MTB-RIF",              # Xpert MTB-RIF Assay G4 Version 6
    "TCM TB ULTRA": "MTB-RIF_ULTRA2", # Xpert MTB-RIF Ultra Version 4
    "TCM TB XDR": "MTB-XDR",          # Xpert MTB-XDR Version 1
    "COVID-19": "COV-2 2",            # Xpert Xpress SARS-CoV-2 Version 2
    "HCV VL": "HCV",                  # Xpert HCV Viral Load Version 1
    
    # Mapping untuk IP 10.10.120.74 (Khusus)
    # Catatan: Di dokumen Anda tertulis Assay 'HBV' tapi kodenya 'HCV'. 
    # Pastikan ini benar, atau sesuaikan jika itu typo di dokumen.
    "HBV VL": "HCV",
}
GENEXPERT_IP_CAPABILITIES = {
    "10.10.120.75": ["MTB-RIF", "MTB-RIF_ULTRA2", "MTB-XDR", "HIV-1_VL", "COV-2 2"],
    "10.10.120.74": ["HCV", "HBV"],
    "10.10.120.73": [] # Belum ada yang aktif
}
# Default code jika nama tes di database tidak dikenali
DEFAULT_GXP_CODE = "MTB-RIF"

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': 19200, '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 send_all_orders(conn, ip_addr, hl7_msg, msg_id):
    orders = get_pending_orders(ip_addr)
    session = SessionLocal()
    scheduled_orders = []
    try:
        if not orders:
            print(f"[GENEXPERT] Tidak ada order pending untuk {ip_addr}")
            rsp = (
                f"MSH|^~\\&|LIS|LAB|GeneXpert|Cepheid|{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}||RSP^Z03|{msg_id}|P|2.5\r"
                f"MSA|AA|{msg_id}\r"
                f"QAK|NF|{msg_id}\r"
            )
            conn.sendall(f"\x0b{rsp}\x1c\r".encode('utf-8'))
            return

        print(f"[GENEXPERT] Mengirim {len(orders)} order ke {ip_addr}")

        for order in orders:
            rsp = create_hl7_dsr_response(order, msg_id, "")
            print(f"[GENEXPERT] Order ditemukan untuk {ip_addr}")
                    
            mllp = f"\x0b{rsp}\x1c\r"
            conn.sendall(mllp.encode('utf-8'))
            flag = get_flag_by_device(ip_addr)
            setattr(order, flag, True)
            session.add(order)
            scheduled_orders.append({
                "accnumber": str(order.rnoreg or "").strip(),
                "register_no": str(order.rnoreg or "").strip(),
                "target_ip": ip_addr,
            })

        session.commit()
        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"],
            )
    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_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 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):
    while True:
        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"]

        logging.info(
            f"[GENEXPERT-SCHEDULER] Trigger query hasil accnumber={accnumber}, "
            f"register_no={register_no}, target_ip={target_ip}, attempt={attempt_no}"
        )
        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

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,
):
    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:
        logging.warning("[GENEXPERT-SCHEDULER] Jadwal query hasil dilewati karena accnumber kosong.")
        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"
            logging.info(f"[GENEXPERT-SCHEDULER] Jadwal sudah aktif untuk accnumber={accnumber}")
            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()
    logging.info(
        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"
    )
    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):
    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:
        with connection_lock:
            conn = active_genexpert_connections.get(ip)
        if not conn:
            failed_ips.append({"ip": ip, "error": "connection-not-active"})
            continue
        try:
            conn.sendall(mllp_payload)
            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)})

    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():
    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")

        logging.info(f"[HL7 Parser] Menyimpan hasil untuk Sample: {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()
        logging.info(f"[DB] Berhasil simpan ke LisPhoenix: {sample_id}")
        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: MTB-RIF
        
        # 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}"

        logging.info(f"[MYLA] Save DB -> Sample: {sample_id}, Hasil: {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()
            logging.info(f"[MYLA-ASTM] Save DB -> Sample: {sample_id}, Hasil: {final_result_str}")
        else:
            logging.info("[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"):
        logging.info(f"[MYLA-HL7] Menerima hasil dari {peer_ip} (type={message_type}, ID: {incoming_control_id})")
        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]
    logging.info(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).
    """
    logging.info(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:
                            logging.info(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]
                            logging.info(f"[MYLA] Menerima Query dari {client_ip} (Control ID: {incoming_control_id})")
                            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()
                                    logging.info(f"[MYLA] Order terkirim rnoreg={order.rnoreg}, set {target_flag_col}=TRUE")
                                    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'))
                                    logging.info(f"[MYLA] Tidak ada order pending untuk sample={search_sample_id or '-'}")
                                    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'))
                            logging.info(f"[MYLA ACK] Terkirim untuk ID {incoming_control_id}")
                            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:
            logging.info(f"[MYLA-CLIENT] Mencoba konek ke MYLA {host}:{port} ...")
            print(f"[MYLA-CLIENT] Mencoba konek ke MYLA {host}:{port} ...")
            conn = socket.create_connection((host, port), timeout=10)
            conn.settimeout(1.0)
            logging.info(f"[MYLA-CLIENT] Terhubung ke MYLA {host}:{port}")
            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:
                        logging.info(
                            "[MYLA-CLIENT] Polling aktif: tidak ada order pending "
                            "(flg_vitek3 = FALSE/NULL)."
                        )
                        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

                logging.info(f"[MYLA-CLIENT] Ditemukan {len(pending_orders)} order pending untuk MYLA")
                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)
                        logging.info(f"[MYLA-HL7] Order sukses, set flg_vitek3=TRUE rnoreg={order.rnoreg}")
                        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)
        logging.info(f"[MYLA-INBOUND] Listening di {host}:{port}")
        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 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
        logging.info(f"[TCP-SERVER] Listening GeneXpert di port {TCP_LISTENER_PORT}...")
        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():
    logging.info(f"[HTTP-API] Listening di port {HTTP_API_PORT}...")
    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 handle_genexpert_client(conn, addr):
    print(f"[GenExpert_TCP] Koneksi baru dari {addr}")
    buffer = b""
    conn.settimeout(60)
    client_ip = addr[0]
    with connection_lock:
        active_genexpert_connections[client_ip] = conn
    logging.info(f"[GenExpert_TCP] Register koneksi aktif {client_ip}")
    
    try:
        while True:
            try:
                data = conn.recv(4096)
                if not data:
                    logging.info(f"[GenExpert_TCP] Client {addr} menutup koneksi.")
                    break
                
                buffer += data
                
                # --- 1. HANDLE HANDSHAKE (ENQ) ---
                # Jika alat kirim ENQ (\x05/♣), langsung balas ACK (\x06)
                if b'\x05' in buffer:
                    # logging.info(f"[TCP] Terima ENQ dari {addr}, kirim ACK.")
                    conn.sendall(b'\x06')
                    # Hapus ENQ dari buffer agar tidak mengganggu
                    buffer = buffer.replace(b'\x05', 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: # Pola ASTM (Ada Checksum setelahnya)
                    # ASTM: <STX>...<ETX>CS<CR><LF>
                    # Kita cari \x03, lalu tambah 4 byte (CS + CRLF) untuk aman
                    pos = buffer.find(b'\x03')
                    if len(buffer) >= pos + 4: 
                        end_marker_pos = pos + 4
                        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:
                    # 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]
                    
                    # Sisa buffer (jika ada paket nempel di belakangnya) disimpan untuk loop berikutnya
                    buffer = buffer[end_marker_pos:]
                    
                    # Bersihkan buffer dari sisa marker di awal (jika loop sebelumnya menyisakan sampah)
                    buffer = buffer.lstrip(b'\x04').lstrip(b'\r').lstrip(b'\n')

                    # Decode ke string
                    temp_str = full_message_bytes.decode('latin-1', errors='ignore')

                    # --- SANITIZING (PEMBERSIHAN) ---
                    # Cari MSH pertama
                    if "MSH|" in temp_str:
                        msh_index = temp_str.find("MSH|")
                        clean_hl7 = temp_str[msh_index:]
                        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
                        # ==========================================
                        # SKENARIO 1: ALAT BERTANYA (QUERY / QRY)
                        # ==========================================
                        if "QRY^" in clean_hl7 or "QRY|" in clean_hl7:
                            client_ip = addr[0]
                            logging.info(f"[QUERY] Request dari IP: {client_ip} (Control ID: {incoming_control_id})")
                            print(f"[QUERY] Request dari IP: {client_ip} (Control ID: {incoming_control_id})")
                            
                            # 1. Tentukan Kolom Flag mana yang harus dicek berdasarkan IP
                            # Kita balik mappingnya: IP -> Nama Kolom
                            target_flag_col = None
                            for col_name, ip_addr in TARGET_MAPPING.items():
                                if ip_addr == client_ip:
                                    target_flag_col = col_name
                                    break
                            
                            if not target_flag_col:
                                logging.warning(f"[DENIED] IP {client_ip} tidak terdaftar di TARGET_MAPPING. Abaikan.")
                                print(f"[DENIED] IP {client_ip} tidak terdaftar di TARGET_MAPPING. Abaikan.")
                                # Kirim jawaban kosong agar alat tidak hang
                                reply_msg = create_hl7_dsr_response(None, incoming_control_id, "")
                                conn.sendall(f"\x0b{reply_msg}\x1c\r".encode('utf-8'))
                                continue # Skip proses selanjutnya

                            logging.info(f"[TARGET] IP {client_ip} akan mengecek kolom: {target_flag_col}")
                            print(f"[TARGET] IP {client_ip} akan mengecek kolom: {target_flag_col}")

                            # 2. Cari Sample ID di pesan QRY
                            search_sample_id = None
                            qrd_line = ""
                            for line in lines:
                                if line.startswith("QRD|"):
                                    qrd_line = line
                                    fields = line.split('|')
                                    if len(fields) > 8:
                                        search_sample_id = fields[8]
                                    break
                            
                            if search_sample_id:
                                session = SessionLocal()
                                try:
                                    # 3. Query Database Dinamis (Pakai getattr)
                                    # Kita cari RN yang cocok DAN (Flag Kolom Tersebut False ATAU Null)
                                    
                                    # Ambil atribut kolom Paslab berdasarkan nama string (misal: Paslab.flg_gxp3)
                                    flag_attr = getattr(PaslabOrder, target_flag_col, None)
                                    
                                    if flag_attr is None:
                                        logging.error(f"Kolom '{target_flag_col}' tidak ditemukan di Model Paslab!")
                                        print(f"Kolom '{target_flag_col}' tidak ditemukan di Model Paslab!")
                                        raise Exception("Invalid Column Name")

                                    logging.info(f"[DB LOOKUP] Mencari {search_sample_id} dimana {target_flag_col} == False")
                                    print(f"[DB LOOKUP] Mencari {search_sample_id} dimana {target_flag_col} == False")

                                    order = session.query(PaslabOrder).filter(
                                        PaslabOrder.rnoreg == search_sample_id,
                                        (flag_attr == False) | (flag_attr == None)
                                    ).first()
                                    
                                    # Variabel kontrol kirim
                                    send_order = False
                                    
                                    if order:
                                        # (Opsional) Validasi Kapabilitas Mesin di sini jika perlu
                                        # ...
                                        send_order = True
                                    
                                    # 4. Kirim Respon
                                    if send_order and order:
                                        logging.info(f"[FOUND] Order ditemukan: {order.nama}. Mengirim ke {client_ip}...")
                                        print(f"[FOUND] Order ditemukan: {order.nama}. Mengirim ke {client_ip}...")
                                        
                                        reply_msg = create_hl7_dsr_response(order, incoming_control_id, qrd_line)
                                        conn.sendall(f"\x0b{reply_msg}\x1c\r".encode('utf-8'))
                                        
                                        # 5. UPDATE FLAG DINAMIS (PENTING)
                                        # Set kolom yang sesuai (misal flg_gxp3) menjadi True
                                        setattr(order, target_flag_col, True)
                                        session.commit()
                                        logging.info(f"[UPDATE] {target_flag_col} diset TRUE untuk {search_sample_id}")
                                        print(f"[UPDATE] {target_flag_col} diset TRUE untuk {search_sample_id}")
                                        schedule_result_query_for_order(
                                            accnumber=str(order.rnoreg or "").strip(),
                                            register_no=str(order.rnoreg or "").strip(),
                                            target_ip=client_ip,
                                        )
                                        
                                    else:
                                        logging.warning(f"[NOT FOUND/ALREADY SENT] Tidak ada order baru untuk {search_sample_id} di kolom {target_flag_col}")
                                        print(f"[NOT FOUND/ALREADY SENT] Tidak ada order baru untuk {search_sample_id} di kolom {target_flag_col}")
                                        # Kirim DSR Kosong (NF)
                                        reply_msg = create_hl7_dsr_response(None, incoming_control_id, qrd_line)
                                        conn.sendall(f"\x0b{reply_msg}\x1c\r".encode('utf-8'))

                                except Exception as db_err:
                                    logging.error(f"Database Error: {db_err}")
                                    print(f"Database Error: {db_err}")
                                    session.rollback()
                                finally:
                                    session.close()
                            # Selesai tangani QRY, jangan lanjut parsing umum
                            continue

                        # ==========================================
                        # SKENARIO 2: ALAT KIRIM HASIL (RESULT / ORU)
                        # ==========================================
                        elif "ORU^" in clean_hl7:
                            logging.info(f"[RESULT] Menerima Hasil Lab.")
                            
                            # 1. Parse dan Simpan Hasil (Panggil fungsi parser Anda)
                            parse_hl7_result(conn, msg_id, clean_hl7, device_name=f"GeneXpert-{addr[0]}")
                            
                            # 2. Kirim ACK (Terima Kasih)
                            ack_time = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
                            ack_msg = f"MSH|^~\\&|LIS|LAB|GeneXpert|Cepheid|{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'))
                            logging.info(f"[ACK SENT] Untuk hasil ID {incoming_control_id}")
                            print(f"[ACK SENT] Untuk hasil ID {incoming_control_id}")
                            continue
                            
                        # ==========================================
                        # SKENARIO 3: Handle Pesan Apa Adanya
                        # ==========================================
                        elif "QBP^Z03" in clean_hl7:
                            print("[GENEXPERT] Alat meminta ORDER")
                            msg_id = extract_msg_control_id(clean_hl7)
                            send_all_orders(conn, addr[0], clean_hl7, msg_id)
                            continue

                        elif "QCN^J01" in clean_hl7:
                            logging.info(f"[GENEXPERT] Menerima konfirmasi query dari {addr[0]}.")
                            print(f"[GENEXPERT] Menerima konfirmasi query dari {addr[0]}.")
                            continue

                        # Logging sample data (50 karakter)
                        print(f"[GenExpert_TCP] Pesan Lengkap Diterima: {clean_hl7[:50]}...")
                        
                        # Panggil Parser
                        parse_hl7_result(conn, msg_id, clean_hl7, device_name=f"GeneXpert-{addr[0]}, ")
                        
                        # --- KIRIM ACK ---
                        try:
                            lines = clean_hl7.split('\r')
                            msh_fields = lines[0].split('|')
                            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"
                                
                                # Kirim ACK format MLLP
                                full_ack = f"\x0b{ack_msg}\x1c\r"
                                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}")
                    else:
                        # Jika pesan lengkap tapi tidak ada MSH (misal cuma EOT doang)
                        pass

            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]
        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 Python.")
        print(f"Gagal kirim Order: GeneXpert dengan IP {target_ip} BELUM TERKONEKSI ke Python.")
        return False # Indikasi gagal

    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}...")
        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}")
        
        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) ===
                logging.info(f"[{port_name}] UPDATE Data (Tahap 2) -> ID: {sample_id}, Pasien: {patient_name}")
                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) ===
                logging.info(f"[{port_name}] INSERT Data Baru (Tahap 1) -> ID: {sample_id}, Pasien: {patient_name}")
                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):
    """
    Pecah nama pasien menjadi first_name dan last_name.
    - Jika ada '^', diasumsikan format ASTM/HL7: Last^First.
    - Jika nama biasa, token terakhir = last_name, sisanya = first_name.
    """
    if not full_name:
        return "NO", "NAME"

    raw = str(full_name).strip()
    if not raw:
        return "NO", "NAME"

    first_name = ""
    last_name = ""

    if "^" in raw:
        parts = [p.strip() for p in raw.split("^") if p.strip()]
        if len(parts) >= 2:
            last_name = parts[0]
            first_name = parts[1]
        elif len(parts) == 1:
            first_name = parts[0]
    else:
        tokens = raw.split()
        if len(tokens) >= 2:
            last_name = tokens[-1]
            first_name = " ".join(tokens[:-1])
        elif len(tokens) == 1:
            first_name = tokens[0]

    first_name = (first_name or "NO").upper()[:20]
    last_name = (last_name or "NAME").upper()[:20]
    return first_name, last_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})"

            logging.info(f"[{port_name}] Save DB -> ID: {sample_id}, Pasien: {patient_name}, Hasil: {final_res_string}")
            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=40)
    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) ---
    head = r"H|\^&|||LIS||||||||1"
    
    # --- RECORD PATIENT (P) ---
    # Kita buat array kosong sebanyak 30 kolom dulu
    p_rec = [""] * 30
    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] = location    # Field 26: Location (Index 25)
    
    # Potong array sampai index 26 saja (sisanya buang) lalu gabung
    pat_str = "|".join(p_rec[:26])
    
    # --- 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}\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 = b"" 

    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:
                            # 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
                                continue
                            
                            # 2. Handle Akhir Transmisi (EOT)
                            if b'\x04' in data_chunk:
                                print(f"[{port_name}] Terima EOT (Selesai). Memproses data...")
                                # Proses semua data yang terkumpul di buffer
                                parse_and_save_bd_result(rx_buffer, alat_name)
                                rx_buffer = "" # Kosongkan buffer setelah save
                                continue

                            # 3. Handle Data Frame Normal (STX ... ETX)
                            # ASTM butuh kita balas ACK setiap kali dikirim Frame
                            # Frame biasanya diawali STX (\x02)
                            if b'\x02' in data_chunk:
                                # Simpan ke buffer (decode dulu ke string)
                                try:
                                    decoded_str = data_chunk.decode('utf-8', errors='ignore')
                                    rx_buffer += decoded_str
                                    
                                    # WAJIB: Kirim ACK agar alat lanjut kirim baris berikutnya
                                    ser.write(b'\x06') 
                                    # logging.debug(f"[{port_name}] Frame diterima, ACK dikirim.")
                                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 = b"" # 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:
        logging.error(f"Tipe alat tidak diketahui: '{device_type}' untuk port {config.get('port')}. Thread dihentikan.")
        print(f"Tipe alat tidak diketahui: '{device_type}' untuk port {config.get('port')}. Thread dihentikan.")

# ==========================================
# 6. MAIN EXECUTION
# ==========================================
if __name__ == "__main__":
    print("--- MEMULAI LIS INTERFACE SYSTEM ---")
    
    # List untuk menampung semua thread agar bisa dimonitor
    all_threads = []
    stop_event = threading.Event()
    # 1. Start Thread Order Poller (Pengecek Order Baru di DB)
    #t_poller = threading.Thread(target=order_poller, args=(stop_event,), name="OrderPoller", daemon=True)
    #t_poller.start()
    #all_threads.append(t_poller)

    # 2. Start Thread Serial Manager (Vitek & BD)
    for config in DEVICE_CONFIGS:
        if config['protocol'] == 'serial':
            t_serial = threading.Thread(
                target=manage_serial_port, 
                args=(config,), 
                name=f"Manager-{config['device_type']}-{config['port']}", # Nama lebih jelas
                daemon=True 
            )
            t_serial.start()
            all_threads.append(t_serial)
    
    # 3. Start Thread TCP Server (GeneXpert)
    t_tcp = threading.Thread(target=manage_tcp_server, name="Manager-TCP-GeneXpert", daemon=True)
    t_tcp.start()
    all_threads.append(t_tcp)

    # 3. Start Thread TCP Client (LIS -> MyLA: kirim order)
    myla_thread = threading.Thread(
        target=start_myla_server,
        args=(MYLA_HOST, MYLA_PORT),
        name="Manager-TCP-MyLA-Client",
        daemon=True
    )
    myla_thread.start()
    all_threads.append(myla_thread)

    # 3b. Start Thread TCP Server Inbound (MyLA/BCI -> LIS: kirim hasil)
    myla_inbound_thread = threading.Thread(
        target=start_myla_inbound_server,
        args=(MYLA_INBOUND_HOST, MYLA_INBOUND_PORT),
        name="Manager-TCP-MyLA-Inbound",
        daemon=True
    )
    myla_inbound_thread.start()
    all_threads.append(myla_inbound_thread)

    # 4. Start Thread HTTP API (Trigger dari Laravel)
    t_http = threading.Thread(target=run_http_api_server, name="Manager-HTTP-API", daemon=True)
    t_http.start()
    all_threads.append(t_http)

    # 5. LOOP UTAMA (Keep-Alive & Monitoring)
    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 mati. System Shutdown.")
                break
                
            time.sleep(10)
            
    except KeyboardInterrupt:
        print("Mematikan Service (Ctrl+C)...")
        stop_event.set()
