#if defined(HOST_BLE_DUAL)
#include "HostBluezBleInterface.h"
#include "HostBluezPeripheralInterface.h"
#elif defined(HOST_BLE_PERIPHERAL)
#include "HostBluezPeripheralInterface.h"
using HostBleInterface = HostBluezPeripheralInterface;
#else
#include "HostBluezBleInterface.h"
using HostBleInterface = HostBluezBleInterface;
#endif
#include "SelectedText.h"

#include <Destination.h>
#include <Identity.h>
#include <Interface.h>
#include <Link.h>
#include <Log.h>
#include <Packet.h>
#include <Reticulum.h>
#include <Transport.h>
#include <Type.h>
#include <Utilities/OS.h>
#include <microStore/Adapters/UniversalFileSystem.h>
#include <microStore/FileSystem.h>

#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <vector>

static constexpr const char* APP_NAME = "microreticulum";
static constexpr const char* APP_ASPECT = "filetransfer";
static constexpr const char* ANNOUNCE_FILTER = "microreticulum.filetransfer";

#ifndef FILE_TRANSFER_CHUNK_SIZE
#define FILE_TRANSFER_CHUNK_SIZE 32
#endif

#ifndef FILE_TRANSFER_CHUNK_INTERVAL_MS
#define FILE_TRANSFER_CHUNK_INTERVAL_MS 500
#endif

#ifndef FILE_TRANSFER_REPEAT_INTERVAL_MS
#define FILE_TRANSFER_REPEAT_INTERVAL_MS 10000
#endif

#ifndef HOST_NODE_LABEL
#define HOST_NODE_LABEL "Node-JP-CLIENT"
#endif

static constexpr size_t TRANSFER_CHUNK_SIZE = FILE_TRANSFER_CHUNK_SIZE;
static constexpr uint32_t TRANSFER_CHUNK_INTERVAL_MS = FILE_TRANSFER_CHUNK_INTERVAL_MS;
static constexpr uint32_t TRANSFER_REPEAT_INTERVAL_MS = FILE_TRANSFER_REPEAT_INTERVAL_MS;
static constexpr uint32_t FNV1A_OFFSET = 2166136261UL;
static constexpr uint32_t FNV1A_PRIME = 16777619UL;

static RNS::Reticulum reticulum({RNS::Type::NONE});
#if defined(HOST_BLE_DUAL)
enum class BleDualPolicy {
  Both,
  FirstPathWins,
  CentralOnly,
  PeripheralOnly,
};

static std::vector<RNS::Interface> ble_interfaces;
static HostBluezBleInterface* ble_central_impl = nullptr;
static HostBluezPeripheralInterface* ble_peripheral_impl = nullptr;
static BleDualPolicy ble_dual_policy = BleDualPolicy::FirstPathWins;
static const char* ble_dual_winner = nullptr;
#else
static RNS::Interface ble_interface({RNS::Type::NONE});
static HostBleInterface* ble_impl = nullptr;
#endif
static RNS::Identity local_identity({RNS::Type::NONE});
static RNS::Destination inbound_destination({RNS::Type::NONE});
static RNS::Destination peer_destination({RNS::Type::NONE});
static RNS::Link active_link({RNS::Type::NONE});
static RNS::Link pending_link({RNS::Type::NONE});
static RNS::Bytes peer_hash;
static std::string peer_label;
static bool have_peer = false;
static bool link_active = false;
static bool link_attempted = false;
static std::string node_label = HOST_NODE_LABEL;
static bool ble_frame_log_enabled = false;
static bool running = true;

#if defined(HOST_BLE_DUAL)
static const char* ble_dual_policy_name(BleDualPolicy policy) {
  switch (policy) {
    case BleDualPolicy::Both:
      return "both";
    case BleDualPolicy::FirstPathWins:
      return "first-path-wins";
    case BleDualPolicy::CentralOnly:
      return "central-only";
    case BleDualPolicy::PeripheralOnly:
      return "peripheral-only";
  }
  return "unknown";
}

static bool set_ble_dual_policy(const char* value) {
  if (std::strcmp(value, "both") == 0) {
    ble_dual_policy = BleDualPolicy::Both;
    return true;
  }
  if (std::strcmp(value, "first-path-wins") == 0) {
    ble_dual_policy = BleDualPolicy::FirstPathWins;
    return true;
  }
  if (std::strcmp(value, "central-only") == 0) {
    ble_dual_policy = BleDualPolicy::CentralOnly;
    return true;
  }
  if (std::strcmp(value, "peripheral-only") == 0) {
    ble_dual_policy = BleDualPolicy::PeripheralOnly;
    return true;
  }
  return false;
}
#endif

