package com.unibuy.smartdevice.ui.dialog;

import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.InputType;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.databinding.DialogCheckoutCashPayBinding;
import com.unibuy.smartdevice.exception.ErrorCode;
import com.unibuy.smartdevice.exception.LogsIOException;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.structure.ReportFlowInfoStructure;
import com.unibuy.smartdevice.structure.ReportFreeGiftCodeStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.SimpleCashAcceptorHandler;
import com.unibuy.smartdevice.tools.SerialCashAcceptorHandler;
import com.unibuy.smartdevice.tools.ICashAcceptorHandler;
import com.unibuy.smartdevice.tools.CashAcceptorFactory;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import com.unibuy.smartdevice.tools.Tools;
import com.unibuy.smartdevice.ui.FontendActivity;


import java.util.ArrayList;
import java.util.List;

/**
 * 現金支付對話框，處理現金支付相關的邏輯，包括：
 * - 接收硬幣和紙鈔
 * - 計算找零
 * - 與 MDB 設備通訊
 * - 處理支付流程
 */
public class CashPayDialogGeneral extends DialogAbstract {

    // 輔助類別，用來儲存找零指令資訊
    private static class ChangeCommand {
        public final String command;
        public final int denomination;
        public final int count;

        public ChangeCommand(String command, int denomination, int count) {
            this.command = command;
            this.denomination = denomination;
            this.count = count;
        }
    }

    // 新增：找零檢查結果類別
    private static class ChangeResult {
        public final boolean canChange;
        public final String reason;

        public ChangeResult(boolean canChange, String reason) {
            this.canChange = canChange;
            this.reason = reason;
        }
    }

    private DialogCheckoutCashPayBinding binding;

    int fourthValueDecimal;
    private int inCash;
    private int OrderAmount;
    ICashAcceptorHandler simpleCashAcceptorHandler;
    private int coin50_count;
    private int coin10_count;
    private int coin5_count;
    private int coin1_count;
    private int note100_count;

    // ─── 本筆交易「收」進的各面額張數（累計，供後台 cash_detail 明細）───
    // 注意：與上方 coin50_count/note100_count(機台找零庫存)語意不同，這是顧客投入的統計。
    // 每種面額都經 calculate_amount_update_UI(面額) 入帳，於該處依金額累加。
    private int recvBill1000;
    private int recvBill500;
    private int recvBill100;
    private int recvCoin50;
    private int recvCoin10;
    private int recvCoin5;
    private int recvCoin1;

    int redundantCash;

    int inCashSAVE;

    // 標記位：避免重複關閉串口
    private boolean isSerialPortClosed = false;
    private final Object closeLock = new Object();

    // B600失敗出貨 與正常出貨
    private boolean isDispatchDialogShown = false;

    private boolean isProcessingExit;

    // 標記位：設備狀態封包是否已收到（note100_count 才有效）
    // 收到第一次 35-byte 狀態封包後設為 true，之前禁止投幣
    private boolean isDeviceReady = false;
    private boolean isJammed = false;

    // 新增：待確認接收金額（暫估金額）
    private int pendingAcceptanceAmount = 0;

    // 定時輪詢相關
    private Handler pollingHandler = new Handler(Looper.getMainLooper());
    private Runnable pollingRunnable;
    private boolean isFastPolling = false;

    // 吸鈔超時檢測（防止滾輪卡鈔）
    private Handler acceptTimeoutHandler = new Handler(Looper.getMainLooper());
    private Runnable acceptTimeoutRunnable;

    // 卡鈔超時定時器
    private Handler jamTimeoutHandler = new Handler(Looper.getMainLooper());
    private Runnable jamTimeoutRunnable;

    // 無操作逾時自動關閉（3 分鐘無投幣動作，防止 MDB 串口長時間佔用）
    private static final long IDLE_TIMEOUT_MS = 3 * 60 * 1000L; // 3 分鐘（保留）
    private Handler idleTimeoutHandler = new Handler(Looper.getMainLooper());
    private Runnable idleTimeoutRunnable;

    private static final int CANCEL_COUNTDOWN_SEC = 45; // 取消並退幣倒數秒數（與 layout 標示一致；投幣會重置，歸零自動退幣）
    private android.os.CountDownTimer cancelCountDownTimer; // 可見倒數計時器（更新取消按鈕秒數）
    private AlertDialog initDialog; // 「設備初始化中」提示（持續顯示直到就緒或失敗才關閉，避免重複跳出）

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // 先呼叫父類別的 onCreate 方法，這會執行所有必要的初始化
        super.onCreate(savedInstanceState);
        // 接著，覆寫父類別的設定，將對話框置中
        Window window = getWindow();
        if (window != null) {
            WindowManager.LayoutParams layoutParams = window.getAttributes();
            layoutParams.gravity = Gravity.CENTER;
            window.setAttributes(layoutParams);
        }
        binding = DialogCheckoutCashPayBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // M5 現金支付示意圖（上）/ R5 實體示意圖（下，投幣口 imgPayPhysical）；未設定則保留預設圖
        com.unibuy.smartdevice.ui.tools.UiSkin.apply("M5", binding.imgPayMethod);
        com.unibuy.smartdevice.ui.tools.UiSkin.apply("R5", binding.imgPayPhysical);

        fourthValueDecimal = 0;

        OrderAmount = Integer.parseInt(String.valueOf(getTotalPrice()));
        inCash = 0;

        // 顯示應付總金額
        binding.textPrice.setText("" + OrderAmount);
        binding.textStatus.setText("已投入的金額:" + inCash);
        binding.textStatus2.setText("未投入的金額:" + OrderAmount);

        // 一進入畫面立刻顯示「初始化中」，不等 connect()（含 reset 5 秒）回來，避免消費者以為當機。
        // 以 post 延後到本對話框視窗附著後才顯示，避免提示視窗被主對話框蓋掉而看不到。
        new Handler(Looper.getMainLooper()).post(this::showInitDialog);

        // 實例化 Handler：依系統設定頁選擇的連線方式（USB / Com Port 3 / ttyS9）由工廠決定
        simpleCashAcceptorHandler = CashAcceptorFactory.create(getCtx(),
                event -> new Handler(Looper.getMainLooper()).post(() -> handleMdbEvent(event)));

        // 啟動一個新執行緒來處理連線
        new Thread(() -> {
            String result = simpleCashAcceptorHandler.connect();
            new Handler(Looper.getMainLooper()).post(() -> {
                // Toast.makeText(getCtx(), result, Toast.LENGTH_LONG).show();
                if ("成功連接到 USB 轉接器！".equals(result)) {

                    // 「初始化中」提示已在 onCreate 開頭顯示，持續到設備就緒才關閉（見 handleMdbEvent 就緒處）

                    simpleCashAcceptorHandler.startReading();
                    // 依序發送初始化指令
                    // 先送 reset（0A）確保機器 Recycler 計數狀態同步，再執行完整初始化序列

                    sendMdbCommand("0A", simpleCashAcceptorHandler, 800, () -> {
                        sendMdbCommand("0CFFFFFFFF", simpleCashAcceptorHandler, 1000, () -> {
                            sendMdbCommand("34FFFFFFFF", simpleCashAcceptorHandler, 500, () -> {
                                sendMdbCommand("370100000002", simpleCashAcceptorHandler, 500, () -> {
                                    sendMdbCommand("3703", simpleCashAcceptorHandler, 500, () -> {
                                        sendMdbCommand("3704000003000000000000000000000000000000", simpleCashAcceptorHandler,
                                                1000, () -> {
                                                    sendMdbCommand("3705", simpleCashAcceptorHandler, 500, () -> {
                                                        sendMdbCommand("0A", simpleCashAcceptorHandler, 500, () -> {
                                                            getLogs().info("[初始化] 指令序列發送完畢。");
                                                            if (!isDeviceReady) {
                                                                getLogs().info("[初始化] 設備尚未就緒，啟動快速輪詢狀態...");
                                                                startPollingStatus(true); // 啟動快速輪詢
                                                            } else {
                                                                startPollingStatus(false); // 啟動正常輪詢
                                                            }
                                                        });
                                                    });
                                                });
                                    });
                                });
                            });
                        });
                    });

                } else {
                    dismissInitDialog();
                    messageShow(result, 3000);
                    cancel();
                }
            });
        }).start();

        // 取消按鈕
        binding.buttonCancel.setOnClickListener(v -> dialogCancel());

        // 嘗試退鈔並重試按鈕
        binding.buttonRetryEscrow.setOnClickListener(v -> performRetryEscrow());

