#include "TBeamSupremeBleInterface.h"
#include "DebugStats.h"

#include <BLE2902.h>
#include <BLEService.h>
#include <Cryptography/Random.h>
#include <Identity.h>
#include <Log.h>

#include <algorithm>
#include <string.h>

using namespace RNS;

static TBeamSupremeBleInterface* active_ble_interface = nullptr;

static void put_u16_be(uint8_t* data, uint16_t value) {
  data[0] = (uint8_t)((value >> 8) & 0xFF);
  data[1] = (uint8_t)(value & 0xFF);
}

static void put_u32_be(uint8_t* data, uint32_t value) {
  data[0] = (uint8_t)((value >> 24) & 0xFF);
  data[1] = (uint8_t)((value >> 16) & 0xFF);
  data[2] = (uint8_t)((value >> 8) & 0xFF);
  data[3] = (uint8_t)(value & 0xFF);
}

static uint16_t get_u16_be(const uint8_t* data) {
  return ((uint16_t)data[0] << 8) | data[1];
}

static uint32_t get_u32_be(const uint8_t* data) {
  return ((uint32_t)data[0] << 24) |
         ((uint32_t)data[1] << 16) |
         ((uint32_t)data[2] << 8) |
         (uint32_t)data[3];
}

class TBeamSupremeBleInterface::ServerCallbacks : public BLEServerCallbacks {
public:
  explicit ServerCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}

  void onConnect(BLEServer* server) override {
    (void)server;
    if (_owner->_client && _owner->_client->isConnected()) {
      Serial.println("BLE dual-role: inbound connection arrived while outbound is active; keeping outbound link");
      server->disconnect(server->getConnId());
      return;
    }
    _owner->_connected = true;
    _owner->_server_handshake_received = false;
    DebugStats::set_role("server");
    DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=connect conn=%u direction=inbound\r\n",
                     (unsigned long)millis(),
                     DebugStats::board(),
                     DebugStats::role(),
                     (unsigned)server->getConnId());
    Serial.println("BLE dual-role: central connected to local server");
    INFO("BLE central connected to local server");
  }

  void onDisconnect(BLEServer* server) override {
    (void)server;
    if (_owner->_client && _owner->_client->isConnected()) {
      _owner->_connected = true;
      DebugStats::set_role("client");
      DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=disconnect conn=%u direction=inbound action=ignored_outbound_active\r\n",
                       (unsigned long)millis(),
                       DebugStats::board(),
                       DebugStats::role(),
                       (unsigned)server->getConnId());
      return;
    }
    _owner->_connected = false;
    _owner->_server_handshake_received = false;
    _owner->reset_reassembly();
    DebugStats::set_role("idle");
    DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=disconnect conn=%u direction=inbound\r\n",
                     (unsigned long)millis(),
                     DebugStats::board(),
                     DebugStats::role(),
                     (unsigned)server->getConnId());
    Serial.println("BLE dual-role: central disconnected; restarting advertising");
    INFO("BLE central disconnected; restarting advertising");
    BLEDevice::startAdvertising();
  }

private:
  TBeamSupremeBleInterface* _owner;
};

class TBeamSupremeBleInterface::RxCallbacks : public BLECharacteristicCallbacks {
public:
  explicit RxCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}

  void onWrite(BLECharacteristic* characteristic) override {
    std::string value = characteristic->getValue();
    if (value.empty()) {
      return;
    }

    if (!_owner->_server_handshake_received && value.size() == 16) {
      _owner->_server_handshake_received = true;
      DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=identity_handshake_rx len=%u crc32=%08lX\r\n",
                       (unsigned long)millis(),
                       DebugStats::board(),
                       DebugStats::role(),
                       (unsigned)value.size(),
                       (unsigned long)DebugStats::crc32(reinterpret_cast<const uint8_t*>(value.data()), value.size()));
      Serial.println("BLE dual-role: identity handshake received by local server");
      INFOF("BLE identity handshake received: %d bytes", (int)value.size());
      return;
    }

    // BLECharacteristic owns/reuses its callback storage; handle_fragment copies
    // payload bytes into RNS::Bytes before data can outlive this callback.
    _owner->handle_fragment(reinterpret_cast<const uint8_t*>(value.data()), value.size());
  }

