#pragma once
// Connection ownership for the Wi-Fi station.
//
// The station interface has exactly one owner at a time. Two actors contend for
// it:
//
//   STATION    the normal reconnect loop that walks the known profiles
//   CANDIDATE  a one-off validation of a newly submitted credential
//              (Web Portal or `USB wifi.add`)
//
// Without an explicit owner plus a generation, three real bugs appear:
//
//   * an old station waiter consumes the candidate's GOT_IP and writes the
//     station's profile back as "current" (B6 / stale waiter);
//   * a candidate that fails leaves the station paused forever, so only a
//     manual Hold-B exit restores connectivity (B4);
//   * a station loop that reconnects every few seconds fights a candidate that
//     just succeeded and drags the link back to the old network (B3).
//
// This module is the single, pure, host-tested answer to all three. It owns no
// driver state and performs no I/O: the firmware asks it what to do, then does
// it. Every result carries the generation it was produced under, so a late
// event from a previous owner is recognised as stale and discarded rather than
// applied.
//
// Model:
//
//   owner        NONE | STATION | CANDIDATE
//   generation   monotonically increasing; bumped on every ownership change
//   attempt      the SSID the current owner is trying to associate with
//   connected    the SSID the owner actually confirmed (empty while pending)
//
//   take_candidate()  invalidate the station generation and hand the interface
//                     to a candidate. The old station waiter can never win.
//   candidate_result() resolve a candidate (success commits, failure releases
//                     ownership so the station may re-acquire and reconnect a
//                     known profile).
//   station_*()       the normal loop's side: begin an attempt, resolve it, and
//                     ask whether a steady connection should be left alone.
#include <cstddef>
#include <cstdint>
#include <cstring>

namespace wearable {

// 802.11 SSID limit, shared with wifi_profiles.h.
constexpr size_t OWNERSHIP_SSID_MAX = 32;

enum class ConnectionOwner : uint8_t {
    None,       // nothing owns the station interface
    Station,    // the normal known-network reconnect loop
    Candidate,  // a one-off validation of a submitted credential
};

constexpr const char *connection_owner_name(ConnectionOwner owner) {
    switch (owner) {
    case ConnectionOwner::Station: return "STATION";
    case ConnectionOwner::Candidate: return "CANDIDATE";
    default: return "NONE";
    }
}

// Why a generation stopped owning the interface. Recorded so a waiter can prove
// its result was discarded rather than silently dropped.
enum class OwnershipStale : uint8_t {
    NotStale,     // the generation is still current
    Superseded,   // another owner took over (candidate takeover)
    Released,     // the owner finished (success/failure/cancel)
    Cancelled,    // the owner was cancelled without resolving (e.g. approval)
};

// A generation-tagged request to connect. `generation` is captured when the
// attempt begins and must be presented again when its GOT_IP/disconnect arrives.
struct ConnectionAttempt {
    ConnectionOwner owner = ConnectionOwner::None;
    uint32_t generation = 0;
    char ssid[OWNERSHIP_SSID_MAX + 1] = {};
    bool valid = false;

    void set(ConnectionOwner who, uint32_t gen, const char *name) {
        owner = who;
        generation = gen;
        std::strncpy(ssid, name ? name : "", OWNERSHIP_SSID_MAX);
        ssid[OWNERSHIP_SSID_MAX] = '\0';
        valid = true;
    }
    bool same_ssid(const char *other) const {
        return other && std::strcmp(ssid, other) == 0;
    }
    void clear() { valid = false; ssid[0] = '\0'; }
};

// The outcome of resolving an attempt. Only `Accepted` may change durable state
// (current SSID, mark_success, persistence).
enum class AttemptOutcome : uint8_t {
    Accepted,      // verified for the owning generation; commit it
    Stale,         // belongs to a superseded/released generation; ignore
    Mismatch,      // association succeeded but the AP is not the expected SSID
    Failed,        // the attempt did not reach GOT_IP
};

// What the firmware should do after resolving an attempt, so the policy stays
// in one place instead of being re-derived at each call site.
struct AttemptDirective {
    bool persist = false;          // write the profile / mark proven
    bool commit_current = false;   // adopt this SSID as the current link
    bool mark_success = false;     // advance the recency sequence for this SSID
    bool disconnect_candidate = false;  // drop the candidate link
    bool release_candidate = false;     // hand ownership back to the station
    bool resume_station = false;        // let the station loop run again
};

inline AttemptDirective accepted_directive() {
    AttemptDirective directive;
    directive.persist = true;
    directive.commit_current = true;
    directive.mark_success = true;
    return directive;
}

// A candidate failure keeps the candidate link down and returns the interface to
// the station so a known profile can reconnect. The AP/Portal itself is not
// affected: provisioning stays usable for another attempt.
inline AttemptDirective candidate_failure_directive() {
    AttemptDirective directive;
    directive.disconnect_candidate = true;
    directive.release_candidate = true;
    directive.resume_station = true;
    return directive;
}

// A candidate was cancelled (for example by a Human Gate preemption): drop the
// half-open link, release ownership, and resume the station. Never persists.
inline AttemptDirective candidate_cancelled_directive() {
    AttemptDirective directive;
    directive.disconnect_candidate = true;
    directive.release_candidate = true;
    directive.resume_station = true;
    return directive;
}

// Pure ownership machine. No I/O, no driver calls, no allocation.
class ConnectionOwnership {
public:
    ConnectionOwnership() = default;

