#include <painlessMesh.h>
#include <PubSubClient.h>
#include <Preferences.h>
#include <SPIFFS.h>
#include <time.h>

// Konstanta Mesh
#define MESH_PREFIX       "SaklarPintarMeshNet"
#define MESH_PASSWORD     "SaKlarPinTarmESHnET"
#define MESH_PORT         5555
#define HOSTNAME          "MESH_NET_Bridge"

// Konstanta Sensor/Aktuator/Indikator
#define TOUCH_INPIN       3 //4
#define RELAY_OUTPIN      1 //14
#define TOUCH_INDICATOR   6 //26
#define NET_INDICATOR     7 //32

// Konstanta Preferences
#define WIFI_SSID         "wfSSID"
#define WIFI_PASS         "wfPASS"
#define MESH_CHANNEL      "meshCH"

// Konstanta SPIFF for Scheduling/Timer
#define SCHEDULEJSON      "/schedule.json"

// Konstanta Broadcast/Single
#define GET_GATEWAY       "getGateway"
#define SEND_STATES       "sendStates"
#define SEND_RSSIS        "sendRSSIS"

// Konstanta MQTT
// Publish
#define ROOTTOPIC         "/gateway/"           // root topic nanti ditambahkan dengan mac wifi esp32 agar unik (ex. dc:4f:22:6e:83:23/gateway/), ambil dari variable rootTopicMACAddr
#define PUBLISHNODESTOPIC ROOTTOPIC "nodes/"
#define PUBLISHMSGTOPIC   ROOTTOPIC "msg"
#define NODEROLE          "/role"
#define NODESTATUS        "/status"             // status switchnya / state switchnya
#define NODERSSI          "/rssi"
// Subcribe
#define SUBSCRIBETOPIC    ROOTTOPIC "controls/"
#define SUBSREQUEST       "request"             // request            -> getNodes/getRSSI;
#define SUBSREQNODE       "request/getNode"     // request data node  -> <id-node>;
#define SUBSBROADCAST     "broadcast"           // broadcast          -> msg;
#define SUBSSETSTATE      "set"                 // <id-node>/set      -> status saklar;
#define SUBSSETSCHEDULE   "schedule"            // <id-node>/schedule -> JSON schedule;

// Lokal MQTT Broker
const char* mqtt_broker   = "192.168.137.1";
const int   mqtt_port     = 1883;

// Inisialisasi library
painlessMesh  mesh;
WiFiClient    wifiClient;
PubSubClient  mqttClient(wifiClient);
Preferences   preferences;
Scheduler     userScheduler;

//// Konstanta Global
// WiFi and Mesh
String rootTopicMACAddr = "";                       // Variabel bantu untuk root topic agar unik dengan menambahkan mac wifi esp32
SimpleList<uint32_t> nodes;
bool ap_connected = false;
// Smartconfig
String wifi_ssid, wifi_pass;
int wifi_channel = 0;
// Saklar status
bool relay_state = LOW;                             // Status terkini relay
bool last_touch_state;                              // Status terakhir touch sensor
bool current_touch_state;                           // Status terkini touch sensor
// Reset Device and Touch sensor
bool is_reset_indicator = false;
int reset_indicator_count = 0;
unsigned long last_reset_blinking_time = 0;         // Waktu sebelumnya untuk berkedip
const unsigned long reset_indicator_interval = 500; // Interval kedip LED
bool can_reset_device = true;
const unsigned long reset_device_expired = 300000;  // Durasi untuk reset device sejak perangkat dinyalakan (5 menit)(Access Point butuh waktu 2 menit untuk menyala)
unsigned long touch_start_reset = 0;                // Waktu saat sentuhan dimulai untuk melakukan reset device
const unsigned long touch_reset_duration = 5000;    // Durasi tombol ditekan untuk reset (5 detik)
// MQTT Status Non-Bloking Loop untuk mempermudah saat reset (karena jika tidak tersambung ke MQTT berarti tidak tersambung ke Access Point atau Internet)
unsigned long last_mqtt_attempt = 0;
const unsigned long mqtt_retry_interval = 10000;    // 10 detik untuk interval retry mqtt saat reconnect
bool is_mqtt_connecting = false;
// Konfigurasi zona waktu Indonesia (WIB = UTC+7) untuk Scheduling/Timer
const char* ntp_server = "pool.ntp.org";
const long gmt_offset_sec = 7 * 3600;
const int daylight_offset_sec = 0;
String current_time = "00:00";
unsigned long last_check_time = 0;
const unsigned long interval_check_time = 60000;    // 1 menit untuk interval cek waktu ntp
String last_activated_time = "";                    // Variabel penanda apakah lampu sudah dinyalakan untuk waktu itu