private:
  TBeamSupremeBleInterface* _owner;
};

class TBeamSupremeBleInterface::AdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
public:
  explicit AdvertisedDeviceCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}

  void onResult(BLEAdvertisedDevice advertised_device) override {
    bool has_service = advertised_device.haveServiceUUID() &&
        advertised_device.isAdvertisingService(BLEUUID(TBeamSupremeBleInterface::SERVICE_UUID));
    bool has_rns_name = advertised_device.haveName() &&
        advertised_device.getName().rfind("RNS-", 0) == 0;

    if ((has_service || has_rns_name) || _owner->_scan_report_count < 5) {
      Serial.printf("BLE scan: addr=%s name=%s service=%s rssi=%d\r\n",
                    advertised_device.getAddress().toString().c_str(),
                    advertised_device.haveName() ? advertised_device.getName().c_str() : "",
                    has_service ? "yes" : "no",
                    advertised_device.getRSSI());
      _owner->_scan_report_count++;
    }

    if (!has_service && !has_rns_name) {
      return;
    }

    String peer_label = _owner->advertised_node_label(advertised_device);
    String peer_address = String(advertised_device.getAddress().toString().c_str());
    if (!_owner->should_connect_to_peer(peer_address)) {
      Serial.printf("BLE dual-role: peer addr=%s label=%s has lower tie-breaker; staying available\r\n",
                    peer_address.c_str(),
                    peer_label.length() ? peer_label.c_str() : "(unknown)");
      return;
    }

    BLEDevice::getScan()->stop();
    delete _owner->_advertised_device;
    _owner->_advertised_device = new BLEAdvertisedDevice(advertised_device);
    _owner->_do_connect = true;
    _owner->_scanning = false;
    Serial.printf("BLE dual-role: peer candidate found: %s label=%s\r\n",
                  advertised_device.getAddress().toString().c_str(),
                  peer_label.c_str());
    INFOF("BLE peer found: %s", advertised_device.getAddress().toString().c_str());
  }

private:
  TBeamSupremeBleInterface* _owner;
};

TBeamSupremeBleInterface::TBeamSupremeBleInterface(const String& node_label, const char* name) : InterfaceImpl(name) {
  _node_label = node_label;
  _IN = true;
  _OUT = true;
  _bitrate = 1000000;
  _HW_MTU = BLE_PAYLOAD_SIZE;
}

TBeamSupremeBleInterface::~TBeamSupremeBleInterface() {
  stop();
  delete _advertised_device;
}

bool TBeamSupremeBleInterface::start() {
  if (_started) {
    return true;
  }

  active_ble_interface = this;
  BLEDevice::init(std::string("RNS-") + _node_label.c_str());
  BLEDevice::setMTU(BLE_ATT_MTU);
  _local_address = String(BLEDevice::getAddress().toString().c_str());

  Serial.printf("BLE dual-role: local addr=%s label=%s\r\n", _local_address.c_str(), _node_label.c_str());
  Serial.println("BLE dual-role: starting advertiser and scanner");
  INFO("BLE starting as dual-role client/server");
  start_peripheral();
  start_central_scan();

  _started = true;
  _online = true;
  return true;
}

void TBeamSupremeBleInterface::stop() {
  _online = false;
  _connected = false;
  if (_client && _client->isConnected()) {
    _client->disconnect();
  }
  DebugStats::set_role("idle");
}