    ConnectionOwner owner() const { return owner_; }
    uint32_t generation() const { return generation_; }
    bool is_current(const ConnectionAttempt &attempt) const {
        return attempt.valid && attempt.owner == owner_ && attempt.generation == generation_;
    }
    // SSID the current owner is trying to associate with, or "" when idle.
    const char *attempt_ssid() const { return attempt_.ssid; }
    // SSID the current owner has actually confirmed, or "" while pending.
    const char *connected_ssid() const { return connected_ssid_; }
    bool station_paused() const { return paused_; }

    // A station waiter captured this attempt; present it back to resolve it.
    const ConnectionAttempt &current_attempt() const { return attempt_; }

    // ---- Station side -----------------------------------------------------

    // Begin a station attempt for `ssid`. Returns the attempt to remember. It is
    // only legal while the station owns (or may take) the interface; if a
    // candidate owns it this returns an invalid attempt so the caller does not
    // touch the driver.
    ConnectionAttempt station_begin(const char *ssid) {
        ConnectionAttempt attempt;
        if (owner_ == ConnectionOwner::Candidate) return attempt;  // invalid
        if (owner_ != ConnectionOwner::Station) {
            owner_ = ConnectionOwner::Station;
        }
        ++generation_;
        connected_ssid_[0] = '\0';
        attempt.set(ConnectionOwner::Station, generation_, ssid);
        attempt_ = attempt;
        return attempt;
    }

    // Mark that the station loop has no more profiles to try (it will back off).
    // Ownership is released so a later candidate or a fresh loop can take it.
    void station_idle() {
        if (owner_ == ConnectionOwner::Station) {
            owner_ = ConnectionOwner::None;
            ++generation_;
            attempt_.clear();
            connected_ssid_[0] = '\0';
        }
    }

    // Resolve a station attempt with the association result. `actual_ssid` is
    // the SSID reported by the driver (esp_wifi_sta_get_ap_info), which lets a
    // stale or mismatched GOT_IP be rejected even when it arrives late.
    // Returns Stale when the attempt's generation no longer owns the interface.
    AttemptOutcome station_resolve(const ConnectionAttempt &attempt, bool got_ip,
                                   const char *actual_ssid) {
        if (!attempt.valid || attempt.owner != ConnectionOwner::Station)
            return AttemptOutcome::Stale;
        if (attempt.generation != generation_ || owner_ != ConnectionOwner::Station)
            return AttemptOutcome::Stale;
        if (!got_ip) return AttemptOutcome::Failed;
        // The driver says we are associated; make sure it is the network we
        // asked for. A GOT_IP for another owner must never count as our success.
        if (!attempt.same_ssid(actual_ssid)) return AttemptOutcome::Mismatch;
        std::strncpy(connected_ssid_, attempt.ssid, OWNERSHIP_SSID_MAX);
        connected_ssid_[OWNERSHIP_SSID_MAX] = '\0';
        attempt_.clear();
        return AttemptOutcome::Accepted;
    }

    // Something interrupted the station attempt (disconnect event). Clears the
    // connected SSID but keeps ownership so the loop retries.
    void station_disconnected() {
        if (owner_ != ConnectionOwner::Station) return;
        // A failed pending attempt must still resolve as Failed/Mismatch so the
        // station tries its next profile. Only a previously steady connection
        // invalidates an already accepted token on disconnect.
        if (connected_ssid_[0]) {
            ++generation_;
            attempt_.clear();
        }
        connected_ssid_[0] = '\0';
    }

