#include <Arduino.h>
#include <WiFi.h>
// Enable verbose debug from Firebase_ESP_Client (helps reveal which host/step fails)
#define FIREBASE_ESP_CLIENT_DEBUG_PORT Serial
#define FIREBASE_ESP_CLIENT_DEBUG_LEVEL 2
#include <Firebase_ESP_Client.h>
#include <WiFiClientSecure.h>
#include "DHT.h"
#include <Wire.h>
#include "RTClib.h"
#include "time.h"
#include <esp_heap_caps.h>

static const char* WIFI_SSID = "oven";
static const char* WIFI_PASSWORD = "1sampai8";

static const char* FIREBASE_API_KEY = "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w";
static const char* FIREBASE_DATABASE_URL = "https://smartoven-f9fdd-default-rtdb.firebaseio.com/";

char apiKey[100] = "";
char databaseUrl[200] = "";

FirebaseData fbdo;
FirebaseAuth auth;
FirebaseConfig config;
static bool signupOK = false;

static bool signUpAnonymousWithRetry(uint32_t retryDelayMs = 2000) {
  // Anonymous sign-in: email/password kosong
  for (;;) {
    signupOK = Firebase.signUp(&config, &auth, "", "");
    if (signupOK) {
      Serial.println("[FB] signUp(anonymous): OK");
      return true;
    }
    Serial.print("[FB] signUp(anonymous): FAIL ");
    Serial.println(config.signer.signupError.message.c_str());
    delay(retryDelayMs);
  }
}

static bool waitFirebaseReady(uint32_t timeoutMs = 15000) {
  Serial.print("[FB] Waiting ready");
  uint32_t start = millis();
  while (!Firebase.ready() && (millis() - start) < timeoutMs) {
    delay(250);
    Serial.print('.');
  }
  Serial.println();
  Serial.println(Firebase.ready() ? "[FB] Ready" : "[FB] Not ready (timeout)");
  return Firebase.ready();
}

static bool fbIsPathNotExist() {
  // Firebase_ESP_Client uses negative codes for internal errors.
  // -103 is commonly used for "path not exist" in RTDB get.
  if (fbdo.httpCode() == -103) return true;
  String r = fbdo.errorReason();
  r.toLowerCase();
  return r.indexOf("path not exist") >= 0;
}

// ===== HISTORY (RTDB) =====
static const bool ENABLE_HISTORY = true;
static const uint32_t HISTORY_INTERVAL_MS = 1800000; // 30 menit
static uint32_t lastHistoryPushMs = 0;

static bool fbPushHistory(float temp, float hum, int heaterDb, int fanDb, int mode, bool active) {
  if (WiFi.status() != WL_CONNECTED) return false;
  if (!Firebase.ready()) return false;

  time_t epoch = time(nullptr);
  if (epoch <= 0) {
    Serial.println("[FB] WARN pushHistory skip (time not set)");
    return false;
  }

  FirebaseJson json;
  json.set("ts", (int)epoch);
  json.set("temp", temp);
  json.set("hum", hum);
  json.set("heater", heaterDb);
  json.set("fan", fanDb);
  json.set("mode", mode);
  json.set("active", active ? 1 : 0);

  bool ok = Firebase.RTDB.pushJSON(&fbdo, "/history", &json);
  if (ok) {
    Serial.print("[FB] OK   pushHistory key=");
    Serial.println(fbdo.pushName());
  } else {
    Serial.print("[FB] FAIL pushHistory code=");
    Serial.print(fbdo.httpCode());
    Serial.print(" reason=");
    Serial.println(fbdo.errorReason());
  }
  return ok;
}

// ===== FIREBASE HELPERS (STATUS LOG) =====
static bool fbSetFloat(const char* path, float value) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.print("[FB] FAIL setFloat ");
    Serial.print(path);
    Serial.println(" (WiFi not connected)");
    return false;
  }
  if (!Firebase.ready()) {
    Serial.print("[FB] FAIL setFloat ");
    Serial.print(path);
    Serial.println(" (Firebase not ready)");
    return false;
  }

  bool ok = Firebase.RTDB.setFloat(&fbdo, path, value);
  if (ok) {
    Serial.print("[FB] OK   setFloat ");
    Serial.print(path);
    Serial.print(" = ");
    Serial.println(value);
  } else {
    Serial.print("[FB] FAIL setFloat ");
    Serial.print(path);
    Serial.print(" code=");
    Serial.print(fbdo.httpCode());
    Serial.print(" reason=");
    Serial.println(fbdo.errorReason());
  }
  return ok;
}