//// Fungsi prototipe
// Mesh Callback
void receivedCallback(const uint32_t &from, const String &msg);
void newConnectionCallback(uint32_t nodeId);
void changedConnectionCallback();
void nodeTimeAdjustedCallback(int32_t offset);
void nodeDelayReceivedCallback(uint32_t nodeId, int32_t delay);
// Send Messages Mesh
void sendStateToNode(uint32_t toNode, bool state);
void sendScheduleToNode(uint32_t toNode, const JsonDocument& doc);
void sendGatewayId(uint32_t toNode);
void sendBroadcastMsg(String msg);
void sendSingleMsg(uint32_t toNode, String msg);
// MQTT Callback
void mqttCallback(char* topic, uint8_t* payload, unsigned int length);
void requestAndSendMeshDataToMQTT();
// System and Indicator
void startSmartConfig();
void scanWifiAndChannel();
void reconnectMQTT();
void handleResetIndicator();
void publishNodeStatus(uint32_t nodeId, bool state);
void publishRSSIStatus(uint32_t nodeId, int rssi);
void printLocalTime();
bool readJsonFile(const char* path, DynamicJsonDocument& doc);
bool writeJsonFile(const char* path, const JsonDocument& doc);
IPAddress getlocalIP();
IPAddress myIP(0, 0, 0, 0);


//// System and Indicator
// Fungsi untuk SmartConfig
void startSmartConfig() {
  // WiFi.mode(WIFI_AP_STA);
  Serial.print("Bridge: Memulai SmartConfig...");

  WiFi.beginSmartConfig();
  digitalWrite(NET_INDICATOR, HIGH);      // Nyalakan LED
  digitalWrite(TOUCH_INDICATOR, LOW);     // Matikan LED

  while (!WiFi.smartConfigDone()) {
    delay(500);
    Serial.print(".");

    // Indikator Blinking
    digitalWrite(NET_INDICATOR, !digitalRead(NET_INDICATOR));
    digitalWrite(TOUCH_INDICATOR, !digitalRead(TOUCH_INDICATOR));
  }

  // Matikan indikator setelah berhasil
  digitalWrite(NET_INDICATOR, LOW); // Matikan LED
  digitalWrite(TOUCH_INDICATOR, LOW); // Matikan LED

  wifi_ssid = WiFi.SSID();
  wifi_pass = WiFi.psk();
  wifi_channel = WiFi.channel();

  preferences.putString(WIFI_SSID, wifi_ssid);
  preferences.putString(WIFI_PASS, wifi_pass);
  preferences.putInt(MESH_CHANNEL, wifi_channel);

  Serial.println("\nBridge: SmartConfig berhasil!");
  Serial.printf("SSID: %-32.32s | CH  : %2ld\n", WiFi.SSID().c_str(), WiFi.channel());
  Serial.printf("IP  : %-32.32s | RSSI: %4ld\n", WiFi.localIP().toString().c_str(), WiFi.RSSI());
} // end startSmartConfig

// Fungsi untuk scanning dan memeriksa channel WiFi
void scanWifiAndChannel() {
  // WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);

  bool wifiFound = false;
  int newChannel = 0;

  const int maxRetries = 25;            // (25 percobaan x 6 detik delay = 2m-30d)(Access Point butuh waktu 2 menit untuk menyala)
  int attemptCount = 0;

  digitalWrite(NET_INDICATOR, LOW);     // Matikan LED

  while (!wifiFound && attemptCount < maxRetries) {
    attemptCount++;
    Serial.printf("Bridge: Memulai Scanning WiFi SSID '%s' untuk Gateway percobaan ke-%d ...\n", wifi_ssid, attemptCount);

    digitalWrite(NET_INDICATOR, !digitalRead(NET_INDICATOR));     // Indikator Net Blinking

    int numNetworks = WiFi.scanNetworks();
    Serial.print("Bridge: Scanning selesai. Hasil: ");

    if (numNetworks == 0) {
      Serial.println("No Networks Found!");
    }
    else {
      Serial.printf("%d Networks Found!\n", numNetworks);
      Serial.println("No | SSID                             | RSSI | CH |");
      for (int i = 0; i < numNetworks; ++i) {
        Serial.printf("%2d | %-32.32s | %4ld | %2ld | ", i + 1, WiFi.SSID(i).c_str(), WiFi.RSSI(i), WiFi.channel(i));

        if (WiFi.SSID(i) == wifi_ssid) {
          Serial.print(" -> Found WiFi SSID and Channel!");

          // Bandingkan dan Simpan channel wifi hasil scan jaringan dengan yang tersimpan pada preferences
          newChannel = WiFi.channel(i);
          wifiFound = true;

          // break; // Hentikan scanning setelah menemukan mesh network
        }

        Serial.println();
        delay(10);
      }
    }

    WiFi.scanDelete();
    delay(6000);
  }

  digitalWrite(NET_INDICATOR, LOW);     // Matikan LED

  if (wifiFound) {
    if (newChannel == wifi_channel) {
      Serial.printf("Bridge: Tetap menggunakan channel WiFi lama (CH-%d) untuk gateway.\n", wifi_channel);
    }
    else {
      wifi_channel = newChannel;
      preferences.putInt(MESH_CHANNEL, wifi_channel);
      Serial.printf("Bridge: Menggunakan channel WiFi baru (CH-%d) untuk gateway.\n", wifi_channel);
    }

    // Blink 5x Indikator Net
    for (int i = 0; i < 5; i++) {
      digitalWrite(NET_INDICATOR, LOW);
      delay(250);
      digitalWrite(NET_INDICATOR, HIGH);
      delay(750);
    }
  }
  else {
    Serial.println("Bridge: Gagal menemukan WiFi setelah 25 kali percobaan, user bisa melakukan reset device");
  }

  digitalWrite(NET_INDICATOR, LOW);     // Matikan LED
} // end scanWifiAndChannel

