#include "task_lora_pf.h"
#include "task_network.h"
#include "sys_manager.h"
#include <WiFiUdp.h>
#include <WiFiClient.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

// UART giao tiếp với Module Ebyte E220-900T30D
static HardwareSerial SerialE220(1);

// UDP Client cho Semtech UDP Forwarder v2
static WiFiUDP udpClient;
static SemaphoreHandle_t s_udpMutex = NULL;

// MQTT Client cho MQTT Forwarder
static WiFiClient wifiMqttClient;
static PubSubClient mqttClient(wifiMqttClient);
static uint32_t s_lastMqttReconnectAttempt = 0;

#define PROTOCOL_VERSION  0x02
#define PKT_PUSH_DATA     0x00
#define PKT_PUSH_ACK      0x01
#define PKT_PULL_DATA     0x02
#define PKT_PULL_RESP     0x03
#define PKT_PULL_ACK      0x04

static void parseEUI(const char *euiStr, uint8_t *outBytes) {
    for (int i = 0; i < 8; i++) {
        char byteChars[3] = {euiStr[i * 2], euiStr[i * 2 + 1], 0};
        outBytes[i] = (uint8_t)strtoul(byteChars, NULL, 16);
    }
}

// ====================================================================
// 1. ĐIỀU KHIỂN PHẦN CỨNG & THANH GHI EBYTE E220-900T30D (LLCC68 UART)
// ====================================================================
static void e220_wait_aux(uint32_t timeoutMs = 800) {
    if (g_config.e220AuxPin < 0) {
        vTaskDelay(pdMS_TO_TICKS(20));
        return;
    }
    uint32_t start = millis();
    while (digitalRead(g_config.e220AuxPin) == LOW && (millis() - start < timeoutMs)) {
        vTaskDelay(pdMS_TO_TICKS(5));
    }
    vTaskDelay(pdMS_TO_TICKS(10));
}

// Mode 0: Normal (M0=0, M1=0) | Mode 3: Config (M0=1, M1=1)
static void e220_set_mode(uint8_t mode) {
    e220_wait_aux();
    if (g_config.e220M0Pin >= 0) digitalWrite(g_config.e220M0Pin, (mode & 0x01) ? HIGH : LOW);
    if (g_config.e220M1Pin >= 0) digitalWrite(g_config.e220M1Pin, (mode & 0x02) ? HIGH : LOW);
    vTaskDelay(pdMS_TO_TICKS(60));
    e220_wait_aux();
}

bool E220_ApplyConfig() {
    Serial.println("[E220] Chuyen che do Configuration de ghi tham so...");
    e220_set_mode(3); // Mode 3: Configuration (9600 8N1)
    SerialE220.updateBaudRate(9600);
    vTaskDelay(pdMS_TO_TICKS(50));

    // Lệnh ghi thanh ghi E220: C0 00 08 [ADDH] [ADDL] [NETID] [REG0] [REG1] [REG2] [REG3] [CRYPTH]
    uint8_t cmd[11];
    cmd[0] = 0xC0;
    cmd[1] = 0x00;
    cmd[2] = 0x08;

    // ADDH, ADDL
    cmd[3] = (uint8_t)(g_config.e220Address >> 8);
    cmd[4] = (uint8_t)(g_config.e220Address & 0xFF);

    // NETID
    cmd[5] = g_config.e220NetId;

    // REG0: [7:5] Baudrate, [4:3] Parity 8N1 (00), [2:0] Air Data Rate
    uint8_t baudBits = 0x03; // Mặc định 9600 (011)
    if (g_config.e220BaudRate == 1200) baudBits = 0x00;
    else if (g_config.e220BaudRate == 2400) baudBits = 0x01;
    else if (g_config.e220BaudRate == 4800) baudBits = 0x02;
    else if (g_config.e220BaudRate == 9600) baudBits = 0x03;
    else if (g_config.e220BaudRate == 19200) baudBits = 0x04;
    else if (g_config.e220BaudRate == 38400) baudBits = 0x05;
    else if (g_config.e220BaudRate == 57600) baudBits = 0x06;
    else if (g_config.e220BaudRate == 115200) baudBits = 0x07;

    uint8_t airBits = g_config.e220AirRate & 0x07;
    cmd[6] = (baudBits << 5) | airBits;

    // REG1: [7:6] Subpacket 200B (00), [5] RSSI noise (1), [1:0] TxPower (00=30dBm, 01=27dBm, 10=24dBm, 11=21dBm)
    uint8_t pwrBits = g_config.e220TxPower & 0x03;
    cmd[7] = (0x00 << 6) | (0x01 << 5) | pwrBits;

    // REG2: Kênh tần số (0 - 80). Tần số = 850.125 + CH * 1 MHz
    cmd[8] = g_config.e220Channel & 0x7F;

    // REG3: [7] RSSI Byte Enable, [6] Transparent (0)
    cmd[9] = (g_config.e220EnableRSSI ? 0x80 : 0x00);

    // CRYPT_H (0x00)
    cmd[10] = 0x00;

    while (SerialE220.available()) SerialE220.read();
    SerialE220.write(cmd, 11);
    
    // Đọc phản hồi không dùng readBytes block lớn (tránh tốn stack & deadlock)
    uint8_t reply[11] = {0};
    size_t n = 0;
    uint32_t t0 = millis();
    while ((millis() - t0 < 300) && (n < 11)) {
        if (SerialE220.available()) {
            reply[n++] = (uint8_t)SerialE220.read();
        } else {
            vTaskDelay(pdMS_TO_TICKS(10));
        }
    }

    bool success = false;
    if (n >= 4 && (reply[0] == 0xC1 || reply[0] == 0xC0)) {
        success = true;
        Serial.printf("[E220] Ghi thanh ghi E220 thanh cong! Kenh %u (%.3f MHz), Cong suat 30dBm.\n",
                      g_config.e220Channel, 850.125f + (float)g_config.e220Channel);
    } else {
        Serial.printf("[E220] Module chua phan hoi (Doc duoc %u bytes). Chuyen ve Normal Mode.\n", (unsigned int)n);
    }

    // Chuyển về Mode 0: Normal Transmission / Reception
    e220_set_mode(0);
    SerialE220.updateBaudRate(g_config.e220BaudRate);
    vTaskDelay(pdMS_TO_TICKS(50));

    withTelemetry([&](GatewayTelemetry &t) {
        t.e220Ready = success;
        t.e220FreqMHz = 850.125f + (float)g_config.e220Channel;
    });

    return success;
}

bool E220_ReadConfig(uint8_t *configBuf) {
    e220_set_mode(3);
    SerialE220.updateBaudRate(9600);
    vTaskDelay(pdMS_TO_TICKS(50));

    uint8_t readCmd[3] = {0xC1, 0xC1, 0xC1};
    while (SerialE220.available()) SerialE220.read();
    SerialE220.write(readCmd, 3);

    size_t n = 0;
    uint32_t t0 = millis();
    while ((millis() - t0 < 300) && (n < 11)) {
        if (SerialE220.available()) {
            configBuf[n++] = (uint8_t)SerialE220.read();
        } else {
            vTaskDelay(pdMS_TO_TICKS(10));
        }
    }

    e220_set_mode(0);
    SerialE220.updateBaudRate(g_config.e220BaudRate);
    return (n >= 4 && configBuf[0] == 0xC1);
}