static bool fbSetInt(const char* path, int value) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.print("[FB] FAIL setInt   ");
    Serial.print(path);
    Serial.println(" (WiFi not connected)");
    return false;
  }
  if (!Firebase.ready()) {
    Serial.print("[FB] FAIL setInt   ");
    Serial.print(path);
    Serial.println(" (Firebase not ready)");
    return false;
  }

  bool ok = Firebase.RTDB.setInt(&fbdo, path, value);
  if (ok) {
    Serial.print("[FB] OK   setInt   ");
    Serial.print(path);
    Serial.print(" = ");
    Serial.println(value);
  } else {
    Serial.print("[FB] FAIL setInt   ");
    Serial.print(path);
    Serial.print(" code=");
    Serial.print(fbdo.httpCode());
    Serial.print(" reason=");
    Serial.println(fbdo.errorReason());
  }
  return ok;
}

static bool fbGetInt(const char* path, int& outValue) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.print("[FB] FAIL getInt   ");
    Serial.print(path);
    Serial.println(" (WiFi not connected)");
    return false;
  }
  if (!Firebase.ready()) {
    Serial.print("[FB] FAIL getInt   ");
    Serial.print(path);
    Serial.println(" (Firebase not ready)");
    return false;
  }

  bool ok = Firebase.RTDB.getInt(&fbdo, path);
  if (ok) {
    outValue = fbdo.intData();
    Serial.print("[FB] OK   getInt   ");
    Serial.print(path);
    Serial.print(" = ");
    Serial.println(outValue);
  } else {
    if (fbIsPathNotExist()) {
      outValue = 0;
      Serial.print("[FB] WARN getInt  ");
      Serial.print(path);
      Serial.println(" (path not exist -> default 0)");
      return true;
    }
    Serial.print("[FB] FAIL getInt   ");
    Serial.print(path);
    Serial.print(" code=");
    Serial.print(fbdo.httpCode());
    Serial.print(" reason=");
    Serial.println(fbdo.errorReason());
  }
  return ok;
}

static bool fbGetFloat(const char* path, float& outValue) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.print("[FB] FAIL getFloat ");
    Serial.print(path);
    Serial.println(" (WiFi not connected)");
    return false;
  }
  if (!Firebase.ready()) {
    Serial.print("[FB] FAIL getFloat ");
    Serial.print(path);
    Serial.println(" (Firebase not ready)");
    return false;
  }

  bool ok = Firebase.RTDB.getFloat(&fbdo, path);
  if (ok) {
    outValue = fbdo.floatData();
    Serial.print("[FB] OK   getFloat ");
    Serial.print(path);
    Serial.print(" = ");
    Serial.println(outValue);
  } else {
    if (fbIsPathNotExist()) {
      outValue = 0.0f;
      Serial.print("[FB] WARN getFloat ");
      Serial.print(path);
      Serial.println(" (path not exist -> default 0)");
      return true;
    }
    Serial.print("[FB] FAIL getFloat ");
    Serial.print(path);
    Serial.print(" code=");
    Serial.print(fbdo.httpCode());
    Serial.print(" reason=");
    Serial.println(fbdo.errorReason());
  }
  return ok;
}

// ===== RTC =====
RTC_DS3231 rtc;
static bool rtcAvailable = false;

static void scanI2C() {
  Serial.println("I2C scan...");
  uint8_t found = 0;

  for (uint8_t addr = 1; addr < 127; addr++) {
    Wire.beginTransmission(addr);
    uint8_t err = Wire.endTransmission();
    if (err == 0) {
      Serial.print("  Found device at 0x");
      if (addr < 16) Serial.print('0');
      Serial.println(addr, HEX);
      found++;
    }
  }

  if (found == 0) {
    Serial.println("  No I2C devices found (cek wiring SDA/SCL + pullup + pin Wire.begin)");
  }
}

static void printHeap(const char* tag) {
  Serial.print("[HEAP] ");
  Serial.print(tag);
  Serial.print(" free=");
  Serial.print(ESP.getFreeHeap());
  Serial.print(" min=");
  Serial.print(ESP.getMinFreeHeap());
  Serial.print(" 8bit=");
  Serial.println((uint32_t)heap_caps_get_free_size(MALLOC_CAP_8BIT));
}

