#include "my_wifi.h"
#include <Wifi.h>
#include <esp_wifi.h>
#include <esp_task_wdt.h>
#include <ESP32Ping.h>
#include <ESPmDNS.h>
#include <DNSServer.h>
#include "esp_log.h"
#include <FS.h>
#include <LittleFS.h>
#include <SD.h>
#include "common/fileSystem.h"
#include "global.h"
#include "my_buzzer.h"
#include <UriDecode.h>
#include <ArduinoJson.h>
#include "jsonconstrain.h"
#include "AppUpgrade.h"
#include "my_board.h"

static const char *tag = "WIFI";

volatile bool InternetAvailable;
AsyncWebServer webServer(80);
AsyncEventSource eventUpgradeProgress("/upgrade-progress");
// DNSServer *dnsServer;
// #define DNS_PORT 53

String client_ssid;
String client_pass;
String ap_ssid;
String ap_pass;

String mDnsName;
String HostName;

IPAddress local_IP(192, 168, 10, 1);
IPAddress gateway(192, 168, 10, 1);
IPAddress subnet(255, 255, 255, 0);

// for file manager page
String filesDropdownOptions((char *)0);
String dirDropdownOptions((char *)0);
String savePath((char *)0); // needed for storing file when editing a file
String savePathInput((char *)0);
const char *http_username = "admin";
const char *http_password = "admin";
const char *param_delete_path = "delete-path";
const char *param_edit_path = "edit-path";
const char *param_dir_pad = "dir-path";
const char *param_edit_textarea = "edit-textarea";
const char *param_save_path = "save-path";
String allowedExtensionsForEdit = "txt, h, htm, html, css, cpp, js, json, ini, cfg";

volatile bool scanInProgress = false;

static String networkList = "";
int networkCount = 0;

volatile int scanStatus = 0; // 0=none, 1=scanning, 2=complete, -1=error
String scanResults = "";     // Store scan results globally

TaskHandle_t Wifi_Task_Handle;
static const uint32_t CONNECT_TIMEOUT_MS = 20000;
static const uint32_t ATTEMPT_DELAY_MS = 2000;
static const uint8_t MAX_ATTEMPTS = 10;
static SemaphoreHandle_t wifiMutex = nullptr;
volatile bool wifi_task_running = false;

void Wifi_Init()
{
    // Initialize LittleFS
    if (!LittleFS.begin(true))
    {
        ESP_LOGE(tag, "LittleFS mount failed");
        return;
    }

    Wifi_Load_Settings("/system/wifi.json");

    // Set Wi-Fi task to run on Core 1
    esp_wifi_set_ps(WIFI_PS_NONE); // Disable power save mode for better responsiveness

    // Set WiFi to AP+STA mode
    WiFi.mode(WIFI_MODE_APSTA);

    // Configure and start AP
    WiFi.softAPConfig(local_IP, gateway, subnet);
    if (!WiFi.softAP(ap_ssid, ap_pass))
    {
        ESP_LOGE(tag, "AP start failed");
        return;
    }

    // Add CORS headers
    DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
    DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, PUT");
    DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type");

    Setup_WebServer_Handlers(webServer);

    WiFi.onEvent(onWiFiEvent);
    WiFi.setHostname(mDnsName.c_str());
    webServer.begin();
    ESP_LOGI(tag, "AP started with IP: %s", WiFi.softAPIP().toString().c_str());

    StartWifiConnectTask(client_ssid, client_pass);

    // Wifi_Scan_for_Networks();
}

bool StartWifiConnectTask(String ssid = "", String pass = "")
{
    if (ssid.isEmpty() || pass.length() < 8)
    {
        ESP_LOGE(tag, "Invalid SSID or password");
        return false;
    }

    if (!wifi_task_running)
    {
        client_ssid = ssid;
        client_pass = pass;
        if (Wifi_Task_Handle == NULL)
        {
            ESP_LOGI(tag, "Creating WiFi task");
            xTaskCreatePinnedToCore(Wifi_ConnectTask, "Wifi_Task", 1024 * 4, NULL, 1, &Wifi_Task_Handle, 0);
        }
        else
        {
            ESP_LOGI(tag, "WiFi task already running");
        }

        return true;
    }
    else
    {
        ESP_LOGE(tag, "Task already running");
    }

    return false;
}

void Wifi_ConnectTask(void *parameter)
{
    static const char *tag = "Wifi_Task";
    wifi_task_running = true;

    if (WiFi.status() != WL_CONNECTED || client_ssid != WiFi.SSID())
    {
        ESP_LOGI(tag, "Connecting to: %s", client_ssid.c_str());

        // Disconnect and connect to new network
        WiFi.disconnect(true);
        WiFi.setAutoReconnect(false);
        WiFi.begin(client_ssid.c_str(), client_pass.c_str());

        // Wait for connection
        uint8_t attempts = 0;
        while (WiFi.status() != WL_CONNECTED && attempts < MAX_ATTEMPTS)
        {
            switch (WiFi.status())
            {
            case WL_NO_SSID_AVAIL:
                ESP_LOGW(tag, "SSID not found: %s", client_ssid.c_str());
                break;
            case WL_CONNECT_FAILED:
                ESP_LOGW(tag, "Connection failed - check password");
                break;
            default:
                ESP_LOGI(tag, "Connecting... (%d/%d)", attempts + 1, MAX_ATTEMPTS);
            }
            vTaskDelay(pdMS_TO_TICKS(ATTEMPT_DELAY_MS));
            attempts++;
        }

        // Check if connected
        if (WiFi.status() == WL_CONNECTED)
        {
            ESP_LOGI(tag, "Connected to %s", client_ssid.c_str());
            WiFi.setAutoReconnect(true);

            if (!Wifi_Save_Credentials("/system/wifi.json"))
            {
                ESP_LOGW(tag, "Failed to save credentials");
            }

            Wifi_Check_Internet();
        }
        else
        {
            ESP_LOGE(tag, "Failed to connect after %d attempts", MAX_ATTEMPTS);
        }
    }

    ESP_LOGI(tag, "Wifi Task ended");

    Wifi_Task_Handle = NULL;
    wifi_task_running = false;
    vTaskDelete(NULL);
}