        // 測試指令 按中間圖片
        // binding.testOne.setOnClickListener(v ->
        // showMdbCommandDialog(simpleCashAcceptorHandler));
    }

    /**
     * [MQTT Finalize 重構]（自 Luzui 移植並改寫上報層）
     * 顯示出貨對話框。原本先呼叫 B600/B650（reportFlowInfo/postInvoiceByGreen）再延遲 2 秒跳轉，
     * 因 Race Condition 風險且不符 TaiwanStar 新 MQTT 架構，已廢棄。
     * 改為直接將現金支付資訊寫入 MyApp 全域狀態，再跳轉 DispatchDialog，
     * 完整交易資料於所有出貨動作完成後，由 DispatchDialog 統一透過 MQTT Finalize 送出。
     */
    private void showDispatchDialog() {
        if (getCtx() == null) return;

        // 填入現金支付資訊至全域狀態（供 TransactionFinalizeBuilder 使用）
        ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData();
        flowInfo.setFlowType(9); // 現金支付
        flowInfo.setFlowSendInfo("收現金:" + inCashSAVE + "元/找零:" + redundantCash + "元");
        flowInfo.setFlowRequestInfo(""); // 現金無原始請求字串
        flowInfo.setOrderId(Tools.generateTimeBasedUUID());
        flowInfo.setTotalPrice(getTotalPrice());
        flowInfo.setFlowBarCode(""); // 現金交易無條碼
        flowInfo.setFlowStatusType(1); // 1 = 成功
        flowInfo.setGiveChange(redundantCash);
        flowInfo.setUsePoints(0);

        // 現金面額明細（收款張數）→ 供 TransactionFinalizeBuilder 組 cash_detail 上報後台
        flowInfo.setCashBill1000(recvBill1000);
        flowInfo.setCashBill500(recvBill500);
        flowInfo.setCashBill100(recvBill100);
        flowInfo.setCashCoin50(recvCoin50);
        flowInfo.setCashCoin10(recvCoin10);
        flowInfo.setCashCoin5(recvCoin5);
        flowInfo.setCashCoin1(recvCoin1);

        // 現金支付無需等待後端確認，直接跳轉出貨（發票/結案由後續 MQTT Finalize 統一處理）
        if (!isDispatchDialogShown) {
            isDispatchDialogShown = true;
            changeDialog(DispatchDialog.class);
        }
    }
    /// ////

    // 接收設備的回應
    /**
     * 處理 MDB 設備的事件回應
     * 
     * @param event 設備發送的原始事件字串
     */
    private void handleMdbEvent(String event) {
        if (event != null) {
            getLogs().info("[MDB事件接收] 原始事件: " + event);
        }

        // ─── 串口(Modbus) handler 語意事件橋接 ───
        // 串口版 SerialCashAcceptorHandler 對硬幣吐「COIN_RECEIVED:金額」語意事件，
        // 而本對話框的硬幣邏輯是以裝置原始碼「08 5x」分派。此處轉譯成對應原始碼後往下走既有邏輯，
        // 讓 USB / 串口兩種連線共用同一套處理。紙鈔(30 9x)、庫存(0A../37 05..)串口端已合成相容格式，無需轉譯。
        if (event != null && event.startsWith("COIN_RECEIVED:")) {
            try {
                int amt = Integer.parseInt(event.substring("COIN_RECEIVED:".length()).trim());
                switch (amt) {
                    case 50: event = "08 53"; break;
                    case 10: event = "08 52"; break;
                    case 5:  event = "08 51"; break;
                    case 1:  event = "08 50"; break;
                    default:
                        getLogs().info("[MDB橋接] 未知硬幣面額，忽略: " + amt);
                        return;
                }
            } catch (NumberFormatException e) {
                getLogs().info("[MDB橋接] COIN_RECEIVED 解析失敗，忽略: " + event);
                return;
            }
        }

        // 全局卡鈔檢測：只要訊息包含 30 05，即視為卡鈔
        if (event != null && event.contains("30 05")) {
            getLogs().error(ErrorCode.UNKNOWN_ERROR, "偵測到紙鈔機卡鈔！(contains 30 05)");
            handleBillJam();
            return;
        }

        // ─── 就緒前投幣補救 ───
        // 收銀器在初始化(0C/34)時就已實體使能，但 App 需收到狀態封包才會 isDeviceReady=true。
        // 若「投幣機已啟動、App 畫面還沒到就緒」時投幣，原本會被判為未就緒而退回/不計。
        // 這裡：只要在就緒前偵測到「真實投幣/投鈔」事件(08 5x / 30 9x)，代表機器已可運作 →
        // 立即標記就緒並往下正常計入這一枚，避免漏計。
        if (!isDeviceReady && event != null && event.length() >= 5) {
            String c0 = event.substring(0, 5);
            boolean isRealMoney = c0.equals("08 50") || c0.equals("08 51") || c0.equals("08 52")
                    || c0.equals("08 53") || c0.equals("08 54")
                    || c0.equals("30 90") || c0.equals("30 92") || c0.equals("30 93");
            if (isRealMoney) {
                isDeviceReady = true;
                getLogs().info("[初始化] 就緒前偵測到投幣，立即標記就緒並計入這一枚");
                dismissInitDialog();
                if (isFastPolling) {
                    startPollingStatus(false);
                }
                startIdleTimeout();
                // 不 return，往下走正常計入邏輯
            }
        }

        // ─── 紙鈔壓鈔入庫確認 (BILL_RECEIVED) ───
        // 串口 handler 在紙鈔「真正壓鈔入庫」後會吐「BILL_RECEIVED:金額」(來自 Reg2 增量)。
        // 本對話框原改用 30 80/30 B0 確認入箱，但 handler 不吐那兩個事件 → 每張鈔都等不到確認而在 6 秒後誤判卡鈔、且不計入。
        // 比照 Luzui：收到 BILL_RECEIVED 即代表壓鈔成功 → 取消卡鈔逾時、扣除暫估、計入該金額（涵蓋 100/500/1000）。
        if (event != null && event.startsWith("BILL_RECEIVED:")) {
            try {
                int amount = Integer.parseInt(event.substring("BILL_RECEIVED:".length()).trim());
                if (pendingAcceptanceAmount >= amount) {
                    pendingAcceptanceAmount -= amount;
                } else {
                    pendingAcceptanceAmount = 0;
                }
                cancelAcceptTimeoutCheck();
                getLogs().info("[紙鈔] 壓鈔入庫確認 BILL_RECEIVED:" + amount + " → 計入並取消卡鈔逾時，pendingAcceptanceAmount=" + pendingAcceptanceAmount);
                calculate_amount_update_UI(amount);
                sendMdbCommand("3705", simpleCashAcceptorHandler, 0, () -> { }); // 刷新紙鈔循環區庫存
            } catch (NumberFormatException e) {
                getLogs().info("[紙鈔] BILL_RECEIVED 解析失敗，忽略: " + event);
            }
            return;
        }

        if (event.length() >= 5) {
            String code = event.substring(0, 5);

            // ─── 設備尚未就緒：退回所有投入的錢幣，不做任何入帳 ───
            if (!isDeviceReady) {
                switch (code) {
                    case "08 53": // 50元硬幣
                        sendMdbCommand("0D13", simpleCashAcceptorHandler);
                        messageShow("設備初始化中，請稍後再投幣。", 2000);
                        getLogs().info("[初始化] 設備尚未就緒，退回50元硬幣");
                        return;
                    case "08 52": // 10元硬幣
                        sendMdbCommand("0D12", simpleCashAcceptorHandler);
                        messageShow("設備初始化中，請稍後再投幣。", 2000);
                        getLogs().info("[初始化] 設備尚未就緒，退回10元硬幣");
                        return;
                    case "08 51": // 5元硬幣
                        sendMdbCommand("0D11", simpleCashAcceptorHandler);
                        messageShow("設備初始化中，請稍後再投幣。", 2000);
                        getLogs().info("[初始化] 設備尚未就緒，退回5元硬幣");
                        return;
                    case "08 50": // 1元硬幣（通常無法退，記錄即可）
                        messageShow("設備初始化中，請稍後再投幣。", 2000);
                        getLogs().info("[初始化] 設備尚未就緒，退回1元硬幣");
                        return;
                    case "30 90": // 100元鈔票（在暫存區，可退）
                        new Handler(Looper.getMainLooper())
                                .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500);
                        messageShow("設備初始化中，請稍後再投幣。", 2000);
                        getLogs().info("[初始化] 設備尚未就緒，退回100元鈔票");
                        return;
                    case "30 92": // 500元鈔票（在暫存區，可退）
                        new Handler(Looper.getMainLooper())
                                .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500);
                        messageShow("設備初始化中，請稍後再投幣。", 2000);
                        getLogs().info("[初始化] 設備尚未就緒，退回500元鈔票");
                        return;
                    case "30 93": // 1000元鈔票（在暫存區，可退）
                        new Handler(Looper.getMainLooper())
                                .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500);
                        messageShow("設備初始化中，請稍後再投幣。", 2000);
                        getLogs().info("[初始化] 設備尚未就緒，退回1000元鈔票");
                        return;
                    default:
                        // 其他事件（如設備回應封包）繼續正常處理
                        break;
                }
            }
            // ─────────────────────────────────────────────────────────

            switch (code) {

                case "08 53": // 50 元
                    getLogs().info("50元收到訊息:08 53 ");
                    resetIdleTimeout(); // 重設無操作逾時
                    int potential_inCash_50 = inCash + 50;
                    int potential_redundantCash_50 = potential_inCash_50 - OrderAmount;

                    sendMdbCommand("0A", simpleCashAcceptorHandler, 0, () -> {
                    });
                    if (potential_inCash_50 < OrderAmount) {
                        // 還沒湊到商品價格，直接收，但要先檢查「退款保障」
                        ChangeResult refundCheck = can_make_change(potential_inCash_50, false);
                        if (refundCheck.canChange) {
                            calculate_amount_update_UI(50);
                        } else {
                            messageShow("零錢不足，無法提供退款保障，請投入佰元鈔。" + refundCheck.reason, 3000);
                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> sendMdbCommand("0D13", simpleCashAcceptorHandler), 2000);
                            getLogs().info("退款保障不足，拒收50元。 - " + refundCheck.reason);
                        }
                    } else {
                        ChangeResult result = can_make_change(potential_redundantCash_50, false);
                        if (result.canChange) {
                            // 湊到或超過價格，要先檢查能不能找零
                            calculate_amount_update_UI(50);
                        } else {
                            // 找零不足，退幣
                            messageShow("找零不足，無法接受50元硬幣。" + result.reason, 3000);

                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> sendMdbCommand("0D13", simpleCashAcceptorHandler), 2000);

                            getLogs().info("找零不足，無法接受50元硬幣。 退50塊 - " + result.reason);
                        }
                    }
                    break;

                case "08 52": // 10 元

                    getLogs().info("10元收到訊息:08 52 ");
                    resetIdleTimeout(); // 重設無操作逾時

                    int potential_inCash_10 = inCash + 10;
                    int potential_redundantCash_10 = potential_inCash_10 - OrderAmount;
                    sendMdbCommand("0A", simpleCashAcceptorHandler, 0, () -> {
                    });
                    if (potential_inCash_10 < OrderAmount) {
                        // 還沒湊到商品價格，直接收，但要先檢查「退款保障」
                        // 雖然 10 元很少，但為了邏輯一致性，我們統一在收錢前檢查
                        ChangeResult refundCheck = can_make_change(potential_inCash_10, false);
                        if (refundCheck.canChange) {
                            calculate_amount_update_UI(10);
                        } else {
                            messageShow("零錢不足，無法提供退款保障，請投入佰元鈔。" + refundCheck.reason, 3000);
                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> sendMdbCommand("0D12", simpleCashAcceptorHandler), 2000);
                            getLogs().info("退款保障不足，拒收10元。 - " + refundCheck.reason);
                        }
                    } else {
                        ChangeResult result = can_make_change(potential_redundantCash_10, false);
                        if (result.canChange) {
                            calculate_amount_update_UI(10);
                        } else {
                            messageShow("找零不足，無法接受10元硬幣。" + result.reason, 3000);

                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> sendMdbCommand("0D12", simpleCashAcceptorHandler), 2000);
                            getLogs().info("找零不足，無法接受10元硬幣。 退10塊 - " + result.reason);
                        }
                    }
                    break;

                case "08 51":
                    getLogs().info("5元收到訊息:08 51 ");
                    resetIdleTimeout(); // 重設無操作逾時
                    int potential_inCash_5 = inCash + 5;
                    int potential_redundantCash_5 = potential_inCash_5 - OrderAmount;
                    if (potential_inCash_5 < OrderAmount) {
                        // 還沒湊到商品價格，直接收，但要先檢查「退款保障」
                        ChangeResult refundCheck = can_make_change(potential_inCash_5, false);
                        if (refundCheck.canChange) {
                            calculate_amount_update_UI(5);
                        } else {
                            messageShow("零錢不足，無法提供退款保障，請投入佰元鈔。" + refundCheck.reason, 3000);
                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> sendMdbCommand("0D11", simpleCashAcceptorHandler), 2000);
                            getLogs().info("退款保障不足，拒收5元。 - " + refundCheck.reason);
                        }
                    } else {
                        ChangeResult result = can_make_change(potential_redundantCash_5, false);
                        if (result.canChange) {
                            // 湊到或超過價格，要先檢查能不能找零
                            calculate_amount_update_UI(5);
                        } else {
                            // 找零不足，退幣
                            messageShow("找零不足，無法接受5元硬幣。" + result.reason, 3000);
                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> sendMdbCommand("0D11", simpleCashAcceptorHandler), 2000);
                            getLogs().info("找零不足，無法接受5元硬幣。 退五塊 - " + result.reason);
                        }
                    }
                    break;

                case "08 50":
                    getLogs().info("1元收到訊息:08 50 ");
                    calculate_amount_update_UI(1);
                    break;

                /// 50元 硬幣機滿了 掉進去內部儲存箱
                case "08 43":
                    getLogs().info("50元收到訊息:50元 硬幣機滿了 掉進去內部儲存箱");
                    int potential_inCash_50_full = inCash + 50;
                    int potential_redundantCash_50_full = potential_inCash_50_full - OrderAmount;
                    if (potential_inCash_50_full <= OrderAmount) {
                        // 還沒湊到或剛好等於商品價格，直接收
                        calculate_amount_update_UI(50);
                    } else {
                        ChangeResult result = can_make_change(potential_redundantCash_50_full, false);
                        if (result.canChange) {
                            // 超過價格，且可以找零
                            calculate_amount_update_UI(50);
                        } else {
                            // 找零不足，但因為已經掉入內部儲存箱，無法退幣
                            messageShow("找零不足，無法接受50元硬幣。" + result.reason, 3000);
                            sendMdbCommand("0D13", simpleCashAcceptorHandler);
                            getLogs().info("找零不足，無法接受50元硬幣。 - " + result.reason);
                        }
                    }
                    break;
                /// 10元 硬幣機滿了 掉進去內部儲存箱
                case "08 42":
                    getLogs().info("10元收到訊息:10元 硬幣機滿了 掉進去內部儲存箱");
                    int potential_inCash_10_full = inCash + 10;
                    int potential_redundantCash_10_full = potential_inCash_10_full - OrderAmount;
                    if (potential_inCash_10_full <= OrderAmount) {
                        calculate_amount_update_UI(10);
                    } else {
                        ChangeResult result = can_make_change(potential_redundantCash_10_full, false);
                        if (result.canChange) {
                            calculate_amount_update_UI(10);
                        } else {
                            messageShow("找零不足，無法接受10元硬幣。" + result.reason, 3000);
                            sendMdbCommand("0D12", simpleCashAcceptorHandler);
                            getLogs().info("找零不足，無法接受10元硬幣。 - " + result.reason);
                        }
                    }
                    break;

                /// 5元 硬幣機滿了 掉進去內部儲存箱
                case "08 41":
                    getLogs().info("5元收到訊息:5元 硬幣機滿了 掉進去內部儲存箱");
                    int potential_inCash_5_full = inCash + 5;
                    int potential_redundantCash_5_full = potential_inCash_5_full - OrderAmount;
                    if (potential_inCash_5_full <= OrderAmount) {
                        calculate_amount_update_UI(5);
                    } else {
                        ChangeResult result = can_make_change(potential_redundantCash_5_full, false);
                        if (result.canChange) {
                            calculate_amount_update_UI(5);
                        } else {
                            messageShow("找零不足，無法接受5元硬幣。" + result.reason, 3000);
                            sendMdbCommand("0D11", simpleCashAcceptorHandler);
                            getLogs().info("找零不足，無法接受5元硬幣。 - " + result.reason);
                        }
                    }
                    break;

                /// 1元 硬幣機滿了 掉進去內部儲存箱
                case "08 40":
                    getLogs().info("1元收到訊息:1元 硬幣機滿了 掉進去內部儲存箱");
                    calculate_amount_update_UI(1);

                    break;

                case "30 90": // 100元
                    getLogs().info("收到100元鈔票 訊息:30 90 ");
                    getLogs().info("收到100元鈔票 暫存區 先檢查是否足夠");
                    resetIdleTimeout(); // 重設無操作逾時

                    // 計算如果接受這張之後會發生什麼
                    int potential_inCash_100 = inCash + 100;
                    int changeNeeded100 = potential_inCash_100 - OrderAmount; // 正數表示需要找零

                    /// OrderAmount 是訂單金額
                    /// changeNeeded100 要找零金額

                    // 如果會需要找零，先檢查能不能找零；不能就拒絕
                    if (changeNeeded100 > 0) {
                        ChangeResult result = can_make_change(changeNeeded100, true);
                        if (!result.canChange) {
                            // 無法找零 -> 拒收
                            messageShow("找零不足，無法接受100元鈔票。" + result.reason, 3000);
                            getLogs().info("找零不足，無法接受100元鈔票。 - " + result.reason);
                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500);
                        } else {

                            getLogs().info("可以找零 -> 接受100進循環區");
                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> {
                                        sendMdbCommand("3503", simpleCashAcceptorHandler);
                                        pendingAcceptanceAmount += 100;
                                        getLogs().info("[暫估金額] 決定接收100元，pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount);
                                        startAcceptTimeoutCheck();
                                    }, 800);
                        }
                    } else {

                        getLogs().info("不需要找零 -> 接受100進循環區A222");
                        new Handler(Looper.getMainLooper())
                                .postDelayed(() -> {
                                    sendMdbCommand("3503", simpleCashAcceptorHandler);
                                    pendingAcceptanceAmount += 100;
                                    getLogs().info("[暫估金額] 決定接收100元，pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount);
                                    startAcceptTimeoutCheck();
                                }, 800);
                    }
                    break;

                /// 進去循環區
                case "30 80": //
                    getLogs().info("收到100元鈔票 30 80 真正進入循環區");
                    if (pendingAcceptanceAmount >= 100) {
                        pendingAcceptanceAmount -= 100;
                    } else {
                        pendingAcceptanceAmount = 0;
                    }
                    cancelAcceptTimeoutCheck();
                    getLogs().info("[暫估金額] 成功入箱，pendingAcceptanceAmount減少至: " + pendingAcceptanceAmount);
                    calculate_amount_update_UI(100);
                    sendMdbCommand("3705", simpleCashAcceptorHandler, 0, () -> { // 刷新紙鈔機內容
                    });
                    break;

                /// 進去循環區
                case "30 B0": //
                    getLogs().info("收到100元鈔票 30 B0 真正進入循環區");
                    if (pendingAcceptanceAmount >= 100) {
                        pendingAcceptanceAmount -= 100;
                    } else {
                        pendingAcceptanceAmount = 0;
                    }
                    cancelAcceptTimeoutCheck();
                    getLogs().info("[暫估金額] 成功入箱，pendingAcceptanceAmount減少至: " + pendingAcceptanceAmount);
                    calculate_amount_update_UI(100);
                    sendMdbCommand("3705", simpleCashAcceptorHandler, 0, () -> { // 刷新紙鈔機內容
                    });
                    break;

                case "30 92": // 500元
                    int potential_inCash_500 = inCash + 500;
                    getLogs().info("收到500元鈔票 訊息30 92");
                    resetIdleTimeout(); // 重設無操作逾時
                    int changeNeeded500 = potential_inCash_500 - OrderAmount;

                    if (changeNeeded500 > 0) {
                        // 會需要找零 → 先用 can_make_change() 判斷（已含 note100_count 檢查）
                        ChangeResult result = can_make_change(changeNeeded500, true);
                        if (!result.canChange) {
                            getLogs().info("找零不足，無法接受500元鈔票。 - " + result.reason);
                            messageShow("找零不足，無法接受500元鈔票。" + result.reason, 3000);

                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500);
                        } else {
                            getLogs().info("接受500元鈔票。進錢箱（循環區百鈔庫存: " + note100_count + "張）");
                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> {
                                        sendMdbCommand("3501", simpleCashAcceptorHandler);
                                        pendingAcceptanceAmount += 500;
                                        getLogs().info("[暫估金額] 決定接收500元，pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount);
                                        startAcceptTimeoutCheck();
                                    }, 1300);
                        }
                    } else {
                        // 不需要找零（還沒付清或剛好）
                        if (potential_inCash_500 < OrderAmount) {
                            // ✅ 尚未付清：檢查「退款保障」，確保萬一取消時退得出錢
                            ChangeResult refundCheck = can_make_change(potential_inCash_500, true);
                            if (refundCheck.canChange) {
                                getLogs().info("收到500元鈔票，尚未付清但退款保障充足，準備進錢箱");
                                new Handler(Looper.getMainLooper())
                                        .postDelayed(() -> {
                                            sendMdbCommand("3501", simpleCashAcceptorHandler);
                                            pendingAcceptanceAmount += 500;
                                            getLogs().info("[暫估金額] 決定接收500元，pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount);
                                            startAcceptTimeoutCheck();
                                        }, 1300);
                            } else {
                                getLogs().info("退款保障不足，拒收500元鈔票。 - " + refundCheck.reason);
                                messageShow("零錢不足，無法提供退款保障，請投入佰元鈔。" + refundCheck.reason, 3000);
                                new Handler(Looper.getMainLooper())
                                        .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500);
                            }
                        } else {
                            // ✅ 剛好付清：直接接受，準備出貨
                            getLogs().info("收到500元鈔票，剛好付清，準備進錢箱出貨");
                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> {
                                        sendMdbCommand("3501", simpleCashAcceptorHandler);
                                        pendingAcceptanceAmount += 500;
                                        getLogs().info("[暫估金額] 決定接收500元，pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount);
                                        startAcceptTimeoutCheck();
                                    }, 1300);
                        }
                    }
                    break;

                /// 進去循環區
                case "30 82": //
                    getLogs().info("500元鈔票 30 82真正進入錢箱");
                    if (pendingAcceptanceAmount >= 500) {
                        pendingAcceptanceAmount -= 500;
                    } else {
                        pendingAcceptanceAmount = 0;
                    }
                    cancelAcceptTimeoutCheck();
                    getLogs().info("[暫估金額] 成功入箱，pendingAcceptanceAmount減少至: " + pendingAcceptanceAmount);
                    calculate_amount_update_UI(500);
                    break;

                case "30 93": // 1000元
                    int potential_inCash_1000 = inCash + 1000;
                    int changeNeeded1000 = potential_inCash_1000 - OrderAmount;
                    resetIdleTimeout(); // 重設無操作逾時

                    if (changeNeeded1000 > 0) {
                        // 會需要找零 → 先用 can_make_change() 判斷（已含 note100_count 檢查）
                        ChangeResult result = can_make_change(changeNeeded1000, true);
                        if (!result.canChange) {
                            messageShow("找零不足，無法接受1000元鈔票。" + result.reason, 3000);
                            Log.d("MDB_EVENT", "1000元找零庫存不足，拒收。 - " + result.reason);
                            getLogs().info("1000元找零庫存不足，拒收。 - " + result.reason);
                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500);
                        } else {

                            getLogs().info("接受1000元鈔票。進錢箱（循環區百鈔庫存: " + note100_count + "張）");
                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> {
                                        sendMdbCommand("3501", simpleCashAcceptorHandler);
                                        pendingAcceptanceAmount += 1000;
                                        getLogs().info("[暫估金額] 決定接收1000元，pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount);
                                        startAcceptTimeoutCheck();
                                    }, 1300);
                        }
                    } else {
                        // 不需要找零（還沒付清或剛好）
                        if (potential_inCash_1000 < OrderAmount) {
                            // ✅ 尚未付清：檢查「退款保障」，確保萬一取消時退得出錢
                            ChangeResult refundCheck = can_make_change(potential_inCash_1000, true);
                            if (refundCheck.canChange) {
                                getLogs().info("收到1000元鈔票，尚未付清但退款保障充足，準備進錢箱");
                                new Handler(Looper.getMainLooper())
                                        .postDelayed(() -> {
                                            sendMdbCommand("3501", simpleCashAcceptorHandler);
                                            pendingAcceptanceAmount += 1000;
                                            getLogs().info("[暫估金額] 決定接收1000元，pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount);
                                            startAcceptTimeoutCheck();
                                        }, 1300);
                            } else {
                                getLogs().info("1000元找零庫存不足，拒收。 - " + refundCheck.reason);
                                messageShow("零錢不足，無法提供退款保障，請投入佰元鈔。" + refundCheck.reason, 3000);
                                new Handler(Looper.getMainLooper())
                                        .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500);
                            }
                        } else {
                            // ✅ 剛好付清：直接接受，準備出貨
                            getLogs().info("收到1000元鈔票，剛好付清，準備進錢箱出貨");
                            new Handler(Looper.getMainLooper())
                                    .postDelayed(() -> {
                                        sendMdbCommand("3501", simpleCashAcceptorHandler);
                                        pendingAcceptanceAmount += 1000;
                                        getLogs().info("[暫估金額] 決定接收1000元，pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount);
                                        startAcceptTimeoutCheck();
                                    }, 1300);
                        }
                    }
                    break;

                /// 進去循環區
                case "30 83": //
                    getLogs().info("收到1000元鈔票  30 83真正進入錢箱");
                    if (pendingAcceptanceAmount >= 1000) {
                        pendingAcceptanceAmount -= 1000;
                    } else {
                        pendingAcceptanceAmount = 0;
                    }
                    cancelAcceptTimeoutCheck();
                    getLogs().info("[暫估金額] 成功入箱，pendingAcceptanceAmount減少至: " + pendingAcceptanceAmount);
                    calculate_amount_update_UI(1000);
                    break;

                case "30 05": // 紙鈔機卡鈔 (Validator Jammed)
                    getLogs().error(ErrorCode.UNKNOWN_ERROR, "偵測到紙鈔機卡鈔！");
//                    handleBillJam();
                    break;

            }

            // 處理狀態封包 (VMC_52_CHECK_VMC_STATUS)
            // 正常 ASCII 封包長度預期為 40 (含標頭與校驗)
            int eventLen = event != null ? event.trim().split(" ").length : 0;
            if (event != null && (eventLen == 35 || eventLen == 40)) {
                String[] parts = event.trim().split(" ");
                boolean isValidFormat = true;
                for (String part : parts) {
                    if (part.length() != 2) {
                        isValidFormat = false;
                        break;
                    }
                }
                if (isValidFormat) {
                    try {
                        // 根據封包長度判斷數據起始位置
                        // 長度 40: FA FB 52 23 [DATA...] XOR -> DATA[0] 在 parts[4]
                        // 長度 35: [DATA...] -> DATA[0] 在 parts[0]
                        int dataStartIndex = (eventLen == 40) ? 4 : 0;
                        // 根據 CashMachineSettings，佰元鈔庫存是在 vmcData 的第 4 個位元組 (index 3)
                        int noteIndex = dataStartIndex + 3;
                        
                        if (noteIndex < parts.length) {
                            String noteValueHex = parts[noteIndex];
                            // 根據 CashMachineSettings 邏輯，直接使用 Hex 解析 (Integer.parseInt(..., 16))
                            int val = Integer.parseInt(noteValueHex, 16);
                            
                            // 暫存區（Recycler）物理上限為 30 張百元鈔
                            if (val >= 0 && val <= 30) {
                                note100_count = val;
                                getLogs().info("循環區百鈔庫存更新: " + note100_count + "張（hex: " + noteValueHex + "，來源索引: " + noteIndex + "）");
                            } else {
                                Log.w("MDB_EVENT", "循環區百鈔數值異常: " + val + "，保留上次庫存: " + note100_count);
                            }
                        }
                        // ✅ 收到第一次有效狀態封包，標記設備已就緒
                        if (!isDeviceReady) {
                            isDeviceReady = true;
                            getLogs().info("[初始化] 設備狀態封包已收到，開放投幣（循環區百鈔: " + note100_count + "張）");
                            
                            // 如果正在快速輪詢，切換回正常輪詢
                            if (isFastPolling) {
                                startPollingStatus(false);
                            }

                            new Handler(Looper.getMainLooper()).post(() -> {
                                dismissInitDialog(); // 就緒才關閉「初始化中」提示（只顯示一次，不再殘留重複跳出）
                                messageShow("設備就緒，請投入紙鈔或硬幣。", 3000);
                                // 設備就緒後啟動可見倒數（45 秒無投幣自動退幣關閉）
                                startIdleTimeout();
                            });

                            // 觸發自癒退款檢查
                            checkAndProcessPendingRefund();
                        }
                        return;
                    } catch (NumberFormatException e) {
                        Log.e("MDB_EVENT", "十六進制轉換失敗，不處理此事件: " + e.getMessage());
                    }
                }
            }

            if (event != null && event.trim().split(" ").length == 19) {
                String[] parts = event.trim().split(" ");
                boolean isValidFormat = true;
                for (String part : parts) {
                    if (part.length() != 2) {
                        isValidFormat = false;
                        break;
                    }
                }
                if (isValidFormat) {
                    try {
                        int oneYuanCount = Integer.parseInt(parts[2], 16);
                        int fiveYuanCount = Integer.parseInt(parts[3], 16);
                        int tenYuanCount = Integer.parseInt(parts[4], 16);
                        int fiftyYuanCount = Integer.parseInt(parts[5], 16);
                        coin1_count = oneYuanCount;
                        coin5_count = fiveYuanCount;
                        coin10_count = tenYuanCount;
                        coin50_count = fiftyYuanCount;
                        String message = String.format(
                                "硬幣機庫存已更新：1元=%d, 5元=%d, 10元=%d, 50元=%d",
                                coin1_count, coin5_count, coin10_count, coin50_count);
                        // Toast.makeText(getCtx(), message, Toast.LENGTH_LONG).show();
                        getLogs().info("[硬幣庫存] " + message);
                        return;
                    } catch (NumberFormatException e) {
                        Log.e("MDB_EVENT", "十六進制轉換失敗，不處理此事件: " + e.getMessage());
                    }
                }
            }
        }
        Log.d("MDB_EVENT", "收到事件: " + event);
        // Toast.makeText(getCtx(), "收到事件: " + event, Toast.LENGTH_LONG).show();
    }

    /**
     * 檢查是否有足夠的零錢可以找
     * 
     * @param changeAmount 需要找零的金額
     * @return 是否可以找零
     */
    // private boolean can_make_change(int changeAmount) {
    //
    // int note100_temp = note100_count;
    // int coin50_temp = coin50_count;
    // int coin10_temp = coin10_count;
    // int coin5_temp = coin5_count;
    // int coin1_temp = coin1_count;
    //
    // if (changeAmount >= 100 && note100_temp > 0) {
    // int hundredsToGive = changeAmount / 100;
    // int actualGive = Math.min(hundredsToGive, note100_temp);
    // changeAmount -= actualGive * 100;
    // note100_temp -= actualGive;
    // }
    // if (changeAmount >= 50 && coin50_temp > 0) {
    // int fiftiesToGive = changeAmount / 50;
    // int actualGive = Math.min(fiftiesToGive, coin50_temp);
    // changeAmount -= actualGive * 50;
    // coin50_temp -= actualGive;
    // }
    // if (changeAmount >= 10 && coin10_temp > 0) {
    // int tensToGive = changeAmount / 10;
    // int actualGive = Math.min(tensToGive, coin10_temp);
    // changeAmount -= actualGive * 10;
    // coin10_temp -= actualGive;
    // }
    // if (changeAmount >= 5 && coin5_temp > 0) {
    // int fivesToGive = changeAmount / 5;
    // int actualGive = Math.min(fivesToGive, coin5_temp);
    // changeAmount -= actualGive * 5;
    // coin5_temp -= actualGive;
    // }
    // if (changeAmount > 0 && coin1_temp > 0) {
    // int onesToGive = changeAmount / 1;
    // int actualGive = Math.min(onesToGive, coin1_temp);
    // changeAmount -= actualGive * 1;
    // coin1_temp -= actualGive;
    // }
    // return changeAmount == 0;
    // }

    private ChangeResult can_make_change(int changeAmount, boolean isBanknote) {
        int originalChangeAmount = changeAmount;
        int note100_temp = note100_count;
        int coin50_temp = coin50_count;
        int coin10_temp = coin10_count;
        int coin5_temp = coin5_count;
        int coin1_temp = coin1_count;

        // 1. 優先處理百鈔（如果是紙鈔退款，必須有足夠百鈔處理 100 元以上的部分）
        if (changeAmount >= 100 && note100_temp > 0) {
            int hundredsToGive = changeAmount / 100;
            int actualGive = Math.min(hundredsToGive, note100_temp);
            changeAmount -= actualGive * 100;
            note100_temp -= actualGive;
        }

        // ✅ 新增：紙鈔退款保障檢查。如果 100 元面額部分無法用百鈔支付，則失敗
        if (isBanknote && changeAmount >= 100) {
            return new ChangeResult(false, String.format("佰元鈔庫存不足(僅有%d張)，無法提供紙鈔退款保障", note100_count));
        }

        // 2. 處理 50 元硬幣（如果是紙鈔退款，禁止使用 50 元硬幣）
        if (!isBanknote && changeAmount >= 50 && coin50_temp > 0) {
            int fiftiesToGive = changeAmount / 50;
            int actualGive = Math.min(fiftiesToGive, coin50_temp);
            changeAmount -= actualGive * 50;
            coin50_temp -= actualGive;
        } else if (!isBanknote && changeAmount >= 50) {
            return new ChangeResult(false, String.format("需要%d個50元硬幣，但庫存不足(僅有%d個)",
                    (changeAmount / 50), coin50_temp));
        } else if (isBanknote && changeAmount >= 50) {
            // 紙鈔退款雖然不能用 50 元，但如果後續 10 元硬幣足夠，仍可繼續檢查
            // 此處不做 return false，讓 10 元硬幣邏輯去處理
            getLogs().debug("紙鈔退款跳過 50 元硬幣檢查，改用 10 元硬幣嘗試");
        }

        if (changeAmount >= 10 && coin10_temp > 0) {
            int tensToGive = changeAmount / 10;
            int actualGive = Math.min(tensToGive, coin10_temp);
            changeAmount -= actualGive * 10;
            coin10_temp -= actualGive;
        } else if (changeAmount >= 10) {
            return new ChangeResult(false, String.format("需要%d個10元硬幣，但庫存不足(僅有%d個)",
                    (changeAmount / 10), coin10_temp));
        }

        if (changeAmount >= 5 && coin5_temp > 0) {
            int fivesToGive = changeAmount / 5;
            int actualGive = Math.min(fivesToGive, coin5_temp);
            changeAmount -= actualGive * 5;
            coin5_temp -= actualGive;
        } else if (changeAmount >= 5) {
            return new ChangeResult(false, String.format("需要%d個5元硬幣，但庫存不足(僅有%d個)",
                    (changeAmount / 5), coin5_temp));
        }

        if (changeAmount > 0 && coin1_temp > 0) {
            int onesToGive = changeAmount / 1;
            int actualGive = Math.min(onesToGive, coin1_temp);
            changeAmount -= actualGive * 1;
            coin1_temp -= actualGive;
        } else if (changeAmount > 0) {
            return new ChangeResult(false, String.format("需要%d個1元硬幣，但庫存不足(僅有%d個，還差%d元)",
                    changeAmount, coin1_temp, changeAmount));
        }

        if (changeAmount != 0) {
            return new ChangeResult(false, String.format("無法完全找零，還差%d元", changeAmount));
        }

        return new ChangeResult(true, "");
    }

    /**
     * 計算並更新 UI 顯示的金額 進行B600以及出貨
     * 
     * @param amount 投入的金額
     */
    private void calculate_amount_update_UI(int amount) {

        // 依面額累計「收」進張數（面額值彼此唯一：1/5/10/50 硬幣、100/500/1000 紙鈔）。
        // 這是本方法唯一的入帳點，故所有投入面額都會被統計；供結案時組 cash_detail。
        switch (amount) {
            case 1000: recvBill1000++; break;
            case 500:  recvBill500++;  break;
            case 100:  recvBill100++;  break;
            case 50:   recvCoin50++;   break;
            case 10:   recvCoin10++;   break;
            case 5:    recvCoin5++;    break;
            case 1:    recvCoin1++;    break;
            default:   /* 非標準面額不統計 */ break;
        }

        inCash += amount;
        int remaining = OrderAmount - inCash; // 未投入的錢
        binding.textStatus.setText("已投入的金額: " + inCash);
        binding.textStatus2.setText("未投入的金額: " + remaining);

        if (inCash >= OrderAmount) {

            redundantCash = inCash - OrderAmount; // 要找的錢
            inCashSAVE = inCash;
            binding.textStatus2.setText("付款完成！即將找零:" + redundantCash + "元,並準備出貨");

            binding.buttonCancel.setVisibility(View.GONE);

            // 修改這裡：傳入一個回調函數，在找零完成後執行，並允許百鈔退幣 (allowBanknote=true)
            performChange(redundantCash, true, () -> {
                // 這個回調會在找零完成後執行

                // 顯示出貨畫面進行B600 602
                showDispatchDialog();
                // 使用 父類的取消畫面
                cancel();

            });

            inCash = 0;
        }

    }

    /// ///// 找零方法 有三個 //////////// ///// 找零方法 有三個 /////////

    /**
     * 執行找零操作，支援 allowBanknote 以在紙鈔機卡鈔時強制進行純硬幣找零
     * 
     * @param changeAmount  需要找零的金額
     * @param allowBanknote 是否允許吐百元鈔
     * @param onComplete    找零完成後的回調
     */
    private void performChange(int changeAmount, boolean allowBanknote, Runnable onComplete) {

        int remainingChange = changeAmount; // 剩餘要找的錢

        // -----------------------------
        // 100元紙鈔 - 先檢查是否需要吐鈔
        // -----------------------------
        int hundredsToGive = 0;
        if (allowBanknote) {
            hundredsToGive = Math.min(remainingChange / 100, note100_count);
        }
        
        if (hundredsToGive > 0) {
            // ✅ 關鍵修正：先執行紙鈔機激活準備（按測試頁面的精確延遲時間）
            getLogs().info("[現金支付] 需要吐百鈔，先激活紙鈔機");

            // 按照測試頁面的延遲時間：0ms, 1000ms, 600ms, 1000ms
            final int finalHundredsToGive = hundredsToGive;
            sendMdbCommand("3400000000", simpleCashAcceptorHandler, 0, () -> {
                sendMdbCommand("370100000002", simpleCashAcceptorHandler, 1000, () -> {
                    sendMdbCommand("3703", simpleCashAcceptorHandler, 600, () -> {
                        sendMdbCommand("3704000003000000000000000000000000000000", simpleCashAcceptorHandler, 1000,
                                () -> {
                                    getLogs().info("[現金支付] 紙鈔機激活完成，開始生成找零指令");

                                    // 紙鈔機準備完成後，生成全部找零指令
                                    List<ChangeCommand> commands = new ArrayList<>();
                                     int remaining = changeAmount;

                                     // Add coin change commands first
                                     addOtherChangeCommands(commands, remaining - (finalHundredsToGive * 100));

                                     // Add banknote change command last
                                     String command = "370600000" + String.format("%01X", finalHundredsToGive);
                                     getLogs().info("[現金支付] 準備吐出 100元紙鈔 x " + finalHundredsToGive + " 張，指令: " + command);
                                     commands.add(new ChangeCommand(command, 100, finalHundredsToGive));

                                    // 執行找零指令
                                    if (!commands.isEmpty()) {
                                        executeChangeCommands(commands, 0, 3500, onComplete);
                                    } else if (onComplete != null) {
                                        onComplete.run();
                                    }
                                });
                    });
                });
            });
            return; // 重要：提前返回，不繼續往下執行
        }

        // 如果不需要吐百鈔，直接生成硬幣找零指令
        List<ChangeCommand> commands = new ArrayList<>();
        addOtherChangeCommands(commands, remainingChange);

        if (!commands.isEmpty()) {
            executeChangeCommands(commands, 0, 3500, onComplete);
        } else if (onComplete != null) {
            onComplete.run();
        }
    }

    // 新增輔助方法：加入硬幣找零指令
    /**
     * 添加硬幣找零指令到指令列表
     * 
     * @param commands        指令列表
     * @param remainingChange 剩餘需要找零的金額
     */
    private void addOtherChangeCommands(List<ChangeCommand> commands, int remainingChange) {

        // -----------------------------
        // 50元硬幣
        // -----------------------------
        int fiftiesToGive = Math.min(remainingChange / 50, coin50_count);
        if (fiftiesToGive > 0) {
            String command = "0D" + String.format("%01X", fiftiesToGive) + "3";
            commands.add(new ChangeCommand(command, 50, fiftiesToGive));
            remainingChange -= fiftiesToGive * 50;
        }

        // -----------------------------
        // 10元硬幣
        // -----------------------------
        int tensToGive = Math.min(remainingChange / 10, coin10_count);
        if (tensToGive > 0) {
            String command = "0D" + String.format("%01X", tensToGive) + "2";
            commands.add(new ChangeCommand(command, 10, tensToGive));
            remainingChange -= tensToGive * 10;
        }

        // -----------------------------
        // 5元硬幣
        // -----------------------------
        int fivesToGive = Math.min(remainingChange / 5, coin5_count);
        if (fivesToGive > 0) {
            String command = "0D" + String.format("%01X", fivesToGive) + "1";
            commands.add(new ChangeCommand(command, 5, fivesToGive));
            remainingChange -= fivesToGive * 5;
        }

        // -----------------------------
        // 1元硬幣
        // -----------------------------
        int onesToGive = Math.min(remainingChange, coin1_count);
        if (onesToGive > 0) {
            String command = "0D" + String.format("%01X", onesToGive) + "0";
            commands.add(new ChangeCommand(command, 1, onesToGive));
            remainingChange -= onesToGive;
        }

        // -----------------------------
        // 如果剩餘金額還有 > 0，表示找零不足
        // -----------------------------
        if (remainingChange > 0) {
            getLogs().info("[現金支付] 警告：找零不足，還差 " + remainingChange + "元");
            messageShow("找零不足，無法完成找零。請洽服務人員處理。", 3000);
            savePendingRefund(remainingChange); // 儲存未退還餘額，用於自癒
        }
    }

    /**
     * 依序執行找零指令
     * 
     * @param commands   找零指令列表
     * @param index      當前執行的指令索引
     * @param onComplete 所有指令執行完成後的回調
     */
    private void executeChangeCommands(List<ChangeCommand> commands, int index, int nextDelay, Runnable onComplete) {
        // Check recursion termination condition
        if (index >= commands.size()) {
            // 延遲 nextDelay 毫秒（確保最後一個退幣指令物理吐幣完畢），再執行關閉與完成回調
            new Handler(Looper.getMainLooper()).postDelayed(() -> {
                sendMdbCommand("3400000000", simpleCashAcceptorHandler);
                if (onComplete != null) {
                    onComplete.run();
                }
            }, nextDelay);
            return;
        }

        final ChangeCommand currentCommand = commands.get(index);

        // Send command sequentially with delay
        new Handler(Looper.getMainLooper()).postDelayed(() -> {

            if (currentCommand != null && simpleCashAcceptorHandler != null) {

                getLogs().info("[\u627e\u96f6\u6307\u4ee4\u767c\u9001] \u6307\u4ee4: " + currentCommand.command + "\uff0c\u9762\u984d: " + currentCommand.denomination + "\uff0c\u5f35\u6578/\u500b\u6578: " + currentCommand.count);
                
                // 使用帶有 Callback 版本的 sendMdbCommand 以支援寫入狀態檢查（斷線偵測）
                sendMdbCommand(currentCommand.command, simpleCashAcceptorHandler, success -> {
                    if (success) {
                        messageShow("\u5410\u5e63\u4e2d\u8acb\u7a0d\u5019", 1000);

                        // Update inventory count based on denomination
                        switch (currentCommand.denomination) {
                            case 50:
                                coin50_count -= currentCommand.count;
                                break;
                            case 10:
                                coin10_count -= currentCommand.count;
                                break;
                            case 5:
                                coin5_count -= currentCommand.count;
                                break;
                            case 1:
                                coin1_count -= currentCommand.count;
                                break;
                            case 100:
                                note100_count -= currentCommand.count;
                                break;
                        }

                        int delay = 3500;
                        if (currentCommand.denomination == 100) {
                            // Banknote dynamic delay: 2.5s per sheet + 3s buffer
                            delay = (currentCommand.count * 2500) + 3000;
                        }
                        executeChangeCommands(commands, index + 1, delay, onComplete);
                    } else {
                        // 寫入失敗！說明 MDB 斷線，設備無法通訊
                        getLogs().error(ErrorCode.UNKNOWN_ERROR, "[找零失敗] 指令寫入失敗，硬體可能中斷通訊。指令: " + currentCommand.command);
                        
                        // 計算剩餘未退幣的金額
                        int unrefunded = 0;
                        for (int i = index; i < commands.size(); i++) {
                            ChangeCommand cmd = commands.get(i);
                            unrefunded += cmd.denomination * cmd.count;
                        }
                        
                        getLogs().error(ErrorCode.UNKNOWN_ERROR, "[找零失敗] 本次找零未成功退還金額: " + unrefunded + " 元");
                        savePendingRefund(unrefunded);
                        
                        messageShow("設備通訊中斷，找零退款失敗！\n未退還金額: " + unrefunded + " 元，請洽服務人員處理。", 10000);
                        completeCancelProcess();
                    }
                });
            } else {
                executeChangeCommands(commands, index + 1, 0, onComplete);
            }
        }, nextDelay); //
    }

    /// ///// 找零方法 有三個 //////////// ///// 找零方法 有三個 /////////

    // 下位機溝通用
    public byte[] hexStringToByteArray(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i + 1), 16));
        }
        return data;
    }

    // 安全關閉串口的核心方法（帶延遲與回調參數）
    /**
     * 安全關閉串口（帶延遲與回調參數）
     * 
     * @param delayBeforeCloseMs 關閉前的延遲時間（毫秒）
     * @param onComplete         串口關閉完成後的回調
     */
    private void safeCloseSerialPort(int delayBeforeCloseMs, Runnable onComplete) {
        synchronized (closeLock) {
            if (isSerialPortClosed) {
                getLogs().info("[CashPayDialogGeneral] 串口已經關閉過了，跳過重複關閉");
                if (onComplete != null) {
                    onComplete.run();
                }
                return;
            }
            isSerialPortClosed = true;
            getLogs().info("[CashPayDialogGeneral] 開始安全關閉串口，延遲: " + delayBeforeCloseMs + "ms");
        }

        if (simpleCashAcceptorHandler != null) {
            // 使用 Handler 確保即使對話框關閉也能執行
            new Handler(Looper.getMainLooper()).postDelayed(() -> {
                new Thread(() -> {
                    try {
                        // 停止接收紙幣和硬幣
                        getLogs().info("[CashPayDialogGeneral] 發送停止接收紙幣硬幣指令");
                        sendMdbCommand("0C00000000", simpleCashAcceptorHandler);
                        Thread.sleep(1000);

                        // 停止找零
                        getLogs().info("[CashPayDialogGeneral] 發送停止找零指令");
                        sendMdbCommand("3400000000", simpleCashAcceptorHandler);
                        Thread.sleep(1000);

                        // 關閉連線
                        getLogs().info("[CashPayDialogGeneral] 關閉串口連線");
                        simpleCashAcceptorHandler.close();

                        stopPollingStatus();

                        getLogs().info("[CashPayDialogGeneral] 串口關閉完成");
                    } catch (Exception e) {
                        getLogs().error(ErrorCode.UNKNOWN_ERROR, "[CashPayDialogGeneral] 關閉串口時發生錯誤: " + e.getMessage());
                    } finally {
                        if (onComplete != null) {
                            new Handler(Looper.getMainLooper()).post(onComplete);
                        }
                    }
                }).start();
            }, delayBeforeCloseMs);
        } else {
            if (onComplete != null) {
                onComplete.run();
            }
        }
    }

    private void safeCloseSerialPort(int delayBeforeCloseMs) {
        safeCloseSerialPort(delayBeforeCloseMs, null);
    }

    /**
     * 啟動吸鈔超時檢測定時器（防止滾輪卡鈔，因為滾輪卡鈔時只會一直報 30 03 忙碌，而沒有 30 05）
     */
    private void startAcceptTimeoutCheck() {
        cancelAcceptTimeoutCheck(); // 確保先清除舊的
        
        acceptTimeoutRunnable = () -> {
            // 如果 6 秒後，待確認金額仍大於 0（紙鈔仍未成功入箱/入循環區）
            if (pendingAcceptanceAmount > 0 && !isJammed) {
                getLogs().error(ErrorCode.UNKNOWN_ERROR, "[卡鈔判定] 紙鈔吸入超時（已過 6 秒仍未成功入箱，且持續忙碌中），判定為滾輪卡鈔！");
                handleBillJam();
            }
        };
        acceptTimeoutHandler.postDelayed(acceptTimeoutRunnable, 6000);
    }

    /**
     * 取消吸鈔超時檢測定時器
     */
    private void cancelAcceptTimeoutCheck() {
        if (acceptTimeoutRunnable != null) {
            acceptTimeoutHandler.removeCallbacks(acceptTimeoutRunnable);
            acceptTimeoutRunnable = null;
        }
    }

    /**
     * 取消卡鈔超時定時器，避免幽靈取消 Bug
     */
    private void cancelJamTimeout() {
        if (jamTimeoutRunnable != null) {
            jamTimeoutHandler.removeCallbacks(jamTimeoutRunnable);
            jamTimeoutRunnable = null;
        }
    }

    /**
     * 消費者手動觸發嘗試退鈔並重新啟用連線
     */
    private void performRetryEscrow() {
        getLogs().info("[手動退鈔重試] 消費者點擊嘗試退鈔並重試");
        cancelJamTimeout();

        new Handler(Looper.getMainLooper()).post(() -> {
            if (binding != null && binding.buttonRetryEscrow != null) {
                binding.buttonRetryEscrow.setVisibility(View.GONE);
            }
            messageShow("嘗試退鈔中，請稍後...", 5000);
        });

        getLogs().info("[手動退鈔重試] 發送 3500 退鈔指令");
        sendMdbCommand("3500", simpleCashAcceptorHandler);

        // 延遲 4 秒讓物理吐鈔動作執行完畢後，重新啟用並連線紙鈔機
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            if (!isShowing()) {
                getLogs().info("[手動退鈔重試] 對話框已關閉，放棄重啟連線");
                return;
            }
            getLogs().info("[手動退鈔重試] 退鈔完畢，重新啟用並連線紙鈔機");
            
            // 重新發送啟用與初始化指令序列
            sendMdbCommand("3704000003000000000000000000000000000000", simpleCashAcceptorHandler, 200, () -> {
                sendMdbCommand("3705", simpleCashAcceptorHandler, 200, () -> {
                    sendMdbCommand("0A", simpleCashAcceptorHandler, 200, () -> {
                        getLogs().info("[手動退鈔重試] 初始化指令發送完畢，重置暫估金額，重啟輪詢");
                        
                        pendingAcceptanceAmount = 0; // 暫估清零（因已退鈔）
                        isJammed = false; // 重設 Jammed 狀態為 false，以利自癒判定與再次卡鈔時的偵測
                        startPollingStatus(false);
                        
                        // 延遲 1.5 秒確認狀態
                        new Handler(Looper.getMainLooper()).postDelayed(() -> {
                            if (!isShowing()) {
                                getLogs().info("[手動退鈔重試] 對話框已關閉，放棄狀態確認");
                                return;
                            }
                            if (!isJammed) {
                                // 代表故障已排除，恢復現金支付啟用狀態
                                try {
                                    Context ctx = getContext();
                                    if (ctx != null) {
                                        android.content.SharedPreferences sp = ctx.getSharedPreferences("MyShoppingCartSettings", Context.MODE_PRIVATE);
                                        sp.edit().putBoolean("isCashModuleEnabled", true).apply();
                                        getLogs().info("[手動退鈔重試] 卡鈔自癒成功，現金支付模組重新啟用 (isCashModuleEnabled = true)");
                                    }
                                } catch (Exception ex) {
                                    getLogs().error(ErrorCode.UNKNOWN_ERROR, "重新啟用現金模組異常: " + ex.getMessage());
                                }
                                
                                new Handler(Looper.getMainLooper()).post(() -> {
                                    if (!isShowing()) return;
                                    if (binding != null) {
                                        if (binding.buttonCancel != null) {
                                            binding.buttonCancel.setText("取消並退幣");
                                            binding.buttonCancel.setVisibility(View.VISIBLE); // 重新顯示取消按鈕
                                        }
                                        if (binding.buttonRetryEscrow != null) {
                                            binding.buttonRetryEscrow.setVisibility(View.GONE);
                                        }
                                    }
                                    messageShow("請拿回退出的紙鈔，可繼續交易。", 5000);
                                    isJammed = false; // 重設 Jammed 狀態
                                });
                            } else {
                                // 重新啟用後如果依然卡鈔，Poll 會再次回報 30 05 並觸發 handleBillJam，那裡會處理 UI
                                getLogs().error(ErrorCode.UNKNOWN_ERROR, "[手動退鈔重試] 重連後依然偵測為卡鈔狀態");
                            }
                        }, 1500);
                    });
                });
            });
        }, 4000);
    }

    /// //////// 取消交易 找零 有二個 ////////
    /**
     * 對話框取消按鈕點擊處理
     */
    public void dialogCancel() {
        if (this.isShowing()) {
            cancelAcceptTimeoutCheck();
            // 如果卡鈔中取消，立刻將現金模組設為停用
            if (isJammed) {
                try {
                    Context ctx = getContext();
                    if (ctx != null) {
                        android.content.SharedPreferences sp = ctx.getSharedPreferences("MyShoppingCartSettings", Context.MODE_PRIVATE);
                        sp.edit().putBoolean("isCashModuleEnabled", false).apply();
                        getLogs().error(ErrorCode.UNKNOWN_ERROR, "[取消交易] 卡鈔中取消交易，已停用現金模組 (isCashModuleEnabled = false)");
                    }
                } catch (Exception e) {
                    getLogs().error(ErrorCode.UNKNOWN_ERROR, "停用現金模組異常: " + e.getMessage());
                }
            }

            int totalRefund = isJammed ? inCash : (inCash + pendingAcceptanceAmount);
            int pendingLoss = isJammed ? pendingAcceptanceAmount : 0;

            if (totalRefund > 0) {
                if (isJammed && pendingLoss > 0) {
                    getLogs().error(ErrorCode.UNKNOWN_ERROR, "卡鈔取消交易：已入箱 " + inCash + " 元以硬幣退還，卡在暫存區之 " + pendingLoss + " 元無法退出，由人工排除");
                    messageShow("設備異常已中止！\n已退還入箱金額 " + inCash + " 元，卡住之 " + pendingLoss + " 元請洽服務人員處理。", 8000);
                } else {
                    getLogs().info("取消交易，退還 " + totalRefund + "元 (inCash: " + inCash + ", pendingAcceptanceAmount: " + pendingAcceptanceAmount + ")");
                    messageShow("取消交易，退還 " + totalRefund + " 元", 3000);
                }

                // 執行退幣，若已卡鈔，只找硬幣
                performChange(totalRefund, !isJammed, () -> {
                    completeCancelProcess();
                });

                // 重置金額
                inCash = 0;
                pendingAcceptanceAmount = 0;
            } else {
                if (isJammed && pendingLoss > 0) {
                    getLogs().error(ErrorCode.UNKNOWN_ERROR, "卡鈔取消交易：無已入箱金額，卡在暫存區之 " + pendingLoss + " 元無法退出，由人工排除");
                    messageShow("設備異常已中止！\n卡住之 " + pendingLoss + " 元請洽服務人員處理。", 8000);
                    
                    new Handler(Looper.getMainLooper()).postDelayed(() -> {
                        completeCancelProcess();
                    }, 500);
                } else {
                    getLogs().info("沒有現金需要退還 先吐暫存區3500再關關閉串口");
                    new Handler(Looper.getMainLooper()).postDelayed(() -> {
                        sendMdbCommand("3500", simpleCashAcceptorHandler);
                        getLogs().info("...已發送退鈔指令3500");

                        new Handler(Looper.getMainLooper()).postDelayed(() -> {
                            getLogs().info("aa執行completeCancelProcess");
                            completeCancelProcess();
                        }, 500);
                    }, 100);
                }
                
                // 重置金額
                inCash = 0;
                pendingAcceptanceAmount = 0;
            }
        }
    }

    /**
     * 取消交易流程，包括退幣和清理資源
     */
    private void completeCancelProcess() {
        getLogs().info("[CashPayDialogGeneral] 執行 completeCancelProcess");

        // 清除優惠代碼 and 購物車
        MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
        getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.STOP_COUNTDOWN.getOption(), "");
        MyApp.getInstance().getBuyList().clear();

        // 提示使用者正在清理連線
        if (!isJammed) {
            messageShow("交易已取消，清理硬體連線中，請稍後...", 4000);
        }

        safeCloseSerialPort(3000, () -> {
            // 確保串口安全釋放後，才真正關閉對話框
            cancel();
        });
    }

    /**
     * 處理紙鈔機卡鈔異常狀態
     * 立即停用硬體、隱藏取消按鈕、提示並暫停 30 秒以利警示，隨後自動重置退回待機頁面
     */
    private void handleBillJam() {
        if (isJammed) return;
        isJammed = true;

        getLogs().info("[卡鈔] 偵測到卡鈔，啟動手動退鈔重試畫面");

        // 1. 停止狀態輪詢，防範干擾
        stopPollingStatus();

        // 2. 更新 UI：顯示重試按鈕、將取消按鈕字樣改為「取消交易」
        new Handler(Looper.getMainLooper()).post(() -> {
            if (binding != null) {
                if (binding.buttonRetryEscrow != null) {
                    binding.buttonRetryEscrow.setVisibility(View.VISIBLE);
                }
                if (binding.buttonCancel != null) {
                    binding.buttonCancel.setVisibility(View.GONE); // 卡鈔時隱藏取消交易按鈕
                }
            }
            // 3. 彈出提示視窗
            messageShow("設備異常：紙鈔通道卡鈔！\n請點選「嘗試退鈔並重試」", 30000);
        });

        // 4. 設定 60 秒超時保護，若消費者無操作則自動執行取消交易
        cancelJamTimeout(); // 先清除舊的，防範幽靈定時器
        jamTimeoutRunnable = () -> {
            if (isJammed && isShowing()) {
                getLogs().info("[卡鈔超時] 超時 60 秒無操作，自動取消交易");
                dialogCancel();
            }
        };
        jamTimeoutHandler.postDelayed(jamTimeoutRunnable, 60000);
    }

    /// //////// 取消交易 找零 有二個 ////////

    // 新增：MDB 指令發送回調介面
    private interface MdbCommandCallback {
        void onResult(boolean success);
    }

    // 新增：發送 MDB 任務，支援回調以檢測寫入是否成功（斷線檢測）
    private void sendMdbCommand(final String hexCommand, final ICashAcceptorHandler handler, final MdbCommandCallback callback) {
        new Thread(() -> {
            boolean success = false;
            try {
                byte[] command = hexStringToByteArray(hexCommand);
                int bytesWritten = handler.writeData(command);
                if (bytesWritten > 0) {
                    getLogs().info("[MDB] 指令發送成功: " + hexCommand);
                    success = true;
                } else {
                    getLogs().error(ErrorCode.UNKNOWN_ERROR, "[MDB] 指令發送失敗: " + hexCommand + " (回傳碼: " + bytesWritten + ")");
                }
            } catch (Exception e) {
                getLogs().error(ErrorCode.UNKNOWN_ERROR, "[MDB] 指令發送異常: " + e.getMessage());
            }
            if (callback != null) {
                final boolean finalSuccess = success;
                new Handler(Looper.getMainLooper()).post(() -> callback.onResult(finalSuccess));
            }
        }).start();
    }

    /**
     * 發送MDB任務
     *
     */
    private void sendMdbCommand(final String hexCommand, final ICashAcceptorHandler handler) {
        sendMdbCommand(hexCommand, handler, null);
    }

    /**
     * 發送MDB任務 參數多兩個 delay跟 Runnable
     *
     */
    private void sendMdbCommand(String command, ICashAcceptorHandler handler, int delay, Runnable onComplete) {
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            sendMdbCommand(command, handler);
            if (onComplete != null) {
                onComplete.run();
            }
        }, delay);
    }

    /// Cancel時會進入安全關閉
    @Override
    public void dismiss() {
        cancelAcceptTimeoutCheck();
        cancelJamTimeout();
        cancelIdleTimeout(); // 取消無操作逾時計時器
        dismissInitDialog(); // 關閉「初始化中」提示，避免殘窗
        getLogs().info("[CashPayDialogGeneral] dismiss() 被調用，確保串口安全關閉");
        // 異常關閉場景：給予 3 秒緩衝時間，以防正在找零
        safeCloseSerialPort(3000);
        stopPollingStatus();
        super.dismiss();
    }

    /// Activity 進背景時 安全關閉
    @Override
    protected void onStop() {
        super.onStop();
        cancelIdleTimeout(); // 取消無操作逾時計時器
        getLogs().info("[CashPayDialogGeneral] onStop() 被調用，確保串口安全關閉");
        // 異常關閉場景：給予 3 秒緩衝時間，以防正在找零
        safeCloseSerialPort(3000);
        stopPollingStatus();

        Log.d("CashPayDialogGeneral", "onStop(): 關閉串口與停止讀取");
    }

    /**
     * 啟動無操作逾時計時器（3 分鐘無投幣自動關閉）
     * 防止 MDB 串口長時間佔用，導致機器 Recycler 計數被清零
     */
    private void startIdleTimeout() {
        cancelIdleTimeout();
        // 可見倒數：每秒更新「取消並退幣(NN秒)」按鈕文字；投幣時 resetIdleTimeout() 會重置；歸零則自動退幣關閉。
        cancelCountDownTimer = new android.os.CountDownTimer(CANCEL_COUNTDOWN_SEC * 1000L, 1000L) {
            @Override
            public void onTick(long millisUntilFinished) {
                int sec = (int) Math.ceil(millisUntilFinished / 1000.0);
                if (binding != null && binding.buttonCancel != null) {
                    binding.buttonCancel.setText("取消並退幣(" + sec + "秒)");
                }
            }

            @Override
            public void onFinish() {
                if (binding != null && binding.buttonCancel != null) {
                    binding.buttonCancel.setText("取消並退幣(0秒)");
                }
                getLogs().info("[倒數逾時] " + CANCEL_COUNTDOWN_SEC + " 秒無投幣動作，自動退幣並關閉");
                if (isShowing()) {
                    dialogCancel();
                }
            }
        };
        cancelCountDownTimer.start();
        getLogs().info("[倒數] 啟動 " + CANCEL_COUNTDOWN_SEC + " 秒取消倒數");
    }

    /**
     * 重設無操作逾時計時器（每次收到投幣事件時呼叫）
     */
    private void resetIdleTimeout() {
        if (isDeviceReady) {
            startIdleTimeout(); // 重新啟動即可重設計時
        }
    }

    /**
     * 取消無操作逾時計時器
     */
    private void cancelIdleTimeout() {
        if (cancelCountDownTimer != null) {
            cancelCountDownTimer.cancel();
            cancelCountDownTimer = null;
        }
        if (idleTimeoutRunnable != null) {
            idleTimeoutHandler.removeCallbacks(idleTimeoutRunnable);
            idleTimeoutRunnable = null;
        }
    }

    /**
     * 啟動定時輪詢設備狀態
     * @param fast 是否使用快速輪詢（1秒一次，用於初始化後等待就緒）
     */
    private void startPollingStatus(boolean fast) {
        stopPollingStatus(); // 先確保之前的停止
        this.isFastPolling = fast;
        int interval = fast ? 1000 : 10000;
        
        getLogs().info("[輪詢] 啟動狀態輪詢，模式: " + (fast ? "快速 (1s)" : "正常 (10s)"));
        
        pollingRunnable = new Runnable() {
            @Override
            public void run() {
                if (simpleCashAcceptorHandler != null) {
                    // getLogs().info("[輪詢] 請求設備庫存狀態...");
                    // 請求紙鈔機回收器庫存
                    sendMdbCommand("3705", simpleCashAcceptorHandler);
                    // 請求硬幣機筒位狀態
                    new Handler(Looper.getMainLooper()).postDelayed(() -> 
                        sendMdbCommand("0A", simpleCashAcceptorHandler), 400);
                }
                pollingHandler.postDelayed(this, interval);
            }
        };
        pollingHandler.post(pollingRunnable);
    }

    /**
     * 停止定時輪詢
     */
    private void stopPollingStatus() {
        if (pollingHandler != null && pollingRunnable != null) {
            getLogs().info("[輪詢] 停止設備狀態輪詢");
            pollingHandler.removeCallbacks(pollingRunnable);
            pollingRunnable = null;
        }
    }

    /**
     * 計算購物車總價
     * 
     * @return 總價
     */
    public int getTotalPrice() {
        int totalPrice = 0;
        if (MyApp.getInstance().getBuyList().size() == 0)
            return totalPrice;
        for (BuyStructure buy : MyApp.getInstance().getBuyList()) {
            if (!buy.isFreeGift())
                totalPrice += buy.getFullPrice();
        }
        if (MyApp.getInstance().getReportFreeGiftCodeStructure() != null) {
            switch (MyApp.getInstance().getReportFreeGiftCodeStructure().getMode()) {
                case 0:
                    break;
                case 1:
                    totalPrice -= MyApp.getInstance().getReportFreeGiftCodeStructure().getDiscountAmount();
                    break;
                case 2:
                    totalPrice = (int) (totalPrice
                            * MyApp.getInstance().getReportFreeGiftCodeStructure().getDiscountPercentage());
                    break;
            }
        }
        if (totalPrice == 0)
            totalPrice = 1;
        return totalPrice;
    }

    // 儲存待退款金額至本地
    private void savePendingRefund(int amount) {
        if (amount <= 0) return;
        try {
            Context ctx = getCtx();
            if (ctx != null) {
                android.content.SharedPreferences sp = ctx.getSharedPreferences("CashRefundPrefs", Context.MODE_PRIVATE);
                int existingAmount = sp.getInt("pending_refund", 0);
                int newAmount = existingAmount + amount;
                sp.edit().putInt("pending_refund", newAmount).apply();
                getLogs().info("[退款紀錄] 已儲存待退款金額: " + amount + " 元，累計待退款: " + newAmount + " 元");
            }
        } catch (Exception e) {
            getLogs().error(ErrorCode.UNKNOWN_ERROR, "儲存待退款金額失敗: " + e.getMessage());
        }
    }

    // 獲取待退款金額
    private int getPendingRefund() {
        try {
            Context ctx = getCtx();
            if (ctx != null) {
                android.content.SharedPreferences sp = ctx.getSharedPreferences("CashRefundPrefs", Context.MODE_PRIVATE);
                return sp.getInt("pending_refund", 0);
            }
        } catch (Exception e) {
            getLogs().error(ErrorCode.UNKNOWN_ERROR, "讀取待退款金額失敗: " + e.getMessage());
        }
        return 0;
    }

    // 清除待退款金額
    private void clearPendingRefund() {
        try {
            Context ctx = getCtx();
            if (ctx != null) {
                android.content.SharedPreferences sp = ctx.getSharedPreferences("CashRefundPrefs", Context.MODE_PRIVATE);
                sp.edit().remove("pending_refund").apply();
                getLogs().info("[退款紀錄] 已清除待退款紀錄");
            }
        } catch (Exception e) {
            getLogs().error(ErrorCode.UNKNOWN_ERROR, "清除待退款金額失敗: " + e.getMessage());
        }
    }

    // 檢查並執行上次未退款的自癒流程
    private void checkAndProcessPendingRefund() {
        final int pendingRefund = getPendingRefund();
        if (pendingRefund <= 0) return;

        getLogs().info("[退款紀錄] 偵測到上次未退還金額: " + pendingRefund + " 元，開始處理自癒退款流程...");
        
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            messageShow("偵測到上次未退還金額 " + pendingRefund + " 元，系統正在自動退還...", 5000);
            
            // 嘗試退款
            performChange(pendingRefund, true, () -> {
                getLogs().info("[退款紀錄] 上次未退還金額 " + pendingRefund + " 元已成功退還！");
                clearPendingRefund();
                messageShow("成功退還上次未退金額 " + pendingRefund + " 元", 3000);
            });
        }, 2000);
    }

    /// ////////////不重要

    /**
     * 顯示提示訊息
     * 
     * @param message 訊息內容
     * @param second  顯示時間（毫秒）
     */
    /** 顯示持續型「初始化中」提示（不自動關閉，直到就緒/失敗才 dismissInitDialog）。可重入。 */
    private void showInitDialog() {
        if (initDialog != null && initDialog.isShowing()) return;
        Builder builder = new Builder(getCtx());
        initDialog = builder.create();
        View dialogView = View.inflate(getCtx(), R.layout.mydialog1, null);
        TextView t1 = dialogView.findViewById(R.id.text01);
        TextView t2 = dialogView.findViewById(R.id.text02);
        t1.setText("提示!");
        t2.setText("設備初始化中，請稍候...");
        initDialog.setView(dialogView);
        initDialog.setCancelable(false);
        if (initDialog.getWindow() != null) {
            initDialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
            initDialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT,
                    WindowManager.LayoutParams.WRAP_CONTENT);
        }
        initDialog.show();
    }

    /** 關閉「初始化中」提示。 */
    private void dismissInitDialog() {
        if (initDialog != null) {
            try {
                if (initDialog.isShowing()) initDialog.dismiss();
            } catch (Exception ignore) {
            }
            initDialog = null;
        }
    }

    void messageShow(String message, int second) {
        Builder builder = new Builder(getCtx());
        final AlertDialog dialog = builder.create();
        View dialogView = View.inflate(getCtx(), R.layout.mydialog1, null);
        TextView t1 = dialogView.findViewById(R.id.text01);
        TextView t2 = dialogView.findViewById(R.id.text02);
        t1.setText("提示!");
        t2.setText(message);
        dialog.setView(dialogView);
        if (dialog.getWindow() != null) {
            dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
            dialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT,
                    WindowManager.LayoutParams.WRAP_CONTENT);
        }
        dialog.show();
        if (second > 0)
            new Handler().postDelayed(dialog::dismiss, second);
    }

    /**
     * 顯示 MDB 指令發送對話框（用於測試）
     * 
     * @param handler MDB 處理器
     */
    private void showMdbCommandDialog(ICashAcceptorHandler handler) {
        Builder builder = new Builder(getCtx());
        builder.setTitle("MDB 指令發送");
        final EditText input = new EditText(getCtx());
        input.setInputType(InputType.TYPE_CLASS_TEXT);
        input.setHint("請輸入HEX指令 (例如: 34FFFFFFFF)");
        builder.setView(input);
        builder.setPositiveButton("發送", (dialog, which) -> {
            String hexCommand = input.getText().toString().trim();
            if (!hexCommand.isEmpty()) {
                sendMdbCommand(hexCommand, handler);
            } else {
                // Toast.makeText(getCtx(), "指令不能為空", Toast.LENGTH_SHORT).show();
            }
        });
        builder.setNegativeButton("取消", (dialog, which) -> dialog.cancel());
        builder.show();
    }

    @Override
    protected void onCreate(Context context) {
    }

    public CashPayDialogGeneral(Context context, HandlerMain srcHandlerMain) {
        super(context, srcHandlerMain);
    }

    @Override
    protected Context setCtx() {
        return getContext();
    }

    @Override
    protected Class<? extends DialogAbstract> setCls() {
        return getClass();
    }

    @Override
    protected HandlerMain setHandlerMain() {
        return new ToastHandlerMain(getContext(), getLogs()) {
            @Override
            protected void execute(Context context, int commandCode, String message) {
            }
        };
    }

    /// ////////////不重要

}