#include "async_network.h"
#include "config.h"
#include "storage.h"
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <esp_mac.h>
#include <esp_sntp.h>
#include <esp_wifi.h> // For TWT APIs


#if defined(CONFIG_SOC_WIFI_HE_SUPPORT) && CONFIG_SOC_WIFI_HE_SUPPORT
#include "esp_wifi_he.h"
#endif

// Standard NTP servers
const char *ntpServer1 = "pool.ntp.org";
const char *ntpServer2 = "time.google.com";
const char *ntpServer3 = "time.nist.gov";

const long fallback_gmtOffset_sec = 0;
const int fallback_daylightOffset_sec = 0;

// Static callback for NTP
static AsyncNetwork *_instance = nullptr;

// ESP32 NTP sync callback
void ntpSyncCallback(struct timeval *tv) {
  if (_instance && _instance->_onTimeSync) {
    Serial.println("[NTP Callback] Time synced!");
    _instance->_onTimeSync();
  }
}

AsyncNetwork::AsyncNetwork()
    : _connected(false), _hasTime(false), _portalRunning(false),
      _lastNtpCheck(0), _lastSignalCheck(0), _lastConnectivityCheck(0),
      _lastReconnectAttempt(0),
      _lastValidActivity(0), _lastWeatherFetch(0), _sunriseTime(0), _sunsetTime(0), _signalStrength(0), _consecutiveFailures(0),
      _consecutiveInternetFailures(0), _backoffMultiplier(1),
      _twtActive(false), _resetState(WiFiResetState::IDLE), _resetStartTime(0),
      _onConnected(nullptr),
      _onDisconnected(nullptr), _onTimeSync(nullptr) {
  _instance = this;
}