void Wifi_Check_Internet()
{
    // Check for internet connection
    const char *host = "8.8.8.8"; // Google DNS server
    if (Ping.ping(host, 1))
    {
        InternetAvailable = true;
        ESP_LOGI(tag, "Internet connection verified");
    }
    else
    {
        InternetAvailable = false;
        ESP_LOGW(tag, "No internet connection");
    }
}

bool Wifi_Save_Credentials(String path)
{
    // Load existing JSON
    JsonDocument doc;
    File readFile = LittleFS.open(path, "r");
    if (readFile)
    {
        DeserializationError error = deserializeJson(doc, readFile);
        readFile.close();
        if (error)
        {
            ESP_LOGE(tag, "Failed to parse existing JSON");
            return false;
        }
    }

    // Update or create wifi-client section
    JsonObject wifiClient = doc["wifi-client"].to<JsonObject>();
    wifiClient["ssid"] = client_ssid;
    wifiClient["pass"] = client_pass;

    // Save updated JSON
    File writeFile = LittleFS.open(path, "w");
    if (!writeFile)
    {
        ESP_LOGE(tag, "Error opening %s for writing", path.c_str());
        return false;
    }

    // Serialize JSON with pretty formatting
    if (serializeJsonPretty(doc, writeFile) == 0)
    {
        ESP_LOGE(tag, "Failed to write JSON to file");
        writeFile.close();
        return false;
    }

    writeFile.close();
    return true;
}

void Wifi_Load_Settings(String path)
{
    // Load WiFi settings
    File file = LittleFS.open(path, "r");
    if (!file)
    {
        ESP_LOGE(tag, "Error opening %s", path.c_str());
        return;
    }

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

    if (error)
    {
        ESP_LOGE(tag, "Failed to deserialize %s", path.c_str());
        return;
    }

    JsonObject wifiJson = doc.as<JsonObject>();
    if (wifiJson.isNull())
    {
        ESP_LOGE(tag, "%s is empty", path.c_str());
        return;
    }

    // Load AP settings
    JsonObject apJson = wifiJson["wifi-ap"];
    if (!apJson.isNull())
    {
        ap_ssid = jsonConstrainString(tag, apJson, "ssid", "ATA-AP");
        ap_pass = jsonConstrainString(tag, apJson, "pass", "12345678");
        local_IP.fromString(jsonConstrainString(tag, apJson, "ip", "192.168.10.1"));
        gateway.fromString(jsonConstrainString(tag, apJson, "gateway", "192.168.10.1"));
        subnet.fromString(jsonConstrainString(tag, apJson, "subnet", "255.255.255.0"));
    }

    // Load Client settings
    JsonObject clientJson = wifiJson["wifi-client"];
    if (!apJson.isNull())
    {
        client_ssid = jsonConstrainString(tag, clientJson, "ssid", "none");
        client_pass = jsonConstrainString(tag, clientJson, "pass", "12345678");
    }
}

void Wifi_Scan_for_Networks()
{
    // Start a scan for available networks
    WiFi.scanNetworks(false, false);
    while (WiFi.scanComplete() == WIFI_SCAN_RUNNING)
    {
        vTaskDelay(100); // Wait for scan to complete
    }

    networkCount = WiFi.scanComplete();
    if (networkCount >= 0)
    {
        JsonDocument doc;
        JsonArray networks = doc["networks"].to<JsonArray>();

        for (int i = 0; i < networkCount; i++)
        {
            auto network = networks.add<JsonObject>();
            network["ssid"] = WiFi.SSID(i);
            network["rssi"] = WiFi.RSSI(i);
            network["encryption"] = WiFi.encryptionType(i) != WIFI_AUTH_OPEN;
        }

        String jsonString;
        serializeJson(doc, jsonString);
        networkList = jsonString;
        WiFi.scanDelete();
    }
    else
    {
        ESP_LOGE(tag, "WiFi scan failed");
    }
}

