#ifndef ASYNC_FS_WEBSERVER_H
#define ASYNC_FS_WEBSERVER_H

#include <FS.h>
#include <DNSServer.h>

#include "ESPAsyncWebServer.h"
#include "Json.h"
#include "WiFiService.h"
#include "SerialLog.h"
#include "Version.h"
#include <type_traits>

class Print;

#ifdef ESP32
  // Arduino-ESP32 v3 splits networking primitives into a dedicated core library.
  // PlatformIO's dependency finder doesn't always pull it in via transitive includes,
  // so include it explicitly to ensure it gets compiled and linked.
  #if defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 3)
    #include <Network.h>
  #endif
  #include <WiFi.h>
  #include <WiFiAP.h>
  #include <Update.h>
  #include <ESPmDNS.h>
  #include "esp_wifi.h"
  #include "esp_task_wdt.h"
  #include "sys/stat.h"
#elif defined(ESP8266)
  #include <ESP8266mDNS.h>
  #include <Updater.h>
#else
  #error Platform not supported
#endif

#ifndef ESP_FS_WS_EDIT
    #define ESP_FS_WS_EDIT              1   // Library has edit methods
    #ifndef ESP_FS_WS_EDIT_HTM
                                            // Disable if you provide your own edit page
        #define ESP_FS_WS_EDIT_HTM      1   // Library serve /edit webpage from progmem                                            
    #endif
#endif

#ifndef ESP_FS_WS_SETUP
    #define ESP_FS_WS_SETUP             1   // Library has setup methods
    #ifndef ESP_FS_WS_SETUP_HTM
                                            // Disable if you provide your own setup page
        #define ESP_FS_WS_SETUP_HTM     1   // Library serve /setup webpage from progmem                                            
    #endif
#endif

#if ESP_FS_WS_EDIT_HTM
    #include "assets/edit_htm.h"
#endif

#if ESP_FS_WS_SETUP_HTM
  #define ESP_FS_WS_CONFIG_FOLDER "/setup"
    #define ESP_FS_WS_CONFIG_FILE ESP_FS_WS_CONFIG_FOLDER "/config.json"
    #include "assets/setup_htm.h"
    #include "assets/logo_svg.h"
    #include "CredentialManager.h"    
    #include "SetupConfig.hpp" 
#endif

#include "CaptivePortal.hpp"
#ifndef ESP_FS_WS_MDNS
  #define ESP_FS_WS_MDNS 1
#endif


#define LIB_URL "https://github.com/cotestatnt/async-esp-fs-webserver/"
#define MIN_F -3.4028235E+38
#define MAX_F 3.4028235E+38

// Watchdog timeout utility
#if defined(ESP32)
    #define AWS_WDT_TIMEOUT (CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000)
    #define AWS_LONG_WDT_TIMEOUT (AWS_WDT_TIMEOUT * 4)
#else
  #define AWS_WDT_TIMEOUT 5000
  #define AWS_LONG_WDT_TIMEOUT 15000
#endif

typedef struct {
  size_t totalBytes;
  size_t usedBytes;
  String fsName;
} fsInfo_t;

using FsInfoCallbackF = std::function<void(fsInfo_t*)>;
using CallbackF = std::function<void(void)>;
using ConfigSavedCallbackF = std::function<void(const char*)>;  // Callback for config file saves

class AsyncFsWebServer : public AsyncWebServer
{
  protected:
    AsyncWebSocket* m_ws = nullptr;
#if ESP_FS_WS_SETUP
    AsyncWebSocket* m_setupWs = nullptr;
#endif
    AsyncWebHandler *m_captive = nullptr;
    DNSServer* m_dnsServer = nullptr;

    char m_apSSID[33] = {0};
    char m_apPassword[65] = {0};
    bool m_isApMode = false;
    bool m_authAll = false;
  
