#include "wifi_manager.h"

#include <algorithm>
#include <atomic>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>

#include "board.h"
#include "cloud_client.h"
#include "esp_event.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_netif.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "board_network.h"
#include "portal_html.h"
#include "wifi_usb_protocol.h"
#include "wifi_store_nvs.h"

namespace {

constexpr const char *TAG = "WIFI";
constexpr int kMaxScanResults = 20;
constexpr uint32_t kConnectTimeoutMs = 15000;
// Bounded wait for the station to leave an existing association before a
// candidate test connects. Kept well under kConnectTimeoutMs.
constexpr uint32_t kDisconnectSettleMs = 3000;
constexpr uint32_t kIdleProvisioningTimeoutMs = 15u * 60u * 1000u;

EventGroupHandle_t s_events;
wearable::NvsWiFiStorage s_storage;
wearable::WiFiProfileStore s_store(s_storage);
SemaphoreHandle_t s_lock;
esp_netif_t *s_sta_netif = nullptr;
esp_netif_t *s_ap_netif = nullptr;
httpd_handle_t s_portal = nullptr;

std::atomic<wearable::WifiLinkState> s_state{wearable::WifiLinkState::NoKnownWifi};
std::atomic<wearable::ProvisioningMode> s_provisioning{wearable::ProvisioningMode::Normal};
std::atomic<wearable::CandidateResult> s_last_candidate{wearable::CandidateResult::Pending};
std::atomic<bool> s_ap_running{false};
std::atomic<uint32_t> s_success_sequence{0};

char s_current_ssid[wearable::WIFI_SSID_MAX + 1] = {};
int s_current_profile = -1;
// Candidate validation handoff between the HTTP handler and the connect worker.
char s_candidate_ssid[wearable::WIFI_SSID_MAX + 1] = {};
char s_candidate_password[wearable::WIFI_PASSWORD_MAX + 1] = {};
std::atomic<bool> s_candidate_pending{false};
// Set while a candidate connection test (Portal or USB) owns the station
// interface, so the normal reconnect loop does not fight the validation.
std::atomic<bool> s_station_paused{false};
// Bumped on every user action so a stale connect attempt cannot report success.
std::atomic<uint32_t> s_connect_epoch{1};
char s_provision_ssid[40] = {};
char s_provision_qr[160] = {};
uint64_t s_provision_started_ms = 0;

struct Guard {
    Guard() { if (s_lock) xSemaphoreTake(s_lock, portMAX_DELAY); }
    ~Guard() { if (s_lock) xSemaphoreGive(s_lock); }
};

// ---- Disconnect reason tracking ------------------------------------------
// The last disconnect reason feeds candidate classification without leaking a
// numeric code to the user.
std::atomic<int> s_last_reason{0};

void set_state(wearable::WifiLinkState state) {
    s_state.store(state, std::memory_order_relaxed);
    board_set_wifi_status(state == wearable::WifiLinkState::Connected, s_current_ssid);
}

bool connect_profile(const wearable::WiFiProfile &profile) {
    wifi_config_t config = {};
    std::strncpy(reinterpret_cast<char *>(config.sta.ssid), profile.ssid, sizeof(config.sta.ssid) - 1);
    std::strncpy(reinterpret_cast<char *>(config.sta.password), profile.password,
                 sizeof(config.sta.password) - 1);
    config.sta.threshold.authmode = profile.password[0] == '\0' ? WIFI_AUTH_OPEN : WIFI_AUTH_WPA2_PSK;
    config.sta.pmf_cfg.capable = true;
    config.sta.pmf_cfg.required = false;
    if (esp_wifi_set_config(WIFI_IF_STA, &config) != ESP_OK) return false;
    // Log only the SSID; never the credential.
    ESP_LOGI(TAG, "[WIFI] connecting ssid=%s", profile.ssid);
    return esp_wifi_connect() == ESP_OK;
}

// Boot the station and try every known network in selection order. The device
// never starts the SoftAP here.
void station_task(void *) {
    for (;;) {
        // While a candidate test owns the station interface (provisioning or a
        // USB add), the normal reconnect loop must not fight it. It waits here
        // without touching esp_wifi_connect().
        if (s_station_paused.load() ||
            s_provisioning.load() != wearable::ProvisioningMode::Normal) {
            vTaskDelay(pdMS_TO_TICKS(500));
            continue;
        }
        std::array<const wearable::WiFiProfile *, wearable::WIFI_MAX_PROFILES> order{};
        size_t count = 0;
        {
            Guard guard;
            count = s_store.selection_order(order);
        }
        if (count == 0) {
            set_state(wearable::WifiLinkState::NoKnownWifi);
            // No known network: stay OFFLINE. This is a blocking requirement,
            // not a provisioning trigger.
            vTaskDelay(pdMS_TO_TICKS(5000));
            continue;
        }
        set_state(wearable::WifiLinkState::Connecting);
        for (size_t i = 0; i < count; ++i) {
            // Copy the profile under the guard so a concurrent Forget cannot
            // free it mid-connect.
            wearable::WiFiProfile profile;
            {
                Guard guard;
                profile = *order[i];
            }
            if (connect_profile(profile)) {
                // Wait for GOT_IP or a timeout; the event handler sets the bit.
                const auto bits = xEventGroupWaitBits(s_events, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE,
                                                      pdMS_TO_TICKS(kConnectTimeoutMs));
                if (bits & WIFI_CONNECTED_BIT) {                    std::strncpy(s_current_ssid, profile.ssid, sizeof(s_current_ssid) - 1);
                    s_current_ssid[sizeof(s_current_ssid) - 1] = '\0';
                    {
                        Guard guard;
                        const uint32_t sequence = ++s_success_sequence;
                        s_store.mark_success(profile.ssid, sequence);
                        for (size_t p = 0; p < s_store.count(); ++p)
                            if (s_store.at(p).same_ssid(profile.ssid)) s_current_profile = int(p);
                    }
                    set_state(wearable::WifiLinkState::Connected);
                    ESP_LOGI(TAG, "[WIFI] connection success ssid=%s", profile.ssid);
                    break;
                }
                esp_wifi_disconnect();
            }
        }
        // If we are not connected, back off before trying the set again.
        if (s_state.load() != wearable::WifiLinkState::Connected) {
            set_state(wearable::WifiLinkState::Offline);
            vTaskDelay(pdMS_TO_TICKS(5000));
        } else {
            vTaskDelay(pdMS_TO_TICKS(2000));
        }
    }
}

void handle_wifi_event(void *, esp_event_base_t base, int32_t id, void *data) {
    if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
        const auto *event = static_cast<wifi_event_sta_disconnected_t *>(data);
        s_last_reason.store(event ? int(event->reason) : 0, std::memory_order_relaxed);
        xEventGroupClearBits(s_events, WIFI_CONNECTED_BIT);
        if (s_state.load() == wearable::WifiLinkState::Connected) {
            s_current_ssid[0] = '\0';
            s_current_profile = -1;
            set_state(wearable::WifiLinkState::Connecting);
        }
        return;
    }
    if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
        auto *event = static_cast<ip_event_got_ip_t *>(data);
        char ip[16] = {};
        std::snprintf(ip, sizeof(ip), IPSTR, IP2STR(&event->ip_info.ip));
        ESP_LOGI(TAG, "[WIFI] connected %s", ip);
        if (event) {
            xEventGroupSetBits(s_events, WIFI_CONNECTED_BIT);
            set_state(wearable::WifiLinkState::Connected);
        }
    }
}

