package com.unibuy.smartdevice.tools;

import android.content.Context;
import android.serialport.SerialPort;
import android.util.Log;

import com.unibuy.smartdevice.exception.Logs;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicBoolean;

public class SerialCashAcceptorHandler implements ICashAcceptorHandler {

    private static final String TAG = "MDB_Handler";
    /** 預設串口路徑（ttyS9）。Com Port 3 等其他埠由建構子傳入覆寫。 */
    public static final String DEFAULT_PORT = "/dev/ttyS9";

    private Context context;
    /** 實際連線的 /dev 串口路徑，由建構子決定（ttyS9 或 Com Port 3）。 */
    private final String portPath;
    private SerialPort serialPort;
    private AtomicBoolean isReading = new AtomicBoolean(false);
    private Thread readThread;
    private MdbEventListener eventListener;
    private DebugLogListener debugListener;
    private int amountDivisor = 1; // 根據設備分辨率調整的除數，預設為 1
    private Logs logs;

    // 收銀器使能控制暫存器(0x0007)的目前狀態：bit0=禁硬幣, bit1=禁紙鈔。復位預設 0x03(皆禁止)。
    private int acceptorCtrlReg = 0x03;

    private int lastCoinRegVal = 0;
    private int lastBillRegVal = 0;
    private int lastCoinReg0Val = 0;
    private int lastEscrowRegVal = 0;
    public static int virtualCoin1Count = 30;
    // 1 元料斗實體上限：1 元硬幣機構為軟體虛擬計數(硬體不回報)，只增不減易飄成不合理值(曾達 362)。
    // 封頂在實體上限，投幣累加不超過此數(溢出的當作進現金箱不計)，避免顯示/找零依據出現不可能的數量。
    public static final int MAX_VIRTUAL_COIN1 = 200;

    // 庫存快取變數，避免頻繁發送真實串口指令造成 synchronized 卡頓
    private int cacheCoin1 = 30;
    private int cacheCoin5 = 0;
    private int cacheCoin10 = 0;
    private int cacheCoin50 = 0;
    private int cacheNote100 = 0;

    // ===== 以硬幣庫存計數器(0x0021)累計增量認列 5/10/50 元 =====
    // 本機台串口在快速投幣時會超時卡頓，導致 Reg1 瞬時值漏抓 >70%；但庫存計數器是累計值、對讀取失敗免疫。
    // baseCoinX：本次交易起始庫存基準(-1=未初始化)；creditedCoinX：已認列枚數(高水位，只增不減，避免庫存瞬時誤讀重複計金)。
    private int baseCoin5 = -1, baseCoin10 = -1, baseCoin50 = -1;
    private int creditedCoin5 = 0, creditedCoin10 = 0, creditedCoin50 = 0;
    private long lastBillStockReadMs = 0; // 紙鈔庫存(0x0013)讀取節流（每 1 秒）

    // 庫存 Delta 補偵測：記錄上次讀取庫存時各面額的數量 (初始 -1 代表尚未初始化)
    private int prevCacheCoin5 = -1;
    private int prevCacheCoin10 = -1;
    private int prevCacheCoin50 = -1;
    // 透過 Reg1 poll 已偵測到的面額數量（用於與庫存 delta 去重，避免重複計數）
    private int reg1DetectedCoin5 = 0;
    private int reg1DetectedCoin10 = 0;
    private int reg1DetectedCoin50 = 0;




    private static SerialCashAcceptorHandler instance;

    // 全域唯一「活躍」handler。每次現金對話框/設定頁都會 new 一個新 handler 並開串口、啟輪詢執行緒；
    // 若舊的沒關就又開新的（快速開關/多頁面重疊），會有多條輪詢執行緒同時搶同一個 ttyUSB0 →
    // 對同一張鈔重複偵測、重複送 3503 壓鈔 → 滾輪動作衝突造成卡鈔。
    // 故：新 handler connect 前，強制關閉前一個活躍 handler（停執行緒+關串口），確保同時只有一條輪詢執行緒。
    private static SerialCashAcceptorHandler sActiveHandler;

    public static synchronized SerialCashAcceptorHandler getInstance(Context context) {
        if (instance == null) {
            instance = new SerialCashAcceptorHandler(context);
        }
        return instance;
    }

    private SerialCashAcceptorHandler(Context context) {
        this(context, null, null, DEFAULT_PORT);
    }

    public synchronized void setEventListener(MdbEventListener listener) {
        this.eventListener = listener;
    }

    public synchronized void setDebugListener(DebugLogListener debugLogListener) {
        this.debugListener = debugLogListener;
    }

    public SerialCashAcceptorHandler(Context context, MdbEventListener listener) {
        this(context, listener, null, DEFAULT_PORT);
    }

    public SerialCashAcceptorHandler(Context context, MdbEventListener listener, DebugLogListener debugLogListener) {
        this(context, listener, debugLogListener, DEFAULT_PORT);
    }

    /**
     * 主建構子：可指定串口路徑（ttyS9 或 Com Port 3）。
     * 由 {@link CashAcceptorFactory} 依系統設定頁選擇的連線方式傳入。
     */
    public SerialCashAcceptorHandler(Context context, MdbEventListener listener, DebugLogListener debugLogListener, String portPath) {
        this.context = context != null ? context.getApplicationContext() : null;
        this.eventListener = listener;
        this.debugListener = debugLogListener;
        this.portPath = (portPath != null && !portPath.isEmpty()) ? portPath : DEFAULT_PORT;
        this.logs = new Logs(SerialCashAcceptorHandler.class);
        loadVirtualCoin1Count();
        synchronized (SerialCashAcceptorHandler.class) {
            instance = this;
        }
    }

    // DebugLogListener 已抽為套件層級共用介面（見 tools/DebugLogListener.java）

    /**
     * 同時寫入日誌檔並觸發 UI Debug 顯示的輔助方法
     */
    private void sendDebug(String message) {
        logs.info(message);
        if (debugListener != null) {
            debugListener.onDebugLog(message);
        }
    }