// Fungsi untuk reconnect MQTT
void reconnectMQTT() {
  is_mqtt_connecting = true;

  if (mqttClient.connected()) {
    is_mqtt_connecting = false;
    return;
  }

  if (is_mqtt_connecting && (unsigned long)millis() - last_mqtt_attempt >= mqtt_retry_interval) {
    uint8_t mac[6];
    esp_read_mac(mac, ESP_MAC_WIFI_STA);

    char client_id[13];
    sprintf(client_id, "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);

    Serial.println("Bridge: Connecting to MQTT...");
    digitalWrite(NET_INDICATOR, !digitalRead(NET_INDICATOR));

    if (mqttClient.connect(client_id)) {
      Serial.printf("Bridge: Connected to MQTT Broker with ID %s\n", client_id);
      is_mqtt_connecting = false;
      mqttClient.publish((rootTopicMACAddr + PUBLISHMSGTOPIC).c_str(), "Gateway ready!");
      mqttClient.subscribe((rootTopicMACAddr + SUBSCRIBETOPIC + "#").c_str());
      Serial.println("Subcribe pada topic " + rootTopicMACAddr + SUBSCRIBETOPIC + "#");

      // saat terkoneksi ke mqtt, kirimkan data mesh
      requestAndSendMeshDataToMQTT();

      // Blink 5x Indikator Net
      for (int i = 0; i < 5; i++) {
        digitalWrite(NET_INDICATOR, LOW);
        delay(250);
        digitalWrite(NET_INDICATOR, HIGH);
        delay(750);
      }
    }
    else {
      Serial.print("Gagal, kode kesalahan: ");
      Serial.println(mqttClient.state());
    }

    digitalWrite(NET_INDICATOR, LOW);     // Matikan LED
    last_mqtt_attempt = millis();
  }
} // end reconnectMQTT

// Fungsi untuk handle indikator pada perangkat berupa kedipan led saat Reset
void handleResetIndicator() {
  if (is_reset_indicator) {
    unsigned long current_millis = millis();

    if (current_millis - last_reset_blinking_time >= reset_indicator_interval) {
      last_reset_blinking_time = current_millis;

      // Toggle LED Reset Indicator
      digitalWrite(TOUCH_INDICATOR, !digitalRead(TOUCH_INDICATOR));
      digitalWrite(NET_INDICATOR, !digitalRead(NET_INDICATOR));

      reset_indicator_count++;

      // Stop blinking after 18 toggles (9 cycles of on and off)
      if (reset_indicator_count >= 18) {
        digitalWrite(TOUCH_INDICATOR, LOW);     // Matikan LED
        digitalWrite(NET_INDICATOR, LOW);       // Matikan LED
        is_reset_indicator = false;

        delay(1000);
        ESP.restart();    // Reset board
      }
    }
  }
} // end handleResetIndicator

// Fungsi menampilkan waktu lokal WIB
void printLocalTime() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    Serial.println("Bridge: Failed to obtain time");
    return;
  }
  char timeHour[3];
  strftime(timeHour, 3, "%H", &timeinfo);
  char timeMinute[3];
  strftime(timeMinute, 3, "%M", &timeinfo);
  current_time = String(timeHour) + ":" + String(timeMinute);
  Serial.printf("Bridge: Current time is %s\n", current_time);
}