// ---- Portal ---------------------------------------------------------------

const char *kPortalIndex = R"HTML(<!doctype html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>AgentPager Wi-Fi Setup</title><style>
body{font-family:system-ui,sans-serif;margin:0;padding:24px;background:#101418;color:#eee}
h1{font-size:20px;margin:0 0 4px}p.note{color:#9aa;margin:0 0 16px;font-size:13px}
ul{list-style:none;padding:0;margin:0 0 16px}li{margin:0 0 6px}
button,input{font:inherit;padding:10px;border-radius:8px;border:1px solid #345;background:#182028;color:#eee}
button{background:#1f6feb;border-color:#1f6feb;width:100%%;margin-top:12px}
label{display:block;margin:12px 0 4px;font-size:13px;color:#9aa}
input{width:100%%;box-sizing:border-box}
.status{margin-top:16px;padding:10px;border-radius:8px;background:#182028;font-size:14px}
</style></head><body>
<h1>AgentPager Wi-Fi Setup</h1>
<p class="note">This device supports 2.4 GHz Wi-Fi only.</p>
<form method="post" action="/connect" id="f">
<ul id="nets">%s</ul>
<label for="ssid">Network name (SSID)</label>
<input id="ssid" name="ssid" maxlength="32" autocomplete="off" required>
<label for="password">Password</label>
<input id="password" name="password" type="password" maxlength="64" autocomplete="off">
<button type="submit">Connect</button>
</form>
<div class="status">%s</div>
<script>
document.querySelectorAll('#nets button').forEach(function(b){b.addEventListener('click',function(e){
e.preventDefault();document.getElementById('ssid').value=b.dataset.ssid;});});
</script></body></html>)HTML";

std::string portal_network_list() {
    wifi_ap_record_t records[kMaxScanResults] = {};
    uint16_t count = kMaxScanResults;
    std::string html;
    if (esp_wifi_scan_start(nullptr, true) == ESP_OK && esp_wifi_scan_get_ap_records(&count, records) == ESP_OK) {
        for (uint16_t i = 0; i < count; ++i) {
            // The SSID is untrusted external input. Escape it separately for the
            // attribute and the element text so a hostile SSID cannot break out
            // of the button or inject markup. The value is escaped, not dropped,
            // so the network stays selectable.
            const std::string raw(reinterpret_cast<const char *>(records[i].ssid));
            const std::string attr = wearable::portal_attr_escape(raw);
            const std::string text = wearable::portal_html_escape(raw);
            char item[320];
            // Only the SSID and RSSI are exposed; never a credential.
            std::snprintf(item, sizeof(item),
                          "<li><button data-ssid=\"%s\" type=\"button\">%s &nbsp; %d dBm</button></li>",
                          attr.c_str(), text.c_str(), int(records[i].rssi));
            html += item;
        }
    }
    if (html.empty()) html = "<li>No compatible Wi-Fi found</li>";
    return html;
}

std::string portal_status() {
    switch (s_last_candidate.load()) {
    case wearable::CandidateResult::Success: return "Setup complete. The device is connecting.";
    case wearable::CandidateResult::Failed: return "Connection failed. Check SSID/password and try again.";
    case wearable::CandidateResult::NotFound: return "Network not found.";
    case wearable::CandidateResult::NoCompatible: return "No compatible Wi-Fi found.";
    case wearable::CandidateResult::StorageError: return "Connected; not saved.";
    default: return "Select a network, enter the password and press Connect.";
    }
}

esp_err_t portal_root(httpd_req_t *request) {
    const std::string html = [] {
        std::string page = kPortalIndex;
        // Two %s placeholders: network list and status.
        const std::string nets = portal_network_list();
        const std::string status = portal_status();
        const size_t first = page.find("%s");
        page.replace(first, 2, nets);
        const size_t second = page.find("%s");
        page.replace(second, 2, status);
        return page;
    }();
    httpd_resp_set_type(request, "text/html");
    httpd_resp_send(request, html.c_str(), html.size());
    return ESP_OK;
}

esp_err_t portal_connect(httpd_req_t *request) {
    // Reject an oversized body before allocating anything. The bound is tiny
    // and documented; a provisioning client must not choose the allocation.
    if (!wearable::portal_body_within_limit(size_t(request->content_len))) {
        ESP_LOGW(TAG, "[WIFI] portal body rejected len=%u", unsigned(request->content_len));
        httpd_resp_send_err(request, HTTPD_413_CONTENT_TOO_LARGE, "body too large");
        return ESP_FAIL;
    }
    std::string body;
    body.resize(size_t(request->content_len));
    size_t received = 0;
    while (received < body.size()) {
        const int chunk = httpd_req_recv(request, body.data() + received, body.size() - received);
        if (chunk <= 0) {
            httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "bad request");
            return ESP_FAIL;
        }
        received += size_t(chunk);
    }
    const std::string ssid = wearable::portal_form_value(body, "ssid");
    const std::string password = wearable::portal_form_value(body, "password");
    // Never log the password; only the SSID is recorded.
    ESP_LOGI(TAG, "[WIFI] portal candidate ssid=%s", ssid.c_str());
    if (ssid.empty()) {
        const char *message = "SSID is required.";
        httpd_resp_set_type(request, "text/html");
        httpd_resp_send(request, message, std::strlen(message));
        return ESP_OK;
    }
    wifi_manager_test_candidate(ssid.c_str(), password.c_str());
    // Give the candidate attempt a bounded window to resolve, then report.
    for (int i = 0; i < 200; ++i) {
        const auto result = s_last_candidate.load();
        if (result != wearable::CandidateResult::Pending) break;
        vTaskDelay(pdMS_TO_TICKS(100));
    }
    const std::string message = portal_status();
    httpd_resp_set_type(request, "text/html");
    std::string page = "<!doctype html><meta charset=\"utf-8\"><body style=\"font-family:system-ui;padding:24px;background:#101418;color:#eee\"><p>";
    page += message;
    page += "</p><p><a style=\"color:#5af\" href=\"/\">Back</a></p></body>";
    httpd_resp_send(request, page.c_str(), page.size());
    return ESP_OK;
}

// Answer the OS captive-portal probes and redirect everything else to the
// Portal root. Auto-popup is a convenience, not a correctness requirement;
// http://192.168.4.1 always works.
esp_err_t portal_redirect(httpd_req_t *request) {
    httpd_resp_set_status(request, "302 Found");
    httpd_resp_set_hdr(request, "Location", "http://192.168.4.1/");
    httpd_resp_send(request, nullptr, 0);
    return ESP_OK;
}

esp_err_t portal_connect_handler(httpd_req_t *request) { return portal_connect(request); }
esp_err_t portal_root_handler(httpd_req_t *request) { return portal_root(request); }
esp_err_t portal_redirect_handler(httpd_req_t *request) { return portal_redirect(request); }

httpd_handle_t start_portal() {
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.lru_purge_enable = true;
    config.max_uri_handlers = 8;
    config.stack_size = 8192;
    httpd_handle_t server = nullptr;
    if (httpd_start(&server, &config) != ESP_OK) {
        ESP_LOGW(TAG, "[WIFI] portal start failed");
        return nullptr;
    }
    const httpd_uri_t root = {"/", HTTP_GET, portal_root_handler, nullptr};
    const httpd_uri_t connect = {"/connect", HTTP_POST, portal_connect_handler, nullptr};
    httpd_register_uri_handler(server, &root);
    httpd_register_uri_handler(server, &connect);
    // Catch-all so OS captive-portal probes are redirected to the Portal. This
    // is convenience only; http://192.168.4.1 always works regardless.
    const httpd_uri_t redirect = {"/*", HTTP_GET, portal_redirect_handler, nullptr};
    httpd_register_uri_handler(server, &redirect);
    return server;
}

void stop_portal() {
    if (s_portal) {
        httpd_stop(s_portal);
        s_portal = nullptr;
    }
}

// ---- Access point ---------------------------------------------------------

esp_err_t start_access_point() {
    if (!s_ap_netif) s_ap_netif = esp_netif_create_default_wifi_ap();
    wifi_config_t config = {};
    std::strncpy(reinterpret_cast<char *>(config.ap.ssid), s_provision_ssid, sizeof(config.ap.ssid) - 1);
    config.ap.ssid_len = std::strlen(s_provision_ssid);
    std::strncpy(reinterpret_cast<char *>(config.ap.password), wearable::PROVISIONING_AP_PASSWORD,
                 sizeof(config.ap.password) - 1);
    config.ap.max_connection = 4;
    config.ap.authmode = WIFI_AUTH_WPA2_PSK;
    config.ap.channel = 1;
    if (esp_wifi_set_mode(WIFI_MODE_APSTA) != ESP_OK) return ESP_FAIL;
    if (esp_wifi_set_config(WIFI_IF_AP, &config) != ESP_OK) return ESP_FAIL;
    s_ap_running.store(true);
    ESP_LOGI(TAG, "[WIFI] provisioning AP started ssid=%s", s_provision_ssid);
    return ESP_OK;
}

void stop_access_point() {
    if (!s_ap_running.load()) return;
    stop_portal();
    // Return to pure station mode; never leave a background AP running.
    esp_wifi_set_mode(WIFI_MODE_STA);
    s_ap_running.store(false);
    ESP_LOGI(TAG, "[WIFI] provisioning AP stopped");
}

// Candidate-first validation, shared by the Portal and the USB recovery path.
//
// The candidate is tested with a real station connection and only persisted on
// success, so a bad credential never overwrites a working profile. This is the
// single implementation of the rule; neither front-end re-implements it.
//
// Returns the verdict and leaves it in s_last_candidate so the Portal status
// line and the USB response report the same outcome.
// Wait until the station reports it is no longer connected, bounded so a stuck
// driver cannot hang validation. Returns true when the link is down.
bool wait_station_disconnected(uint32_t timeout_ms) {
    const uint64_t deadline = esp_timer_get_time() / 1000 + timeout_ms;
    for (;;) {
        wifi_ap_record_t ap = {};
        if (esp_wifi_sta_get_ap_info(&ap) != ESP_OK) return true;
        if (esp_timer_get_time() / 1000 >= deadline) return false;
        vTaskDelay(pdMS_TO_TICKS(20));
    }
}

wearable::CandidateResult validate_candidate(const char *ssid, const char *password) {
    // The step order is the host-tested `candidate_plan`: pause, disconnect,
    // settle, configure, connect, await. ESP-IDF requires an explicit
    // esp_wifi_disconnect() before esp_wifi_connect() when the station is already
    // associated, so a candidate test while the device is online must drop the
    // current link first. The disconnect is unconditional, which removes any
    // code path that connects on a live link.
    wearable::CandidateStep steps[8]{};
    const size_t step_count = wearable::candidate_plan(
        wearable::CandidatePlan{s_state.load() == wearable::WifiLinkState::Connected, true},
        steps, sizeof(steps) / sizeof(steps[0]));

    bool link_up = false;
    // Step 1: the caller already paused the normal reconnect loop (s_station_paused
    // for USB, provisioning mode for the Portal).
    xEventGroupClearBits(s_events, WIFI_CONNECTED_BIT);
    for (size_t i = 0; i < step_count; ++i) {
        switch (steps[i]) {
        case wearable::CandidateStep::PauseReconnect:
            break;  // Owned by the caller.
        case wearable::CandidateStep::DisconnectCurrent:
            if (s_current_ssid[0]) {
                ESP_LOGI(TAG, "[WIFI] candidate test: disconnecting current ssid=%s", s_current_ssid);
            }
            esp_wifi_disconnect();
            break;
        case wearable::CandidateStep::WaitDisconnected:
            if (!wait_station_disconnected(kDisconnectSettleMs)) {
                ESP_LOGW(TAG, "[WIFI] candidate test: station did not disconnect in time");
                // A stuck link must not be treated as a valid candidate.
                const auto aftermath =
                    wearable::candidate_aftermath(wearable::CandidateResult::Failed);
                if (aftermath.disconnect_candidate) esp_wifi_disconnect();
                return wearable::CandidateResult::Failed;
            }
            s_current_ssid[0] = '\0';
            s_current_profile = -1;
            break;
        case wearable::CandidateStep::SetConfig: {
            wifi_config_t config = {};
            std::strncpy(reinterpret_cast<char *>(config.sta.ssid), ssid, sizeof(config.sta.ssid) - 1);
            std::strncpy(reinterpret_cast<char *>(config.sta.password), password ? password : "",
                         sizeof(config.sta.password) - 1);
            const bool open = !password || password[0] == '\0';
            config.sta.threshold.authmode = open ? WIFI_AUTH_OPEN : WIFI_AUTH_WPA2_PSK;
            config.sta.pmf_cfg.capable = true;
            config.sta.pmf_cfg.required = false;
            esp_wifi_set_config(WIFI_IF_STA, &config);
            break;
        }
        case wearable::CandidateStep::Connect:
            set_state(wearable::WifiLinkState::Connecting);
            if (esp_wifi_connect() == ESP_OK) link_up = true;
            break;
        case wearable::CandidateStep::AwaitResult:
            if (link_up) {
                const auto bits = xEventGroupWaitBits(s_events, WIFI_CONNECTED_BIT, pdTRUE, pdTRUE,
                                                      pdMS_TO_TICKS(kConnectTimeoutMs));
                link_up = (bits & WIFI_CONNECTED_BIT) != 0;
            }
            break;
        }
    }

    if (!link_up) {
        // Failure never persists. The aftermath decision (host-tested) drops the
        // candidate link so the normal station manager can resume and reconnect a
        // known profile.
        const auto result = wearable::classify_candidate_failure(s_last_reason.load(), true);
        if (wearable::candidate_aftermath(result).disconnect_candidate) esp_wifi_disconnect();
        // Log only the SSID; a password is never printed.
        ESP_LOGI(TAG, "[WIFI] candidate failed ssid=%s", ssid);
        return result;
    }

    // The credential is good. Persist it now, then mark it proven.
    wearable::WiFiStoreResult result;
    {
        Guard guard;
        result = s_store.add_or_replace(ssid, password ? password : "");
        if (result == wearable::WiFiStoreResult::Ok) {
            const uint32_t sequence = ++s_success_sequence;
            s_store.mark_success(ssid, sequence);
            std::strncpy(s_current_ssid, ssid, sizeof(s_current_ssid) - 1);
            for (size_t p = 0; p < s_store.count(); ++p)
                if (s_store.at(p).same_ssid(ssid)) s_current_profile = int(p);
        }
    }
    if (result != wearable::WiFiStoreResult::Ok) {
        ESP_LOGW(TAG, "[WIFI] profile save failed ssid=%s", ssid);
        return wearable::CandidateResult::StorageError;
    }
    ESP_LOGI(TAG, "[WIFI] profile add ssid=%s", ssid);
    set_state(wearable::WifiLinkState::Connected);
    return wearable::CandidateResult::Success;
}

// Portal-driven candidate validation. Runs off the HTTP handler so the read
// loop is not blocked, and closes the AP on success.
void candidate_task(void *) {
    char ssid[wearable::WIFI_SSID_MAX + 1];
    char password[wearable::WIFI_PASSWORD_MAX + 1];
    {
        Guard guard;
        std::strncpy(ssid, s_candidate_ssid, sizeof(ssid) - 1);
        ssid[sizeof(ssid) - 1] = '\0';
        std::strncpy(password, s_candidate_password, sizeof(password) - 1);
        password[sizeof(password) - 1] = '\0';
    }
    const auto result = validate_candidate(ssid, password);
    s_last_candidate.store(result);
    s_candidate_pending.store(false);
    if (result == wearable::CandidateResult::Success) {
        // Success closes the AP and returns to normal station mode.
        vTaskDelay(pdMS_TO_TICKS(1500));
        wifi_manager_stop_provisioning();
    }
    vTaskDelete(nullptr);
}

// Leaves provisioning if it has been idle too long, so the AP never runs
// forever without the user knowing.
void provisioning_watchdog_task(void *) {
    for (;;) {
        vTaskDelay(pdMS_TO_TICKS(10000));
        if (s_provisioning.load() != wearable::ProvisioningMode::Provisioning) continue;
        const uint64_t now = esp_timer_get_time() / 1000;
        if (now - s_provision_started_ms >= kIdleProvisioningTimeoutMs) {
            ESP_LOGW(TAG, "[WIFI] provisioning idle timeout");
            board_show_message("WIFI SETUP", "Setup expired");
            wifi_manager_stop_provisioning();
        }
    }
}

// USB provisioning recovery path over the existing USB Serial/JTAG console.
// One line in, one framed response out. Passwords are accepted but never
// echoed; the protocol framing is documented in wifi_manager.h.
void usb_provision_task(void *) {
    char line[256];
    size_t used = 0;
    bool overflow = false;
    static char reply[1024];
    for (;;) {
        const int ch = getchar();
        if (ch == EOF) {
            vTaskDelay(pdMS_TO_TICKS(20));
            continue;
        }
        if (ch == '\r' || ch == '\n') {
            if (overflow) {
                std::snprintf(reply, sizeof(reply), "WFP/1 ERROR line-too-long\nWFP/1 END\n");
                std::fputs(reply, stdout);
                std::fflush(stdout);
                overflow = false;
                used = 0;
                continue;
            }
            if (used == 0) continue;
            line[used] = '\0';
            // The command line itself may contain a password; never log it.
            wifi_manager_handle_usb_line(line, reply, sizeof(reply));
            std::fputs(reply, stdout);
            std::fflush(stdout);
            used = 0;
            continue;
        }
        if (used + 1 < sizeof(line)) {
            line[used++] = char(ch);
        } else {
            overflow = true;
        }
    }
}

}  // namespace

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

esp_err_t wifi_manager_start(void) {
    s_lock = xSemaphoreCreateMutex();
    s_events = xEventGroupCreate();
    if (!s_lock || !s_events) return ESP_ERR_NO_MEM;

    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    s_sta_netif = esp_netif_create_default_wifi_sta();

    wifi_init_config_t init = WIFI_INIT_CONFIG_DEFAULT();
    // Profiles live in our own namespace; the driver must not keep a second
    // copy that could drift from WiFiProfileStore.
    init.nvs_enable = false;
    ESP_ERROR_CHECK(esp_wifi_init(&init));
    ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &handle_wifi_event,
                                                        nullptr, nullptr));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &handle_wifi_event,
                                                        nullptr, nullptr));

    s_store.load();
    // Build-time migration is a ONE-TIME first-run convenience, not a fallback.
    // The decision is the host-tested `should_migrate_build_wifi` policy: only a
    // genuinely Missing store may adopt the build-time credential. A valid store
    // (even empty after Forget All) or a Corrupt/unreadable store must not.
    const bool config_present = std::strlen(CONFIG_DEVICE_LAB_WIFI_SSID) > 0;
    const bool migrate = wearable::should_migrate_build_wifi(
        s_store.load_state(), s_store.initialized(), s_store.store_was_corrupt(), config_present);
    if (s_store.store_was_corrupt()) {
        // A damaged store is never repaired by re-importing a development
        // network. Report it and stay offline; the user can re-provision.
        ESP_LOGW(TAG, "[WIFI] store present but invalid; not migrating (no automatic SoftAP)");
    }
    if (migrate) {
        if (s_store.add_or_replace(CONFIG_DEVICE_LAB_WIFI_SSID, CONFIG_DEVICE_LAB_WIFI_PASSWORD) ==
            wearable::WiFiStoreResult::Ok) {
            ESP_LOGI(TAG, "[WIFI] migrated build-time profile ssid=%s", CONFIG_DEVICE_LAB_WIFI_SSID);
        } else {
            // The credential could not be written; still mark the store
            // initialized so a failed migration is not retried on every boot.
            s_store.mark_initialized();
        }
    }
    if (s_store.count() == 0) {
        ESP_LOGW(TAG, "[WIFI] no known networks initialized=%d corrupt=%d; staying offline (no automatic SoftAP)",
                 int(s_store.initialized()), int(s_store.store_was_corrupt()));
        set_state(wearable::WifiLinkState::NoKnownWifi);
    }

    // Deterministic provisioning SSID from the stable Wi-Fi MAC suffix.
    uint8_t mac[6] = {};
    esp_read_mac(mac, ESP_MAC_WIFI_STA);
    char suffix[8];
    std::snprintf(suffix, sizeof(suffix), "%02x%02x", mac[4], mac[5]);
    wearable::provisioning_ssid(suffix, s_provision_ssid, sizeof(s_provision_ssid));
    wearable::provisioning_qr_payload(s_provision_ssid, wearable::PROVISIONING_AP_PASSWORD, s_provision_qr,
                                      sizeof(s_provision_qr));

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());

    xTaskCreate(station_task, "wifi_station", 6144, nullptr, 4, nullptr);
    xTaskCreate(provisioning_watchdog_task, "wifi_ap_watch", 3072, nullptr, 2, nullptr);
    xTaskCreate(usb_provision_task, "wifi_usb", 4096, nullptr, 2, nullptr);
    ESP_LOGI(TAG, "[WIFI] station started profiles=%u provisioning_ssid=%s", unsigned(s_store.count()),
             s_provision_ssid);
    return ESP_OK;
}