void AsyncNetwork::begin(bool forceConfig) {
  // Generate unique AP name from raw MAC bytes â€” guaranteed unique per IEEE
  uint8_t mac[6];
  esp_read_mac(mac, ESP_MAC_WIFI_STA);
  char shortIdStr[8];
  snprintf(shortIdStr, sizeof(shortIdStr), "%02X%02X%02X", mac[3], mac[4],
           mac[5]);
  _apName = "StepperMote-" + String(shortIdStr);

  Serial.println("[AsyncNetwork] Initializing...");

  // Configure WiFi for MAXIMUM STABILITY
  WiFi.persistent(true);
  WiFi.setAutoReconnect(true);
  WiFi.setHostname(_apName.c_str());

  // CRITICAL: Disable WiFi power-save mode to prevent beacon timeout
  // disconnects Reason 200 (BEACON_TIMEOUT) and 201 (NO_AP_FOUND) are caused by
  // modem sleep When sleeping, the radio turns off and misses beacons from the
  // router
  WiFi.setSleep(false); // Keep radio active 100% of the time
  Serial.println("[AsyncNetwork] WiFi sleep DISABLED for stability");

  // Set TX power to 17dBm (approx 50mW) instead of max (19.5dBm / ~90mW)
  // This significantly reduces peak current spikes (preventing brownouts)
  // while maintaining excellent range.
  WiFi.setTxPower(WIFI_POWER_17dBm);
  Serial.println("[AsyncNetwork] TX power set to safe level (17 dBm)");

  // Get credentials - PREFER config-stored credentials over ESP's NVS
  // This ensures setup wizard credentials are always used
  String savedSSID = "";
  String savedPSK = "";

  if (strlen(config.wifi_ssid) > 0) {
    // Use config-stored credentials (from setup wizard)
    savedSSID = String(config.wifi_ssid);
    savedPSK = String(config.wifi_pass);
    Serial.println("[AsyncNetwork] Using config-stored credentials");
  } else {
    // Fallback to ESP's NVS if config is empty
    savedSSID = WiFi.SSID();
    savedPSK = WiFi.psk();
    if (savedSSID.length() > 0) {
      Serial.println("[AsyncNetwork] Using NVS credentials (config empty)");
    }
  }

  if (savedSSID.length() > 0) {
    Serial.println("[AsyncNetwork] Found saved credentials:");
    Serial.printf("  SSID: %s\n", savedSSID.c_str());
  } else {
    Serial.println("[AsyncNetwork] No saved credentials found");
  }

  if (forceConfig) {
    Serial.println(
        "[AsyncNetwork] Force config requested -> Clearing credentials");
    WiFi.disconnect(true, true); // Disconnect and erase credentials
    // Also clear config-stored credentials
    memset(config.wifi_ssid, 0, sizeof(config.wifi_ssid));
    memset(config.wifi_pass, 0, sizeof(config.wifi_pass));
    storage.save();
    savedSSID = "";
  }

  // STA mode only â€” softAP is only needed during setup wizard
  // The setup wizard in StepperMote.ino handles AP mode independently
  Serial.println("[AsyncNetwork] Enabling STA mode...");
  WiFi.mode(WIFI_STA);

  if (savedSSID.length() > 0) {
    Serial.println(
        "[AsyncNetwork] Credentials found. Starting connection attempt...");

    // STATIC IP CONFIGURATION - Only if enabled in secrets.h
#if SECRET_USE_STATIC_IP
    IPAddress staticIP;
    IPAddress gateway;
    IPAddress subnet;
    IPAddress dns1;

    staticIP.fromString(SECRET_STATIC_IP);
    gateway.fromString(SECRET_GATEWAY);
    subnet.fromString(SECRET_SUBNET);
    dns1.fromString(SECRET_DNS);

    if (!WiFi.config(staticIP, gateway, subnet, dns1)) {
      Serial.println("[AsyncNetwork] Static IP config failed, using DHCP");
    } else {
      Serial.printf("[AsyncNetwork] Static IP configured: %s\n",
                    staticIP.toString().c_str());
    }
#else
    Serial.println("[AsyncNetwork] Using DHCP (static IP disabled)");
#endif

    WiFi.begin(savedSSID.c_str(), savedPSK.c_str());

    // Allow a brief initial window for fast-connecting routers.
    // Without this, the device always waits the full backoff period before
    // its first reconnection attempt even if the router responds in <2s.
    Serial.println("[AsyncNetwork] Waiting for initial connection (up to 5s)...");
    unsigned long startWait = millis();
    while (WiFi.status() != WL_CONNECTED && millis() - startWait < 5000) {
      delay(100);
      yield();
    }
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("[AsyncNetwork] Connected during initial window!");
    } else {
      Serial.println("[AsyncNetwork] Initial window expired - will retry via backoff");
    }
  } else {
    Serial.println("[AsyncNetwork] No credentials - Waiting in AP Mode");
  }

  // Setup WiFi event handlers
  setupWiFiEventHandlers();

  _connected = (WiFi.status() == WL_CONNECTED);
  _lastValidActivity = millis();  // Prevent false zombie detection on boot

  if (_connected) {
    Serial.println("[AsyncNetwork] Already connected");
    startNTP();
    if (config.twt_enabled) {
      enableTWT();
    }
    if (_onConnected) {
      _onConnected();
    }
  }
}

void AsyncNetwork::update(bool allowBlockingChecks) {
  handleWiFiEvents();
  handleNTPSync();
  updateSignalStrength();
  checkConnectivity(allowBlockingChecks);
}