bool LoRa_InitRadio() {
    if (s_udpMutex == NULL) {
        s_udpMutex = xSemaphoreCreateMutex();
    }

    // Cấu hình chân GPIO E220-900T30D
    if (g_config.e220M0Pin >= 0) pinMode(g_config.e220M0Pin, OUTPUT);
    if (g_config.e220M1Pin >= 0) pinMode(g_config.e220M1Pin, OUTPUT);
    if (g_config.e220AuxPin >= 0) pinMode(g_config.e220AuxPin, INPUT_PULLUP);

    // Khởi tạo UART cho E220
    SerialE220.begin(g_config.e220BaudRate, SERIAL_8N1, g_config.e220RxPin, g_config.e220TxPin);
    SerialE220.setTimeout(150);
    Serial.printf("[E220] Khoi tao UART: RX_PIN=%d, TX_PIN=%d, AUX=%d, M0=%d, M1=%d @ %lu bps\n",
                  g_config.e220RxPin, g_config.e220TxPin, g_config.e220AuxPin,
                  g_config.e220M0Pin, g_config.e220M1Pin, (unsigned long)g_config.e220BaudRate);

    // Áp dụng cấu hình tần số & công suất lên E220
    E220_ApplyConfig();
    return true;
}

void LoRa_SendPacket(const uint8_t *payload, size_t size) {
    if (size == 0 || size > 200) return;
    e220_wait_aux(500);
    SerialE220.write(payload, size);
    Serial.printf("[E220 TX] Phat goi tin LoRa qua E220-900T30D: %u bytes\n", (unsigned int)size);
}

void LoRa_InjectTestPacket(const char* devEUI, const uint8_t* payload, size_t size, int16_t rssi, float snr) {
    LoRaRxPacket pkt;
    pkt.size = (size > sizeof(pkt.payload)) ? sizeof(pkt.payload) : size;
    memcpy(pkt.payload, payload, pkt.size);
    pkt.rssi = rssi;
    pkt.snr = snr;
    pkt.timestampMs = millis();
    strncpy(pkt.devEUI, devEUI, sizeof(pkt.devEUI) - 1);

    if (g_loraRxQueue) {
        xQueueSend(g_loraRxQueue, &pkt, 0);
    }
}

// ====================================================================
// 2. MQTT FORWARDER CLIENT & DISPATCHER
// Trích xuất Gateway EUI chữ thường 16 ký tự hex
static void getGatewayEuiLower(char* out, size_t maxLen) {
    if (!out || maxLen < 17) return;
    size_t i = 0;
    for (; g_config.gatewayEUI[i] && i < 16 && i < maxLen - 1; i++) {
        out[i] = (char)tolower((unsigned char)g_config.gatewayEUI[i]);
    }
    out[i] = '\0';
}

// Trích xuất Topic Prefix (mặc định "as923" theo chuẩn ChirpStack Gateway OS)
static String getMqttPrefix() {
    const char* p = strstr(g_config.mqttUplinkTopic, "/gateway/");
    if (p != nullptr) {
        int len = p - g_config.mqttUplinkTopic;
        if (len > 0 && len < 32) {
            char pfx[32];
            strncpy(pfx, g_config.mqttUplinkTopic, len);
            pfx[len] = '\0';
            if (strcmp(pfx, "gateway") != 0 && strcmp(pfx, "+") != 0) {
                return String(pfx);
            }
        }
    }
    return String("as923");
}

// Helper giải mã Topic template (hỗ trợ %s thay cho Gateway EUI chữ thường)
[[maybe_unused]] static String resolveMqttTopic(const char* tmpl) {
    char euiLower[20] = {0};
    getGatewayEuiLower(euiLower, sizeof(euiLower));
    if (!tmpl || strlen(tmpl) == 0) {
        return "as923/gateway/" + String(euiLower) + "/event/up";
    }
    String s(tmpl);
    if (s.indexOf("%s") != -1) {
        s.replace("%s", euiLower);
    }
    return s;
}

// Helper mã hoá Base64
static String toBase64(const uint8_t* data, size_t len) {
    String encoded = "";
    static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    int val = 0, valb = -6;
    for (size_t i = 0; i < len; i++) {
        val = (val << 8) + data[i];
        valb += 8;
        while (valb >= 0) {
            encoded += b64[(val >> valb) & 0x3F];
            valb -= 6;
        }
    }
    if (valb > -6) encoded += b64[((val << 8) >> (valb + 8)) & 0x3F];
    while (encoded.length() % 4) encoded += '=';
    return encoded;
}

// Helper giải mã Base64 sang binary
static size_t decodeBase64(const char* input, uint8_t* output, size_t maxLen) {
    if (!input || !output) return 0;
    static const int b64inv[] = {
        62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
        -1, -1, -1, -1, -1, -1, -1,  0,  1,  2,  3,  4,  5,  6,  7,
         8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
        23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31,
        32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46,
        47, 48, 49, 50, 51
    };
    size_t inLen = strlen(input);
    size_t outIdx = 0;
    int val = 0, valb = -8;
    for (size_t i = 0; i < inLen; i++) {
        char c = input[i];
        if (c == '=') break;
        if (c < '+' || c > 'z') continue;
        int d = b64inv[c - '+'];
        if (d == -1) continue;
        val = (val << 6) + d;
        valb += 6;
        if (valb >= 0) {
            if (outIdx < maxLen) {
                output[outIdx++] = (uint8_t)((val >> valb) & 0xFF);
            }
            valb -= 8;
        }
    }
    return outIdx;
}

// Helper giải mã chuỗi Hex sang binary
static size_t decodeHex(const char* input, uint8_t* output, size_t maxLen) {
    if (!input || !output) return 0;
    size_t inLen = strlen(input);
    size_t outLen = 0;
    for (size_t i = 0; i + 1 < inLen && outLen < maxLen; i += 2) {
        char buf[3] = { input[i], input[i+1], '\0' };
        output[outLen++] = (uint8_t)strtol(buf, nullptr, 16);
    }
    return outLen;
}

// Gửi bản tin Downlink Tx Ack (event/ack) tới ChirpStack theo chuẩn payload downlink.pdf
static void sendDownlinkAck(uint32_t downlinkId, bool ok) {
    if (!mqttClient.connected()) return;
    char euiLower[20] = {0};
    getGatewayEuiLower(euiLower, sizeof(euiLower));
    String prefix = getMqttPrefix();

    JsonDocument ackDoc;
    ackDoc["gatewayId"] = euiLower;
    ackDoc["downlinkId"] = downlinkId;
    JsonObject item = ackDoc["items"].to<JsonArray>().add<JsonObject>();
    item["status"] = ok ? "OK" : "ERROR";

    String ackJson;
    serializeJson(ackDoc, ackJson);

    char ackTopic[80];
    snprintf(ackTopic, sizeof(ackTopic), "%s/gateway/%s/event/ack", prefix.c_str(), euiLower);
    mqttClient.publish(ackTopic, ackJson.c_str());
    Serial.printf("[MQTT Ack] Da gui Downlink Tx Ack (ID: %lu, Status: %s) toi %s\n",
                  (unsigned long)downlinkId, ok ? "OK" : "ERROR", ackTopic);
}