struct RxTransferState {
  bool active = false;
  std::string sender;
  std::string file_name;
  size_t expected_size = 0;
  uint32_t expected_chunks = 0;
  uint32_t expected_crc = 0;
  uint32_t received_chunks = 0;
  size_t received_size = 0;
  uint32_t crc = FNV1A_OFFSET;
};

struct TxTransferState {
  bool active = false;
  bool complete = false;
  size_t offset = 0;
  uint32_t sequence = 0;
  uint32_t total_chunks = 0;
  uint32_t crc = 0;
  uint64_t next_start_ms = 0;
  uint32_t round = 0;
};

static RxTransferState rx_transfer;
static TxTransferState tx_transfer;

static uint64_t millis64() {
  return RNS::Utilities::OS::ltime();
}

static uint32_t fnv1a_update(uint32_t crc, uint8_t byte) {
  crc ^= byte;
  crc *= FNV1A_PRIME;
  return crc;
}

static uint32_t selected_text_crc() {
  uint32_t crc = FNV1A_OFFSET;
  for (size_t i = 0; i < SELECTED_TEXT_SIZE; ++i) {
    crc = fnv1a_update(crc, read_selected_text_byte(i));
  }
  return crc;
}

static std::string hex32(uint32_t value) {
  char buffer[9];
  std::snprintf(buffer, sizeof(buffer), "%08lX", (unsigned long)value);
  return std::string(buffer);
}

static std::string link_id_hex(const RNS::Link& link) {
  if (!link) {
    return "none";
  }
  return link.link_id().toHex();
}

static bool should_initiate_link_to(const std::string& label) {
  return std::strcmp(node_label.c_str(), label.c_str()) < 0;
}

static void print_text_payload(const std::string& payload) {
  std::fwrite(payload.data(), 1, payload.size(), stdout);
  std::fflush(stdout);
}

static size_t find_pipe(const std::string& text, size_t start = 0) {
  return text.find('|', start);
}

static uint32_t parse_u32(const std::string& text) {
  return (uint32_t)std::strtoul(text.c_str(), nullptr, 10);
}

static uint32_t parse_hex32(const std::string& text) {
  return (uint32_t)std::strtoul(text.c_str(), nullptr, 16);
}

static void handle_file_begin(const std::string& sender,
                              const std::string& file_name,
                              size_t size,
                              uint32_t chunks,
                              uint32_t crc) {
  rx_transfer.active = true;
  rx_transfer.sender = sender;
  rx_transfer.file_name = file_name;
  rx_transfer.expected_size = size;
  rx_transfer.expected_chunks = chunks;
  rx_transfer.expected_crc = crc;
  rx_transfer.received_chunks = 0;
  rx_transfer.received_size = 0;
  rx_transfer.crc = FNV1A_OFFSET;

  std::printf("\nRX FILE BEGIN: from=%s file=%s bytes=%u chunks=%lu crc=%s\n",
              sender.c_str(), file_name.c_str(), (unsigned)size,
              (unsigned long)chunks, hex32(crc).c_str());
}

static void handle_file_data(const std::string& sender,
                             uint32_t sequence,
                             uint32_t total,
                             const std::string& payload) {
  if (!rx_transfer.active || rx_transfer.sender != sender) {
    std::printf("\nRX FILE DATA ignored: sender=%s seq=%lu no active transfer\n",
                sender.c_str(), (unsigned long)sequence);
    return;
  }

  for (char c : payload) {
    rx_transfer.crc = fnv1a_update(rx_transfer.crc, (uint8_t)c);
  }
  rx_transfer.received_chunks++;
  rx_transfer.received_size += payload.size();

  (void)sequence;
  (void)total;
  print_text_payload(payload);
}

