// ============================================================================
// Radio abstraction layer — implementation
//
// Each band_<name>_* function is a thin adapter. For the primary LoRa 915
// path we reuse the existing mesh_publish SX1262 instance so we don't
// claim the SPI bus twice. For the other bands we stub by default and
// only compile the real driver when the relevant build flag is defined.
// ============================================================================

#include "radio_abstraction.h"
#include <cstring>

#ifndef UNIT_TEST
#include <Arduino.h>
#include "mesh_publish.h"
#endif

// ---------------------------------------------------------------------------
// Duty-cycle bookkeeping (rolling 1-hour window, bucketed per band)
// ---------------------------------------------------------------------------
namespace {
struct DutyState {
    uint32_t window_start_ms;
    uint32_t airtime_used_ms;
};

DutyState g_duty[(size_t)BandId::BAND_COUNT] = {};

// Per-band airtime budgets per hour (ms). 1 hour = 3,600,000 ms.
//   EU 868 1%  ->   36,000 ms
//   EU 869.4 10% -> 360,000 ms (only for the 869.4 sub-band)
//   US 915 "unlimited with LBT" -> we cap at 10% to be friendly
//   WiFi 2G4 / BLE: not per-spectrum-rule here; big budget
constexpr uint32_t BUDGET_MS[(size_t)BandId::BAND_COUNT] = {
    36000,      // LORA_433  (EU 433 is lightly regulated; pick 1%)
    36000,      // LORA_868  (EU 1%)
    360000,     // LORA_915  (US: generous with LBT)
    3600000,    // WIFI_2G4
    1000000,    // NRF24_2G4
    360000,     // ESP32_5G
    36000,      // CC1101_433
    36000,      // CC1101_868
    360000,     // CC1101_915
};

uint32_t now_ms() {
#ifndef UNIT_TEST
    return millis();
#else
    static uint32_t t = 0;
    t += 10;  // advance during native tests
    return t;
#endif
}

} // namespace

// ---------------------------------------------------------------------------
// Per-band adapters. Only LORA_915 has a real backend wired; others are
// default-stubs that return false. Operators enabling a daughterboard
// drop in the appropriate driver behind a PDL_RADIO_<NAME> build flag.
// ---------------------------------------------------------------------------

#ifndef UNIT_TEST

static bool band_lora915_init() { return true; }  // adopted from mesh_publish
static bool band_lora915_tx(const uint8_t* buf, size_t len, uint16_t timeout_ms) {
    (void)timeout_ms;
    return mesh_publish_raw(buf, len);
}
static bool band_lora915_rx(uint8_t*, size_t*, uint16_t) { return false; }
static int  band_lora915_rssi() { return -120; }

#ifdef PDL_RADIO_CC1101_433
// Stubbed CC1101 433 MHz shim — operator patches in driver under this flag.
static bool band_cc1101_433_init() {
    // SPI bus share with LoRa; assumes CS on a spare GPIO (GPIO 48 default).
    // TODO(PDL-CC1101): drive SPI via cc1101 driver lib.
    return true;
}
static bool band_cc1101_433_tx(const uint8_t* buf, size_t len, uint16_t) {
    (void)buf; (void)len;
    return false;  // not yet driven
}
static bool band_cc1101_433_rx(uint8_t*, size_t*, uint16_t) { return false; }
static int  band_cc1101_433_rssi() { return 0; }
#endif

#ifdef PDL_RADIO_NRF24
// Stubbed nRF24L01+ 2.4 GHz shim
static bool band_nrf24_init()  { return true; }
static bool band_nrf24_tx(const uint8_t*, size_t, uint16_t) { return false; }
static bool band_nrf24_rx(uint8_t*, size_t*, uint16_t) { return false; }
static int  band_nrf24_rssi()  { return 0; }
#endif

#ifdef PDL_RADIO_ESP32_C5
// Stubbed ESP32-C5 5.8 GHz shim (companion chip over UART)
static bool band_esp32c5_init() { return true; }
static bool band_esp32c5_tx(const uint8_t*, size_t, uint16_t) { return false; }
static bool band_esp32c5_rx(uint8_t*, size_t*, uint16_t) { return false; }
static int  band_esp32c5_rssi() { return 0; }
#endif