EventGroupHandle_t wifi_manager_event_group(void) { return s_events; }

wearable::WifiLinkState wifi_manager_state(void) {
    if (s_provisioning.load() == wearable::ProvisioningMode::Provisioning)
        return wearable::WifiLinkState::Offline;
    return s_state.load();
}

const char *wifi_manager_current_ssid(void) { return s_current_ssid; }

size_t wifi_manager_profile_count(void) {
    Guard guard;
    return s_store.count();
}

const char *wifi_manager_profile_ssid(size_t index) {
    Guard guard;
    if (index >= s_store.count()) return "";
    return s_store.at(index).ssid;
}

int wifi_manager_current_profile_index(void) {
    Guard guard;
    return s_current_profile;
}

esp_err_t wifi_manager_forget(const char *ssid) {
    if (!ssid || !*ssid) return ESP_ERR_INVALID_ARG;
    bool was_current = false;
    {
        Guard guard;
        was_current = std::strcmp(s_current_ssid, ssid) == 0;
        if (s_store.remove(ssid) != wearable::WiFiStoreResult::Ok) return ESP_ERR_NOT_FOUND;
    }
    ESP_LOGI(TAG, "[WIFI] profile remove ssid=%s", ssid);
    if (was_current) {
        // Disconnect and let the station task try the remaining profiles;
        // still never auto-start the AP.
        s_current_ssid[0] = '\0';
        s_current_profile = -1;
        esp_wifi_disconnect();
    }
    return ESP_OK;
}