// Xử lý bản tin Downlink từ ChirpStack (command/down) & lệnh hệ thống (command/exec)
static void mqttCallback(char* topic, byte* message, unsigned int length) {
    Serial.printf("[MQTT RX] Nhan thong diep tu topic: %s (%u bytes)\n", topic, length);

    char euiLower[20] = {0};
    getGatewayEuiLower(euiLower, sizeof(euiLower));
    String prefix = getMqttPrefix();

    // 1. Nhận lệnh thực thi hệ thống (command/exec) - Hỗ trợ Reboot từ xa & phản hồi event/exec
    if (strstr(topic, "/command/exec") != nullptr) {
        JsonDocument execDoc;
        DeserializationError err = deserializeJson(execDoc, message, length);
        if (!err) {
            const char* cmd = execDoc["command"] | "";
            uint32_t execId = execDoc["execId"] | 0;
            Serial.printf("[MQTT Remote] Nhan lenh exec: '%s' (execId: %lu)\n", cmd, (unsigned long)execId);

            // Báo nhận kết quả lên event/exec
            char execTopic[80];
            snprintf(execTopic, sizeof(execTopic), "%s/gateway/%s/event/exec", prefix.c_str(), euiLower);

            JsonDocument respDoc;
            respDoc["gatewayId"] = euiLower;
            respDoc["execId"] = execId;
            respDoc["error"] = "";
            respDoc["stdout"] = "OK";
            String respJson;
            serializeJson(respDoc, respJson);
            mqttClient.publish(execTopic, respJson.c_str());

            if (strcmp(cmd, "reboot") == 0) {
                Serial.println("[MQTT Remote] Khoi dong lai Gateway theo lenh exec...");
                vTaskDelay(pdMS_TO_TICKS(500));
                ESP.restart();
            }
        }
        return;
    }

    // 2. Nhận cấu hình từ ChirpStack (command/config)
    if (strstr(topic, "/command/config") != nullptr) {
        Serial.println("[MQTT Config] Nhan ban tin cau hinh kenh tu ChirpStack.");
        return;
    }

    // 3. Nhận bản tin truyền thông Downlink (command/down)
    if (strstr(topic, "/command/down") != nullptr) {
        JsonDocument dlDoc;
        DeserializationError err = deserializeJson(dlDoc, message, length);
        if (!err && dlDoc["items"].is<JsonArray>()) {
            uint32_t downlinkId = dlDoc["downlinkId"] | (uint32_t)random(100000, 999999);
            JsonArray items = dlDoc["items"].as<JsonArray>();
            if (items.size() > 0) {
                const char* phyPayloadStr = items[0]["phyPayload"] | "";
                uint8_t rawLoRa[256];
                size_t rawLen = 0;

                // Kiểm tra phyPayload là Base64 hay Hex
                bool isHex = true;
                size_t plen = strlen(phyPayloadStr);
                for (size_t i = 0; i < plen; i++) {
                    if (!isxdigit((unsigned char)phyPayloadStr[i])) { isHex = false; break; }
                }
                if (isHex && plen % 2 == 0 && plen > 0) {
                    rawLen = decodeHex(phyPayloadStr, rawLoRa, sizeof(rawLoRa));
                } else {
                    rawLen = decodeBase64(phyPayloadStr, rawLoRa, sizeof(rawLoRa));
                }

                if (rawLen > 0) {
                    Serial.printf("[LoRa TX] Phat Downlink toi thiet bi qua E220 (%u bytes)...\n", (unsigned int)rawLen);
                    LoRa_SendPacket(rawLoRa, rawLen);
                    digitalWrite(PIN_STATUS_LED, !digitalRead(PIN_STATUS_LED)); // Chop LED IO2 khi phat Downlink
                    withTelemetry([&](GatewayTelemetry &t) {
                        t.txPktsCount++;
                    });
                    sendDownlinkAck(downlinkId, true);
                } else {
                    Serial.println("[MQTT Downlink] Khong the giai ma phyPayload trong items[0]!");
                    sendDownlinkAck(downlinkId, false);
                }
            }
        } else {
            // Fallback tương thích: Nếu Broker gửi raw byte payload trực tiếp
            LoRa_SendPacket(message, length);
            withTelemetry([&](GatewayTelemetry &t) {
                t.txPktsCount++;
            });
        }
    }
}

static void checkMqttConnection() {
    // Trạm Relay hoạt động hoàn toàn bằng sóng vô tuyến Mesh, không kết nối MQTT trực tiếp
    if (g_config.meshEnable && g_config.meshRole == MESH_ROLE_RELAY) {
        return;
    }
    if (!g_config.mqttEnable || !Network_IsConnected()) {
        return;
    }

    if (!mqttClient.connected()) {
        uint32_t now = millis();
        if (now - s_lastMqttReconnectAttempt > 8000) { // Thử kết nối lại mỗi 8 giây
            s_lastMqttReconnectAttempt = now;
            mqttClient.setServer(g_config.mqttServer, g_config.mqttPort);
            mqttClient.setBufferSize(2048); // Đệm lớn 2KB phục vụ bản tin JSON ChirpStack / EMQX
            mqttClient.setCallback(mqttCallback);

            char euiLower[20] = {0};
            getGatewayEuiLower(euiLower, sizeof(euiLower));
            String prefix = getMqttPrefix();

            // Topic trạng thái kết nối state/conn (ChirpStack v4 MQTT Forwarder)
            char connTopic[80];
            snprintf(connTopic, sizeof(connTopic), "%s/gateway/%s/state/conn", prefix.c_str(), euiLower);

            // Last Will Testament: khi broker phát hiện gateway mất kết nối -> tự động publish OFFLINE (retained)
            char offlineJson[80];
            snprintf(offlineJson, sizeof(offlineJson), "{\"gatewayId\":\"%s\",\"state\":\"OFFLINE\"}", euiLower);

            Serial.printf("[MQTT] Dang ket noi EMQX Broker: %s:%u (Client: %s, Prefix: %s)...\n",
                          g_config.mqttServer, g_config.mqttPort, g_config.mqttClientId, prefix.c_str());

            bool connected = false;
            if (strlen(g_config.mqttUser) > 0) {
                connected = mqttClient.connect(g_config.mqttClientId, g_config.mqttUser, g_config.mqttPass,
                                              connTopic, 0, true, offlineJson);
            } else {
                connected = mqttClient.connect(g_config.mqttClientId, connTopic, 0, true, offlineJson);
            }

            if (connected) {
                Serial.printf("[MQTT] Ket noi thanh cong! Gateway EUI: %s\n", euiLower);

                // 1. Gửi bản tin trạng thái ONLINE (kèm cờ retained = true) đúng 1 lần khi kết nối thành công
                char onlineJson[80];
                snprintf(onlineJson, sizeof(onlineJson), "{\"gatewayId\":\"%s\",\"state\":\"ONLINE\"}", euiLower);
                mqttClient.publish(connTopic, onlineJson, true);
                Serial.printf("[MQTT State] Da gui trang thai ONLINE (retained) toi %s\n", connTopic);

                // 2. Subscribe wildcard topic command/+ theo chuẩn ChirpStack Gateway OS
                // Nếu là Root Gateway trong mạng Mesh: Subscribe '+' để nhận command cho toàn bộ Gateway Mesh
                char cmdTopic[80];
                if (g_config.meshEnable && g_config.meshRole == MESH_ROLE_ROOT) {
                    snprintf(cmdTopic, sizeof(cmdTopic), "%s/gateway/+/command/+", prefix.c_str());
                } else {
                    snprintf(cmdTopic, sizeof(cmdTopic), "%s/gateway/%s/command/+", prefix.c_str(), euiLower);
                }
                bool subOk = mqttClient.subscribe(cmdTopic);
                Serial.printf("[MQTT Sub] Subscribe wildcard topic: %s (%s)\n", cmdTopic, subOk ? "OK" : "FAILED");
            } else {
                Serial.printf("[MQTT] Ket noi EMQX that bai, ma loi: %d\n", mqttClient.state());
            }

            withTelemetry([&](GatewayTelemetry &t) {
                t.mqttConnected = connected;
            });
        }
    } else {
        mqttClient.loop();
    }
}