static void handle_file_end(const std::string& sender,
                            const std::string& file_name,
                            size_t size,
                            uint32_t chunks,
                            uint32_t crc) {
  bool ok = rx_transfer.active &&
            rx_transfer.sender == sender &&
            rx_transfer.file_name == file_name &&
            rx_transfer.expected_size == size &&
            rx_transfer.expected_chunks == chunks &&
            rx_transfer.expected_crc == crc &&
            rx_transfer.received_size == size &&
            rx_transfer.received_chunks == chunks &&
            rx_transfer.crc == crc;

  std::printf("\nRX FILE END: from=%s file=%s received=%u/%u chunks=%lu/%lu crc=%s status=%s\n",
              sender.c_str(), file_name.c_str(),
              (unsigned)rx_transfer.received_size, (unsigned)size,
              (unsigned long)rx_transfer.received_chunks, (unsigned long)chunks,
              hex32(rx_transfer.crc).c_str(), ok ? "OK" : "VERIFY_FAIL");
  rx_transfer.active = false;
}

static void parse_file_transfer_packet(const std::string& message) {
  size_t p1 = find_pipe(message);
  size_t p2 = p1 == std::string::npos ? std::string::npos : find_pipe(message, p1 + 1);
  if (p1 == std::string::npos || p2 == std::string::npos) {
    print_text_payload(message);
    return;
  }

  std::string type = message.substr(0, p1);
  std::string sender = message.substr(p1 + 1, p2 - p1 - 1);

  if (type == "FTB") {
    size_t p3 = find_pipe(message, p2 + 1);
    size_t p4 = p3 == std::string::npos ? std::string::npos : find_pipe(message, p3 + 1);
    size_t p5 = p4 == std::string::npos ? std::string::npos : find_pipe(message, p4 + 1);
    if (p3 == std::string::npos || p4 == std::string::npos || p5 == std::string::npos) {
      return;
    }
    handle_file_begin(sender,
                      message.substr(p2 + 1, p3 - p2 - 1),
                      parse_u32(message.substr(p3 + 1, p4 - p3 - 1)),
                      parse_u32(message.substr(p4 + 1, p5 - p4 - 1)),
                      parse_hex32(message.substr(p5 + 1)));
    return;
  }

  if (type == "FTD") {
    size_t p3 = find_pipe(message, p2 + 1);
    size_t p4 = p3 == std::string::npos ? std::string::npos : find_pipe(message, p3 + 1);
    if (p3 == std::string::npos || p4 == std::string::npos) {
      return;
    }
    handle_file_data(sender,
                     parse_u32(message.substr(p2 + 1, p3 - p2 - 1)),
                     parse_u32(message.substr(p3 + 1, p4 - p3 - 1)),
                     message.substr(p4 + 1));
    return;
  }

  if (type == "FTE") {
    size_t p3 = find_pipe(message, p2 + 1);
    size_t p4 = p3 == std::string::npos ? std::string::npos : find_pipe(message, p3 + 1);
    size_t p5 = p4 == std::string::npos ? std::string::npos : find_pipe(message, p4 + 1);
    if (p3 == std::string::npos || p4 == std::string::npos || p5 == std::string::npos) {
      return;
    }
    handle_file_end(sender,
                    message.substr(p2 + 1, p3 - p2 - 1),
                    parse_u32(message.substr(p3 + 1, p4 - p3 - 1)),
                    parse_u32(message.substr(p4 + 1, p5 - p4 - 1)),
                    parse_hex32(message.substr(p5 + 1)));
    return;
  }

  print_text_payload(message);
}

static void on_link_packet(const RNS::Bytes& data, const RNS::Packet& packet) {
  (void)packet;
  parse_file_transfer_packet(data.toString());
}

static void on_link_closed(RNS::Link& link) {
  std::printf("LINK CLOSED: peer=%s link_id=%s\n",
              peer_label.empty() ? "unknown" : peer_label.c_str(),
              link_id_hex(link).c_str());
  active_link = {RNS::Type::NONE};
  pending_link = {RNS::Type::NONE};
  link_active = false;
  link_attempted = false;
}

static void on_outbound_link_established(RNS::Link& link) {
  active_link = link;
  active_link.set_packet_callback(on_link_packet);
  active_link.set_link_closed_callback(on_link_closed);
  link_active = true;
  std::printf("LINK ACTIVE: outbound peer=%s link_id=%s\n",
              peer_label.empty() ? "unknown" : peer_label.c_str(),
              link_id_hex(active_link).c_str());
}

static void on_inbound_link_established(RNS::Link& link) {
  active_link = link;
  active_link.set_packet_callback(on_link_packet);
  active_link.set_link_closed_callback(on_link_closed);
  link_active = true;
  link_attempted = true;
  std::printf("LINK ACTIVE: inbound peer=%s link_id=%s\n",
              peer_label.empty() ? "unknown" : peer_label.c_str(),
              link_id_hex(active_link).c_str());
}