void Setup_WebServer_Handlers(AsyncWebServer &server)
{

    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(LittleFS, "/www/index.html", "text/html"); });
    server.on("/home", HTTP_GET, [](AsyncWebServerRequest *request)
              { sendHtmlFile("/www/home.html", request, HomeHtmlProcessor); });
    server.on("/setup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
                  if (!request->authenticate(http_username, http_password))
                  {
                      return request->requestAuthentication();
                  }
                  // sendHtmlFile("/www/setup.html", request, htmlProcessor);
              });
    server.on("/about", HTTP_GET, [](AsyncWebServerRequest *request)
              {
                  sendHtmlFile("/www/about.html", request, fileManagerHtmlProcessor);
                  // request->send(LittleFS, "/www/about.html", "text/html");
              });

    // Wifi related handlers
    server.on("/wifi/connect", HTTP_POST, [](AsyncWebServerRequest *request)
              {
    // Input validation
    if (!request->hasParam("ssid", false, false) || !request->hasParam("pass", false, false)) {
      ESP_LOGE(tag, "Missing required parameters");
      request->send(400, "application/json", "{\"error\":\"Missing ssid or password\"}");
      return;
    }

    // Get and store credentials
    String ssid = request->getParam("ssid", false, false)->value();
    String pass = request->getParam("pass", false, false)->value();

    // Validate credentials
    if (client_ssid.length() < 1 || client_pass.length() < 8) {
      ESP_LOGE(tag, "Invalid credentials");
      request->send(400, "application/json", "{\"error\":\"Invalid credentials\"}");
      return;
    }

    // Start connection
    StartWifiConnectTask(ssid, pass);
    
    ESP_LOGI(tag, "Starting connection to %s", client_ssid.c_str());
    request->send(200, "application/json", "{\"status\":\"connecting\"}"); });
    server.on("/wifi/status", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      String jsonStr = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + 
                       "\",\"ip\":\"" + WiFi.localIP().toString() + "\"}";
      request->send(200, "application/json", jsonStr); });
    server.on("/wifi/scans", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      if(networkCount <= 0) {
          request->send(400, "application/json", "{\"error\":\"No scan results\"}");
          return;
      }
      
      request->send(200, "application/json", networkList); });
    server.on("/wifi", HTTP_GET, [](AsyncWebServerRequest *request)
              {
    if(WiFi.getMode() == WIFI_MODE_APSTA){
      // TODO Disable navigation bar
    }
    //sendHtmlFile("/www/wifi.html", request, htmlProcessor);
    request->send(LittleFS, "/www/wifi.html", "text/html"); });

    // File Manager related handlers
    server.on("/files/upload", HTTP_POST, [](AsyncWebServerRequest *request)
              { request->send(200); }, handleFilesUpload_OnBody);

    server.on("/files/download", HTTP_GET, [](AsyncWebServerRequest *request)
              {
    if (!request->hasParam("file")) {
        ESP_LOGE(tag, "Missing file parameter");
        request->send(400, "text/plain", "Missing file parameter");
        return;
    }

    try {
        String filename = uriDecode(request->getParam("file")->value());
        ESP_LOGI(tag, "Download request for: %s", filename.c_str());
        request->send(LittleFS, filename, "application/octet-stream");
    }
    catch (const std::exception& e) {
        ESP_LOGE(tag, "Download failed: %s", e.what());
        request->send(404, "text/plain", "File not found!");
    } });
    server.on("/files/delete", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      static const char* tag = "DeleteHandler";

      // Authentication check
      if (!request->authenticate(http_username, http_password)) {
          ESP_LOGW(tag, "Authentication failed for delete request");
          return request->requestAuthentication();
      }

      // Parameter validation
      if (!request->hasParam(param_delete_path)) {
          ESP_LOGE(tag, "Missing delete path parameter");
          request->send(400, "text/plain", "Missing file path");
          return;
      }

      // Get and validate filename
      String filename = uriDecode(request->getParam(param_delete_path)->value());
      if (filename == "choose") {
          request->redirect("/files");
          return;
      }

      // Ensure path starts with /
      if (!filename.startsWith("/")) {
          filename = "/" + filename;
      }

      try {
          if (LittleFS.remove(filename.c_str())) {
              ESP_LOGI(tag, "Successfully deleted file: %s", filename.c_str());
          } else {
              ESP_LOGE(tag, "Failed to delete file: %s", filename.c_str());
              request->send(500, "text/plain", "Delete failed");
              return;
          }
      } catch (const std::exception& e) {
          ESP_LOGE(tag, "Exception during delete: %s", e.what());
          request->send(500, "text/plain", "Delete failed");
          return;
      }

      request->redirect("/files"); });
    server.on("/files/edit", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      static const char* tag = "EditHandler";

      // Authentication check
      if (!request->authenticate(http_username, http_password)) {
          ESP_LOGW(tag, "Authentication failed");
          return request->requestAuthentication();
      }

      // Parameter validation
      if (!request->hasParam(param_edit_path)) {
          ESP_LOGE(tag, "Missing edit path parameter");
          request->send(400, "text/plain", "Missing file path");
          return;
      }

      // Get and decode filename
      String fileName = uriDecode(request->getParam(param_edit_path)->value());
      ESP_LOGI(tag, "Edit request for file: %s", fileName.c_str());

      // Set save path
      savePath = (fileName == "new") ? "/new.txt" : fileName;

      // Validate path
      if (!savePath.startsWith("/")) {
          savePath = "/" + savePath;
      }

      ESP_LOGI(tag, "Save path set to: %s", savePath.c_str());
      
      try {
          sendHtmlFile("/www/edit.html", request, fileManagerHtmlProcessor);
      } catch (const std::exception& e) {
          ESP_LOGE(tag, "Failed to send edit page: %s", e.what());
          request->send(500, "text/plain", "Internal server error");
      } });
    server.on("/files/save", HTTP_GET, [](AsyncWebServerRequest *request)
              {
    if(!request->authenticate(http_username, http_password)){
      return request->requestAuthentication();
    }
    String inputMessage((char*)0);
    if (request->hasParam(param_edit_textarea)) {
      inputMessage = request->getParam(param_edit_textarea)->value();
    }
    if (request->hasParam(param_save_path)) {
      savePath = uriDecode(request->getParam(param_save_path)->value());
    }
    writeFile(LittleFS, savePath.c_str(), inputMessage.c_str());

    request->redirect("/files"); });
    server.on("/files", HTTP_GET, [](AsyncWebServerRequest *request)
              {
    if(!request->authenticate(http_username, http_password)){
      return request->requestAuthentication();
    }
    sendHtmlFile("/www/files.html", request, fileManagerHtmlProcessor); });

    // Lights related handlers
    server.on("/lights/settings", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200); });
    server.on("/lights/settings", HTTP_POST, [](AsyncWebServerRequest *request)
              { request->send(200); });
    server.on("/lights/animation", HTTP_POST, [](AsyncWebServerRequest *request)
              { request->send(200); });
    server.on("/lights/setpixel", HTTP_POST, [](AsyncWebServerRequest *request)
              { request->send(200); });

    // System and LED related handlers
    server.on("/system/summary", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
      request->send(200, "application/json", response); });
    server.on("/leds/settings", HTTP_GET, [](AsyncWebServerRequest *request)
              {
    /*
      //CreateSysSummmaryPacket(doc);
      String summary;
      serializeJson(jsDoc, summary); 
      request->send(200, "application/json", summary);
    */
      String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
      request->send(200, "application/json", response); });
    server.on("/leds/settings", HTTP_POST, [](AsyncWebServerRequest *request)
              {
      String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
      request->send(200, "application/json", response); });

    // LightStik related handlers
    server.on("/lightstik/settings", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
      request->send(200, "application/json", response); });
    server.on("/lightstik/settings", HTTP_POST, [](AsyncWebServerRequest *request)
              {
      String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
      request->send(200, "application/json", response); });
    server.on("/lightstik/register", HTTP_POST, [](AsyncWebServerRequest *request)
              {
      String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
      request->send(200, "application/json", response); });

    // Firmware Update Handlers
    server.on("/upgrade/check", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      //String newVersion;
      loadUpdateJson();
      //bool avai = checkManifest(FIRMWARE_VERSION, newVersion);
      checkManifest(otaVersion);
      bool avail = otaVersion > localVersion;

      JsonDocument doc;
      doc["currentVersion"] = localVersion.toString();
      doc["latestVersion"] = otaVersion.toString();  
      doc["updateAvailable"] = avail;
      
      String response;
      serializeJson(doc, response);
      request->send(200, "application/json", response); });
    // Start update process
    server.on("/upgrade/start", HTTP_POST, [](AsyncWebServerRequest *request)
              {
      startFirmwareUpdateTask(&eventUpgradeProgress);
      request->send(200); });
    eventUpgradeProgress.onConnect([](AsyncEventSourceClient *client)
                                   {
                                       if (client->lastId())
                                       {
                                           Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
                                       }
                                       // send event with message "hello!", id current millis
                                       // and set reconnect delay to 1 second
                                       // client->send("hello!", NULL, millis(), 10000);
                                   });
    server.on("/upgrade", HTTP_GET, [](AsyncWebServerRequest *request)
              {
    if(!request->authenticate(http_username, http_password)){
      return request->requestAuthentication();
    }
    request->send(LittleFS, "/www/upgrade.html", "text/html"); });
    server.addHandler(&eventUpgradeProgress);

    // Basic Connection status check
    server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "application/json", "{\"status\":\"connected\"}"); });
    // Server requested files that aren't template processed
    server.on("/*", HTTP_GET, [](AsyncWebServerRequest *request) { // handle file uploads
                                                                   // Validate request
        if (!request)
        {
            ESP_LOGE(tag, "Invalid request");
            return;
        }

        // Get and validate file path
        String filePath = request->url();
        if (filePath.isEmpty())
        {
            ESP_LOGE(tag, "Empty file path");
            request->send(400, "text/plain", "Invalid file path");
            return;
        }

        // Ensure path starts with '/'
        if (!filePath.startsWith("/"))
        {
            filePath = "/" + filePath;
        }

        try
        {
            // Get content type once
            const char *contentType = getFileType(getFileExtension(filePath.c_str()));
            if (!contentType)
            {
                ESP_LOGW(tag, "Unknown file type: %s", filePath.c_str());
                contentType = "application/octet-stream";
            }

            ESP_LOGI(tag, "Sending file: %s (%s)", filePath.c_str(), contentType);
            request->send(LittleFS, filePath, contentType);
        }
        catch (const std::runtime_error &e)
        {
            ESP_LOGE(tag, "FileSystem error: %s for path: %s", e.what(), filePath.c_str());
            request->send(404, "text/plain", "File not found");
        }
        catch (const std::exception &e)
        {
            ESP_LOGE(tag, "Error: %s for path: %s", e.what(), filePath.c_str());
            request->send(500, "text/plain", "Internal server error");
        }
    });
    // 404 handler
    server.onNotFound([](AsyncWebServerRequest *request)
                      {
      ESP_LOGE(tag, "404: %s", request->url().c_str());
      request->send(404, "text/plain", "Not found"); });
}