    /**
     * 連接到指定的串口（ttyS9 或 Com Port 3，由建構子傳入的 portPath 決定）
     */
    public synchronized String connect() {
        // 強制關閉前一個仍在活動的 handler，確保全域同時只有一條輪詢執行緒 / 一個串口連線。
        SerialCashAcceptorHandler prev;
        synchronized (SerialCashAcceptorHandler.class) {
            prev = (sActiveHandler != this) ? sActiveHandler : null;
            sActiveHandler = this;
        }
        if (prev != null) {
            sendDebug("[連線] 偵測到前一個活躍 handler，先強制關閉以避免多執行緒搶串口");
            try { prev.close(); } catch (Exception ignore) { }
        }

        if (serialPort != null) {
            sendDebug("[連線] 串口已連接，執行 MDB 復位與使能序列");
        } else {
            // 連接使用者所選連線對應的串口（USB=/dev/ttyUSB0、Com Port 3=/dev/ttyS3、ttyS9=/dev/ttyS9，由工廠傳入 portPath）。
            String path = this.portPath;
            sendDebug("[連線] 準備連接串口: " + path);

            File file = new File(path);
            if (!file.exists()) {
                sendDebug("[連線] 串口檔案不存在: " + path);
                return "開啟串口失敗: 串口檔案不存在: " + path;
            }

            sendDebug("[連線] 串口權限: canRead=" + file.canRead() + ", canWrite=" + file.canWrite());
            if (!file.canRead() || !file.canWrite()) {
                sendDebug("[連線] 無讀寫權限，嘗試 chmod 666 " + path);
                try {
                    boolean success = runChmodWithTimeout(path, 2000);
                    sendDebug("[連線] chmod 666 結果: " + success);
                } catch (Exception e) {
                    logs.warning(e);
                }
            }

            try {
                // 設定一個不存在的 suPath 以免 licheedev 內部執行 su 卡死
                SerialPort.setSuPath("/system/bin/non_existent_su");
                serialPort = SerialPort.newBuilder(path, 9600).build();
                sendDebug("[連線] ✅ 成功連接串口: " + path);
            } catch (Exception e) {
                sendDebug("[連線] ❌ 開啟串口失敗: " + e.getMessage());
                logs.warning(e);
                return "開啟串口失敗: " + e.getMessage();
            }
        }

        // 每次連線都強制執行復位與初始化，以重設硬體狀態並清空找零殘留
        resetMdbBox();
        detectResolution();
        return "成功連接到 USB 轉接器！";
    }