void TBeamSupremeBleInterface::loop() {
  if (!_online) {
    return;
  }

  if (_do_connect && _advertised_device) {
    _do_connect = false;
    if (!connect_to_advertised_device(_advertised_device)) {
      _connected = false;
      _next_scan_ms = millis() + SCAN_RETRY_MS;
    }
  }

  if (!_connected && !_scanning && (int32_t)(millis() - _next_scan_ms) >= 0) {
    start_central_scan();
  }

  if (_reassembly_started_ms != 0 && millis() - _reassembly_started_ms > REASSEMBLY_TIMEOUT_MS) {
    DebugStats::note_failure();
    DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_reassembly_timeout detail=msg_id_%lu received=%u expected=%u bytes=%u expected_bytes=%lu\r\n",
                     (unsigned long)millis(),
                     DebugStats::board(),
                     DebugStats::role(),
                     (unsigned long)_current_rx_message_id,
                     (unsigned)_received_fragments,
                     (unsigned)_expected_total,
                     (unsigned)_reassembly_buffer.size(),
                     (unsigned long)_expected_message_len);
    DebugStats::print_mem("rx_reassembly_timeout");
    WARNING("BLE reassembly timeout; dropping partial packet");
    reset_reassembly();
  }

  RNS::Bytes packet({RNS::Type::NONE});
  while (dequeue_packet(packet)) {
    _packet_rx_seq++;
    if (DebugStats::should_log_packet(_packet_rx_seq)) {
      DebugStats::logf("RNSRX ms=%lu board=%s role=%s seq=%lu link_id=unknown len=%u crc32=%08lX first4=%s last4=%s\r\n",
                       (unsigned long)millis(),
                       DebugStats::board(),
                       DebugStats::role(),
                       (unsigned long)_packet_rx_seq,
                       (unsigned)packet.size(),
                       (unsigned long)DebugStats::bytes_crc32(packet),
                       DebugStats::bytes_first4(packet).c_str(),
                       DebugStats::bytes_last4(packet).c_str());
      DebugStats::logf("RNSDEC ms=%lu board=%s role=%s event=attempt link_id=unknown token_len=%u token_crc32=%08lX link_obj=unknown\r\n",
                       (unsigned long)millis(),
                       DebugStats::board(),
                       DebugStats::role(),
                       (unsigned)packet.size(),
                       (unsigned long)DebugStats::bytes_crc32(packet));
    }
    InterfaceImpl::handle_incoming(packet);
  }
  print_queue_stats(false);
  DebugStats::maybe_print_periodic();
}

