#include "HostBluezPeripheralInterface.h"

#include <Utilities/OS.h>

#include <algorithm>
#include <cstdio>
#include <cstring>

using namespace RNS;

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

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];
}

static void log_error(const char* what, GError* error) {
  if (error) {
    std::fprintf(stderr, "%s: %s\n", what, error->message);
    g_error_free(error);
  } else {
    std::fprintf(stderr, "%s\n", what);
  }
}

static GVariant* bytes_variant(const RNS::Bytes& bytes) {
  GVariantBuilder builder;
  g_variant_builder_init(&builder, G_VARIANT_TYPE("ay"));
  for (size_t i = 0; i < bytes.size(); ++i) {
    g_variant_builder_add(&builder, "y", bytes[i]);
  }
  return g_variant_builder_end(&builder);
}

static GVariant* flags_variant(std::initializer_list<const char*> flags) {
  GVariantBuilder builder;
  g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));
  for (const char* flag : flags) {
    g_variant_builder_add(&builder, "s", flag);
  }
  return g_variant_builder_end(&builder);
}

static std::string peer_address_from_path(const std::string& path) {
  size_t pos = path.find("/dev_");
  if (pos == std::string::npos) {
    return path.empty() ? std::string("unknown") : path;
  }
  std::string addr = path.substr(pos + 5);
  std::replace(addr.begin(), addr.end(), '_', ':');
  return addr;
}

static void log_reticulum_frame(const char* direction,
                                const std::string& node_label,
                                const std::string& peer_path,
                                const char* object_path,
                                const void* iface,
                                const RNS::Bytes& packet) {
  unsigned first = packet.size() > 0 ? packet[0] : 0;
  std::printf("BLE-FRAME t=%llu node=%s role=peripheral dir=%s peer=%s object=%s if=%p len=%zu packet0=0x%02X link_hint=unknown\n",
              (unsigned long long)now_ms(),
              node_label.c_str(),
              direction,
              peer_address_from_path(peer_path).c_str(),
              object_path ? object_path : "unknown",
              iface,
              packet.size(),
              first);
}

static const char* OBJECT_MANAGER_XML =
    "<node>"
    "  <interface name='org.freedesktop.DBus.ObjectManager'>"
    "    <method name='GetManagedObjects'>"
    "      <arg name='objects' type='a{oa{sa{sv}}}' direction='out'/>"
    "    </method>"
    "  </interface>"
    "</node>";

static const char* GATT_SERVICE_XML =
    "<node>"
    "  <interface name='org.bluez.GattService1'>"
    "    <property name='UUID' type='s' access='read'/>"
    "    <property name='Primary' type='b' access='read'/>"
    "  </interface>"
    "</node>";

static const char* GATT_CHARACTERISTIC_XML =
    "<node>"
    "  <interface name='org.bluez.GattCharacteristic1'>"
    "    <method name='ReadValue'>"
    "      <arg name='options' type='a{sv}' direction='in'/>"
    "      <arg name='value' type='ay' direction='out'/>"
    "    </method>"
    "    <method name='WriteValue'>"
    "      <arg name='value' type='ay' direction='in'/>"
    "      <arg name='options' type='a{sv}' direction='in'/>"
    "    </method>"
    "    <method name='StartNotify'/>"
    "    <method name='StopNotify'/>"
    "    <property name='UUID' type='s' access='read'/>"
    "    <property name='Service' type='o' access='read'/>"
    "    <property name='Value' type='ay' access='read'/>"
    "    <property name='Notifying' type='b' access='read'/>"
    "    <property name='Flags' type='as' access='read'/>"
    "  </interface>"
    "</node>";

static const char* ADVERTISEMENT_XML =
    "<node>"
    "  <interface name='org.bluez.LEAdvertisement1'>"
    "    <method name='Release'/>"
    "    <property name='Type' type='s' access='read'/>"
    "    <property name='ServiceUUIDs' type='as' access='read'/>"
    "    <property name='LocalName' type='s' access='read'/>"
    "  </interface>"
    "</node>";

