// ============================================================================
// ble_proxy_chars.cpp — Implementation of ProxyTx / ProxyRx GATT chars
//
// See ble_proxy_chars.h for full architecture. This file:
//   * Adds two characteristics (ProxyTx write, ProxyRx notify) to the service
//     created by ble_companion.cpp.
//   * Handles ProxyTx writes by forwarding to c2_proxy_handle_ble_write().
//   * Overrides the weak c2_proxy_ble_notify() symbol to push notifications
//     out via the ProxyRx characteristic.
//
// Coexistence with NimBLE
// -----------------------
// NimBLE write callbacks run on the NimBLE host task. We immediately forward
// to c2_proxy_handle_ble_write() which does only memcpy + queue manipulation;
// no LoRa transmit, no flash I/O, no radio_scheduler call. The actual LoRa
// emission happens in the main loop's c2_proxy_tunnel_tick().
//
// notify() may be called from either core; NimBLE-Arduino is thread-safe for
// notify on connected characteristics. We still queue inside the proxy tunnel
// so the BLE callback path stays fast.
// ============================================================================

#include "ble_proxy_chars.h"
#include "c2_proxy_tunnel.h"

#include <cstring>

#ifndef UNIT_TEST
#  ifdef PDL_BLE_COMPANION_ENABLED
#    include <NimBLEDevice.h>
#  endif
#endif

// ---------------------------------------------------------------------------
// Module-local state
// ---------------------------------------------------------------------------
static BleProxyStats s_stats;
static bool          s_registered = false;

#ifdef UNIT_TEST
// Host-test: in-memory notify queue (replaces NimBLE characteristic).
struct TestNotifyEntry {
    uint8_t  data[BLE_PROXY_MAX_WRITE_BYTES];
    size_t   len;
    bool     used;
};
static TestNotifyEntry s_test_notify_q[8];
#endif

#ifndef UNIT_TEST
#  ifdef PDL_BLE_COMPANION_ENABLED
// Pointer to the ProxyRx characteristic for notify-on-demand. Held weakly:
// if the service is torn down, our pointer becomes stale; NimBLE handles
// disconnect cleanly so notify() on a stale ptr returns false rather than
// crashing — but we additionally null it in the disconnect callback if/when
// integrators wire one. For v1 we simply test for null on every notify.
static NimBLECharacteristic* s_proxy_rx_char = nullptr;

class ProxyTxWriteCB : public NimBLECharacteristicCallbacks {
public:
    void onWrite(NimBLECharacteristic* chr) override {
        const std::string val = chr->getValue();
        if (val.size() < 24 || val.size() > BLE_PROXY_MAX_WRITE_BYTES) {
            s_stats.proxy_tx_writes_bad++;
            return;
        }
        s_stats.proxy_tx_writes++;
        C2ProxyStatus rc = c2_proxy_handle_ble_write(
            reinterpret_cast<const uint8_t*>(val.data()), val.size());
        if (rc != C2P_OK) {
            s_stats.proxy_tx_writes_bad++;
        }
    }
};

static ProxyTxWriteCB s_proxy_tx_cb;
#  endif // PDL_BLE_COMPANION_ENABLED
#endif // !UNIT_TEST

// ---------------------------------------------------------------------------
// c2_proxy_ble_notify override — strong definition replaces the weak one in
// c2_proxy_tunnel.cpp.
// ---------------------------------------------------------------------------
extern "C" bool c2_proxy_ble_notify(const uint8_t* buf, size_t len) {
    if (!buf || len == 0 || len > BLE_PROXY_MAX_WRITE_BYTES) return false;

#ifdef UNIT_TEST
    for (auto& e : s_test_notify_q) {
        if (!e.used) {
            memcpy(e.data, buf, len);
            e.len  = len;
            e.used = true;
            s_stats.proxy_rx_notifies++;
            return true;
        }
    }
    s_stats.proxy_rx_notify_drops++;
    return false;
#else
#  ifdef PDL_BLE_COMPANION_ENABLED
    if (!s_proxy_rx_char) {
        s_stats.proxy_rx_notify_drops++;
        return false;
    }
    s_proxy_rx_char->setValue(buf, len);
    s_proxy_rx_char->notify();   // NimBLE-Arduino 1.4.x: notify() is void
    s_stats.proxy_rx_notifies++;
    return true;
#  else
    (void)buf; (void)len;
    return false;
#  endif
#endif
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
bool ble_proxy_register(void* svc) {
    if (s_registered) return true;
    if (!svc) return false;

#ifdef UNIT_TEST
    s_registered = true;
    s_stats.registered = true;
    return true;
#else
#  ifdef PDL_BLE_COMPANION_ENABLED
    auto* nim_svc = reinterpret_cast<NimBLEService*>(svc);

    // ProxyTx — write only. Phone writes chunked envelopes here.
    auto* tx_char = nim_svc->createCharacteristic(
        BLE_PDL_CHAR_PROXY_TX_UUID,
        NIMBLE_PROPERTY::WRITE
    );
    tx_char->setCallbacks(&s_proxy_tx_cb);

    // ProxyRx — notify only. Sensor pushes chunked responses here.
    s_proxy_rx_char = nim_svc->createCharacteristic(
        BLE_PDL_CHAR_PROXY_RX_UUID,
        NIMBLE_PROPERTY::NOTIFY
    );

    s_registered       = true;
    s_stats.registered = true;
    return true;
#  else
    (void)svc;
    return false;
#  endif
#endif
}

void ble_proxy_get_stats(BleProxyStats* out) {
    if (!out) return;
    *out = s_stats;
    out->registered = s_registered;
}

#ifdef UNIT_TEST
void ble_proxy_test_reset() {
    memset(&s_stats, 0, sizeof(s_stats));
    memset(s_test_notify_q, 0, sizeof(s_test_notify_q));
    s_registered = false;
}

bool ble_proxy_test_inject_write(const uint8_t* buf, size_t len) {
    if (!buf || len < 24 || len > BLE_PROXY_MAX_WRITE_BYTES) {
        s_stats.proxy_tx_writes_bad++;
        return false;
    }
    s_stats.proxy_tx_writes++;
    C2ProxyStatus rc = c2_proxy_handle_ble_write(buf, len);
    if (rc != C2P_OK) {
        s_stats.proxy_tx_writes_bad++;
        return false;
    }
    return true;
}

size_t ble_proxy_test_pop_notify(uint8_t* out_buf, size_t cap) {
    if (!out_buf || cap == 0) return 0;
    for (auto& e : s_test_notify_q) {
        if (e.used) {
            size_t n = e.len < cap ? e.len : cap;
            memcpy(out_buf, e.data, n);
            e.used = false;
            e.len  = 0;
            return n;
        }
    }
    return 0;
}
#endif
