#include "TBeamSupremeBleInterface.h"

#include <Arduino.h>
#include <Destination.h>
#include <Identity.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>

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

static RNS::Reticulum reticulum({RNS::Type::NONE});
static RNS::Interface ble_interface({RNS::Type::NONE});
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 String peer_label;
static bool have_peer = false;
static bool link_active = false;
static bool link_attempted = false;
static TBeamSupremeBleInterface* ble_impl = nullptr;
static String node_label;

static String default_node_label() {
  uint64_t mac = ESP.getEfuseMac();
  char label[24];
  snprintf(label, sizeof(label), "Node-%012llX", (unsigned long long)(mac & 0xFFFFFFFFFFFFULL));
  return String(label);
}

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

static void on_link_packet(const RNS::Bytes& data, const RNS::Packet& packet) {
  (void)packet;
  Serial.printf("RX LINK BLE: %s\r\n", data.toString().c_str());
}

static void on_link_closed(RNS::Link& link) {
  (void)link;
  Serial.println("LINK CLOSED");
  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;
  Serial.println("LINK ACTIVE: initiator link established");
}

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;
  Serial.println("RX LINK: inbound link established");
}

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

  void received_announce(const RNS::Bytes& destination_hash,
                         const RNS::Identity& announced_identity,
                         const RNS::Bytes& app_data) override {
    String label = app_data ? String(app_data.toString().c_str()) : String("(no label)");
    if (label == node_label) {
      return;
    }
    if (!announced_identity) {
      Serial.printf("RX ANNOUNCE ignored: missing identity for hash=%s\r\n",
                    destination_hash.toHex().c_str());
      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;

    Serial.printf("RX ANNOUNCE: label=%s hash=%s\r\n",
                  peer_label.c_str(), peer_hash.toHex().c_str());
  }
};

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

static void print_config() {
  Serial.printf("Node=%s\r\n", node_label.c_str());
  Serial.printf("BLE role=%s service=37145b00-442d-4a94-917f-8f42c5da28e3\r\n",
                ble_impl ? ble_impl->role_name() : "?");
}

static void send_announce() {
  if (!inbound_destination) {
    return;
  }
  Serial.printf("TX ANNOUNCE: %s\r\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;
  }

  Serial.printf("TX LINKREQUEST: opening link to %s\r\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 setup_reticulum() {
  microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()};
  filesystem.init();
  RNS::Utilities::OS::register_filesystem(filesystem);

  ble_impl = new TBeamSupremeBleInterface(node_label);
  ble_interface = ble_impl;
  ble_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
  RNS::Transport::register_interface(ble_interface);
  ble_interface.start();

  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);

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

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 5000) {
    delay(100);
  }
  delay(250);

  Serial.println();
  RNS::loglevel(RNS::LOG_NOTICE);
  node_label = default_node_label();
  setup_reticulum();
  Serial.println("Exercise 304: microReticulum BLE dual-role Link ping-pong");
  print_config();
  Serial.println("microReticulum ready");
}

void loop() {
  reticulum.loop();

  static uint32_t next_announce_ms = 0;
  static uint32_t next_link_tx_ms = 0;
  static uint32_t next_wait_log_ms = 0;
  static uint32_t iter = 0;
  uint32_t now = millis();

  if (ble_impl && !ble_impl->connected()) {
    if (next_wait_log_ms == 0 || (int32_t)(now - next_wait_log_ms) >= 0) {
      next_wait_log_ms = now + 10000;
      Serial.printf("BLE %s waiting for peer\r\n", ble_impl->role_name());
    }
    delay(5);
    return;
  }

  if (next_announce_ms == 0) {
    uint32_t offset = 700 + ((uint32_t)node_label.charAt(node_label.length() - 1) % 5) * 400;
    next_announce_ms = now + offset;
  }
  if (!link_active && (int32_t)(now - next_announce_ms) >= 0) {
    next_announce_ms = now + 15000;
    send_announce();
  }

  maybe_open_link();

  if (link_active && next_link_tx_ms == 0) {
    uint32_t offset = should_initiate_link_to(peer_label) ? 1200 : 2400;
    next_link_tx_ms = now + offset;
  }
  if (link_active && (int32_t)(now - next_link_tx_ms) >= 0) {
    next_link_tx_ms = now + 3000;

    String message = node_label + " -> " + peer_label + " iter=" + String(iter++);
    Serial.printf("TX LINK BLE: %s\r\n", message.c_str());
    RNS::Packet(active_link, RNS::bytesFromString(message.c_str())).send();
  }

  delay(5);
}

int _write(int file, char* ptr, int len) {
  (void)file;
  int wrote = Serial.write(ptr, len);
  Serial.flush();
  return wrote;
}