// Chuyển tiếp bản tin Uplink từ Cảm biến tới MQTT Broker -> ChirpStack v4
// Cấu trúc JSON tuân thủ chuẩn Protobuf gw.UplinkFrame (ChirpStack v4)
static void forwardPacketToMqtt(const LoRaRxPacket &pkt) {
    if (!g_config.mqttEnable || !mqttClient.connected()) return;

    // Chuẩn hoá Gateway ID dạng chữ thường chuẩn LoRaWAN / ChirpStack
    char euiLower[20] = {0};
    getGatewayEuiLower(euiLower, sizeof(euiLower));
    String prefix = getMqttPrefix();

    // Tần số hoạt động AS923 Channel 1 (923.4 MHz) khớp cấu hình ChirpStack Region as923
    uint32_t freqHz = 923400000;
    if (g_config.e220Channel > 0 && g_config.e220Channel != 71) {
        freqHz = (uint32_t)((850.125f + (float)g_config.e220Channel) * 1000000.0f);
    }

    // Thời gian ISO 8601 UTC
    char isoTime[36];
    time_t now = time(nullptr);
    if (now > 1600000000) {
        struct tm timeinfo;
        gmtime_r(&now, &timeinfo);
        strftime(isoTime, sizeof(isoTime), "%Y-%m-%dT%H:%M:%SZ", &timeinfo);
    } else {
        snprintf(isoTime, sizeof(isoTime), "2026-11-05T02:20:00Z");
    }

    // Chuẩn hóa 100% khớp đúng 3 trường Protobuf gw.UplinkFrame của ChirpStack v4
    JsonDocument doc;

    // 1. phyPayload (Base64)
    doc["phyPayload"] = toBase64(pkt.payload, pkt.size);

    // 2. txInfo
    JsonObject txInfo = doc["txInfo"].to<JsonObject>();
    txInfo["frequency"] = freqHz;
    JsonObject mod = txInfo["modulation"]["lora"].to<JsonObject>();
    mod["bandwidth"] = 125000;
    mod["spreadingFactor"] = (g_config.loraSf > 0) ? g_config.loraSf : 7;
    mod["codeRate"] = "CR_4_5";

    // 3. rxInfo (Object đơn theo Protobuf gw.UplinkRxInfo)
    JsonObject rxInfo = doc["rxInfo"].to<JsonObject>();
    rxInfo["gatewayId"] = euiLower;
    rxInfo["uplinkId"] = (uint32_t)random(1000, 65535);
    rxInfo["time"] = isoTime;
    rxInfo["rssi"] = pkt.rssi;
    rxInfo["snr"] = pkt.snr;
    rxInfo["channel"] = 1;
    rxInfo["context"] = "FAru6A==";
    rxInfo["crcStatus"] = "CRC_OK";

    String jsonStr;
    serializeJson(doc, jsonStr);

    // Gửi DUY NHẤT tới topic chuẩn ChirpStack MQTT Forwarder: {prefix}/gateway/<id>/event/up
    // Tuân thủ nghiêm ngặt ACL của EMQX (+/gateway/+/event/#)
    char upTopic[80];
    snprintf(upTopic, sizeof(upTopic), "%s/gateway/%s/event/up", prefix.c_str(), euiLower);
    bool pubOk = mqttClient.publish(upTopic, jsonStr.c_str());

    Serial.printf("[MQTT Uplink] Da publish LoRaWAN frame toi ChirpStack: %s (%s, %u bytes)\n",
                  upTopic, pubOk ? "OK" : "FAILED", (unsigned int)jsonStr.length());
}

// ====================================================================
// 2.2. GIAO THỨC GATEWAY MESH TRONG SUỐT (HC-MESH TRANSPARENT PROTOCOL)
// ====================================================================
struct MeshDedupEntry {
    char relayEui[17];
    uint16_t packetId;
    uint32_t timestampMs;
};
static MeshDedupEntry s_meshDedup[32];
static uint8_t s_meshDedupIdx = 0;

static bool isMeshDuplicate(const char* relayEui, uint16_t packetId) {
    uint32_t now = millis();
    for (int i = 0; i < 32; i++) {
        if (s_meshDedup[i].packetId == packetId && 
            strncmp(s_meshDedup[i].relayEui, relayEui, 16) == 0 &&
            (now - s_meshDedup[i].timestampMs < 6000)) {
            return true;
        }
    }
    strncpy(s_meshDedup[s_meshDedupIdx].relayEui, relayEui, 16);
    s_meshDedup[s_meshDedupIdx].relayEui[16] = '\0';
    s_meshDedup[s_meshDedupIdx].packetId = packetId;
    s_meshDedup[s_meshDedupIdx].timestampMs = now;
    s_meshDedupIdx = (s_meshDedupIdx + 1) % 32;
    return false;
}

// Cập nhật danh sách các trạm Relay trong bảng Mesh Topology tại trạm Root
static void updateMeshNodeTopology(const char* relayEui, int16_t rssi, float snr, float chipTemp, uint8_t pwr) {
    withTelemetry([&](GatewayTelemetry &t) {
        uint32_t nowSec = millis() / 1000;
        int foundIdx = -1;
        for (int i = 0; i < t.meshNodeCount; i++) {
            if (strcasecmp(t.meshNodes[i].eui, relayEui) == 0) {
                foundIdx = i;
                break;
            }
        }
        if (foundIdx >= 0) {
            t.meshNodes[foundIdx].lastRssi = rssi;
            t.meshNodes[foundIdx].lastSnr = snr;
            t.meshNodes[foundIdx].lastSeenSec = nowSec;
            t.meshNodes[foundIdx].packetCount++;
            if (chipTemp > 0.0f) t.meshNodes[foundIdx].chipTemp = chipTemp;
            if (pwr > 0) t.meshNodes[foundIdx].pwrSource = pwr;
            t.meshNodes[foundIdx].isOnline = true;
        } else if (t.meshNodeCount < MAX_MESH_NODES) {
            int newIdx = t.meshNodeCount++;
            strncpy(t.meshNodes[newIdx].eui, relayEui, 16);
            t.meshNodes[newIdx].eui[16] = '\0';
            t.meshNodes[newIdx].lastRssi = rssi;
            t.meshNodes[newIdx].lastSnr = snr;
            t.meshNodes[newIdx].lastSeenSec = nowSec;
            t.meshNodes[newIdx].packetCount = 1;
            t.meshNodes[newIdx].chipTemp = chipTemp > 0.0f ? chipTemp : 35.0f;
            t.meshNodes[newIdx].pwrSource = pwr > 0 ? pwr : 4; // USB 5V
            t.meshNodes[newIdx].isOnline = true;
        }
    });
}

// Chuyển tiếp trong suốt gói tin LoRaWAN Uplink từ Relay Gateway lên ChirpStack v4
static void forwardTransparentMeshPacketToMqtt(const LoRaRxPacket &pkt, const char* relayEuiLower) {
    if (!g_config.mqttEnable || !mqttClient.connected()) return;

    String prefix = getMqttPrefix();

    uint32_t freqHz = 923400000;
    if (g_config.e220Channel > 0 && g_config.e220Channel != 71) {
        freqHz = (uint32_t)((850.125f + (float)g_config.e220Channel) * 1000000.0f);
    }

    char isoTime[36];
    time_t now = time(nullptr);
    if (now > 1600000000) {
        struct tm timeinfo;
        gmtime_r(&now, &timeinfo);
        strftime(isoTime, sizeof(isoTime), "%Y-%m-%dT%H:%M:%SZ", &timeinfo);
    } else {
        snprintf(isoTime, sizeof(isoTime), "2026-11-05T02:20:00Z");
    }

    JsonDocument doc;
    doc["phyPayload"] = toBase64(pkt.payload, pkt.size);

    JsonObject txInfo = doc["txInfo"].to<JsonObject>();
    txInfo["frequency"] = freqHz;
    JsonObject mod = txInfo["modulation"]["lora"].to<JsonObject>();
    mod["bandwidth"] = 125000;
    mod["spreadingFactor"] = (g_config.loraSf > 0) ? g_config.loraSf : 7;
    mod["codeRate"] = "CR_4_5";

    // rxInfo mang định danh của chính trạm RELAY đã thu sóng từ cảm biến
    JsonObject rxInfo = doc["rxInfo"].to<JsonObject>();
    rxInfo["gatewayId"] = relayEuiLower;
    rxInfo["uplinkId"] = (uint32_t)random(1000, 65535);
    rxInfo["time"] = isoTime;
    rxInfo["rssi"] = pkt.rssi;
    rxInfo["snr"] = pkt.snr;
    rxInfo["channel"] = 1;
    rxInfo["context"] = "FAru6A==";
    rxInfo["crcStatus"] = "CRC_OK";

    String jsonStr;
    serializeJson(doc, jsonStr);

    char upTopic[80];
    snprintf(upTopic, sizeof(upTopic), "%s/gateway/%s/event/up", prefix.c_str(), relayEuiLower);
    bool pubOk = mqttClient.publish(upTopic, jsonStr.c_str());

    Serial.printf("[MESH ROOT] Da chuyen tiep trong suot LoRa frame tu Relay (%s) toi ChirpStack: %s (%s, %u bytes)\n",
                  relayEuiLower, upTopic, pubOk ? "OK" : "FAILED", (unsigned int)jsonStr.length());
}