esp_err_t wifi_manager_forget_all(void) {
    {
        Guard guard;
        if (s_store.clear() != wearable::WiFiStoreResult::Ok) return ESP_FAIL;
    }
    ESP_LOGI(TAG, "[WIFI] profiles cleared");
    s_current_ssid[0] = '\0';
    s_current_profile = -1;
    esp_wifi_disconnect();
    return ESP_OK;
}

bool wifi_manager_setup_blocked(void) { return board_approval_pending(); }

esp_err_t wifi_manager_start_provisioning(void) {
    if (wifi_manager_setup_blocked()) {
        ESP_LOGW(TAG, "[WIFI] setup refused: pending approval");
        board_show_message("WIFI SETUP", "Finish pending approval first");
        return ESP_ERR_INVALID_STATE;
    }
    if (s_provisioning.load() != wearable::ProvisioningMode::Normal) return ESP_ERR_INVALID_STATE;
    s_provisioning.store(wearable::ProvisioningMode::StartingAccessPoint);
    s_last_candidate.store(wearable::CandidateResult::Pending);
    s_provision_started_ms = esp_timer_get_time() / 1000;
    if (start_access_point() != ESP_OK) {
        s_provisioning.store(wearable::ProvisioningMode::Failed);
        board_show_message("WIFI SETUP", "Wi-Fi setup unavailable");
        return ESP_FAIL;
    }
    s_portal = start_portal();
    s_provisioning.store(wearable::ProvisioningMode::Provisioning);
    return ESP_OK;
}