    void notFound(AsyncWebServerRequest *request);
    void handleFileName(AsyncWebServerRequest *request);
    void handleIndex(AsyncWebServerRequest *request);

#if ESP_FS_WS_SETUP    
    void handleSetup(AsyncWebServerRequest *request);
    void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);    
    void onUpdate();
#endif

    // edit page, in useful in some situation, but if you need to provide only a web interface, you can disable
#if ESP_FS_WS_EDIT_HTM
    void deleteContent(String& path) ;
    void handleFileDelete(AsyncWebServerRequest *request);
    void handleFileCreate(AsyncWebServerRequest *request);
    void handleFsStatus(AsyncWebServerRequest *request);
    void handleFileList(AsyncWebServerRequest *request);
    void handleFileEdit(AsyncWebServerRequest *request);
#endif

  /*
    Create a dir if not exist on uploading files
  */
  bool createDirFromPath( const String& path) ;

  private:
    char* m_pageUser = nullptr;
    char* m_pagePswd = nullptr;
    String m_host = "esphost";
    String m_captiveUrl = "/setup";
    String typeName = "FileSystem";

    uint16_t m_port;
    uint32_t m_timeout = AWS_LONG_WDT_TIMEOUT;

    // Firmware version buffer (expanded to accommodate custom version strings)
    String m_version;
    bool m_filesystem_ok = false;

    fs::FS* m_filesystem = nullptr;
    FsInfoCallbackF getFsInfo = nullptr;
    ConfigSavedCallbackF m_configSavedCallback = nullptr;  // Callback for config file saves

    IPAddress m_serverIp = IPAddress(192, 168, 4, 1);
    CredentialManager *m_credentialManager = nullptr;

