// ============================================================================
// Spoof Gate Client — implementation
// See spoof_gate_client.h for architecture notes.
// ============================================================================

#include "spoof_gate_client.h"

#include <cstring>

// millis() shim
// - On Arduino: pulled in from <Arduino.h>
// - In UNIT_TEST (native env): the test file defines millis() once and owns
//   the shared clock.  We just forward-declare it here.
// - Otherwise (e.g. bare-metal non-Arduino host): provide a default stub.
#ifdef ARDUINO
#include <Arduino.h>
#elif defined(UNIT_TEST)
#include <cstdint>
extern uint32_t millis();   // provided by test_spoof_pec.cpp
#else
#include <cstdint>
static uint32_t s_fallback_millis = 0;
static inline uint32_t millis() { return s_fallback_millis; }
#endif

// ---------------------------------------------------------------------------
// LRU cache — fixed-size, no dynamic allocation
// ---------------------------------------------------------------------------

struct SpoofeEntry {
    uint32_t track_id;
    uint8_t  score_q8;
    uint32_t ts_ms;   // millis() when last updated
    bool     valid;
};

// Cache arranged as a flat array; LRU eviction uses a simple access-order
// counter (uint32_t lru_seq) — the entry with the smallest seq is the oldest.
struct SpoofeCache {
    SpoofeEntry entries[SPOOF_GATE_CACHE_SIZE];
    uint32_t    lru_seq;  // monotonically incremented on each insert/update
};

static SpoofeCache s_cache;

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

// Return pointer to existing entry for track_id, or nullptr.
static SpoofeEntry* cache_find(uint32_t track_id) {
    for (uint8_t i = 0; i < SPOOF_GATE_CACHE_SIZE; i++) {
        if (s_cache.entries[i].valid && s_cache.entries[i].track_id == track_id) {
            return &s_cache.entries[i];
        }
    }
    return nullptr;
}

// Return index of slot to evict: prefer an invalid slot, otherwise the one
// with the smallest lru_seq (true least-recently-used eviction via expiry
// preference: expired > oldest live).
static uint8_t cache_evict_slot(uint32_t now_ms) {
    // Prefer an already-invalid slot
    for (uint8_t i = 0; i < SPOOF_GATE_CACHE_SIZE; i++) {
        if (!s_cache.entries[i].valid) return i;
    }

    // Prefer an expired slot
    for (uint8_t i = 0; i < SPOOF_GATE_CACHE_SIZE; i++) {
        uint32_t age = now_ms - s_cache.entries[i].ts_ms;
        if (age >= SPOOF_GATE_ENTRY_TTL_MS) return i;
    }

    // All live — evict true LRU (placeholder: index 0, could track seq per
    // entry, but SPOOF_GATE_CACHE_SIZE=8 means linear scan is fine).
    // We track recency through ts_ms: oldest ts wins eviction.
    uint8_t oldest_idx = 0;
    uint32_t oldest_ts = s_cache.entries[0].ts_ms;
    for (uint8_t i = 1; i < SPOOF_GATE_CACHE_SIZE; i++) {
        if (s_cache.entries[i].ts_ms < oldest_ts) {
            oldest_ts = s_cache.entries[i].ts_ms;
            oldest_idx = i;
        }
    }
    return oldest_idx;
}

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

void spoof_gate_init() {
    memset(&s_cache, 0, sizeof(s_cache));
}

void spoof_gate_handle_packet(const uint8_t* buf, size_t len) {
    // Require at least msg_type (1 byte) + SpoofScoreMsg (6 bytes) = 7 bytes
    if (buf == nullptr || len < SPOOF_SCORE_MSG_MIN_LEN) return;

    // buf[0] is msg_type (already checked by caller's switch).
    // SpoofScoreMsg starts at offset 1.
    SpoofScoreMsg msg;
    memcpy(&msg, buf + 1, sizeof(SpoofScoreMsg));

    uint32_t now = millis();

    // Update existing entry or insert new one
    SpoofeEntry* existing = cache_find(msg.track_id);
    if (existing) {
        existing->score_q8 = msg.score_q8;
        existing->ts_ms    = now;
    } else {
        uint8_t slot = cache_evict_slot(now);
        s_cache.entries[slot].track_id = msg.track_id;
        s_cache.entries[slot].score_q8 = msg.score_q8;
        s_cache.entries[slot].ts_ms    = now;
        s_cache.entries[slot].valid    = true;
    }

    s_cache.lru_seq++;
}

uint8_t spoof_gate_score_for_track(uint32_t track_id) {
    uint32_t now = millis();
    SpoofeEntry* e = cache_find(track_id);
    if (!e) return 0;

    uint32_t age = now - e->ts_ms;
    if (age >= SPOOF_GATE_ENTRY_TTL_MS) {
        // Mark expired so future lookups skip it
        e->valid = false;
        return 0;
    }

    return e->score_q8;
}

bool spoof_gate_should_promote(uint32_t track_id, uint8_t threshold) {
    return spoof_gate_score_for_track(track_id) >= threshold;
}
