// Copyright (C) 2026, Boundary Mode Extension
// Based on microReticulum_Firmware by Mark Qvist
//
// BoundaryConfig.h — Captive-portal web configuration for the legacy
// "Boundary Mode" path. This should be renamed to "Transport Mode"
// together with the rest of the boundary-mode terminology. In this fork,
// transport/boundary mode is the only intended mode of operation.
// When triggered (first boot with no config, or button hold >5s),
// the device starts a WiFi AP with a web form for all settings:
//   WiFi STA credentials, TCP backbone params, LoRa radio params,
//   and optional AP-mode TCP server.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

#ifndef BOUNDARY_CONFIG_H
#define BOUNDARY_CONFIG_H

#ifdef BOUNDARY_MODE

#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>

// ─── Node hash (cached in RTC by normal boot, read here without starting RNS) ─
#define NODE_HASH_RTC_MAGIC  0x504B4841UL
extern uint32_t rtc_node_hash_magic;
extern char     rtc_node_hash_hex[33];

// ─── Config Portal State ─────────────────────────────────────────────────────
static bool config_portal_active = false;
static WebServer* config_server = nullptr;
static DNSServer* config_dns    = nullptr;

static const char CONFIG_AP_SSID[] = "RNode-Boundary-Setup";
static const uint16_t DNS_PORT = 53;
static const uint16_t HTTP_PORT = 80;

// Forward declarations
void config_portal_start();
void config_portal_stop();
void config_portal_loop();
bool config_portal_is_active();
bool boundary_needs_config();

// ─── Common bandwidth values (Hz) ───────────────────────────────────────────
// These match Reticulum standard channel plans
// Stored in flash (PROGMEM) to save ~200 bytes of RAM
static const uint32_t BW_OPTIONS_HZ[] PROGMEM = {
    7800, 10400, 15600, 20800, 31250, 41700, 62500, 125000, 250000, 500000,
};
static const char BW_LABEL_0[]  PROGMEM = "7.8 kHz";
static const char BW_LABEL_1[]  PROGMEM = "10.4 kHz";
static const char BW_LABEL_2[]  PROGMEM = "15.6 kHz";
static const char BW_LABEL_3[]  PROGMEM = "20.8 kHz";
static const char BW_LABEL_4[]  PROGMEM = "31.25 kHz";
static const char BW_LABEL_5[]  PROGMEM = "41.7 kHz";
static const char BW_LABEL_6[]  PROGMEM = "62.5 kHz";
static const char BW_LABEL_7[]  PROGMEM = "125 kHz";
static const char BW_LABEL_8[]  PROGMEM = "250 kHz";
static const char BW_LABEL_9[]  PROGMEM = "500 kHz";
static const char* const BW_OPTIONS_LABELS[] PROGMEM = {
    BW_LABEL_0, BW_LABEL_1, BW_LABEL_2, BW_LABEL_3, BW_LABEL_4,
    BW_LABEL_5, BW_LABEL_6, BW_LABEL_7, BW_LABEL_8, BW_LABEL_9,
};
static const int BW_OPTIONS_COUNT = sizeof(BW_OPTIONS_HZ) / sizeof(BW_OPTIONS_HZ[0]);

static uint8_t config_default_display_rotation() {
    #if BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_RAK4631
    return 0;
    #elif BOARD_MODEL == BOARD_HELTEC32_V2 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC_T114 || BOARD_MODEL == BOARD_TBEAM_S_V1
    return 1;
    #else
    return 3;
    #endif
}

// ─── HTML Page Generation ────────────────────────────────────────────────────