#if ESP_FS_WS_SETUP
    SetupConfigurator* setup = nullptr;
    void initSetupWebSocket();
  void releaseSetupWebSocketIfIdle();
    void handleSetupWebSocket(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
    void handleSetupWebSocketMessage(AsyncWebSocketClient *client, const uint8_t *data, size_t len);
    void sendSetupWsResponse(AsyncWebSocketClient *client, const String& reqId, bool ok, const char *name, const String& payload = String(), const String& error = String());
    void sendSetupWsEvent(AsyncWebSocketClient *client, const char *name, const String& payload = String());
    void sendSetupWsEventById(uint32_t clientId, const char *name, const String& payload = String());
    String buildSetupStatusPayload() const;
    String buildSetupConfigPayload() const;
    String buildSetupCredentialsPayload() const;
    bool saveSetupConfigJson(const String& jsonText);
    void runSetupWifiConnect(uint32_t clientId, WiFiConnectParams params, bool persistent, bool allowApFallback, bool fromApClient);
    
    // Lazy initialization: create setup object only when first needed
    SetupConfigurator* getSetupConfigurator() {
      if (!setup) {
        setup = new SetupConfigurator(m_filesystem, m_port, m_host);
      }
      return setup;
    }
#endif

  public:
    
    // Template Constructor for derived filesystem classes (LittleFS, SPIFFS, etc)
    template <typename T>
    AsyncFsWebServer(T &fs, uint16_t port, const char* hostname = "") : AsyncWebServer(port), m_filesystem(&fs)
    {
      m_port = port;      

      if (!m_credentialManager) {
        log_error("Credential manager not initialized");        
        m_credentialManager = new CredentialManager();
        m_credentialManager->begin();
        #ifdef ESP8266        
        m_credentialManager->setFilesystem(m_filesystem);  // Set FS for persistence
        #endif
      }     

      // Set hostname if provided from constructor
      if (strlen(hostname))
        m_host = hostname;

      // Sync hostname into shared CredentialManager
      if (m_credentialManager) {
        m_credentialManager->setHostname(m_host.c_str());
      }
      
#ifdef ESP32
        // Auto-configure getFsInfo for ESP32 filesystems
        // Try to infer filesystem name from template type
        String pretty = __PRETTY_FUNCTION__;
        int start = pretty.indexOf("T = ");
        if (start != -1)
        {
            start += 4;
            int end = pretty.indexOf("]", start);
            int semi = pretty.indexOf(";", start);
            if (semi != -1 && semi < end)
            {
                end = semi;
            }
            if (end != -1)
            {
                typeName = pretty.substring(start, end);
                // Clean up common prefixes/suffixes
                typeName.replace("fs::", "");
                if (typeName.endsWith("FS"))
                {
                    typeName = typeName.substring(0, typeName.length() - 2);
                }
            }
        }

        getFsInfo = [&fs, this](fsInfo_t *info)
        {
            info->totalBytes = fs.totalBytes();
            info->usedBytes = fs.usedBytes();
            info->fsName = this->typeName;
        };
#endif    
    }

    // Class destructor
    ~AsyncFsWebServer() {
      reset();
      end();
      if(_catchAllHandler) delete _catchAllHandler;
      
      // Deallocate all dynamically allocated resources
      if(m_pageUser) delete[] m_pageUser;
      if(m_pagePswd) delete[] m_pagePswd;
      if(m_ws) delete m_ws;
      if(m_captive) delete m_captive;
      if(m_dnsServer) delete m_dnsServer;
      if (m_credentialManager) delete m_credentialManager;
#if ESP_FS_WS_SETUP
      if(setup) delete setup;  // Only delete if it was lazily initialized
#endif
    }

  #ifdef ESP32
    inline TaskHandle_t getTaskHandler() {
      return xTaskGetCurrentTaskHandle();
    }
  #endif

    /*
      Get the webserver IP address
    */
    inline IPAddress getServerIP() {
      return m_serverIp;
    }
    /*
      Return true if the device is currently running in Access Point mode
    */
    inline bool isAccessPointMode() const { return m_isApMode; }
    /*
      Start webserver and bind a websocket event handler (optional)
    */
    bool init(AwsEventHandler wsHandle = nullptr);

#if ESP_FS_WS_EDIT

    /*
      Enable the built-in ACE web file editor
    */
    void enableFsCodeEditor();

    // Backward compatibility method
    [[deprecated("Use enableFsCodeEditor() instead (use built-in callback to provide FS info).")]]
    void enableFsCodeEditor(FsInfoCallbackF fsCallback) {
        if (fsCallback)
            getFsInfo = fsCallback;
        enableFsCodeEditor();
    }

    /*
     * Set callback function to provide updated FS info to library
     * This it is necessary due to the different implementation of
     * libraries for the filesystem (LittleFS, FFat, SPIFFS etc etc)
     */
    [[deprecated("Use enableFsCodeEditor() instead (use built-in callback to provide FS info)")]]
    inline void setFsInfoCallback(FsInfoCallbackF fsCallback)
    {
        getFsInfo = fsCallback;
    }
#endif

    /*
      Enable authenticate for /setup webpage
    */
    void setAuthentication(const char* user, const char* pswd);
    /*
    Enable the flag which turns on basic authentication for all pages
    */
    inline void requireAuthentication(bool require){
      m_authAll = require;
    }

    /*
      List FS content
    */
    void printFileList(fs::FS &fs, const char * dirname, uint8_t levels);

    /*
      List FS content to a destination stream (e.g. Serial, WiFiClient)
    */
    void printFileList(fs::FS &fs, const char * dirname, uint8_t levels, Print& out);

    /*
      Send a default "OK" reply to client
    */
    void sendOK(AsyncWebServerRequest *request);

    /*
      Set wifi event callback to handle captive portal fallback on wifi disconnect    
    */
    #if defined(ESP32) || defined(ESP8266)
      inline void setWiFiConnectionCallbacks(WiFiConnectedCallbackF connectedCallback, WiFiDisconnectedCallbackF disconnectedCallback) {
        WiFiService::setWiFiConnectionCallbacks(connectedCallback, disconnectedCallback);          
      }
    #endif

    /*
      Start WiFi connection, callback function is called when trying to connect
    */
    bool startWiFi(uint32_t timeout, CallbackF fn=nullptr) ;

    /**
     * @brief Access shared CredentialManager instance (for advanced configuration)
     */
    inline CredentialManager* getCredentialManager() { return m_credentialManager; }


    [[deprecated("Use startWiFi(timeout) and if it fails, use startCaptivePortal(ssid, pswd) instead.")]]
    IPAddress startWiFi(uint32_t timeout, const char* ssid, const char* pswd, CallbackF fn = nullptr, const char* redirectTargetURL = nullptr) {
      if (!startWiFi(timeout, fn)) {
        delay(100);
        startCaptivePortal(ssid, pswd, redirectTargetURL == nullptr ? "/setup" : redirectTargetURL);
      }
      return m_serverIp;
    }

    /*
     * Redirect to captive portal if we got a request for another domain.
    */
    bool startCaptivePortal(const char* ssid, const char* pass, const char* redirectTargetURL = "/setup");
    bool startCaptivePortal(WiFiConnectParams& params, const char *redirectTargetURL = "/setup");

    /*
      Set AP SSID and Password (backward compatibility)
    */
    void setAP(const char *ssid, const char *pass) {
      strlcpy(m_apSSID, ssid, sizeof(m_apSSID));
      strlcpy(m_apPassword, pass, sizeof(m_apPassword));
    }

    /*
    * Setup and start mDNS responder
    */
    bool startMDNSResponder();

    /*
    * Need to be run in loop to handle DNS requests
    */
    inline void updateDNS() {
      m_dnsServer->processNextRequest();
    }

    /*
     * get instance of current websocket handler (enabled at runtime)
    */
    AsyncWebSocket* getWebSocket() { return m_ws; }

    /*
     * Enable WebSocket at runtime. Creates WS on `path` and registers handler.
    */
    void enableWebSocket(const char* path, AwsEventHandler handler);

        /*
     * Broadcast a websocket message to all clients connected
    */
    void wsBroadcast(const char * buffer) {
      if (m_ws != nullptr)
        m_ws->textAll(buffer);
    }

    /*
    * Broadcast a binary websocket message to all clients connected
    */
    void wsBroadcastBinary(uint8_t * message, size_t len) {
      if (m_ws != nullptr)
        m_ws->binaryAll(message, len);
    }

    /*
    * Set current firmware version (shown in /setup webpage)
    */
    inline void setFirmwareVersion(const char* version) {
      m_version = String(version);
    }

    inline void setFirmwareVersion(const String& version) {
      m_version = version;
    }

    /*
    * Set hostmane
    */
    inline void setHostname(const char * host) {
      m_host = host;
      if (m_credentialManager) {
        m_credentialManager->setHostname(host);
      }
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////   SETUP PAGE CONFIGURATION /////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////////////////////////
#if ESP_FS_WS_SETUP    

    // Public alias to dropdown definition type (available only when /setup is enabled)
    using DropdownList = SetupConfig::DropdownList;
    // Public alias to slider definition type
    using Slider = SetupConfig::Slider;

    /*
    * Set callback function to be called when configuration file is saved via /edit POST
    * The callback receives the filename path as parameter
    */
    inline void setConfigSavedCallback(ConfigSavedCallbackF callback) {
      m_configSavedCallback = callback;
    }

    /*
    * Get reference to current config.json file
    */
    inline File getConfigFile(const char* mode) {
      File file = m_filesystem->open(ESP_FS_WS_CONFIG_FILE, mode);
      return file;
    }

    /*
    * Clear the saved configuration options by removing config.json.
    * Returns true if the file was removed or did not exist.
    */
    inline bool clearConfigFile() {
      if (m_filesystem->exists(ESP_FS_WS_CONFIG_FILE)) {
        return m_filesystem->remove(ESP_FS_WS_CONFIG_FILE);
      }
      return true;
    }

    /*
    * Get complete path of config.json file
    */
    inline const char* getConfiFileName() {
      return ESP_FS_WS_CONFIG_FILE;
    }

    void setSetupPageTitle(const char* title) { getSetupConfigurator()->setSetupPageTitle(title); }
    void addHTML(const char* html, const char* id, bool ow = false) {getSetupConfigurator()->addHTML(html, id, ow);}
    void addCSS(const char* css, const char* id, bool ow = false){getSetupConfigurator()->addCSS(css, id, ow);}
    void addJavascript(const char* script, const char* id, bool ow = false) {getSetupConfigurator()->addJavascript(script, id, ow);}
    void addDropdownList(const char *lbl, const char** a, size_t size){getSetupConfigurator()->addDropdownList(lbl, a, size);}    
    void addDropdownList(DropdownList &def){ getSetupConfigurator()->addDropdownList(def); }
    void addSlider(Slider &def){ getSetupConfigurator()->addSlider(def); }
    void addOptionBox(const char* title) { getSetupConfigurator()->addOptionBox(title); }
    void setSetupPageLogo(const uint8_t* imageData, size_t imageSize, const char* mimeType = "image/png", bool ow = false) {
      getSetupConfigurator()->setSetupPageLogo(imageData, imageSize, mimeType, ow);
    }
    void setSetupPageLogo(const char* svgText, bool ow = false) {
      getSetupConfigurator()->setSetupPageLogo(svgText, ow);
    }
    // boolean option overload with per-option grouping control
    void addOption(const char *lbl, bool val, bool hidden = false, bool grouped = true) {
      getSetupConfigurator()->addOption(lbl, val, hidden, grouped);
    }

    // specialized bool overload that also accepts a comment string
    void addOption(const char *lbl, bool val, const char *comment,
                   bool hidden = false, bool grouped = false) {
      getSetupConfigurator()->addOption(lbl, val, hidden, grouped);
      addComment(lbl, comment);
    }

    template <typename T>
    void addOption(const char *lbl, T val, double min, double max, double st){
      getSetupConfigurator()->addOption(lbl, val, false, min, max, st);
    }

    template <typename T>
    void addOption(const char *lbl, T val, bool hidden = false,  double min = MIN_F,
      double max = MAX_F, double st = 1.0) {
      getSetupConfigurator()->addOption(lbl, val, hidden, min, max, st);
    }

    // Overload for options with comments (added as metadata to be displayed under the input in the UI)
    // disable for bool so the bool-specific overload is used instead
    template <typename T, typename std::enable_if<!std::is_same<T, bool>::value, int>::type = 0>
    void addOption(const char *lbl, T val, const char* comment) {
      getSetupConfigurator()->addOption(lbl, val, false, MIN_F, MAX_F, 1.0);
      addComment(lbl, comment);
    }

    // Associate a short comment with a configuration element (displayed under the input)
    void addComment(const char *lbl, const char *comment) { getSetupConfigurator()->addComment(lbl, comment); }

    template <typename T>
    bool getOptionValue(const char *lbl, T &var) { return getSetupConfigurator()->getOptionValue(lbl, var);}
    template <typename T>
    bool saveOptionValue(const char *lbl, T val) { return getSetupConfigurator()->saveOptionValue(lbl, val);}

    // Update a dropdown definition's selectedIndex from persisted config
    bool getDropdownSelection(DropdownList &def) { return getSetupConfigurator()->getDropdownSelection(def); }
    // Read slider value back into struct
    bool getSliderValue(Slider &def) { return getSetupConfigurator()->getSliderValue(def); }

    void closeSetupConfiguration() {
      getSetupConfigurator()->closeConfiguration();
    }
    /////////////////////////////////////////////////////////////////////////////////////////////////
#endif

};




#endif
