// ============================================================================
// rf_cataloger.cpp — see rf_cataloger.h for design notes, real frequency
// sourcing, and the honest validation caveat.
// ============================================================================

#include "rf_cataloger.h"

// ---------------------------------------------------------------------------
// Pure classification logic — no radio access, always compiled (native and
// ARDUINO alike) so it can be unit tested directly.
//
// Thresholds share subghz_scanner.h's control-link band (20-600 Hz, sourced
// from ExpressLRS's real 50/150/250/500 Hz packet rates) so a genuine
// control-link burst rate classifies consistently between the two modules.
// Below that: LOW_RATE covers sparse single/few-packet bursts typical of a
// keyfob/garage-remote/TPMS transmission (these fire for well under a
// second, a handful of times, not a steady stream); PERIODIC covers a
// steady moderate rate more consistent with a sensor/telemetry beacon
// (e.g. a weather station reporting every few seconds produces multiple
// threshold crossings across a longer sampling window at a much lower
// rate than a control link, but still a real periodic presence rather
// than one-off sparse bursts). ERRATIC catches rates too high/inconsistent
// to be a real periodic device at all (more consistent with broadband
// noise or an unrelated wideband signal sweeping through).
// ---------------------------------------------------------------------------
#define RF_CATALOG_LOW_RATE_MAX_HZ        5.0f
#define RF_CATALOG_PERIODIC_MAX_HZ       20.0f
#define RF_CATALOG_CONTROL_LINK_MIN_HZ   20.0f
// FIXED (2026-07-30): was 600 Hz -- see subghz_scanner.cpp's
// SUBGHZ_MAX_PLAUSIBLE_HZ for the full real-data sourcing (ExpressLRS's
// own docs list real 900MHz-band rates up to 1000 Hz FSK). Keep these two
// files' bounds in sync.
#define RF_CATALOG_CONTROL_LINK_MAX_HZ 1200.0f
#define RF_CATALOG_ERRATIC_MIN_HZ      1200.0f

RfActivityClass rf_cataloger_classify(float crossing_rate_hz, bool continuously_above) {
    if (continuously_above) return RF_ACTIVITY_CONTINUOUS;
    if (crossing_rate_hz <= 0.0f) return RF_ACTIVITY_NONE;
    if (crossing_rate_hz < RF_CATALOG_LOW_RATE_MAX_HZ) return RF_ACTIVITY_LOW_RATE;
    if (crossing_rate_hz < RF_CATALOG_PERIODIC_MAX_HZ) return RF_ACTIVITY_PERIODIC;
    if (crossing_rate_hz <= RF_CATALOG_CONTROL_LINK_MAX_HZ) return RF_ACTIVITY_CONTROL_LINK_RATE;
    return RF_ACTIVITY_ERRATIC;
}

#ifdef ARDUINO

#include <Arduino.h>
#include <cstring>
#include "mesh_publish.h"

#define RF_CATALOG_SCAN_INTERVAL_MS   90000u   // offset from subghz_scanner's
                                                // 60s cadence so the two never
                                                // contend for the shared radio
                                                // at exactly the same tick
#define RF_CATALOG_DWELL_MS             300u
#define RF_CATALOG_MAX_ENTRIES            8
#define RF_CATALOG_MESH_FREQ_MHZ      915.0f

// Real, complementary ISM sampling points -- see rf_cataloger.h for sourcing
// of each. Deliberately distinct from subghz_scanner.cpp's own candidate
// list (which targets ExpressLRS/Crossfire's specific FHSS spans) so the
// two modules cover different slices of the sub-GHz ISM environment rather
// than re-scanning the same points twice per two-minute window.
struct CatalogFreq { float mhz; };
static const CatalogFreq kCatalogFreqs[] = {
    {315.0f}, {390.0f},                          // US garage/keyfob/TPMS
    {433.92f},                                    // EU/global ISM center
    {868.3f},                                     // EU868 LoRaWAN fixed uplink
    {902.3f}, {904.6f}, {906.9f},                 // US915 LoRaWAN fixed channels
};
#define N_CATALOG_FREQS (sizeof(kCatalogFreqs) / sizeof(kCatalogFreqs[0]))

static uint32_t s_last_scan_ms = 0;
static RfCatalogerStats s_stats{};
static RfCatalogEntry s_entries[RF_CATALOG_MAX_ENTRIES];
static size_t s_entry_count = 0;