HostBluezPeripheralInterface::HostBluezPeripheralInterface(const std::string& node_label,
                                                           bool frame_log_enabled,
                                                           const char* name)
    : InterfaceImpl(name), node_label_(node_label), frame_log_enabled_(frame_log_enabled) {
  _IN = true;
  _OUT = true;
  _bitrate = 1000000;
  _HW_MTU = BLE_PAYLOAD_SIZE;
  identity_value_ = local_identity_hash();
}

HostBluezPeripheralInterface::~HostBluezPeripheralInterface() {
  stop();
  if (object_manager_node_) {
    g_dbus_node_info_unref(object_manager_node_);
  }
  if (gatt_service_node_) {
    g_dbus_node_info_unref(gatt_service_node_);
  }
  if (gatt_characteristic_node_) {
    g_dbus_node_info_unref(gatt_characteristic_node_);
  }
  if (advertisement_node_) {
    g_dbus_node_info_unref(advertisement_node_);
  }
  if (bus_) {
    g_object_unref(bus_);
  }
}

bool HostBluezPeripheralInterface::start() {
  if (online_) {
    return true;
  }
  if (!connect_bus() || !find_adapter()) {
    return false;
  }

  online_ = true;
  _online = true;
  std::printf("BLE linux-peripheral: adapter=%s label=%s service=%s\n",
              adapter_path_.c_str(), node_label_.c_str(), SERVICE_UUID);

  if (!adapter_supports_peripheral()) {
    std::fprintf(stderr,
                 "BLE linux-peripheral: adapter/BlueZ is missing GattManager1 or LEAdvertisingManager1; peripheral mode cannot run on this adapter as configured\n");
    return true;
  }

  register_gatt_application();
  return true;
}

void HostBluezPeripheralInterface::stop() {
  unregister_bluez_objects();
  online_ = false;
  connected_ = false;
  notifying_ = false;
  peer_path_.clear();
  _online = false;
}

bool HostBluezPeripheralInterface::connect_bus() {
  GError* error = nullptr;
  bus_ = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, &error);
  if (!bus_) {
    log_error("BLE linux-peripheral: cannot connect to system D-Bus", error);
    return false;
  }
  return true;
}

bool HostBluezPeripheralInterface::find_adapter() {
  GError* error = nullptr;
  GVariant* managed = g_dbus_connection_call_sync(bus_, BLUEZ_BUS, "/",
                                                  "org.freedesktop.DBus.ObjectManager",
                                                  "GetManagedObjects", nullptr,
                                                  G_VARIANT_TYPE("(a{oa{sa{sv}}})"),
                                                  G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error);
  if (!managed) {
    log_error("BLE linux-peripheral: GetManagedObjects failed", error);
    return false;
  }

  GVariantIter* objects = nullptr;
  g_variant_get(managed, "(a{oa{sa{sv}}})", &objects);
  const gchar* object_path = nullptr;
  GVariant* ifaces = nullptr;
  while (g_variant_iter_next(objects, "{&o@a{sa{sv}}}", &object_path, &ifaces)) {
    GVariant* adapter = g_variant_lookup_value(ifaces, "org.bluez.Adapter1", G_VARIANT_TYPE("a{sv}"));
    if (adapter) {
      adapter_path_ = object_path;
      g_variant_unref(adapter);
      GVariant* gatt_manager = g_variant_lookup_value(ifaces, "org.bluez.GattManager1", G_VARIANT_TYPE("a{sv}"));
      GVariant* advertising_manager = g_variant_lookup_value(ifaces, "org.bluez.LEAdvertisingManager1", G_VARIANT_TYPE("a{sv}"));
      has_gatt_manager_ = gatt_manager != nullptr;
      has_advertising_manager_ = advertising_manager != nullptr;
      if (gatt_manager) {
        g_variant_unref(gatt_manager);
      }
      if (advertising_manager) {
        g_variant_unref(advertising_manager);
      }
      g_variant_unref(ifaces);
      break;
    }
    g_variant_unref(ifaces);
  }
  g_variant_iter_free(objects);
  g_variant_unref(managed);

  if (adapter_path_.empty()) {
    std::fprintf(stderr, "BLE linux-peripheral: no BlueZ adapter found\n");
    return false;
  }

  error = nullptr;
  GVariant* powered = g_dbus_connection_call_sync(
      bus_, BLUEZ_BUS, adapter_path_.c_str(), "org.freedesktop.DBus.Properties", "Set",
      g_variant_new("(ssv)", "org.bluez.Adapter1", "Powered", g_variant_new_boolean(TRUE)),
      nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error);
  if (!powered) {
    log_error("BLE linux-peripheral: could not power adapter", error);
    return false;
  }
  g_variant_unref(powered);
  return true;
}