#endif  // !UNIT_TEST

// ---------------------------------------------------------------------------
// Global registry
// ---------------------------------------------------------------------------
RadioInterface g_radios[(size_t)BandId::BAND_COUNT] = {};


static void register_radio(BandId b,
                           bool (*init)(),
                           bool (*tx)(const uint8_t*, size_t, uint16_t),
                           bool (*rx)(uint8_t*, size_t*, uint16_t),
                           int  (*rssi)(),
                           const char* name) {
    g_radios[(size_t)b] = { b, init, tx, rx, rssi, name };
}

void radio_abstraction_init() {
#ifndef UNIT_TEST
    register_radio(BandId::LORA_915,
                   band_lora915_init, band_lora915_tx, band_lora915_rx,
                   band_lora915_rssi, "LORA_915");
  #ifdef PDL_RADIO_CC1101_433
    register_radio(BandId::CC1101_433,
                   band_cc1101_433_init, band_cc1101_433_tx, band_cc1101_433_rx,
                   band_cc1101_433_rssi, "CC1101_433");
  #endif
  #ifdef PDL_RADIO_NRF24
    register_radio(BandId::NRF24_2G4,
                   band_nrf24_init, band_nrf24_tx, band_nrf24_rx,
                   band_nrf24_rssi, "NRF24_2G4");
  #endif
  #ifdef PDL_RADIO_ESP32_C5
    register_radio(BandId::ESP32_5G,
                   band_esp32c5_init, band_esp32c5_tx, band_esp32c5_rx,
                   band_esp32c5_rssi, "ESP32_5G");
  #endif
#endif
    // Reset duty counters
    for (size_t i = 0; i < (size_t)BandId::BAND_COUNT; i++) {
        g_duty[i].window_start_ms = now_ms();
        g_duty[i].airtime_used_ms = 0;
    }
}

size_t radio_available(BandId* out, size_t max) {
    size_t n = 0;
    for (size_t i = 0; i < (size_t)BandId::BAND_COUNT && n < max; i++) {
        if (g_radios[i].tx != nullptr) out[n++] = (BandId)i;
    }
    return n;
}

bool radio_duty_ok(BandId band, size_t airtime_ms) {
    size_t i = (size_t)band;
    if (i >= (size_t)BandId::BAND_COUNT) return false;
    uint32_t now = now_ms();
    if ((uint32_t)(now - g_duty[i].window_start_ms) > 3600000u) {
        g_duty[i].window_start_ms = now;
        g_duty[i].airtime_used_ms = 0;
    }
    return (g_duty[i].airtime_used_ms + airtime_ms) <= BUDGET_MS[i];
}

void radio_duty_record(BandId band, size_t airtime_ms) {
    size_t i = (size_t)band;
    if (i >= (size_t)BandId::BAND_COUNT) return;
    g_duty[i].airtime_used_ms += airtime_ms;
}

size_t radio_duty_budget_remaining_ms(BandId band) {
    size_t i = (size_t)band;
    if (i >= (size_t)BandId::BAND_COUNT) return 0;
    if (g_duty[i].airtime_used_ms >= BUDGET_MS[i]) return 0;
    return BUDGET_MS[i] - g_duty[i].airtime_used_ms;
}

bool radio_tx(BandId band, const uint8_t* buf, size_t len, uint16_t timeout_ms) {
    size_t i = (size_t)band;
    if (i >= (size_t)BandId::BAND_COUNT) return false;
    if (g_radios[i].tx == nullptr) return false;
    // Estimate airtime as len*8 bits at SF10 BW125 ~= ~100 bits/byte, so
    // ~0.5 ms per byte. Close enough for duty accounting; fine-grain is
    // per-band specific and would require a modem-specific estimator.
    size_t airtime = len * 1 + 10;
    if (!radio_duty_ok(band, airtime)) return false;
    bool ok = g_radios[i].tx(buf, len, timeout_ms);
    if (ok) radio_duty_record(band, airtime);
    return ok;
}

void radio_register_mock(BandId band, const RadioInterface& iface) {
    size_t i = (size_t)band;
    if (i < (size_t)BandId::BAND_COUNT) g_radios[i] = iface;
}