// Fungsi untuk membaca file JSON dan mengembalikan sebagai JsonDocument
bool readJsonFile(const char* path, DynamicJsonDocument& doc) {
  Serial.printf("Bridge: Read file JSON: %s", path);
  File file = SPIFFS.open(path, "r");
  if (!file || file.isDirectory()) {
    Serial.println(" - Gagal membuka file");
    return false;
  }

  DeserializationError error = deserializeJson(doc, file);
  file.close();

  if (error) {
    Serial.print(" - Gagal parsing JSON: ");
    Serial.println(error.c_str());
    return false;
  }

  Serial.println(" - JSON berhasil dibaca");
  return true;
}

// Fungsi untuk menulis JSON ke file
bool writeJsonFile(const char* path, const JsonDocument& doc) {
  Serial.printf("Bridge: Write file JSON: %s", path);
  File file = SPIFFS.open(path, "w");
  if (!file) {
    Serial.println(" - Gagal membuka file untuk menulis");
    return false;
  }

  if (serializeJson(doc, file) == 0) {
    Serial.println(" - Gagal menulis JSON ke file");
    file.close();
    return false;
  }

  file.close();
  Serial.println(" - JSON berhasil ditulis ke file");
  return true;
}

// Fungsi mendapatkan IP lokal dari mesh
IPAddress getlocalIP() {
  return IPAddress(mesh.getStationIP());
} // end getlocalIP

// Fungsi helper untuk kirim status saklar pada node
void publishNodeStatus(uint32_t nodeId, bool state) {
  String topic = rootTopicMACAddr + PUBLISHNODESTOPIC + String(nodeId) + NODESTATUS;
  // String stateStr = relay_state ? "true" : "false";   // use true / false
  String stateStr = state ? "ON" : "OFF";       // use ON / OFF
  mqttClient.publish(topic.c_str(), stateStr.c_str());
} // end publishNodeStatus

// Fungsi helper untuk kirim signal strength / rssi pada node
void publishRSSIStatus(uint32_t nodeId, int rssi) {
  String topic = rootTopicMACAddr + PUBLISHNODESTOPIC + String(nodeId) + NODERSSI;
  String rssiStr = String(rssi) + "dBm";
  mqttClient.publish(topic.c_str(), rssiStr.c_str());
} // end publishRSSIStatus

// Fungsi helper untuk meminta data Mesh pada setiap node dan mengirimkan ke mqtt
void requestAndSendMeshDataToMQTT() {
  auto nodes = mesh.getNodeList(true);
  for (auto && id : nodes) {
    String node = String(id);
    String role =  (id == mesh.getNodeId()) ? "root" : "node";    // Root adalah node ini sendiri

    // Menggabungkan topik dan mengirim payload
    String topicRole = rootTopicMACAddr + PUBLISHNODESTOPIC + node + NODEROLE;
    mqttClient.publish(topicRole.c_str(), role.c_str());
  }

  // Kirim status saklar dan RSSI Root ke MQTT
  publishNodeStatus(mesh.getNodeId(), relay_state);
  int rootRSSI = WiFi.RSSI();
  publishRSSIStatus(mesh.getNodeId(), rootRSSI);

  sendBroadcastMsg(SEND_STATES);    // Meminta semua node untuk mengirimkan status saklar mereka
  sendBroadcastMsg(SEND_RSSIS);     // Meminta semua node untuk mengirimkan signal strength mereka
} // end requestAndSendMeshDataToMQTT

