#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <WiFiManager.h>
#include <Preferences.h>
#include <string.h>

#include <MicroOcpp.h>
#include <MicroOcppMongooseClient.h>
#include <MicroOcpp/Core/Context.h>

#include <Adafruit_SSD1306.h>
#include <SmartLeds.h>
#include <MFRC522.h>

#include "esp_system.h"
#include "config.h"
#include "pins.h"

/* LED State Enum */
enum LEDState
{
  LED_INITIALIZING,   // Blue blinking - Initialization and WiFi connecting
  LED_WIFI_CONNECTED, // Blue solid - WiFi connected, connecting to OCPP server
  LED_OCPP_CONNECTED, // Green solid - Successfully connected to OCPP server
  LED_ERROR,          // Red solid - Error state
  LED_RESET_TX,       // Yellow solid - 3s BOOT button hold (Ready to clear transaction)
  LED_FACTORY_RESET   // Magenta fast blink - 7s BOOT button hold (Ready to factory reset)
};

static int s_retry_num = 0;
static volatile bool s_ocpp_connected = false;
static volatile LEDState s_led_state = LED_INITIALIZING;
static volatile unsigned long s_blink_last_time = 0;
static volatile bool s_blink_on = false;
static const unsigned long BLINK_INTERVAL = 200; // 200ms blink interval
static const unsigned long CC_DEBOUNCE_MS = 30;
static const unsigned long AUTH_WINDOW_MS = 30000;

static bool s_cc1_plugged = false;
static bool s_cc2_plugged = false;
static bool s_cc1_raw_last = false;
static bool s_cc2_raw_last = false;
static unsigned long s_cc1_last_change_ms = 0;
static unsigned long s_cc2_last_change_ms = 0;
static bool s_cc1_prev_plugged = false;
static bool s_cc2_prev_plugged = false;

static bool s_auth_in_progress = false;
static bool s_auth_ok = false;
static unsigned long s_auth_ok_at_ms = 0;
static String s_auth_id_tag;

static bool s_remote_start_accepted = false;

static bool authWindowValid();
static void clearAuthWait(const char *reason)
{
  if (s_auth_ok || s_auth_id_tag.length() > 0)
  {
    Serial.printf("[main] Clear pending authorization: %s\n", reason);
  }
  s_auth_ok = false;
  s_auth_ok_at_ms = 0;
  s_auth_id_tag = "";
}

uint8_t mac[6];
char cpSerial[13];

// OCPP Configuration Variables
char ocpp_backend[128];
char cp_identifier[64];
char ocpp_password[64];
bool shouldSaveConfig = false;

// callback notifying us of the need to save config
void saveConfigCallback()
{
  Serial.println("Should save config");
  shouldSaveConfig = true;
}

struct mg_mgr mgr;

Adafruit_SSD1306 display(128, 64, &Wire, -1);

/**
 * WS2812B LED Pin
 * - GPIO 17 - RYMCU ESP32-DevKitC
 * - GPIO 16 - YD-ESP32-A
 */
#define LED_PIN 17
#define LED_COUNT 1

SmartLed leds(LED_WS2812B, LED_COUNT, LED_PIN, 0, DoubleBuffer);
MFRC522 rfid(PIN_RC_CS, PIN_RC_RST);

static bool isConnectorPlugged(unsigned int connectorId)
{
  if (connectorId == 1)
    return s_cc1_plugged;
  if (connectorId == 2)
    return s_cc2_plugged;
  return false;
}

static bool isConnectorIdle(unsigned int connectorId)
{
  return !isTransactionActive(connectorId) && !isTransactionRunning(connectorId) && !getTransaction(connectorId);
}

static bool isConnectorStartReady(unsigned int connectorId)
{
  return connectorId >= 1 && connectorId <= 2 && isOperative(connectorId) && isConnectorIdle(connectorId) && isConnectorPlugged(connectorId);
}