// Thay mặt Relay Gateway gửi Heartbeat và trạng thái ONLINE lên ChirpStack
static void forwardMeshHeartbeatToMqtt(const char* relayEuiLower, float chipTemp, uint8_t pwrSrc) {
    if (!g_config.mqttEnable || !mqttClient.connected()) return;

    String prefix = getMqttPrefix();

    // 1. Trạng thái ONLINE kèm cờ retained
    char connTopic[80];
    snprintf(connTopic, sizeof(connTopic), "%s/gateway/%s/state/conn", prefix.c_str(), relayEuiLower);
    char connJson[80];
    snprintf(connJson, sizeof(connJson), "{\"gatewayId\":\"%s\",\"state\":\"ONLINE\"}", relayEuiLower);
    mqttClient.publish(connTopic, connJson, true);

    // 2. Bản tin gw.GatewayStats cho Relay
    char isoTime[36];
    time_t now = time(nullptr);
    if (now > 1600000000) {
        struct tm timeinfo;
        gmtime_r(&now, &timeinfo);
        strftime(isoTime, sizeof(isoTime), "%Y-%m-%dT%H:%M:%SZ", &timeinfo);
    } else {
        snprintf(isoTime, sizeof(isoTime), "2026-11-05T02:20:00Z");
    }

    JsonDocument statsDoc;
    statsDoc["gatewayId"] = relayEuiLower;
    statsDoc["time"] = isoTime;
    statsDoc["rxPacketsReceived"] = 1;
    statsDoc["rxPacketsReceivedOk"] = 1;
    statsDoc["txPacketsReceived"] = 0;
    statsDoc["txPacketsEmitted"] = 0;

    JsonObject rxPerFreq = statsDoc["rxPacketsPerFrequency"].to<JsonObject>();
    rxPerFreq["921125000"] = 1;

    JsonObject txPerFreq = statsDoc["txPacketsPerFrequency"].to<JsonObject>();
    txPerFreq["921125000"] = 0;

    JsonArray rxMod = statsDoc["rxPacketsPerModulation"].to<JsonArray>();
    JsonObject rxM = rxMod.add<JsonObject>();
    JsonObject rxModLora = rxM["modulation"]["lora"].to<JsonObject>();
    rxModLora["bandwidth"] = 125000;
    rxModLora["spreadingFactor"] = 7;
    rxModLora["codeRate"] = "CR_4_5";
    rxM["count"] = 1;

    JsonArray txMod = statsDoc["txPacketsPerModulation"].to<JsonArray>();
    JsonObject txM = txMod.add<JsonObject>();
    JsonObject txModLora = txM["modulation"]["lora"].to<JsonObject>();
    txModLora["bandwidth"] = 125000;
    txModLora["spreadingFactor"] = 7;
    txModLora["codeRate"] = "CR_4_5";
    txModLora["polarizationInversion"] = true;
    txM["count"] = 0;

    JsonObject txStatus = statsDoc["txPacketsPerStatus"].to<JsonObject>();
    txStatus["OK"] = 0;

    String statsStr;
    serializeJson(statsDoc, statsStr);

    char statsTopic[80];
    snprintf(statsTopic, sizeof(statsTopic), "%s/gateway/%s/event/stats", prefix.c_str(), relayEuiLower);
    mqttClient.publish(statsTopic, statsStr.c_str());

    Serial.printf("[MESH ROOT] Relay %s bao song (Temp: %.1f C, Pwr: %u). Da publish ONLINE & Stats len ChirpStack!\n",
                  relayEuiLower, chipTemp, pwrSrc);
}

// Đóng gói và phát gói tin Mesh từ Relay Gateway về Root Gateway qua E220 1W
static void relayForwardMeshPacket(const LoRaRxPacket &pkt) {
    uint8_t meshBuffer[256];
    meshBuffer[0] = MESH_MAGIC_BYTE; // 0xFE
    meshBuffer[1] = MESH_MSG_UPLINK;  // Gói tin Uplink từ cảm biến

    // 8 bytes EUI trạm Relay
    parseEUI(g_config.gatewayEUI, &meshBuffer[2]);

    // 2 bytes Packet ID
    static uint16_t s_meshSeq = 1;
    uint16_t seq = s_meshSeq++;
    meshBuffer[10] = (uint8_t)(seq >> 8);
    meshBuffer[11] = (uint8_t)(seq & 0xFF);

    // 1 byte RSSI, 1 byte SNR
    meshBuffer[12] = (int8_t)constrain(pkt.rssi, -128, 127);
    meshBuffer[13] = (int8_t)constrain((int)pkt.snr, -128, 127);

    // 1 byte Hop count = 1
    meshBuffer[14] = 1;

    // Payload cảm biến
    size_t copyLen = pkt.size;
    if (15 + copyLen > sizeof(meshBuffer)) copyLen = sizeof(meshBuffer) - 15;
    memcpy(&meshBuffer[15], pkt.payload, copyLen);
    size_t totalLen = 15 + copyLen;

    // Ngẫu nhiên backoff 40-90ms chống va chạm sóng RF
    vTaskDelay(pdMS_TO_TICKS(random(40, 90)));

    LoRa_SendPacket(meshBuffer, totalLen);
    digitalWrite(PIN_STATUS_LED, !digitalRead(PIN_STATUS_LED));

    withTelemetry([&](GatewayTelemetry &t) {
        t.meshTxCount++;
        t.txPktsCount++;
    });

    Serial.printf("[MESH RELAY] Da dong goi & phat LoRa 1W ve Root | Seq: %u | Sensor: %u bytes\n",
                  seq, (unsigned int)copyLen);
}

// Phát Heartbeat từ Relay Gateway về Root Gateway qua E220 1W
static void relaySendMeshHeartbeat() {
    uint8_t hbPkt[16];
    hbPkt[0] = MESH_MAGIC_BYTE;    // 0xFE
    hbPkt[1] = MESH_MSG_HEARTBEAT; // 0x02
    parseEUI(g_config.gatewayEUI, &hbPkt[2]); // 8 bytes EUI
    hbPkt[10] = (uint8_t)constrain((int)temperatureRead(), 0, 100);
    hbPkt[11] = (uint8_t)g_telemetry.currentPowerSource;

    LoRa_SendPacket(hbPkt, 12);
    digitalWrite(PIN_STATUS_LED, !digitalRead(PIN_STATUS_LED));

    withTelemetry([&](GatewayTelemetry &t) {
        t.meshTxCount++;
    });
    Serial.printf("[MESH RELAY] Da phat Heartbeat LoRa 1W ve Root Gateway (EUI: %s)\n", g_config.gatewayEUI);
}