void handleFilesUpload_OnBody(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
{
    static const size_t MAX_UPLOAD_SIZE = 1024 * 1024; // 1MB limit

    if (!index)
    {
        // Initial upload chunk
        if (!request->hasParam("dir-path", true, false))
        {
            ESP_LOGE(tag, "Missing dir-path parameter");
            request->send(400, "text/plain", "Missing dir-path");
            return;
        }

        AsyncWebParameter *p = request->getParam("dir-path", true, false);
        String path = p->value() + "/" + filename;
        ESP_LOGI(tag, "Starting upload: %s", path.c_str());

        // Validate path
        if (!path.startsWith("/"))
        {
            path = "/" + path;
        }

        request->_tempFile = LittleFS.open(path, "w");
        if (!request->_tempFile)
        {
            ESP_LOGE(tag, "Failed to create file: %s", path.c_str());
            request->send(500, "text/plain", "Failed to create file");
            return;
        }
    }

    // Write chunk
    if (len && request->_tempFile)
    {
        if (index + len > MAX_UPLOAD_SIZE)
        {
            request->_tempFile.close();
            LittleFS.remove(request->_tempFile.name());
            ESP_LOGE(tag, "Upload too large");
            request->send(413, "text/plain", "File too large");
            return;
        }

        if (!request->_tempFile.write(data, len))
        {
            ESP_LOGE(tag, "Write failed");
            request->_tempFile.close();
            request->send(500, "text/plain", "Write failed");
            return;
        }
    }

    if (final)
    {
        request->_tempFile.close();
        ESP_LOGI(tag, "Upload complete: %s, %u bytes", filename.c_str(), index + len);
        request->redirect("/files");
    }
}

// Send html file with template processing {{VAR}}
void sendHtmlFile(const char *filePath, AsyncWebServerRequest *request, String (*callback)(const String &))
{
    try
    {
        const char *htmlFile = readFile(LittleFS, filePath);
        if (!htmlFile)
        {
            ESP_LOGE(tag, "Failed to read file: %s", filePath);
            request->send(404, "text/plain", "File not found");
            return;
        }

        String processedData = varReplace(htmlFile, callback);
        delete[] htmlFile; // Clean up allocated memory

        ESP_LOGI(tag, "Sent file: %s", filePath);
        request->send(200, "text/html", processedData);
    }
    catch (const std::exception &e)
    {
        ESP_LOGE(tag, "Error processing file %s: %s", filePath, e.what());
        request->send(500, "text/plain", "Server error");
    }
}

const char *getFileExtension(const char *filename)
{
    // Input validation
    if (!filename)
    {
        ESP_LOGW(tag, "Null filename provided");
        return "";
    }

    // Find last dot
    const char *lastDot = strrchr(filename, '.');
    if (!lastDot || lastDot == filename || *(lastDot + 1) == '\0')
    {
        ESP_LOGI(tag, "No valid extension found in: %s", filename);
        return "";
    }

    ESP_LOGI(tag, "Found extension: %s", lastDot + 1);
    return lastDot + 1;
}

const char *getFileType(const char *ext)
{
    if (!ext)
        return "application/octet-stream";

    ESP_LOGI(tag, "Getting file type for extension: %s", ext);

    if (strcmp(ext, "png") == 0)
        return "image/png";
    if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0)
        return "image/jpeg";
    if (strcmp(ext, "gif") == 0)
        return "image/gif";
    if (strcmp(ext, "ico") == 0)
        return "image/x-icon";
    if (strcmp(ext, "txt") == 0)
        return "text/plain";
    if (strcmp(ext, "css") == 0)
        return "text/css";
    if (strcmp(ext, "htm") == 0 || strcmp(ext, "html") == 0)
        return "text/html";
    if (strcmp(ext, "js") == 0)
        return "text/javascript";
    if (strcmp(ext, "json") == 0)
        return "application/json";

    ESP_LOGW(tag, "Unknown file extension: %s", ext);
    return "application/octet-stream";
}