bool HostBluezPeripheralInterface::adapter_supports_peripheral() const {
  return has_gatt_manager_ && has_advertising_manager_;
}

bool HostBluezPeripheralInterface::register_gatt_application() {
  GError* error = nullptr;
  object_manager_node_ = g_dbus_node_info_new_for_xml(OBJECT_MANAGER_XML, &error);
  if (!object_manager_node_) {
    log_error("BLE linux-peripheral: ObjectManager XML parse failed", error);
    return false;
  }
  gatt_service_node_ = g_dbus_node_info_new_for_xml(GATT_SERVICE_XML, &error);
  if (!gatt_service_node_) {
    log_error("BLE linux-peripheral: GattService XML parse failed", error);
    return false;
  }
  gatt_characteristic_node_ = g_dbus_node_info_new_for_xml(GATT_CHARACTERISTIC_XML, &error);
  if (!gatt_characteristic_node_) {
    log_error("BLE linux-peripheral: GattCharacteristic XML parse failed", error);
    return false;
  }

  static const GDBusInterfaceVTable vtable = {
      HostBluezPeripheralInterface::method_call,
      HostBluezPeripheralInterface::property_get,
      nullptr,
      {nullptr}};

  auto register_one = [&](const char* path, GDBusNodeInfo* node) -> bool {
    guint id = g_dbus_connection_register_object(bus_, path, node->interfaces[0], &vtable, this, nullptr, &error);
    if (id == 0) {
      log_error("BLE linux-peripheral: D-Bus object registration failed", error);
      return false;
    }
    object_registration_ids_.push_back(id);
    return true;
  };

  if (!register_one(APP_PATH, object_manager_node_) ||
      !register_one(SERVICE_PATH, gatt_service_node_) ||
      !register_one(TX_PATH, gatt_characteristic_node_) ||
      !register_one(RX_PATH, gatt_characteristic_node_) ||
      !register_one(IDENTITY_PATH, gatt_characteristic_node_)) {
    return false;
  }

  g_dbus_connection_call(bus_, BLUEZ_BUS, adapter_path_.c_str(),
                         "org.bluez.GattManager1", "RegisterApplication",
                         g_variant_new("(oa{sv})", APP_PATH, nullptr),
                         nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr,
                         register_application_done, this);
  std::printf("BLE linux-peripheral: GATT application registration requested\n");
  return true;
}

bool HostBluezPeripheralInterface::register_advertisement() {
  GError* error = nullptr;
  advertisement_node_ = g_dbus_node_info_new_for_xml(ADVERTISEMENT_XML, &error);
  if (!advertisement_node_) {
    log_error("BLE linux-peripheral: Advertisement XML parse failed", error);
    return false;
  }

  static const GDBusInterfaceVTable vtable = {
      HostBluezPeripheralInterface::method_call,
      HostBluezPeripheralInterface::property_get,
      nullptr,
      {nullptr}};

  guint id = g_dbus_connection_register_object(bus_, ADV_PATH, advertisement_node_->interfaces[0],
                                               &vtable, this, nullptr, &error);
  if (id == 0) {
    log_error("BLE linux-peripheral: advertisement object registration failed", error);
    return false;
  }
  object_registration_ids_.push_back(id);

  g_dbus_connection_call(bus_, BLUEZ_BUS, adapter_path_.c_str(),
                         "org.bluez.LEAdvertisingManager1", "RegisterAdvertisement",
                         g_variant_new("(oa{sv})", ADV_PATH, nullptr),
                         nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr,
                         register_advertisement_done, this);
  std::printf("BLE linux-peripheral: advertisement registration requested name=RNS-%s\n", node_label_.c_str());
  return true;
}