void AsyncNetwork::handleWiFiEvents() {
  bool nowConnected = (WiFi.status() == WL_CONNECTED);

  if (nowConnected != _connected) {
    _connected = nowConnected;

    if (_connected) {
      Serial.println("[AsyncNetwork] WiFi Connected");
      Serial.printf("  IP: %s\n", WiFi.localIP().toString().c_str());
      Serial.printf("  Signal: %d dBm\n", WiFi.RSSI());

      WiFi.setAutoReconnect(true);

      // Re-apply sleep disable after connection (some implementations reset
      // this)
      WiFi.setSleep(false);

      _backoffMultiplier = 1;
      _consecutiveFailures = 0;
      _consecutiveInternetFailures = 0;
      _lastValidActivity = millis();

      yield();

      // DISABLED: Keep AP+STA mode for stability - don't trigger mode switch

      startNTP();

      // Auto-enable TWT if configured
      if (config.twt_enabled) {
        enableTWT();
      }

      if (_onConnected) {
        _onConnected();
      }
    } else {
      Serial.println("[AsyncNetwork] WiFi Disconnected");
      Serial.printf("  Backoff multiplier: %dx\n", _backoffMultiplier);

      // NOTE: Do NOT switch WiFi mode here - it interrupts the WiFi stack
      // and causes additional disconnects. Let the reconnection logic handle
      // it.

      if (_onDisconnected) {
        _onDisconnected();
      }
    }
  }

  // Exponential backoff reconnection with LONGER delays for stability
  // Short delays cause connection storms which destabilize the WiFi stack
  if (!_connected) {
    // --- Non-blocking reset state machine ---
    // Replaces blocking delay() calls with staged progression
    if (_resetState != WiFiResetState::IDLE) {
      unsigned long elapsed = millis() - _resetStartTime;
      switch (_resetState) {
        case WiFiResetState::WIFI_OFF:
          if (elapsed >= 200) {  // 200ms off period
            WiFi.mode(WIFI_STA);
            _resetState = WiFiResetState::WIFI_ON;
            _resetStartTime = millis();
          }
          return;  // Skip normal reconnection logic during reset
        case WiFiResetState::WIFI_ON:
          if (elapsed >= 200) {  // 200ms stabilization
            reapplyStabilitySettings();
            connectWithCredentials();
            WiFi.setAutoReconnect(true);
            _resetState = WiFiResetState::IDLE;
            _backoffMultiplier = 1;
          }
          return;
        default:
          _resetState = WiFiResetState::IDLE;
          break;
      }
    }

    unsigned long now = millis();
    // 5-second base delay with exponential backoff, max 60 seconds.
    // Gives WiFi stack time to stabilize between attempts.
    unsigned long backoffDelay =
        min(60000UL, 5000UL * (unsigned long)_backoffMultiplier);

    if (now - _lastReconnectAttempt > backoffDelay) {
      _lastReconnectAttempt = now;
      _consecutiveFailures++;

      Serial.printf(
          "[AsyncNetwork] Reconnection attempt #%d (backoff: %lums)\n",
          _consecutiveFailures, backoffDelay);

      if (_backoffMultiplier < 8) {
        _backoffMultiplier *= 2;
      }

      if (_consecutiveFailures == 5) {
        Serial.println(
            "[AsyncNetwork] Connection struggling - continuing reconnection attempts");
      }

      // Use config-stored credentials directly (avoids String heap allocations)
      bool hasCredentials = (strlen(config.wifi_ssid) > 0);

      // Fallback: if config credentials are empty, check NVS
      if (!hasCredentials) {
        // WiFi.SSID() returns a temporary String — we can't store a pointer
        // to it. Instead, just check if NVS has credentials and let
        // auto-reconnect handle the actual connection.
        hasCredentials = (WiFi.SSID().length() > 0);
      }

      if (hasCredentials) {
        // Hard reset every 10 failures to recover from stuck radio states
        if (_consecutiveFailures % 10 == 0 && _consecutiveFailures > 0) {
          Serial.println(
              "[AsyncNetwork] Hard WiFi reset (long persistent failure)...");

          // NON-BLOCKING: Enter state machine instead of delay()
          WiFi.setAutoReconnect(false);
          WiFi.disconnect();
          WiFi.mode(WIFI_OFF);
          _resetState = WiFiResetState::WIFI_OFF;
          _resetStartTime = millis();
          // State machine continues in next update() call
        } else {
          // CRITICAL FIX: Actively call WiFi.begin() on every attempt.
          // The ESP32's built-in auto-reconnect is unreliable on ESP-IDF 5.x,
          // especially if the initial connection never succeeded or the radio
          // was previously in WIFI_OFF mode. Without this, the device can
          // wait 40+ minutes before making a real reconnection attempt.
          Serial.println("[AsyncNetwork] Active reconnection attempt...");
          WiFi.disconnect();  // Clean up any stale connection state first
          reapplyStabilitySettings();
          connectWithCredentials();
        }
      } else {
        Serial.println("[AsyncNetwork] No credentials - waiting for config");
        _consecutiveFailures = 0;
        _backoffMultiplier = 1;
      }
    }
  } else {
    if (_consecutiveFailures > 0) {
      _consecutiveFailures = 0;
      _backoffMultiplier = 1;
    }
  }
}