static void config_send_html() {
    // Read current values from EEPROM/globals for pre-population
    char cur_ssid[33] = "";
    char cur_psk[33]  = "";

    for (int i = 0; i < 32; i++) {
        cur_ssid[i] = EEPROM.read(config_addr(ADDR_CONF_SSID + i));
        if (cur_ssid[i] == (char)0xFF) cur_ssid[i] = '\0';
    }
    cur_ssid[32] = '\0';

    for (int i = 0; i < 32; i++) {
        cur_psk[i] = EEPROM.read(config_addr(ADDR_CONF_PSK + i));
        if (cur_psk[i] == (char)0xFF) cur_psk[i] = '\0';
    }
    cur_psk[32] = '\0';

    // Current LoRa values (from globals, which were loaded from EEPROM)
    uint32_t cur_freq = lora_freq;
    uint32_t cur_bw   = lora_bw;
    int cur_sf        = lora_sf;
    int cur_cr        = lora_cr;
    int cur_txp       = lora_txp;
    if (cur_txp == 0xFF) cur_txp = PA_MAX_OUTPUT;  // Default to board max

    // Default frequency if not set
    if (cur_freq == 0) cur_freq = 868825000;  // 914.875 MHz default
    if (cur_bw == 0)   cur_bw   = 125000;     // 125 kHz default
    if (cur_sf == 0)   cur_sf   = 10;         // SF10 default
    if (cur_cr < 5 || cur_cr > 8) cur_cr = 7; // CR 4/5 default

    // Build the HTML page
    String html = F(
        "<!DOCTYPE html><html><head>"
        "<meta name='viewport' content='width=device-width,initial-scale=1'>"
        "<title>RNode Boundary Setup</title>"
        "<style>"
        "body{font-family:sans-serif;background:#1a1a2e;color:#e0e0e0;margin:0;padding:16px;}"
        "h1{color:#e94560;font-size:1.4em;margin:0 0 8px;}"
        "h2{color:#0f3460;background:#e0e0e0;padding:6px 10px;margin:18px -10px 10px;font-size:1em;border-radius:4px;}"
        "form{max-width:480px;margin:0 auto;}"
        "label{display:block;margin:8px 0 2px;font-size:0.9em;color:#aaa;}"
        "input,select{width:100%;padding:8px;margin:2px 0 6px;box-sizing:border-box;"
        "background:#16213e;border:1px solid #0f3460;color:#e0e0e0;border-radius:4px;font-size:0.95em;}"
        "input:focus,select:focus{border-color:#e94560;outline:none;}"
        ".row{display:flex;gap:10px;}.row>div{flex:1;}"
        ".note{font-size:0.8em;color:#666;margin:2px 0 8px;}"
        "button{width:100%;padding:12px;margin:20px 0;background:#e94560;color:#fff;"
        "border:none;border-radius:4px;font-size:1.1em;cursor:pointer;}"
        "button:hover{background:#c73e54;}"
        ".ok{background:#16213e;padding:20px;border-radius:8px;text-align:center;}"
        ".ok h1{color:#0f0;}"
        ".node-hash{background:#0f1a30;border:1px solid #0f3460;border-radius:6px;"
        "padding:10px 14px;margin:0 0 16px;}"
        ".node-hash .nh-label{display:block;font-size:0.75em;color:#888;margin-bottom:4px;}"
        ".node-hash code{font-family:monospace;font-size:0.95em;color:#7ecfff;"
        "word-break:break-all;letter-spacing:0.05em;}"
        "</style></head><body>"
        "<h1>&#x1f4e1; RNode Boundary Node</h1>"
    );

    // ── Node public hash ──
    html += F("<div class='node-hash'><span class='nh-label'>&#x1f511; Node Hash (Reticulum destination)</span><code>");
    if (rtc_node_hash_magic == NODE_HASH_RTC_MAGIC && rtc_node_hash_hex[0] != '\0') {
        html += String(rtc_node_hash_hex);
    } else {
        html += F("<span style='color:#888;font-style:italic;'>Not yet assigned &mdash; will be set on first normal boot</span>");
    }
    html += F("</code></div>");

    html += F("<form method='POST' action='/save'>");
    html += F(
        "<h2>&#x1f4f6; WiFi Network</h2>"
        "<label>WiFi</label>"
        "<select name='wifi_en'>"
    );
    html += F("<option value='1'");
    if (boundary_state.wifi_enabled) html += F(" selected");
    html += F(">Enabled</option>");
    html += F("<option value='0'");
    if (!boundary_state.wifi_enabled) html += F(" selected");
    html += F(">Disabled (LoRa-only repeater)</option>");
    html += F("</select>");

    html += F(
        "<label>SSID</label>"
        "<input name='ssid' maxlength='32' placeholder='Your WiFi network' value='"
    );
    html += String(cur_ssid);
    html += F(
        "'>"
        "<label>Password</label>"
        "<input name='psk' type='password' maxlength='32' placeholder='WiFi password' value='"
    );
    html += String(cur_psk);
    html += F("'>");

    // ── TCP Backbone Section ──
    html += F(
        "<h2>&#x1f310; TCP Backbone</h2>"
        "<label>Mode</label>"
        "<select name='tcp_mode'>"
    );
    html += F("<option value='0'");
    if (boundary_state.tcp_mode == 0) html += F(" selected");
    html += F(">Disabled</option>");
    html += F("<option value='1'");
    if (boundary_state.tcp_mode == 1) html += F(" selected");
    html += F(">Client (connect to backbone)</option>");
    html += F("</select>");

    html += F("<label>Backbone Host</label>");
    html += F("<input name='bb_host' maxlength='63' placeholder='e.g. 192.168.1.100' value='");
    html += String(boundary_state.backbone_host);
    html += F("'>");

    html += F("<label>Backbone Port</label>");
    html += F("<input name='bb_port' type='number' min='1' max='65535' value='");
    html += String(boundary_state.backbone_port);
    html += F("'>");

    // ── Local TCP Server Section ──
    html += F(
        "<h2>&#x1f4e1; Local TCP Server (optional)</h2>"
        "<p class='note'>Run a TCP server on the same WiFi network so local devices can connect. "
        "Uses Access Point mode (does not forward announces).</p>"
        "<label>Local TCP Server</label>"
        "<select name='ap_tcp_en'>"
    );
    html += F("<option value='0'");
    if (!boundary_state.ap_tcp_enabled) html += F(" selected");
    html += F(">Disabled</option>");
    html += F("<option value='1'");
    if (boundary_state.ap_tcp_enabled) html += F(" selected");
    html += F(">Enabled</option>");
    html += F("</select>");

    html += F("<label>TCP Port</label>");
    html += F("<input name='ap_tcp_port' type='number' min='1' max='65535' value='");
    html += String(boundary_state.ap_tcp_port);
    html += F("'>");

    // ── LoRa Radio Section ──
    html += F(
        "<h2>&#x1f4fb; LoRa Radio</h2>"
    );

    // Frequency — show in MHz for human-friendliness
    char freq_str[16];
    dtostrf((double)cur_freq / 1000000.0, 1, 3, freq_str);
    html += F("<label>Frequency (MHz)</label>");
    html += F("<input name='freq' type='text' placeholder='914.875' value='");
    html += String(freq_str);
    html += F("'>");
    html += F("<p class='note'>e.g. 914.875, 868.000, 433.000</p>");

    // Bandwidth — dropdown
    html += F("<label>Bandwidth</label><select name='bw'>");
    for (int i = 0; i < BW_OPTIONS_COUNT; i++) {
        uint32_t bw_hz = pgm_read_dword(&BW_OPTIONS_HZ[i]);
        char label_buf[16];
        strncpy_P(label_buf, (const char*)pgm_read_ptr(&BW_OPTIONS_LABELS[i]), sizeof(label_buf)-1);
        label_buf[sizeof(label_buf)-1] = '\0';
        html += F("<option value='");
        html += String(bw_hz);
        html += "'";
        if (bw_hz == cur_bw) html += F(" selected");
        html += ">";
        html += label_buf;
        html += F("</option>");
    }
    html += F("</select>");

    // Spreading Factor — dropdown 5-12
    html += F("<label>Spreading Factor</label><select name='sf'>");
    for (int sf = 5; sf <= 12; sf++) {
        html += F("<option value='");
        html += String(sf);
        html += "'";
        if (sf == cur_sf) html += F(" selected");
        html += ">SF";
        html += String(sf);
        html += F("</option>");
    }
    html += F("</select>");

    // Coding Rate — dropdown 5-8 (maps to 4/5 through 4/8)
    html += F("<label>Coding Rate</label><select name='cr'>");
    for (int cr = 5; cr <= 8; cr++) {
        html += F("<option value='");
        html += String(cr);
        html += "'";
        if (cr == cur_cr) html += F(" selected");
        html += ">4/";
        html += String(cr);
        html += F("</option>");
    }
    html += F("</select>");

    // TX Power
    html += F("<label>TX Power (dBm)</label>");
    html += F("<input name='txp' type='number' min='2' max='");
    #ifdef PA_MAX_OUTPUT
    html += String(PA_MAX_OUTPUT);
    #else
    html += "22";
    #endif
    html += F("' value='");
    html += String(cur_txp);
    html += F("'>");

    #ifdef PA_MAX_OUTPUT
    html += F("<p class='note'>Max output for this board: ");
    html += String(PA_MAX_OUTPUT);
    html += F(" dBm (with PA)</p>");
    #endif

    // ── IFAC (Interface Access Code) Section ──
    html += F(
        "<h2>&#x1f512; Network Access (IFAC)</h2>"
        "<p class='note'>Set a network name and/or passphrase to restrict LoRa interface access. "
        "Only nodes with matching settings can communicate. Both fields are optional.</p>"
        "<label>IFAC</label>"
        "<select name='ifac_en'>"
    );
    html += F("<option value='0'");
    if (!boundary_state.ifac_enabled) html += F(" selected");
    html += F(">Disabled</option>");
    html += F("<option value='1'");
    if (boundary_state.ifac_enabled) html += F(" selected");
    html += F(">Enabled</option>");
    html += F("</select>");

    html += F("<label>Network Name</label>");
    html += F("<input name='ifac_name' maxlength='32' placeholder='e.g. MyNetwork' value='");
    html += String(boundary_state.ifac_netname);
    html += F("'>");

    html += F("<label>Passphrase</label>");
    html += F("<input name='ifac_pass' type='password' maxlength='32' placeholder='Shared secret' value='");
    html += String(boundary_state.ifac_passphrase);
    html += F("'>");

    // ── Options Section ──
    html += F(
        "<h2>&#x2699; Options</h2>"
        "<label>Display Blanking</label>"
        "<select name='disp_blank'>"
    );

    // Read current blanking timeout from EEPROM (stored as minutes, 0 = never)
    uint8_t cur_blank = 5;
    if (EEPROM.read(eeprom_addr(ADDR_CONF_BSET)) == CONF_OK_BYTE) {
        cur_blank = EEPROM.read(eeprom_addr(ADDR_CONF_DBLK));
    }

    uint8_t cur_rotation = EEPROM.read(eeprom_addr(ADDR_CONF_DROT));
    if (cur_rotation > 3) {
        cur_rotation = config_default_display_rotation();
    }

    static const uint8_t blank_vals[]   = { 0, 1, 5, 10, 30, 60 };
    static const char* blank_labels[]   = { "Never", "1 minute", "5 minutes", "10 minutes", "30 minutes", "60 minutes" };
    static const int blank_count = 6;

    for (int i = 0; i < blank_count; i++) {
        html += F("<option value='");
        html += String(blank_vals[i]);
        html += "'";
        if (blank_vals[i] == cur_blank) html += F(" selected");
        html += ">";
        html += blank_labels[i];
        html += F("</option>");
    }
    html += F("</select>");
    html += F("<p class='note'>Turn off display after inactivity to save power</p>");

    html += F("<label>Display Orientation</label><select name='disp_rot'>");
    html += F("<option value='0'");
    if (cur_rotation == 0) html += F(" selected");
    html += F(">Landscape</option>");
    html += F("<option value='1'");
    if (cur_rotation == 1) html += F(" selected");
    html += F(">Portrait</option>");
    html += F("<option value='2'");
    if (cur_rotation == 2) html += F(" selected");
    html += F(">Landscape Flipped</option>");
    html += F("<option value='3'");
    if (cur_rotation == 3) html += F(" selected");
    html += F(">Portrait Flipped</option>");
    html += F("</select>");
    html += F("<p class='note'>Choose the orientation that matches your OLED mounting. "
              "Landscape modes place the two status panes side by side; portrait modes stack them.</p>");

    // ── Submit ──
    html += F(
        "<button type='submit'>Save &amp; Reboot</button>"
        "</form></body></html>"
    );

    config_server->send(200, "text/html", html);
}