void HostBluezPeripheralInterface::unregister_bluez_objects() {
  if (!bus_) {
    return;
  }
  if (advertisement_registered_) {
    GError* error = nullptr;
    GVariant* result = g_dbus_connection_call_sync(
        bus_, BLUEZ_BUS, adapter_path_.c_str(), "org.bluez.LEAdvertisingManager1", "UnregisterAdvertisement",
        g_variant_new("(o)", ADV_PATH), nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error);
    if (!result) {
      log_error("BLE linux-peripheral: UnregisterAdvertisement failed", error);
    } else {
      g_variant_unref(result);
    }
    advertisement_registered_ = false;
  }
  if (gatt_registered_) {
    GError* error = nullptr;
    GVariant* result = g_dbus_connection_call_sync(
        bus_, BLUEZ_BUS, adapter_path_.c_str(), "org.bluez.GattManager1", "UnregisterApplication",
        g_variant_new("(o)", APP_PATH), nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error);
    if (!result) {
      log_error("BLE linux-peripheral: UnregisterApplication failed", error);
    } else {
      g_variant_unref(result);
    }
    gatt_registered_ = false;
  }

  for (guint id : object_registration_ids_) {
    g_dbus_connection_unregister_object(bus_, id);
  }
  object_registration_ids_.clear();
}

void HostBluezPeripheralInterface::loop() {
  if (!online_) {
    return;
  }
  while (g_main_context_iteration(nullptr, FALSE)) {
  }

  if (reassembly_started_ms_ != 0 && now_ms() - reassembly_started_ms_ > REASSEMBLY_TIMEOUT_MS) {
    std::fprintf(stderr, "BLE linux-peripheral: reassembly timeout; dropping partial packet\n");
    reset_reassembly();
  }

  RNS::Bytes packet({RNS::Type::NONE});
  while (dequeue_packet(packet)) {
    InterfaceImpl::handle_incoming(packet);
  }
}

void HostBluezPeripheralInterface::send_outgoing(const RNS::Bytes& data) {
  if (!online_ || !connected_ || !notifying_) {
    return;
  }

  if (frame_log_enabled_) {
    log_reticulum_frame("tx", node_label_, peer_path_, TX_PATH, this, data);
  }

  size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
  if (total == 0 || total > 65535) {
    std::fprintf(stderr, "BLE linux-peripheral: cannot fragment packet len=%zu fragments=%zu\n", data.size(), total);
    return;
  }

  uint32_t msg_id = ++tx_message_id_;
  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());
    std::memcpy(fragment + FRAG_HEADER_SIZE, data.data() + offset, chunk);
    send_fragment(fragment, FRAG_HEADER_SIZE + chunk);
    RNS::Utilities::OS::sleep(0.020);
  }

  InterfaceImpl::handle_outgoing(data);
}

void HostBluezPeripheralInterface::send_fragment(const uint8_t* data, size_t len) {
  tx_value_.assign(data, len);
  emit_tx_value_changed();
}

void HostBluezPeripheralInterface::emit_tx_value_changed() {
  if (!bus_ || !notifying_) {
    return;
  }

  GVariantBuilder changed;
  g_variant_builder_init(&changed, G_VARIANT_TYPE("a{sv}"));
  g_variant_builder_add(&changed, "{sv}", "Value", bytes_variant(tx_value_));
  GVariantBuilder invalidated;
  g_variant_builder_init(&invalidated, G_VARIANT_TYPE("as"));
  g_dbus_connection_emit_signal(bus_, nullptr, TX_PATH,
                                "org.freedesktop.DBus.Properties", "PropertiesChanged",
                                g_variant_new("(sa{sv}as)", "org.bluez.GattCharacteristic1", &changed, &invalidated),
                                nullptr);
}