// Finds segments between {{VAR}} and calls a callback function to replace VAR with new content
String varReplace(const String &input, String (*callback)(const String &))
{
    static const char *tag = "varReplace";

    // Validate inputs
    if (input.isEmpty() || !callback)
    {
        ESP_LOGW(tag, "Empty input or null callback");
        return input;
    }

    // Pre-allocate result string with estimated size
    String result;
    result.reserve(input.length() * 1.2); // Add 20% for potential replacements

    const int maxSegmentLength = 32;
    int startPos = 0;

    // Process all segments
    while (true)
    {
        // Find next variable
        int start = input.indexOf("{{", startPos);
        if (start == -1)
        {
            break;
        }

        // Add text before variable
        result += input.substring(startPos, start);

        // Find end of variable
        int end = input.indexOf("}}", start + 2);
        if (end == -1)
        {
            ESP_LOGW(tag, "Unmatched {{ at position %d", start);
            result += input.substring(start);
            break;
        }

        // Extract and validate segment
        String segment = input.substring(start + 2, end);
        if (segment.length() <= maxSegmentLength)
        {
            try
            {
                String replacement = callback(segment);
                result += replacement;
            }
            catch (const std::exception &e)
            {
                ESP_LOGE(tag, "Callback error: %s", e.what());
                result += input.substring(start, end + 2);
            }
        }
        else
        {
            ESP_LOGW(tag, "Segment too long: %d chars", segment.length());
            result += input.substring(start, end + 2);
        }

        startPos = end + 2;
    }

    // Add remaining text
    if (startPos < input.length())
    {
        result += input.substring(startPos);
    }

    return result;
}