void TBeamSupremeBleInterface::start_peripheral() {
  _server = BLEDevice::createServer();
  _server->setCallbacks(new ServerCallbacks(this));

  BLEService* service = _server->createService(SERVICE_UUID);
  _tx_characteristic = service->createCharacteristic(
      TX_UUID,
      BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
  _tx_characteristic->addDescriptor(new BLE2902());

  BLECharacteristic* rx_characteristic = service->createCharacteristic(
      RX_UUID,
      BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
  rx_characteristic->setCallbacks(new RxCallbacks(this));

  _identity_characteristic = service->createCharacteristic(
      IDENTITY_UUID,
      BLECharacteristic::PROPERTY_READ);
  RNS::Bytes identity = local_identity_hash();
  _identity_characteristic->setValue((uint8_t*)identity.data(), identity.size());

  service->start();

  BLEAdvertising* advertising = BLEDevice::getAdvertising();
  advertising->addServiceUUID(SERVICE_UUID);
  advertising->setScanResponse(true);
  advertising->setMinPreferred(0x00);
  BLEDevice::startAdvertising();
  Serial.println("BLE dual-role: advertising Reticulum service");
  INFO("BLE advertising Reticulum service");
}

void TBeamSupremeBleInterface::start_central_scan() {
  _scanning = true;
  _next_scan_ms = millis() + SCAN_RETRY_MS;
  _scan_report_count = 0;

  BLEScan* scan = BLEDevice::getScan();
  scan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks(this), true);
  scan->setInterval(1349);
  scan->setWindow(449);
  scan->setActiveScan(true);
  Serial.println("BLE dual-role: scanning for Reticulum service");
  scan->start(5, false);
  if (!_do_connect && !_connected) {
    _scanning = false;
    _next_scan_ms = millis() + SCAN_RETRY_MS;
    Serial.println("BLE dual-role: scan complete; no eligible peer found");
  }
  scan->clearResults();
  INFO("BLE scanning for Reticulum service");
}

bool TBeamSupremeBleInterface::connect_to_advertised_device(BLEAdvertisedDevice* device) {
  Serial.printf("BLE dual-role: connecting to %s\r\n", device->getAddress().toString().c_str());
  INFOF("BLE connecting to %s", device->getAddress().toString().c_str());

  _client = BLEDevice::createClient();
  if (!_client->connect(device)) {
    DebugStats::note_failure();
    DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=ble_connect_failed detail=connect_failed\r\n",
                     (unsigned long)millis(), DebugStats::board(), DebugStats::role());
    Serial.println("BLE dual-role: connect failed");
    WARNING("BLE connect failed");
    return false;
  }

  _client->setMTU(BLE_ATT_MTU);
  BLERemoteService* service = _client->getService(BLEUUID(SERVICE_UUID));
  if (!service) {
    DebugStats::note_failure();
    DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=ble_service_missing detail=reticulum_service_not_found\r\n",
                     (unsigned long)millis(), DebugStats::board(), DebugStats::role());
    Serial.println("BLE dual-role: Reticulum service not found after connect");
    WARNING("BLE Reticulum service not found after connect");
    _client->disconnect();
    return false;
  }

  _remote_rx_characteristic = service->getCharacteristic(BLEUUID(RX_UUID));
  _remote_tx_characteristic = service->getCharacteristic(BLEUUID(TX_UUID));
  BLERemoteCharacteristic* identity_characteristic = service->getCharacteristic(BLEUUID(IDENTITY_UUID));

  if (!_remote_rx_characteristic || !_remote_tx_characteristic) {
    DebugStats::note_failure();
    DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=ble_characteristic_missing detail=rx_or_tx_missing\r\n",
                     (unsigned long)millis(), DebugStats::board(), DebugStats::role());
    Serial.println("BLE dual-role: RX/TX characteristics not found");
    WARNING("BLE Reticulum RX/TX characteristics not found");
    _client->disconnect();
    return false;
  }

  if (identity_characteristic && identity_characteristic->canRead()) {
    std::string peer_identity = identity_characteristic->readValue();
    DebugStats::logf("RNSBLE ms=%lu board=%s role=client event=identity_read len=%u crc32=%08lX\r\n",
                     (unsigned long)millis(),
                     DebugStats::board(),
                     (unsigned)peer_identity.size(),
                     (unsigned long)DebugStats::crc32(reinterpret_cast<const uint8_t*>(peer_identity.data()), peer_identity.size()));
    Serial.printf("BLE dual-role: peer identity read: %d bytes\r\n", (int)peer_identity.size());
    INFOF("BLE peer identity read: %d bytes", (int)peer_identity.size());
  }

  if (_remote_tx_characteristic->canNotify()) {
    _remote_tx_characteristic->registerForNotify(client_notify_callback);
  } else {
    Serial.println("BLE dual-role: peer TX characteristic does not notify");
  }

  RNS::Bytes identity = local_identity_hash();
  _remote_rx_characteristic->writeValue((uint8_t*)identity.data(), identity.size(), true);
  _connected = true;
  _scanning = false;
  DebugStats::set_role("client");
  DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=connect conn=client direction=outbound peer=%s identity_crc32=%08lX\r\n",
                   (unsigned long)millis(),
                   DebugStats::board(),
                   DebugStats::role(),
                   device->getAddress().toString().c_str(),
                   (unsigned long)DebugStats::bytes_crc32(identity));
  Serial.println("BLE dual-role: connected and identity handshake sent");
  INFO("BLE connected and identity handshake sent");
  return true;
}

bool TBeamSupremeBleInterface::should_connect_to_peer(const String& peer_address) const {
  if (peer_address.length() == 0 || peer_address == _local_address) {
    return false;
  }
  return strcmp(_local_address.c_str(), peer_address.c_str()) < 0;
}