//// MQTT Callback
// Fungsi untuk menangani pesan MQTT
void mqttCallback(char* topic, uint8_t* payload, unsigned int length) {
  char* cleanPayload = (char*)malloc(length + 1);
  payload[length] = '\0';
  memcpy(cleanPayload, payload, length + 1);
  String msg = String(cleanPayload);
  free(cleanPayload);
  Serial.printf("Bridge: MQTT Received -> Topic: %s, Payload: %s\n", topic, msg.c_str());

  String topicStr = String(topic);
  String prefix = rootTopicMACAddr + SUBSCRIBETOPIC;
  String targetStr = topicStr.substring(prefix.length());

  // Subcribe topik dari client yang meminta data jaringan Mesh
  if (targetStr == SUBSREQUEST) {
    if (msg == "getNodes") {
      Serial.println("Bridge: MQTT client Request getNodes");
      requestAndSendMeshDataToMQTT();   // panggil fungsi untuk meminta semua data mesh dan kirim ke mqtt
    }
    else if (msg == "getRSSI") {
      Serial.println("Bridge: MQTT client request getRSSI");

      // Kirim RSSI Root
      int rootRSSI = WiFi.RSSI();
      publishRSSIStatus(mesh.getNodeId(), rootRSSI);

      sendBroadcastMsg(SEND_RSSIS);   // Meminta semua node untuk mengirimkan signal strength mereka
    }
  }
  else if (targetStr == SUBSREQNODE) {
    uint32_t targetNode = strtoul(msg.c_str(), NULL, 10);
    Serial.printf("Bridge: MQTT client Request getNode to %u\n", targetNode);

    if (mesh.isConnected(targetNode)) {
      sendSingleMsg(targetNode, SEND_STATES);    // Meminta target node untuk mengirimkan status saklar mereka
      sendSingleMsg(targetNode, SEND_RSSIS);     // Meminta target node untuk mengirimkan signal strength mereka
    }
    else if (targetNode == mesh.getNodeId()) {
      // Kirim status saklar dan RSSI Root ke MQTT
      publishNodeStatus(mesh.getNodeId(), relay_state);
      int rootRSSI = WiFi.RSSI();
      publishRSSIStatus(mesh.getNodeId(), rootRSSI);
    }
  }
  else if (targetStr == SUBSBROADCAST) {
    Serial.println("Bridge: MQTT client Broadcast messages");
    sendBroadcastMsg(msg);
  }
  else {
    // Subcribe topik status saklar untuk Node tertentu dari client
    uint32_t target = strtoul(targetStr.c_str(), NULL, 10);
    int slashIndex = targetStr.indexOf('/');
    String command = targetStr.substring(slashIndex + 1);

    if (command == SUBSSETSTATE) {
      Serial.println("Bridge: MQTT client Controls State");

      // bool state = (msg == "true" || msg == "1");    // use true / false
      bool state = (msg == "ON");     // use ON / OFF
      if (mesh.isConnected(target)) {
        sendStateToNode(target, state);
      }
      else if (target == mesh.getNodeId()) {
        relay_state = state;
        digitalWrite(RELAY_OUTPIN, relay_state);

        // Kirim status saklar ke MQTT
        publishNodeStatus(mesh.getNodeId(), relay_state);
        Serial.printf("Bridge: Change switch state to %s\n", state ? "ON" : "OFF");
      }
    }
    else if (command == SUBSSETSCHEDULE) {
      Serial.println("Bridge: MQTT client Controls Schedule");

      // Parsing JSON
      DynamicJsonDocument writeDoc(1024);
      DeserializationError err = deserializeJson(writeDoc, msg);

      if (err) {
        Serial.print("Bridge: MQTT JSON Error: ");
        Serial.println(err.c_str());
        return;
      }

      // serializeJsonPretty(writeDoc, Serial);
      // Serial.println();

      if (mesh.isConnected(target)) {
        sendScheduleToNode(target, writeDoc);
      }
      else if (target == mesh.getNodeId()) {
        writeJsonFile(SCHEDULEJSON, writeDoc);

        String pubMsg = "Berhasil update Schedule pada Root";
        mqttClient.publish((rootTopicMACAddr + PUBLISHMSGTOPIC).c_str(), pubMsg.c_str());
        Serial.println("Bridge: Schedule successfully updated");
      }
    }
    else {
      Serial.println("Bridge: MQTT client not defined!");
      mqttClient.publish((rootTopicMACAddr + PUBLISHMSGTOPIC).c_str(), "Topic, Msg or Node are not defined!");
    }
  }
} // end mqttCallback

//// Fungsi untuk mengirim pesan ke mesh
void sendStateToNode(uint32_t toNode, bool state) {
  StaticJsonDocument<200> jsonDoc;
  jsonDoc["nodeId"] = mesh.getNodeId();     // uint32_t
  jsonDoc["status"] = state;                // bool status saklar

  String jsonString;
  serializeJson(jsonDoc, jsonString);

  mesh.sendSingle(toNode, jsonString);
  Serial.printf("Bridge: Send switch status %s to %u\n", state ? "ON" : "OFF", toNode);
} // end sendStateToNode

void sendScheduleToNode(uint32_t toNode, const JsonDocument& doc) {
  DynamicJsonDocument jsonDoc(1024);
  jsonDoc["nodeId"] = mesh.getNodeId();     // uint32_t
  jsonDoc["schedule"] = doc;                // JSON Schedule Timer

  // Serial.print("Bridge: Schedule Node == ");
  // serializeJsonPretty(jsonDoc, Serial);
  // Serial.println();

  String jsonString;
  serializeJson(jsonDoc, jsonString);

  mesh.sendSingle(toNode, jsonString);
  Serial.printf("Bridge: Send New Schedule Timer to %u\n", toNode);
} // end sendScheduleToNode

