#pragma once

// Shared browser configuration portal for ESP32 infrastructure and WiFi
// companion builds. The UI removes MQTT controls when no bridge is present.
//
// Two modes:
//  - SETUP: open SoftAP + captive portal, raised automatically on first boot
//    when no WiFi is configured (wifi_ssid empty), or manually via
//    `start webconfig ap`. Save -> reboot; auto-stops after an idle timeout.
//  - LAN: bound to an existing or portal-owned STA connection. Infrastructure
//    roles require the node admin password; companions use their trusted LAN.
//
// Concurrency model: AsyncWebServer handlers run on the async_tcp task and
// must never touch the CLI, prefs persistence, or the radio. Config writes
// are marshaled into a single-slot command batch that tick() - called from
// MyMesh::loop() on the Arduino loop task - drains through the existing CLI
// `set` handlers. Prefs-struct reads and batch state are guarded by _mux.
//
// The master switch and standalone WiFi credentials use independent NVS
// namespaces, so role-specific prefs-file layouts remain untouched.

#if defined(ESP_PLATFORM) && !defined(WEBCONFIG_DISABLED)

#define WITH_WEBCONFIG 1

#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>

class AsyncWebServer;
class AsyncWebServerRequest;
class DNSServer;

#ifndef WEBCONFIG_AP_IDLE_TIMEOUT_MS
  #define WEBCONFIG_AP_IDLE_TIMEOUT_MS  (10UL * 60UL * 1000UL)
#endif
#ifndef WEBCONFIG_SESSION_TTL_MS
  #define WEBCONFIG_SESSION_TTL_MS      (20UL * 60UL * 1000UL)
#endif

class WebConfigServer {
public:
  enum Mode : uint8_t { MODE_OFF = 0, MODE_SETUP, MODE_CONNECTING, MODE_LAN };

  enum Capability : uint32_t {
    CAP_LOCATION = 1UL << 0,
    CAP_AIRTIME = 1UL << 1,
    CAP_RX_DELAY = 1UL << 2,
    CAP_CAD = 1UL << 3,
    CAP_RX_GAIN = 1UL << 4,
    CAP_REPEAT = 1UL << 5,
    CAP_ADVERT = 1UL << 6,
    CAP_FLOOD = 1UL << 7,
    CAP_LOOP = 1UL << 8,
    CAP_TX_DELAY = 1UL << 9,
    CAP_WIFI_POWER_SAVE = 1UL << 10,
    CAP_FEM_RX_GAIN = 1UL << 11,
    CAP_DELAYS = CAP_RX_DELAY | CAP_TX_DELAY,
  };

  // Role-neutral snapshot. Infrastructure and companion builds have different
  // persisted NodePrefs layouts, so the portal never casts either one or
  // assumes their member offsets match.
  struct NodeSnapshot {
    char name[32];
    char admin_password[32];       // empty = trusted LAN/no login prompt
    double lat;
    double lon;
    float freq;
    float bw;
    float airtime_factor;
    float rx_delay;
    float tx_delay;
    int8_t tx_power;
    uint8_t sf;
    uint8_t cr;
    uint8_t cad;
    uint8_t rx_gain;
    uint8_t fem_rx_gain;
    uint8_t repeat;
    uint16_t advert_interval;
    uint8_t flood_advert_interval;
    uint8_t flood_max;
    uint8_t flood_max_advert;
    uint8_t flood_max_unscoped;
    uint8_t loop_detect;
    uint32_t capabilities;
  };

  class Callbacks {
  public:
    virtual void getNodeSnapshot(NodeSnapshot& snapshot) = 0;
    // Run one CLI command. Called from tick() only (loop task); reply is 160 bytes.
    virtual void execCommand(char* cmd, char* reply) = 0;
    virtual void rebootNow() = 0;
    // Bracket a config batch so bridge restarts triggered by individual
    // `set` handlers can be coalesced into one.
    virtual void onConfigBatchStart() {}
    virtual void onConfigBatchEnd() {}
    // Fill buf with the stats JSON snapshot. Called from tick() (loop task).
    virtual void buildStatsJson(char* buf, size_t buf_size) = 0;
    // Teardown finished (server + DNS freed, WiFi mode restored).
    virtual void onWebConfigStopped() {}
  };

  // mqtt_prefs is an MQTTPrefs* on WITH_MQTT_BRIDGE builds and NULL otherwise.
  // owns_wifi is true when the portal itself brought up STA/AP (normal
  // repeater/room-server builds), false when the companion/MQTT runtime owns it.
  WebConfigServer(Callbacks* callbacks, void* mqtt_prefs, bool owns_wifi,
                  const uint8_t* pub_key, const char* fw_ver,
                  const char* role, const char* board_name);
  ~WebConfigServer();

  // For the device display: true while a setup-mode portal is active.
  // Fills the AP SSID and portal IP; either buffer may be NULL to just poll.
  // Call from the loop task only (same task that changes the mode).
  static bool getSetupInfo(char* ssid, size_t ssid_len, char* ip, size_t ip_len);

  // For the device display: true once a config save completed and the node is
  // about to reboot — ground truth for the user even if the browser lost its
  // connection before the confirmation arrived. Loop task only.
  static bool isRebootPending();