String TBeamSupremeBleInterface::advertised_node_label(BLEAdvertisedDevice& device) const {
  if (!device.haveName()) {
    return "";
  }
  std::string name = device.getName();
  if (name.rfind("RNS-", 0) != 0) {
    return "";
  }
  return String(name.substr(4).c_str());
}

void TBeamSupremeBleInterface::send_outgoing(const Bytes& data) {
  if (!_online || !_connected) {
    return;
  }

  size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
  if (total == 0 || total > 65535) {
    DebugStats::note_failure();
    DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=tx_fragment_count_invalid detail=packet_len_%u fragments_%u\r\n",
                     (unsigned long)millis(),
                     DebugStats::board(),
                     DebugStats::role(),
                     (unsigned)data.size(),
                     (unsigned)total);
    DebugStats::print_mem("tx_fragment_count_invalid");
    WARNINGF("BLE cannot fragment packet of size %d", (int)data.size());
    return;
  }

  _packet_tx_seq++;
  uint32_t msg_id = ++_tx_message_id;
  if (DebugStats::should_log_packet(_packet_tx_seq)) {
    DebugStats::logf("RNSTX ms=%lu board=%s role=%s seq=%lu link_id=unknown msg_id=%lu len=%u crc32=%08lX first4=%s last4=%s\r\n",
                     (unsigned long)millis(),
                     DebugStats::board(),
                     DebugStats::role(),
                     (unsigned long)_packet_tx_seq,
                     (unsigned long)msg_id,
                     (unsigned)data.size(),
                     (unsigned long)DebugStats::bytes_crc32(data),
                     DebugStats::bytes_first4(data).c_str(),
                     DebugStats::bytes_last4(data).c_str());
  }

  for (size_t i = 0; i < total; ++i) {
    uint8_t fragment[BLE_VALUE_SIZE];
    uint8_t fragment_type = FRAG_CONTINUE;
    if (i == 0) {
      fragment_type = FRAG_START;
    } else if (i == total - 1) {
      fragment_type = FRAG_END;
    }

    size_t offset = i * BLE_PAYLOAD_SIZE;
    size_t chunk = std::min(BLE_PAYLOAD_SIZE, data.size() - offset);
    fragment[0] = fragment_type;
    fragment[1] = FRAG_HEADER_VERSION;
    put_u16_be(fragment + 2, (uint16_t)i);
    put_u16_be(fragment + 4, (uint16_t)total);
    put_u32_be(fragment + 6, msg_id);
    put_u32_be(fragment + 10, (uint32_t)data.size());
    memcpy(fragment + FRAG_HEADER_SIZE, data.data() + offset, chunk);
    _ble_frag_tx_seq++;
    if (DebugStats::should_log_packet(_packet_tx_seq)) {
      DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=fragment_tx conn=%s frag_seq=%lu frag_index=%u frag_total=%u msg_id=%lu msg_off=%u msg_total=%u frag_len=%u payload_len=%u frag_crc32=%08lX\r\n",
                       (unsigned long)millis(),
                       DebugStats::board(),
                       DebugStats::role(),
                       _client && _client->isConnected() ? "client" : "server",
                       (unsigned long)_ble_frag_tx_seq,
                       (unsigned)i,
                       (unsigned)total,
                       (unsigned long)msg_id,
                       (unsigned)offset,
                       (unsigned)data.size(),
                       (unsigned)(FRAG_HEADER_SIZE + chunk),
                       (unsigned)chunk,
                       (unsigned long)DebugStats::crc32(fragment, FRAG_HEADER_SIZE + chunk));
    }
    send_fragment(fragment, FRAG_HEADER_SIZE + chunk);
    delay(8);
  }

  InterfaceImpl::handle_outgoing(data);
}

void TBeamSupremeBleInterface::send_fragment(const uint8_t* data, size_t len) {
  if (_client && _client->isConnected() && _remote_rx_characteristic) {
    _remote_rx_characteristic->writeValue((uint8_t*)data, len, true);
    return;
  }

  if (_tx_characteristic) {
    _tx_characteristic->setValue((uint8_t*)data, len);
    _tx_characteristic->notify();
    delay(20);
  }
}