static void updateConnectorPluggedState()
{
  unsigned long now = millis();

  const bool cc1_raw = (digitalRead(PIN_CC1) == HIGH);
  if (cc1_raw != s_cc1_raw_last)
  {
    s_cc1_raw_last = cc1_raw;
    s_cc1_last_change_ms = now;
  }
  if ((now - s_cc1_last_change_ms) >= CC_DEBOUNCE_MS)
  {
    bool cc1_plugged_old = s_cc1_plugged;
    s_cc1_plugged = s_cc1_raw_last;

    // If connector1 just connected and auth window is valid, try to start
    if (!cc1_plugged_old && s_cc1_plugged && s_auth_ok && (millis() - s_auth_ok_at_ms) <= AUTH_WINDOW_MS && s_auth_id_tag.length() > 0 && isConnectorIdle(1))
    {
      Serial.printf("[main] Connector 1 plugged in, auto-starting with idTag %s\n", s_auth_id_tag.c_str());
      auto tx = beginTransaction_authorized(s_auth_id_tag.c_str(), nullptr, 1);
      if (tx != nullptr)
      {
        s_auth_ok = false;
        s_auth_id_tag = "";
      }
    }
  }

  const bool cc2_raw = (digitalRead(PIN_CC2) == HIGH);
  if (cc2_raw != s_cc2_raw_last)
  {
    s_cc2_raw_last = cc2_raw;
    s_cc2_last_change_ms = now;
  }
  if ((now - s_cc2_last_change_ms) >= CC_DEBOUNCE_MS)
  {
    bool cc2_plugged_old = s_cc2_plugged;
    s_cc2_plugged = s_cc2_raw_last;

    // If connector2 just connected and auth window is valid, try to start
    if (!cc2_plugged_old && s_cc2_plugged && s_auth_ok && (millis() - s_auth_ok_at_ms) <= AUTH_WINDOW_MS && s_auth_id_tag.length() > 0 && isConnectorIdle(2))
    {
      Serial.printf("[main] Connector 2 plugged in, auto-starting with idTag %s\n", s_auth_id_tag.c_str());
      auto tx = beginTransaction_authorized(s_auth_id_tag.c_str(), nullptr, 2);
      if (tx != nullptr)
      {
        s_auth_ok = false;
        s_auth_id_tag = "";
      }
    }
  }
}

static void updatePanelLedsFromPlugState()
{
  // Reserved for startup sync; runtime LED behavior is tied to charging permission.
  digitalWrite(PIN_LED1, HIGH);
  digitalWrite(PIN_LED2, HIGH);
}

static void updateChargeActuators()
{
  const bool chg1_on = ocppPermitsCharge(1);
  const bool chg2_on = ocppPermitsCharge(2);

  // LEDs and relays are low-active
  digitalWrite(PIN_LED1, chg1_on ? LOW : HIGH);
  digitalWrite(PIN_LED2, chg2_on ? LOW : HIGH);
  digitalWrite(PIN_RELAY1, chg1_on ? LOW : HIGH);
  digitalWrite(PIN_RELAY2, chg2_on ? LOW : HIGH);
}

static void stopIfUnplugged()
{
  if (s_cc1_prev_plugged && !s_cc1_plugged && (isTransactionActive(1) || isTransactionRunning(1)))
  {
    Serial.println("[main] Connector 1 unplugged. Stop transaction immediately.");
    endTransaction(nullptr, "EVDisconnected", 1);
  }

  if (s_cc2_prev_plugged && !s_cc2_plugged && (isTransactionActive(2) || isTransactionRunning(2)))
  {
    Serial.println("[main] Connector 2 unplugged. Stop transaction immediately.");
    endTransaction(nullptr, "EVDisconnected", 2);
  }

  s_cc1_prev_plugged = s_cc1_plugged;
  s_cc2_prev_plugged = s_cc2_plugged;
}

static bool authWindowValid()
{
  return s_auth_ok && (millis() - s_auth_ok_at_ms) <= AUTH_WINDOW_MS && s_auth_id_tag.length() > 0;
}