void AsyncNetwork::handleNTPSync() {
  if (!_connected)
    return;

  unsigned long now = millis();

  time_t currentTime = time(nullptr);
  bool timeValid = (currentTime > 1000000000);

  if (timeValid && !_hasTime) {
    _hasTime = true;
    Serial.println("[AsyncNetwork] Time Synced");
    yield();
    if (_onTimeSync) {
      _onTimeSync();
    }
  }

  if (!timeValid) {
    if (now - _lastNtpCheck > 60000) {
      Serial.println("[AsyncNetwork] Time not synced yet, retrying NTP...");
      // FEAT-5: Prefer POSIX timezone if configured, otherwise use static offsets.
      if (strlen(config.tz_posix) > 0) {
        configTime(0, 0, ntpServer1, ntpServer2, ntpServer3);
        setenv("TZ", config.tz_posix, 1);
        tzset();
      } else {
        configTime(config.gmt_offset, config.daylight_offset, ntpServer1, ntpServer2,
                   ntpServer3);
      }
      _lastNtpCheck = now;

      static int ntpFailures = 0;
      ntpFailures++;
      if (ntpFailures > 5) {
        tryHttpTimeSync();
        ntpFailures = 0;
      }
    }
  }
}

bool AsyncNetwork::tryHttpTimeSync() {
  Serial.println("[HTTP Time] Trying HTTP fallback...");

  if (WiFi.status() != WL_CONNECTED) {
    return false;
  }

  HTTPClient http;
  http.setTimeout(3000);

  if (http.begin("http://worldtimeapi.org/api/timezone/Etc/UTC")) {
    int httpCode = http.GET();
    if (httpCode == 200) {
      String payload = http.getString();
      JsonDocument doc;
      DeserializationError error = deserializeJson(doc, payload);

      if (!error && doc.containsKey("unixtime")) {
        long unixtime = doc["unixtime"];
        struct timeval tv;
        tv.tv_sec = unixtime;
        tv.tv_usec = 0;
        settimeofday(&tv, nullptr);
        Serial.println("[HTTP Time] Time set via HTTP");
        http.end();
        return true;
      }
    }
    http.end();
  }
  return false;
}

void AsyncNetwork::startNTP() {
  Serial.println("[AsyncNetwork] Starting NTP...");

  // FEAT-5: Prefer POSIX timezone string for correct DST handling.
  // If tz_posix is set (e.g., "GMT0BST,M3.5.0/1,M10.5.0" for UK),
  // use setenv("TZ") so localtime() automatically applies DST rules.
  // Otherwise fall back to static gmt_offset/daylight_offset.
  if (strlen(config.tz_posix) > 0) {
    configTime(0, 0, ntpServer1, ntpServer2, ntpServer3);
    setenv("TZ", config.tz_posix, 1);
    tzset();
    Serial.printf("[NTP] POSIX TZ active: %s\n", config.tz_posix);
  } else {
    configTime(config.gmt_offset, config.daylight_offset, ntpServer1, ntpServer2,
               ntpServer3);
    Serial.printf("[NTP] Static offset: GMT%+ld DST%+ld\n",
                  config.gmt_offset, config.daylight_offset);
  }

  // Register ESP32 SNTP callback
  sntp_set_time_sync_notification_cb(ntpSyncCallback);
}

const char *AsyncNetwork::getFormattedTime() {
  static char buffer[10];
  time_t now = time(nullptr);
  struct tm *timeinfo = localtime(&now);
  if (timeinfo) {
    snprintf(buffer, sizeof(buffer), "%02d:%02d:%02d", timeinfo->tm_hour,
             timeinfo->tm_min, timeinfo->tm_sec);
  } else {
    snprintf(buffer, sizeof(buffer), "--:--:--");
  }
  return buffer;
}

int AsyncNetwork::getHour() {
  time_t now = time(nullptr);
  struct tm *timeinfo = localtime(&now);
  return timeinfo ? timeinfo->tm_hour : 0;
}

int AsyncNetwork::getMinute() {
  time_t now = time(nullptr);
  struct tm *timeinfo = localtime(&now);
  return timeinfo ? timeinfo->tm_min : 0;
}

