package com.unibuy.smartdevice;

import android.os.Build;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.util.DisplayMetrics;
import android.view.WindowManager;

import androidx.work.BackoffPolicy;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;

import com.unibuy.smartdevice.controller.DevController;
import com.unibuy.smartdevice.controller.DevXinYuanController;
import com.unibuy.smartdevice.database.MemoDao;
import com.unibuy.smartdevice.database.ProductsDao;
import com.unibuy.smartdevice.database.SettingsDao;
import com.unibuy.smartdevice.database.SlotsDao;
import com.unibuy.smartdevice.devices.ComPort;
import com.unibuy.smartdevice.devices.DevXinYuan;
import com.unibuy.smartdevice.devices.Rs232Dev;
import com.unibuy.smartdevice.devices.SlotField;
import com.unibuy.smartdevice.devices.UsbDev;
import com.unibuy.smartdevice.exception.ErrorCode;
import com.unibuy.smartdevice.exception.Logs;
import com.unibuy.smartdevice.exception.LogsEmptyException;
import com.unibuy.smartdevice.exception.LogsNoSuchElementException;
import com.unibuy.smartdevice.exception.LogsUnsupportedOperationException;
import com.unibuy.smartdevice.structure.AccessCodeStructure;
import com.unibuy.smartdevice.structure.CashSetStructure;
import com.unibuy.smartdevice.structure.HardwareSetStructure;
import com.unibuy.smartdevice.structure.CreditCardReceiptStructure;
import com.unibuy.smartdevice.structure.FreeGiftStructure;
import com.unibuy.smartdevice.structure.MemberVerificationStructure;
import com.unibuy.smartdevice.structure.MemoStructure;
import com.unibuy.smartdevice.structure.ReportAccessCodeStructure;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.structure.DevSetStructure;
import com.unibuy.smartdevice.structure.EsunpayStructure;
import com.unibuy.smartdevice.structure.GreenInvoiceStructure;
import com.unibuy.smartdevice.structure.GreenpayMerchantStructure;
import com.unibuy.smartdevice.structure.GreenpaySettingStructure;
import com.unibuy.smartdevice.structure.InvoiceItemStructure;
import com.unibuy.smartdevice.structure.InvoiceStructure;
import com.unibuy.smartdevice.structure.MachineStructure;
import com.unibuy.smartdevice.structure.MarketingPlanStructure;
import com.unibuy.smartdevice.structure.PostInvoiceByGreenStructure;
import com.unibuy.smartdevice.structure.ProductStructure;
import com.unibuy.smartdevice.structure.ReportDispatchInfoStructure;
import com.unibuy.smartdevice.structure.ReportFlowInfoStructure;
import com.unibuy.smartdevice.structure.ReportFreeGiftCodeStructure;
import com.unibuy.smartdevice.structure.ReportInvoiceInfoStructure;
import com.unibuy.smartdevice.structure.ReportSlotListStructure;
import com.unibuy.smartdevice.structure.SettingStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import com.unibuy.smartdevice.tools.Tools;
import com.unibuy.smartdevice.ui.FontendActivity;
import com.unibuy.smartdevice.ui.tools.VideoEoxPlayer;
import com.unibuy.smartdevice.worker.AppCrashHandler;
import com.unibuy.smartdevice.worker.UploadLogOneTimeWorker;
import com.unibuy.smartdevice.worker.UploadLogPeriodicWorker;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class MyApp extends Application {
    /** 取貨口關閉時間(秒)：開機時寫入 VMC 韌體(4.5.11)。取貨倒數為 15 秒，設 17 可讓門開到倒數接近結束才關。range 3-250。 */
    public static final int PICKUP_DOOR_CLOSE_SECONDS = 17;

    private Logs logs;
    private static MyApp instance;
    public static MyApp getInstance() {
        return instance;
    }
    private int screenWidthPx;
    private int screenHeightPx;
    private int closeSlot;
    private float density;
    private ComPort comPort1ByXinYuan;
    private ComPort comPort2ByNFCPay;
    private List<ProductStructure> uploadProductList;
    private List<ProductStructure> productList;
    private List<String> fontendAdUrlString;
    private Map<Integer, List<SlotStructure>> slotMap;
    private List<MarketingPlanStructure> marketingPlanList;
    private List<BuyStructure> buyList;
    private String decryptedData;
    private String cmuhPickupErrorMessage;
    private Map<String, List<MemoStructure>> memoMap;
    private CashSetStructure cashSetStructure;
    private HardwareSetStructure hardwareSetStructure;
    private AccessCodeStructure accessCodeStructure;
    private FreeGiftStructure freeGiftStructure;
    private MemberVerificationStructure memberVerificationStructure;
    private ReportAccessCodeStructure reportAccessCodeStructure;
    private ReportFreeGiftCodeStructure reportFreeGiftCodeStructure;
    private ReportSlotListStructure reportSlotListStructure;
    private EsunpayStructure esunpay;
    private MachineStructure machine;
    private DevSetStructure devSet;
    // 購買流程模式：basic（多品項購物車）/ employee_card（員工卡）/ pickup_sheet（取貨單），
    // 由雲端 B014 ShoppingMode 下發，取代舊 project flavor 分家。預設 basic。
    private String shoppingMode = "basic";
    // 上傳 log 的寫檔門檻等級。預設 INFO：debug.txt 只留 INFO/WARNING/ERROR/UPDATE，過濾掉 DEBUG/IGNORE 雜訊。
    // 工程師現場除錯硬體時可設為 DEBUG，讓 hex/協議細節重新寫檔（logcat 一律照印，不受此值影響）。
    private String logLevel = "INFO";
    // 機台顯示語系（由雲端 B014 LangSet 下發）。標準標籤(zh-TW/en/...)，有序，第 0 個為預設。
    private java.util.List<String> machineLanguages = new java.util.ArrayList<>();
    private String defaultLanguage = "";
    // 商品名稱多語 map（B012 t060v01_i18n），key=商品ID，value=底線代碼→名稱 的 JSONObject。
    private JSONObject productNameI18nBlob = new JSONObject();
    private JSONObject productSpecI18nBlob = new JSONObject();
    // 商品彈簧補貨上限 map（B012 spring_limit），key=商品ID，value=int。履帶上限沿用 products 表 slotUpperLimit。
    private JSONObject productSpringLimitBlob = new JSONObject();
    private GreenInvoiceStructure greenInvoice;
    private GreenpaySettingStructure greenpaySetting;
    private GreenpayMerchantStructure greenpayMerchant;
    private static DevXinYuanController devXinYuanController;
    public ReportFlowInfoStructure reportFlowInfoData;
    public PostInvoiceByGreenStructure postInvoiceByGreenData;
    public ReportInvoiceInfoStructure reportInvoiceInfoData;
    public InvoiceStructure invoiceData;
    public List<InvoiceItemStructure> invoiceItems;
    public ReportDispatchInfoStructure reportDispatchInfoData;
    public CreditCardReceiptStructure creditCardReceiptInfoData;
    public volatile boolean isNfcPaymentActive; // 用於控制 NFC 支付感應器的活動門禁

    public boolean isDetectHost;
    public String suAccess;
    // 登入身分（B000 回傳 identity）：system=系統方（可編輯系統設定並回寫雲端）/ company / staff（唯讀）。
    private String identity = "";
    // B000 登入核發的使用者 Token，供系統設定回寫（B016, auth:sanctum）使用；不覆蓋 machine ApiKey。
    private String loginToken = "";

    // ─── 全域閒置計時器 ───────────────────────────────────────
    // 記錄使用者最後一次觸碰螢幕的時間 (ms)
    private volatile long lastInteractionTime = System.currentTimeMillis();

    /** 任何 Activity 偵測到使用者互動時呼叫此方法，讓計時器歸零 */
    public void updateLastInteraction() {
        lastInteractionTime = System.currentTimeMillis();
    }

    /** 回傳距離上次互動已過了幾秒 */
    public long getIdleSeconds() {
        return (System.currentTimeMillis() - lastInteractionTime) / 1000;
    }
    // ─────────────────────────────────────────────────────────

    @Override
    public void onCreate() {
        super.onCreate();
        logs = new Logs(this.getClass());

        UsbDev.setUsbManager(getApplicationContext());
        Rs232Dev.setSuPath(getApplicationContext());
        Tools.setExternalFilesDir(getApplicationContext());

        instance = this;
        isShowDialog = false;
        suAccess = "";

        File crashLogFile = new File(getExternalFilesDir(null), "crash_pending.txt");
        if (crashLogFile.exists()) {
            try {
                String content = Tools.readFile(crashLogFile); // 使用上面方法讀檔

                logs.info("============================ crash ============================");
                logs.update("lastCrashInfo", content);

                // 上傳或排程任務
                OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(UploadLogOneTimeWorker.class).build();
                WorkManager.getInstance(this).enqueueUniqueWork(
                        "UploadCrashLog",
                        ExistingWorkPolicy.REPLACE,
                        request
                );

                crashLogFile.delete(); // 清除
            } catch (IOException e) {
                logs.warning(e);
            }
        }

        logs.info("============================ start ============================");

        try {
            UsbDev.getDeviceInfo(getApplicationContext());
        } catch (LogsUnsupportedOperationException e) {
            logs.warning(e);
        }

        this.comPort1ByXinYuan = ComPort.COM_PORT_1;
        this.comPort2ByNFCPay = ComPort.COM_PORT_2;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            this.comPort1ByXinYuan = ComPort.COM_PORT_S1;
            this.comPort2ByNFCPay = ComPort.COM_PORT_S2;
        }

        // DevSet 初始預設值改由 res/values/devset_defaults.xml 提供，
        // 各 flavor 可覆寫（General override 為「購物車 + 現金/刷卡/掃碼/發票」全開）。
        android.content.res.Resources devSetRes = getResources();
        this.devSet = new DevSetStructure(
                devSetRes.getBoolean(R.bool.devset_default_vmc),
                devSetRes.getBoolean(R.bool.devset_default_electric),
                devSetRes.getBoolean(R.bool.devset_default_shopping_cart),
                devSetRes.getBoolean(R.bool.devset_default_invoice),
                devSetRes.getBoolean(R.bool.devset_default_nfc_pay),
                devSetRes.getBoolean(R.bool.devset_default_esun_pay),
                devSetRes.getBoolean(R.bool.devset_default_tap_pay),
                devSetRes.getBoolean(R.bool.devset_default_cash),
                devSetRes.getBoolean(R.bool.devset_default_line_pay),
                devSetRes.getBoolean(R.bool.devset_default_tap_pay_30),
                devSetRes.getBoolean(R.bool.devset_default_tap_pay_31),
                devSetRes.getBoolean(R.bool.devset_default_tap_pay_32),
                devSetRes.getBoolean(R.bool.devset_default_tap_pay_33),
                devSetRes.getBoolean(R.bool.devset_default_tap_pay_34),
                devSetRes.getBoolean(R.bool.devset_default_pickup_code),
                devSetRes.getBoolean(R.bool.devset_default_pass_code));
        // FunctionSet 母開關/來店禮不在建構子參數，建構後補套 bootstrap 預設（雲端 B014 一到即覆蓋）
        this.devSet.setPickupModule(devSetRes.getBoolean(R.bool.devset_default_pickup_module));
        this.devSet.setWelcomeGift(devSetRes.getBoolean(R.bool.devset_default_welcome_gift));
        this.machine = new MachineStructure("");
        this.esunpay = new EsunpayStructure("", "", "");
        this.greenInvoice = new GreenInvoiceStructure("", "", "", "");
        this.greenpaySetting = new GreenpaySettingStructure(
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "");
        this.greenpayMerchant = new GreenpayMerchantStructure(
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "");
        this.cashSetStructure = new CashSetStructure(true, true, true, true, true, true, true);
        // HardwareSet（貨道型式）預設全 false，由雲端 B014 下發或系統方主控台設定後覆寫。
        this.hardwareSetStructure = new HardwareSetStructure(false, false, false, false, false, false);
        this.accessCodeStructure = new AccessCodeStructure("");
        this.freeGiftStructure = new FreeGiftStructure("");

        this.memberVerificationStructure = new MemberVerificationStructure("", 0, "", 0);
        this.reportAccessCodeStructure = new ReportAccessCodeStructure("", 0, 0, "", "");
        this.reportFreeGiftCodeStructure = new ReportFreeGiftCodeStructure("", 0, "", 0, 0, "");
        this.reportSlotListStructure = new ReportSlotListStructure("","","","");
        this.fontendAdUrlString = new ArrayList<>();

        ToastHandlerMain toastHandlerMain = new ToastHandlerMain(getApplicationContext(), logs);

        if (this.devXinYuanController != null) {
            this.devXinYuanController.shutdown();
        }

        this.devXinYuanController = new DevXinYuanController(toastHandlerMain);
        // 維修模式：只建立 controller 物件(避免下游 getDevXinYuanController() NPE)，但不 start()、不送
        // 初始化指令 → 不佔用下位機串口，留給原廠下位機軟體讀取。(移植自 StarApp-LockBoard)
        if (!com.unibuy.smartdevice.devices.MaintenanceMode.isSerialReleased()) {
        devXinYuanController.start();

        devXinYuanController.addSendBuffer(devXinYuanController.getDevXinYuan().syncPackNo((byte)0x01));

        devXinYuanController.getReadBufferByNow(new DevController.ReadBufferOnScheduler(toastHandlerMain) {
            @Override
            protected HandlerMain setHandlerMain() {
                return getSrcHandlerMain();
            }

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

            @Override
            public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) {
                int moneySwitchOkCount = 0;
                for(byte[] buffer: devController.getReadBufferByMax()) {
                    DevXinYuan.VMC vmc = DevXinYuanController.vmcMap.get(buffer[0]);
                    switch (vmc) {
                        case VMC_29_MONEY_SWITCH:
                            if (buffer[1] == 0x00) {
                                moneySwitchOkCount++;
                            }
                            break;
                    }
                }
            }
        });
        devXinYuanController.addSendBuffer(devXinYuanController.getDevXinYuan().inComeByIPC((byte) 0x08, 9999));
        devXinYuanController.addSendBuffer(devXinYuanController.getDevXinYuan().setMoneyType((byte) 0x01, (byte) 0x00, (byte) 0x00));
        devXinYuanController.addSendBuffer(devXinYuanController.getDevXinYuan().setMoneyType((byte) 0x02, (byte) 0x00, (byte) 0x00));

        // 取貨口關閉時間：開機時寫入 VMC 韌體(4.5.11)，讓取貨口開啟後維持到出貨畫面倒數接近結束才自動關閉。
        // 畫面取貨倒數為 15 秒、且取貨口門約在倒數開始前 1~2 秒就已開啟，故設 15 秒可讓門開到倒數剩約 1~2 秒才關，
        // 同時在多商品時於下一個商品出貨前已關門。range 3-250，主機機櫃號=0。如需微調只改此常數。
        devXinYuanController.addSendBuffer(
                devXinYuanController.getDevXinYuan().setPickupDoorCloseTime(0, PICKUP_DOOR_CLOSE_SECONDS));
        } // end if (!MaintenanceMode.isSerialReleased())：維修模式略過下位機串口啟動與初始化

        initialSettingData();
        initialSystemSet(getApplicationContext());
        initialReportData();
        calculateScreenSize();

        this.uploadProductList = new ArrayList<>();
        this.productList = new ArrayList<>();
        initialProductData();
        this.slotMap = new HashMap<>();
        initialSlotData();
        this.marketingPlanList = new ArrayList<>();
        this.buyList = new ArrayList<>();
        this.memoMap = new HashMap<>();
        initialMemoData();

        PeriodicWorkRequest periodicWorkRequest = new PeriodicWorkRequest.Builder(
                UploadLogPeriodicWorker.class,
                UploadLogPeriodicWorker.getTimeinByMinute(),
                TimeUnit.MINUTES)
                .setBackoffCriteria(
                        BackoffPolicy.EXPONENTIAL,
                        2,
                        TimeUnit.MINUTES)
                .build();
        WorkManager.getInstance(this).enqueueUniquePeriodicWork(
                "PeriodicWorkRequest",  // 確保這個名稱的 WorkRequest 只存在一個
                ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
                periodicWorkRequest
        );

        Thread.setDefaultUncaughtExceptionHandler(new AppCrashHandler(this));
    }

    public void initialReportData() {
        setReportFlowInfoData(new ReportFlowInfoStructure("",0, "", "", "", 0, "", "", 0, 0, 0, new JSONArray()));
        setPostInvoiceByGreenData(new PostInvoiceByGreenStructure("","", 0, new InvoiceStructure("", "", "", ""), new ArrayList<>()));
        setReportInvoiceInfoData(new ReportInvoiceInfoStructure("", "", "", "", "", "", ""));
        setInvoiceData(new InvoiceStructure("", "", "", ""));
        setInvoiceItems(new ArrayList<>());
        setReportDispatchInfoData(new ReportDispatchInfoStructure("", 0, "", 0, 0, "", 0, "", "", 0, 0, 0));
        setReportAccessCodeStructure(new ReportAccessCodeStructure("", 0, 0, "", ""));
        setAccessCodeStructure(new AccessCodeStructure(""));
        setFreeGiftStructure(new FreeGiftStructure(""));
//        setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
        setMemberVerificationStructure(new MemberVerificationStructure("", 0, "", 0));
        //addReportDispatchInfoData(new ReportDispatchInfoStructure("", 0, "", 0, 0, "", 0, "", "", 0, 0, 0));
    }

    private void initialSystemSet(Context context) {
        MemoDao memoDao = new MemoDao(context);

        if (memoDao.existOnlyOne(MyApp.getInstance().getDevSet().getClass().getSimpleName())) {
            JSONObject jsonObject = memoDao.getOnlyOne(MyApp.getInstance().getDevSet().getClass().getSimpleName()).getData();
            logs.debug("initialSystemSet:" + jsonObject.toString());
            try {
                MyApp.getInstance().getDevSet().setVMC(jsonObject.getBoolean("VMC"));
                MyApp.getInstance().getDevSet().setElectic(jsonObject.getBoolean("Electic"));
                MyApp.getInstance().getDevSet().setShoppingCar(jsonObject.getBoolean("ShoppingCar"));
                MyApp.getInstance().getDevSet().setInvoice(jsonObject.getBoolean("Invoice"));
                MyApp.getInstance().getDevSet().setDevNFCPay(jsonObject.getBoolean("DevNFCPay"));
                MyApp.getInstance().getDevSet().setDevEsunPay(jsonObject.getBoolean("DevEsunPay"));
                MyApp.getInstance().getDevSet().setDevTapPay(jsonObject.getBoolean("DevTapPay"));
                MyApp.getInstance().getDevSet().setDevCash(jsonObject.getBoolean("DevCash"));
                MyApp.getInstance().getDevSet().setDevLinePay(jsonObject.getBoolean("DevLinePay"));
                // TapPay30~34 與取貨碼/通行碼：用 optBoolean 容忍舊紀錄缺 key，避免整段載入中斷
                MyApp.getInstance().getDevSet().setTapPay30(jsonObject.optBoolean("TapPay30", false));
                MyApp.getInstance().getDevSet().setTapPay31(jsonObject.optBoolean("TapPay31", false));
                MyApp.getInstance().getDevSet().setTapPay32(jsonObject.optBoolean("TapPay32", false));
                MyApp.getInstance().getDevSet().setTapPay33(jsonObject.optBoolean("TapPay33", false));
                MyApp.getInstance().getDevSet().setTapPay34(jsonObject.optBoolean("TapPay34", false));
                MyApp.getInstance().getDevSet().setPickupCode(jsonObject.optBoolean("PickupCode", false));
                MyApp.getInstance().getDevSet().setPassCode(jsonObject.optBoolean("PassCode", false));
                // B014 新增開關（DevSet 細項 + FunctionSet 模組），optBoolean 容忍舊紀錄缺 key
                MyApp.getInstance().getDevSet().setDevCreditCard(jsonObject.optBoolean("DevCreditCard", false));
                MyApp.getInstance().getDevSet().setDevMobilePay(jsonObject.optBoolean("DevMobilePay", false));
                MyApp.getInstance().getDevSet().setDevCardPay(jsonObject.optBoolean("DevCardPay", false));
                MyApp.getInstance().getDevSet().setDevScanPay(jsonObject.optBoolean("DevScanPay", false));
                MyApp.getInstance().getDevSet().setPickupModule(jsonObject.optBoolean("PickupModule", false));
                MyApp.getInstance().getDevSet().setPharmacyPickup(jsonObject.optBoolean("PharmacyPickup", false));
                MyApp.getInstance().getDevSet().setWelcomeGift(jsonObject.optBoolean("WelcomeGift", false));
                MyApp.getInstance().getDevSet().setMemberSystem(jsonObject.optBoolean("MemberSystem", false));
                MyApp.getInstance().getDevSet().setAmbientTemp(jsonObject.optBoolean("AmbientTemp", false));
                MyApp.getInstance().setShoppingMode(jsonObject.optString("ShoppingMode", "basic"));
                MyApp.getInstance().setLogLevel(jsonObject.optString("LogLevel", "INFO"));
                // 機台顯示語系（B014 LangSet）
                java.util.List<String> langs = new java.util.ArrayList<>();
                JSONArray langArr = jsonObject.optJSONArray("MachineLanguages");
                if (langArr != null) {
                    for (int i = 0; i < langArr.length(); i++) {
                        String l = langArr.optString(i, "");
                        if (l != null && !l.isEmpty()) langs.add(l);
                    }
                }
                MyApp.getInstance().setMachineLanguages(langs);
                MyApp.getInstance().setDefaultLanguage(jsonObject.optString("DefaultLanguage", ""));
            } catch (JSONException e) {
                new Logs(MyApp.class).warning(e);
            }
        }

        // 商品名稱多語 blob（B012 t060v01_i18n），離線重啟時供 fallback
        if (memoDao.existOnlyOne("ProductNameI18n")) {
            MyApp.getInstance().setProductNameI18nBlob(memoDao.getOnlyOne("ProductNameI18n").getData());
        }

        // 商品規格多語 blob（B012 t060v03_i18n），離線重啟時供 fallback
        if (memoDao.existOnlyOne("ProductSpecI18n")) {
            MyApp.getInstance().setProductSpecI18nBlob(memoDao.getOnlyOne("ProductSpecI18n").getData());
        }

        // 商品彈簧補貨上限 blob（B012 spring_limit），離線重啟時供一鍵補貨 fallback
        if (memoDao.existOnlyOne("ProductSpringLimit")) {
            MyApp.getInstance().setProductSpringLimitBlob(memoDao.getOnlyOne("ProductSpringLimit").getData());
        }

        if (memoDao.existOnlyOne(MyApp.getInstance().getCashSetStructure().getClass().getSimpleName())) {
            JSONObject jsonObject = memoDao.getOnlyOne(MyApp.getInstance().getCashSetStructure().getClass().getSimpleName()).getData();
            logs.debug("initialSystemSet:" + jsonObject.toString());
            try {
                MyApp.getInstance().getCashSetStructure().setBillF1000(jsonObject.getBoolean("BillF1000"));
                MyApp.getInstance().getCashSetStructure().setBillE500(jsonObject.getBoolean("BillE500"));
                MyApp.getInstance().getCashSetStructure().setBillD100(jsonObject.getBoolean("BillD100"));
                MyApp.getInstance().getCashSetStructure().setCoinF50(jsonObject.getBoolean("CoinF50"));
                MyApp.getInstance().getCashSetStructure().setCoinE10(jsonObject.getBoolean("CoinE10"));
                MyApp.getInstance().getCashSetStructure().setCoinD5(jsonObject.getBoolean("CoinD5"));
                MyApp.getInstance().getCashSetStructure().setCoinC1(jsonObject.getBoolean("CoinC1"));
            } catch (JSONException e) {
                new Logs(MyApp.class).warning(e);
            }
        }

        if (memoDao.existOnlyOne(MyApp.getInstance().getHardwareSetStructure().getClass().getSimpleName())) {
            JSONObject jsonObject = memoDao.getOnlyOne(MyApp.getInstance().getHardwareSetStructure().getClass().getSimpleName()).getData();
            logs.debug("initialSystemSet:" + jsonObject.toString());
            try {
                HardwareSetStructure h = MyApp.getInstance().getHardwareSetStructure();
                h.setSpringSlot1_10(jsonObject.optBoolean("SpringSlot1_10", false));
                h.setSpringSlot11_20(jsonObject.optBoolean("SpringSlot11_20", false));
                h.setSpringSlot21_30(jsonObject.optBoolean("SpringSlot21_30", false));
                h.setSpringSlot31_40(jsonObject.optBoolean("SpringSlot31_40", false));
                h.setSpringSlot41_50(jsonObject.optBoolean("SpringSlot41_50", false));
                h.setSpringSlot51_60(jsonObject.optBoolean("SpringSlot51_60", false));
            } catch (Exception e) {
                new Logs(MyApp.class).warning(e);
            }
        }
    }
    public static void saveSystemSet(Context context) {
        MemoDao memoDao = new MemoDao(context);

        try {
            JSONObject systemSetByJsonObject = new JSONObject();
            systemSetByJsonObject.put("VMC", MyApp.getInstance().getDevSet().isVMC());
            systemSetByJsonObject.put("Electic", MyApp.getInstance().getDevSet().isElectic());
            systemSetByJsonObject.put("ShoppingCar", MyApp.getInstance().getDevSet().isShoppingCar());
            systemSetByJsonObject.put("Invoice", MyApp.getInstance().getDevSet().isInvoice());
            systemSetByJsonObject.put("DevNFCPay", MyApp.getInstance().getDevSet().isDevNFCPay());
            systemSetByJsonObject.put("DevEsunPay", MyApp.getInstance().getDevSet().isDevEsunPay());
            systemSetByJsonObject.put("DevTapPay", MyApp.getInstance().getDevSet().isDevTapPay());
            systemSetByJsonObject.put("DevCash", MyApp.getInstance().getDevSet().isDevCash());
            systemSetByJsonObject.put("DevLinePay", MyApp.getInstance().getDevSet().isDevLinePay());
            systemSetByJsonObject.put("TapPay30", MyApp.getInstance().getDevSet().isTapPay30());
            systemSetByJsonObject.put("TapPay31", MyApp.getInstance().getDevSet().isTapPay31());
            systemSetByJsonObject.put("TapPay32", MyApp.getInstance().getDevSet().isTapPay32());
            systemSetByJsonObject.put("TapPay33", MyApp.getInstance().getDevSet().isTapPay33());
            systemSetByJsonObject.put("TapPay34", MyApp.getInstance().getDevSet().isTapPay34());
            systemSetByJsonObject.put("PickupCode", MyApp.getInstance().getDevSet().isPickupCode());
            systemSetByJsonObject.put("PassCode", MyApp.getInstance().getDevSet().isPassCode());
            systemSetByJsonObject.put("DevCreditCard", MyApp.getInstance().getDevSet().isDevCreditCard());
            systemSetByJsonObject.put("DevMobilePay", MyApp.getInstance().getDevSet().isDevMobilePay());
            systemSetByJsonObject.put("DevCardPay", MyApp.getInstance().getDevSet().isDevCardPay());
            systemSetByJsonObject.put("DevScanPay", MyApp.getInstance().getDevSet().isDevScanPay());
            systemSetByJsonObject.put("PickupModule", MyApp.getInstance().getDevSet().isPickupModule());
            systemSetByJsonObject.put("PharmacyPickup", MyApp.getInstance().getDevSet().isPharmacyPickup());
            systemSetByJsonObject.put("WelcomeGift", MyApp.getInstance().getDevSet().isWelcomeGift());
            systemSetByJsonObject.put("MemberSystem", MyApp.getInstance().getDevSet().isMemberSystem());
            systemSetByJsonObject.put("AmbientTemp", MyApp.getInstance().getDevSet().isAmbientTemp());
            systemSetByJsonObject.put("ShoppingMode", MyApp.getInstance().getShoppingMode());
            systemSetByJsonObject.put("LogLevel", MyApp.getInstance().getLogLevel());
            systemSetByJsonObject.put("MachineLanguages", new JSONArray(MyApp.getInstance().getMachineLanguages()));
            systemSetByJsonObject.put("DefaultLanguage", MyApp.getInstance().getDefaultLanguage());

            JSONObject cashSetByJsonObject = new JSONObject();
            cashSetByJsonObject.put("BillF1000", MyApp.getInstance().getCashSetStructure().isBillF1000());
            cashSetByJsonObject.put("BillE500", MyApp.getInstance().getCashSetStructure().isBillE500());
            cashSetByJsonObject.put("BillD100", MyApp.getInstance().getCashSetStructure().isBillD100());
            cashSetByJsonObject.put("CoinF50", MyApp.getInstance().getCashSetStructure().isCoinF50());
            cashSetByJsonObject.put("CoinE10", MyApp.getInstance().getCashSetStructure().isCoinE10());
            cashSetByJsonObject.put("CoinD5", MyApp.getInstance().getCashSetStructure().isCoinD5());
            cashSetByJsonObject.put("CoinC1", MyApp.getInstance().getCashSetStructure().isCoinC1());

            JSONObject hardwareSetByJsonObject = new JSONObject();
            hardwareSetByJsonObject.put("SpringSlot1_10", MyApp.getInstance().getHardwareSetStructure().isSpringSlot1_10());
            hardwareSetByJsonObject.put("SpringSlot11_20", MyApp.getInstance().getHardwareSetStructure().isSpringSlot11_20());
            hardwareSetByJsonObject.put("SpringSlot21_30", MyApp.getInstance().getHardwareSetStructure().isSpringSlot21_30());
            hardwareSetByJsonObject.put("SpringSlot31_40", MyApp.getInstance().getHardwareSetStructure().isSpringSlot31_40());
            hardwareSetByJsonObject.put("SpringSlot41_50", MyApp.getInstance().getHardwareSetStructure().isSpringSlot41_50());
            hardwareSetByJsonObject.put("SpringSlot51_60", MyApp.getInstance().getHardwareSetStructure().isSpringSlot51_60());

            memoDao.insertOnlyOne(new MemoStructure(
                    MyApp.getInstance().getDevSet().getClass().getSimpleName(),
                    systemSetByJsonObject
                    , new JSONObject()));

            memoDao.mapUpdate();

            new Logs(MyApp.class).info("saveSystemSet count:" + memoDao.getCount(MyApp.getInstance().getDevSet().getClass().getSimpleName()));

            memoDao.insertOnlyOne(new MemoStructure(
                    MyApp.getInstance().getCashSetStructure().getClass().getSimpleName(),
                    cashSetByJsonObject
                    , new JSONObject()));

            memoDao.mapUpdate();

            new Logs(MyApp.class).info("saveSystemSet count:" + memoDao.getCount(MyApp.getInstance().getCashSetStructure().getClass().getSimpleName()));

            memoDao.insertOnlyOne(new MemoStructure(
                    MyApp.getInstance().getHardwareSetStructure().getClass().getSimpleName(),
                    hardwareSetByJsonObject
                    , new JSONObject()));

            memoDao.mapUpdate();

            new Logs(MyApp.class).info("saveSystemSet count:" + memoDao.getCount(MyApp.getInstance().getHardwareSetStructure().getClass().getSimpleName()));
        } catch (JSONException e) {
            new Logs(MyApp.class).warning(e);
        }
    }

    // 持久化商品名稱多語 blob（不動 products 表 schema，存於 MemoDao），供離線重啟 fallback
    public static void saveProductNameI18n(Context context) {
        MemoDao memoDao = new MemoDao(context);
        try {
            memoDao.insertOnlyOne(new MemoStructure(
                    "ProductNameI18n",
                    MyApp.getInstance().getProductNameI18nBlob(),
                    new JSONObject()));
            memoDao.mapUpdate();
        } catch (Exception e) {
            new Logs(MyApp.class).warning(e);
        }
    }

    // 持久化商品規格多語 blob（不動 products 表 schema，存於 MemoDao），供離線重啟 fallback
    public static void saveProductSpecI18n(Context context) {
        MemoDao memoDao = new MemoDao(context);
        try {
            memoDao.insertOnlyOne(new MemoStructure(
                    "ProductSpecI18n",
                    MyApp.getInstance().getProductSpecI18nBlob(),
                    new JSONObject()));
            memoDao.mapUpdate();
        } catch (Exception e) {
            new Logs(MyApp.class).warning(e);
        }
    }

    // 持久化商品彈簧補貨上限 blob（不動 products 表 schema，存於 MemoDao），供離線重啟一鍵補貨 fallback
    public static void saveProductSpringLimit(Context context) {
        MemoDao memoDao = new MemoDao(context);
        try {
            memoDao.insertOnlyOne(new MemoStructure(
                    "ProductSpringLimit",
                    MyApp.getInstance().getProductSpringLimitBlob(),
                    new JSONObject()));
            memoDao.mapUpdate();
        } catch (Exception e) {
            new Logs(MyApp.class).warning(e);
        }
    }

    public static void updateLog(Context context) {
        OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(UploadLogOneTimeWorker.class)
                .setInitialDelay(5, TimeUnit.SECONDS)
                .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
                .build();
        WorkManager.getInstance(context).enqueueUniqueWork(
                "oneTimeWorkRequest",  // 設定唯一名稱，避免重複
                ExistingWorkPolicy.REPLACE,
                oneTimeWorkRequest
        );
    }

    public static boolean checkAndReconnectDev() {
        boolean isAlive = false;
        Logs staticLogs = new Logs(MyApp.class);

        // 維修模式：串口已釋放給下位機軟體，不可自動重連(否則會再度搶回串口)。(移植自 StarApp-LockBoard)
        if (com.unibuy.smartdevice.devices.MaintenanceMode.isSerialReleased()) {
            return true;
        }

        if (devXinYuanController != null) {
            try {
                // 發送同步包測試
                devXinYuanController.addSendBuffer(
                        devXinYuanController.getDevXinYuan().syncPackNo((byte) 0x01)
                );
                Thread.sleep(100);
                if (!devXinYuanController.getReadBufferByMax().isEmpty()) {
                    isAlive = true;
                }
            } catch (Exception e) {
                isAlive = false;
            }
        }

        // 若不活躍則重連
        if (!isAlive) {
            staticLogs.info("下位機失連，重新連線中...");
            HandlerMain oldHandlerMain = devXinYuanController != null
                    ? devXinYuanController.getHandlerMain() : null;
            if (devXinYuanController != null) {
                // ★ 必須用 releasePort()（setRun(false)+closePort()+shutdown()）完整關閉舊控制器，
                //   讓舊的讀取 while(isRun()) 執行緒真正跳出、釋放 ttyS7。
                //   之前只呼叫 shutdown()（僅停排程器），舊讀取執行緒還活著、舊串口還開著，
                //   接著又 new DevXinYuanController() 把同一個 ttyS7 再開一次 → 兩條執行緒搶讀同一串口
                //   → 位元組框架被撕裂、VMC_41_POLL 認不出、出貨指令送不出去 → 出貨失敗（VMC_52 洪水即此徵兆）。
                devXinYuanController.releasePort();
            }
            // 給舊讀取執行緒一點時間因 closePort() 拋例外跳出迴圈、釋放串口，再重開新串口，避免搶佔。
            try {
                Thread.sleep(150);
            } catch (InterruptedException ignore) {
            }
            devXinYuanController = new DevXinYuanController(oldHandlerMain);
            devXinYuanController.start();
            devXinYuanController.addSendBuffer(
                    devXinYuanController.getDevXinYuan().syncPackNo((byte) 0x01)
            );
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    /**
     * 維修模式：立即釋放下位機串口(主櫃 XinYuan)，讓原廠下位機軟體可讀取。呼叫端須先設好
     * MaintenanceMode.setSerialReleased(true)。搭配持久化旗標，後續自動重啟/開機自啟也不再佔用串口，
     * 直到手動退出維修模式。(移植自 StarApp-LockBoard；本機為 XinYuan 單下位機，無副櫃鎖控板)
     */
    public synchronized void releaseAllDeviceSerialPorts() {
        logs.info("[維修模式] 釋放下位機串口(主櫃 XinYuan)");
        if (devXinYuanController != null) {
            devXinYuanController.releasePort();
        }
    }

    /**
     * 進支付前用：送同步包確認主櫃(XinYuan)下位機是否真的有回應。
     * 先清掉舊 buffer 再送，避免讀到先前殘留的回應誤判為已連線。
     * 注意：會 Thread.sleep，請在背景執行緒呼叫。
     */
    private static boolean pingXinYuanOnce(long waitMs) {
        if (devXinYuanController == null) return false;
        try {
            devXinYuanController.clearReadBuffers();
            devXinYuanController.addSendBuffer(
                    devXinYuanController.getDevXinYuan().syncPackNo((byte) 0x01));
            Thread.sleep(waitMs);
            return !devXinYuanController.getReadBufferByMax().isEmpty();
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 下位機是否在線。優先採信「近期任何 VMC 通訊」——下位機為輪詢式主從、持續在 POLL，
     * 只要近 4 秒收過任何 VMC 幀(POLL/ACK/廣播…)即代表在線，直接判定連線，
     * 避免脆弱的 300ms 主動 ping 在機台忙碌/送出佇列消化時收不到即時回應而誤判「尚未連線」。
     * 近期完全沒通訊時，才主動 ping 兩次(各 500ms)確認（降低偶發誤判，單次沒回不代表失連）。
     */
    public static boolean isXinYuanAlive() {
        if (com.unibuy.smartdevice.controller.DevXinYuanController.isRecentlyAlive(4000)) return true;
        return pingXinYuanOnce(500) || pingXinYuanOnce(500);
    }

    /**
     * 進支付前用：確認主櫃下位機連線；若失連則主動重連後再確認。
     * 回傳最終是否連線成功（false=連不上，呼叫端應擋住不進支付）。
     * 注意：會 Thread.sleep，請在背景執行緒呼叫。
     */
    public static boolean ensureXinYuanConnected() {
        if (isXinYuanAlive()) return true;
        checkAndReconnectDev();   // 主動重連
        return isXinYuanAlive();  // 重連後再確認
    }

    /**
     * 出貨當下用：出貨前最後一道保險，確認主櫃(XinYuan)下位機連線；
     * 失連就「反覆重連並等到活著」直到成功或逾時 maxWaitMs。
     * 與進支付前的 ensureXinYuanConnected 不同：這裡會多輪重連(每輪等重開串口/重啟讀取迴圈)，
     * 因為下位機常在重啟後短暫失聯，單輪重連可能還沒起來，需持續重試到真的活著才送出貨指令。
     * 回傳最終是否連線成功。注意：含 Thread.sleep，請在背景執行緒呼叫。
     */
    public static boolean ensureXinYuanConnectedForDispatch(long maxWaitMs) {
        Logs staticLogs = new Logs(MyApp.class);
        if (isXinYuanAlive()) return true;
        long deadline = System.currentTimeMillis() + maxWaitMs;
        int round = 0;
        while (System.currentTimeMillis() < deadline) {
            round++;
            staticLogs.info("出貨前下位機失連，第 " + round + " 次自動重連...");
            checkAndReconnectDev();          // 重建 controller、重開串口、重啟讀取迴圈
            try { Thread.sleep(800); } catch (InterruptedException ignore) {}
            if (isXinYuanAlive()) {
                staticLogs.info("出貨前重連成功(第 " + round + " 輪)，下位機已連線");
                return true;
            }
        }
        boolean alive = isXinYuanAlive();
        staticLogs.info("出貨前重連結束，逾時=" + maxWaitMs + "ms，最終連線=" + alive);
        return alive;
    }

    public void initMqtt() {
        String machineId = MyApp.getInstance().getMachine().getMachineID();
        String apiKey = MyApp.getInstance().getMachine().getApiKey();

        if (machineId != null && !machineId.isEmpty() && apiKey != null && !apiKey.isEmpty()) {
            logs.info("Starting MQTT Service for machine: " + machineId);
            
            Intent serviceIntent = new Intent(this, com.unibuy.smartdevice.service.MqttService.class);
            serviceIntent.putExtra("machineId", machineId);
            serviceIntent.putExtra("apiToken", apiKey);
            
            try {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    startForegroundService(serviceIntent);
                } else {
                    startService(serviceIntent);
                }
            } catch (Exception e) {
                // Android 12+ 在背景狀態下（如安裝後自動重啟）不允許啟動前台/背景服務
                // 這裡只記錄日誌，不再嘗試 fallback，等使用者開啟 App 進前台後再由 Activity 觸發
                logs.info("MQTT Service start blocked (app in background): " + e.getMessage());
            }
        }
    }

    public void initialSettingData() {
        SettingsDao settingsDao = new SettingsDao(getApplicationContext());

        if (MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
            SettingStructure setting = settingsDao.getOne(MyApp.getInstance().getMachine().getClass().getSimpleName());
            if (setting != null) {
                logs.info("initialSettingData Machine:" + setting.toString());
                MyApp.getInstance().getMachine().setApiKey(setting.getData(0));
                MyApp.getInstance().getMachine().setMachineID(setting.getData(1));
                if (setting.getDataCount() >= 3 && setting.getData(2) != null && !setting.getData(2).isEmpty()) {
                    MyApp.getInstance().getMachine().setMqttEnv(setting.getData(2));
                }
            }
        }

        initMqtt();

        if (MyApp.getInstance().getGreenInvoice().getGreenMerchantID().isEmpty()) {
            SettingStructure setting = settingsDao.getOne(MyApp.getInstance().getGreenInvoice().getClass().getSimpleName());
            MyApp.getInstance().getGreenInvoice().setGreenMerchantID(setting.getData(0));
            MyApp.getInstance().getGreenInvoice().setGreenHashKey(setting.getData(1));
            MyApp.getInstance().getGreenInvoice().setGreenHashIV(setting.getData(2));
            MyApp.getInstance().getGreenInvoice().setGreenEmail(setting.getData(3));
        }

        if (MyApp.getInstance().getEsunpay().getStoreID().isEmpty()) {
            SettingStructure setting = settingsDao.getOne(MyApp.getInstance().getEsunpay().getClass().getSimpleName());
            MyApp.getInstance().getEsunpay().setStoreID(setting.getData(0));
            MyApp.getInstance().getEsunpay().setTermID(setting.getData(1));
            MyApp.getInstance().getEsunpay().setHash(setting.getData(2));
        }

        if (MyApp.getInstance().getGreenpaySetting().getAppId().isEmpty()) {
            SettingStructure setting = settingsDao.getOne(MyApp.getInstance().getGreenpaySetting().getClass().getSimpleName());
            MyApp.getInstance().getGreenpaySetting().setAppId(setting.getData(0));
            MyApp.getInstance().getGreenpaySetting().setAppKey(setting.getData(1));
            MyApp.getInstance().getGreenpaySetting().setPartnerKey(setting.getData(2));
            MyApp.getInstance().getGreenpaySetting().setCtbcMerchantId(setting.getData(3));
            MyApp.getInstance().getGreenpaySetting().setNcccMerchantId(setting.getData(4));
            MyApp.getInstance().getGreenpaySetting().setTaishinMerchantId(setting.getData(5));
            MyApp.getInstance().getGreenpaySetting().setEsunMerchantId(setting.getData(6));
            MyApp.getInstance().getGreenpaySetting().setGpuseMerchantId(setting.getData(7));

            SettingStructure setting2 = settingsDao.getOne(MyApp.getInstance().getGreenpayMerchant().getClass().getSimpleName());
            MyApp.getInstance().getGreenpayMerchant().setLineMerchantId(setting2.getData(0));
            MyApp.getInstance().getGreenpayMerchant().setPsMerchantId(setting2.getData(1));
            MyApp.getInstance().getGreenpayMerchant().setEasyMerchantId(setting2.getData(2));
            MyApp.getInstance().getGreenpayMerchant().setPiMerchantId(setting2.getData(3));
            MyApp.getInstance().getGreenpayMerchant().setJkoMerchantId(setting2.getData(4));
            MyApp.getInstance().getGreenpayMerchant().setDirectMerchantId(setting2.getData(5));
            MyApp.getInstance().getGreenpayMerchant().setAppleMerchantId(setting2.getData(6));
            MyApp.getInstance().getGreenpayMerchant().setGoogleMerchantId(setting2.getData(7));
            MyApp.getInstance().getGreenpayMerchant().setSamsungMerchantId(setting2.getData(8));
            MyApp.getInstance().getGreenpayMerchant().setAtomeMerchantId(setting2.getData(9));
        }

        if (MyApp.getInstance().getFontendAdUrlString().isEmpty()) {
            List<SettingStructure> settingList = settingsDao.getWhere(VideoEoxPlayer.class.getSimpleName());
            for (SettingStructure setting: settingList) {
                MyApp.getInstance().getFontendAdUrlString().add(setting.getData(0));
            }
        }

        settingsDao.close();
    }
    private void initialProductData() {
        if (MyApp.getInstance().getProductList().isEmpty()) {
            ProductsDao productsDao = new ProductsDao(this);
            for (ProductStructure product: productsDao.getAll()) {
                MyApp.getInstance().getProductList().add(product);
            }
            productsDao.close();
            logs.info("product count:"+ MyApp.getInstance().getProductList().size());
        }
    }

    public void initialSlotData() {
        logs.debug("initialSlotData");

        //開架構
        for (SlotField slotField: SlotField.values()) {
            if (!getInstance().getSlotMap().containsKey(slotField.getField())) {
                getInstance().getSlotMap().put(slotField.getField(), new ArrayList<>());
                logs.info(slotField.getField()+" slot db open");
            }
        }

        //查詢資料庫
        SlotsDao slotsDao = new SlotsDao(this);
        Map<Integer, List<SlotStructure>> slotMapNew = slotsDao.getAll();
        for (SlotField slotField: SlotField.values()) {
            if (getInstance().getSlotList(slotField.getField()).isEmpty() && slotMapNew.get(slotField.getField()) != null && !slotMapNew.get(slotField.getField()).isEmpty()) {
                List<SlotStructure> slotList = slotMapNew.get(slotField.getField());
                for (SlotStructure slot : slotList) {
                    getInstance().getSlotList(slotField.getField()).add(slot);
                }
                logs.info(slotField.getField()+" slot db count:"+getInstance().getSlotList(slotField.getField()).size());
            }
        }
        slotsDao.close();

        //無資料就初始化
        if (getInstance().getSlotList(SlotField.VMC.getField()).isEmpty()) {
            for (int i = 1; i <= 60; i++) {
                getInstance().getSlotList(SlotField.VMC.getField()).add(new SlotStructure(SlotField.VMC.getField(), i,
                        new ProductStructure("?", "", "", 0, 0, 0, 0, "", "","",""),
                        0));
            }
            logs.info(SlotField.VMC.getField()+" slot init count:" + getInstance().getSlotList(SlotField.VMC.getField()).size());
        }

        if (getInstance().getSlotList(SlotField.ELECTRIC.getField()).isEmpty()) {
            for (int i = 1; i <= 10; i++) {
                getInstance().getSlotList(SlotField.ELECTRIC.getField()).add(new SlotStructure(SlotField.ELECTRIC.getField(), i,
                        new ProductStructure("?", "", "", 0, 0, 0, 0, "", "","",""),
                        0));
            }
            logs.info(SlotField.ELECTRIC.getField()+" slot init count:" + getInstance().getSlotList(SlotField.ELECTRIC.getField()).size());
        }
    }

    private void calculateScreenSize() {
        // 取得 WindowManager
        WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics metrics = new DisplayMetrics();

        // 取得螢幕尺寸
        if (wm != null) {
            wm.getDefaultDisplay().getMetrics(metrics);
            this.screenWidthPx = metrics.widthPixels;
            this.screenHeightPx = metrics.heightPixels;
            density = metrics.density;  // 保存密度值
        }
    }

    // 新增 px 轉 dp 的函數
    public int pxToDp(int px) {
        return Math.round(px / density);
    }

    // 新增 dp 轉 px 的函數（可選）
    public int dpToPx(int dp) {
        return Math.round(dp * density);
    }

    public int getScreenWidthPx() {
        return screenWidthPx;
    }

    public int getScreenHeightPx() {
        return screenHeightPx;
    }

    public ComPort getComPort1ByXinYuan() {
        return comPort1ByXinYuan;
    }

    public ComPort getComPort2ByNFCPay() {
        return comPort2ByNFCPay;
    }

    public List<ProductStructure> getUploadProductList() {
        return uploadProductList;
    }

    public List<ProductStructure> getProductList() {
        return productList;
    }

    public List<String> getFontendAdUrlString() {
        return fontendAdUrlString;
    }

    public ProductStructure getProduct(String productId) throws LogsNoSuchElementException {
        for (ProductStructure product: getProductList()) {
            if (product.getProductID().equals(productId)) {
                return product;
            }
        }

        throw new LogsNoSuchElementException(logs, "get product "+productId+" not found");
    }

    public Map<Integer, List<SlotStructure>> getSlotMap() {
        return slotMap;
    }

    public List<SlotStructure> getSlotList(int field) {
        return getSlotMap().get(field);
    }

    public SlotStructure getSlotData(int field, int slot) throws LogsEmptyException {
        for (SlotStructure slotData: getSlotList(field)) {
            if (slotData.getSlot() == slot) {
                return slotData;
            }
        }

        throw new LogsEmptyException(logs, "Slot data not found");
    }

    public SlotStructure getSlotDataByPid(int field, String pid) throws LogsEmptyException {
        int i = 0;
        for (SlotStructure slotData: getSlotList(field)) {
            i += 1;
            logs.debug("slot index:" + i);
            if (slotData.getProduct().getProductID().equals(String.valueOf(pid))) {
                if(slotData.getCount() > 0) {
                    return slotData;
                }
            }
        }

        throw new LogsEmptyException(logs, "Slot data not found");
    }

    public SlotStructure getSlotDataByPid(String pid) throws LogsEmptyException {
        for (SlotField slotField: SlotField.values()) {
            try {
                return getSlotDataByPid(slotField.getField(), pid);
            } catch (LogsEmptyException e) {

            }
        }

        throw new LogsEmptyException(logs, "Slot data not found");
    }

    /**
     * 把「某商品要買 quantity 個」依可賣貨道(count>0 && !lock && !expired)拆成多筆 count=1 的 BuyStructure。
     *
     * 用途：VMC 出貨狀態機不支援單筆 DispatchStructure quantity>1（出完第 1 個後 count<quantity，
     * 狀態機不會主動再觸發下一次出貨，只能等 40 秒看門狗逾時被動補出，形同卡死）。因此多數量必須在
     * 進狀態機之前就拆成「一筆一個」，與中國醫領藥既有做法一致。
     *
     * greedy 分配：優先用代表貨道(representative)，再依貨道號遞增溢位到下一個可賣道，盡量集中以維持
     * 貨道補貨整齊；單道不足才換道。可賣總庫存若仍不足 quantity（例如加入購物車後庫存又被消耗），
     * 剩餘數量掛回代表貨道，交由既有「出貨失敗→後台依 dispense 退款」保護網處理，不在此處吞掉訂單金額。
     *
     * 注意：representative 可能是銷售畫面合併後的「代表卡」(getCount() 為加總值)，所以這裡一律以
     * getSlotList(field) 的真實逐道庫存為準，不信任 representative.getCount()。
     */
    public List<BuyStructure> allocateBuyStructures(SlotStructure representative, int quantity) {
        List<BuyStructure> result = new ArrayList<>();
        if (representative == null || quantity <= 0) {
            return result;
        }
        ProductStructure repProduct = representative.getProduct();
        if (repProduct == null || repProduct.getProductID() == null) {
            // 退化情形：仍維持「一筆一個」不變式，避免漏單或重新塞回 quantity>1
            for (int i = 0; i < quantity; i++) {
                result.add(new BuyStructure(representative.getField(), representative.getSlot(), repProduct, 1));
            }
            return result;
        }

        int field = representative.getField();
        String pid = repProduct.getProductID();
        int preferredSlot = representative.getSlot();

        // 收集同商品可賣貨道：代表道優先，其餘依貨道號遞增
        SlotStructure preferred = null;
        List<SlotStructure> others = new ArrayList<>();
        List<SlotStructure> slotList = getSlotList(field);
        if (slotList != null) {
            for (SlotStructure s : slotList) {
                if (s == null || s.getProduct() == null) continue;
                if (!pid.equals(s.getProduct().getProductID())) continue;
                if (s.getCount() <= 0 || s.isLock() || s.isExpired()) continue;
                if (s.getSlot() == preferredSlot) {
                    preferred = s;
                } else {
                    others.add(s);
                }
            }
        }
        others.sort((a, b) -> Integer.compare(a.getSlot(), b.getSlot()));

        List<SlotStructure> candidates = new ArrayList<>();
        if (preferred != null) candidates.add(preferred);
        candidates.addAll(others);

        // greedy 配貨：每個候選道按其真實庫存填，逐一拆成 count=1
        int need = quantity;
        for (SlotStructure s : candidates) {
            if (need <= 0) break;
            int take = Math.min(need, s.getCount());
            for (int i = 0; i < take; i++) {
                result.add(new BuyStructure(s.getField(), s.getSlot(), s.getProduct(), 1));
            }
            need -= take;
        }

        // 可賣庫存不足 quantity 的溢位：掛回代表貨道，交既有失敗→退款保護網
        for (int i = 0; i < need; i++) {
            result.add(new BuyStructure(field, preferredSlot, repProduct, 1));
        }
        return result;
    }

    public List<MarketingPlanStructure> getMarketingPlanList() {
        return marketingPlanList;
    }

    public List<BuyStructure> getBuyList() {
        return buyList;
    }

    public Logs getLogs() {
        return logs;
    }

    public void setDecryptedData(String decryptedData) {
        this.decryptedData = decryptedData;
    }

    public String getDecryptedData() {
        return decryptedData;
    }

    public void setCmuhPickupErrorMessage(String cmuhPickupErrorMessage) {
        this.cmuhPickupErrorMessage = cmuhPickupErrorMessage;
    }

    public String getCmuhPickupErrorMessage() {
        return cmuhPickupErrorMessage;
    }

    public Map<String, List<MemoStructure>> getMemoMap() {
        return memoMap;
    }

    private void initialMemoData() {
        logs.debug("initialMemoData");

        // 確保 memoMap 已初始化
        if (getInstance().getMemoMap() == null) {
            logs.info("memoMap初始化");
            getInstance().memoMap = new HashMap<>();
        }
        
        // 開架構
        if (!getInstance().getMemoMap().containsKey("CMUH")) {
            getInstance().getMemoMap().put("CMUH", new ArrayList<>());
            logs.info("CMUH memo db open");
        }

        // 查詢資料庫
        MemoDao memoDao = new MemoDao(this);
        try {
            Map<String, List<MemoStructure>> memoMapNew = memoDao.getAll();
            if (memoMapNew != null && memoMapNew.get("CMUH") != null && !memoMapNew.get("CMUH").isEmpty()) {
                getInstance().getMemoMap().get("CMUH").addAll(memoMapNew.get("CMUH"));
            }
        } catch (Exception e) {
            logs.info("📛 讀取 memo 資料庫失敗或無資料，跳過。");
        } finally {
            memoDao.close();
        }

        // 無資料就初始化一筆資料
        if (getInstance().getMemoMap().get("CMUH").isEmpty()) {
            logs.info("初始化CMUH Memo資料表");
            String memoStr = "{\"ITEM\": [{\"SN\": \"00000000\", \"DRGNO\": \"0000\"}]}";
            try {
                getInstance().getMemoMap().get("CMUH").add(new MemoStructure("CMUH", new JSONObject(memoStr), new JSONObject()));
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public EsunpayStructure getEsunpay() {
        return esunpay;
    }

    public MachineStructure getMachine() {
        return machine;
    }

    public GreenInvoiceStructure getGreenInvoice() {
        return greenInvoice;
    }

    public GreenpaySettingStructure getGreenpaySetting() {
        return greenpaySetting;
    }

    public GreenpayMerchantStructure getGreenpayMerchant() {
        return greenpayMerchant;
    }

    public DevXinYuanController getDevXinYuanController() {
        return devXinYuanController;
    }

    public void setReportFlowInfoData(ReportFlowInfoStructure reportFlowInfoData) {
        this.reportFlowInfoData = reportFlowInfoData;
    }

    public ReportFlowInfoStructure getReportFlowInfoData() {
        return reportFlowInfoData;
    }

    public void setPostInvoiceByGreenData(PostInvoiceByGreenStructure postInvoiceByGreenData) {
        this.postInvoiceByGreenData = postInvoiceByGreenData;
    }

    public PostInvoiceByGreenStructure getPostInvoiceByGreenData() {
        return postInvoiceByGreenData;
    }

    public void setReportInvoiceInfoData(ReportInvoiceInfoStructure reportInvoiceInfoData) {
        this.reportInvoiceInfoData = reportInvoiceInfoData;
    }

    public void setReportAccessCodeStructure(ReportAccessCodeStructure reportAccessCodeStructure) {
        this.reportAccessCodeStructure = reportAccessCodeStructure;
    }

    public void setReportFreeGiftCodeStructure(ReportFreeGiftCodeStructure reportFreeGiftCodeStructure) {
        this.reportFreeGiftCodeStructure = reportFreeGiftCodeStructure;
    }

    public ReportFreeGiftCodeStructure getReportFreeGiftCodeStructure() {
        return reportFreeGiftCodeStructure;
    }

    public ReportInvoiceInfoStructure getReportInvoiceInfoData() {
        return reportInvoiceInfoData;
    }

    public void setInvoiceData(InvoiceStructure invoiceData) {
        this.invoiceData = invoiceData;
    }

    public InvoiceStructure getInvoiceData() {
        return invoiceData;
    }

    public List<InvoiceItemStructure> getInvoiceItems() {
        return invoiceItems;
    }

    public void setInvoiceItems(List<InvoiceItemStructure> invoiceItems) {
        this.invoiceItems = invoiceItems;
    }

    public void setReportDispatchInfoData(ReportDispatchInfoStructure reportDispatchInfoData) {
        this.reportDispatchInfoData = reportDispatchInfoData;
    }

    public ReportDispatchInfoStructure getReportDispatchInfoData() {
        return reportDispatchInfoData;
    }

    public void setCreditCardReceiptInfoData(CreditCardReceiptStructure creditCardReceiptInfoData){
        this.creditCardReceiptInfoData = creditCardReceiptInfoData;
    }

    public CreditCardReceiptStructure getCreditCardReceiptInfoData() {
        return creditCardReceiptInfoData;
    }


    public DevSetStructure getDevSet() {
        return devSet;
    }

    public String getShoppingMode() {
        return shoppingMode;
    }

    public void setShoppingMode(String shoppingMode) {
        this.shoppingMode = (shoppingMode == null || shoppingMode.isEmpty()) ? "basic" : shoppingMode;
    }

    public String getLogLevel() {
        return logLevel;
    }

    public void setLogLevel(String logLevel) {
        this.logLevel = (logLevel == null || logLevel.isEmpty()) ? "INFO" : logLevel.toUpperCase(java.util.Locale.US);
    }

    public java.util.List<String> getMachineLanguages() {
        return machineLanguages;
    }

    public void setMachineLanguages(java.util.List<String> machineLanguages) {
        this.machineLanguages = (machineLanguages == null) ? new java.util.ArrayList<>() : machineLanguages;
    }

    public String getDefaultLanguage() {
        return defaultLanguage;
    }

    public void setDefaultLanguage(String defaultLanguage) {
        this.defaultLanguage = (defaultLanguage == null) ? "" : defaultLanguage;
    }

    public void setProductNameI18nBlob(JSONObject blob) {
        this.productNameI18nBlob = (blob == null) ? new JSONObject() : blob;
    }

    public JSONObject getProductNameI18nBlob() {
        return productNameI18nBlob;
    }

    // 取某商品的名稱多語 JSON 字串（底線代碼 key），無則回 null。供 ProductStructure 離線時 fallback。
    public String getProductNameI18nJson(String productId) {
        if (productId == null || productNameI18nBlob == null) return null;
        JSONObject obj = productNameI18nBlob.optJSONObject(productId);
        return (obj == null) ? null : obj.toString();
    }

    public void setProductSpecI18nBlob(JSONObject blob) {
        this.productSpecI18nBlob = (blob == null) ? new JSONObject() : blob;
    }

    public JSONObject getProductSpecI18nBlob() {
        return productSpecI18nBlob;
    }

    // 取某商品的規格多語 JSON 字串（底線代碼 key），無則回 null。供 ProductStructure 離線時 fallback。
    public String getProductSpecI18nJson(String productId) {
        if (productId == null || productSpecI18nBlob == null) return null;
        JSONObject obj = productSpecI18nBlob.optJSONObject(productId);
        return (obj == null) ? null : obj.toString();
    }

    public void setProductSpringLimitBlob(JSONObject blob) {
        this.productSpringLimitBlob = (blob == null) ? new JSONObject() : blob;
    }

    public JSONObject getProductSpringLimitBlob() {
        return productSpringLimitBlob;
    }

    // 取某商品的彈簧補貨上限；blob 無此商品時回 -1（由 ProductStructure 退回履帶上限）。
    public int getProductSpringLimit(String productId) {
        if (productId == null || productSpringLimitBlob == null) return -1;
        return productSpringLimitBlob.optInt(productId, -1);
    }


    public ReportAccessCodeStructure getReportAccessCodeStructure() {
        return reportAccessCodeStructure;
    }

    public ReportSlotListStructure getReportSlotListStructure(){
        return reportSlotListStructure;
    }

    public CashSetStructure getCashSetStructure() {
        return cashSetStructure;
    }

    public HardwareSetStructure getHardwareSetStructure() {
        return hardwareSetStructure;
    }

    public AccessCodeStructure getAccessCodeStructure() {
        return accessCodeStructure;
    }

    public void setAccessCodeStructure(AccessCodeStructure accessCodeStructure) {
        this.accessCodeStructure = accessCodeStructure;
    }

    public FreeGiftStructure getFreeGiftStructure() {
        return freeGiftStructure;
    }

    public void setFreeGiftStructure(FreeGiftStructure freeGiftStructure) {
        this.freeGiftStructure = freeGiftStructure;
    }

    public MemberVerificationStructure getMemberVerificationStructure() {
        return memberVerificationStructure;
    }

    public void setMemberVerificationStructure(MemberVerificationStructure memberVerificationStructure) {
        this.memberVerificationStructure = memberVerificationStructure;
    }

    public int getCloseSlot() {
        return closeSlot;
    }

    public void setCloseSlot(int closeSlot) {
        this.closeSlot = closeSlot;
    }

    private volatile boolean isShowDialog;

    public boolean isShowDialog() {
        return isShowDialog;
    }

    public void setShowDialog(boolean showDialog) {
        if (this.isShowDialog != showDialog) {
            logs.info("setShowDialog: " + isShowDialog + " -> " + showDialog);
        }
        isShowDialog = showDialog;
    }

    public boolean isMachineBusy() {
        return !getBusyReason().isEmpty();
    }

    /**
     * 取得機台忙碌的原因，若不忙碌則回傳空字串
     * @return 忙碌原因描述
     */
    public String getBusyReason() {
        StringBuilder reason = new StringBuilder();
        // 只計入「忙碌型」對話框（付款/出貨等 isBusyDialog()==true）。
        // 純瀏覽的商品詳情(isBusyDialog()==false)不算忙碌，否則它一旦被 HID 誤觸卡住，
        // 就會讓機台長期 [Dialog Open] → 待機看門狗略過、遠端重啟被拒(Machine busy)。
        if (DialogAbstract.hasBusyDialogShowing()) {
            reason.append("[Busy Dialog Open] ");
        }
        // 注意：購物車非空「不可」計入忙碌。否則待機/重啟會與清車死結——
        // resetIdleTimer 忙碌即不待機並重新計時，但清空購物車的程式就寫在待機那一支，
        // 購物車有殘留 → 永遠忙碌 → 永遠不待機 → 永遠清不掉。真正「進行中」狀態
        // 已由忙碌型對話框（付款/出貨）與 isNfcPaymentActive（感應支付）涵蓋。
        if (isNfcPaymentActive()) {
            reason.append("[NFC Payment Active] ");
        }
        
        String result = reason.toString().trim();
        if (!result.isEmpty()) {
            logs.debug("Machine busy check: " + result);
        }
        return result;
    }

    public String getSuAccess() {
        return suAccess;
    }

    public void setSuAccess(String suAccess) {
        this.suAccess = suAccess;
    }

    public String getIdentity() {
        return identity == null ? "" : identity;
    }

    public void setIdentity(String identity) {
        this.identity = identity == null ? "" : identity;
    }

    /** 是否為系統方（可編輯系統設定並回寫雲端）。 */
    public boolean isSystemIdentity() {
        return "system".equals(getIdentity());
    }

    public String getLoginToken() {
        return loginToken == null ? "" : loginToken;
    }

    public void setLoginToken(String loginToken) {
        this.loginToken = loginToken == null ? "" : loginToken;
    }

    public boolean isDetectHost() {
        return isDetectHost;
    }

    public void setDetectHost(boolean detectHost) {
        isDetectHost = detectHost;
    }

    public boolean isNfcPaymentActive() {
        return isNfcPaymentActive;
    }

    public void setNfcPaymentActive(boolean nfcPaymentActive) {
        isNfcPaymentActive = nfcPaymentActive;
    }



}