void HostBluezPeripheralInterface::handle_fragment(const uint8_t* data, size_t len) {
  if (len == 16) {
    connected_ = true;
    std::printf("BLE linux-peripheral: identity handshake received\n");
    return;
  }
  if (len < FRAG_HEADER_SIZE) {
    std::fprintf(stderr, "BLE linux-peripheral: fragment too short len=%zu\n", 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;
  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)) {
    std::fprintf(stderr, "BLE linux-peripheral: invalid fragment header\n");
    reset_reassembly();
    return;
  }

  if (sequence == 0) {
    reset_reassembly();
    expected_total_ = total;
    received_fragments_ = 0;
    expected_message_len_ = msg_len;
    reassembly_started_ms_ = now_ms();
    current_rx_message_id_ = msg_id;
  } else if (reassembly_started_ms_ == 0 || msg_id != current_rx_message_id_) {
    return;
  }

  if (expected_total_ != total || expected_message_len_ != msg_len || sequence != received_fragments_) {
    std::fprintf(stderr, "BLE linux-peripheral: out-of-order fragment; dropping partial packet\n");
    reset_reassembly();
    return;
  }

  reassembly_buffer_.append(payload, payload_len);
  received_fragments_++;

  if (received_fragments_ == expected_total_) {
    if (reassembly_buffer_.size() == expected_message_len_) {
      enqueue_packet(reassembly_buffer_);
    }
    reset_reassembly();
  }
}

void HostBluezPeripheralInterface::enqueue_packet(const RNS::Bytes& packet) {
  if (frame_log_enabled_) {
    log_reticulum_frame("rx", node_label_, peer_path_, RX_PATH, this, packet);
  }
  incoming_packets_.push_back(packet);
}

bool HostBluezPeripheralInterface::dequeue_packet(RNS::Bytes& packet) {
  if (incoming_packets_.empty()) {
    return false;
  }
  packet = incoming_packets_.front();
  incoming_packets_.pop_front();
  return true;
}

void HostBluezPeripheralInterface::reset_reassembly() {
  reassembly_buffer_.clear();
  expected_total_ = 0;
  received_fragments_ = 0;
  expected_message_len_ = 0;
  reassembly_started_ms_ = 0;
  current_rx_message_id_ = 0;
}

RNS::Bytes HostBluezPeripheralInterface::local_identity_hash() const {
  std::string material = std::string("microReticulum BLE ") + node_label_;
  return RNS::Identity::full_hash(RNS::bytesFromString(material.c_str())).left(16);
}

GVariant* HostBluezPeripheralInterface::get_managed_objects() const {
  GVariantBuilder objects;
  g_variant_builder_init(&objects, G_VARIANT_TYPE("a{oa{sa{sv}}}"));

  GVariantBuilder service_ifaces;
  g_variant_builder_init(&service_ifaces, G_VARIANT_TYPE("a{sa{sv}}"));
  GVariantBuilder service_props;
  g_variant_builder_init(&service_props, G_VARIANT_TYPE("a{sv}"));
  g_variant_builder_add(&service_props, "{sv}", "UUID", g_variant_new_string(SERVICE_UUID));
  g_variant_builder_add(&service_props, "{sv}", "Primary", g_variant_new_boolean(TRUE));
  g_variant_builder_add(&service_ifaces, "{sa{sv}}", "org.bluez.GattService1", &service_props);
  g_variant_builder_add(&objects, "{oa{sa{sv}}}", SERVICE_PATH, &service_ifaces);

  auto add_characteristic = [&](const char* path, const char* uuid, GVariant* flags, const RNS::Bytes& value, bool notifying) {
    GVariantBuilder ifaces;
    g_variant_builder_init(&ifaces, G_VARIANT_TYPE("a{sa{sv}}"));
    GVariantBuilder props;
    g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}"));
    g_variant_builder_add(&props, "{sv}", "UUID", g_variant_new_string(uuid));
    g_variant_builder_add(&props, "{sv}", "Service", g_variant_new_object_path(SERVICE_PATH));
    g_variant_builder_add(&props, "{sv}", "Flags", flags);
    g_variant_builder_add(&props, "{sv}", "Value", bytes_variant(value));
    g_variant_builder_add(&props, "{sv}", "Notifying", g_variant_new_boolean(notifying));
    g_variant_builder_add(&ifaces, "{sa{sv}}", "org.bluez.GattCharacteristic1", &props);
    g_variant_builder_add(&objects, "{oa{sa{sv}}}", path, &ifaces);
  };

  add_characteristic(TX_PATH, TX_UUID, flags_variant({"read", "notify"}), tx_value_, notifying_);
  add_characteristic(RX_PATH, RX_UUID, flags_variant({"write", "write-without-response"}), RNS::Bytes(), false);
  add_characteristic(IDENTITY_PATH, IDENTITY_UUID, flags_variant({"read"}), identity_value_, false);

  return g_variant_new("(a{oa{sa{sv}}})", &objects);
}