    // True when the station is connected and still owns the interface, so the
    // loop must leave the link alone: no set_config, no connect, no mark_success.
    bool station_steady() const {
        return owner_ == ConnectionOwner::Station && connected_ssid_[0] != '\0';
    }

    // ---- Candidate side ---------------------------------------------------

    // Take the interface for a candidate. This invalidates the station
    // generation (a waiting station attempt becomes Stale) and pauses the loop.
    ConnectionAttempt take_candidate(const char *ssid) {
        if (owner_==ConnectionOwner::Station) station_stale_ = OwnershipStale::Superseded;
        owner_ = ConnectionOwner::Candidate;
        ++generation_;
        paused_ = true;
        connected_ssid_[0] = '\0';
        ConnectionAttempt attempt;
        attempt.set(ConnectionOwner::Candidate, generation_, ssid);
        attempt_ = attempt;
        return attempt;
    }

    // Resolve a candidate attempt. Success keeps the link and commits it;
    // failure/cancel releases ownership so the station can reconnect a known
    // profile (the AP/Portal itself stays up).
    AttemptOutcome candidate_resolve(const ConnectionAttempt &attempt, bool got_ip,
                                     const char *actual_ssid, AttemptDirective &directive) {
        directive = AttemptDirective{};
        if (!attempt.valid || attempt.owner != ConnectionOwner::Candidate)
            return AttemptOutcome::Stale;
        if (attempt.generation != generation_ || owner_ != ConnectionOwner::Candidate)
            return AttemptOutcome::Stale;
        if (got_ip && !attempt.same_ssid(actual_ssid)) {
            // The link came up, but on the wrong network: never persist it.
            directive = candidate_failure_directive();
            release_candidate();
            return AttemptOutcome::Mismatch;
        }
        if (!got_ip) {
            directive = candidate_failure_directive();
            release_candidate();
            return AttemptOutcome::Failed;
        }
        directive = accepted_directive();
        std::strncpy(connected_ssid_, attempt.ssid, OWNERSHIP_SSID_MAX);
        connected_ssid_[OWNERSHIP_SSID_MAX] = '\0';
        // A successful candidate becomes the station's connection: ownership
        // returns to the station loop, which now sees a steady link and idles.
        owner_ = ConnectionOwner::Station;
        paused_ = false;
        attempt_.clear();
        return AttemptOutcome::Accepted;
    }

    // Cancel the current candidate without a verdict (Human Gate preemption).
    // Returns false when there was no candidate to cancel.
    bool cancel_candidate() {
        if (owner_ != ConnectionOwner::Candidate) return false;
        release_candidate();
        return true;
    }

    // Invalidate the current owner as cancelled so any waiter is told Stale.
    void cancel_owner() {
        if (owner_ != ConnectionOwner::None) {
            station_stale_ = OwnershipStale::Cancelled;
            owner_ = ConnectionOwner::None;
            ++generation_;
            paused_ = false;
            attempt_.clear();
            connected_ssid_[0] = '\0';
        }
    }

    // Ownership status of a candidate attempt, for logging/tests.
    OwnershipStale candidate_status(const ConnectionAttempt &attempt) const {
        if (attempt.valid && attempt.generation == generation_ &&
            owner_ == ConnectionOwner::Candidate)
            return OwnershipStale::NotStale;
        return station_stale_;
    }

    // The last reason a generation stopped owning the interface.
    OwnershipStale last_stale() const { return station_stale_; }

    // ---- Explicit user actions -------------------------------------------

    // Forget the profile the station is currently connected to. The link is
    // dropped and the loop will try the remaining profiles.
    void forget_current() {
        cancel_owner();
    }

private:
    void release_candidate() {
        owner_ = ConnectionOwner::None;
        ++generation_;
        paused_ = false;
        attempt_.clear();
        station_stale_ = OwnershipStale::Released;
    }

    ConnectionOwner owner_ = ConnectionOwner::None;
    uint32_t generation_ = 1;
    bool paused_ = false;
    ConnectionAttempt attempt_{};
    char connected_ssid_[OWNERSHIP_SSID_MAX + 1] = {};
    OwnershipStale station_stale_ = OwnershipStale::NotStale;
};

}  // namespace wearable