void wifi_manager_stop_provisioning(void) {
    if (s_provisioning.load() == wearable::ProvisioningMode::Normal) return;
    stop_access_point();
    s_provisioning.store(wearable::ProvisioningMode::Normal);
    // The candidate worker may have just established a working station link.
    // Keep it rather than forcing a needless disconnect/reconnect cycle; only
    // drop the link when the exit is a plain cancel with no good connection.
    if (s_state.load() == wearable::WifiLinkState::Connected && s_current_ssid[0]) {
        ESP_LOGI(TAG, "[WIFI] provisioning exited, keeping ssid=%s", s_current_ssid);
        return;
    }
    // Nudge the station task so it retries the known networks on the normal path.
    esp_wifi_disconnect();
    ESP_LOGI(TAG, "[WIFI] provisioning exited");
}

wearable::ProvisioningMode wifi_manager_provisioning_mode(void) { return s_provisioning.load(); }
const char *wifi_manager_provisioning_ssid(void) { return s_provision_ssid; }
const char *wifi_manager_provisioning_password(void) { return wearable::PROVISIONING_AP_PASSWORD; }
const char *wifi_manager_provisioning_address(void) { return wearable::PROVISIONING_AP_ADDRESS; }
const char *wifi_manager_provisioning_qr(void) { return s_provision_qr; }
wearable::CandidateResult wifi_manager_last_candidate(void) { return s_last_candidate.load(); }
const char *wifi_manager_last_candidate_ssid(void) { return s_candidate_ssid; }

