package com.unibuy.smartdevice.tools;

import android.content.Context;
import android.content.SharedPreferences;

import com.google.gson.Gson;
import com.unibuy.smartdevice.exception.Logs;
import com.unibuy.smartdevice.external.mqtt.MqttManager;
import com.unibuy.smartdevice.structure.mqtt.TransactionFinalizePayload;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 交易結案上報的本地發件箱（Outbox）。
 *
 * 目的：解決「出貨完成但 MQTT 斷線 / App 被殺 → finalize 直接遺失」的漏送。
 *
 * 設計重點：
 *  - 單一背景執行緒（outboxExecutor）處理所有落地 / 送出 / 移除：不卡 UI 主執行緒、
 *    天然序列化（不需 synchronized、無鎖競爭）。
 *  - 每筆交易一個 SharedPreferences key（"tx:" + entryId）：O(1) 落地 / 移除，無整包重寫。
 *  - 落地用 commit()（在背景緒，安全）→ 送出前確保已在磁碟，crash / 斷電不遺失。
 *  - in-flight 防重送：正在送、等 PUBACK 的單會被標記，drain 不重複送同一筆（避免流量放大）。
 *  - 送出成功（broker PUBACK）才移除；失敗則清 in-flight、累計失敗次數，留待下次 drain。
 *  - 重試上限 / 死信：連續送出失敗達上限的「毒丸」單移入 dead: key（保留資料、不丟），
 *    停止反覆重送並出聲，交維運處理。
 *  - 補送觸發：連線恢復（onConnected，順帶清 in-flight 防殘留）＋ 心跳定時（每 60s）。
 *
 * 可靠性：QoS1 + broker PUBACK 後負責後續投遞；後台對同一 flow_id 完全冪等
 * （serial 前綴防撞號 + 終態短路 + lockForUpdate），故重送 / 亂序皆安全、不會重複扣庫存。
 *
 * entryId = flow_id + ":" + status，讓 pending / failed / completed 各自獨立不互蓋。
 */
public class TransactionOutbox {

    private static final String PREFS = "TransactionOutboxPrefs";
    private static final String KEY_PREFIX = "tx:";     // 待送出
    private static final String ATT_PREFIX = "att:";    // 累計送出失敗次數
    private static final String DEAD_PREFIX = "dead:";  // 死信（達重試上限，保留待維運）
    /** 連續送出失敗（broker 層）達此上限即移入死信，停止反覆重送。 */
    private static final int MAX_ATTEMPTS = 100;
    /** 堆積警戒線：超過即出聲警告（不丟資料），代表網路 / broker 長時間異常。 */
    private static final int WARN_THRESHOLD = 200;

    private static final Logs logs = new Logs(TransactionOutbox.class);
    private static final Gson gson = new Gson();

    /** 單一背景執行緒：序列化所有 outbox 操作，不卡 UI、不需鎖。 */
    private static final ExecutorService outboxExecutor = Executors.newSingleThreadExecutor(r -> {
        Thread t = new Thread(r, "tx-outbox");
        t.setDaemon(true);
        return t;
    });

    /** 正在送出、等待 PUBACK 的 entryId。只在 outboxExecutor 單一緒存取 → 免鎖。 */
    private static final Set<String> inflight = new HashSet<>();

    /**
     * 結案上報：背景緒落地後嘗試送出（成功才移除）。
     * 呼叫端（出貨 / 付款結案的 UI 主執行緒）只做一次輕量序列化後立即返回，不阻塞、不做磁碟 I/O。
     */
    public static void submit(Context context, TransactionFinalizePayload payload) {
        if (context == null || payload == null) {
            return;
        }
        final Context app = context.getApplicationContext();
        final String json;
        final String entryId;
        try {
            json = gson.toJson(payload);
            String flowId = payload.getFlow_id();
            String status = (payload.getOrder() != null && payload.getOrder().getStatus() != null)
                    ? payload.getOrder().getStatus() : "completed";
            entryId = (flowId != null ? flowId : "unknown") + ":" + status;
        } catch (Exception e) {
            logs.info("TransactionOutbox.submit serialize failed: " + e.getMessage());
            return;
        }
        outboxExecutor.execute(() -> {
            persist(app, entryId, json);
            drain(app);
        });
    }

    /** 心跳定時呼叫：背景緒補送所有未送出的交易（保留 in-flight 狀態）。 */
    public static void resumeIfPending(Context context) {
        if (context == null) {
            return;
        }
        final Context app = context.getApplicationContext();
        outboxExecutor.execute(() -> drain(app));
    }