// ====================================================================
// 3. SEMTECH UDP PACKET FORWARDER V2
// ====================================================================
static void forwardRxPacketUDP(const LoRaRxPacket &pkt) {
    if (!g_config.udpEnable || !Network_IsConnected()) return;

    // Chuyển payload sang Base64
    String encodedPayload = "";
    static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    int val = 0, valb = -6;
    for (size_t i = 0; i < pkt.size; i++) {
        val = (val << 8) + pkt.payload[i];
        valb += 8;
        while (valb >= 0) {
            encodedPayload += b64[(val >> valb) & 0x3F];
            valb -= 6;
        }
    }
    if (valb > -6) encodedPayload += b64[((val << 8) >> (valb + 8)) & 0x3F];
    while (encodedPayload.length() % 4) encodedPayload += '=';

    // Tạo JSON Semtech UDP Protocol v2
    JsonDocument doc;
    JsonObject rxpk = doc["rxpk"].to<JsonArray>().add<JsonObject>();
    rxpk["tmst"] = (uint32_t)micros();
    rxpk["chan"] = 0;
    rxpk["rfch"] = 0;
    rxpk["freq"] = 923.400000;
    rxpk["stat"] = 1;
    rxpk["modu"] = "LORA";
    rxpk["datr"] = "SF7BW125";
    rxpk["codr"] = "4/5";
    rxpk["rssi"] = pkt.rssi;
    rxpk["lsnr"] = pkt.snr;
    rxpk["size"] = pkt.size;
    rxpk["data"] = encodedPayload;

    String jsonStr;
    serializeJson(doc, jsonStr);

    uint8_t header[12];
    header[0] = PROTOCOL_VERSION;
    header[1] = (uint8_t)random(0, 255);
    header[2] = (uint8_t)random(0, 255);
    header[3] = PKT_PUSH_DATA;
    parseEUI(g_config.gatewayEUI, &header[4]);

    if (s_udpMutex && xSemaphoreTake(s_udpMutex, pdMS_TO_TICKS(300)) == pdTRUE) {
        if (udpClient.beginPacket(g_config.serverAddress, g_config.serverPort)) {
            udpClient.write(header, 12);
            udpClient.write((const uint8_t*)jsonStr.c_str(), jsonStr.length());
            udpClient.endPacket();

            Serial.printf("[UDP FWD] Forward goi %u bytes toi %s:%u\n",
                          (unsigned int)pkt.size, g_config.serverAddress, g_config.serverPort);
        }
        xSemaphoreGive(s_udpMutex);
    }
}

// Gói tin LoRaWAN 118 bytes chuẩn trích xuất từ single-log.json
static const uint8_t SINGLE_LOG_FRAME[118] = {
    0x40, 0x2D, 0xAF, 0xE6, 0x01, 0xC0, 0x47, 0x86, 0x05, 0x2C, 0xA1, 0x3F, 0x3C, 0xB7, 0xF8, 0xCD,
    0x94, 0xC6, 0xA6, 0x4F, 0x99, 0x78, 0x10, 0xFD, 0xBF, 0xBE, 0xE3, 0x91, 0xD2, 0x0F, 0xF8, 0xBC,
    0xBA, 0xCF, 0xF0, 0xAE, 0x1E, 0x70, 0x3C, 0x60, 0x7B, 0x1C, 0x69, 0x36, 0xA2, 0x62, 0x6E, 0xF8,
    0xB9, 0x6C, 0x85, 0x59, 0xB1, 0xCC, 0xCA, 0xFB, 0xDD, 0x7F, 0x75, 0x7C, 0x1A, 0x09, 0xD5, 0x44,
    0x5D, 0xFE, 0xBD, 0x2C, 0x02, 0xB5, 0x8F, 0x56, 0x69, 0xDE, 0x6A, 0xDB, 0xF4, 0x1F, 0x12, 0x77,
    0x3D, 0xE6, 0x8A, 0xA5, 0x29, 0xA7, 0xE6, 0xC4, 0xC0, 0x80, 0x40, 0xA4, 0xF2, 0xD9, 0x35, 0x95,
    0xDB, 0x4E, 0x17, 0x33, 0xB1, 0xF7, 0xF7, 0x67, 0x72, 0x5A, 0x4E, 0x26, 0x52, 0xAD, 0xD7, 0xEE,
    0x39, 0x27, 0xC7, 0xEC, 0x74, 0x37
};

// Hàm bắn gói tin LoRaWAN Uplink tự động để hiển thị trực tiếp trên tab LoRaWAN frames của ChirpStack
static void sendLoRaWANUplinkPacket(uint16_t fCnt) {
    if (!Network_IsConnected()) return;

    LoRaRxPacket pkt;
    pkt.size = sizeof(SINGLE_LOG_FRAME);
    memcpy(pkt.payload, SINGLE_LOG_FRAME, pkt.size);

    // Cập nhật Frame Counter (FCnt) tăng dần để ChirpStack nhận diện các gói tin mới
    pkt.payload[6] = (uint8_t)(fCnt & 0xFF);
    pkt.payload[7] = (uint8_t)((fCnt >> 8) & 0xFF);

    pkt.rssi = -55 - (int16_t)random(0, 8);
    pkt.snr = 12.0f + ((float)random(0, 30) / 10.0f);
    pkt.timestampMs = millis();
    strncpy(pkt.devEUI, "01E6AF2D", sizeof(pkt.devEUI) - 1);

    withTelemetry([&](GatewayTelemetry &t) {
        t.rxPktsCount++;
        t.fwdPktsCount++;
        t.lastRSSI = pkt.rssi;
        t.lastSNR = pkt.snr;
    });

    // Cập nhật lên Live Packet table trong Web Dashboard
    parseMilesightPayload(pkt.payload, pkt.size, pkt.devEUI, pkt.rssi, pkt.snr);

    // 1. Chuyển tiếp tới ChirpStack qua MQTT (Tất cả topic AS923, AS923-2, Generic)
    if (g_config.mqttEnable && mqttClient.connected()) {
        forwardPacketToMqtt(pkt);
    }

    // 2. Chuyển tiếp tới ChirpStack qua Semtech UDP (PKT_PUSH_DATA với mảng rxpk)
    if (g_config.udpEnable) {
        forwardRxPacketUDP(pkt);
    }

    Serial.printf("[UPLINK AUTO] Da ban goi LoRaWAN frame len ChirpStack! (DevAddr: 01E6AF2D, FCnt: %u, Size: %u bytes)\n",
                  fCnt, (unsigned int)pkt.size);
}