GVariant* HostBluezPeripheralInterface::get_property(const char* object_path,
                                                     const char* interface_name,
                                                     const char* property_name) const {
  if (std::strcmp(interface_name, "org.bluez.GattService1") == 0) {
    if (std::strcmp(property_name, "UUID") == 0) {
      return g_variant_new_string(SERVICE_UUID);
    }
    if (std::strcmp(property_name, "Primary") == 0) {
      return g_variant_new_boolean(TRUE);
    }
  }

  if (std::strcmp(interface_name, "org.bluez.GattCharacteristic1") == 0) {
    bool is_tx = std::strcmp(object_path, TX_PATH) == 0;
    bool is_rx = std::strcmp(object_path, RX_PATH) == 0;
    bool is_identity = std::strcmp(object_path, IDENTITY_PATH) == 0;

    if (std::strcmp(property_name, "UUID") == 0) {
      return g_variant_new_string(is_tx ? TX_UUID : (is_rx ? RX_UUID : IDENTITY_UUID));
    }
    if (std::strcmp(property_name, "Service") == 0) {
      return g_variant_new_object_path(SERVICE_PATH);
    }
    if (std::strcmp(property_name, "Flags") == 0) {
      if (is_tx) {
        return flags_variant({"read", "notify"});
      }
      if (is_rx) {
        return flags_variant({"write", "write-without-response"});
      }
      return flags_variant({"read"});
    }
    if (std::strcmp(property_name, "Value") == 0) {
      return bytes_variant(is_identity ? identity_value_ : tx_value_);
    }
    if (std::strcmp(property_name, "Notifying") == 0) {
      return g_variant_new_boolean(is_tx && notifying_);
    }
  }

  if (std::strcmp(interface_name, "org.bluez.LEAdvertisement1") == 0) {
    if (std::strcmp(property_name, "Type") == 0) {
      return g_variant_new_string("peripheral");
    }
    if (std::strcmp(property_name, "ServiceUUIDs") == 0) {
      return flags_variant({SERVICE_UUID});
    }
    if (std::strcmp(property_name, "LocalName") == 0) {
      std::string name = std::string("RNS-") + node_label_;
      return g_variant_new_string(name.c_str());
    }
  }

  return nullptr;
}

void HostBluezPeripheralInterface::handle_method_call(const char* object_path,
                                                      const char* interface_name,
                                                      const char* method_name,
                                                      const char* sender,
                                                      GVariant* parameters,
                                                      GDBusMethodInvocation* invocation) {
  if (std::strcmp(interface_name, "org.freedesktop.DBus.ObjectManager") == 0 &&
      std::strcmp(method_name, "GetManagedObjects") == 0) {
    g_dbus_method_invocation_return_value(invocation, get_managed_objects());
    return;
  }

  if (std::strcmp(interface_name, "org.bluez.LEAdvertisement1") == 0 &&
      std::strcmp(method_name, "Release") == 0) {
    advertisement_registered_ = false;
    g_dbus_method_invocation_return_value(invocation, nullptr);
    return;
  }

  if (std::strcmp(interface_name, "org.bluez.GattCharacteristic1") != 0) {
    g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD, "Unknown method");
    return;
  }

  if (std::strcmp(method_name, "ReadValue") == 0) {
    GVariant* options = nullptr;
    g_variant_get(parameters, "(@a{sv})", &options);
    if (options) {
      g_variant_unref(options);
    }
    if (std::strcmp(object_path, IDENTITY_PATH) == 0) {
      g_dbus_method_invocation_return_value(invocation, g_variant_new("(@ay)", bytes_variant(identity_value_)));
      return;
    }
    if (std::strcmp(object_path, TX_PATH) == 0) {
      g_dbus_method_invocation_return_value(invocation, g_variant_new("(@ay)", bytes_variant(tx_value_)));
      return;
    }
    g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "ReadValue not supported");
    return;
  }

  if (std::strcmp(method_name, "WriteValue") == 0) {
    if (std::strcmp(object_path, RX_PATH) != 0) {
      g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "WriteValue not supported");
      return;
    }
    GVariant* value = nullptr;
    GVariant* options = nullptr;
    g_variant_get(parameters, "(@ay@a{sv})", &value, &options);
    const gchar* device = nullptr;
    if (options && g_variant_lookup(options, "device", "&o", &device) && device) {
      peer_path_ = device;
    } else if (sender && peer_path_.empty()) {
      peer_path_ = sender;
    }
    gsize len = 0;
    const uint8_t* data = static_cast<const uint8_t*>(g_variant_get_fixed_array(value, &len, sizeof(uint8_t)));
    if (data && len > 0) {
      handle_fragment(data, len);
    }
    if (value) {
      g_variant_unref(value);
    }
    if (options) {
      g_variant_unref(options);
    }
    g_dbus_method_invocation_return_value(invocation, nullptr);
    return;
  }

  if (std::strcmp(method_name, "StartNotify") == 0) {
    if (std::strcmp(object_path, TX_PATH) != 0) {
      g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "Notify not supported");
      return;
    }
    notifying_ = true;
    connected_ = true;
    if (sender && peer_path_.empty()) {
      peer_path_ = sender;
    }
    std::printf("BLE linux-peripheral: central subscribed to TX notifications\n");
    g_dbus_method_invocation_return_value(invocation, nullptr);
    return;
  }

  if (std::strcmp(method_name, "StopNotify") == 0) {
    notifying_ = false;
    connected_ = false;
    peer_path_.clear();
    reset_reassembly();
    std::printf("BLE linux-peripheral: central unsubscribed from TX notifications\n");
    g_dbus_method_invocation_return_value(invocation, nullptr);
    return;
  }

  g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD, "Unknown method");
}

