#ifndef CSI_CARRIER_DETECTOR_H
#define CSI_CARRIER_DETECTOR_H

// ============================================================================
// csi_carrier_detector.h — CSI-based continuous-carrier / hidden-camera
// detection (ADDITIVE, off by default until enabled).
//
// This exists to unblock hidden_tx_scanner.cpp's disabled continuous-carrier
// detection. IMPORTANT CORRECTION vs. how this was described earlier tonight:
// CSI does NOT give the ESP32 true idle-channel energy sensing independent
// of frame reception -- esp_wifi_set_csi_rx_cb() still only fires once per
// RECEIVED frame, same fundamental gate as the RSSI path hidden_tx_scanner
// already had. What CSI actually adds is much richer information PER
// RECEIVED FRAME: fine per-subcarrier amplitude/phase instead of one
// scalar RSSI number.
//
// The real mechanism this module uses: a genuine narrowband continuous
// carrier (analog/low-cost video TX) sitting near a WiFi channel bleeds
// into nearby frame receptions as energy CONCENTRATED in a specific,
// narrow subcarrier range -- inconsistent with normal frequency-selective
// multipath fading, which is smoother and spread across the OFDM channel.
// So: whenever a real WiFi frame IS received (same gate as before), this
// looks at THAT frame's CSI for a localized amplitude spike, and tracks
// whether the SAME channel keeps showing this signature across multiple
// received frames over time. This is a materially different, higher-
// resolution signal than hidden_tx_scanner's old RSSI-level+range
// approach, but it inherits the same "needs an actual frame to arrive"
// constraint -- a channel with zero traffic at all still produces zero
// samples either way. (There is no way to do true idle-channel continuous
// energy detection on this chip's WiFi radio in this operating mode; that
// would need dedicated spectrum-analyzer/SDR hardware this sensor doesn't
// have.)
//
// VALIDATION CAVEAT (same honesty standard as subghz_scanner.h): the
// spike-detection classifier is pure, deterministic logic and fully unit
// tested against synthetic CSI amplitude arrays. It has NOT been
// exercised against a real captured continuous-carrier transmitter (e.g.
// an analog FPV video TX or a bench CW signal generator) -- same "no real
// transmitter available" gap subghz_scanner.h documents for ELRS/
// Crossfire. Treat detections as a heuristic lead, not confirmed camera
// identification -- same posture hidden_tx_scanner.h already documents.
//
// Reuses the promiscuous WiFi capture already running (esp_wifi_set_csi_config
// requires promiscuous mode already active per its own documented error
// conditions -- this firmware has had promiscuous mode on since setup(),
// so there is no new radio duty cycle, only additional per-frame data).
// ============================================================================

#include <cstdint>
#include <cstddef>

void csi_carrier_detector_init();

// Feed one frame's raw CSI buffer (wifi_csi_info_t.buf/.len -- alternating
// int8 imaginary/real pairs per subcarrier) plus the channel it was
// received on and now_ms (explicit, not read internally, for determinism
// and testability -- same pattern as wifi_extended_scan). IRAM-safe-ish:
// no allocation, bounded work proportional to len (a few hundred bytes at
// most per real CSI frame).
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);

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

// Computes per-subcarrier amplitude (|imag|+|real|, a cheap proxy for
// magnitude that avoids floating-point sqrt) from raw CSI (imag,real) int8
// pairs. out must hold at least csi_len/2 entries. Returns the number of
// subcarriers written.
size_t csi_compute_amplitude(const int8_t* csi_buf, size_t csi_len, uint16_t* out);

// Does this amplitude array show a spike localized to a narrow subcarrier
// range (consistent with narrowband continuous-carrier bleed), as opposed
// to smooth/broad variation (consistent with normal multipath) or no
// significant variation at all (clean, uncontested reception)?
//   - spike_ratio: how many times the local peak must exceed the array's
//     median to count as a spike (generous default in the .cpp: 2.5x).
//   - max_spike_width_frac: the spike region's width, as a fraction of
//     total subcarriers, must be below this to count as "localized"
//     rather than broadband (default 0.25 -- a spike spanning more than a
//     quarter of the channel isn't narrowband interference anymore).
bool csi_has_localized_spike(const uint16_t* amplitudes, size_t n,
                             float spike_ratio, float max_spike_width_frac);

#define CSI_SPIKE_RATIO_DEFAULT              2.5f
#define CSI_SPIKE_MAX_WIDTH_FRAC_DEFAULT      0.25f
#define CSI_SUSTAINED_FRAMES_TO_FLAG         10u    // consecutive spiky receptions on one channel
#define CSI_MAX_FINDINGS                      8u
#define CSI_CHANNEL_TABLE_SIZE                14u   // channels 1-13, index 0 unused

struct CsiCarrierFinding {
    uint8_t  channel;
    uint32_t sustained_receptions;
    uint32_t last_seen_ms;
    int8_t   last_rssi;
};

struct CsiCarrierStats {
    uint16_t active_suspects;
    uint32_t frames_analyzed;
    uint32_t spikes_observed;
};

void csi_carrier_detector_get_stats(CsiCarrierStats* out);
size_t csi_carrier_detector_list_findings(CsiCarrierFinding* out, size_t max);

#endif // CSI_CARRIER_DETECTOR_H
