package com.unibuy.smartdevice.external;

import android.util.Log;
import com.unibuy.smartdevice.MyApp;
// Removed unused Scheduler imports
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.HttpConnect;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.HashMap;
import java.util.Map;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.database.ProductsDao;
import com.unibuy.smartdevice.database.SettingsDao;
import com.unibuy.smartdevice.structure.CashSetStructure;
import com.unibuy.smartdevice.structure.DevSetStructure;
import com.unibuy.smartdevice.structure.ProductStructure;
import com.unibuy.smartdevice.structure.ReportSlotListStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.devices.SlotField;
import com.unibuy.smartdevice.structure.SettingStructure;
import com.unibuy.smartdevice.ui.SettingActivity;
import com.unibuy.smartdevice.ui.tools.VideoEoxPlayer;
import com.unibuy.smartdevice.ui.tools.VideoImageMixedPlayer;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;

/**
 * 專門對接 Star-Cloud 後台的 API 類別
 */
public class StarCloudAPI {
    private static final String TAG = "StarCloudAPI";

    public static String domainNameByNewHost = "https://logs.setdomains.work/";
    public static String updateOnDomainNameByNewHost = domainNameByNewHost + "api/machine/"
            + MyApp.getInstance().getMachine().getMachineID() + "/log/update";

    private String getBaseUrl() {
        String env = MyApp.getInstance().getMachine().getMqttEnv();
        if ("prod".equals(env)) {
            return "https://starcloud.taiwan-star.com.tw/api/v1/app/";
        } else if ("local".equals(env)) {
            // 130 測試站（領藥單測試用，綁定畫面選「130測試」）
            return "http://192.168.0.130:8090/api/v1/app/";
        } else {
            return "https://demo-cloud.setdomains.work/api/v1/app/";
        }
    }

    private HandlerMain handlerMain;
    private HandlerMain srcHandlerMain;

    public StarCloudAPI(HandlerMain handlerMain, HandlerMain srcHandlerMain) {
        this.handlerMain = handlerMain;
        this.srcHandlerMain = srcHandlerMain;
    }

    private void traceLogin(String message) {
        if (handlerMain != null && handlerMain.getLogs() != null) {
            handlerMain.getLogs().info("[LoginTrace]" + message);
        } else {
            Log.i("LoginTrace", message);
        }
    }

    /**
     * B000: 管理員登入驗證
     */
    public void login(final String account, final String password) {
        login(account, password, "NO_TRACE", System.currentTimeMillis());
    }