static void requestAuthorizeByCard(const String &idTag)
{
  if (s_auth_in_progress)
  {
    return;
  }

  clearAuthWait("new card swiped");
  s_auth_in_progress = true;

  Serial.printf("[main] Authorize idTag: %s\n", idTag.c_str());
  authorize(
      idTag.c_str(),
      [idTag](JsonObject payload)
      {
        s_auth_in_progress = false;
        const char *status = payload["idTagInfo"]["status"] | "";
        if (!strcmp(status, "Accepted"))
        {
          s_auth_ok = true;
          s_auth_ok_at_ms = millis();
          s_auth_id_tag = idTag;
          Serial.printf("[main] Authorize accepted for idTag %s\n", idTag.c_str());

          // Check if there's already a connector plugged in; if so, start immediately
          unsigned int targetConnector = 0;
          if (s_cc1_plugged && isConnectorIdle(1) && isOperative(1))
          {
            targetConnector = 1;
          }
          else if (s_cc2_plugged && isConnectorIdle(2) && isOperative(2))
          {
            targetConnector = 2;
          }

          if (targetConnector > 0)
          {
            // Immediately start the transaction on the plugged connector
            Serial.printf("[main] Connector %u is plugged in, auto-starting transaction\n", targetConnector);
            auto tx = beginTransaction_authorized(idTag.c_str(), nullptr, targetConnector);
            if (tx != nullptr)
            {
              clearAuthWait("transaction started");
            }
          }
          // Otherwise, wait for a connector to be plugged in
        }
        else
        {
          clearAuthWait("authorize rejected");
          Serial.printf("[main] Authorize rejected, status=%s\n", status);
        }
      },
      []()
      {
        s_auth_in_progress = false;
        clearAuthWait("authorize aborted");
        Serial.println("[main] Authorize aborted");
      });
}

static void pollRfidCard()
{
  if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial())
  {
    return;
  }

  String idTag;
  for (byte i = 0; i < rfid.uid.size; i++)
  {
    if (rfid.uid.uidByte[i] < 0x10)
    {
      idTag += '0';
    }
    idTag += String(rfid.uid.uidByte[i], HEX);
  }
  idTag.toUpperCase();

  rfid.PICC_HaltA();
  rfid.PCD_StopCrypto1();

  requestAuthorizeByCard(idTag);
}

static void expireAuthWaitIfNeeded()
{
  if (s_auth_ok && s_auth_id_tag.length() > 0 && (millis() - s_auth_ok_at_ms) > AUTH_WINDOW_MS)
  {
    clearAuthWait("authorization timeout");
  }
}

/* LED Control Functions */
void updateLED()
{
  unsigned long current_time = millis();

  switch (s_led_state)
  {
  case LED_INITIALIZING:
    // Blue blinking during initialization
    if (current_time - s_blink_last_time >= BLINK_INTERVAL)
    {
      s_blink_last_time = current_time;
      s_blink_on = !s_blink_on;

      if (s_blink_on)
      {
        leds[0] = Rgb{0, 0, 255}; // Blue on
      }
      else
      {
        leds[0] = Rgb{0, 0, 0}; // Off
      }
      leds.show();
    }
    break;

  case LED_WIFI_CONNECTED:
    // Blue solid - WiFi connected, OCPP connecting
    leds[0] = Rgb{0, 0, 255}; // Blue solid
    leds.show();
    break;

  case LED_OCPP_CONNECTED:
    // Green solid - OCPP connected
    leds[0] = Rgb{0, 255, 0}; // Green solid
    leds.show();
    break;

  case LED_ERROR:
    // Red solid - Error state
    leds[0] = Rgb{255, 0, 0}; // Red solid
    leds.show();
    break;

  case LED_RESET_TX:
    // Yellow fast blink - Ready to clear transaction
    if (current_time - s_blink_last_time >= 100)
    {
      s_blink_last_time = current_time;
      s_blink_on = !s_blink_on;

      if (s_blink_on)
        leds[0] = Rgb{150, 150, 0}; // Yellow
      else
        leds[0] = Rgb{0, 0, 0};

      leds.show();
    }
    break;

  case LED_FACTORY_RESET:
    // Magenta fast blink - Ready to factory reset
    if (current_time - s_blink_last_time >= 100)
    {
      s_blink_last_time = current_time;
      s_blink_on = !s_blink_on;

      if (s_blink_on)
        leds[0] = Rgb{255, 0, 255}; // Magenta
      else
        leds[0] = Rgb{0, 0, 0};

      leds.show();
    }
    break;
  }
}