// ─── Handle POST /save ──────────────────────────────────────────────────────

static void config_handle_save() {
    // ── WiFi STA credentials ──
    String ssid = config_server->arg("ssid");
    String psk  = config_server->arg("psk");

    // Write SSID to config EEPROM area
    for (int i = 0; i < 32; i++) {
        uint8_t c = (i < (int)ssid.length()) ? ssid[i] : 0x00;
        EEPROM.write(config_addr(ADDR_CONF_SSID + i), c);
    }
    EEPROM.write(config_addr(ADDR_CONF_SSID + 32), 0x00);

    // Write PSK
    for (int i = 0; i < 32; i++) {
        uint8_t c = (i < (int)psk.length()) ? psk[i] : 0x00;
        EEPROM.write(config_addr(ADDR_CONF_PSK + i), c);
    }
    EEPROM.write(config_addr(ADDR_CONF_PSK + 32), 0x00);

    // Set WiFi mode to STA
    EEPROM.write(eeprom_addr(ADDR_CONF_WIFI), WR_WIFI_STA);

    // Boundary mode always uses DHCP on the STA interface. Clear the legacy
    // static IP and netmask slots so stale values from older firmware or tools
    // cannot force a persistent static address.
    for (int i = 0; i < 4; i++) {
        EEPROM.write(config_addr(ADDR_CONF_IP + i), 0x00);
        EEPROM.write(config_addr(ADDR_CONF_NM + i), 0x00);
    }

    // ── WiFi enable setting ──
    boundary_state.wifi_enabled = (config_server->arg("wifi_en").toInt() == 1);

    // ── Display blanking (EEPROM stores minutes, 0 = disabled) ──
    int blank_minutes = config_server->arg("disp_blank").toInt();
    if (blank_minutes <= 0) {
        display_blanking_enabled = false;
        eeprom_update(eeprom_addr(ADDR_CONF_BSET), CONF_OK_BYTE);
        eeprom_update(eeprom_addr(ADDR_CONF_DBLK), 0);
    } else {
        uint8_t blank_val = (uint8_t)(blank_minutes > 255 ? 255 : blank_minutes);
        display_blanking_enabled = true;
        display_blanking_timeout = (uint32_t)blank_val * 60UL * 1000UL;
        eeprom_update(eeprom_addr(ADDR_CONF_BSET), CONF_OK_BYTE);
        eeprom_update(eeprom_addr(ADDR_CONF_DBLK), blank_val);
    }

    int display_rotation = config_server->arg("disp_rot").toInt();
    if (display_rotation < 0 || display_rotation > 3) {
        display_rotation = config_default_display_rotation();
    }
    eeprom_update(eeprom_addr(ADDR_CONF_DROT), (uint8_t)display_rotation);

    // ── TCP backbone settings ──
    boundary_state.tcp_mode = (uint8_t)config_server->arg("tcp_mode").toInt(); // 0=disabled, 1=client
    if (boundary_state.tcp_mode > 1) boundary_state.tcp_mode = 0;
    boundary_state.tcp_port = (uint16_t)config_server->arg("tcp_port").toInt();
    if (boundary_state.tcp_port == 0) boundary_state.tcp_port = 4242;

    String bb_host = config_server->arg("bb_host");
    memset(boundary_state.backbone_host, 0, sizeof(boundary_state.backbone_host));
    strncpy(boundary_state.backbone_host, bb_host.c_str(), sizeof(boundary_state.backbone_host) - 1);

    boundary_state.backbone_port = (uint16_t)config_server->arg("bb_port").toInt();
    if (boundary_state.backbone_port == 0) boundary_state.backbone_port = 4242;

    // ── Local TCP server settings ──
    boundary_state.ap_tcp_enabled = (config_server->arg("ap_tcp_en").toInt() == 1);
    boundary_state.ap_tcp_port = (uint16_t)config_server->arg("ap_tcp_port").toInt();
    if (boundary_state.ap_tcp_port == 0) boundary_state.ap_tcp_port = 4242;

    // ── IFAC settings ──
    boundary_state.ifac_enabled = (config_server->arg("ifac_en").toInt() == 1);

    String ifac_name = config_server->arg("ifac_name");
    memset(boundary_state.ifac_netname, 0, sizeof(boundary_state.ifac_netname));
    strncpy(boundary_state.ifac_netname, ifac_name.c_str(), sizeof(boundary_state.ifac_netname) - 1);

    String ifac_pass = config_server->arg("ifac_pass");
    memset(boundary_state.ifac_passphrase, 0, sizeof(boundary_state.ifac_passphrase));
    strncpy(boundary_state.ifac_passphrase, ifac_pass.c_str(), sizeof(boundary_state.ifac_passphrase) - 1);

    // If IFAC is enabled but both fields are empty, disable it
    if (boundary_state.ifac_enabled &&
        boundary_state.ifac_netname[0] == '\0' &&
        boundary_state.ifac_passphrase[0] == '\0') {
        boundary_state.ifac_enabled = false;
    }

    // Save boundary config to EEPROM
    boundary_save_config();

    // ── LoRa radio settings ──
    String freq_str = config_server->arg("freq");
    double freq_mhz = freq_str.toDouble();
    if (freq_mhz > 0) {
        lora_freq = (uint32_t)(freq_mhz * 1000000.0);
    }

    String bw_str = config_server->arg("bw");
    uint32_t bw_val = (uint32_t)bw_str.toInt();
    if (bw_val > 0) lora_bw = bw_val;

    int sf_val = config_server->arg("sf").toInt();
    if (sf_val >= 5 && sf_val <= 12) lora_sf = sf_val;

    int cr_val = config_server->arg("cr").toInt();
    if (cr_val >= 5 && cr_val <= 8) lora_cr = cr_val;

    int txp_val = config_server->arg("txp").toInt();
    if (txp_val >= 2 && txp_val <= 30) lora_txp = txp_val;

    // Save LoRa config to EEPROM (reuse existing eeprom_conf functions)
    // Write directly since hw_ready may not be set yet
    eeprom_update(eeprom_addr(ADDR_CONF_SF), lora_sf);
    eeprom_update(eeprom_addr(ADDR_CONF_CR), lora_cr);
    eeprom_update(eeprom_addr(ADDR_CONF_TXP), lora_txp);
    eeprom_update(eeprom_addr(ADDR_CONF_BW) + 0, lora_bw >> 24);
    eeprom_update(eeprom_addr(ADDR_CONF_BW) + 1, lora_bw >> 16);
    eeprom_update(eeprom_addr(ADDR_CONF_BW) + 2, lora_bw >> 8);
    eeprom_update(eeprom_addr(ADDR_CONF_BW) + 3, lora_bw);
    eeprom_update(eeprom_addr(ADDR_CONF_FREQ) + 0, lora_freq >> 24);
    eeprom_update(eeprom_addr(ADDR_CONF_FREQ) + 1, lora_freq >> 16);
    eeprom_update(eeprom_addr(ADDR_CONF_FREQ) + 2, lora_freq >> 8);
    eeprom_update(eeprom_addr(ADDR_CONF_FREQ) + 3, lora_freq);
    eeprom_update(eeprom_addr(ADDR_CONF_OK), CONF_OK_BYTE);

    EEPROM.commit();

    // ── Send confirmation page ──
    String ok = F(
        "<!DOCTYPE html><html><head>"
        "<meta name='viewport' content='width=device-width,initial-scale=1'>"
        "<title>Saved</title>"
        "<style>"
        "body{font-family:sans-serif;background:#1a1a2e;color:#e0e0e0;padding:40px;"
        "display:flex;align-items:center;justify-content:center;min-height:80vh;}"
        ".ok{background:#16213e;padding:30px;border-radius:12px;text-align:center;max-width:400px;}"
        "h1{color:#4caf50;margin-bottom:16px;}"
        "p{color:#aaa;}"
        "</style></head><body>"
        "<div class='ok'>"
        "<h1>&#x2705; Configuration Saved</h1>"
        "<p>Device will reboot in 3 seconds and connect to your WiFi network.</p>"
        "<p style='color:#666;font-size:0.85em;'>If the device cannot connect, hold the button for 5+ seconds to re-enter setup.</p>"
        "</div></body></html>"
    );
    config_server->send(200, "text/html", ok);

    // Give the response time to send
    delay(3000);

    // Reboot
    ESP.restart();
}