class FileAnnounceHandler : public RNS::AnnounceHandler {
public:
  FileAnnounceHandler() : RNS::AnnounceHandler(ANNOUNCE_FILTER) {}

  void received_announce(const RNS::Bytes& destination_hash,
                         const RNS::Identity& announced_identity,
                         const RNS::Bytes& app_data) override {
    std::string label = app_data ? app_data.toString() : "(no label)";
    if (label == node_label || !announced_identity) {
      return;
    }

    peer_hash = destination_hash;
    peer_label = label;
    peer_destination = RNS::Destination(announced_identity,
                                        RNS::Type::Destination::OUT,
                                        RNS::Type::Destination::SINGLE,
                                        destination_hash);
    have_peer = true;
    std::printf("RX ANNOUNCE: label=%s hash=%s\n",
                peer_label.c_str(), peer_hash.toHex().c_str());
  }
};

static RNS::HAnnounceHandler announce_handler(new FileAnnounceHandler());

static void send_announce() {
  if (!inbound_destination) {
    return;
  }
  std::printf("TX ANNOUNCE: %s\n", node_label.c_str());
  inbound_destination.announce(RNS::bytesFromString(node_label.c_str()));
}

static void maybe_open_link() {
  if (!have_peer || link_active || link_attempted || !peer_destination) {
    return;
  }
  if (!should_initiate_link_to(peer_label)) {
    return;
  }

  std::printf("TX LINKREQUEST: opening link to %s\n", peer_label.c_str());
  pending_link = RNS::Link(peer_destination);
  pending_link.set_packet_callback(on_link_packet);
  pending_link.set_link_established_callback(on_outbound_link_established);
  pending_link.set_link_closed_callback(on_link_closed);
  link_attempted = true;
}

static void send_link_message(const std::string& message) {
  RNS::Packet(active_link, RNS::bytesFromString(message.c_str())).send();
}

static bool send_next_file_packet(uint64_t now) {
  if (tx_transfer.complete && now < tx_transfer.next_start_ms) {
    return false;
  }

  if (!tx_transfer.active) {
    tx_transfer.active = true;
    tx_transfer.complete = false;
    tx_transfer.offset = 0;
    tx_transfer.sequence = 0;
    tx_transfer.crc = selected_text_crc();
    tx_transfer.total_chunks = (SELECTED_TEXT_SIZE + TRANSFER_CHUNK_SIZE - 1) / TRANSFER_CHUNK_SIZE;
    tx_transfer.round++;

    std::string begin = std::string("FTB|") + node_label + "|" + SELECTED_TEXT_NAME + "|" +
                        std::to_string((unsigned)SELECTED_TEXT_SIZE) + "|" +
                        std::to_string((unsigned long)tx_transfer.total_chunks) + "|" +
                        hex32(tx_transfer.crc);
    std::printf("TX FILE BEGIN: round=%lu file=%s bytes=%u chunks=%lu crc=%s\n",
                (unsigned long)tx_transfer.round, SELECTED_TEXT_NAME,
                (unsigned)SELECTED_TEXT_SIZE, (unsigned long)tx_transfer.total_chunks,
                hex32(tx_transfer.crc).c_str());
    send_link_message(begin);
    return true;
  }

  if (tx_transfer.offset < SELECTED_TEXT_SIZE) {
    size_t remaining = SELECTED_TEXT_SIZE - tx_transfer.offset;
    size_t count = remaining < TRANSFER_CHUNK_SIZE ? remaining : TRANSFER_CHUNK_SIZE;
    std::string payload;
    payload.reserve(count);
    for (size_t i = 0; i < count; ++i) {
      payload += (char)read_selected_text_byte(tx_transfer.offset + i);
    }
    tx_transfer.sequence++;
    tx_transfer.offset += count;

    std::string data = std::string("FTD|") + node_label + "|" +
                       std::to_string((unsigned long)tx_transfer.sequence) + "|" +
                       std::to_string((unsigned long)tx_transfer.total_chunks) + "|" + payload;
    send_link_message(data);
    return true;
  }

  std::string end = std::string("FTE|") + node_label + "|" + SELECTED_TEXT_NAME + "|" +
                    std::to_string((unsigned)SELECTED_TEXT_SIZE) + "|" +
                    std::to_string((unsigned long)tx_transfer.total_chunks) + "|" +
                    hex32(tx_transfer.crc);
  std::printf("TX FILE END: round=%lu file=%s bytes=%u chunks=%lu crc=%s next_round_in_ms=%lu\n",
              (unsigned long)tx_transfer.round, SELECTED_TEXT_NAME,
              (unsigned)SELECTED_TEXT_SIZE, (unsigned long)tx_transfer.total_chunks,
              hex32(tx_transfer.crc).c_str(), (unsigned long)TRANSFER_REPEAT_INTERVAL_MS);
  send_link_message(end);
  tx_transfer.active = false;
  tx_transfer.complete = true;
  tx_transfer.next_start_ms = now + TRANSFER_REPEAT_INTERVAL_MS;
  return true;
}