esp_err_t wifi_manager_test_candidate(const char *ssid, const char *password) {
    if (!ssid || !*ssid) return ESP_ERR_INVALID_ARG;
    if (s_provisioning.load() != wearable::ProvisioningMode::Provisioning) return ESP_ERR_INVALID_STATE;
    if (s_candidate_pending.load()) return ESP_ERR_INVALID_STATE;
    {
        Guard guard;
        std::strncpy(s_candidate_ssid, ssid, sizeof(s_candidate_ssid) - 1);
        s_candidate_ssid[sizeof(s_candidate_ssid) - 1] = '\0';
        std::strncpy(s_candidate_password, password ? password : "", sizeof(s_candidate_password) - 1);
        s_candidate_password[sizeof(s_candidate_password) - 1] = '\0';
    }
    s_last_candidate.store(wearable::CandidateResult::Pending);
    s_candidate_pending.store(true);
    if (xTaskCreate(candidate_task, "wifi_candidate", 6144, nullptr, 4, nullptr) != pdPASS) {
        s_candidate_pending.store(false);
        return ESP_ERR_NO_MEM;
    }
    return ESP_OK;
}

// ---------------------------------------------------------------------------
// USB provisioning protocol
//
// Parsing and formatting live in the pure wifi_usb_protocol.h; this section
// binds the verbs to the real profile store. A password is accepted but never
// echoed in any response line.
// ---------------------------------------------------------------------------
namespace {
const char *usb_state_label(void *) { return wearable::wifi_link_label(wifi_manager_state()); }
const char *usb_current_ssid(void *) { return s_current_ssid; }
size_t usb_profile_count(void *) {
    Guard guard;
    return s_store.count();
}
const char *usb_profile_ssid(void *, size_t index) {
    Guard guard;
    return index < s_store.count() ? s_store.at(index).ssid : "";
}
bool usb_profile_proven(void *, size_t index) {
    Guard guard;
    return index < s_store.count() && s_store.at(index).last_success != 0;
}
// USB add is candidate-first, exactly like the Portal: the credential is
// tested with a real station connection and only persisted on success. The USB
// console task runs this synchronously so the response can report the verdict.
//
// If the station cannot be tested safely (a Portal candidate test is already
// running, or provisioning owns the interface), the verb reports UNAVAILABLE
// instead of pretending the credential was verified.
wearable::UsbAddOutcome usb_add(void *, const char *ssid, const char *password) {
    if (!ssid || !*ssid) return wearable::UsbAddOutcome::Invalid;
    if (!wearable::wifi_ssid_valid(ssid)) return wearable::UsbAddOutcome::Invalid;
    if (!wearable::wifi_password_valid(password)) return wearable::UsbAddOutcome::Invalid;
    // A concurrent candidate test owns the station; do not race it. During
    // provisioning the Portal owns the interface, so USB reports UNAVAILABLE.
    if (s_candidate_pending.load()) return wearable::UsbAddOutcome::Unavailable;
    if (s_provisioning.load() != wearable::ProvisioningMode::Normal)
        return wearable::UsbAddOutcome::Unavailable;
    if (s_station_paused.exchange(true)) return wearable::UsbAddOutcome::Unavailable;
    // Log the SSID only. The password never reaches a log line.
    ESP_LOGI(TAG, "[WIFI] usb candidate test ssid=%s", ssid);
    const auto result = validate_candidate(ssid, password ? password : "");
    s_station_paused.store(false);
    switch (result) {
    case wearable::CandidateResult::Success:
        return wearable::UsbAddOutcome::Verified;
    case wearable::CandidateResult::StorageError:
        return wearable::UsbAddOutcome::StorageError;
    case wearable::CandidateResult::NotFound:
    case wearable::CandidateResult::NoCompatible:
    case wearable::CandidateResult::Failed:
        // Candidate discarded; every existing profile is preserved.
        return wearable::UsbAddOutcome::Rejected;
    default:
        return wearable::UsbAddOutcome::Unavailable;
    }
}
bool usb_remove(void *, const char *ssid) { return wifi_manager_forget(ssid) == ESP_OK; }
bool usb_clear(void *) { return wifi_manager_forget_all() == ESP_OK; }
}