// ─── Captive Portal redirect ─────────────────────────────────────────────────
static void config_handle_redirect() {
    config_server->sendHeader("Location", "http://10.0.0.1/", true);
    config_server->send(302, "text/plain", "Redirecting to setup...");
}

// ─── Check if config is needed ───────────────────────────────────────────────
bool boundary_needs_config() {
    // If the RTNode app marker is missing, this node was either never
    // configured by RTNode or was flashed from a different firmware family
    // such as stock RNode. Force the portal so RTNode can claim and rewrite
    // its persisted settings explicitly.
    if (!boundary_app_marker_valid()) {
        return true;
    }

    // Check if WiFi SSID is configured
    char ssid[33];
    for (int i = 0; i < 32; i++) {
        ssid[i] = EEPROM.read(config_addr(ADDR_CONF_SSID + i));
        if (ssid[i] == (char)0xFF) ssid[i] = '\0';
    }
    ssid[32] = '\0';

    // Also check boundary mode enable flag
    uint8_t bmode = EEPROM.read(config_addr(ADDR_CONF_BMODE));

    // Need config if no SSID set and boundary not yet configured
    if (ssid[0] == '\0' && bmode != BOUNDARY_ENABLE_BYTE) {
        return true;
    }
    return false;
}

// ─── Start Config Portal ─────────────────────────────────────────────────────
void config_portal_start() {
    if (config_portal_active) return;

    Serial.println("[Config] Starting configuration portal...");

    // Stop any existing WiFi
    WiFi.softAPdisconnect(true);
    WiFi.disconnect(true, true);
    WiFi.mode(WIFI_MODE_NULL);
    delay(100);

    // Start AP
    WiFi.mode(WIFI_AP);
    WiFi.softAP(CONFIG_AP_SSID, NULL);  // Open AP for easy setup
    delay(150);

    IPAddress ap_addr(10, 0, 0, 1);
    IPAddress ap_mask(255, 255, 255, 0);
    WiFi.softAPConfig(ap_addr, ap_addr, ap_mask);

    Serial.print("[Config] AP started: ");
    Serial.println(CONFIG_AP_SSID);
    Serial.print("[Config] IP: ");
    Serial.println(WiFi.softAPIP());

    // Start DNS server for captive portal (redirect all domains to us)
    config_dns = new DNSServer();
    config_dns->start(DNS_PORT, "*", ap_addr);

    // Start web server
    config_server = new WebServer(HTTP_PORT);
    config_server->on("/", HTTP_GET, config_send_html);
    config_server->on("/save", HTTP_POST, config_handle_save);
    config_server->onNotFound(config_handle_redirect);  // Captive portal catch-all
    config_server->begin();

    config_portal_active = true;

    Serial.println("[Config] Portal ready — connect to WiFi: " + String(CONFIG_AP_SSID));

    #if HAS_DISPLAY
    if (disp_ready) {
        // Show config mode on display
        stat_area.fillScreen(SSD1306_BLACK);
        stat_area.setCursor(0, 0);
        stat_area.println("CONFIG MODE");
        stat_area.println("");
        stat_area.println("Connect to:");
        stat_area.println(CONFIG_AP_SSID);
        stat_area.println("");
        stat_area.println("Open browser");
        stat_area.println("http://10.0.0.1");
        display.clearDisplay();
        display.drawBitmap(0, 0, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK);
        display.display();
    }
    #endif
    // Headless: LED ramp will be driven from the WCC portal loop
    if (headless_mode) {
        Serial.println("[Config] Headless mode — LED will breathe during config portal");
    }
}

// ─── Stop Config Portal ──────────────────────────────────────────────────────
void config_portal_stop() {
    if (!config_portal_active) return;

    Serial.println("[Config] Stopping configuration portal");

    if (config_server) {
        config_server->stop();
        delete config_server;
        config_server = nullptr;
    }
    if (config_dns) {
        config_dns->stop();
        delete config_dns;
        config_dns = nullptr;
    }

    WiFi.softAPdisconnect(true);
    WiFi.mode(WIFI_MODE_NULL);
    config_portal_active = false;
}

// ─── Portal Loop — call from main loop() ─────────────────────────────────────
void config_portal_loop() {
    if (!config_portal_active) return;
    if (config_dns)    config_dns->processNextRequest();
    if (config_server) config_server->handleClient();
}

// ─── Is portal active? ──────────────────────────────────────────────────────
bool config_portal_is_active() {
    return config_portal_active;
}

#endif // BOUNDARY_MODE
#endif // BOUNDARY_CONFIG_H