void sendGatewayId(uint32_t toNode) {
  StaticJsonDocument<200> jsonDoc;
  jsonDoc["nodeId"]     = mesh.getNodeId();     // uint32_t
  jsonDoc["gatewayId"]  = mesh.getNodeId();     // Id Gateway

  String jsonString;
  serializeJson(jsonDoc, jsonString);

  mesh.sendSingle(toNode, jsonString);
  Serial.printf("Bridge: Send Gateway ID to %u\n", toNode);
} // end sendGatewayId

void sendBroadcastMsg(String msg) {
  StaticJsonDocument<200> jsonDoc;
  jsonDoc["nodeId"] = mesh.getNodeId();     // uint32_t
  jsonDoc["msg"]    = msg;                  // Isi pesan
  jsonDoc["bc"]     = true;                 // Pesan adalah broadcast

  String jsonString;
  serializeJson(jsonDoc, jsonString);

  mesh.sendBroadcast(jsonString);
  Serial.println("Bridge: Send broadcast msg to all nodes");
} // end sendBroadcastMsg

void sendSingleMsg(uint32_t toNode, String msg) {
  StaticJsonDocument<200> jsonDoc;
  jsonDoc["nodeId"] = mesh.getNodeId();     // uint32_t
  jsonDoc["msg"]    = msg;                  // Isi pesan
  jsonDoc["bc"]     = false;                // Pesan adalah single

  String jsonString;
  serializeJson(jsonDoc, jsonString);

  mesh.sendSingle(toNode, jsonString);
  Serial.printf("Bridge: Send single msg to %u\n", toNode);
} // end sendSingleMsg

//// Mesh Callback
// Fungsi untuk menerima pesan dari mesh
void receivedCallback(const uint32_t &from, const String &msg) {
  Serial.printf("Bridge: MESH Received -> From: %u, Msg: %s\n", from, msg.c_str());

  // Buat objek JSON
  StaticJsonDocument<200> jsonDoc;
  DeserializationError error = deserializeJson(jsonDoc, msg);

  // Cek apakah parsing JSON berhasil
  if (error) {
    Serial.println("Bridge: Failed to parse MESH JSON!");
    return;
  }

  // Ambil "nodeId" (wajib ada dalam setiap pesan)
  uint32_t nodeId = jsonDoc["nodeId"];
  if (!nodeId) {
    Serial.println("Bridge: Missing MESH nodeId!");
    return;
  }

  Serial.printf("Bridge: Getting message from nodeId: %u\n", nodeId);

  // Cek apakah ada field "status" dari Node
  if (jsonDoc.containsKey("status")) {
    bool state = jsonDoc["status"];

    // Kirim status saklar ke MQTT
    publishNodeStatus(nodeId, state);
    Serial.printf("Bridge: Get switch status %s from %u\n", state ? "ON" : "OFF", nodeId);
  }

  // Cek apakah ada field "rssi" dari Node
  if (jsonDoc.containsKey("rssi")) {
    int rssi = jsonDoc["rssi"];

    // Kirim RSSI node ke MQTT
    publishRSSIStatus(nodeId, rssi);
    Serial.printf("Bridge: Get signal strength %ddBm from %u\n", rssi, nodeId);
  }

  // Cek apakah ada field "msg" untuk pesan broadcast
  if (jsonDoc.containsKey("bc")) {
    bool bcMsg = jsonDoc["bc"];
    if (bcMsg) {  // Pesan Broadcast
      String msg = jsonDoc["msg"];

      if (msg == GET_GATEWAY) {
        sendGatewayId(nodeId);
      }

      /////// if apa lagi
    }
    else {        // Pesan Single
      String msg = jsonDoc["msg"];

      // menerima pesan berhasil update Schedule
      Serial.printf("Bridge: Mendapat message: %s dari Node %u\n", msg.c_str(), nodeId);
      String pubMsg = "Berhasil update Schedule pada node " + String(nodeId);
      mqttClient.publish((rootTopicMACAddr + PUBLISHMSGTOPIC).c_str(), pubMsg.c_str());

      /////// if apa lagi
    }
  }
} // end receivedCallback

void newConnectionCallback(uint32_t nodeId) {
  Serial.printf("Bridge: New Connection, nodeId = %u\n", nodeId);
  Serial.printf("Bridge: New Connection, %s\n", mesh.subConnectionJson(true).c_str());
  requestAndSendMeshDataToMQTT();   // Kirimkan semua status koneksi Node baru ke MQTT
} // end newConnectionCallback