    public void login(final String account, final String password, final String traceId, final long startMs) {
        Log.i(TAG, "login() called for account: " + account);
        final String url = getBaseUrl() + "admin/login/B000";
        Log.i(TAG, "Starting B000 request to: " + url);
        traceLogin("[" + traceId + "] b000_start fromUiMs=" + (System.currentTimeMillis() - startMs));

        // 不依賴 FormPostMainScheduler (會靜默吞掉 IO exception)
        // 直接用 Thread + HttpURLConnection 確保錯誤一定看得到
        new Thread(new Runnable() {
            @Override
            public void run() {
                int responseCode = -1;
                String receive = "";
                long threadStartMs = System.currentTimeMillis();
                try {
                    JSONObject json = new JSONObject();
                    json.put("Su_Account", account);
                    json.put("Su_Password", password);
                    json.put("ip", getLocalIpAddress());

                    java.net.URL reqUrl = new java.net.URL(url);
                    java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
                    conn.setRequestMethod("POST");
                    conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                    conn.setRequestProperty("Accept", "application/json");
                    
                    // 加入機台 Token 驗證
                    String machineToken = MyApp.getInstance().getMachine().getApiKey();
                    if (machineToken != null && !machineToken.isEmpty()) {
                        conn.setRequestProperty("Authorization", "Bearer " + machineToken);
                        Log.i(TAG, "B000 Request with Token: Bearer " + machineToken.substring(0, Math.min(machineToken.length(), 10)) + "...");
                    }

                    conn.setConnectTimeout(15000);
                    conn.setReadTimeout(15000);
                    conn.setDoOutput(true);

                    byte[] bodyBytes = json.toString().getBytes("UTF-8");
                    long writeStartMs = System.currentTimeMillis();
                    conn.getOutputStream().write(bodyBytes);
                    conn.getOutputStream().flush();
                    traceLogin("[" + traceId + "] b000_write_done tMs=" + (System.currentTimeMillis() - writeStartMs));

                    long responseStartMs = System.currentTimeMillis();
                    responseCode = conn.getResponseCode();
                    traceLogin("[" + traceId + "] b000_response_code=" + responseCode
                            + " waitMs=" + (System.currentTimeMillis() - responseStartMs));
                    java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
                            ? conn.getInputStream() : conn.getErrorStream();
                    java.io.BufferedReader br = new java.io.BufferedReader(
                            new java.io.InputStreamReader(is, "UTF-8"));
                    StringBuilder sb = new StringBuilder();
                    String line;
                    while ((line = br.readLine()) != null) sb.append(line);
                    receive = sb.toString();
                    conn.disconnect();

                    Log.d(TAG, "B000 Response (" + responseCode + "): " + receive);
                    traceLogin("[" + traceId + "] b000_network_done totalNetMs="
                            + (System.currentTimeMillis() - threadStartMs));
                } catch (final Exception e) {
                    Log.e(TAG, "B000 Network Error", e);
                    traceLogin("[" + traceId + "] b000_network_error afterMs="
                            + (System.currentTimeMillis() - threadStartMs) + " err=" + e.getMessage());
                    new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            android.widget.Toast.makeText(MyApp.getInstance(),
                                    "連線失敗: " + e.getMessage(), android.widget.Toast.LENGTH_LONG).show();
                        }
                    });
                    return;
                }

                final int finalCode = responseCode;
                final String finalReceive = receive;
                new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        long mainStartMs = System.currentTimeMillis();
                        if (finalCode >= 200 && finalCode < 300) {
                            try {
                                JSONObject res = new JSONObject(finalReceive);
                                if (res.optBoolean("success", false)) {
                                    MyApp.getInstance().setSuAccess(res.optString("Su_Account", account));
                                    // 身分與登入 Token：identity 決定系統設定是否可編輯；loginToken 供回寫 B016 使用。
                                    MyApp.getInstance().setIdentity(res.optString("identity", ""));
                                    MyApp.getInstance().setLoginToken(res.optString("token", ""));
                                    // 移除覆蓋 ApiKey 的邏輯，因為 B000 回傳的可能是 Admin Token，但我們需要保留 Machine Token
                                    Log.i(TAG, "Login success for user: " + account);
                                    traceLogin("[" + traceId + "] b000_parse_success parseMs="
                                            + (System.currentTimeMillis() - mainStartMs)
                                            + " totalMs=" + (System.currentTimeMillis() - startMs));
                                    handlerMain.start("StarCloudAPI", 200, "登入成功");
                                } else {
                                    traceLogin("[" + traceId + "] b000_business_fail code=401 parseMs="
                                            + (System.currentTimeMillis() - mainStartMs)
                                            + " totalMs=" + (System.currentTimeMillis() - startMs));
                                    handlerMain.start("StarCloudAPI", 401,
                                            "登入失敗: " + res.optString("message", "帳號密碼錯誤"));
                                }
                            } catch (Exception e) {
                                Log.e(TAG, "B000 Parse Error: " + e.getMessage());
                                traceLogin("[" + traceId + "] b000_parse_error parseMs="
                                        + (System.currentTimeMillis() - mainStartMs)
                                        + " totalMs=" + (System.currentTimeMillis() - startMs)
                                        + " err=" + e.getMessage());
                                handlerMain.start("StarCloudAPI", 500, "登入解析失敗: " + e.getMessage());
                            }
                        } else {
                            traceLogin("[" + traceId + "] b000_http_fail code=" + finalCode
                                    + " totalMs=" + (System.currentTimeMillis() - startMs));
                            handlerMain.start("StarCloudAPI", finalCode,
                                    "網路錯誤 (" + finalCode + "): " + finalReceive);
                        }
                    }
                });
            }
        }).start();
    }


    /**
     * B014: 下載機器設定 (綁定機器)
     * 不需要 ApiKey 即可呼叫，僅需機器序號。
     */
    public void downloadSetting() {
        downloadSetting(true);
    }

    /**
     * B014: 重新下載系統設定。
     *
     * @param reconnectMqtt 設定落地後是否立即重連 MQTT。
     *                      綁定 UI 流程傳 true；遠端 update_settings 指令傳 false，
     *                      改由呼叫端在發出 ACK 之後再重連，避免重連把送指令的連線斷掉導致 ACK 發不出去。
     */
    public void downloadSetting(final boolean reconnectMqtt) {
        final String machineId = MyApp.getInstance().getMachine().getMachineID();
        if (machineId.isEmpty()) {
            Log.e(TAG, "Machine ID is empty, cannot bind.");
            handlerMain.start("StarCloudAPI", 400, "請先輸入機器序號");
            return;
        }

        final String url = getBaseUrl() + "machine/setting/B014?machine=" + machineId;
        Log.i(TAG, "Starting B014 request to: " + url);

        new Thread(new Runnable() {
            @Override
            public void run() {
                int responseCode = -1;
                String receive = "";
                try {
                    java.net.URL reqUrl = new java.net.URL(url);
                    java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
                    conn.setRequestMethod("GET");
                    conn.setRequestProperty("Accept", "application/json");
                    conn.setConnectTimeout(15000);
                    conn.setReadTimeout(15000);

                    responseCode = conn.getResponseCode();
                    java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
                            ? conn.getInputStream() : conn.getErrorStream();
                    if (is != null) {
                        java.io.BufferedReader br = new java.io.BufferedReader(
                                new java.io.InputStreamReader(is, "UTF-8"));
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while ((line = br.readLine()) != null) sb.append(line);
                        receive = sb.toString();
                    }
                    conn.disconnect();

                    Log.d(TAG, "B014 Response (" + responseCode + "): " + receive);
                } catch (final Exception e) {
                    Log.e(TAG, "B014 Network Error", e);
                    new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            android.widget.Toast.makeText(MyApp.getInstance(),
                                    "綁定連線失敗: " + e.getMessage(), android.widget.Toast.LENGTH_LONG).show();
                        }
                    });
                    handlerMain.start("StarCloudAPI", 500, "連線失敗: " + e.getMessage());
                    return;
                }

                final int finalCode = responseCode;
                final String finalReceive = receive;
                new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        if (finalCode >= 200 && finalCode < 300) {
                            try {
                                JSONObject res = new JSONObject(finalReceive);
                                if (res.optBoolean("success", false)) {
                                    JSONObject config = res.optJSONObject("data");
                                    if (config != null) {
                                        saveSettings(config, reconnectMqtt);
                                        handlerMain.start("StarCloudAPI", 200, "綁定成功");
                                    } else {
                                        handlerMain.start("StarCloudAPI", 500, "設定內容為空，原始資料：" + finalReceive);
                                    }
                                } else {
                                    handlerMain.start("StarCloudAPI", 401, "綁定失敗: " + res.optString("message"));
                                }
                            } catch (JSONException e) {
                                Log.e(TAG, "B014 Parse Error: " + e.getMessage());
                                handlerMain.start("StarCloudAPI", 500, "設定解析錯誤: " + e.getMessage());
                            }
                        } else {
                            try {
                                JSONObject res = new JSONObject(finalReceive);
                                String msg = res.optString("message", "網路錯誤: " + finalCode);
                                handlerMain.start("StarCloudAPI", finalCode, msg);
                            } catch (Exception e) {
                                handlerMain.start("StarCloudAPI", finalCode, "網路錯誤: " + finalCode);
                            }
                        }
                    }
                });
            }
        }).start();
    }

    /**
     * B012: 同步商品清單
     * 需要 Bearer Token 認證。
     */
    public void downloadProductInfo() {
        final String url = getBaseUrl() + "machine/products/B012";
        Log.i(TAG, "Starting B012 request to: " + url);

        new Thread(new Runnable() {
            @Override
            public void run() {
                int responseCode = -1;
                String receive = "";
                try {
                    java.net.URL reqUrl = new java.net.URL(url);
                    java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
                    conn.setRequestMethod("GET");
                    conn.setRequestProperty("Accept", "application/json");
                    
                    // 加入機台 Token 驗證
                    String machineToken = MyApp.getInstance().getMachine().getApiKey();
                    if (machineToken != null && !machineToken.isEmpty() && !machineToken.equals("null")) {
                        conn.setRequestProperty("Authorization", "Bearer " + machineToken);
                        Log.i(TAG, "B012 Request with Token: Bearer " + machineToken.substring(0, Math.min(machineToken.length(), 10)) + "...");
                    }

                    conn.setConnectTimeout(15000);
                    conn.setReadTimeout(15000);

                    responseCode = conn.getResponseCode();
                    java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
                            ? conn.getInputStream() : conn.getErrorStream();
                    if (is != null) {
                        java.io.BufferedReader br = new java.io.BufferedReader(
                                new java.io.InputStreamReader(is, "UTF-8"));
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while ((line = br.readLine()) != null) sb.append(line);
                        receive = sb.toString();
                    }
                    conn.disconnect();

                    Log.d(TAG, "B012 Response (" + responseCode + "): " + receive);
                } catch (final Exception e) {
                    Log.e(TAG, "B012 Network Error", e);
                    new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            android.widget.Toast.makeText(MyApp.getInstance(),
                                    "下載連線失敗: " + e.getMessage(), android.widget.Toast.LENGTH_LONG).show();
                        }
                    });
                    handlerMain.start("StarCloudAPI", 500, "連線失敗: " + e.getMessage());
                    return;
                }

                final int finalCode = responseCode;
                final String finalReceive = receive;
                new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        if (finalCode >= 200 && finalCode < 300) {
                            try {
                                JSONObject res = new JSONObject(finalReceive);
                                if (res.optBoolean("success", false)) {
                                    JSONArray products = res.getJSONArray("data");
                                    saveProducts(products);
                                    handlerMain.start("StarCloudAPI", 200, "商品下載成功");
                                } else {
                                    handlerMain.start("StarCloudAPI", 401, "下載失敗: " + res.optString("message"));
                                }
                            } catch (JSONException e) {
                                handlerMain.start("StarCloudAPI", 500, "商品解析錯誤");
                            }
                        } else {
                            String errorMsg;
                            if (finalCode == 502) {
                                errorMsg = "伺服器忙碌或維護中 (502)，請稍後再試";
                            } else {
                                try {
                                    JSONObject res = new JSONObject(finalReceive);
                                    errorMsg = res.optString("message", "下載網路錯誤: " + finalCode);
                                } catch (Exception e) {
                                    errorMsg = "下載網路錯誤: " + finalCode;
                                }
                            }
                            handlerMain.start("StarCloudAPI", finalCode, errorMsg);
                        }
                    }
                });
            }
        }).start();
    }

    /**
     * B009: 貨道庫存即時回報
     * 需要 Bearer Token 認證。
     */
    public void reportSlotList(final ReportSlotListStructure data) {
        final String url = getBaseUrl() + "products/supplementary/B009";
        Log.i(TAG, "Starting B009 request to: " + url);

        new Thread(new Runnable() {
            @Override
            public void run() {
                int responseCode = -1;
                String receive = "";
                try {
                    JSONArray jsonArray = new JSONArray();
                    // 記錄本次快照上報的 rev，成功後回填 synced_rev（用快照值而非當下值，避免吃掉上報期間的新編輯）
                    final java.util.List<int[]> reportedRevs = new java.util.ArrayList<>();
                    for (SlotField slotField : SlotField.values()) {
                        for (SlotStructure slotData : MyApp.getInstance().getSlotList(slotField.getField())) {
                            boolean hasProduct = slotData.getProduct() != null
                                    && !slotData.getProduct().getProductID().equals("?");
                            // 即使無商品，只要有鎖定或 rev>0（現場改過）也要上報，否則被鎖的空貨道狀態無法同步
                            boolean hasState = slotData.isLock()
                                    || slotData.getLockRev() > 0 || slotData.getExpiryRev() > 0;
                            if (!hasProduct && !hasState) {
                                continue; // 無商品又無狀態 → 不送，維持後台全量同步移除語意
                            }
                            JSONObject item = new JSONObject();
                            item.put("tid", String.valueOf(slotData.getSlot())); // 貨道編號轉字串
                            item.put("t060v00", hasProduct ? slotData.getProduct().getProductID() : ""); // 商品 ID
                            item.put("num", String.valueOf(slotData.getCount())); // 實體庫存轉字串
                            // 雙向 LWW：鎖定 + 效期/批號 + 各自的編輯序號
                            item.put("is_locked", slotData.isLock());
                            item.put("lock_rev", slotData.getLockRev());
                            String exp = slotData.getExpiryDate();
                            item.put("expiry_date", (exp == null || exp.isEmpty()) ? JSONObject.NULL : exp);
                            String batch = slotData.getBatchNo();
                            item.put("batch_no", (batch == null || batch.isEmpty()) ? JSONObject.NULL : batch);
                            item.put("expiry_rev", slotData.getExpiryRev());
                            jsonArray.put(item);
                            reportedRevs.add(new int[]{ slotField.getField(), slotData.getSlot(),
                                    slotData.getLockRev(), slotData.getExpiryRev() });
                        }
                    }

                    JSONObject json = new JSONObject();
                    json.put("data", jsonArray);
                    json.put("account", data.getAccount());

                    Log.d(TAG, "B009 Request JSON: " + json.toString());

                    java.net.URL reqUrl = new java.net.URL(url);
                    java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
                    conn.setRequestMethod("PUT"); // B009 使用 PUT
                    conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                    conn.setRequestProperty("Accept", "application/json");

                    // 加入機台 Token 驗證
                    String machineToken = MyApp.getInstance().getMachine().getApiKey();
                    if (machineToken != null && !machineToken.isEmpty() && !machineToken.equals("null")) {
                        conn.setRequestProperty("Authorization", "Bearer " + machineToken);
                        Log.i(TAG, "B009 Request with Token: Bearer " + machineToken.substring(0, Math.min(machineToken.length(), 10)) + "...");
                    } else {
                        Log.e(TAG, "B009 Request Failed: Machine Token is empty!");
                    }

                    conn.setConnectTimeout(15000);
                    conn.setReadTimeout(15000);
                    conn.setDoOutput(true);

                    byte[] bodyBytes = json.toString().getBytes("UTF-8");
                    conn.getOutputStream().write(bodyBytes);
                    conn.getOutputStream().flush();

                    responseCode = conn.getResponseCode();
                    java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
                            ? conn.getInputStream() : conn.getErrorStream();
                    if (is != null) {
                        java.io.BufferedReader br = new java.io.BufferedReader(
                                new java.io.InputStreamReader(is, "UTF-8"));
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while ((line = br.readLine()) != null) sb.append(line);
                        receive = sb.toString();
                    }
                    conn.disconnect();

                    if (responseCode >= 200 && responseCode < 300) {
                        Log.i(TAG, "B009 Success: " + receive);
                        // 回填 synced_rev = 本次快照上報的 rev（用快照值，期間若有新編輯則 rev>synced 仍視為未同步）
                        try {
                            com.unibuy.smartdevice.database.SlotsDao revDao =
                                    new com.unibuy.smartdevice.database.SlotsDao(handlerMain.getContext());
                            for (int[] r : reportedRevs) {
                                try {
                                    SlotStructure s = MyApp.getInstance().getSlotData(r[0], r[1]);
                                    if (s == null) continue;
                                    if (s.getLockRev() == r[2]) s.setLockSyncedRev(r[2]);
                                    if (s.getExpiryRev() == r[3]) s.setExpirySyncedRev(r[3]);
                                    revDao.updateSlot(s);
                                } catch (Exception ignore) {}
                            }
                            revDao.close();
                        } catch (Exception e) {
                            Log.e(TAG, "B009 synced_rev backfill error: " + e.getMessage());
                        }
                    } else {
                        Log.e(TAG, "B009 Failed (" + responseCode + "): " + receive);
                    }
                } catch (final Exception e) {
                    Log.e(TAG, "B009 Network Error", e);
                    handlerMain.start(TAG, 500, "同步失敗: " + e.getMessage());
                    return;
                }

                final int finalCode = responseCode;
                final String finalReceive = receive;
                new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        if (finalCode >= 200 && finalCode < 300) {
                            try {
                                JSONObject res = new JSONObject(finalReceive);
                                if (res.optBoolean("success", false)) {
                                    handlerMain.start(TAG, 200, handlerMain.getContext().getString(R.string.upload_success));
                                } else {
                                    String errorMsg = res.optString("message", handlerMain.getContext().getString(R.string.upload_failed));
                                    handlerMain.start(TAG, finalCode, handlerMain.getContext().getString(R.string.upload_failed) + ": " + errorMsg);
                                }
                            } catch (Exception e) {
                                Log.e(TAG, "B009 Parse Error: " + e.getMessage());
                                handlerMain.start(TAG, 500, "解析失敗: " + finalReceive);
                            }
                        } else {
                            handlerMain.start(TAG, finalCode, "同步失敗 (" + finalCode + ") " + finalReceive);
                        }
                    }
                });
            }
        }).start();
    }

    /**
     * 將下載回來的設定儲存到 MyApp 中
     */
    /**
     * B680: 員工卡身分驗證
     * 驗證成功後會回傳 code_id (staff_cards 資料表的 ID)
     */
    public void verifyStaffCard(final String cardNo, final VerificationCallback callback) {
        final String machineId = MyApp.getInstance().getMachine().getMachineID();
        final String url = getBaseUrl() + "machine/staff/verify/B680";
        Log.i(TAG, "Starting B680 request for card: " + cardNo);

        new Thread(new Runnable() {
            @Override
            public void run() {
                int responseCode = -1;
                String receive = "";
                try {
                    JSONObject json = new JSONObject();
                    // 根據 API 規格，參數名應為 "code"
                    json.put("code", cardNo);

                    java.net.URL reqUrl = new java.net.URL(url);
                    java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
                    conn.setRequestMethod("POST");
                    conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                    conn.setRequestProperty("Accept", "application/json");

                    // 加入機台 Token 驗證
                    String machineToken = MyApp.getInstance().getMachine().getApiKey();
                    if (machineToken != null && !machineToken.isEmpty()) {
                        conn.setRequestProperty("Authorization", "Bearer " + machineToken);
                    }

                    conn.setConnectTimeout(10000);
                    conn.setReadTimeout(10000);
                    conn.setDoOutput(true);

                    byte[] bodyBytes = json.toString().getBytes("UTF-8");
                    conn.getOutputStream().write(bodyBytes);
                    conn.getOutputStream().flush();

                    responseCode = conn.getResponseCode();
                    java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
                            ? conn.getInputStream() : conn.getErrorStream();
                    
                    if (is != null) {
                        java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while ((line = br.readLine()) != null) sb.append(line);
                        receive = sb.toString();
                    }
                    conn.disconnect();
                    Log.d(TAG, "B680 Response (" + responseCode + "): " + receive);
                } catch (final Exception e) {
                    Log.e(TAG, "B680 Network Error", e);
                    new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> callback.onFailure("連線失敗: " + e.getMessage()));
                    return;
                }

                final int finalCode = responseCode;
                final String finalReceive = receive;
                new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
                    if (finalCode >= 200 && finalCode < 300) {
                        try {
                            JSONObject res = new JSONObject(finalReceive);
                            // 根據規格，後台回傳 success: true 且包含 code_id
                            if (res.optBoolean("success", false)) {
                                // 規格顯示 code_id 可能在 data 層級或直接在根層級，這裡做相容處理
                                String codeId = "";
                                if (res.has("data")) {
                                    codeId = res.optJSONObject("data").optString("code_id", "");
                                } else {
                                    codeId = res.optString("code_id", "");
                                }
                                callback.onSuccess(codeId);
                            } else {
                                callback.onFailure(res.optString("message", "驗證失敗"));
                            }
                        } catch (Exception e) {
                            callback.onFailure("解析錯誤: " + finalCode);
                        }
                    } else if (finalCode == 404) {
                        callback.onFailure("查無此員工卡，請確認卡片是否已註冊");
                    } else if (finalCode == 403) {
                        callback.onFailure("此卡片目前無權限使用");
                    } else if (finalCode == 401) {
                        callback.onFailure("系統認證失效，請聯繫管理員");
                    } else {
                        // 嘗試從錯誤訊息中提取 message
                        try {
                            JSONObject res = new JSONObject(finalReceive);
                            callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
                        } catch (Exception e) {
                            callback.onFailure("系統連線錯誤 (" + finalCode + ")");
                        }
                    }
                });
            }
        }).start();
    }

    public interface VerificationCallback {
        void onSuccess(String codeId);
        void onFailure(String message);
    }

    /**
     * 取貨碼驗證回呼：成功時回傳 code_id。
     * 綁商品碼 → productId 有值(App 依即時庫存自挑可出貨道)，slotNo 為 -1；
     * 綁貨道碼(舊) → slotNo 有值、productId 為空字串。
     */
    public interface PickupVerificationCallback {
        void onSuccess(String codeId, int slotNo, String productId);
        void onFailure(String message);
    }

    /** 領藥單：單筆出貨明細（一商品可跨多貨道，每筆對應一個貨道與數量）。 */
    public static class PharmacyPickupItem {
        public String slotNo;
        public int productId;
        public String productName;
        public int qty;
    }

    /** 領藥單(雲端)驗證回呼：成功時回傳 order_no(=後端領藥單號/flow_id)、code_id、批價單號與多貨道出貨清單。 */
    public interface PharmacyPickupCallback {
        void onSuccess(String orderNo, String codeId, String pricingSlipNo, java.util.List<PharmacyPickupItem> items);
        void onFailure(String message);
    }

    /** 系統設定回寫回呼（B016）。 */
    public interface SettingsUploadCallback {
        void onSuccess();
        void onFailure(String message);
    }

    /**
     * 來店禮驗證回呼（B690）：成功回傳折扣資訊。
     * discountType：amount（折抵金額）/ percentage（折扣百分比，discountValue 為折扣趴數，八折=20）。
     */
    public interface WelcomeGiftCallback {
        void onSuccess(String codeId, String discountType, int discountValue, String discountLabel, String name);
        void onFailure(String message);
    }

    /**
     * B016：系統方（identity=system）回寫機台系統設定到雲端 star-cloud。
     * 使用 B000 登入核發的使用者 Token（auth:sanctum），後端再驗一次系統方權限。
     * @param settings 後端鍵名的開關 JSON（鍵名對照見 SystemSetActivity#buildBackendSettings）。
     */
    public void uploadSystemSettings(final JSONObject settings, final SettingsUploadCallback callback) {
        final String url = getBaseUrl() + "machine/setting/B016";
        final String machineId = MyApp.getInstance().getMachine().getMachineID();
        final String loginToken = MyApp.getInstance().getLoginToken();
        Log.i(TAG, "Starting B016 (update settings) for machine: " + machineId);

        if (loginToken == null || loginToken.isEmpty()) {
            new android.os.Handler(android.os.Looper.getMainLooper()).post(
                    () -> callback.onFailure("尚未登入或登入逾時，請重新登入後台"));
            return;
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                int responseCode = -1;
                String receive = "";
                try {
                    JSONObject json = new JSONObject();
                    json.put("machine", machineId);
                    json.put("settings", settings);

                    java.net.URL reqUrl = new java.net.URL(url);
                    java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
                    conn.setRequestMethod("PATCH");
                    conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                    conn.setRequestProperty("Accept", "application/json");
                    conn.setRequestProperty("Authorization", "Bearer " + loginToken);
                    conn.setConnectTimeout(15000);
                    conn.setReadTimeout(15000);
                    conn.setDoOutput(true);
                    conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
                    conn.getOutputStream().flush();

                    responseCode = conn.getResponseCode();
                    java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
                            ? conn.getInputStream() : conn.getErrorStream();
                    if (is != null) {
                        java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while ((line = br.readLine()) != null) sb.append(line);
                        receive = sb.toString();
                    }
                    conn.disconnect();
                    Log.d(TAG, "B016 Response (" + responseCode + "): " + receive);
                } catch (final Exception e) {
                    Log.e(TAG, "B016 Network Error", e);
                    new android.os.Handler(android.os.Looper.getMainLooper()).post(
                            () -> callback.onFailure("連線失敗: " + e.getMessage()));
                    return;
                }

                final int finalCode = responseCode;
                final String finalReceive = receive;
                new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
                    if (finalCode >= 200 && finalCode < 300) {
                        try {
                            JSONObject res = new JSONObject(finalReceive);
                            if (res.optBoolean("success", false)) {
                                callback.onSuccess();
                            } else {
                                callback.onFailure(res.optString("message", "設定儲存失敗"));
                            }
                        } catch (Exception e) {
                            callback.onFailure("解析錯誤 (" + finalCode + ")");
                        }
                    } else if (finalCode == 403) {
                        callback.onFailure("權限不足：僅系統方可修改系統設定");
                    } else if (finalCode == 401) {
                        callback.onFailure("登入逾時，請重新登入後台");
                    } else if (finalCode == 404) {
                        callback.onFailure("查無此機台");
                    } else {
                        try {
                            JSONObject res = new JSONObject(finalReceive);
                            callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
                        } catch (Exception e) {
                            callback.onFailure("系統連線錯誤 (" + finalCode + ")");
                        }
                    }
                });
            }
        }).start();
    }

    /**
     * B660: 取貨碼驗證（邏輯比照 B680 員工卡，驗證通道不同）。
     * 成功回傳 data.code_id 與 data.slot_no（要出貨的貨道）。
     */
    public void verifyPickupCode(final String code, final PickupVerificationCallback callback) {
        final String url = getBaseUrl() + "machine/pickup/verify/B660";
        Log.i(TAG, "Starting B660 request for code: " + code);

        new Thread(new Runnable() {
            @Override
            public void run() {
                int responseCode = -1;
                String receive = "";
                try {
                    JSONObject json = new JSONObject();
                    json.put("code", code);

                    java.net.URL reqUrl = new java.net.URL(url);
                    java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
                    conn.setRequestMethod("POST");
                    conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                    conn.setRequestProperty("Accept", "application/json");
                    String machineToken = MyApp.getInstance().getMachine().getApiKey();
                    if (machineToken != null && !machineToken.isEmpty()) {
                        conn.setRequestProperty("Authorization", "Bearer " + machineToken);
                    }
                    conn.setConnectTimeout(10000);
                    conn.setReadTimeout(10000);
                    conn.setDoOutput(true);
                    conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
                    conn.getOutputStream().flush();

                    responseCode = conn.getResponseCode();
                    java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
                            ? conn.getInputStream() : conn.getErrorStream();
                    if (is != null) {
                        java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while ((line = br.readLine()) != null) sb.append(line);
                        receive = sb.toString();
                    }
                    conn.disconnect();
                    Log.d(TAG, "B660 Response (" + responseCode + "): " + receive);
                } catch (final Exception e) {
                    Log.e(TAG, "B660 Network Error", e);
                    new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> callback.onFailure("連線失敗: " + e.getMessage()));
                    return;
                }

                final int finalCode = responseCode;
                final String finalReceive = receive;
                new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
                    if (finalCode >= 200 && finalCode < 300) {
                        try {
                            JSONObject res = new JSONObject(finalReceive);
                            if (res.optBoolean("success", false)) {
                                JSONObject data = res.optJSONObject("data");
                                String codeId = data != null ? data.optString("code_id", "") : "";
                                int slotNo = data != null ? data.optInt("slot_no", -1) : -1;
                                // product_id 為數字且可能為 JSON null；用 optInt 避免 optString 把 null 轉成 "null" 字串
                                int pid = data != null ? data.optInt("product_id", 0) : 0;
                                String productId = pid > 0 ? String.valueOf(pid) : "";
                                callback.onSuccess(codeId, slotNo, productId);
                            } else {
                                callback.onFailure(res.optString("message", "取貨碼驗證失敗"));
                            }
                        } catch (Exception e) {
                            callback.onFailure("解析錯誤: " + finalCode);
                        }
                    } else if (finalCode == 404) {
                        callback.onFailure("查無此取貨碼或已使用");
                    } else if (finalCode == 429) {
                        callback.onFailure("錯誤次數過多，請稍後再試");
                    } else if (finalCode == 401) {
                        callback.onFailure("系統認證失效，請聯繫管理員");
                    } else {
                        try {
                            JSONObject res = new JSONObject(finalReceive);
                            callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
                        } catch (Exception e) {
                            callback.onFailure("系統連線錯誤 (" + finalCode + ")");
                        }
                    }
                });
            }
        }).start();
    }

    /**
     * 領藥單(雲端)驗證 B660：與 verifyPickupCode 同一端點，但解析回應的 order_no 與多貨道 items[]。
     * 後端回應 data：{ slot_no(可能 null), code_id, order_no, order_type:"pharmacy_pickup",
     *                items:[{slot_no, product_id, product_name, qty}, ...] }
     */
    public void verifyPharmacyPickupCode(final String code, final PharmacyPickupCallback callback) {
        final String url = getBaseUrl() + "machine/pickup/verify/B660";
        Log.i(TAG, "Starting B660 (pharmacy) request for code: " + code);

        new Thread(new Runnable() {
            @Override
            public void run() {
                int responseCode = -1;
                String receive = "";
                try {
                    JSONObject json = new JSONObject();
                    json.put("code", code);

                    java.net.URL reqUrl = new java.net.URL(url);
                    java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
                    conn.setRequestMethod("POST");
                    conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                    conn.setRequestProperty("Accept", "application/json");
                    String machineToken = MyApp.getInstance().getMachine().getApiKey();
                    if (machineToken != null && !machineToken.isEmpty()) {
                        conn.setRequestProperty("Authorization", "Bearer " + machineToken);
                    }
                    conn.setConnectTimeout(10000);
                    conn.setReadTimeout(10000);
                    conn.setDoOutput(true);
                    conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
                    conn.getOutputStream().flush();

                    responseCode = conn.getResponseCode();
                    java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
                            ? conn.getInputStream() : conn.getErrorStream();
                    if (is != null) {
                        java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while ((line = br.readLine()) != null) sb.append(line);
                        receive = sb.toString();
                    }
                    conn.disconnect();
                    Log.d(TAG, "B660(pharmacy) Response (" + responseCode + "): " + receive);
                } catch (final Exception e) {
                    Log.e(TAG, "B660(pharmacy) Network Error", e);
                    new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> callback.onFailure("連線失敗: " + e.getMessage()));
                    return;
                }

                final int finalCode = responseCode;
                final String finalReceive = receive;
                new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
                    if (finalCode >= 200 && finalCode < 300) {
                        // 只把「JSON 解析」放進 try；onSuccess/onFailure 移到 try 之外，
                        // 避免 onSuccess 內的後續處理（組購物車/開對話框）丟例外時被誤判成「解析錯誤」。
                        String pOrderNo = "";
                        String pCodeId = "";
                        String pPricingSlipNo = "";
                        java.util.List<PharmacyPickupItem> pItems = null;
                        String pFail = null;
                        try {
                            JSONObject res = new JSONObject(finalReceive);
                            if (res.optBoolean("success", false)) {
                                JSONObject data = res.optJSONObject("data");
                                pOrderNo = data != null ? data.optString("order_no", "") : "";
                                pCodeId = data != null ? data.optString("code_id", "") : "";
                                pPricingSlipNo = data != null ? data.optString("pricing_slip_no", "") : "";
                                pItems = new java.util.ArrayList<>();
                                org.json.JSONArray arr = data != null ? data.optJSONArray("items") : null;
                                if (arr != null) {
                                    for (int i = 0; i < arr.length(); i++) {
                                        JSONObject it = arr.optJSONObject(i);
                                        if (it == null) continue;
                                        PharmacyPickupItem item = new PharmacyPickupItem();
                                        item.slotNo = it.optString("slot_no", "");
                                        item.productId = it.optInt("product_id", 0);
                                        item.productName = it.optString("product_name", "");
                                        item.qty = it.optInt("qty", 0);
                                        pItems.add(item);
                                    }
                                }
                            } else {
                                pFail = res.optString("message", "領藥碼驗證失敗");
                            }
                        } catch (Exception e) {
                            pFail = "解析錯誤: " + finalCode;
                        }

                        if (pFail != null) {
                            callback.onFailure(pFail);
                        } else if (pOrderNo.isEmpty() || pItems == null || pItems.isEmpty()) {
                            callback.onFailure("領藥單回應不完整（缺 order_no 或 items）");
                        } else {
                            callback.onSuccess(pOrderNo, pCodeId, pPricingSlipNo, pItems);
                        }
                    } else if (finalCode == 404) {
                        callback.onFailure("查無此領藥碼或已領取");
                    } else if (finalCode == 429) {
                        callback.onFailure("錯誤次數過多，請稍後再試");
                    } else if (finalCode == 403) {
                        callback.onFailure("此機台未啟用領藥單");
                    } else if (finalCode == 401) {
                        callback.onFailure("系統認證失效，請聯繫管理員");
                    } else {
                        try {
                            JSONObject res = new JSONObject(finalReceive);
                            callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
                        } catch (Exception e) {
                            callback.onFailure("系統連線錯誤 (" + finalCode + ")");
                        }
                    }
                });
            }
        }).start();
    }

    /**
     * B670: 通行碼驗證（技師用，邏輯比照 B680，驗證通道不同）。
     * 成功回傳 data.code_id。
     */
    public void verifyPassCode(final String code, final VerificationCallback callback) {
        final String url = getBaseUrl() + "machine/passcode/verify/B670";
        Log.i(TAG, "Starting B670 request for code: " + code);

        new Thread(new Runnable() {
            @Override
            public void run() {
                int responseCode = -1;
                String receive = "";
                try {
                    JSONObject json = new JSONObject();
                    json.put("code", code);

                    java.net.URL reqUrl = new java.net.URL(url);
                    java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
                    conn.setRequestMethod("POST");
                    conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                    conn.setRequestProperty("Accept", "application/json");
                    String machineToken = MyApp.getInstance().getMachine().getApiKey();
                    if (machineToken != null && !machineToken.isEmpty()) {
                        conn.setRequestProperty("Authorization", "Bearer " + machineToken);
                    }
                    conn.setConnectTimeout(10000);
                    conn.setReadTimeout(10000);
                    conn.setDoOutput(true);
                    conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
                    conn.getOutputStream().flush();

                    responseCode = conn.getResponseCode();
                    java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
                            ? conn.getInputStream() : conn.getErrorStream();
                    if (is != null) {
                        java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while ((line = br.readLine()) != null) sb.append(line);
                        receive = sb.toString();
                    }
                    conn.disconnect();
                    Log.d(TAG, "B670 Response (" + responseCode + "): " + receive);
                } catch (final Exception e) {
                    Log.e(TAG, "B670 Network Error", e);
                    new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> callback.onFailure("連線失敗: " + e.getMessage()));
                    return;
                }

                final int finalCode = responseCode;
                final String finalReceive = receive;
                new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
                    if (finalCode >= 200 && finalCode < 300) {
                        try {
                            JSONObject res = new JSONObject(finalReceive);
                            if (res.optBoolean("success", false)) {
                                String codeId = "";
                                if (res.has("data")) {
                                    codeId = res.optJSONObject("data").optString("code_id", "");
                                } else {
                                    codeId = res.optString("code_id", "");
                                }
                                callback.onSuccess(codeId);
                            } else {
                                callback.onFailure(res.optString("message", "通行碼驗證失敗"));
                            }
                        } catch (Exception e) {
                            callback.onFailure("解析錯誤: " + finalCode);
                        }
                    } else if (finalCode == 404) {
                        callback.onFailure("查無此通行碼或已失效");
                    } else if (finalCode == 429) {
                        callback.onFailure("錯誤次數過多，請稍後再試");
                    } else if (finalCode == 401) {
                        callback.onFailure("系統認證失效，請聯繫管理員");
                    } else {
                        try {
                            JSONObject res = new JSONObject(finalReceive);
                            callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
                        } catch (Exception e) {
                            callback.onFailure("系統連線錯誤 (" + finalCode + ")");
                        }
                    }
                });
            }
        }).start();
    }

    /**
     * B690: 來店禮驗證（邏輯比照 B660/B670，驗證通道不同）。
     * 成功回傳 data.code_id 與折扣資訊（discount_type/discount_value/discount_label/name）。
     */
    public void verifyWelcomeGift(final String code, final WelcomeGiftCallback callback) {
        final String url = getBaseUrl() + "machine/welcome-gift/verify/B690";
        Log.i(TAG, "Starting B690 request for code: " + code);

        new Thread(new Runnable() {
            @Override
            public void run() {
                int responseCode = -1;
                String receive = "";
                try {
                    JSONObject json = new JSONObject();
                    json.put("code", code);

                    java.net.URL reqUrl = new java.net.URL(url);
                    java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
                    conn.setRequestMethod("POST");
                    conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                    conn.setRequestProperty("Accept", "application/json");
                    String machineToken = MyApp.getInstance().getMachine().getApiKey();
                    if (machineToken != null && !machineToken.isEmpty()) {
                        conn.setRequestProperty("Authorization", "Bearer " + machineToken);
                    }
                    conn.setConnectTimeout(10000);
                    conn.setReadTimeout(10000);
                    conn.setDoOutput(true);
                    conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
                    conn.getOutputStream().flush();

                    responseCode = conn.getResponseCode();
                    java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
                            ? conn.getInputStream() : conn.getErrorStream();
                    if (is != null) {
                        java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while ((line = br.readLine()) != null) sb.append(line);
                        receive = sb.toString();
                    }
                    conn.disconnect();
                    Log.d(TAG, "B690 Response (" + responseCode + "): " + receive);
                } catch (final Exception e) {
                    Log.e(TAG, "B690 Network Error", e);
                    new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> callback.onFailure("連線失敗: " + e.getMessage()));
                    return;
                }

                final int finalCode = responseCode;
                final String finalReceive = receive;
                new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
                    if (finalCode >= 200 && finalCode < 300) {
                        try {
                            JSONObject res = new JSONObject(finalReceive);
                            if (res.optBoolean("success", false)) {
                                JSONObject data = res.optJSONObject("data");
                                String codeId = data != null ? data.optString("code_id", "") : "";
                                String discountType = data != null ? data.optString("discount_type", "") : "";
                                int discountValue = data != null ? data.optInt("discount_value", 0) : 0;
                                String discountLabel = data != null ? data.optString("discount_label", "") : "";
                                String name = data != null ? data.optString("name", "") : "";
                                callback.onSuccess(codeId, discountType, discountValue, discountLabel, name);
                            } else {
                                callback.onFailure(res.optString("message", "來店禮驗證失敗"));
                            }
                        } catch (Exception e) {
                            callback.onFailure("解析錯誤: " + finalCode);
                        }
                    } else if (finalCode == 404) {
                        callback.onFailure("查無此來店禮或已失效");
                    } else if (finalCode == 429) {
                        callback.onFailure("錯誤次數過多，請稍後再試");
                    } else if (finalCode == 401) {
                        callback.onFailure("系統認證失效，請聯繫管理員");
                    } else if (finalCode == 400) {
                        callback.onFailure("來店禮格式錯誤（需 8 碼）");
                    } else {
                        try {
                            JSONObject res = new JSONObject(finalReceive);
                            callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
                        } catch (Exception e) {
                            callback.onFailure("系統連線錯誤 (" + finalCode + ")");
                        }
                    }
                });
            }
        }).start();
    }

    private void saveSettings(JSONObject config) {
        saveSettings(config, true);
    }

    private void saveSettings(JSONObject config, boolean reconnectMqtt) {
        // 使用 optString 避免欄位缺失導致 JSONException

        // 1. 儲存 ApiToken 與 序號
        String apiToken = config.optString("api_token", "");
        if (!apiToken.isEmpty()) {
            MyApp.getInstance().getMachine().setApiKey(apiToken);
        }

        String machineID = config.optString("t050v01", "");
        if (!machineID.isEmpty()) {
            MyApp.getInstance().getMachine().setMachineID(machineID);
        }

        // 2. 儲存玉山支付設定
        MyApp.getInstance().getEsunpay().setStoreID(config.optString("t050v41", ""));
        MyApp.getInstance().getEsunpay().setTermID(config.optString("t050v42", ""));
        MyApp.getInstance().getEsunpay().setHash(config.optString("t050v43", ""));

        // 3. 儲存趨勢支付設定 (TrendPay)
        MyApp.getInstance().getGreenpaySetting().setAppId(config.optString("TP_APP_ID", ""));
        MyApp.getInstance().getGreenpaySetting().setAppKey(config.optString("TP_APP_KEY", ""));
        MyApp.getInstance().getGreenpaySetting().setPartnerKey(config.optString("TP_PARTNER_KEY", ""));

        // 4. 儲存電子發票設定
        MyApp.getInstance().getGreenInvoice().setGreenMerchantID(config.optString("t050v34", ""));
        MyApp.getInstance().getGreenInvoice().setGreenHashKey(config.optString("t050v35", ""));
        MyApp.getInstance().getGreenInvoice().setGreenHashIV(config.optString("t050v36", ""));
        MyApp.getInstance().getGreenInvoice().setGreenEmail(config.optString("t050v38", ""));

        // 5. 解析雲端功能開關（缺漏「欄位」→ false；缺漏「整個物件」→ 不動本地，避免連線異常把現場全關）
        JSONObject devSet = config.optJSONObject("DevSet");
        if (devSet != null) {
            DevSetStructure d = MyApp.getInstance().getDevSet();
            d.setShoppingCar(devSet.optBoolean("ShoppingCar", false));
            d.setInvoice(devSet.optBoolean("Invoice", false));
            d.setDevNFCPay(devSet.optBoolean("DevNFCPay", false));
            d.setDevEsunPay(devSet.optBoolean("DevEsunPay", false));
            d.setDevTapPay(devSet.optBoolean("DevTapPay", false));
            d.setDevCash(devSet.optBoolean("DevCash", false));
            d.setDevLinePay(devSet.optBoolean("DevLinePay", false));
            d.setTapPay30(devSet.optBoolean("TapPay30", false));
            d.setTapPay31(devSet.optBoolean("TapPay31", false));
            d.setTapPay32(devSet.optBoolean("TapPay32", false));
            d.setTapPay33(devSet.optBoolean("TapPay33", false));
            d.setTapPay34(devSet.optBoolean("TapPay34", false));
            // B014 新增支付細項（刷卡機/掃碼子開關）
            d.setDevCreditCard(devSet.optBoolean("DevCreditCard", false));
            d.setDevMobilePay(devSet.optBoolean("DevMobilePay", false));
            d.setDevCardPay(devSet.optBoolean("DevCardPay", false));
            d.setDevScanPay(devSet.optBoolean("DevScanPay", false));
            // VMC/Electic 雲端不下發 → 不碰，保留本地值
        }
        JSONObject cashSet = config.optJSONObject("CashSet");
        if (cashSet != null) {
            CashSetStructure c = MyApp.getInstance().getCashSetStructure();
            c.setBillF1000(cashSet.optBoolean("BillF1000", false));
            c.setBillE500(cashSet.optBoolean("BillE500", false));
            c.setBillD100(cashSet.optBoolean("BillD100", false));
            c.setCoinF50(cashSet.optBoolean("CoinF50", false));
            c.setCoinE10(cashSet.optBoolean("CoinE10", false));
            c.setCoinD5(cashSet.optBoolean("CoinD5", false));
            c.setCoinC1(cashSet.optBoolean("CoinC1", false));
        }
        // 6. FunctionSet：取貨模組（母）/取貨碼/通行碼/來店禮/會員/環溫
        //    取貨碼、通行碼從屬取貨模組（PickupModule）；來店禮獨立判斷。
        JSONObject funcSet = config.optJSONObject("FunctionSet");
        if (funcSet != null) {
            DevSetStructure d = MyApp.getInstance().getDevSet();
            d.setPickupModule(funcSet.optBoolean("PickupModule", false));
            d.setPharmacyPickup(funcSet.optBoolean("PharmacyPickup", false));
            d.setPickupCode(funcSet.optBoolean("PickupCode", false));
            d.setPassCode(funcSet.optBoolean("PassCode", false));
            d.setWelcomeGift(funcSet.optBoolean("WelcomeGift", false));
            d.setMemberSystem(funcSet.optBoolean("MemberSystem", false));
            d.setAmbientTemp(funcSet.optBoolean("AmbientTemp", false));
        }
        // 6c. HardwareSet 貨道型式（缺整個物件→不動本地，比照 DevSet/CashSet 防呆）
        JSONObject hardwareSet = config.optJSONObject("HardwareSet");
        if (hardwareSet != null) {
            com.unibuy.smartdevice.structure.HardwareSetStructure h = MyApp.getInstance().getHardwareSetStructure();
            h.setSpringSlot1_10(hardwareSet.optBoolean("SpringSlot1_10", false));
            h.setSpringSlot11_20(hardwareSet.optBoolean("SpringSlot11_20", false));
            h.setSpringSlot21_30(hardwareSet.optBoolean("SpringSlot21_30", false));
            h.setSpringSlot31_40(hardwareSet.optBoolean("SpringSlot31_40", false));
            h.setSpringSlot41_50(hardwareSet.optBoolean("SpringSlot41_50", false));
            h.setSpringSlot51_60(hardwareSet.optBoolean("SpringSlot51_60", false));
        }
        // 6b. ShoppingMode：購買流程模式（basic/employee_card/pickup_sheet），取代舊 project flavor
        boolean hasShoppingMode = config.has("ShoppingMode");
        if (hasShoppingMode) {
            MyApp.getInstance().setShoppingMode(config.optString("ShoppingMode", "basic"));
        }

        // 6d. LangSet：機台啟用語系（最多 5 種，有序，第 0 個為預設）。後台單向下發，機台僅顯示與切換。
        JSONObject langSet = config.optJSONObject("LangSet");
        if (langSet != null) {
            java.util.List<String> langs = new java.util.ArrayList<>();
            org.json.JSONArray arr = langSet.optJSONArray("Languages");
            if (arr != null) {
                for (int i = 0; i < arr.length(); i++) {
                    String code = arr.optString(i, "");
                    if (code != null && !code.isEmpty()) {
                        langs.add(com.unibuy.smartdevice.utils.LanguageHelper.canonicalize(code));
                    }
                }
            }
            MyApp.getInstance().setMachineLanguages(langs);
            String def = langSet.optString("Default", "");
            String canonicalDef = (def == null || def.isEmpty())
                    ? (langs.isEmpty() ? "" : langs.get(0))
                    : com.unibuy.smartdevice.utils.LanguageHelper.canonicalize(def);
            MyApp.getInstance().setDefaultLanguage(canonicalDef);

            // 後台優先：每次下發都以機台預設語系為準，立即套用（覆蓋使用者先前手動選擇）。
            // 設定下發頻率極低且皆在無人時段，不會打斷使用者操作。
            if (!canonicalDef.isEmpty()) {
                com.unibuy.smartdevice.utils.LanguageHelper.changeLanguage(handlerMain.getContext(), canonicalDef);
            }
        }

        // 6e. UISet：後台「APP UI 元素設定」下發（部位 part_key → 圖片 URL）。
        //     落地供各部位 UiSkin.apply 套用；缺整個物件→不動本地既有換膚。
        JSONObject uiSet = config.optJSONObject("UISet");
        if (uiSet != null) {
            com.unibuy.smartdevice.ui.tools.UiSkin.save(handlerMain.getContext(), uiSet);
        }

        if (devSet != null || cashSet != null || funcSet != null || hardwareSet != null || hasShoppingMode || langSet != null) {
            MyApp.saveSystemSet(handlerMain.getContext());
        }

        // 7. 重要：將所有設定同步回資料庫，防止 App 重啟後消失
        persistCurrentSettings();
        if (reconnectMqtt) {
            MyApp.getInstance().initMqtt();
        }
    }

    /**
     * 將目前記憶體中的所有設定（包含 ApiKey）強制持久化到資料庫
     */
    private void persistCurrentSettings() {
        Log.i(TAG, "persistCurrentSettings() - Saving to DB: machineID=" + MyApp.getInstance().getMachine().getMachineID()
                + ", token=" + MyApp.getInstance().getMachine().getApiKey());
        SettingsDao settingsDao = new SettingsDao(handlerMain.getContext());
        // 只刪除本方法接下來要重寫的這 4 個設定 key，避免 deleteAll() 連坐刪掉廣告(VideoEoxPlayer)等其他持久資料。
        // 否則「遠端更新設定→重啟」會因廣告列被清空且啟動不會主動抓 B005，導致廣告消失。
        settingsDao.deleteWhere(MyApp.getInstance().getMachine().getClass().getSimpleName());
        settingsDao.deleteWhere(MyApp.getInstance().getEsunpay().getClass().getSimpleName());
        settingsDao.deleteWhere(MyApp.getInstance().getGreenInvoice().getClass().getSimpleName());
        settingsDao.deleteWhere(MyApp.getInstance().getGreenpaySetting().getClass().getSimpleName());

        settingsDao.insertOne(new SettingStructure(MyApp.getInstance().getMachine().getClass().getSimpleName(),
                new String[] {
                        MyApp.getInstance().getMachine().getApiKey(),
                        MyApp.getInstance().getMachine().getMachineID(),
                        MyApp.getInstance().getMachine().getMqttEnv()
                }));

        settingsDao.insertOne(new SettingStructure(MyApp.getInstance().getEsunpay().getClass().getSimpleName(),
                new String[] {
                        MyApp.getInstance().getEsunpay().getStoreID(),
                        MyApp.getInstance().getEsunpay().getTermID(),
                        MyApp.getInstance().getEsunpay().getHash()
                }));

        settingsDao.insertOne(new SettingStructure(MyApp.getInstance().getGreenInvoice().getClass().getSimpleName(),
                new String[] {
                        MyApp.getInstance().getGreenInvoice().getGreenMerchantID(),
                        MyApp.getInstance().getGreenInvoice().getGreenHashKey(),
                        MyApp.getInstance().getGreenInvoice().getGreenHashIV(),
                        MyApp.getInstance().getGreenInvoice().getGreenEmail()
                }));

        settingsDao.insertOne(new SettingStructure(MyApp.getInstance().getGreenpaySetting().getClass().getSimpleName(),
                new String[] {
                        String.valueOf(MyApp.getInstance().getGreenpaySetting().getAppId()),
                        MyApp.getInstance().getGreenpaySetting().getAppKey(),
                        MyApp.getInstance().getGreenpaySetting().getPartnerKey()
                }));

        settingsDao.close();
        Log.i(TAG, "Persistence completed successfully.");
    }

    private void saveProducts(JSONArray products) throws JSONException {
        MyApp.getInstance().getProductList().clear();
        // 商品名稱多語 blob：商品ID → t060v01_i18n（底線代碼 key），存於 MemoDao 持久化（不動 products 表）
        JSONObject nameI18nBlob = new JSONObject();
        // 商品規格多語 blob：商品ID → t060v03_i18n（純文字含換行）
        JSONObject specI18nBlob = new JSONObject();
        // 商品彈簧補貨上限 blob：商品ID → spring_limit。履帶上限沿用 t060v11(=track_limit) 進 products 表，故不另存。
        JSONObject springLimitBlob = new JSONObject();
        for (int i = 0; i < products.length(); i++) {
            JSONObject p = products.getJSONObject(i);

            // 對應 Star-Cloud 欄位與 App 內部 ProductStructure 欄位
            ProductStructure product = new ProductStructure(
                    p.optString("t060v00", ""), // PID
                    p.optString("t060v06", ""), // Image
                    p.optString("t060v01", ""), // Name
                    p.optInt("t063v03", 0), // Machine Price
                    p.optInt("t060v09", 0), // Selling Price
                    p.optInt("t060v30", 0), // Member Price
                    p.optInt("t060v11", 99), // Upper Limit
                    p.optString("t060v40", ""), // Plan
                    p.optString("t060v41", ""), // Material
                    p.optString("t060v01_en", ""),
                    p.optString("t060v01_jp", ""));

            // 新增：商品名稱多語 map（B012 t060v01_i18n），支援後台啟用的所有語系
            JSONObject nameI18n = p.optJSONObject("t060v01_i18n");
            if (nameI18n != null) {
                product.setNameI18nJson(nameI18n.toString());
                String pid = p.optString("t060v00", "");
                if (!pid.isEmpty()) {
                    try { nameI18nBlob.put(pid, nameI18n); } catch (JSONException ignore) {}
                }
            }

            // 新增：商品規格多語 map（B012 t060v03_i18n，純文字含換行）
            JSONObject specI18n = p.optJSONObject("t060v03_i18n");
            if (specI18n != null) {
                product.setSpecI18nJson(specI18n.toString());
                String pid = p.optString("t060v00", "");
                if (!pid.isEmpty()) {
                    try { specI18nBlob.put(pid, specI18n); } catch (JSONException ignore) {}
                }
            }
            // 履帶/彈簧 補貨上限（B012 spring_limit / track_limit）。track 沿用 slotUpperLimit(t060v11)；
            // spring 另存物件欄位 + blob（不動 products 表 schema，避免升 DB 版本洗掉貨道綁定）。
            int springLimit = p.optInt("spring_limit", -1);
            if (springLimit >= 0) {
                product.setSpringLimit(springLimit);
                String pid = p.optString("t060v00", "");
                if (!pid.isEmpty()) {
                    try { springLimitBlob.put(pid, springLimit); } catch (JSONException ignore) {}
                }
            }

            MyApp.getInstance().getProductList().add(product);
        }
        // 持久化商品多語 blob，供離線重啟時 fallback
        MyApp.getInstance().setProductNameI18nBlob(nameI18nBlob);
        MyApp.saveProductNameI18n(handlerMain.getContext());
        MyApp.getInstance().setProductSpecI18nBlob(specI18nBlob);
        MyApp.saveProductSpecI18n(handlerMain.getContext());
        // 持久化彈簧補貨上限 blob，供離線重啟時一鍵補貨 fallback
        MyApp.getInstance().setProductSpringLimitBlob(springLimitBlob);
        MyApp.saveProductSpringLimit(handlerMain.getContext());

        // 將商品同步至資料庫
        ProductsDao productsDao = new ProductsDao(handlerMain.getContext());
        productsDao.deleteAll(); // 先清空舊資料
        productsDao.insertAll(MyApp.getInstance().getProductList()); // 批量插入
        productsDao.close();

        Log.i(TAG, "Synced and persisted " + products.length() + " products from star-cloud.");

        // 🔹 重要：同步更新 MyApp 貨道中的商品引用
        // 因為貨道資料是在 App 啟動時載入的，它們握著舊的 ProductStructure 物件。
        // 如果不手動更新，銷售平台會一直看到舊的圖片網址。
        for (SlotField slotField : SlotField.values()) {
            List<SlotStructure> slotList = MyApp.getInstance().getSlotList(slotField.getField());
            if (slotList != null) {
                for (SlotStructure slot : slotList) {
                    if (slot.getProduct() != null) {
                        String pid = slot.getProduct().getProductID();
                        // 在新下載的清單中尋找匹配的商品
                        for (ProductStructure newP : MyApp.getInstance().getProductList()) {
                            if (newP.getProductID().equals(pid)) {
                                slot.setProduct(newP); // 更新引用
                                break;
                            }
                        }
                    }
                }
            }
        }
        Log.i(TAG, "Updated product references in all slots.");

        // 🔹 發送廣播通知所有監聯者（如銷售平台）商品與貨道資料已更新
        android.content.Intent intent = new android.content.Intent("com.unibuy.smartdevice.ACTION_SLOT_UPDATED");
        intent.setPackage(handlerMain.getContext().getPackageName());
        handlerMain.getContext().sendBroadcast(intent);
    }

    /**
     * B005: 下載廣告清單並同步媒體檔案
     */
    public void downloadAdvertisement() {
        downloadAdvertisement(null);
    }

    public void downloadAdvertisement(final Runnable onComplete) {
        final String url = getBaseUrl() + "machine/ad/B005";
        Log.i(TAG, "Starting B005 request to: " + url);

        new Thread(new Runnable() {
            @Override
            public void run() {
                int responseCode = -1;
                String receive = "";
                try {
                    java.net.URL reqUrl = new java.net.URL(url);
                    java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
                    conn.setRequestMethod("GET");
                    conn.setRequestProperty("Accept", "application/json");

                    String machineToken = MyApp.getInstance().getMachine().getApiKey();
                    if (machineToken != null && !machineToken.isEmpty()) {
                        conn.setRequestProperty("Authorization", "Bearer " + machineToken);
                    }

                    conn.setConnectTimeout(15000);
                    conn.setReadTimeout(30000); // 廣告清單可能較大

                    responseCode = conn.getResponseCode();
                    java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
                            ? conn.getInputStream() : conn.getErrorStream();
                    if (is != null) {
                        java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while ((line = br.readLine()) != null) sb.append(line);
                        receive = sb.toString();
                    }
                    conn.disconnect();
                } catch (Exception e) {
                    Log.e(TAG, "B005 Network Error", e);
                    handlerMain.start("StarCloudAPI", 500, "廣告同步連線失敗");
                    if (onComplete != null) onComplete.run();
                    return;
                }

                final int finalCode = responseCode;
                final String finalReceive = receive;
                if (finalCode >= 200 && finalCode < 300) {
                    try {
                        JSONObject res = new JSONObject(finalReceive);
                        if (res.optBoolean("success", false)) {
                            JSONArray ads = res.getJSONArray("data");
                            saveAds(ads);
                            handlerMain.start("StarCloudAPI", 200, "廣告同步完成");
                        } else {
                            handlerMain.start("StarCloudAPI", 401, "廣告同步失敗: " + res.optString("message"));
                        }
                    } catch (Exception e) {
                        Log.e(TAG, "B005 Parse Error", e);
                        handlerMain.start("StarCloudAPI", 500, "廣告解析錯誤");
                    }
                } else {
                    handlerMain.start("StarCloudAPI", finalCode, "廣告同步網路錯誤: " + finalCode);
                }
                if (onComplete != null) onComplete.run();
            }
        }).start();
    }

    private void saveAds(JSONArray ads) throws JSONException {
        SettingsDao settingsDao = new SettingsDao(handlerMain.getContext());
        // 清除舊廣告設定
        settingsDao.deleteWhere(VideoEoxPlayer.class.getSimpleName());

        // 🔹 清除 MediaCacheHelper 本地快取，避免 URL 相同但內容變更時播放器讀到過期快取
        VideoImageMixedPlayer.clearAdsCache(handlerMain.getContext());
        Log.i(TAG, "Cleared local ads media cache before sync.");
        
        MyApp.getInstance().getFontendAdUrlString().clear();

        for (int i = 0; i < ads.length(); i++) {
            JSONObject ad = ads.getJSONObject(i);
            String url = ad.optString("t070v04", "");
            String flag = String.valueOf(ad.optInt("t070v03", 0));
            String duration = String.valueOf(ad.optInt("t070v02", 10));

            if (url.isEmpty()) continue;

            MyApp.getInstance().getFontendAdUrlString().add(url);

            // 下載檔案
            try {
                Log.i(TAG, "Downloading Ad (" + (i+1) + "/" + ads.length() + "): " + url);
                HttpConnect httpConnect = new HttpConnect();
                httpConnect.download(url, true);
            } catch (Exception e) {
                Log.e(TAG, "Download Ad Failed: " + url, e);
            }

            // 儲存至資料庫 [URL, Flag, Duration]
            settingsDao.insertOne(new SettingStructure(
                    VideoEoxPlayer.class.getSimpleName(),
                    new String[]{url, flag, duration}
            ));

            // 更新 UI 進度
            final int percent = (i + 1) * 100 / ads.length();
            new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    SettingActivity.updateProgressUI(percent, "廣告下載中...");
                }
            });
        }
        settingsDao.close();
        Log.i(TAG, "Synced " + ads.length() + " ads from star-cloud.");

        // ✅ 發送廣播通知前台 Activity（HomeActivity / FontendActivity）刷新廣告播放器
        // 🔹 必須設定 setPackage 才能被 RECEIVER_NOT_EXPORTED 的接收器收到（Android 13+）
        android.content.Intent adIntent = new android.content.Intent("com.unibuy.smartdevice.ACTION_AD_UPDATED");
        adIntent.setPackage(handlerMain.getContext().getPackageName());
        handlerMain.getContext().sendBroadcast(adIntent);
        Log.i(TAG, "ACTION_AD_UPDATED broadcast sent.");
    }

    /**
     * 取得設備真實 IP 地址
     */
    private String getLocalIpAddress() {
        try {
            List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
            for (NetworkInterface intf : interfaces) {
                List<InetAddress> addrs = Collections.list(intf.getInetAddresses());
                for (InetAddress addr : addrs) {
                    if (!addr.isLoopbackAddress()) {
                        String sAddr = addr.getHostAddress();
                        boolean isIPv4 = sAddr.indexOf(':') < 0;
                        if (isIPv4)
                            return sAddr;
                    }
                }
            }
        } catch (Exception ex) {
            Log.e(TAG, "Get IP Error", ex);
        }
        return "127.0.0.1";
    }

}