static void setup_reticulum() {
  microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()};
  filesystem.init();
  RNS::Utilities::OS::register_filesystem(filesystem);

#if defined(HOST_BLE_DUAL)
  if (ble_dual_policy != BleDualPolicy::PeripheralOnly) {
    auto central = std::shared_ptr<RNS::InterfaceImpl>(new HostBluezBleInterface(node_label, ble_frame_log_enabled));
    ble_central_impl = static_cast<HostBluezBleInterface*>(central.get());
    RNS::Interface central_interface(central);
    central_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
    RNS::Transport::register_interface(central_interface);
    ble_interfaces.push_back(central_interface);
    central_interface.start();
  }

  if (ble_dual_policy != BleDualPolicy::CentralOnly) {
    auto peripheral = std::shared_ptr<RNS::InterfaceImpl>(new HostBluezPeripheralInterface(node_label, ble_frame_log_enabled));
    ble_peripheral_impl = static_cast<HostBluezPeripheralInterface*>(peripheral.get());
    RNS::Interface peripheral_interface(peripheral);
    peripheral_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
    RNS::Transport::register_interface(peripheral_interface);
    ble_interfaces.push_back(peripheral_interface);
    peripheral_interface.start();
  }
#else
  auto impl = std::shared_ptr<RNS::InterfaceImpl>(new HostBleInterface(node_label, ble_frame_log_enabled));
  ble_impl = static_cast<HostBleInterface*>(impl.get());
  ble_interface = RNS::Interface(impl);
  ble_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
  RNS::Transport::register_interface(ble_interface);
  ble_interface.start();
#endif

  reticulum = RNS::Reticulum();
  reticulum.transport_enabled(false);
  reticulum.probe_destination_enabled(false);
  reticulum.start();

  local_identity = RNS::Identity();
  inbound_destination = RNS::Destination(local_identity,
                                         RNS::Type::Destination::IN,
                                         RNS::Type::Destination::SINGLE,
                                         APP_NAME,
                                         APP_ASPECT);
  inbound_destination.set_link_established_callback(on_inbound_link_established);
  inbound_destination.set_proof_strategy(RNS::Type::Destination::PROVE_NONE);

  RNS::Transport::register_announce_handler(announce_handler);

  std::printf("Local SINGLE destination: %s\n", inbound_destination.hash().toHex().c_str());
}

static void loop_ble_interfaces() {
#if defined(HOST_BLE_DUAL)
  if (ble_peripheral_impl) {
    ble_peripheral_impl->loop();
  }
  if (ble_central_impl) {
    ble_central_impl->loop();
  }
#else
  if (ble_impl) {
    ble_impl->loop();
  }
#endif
}

#if defined(HOST_BLE_DUAL)
static void enforce_ble_dual_policy() {
  if (ble_dual_policy != BleDualPolicy::FirstPathWins || ble_dual_winner) {
    return;
  }

  if (ble_peripheral_impl && ble_peripheral_impl->connected()) {
    ble_dual_winner = "peripheral";
    std::printf("BLE dual policy first-path-wins: peripheral path won peer=%s; stopping central role\n",
                ble_peripheral_impl->peer_path().empty() ? "unknown" : ble_peripheral_impl->peer_path().c_str());
    if (ble_central_impl) {
      ble_central_impl->stop();
    }
    return;
  }

  if (ble_central_impl && ble_central_impl->connected()) {
    ble_dual_winner = "central";
    std::printf("BLE dual policy first-path-wins: central path won peer=%s; stopping peripheral role\n",
                ble_central_impl->peer_path().empty() ? "unknown" : ble_central_impl->peer_path().c_str());
    if (ble_peripheral_impl) {
      ble_peripheral_impl->stop();
    }
  }
}
#endif