static bool extractHostFromUrl(const char* url, char* outHost, size_t outSize) {
  if (!url || !outHost || outSize < 4) return false;
  outHost[0] = '\0';

  const char* p = url;
  if (strncmp(p, "https://", 8) == 0) p += 8;
  else if (strncmp(p, "http://", 7) == 0) p += 7;

  const char* end = strchr(p, '/');
  size_t len = end ? (size_t)(end - p) : strlen(p);
  if (len == 0 || len >= outSize) return false;

  memcpy(outHost, p, len);
  outHost[len] = '\0';
  return true;
}

static bool testTcpConnect(const char* host, uint16_t port) {
  WiFiClient client;
  client.setTimeout(5000);
  bool ok = client.connect(host, port);
  client.stop();
  Serial.print("[NET] TCP ");
  Serial.print(host);
  Serial.print(":");
  Serial.print(port);
  Serial.print(" => ");
  Serial.println(ok ? "OK" : "FAIL");
  return ok;
}

static bool testTlsConnectInsecure(const char* host, uint16_t port) {
  WiFiClientSecure client;
  client.setInsecure();
  client.setTimeout(7000);
  printHeap("before TLS");
  bool ok = client.connect(host, port);
  Serial.print("[NET] TLS(insecure) ");
  Serial.print(host);
  Serial.print(":");
  Serial.print(port);
  Serial.print(" => ");
  Serial.println(ok ? "OK" : "FAIL");
  client.stop();
  printHeap("after TLS");
  return ok;
}

static void netDiagnostics() {
  Serial.println("[NET] Diagnostics...");
  printHeap("start");

  // DNS test
  IPAddress ip;
  bool dnsOk = WiFi.hostByName("www.google.com", ip);
  Serial.print("[NET] DNS www.google.com => ");
  Serial.println(dnsOk ? ip.toString() : String("FAIL"));

  // TCP/TLS to Google
  testTcpConnect("www.google.com", 443);
  testTlsConnectInsecure("www.google.com", 443);

  // Firebase host test
  char fbHost[128];
  if (extractHostFromUrl(FIREBASE_DATABASE_URL, fbHost, sizeof(fbHost))) {
    bool fbDnsOk = WiFi.hostByName(fbHost, ip);
    Serial.print("[NET] DNS ");
    Serial.print(fbHost);
    Serial.print(" => ");
    Serial.println(fbDnsOk ? ip.toString() : String("FAIL"));
    testTcpConnect(fbHost, 443);
    testTlsConnectInsecure(fbHost, 443);
  } else {
    Serial.println("[NET] Could not parse Firebase host from URL");
  }
}

// ===== NTP =====
const char* ntpServer1 = "pool.ntp.org";
const char* ntpServer2 = "time.google.com";
const char* ntpServer3 = "time.nist.gov";
const long gmtOffset_sec = 7 * 3600;
const int daylightOffset_sec = 0;

unsigned long lastSync = 0;
const unsigned long syncInterval = 3600000; // 1 jam

// ===== DHT =====
#define DHTPIN 23
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

// ===== RELAY =====
const int relayHeater = 12;
const int relayFan = 13;

// ===== LED =====
const int ledHeater = 12;

// ===== RELAY LOGIC =====
#define RELAY_ON LOW
#define RELAY_OFF HIGH

// ===== VARIABLE =====
int startHour, startMinute;
int endHour, endMinute;

float targetTemp;
float targetTempFan;

// ===== KALIBRASI =====
float tempOffset = 0;
float humOffset = 0;

int mode = 0;
int manualHeater = 0;
int manualFan = 0;

// ===== WIFI CONNECT =====
static bool connectWiFi(uint32_t timeoutMs = 20000) {
  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  Serial.print("WiFi connecting to: ");
  Serial.println(WIFI_SSID);

  uint32_t start = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - start) < timeoutMs) {
    delay(300);
    Serial.print('.');
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("WiFi connected, IP: ");
    Serial.println(WiFi.localIP());
    return true;
  }

  Serial.println("WiFi gagal connect");
  return false;
}