    /**
     * 復位並使能所有外設 (寫入 00 08 暫存器，設為 0x0001)
     */
    private void resetMdbBox() {
        sendDebug("[復位] 發送 MDB 復位/使能 (00 08 -> 00 01)");
        byte[] cmd = buildModbusWriteCommand(0x0008, 0x0001);
        byte[] resp = sendAndReceive(cmd, 8, 1000);
        if (resp != null && resp.length == 8 && checkCrc(resp)) {
            sendDebug("[復位] ✅ MDB 轉換盒復位/使能成功，等待5秒使其重啟...");
            try {
                Thread.sleep(5000); // 轉換盒復位後需要時間重啟，給予 5 秒等待時間
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        } else {
            sendDebug("[復位] ❌ MDB 轉換盒無回應或 CRC 校驗失敗");
        }

        // 解除紙鈔機的入幣金額限制 (預設受硬幣機零錢限制，導致沒零錢時紙鈔機燈不亮)
        // 00 FA: 0=受零錢控制, 1=不受限制
        sendDebug("[設定] 解除紙鈔機入鈔限制 (00 FA -> 00 01)");
        byte[] cmdFa = buildModbusWriteCommand(0x00FA, 0x0001);
        byte[] respFa = sendAndReceive(cmdFa, 8, 2000);
        if (respFa != null && respFa.length == 8 && checkCrc(respFa)) {
            sendDebug("[設定] ✅ 解除紙鈔限制成功");
        } else {
            sendDebug("[設定] ❌ 解除紙鈔限制無回應");
        }

        // 強制設定分辨率為 1 (00 F7 -> 00 01)
        // 因為台灣幣值較大 (1000元)，若用預設 0.01 分辨率會造成 100000 超出 16bit 最大值 65535 導致溢位 (變成344)
        sendDebug("[設定] 設定系統分辨率為 1 (00 F7 -> 00 01)");
        byte[] cmdF7 = buildModbusWriteCommand(0x00F7, 0x0001);
        byte[] respF7 = sendAndReceive(cmdF7, 8, 2000);
        if (respF7 != null && respF7.length == 8 && checkCrc(respF7)) {
            sendDebug("[設定] ✅ 分辨率設定為 1 成功");
        } else {
            sendDebug("[設定] ❌ 分辨率設定無回應");
        }

        // 解除硬幣機幣種限制 (00 F3 -> FF FF)
        // bit 對應幣種，設為 FFFF (1111111111111111) 代表允許全部 16 種可能的硬幣，避免某些硬幣機將 5/10/50 元定義在高位元導致被拒收
        sendDebug("[設定] 允許所有硬幣幣種 (00 F3 -> FF FF)");
        byte[] cmdF3 = buildModbusWriteCommand(0x00F3, 0xFFFF);
        byte[] respF3 = sendAndReceive(cmdF3, 8, 2000);
        if (respF3 != null && respF3.length == 8 && checkCrc(respF3)) {
            sendDebug("[設定] ✅ 允許所有硬幣成功");
        } else {
            sendDebug("[設定] ❌ 硬幣限制設定無回應");
        }

        // 上位機控制壓鈔暫存模式 (00 F4 -> 00 01)
        sendDebug("[設定] 設定模組為上位機控制壓鈔暫存模式 (00 F4 -> 00 01)");
        byte[] cmdF4 = buildModbusWriteCommand(0x00F4, 0x0001);
        byte[] respF4 = sendAndReceive(cmdF4, 8, 2000);
        if (respF4 != null && respF4.length == 8 && checkCrc(respF4)) {
            sendDebug("[設定] ✅ 設定模組為上位機控制壓鈔暫存模式成功");
        } else {
            sendDebug("[設定] ❌ 設定模組為上位機控制壓鈔暫存模式無回應");
        }

        // 復位後預設「禁止」收幣 (00 07 -> 00 03)
        // bit0=1 禁止硬幣, bit1=1 禁止紙鈔; 0003 表示兩者皆禁止。
        // 原本此處設為 0000(全部啟用)，會在 App 尚未就緒(isDeviceReady=false)前就開始實體收幣，
        // 造成「使能後、就緒前」的吃幣空窗。改為復位後保持禁止，由上層 Dialog 在就緒後才主動使能。
        sendDebug("[設定] 復位後禁止硬幣器與紙幣器 (00 07 -> 00 03)，待上層就緒後再使能");
        acceptorCtrlReg = 0x03; // 同步狀態：復位後兩者皆禁止
        byte[] cmdEnable = buildModbusWriteCommand(0x0007, 0x0003);
        byte[] respEnable = sendAndReceive(cmdEnable, 8, 2000);
        if (respEnable != null && respEnable.length == 8 && checkCrc(respEnable)) {
            sendDebug("[設定] ✅ 禁止硬幣器與紙幣器成功");
        } else {
            sendDebug("[設定] ❌ 禁止硬幣器與紙幣器無回應");
        }
    }

    /**
     * 帶有超時控制的 chmod 執行方法，防止執行緒被 su 永久阻塞
     */
    private boolean runChmodWithTimeout(String path, long timeoutMs) {
        java.lang.Process process = null;
        try {
            process = Runtime.getRuntime().exec(new String[]{"su", "-c", "chmod 666 " + path});

            // 使用背景線程監控超時，防止 process.waitFor() 造成線程卡死
            final java.lang.Process finalProcess = process;
            final boolean[] finished = new boolean[]{false};

            Thread watchThread = new Thread(() -> {
                try {
                    Thread.sleep(timeoutMs);
                    if (!finished[0]) {
                        logs.info("chmod 執行超時，強制銷毀進程");
                        finalProcess.destroy();
                    }
                } catch (InterruptedException ignored) {}
            });
            watchThread.start();

            int exitCode = process.waitFor();
            finished[0] = true;
            watchThread.interrupt();
            return exitCode == 0;
        } catch (Exception e) {
            logs.info("執行 chmod 失敗: " + e.getMessage());
            if (process != null) {
                process.destroy();
            }
            return false;
        }
    }

    /**
     * 偵測設備分辨率
     */
    private void detectResolution() {
        // 發送：01 03 00 F7 00 01 35 F8 (讀取 00 F7 暫存器，長度 0001)
        byte[] cmd = new byte[]{0x01, 0x03, 0x00, (byte) 0xF7, 0x00, 0x01, 0x35, (byte) 0xF8};
        byte[] resp = sendAndReceive(cmd, 7, 300);
        if (resp != null && resp.length == 7 && checkCrc(resp)) {
            int resCode = resp[4] & 0xFF;
            if (resCode == 0) {
                amountDivisor = 100;
                sendDebug("[分辨率] 0.01 (除數 100)");
            } else if (resCode == 1) {
                amountDivisor = 1;
                sendDebug("[分辨率] 1.0 (除數 1)");
            } else if (resCode == 2) {
                amountDivisor = 10;
                sendDebug("[分辨率] 0.1 (除數 10)");
            } else if (resCode == 3) {
                amountDivisor = 100;
                sendDebug("[分辨率] 0.01 (除數 100)");
            }
        } else {
            sendDebug("[分辨率] ❌ 偵測失敗，預設除數為 1");
            amountDivisor = 1;
        }
    }

    /**
     * 舊版寫入資料介面 (在此進行舊指令與新 Modbus 協定的轉換與映射)
     */
    public int writeData(byte[] data) {
        if (serialPort == null) return -1;
        String hex = byteArrayToHexString(data).toUpperCase();
        logs.info("攔截舊指令寫入: " + hex);

        try {
            // 使能/禁止收銀器：改用「狀態位元追蹤」翻成 Modbus 0x0007。
            // 0x0007 語意：bit0=1 禁止硬幣器、bit1=1 禁止紙幣器；0x00=兩者皆使能、0x03=兩者皆禁止。
            // ⚠️ 修正：原本 0CFFFFFFFF/34FFFFFFFF「使能」是把原始位元組直送串口，Modbus 盒不認 → 收銀器永遠沒被使能、投不進幣。
            //         改為與「禁止」對稱地寫 0x0007，且逐位元記住狀態，避免使能其一時把另一個關掉。
            // 開收硬幣 (0CFFFFFFFF)：清除 bit0
            if (hex.equals("0CFFFFFFFF")) {
                acceptorCtrlReg &= ~0x01;
                byte[] cmd = buildModbusWriteCommand(0x0007, acceptorCtrlReg);
                sendDebug("[使能] 開硬幣器，0x0007 -> " + String.format("%04X", acceptorCtrlReg));
                sendAndReceive(cmd, 8, 200);
                return data.length;
            }
            // 開收鈔 (34FFFFFFFF)：清除 bit1
            if (hex.equals("34FFFFFFFF")) {
                acceptorCtrlReg &= ~0x02;
                byte[] cmd = buildModbusWriteCommand(0x0007, acceptorCtrlReg);
                sendDebug("[使能] 開紙幣器，0x0007 -> " + String.format("%04X", acceptorCtrlReg));
                sendAndReceive(cmd, 8, 200);
                return data.length;
            }
            // 關收硬幣 (0C00000000)：設定 bit0
            if (hex.equals("0C00000000")) {
                acceptorCtrlReg |= 0x01;
                byte[] cmd = buildModbusWriteCommand(0x0007, acceptorCtrlReg);
                sendDebug("[禁止] 關硬幣器，0x0007 -> " + String.format("%04X", acceptorCtrlReg));
                sendAndReceive(cmd, 8, 200);
                return data.length;
            }
            // 關收鈔 (3400000000)：設定 bit1
            if (hex.equals("3400000000")) {
                acceptorCtrlReg |= 0x02;
                byte[] cmd = buildModbusWriteCommand(0x0007, acceptorCtrlReg);
                sendDebug("[禁止] 關紙幣器，0x0007 -> " + String.format("%04X", acceptorCtrlReg));
                sendAndReceive(cmd, 8, 200);
                return data.length;
            }

            // 4. 查詢硬幣數量 (0A)
            if (hex.equals("0A")) {
                sendDebug("[攔截查詢] 收到0A查詢，直接返回記憶體快取");
                triggerStockUpdateEvent();
                return data.length;
            }

            // 5. 查詢百鈔數量 (3705)
            if (hex.equals("3705")) {
                sendDebug("[攔截查詢] 收到3705查詢，直接返回記憶體快取");
                triggerNoteUpdateEvent();
                return data.length;
            }

            // 6. 退硬幣指令 (0D 開頭)
            if (hex.startsWith("0D") && hex.length() == 4) {
                char countChar = hex.charAt(2);
                char denomChar = hex.charAt(3);
                int count = Character.digit(countChar, 16);
                int denomIndex = Character.digit(denomChar, 16); // 通道號 (0~3)
                
                if (denomIndex == 0) { // 1 元通道退幣
                    virtualCoin1Count -= count;
                    if (virtualCoin1Count < 0) virtualCoin1Count = 0;
                    cacheCoin1 = virtualCoin1Count; // 更新快取
                    saveVirtualCoin1Count();
                    sendDebug("[退幣] 1元虛擬庫存扣除 " + count + " 個，剩餘: " + virtualCoin1Count);
                } else if (denomIndex == 1) {
                    cacheCoin5 = Math.max(0, cacheCoin5 - count);
                } else if (denomIndex == 2) {
                    cacheCoin10 = Math.max(0, cacheCoin10 - count);
                } else if (denomIndex == 4) { // 50 元硬幣通道為 type 4（與投幣 "08 54" 硬體慣例一致），非 3
                    cacheCoin50 = Math.max(0, cacheCoin50 - count);
                }
                triggerStockUpdateEvent(); // 退幣後主動上報最新快存

                int denom = 0;
                if (denomIndex == 0) denom = 1;
                else if (denomIndex == 1) denom = 5;
                else if (denomIndex == 2) denom = 10;
                else if (denomIndex == 4) denom = 50; // 50 元 = 硬幣 type 4（CashMachineSettings 送 "0D"+張數+"4"）

                int totalRefund = count * denom;
                if (totalRefund > 0) {
                    sendDebug("[退幣] 使用退款總金額 " + totalRefund + " 元 (暫存器 00 06)");
                    byte[] cmd06 = buildModbusWriteCommand(0x0006, totalRefund * amountDivisor);
                    sendAndReceive(cmd06, 8, 300);
                }
                return data.length;
            }

            // 7. 退紙鈔指令 (3706 開頭)
            if (hex.startsWith("3706") && hex.length() == 12) {
                String countStr = hex.substring(10, 12);
                int count = Integer.parseInt(countStr, 16);
                
                cacheNote100 = Math.max(0, cacheNote100 - count); // 同步扣除百鈔庫存快取
                triggerNoteUpdateEvent(); // 退鈔後主動上報
                
                int totalRefund = count * 100;
                if (totalRefund > 0) {
                    byte[] cmd = buildModbusWriteCommand(0x0006, totalRefund * amountDivisor);
                    sendAndReceive(cmd, 8, 300);
                }
                return data.length;
            }

            // 8. 紙幣壓鈔/收鈔 (3503 或 3501)
            if (hex.equals("3503") || hex.equals("3501")) {
                // 暫存紙幣壓鈔 (暫存器 00 04，寫入 00 01)
                byte[] cmd = buildModbusWriteCommand(0x0004, 0x0001);
                sendAndReceive(cmd, 8, 200);
                return data.length;
            }

            // 8.1 暫存紙幣退鈔 (3500)
            if (hex.equals("3500")) {
                // 暫存紙幣退鈔 (暫存器 00 04，寫入 00 02)
                byte[] cmd = buildModbusWriteCommand(0x0004, 0x0002);
                sendAndReceive(cmd, 8, 200);
                return data.length;
            }

            // 9. 其他舊的初始化/狀態查詢，除 0A 外其餘直接透過串口發送，不再忽略
            if (hex.startsWith("3701") || hex.startsWith("3703") || hex.startsWith("3704") || hex.startsWith("34")) {
                OutputStream out = serialPort.getOutputStream();
                out.write(data);
                out.flush();
                return data.length;
            }

            // 若沒有被攔截，預設直接透過串口發送
            OutputStream out = serialPort.getOutputStream();
            out.write(data);
            out.flush();
            return data.length;
        } catch (Exception e) {
            logs.info("writeData 寫入或轉換錯誤: " + e.getMessage());
        }
        return -1;
    }

    /**
     * 讀取串口數據 (相容舊方法，但不常被外部調用，因為已改為背景線程主動 Poll)
     */
    public byte[] readData() {
        if (serialPort != null) {
            try {
                InputStream inputStream = serialPort.getInputStream();
                int available = inputStream.available();
                if (available > 0) {
                    byte[] buffer = new byte[available];
                    int read = inputStream.read(buffer);
                    if (read > 0) {
                        byte[] result = new byte[read];
                        System.arraycopy(buffer, 0, result, 0, read);
                        return result;
                    }
                }
            } catch (Exception e) {
                logs.info("readData 錯誤: " + e.getMessage());
            }
        }
        return null;
    }

    /**
     * 關閉連線
     */
    public synchronized void close() {
        // 一律停止輪詢執行緒並關閉串口。
        // 本專案每次現金交易都由工廠 new 一個獨立 handler，若因「還有 listener」而跳過關閉，
        // 舊的輪詢執行緒與串口不會釋放，下一次交易又開一條 → 多執行緒搶同一個 ttyUSB0，
        // 造成「第一次正常、之後交易越來越常漏抓」。故移除跳過邏輯，確實釋放資源。
        synchronized (SerialCashAcceptorHandler.class) {
            if (sActiveHandler == this) {
                sActiveHandler = null;
            }
        }
        stopReading();
        if (serialPort != null) {
            try {
                // 關閉串口前禁止所有外設 (硬幣和紙鈔皆寫入禁止)
                byte[] cmd = buildModbusWriteCommand(0x0007, 0x0003);
                sendAndReceive(cmd, 8, 200);
                
                serialPort.close();
                sendDebug("[關閉] 已禁止外設並關閉串口");
            } catch (Exception e) {
                sendDebug("[關閉] 錯誤: " + e.getMessage());
            }
            serialPort = null;
        }
    }

    /**
     * 啟動定時輪詢 (Poll) 讀取服務
     */
    public void startReading() {
        if (readThread != null && readThread.isAlive()) {
            return;
        }
        isReading.set(true);
        readThread = new Thread(() -> {
            sendDebug("[Poll] MDB 輪詢線程啟動");
            // 發送：01 03 00 00 00 05 85 C9 (讀取 0000~0004 暫存器，長度共 5 個暫存器)
            byte[] pollCmd = new byte[]{0x01, 0x03, 0x00, 0x00, 0x00, 0x05, (byte) 0x85, (byte) 0xC9};
            byte[] stockCmd = buildModbusReadCommand(0x0021, 8);
            int loopCounter = 0;
            long lastStockReadMs = System.currentTimeMillis(); // 庫存讀取節流計時（避免頻繁庫存讀取造成硬幣輪詢盲區）
            int[] lastRegs = new int[5];
            boolean isFirstPoll = true;
            
            // 初始化邊緣觸發狀態
            lastCoinRegVal = 0;
            lastBillRegVal = 0;
            lastCoinReg0Val = 0;
            lastEscrowRegVal = 0;
            
            // 輪詢啟動時立刻先在背景執行緒中讀取一次真實庫存，填充快取以利 UI 顯示
            updateStockCacheInPollThread();
            
            while (isReading.get()) {
                try {
                    // 發送 Poll 並等待 15 位元組回應（超時改為 800ms，徹底防止設備繁忙時超時丟包導致串流錯位）
                    byte[] response = sendAndReceiveSilent(pollCmd, 15, 800);
                    if (response != null && response.length == 15 && checkCrc(response)) {
                        int[] currentRegs = new int[5];
                        boolean changed = false;
                        for (int i = 0; i < 5; i++) {
                            int hi = response[3 + i * 2] & 0xFF;
                            int lo = response[3 + i * 2 + 1] & 0xFF;
                            currentRegs[i] = (hi << 8) | lo;
                            if (currentRegs[i] != lastRegs[i]) {
                                if (!isFirstPoll) {
                                    changed = true;
                                }
                                lastRegs[i] = currentRegs[i];
                            }
                        }
                        isFirstPoll = false;

                        if (changed) {
                            StringBuilder sb = new StringBuilder();
                            sb.append("[暫存器變化] ");
                            for (int i = 0; i < 5; i++) {
                                sb.append(String.format("[%d]:%04X ", i, currentRegs[i]));
                            }
                            sendDebug(sb.toString());
                        }

                        int coinAmount = currentRegs[1]; // Reg 0001
                        int billAmount = currentRegs[2]; // Reg 0002
                        int escrowAmount = currentRegs[4] / amountDivisor; // Reg 0004
                        
                        // 套用除數取得實際投入金額
                        int actualCoin = coinAmount / amountDivisor;
                        int actualBill = billAmount / amountDivisor;

                        // 紙鈔暫存偵測
                        if (escrowAmount != lastEscrowRegVal) {
                            sendDebug("[暫存] 檢測到暫存區金額變化: " + lastEscrowRegVal + " -> " + escrowAmount);
                            triggerEscrowEvent(escrowAmount);
                        }
                        lastEscrowRegVal = escrowAmount;

                        // 硬幣偵測 - 增量判定與拆分法
                        if (actualCoin > lastCoinRegVal) {
                            int diff = actualCoin - lastCoinRegVal;
                            sendDebug("[入幣] 檢測到硬幣增量: " + diff + " 元 (Reg1: " + lastCoinRegVal + " -> " + actualCoin + ")");
                            handleCoinDiff(diff);
                        }
                        lastCoinRegVal = actualCoin;

                        // 紙鈔偵測 - 增量判定與拆分法
                        if (actualBill > lastBillRegVal) {
                            int diff = actualBill - lastBillRegVal;
                            sendDebug("[入鈔] 檢測到紙鈔增量: " + diff + " 元 (Reg2: " + lastBillRegVal + " -> " + actualBill + ")");
                            handleBillDiff(diff);
                        }
                        lastBillRegVal = actualBill;

                        // 移除原有的 Reg0 低位元組防呆計數，因為在研龍協議中該位元組為狀態碼（例如 3 代表按下退幣桿），以免造成誤加投幣額。
                    }
                    loopCounter++;

                    // 認列已回 Reg1 即時偵測，庫存讀取(0x0021/0x0013)僅供找零保障/UI，每 5 次輪詢讀一次即可，
                    // 讓 poll 絕大多數時間都在讀 0x0000(Reg1)→ 維持高頻、即時抓幣。
                    if (loopCounter >= 5) {
                        loopCounter = 0;
                        updateStockCacheInPollThread();
                    }

                    // 輪詢間隔 5 毫秒
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                } catch (Exception e) {
                    logs.info("輪詢迴圈錯誤: " + e.getMessage());
                    try { Thread.sleep(1000); } catch (Exception ignored) {}
                }
            }
            logs.info("MDB 輪詢線程結束");
        });
        readThread.start();
    }

    /**
     * 停止背景讀取
     */
    public void stopReading() {
        isReading.set(false);
        if (readThread != null) {
            readThread.interrupt();
            readThread = null;
        }
    }

    /**
     * 串口發送並等待回應的互斥保護方法（會將 Hex 顯示於 UI Debug 區）
     */
    private synchronized byte[] sendAndReceive(byte[] request, int expectedResponseLen, int timeoutMs) {
        if (serialPort == null) return null;
        sendDebug("--> " + byteArrayToHexString(request));
        try {
            InputStream in = serialPort.getInputStream();
            OutputStream out = serialPort.getOutputStream();

            // 清除之前的緩衝殘留
            while (in.available() > 0) {
                in.read();
            }

            // 寫入指令
            out.write(request);
            out.flush();

            // 讀取應答
            byte[] response = new byte[expectedResponseLen];
            int bytesRead = 0;
            long startTime = System.currentTimeMillis();
            
            while (bytesRead < expectedResponseLen && (System.currentTimeMillis() - startTime) < timeoutMs) {
                if (in.available() > 0) {
                    int read = in.read(response, bytesRead, expectedResponseLen - bytesRead);
                    if (read > 0) {
                        bytesRead += read;
                    }
                } else {
                    Thread.sleep(10);
                }
            }

            if (bytesRead == expectedResponseLen) {
                sendDebug("<-- " + byteArrayToHexString(response));
                return response;
            } else if (bytesRead > 0) {
                byte[] partial = new byte[bytesRead];
                System.arraycopy(response, 0, partial, 0, bytesRead);
                sendDebug("<-- (部分) " + byteArrayToHexString(partial));
                return partial;
            } else {
                sendDebug("<-- 超時，無回應");
            }
        } catch (Exception e) {
            sendDebug("[錯誤] 通訊異常: " + e.getMessage());
        }
        return null;
    }

    // 輪詢計數器，用於降低 log 輸出頻率
    private int pollLogCounter = 0;
    // 庫存讀取標誌：防止定時庫存讀取與 Poll 衝突
    private volatile boolean isStockReadingInProgress = false;

    /**
     * 輪詢專用版本：僅寫入 debug.txt，不觸發 UI 顯示。並降低寫入頻率 (每 10 次寫一次，約 3 秒)。
     */
    private synchronized byte[] sendAndReceiveSilent(byte[] request, int expectedResponseLen, int timeoutMs) {
        if (serialPort == null) return null;
        pollLogCounter++;
        boolean shouldLog = (pollLogCounter % 10 == 0); // 每 10 次 (約 3 秒) 才寫入一次 log

        if (shouldLog) logs.info("--> " + byteArrayToHexString(request));
        try {
            InputStream in = serialPort.getInputStream();
            OutputStream out = serialPort.getOutputStream();

            while (in.available() > 0) {
                in.read();
            }

            out.write(request);
            out.flush();

            // 固定長度讀取（與 260529 專案一致；走 ttyUSB0 時線路乾淨，讀取快且即時）。
            byte[] response = new byte[expectedResponseLen];
            int bytesRead = 0;
            long startTime = System.currentTimeMillis();

            while (bytesRead < expectedResponseLen && (System.currentTimeMillis() - startTime) < timeoutMs) {
                if (in.available() > 0) {
                    int read = in.read(response, bytesRead, expectedResponseLen - bytesRead);
                    if (read > 0) {
                        bytesRead += read;
                    }
                } else {
                    Thread.sleep(3);
                }
            }

            if (bytesRead == expectedResponseLen) {
                if (shouldLog) logs.info("<-- " + byteArrayToHexString(response));
                return response;
            } else if (bytesRead > 0) {
                byte[] partial = new byte[bytesRead];
                System.arraycopy(response, 0, partial, 0, bytesRead);
                logs.info("[串口警告] 靜默讀取超時，僅收到部分數據: " + byteArrayToHexString(partial));
                return partial;
            } else {
                return null;
            }
        } catch (Exception e) {
            logs.info("[輪詢錯誤] " + e.getMessage());
        }
        return null;
    }

    /**
     * 庫存讀取背景執行緒專用版本：帶 synchronized 但使用較長超時。
     * 由於透過獨立執行緒呼叫，synchronized 鎖只在讀取庫存期間持有，
     * Poll 執行緒在此期間須等待。此方法加了 isStockReadingInProgress 標誌，
     * Poll 執行緒可以偵測此標誌並適當等待，避免互搶串口。
     */
    private synchronized byte[] sendAndReceiveForStock(byte[] request, int expectedResponseLen, int timeoutMs) {
        if (serialPort == null) return null;
        try {
            InputStream in = serialPort.getInputStream();
            OutputStream out = serialPort.getOutputStream();

            // 清除緩衝殘留
            while (in.available() > 0) {
                in.read();
            }

            out.write(request);
            out.flush();

            byte[] response = new byte[expectedResponseLen];
            int bytesRead = 0;
            long startTime = System.currentTimeMillis();
            
            while (bytesRead < expectedResponseLen && (System.currentTimeMillis() - startTime) < timeoutMs) {
                if (in.available() > 0) {
                    int read = in.read(response, bytesRead, expectedResponseLen - bytesRead);
                    if (read > 0) {
                        bytesRead += read;
                    }
                } else {
                    Thread.sleep(5);
                }
            }

            if (bytesRead == expectedResponseLen) {
                return response;
            }
        } catch (Exception e) {
            logs.info("[庫存讀取錯誤] " + e.getMessage());
        }
        return null;
    }

    public boolean isConnected() {
        return serialPort != null;
    }

    /**
     * 讀取轉換盒關鍵狀態暫存器 (0x0000~0x0009)，顯示於 UI debug 區供診斷使用
     */
    public void readDeviceStatus() {
        new Thread(() -> {
            sendDebug("=== 讀取設備狀態暫存器 ===");
            // Modbus 03 讀取 0x0000 起始，共 10 個暫存器
            // 指令: 01 03 00 00 00 0A C5 CD
            byte[] cmd = buildModbusReadCommand(0x0000, 10);
            byte[] resp = sendAndReceive(cmd, 25, 500);
            if (resp != null && resp.length >= 23 && checkCrc(resp)) {
                // 每個暫存器 2 bytes，從 resp[3] 開始
                for (int i = 0; i < 10; i++) {
                    int hi = resp[3 + i * 2] & 0xFF;
                    int lo = resp[3 + i * 2 + 1] & 0xFF;
                    int val = (hi << 8) | lo;
                    String regName = getRegisterName(i);
                    sendDebug(String.format("  [%04X] %s = %04X (%d)", i, regName, val, val));
                }
            } else {
                sendDebug("  ❌ 讀取狀態暫存器失敗或超時");
            }
            sendDebug("=========================");
        }).start();
    }

    private String getRegisterName(int offset) {
        switch (offset) {
            case 0: return "投幣金額";
            case 1: return "紙鈔機狀態";
            case 2: return "硬幣機狀態";
            case 3: return "存量查詢結果";
            case 4: return "壓鈔/收鈔控制";
            case 5: return "保留";
            case 6: return "退幣金額";
            case 7: return "使能控制(bit0=禁硬幣,bit1=禁紙鈔)";
            case 8: return "復位使能(00 01=復位)";
            case 9: return "保留";
            default: return "未知";
        }
    }

    private byte[] buildModbusReadCommand(int startRegister, int count) {
        byte[] cmd = new byte[8];
        cmd[0] = 0x01; // 從機地址
        cmd[1] = 0x03; // 功能碼 (Read Holding Registers)
        cmd[2] = (byte) ((startRegister >> 8) & 0xFF);
        cmd[3] = (byte) (startRegister & 0xFF);
        cmd[4] = (byte) ((count >> 8) & 0xFF);
        cmd[5] = (byte) (count & 0xFF);
        int crc = calculateCRC(cmd, 6);
        cmd[6] = (byte) (crc & 0xFF);
        cmd[7] = (byte) ((crc >> 8) & 0xFF);
        return cmd;
    }

    /**
     * 直接發送退幣/退鈔指令 (指定總金額)
     */
    public void refundAmount(int totalAmount) {
        new Thread(() -> {
            if (totalAmount > 0) {
                sendDebug("[退款] 請求退出總金額 " + totalAmount + " 元...");

                // 自動計算並扣除 1 元的找零數量 (最優找零拆分算法)
                int remain = totalAmount;
                remain = remain % 50;
                remain = remain % 10;
                remain = remain % 5;
                int onesUsed = remain;
                if (onesUsed > 0) {
                    virtualCoin1Count -= onesUsed;
                    if (virtualCoin1Count < 0) virtualCoin1Count = 0;
                    saveVirtualCoin1Count();
                    sendDebug("[退款] 1元虛擬庫存自動扣除 " + onesUsed + " 個，剩餘: " + virtualCoin1Count);
                }

                byte[] cmd = buildModbusWriteCommand(0x0006, totalAmount * amountDivisor);
                byte[] resp = sendAndReceive(cmd, 8, 500);
                if (resp != null && resp.length == 8 && checkCrc(resp)) {
                    sendDebug("[退款] ✅ 退出指令已成功送出");
                } else {
                    sendDebug("[退款] ❌ 退出指令無回應或校驗失敗");
                }
            }
        }).start();
    }

    /**
     * 設定硬幣機與紙鈔機的使能狀態 (0x0007 暫存器)
     * @param enableCoin 是否允許收硬幣
     * @param enableBill 是否允許收紙鈔
     */
    public void setAcceptorEnableStatus(boolean enableCoin, boolean enableBill) {
        new Thread(() -> {
            int status = 0;
            if (!enableCoin) status |= 0x01; // bit0 = 1 (禁止硬幣)
            if (!enableBill) status |= 0x02; // bit1 = 1 (禁止紙鈔)
            
            sendDebug(String.format("[設定] 修改設備使能狀態 (硬幣:%s, 紙鈔:%s)", enableCoin, enableBill));
            byte[] cmd = buildModbusWriteCommand(0x0007, status);
            byte[] resp = sendAndReceive(cmd, 8, 500);
            if (resp != null && resp.length == 8 && checkCrc(resp)) {
                sendDebug("[設定] ✅ 使能狀態修改成功");
            } else {
                sendDebug("[設定] ❌ 使能狀態修改失敗或無回應");
            }
        }).start();
    }

    /**
     * 設定紙鈔機暫存模式
     * @param enable true=上位機控制壓鈔 (Escrow模式), false=模組自動壓鈔
     */
    public void setEscrowMode(boolean enable) {
        new Thread(() -> {
            int value = enable ? 0x0001 : 0x0000;
            sendDebug(String.format("[設定] 修改暫存模式 (上位機控制:%s, 00 F4 -> %04X)", enable, value));
            byte[] cmd = buildModbusWriteCommand(0x00F4, value);
            byte[] resp = sendAndReceive(cmd, 8, 500);
            if (resp != null && resp.length == 8 && checkCrc(resp)) {
                sendDebug("[設定] ✅ 暫存模式修改成功");
            } else {
                sendDebug("[設定] ❌ 暫存模式修改失敗或無回應");
            }
        }).start();
    }

    /**
     * 觸發硬幣事件 (直接傳遞金額給 UI)
     */
    private void triggerCoinEvent(int amount) {
        if (eventListener == null) return;
        eventListener.onEventReceived("COIN_RECEIVED:" + amount);
    }

    /**
     * 觸發紙鈔事件 (直接傳遞金額給 UI)
     */
    private void triggerBillEvent(int amount) {
        if (eventListener == null) return;
        eventListener.onEventReceived("BILL_RECEIVED:" + amount);
    }

    /**
     * 觸發紙鈔暫存事件 (直接傳遞金額給 UI)
     */
    private void triggerEscrowEvent(int amount) {
        if (eventListener == null) return;
        eventListener.onEventReceived("ESCROW_RECEIVED:" + amount);
        
        // 同時發送舊版事件以相容舊有的 CashMachineSettings 頁面
        if (amount == 100) {
            eventListener.onEventReceived("30 90");
        } else if (amount == 500) {
            eventListener.onEventReceived("30 92");
        } else if (amount == 1000) {
            eventListener.onEventReceived("30 93");
        }
    }

    private void loadVirtualCoin1Count() {
        if (context != null) {
            try {
                android.content.SharedPreferences sp = context.getSharedPreferences("MDB_PREFS", Context.MODE_PRIVATE);
                virtualCoin1Count = sp.getInt("virtualCoin1Count", 30);
                // 舊資料可能已飄超過實體上限(如 362)，載入時一併夾回上限，並回存修正值
                if (virtualCoin1Count > MAX_VIRTUAL_COIN1) {
                    virtualCoin1Count = MAX_VIRTUAL_COIN1;
                    sp.edit().putInt("virtualCoin1Count", virtualCoin1Count).apply();
                }
                cacheCoin1 = virtualCoin1Count; // 同步初始化快取
                logs.info("[庫存] 初始化1元虛擬庫存自 SharedPreferences: " + virtualCoin1Count);
            } catch (Exception e) {
                logs.info("[庫存] 載入1元虛擬庫存失敗: " + e.getMessage());
            }
        }
    }

    private void saveVirtualCoin1Count() {
        if (context != null) {
            try {
                android.content.SharedPreferences sp = context.getSharedPreferences("MDB_PREFS", Context.MODE_PRIVATE);
                sp.edit().putInt("virtualCoin1Count", virtualCoin1Count).apply();
                logs.info("[庫存] 儲存1元虛擬庫存至 SharedPreferences: " + virtualCoin1Count);
            } catch (Exception e) {
                logs.info("[庫存] 儲存1元虛擬庫存失敗: " + e.getMessage());
            }
        }
    }

    /**
     * 以硬幣庫存計數器(0x0021)的累計增量認列硬幣（高水位：creditedCoinX 只增不減）。
     * 對串口卡頓/漏讀免疫：即使某幾次庫存讀取失敗，下次讀到的累計總數仍正確。
     * @param denom      面額 (5/10/50)
     * @param addedTotal 相對本次交易基準的累計增加枚數
     */
    private void creditCoinByStock(int denom, int addedTotal) {
        if (addedTotal < 0) return; // 庫存瞬時誤讀為負，忽略（不倒扣）
        int credited = (denom == 5) ? creditedCoin5 : (denom == 10) ? creditedCoin10 : creditedCoin50;
        if (addedTotal > credited) {
            int n = addedTotal - credited;
            if (denom == 5) creditedCoin5 = addedTotal;
            else if (denom == 10) creditedCoin10 = addedTotal;
            else creditedCoin50 = addedTotal;
            sendDebug("[庫存認列] " + denom + "元 x" + n + "（庫存累計+" + addedTotal + "）");
            for (int i = 0; i < n; i++) {
                triggerCoinEvent(denom);
            }
        }
    }

    private void handleCoinDiff(int diff) {
        if (diff <= 0) return;
        // Reg1 邊緣偵測即時認列（走 ttyUSB0 時讀取快又乾淨，Reg1 可即時抓到每一枚，與 260529 一致）。拆分法處理一次多枚。
        int temp = diff;
        int count50 = temp / 50; temp %= 50;
        int count10 = temp / 10; temp %= 10;
        int count5  = temp / 5;  temp %= 5;
        int count1  = temp;

        for (int i = 0; i < count50; i++) triggerCoinEvent(50);
        for (int i = 0; i < count10; i++) triggerCoinEvent(10);
        for (int i = 0; i < count5; i++)  triggerCoinEvent(5);
        cacheCoin50 += count50;
        cacheCoin10 += count10;
        cacheCoin5  += count5;

        if (count1 > 0) {
            virtualCoin1Count += count1;
            if (virtualCoin1Count > MAX_VIRTUAL_COIN1) virtualCoin1Count = MAX_VIRTUAL_COIN1; // 封頂在實體上限
            cacheCoin1 = virtualCoin1Count;
            saveVirtualCoin1Count();
            for (int i = 0; i < count1; i++) triggerCoinEvent(1);
        }
        triggerStockUpdateEvent();
    }

    private void handleBillDiff(int diff) {
        if (diff <= 0) return;
        int temp = diff;
        int count1000 = temp / 1000;
        temp %= 1000;
        int count500 = temp / 500;
        temp %= 500;
        int count100 = temp / 100;
        temp %= 100;

        for (int i = 0; i < count1000; i++) triggerBillEvent(1000);
        for (int i = 0; i < count500; i++)  triggerBillEvent(500);
        for (int i = 0; i < count100; i++)  triggerBillEvent(100);
        
        // 更新快取
        cacheNote100 += count100;
        triggerNoteUpdateEvent(); // 主動上報

        if (temp > 0) {
            triggerBillEvent(temp);
        }
    }

    /**
     * 在 Poll 執行緒計數達到閾值時直接呼叫（在 Poll 執行緒內執行，不開新 Thread）。
     * 透過庫存暫存器 0x0021 讀取各面額實際數量，與前次快照比對，
     * 補偵測因 MDB 轉換盒自行清零 Reg1 而漏讀的硬幣，並通知上層更新 UI。
     */
    private void updateStockCacheInPollThread() {
        // 在 Poll 執行緒內直接讀取硬幣庫存暫存器 0x0021（與 poll 共用同一個 synchronized 鎖，無競爭問題）
        // 指令: 01 03 00 21 00 08 14 06
        byte[] coinStockCmd = new byte[]{0x01, 0x03, 0x00, 0x21, 0x00, 0x08, 0x14, 0x06};
        // 增加超時至 800ms，徹底防止設備繁忙時庫存查詢超時丟包，導致後續輪詢讀到殘留資料（這會導致 Reg1 漏幣）
        byte[] respCoin = sendAndReceiveSilent(coinStockCmd, 21, 800);
        if (respCoin != null && respCoin.length == 21 && checkCrc(respCoin)) {
            // 解析各面額庫存
            int newCoin5 = cacheCoin5, newCoin10 = cacheCoin10, newCoin50 = cacheCoin50;
            for (int i = 0; i < 8; i++) {
                int val   = respCoin[3 + i * 2]     & 0xFF;  // 面額
                int count = respCoin[3 + i * 2 + 1] & 0xFF;  // 數量
                if (val == 5)  newCoin5  = count;
                else if (val == 10) newCoin10 = count;
                else if (val == 50) newCoin50 = count;
            }

            // 硬幣認列已回 Reg1 即時偵測（見 handleCoinDiff）。此處庫存讀取僅更新快取供找零保障判斷與 UI 顯示。
            boolean stockChanged = false;
            if (prevCacheCoin10 >= 0) {
                stockChanged = (newCoin5 != prevCacheCoin5) ||
                               (newCoin10 != prevCacheCoin10) ||
                               (newCoin50 != prevCacheCoin50);
            }

            // 更新快取與前次快照
            cacheCoin5  = newCoin5;
            cacheCoin10 = newCoin10;
            cacheCoin50 = newCoin50;
            
            // 只有在庫存真正有變動，或者是第一次初始化時，才輸出 Log 與觸發更新事件
            if (stockChanged || prevCacheCoin10 < 0) {
                cacheCoin1 = virtualCoin1Count;
                logs.info(String.format("[庫存快取更新] 1元=%d, 5元=%d, 10元=%d, 50元=%d",
                        cacheCoin1, cacheCoin5, cacheCoin10, cacheCoin50));
                triggerStockUpdateEvent();
            }

            prevCacheCoin5  = newCoin5;
            prevCacheCoin10 = newCoin10;
            prevCacheCoin50 = newCoin50;

            // 重置 Reg1 已偵測計數（新一輪開始）
            reg1DetectedCoin5  = 0;
            reg1DetectedCoin10 = 0;
            reg1DetectedCoin50 = 0;
        }

        // --- 讀取紙鈔器零錢存量 (暫存器 0x0013)：找零保障用，非時間關鍵，每 1 秒讀一次即可（避免每週期多一筆讀取拖慢硬幣輪詢）---
        long nowMs = System.currentTimeMillis();
        if (nowMs - lastBillStockReadMs < 1000) {
            return;
        }
        lastBillStockReadMs = nowMs;
        byte[] billStockCmd = new byte[]{0x01, 0x03, 0x00, 0x13, 0x00, 0x02, 0x35, (byte) 0xCE};
        byte[] respBill = sendAndReceiveSilent(billStockCmd, 9, 800);
        if (respBill != null && respBill.length == 9 && checkCrc(respBill)) {
            long val = 0;
            val |= (respBill[3] & 0xFFL) << 24;
            val |= (respBill[4] & 0xFFL) << 16;
            val |= (respBill[5] & 0xFFL) << 8;
            val |= (respBill[6] & 0xFFL);
            
            int actualBillAmount = (int) (val / amountDivisor);
            int noteCount = actualBillAmount / 100; // 假設全部都是百鈔
            
            if (noteCount != cacheNote100) {
                cacheNote100 = noteCount;
                logs.info(String.format("[庫存快取更新] 100元紙鈔張數=%d (總金額: %d)", cacheNote100, actualBillAmount));
                triggerNoteUpdateEvent();
            }
        }
    }

    /**
     * 補偵測漏讀的硬幣：比對庫存 delta 與 Reg1 已偵測數量，若庫存增量超過 Reg1 偵測量，
     * 則對差額補發 COIN_RECEIVED 事件，確保 UI 和金額計算不漏幣。
     */
    private boolean compensateMissedCoins(int denom, int newCount, int prevCount, int reg1Counted) {
        int delta = newCount - prevCount;
        if (delta <= 0) return false; // 庫存沒增加，無需補偵測
        int missed = Math.max(0, delta - reg1Counted);
        if (missed > 0) {
            logs.info(String.format("[補偵測] %d元: 庫存+%d, Reg1已偵測%d枚, 補觸發%d枚",
                    denom, delta, reg1Counted, missed));
            // 更新快取並補觸發事件
            if (denom == 5)  cacheCoin5  += missed;
            else if (denom == 10) cacheCoin10 += missed;
            else if (denom == 50) cacheCoin50 += missed;
            for (int i = 0; i < missed; i++) {
                triggerCoinEvent(denom);
            }
        }
        return true; // 庫存有增加（無論是否漏讀），回傳 true 表示庫存已變動
    }

    private void triggerStockUpdateEvent() {
        if (eventListener == null) return;

        // 新格式：COIN_STOCK:1元,5元,10元,50元 (供 CashPayDialog 的找零保障判斷使用)
        eventListener.onEventReceived(String.format("COIN_STOCK:%d,%d,%d,%d",
                cacheCoin1, cacheCoin5, cacheCoin10, cacheCoin50));

        // 舊格式：0A 01 xx xx xx xx 00 ... (供 CashMachineSettings 的 UI 顯示使用)
        StringBuilder eventSb = new StringBuilder();
        eventSb.append("0A 01");
        eventSb.append(String.format(" %02X", cacheCoin1));   // parts[2] 1元
        eventSb.append(String.format(" %02X", cacheCoin5));   // parts[3] 5元
        eventSb.append(String.format(" %02X", cacheCoin10));  // parts[4] 10元
        eventSb.append(String.format(" %02X", cacheCoin50));  // parts[5] 50元 (CashPayDialogGeneral 讀此)
        eventSb.append(String.format(" %02X", cacheCoin50));  // parts[6] 50元 (CashPayoutManager 讀此，位差相容)
        for (int i = 7; i < 19; i++) {
            eventSb.append(" 00");
        }
        eventListener.onEventReceived(eventSb.toString());
    }

    private void triggerNoteUpdateEvent() {
        if (eventListener == null) return;

        // 新格式：NOTE_STOCK:張數 (供 CashPayDialog 的找零保障判斷使用)
        eventListener.onEventReceived("NOTE_STOCK:" + cacheNote100);

        // 舊格式：37 05 00 xx 00 00... (供 CashMachineSettings UI 顯示)
        StringBuilder eventSb = new StringBuilder();
        eventSb.append("37 05 00");
        eventSb.append(String.format(" %02X", cacheNote100));
        for (int i = 4; i < 35; i++) {
            eventSb.append(" 00");
        }
        eventListener.onEventReceived(eventSb.toString());
    }

    /**
     * 構建 Modbus 寫入單個暫存器指令 (06 功能碼)
     */
    private byte[] buildModbusWriteCommand(int registerAddress, int value) {
        byte[] cmd = new byte[8];
        cmd[0] = 0x01; // 從機地址
        cmd[1] = 0x06; // 功能碼
        cmd[2] = (byte) ((registerAddress >> 8) & 0xFF);
        cmd[3] = (byte) (registerAddress & 0xFF);
        cmd[4] = (byte) ((value >> 8) & 0xFF);
        cmd[5] = (byte) (value & 0xFF);

        int crc = calculateCRC(cmd, 6);
        cmd[6] = (byte) (crc & 0xFF);
        cmd[7] = (byte) ((crc >> 8) & 0xFF);
        return cmd;
    }

    /**
     * 標準 Modbus RTU CRC16 計算方法
     */
    public static int calculateCRC(byte[] buffer, int length) {
        int crc = 0xFFFF;
        for (int i = 0; i < length; i++) {
            crc ^= (buffer[i] & 0xFF);
            for (int j = 0; j < 8; j++) {
                if ((crc & 0x0001) != 0) {
                    crc = (crc >> 1) ^ 0xA001;
                } else {
                    crc >>= 1;
                }
            }
        }
        return crc;
    }

    /**
     * 檢查 Modbus 回應的 CRC
     */
    public static boolean checkCrc(byte[] buffer) {
        if (buffer == null || buffer.length < 3) return false;
        int len = buffer.length;
        int calculated = calculateCRC(buffer, len - 2);
        int received = ((buffer[len - 1] & 0xFF) << 8) | (buffer[len - 2] & 0xFF);
        return calculated == received;
    }

    /**
     * Byte 陣列轉 Hex 字串的輔助方法
     */
    public static String byteArrayToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X", b));
        }
        return sb.toString();
    }
}