// ============================================================================
// csi_carrier_detector.cpp — see csi_carrier_detector.h for design notes,
// the corrected explanation of what CSI actually provides, and the honest
// validation caveat.
// ============================================================================

#include "csi_carrier_detector.h"

#include <cstring>

#define CSI_MAX_SUBCARRIERS 200   // defensive cap; real CSI buffers are well under this

// ---------------------------------------------------------------------------
// Pure classifier — no radio access, always compiled, directly testable.
// ---------------------------------------------------------------------------

size_t csi_compute_amplitude(const int8_t* csi_buf, size_t csi_len, uint16_t* out) {
    if (!csi_buf || !out) return 0;
    size_t n = csi_len / 2;
    if (n > CSI_MAX_SUBCARRIERS) n = CSI_MAX_SUBCARRIERS;
    for (size_t i = 0; i < n; i++) {
        int imag = csi_buf[i * 2];
        int real = csi_buf[i * 2 + 1];
        int mag = (imag < 0 ? -imag : imag) + (real < 0 ? -real : real);
        out[i] = (uint16_t)mag;
    }
    return n;
}

// Simple insertion-sort median on a small (<=200 element) copy -- cheap
// enough at this size, avoids pulling in <algorithm>/qsort overhead, and
// keeps this fully deterministic for the native test build.
static uint16_t median_of(const uint16_t* amplitudes, size_t n) {
    if (n == 0) return 0;
    uint16_t tmp[CSI_MAX_SUBCARRIERS];
    size_t cap = n > CSI_MAX_SUBCARRIERS ? CSI_MAX_SUBCARRIERS : n;
    memcpy(tmp, amplitudes, cap * sizeof(uint16_t));
    for (size_t i = 1; i < cap; i++) {
        uint16_t key = tmp[i];
        size_t j = i;
        while (j > 0 && tmp[j - 1] > key) { tmp[j] = tmp[j - 1]; j--; }
        tmp[j] = key;
    }
    return tmp[cap / 2];
}

bool csi_has_localized_spike(const uint16_t* amplitudes, size_t n,
                             float spike_ratio, float max_spike_width_frac) {
    if (!amplitudes || n < 4) return false;   // too few subcarriers to say anything

    uint16_t med = median_of(amplitudes, n);
    if (med == 0) return false;   // no meaningful baseline -- can't judge a "spike" over nothing

    float threshold = (float)med * spike_ratio;

    // Find the longest contiguous run of subcarriers above threshold.
    size_t longest_run = 0;
    size_t run = 0;
    for (size_t i = 0; i < n; i++) {
        if ((float)amplitudes[i] > threshold) {
            run++;
            if (run > longest_run) longest_run = run;
        } else {
            run = 0;
        }
    }
    if (longest_run == 0) return false;   // no spike at all

    float width_frac = (float)longest_run / (float)n;
    return width_frac <= max_spike_width_frac;
}

// ---------------------------------------------------------------------------
// Shared state — bounded, no dynamic allocation.
// ---------------------------------------------------------------------------
namespace {

struct ChannelTrack {
    uint32_t consecutive_spiky;
    uint32_t last_seen_ms;
    int8_t   last_rssi;
    bool     flagged;
};

static ChannelTrack s_channels[CSI_CHANNEL_TABLE_SIZE];
static CsiCarrierFinding s_findings[CSI_MAX_FINDINGS];
static size_t s_finding_count = 0;
static CsiCarrierStats s_stats{};

}  // namespace

void csi_carrier_detector_init() {
    memset(s_channels, 0, sizeof(s_channels));
    s_finding_count = 0;
    s_stats = CsiCarrierStats{};
}

void csi_carrier_detector_process_csi(const int8_t* csi_buf, size_t csi_len,
                                      uint8_t channel, int8_t rssi,
                                      uint32_t now_ms) {
    if (!csi_buf || csi_len == 0) return;
    if (channel < 1 || channel >= CSI_CHANNEL_TABLE_SIZE) return;

    s_stats.frames_analyzed++;

    uint16_t amps[CSI_MAX_SUBCARRIERS];
    size_t n = csi_compute_amplitude(csi_buf, csi_len, amps);
    if (n == 0) return;

    bool spiky = csi_has_localized_spike(amps, n, CSI_SPIKE_RATIO_DEFAULT,
                                         CSI_SPIKE_MAX_WIDTH_FRAC_DEFAULT);
    ChannelTrack& t = s_channels[channel];
    t.last_seen_ms = now_ms;
    t.last_rssi = rssi;

    if (spiky) {
        s_stats.spikes_observed++;
        t.consecutive_spiky++;
        if (t.consecutive_spiky >= CSI_SUSTAINED_FRAMES_TO_FLAG && !t.flagged) {
            t.flagged = true;
            if (s_finding_count < CSI_MAX_FINDINGS) {
                CsiCarrierFinding& f = s_findings[s_finding_count++];
                f.channel = channel;
                f.sustained_receptions = t.consecutive_spiky;
                f.last_seen_ms = now_ms;
                f.last_rssi = rssi;
            }
        }
    } else {
        // A clean (non-spiky) reception on this channel breaks the streak --
        // same "must be SUSTAINED, not a one-off" discipline hidden_tx_scanner
        // already uses, so an occasional false spike from real multipath
        // doesn't accumulate toward a finding.
        t.consecutive_spiky = 0;
        t.flagged = false;
    }
}

void csi_carrier_detector_get_stats(CsiCarrierStats* out) {
    if (!out) return;
    s_stats.active_suspects = (uint16_t)s_finding_count;
    *out = s_stats;
}

size_t csi_carrier_detector_list_findings(CsiCarrierFinding* out, size_t max) {
    if (!out) return 0;
    size_t n = s_finding_count < max ? s_finding_count : max;
    memcpy(out, s_findings, n * sizeof(CsiCarrierFinding));
    return n;
}