const char *convertFileSize(const size_t bytes)
{
    static char fileSizeBuffer[16]; // Pre-allocated buffer for the file size

    if (bytes < 1024)
    {
        snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%d B", bytes);
    }
    else if (bytes < 1024 * 1024)
    {
        snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f kB", bytes / 1024.0);
    }
    else if (bytes < 1024 * 1024 * 1024)
    {
        snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f MB", bytes / (1024.0 * 1024.0));
    }
    else
    {
        snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
    }

    return fileSizeBuffer;
}

void onWiFiEvent(WiFiEvent_t event)
{
    // Serial.printf("[WiFi-event] event: %d\n", event);
    switch (event)
    {
    case ARDUINO_EVENT_WIFI_READY:
        ESP_LOGI(tag, "WiFi interface ready");
        break;
    case ARDUINO_EVENT_WIFI_SCAN_DONE:
        ESP_LOGI(tag, "Completed scan for access points");
        break;
    case ARDUINO_EVENT_WIFI_STA_START:
        ESP_LOGI(tag, "WiFi client started");
        break;
    case ARDUINO_EVENT_WIFI_STA_STOP:
        ESP_LOGI(tag, "WiFi clients stopped");
        break;
    case ARDUINO_EVENT_WIFI_STA_CONNECTED:
        ESP_LOGI(tag, "Connected to AP");
        break;
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
        ESP_LOGI(tag, "WiFi Disconnected");
        setStatusPin1(false);
        Buzzer_Play_Tune(TUNE_DISCONNECTED);
        break;
    case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE:
        ESP_LOGI(tag, "Authentication mode of access point has changed");
        break;
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
        ESP_LOGI(tag, "My IP: %s", WiFi.localIP().toString());
        // Wifi_Start_MDNS();
        setStatusPin1(true);
        Buzzer_Play_Tune(TUNE_CONNECTED);
        break;
    case ARDUINO_EVENT_WIFI_STA_LOST_IP:
        ESP_LOGI(tag, "Lost IP address and IP address is reset to 0");
        break;
    case ARDUINO_EVENT_WPS_ER_SUCCESS:
        ESP_LOGI(tag, "WiFi Protected Setup (WPS): succeeded in enrollee mode");
        break;
    case ARDUINO_EVENT_WPS_ER_FAILED:
        ESP_LOGI(tag, "WiFi Protected Setup (WPS): failed in enrollee mode");
        break;
    case ARDUINO_EVENT_WPS_ER_TIMEOUT:
        ESP_LOGI(tag, "WiFi Protected Setup (WPS): timeout in enrollee mode");
        break;
    case ARDUINO_EVENT_WPS_ER_PIN:
        ESP_LOGI(tag, "WiFi Protected Setup (WPS): pin code in enrollee mode");
        break;
    case ARDUINO_EVENT_WIFI_AP_START:
        ESP_LOGI(tag, "WiFi access point started");
        break;
    case ARDUINO_EVENT_WIFI_AP_STOP:
        ESP_LOGI(tag, "WiFi access point  stopped");
        break;
    case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
        ESP_LOGI(tag, "Client connected");
        break;
    case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:
        ESP_LOGI(tag, "SoftAP Client Disconnected");
        Buzzer_Play_Tune(TUNE_DISCONNECTED);
        break;
    case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED:
        ESP_LOGI(tag, "SoftAP Client Connected");
        Buzzer_Play_Tune(TUNE_CONNECTED);
        break;
    case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED:
        ESP_LOGI(tag, "Received probe request");
        break;
    case ARDUINO_EVENT_WIFI_AP_GOT_IP6:
        ESP_LOGI(tag, "AP IPv6 is preferred");
        break;
    case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
        ESP_LOGI(tag, "STA IPv6 is preferred");
        break;
    case ARDUINO_EVENT_ETH_GOT_IP6:
        ESP_LOGI(tag, "Ethernet IPv6 is preferred");
        break;
    case ARDUINO_EVENT_ETH_START:
        ESP_LOGI(tag, "Ethernet started");
        break;
    case ARDUINO_EVENT_ETH_STOP:
        ESP_LOGI(tag, "Ethernet stopped");
        break;
    case ARDUINO_EVENT_ETH_CONNECTED:
        ESP_LOGI(tag, "Ethernet connected");
        break;
    case ARDUINO_EVENT_ETH_DISCONNECTED:
        ESP_LOGI(tag, "Ethernet disconnected");
        break;
    case ARDUINO_EVENT_ETH_GOT_IP:
        ESP_LOGI(tag, "Obtained IP address");
        break;
    default:
        break;
    }
}

void Wifi_Start_MDNS(void)
{
    ESP_LOGV(tag, "Initializing MDNS: %s", mDnsName.c_str());
    if (!MDNS.begin(mDnsName.c_str()))
    {
        ESP_LOGE(tag, "Error setting up MDNS responder!");
    }
    else
    {
        ESP_LOGV(tag, "You can access device via http://%s.local", mDnsName);
    }
}