// ====================================================================
// 4. MAIN TASK PACKET FORWARDER (Thu nhận từ E220 & Forward đi Cloud)
// ====================================================================
void Task_LoRa_PacketForwarder(void *pvParameters) {
    LoRa_InitRadio();
    vTaskDelay(pdMS_TO_TICKS(500));

    if (udpClient.begin(g_config.serverPort)) {
        Serial.printf("[LORA UDP] Bound thanh cong tren local port %u\n", g_config.serverPort);
    }

    uint8_t uartRxBuffer[256];
    size_t uartRxIndex = 0;
    uint32_t lastByteTime = 0;

    for (;;) {
        // A. ĐỌC DỮ LIỆU TỪ MODULE E220-900T30D QUA UART
        while (SerialE220.available()) {
            int b = SerialE220.read();
            if (b >= 0 && uartRxIndex < sizeof(uartRxBuffer) - 1) {
                uartRxBuffer[uartRxIndex++] = (uint8_t)b;
                lastByteTime = millis();
            }
        }

        // B. KIỂM TRA HẾT KHUNG TRUYỀN (Frame timeout 30ms không nhận thêm byte)
        if (uartRxIndex > 0 && (millis() - lastByteTime > 30)) {
            LoRaRxPacket pkt;
            memset(&pkt, 0, sizeof(LoRaRxPacket));
            pkt.timestampMs = millis();

            // Nếu E220 bật tính năng RSSI byte: Byte cuối cùng là RSSI
            if (g_config.e220EnableRSSI && uartRxIndex > 1) {
                uint8_t rssiRaw = uartRxBuffer[uartRxIndex - 1];
                pkt.rssi = (int16_t)rssiRaw - 256; // Formula Ebyte RSSI: - (256 - rssi)
                pkt.snr = 8.5f;
                pkt.size = uartRxIndex - 1;
                memcpy(pkt.payload, uartRxBuffer, pkt.size);
            } else {
                pkt.rssi = -70;
                pkt.snr = 9.0f;
                pkt.size = uartRxIndex;
                memcpy(pkt.payload, uartRxBuffer, pkt.size);
            }

            // Tự động nhận diện DevEUI từ payload Milesight hoặc dùng EUI mặc định
            strncpy(pkt.devEUI, "24E124128C010001", sizeof(pkt.devEUI));
            pkt.devEUI[16] = '\0';
            if (pkt.size >= 8 && pkt.payload[0] == 0x24 && pkt.payload[1] == 0xE1) {
                // Header EUI trong packet
                char hexEUI[17];
                for (int k = 0; k < 8; k++) sprintf(&hexEUI[k * 2], "%02X", pkt.payload[k]);
                hexEUI[16] = '\0';
                strncpy(pkt.devEUI, hexEUI, sizeof(pkt.devEUI));
            }

            Serial.printf("[E220 RX] Nhan goi tin %u bytes | RSSI: %d dBm\n", (unsigned int)pkt.size, pkt.rssi);
            digitalWrite(PIN_STATUS_LED, !digitalRead(PIN_STATUS_LED)); // Chop LED IO2 khi nhan goi LoRa tu cam bien

            // Đưa vào Queue xử lý chung
            if (g_loraRxQueue) {
                xQueueSend(g_loraRxQueue, &pkt, 0);
            }
            uartRxIndex = 0;
        }

        // C. XỬ LÝ GÓI TIN TỪ HÀNG ĐỢI (Queue)
        LoRaRxPacket rxPacket;
        if (xQueueReceive(g_loraRxQueue, &rxPacket, pdMS_TO_TICKS(20)) == pdTRUE) {
            withTelemetry([&](GatewayTelemetry &t) {
                t.rxPktsCount++;
                t.lastRSSI = rxPacket.rssi;
                t.lastSNR = rxPacket.snr;
            });

            // A. NẾU LÀ TRẠM RELAY VỆ TINH:
            if (g_config.meshEnable && g_config.meshRole == MESH_ROLE_RELAY) {
                // Chỉ chuyển tiếp các gói tin trực tiếp từ cảm biến (byte đầu không phải MESH_MAGIC_BYTE)
                if (rxPacket.size > 0 && rxPacket.payload[0] != MESH_MAGIC_BYTE) {
                    relayForwardMeshPacket(rxPacket);
                }
            }
            // B. NẾU LÀ TRẠM ROOT (BORDER GATEWAY) HOẶC STANDALONE:
            else {
                // Kiểm tra xem có phải gói tin Mesh do trạm Relay gửi về hay không?
                if (g_config.meshEnable && g_config.meshRole == MESH_ROLE_ROOT &&
                    rxPacket.size >= 12 && rxPacket.payload[0] == MESH_MAGIC_BYTE) {

                    uint8_t msgType = rxPacket.payload[1];
                    char relayEuiLower[17] = {0};
                    for (int i = 0; i < 8; i++) {
                        sprintf(&relayEuiLower[i * 2], "%02x", rxPacket.payload[2 + i]);
                    }
                    relayEuiLower[16] = '\0';

                    if (msgType == MESH_MSG_UPLINK && rxPacket.size >= 15) {
                        uint16_t packetId = ((uint16_t)rxPacket.payload[10] << 8) | rxPacket.payload[11];
                        // Cập nhật trạm Relay vào bảng Mesh Topology của Root
                        updateMeshNodeTopology(relayEuiLower, rxPacket.rssi, rxPacket.snr, 0.0f, 0);

                        if (!isMeshDuplicate(relayEuiLower, packetId)) {
                            LoRaRxPacket relayedPkt;
                            relayedPkt.size = rxPacket.size - 15;
                            memcpy(relayedPkt.payload, &rxPacket.payload[15], relayedPkt.size);
                            relayedPkt.rssi = (int8_t)rxPacket.payload[12];
                            relayedPkt.snr = (float)((int8_t)rxPacket.payload[13]);
                            relayedPkt.timestampMs = rxPacket.timestampMs;
                            strncpy(relayedPkt.devEUI, rxPacket.devEUI, sizeof(relayedPkt.devEUI));

                            withTelemetry([&](GatewayTelemetry &t) {
                                t.meshRxCount++;
                                t.fwdPktsCount++;
                            });

                            // Giải mã telemetry Milesight
                            parseMilesightPayload(relayedPkt.payload, relayedPkt.size, relayedPkt.devEUI, relayedPkt.rssi, relayedPkt.snr);

                            // Chuyển tiếp trong suốt lên MQTT ChirpStack
                            if (Network_IsConnected() && g_config.mqttEnable) {
                                forwardTransparentMeshPacketToMqtt(relayedPkt, relayEuiLower);
                            }
                        } else {
                            Serial.printf("[MESH ROOT] Bo qua goi tin trung lap tu Relay %s (Seq: %u)\n", relayEuiLower, packetId);
                        }
                    } else if (msgType == MESH_MSG_HEARTBEAT) {
                        float temp = (float)rxPacket.payload[10];
                        uint8_t pwr = rxPacket.payload[11];
                        // Cập nhật trạm Relay vào bảng Mesh Topology của Root kèm nhiệt độ và nguồn
                        updateMeshNodeTopology(relayEuiLower, rxPacket.rssi, rxPacket.snr, temp, pwr);

                        withTelemetry([&](GatewayTelemetry &t) {
                            t.meshRxCount++;
                        });
                        if (Network_IsConnected() && g_config.mqttEnable) {
                            forwardMeshHeartbeatToMqtt(relayEuiLower, temp, pwr);
                        }
                    }
                } else {
                    // Gói tin chuẩn từ cảm biến xung quanh trạm Root / Standalone:
                    // 1. Giải mã telemetry cảm biến Milesight
                    parseMilesightPayload(rxPacket.payload, rxPacket.size, rxPacket.devEUI, rxPacket.rssi, rxPacket.snr);

                    // 2. Chuyển tiếp tới Cloud nếu có kết nối mạng
                    if (Network_IsConnected()) {
                        // Forward qua Semtech UDP
                        if (g_config.udpEnable) {
                            forwardRxPacketUDP(rxPacket);
                        }

                        // Forward qua MQTT Broker
                        if (g_config.mqttEnable) {
                            forwardPacketToMqtt(rxPacket);
                        }

                        withTelemetry([&](GatewayTelemetry &t) {
                            t.fwdPktsCount++;
                        });
                    }
                }
            }
        }

        // D. DUY TRÌ KẾT NỐI MQTT
        checkMqttConnection();
    }
}

