// ============================================================================
// hidden_tx_scanner.cpp — see hidden_tx_scanner.h for design notes and
// honest limitations.
//
// BUG FOUND VIA REAL HARDWARE TESTING (2026-07-27): the first version
// flagged all 128 bins as "active suspects" after ~45s in a real, busy
// indoor environment (13-16 nearby BLE/WiFi devices). Root cause: the
// underlying s_channel_rssi feeding capture_rf_spectrum() is itself an
// EWMA-smoothed per-channel value (updated on every received frame,
// smoothed at 0.8/0.2), which erases the burstiness that should
// distinguish a genuine continuous carrier from a busy-but-packetized
// real environment -- a densely-used channel legitimately stays
// persistently elevated after smoothing, indistinguishable from a true
// continuous carrier using occupancy LEVEL alone. Same lesson as the
// drone_classifier literature review: averaging/smoothing destroys the
// temporal structure that actually discriminates.
//
// Fix: track variance (min/max range), not just level, over the sustained
// window. A genuine continuous carrier stays nearly constant; a busy
// multi-device channel still fluctuates as different devices/distances
// come and go, even after smoothing.
// ============================================================================

#include "hidden_tx_scanner.h"

#include <cstring>

#define HTX_MAX_BINS 128
#define HTX_OCCUPANCY_THRESHOLD   0.65f   // normalized [0,1]
#define HTX_SUSTAINED_TICKS_FLAG  45u     // ~45s at the main loop's ~1Hz target
#define HTX_MAX_RANGE_FOR_CARRIER 0.05f   // ~5dB -- tight enough to mean "barely moving"
#define HTX_MAX_FINDINGS 8

static uint16_t s_consecutive_ticks[HTX_MAX_BINS];
static float    s_streak_min[HTX_MAX_BINS];
static float    s_streak_max[HTX_MAX_BINS];
static float    s_last_occupancy[HTX_MAX_BINS];
static HiddenTxStats s_stats{};

static bool is_flagged(size_t i) {
    if (s_consecutive_ticks[i] < HTX_SUSTAINED_TICKS_FLAG) return false;
    return (s_streak_max[i] - s_streak_min[i]) <= HTX_MAX_RANGE_FOR_CARRIER;
}

void hidden_tx_scanner_init() {
    memset(s_consecutive_ticks, 0, sizeof(s_consecutive_ticks));
    memset(s_last_occupancy, 0, sizeof(s_last_occupancy));
    for (size_t i = 0; i < HTX_MAX_BINS; i++) {
        s_streak_min[i] = 1.0f;
        s_streak_max[i] = 0.0f;
    }
    s_stats = HiddenTxStats{};
}

void hidden_tx_scanner_observe(const float* bins, size_t n_bins) {
    if (!bins) return;
    size_t n = n_bins < HTX_MAX_BINS ? n_bins : HTX_MAX_BINS;

    uint16_t active = 0;
    uint32_t longest = 0;
    for (size_t i = 0; i < n; i++) {
        s_last_occupancy[i] = bins[i];
        if (bins[i] > HTX_OCCUPANCY_THRESHOLD) {
            if (s_consecutive_ticks[i] == 0) {
                // Start of a new above-threshold streak -- reset the range.
                s_streak_min[i] = bins[i];
                s_streak_max[i] = bins[i];
            } else {
                if (bins[i] < s_streak_min[i]) s_streak_min[i] = bins[i];
                if (bins[i] > s_streak_max[i]) s_streak_max[i] = bins[i];
            }
            if (s_consecutive_ticks[i] < 0xFFFF) s_consecutive_ticks[i]++;
        } else {
            s_consecutive_ticks[i] = 0;
        }
        if (is_flagged(i)) {
            active++;
            if (s_consecutive_ticks[i] > longest) longest = s_consecutive_ticks[i];
        }
    }
    s_stats.active_suspects = active;
    if (longest > s_stats.longest_sustained_seconds) {
        s_stats.longest_sustained_seconds = longest;
    }
}

void hidden_tx_scanner_get_stats(HiddenTxStats* out) {
    if (out) *out = s_stats;
}

size_t hidden_tx_scanner_list_findings(HiddenTxFinding* out, size_t max) {
    if (!out) return 0;
    size_t n = 0;
    for (size_t i = 0; i < HTX_MAX_BINS && n < max; i++) {
        if (is_flagged(i)) {
            // Matches drone_classifier's own documented bin-to-frequency
            // mapping: 128 bins spanning 2400-2500 MHz.
            out[n].freq_mhz = 2400.0f + (float)i * (100.0f / (float)HTX_MAX_BINS);
            out[n].sustained_seconds = s_consecutive_ticks[i];
            out[n].occupancy = s_last_occupancy[i];
            n++;
        }
    }
    return n;
}