void TBeamSupremeBleInterface::handle_fragment(const uint8_t* data, size_t len) {
  if (len < FRAG_HEADER_SIZE) {
    DebugStats::note_failure();
    DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_fragment_too_short detail=len_%u\r\n",
                     (unsigned long)millis(), DebugStats::board(), DebugStats::role(), (unsigned)len);
    DebugStats::print_mem("rx_fragment_too_short");
    WARNINGF("BLE fragment too short: %d", (int)len);
    return;
  }

  uint8_t fragment_type = data[0];
  uint8_t version = data[1];
  uint16_t sequence = get_u16_be(data + 2);
  uint16_t total = get_u16_be(data + 4);
  uint32_t msg_id = get_u32_be(data + 6);
  uint32_t msg_len = get_u32_be(data + 10);
  const uint8_t* payload = data + FRAG_HEADER_SIZE;
  size_t payload_len = len - FRAG_HEADER_SIZE;
  _ble_frag_rx_seq++;
  uint32_t expected_fragments = (msg_len + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
  size_t expected_offset = (size_t)sequence * BLE_PAYLOAD_SIZE;

  if ((fragment_type != FRAG_START && fragment_type != FRAG_CONTINUE && fragment_type != FRAG_END) ||
      version != FRAG_HEADER_VERSION ||
      total == 0 || sequence >= total ||
      msg_id == 0 || msg_len == 0 ||
      expected_fragments == 0 || expected_fragments != total ||
      expected_offset >= msg_len || payload_len > (msg_len - expected_offset)) {
    DebugStats::note_failure();
    DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_fragment_header_bad detail=type_%u_ver_%u_seq_%u_total_%u_msg_id_%lu_msg_len_%lu_len_%u_payload_%u_expected_frags_%lu\r\n",
                     (unsigned long)millis(),
                     DebugStats::board(),
                     DebugStats::role(),
                     (unsigned)fragment_type,
                     (unsigned)version,
                     (unsigned)sequence,
                     (unsigned)total,
                     (unsigned long)msg_id,
                     (unsigned long)msg_len,
                     (unsigned)len,
                     (unsigned)payload_len,
                     (unsigned long)expected_fragments);
    DebugStats::print_mem("rx_fragment_header_bad");
    WARNING("BLE invalid fragment header");
    reset_reassembly();
    return;
  }

  if (sequence == 0) {
    if (_reassembly_started_ms != 0 && _received_fragments != _expected_total) {
      _reassembly_gaps++;
      DebugStats::note_failure();
      DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_reassembly_restart detail=old_msg_id_%lu_new_msg_id_%lu_received_%u_expected_%u bytes_%u gaps=%lu\r\n",
                       (unsigned long)millis(),
                       DebugStats::board(),
                       DebugStats::role(),
                       (unsigned long)_current_rx_message_id,
                       (unsigned long)msg_id,
                       (unsigned)_received_fragments,
                       (unsigned)_expected_total,
                       (unsigned)_reassembly_buffer.size(),
                       (unsigned long)_reassembly_gaps);
      DebugStats::print_mem("rx_reassembly_restart");
    }
    reset_reassembly();
    _expected_total = total;
    _received_fragments = 0;
    _expected_message_len = msg_len;
    _reassembly_started_ms = millis();
    _current_rx_message_id = msg_id;
    if (msg_id > _rx_message_id) {
      _rx_message_id = msg_id;
    }
  } else if (_reassembly_started_ms == 0 || msg_id != _current_rx_message_id) {
    _reassembly_out_of_order++;
    DebugStats::note_failure();
    DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_reassembly_stale_fragment detail=msg_id_%lu_current_%lu_seq_%u_received_%u total_%u expected_total_%u stale=%lu\r\n",
                     (unsigned long)millis(),
                     DebugStats::board(),
                     DebugStats::role(),
                     (unsigned long)msg_id,
                     (unsigned long)_current_rx_message_id,
                     (unsigned)sequence,
                     (unsigned)_received_fragments,
                     (unsigned)total,
                     (unsigned)_expected_total,
                     (unsigned long)_reassembly_out_of_order);
    DebugStats::print_mem("rx_reassembly_stale_fragment");
    return;
  }

  DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=fragment_rx conn=%s frag_seq=%lu frag_index=%u frag_total=%u msg_id=%lu msg_off=%u msg_total=%lu frag_len=%u payload_len=%u frag_crc32=%08lX\r\n",
                   (unsigned long)millis(),
                   DebugStats::board(),
                   DebugStats::role(),
                   _client && _client->isConnected() ? "client" : "server",
                   (unsigned long)_ble_frag_rx_seq,
                   (unsigned)sequence,
                   (unsigned)total,
                   (unsigned long)_current_rx_message_id,
                   (unsigned)expected_offset,
                   (unsigned long)msg_len,
                   (unsigned)len,
                   (unsigned)payload_len,
                   (unsigned long)DebugStats::crc32(data, len));

  if (_expected_total != total || _expected_message_len != msg_len || sequence != _received_fragments) {
    if (sequence < _received_fragments) {
      _reassembly_duplicates++;
    } else if (sequence > _received_fragments) {
      _reassembly_gaps++;
    } else {
      _reassembly_out_of_order++;
    }
    DebugStats::note_failure();
    DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_reassembly_gap detail=msg_id_%lu_seq_%u_expected_seq_%u_total_%u_expected_total_%u_msg_len_%lu_expected_len_%lu gaps=%lu duplicates=%lu out_of_order=%lu\r\n",
                     (unsigned long)millis(),
                     DebugStats::board(),
                     DebugStats::role(),
                     (unsigned long)_current_rx_message_id,
                     (unsigned)sequence,
                     (unsigned)_received_fragments,
                     (unsigned)total,
                     (unsigned)_expected_total,
                     (unsigned long)msg_len,
                     (unsigned long)_expected_message_len,
                     (unsigned long)_reassembly_gaps,
                     (unsigned long)_reassembly_duplicates,
                     (unsigned long)_reassembly_out_of_order);
    DebugStats::print_mem("rx_reassembly_gap");
    WARNING("BLE out-of-order fragment; dropping partial packet");
    reset_reassembly();
    return;
  }

  // Own the bytes from this point forward. The next Reticulum layer receives
  // a deep-copied RNS::Bytes packet, never a pointer into a BLE callback buffer.
  _reassembly_buffer.append(payload, payload_len);
  _received_fragments++;

  if (_received_fragments == _expected_total) {
    if (_reassembly_buffer.size() != _expected_message_len) {
      DebugStats::note_failure();
      DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_reassembly_len_mismatch detail=msg_id_%lu assembled_%u expected_%lu fragments_%u\r\n",
                       (unsigned long)millis(),
                       DebugStats::board(),
                       DebugStats::role(),
                       (unsigned long)_current_rx_message_id,
                       (unsigned)_reassembly_buffer.size(),
                       (unsigned long)_expected_message_len,
                       (unsigned)_received_fragments);
      DebugStats::print_mem("rx_reassembly_len_mismatch");
      reset_reassembly();
      return;
    }
    DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=packet_assembled msg_id=%lu assembled_len=%u assembled_crc32=%08lX fragments=%u\r\n",
                     (unsigned long)millis(),
                     DebugStats::board(),
                     DebugStats::role(),
                     (unsigned long)_current_rx_message_id,
                     (unsigned)_reassembly_buffer.size(),
                     (unsigned long)DebugStats::bytes_crc32(_reassembly_buffer),
                     (unsigned)_received_fragments);
    enqueue_packet(_reassembly_buffer);
    reset_reassembly();
  }
}

