#pragma once
// Network selection and provisioning policy for Wi-Fi provisioning.
//
// Pure, host-testable. This file encodes the plan's blocking requirements
// (docs/wifi-provisioning-plan.md sections 3, 16, 17, 20, 34):
//
//   * The device NEVER enters SoftAP because a connection failed. Only an
//     explicit user request may start provisioning.
//   * Normal boot loads known profiles, tries last-successful first, then the
//     rest, and falls back to OFFLINE with backoff.
//   * Candidate credentials are validated before they are persisted; a failed
//     candidate never disturbs existing profiles.
#include "wifi_profiles.h"
#include <cstdint>

namespace wearable {

// Connection supervision state surfaced to the UI. There is deliberately no
// "starting AP" state here: provisioning is a separate, explicit mode.
enum class WifiLinkState : uint8_t {
    Offline,        // no usable known network
    Connecting,     // attempting a known profile
    Connected,      // station has an IP
    NoKnownWifi,    // profile store is empty
};

constexpr const char *wifi_link_label(WifiLinkState state) {
    switch (state) {
    case WifiLinkState::Connected: return "CONNECTED";
    case WifiLinkState::Connecting: return "CONNECTING";
    case WifiLinkState::NoKnownWifi: return "NO KNOWN WIFI";
    default: return "OFFLINE";
    }
}

// Explicit user-visible provisioning mode. Transitions are:
//   Normal -> StartingAccessPoint -> Provisioning -> (Normal | Failed)
// Only `request_start` may leave Normal.
enum class ProvisioningMode : uint8_t {
    Normal,
    StartingAccessPoint,
    Provisioning,
    Failed,
};

// Outcome of testing candidate credentials before persisting them.
enum class CandidateResult : uint8_t {
    Pending,
    Success,
    Failed,       // wrong password / association rejected
    NotFound,     // SSID not present in a scan
    NoCompatible, // no usable 2.4 GHz AP
    StorageError, // connected but could not persist
};

// Reason text for the failure UX (plan section 35), never a bare "ERROR".
// Kept short enough for the round watch status line; the Portal shows the
// same wording.
constexpr const char *candidate_reason(CandidateResult result) {
    switch (result) {
    case CandidateResult::Success: return "Connected";
    case CandidateResult::NotFound: return "Network not found";
    case CandidateResult::NoCompatible: return "No compatible Wi-Fi";
    case CandidateResult::StorageError: return "Connected; not saved";
    case CandidateResult::Failed: return "Wrong password?";
    default: return "Connecting";
    }
}

// Backoff for repeated connection attempts. Doubles up to a ceiling so an
// absent network is retried but not hammered. Pure and bounded.
class WifiRetryBackoff {
public:
    explicit WifiRetryBackoff(uint32_t base_ms = 2000, uint32_t ceiling_ms = 60000)
        : base_(base_ms), ceiling_(ceiling_ms), next_(base_ms) {}
    uint32_t next_delay() const { return next_; }
    void failed() {
        attempts_++;
        next_ = next_ >= ceiling_ ? ceiling_ : next_ * 2;
        if (next_ > ceiling_) next_ = ceiling_;
    }
    void succeeded() {
        attempts_ = 0;
        next_ = base_;
    }
    uint32_t attempts() const { return attempts_; }
private:
    uint32_t base_, ceiling_, next_;
    uint32_t attempts_ = 0;
};

// Does this mode allow the user to open Wi-Fi Setup? A pending approval blocks
// it so provisioning can never cover the Human Gate (plan section 32).
inline bool wifi_setup_allowed(bool approval_pending) { return !approval_pending; }

// Start gating: only a fully normal runtime may start the AP, and only after
// an explicit confirmation. Returns the new mode.
inline ProvisioningMode request_start_access_point(ProvisioningMode current, bool confirmed) {
    if (!confirmed) return current;
    if (current != ProvisioningMode::Normal) return current;
    return ProvisioningMode::StartingAccessPoint;
}

inline ProvisioningMode access_point_ready(ProvisioningMode current, bool started) {
    if (current != ProvisioningMode::StartingAccessPoint) return current;
    return started ? ProvisioningMode::Provisioning : ProvisioningMode::Failed;
}

// Leaving provisioning always lands in Normal. It never leaves a background AP.
inline ProvisioningMode exit_provisioning(ProvisioningMode) { return ProvisioningMode::Normal; }

// The AP must only exist while explicitly provisioning.
inline bool access_point_should_run(ProvisioningMode mode) {
    return mode == ProvisioningMode::Provisioning || mode == ProvisioningMode::StartingAccessPoint;
}

// Classify a station disconnect reason into a candidate result without leaking
// numeric reason codes to the user.
inline CandidateResult classify_candidate_failure(int reason_code, bool saw_in_scan) {
    if (!saw_in_scan) return CandidateResult::NotFound;
    // 15 = 4WAY_HANDSHAKE_TIMEOUT, 205 = CONNECTION_FAIL (bad password),
    // 202 = AUTH_FAIL. Everything else is treated as a failed connection.
    switch (reason_code) {
    case 15: case 202: case 205: return CandidateResult::Failed;
    default: return CandidateResult::Failed;
    }
}

// Candidate-test step ordering.
//
// ESP-IDF requires an explicit `esp_wifi_disconnect()` before
// `esp_wifi_connect()` when the station is already associated. This enum and the
// planner below encode that ordering as pure logic so the sequence can be
// host-tested, while the firmware performs the actual driver calls.
enum class CandidateStep : uint8_t {
    PauseReconnect,      // stop the normal station loop owning the interface
    DisconnectCurrent,   // esp_wifi_disconnect() if (and only if) associated
    WaitDisconnected,    // bounded, clear the stale connected state
    SetConfig,           // install the candidate configuration
    Connect,             // esp_wifi_connect()
    AwaitResult,         // wait for GOT_IP or failure
};

struct CandidatePlan {
    bool already_connected = false;  // station was associated when the test began
    bool paused_reconnect = false;   // caller confirmed the normal loop is paused
};

// Produce the ordered steps to run. `DisconnectCurrent`/`WaitDisconnected` are
// always included: a disconnect is a documented precondition for a fresh
// connect, and it is a harmless no-op when the station is already idle. Keeping
// them unconditional removes any chance of a code path that connects on a live
// link. `plan` is accepted for call-site clarity and future variation; the
// ordering is intentionally identical for connected and idle stations.
inline size_t candidate_plan(const CandidatePlan & /*plan*/, CandidateStep *out, size_t capacity) {
    static const CandidateStep kSteps[] = {
        CandidateStep::PauseReconnect,
        CandidateStep::DisconnectCurrent,
        CandidateStep::WaitDisconnected,
        CandidateStep::SetConfig,
        CandidateStep::Connect,
        CandidateStep::AwaitResult,
    };
    const size_t count = sizeof(kSteps) / sizeof(kSteps[0]);
    if (!out) return count;
    const size_t copy = count < capacity ? count : capacity;
    for (size_t i = 0; i < copy; ++i) out[i] = kSteps[i];
    return count;
}

// After a candidate failure, the normal station manager must be free to
// reconnect a known profile. The candidate link is dropped (never left half
// associated) and the pause is released.
struct CandidateAftermath {
    bool disconnect_candidate = true;
    bool resume_reconnect = true;
    bool persist = false;
};

inline CandidateAftermath candidate_aftermath(CandidateResult result) {
    CandidateAftermath aftermath;
    switch (result) {
    case CandidateResult::Success:
        // The candidate link is the good one; keep it and persist.
        aftermath.disconnect_candidate = false;
        aftermath.persist = true;
        break;
    case CandidateResult::StorageError:
        // Connected but unsaved: keep the link so the user is still online, but
        // report that nothing was written.
        aftermath.disconnect_candidate = false;
        break;
    default:
        // Failure: drop the candidate so the manager can reconnect a known one.
        break;
    }
    return aftermath;
}

// Stable SoftAP SSID: "AgentPager-XXXX" from a device identity suffix. The
// caller supplies the last four uppercase hex characters of a stable identity
// (MAC suffix) so the SSID is identical across boots and provisioning sessions
// (plan section 6.1).
inline void provisioning_ssid(const char *identity_suffix, char *out, size_t size) {
    char suffix[5] = {'0', '0', '0', '0', '\0'};
    if (identity_suffix && identity_suffix[0]) {
        size_t n = 0;
        for (const char *p = identity_suffix; *p && n < 4; ++p) {
            char c = *p;
            if (c >= 'a' && c <= 'f') c = char(c - 'a' + 'A');
            if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')) suffix[n++] = c;
        }
        while (n < 4) suffix[n++] = '0';
    }
    // snprintf into a caller buffer; bounded with the provided size.
    const char *prefix = "AgentPager-";
    size_t written = 0;
    for (const char *p = prefix; *p && written + 1 < size; ++p) out[written++] = *p;
    for (int i = 0; i < 4 && written + 1 < size; ++i) out[written++] = suffix[i];
    if (size) out[written] = '\0';
}

// Standard Wi-Fi QR payload for the provisioning AP. Contains only the
// provisioning SSID and the fixed provisioning password; never a device token
// (plan sections 9, 33). Only the values are escaped; the structural
// "WIFI:T:WPA;S:...;P:...;;" syntax must keep its literal separators.
inline void provisioning_qr_payload(const char *ssid, const char *password, char *out, size_t size) {
    size_t used = 0;
    auto literal = [&](const char *text) {
        for (const char *p = text; p && *p && used + 1 < size; ++p) out[used++] = *p;
    };
    auto value = [&](const char *text) {
        for (const char *p = text; p && *p && used + 2 < size; ++p) {
            const char c = *p;
            if (c == '\\' || c == ';' || c == ',' || c == ':' || c == '"') out[used++] = '\\';
            out[used++] = c;
        }
    };
    literal("WIFI:T:WPA;S:");
    value(ssid);
    literal(";P:");
    value(password);
    literal(";;");
    if (size) out[used < size ? used : size - 1] = '\0';
}

// The provisioning AP password is a fixed, centralized development constant.
// It is not derived from any device/gateway credential and is safe to display
// because the AP only exists during an explicit, on-device provisioning mode
// (plan sections 7, 8). WPA2 minimum is 8 characters.
constexpr const char *PROVISIONING_AP_PASSWORD = "agentpager-setup";

// SoftAP address; the Portal must always answer here (plan section 11).
constexpr const char *PROVISIONING_AP_ADDRESS = "192.168.4.1";

}  // namespace wearable