void changedConnectionCallback() {
  Serial.printf("Bridge: Changed connections\n");
  nodes = mesh.getNodeList();
  Serial.printf("Bridge: Num nodes: %d\n", nodes.size());
  Serial.printf("Bridge: Connection list:");
  SimpleList<uint32_t>::iterator node = nodes.begin();
  while (node != nodes.end()) {
    Serial.printf(" %u", *node);
    node++;
  }
  Serial.println();
} // end changedConnectionCallback

void nodeTimeAdjustedCallback(int32_t offset) {
  Serial.printf("Bridge: Adjusted time %u. Offset = %d\n", mesh.getNodeTime(), offset);
} // end nodeTimeAdjustedCallback

void nodeDelayReceivedCallback(uint32_t nodeId, int32_t delay) {
  Serial.printf("Bridge: Delay from node %u. Delay = %d\n", nodeId, delay);
} // end nodeDelayReceivedCallback


void setup() {
  // Inisialisasi serial, penyimpanan lokal dan wifi
  Serial.begin(115200);
  preferences.begin("mesh-gate", false);
  if (!SPIFFS.begin(true)) {
    Serial.println("Bridge: Failed mount SPIFFS");
    return;
  }
  WiFi.mode(WIFI_AP_STA);
  delay(100);              // beri sedikit waktu untuk inisialisasi
  // Ambil MAC address esp32 dalam lowercase (ex: dc:4f:22:6e:83:23)
  // karena library smartconfig pada flutter mengambil data mac address esp32 juga dalam bentuk lowercase
  rootTopicMACAddr = WiFi.macAddress();
  rootTopicMACAddr.toLowerCase();
  Serial.println("Bridge: Mendapatkan MAC Address ESP32: " + rootTopicMACAddr);

  // Inisialisasi penggunaan pin
  pinMode(TOUCH_INPIN, INPUT);
  pinMode(RELAY_OUTPIN, OUTPUT);
  pinMode(TOUCH_INDICATOR, OUTPUT);
  pinMode(NET_INDICATOR, OUTPUT);

  current_touch_state = digitalRead(TOUCH_INPIN);   // Mengambil status awal touch sensor

  // Inisisalisasi proses untuk menghubungkan dan mengambil data access point/router wifi
  wifi_ssid = preferences.getString(WIFI_SSID, "");
  wifi_channel = preferences.getInt(MESH_CHANNEL, 0);
  if (wifi_ssid == "" && wifi_channel == 0) {   // jika masih menggunakan nilai default, maka lakukan smartconfig untuk terhubung ke wifi
    startSmartConfig();
  }
  else {
    scanWifiAndChannel();   // Scanning untuk melihat ssid dan channel wifi apakah ada perubahan atau tidak
    wifi_pass = preferences.getString(WIFI_PASS, "");
  }

  // Inisialisasi mesh
  mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION);
  mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT, WIFI_AP_STA, wifi_channel);
  mesh.onReceive(&receivedCallback);
  mesh.onNewConnection(&newConnectionCallback);
  mesh.onChangedConnections(&changedConnectionCallback);
  mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
  mesh.onNodeDelayReceived(&nodeDelayReceivedCallback);

  mesh.stationManual(wifi_ssid, wifi_pass);
  mesh.setHostname(HOSTNAME);

  // Bridge node harus didefinisikan sebagai root node.
  mesh.setRoot(true);
  mesh.setContainsRoot(true);
  mesh.initOTAReceive("bridge");

  // Inisialisasi MQTT
  mqttClient.setServer(mqtt_broker, mqtt_port);
  mqttClient.setCallback(mqttCallback);

  // Inisialisasi NTP
  configTime(gmt_offset_sec, daylight_offset_sec, ntp_server);
  printLocalTime();

  // Inisialisasi pengecekan Data Schedule sudah tersedia atau belum
  if (!SPIFFS.exists(SCHEDULEJSON)) {
    File file = SPIFFS.open(SCHEDULEJSON, "w");
    if (file) {
      // Tidak menulis apa pun = file kosong
      file.close();
      Serial.println("Bridge: File kosong schedule.json berhasil dibuat");
    }
    else {
      Serial.println("Bridge: Gagal membuat file schedule.json");
    }
  }
  else {
    Serial.println("Bridge: File schedule.json sudah tersedia di SPIFF");
  }

  // preferences.end();
} // end setup