void TBeamSupremeBleInterface::enqueue_packet(const RNS::Bytes& packet) {
  portENTER_CRITICAL(&_queue_mux);
  if (_incoming_packets.size() >= MAX_INCOMING_QUEUE_DEPTH) {
    _queue_drops++;
    size_t depth = _incoming_packets.size();
    portEXIT_CRITICAL(&_queue_mux);
    DebugStats::note_failure();
    DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=queue_overflow detail=ble_rx_depth_%u_drops_%lu\r\n",
                     (unsigned long)millis(),
                     DebugStats::board(),
                     DebugStats::role(),
                     (unsigned)depth,
                     (unsigned long)_queue_drops);
    DebugStats::print_mem("queue_overflow");
    print_queue_stats(true);
    return;
  }
  try {
    _incoming_packets.push_back(packet);
    _queue_pushes++;
    if (_incoming_packets.size() > _queue_highwater) {
      _queue_highwater = _incoming_packets.size();
    }
  } catch (...) {
    _queue_alloc_failures++;
    portEXIT_CRITICAL(&_queue_mux);
    DebugStats::note_failure();
    DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=queue_alloc_failure detail=ble_rx_push_failed failures=%lu\r\n",
                     (unsigned long)millis(),
                     DebugStats::board(),
                     DebugStats::role(),
                     (unsigned long)_queue_alloc_failures);
    DebugStats::print_mem("queue_alloc_failure");
    print_queue_stats(true);
    return;
  }
  portEXIT_CRITICAL(&_queue_mux);
}

