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 # 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

# ==========================================
# 1. KONFIGURASI SISTEM
# ==========================================

# 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
# 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"

# 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])

# Global Variables
app = Flask(__name__)
active_genexpert_connections = {}
connection_lock = threading.Lock()
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'
    #},
]

# 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' # Record Separator, Group Separator
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)
    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()

    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)

    session.commit()
    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 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
        
        # 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':
                # 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 (DIMODIFIKASI)
        if not sample_id:
            send_all_orders(conn, device_name, clean_hl7, msg_id)
            print("[HL7 Parser] Meminta order")
            return

        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=sample_id,
            seq_no=patient_id,
            rnmpas=patient_name,
            tgl_data=result_date,
            rawdt=hl7_message,
            organisme=final_result,
            alat=device_name
        )
        
        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}")

    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 ""
        pid_nama = order.nama if order.nama else "No Name"
        
        # 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}"
        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{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"
    
# ==========================================
# 4. NETWORK & COMMUNICATION LOGIC
# ==========================================
# ==========================================
# 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 handle_genexpert_client(conn, addr):
    print(f"[GenExpert_TCP] Koneksi baru dari {addr}")
    buffer = b""
    
    try:
        while True:
            try:
                data = conn.recv(4096)
                if not data:
                    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"
                        # ==========================================
                        # 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}")
                                        
                                    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(clean_hl7, 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

                        # 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 Exception as e:
                print(f"[Loop Error] {e}")
                # Jangan break, lanjut terima data
                time.sleep(0.1) 

    except Exception as e:
        logging.error(f"[GenExpert_TCP Error] Koneksi {addr} terputus: {e}")
    finally:
        conn.close()
        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 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 ""
    p_name = str(order.nama).strip().replace('^', ' ').upper()[:20] if order.nama else "NO NAME"
    specimen = str(order.kd_spesimen).upper() if order.kd_spesimen else "BLOOD"
    
    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"|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')
    
    logging.info(f"[{port_name}] START VITEK SERVICE (Relaxed Mode)...")
    print(f"[{port_name}] START VITEK SERVICE (Relaxed Mode)...")
    # Deteksi stuck dalam window waktu tertentu.
    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()
                logging.info(f"[{port_name}] Ready & Listening.")

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

                        # --- DATA FRAME ---
                        elif header == b'\x02': 
                            logging.info(f"[{port_name}] Frame Start. Reading...")
                            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
                                logging.warning(f"[{port_name}] Frame tanpa ETX tapi ada Checksum. Menerima paksa...")
                                print(f"[{port_name}] Frame tanpa ETX tapi ada Checksum. Menerima paksa...")
                            if is_valid:
                                logging.info(f"[{port_name}] Frame Accepted. Sending ACK.")
                                print(f"[{port_name}] Frame OK -> ACK Sent.")
                                print(f"[{port_name}] FULL FRAME: {full_frame}")
                                # 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:
                                    logging.info(f"[{port_name}] Ada Order: {pending_order.rnoreg}. Coba Handshake...")
                                    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 ===
                                        logging.info(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:
                                            logging.info(f"[{port_name}] Order SELESAI Terkirim.")
                                            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).")
                                    
                                    else:
                                        # === FORCE RESET (ANTI-STUCK) ===
                                        # Jika sudah 3x ENQ tidak dibalas, anggap alat 'bengong'
                                        # Kirim EOT untuk mereset status alat
                                        logging.warning(f"[{port_name}] Alat Sibuk/Stuck. Kirim Force EOT.")
                                        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 = str(order.norm).strip() if order.norm else ""
    sid = str(order.rnoreg).strip() if order.rnoreg else ""
    # Nama Pasien: Ganti karakter topi '^' dengan spasi agar tidak merusak format
    p_name = str(order.nama).strip().replace('^', ' ')[:20] if order.nama else "No Name"
    sex = "M" if str(order.rjenis).upper().startswith("L") else "F"
    
    # Lokasi / Ruangan (Field 26)
    location = getattr(order, 'ruangan', "RSSA Malang") 
    if not location: location = "RSSA Malang"
    
    # Diagnosis (Clinical Info - Field 14 di ASTM standar atau 13 di beberapa varian)
    # Kita pasang di Index 13 (Field 14) agar aman
    diagnosis = getattr(order, 'diagnosa', "Unspecified")
    if not diagnosis: diagnosis = "Unspecified"

    # Specimen Info (Field 16)
    # Format: SpecimenType^BodySite^Container^Condition
    specimen_type = str(order.kd_spesimen).upper() 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] = "^^^BD_BACTEC" # 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')
    logging.info(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:
                                logging.info(f"[{port_name}] Terima ENQ (Alat mau kirim data). Kirim ACK.")
                                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:
                                logging.info(f"[{port_name}] Terima EOT (Selesai). Memproses data...")
                                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
                            logging.info(f"[{port_name}] Menemukan Order: {pending_order.rnoreg}. Memulai Handshake...")
                            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':
                                logging.info(f"[{port_name}] Handshake Sukses (Dapat ACK). Menunggu alat siap...")
                                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()
                                        
                                        logging.info(f"[{port_name}] Kirim Frame {i+1} (Percobaan {retry_count+1})...")
                                        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
                                            logging.info(f"[{port_name}] Frame {i+1} ACK diterima.")
                                            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)
                                            logging.warning(f"[{port_name}] Frame ditolak (NAK). Checksum mungkin 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 ---
                                            logging.warning(f"[{port_name}] Timeout (Alat diam). Menunggu sebelum retry...")
                                            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)
                                    logging.info(f"[{port_name}] Order {pending_order.rnoreg} SUKSES Terkirim.")
                                    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)

    # 5. LOOP UTAMA (Keep-Alive & Monitoring)
    # Ini sekarang bisa berjalan karena Flask sudah dipindah ke thread
    try:
        while True:
            logging.debug(f"--- Monitoring {len(all_threads)} Threads ---")
            print(f"--- Monitoring {len(all_threads)} Threads ---")
            # Cek status setiap thread
            alive_count = 0
            for t in all_threads:
                if t.is_alive():
                    alive_count += 1
                else:
                    logging.warning(f"!!! THREAD MATI: {t.name} !!!")
                    print(f"!!! THREAD MATI: {t.name} !!!")
                    # Disini Anda bisa menambahkan logika restart thread jika mati
                    
            # Jika semua mati, exit (atau restart service)
            if alive_count == 0:
                logging.critical("Semua thread mati. System Shutdown.")
                print("Semua thread mati. System Shutdown.")
                break
                
            time.sleep(10) # Cek setiap 10 detik (Hemat CPU)
            
    except KeyboardInterrupt:
        logging.info("Mematikan Service (Ctrl+C)...")
        print("Mematikan Service (Ctrl+C)...")
        stop_event.set()
        # Thread daemon akan mati otomatis saat main exit