    /** 連線恢復呼叫：先清 in-flight（舊連線在途的送出已失效，避免殘留卡住）再補送。 */
    public static void onConnected(Context context) {
        if (context == null) {
            return;
        }
        final Context app = context.getApplicationContext();
        outboxExecutor.execute(() -> {
            inflight.clear();
            drain(app);
        });
    }

    // ==== 以下全部只在 outboxExecutor 單一背景緒執行，故無併發、不需鎖 ====

    /** 落地單筆（commit 同步寫磁碟，確保 publish 前已在磁碟）。 */
    private static void persist(Context app, String entryId, String json) {
        try {
            prefs(app).edit().putString(KEY_PREFIX + entryId, json).commit();
            logs.info("TransactionOutbox persisted " + entryId);
            int count = countPending(prefs(app).getAll());
            if (count > WARN_THRESHOLD) {
                logs.info("TransactionOutbox WARNING: " + count
                        + " entries pending — MQTT/network likely down for a long time");
            }
        } catch (Exception e) {
            logs.info("TransactionOutbox.persist failed: " + e.getMessage());
        }
    }

    /** 嘗試送出所有未送出且未在途的交易；未連線即停，留待下次 drain。 */
    private static void drain(Context app) {
        try {
            Map<String, ?> all = prefs(app).getAll();
            int pending = countPending(all);
            if (pending == 0) {
                return;
            }
            logs.info("TransactionOutbox.drain: " + pending + " pending, " + inflight.size() + " in-flight");
            for (Map.Entry<String, ?> e : all.entrySet()) {
                String key = e.getKey();
                if (key == null || !key.startsWith(KEY_PREFIX)) {
                    continue;
                }
                final String entryId = key.substring(KEY_PREFIX.length());
                if (inflight.contains(entryId)) {
                    continue; // A：已在送、等 PUBACK，不重送
                }
                Object v = e.getValue();
                if (!(v instanceof String)) {
                    continue;
                }
                boolean sent = MqttManager.getInstance().rawPublishTransaction(
                        (String) v,
                        () -> outboxExecutor.execute(() -> onSendSuccess(app, entryId)),
                        () -> outboxExecutor.execute(() -> onSendFailure(app, entryId)));
                if (sent) {
                    inflight.add(entryId);
                } else {
                    // 未連線：其餘也會失敗，留待下次 drain（連線恢復或下次心跳）。
                    logs.info("TransactionOutbox.drain: not connected, keep pending for next drain");
                    break;
                }
            }
        } catch (Exception e) {
            logs.info("TransactionOutbox.drain failed: " + e.getMessage());
        }
    }

    /** 送出成功（PUBACK）：清 in-flight、移除該筆與其失敗計數。 */
    private static void onSendSuccess(Context app, String entryId) {
        try {
            inflight.remove(entryId);
            prefs(app).edit().remove(KEY_PREFIX + entryId).remove(ATT_PREFIX + entryId).apply();
            logs.info("TransactionOutbox removed " + entryId + " (published)");
        } catch (Exception e) {
            logs.info("TransactionOutbox.onSendSuccess failed: " + e.getMessage());
        }
    }

    /** 送出失敗：清 in-flight、累計失敗；達上限移入死信（保留資料、不丟、出聲）。 */
    private static void onSendFailure(Context app, String entryId) {
        try {
            inflight.remove(entryId);
            SharedPreferences p = prefs(app);
            int att = p.getInt(ATT_PREFIX + entryId, 0) + 1;
            if (att >= MAX_ATTEMPTS) {
                String json = p.getString(KEY_PREFIX + entryId, null);
                SharedPreferences.Editor ed = p.edit()
                        .remove(KEY_PREFIX + entryId)
                        .remove(ATT_PREFIX + entryId);
                if (json != null) {
                    ed.putString(DEAD_PREFIX + entryId, json); // 保留資料供維運處理，不丟棄
                }
                ed.apply();
                logs.info("TransactionOutbox DEAD-LETTER " + entryId + " after " + att
                        + " failed attempts — moved to dead:, NEEDS OPS ATTENTION");
            } else {
                p.edit().putInt(ATT_PREFIX + entryId, att).apply();
                logs.info("TransactionOutbox send failed " + entryId + " attempt " + att + "/" + MAX_ATTEMPTS);
            }
        } catch (Exception e) {
            logs.info("TransactionOutbox.onSendFailure failed: " + e.getMessage());
        }
    }

    /** 計算待送出（tx:）筆數；傳入已取得的快照避免重複 getAll。 */
    private static int countPending(Map<String, ?> all) {
        int n = 0;
        for (String key : all.keySet()) {
            if (key != null && key.startsWith(KEY_PREFIX)) {
                n++;
            }
        }
        return n;
    }

    private static SharedPreferences prefs(Context app) {
        return app.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
    }
}