bool TBeamSupremeBleInterface::dequeue_packet(RNS::Bytes& packet) {
  portENTER_CRITICAL(&_queue_mux);
  if (_incoming_packets.empty()) {
    portEXIT_CRITICAL(&_queue_mux);
    return false;
  }
  packet = _incoming_packets.front();
  _incoming_packets.pop_front();
  _queue_pops++;
  portEXIT_CRITICAL(&_queue_mux);
  return true;
}

void TBeamSupremeBleInterface::print_queue_stats(bool force) {
  uint32_t now = millis();
  if (!force && _next_queue_log_ms != 0 && (int32_t)(now - _next_queue_log_ms) < 0) {
    return;
  }
  _next_queue_log_ms = now + 1000;
  portENTER_CRITICAL(&_queue_mux);
  size_t depth = _incoming_packets.size();
  size_t highwater = _queue_highwater;
  uint32_t pushes = _queue_pushes;
  uint32_t pops = _queue_pops;
  uint32_t drops = _queue_drops;
  uint32_t overwrites = _queue_overwrites;
  uint32_t alloc_failures = _queue_alloc_failures;
  portEXIT_CRITICAL(&_queue_mux);
  DebugStats::logf("RNSQUEUE ms=%lu board=%s role=%s name=ble_rx depth=%u max_depth=%u pushes=%lu pops=%lu drops=%lu overwrites=%lu alloc_failures=%lu highwater=%u\r\n",
                   (unsigned long)now,
                   DebugStats::board(),
                   DebugStats::role(),
                   (unsigned)depth,
                   (unsigned)MAX_INCOMING_QUEUE_DEPTH,
                   (unsigned long)pushes,
                   (unsigned long)pops,
                   (unsigned long)drops,
                   (unsigned long)overwrites,
                   (unsigned long)alloc_failures,
                   (unsigned)highwater);
}

void TBeamSupremeBleInterface::reset_reassembly() {
  _reassembly_buffer.clear();
  _expected_total = 0;
  _received_fragments = 0;
  _expected_message_len = 0;
  _reassembly_started_ms = 0;
}

RNS::Bytes TBeamSupremeBleInterface::local_identity_hash() const {
  String material = String("microReticulum BLE ") + _node_label;
  return Identity::truncated_hash(RNS::bytesFromString(material.c_str()));
}

void TBeamSupremeBleInterface::client_notify_callback(BLERemoteCharacteristic* characteristic,
                                                      uint8_t* data,
                                                      size_t length,
                                                      bool is_notify) {
  (void)characteristic;
  (void)is_notify;
  if (active_ble_interface) {
    // NimBLE/Arduino BLE owns this callback buffer. handle_fragment copies it
    // synchronously into owned reassembly storage before returning.
    active_ble_interface->handle_fragment(data, length);
  }
}