void HostBluezPeripheralInterface::method_call(GDBusConnection* connection,
                                               const gchar* sender,
                                               const gchar* object_path,
                                               const gchar* interface_name,
                                               const gchar* method_name,
                                               GVariant* parameters,
                                               GDBusMethodInvocation* invocation,
                                               gpointer user_data) {
  (void)connection;
  (void)sender;
  auto* self = static_cast<HostBluezPeripheralInterface*>(user_data);
  if (!self) {
    g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "No peripheral instance");
    return;
  }
  self->handle_method_call(object_path, interface_name, method_name, sender, parameters, invocation);
}

GVariant* HostBluezPeripheralInterface::property_get(GDBusConnection* connection,
                                                     const gchar* sender,
                                                     const gchar* object_path,
                                                     const gchar* interface_name,
                                                     const gchar* property_name,
                                                     GError** error,
                                                     gpointer user_data) {
  (void)connection;
  (void)sender;
  auto* self = static_cast<HostBluezPeripheralInterface*>(user_data);
  if (!self) {
    g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "No peripheral instance");
    return nullptr;
  }
  GVariant* value = self->get_property(object_path, interface_name, property_name);
  if (!value) {
    g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Unknown property");
  }
  return value;
}

void HostBluezPeripheralInterface::register_application_done(GObject* source_object,
                                                             GAsyncResult* result,
                                                             gpointer user_data) {
  auto* self = static_cast<HostBluezPeripheralInterface*>(user_data);
  if (!self) {
    return;
  }
  GError* error = nullptr;
  GVariant* reply = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), result, &error);
  if (!reply) {
    log_error("BLE linux-peripheral: RegisterApplication failed", error);
    return;
  }
  g_variant_unref(reply);
  self->gatt_registered_ = true;
  std::printf("BLE linux-peripheral: GATT application registered\n");
  self->register_advertisement();
}

void HostBluezPeripheralInterface::register_advertisement_done(GObject* source_object,
                                                               GAsyncResult* result,
                                                               gpointer user_data) {
  auto* self = static_cast<HostBluezPeripheralInterface*>(user_data);
  if (!self) {
    return;
  }
  GError* error = nullptr;
  GVariant* reply = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), result, &error);
  if (!reply) {
    log_error("BLE linux-peripheral: RegisterAdvertisement failed", error);
    return;
  }
  g_variant_unref(reply);
  self->advertisement_registered_ = true;
  std::printf("BLE linux-peripheral: advertising Reticulum service; waiting for central\n");
}