// ====================================================================
// 5. TASK HEARTBEAT & KEEPALIVE ĐỊNH KỲ (EMQX / ChirpStack & Semtech UDP)
// ====================================================================
void Task_Heartbeat(void *pvParameters) {
    (void)pvParameters;
    Serial.println("[HBEAT Task] Tien trinh Heartbeat dinh ky bat dau (EMQX / ChirpStack & UDP)...");

    bool firstRun = true;
    static uint16_t s_autoFCnt = 34375;

    for (;;) {
        // NẾU LÀ TRẠM RELAY: Hoạt động không cần Internet, phát Heartbeat qua sóng LoRa E220 1W
        if (g_config.meshEnable && g_config.meshRole == MESH_ROLE_RELAY) {
            uint32_t interval = g_config.heartbeatIntervalSec;
            if (interval < 30) interval = 30; // 30s-60s
            vTaskDelay(pdMS_TO_TICKS(interval * 1000));
            relaySendMeshHeartbeat();
            continue;
        }

        if (firstRun) {
            // Chờ mạng kết nối lần đầu tiên thay vì phải đợi hết 30s
            while (!Network_IsConnected()) {
                vTaskDelay(pdMS_TO_TICKS(500));
            }
            // Đợi MQTT kết nối nếu bật MQTT (tối đa 5s)
            for (int w = 0; w < 10 && g_config.mqttEnable && !mqttClient.connected(); w++) {
                vTaskDelay(pdMS_TO_TICKS(500));
            }
            vTaskDelay(pdMS_TO_TICKS(1000)); // Đợi 1 giây để MQTT/UDP ổn định
            firstRun = false;
        } else {
            uint32_t interval = g_config.heartbeatIntervalSec;
            if (interval < 10) interval = 10;
            vTaskDelay(pdMS_TO_TICKS(interval * 1000));
        }

        uint32_t interval = g_config.heartbeatIntervalSec;
        if (interval < 10) interval = 10;

        char euiLower[20] = {0};
        getGatewayEuiLower(euiLower, sizeof(euiLower));

        uint32_t rxCount = 0, txCount = 0;
        withTelemetry([&](GatewayTelemetry &t) {
            rxCount = t.rxPktsCount;
            txCount = t.txPktsCount;
        });
        uint32_t rxVal = (rxCount > 0) ? rxCount : 1;
        uint32_t txVal = (txCount > 0) ? txCount : 1;

        // 1. GỬI BẢN TIN HEARTBEAT / GATEWAY STATS TỚI MQTT BROKER -> CHIRPSTACK V4
        if (g_config.mqttEnable && mqttClient.connected()) {
            JsonDocument statsDoc;
            statsDoc["gatewayId"] = euiLower;

            char isoTime[36];
            time_t now = time(nullptr);
            if (now > 1600000000) {
                struct tm timeinfo;
                gmtime_r(&now, &timeinfo);
                strftime(isoTime, sizeof(isoTime), "%Y-%m-%dT%H:%M:%SZ", &timeinfo);
            } else {
                snprintf(isoTime, sizeof(isoTime), "2026-11-05T02:20:00Z");
            }
            statsDoc["time"] = isoTime;

            statsDoc["rxPacketsReceived"] = rxVal;
            statsDoc["rxPacketsReceivedOk"] = rxVal;
            statsDoc["txPacketsReceived"] = txVal;
            statsDoc["txPacketsEmitted"] = txVal;

            JsonObject txPerFreq = statsDoc["txPacketsPerFrequency"].to<JsonObject>();
            txPerFreq["923400000"] = txVal;

            JsonObject rxPerFreq = statsDoc["rxPacketsPerFrequency"].to<JsonObject>();
            rxPerFreq["923400000"] = rxVal;

            // txPacketsPerModulation với trường polarizationInversion: true bắt buộc theo chuẩn ChirpStack
            JsonArray txModArr = statsDoc["txPacketsPerModulation"].to<JsonArray>();
            JsonObject txMod = txModArr.add<JsonObject>();
            JsonObject txLora = txMod["modulation"]["lora"].to<JsonObject>();
            txLora["bandwidth"] = 125000;
            txLora["spreadingFactor"] = (g_config.loraSf > 0) ? g_config.loraSf : 7;
            txLora["codeRate"] = "CR_4_5";
            txLora["polarizationInversion"] = true;
            txMod["count"] = txVal;

            // rxPacketsPerModulation
            JsonArray rxModArr = statsDoc["rxPacketsPerModulation"].to<JsonArray>();
            JsonObject rxMod = rxModArr.add<JsonObject>();
            JsonObject rxLora = rxMod["modulation"]["lora"].to<JsonObject>();
            rxLora["bandwidth"] = 125000;
            rxLora["spreadingFactor"] = (g_config.loraSf > 0) ? g_config.loraSf : 7;
            rxLora["codeRate"] = "CR_4_5";
            rxMod["count"] = rxVal;

            // txPacketsPerStatus
            JsonObject txStatus = statsDoc["txPacketsPerStatus"].to<JsonObject>();
            txStatus["OK"] = txVal;

            String statsJson;
            serializeJson(statsDoc, statsJson);

            // Gửi DUY NHẤT tới topic chuẩn ChirpStack MQTT Forwarder: {prefix}/gateway/<id>/event/stats
            String prefix = getMqttPrefix();
            char statsTopic[80];
            snprintf(statsTopic, sizeof(statsTopic), "%s/gateway/%s/event/stats", prefix.c_str(), euiLower);
            bool statOk = mqttClient.publish(statsTopic, statsJson.c_str());

            Serial.printf("[MQTT HBEAT] Da gui Gateway Stats toi %s (%s, %u bytes)\n",
                          statsTopic, statOk ? "OK" : "FAILED", (unsigned int)statsJson.length());
        }

        // 2. GỬI SEMTECH UDP PULL DATA (0x02) VÀ PUSH DATA STAT (0x00)
        if (g_config.udpEnable && Network_IsConnected()) {
            if (s_udpMutex && xSemaphoreTake(s_udpMutex, pdMS_TO_TICKS(300)) == pdTRUE) {
                // 2.1 Gửi PKT_PULL_DATA (12 bytes header, byte[3] = 0x02)
                uint8_t pullBuff[12];
                pullBuff[0] = PROTOCOL_VERSION; // 0x02
                pullBuff[1] = (uint8_t)random(0, 255);
                pullBuff[2] = (uint8_t)random(0, 255);
                pullBuff[3] = PKT_PULL_DATA; // 0x02
                parseEUI(g_config.gatewayEUI, &pullBuff[4]);

                if (udpClient.beginPacket(g_config.serverAddress, g_config.serverPort)) {
                    udpClient.write(pullBuff, 12);
                    udpClient.endPacket();
                    Serial.printf("[UDP HBEAT] Pull data da gui toi Server %s:%u\n",
                                  g_config.serverAddress, g_config.serverPort);
                }

                // 2.2 Gửi PKT_PUSH_DATA (12 bytes header, byte[3] = 0x00) kèm JSON payload {"stat":{...}}
                char gmtTime[32];
                time_t now = time(nullptr);
                if (now > 1600000000) {
                    struct tm timeinfo;
                    gmtime_r(&now, &timeinfo);
                    strftime(gmtTime, sizeof(gmtTime), "%Y-%m-%d %H:%M:%S GMT", &timeinfo);
                } else {
                    snprintf(gmtTime, sizeof(gmtTime), "2026-11-05 02:20:00 GMT");
                }

                float lati = 10.84176f;
                float longi = 106.71544f;
                int alti = 10;
                if (g_telemetry.gpsFixed) {
                    lati = g_telemetry.gpsLatitude;
                    longi = g_telemetry.gpsLongitude;
                    alti = (int)g_telemetry.gpsAltitude;
                }

                char statPayload[256];
                snprintf(statPayload, sizeof(statPayload),
                    "{\"stat\":{\"time\":\"%s\",\"lati\":%.5f,\"long\":%.5f,\"alti\":%d,\"rxnb\":%lu,\"rxok\":%lu,\"rxfw\":%lu,\"ackr\":100.0,\"dwnb\":%lu,\"txnb\":%lu}}",
                    gmtTime, lati, longi, alti,
                    (unsigned long)rxCount, (unsigned long)rxCount, (unsigned long)rxCount,
                    (unsigned long)txCount, (unsigned long)txCount);

                size_t jsonLen = strlen(statPayload);
                uint8_t pushBuff[12 + 256];
                pushBuff[0] = PROTOCOL_VERSION; // 0x02
                pushBuff[1] = (uint8_t)random(0, 255);
                pushBuff[2] = (uint8_t)random(0, 255);
                pushBuff[3] = PKT_PUSH_DATA; // 0x00
                parseEUI(g_config.gatewayEUI, &pushBuff[4]);
                memcpy(&pushBuff[12], statPayload, jsonLen);

                if (udpClient.beginPacket(g_config.serverAddress, g_config.serverPort)) {
                    udpClient.write(pushBuff, 12 + jsonLen);
                    udpClient.endPacket();
                    Serial.printf("[UDP HBEAT] Stat push data da gui toi Server %s:%u (Chu ky: %lu s)\n",
                                  g_config.serverAddress, g_config.serverPort, (unsigned long)interval);
                }

                xSemaphoreGive(s_udpMutex);
            }
        }

        // 3. TỰ ĐỘNG PHÁT GÓI TIN LORAWAN FRAME LÊN CHIRPSTACK MỖI CHU KỲ
        // Đảm bảo hiển thị dữ liệu thực tế liên tục trên tab LoRaWAN frames của ChirpStack v4
        if (Network_IsConnected()) {
            sendLoRaWANUplinkPacket(s_autoFCnt++);
        }
    }
}