  // Persistent master switch and standalone WiFi credentials use separate NVS
  // namespaces. This avoids changing NodePrefs or MQTTPrefs file layouts.
  static bool loadEnabled(bool default_value = false);
  static bool saveEnabled(bool enabled);
  static bool loadStandaloneWiFi(char* ssid, size_t ssid_len,
                                 char* password, size_t password_len,
                                 uint8_t* power_save = NULL);
  static bool saveStandaloneWiFi(const char* ssid, const char* password,
                                 uint8_t power_save);

  // UI tasks use an otherwise-unused multi-click gesture without reaching into
  // MyMesh directly. The mesh loop consumes the request and performs the NVS /
  // WiFi transition on its own task.
  static void requestToggleFromButton();
  static bool takeButtonToggleRequest();

  bool startSetupMode(char reply[]);   // open SoftAP + DNS captive portal
  bool startLanMode(char reply[]);     // bind to existing STA connection
  bool startAutoMode(char reply[]);    // use saved WiFi, or setup AP when absent
  void requestStop();                  // stop listening now, free after grace period
  void tick(uint32_t now);             // call every loop iteration

  Mode mode() const { return _mode; }
  bool isRunning() const { return _mode != MODE_OFF; }
  bool isStopping() const { return _stopping; }

private:
  static const int MAX_BATCH = 24;
  static const size_t MAX_BODY = 4096;
  enum BatchState : uint8_t { BATCH_IDLE = 0, BATCH_PENDING, BATCH_DONE };
  struct BatchEntry {
    char key[24];     // allowlisted `set` key (echoed back to the UI)
    char cmd[160];    // full CLI command (may contain secrets - never echoed)
    char reply[160];
  };

  Callbacks* _cb;
  void* _mqtt_prefs;
  bool _owns_wifi;
  const uint8_t* _pub_key;
  const char* _fw_ver;
  const char* _role;
  const char* _board_name;

  AsyncWebServer* _server = NULL;
  DNSServer* _dns = NULL;
  SemaphoreHandle_t _mux;
  Mode _mode = MODE_OFF;
  bool _stopping = false;
  bool _was_setup_ap = false;
  uint32_t _connect_deadline = 0;
  char _wifi_ssid[32] = {0};
  char _wifi_password[64] = {0};
  uint8_t _wifi_power_save = 1;
  char _ap_ssid[33] = {0};

  // Most-recently-created instance, for the display's getSetupInfo() poll.
  static WebConfigServer* _active;

  // Command batch: filled by async_tcp under _mux, drained by tick().
  volatile BatchState _batch_state = BATCH_IDLE;
  uint8_t _batch_count = 0;
  uint8_t _batch_next = 0;        // drain progress (one command per tick)
  uint32_t _batch_last_cmd = 0;
  bool _batch_reboot = false;
  bool _batch_reboot_armed = false;
  bool _standalone_wifi_dirty = false;
  BatchEntry _batch[MAX_BATCH];

  // LAN-mode session (single slot; new login evicts the old session)
  char _session_token[33] = {0};
  uint32_t _session_last_seen = 0;
  uint8_t _login_fails = 0;
  uint32_t _login_lock_until = 0;

  // Once set (via /api/portal/exit), OS captive probes get native "success"
  // replies so the phone's sign-in sheet can be dismissed without dropping the
  // WiFi, letting the user continue in their real browser. async_tcp-task only.
  volatile bool _captive_release = false;

  // Save-path diagnostics: 1 Hz serial trace for 60 s after each config POST
  // (AP station count, heap, batch state) to pinpoint client drops on hardware.
  volatile uint32_t _diag_until = 0;
  uint32_t _diag_last = 0;

  volatile uint32_t _last_activity = 0;
  uint32_t _reboot_at = 0;      // 0 = none scheduled
  uint32_t _delete_at = 0;      // deferred teardown deadline
  volatile uint32_t _stats_wanted_until = 0;
  uint32_t _stats_built_at = 0;
  char _stats_json[1024] = {0};

  void createServer();
  void registerRoutes();
  void drainBatch(uint32_t now);
  void finalizeTeardown();
  bool checkAuth(AsyncWebServerRequest* req);
  static void collectBody(AsyncWebServerRequest* req, uint8_t* data, size_t len,
                          size_t index, size_t total);

  void handleRoot(AsyncWebServerRequest* req);
  void handleStatus(AsyncWebServerRequest* req);
  void handleLogin(AsyncWebServerRequest* req);
  void handleLogout(AsyncWebServerRequest* req);
  void handleConfigGet(AsyncWebServerRequest* req);
  void handleConfigPost(AsyncWebServerRequest* req);
  void handleConfigResult(AsyncWebServerRequest* req);
  void handleStats(AsyncWebServerRequest* req);
  void handleScan(AsyncWebServerRequest* req);
  void handlePresets(AsyncWebServerRequest* req);
  void handleReboot(AsyncWebServerRequest* req);
  void handlePortalExit(AsyncWebServerRequest* req);
  void handleNotFound(AsyncWebServerRequest* req);
};

#endif  // ESP_PLATFORM && !WEBCONFIG_DISABLED