static int monthStrToIndex(const char* mon) {
  // __DATE__ format: "Mmm dd yyyy"
  static const char* months[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
  for (int i = 0; i < 12; i++) {
    if (strncmp(mon, months[i], 3) == 0) return i;
  }
  return 0;
}

static bool setTimeFromCompileUtc() {
  // fallback kalau NTP diblokir: set waktu dari waktu compile sketch
  // Ini cukup buat TLS (sertifikat) asal firmware baru di-upload (waktunya mendekati sekarang).
  const char* dateStr = __DATE__; // "May 20 2026"
  const char* timeStr = __TIME__; // "HH:MM:SS"

  char mon[4] = {0};
  int day = 1;
  int year = 2020;
  int hour = 0;
  int minute = 0;
  int second = 0;

  memcpy(mon, dateStr, 3);
  mon[3] = '\0';
  day = atoi(dateStr + 4);
  year = atoi(dateStr + 7);
  hour = atoi(timeStr);
  minute = atoi(timeStr + 3);
  second = atoi(timeStr + 6);

  struct tm t;
  memset(&t, 0, sizeof(t));
  t.tm_year = year - 1900;
  t.tm_mon = monthStrToIndex(mon);
  t.tm_mday = day;
  t.tm_hour = hour;
  t.tm_min = minute;
  t.tm_sec = second;
  t.tm_isdst = 0;

  setenv("TZ", "UTC0", 1);
  tzset();

  time_t epoch = mktime(&t);
  if (epoch <= 0) return false;

  struct timeval tv;
  tv.tv_sec = epoch;
  tv.tv_usec = 0;
  settimeofday(&tv, nullptr);

  Serial.print("[TIME] Fallback set from compile UTC: ");
  Serial.println(ctime(&epoch));
  return true;
}

// ===== SYNC RTC =====
static bool syncNtpTime(uint32_t timeoutMs = 20000) {
  // set timezone for localtime
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2, ntpServer3);

  struct tm timeinfo;
  uint32_t start = millis();
  while (!getLocalTime(&timeinfo) && (millis() - start) < timeoutMs) {
    delay(500);
    Serial.print('#');
  }
  Serial.println();

  if (!getLocalTime(&timeinfo)) {
    Serial.println("NTP gagal");
    return false;
  }

  // validasi kasar: tahun harus sudah masuk akal
  if (timeinfo.tm_year + 1900 < 2020) {
    Serial.println("NTP dapat tapi waktu belum valid");
    return false;
  }

  Serial.print("NTP OK: ");
  Serial.print(timeinfo.tm_year + 1900);
  Serial.print('-');
  Serial.print(timeinfo.tm_mon + 1);
  Serial.print('-');
  Serial.print(timeinfo.tm_mday);
  Serial.print(' ');
  Serial.print(timeinfo.tm_hour);
  Serial.print(':');
  Serial.print(timeinfo.tm_min);
  Serial.print(':');
  Serial.println(timeinfo.tm_sec);

  if (rtcAvailable) {
    rtc.adjust(DateTime(
      timeinfo.tm_year + 1900,
      timeinfo.tm_mon + 1,
      timeinfo.tm_mday,
      timeinfo.tm_hour,
      timeinfo.tm_min,
      timeinfo.tm_sec
    ));
    Serial.println("RTC Sync NTP");
  }

  return true;
}

static void syncRTC() {
  if (syncNtpTime()) {
    Serial.println(rtcAvailable ? "RTC/NTP sync OK" : "NTP sync OK (RTC tidak ada)");
  }
}