bool wifi_usb_line_has_password(const char *line) { return wearable::usb_line_contains_password(line); }

void wifi_manager_handle_usb_line(const char *line, char *reply, size_t reply_size) {
    wifi_manager_dispatch(line, reply, reply_size);
}

size_t wifi_manager_dispatch(const char *line, char *reply, size_t reply_size) {
    if (!reply || reply_size == 0) return 0;
    reply[0] = '\0';
    wearable::UsbWiFiRequest request;
    const bool framed = wearable::usb_parse_line(line, request);
    if (!framed) {
        std::snprintf(reply, reply_size, "WFP/1 ERROR version\n");
        return std::strlen(reply);
    }
    const wearable::UsbWiFiOps ops{usb_state_label, usb_current_ssid, usb_profile_count,
                                   usb_profile_ssid, usb_profile_proven, usb_add, usb_remove,
                                   usb_clear, nullptr};
    const std::string response = wearable::usb_handle_request(request, ops);
    const size_t copy = response.size() < reply_size - 1 ? response.size() : reply_size - 1;
    std::memcpy(reply, response.data(), copy);
    reply[copy] = '\0';
    return copy;
}

// ---------------------------------------------------------------------------
// Board-facing network interface (boards/common/include/board_network.h)
//
// These are the only functions board.cpp may call. They expose SSIDs, counts
// and state labels; a password never crosses this boundary.
// ---------------------------------------------------------------------------