void setup()
{
  // Get MAC address and set as Charge Point Serial Number
  esp_efuse_mac_get_default(mac);
  snprintf(cpSerial, sizeof(cpSerial),
           "%02X%02X%02X%02X%02X%02X",
           mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);

  if (strlen(CFG_CP_IDENTIFIER) > 0)
  {
    strncpy(cp_identifier, CFG_CP_IDENTIFIER, sizeof(cp_identifier) - 1);
    cp_identifier[sizeof(cp_identifier) - 1] = '\0';
  }
  else
  {
    // Auto-generate Charge Point Identifier based on MAC (e.g. HLCP_A1B2C3)
    snprintf(cp_identifier, sizeof(cp_identifier), "HLCP_%s", cpSerial + 6);
  }

  // reset LED
  leds[0] = Rgb{0, 0, 0};
  leds.show();
  // initialize Serial
  Serial.begin(115200);
  delay(1000);
  Serial.printf("\n\n%s(%s) made by %s\n", CFG_CP_MODAL, cpSerial, CFG_CP_VENDOR);
  Serial.printf("Charge Point Identifier: %s\n", cp_identifier);
  Serial.println("Initializing firmware...\n");

  // Initialize LED
  s_led_state = LED_INITIALIZING;
  s_blink_last_time = 0;
  s_blink_on = false;

  // Initialize CC switches (input) and panel LEDs (low-active output)
  pinMode(PIN_CC1, INPUT);
  pinMode(PIN_CC2, INPUT);
  pinMode(PIN_LED1, OUTPUT);
  pinMode(PIN_LED2, OUTPUT);
  pinMode(PIN_RELAY1, OUTPUT);
  pinMode(PIN_RELAY2, OUTPUT);
  digitalWrite(PIN_LED1, HIGH);   // Off by default (low-active)
  digitalWrite(PIN_LED2, HIGH);   // Off by default (low-active)
  digitalWrite(PIN_RELAY1, HIGH); // Off by default (low-active)
  digitalWrite(PIN_RELAY2, HIGH); // Off by default (low-active)
  updateConnectorPluggedState();
  updatePanelLedsFromPlugState();

  leds[0] = Rgb{255, 255, 0};
  leds.show();

  // Initialize I2C for OLED (from schematic pin map)
  Wire.begin(PIN_OLED_SDA, PIN_OLED_SCL);

  // Initialize UART2 for IM1281C (U2)
  Serial2.begin(BAUD_IM1281C, SERIAL_8N1, PIN_U2RXD, PIN_U2TXD);

  // Initialize SPI bus for RC522
  SPI.begin(PIN_RC_SCK, PIN_RC_MISO, PIN_RC_MOSI, PIN_RC_CS);
  pinMode(PIN_RC_CS, OUTPUT);
  digitalWrite(PIN_RC_CS, HIGH);
  pinMode(PIN_RC_RST, OUTPUT);
  digitalWrite(PIN_RC_RST, HIGH);
  rfid.PCD_Init();

  // Load configuration from Preferences
  Preferences preferences;
  preferences.begin("ocpp-config", false);
  String b = preferences.getString("backend", CFG_OCPP_BACKEND);
  String p = preferences.getString("ocpp_password", CFG_OCPP_PASSWORD ? CFG_OCPP_PASSWORD : "");

  Serial.printf("\n[OCPP] Loaded Backend URL: %s\n", b.c_str());
  Serial.printf("[OCPP] Loaded Password length: %d\n", p.length());

  strncpy(ocpp_backend, b.c_str(), sizeof(ocpp_backend) - 1);
  ocpp_backend[sizeof(ocpp_backend) - 1] = '\0';
  strncpy(ocpp_password, p.c_str(), sizeof(ocpp_password) - 1);
  ocpp_password[sizeof(ocpp_password) - 1] = '\0';

  WiFiManager wm;
  wm.setSaveConfigCallback(saveConfigCallback);
  wm.setSaveParamsCallback(saveConfigCallback);
  wm.setParamsPage(true);

  // Use autocomplete=off to prevent browsers from autofilling old URLs after a reset
  WiFiManagerParameter custom_ocpp_backend("backend", "OCPP Backend URL", ocpp_backend, 128, "autocomplete=\"off\"");
  WiFiManagerParameter custom_ocpp_password("ocpp_password", "OCPP Basic AuthKey", ocpp_password, 64, "autocomplete=\"off\" type=\"password\"");

  wm.addParameter(&custom_ocpp_backend);
  wm.addParameter(&custom_ocpp_password);

  const char *customHeadElement = R"rawliteral(
    <style>
      :root {
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        color: #1f2a37;
        --primarycolor: #2563eb;
      }

      body,
      body.invert {
        margin: 0;
        background: #f4f6fb;
        min-height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        padding: 16px;
        color: #1f2a37;
      }

      .wrap {
        width: min(360px, 100%);
        background: #fff;
        border-radius: 16px;
        padding: 24px;
        box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
        text-align: left;
      }

      h1,
      h2,
      h3,
      h4 {
        color: #111827;
        margin-top: 0;
        text-align: center;
      }

      label {
        display: block;
        margin-bottom: 6px;
        font-weight: 600;
      }

      input,
      select,
      button {
        border-radius: 10px;
        width: 100%;
        font-size: 1rem;
        box-sizing: border-box;
      }

      input[type="text"],
      input[type="password"],
      input[type="number"],
      input[type="email"],
      select {
        padding: 12px;
        margin: 0 0 16px 0;
        border: 1px solid #d0d7e2;
        background: #f9fafc;
        transition: border 0.2s, box-shadow 0.2s, background 0.2s;
      }

      input:focus,
      select:focus {
        outline: none;
        border-color: #2563eb;
        background: #fff;
        box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
      }

      button,
      input[type="submit"],
      button.D {
        padding: 12px;
        border: none;
        background: #2563eb;
        color: #fff;
        font-weight: 600;
        cursor: pointer;
        transition: background 0.2s, transform 0.1s;
      }

      button.D {
        background: #dc2626;
      }

      button:hover,
      input[type="submit"]:hover {
        background: #1d4ed8;
      }

      button:active,
      input[type="submit"]:active {
        transform: scale(0.99);
      }

      .msg {
        border-radius: 12px;
        border: 1px solid #e5e7eb;
        background: #f9fafb;
        padding: 16px;
        color: #374151;
      }

      .msg.P {
        border-color: #2563eb;
        background: rgba(37, 99, 235, 0.08);
        color: #1f2a37;
      }

      .msg.S {
        border-color: #16a34a;
        background: rgba(22, 163, 74, 0.08);
      }

      .msg.D {
        border-color: #dc2626;
        background: rgba(220, 38, 38, 0.08);
      }

      .q {
        display: none;
      }

      a {
        color: #2563eb;
        font-weight: 600;
      }

      hr {
        border: none;
        height: 1px;
        background: #e5e7eb;
        margin: 24px 0;
      }
    </style>
  )rawliteral";
  wm.setCustomHeadElement(customHeadElement);
  bool autoConnectRet = wm.autoConnect(cp_identifier, cpSerial);

  if (shouldSaveConfig)
  {
    strncpy(ocpp_backend, custom_ocpp_backend.getValue(), sizeof(ocpp_backend) - 1);
    ocpp_backend[sizeof(ocpp_backend) - 1] = '\0';
    strncpy(ocpp_password, custom_ocpp_password.getValue(), sizeof(ocpp_password) - 1);
    ocpp_password[sizeof(ocpp_password) - 1] = '\0';

    preferences.putString("backend", ocpp_backend);
    preferences.putString("ocpp_password", ocpp_password);
    Serial.println("Saved new OCPP config to Preferences");
  }
  preferences.end();

  if (!autoConnectRet)
  {
    Serial.println("Failed to connect and hit timeout");
    s_led_state = LED_ERROR;
  }
  else
  {
    Serial.println("WiFi connected successfully");
    s_led_state = LED_WIFI_CONNECTED;

    mg_mgr_init(&mgr);
    const char *basic_auth_password = (strlen(ocpp_password) > 0) ? ocpp_password : nullptr;
    unsigned char *basic_auth_password_bytes = nullptr;
    size_t basic_auth_password_len = 0;

    if (basic_auth_password)
    {
      basic_auth_password_bytes = reinterpret_cast<unsigned char *>(const_cast<char *>(basic_auth_password));
      basic_auth_password_len = strlen(basic_auth_password);
    }

    MicroOcpp::MOcppMongooseClient *client = new MicroOcpp::MOcppMongooseClient(
        &mgr,
        ocpp_backend,
        cp_identifier,
        nullptr,
        0,
        "",
        MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail),
        MicroOcpp::ProtocolVersion(1, 6));

    // Preferences and firmware config are the source of truth. Override any stale
    // values cached in MicroOcpp's ws-conn storage before the first reconnect cycle.
    client->setBackendUrl(ocpp_backend);
    client->setChargeBoxId(cp_identifier);
    if (basic_auth_password_bytes)
    {
      client->setAuthKey(basic_auth_password_bytes, basic_auth_password_len);
    }
    else
    {
      client->setAuthKey(reinterpret_cast<const unsigned char *>(""), 0);
    }
    client->reloadConfigs();

    mocpp_initialize(*client, ChargerCredentials(CFG_CP_MODAL, CFG_CP_VENDOR, CFG_CP_FW_VERSION, cpSerial, nullptr, nullptr, CFG_CB_SERIAL, nullptr, nullptr), MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail));

    // Expose both physical connectors to CSMS and feed live plug-state from CC switches.
    // connectorId 1 <-> CC1, connectorId 2 <-> CC2
    setConnectorPluggedInput([]()
                             { return s_cc1_plugged; },
                             1);
    setConnectorPluggedInput([]()
                             { return s_cc2_plugged; },
                             2);

    // Occupied state drives StatusNotification (Available <-> Preparing/Finishing)
    // to report plug-in / unplug events even without an active transaction.
    setOccupiedInput([]()
                     { return s_cc1_plugged; },
                     1);
    setOccupiedInput([]()
                     { return s_cc2_plugged; },
                     2);

    // Custom RemoteStartTransaction policy:
    // accept only when target connector is idle + operative + plugged.
    setRequestHandler(
        "RemoteStartTransaction",
        [](JsonObject payload)
        {
          s_remote_start_accepted = false;

          const char *idTag = payload["idTag"] | "";
          if (!idTag || !*idTag)
          {
            return;
          }

          int reqConnectorId = payload["connectorId"] | -1;
          unsigned int targetConnector = 0;

          if (reqConnectorId >= 1 && reqConnectorId <= 2)
          {
            if (isConnectorStartReady((unsigned int)reqConnectorId))
            {
              targetConnector = (unsigned int)reqConnectorId;
            }
          }
          else
          {
            for (unsigned int cid = 1; cid <= 2; cid++)
            {
              if (isConnectorStartReady(cid))
              {
                targetConnector = cid;
                break;
              }
            }
          }

          if (targetConnector == 0)
          {
            return;
          }

          auto tx = beginTransaction_authorized(idTag, nullptr, targetConnector);
          s_remote_start_accepted = (tx != nullptr);
          if (s_remote_start_accepted)
          {
            Serial.printf("[main] Remote start accepted on connector %u\n", targetConnector);
          }
        },
        []() -> std::unique_ptr<MicroOcpp::JsonDoc>
        {
          auto doc = std::unique_ptr<MicroOcpp::JsonDoc>(new MicroOcpp::JsonDoc(JSON_OBJECT_SIZE(1)));
          JsonObject payload = doc->to<JsonObject>();
          payload["status"] = s_remote_start_accepted ? "Accepted" : "Rejected";
          return doc;
        });

    // For development/recovery: Set up BOOT button (GPIO 0)
    pinMode(0, INPUT_PULLUP);

    // Forcefully accept rejected RemoteStopTransaction (if hardware goes out of sync with CSMS)
    setOnSendConf("RemoteStopTransaction", [](JsonObject payload)
                  {
        if (!strcmp(payload["status"], "Rejected")) {
          unsigned int connectorId = payload["connectorId"] | 1;
          if (connectorId < 1 || connectorId > 2)
          {
            connectorId = 1;
          }
          Serial.printf("[main] MicroOcpp rejected RemoteStopTransaction on connector %u. Force overriding and stopping charging...\n", connectorId);
          endTransaction(nullptr, "Remote", connectorId);
        } });
  }
}