void AsyncNetwork::updateSignalStrength() {
  if (!_connected) {
    _signalStrength = 0;
    return;
  }
  unsigned long now = millis();
  if (now - _lastSignalCheck < 15000)
    return;
  _lastSignalCheck = now;
  _signalStrength = WiFi.RSSI();
}

void AsyncNetwork::setupWiFiEventHandlers() {
  // ESP32 WiFi event handlers - LOGGING ONLY
  // Do NOT call WiFi.reconnect() here - it creates connection storms
  // and conflicts with the exponential backoff logic in handleWiFiEvents()

  WiFi.onEvent(
      [](WiFiEvent_t event, WiFiEventInfo_t info) {
        Serial.print("[WiFi Event] Disconnected! Reason: ");
        Serial.println(info.wifi_sta_disconnected.reason);
        // Reconnection is handled by handleWiFiEvents() with proper backoff
      },
      WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);

  WiFi.onEvent(
      [](WiFiEvent_t event, WiFiEventInfo_t info) {
        Serial.print("[WiFi Event] Connected! IP: ");
        Serial.println(WiFi.localIP());
        // Update activity timestamp to prevent stale connection detection
        if (_instance) {
          _instance->_lastValidActivity = millis();
        }
      },
      WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);

  Serial.println("[AsyncNetwork] WiFi event handlers registered");
}

void AsyncNetwork::checkConnectivity(bool allowBlockingChecks) {
  if (!_connected)
    return;

  unsigned long now = millis();

  // Check only every 5 minutes instead of 1 minute
  // Frequent checks were causing unnecessary state changes
  if (now - _lastConnectivityCheck < 300000)
    return;
  _lastConnectivityCheck = now;

  // 1. IP Address Check
  if (WiFi.localIP() == IPAddress(0, 0, 0, 0)) {
    // Non-blocking wait for IP (instead of delay(100))
    static unsigned long lostIpStart = 0;
    if (lostIpStart == 0)
      lostIpStart = millis();

    if (millis() - lostIpStart > 100) {
      lostIpStart = 0; // Reset for next time
      // If still no IP after wait, fail
      if (WiFi.localIP() == IPAddress(0, 0, 0, 0)) {
        Serial.println("[Connectivity] No IP confirmed - marking disconnected");
        _connected = false;
        _backoffMultiplier = 1;
      }
    }
    return;
  }

  // Check signal quality
  int rssi = WiFi.RSSI();
  static int lastRssi = 0;
  static uint8_t weakSignalCount = 0;

  if (rssi < -85) {
    weakSignalCount++;
    if (weakSignalCount == 1) {
      Serial.printf("[Connectivity] Weak signal: %d dBm\n", rssi);
    }

    if (weakSignalCount >= 3) {
      Serial.printf("[Connectivity] Persistently weak signal (%d dBm)\n", rssi);
    }
  } else {
    weakSignalCount = 0;
  }

  // 2. Internet Connectivity Check (Active)
  // If we have an IP but MQTT is failing or connection seems stale,
  // verify actual internet access via HTTP
  // CRITICAL: Respect allowBlockingChecks to prevent 1.5s freeze during motion
  if (allowBlockingChecks &&
      now - _lastValidActivity > 600000) { // 10 minutes (reduced from 30)
    Serial.println(
        "[Connectivity] Connection stale (10min) - checking internet...");

    if (checkInternetConnectivity()) {
      Serial.println(
          "[Connectivity] Internet access CONFIRMED - Connection healthy");
      _lastValidActivity =
          now; // Update activity to prevent re-check for another 10m
      _consecutiveInternetFailures = 0;
    } else {
      Serial.println("[Connectivity] Internet check FAILED");
      _consecutiveInternetFailures++;

      if (_consecutiveInternetFailures >= 3) {
        Serial.println(
            "[Connectivity] Multiple internet failures - forcing reset...");
        // Force flow into the reset logic below
        _lastValidActivity = 0;
      }
    }
  }

  // 3. Zombie Connection Reset
  // If explicitly flagged as stale (0) or timeout reached with confirmed
  // failures 10 minutes was too aggressive and caused false reconnection
  // triggers Web requests update activity via updateActivity(), so this only
  // triggers if there's genuinely been no network activity at all if there's
  // genuinely been no network activity at all
  bool forceReset =
      (_lastValidActivity == 0) || (now - _lastValidActivity > 1800000);

  if (forceReset && _lastValidActivity > 0) {
    // Determine if we should check internet first before killing everything
    // (Only if we haven't checked recently, and blocking checks are allowed)
    if (allowBlockingChecks && checkInternetConnectivity()) {
      Serial.println("[Connectivity] Zombie check passed (Internet OK)");
      _lastValidActivity = now;
      _consecutiveInternetFailures = 0;
      return;
    }
  }

  if (forceReset) {
    Serial.println(
        "[Connectivity] Connection dead - forcing hard reconnect...");

    _connected = false;
    _backoffMultiplier = 1;

    // NON-BLOCKING: Enter state machine instead of delay()
    WiFi.disconnect();
    WiFi.mode(WIFI_OFF);
    _resetState = WiFiResetState::WIFI_OFF;
    _resetStartTime = millis();
    // State machine in handleWiFiEvents() handles the rest
    return;
  }

  // FIXED: Removed "blind" activity update.
  // Just because we have an IP (~lines 599-600) does NOT mean the connection is
  // alive. We must rely on ACTUAL traffic (MQTT, NTP, HTTP) to update
  // _lastValidActivity. This allows the zombie connection logic to actually
  // work.

  lastRssi = rssi;

  static unsigned long lastHealthLog = 0;
  if (now - lastHealthLog > 300000) {
    Serial.printf(
        "[Health] WiFi: %s | IP: %s | Signal: %d dBm | Activity: %lus ago\n",
        WiFi.SSID().c_str(), WiFi.localIP().toString().c_str(), rssi,
        (now - _lastValidActivity) / 1000);
    lastHealthLog = now;
  }
}