// ===== SETUP =====
void setup() {

  Serial.begin(115200);
  delay(2000);

  Serial.println("BOOT...");

  // ===== PIN MODE =====
  pinMode(relayHeater, OUTPUT);
  pinMode(relayFan, OUTPUT);
  pinMode(ledHeater, OUTPUT);

  digitalWrite(relayHeater, RELAY_OFF);
  digitalWrite(relayFan, RELAY_OFF);
  digitalWrite(ledHeater, LOW);

  // ===== DHT =====
  dht.begin();

  // ===== I2C =====
  Wire.begin(13, 14);
  scanI2C();

  // ===== RTC =====
  rtcAvailable = rtc.begin();
  if (!rtcAvailable) {
    Serial.println("RTC error (DS3231 tidak terdeteksi). Lanjut tanpa RTC (pakai NTP). ");
  }

  // ===== FIREBASE CONFIG (HARDCODE) =====
  strncpy(apiKey, FIREBASE_API_KEY, sizeof(apiKey) - 1);
  apiKey[sizeof(apiKey) - 1] = '\0';

  strncpy(databaseUrl, FIREBASE_DATABASE_URL, sizeof(databaseUrl) - 1);
  databaseUrl[sizeof(databaseUrl) - 1] = '\0';

  if (strlen(apiKey) == 0 || strlen(databaseUrl) == 0) {
    Serial.println("[WARN] FIREBASE_API_KEY / FIREBASE_DATABASE_URL masih kosong. Isi dulu di bagian atas sketch.");
  }

  // ===== WIFI =====
  if (!connectWiFi()) {
    // retry terus sampai konek
    while (!connectWiFi()) {
      delay(2000);
    }
  }

  // ===== NTP TIME (PENTING UNTUK SSL/TLS) =====
  // Banyak error SSL Firebase terjadi kalau waktu belum terset saat TLS handshake.
  Serial.println("Sync NTP...");
  bool ntpOk = syncNtpTime(60000);
  if (!ntpOk) {
    Serial.println("[TIME] NTP gagal. Coba fallback waktu compile...");
    setTimeFromCompileUtc();
  }

  time_t nowEpoch = time(nullptr);
  Serial.print("[TIME] Now epoch: ");
  Serial.println((long)nowEpoch);
  if (nowEpoch > 0) {
    Serial.print("[TIME] Now: ");
    Serial.println(ctime(&nowEpoch));
  }

  Serial.print("Heap before Firebase: ");
  Serial.println(ESP.getFreeHeap());

  // ===== NETWORK DIAGNOSTICS =====
  // Ini bantu bedain: DNS/port443 block vs TLS init/heap issue.
  netDiagnostics();

  // ===== FIREBASE =====
  config.api_key = apiKey;
  config.database_url = databaseUrl;

  // Anonymous auth ONLY (tanpa test_mode).
  // Pastikan di Firebase Console: Authentication -> Sign-in method -> Anonymous di-enable.
  signUpAnonymousWithRetry();

  // buffer & timeout tuning (membantu masalah SSL init / write error)
  // coba lebih kecil dulu biar nggak kehabisan heap
  fbdo.setBSSLBufferSize(2048, 512);
  fbdo.setResponseSize(2048);
  config.timeout.serverResponse = 10 * 1000;

  Firebase.begin(&config, &auth);
  Firebase.reconnectWiFi(true);

  waitFirebaseReady();

  // ===== RTC SYNC =====
  if (rtcAvailable) {
    // kalau RTC ada, pastikan sudah disesuaikan
    syncRTC();
  }
  lastSync = millis();

  Serial.println("🔥 SYSTEM READY");
}