bool writeFile(fs::FS &fs, const char *path, const char *message)
{
    // Validate inputs
    if (!path || !message)
    {
        ESP_LOGE(tag, "Invalid parameters: path=%p message=%p", path, message);
        return false;
    }

    // Normalize and validate path
    String finalPath(path);
    if (!finalPath.startsWith("/"))
    {
        finalPath = String("/") + finalPath;
    }
    // Prevent directory traversal
    if (finalPath.indexOf("..") >= 0)
    {
        ESP_LOGE(tag, "Rejected unsafe path: %s", finalPath.c_str());
        return false;
    }
    // Collapse duplicate slashes (optional hardening)
    while (finalPath.indexOf("//") >= 0)
    {
        finalPath.replace("//", "/");
    }

    // Size checks
    const size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB cap (aligned with readFile)
    const size_t totalSize = strlen(message);
    if (totalSize > MAX_FILE_SIZE)
    {
        ESP_LOGE(tag, "Write too large: %u bytes for %s", (unsigned)totalSize, finalPath.c_str());
        return false;
    }

    // Write to a temporary file first for atomicity
    String tmpPath = finalPath + ".tmp";
    File tmp = fs.open(tmpPath.c_str(), "w");
    if (!tmp)
    {
        ESP_LOGE(tag, "Failed to open temp file: %s", tmpPath.c_str());
        return false;
    }

    // Write in a loop to ensure all bytes are written
    size_t written = 0;
    const uint8_t *buf = reinterpret_cast<const uint8_t *>(message);
    while (written < totalSize)
    {
        size_t n = tmp.write(buf + written, totalSize - written);
        if (n == 0)
        {
            ESP_LOGE(tag, "Write failed to temp file: %s at %u/%u bytes", tmpPath.c_str(), (unsigned)written, (unsigned)totalSize);
            tmp.close();
            fs.remove(tmpPath.c_str());
            return false;
        }
        written += n;
    }

    // Flush and close temp file
    tmp.flush();
    tmp.close();

    // Replace the target file atomically: remove existing then rename
    if (fs.exists(finalPath.c_str()))
    {
        if (!fs.remove(finalPath.c_str()))
        {
            ESP_LOGE(tag, "Failed to remove existing file: %s", finalPath.c_str());
            fs.remove(tmpPath.c_str());
            return false;
        }
    }
    if (!fs.rename(tmpPath.c_str(), finalPath.c_str()))
    {
        ESP_LOGE(tag, "Failed to rename %s to %s", tmpPath.c_str(), finalPath.c_str());
        fs.remove(tmpPath.c_str());
        return false;
    }

    ESP_LOGI(tag, "Successfully wrote %u bytes to %s", (unsigned)totalSize, finalPath.c_str());
    return true;
}

char *readFile(fs::FS &fs, const char *path)
{
    static const char *tag = "readFile";
    static const size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB limit

    // Validate input
    if (!path)
    {
        ESP_LOGE(tag, "Invalid path parameter");
        return nullptr;
    }

    // Open file
    File file = fs.open(path, "r");
    if (!file || file.isDirectory())
    {
        ESP_LOGE(tag, "Failed to open file: %s", path);
        return nullptr;
    }

    // Check file size
    size_t fileSize = file.size();
    if (fileSize == 0 || fileSize > MAX_FILE_SIZE)
    {
        ESP_LOGE(tag, "Invalid file size: %u bytes", fileSize);
        file.close();
        return nullptr;
    }

    // Allocate memory
    char *fileContent = new (std::nothrow) char[fileSize + 1];
    if (!fileContent)
    {
        ESP_LOGE(tag, "Memory allocation failed for size: %u", fileSize + 1);
        file.close();
        return nullptr;
    }

    // Read file
    size_t bytesRead = file.readBytes(fileContent, fileSize);
    file.close();

    if (bytesRead != fileSize)
    {
        ESP_LOGE(tag, "Read failed: expected %u bytes, got %u", fileSize, bytesRead);
        delete[] fileContent;
        return nullptr;
    }

    // Null terminate
    fileContent[bytesRead] = '\0';
    ESP_LOGI(tag, "Successfully read %u bytes from %s", bytesRead, path);
    return fileContent;
}

String getSoftAPMacAddress()
{
    uint8_t mac[6];
    WiFi.softAPmacAddress(mac);

    String macString = "";
    for (int i = 0; i < 6; i++)
    {
        macString += String(mac[i], HEX);
        if (i < 5)
            macString += ":";
    }
    return macString;
}

String listDirAsHtml(String directoryList[], int count)
{
    String listedFiles;

    for (int i = 0; i < count; i++)
    {
        // directory html
        listedFiles += "<tr id=\"file-row\"><td>Dir: ";
        listedFiles += directoryList[i];
        listedFiles += "/</td><td>-</td></tr>\n";

        filesDropdownOptions += "<option value=\"";
        filesDropdownOptions += directoryList[i];
        filesDropdownOptions += "\">";
        filesDropdownOptions += directoryList[i];
        filesDropdownOptions += "/</option>\n";

        dirDropdownOptions += "<option value=\"";
        dirDropdownOptions += directoryList[i];
        dirDropdownOptions += "\">";
        dirDropdownOptions += directoryList[i];
        dirDropdownOptions += "/</option>\n";

        File dir = LittleFS.open(directoryList[i]);
        File file = dir.openNextFile();
        while (file)
        {
            String fileName = file.name();
            if (!file.isDirectory())
            {
                // Serial.println("    File: " + String(file.name()));
                listedFiles += "<tr id=\"file-row\"><td>&emsp;<button onclick=\"window.location.href='/download?file=";
                listedFiles += file.path();
                listedFiles += "\" \"\">&#8681;</button>&emsp;";
                listedFiles += fileName;
                listedFiles += "</td><td>";
                listedFiles += convertFileSize(file.size());
                listedFiles += "</td></tr>\n";

                filesDropdownOptions += "<option value=\"";
                filesDropdownOptions += file.path();
                filesDropdownOptions += "\">&emsp;&emsp;";
                filesDropdownOptions += fileName;
                filesDropdownOptions += "</option>\n";
            }
            file = dir.openNextFile();
        }
        dir.close();
    }

    return listedFiles;
}