bool AsyncNetwork::isHealthy() {
  if (!_connected)
    return false;

  unsigned long now = millis();
  if (_lastValidActivity > 0 && now - _lastValidActivity > 180000) {
    return false;
  }

  int rssi = WiFi.RSSI();
  if (rssi < -90) {
    return false;
  }

  if (WiFi.localIP() == IPAddress(0, 0, 0, 0)) {
    return false;
  }

  return true;
}

// ==============================================================================
// TWT (Target Wake Time) - WiFi 6 Power Saving
// ==============================================================================
// TWT allows the ESP32-C6 to negotiate wake-up schedules with WiFi 6 routers,
// enabling significant power savings by allowing longer sleep periods.
// Requires: WiFi 6 router with TWT support, ESP-IDF 5.1+

void AsyncNetwork::enableTWT() {
  if (!_connected) {
    Serial.println("[TWT] Cannot enable - not connected");
    return;
  }

  if (_twtActive) {
    Serial.println("[TWT] Already active");
    return;
  }

#if defined(CONFIG_SOC_WIFI_HE_SUPPORT) && CONFIG_SOC_WIFI_HE_SUPPORT
  // ESP32-C6 supports WiFi 6 HE (High Efficiency) including TWT

  Serial.println("[TWT] Attempting to setup TWT...");
  Serial.println("[TWT] WARNING: If your router doesn't support TWT, "
                 "connection may become unstable");

  // Store connection state before TWT attempt
  bool wasConnected = (WiFi.status() == WL_CONNECTED);
  IPAddress currentIP = WiFi.localIP();

  // Configure TWT parameters
  // Wake interval: 500ms, minimum wake duration: 10ms
  wifi_twt_setup_config_t twt_config;
  twt_config.setup_cmd = TWT_REQUEST;
  twt_config.flow_id = 0;
  twt_config.twt_id = 0;
  twt_config.flow_type = 0; // 0 = announced TWT (device announces wake)
  twt_config.min_wake_dura =
      10; // Minimum wake duration in 256us units (10 = 2.56ms)
  twt_config.wake_invl_expn = 10; // Wake interval exponent (2^10 = 1024)
  twt_config.wake_invl_mant =
      512;                // Wake interval mantissa (1024 * 512us = 524ms)
  twt_config.trigger = 1; // Trigger-enabled TWT
  twt_config.timeout_time_ms = 3000; // REDUCED: 3 second timeout (was 5s)

  esp_err_t err = esp_wifi_sta_itwt_setup(&twt_config);

  if (err == ESP_OK) {
    _twtActive = true;
    Serial.println("[TWT] Setup successful! Power saving active.");
    Serial.printf("[TWT] Wake interval: ~500ms, min wake: 2.56ms\n");
  } else if (err == ESP_ERR_NOT_SUPPORTED || err == ESP_ERR_INVALID_STATE) {
    // ESP_ERR_NOT_SUPPORTED: Router doesn't support TWT
    // ESP_ERR_INVALID_STATE: WiFi not ready or TWT not available
    Serial.println("[TWT] Router does not support TWT (WiFi 6 required)");
    _twtActive = false;
    // FIX Issue #12: Don't disable in config permanently, just for this session
    // config.twt_enabled = false;
    // storage.save();
    Serial.println(
        "[TWT] TWT disabled for this session (will retry on next boot)");
  } else {
    Serial.printf("[TWT] Setup failed: %s\n", esp_err_to_name(err));
    _twtActive = false;

    // Check if connection was disrupted by TWT attempt
    // NOTE: No blocking delay here — the next update() cycle will
    // detect the disconnection and handle reconnection properly.
    if (wasConnected && WiFi.status() != WL_CONNECTED) {
      Serial.println(
          "[TWT] Connection lost during TWT setup - will reconnect via normal path");
      config.twt_enabled = false; // Disable TWT
      storage.save();

      // Let the normal reconnection state machine handle it
      _connected = false;
      _backoffMultiplier = 1;
      _consecutiveFailures = 0;
    }
  }
#else
  Serial.println("[TWT] Not supported on this chip (requires ESP32-C6 WiFi 6)");
#endif
}