void loop() {
  // Update jaringan mesh dan mqtt
  mesh.update();
  mqttClient.loop();

  // kontrol saklar dari touch sensor
  last_touch_state    = current_touch_state;              // Menyimpan status terakhir touch sensor
  current_touch_state = digitalRead(TOUCH_INPIN);         // Membaca status baru touch sensor

  if (last_touch_state == LOW && current_touch_state == HIGH) {
    digitalWrite(TOUCH_INDICATOR, HIGH);
    relay_state = !relay_state;                           // Status relay beralih
    digitalWrite(RELAY_OUTPIN, relay_state);              // Mengontrol relay sesuai status peralihan

    touch_start_reset = millis();                         // Catat waktu saat sentuhan dimulai
    publishNodeStatus(mesh.getNodeId(), relay_state);     // Kirim status saklar ke MQTT
    Serial.println("Bridge: Sensor pressed, send switch state to MQTT");
  }

  // Jika tombol tetap ditekan selama 5 detik
  if (can_reset_device && current_touch_state == HIGH && ((unsigned long)millis() - touch_start_reset) >= touch_reset_duration) {
    Serial.println("Bridge: Resetting Device Data...");

    preferences.clear();        // hapus semua key value
    if (SPIFFS.exists(SCHEDULEJSON)) {
      Serial.println("Bridge: File schedule.json ditemukan, menghapus...");
      if (SPIFFS.remove(SCHEDULEJSON)) {
        Serial.println("Bridge: File schedule.json berhasil dihapus");
      }
      else {
        Serial.println("Bridge: Gagal menghapus file schedule.json");
      }
    }
    else {
      Serial.println("Bridge: File schedule.json tidak ditemukan");
    }

    can_reset_device = false;   // Mengubah status reset device
    relay_state = LOW;          // Mematikan saklar
    digitalWrite(RELAY_OUTPIN, relay_state);

    // Mulai proses berkedip LED
    is_reset_indicator = true;
    reset_indicator_count = 0;
    last_reset_blinking_time = millis();

    touch_start_reset = 0;      // Reset waktu sentuhan
  }

  // Reset waktu sentuhan jika tombol dilepas
  if (last_touch_state == HIGH && current_touch_state == LOW) {
    digitalWrite(TOUCH_INDICATOR, LOW);   // Matikan LED
    digitalWrite(NET_INDICATOR, LOW);     // Matikan LED
    touch_start_reset = 0;                // Reset waktu sentuhan
    // Serial.println("Reset waktu sentuhan!");
  }

  // Memanggil fungsi kedip LED saat Reset
  handleResetIndicator();

  // mendapatkan nilai IP Address dari Access Point
  if (myIP != getlocalIP()) {
    myIP = getlocalIP();
    Serial.println("Bridge: IP Station: " + myIP.toString());
    ap_connected = true;
  }

  // Melakukan pengecekan koneksi MQTT
  if (ap_connected && !is_reset_indicator) {
    if (!mqttClient.connected()) {
      reconnectMQTT();
    }
  }

  if ((unsigned long)millis() - last_check_time >= interval_check_time) {
    last_check_time = millis();

    printLocalTime();
    sendBroadcastMsg(current_time);     // Send to broadcast current time from ntp

    DynamicJsonDocument readDoc(1024);
    if (readJsonFile(SCHEDULEJSON, readDoc)) {
      // serializeJsonPretty(readDoc, Serial);
      // Serial.println();
      for (JsonObject schedule : readDoc.as<JsonArray>()) {
        String time = schedule["time"];
        String state = schedule["state"];

        if (time == current_time && last_activated_time != time) {
          Serial.printf("Bridge: Schedule cocok: %s → %s\n", time.c_str(), state.c_str());

          if (state == "ON") {
            relay_state = HIGH;
            digitalWrite(RELAY_OUTPIN, relay_state);
            Serial.println("Bridge: Switch is turned ON from Schedule, send State to MQTT");
          }
          else if (state == "OFF") {
            relay_state = LOW;
            digitalWrite(RELAY_OUTPIN, relay_state);
            Serial.println("Bridge: Switch is turned OFF from Schedule, send State to MQTT");
          }

          publishNodeStatus(mesh.getNodeId(), relay_state);     // Kirim status saklar ke MQTT

          last_activated_time = time;
          break;
        }
      }
    }
    else {
      Serial.println("Bridge: Failed to read schedule.json");
    }
  }

  // Reset device akan expired dalam waktu yang telah ditentukan hanya setelah perangkat dinyalakan
  // Hanya bisa reset device setelah alat dimatikan dan dinyalakan lagi
  if (can_reset_device && (unsigned long)millis() >= reset_device_expired) {
    can_reset_device = false;     // Mengubah status reset device
    Serial.println("Bridge: Reset device has expired!");
  }
} // end loop