void rf_cataloger_init() {
    s_stats = RfCatalogerStats{};
    s_entry_count = 0;
    s_last_scan_ms = millis();
}

// Same dwell-and-count technique as subghz_scanner.cpp's scan_one_frequency
// (proven correct via the real TSCM_SWEEP bug fix: getRSSI(false) after
// startReceive() for a genuinely live reading, not the last-packet value).
static void scan_one_frequency(SX1262& radio, float freq_mhz,
                                float* out_rate, int16_t* out_peak,
                                bool* out_continuous) {
    radio.setFrequency(freq_mhz);
    radio.startReceive();
    delay(5);

    uint32_t t_start = millis();
    uint32_t baseline_end = t_start + (RF_CATALOG_DWELL_MS / 5);
    float baseline_sum = 0.0f;
    uint32_t baseline_n = 0;
    while (millis() < baseline_end) {
        baseline_sum += radio.getRSSI(false);
        baseline_n++;
    }
    float baseline = (baseline_n > 0) ? (baseline_sum / baseline_n) : -110.0f;
    float threshold = baseline + 6.0f;

    uint32_t dwell_end = t_start + RF_CATALOG_DWELL_MS;
    bool above = false;
    bool ever_below_after_first_above = false;
    bool seen_above = false;
    uint32_t crossings = 0;
    int16_t peak = -128;
    while (millis() < dwell_end) {
        float rssi = radio.getRSSI(false);
        if (rssi > peak) peak = (int16_t)rssi;
        bool now_above = rssi > threshold;
        if (now_above && !above) crossings++;
        if (seen_above && !now_above) ever_below_after_first_above = true;
        if (now_above) seen_above = true;
        above = now_above;
    }

    float dwell_s = (float)RF_CATALOG_DWELL_MS / 1000.0f;
    *out_rate = (float)crossings / dwell_s;
    *out_peak = peak;
    // "Continuously above" = it went above threshold and NEVER dropped back
    // below for the rest of the dwell (a genuine continuous carrier), not
    // merely "spent most of the dwell above" (a busy periodic signal can
    // do that too without being continuous).
    *out_continuous = seen_above && !ever_below_after_first_above;
}

void rf_cataloger_tick() {
    uint32_t now = millis();
    if (now - s_last_scan_ms < RF_CATALOG_SCAN_INTERVAL_MS) return;
    s_last_scan_ms = now;

    SX1262& radio = mesh_get_radio();
    s_entry_count = 0;

    for (size_t i = 0; i < N_CATALOG_FREQS; i++) {
        float rate = 0.0f;
        int16_t peak = -128;
        bool continuous = false;
        scan_one_frequency(radio, kCatalogFreqs[i].mhz, &rate, &peak, &continuous);
        RfActivityClass cls = rf_cataloger_classify(rate, continuous);
        if (s_entry_count < RF_CATALOG_MAX_ENTRIES) {
            s_entries[s_entry_count].freq_mhz = kCatalogFreqs[i].mhz;
            s_entries[s_entry_count].activity_class = cls;
            s_entries[s_entry_count].crossing_rate_hz = rate;
            s_entries[s_entry_count].peak_rssi_dbm = peak;
            s_entry_count++;
        }
    }

    radio.setFrequency(RF_CATALOG_MESH_FREQ_MHZ);
    mesh_start_receive();

    s_stats.scan_cycles_completed++;
    uint16_t active = 0;
    for (size_t i = 0; i < s_entry_count; i++) {
        if (s_entries[i].activity_class != RF_ACTIVITY_NONE) active++;
    }
    s_stats.entries_last_cycle = active;
}

void rf_cataloger_get_stats(RfCatalogerStats* out) {
    if (out) *out = s_stats;
}

size_t rf_cataloger_list(RfCatalogEntry* out, size_t max) {
    if (!out) return 0;
    size_t n = s_entry_count < max ? s_entry_count : max;
    memcpy(out, s_entries, n * sizeof(RfCatalogEntry));
    return n;
}

#else  // !ARDUINO — native/test stub

void rf_cataloger_init() {}
void rf_cataloger_tick() {}
void rf_cataloger_get_stats(RfCatalogerStats* out) {
    if (out) { RfCatalogerStats z{}; *out = z; }
}
size_t rf_cataloger_list(RfCatalogEntry*, size_t) { return 0; }

#endif