void AsyncNetwork::disableTWT() {
  if (!_twtActive) {
    Serial.println("[TWT] Already inactive");
    return;
  }

#if defined(CONFIG_SOC_WIFI_HE_SUPPORT) && CONFIG_SOC_WIFI_HE_SUPPORT
  Serial.println("[TWT] Tearing down TWT...");

  // Teardown TWT agreement
  esp_err_t err = esp_wifi_sta_itwt_teardown(0); // flow_id = 0

  if (err == ESP_OK) {
    Serial.println("[TWT] Teardown successful");
  } else {
    Serial.printf("[TWT] Teardown error: %s\n", esp_err_to_name(err));
  }
#endif

  _twtActive = false;
}

bool AsyncNetwork::checkInternetConnectivity() {
  if (WiFi.status() != WL_CONNECTED)
    return false;

  HTTPClient http;
  // Use a reliable, high-uptime endpoint that returns 204 No Content
  // (lightweight) Google's generate_204 is industry standard for captive portal
  // checks
  bool success = false;

  Serial.println("[Internet] Pinging http://clients3.google.com/generate_204");

  if (http.begin("http://clients3.google.com/generate_204")) {
    http.setTimeout(1500); // 1.5s timeout
    int code = http.GET();
    if (code == 204) {
      success = true;
    } else {
      Serial.printf("[Internet] Check failed: HTTP %d\n", code);
    }
    http.end();
  } else {
    Serial.println("[Internet] HTTP begin failed");
  }

  return success;
}

void AsyncNetwork::reportNetworkFailure() {
  if (!_connected)
    return;

  _consecutiveInternetFailures++;
  Serial.printf("[AsyncNetwork] External failure reported (count: %d)\n",
                _consecutiveInternetFailures);

  // If we have distinct failures, trigger an immediate check
  if (_consecutiveInternetFailures >= 5) {
    Serial.println("[AsyncNetwork] Frequent external failures - triggering "
                   "internet check...");
    if (checkInternetConnectivity()) {
      _consecutiveInternetFailures = 0;
      _lastValidActivity = millis();
    } else {
      // Force the checkConnectivity loop to see the failure next time it runs
      _lastValidActivity = 0; // Mark stale immediately
    }
  }
}

void AsyncNetwork::reportNetworkSuccess() {
  if (_consecutiveInternetFailures > 0) {
    _consecutiveInternetFailures = 0;
  }
  // Also treat MQTT success as valid network activity
  _lastValidActivity = millis();
}