// ===== LOOP =====
void loop() {

  // ===== WIFI CHECK =====
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.reconnect();
  }

  // ===== RTC PERIODIC SYNC =====
  if (WiFi.status() == WL_CONNECTED &&
      millis() - lastSync > syncInterval) {

    syncRTC();
    lastSync = millis();
  }

  // ===== FIREBASE READY CHECK =====
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[FB] Skip: WiFi not connected");
    delay(1000);
    return;
  }
  if (!Firebase.ready()) {
    static uint32_t lastNotReadyLog = 0;
    if (millis() - lastNotReadyLog > 5000) {
      Serial.println("[FB] Skip: Firebase not ready");
      lastNotReadyLog = millis();
    }
    delay(200);
    // tetap lanjut loop (biar logic lokal jalan), tapi operasi Firebase akan FAIL di wrapper
  }
  if (!signupOK) {
    Serial.println("[FB] Skip: signUp not OK");
    delay(2000);
    return;
  }

  // ===== GET FIREBASE =====

  fbGetInt("/control/start_hour", startHour);
  fbGetInt("/control/start_minute", startMinute);
  fbGetInt("/control/end_hour", endHour);
  fbGetInt("/control/end_minute", endMinute);
  fbGetFloat("/control/target_temp", targetTemp);
  fbGetFloat("/control/target_temp_fan", targetTempFan);
  fbGetInt("/control/mode", mode);
  fbGetInt("/manual/heater", manualHeater);
  fbGetInt("/manual/fan", manualFan);

  // fallback kalau target fan belum diset
  if (targetTempFan <= 0) targetTempFan = targetTemp;

  // ===== GET KALIBRASI =====
  fbGetFloat("/calibration/temp_offset", tempOffset);
  fbGetFloat("/calibration/hum_offset", humOffset);

  // ===== CURRENT TIME =====
  int hourNow = 0;
  int minuteNow = 0;
  bool timeOk = false;

  if (rtcAvailable) {
    DateTime now = rtc.now();
    hourNow = now.hour();
    minuteNow = now.minute();
    timeOk = true;
  } else {
    struct tm timeinfo;
    if (getLocalTime(&timeinfo, 1000)) {
      hourNow = timeinfo.tm_hour;
      minuteNow = timeinfo.tm_min;
      timeOk = true;
    }
  }

  int currentTime = hourNow * 60 + minuteNow;
  int startTime = startHour * 60 + startMinute;
  int endTime = endHour * 60 + endMinute;

  bool active = false;
  if (timeOk) {
    active = (startTime <= endTime)
             ? (currentTime >= startTime &&
                currentTime <= endTime)
             : (currentTime >= startTime ||
                currentTime <= endTime);
  } else {
    // kalau waktu tidak valid, fail-safe: nonaktif
    Serial.println("[TIME] Tidak dapat ambil waktu (RTC/NTP). Mode AUTO akan nonaktif.");
    active = false;
  }

  // ===== SENSOR =====
  float temp = dht.readTemperature();
  float hum = dht.readHumidity();

  // DHT read kadang gagal (NaN) karena timing/interrupt/WiFi atau wiring.
  // Coba retry sekali agar lebih stabil.
  if (isnan(temp) || isnan(hum)) {
    Serial.println("[SENSOR] DHT read failed, retry...");
    delay(250);
    temp = dht.readTemperature();
    hum = dht.readHumidity();
  }

  // ===== VALIDASI SENSOR =====
  if (isnan(temp) || isnan(hum)) {
    Serial.println("[SENSOR] DHT error (NaN). Cek wiring VCC/GND/DATA, pullup 4.7k-10k ke VCC, kabel jangan panjang, pastikan sensor DHT22 & pin GPIO benar.");
    delay(2000);
    return;
  }

  // ===== APPLY KALIBRASI =====
  temp += tempOffset;
  hum += humOffset;

  // ===== CONTROL =====
  bool heaterState = false;
  bool fanState = false;

  if (mode == 1) {

    // ===== MANUAL MODE =====
    heaterState = (manualHeater != 0);
    fanState = (manualFan != 0);

    digitalWrite(
      relayFan,
      fanState ? RELAY_ON : RELAY_OFF
    );

  } else {

    // ===== AUTO MODE =====
    if (active) {

      heaterState = (temp < targetTemp);
      fanState = (temp > targetTempFan);

      digitalWrite(
        relayFan,
        fanState ? RELAY_ON : RELAY_OFF
      );

    } else {

      heaterState = false;
      fanState = false;

      digitalWrite(relayFan, RELAY_OFF);
    }
  }

  // ===== APPLY HEATER =====
  digitalWrite(
    relayHeater,
    heaterState ? RELAY_ON : RELAY_OFF
  );

  digitalWrite(
    ledHeater,
    heaterState ? HIGH : LOW
  );

  // ===== SEND SENSOR =====
  bool okTemp = fbSetFloat("/sensor/temperature", temp);
  bool okHum = fbSetFloat("/sensor/humidity", hum);

  // ===== SEND RELAY =====
  int heaterDb = heaterState ? 1 : 0;
  int fanDb = fanState ? 1 : 0;

  bool okHeater = fbSetInt("/relay/heater", heaterDb);
  bool okFan = fbSetInt("/relay/fan", fanDb);

  // Mirror manual state to match relay state (biar UI manual & relay selalu sama)
  bool okManualHeater = fbSetInt("/manual/heater", heaterDb);
  bool okManualFan = fbSetInt("/manual/fan", fanDb);

  Serial.print("[FB] Upload summary: ");
  Serial.println((okTemp && okHum && okHeater && okFan && okManualHeater && okManualFan) ? "OK" : "FAIL");

  // ===== DEBUG =====

  Serial.print("Time: ");
  Serial.print(hourNow);
  Serial.print(":");
  Serial.print(minuteNow);
  Serial.print(" (");
  Serial.print(rtcAvailable ? "RTC" : "NTP");
  Serial.println(")");

  Serial.print("Temp: ");
  Serial.println(temp);

  Serial.print("Humidity: ");
  Serial.println(hum);

  Serial.print("Temp Offset: ");
  Serial.println(tempOffset);

  Serial.print("Hum Offset: ");
  Serial.println(humOffset);

  Serial.print("Mode: ");
  Serial.println(mode == 1 ? "MANUAL" : "AUTO");

  Serial.print("Active: ");
  Serial.println(active);

  // ===== PUSH HISTORY =====
  if (ENABLE_HISTORY) {
    uint32_t nowMs = millis();
    if (lastHistoryPushMs == 0 || (nowMs - lastHistoryPushMs) >= HISTORY_INTERVAL_MS) {
      fbPushHistory(temp, hum, heaterDb, fanDb, mode, active);
      lastHistoryPushMs = nowMs;
    }
  }

  delay(3000);
}