package com.unibuy.smartdevice.external.mqtt;

import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;

import com.google.gson.Gson;
import com.hivemq.client.mqtt.MqttClient;
import com.hivemq.client.mqtt.MqttClientState;
import com.hivemq.client.mqtt.datatypes.MqttQos;
import com.hivemq.client.mqtt.mqtt3.Mqtt3AsyncClient;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.controller.DevXinYuanController;
import com.unibuy.smartdevice.structure.mqtt.CommandAckPayload;
import com.unibuy.smartdevice.structure.mqtt.CommandPayload;
import com.unibuy.smartdevice.structure.mqtt.ErrorPayload;
import com.unibuy.smartdevice.structure.mqtt.HeartbeatPayload;
import com.unibuy.smartdevice.structure.mqtt.TransactionFinalizePayload;

import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class MqttManager {
    private static final String TAG = "MqttManager";
    private static MqttManager instance;
    private Mqtt3AsyncClient client;
    private final Gson gson = new Gson();
    private com.unibuy.smartdevice.exception.Logs logs;
    private String serialNo;
    private MqttCommandListener commandListener;
    private ScheduledExecutorService heartbeatExecutor;

    private MqttManager() {
        logs = new com.unibuy.smartdevice.exception.Logs(getClass());
    }

    public static synchronized MqttManager getInstance() {
        if (instance == null) {
            instance = new MqttManager();
        }
        return instance;
    }

    public void setCommandListener(MqttCommandListener listener) {
        this.commandListener = listener;
    }

    public void connect(String machineId, String apiToken) {
        if (machineId == null || machineId.isEmpty() || apiToken == null || apiToken.isEmpty()) {
            logs.info("Cannot connect to MQTT: machineId or apiToken is empty");
            return;
        }

        // 如果已經連線或正在連線中，且 ID 沒變，就不重複執行
        if (client != null && serialNo != null && serialNo.equals(machineId)) {
            MqttClientState state = client.getState();
            if (state != MqttClientState.DISCONNECTED) {
                logs.info("MQTT is already " + state + " for: " + machineId);
                return;
            }
        }

        this.serialNo = machineId;
        String clientId = "SC_" + machineId;

        String willTopic = "machine/" + machineId + "/status";
        String willPayload = "{\"status\":\"offline\"}";

        // 如果舊的 client 存在但狀態不對，先關閉
        if (client != null) {
            try {
                client.disconnect();
            } catch (Exception e) {
                // ignore
            }
        }

        String serverHost = "mqtt.setdomains.work";
        if ("prod".equals(MyApp.getInstance().getMachine().getMqttEnv())) {
            serverHost = "emqx.taiwan-star.com.tw";
        }

        client = MqttClient.builder()
                .useMqttVersion3()
                .identifier(clientId)
                .serverHost(serverHost)
                .serverPort(443)
                .useSslWithDefaultConfig()
                .webSocketConfig()
                .serverPath("/mqtt")
                .applyWebSocketConfig()
                .simpleAuth()
                .username(machineId)
                .password(apiToken.getBytes(StandardCharsets.UTF_8))
                .applySimpleAuth()
                .automaticReconnectWithDefaultConfig()
                .addConnectedListener(context -> {
                    logs.info("MQTT Connection established/recovered");
                    publishStatus("online");
                    subscribeToCommandTopic();
                    startHeartbeatScheduler();
                })
                .buildAsync();

        client.connectWith()
                .cleanSession(true)
                .keepAlive(30) // Broker 判斷斷線 = 30 × 1.5 = 45 秒，加快 LWT 觸發
                .willPublish()
                .topic(willTopic)
                .payload(willPayload.getBytes(StandardCharsets.UTF_8))
                .qos(MqttQos.AT_LEAST_ONCE)
                .retain(true)
                .applyWillPublish()
                .send()
                .whenComplete((connAck, throwable) -> {
                    if (throwable != null) {
                        logs.info("MQTT Initial connection failed: " + throwable.getMessage());
                    } else {
                        logs.info("MQTT Initial connection successful");
                    }
                });
    }

    private void startHeartbeatScheduler() {
        if (heartbeatExecutor != null && !heartbeatExecutor.isShutdown()) {
            heartbeatExecutor.shutdownNow();
        }
        heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
        heartbeatExecutor.scheduleAtFixedRate(() -> {
            try {
                String ver = "0";
                try {
                    PackageInfo packageInfo = MyApp.getInstance().getPackageManager()
                            .getPackageInfo(MyApp.getInstance().getPackageName(), 0);
                    ver = packageInfo.versionName;
                } catch (PackageManager.NameNotFoundException e) {
                    // ignore
                }
                int temperature = DevXinYuanController.getTemperature();
                publishHeartbeat(ver, temperature);
            } catch (Exception e) {
                logs.info("Error in heartbeat scheduler: " + e.getMessage());
            }
        }, 10, 60, TimeUnit.SECONDS);
    }

    /**
     * 一般斷線（內部或非計畫性使用）。
     */
    public void disconnect() {
        if (heartbeatExecutor != null && !heartbeatExecutor.isShutdown()) {
            heartbeatExecutor.shutdownNow();
        }
        if (client != null && client.getState().isConnected()) {
            client.disconnect();
        }
    }

    /**
     * 優雅重啟斷線（Graceful Disconnect）。
     *
     * 依照 MQTT 3.1.1 規範：Client 主動發送 DISCONNECT 封包後正常離線，
     * Broker 不會觸發 LWT 遺囑訊息，因此可避免管理後台誤報「離線」。
     *
     * 呼叫流程：
     * 1. publishStatus("restarting") — 告知後台為計畫性重啟
     * 2. 等發布完成後呼叫 client.disconnect() — 正常斷線，LWT 不觸發
     * 3. 執行 onComplete callback — 呼叫方在此執行 killProcess()
     *
     * @param onComplete 斷線完成後的回呼，用來執行 killProcess/System.exit 等後續動作
     */
    public void gracefulDisconnect(Runnable onComplete) {
        // 先停止心跳，避免斷線期間還在發送
        if (heartbeatExecutor != null && !heartbeatExecutor.isShutdown()) {
            heartbeatExecutor.shutdownNow();
        }

        if (client == null || !client.getState().isConnected()) {
            // 如果本來就沒有連線，直接執行後續動作
            logs.info("gracefulDisconnect: MQTT not connected, skip directly.");
            if (onComplete != null)
                onComplete.run();
            return;
        }

        // 發布 restarting 狀態（retain=true，後台可感知這是計畫性重啟）
        String payload = "{\"status\":\"restarting\"}";
        String topic = "machine/" + serialNo + "/status";
        logs.info("gracefulDisconnect: publishing restarting status...");

        client.publishWith()
                .topic(topic)
                .payload(payload.getBytes(StandardCharsets.UTF_8))
                .qos(MqttQos.AT_LEAST_ONCE)
                .retain(true)
                .send()
                .whenComplete((publishResult, pubThrowable) -> {
                    if (pubThrowable != null) {
                        logs.info("gracefulDisconnect: failed to publish restarting status, disconnecting anyway.");
                    } else {
                        logs.info("gracefulDisconnect: restarting status published.");
                    }

                    // 無論發布成否，都執行正常 DISCONNECT（避免 LWT 觸發）
                    client.disconnect()
                            .whenComplete((v, disconnThrowable) -> {
                                if (disconnThrowable != null) {
                                    logs.info("gracefulDisconnect: disconnect error: " + disconnThrowable.getMessage());
                                } else {
                                    logs.info("gracefulDisconnect: disconnected cleanly, LWT suppressed.");
                                }
                                // 回呼呼叫方，可以安全地執行 killProcess
                                if (onComplete != null)
                                    onComplete.run();
                            });
                });
    }

    private void subscribeToCommandTopic() {
        if (client == null || serialNo == null)
            return;

        String commandTopic = "machine/" + serialNo + "/command";
        client.subscribeWith()
                .topicFilter(commandTopic)
                .qos(MqttQos.AT_LEAST_ONCE)
                .callback(publish -> {
                    String payloadString = new String(publish.getPayloadAsBytes(), StandardCharsets.UTF_8);
                    logs.info("Received command payload: " + payloadString);
                    try {
                        CommandPayload command = gson.fromJson(payloadString, CommandPayload.class);
                        if (commandListener != null && command != null) {
                            commandListener.onCommandReceived(command);
                        }
                    } catch (Exception e) {
                        logs.info("Error parsing command payload: " + e.getMessage());
                    }
                })
                .send()
                .whenComplete((subAck, throwable) -> {
                    if (throwable != null) {
                        logs.info("Failed to subscribe to command topic: " + throwable.getMessage());
                    } else {
                        logs.info("Subscribed to " + commandTopic);
                    }
                });
    }

    public void publishStatus(String status) {
        if (client == null || !client.getState().isConnected())
            return;

        String payload = "{\"status\":\"" + status + "\"}";
        String topic = "machine/" + serialNo + "/status";

        client.publishWith()
                .topic(topic)
                .payload(payload.getBytes(StandardCharsets.UTF_8))
                .qos(MqttQos.AT_LEAST_ONCE)
                .retain(true)
                .send()
                .whenComplete((publishResult, throwable) -> {
                    if (throwable != null) {
                        logs.info("Failed to publish status: " + throwable.getMessage());
                    } else {
                        logs.info("Status published: " + payload);
                    }
                });
    }

    public void publishHeartbeat(String firmwareVersion, int temperature) {
        if (client == null || !client.getState().isConnected())
            return;

        HeartbeatPayload payloadObj = new HeartbeatPayload(firmwareVersion, temperature);
        String payload = gson.toJson(payloadObj);
        String topic = "machine/" + serialNo + "/heartbeat";

        client.publishWith()
                .topic(topic)
                .payload(payload.getBytes(StandardCharsets.UTF_8))
                .qos(MqttQos.AT_MOST_ONCE)
                .send()
                .whenComplete((publishResult, throwable) -> {
                    if (throwable != null) {
                        logs.info("Failed to publish heartbeat: " + throwable.getMessage());
                    } else {
                        logs.info("Heartbeat published: " + payload);
                    }
                });
    }

    public void publishCommandAck(String commandId, String result, String message) {
        publishCommandAck(new CommandAckPayload(commandId, result, message));
    }

    public void publishCommandAck(CommandAckPayload ack) {
        if (client == null || !client.getState().isConnected()) {
            if (logs != null) logs.info("Cannot publish ACK: MQTT not connected");
            return;
        }

        String payload = gson.toJson(ack);
        String topic = "machine/" + serialNo + "/command/ack";

        logs.info("Publishing Command ACK to [" + topic + "]: " + payload);

        client.publishWith()
                .topic(topic)
                .payload(payload.getBytes(StandardCharsets.UTF_8))
                .qos(MqttQos.AT_LEAST_ONCE)
                .send()
                .whenComplete((publishResult, throwable) -> {
                    if (throwable != null) {
                        logs.info("Failed to publish command ack: " + throwable.getMessage());
                    } else {
                        logs.info("Command ACK published: " + payload);
                    }
                });
    }

    public void publishError(int tid, String errorCode) {
        if (client == null || !client.getState().isConnected())
            return;

        ErrorPayload payloadObj = new ErrorPayload(tid, errorCode);
        String payload = gson.toJson(payloadObj);
        String topic = "machine/" + serialNo + "/error";

        client.publishWith()
                .topic(topic)
                .payload(payload.getBytes(StandardCharsets.UTF_8))
                .qos(MqttQos.AT_LEAST_ONCE)
                .send()
                .whenComplete((publishResult, throwable) -> {
                    if (throwable != null) {
                        logs.info("Failed to publish error to MQTT: " + throwable.getMessage());
                    } else {
                        logs.info("Hardware error published to " + topic + ": " + payload);
                    }
                });
    }

    /**
     * 發送交易結案訊息 (MQTT Finalize)。
     * 在 App 端完成所有本地動作（支付 + 出貨）後呼叫。
     * Topic: machine/[MachineID]/transaction
     * 取代舊的 B600/B601/B602 分段 HTTP 上報機制。
     *
     * @param payload 由 TransactionFinalizeBuilder.build() 產生的完整 payload
     */
    public void publishTransactionFinalize(TransactionFinalizePayload payload) {
        if (client == null || !client.getState().isConnected()) {
            logs.info("Cannot publish finalize: MQTT not connected");
            return;
        }

        String payloadJson = gson.toJson(payload);
        String topic = "machine/" + serialNo + "/transaction";

        client.publishWith()
                .topic(topic)
                .payload(payloadJson.getBytes(StandardCharsets.UTF_8))
                .qos(MqttQos.AT_LEAST_ONCE)
                .send()
                .whenComplete((publishResult, throwable) -> {
                    if (throwable != null) {
                        logs.info("Failed to publish transaction finalize: " + throwable.getMessage());
                    } else {
                        logs.info("Transaction finalize published to " + topic + ": " + payloadJson);
                    }
                });
    }

    public interface MqttCommandListener {
        void onCommandReceived(CommandPayload command);
    }
}