/********************  Specific Html Processors  ********************/
// file manager html processor
String fileManagerHtmlProcessor(const String &var)
{
    if (var == "ALLOWED_EXTENSIONS_EDIT")
    {
        return allowedExtensionsForEdit;
    }

    if (var == "FS_FREE_BYTES")
    {
        return convertFileSize(LittleFS.totalBytes() - LittleFS.usedBytes());
    }

    if (var == "FS_USED_BYTES")
    {
        return convertFileSize(LittleFS.usedBytes());
    }

    if (var == "FS_TOTAL_BYTES")
    {
        return convertFileSize(LittleFS.totalBytes());
    }

    if (var == "RAM_FREE_BYTES")
    {
        return convertFileSize(LittleFS.totalBytes());
    }

    if (var == "RAM_USED_BYTES")
    {
        return convertFileSize(LittleFS.totalBytes());
    }

    if (var == "RAM_TOTAL_BYTES")
    {
        return convertFileSize(LittleFS.totalBytes());
    }

    if (var == "LISTED_FILES")
    {
        filesDropdownOptions = ""; // clear out
        dirDropdownOptions = "";   // clear out
        String directories[MAX_DIRECTORIES];
        int dirCount = 0;
        getAllDirectories(directories, dirCount);
        return listDirAsHtml(directories, dirCount);
    }

    if (var == "EDIT-DEL_FILES")
    {
        return filesDropdownOptions;
    }

    if (var == "DIR_LIST")
    {
        return dirDropdownOptions;
    }

    if (var == "SAVE_PATH_INPUT")
    {
        return savePath;
    }

    if (var == "FIRM_VER")
    {
        return localVersion.toString();
    }

    return var;
}

String HomeHtmlProcessor(const String &var)
{
    if (var == "APP_NAME")
    {
        // return sysProps.appName;
        return "N/A";
    }
    if (var == "OLED")
    {
        return "N/A";
    }
    if (var == "STRIP1")
    {
        // return (strip1) ? "Yes" : "No";
        return "N/A";
    }
    if (var == "STRIP2")
    {
        // return (strip2) ? "Yes" : "No";
        return "N/A";
    }
    if (var == "FRONT_LIGHT")
    {
        // return (animProps.frontLight.enabled) ? "Yes" : "No";
        return "N/A";
    }
    if (var == "REAR_LIGHT")
    {
        // return (animProps.rearLight.enabled) ? "Yes" : "No";
        return "N/A";
    }
    if (var == "FIRMWARE")
    {
        return localVersion.toString();
    }
    if (var == "BOOTH_T")
    {
        // return String(sysProps.t_sensor.temperature) + "F";
        return "N/A";
    }
    if (var == "SETPOINT")
    {
        // return String(sysProps.t_sensor.Setpoint1) + "F";
        return "N/A";
    }
    if (var == "FLASH_SIZE")
    {
        return convertFileSize(ESP.getSketchSize());
    }
    if (var == "FLASH_FREE")
    {
        return convertFileSize(ESP.getFreeSketchSpace());
    }
    if (var == "HEAP_SIZE")
    {
        return convertFileSize(ESP.getHeapSize());
    }
    if (var == "HEAP_FREE")
    {
        return convertFileSize(ESP.getFreeHeap());
    }
    if (var == "CPU_FREQ")
    {
        return String(ESP.getCpuFreqMHz()) + "Mhz";
    }
    if (var == "IP")
    {
        return WiFi.localIP().toString();
    }
    if (var == "MAC")
    {
        return chipInfo.macStr;
    }
    if (var == "SSID")
    {
        return WiFi.SSID();
    }
    if (var == "RSSI")
    {
        return String(WiFi.RSSI());
    }
    if (var == "WIFI_CH")
    {
        return String(WiFi.channel());
    }
    if (var == "ENCRYP")
    {
        return String(WiFi.encryptionType(0));
    }
    if (var == "AP_SSID")
    {
        return WiFi.softAPSSID();
    }
    if (var == "AP_CLIENTS")
    {
        return String(WiFi.softAPgetStationNum());
    }
    if (var == "BLE")
    {
        return (commMode == COMM_WIFI_AP_BLE) ? "Yes" : "No";
    }
    if (var == "BLE_SSID")
    {
        // return (commMode == COMM_WIFI_AP_BLE) ? BLEDeviceName : "";
        return "N/A";
    }
    if (var == "BLE_CLIENTS")
    {
        // return (BTDeviceConnected) ? "1" : "0";
        return "N/A";
    }
    if (var == "AP_MAC")
    {
        return getSoftAPMacAddress();
    }

    // Return an empty string if the variable is not recognized
    return var;
}

void handleUpdateProgress(AsyncWebServerRequest *request)
{
    static const char *tag = "UpdateProgress";

    // if (!request->authenticate(http_username, http_password)) {
    //     return request->requestAuthentication();
    // }

    request->send(200, "text/plain", "Update progress"); // Send a simple response
}