static bool ble_connected() {
#if defined(HOST_BLE_DUAL)
  return (ble_central_impl && ble_central_impl->connected()) ||
         (ble_peripheral_impl && ble_peripheral_impl->connected());
#else
  return ble_impl && ble_impl->connected();
#endif
}

static const char* ble_role_status() {
#if defined(HOST_BLE_DUAL)
  if (ble_dual_winner) {
    return ble_dual_winner;
  }
  if (ble_central_impl && ble_central_impl->connected()) {
    return ble_central_impl->role_name();
  }
  if (ble_peripheral_impl && ble_peripheral_impl->connected()) {
    return ble_peripheral_impl->role_name();
  }
  return "linux-dual";
#else
  return ble_impl ? ble_impl->role_name() : "unknown";
#endif
}

static void stop_ble_interfaces() {
#if defined(HOST_BLE_DUAL)
  if (ble_peripheral_impl) {
    ble_peripheral_impl->stop();
  }
  if (ble_central_impl) {
    ble_central_impl->stop();
  }
#else
  if (ble_impl) {
    ble_impl->stop();
  }
#endif
}

static void handle_signal(int signal) {
  (void)signal;
  running = false;
}

int main(int argc, char** argv) {
  std::signal(SIGINT, handle_signal);
  std::signal(SIGTERM, handle_signal);

  static constexpr const char* policy_prefix = "--ble-dual-policy=";
  for (int i = 1; i < argc; ++i) {
    if (std::strcmp(argv[i], "--ble-frame-log") == 0) {
      ble_frame_log_enabled = true;
    }
#if defined(HOST_BLE_DUAL)
    else if (std::strncmp(argv[i], policy_prefix, std::strlen(policy_prefix)) == 0) {
      const char* value = argv[i] + std::strlen(policy_prefix);
      if (!set_ble_dual_policy(value)) {
        std::fprintf(stderr,
                     "Unknown --ble-dual-policy=%s; valid values: both, first-path-wins, central-only, peripheral-only\n",
                     value);
        return 2;
      }
    }
#endif
    else {
      std::fprintf(stderr, "Unknown option: %s\n", argv[i]);
      return 2;
    }
  }

  RNS::loglevel(RNS::LOG_NOTICE);
  std::printf("Exercise 306 native BLE file transfer console\n");
  std::printf("Node=%s\n", node_label.c_str());
#if defined(HOST_BLE_DUAL)
  std::printf("BLE dual policy=%s\n", ble_dual_policy_name(ble_dual_policy));
#endif
  std::printf("BLE frame log=%s\n", ble_frame_log_enabled ? "on" : "off");
  std::printf("Selected file=%s bytes=%u chunk=%u interval_ms=%lu repeat_rest_ms=%lu\n",
              SELECTED_TEXT_NAME, (unsigned)SELECTED_TEXT_SIZE,
              (unsigned)TRANSFER_CHUNK_SIZE,
              (unsigned long)TRANSFER_CHUNK_INTERVAL_MS,
              (unsigned long)TRANSFER_REPEAT_INTERVAL_MS);

  setup_reticulum();
  std::printf("microReticulum ready; OLED skipped on host native build\n");

  uint64_t next_announce_ms = 0;
  uint64_t next_transfer_ms = 0;
  uint64_t next_wait_log_ms = 0;

  while (running) {
    reticulum.loop();
    loop_ble_interfaces();
#if defined(HOST_BLE_DUAL)
    enforce_ble_dual_policy();
#endif

    uint64_t now = millis64();
    if (!ble_connected()) {
      if (next_wait_log_ms == 0 || now >= next_wait_log_ms) {
        next_wait_log_ms = now + 10000;
        std::printf("BLE %s waiting for peer\n", ble_role_status());
      }
      RNS::Utilities::OS::sleep(0.005);
      continue;
    }

    if (next_announce_ms == 0) {
      next_announce_ms = now + 1100;
    }
    if (!link_active && now >= next_announce_ms) {
      next_announce_ms = now + 15000;
      send_announce();
    }

    maybe_open_link();

    if (link_active && next_transfer_ms == 0) {
      next_transfer_ms = now + (should_initiate_link_to(peer_label) ? 900 : 1200);
    }

    if (link_active && now >= next_transfer_ms) {
      next_transfer_ms = now + TRANSFER_CHUNK_INTERVAL_MS;
      send_next_file_packet(now);
    }

    RNS::Utilities::OS::sleep(0.005);
  }

  stop_ble_interfaces();
  std::printf("\nStopped\n");
  return 0;
}