void loop()
{
  updateConnectorPluggedState();
  stopIfUnplugged();
  pollRfidCard();
  expireAuthWaitIfNeeded();

  mg_mgr_poll(&mgr, 10);
  mocpp_loop();
  updateChargeActuators();

  // Handle BOOT button (GPIO 0) interactions for recovery
  bool is_btn_pressed = (digitalRead(0) == LOW);
  static unsigned long boot_press_time = 0;
  static bool boot_was_pressed = false;

  if (is_btn_pressed)
  {
    if (!boot_was_pressed)
    {
      boot_was_pressed = true;
      boot_press_time = millis();
    }

    unsigned long held_time = millis() - boot_press_time;
    if (held_time >= 7000)
    {
      s_led_state = LED_FACTORY_RESET;
    }
    else if (held_time >= 3000)
    {
      s_led_state = LED_RESET_TX;
    }
  }
  else
  {
    if (boot_was_pressed)
    {
      unsigned long held_time = millis() - boot_press_time;
      if (held_time >= 7000)
      {
        Serial.println("BOOT button held for > 7s! Clearing WiFi and OCPP settings, then restarting...");

        // Clear WiFi completely
        WiFi.disconnect(true, true);
        WiFiManager wm;
        wm.resetSettings();

        // Clear Preferences explicitely
        Preferences preferences;
        preferences.begin("ocpp-config", false);
        preferences.remove("backend");
        preferences.remove("ocpp_password");
        preferences.clear();
        preferences.end();
        Serial.println("NVS ocpp-config cleared.");

        // Clear MicroOcpp FS configs (this removes MO's cached URL)
        auto fs = MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail);
        fs->remove(MO_WSCONN_FN);
        Serial.println("MicroOcpp config cache cleared.");

        // Give time for serial to print and NVS to sync
        delay(1000);
        ESP.restart();
      }
      else if (held_time >= 3000)
      {
        Serial.println("BOOT button held for > 3s! Forcefully ending dangling transactions on connector 1 and 2...");
        endTransaction(nullptr, "PowerLoss", 1);
        endTransaction(nullptr, "PowerLoss", 2);
      }
      boot_was_pressed = false;
      // Temporarily set to init so the logic below restores the actual network state accurately
      s_led_state = LED_INITIALIZING;
    }
  }

  // Only update default LED states if button is not overriding them
  if (!is_btn_pressed)
  {
    auto ctx = getOcppContext();
    if (ctx && ctx->getConnection().isConnected())
    {
      if (s_led_state != LED_OCPP_CONNECTED)
      {
        s_led_state = LED_OCPP_CONNECTED;
      }
    }
    else
    {
      if (s_led_state != LED_WIFI_CONNECTED)
      {
        s_led_state = LED_WIFI_CONNECTED;
      }
    }
  }

  updateLED();

  delay(10);
}