void AsyncNetwork::fetchWeatherIfNeeded() {
  if (!_connected || !_hasTime) return;
  // Guard: if internet is confirmed down, don't waste 2s waiting for HTTP timeout
  if (_consecutiveInternetFailures >= 3) return;
  
  unsigned long now = millis();
  
  // If we already have the weather, update every 6 hours
  if (_sunriseTime != 0 && _sunsetTime != 0) {
      if (now - _lastWeatherFetch < 6 * 3600 * 1000) return;
  } else {
      // If we don't have the weather (or failed), retry every 5 minutes so we don't spam the API
      if (_lastWeatherFetch != 0 && now - _lastWeatherFetch < 300000) return;
  }
  
  _lastWeatherFetch = now; // Prevent spamming
  
  Serial.println("[Weather] Fetching from OpenWeatherMap...");
  HTTPClient http;
  
  String url = "http://api.openweathermap.org/data/2.5/weather?q="
               + String(SECRET_CITY)
               + "&appid=" + String(SECRET_WEATHER_API_KEY)
               + "&units=metric";
  http.setTimeout(2000); // Reduced from 5000 to prevent blocking main loop
  if (http.begin(url)) {
    int httpCode = http.GET();
    if (httpCode == 200) {
      String payload = http.getString();
      JsonDocument doc;
      DeserializationError error = deserializeJson(doc, payload);
      if (!error && doc.containsKey("sys")) {
        _sunriseTime = doc["sys"]["sunrise"];
        _sunsetTime = doc["sys"]["sunset"];
        Serial.printf("[Weather] Synced! Sunrise: %lu, Sunset: %lu\n", _sunriseTime, _sunsetTime);
        
        if (doc.containsKey("timezone")) {
          long tzOffset = doc["timezone"];
          Serial.printf("[Weather] Timezone offset from API: %ld seconds\n", tzOffset);
          
          // FEAT-5: Skip timezone override when POSIX TZ is active.
          // POSIX TZ already encodes DST rules; applying the weather API's
          // static offset would conflict with automatic DST handling.
          if (strlen(config.tz_posix) > 0) {
            Serial.println("[Weather] POSIX TZ active — skipping static offset update");
          } else {
            // Fallback: use weather API offset for devices without POSIX TZ
            bool tzChanged = (config.gmt_offset != tzOffset || config.daylight_offset != 0);
            config.gmt_offset = tzOffset;
            config.daylight_offset = 0; // API offset already includes DST
            
            configTime(config.gmt_offset, config.daylight_offset, ntpServer1, ntpServer2, ntpServer3);
            
            // Persist only if timezone actually changed (avoid EEPROM wear)
            if (tzChanged) {
              storage.save();
              Serial.println("[Weather] Timezone saved to config");
            }
          }
        }
      }
    } else {
      Serial.printf("[Weather] HTTP failed: %d\n", httpCode);
    }
    http.end();
  } else {
    Serial.println("[Weather] HTTP begin failed");
  }
}

void AsyncNetwork::reapplyStabilitySettings() {
  WiFi.setSleep(false);
  WiFi.setTxPower(WIFI_POWER_17dBm);

#if SECRET_USE_STATIC_IP
  IPAddress staticIP, gateway, subnet, dns1;
  staticIP.fromString(SECRET_STATIC_IP);
  gateway.fromString(SECRET_GATEWAY);
  subnet.fromString(SECRET_SUBNET);
  dns1.fromString(SECRET_DNS);
  WiFi.config(staticIP, gateway, subnet, dns1);
#endif
}

void AsyncNetwork::connectWithCredentials() {
  // Use config-stored credentials directly (no String heap allocations)
  const char* ssid = strlen(config.wifi_ssid) > 0 ? config.wifi_ssid : nullptr;
  const char* pass = strlen(config.wifi_pass) > 0 ? config.wifi_pass : nullptr;

  if (ssid) {
    if (pass && strlen(pass) > 0) {
      WiFi.begin(ssid, pass);
    } else {
      WiFi.begin(ssid);
    }
    Serial.println("[AsyncNetwork] WiFi.begin() with config credentials");
  } else {
    // Fallback: let ESP32 use NVS-stored credentials
    WiFi.begin();
    Serial.println("[AsyncNetwork] WiFi.begin() with NVS credentials");
  }
}