board_wifi_state_t board_network_state(void) {
    switch (wifi_manager_state()) {
    case wearable::WifiLinkState::Connected: return BOARD_WIFI_CONNECTED;
    case wearable::WifiLinkState::Connecting: return BOARD_WIFI_CONNECTING;
    case wearable::WifiLinkState::NoKnownWifi: return BOARD_WIFI_NO_KNOWN;
    default: return BOARD_WIFI_OFFLINE;
    }
}

const char *board_network_state_label(void) {
    return wearable::wifi_link_label(wifi_manager_state());
}

const char *board_network_current_ssid(void) { return wifi_manager_current_ssid(); }

size_t board_network_profile_count(void) { return wifi_manager_profile_count(); }

const char *board_network_profile_ssid(size_t index) { return wifi_manager_profile_ssid(index); }

esp_err_t board_network_forget(const char *ssid) { return wifi_manager_forget(ssid); }

esp_err_t board_network_forget_all(void) { return wifi_manager_forget_all(); }

esp_err_t board_network_start_setup(void) { return wifi_manager_start_provisioning(); }

void board_network_stop_setup(void) { wifi_manager_stop_provisioning(); }

bool board_network_setup_running(void) {
    return wearable::access_point_should_run(wifi_manager_provisioning_mode());
}

bool board_network_setup_blocked(void) { return wifi_manager_setup_blocked(); }

const char *board_network_setup_ssid(void) { return wifi_manager_provisioning_ssid(); }

const char *board_network_setup_password(void) { return wifi_manager_provisioning_password(); }

const char *board_network_setup_address(void) { return wifi_manager_provisioning_address(); }

const char *board_network_setup_status(void) {
    return wearable::candidate_reason(wifi_manager_last_candidate());
}
