#pragma once

#ifdef USE_ESP32

#include "espnow_err.h"

#include <cstdint>
#include <cstring>
#include <functional>
#include <memory>
#include <vector>

#include <esp_err.h>
#include <esp_idf_version.h>
#include <esp_now.h>

namespace esphome::espnow {

static const uint8_t ESPNOW_BROADCAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static const uint8_t ESPNOW_MULTICAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE};

// Maximum payload this component sends and receives, from the
// ``max_payload_size`` option. The radio stack speaks ESP-NOW v2 regardless
// (negotiated per peer); payloads beyond the v1 limit (250 bytes) are opt-in
// because the packet pools are statically sized from this, so their RAM cost
// is proportional (~8 KB at 250 bytes, ~44 KB at the v2 limit of 1470).
#ifndef USE_ESPNOW_MAX_PAYLOAD_SIZE
#define USE_ESPNOW_MAX_PAYLOAD_SIZE ESP_NOW_MAX_DATA_LEN
#endif
static constexpr uint16_t ESPNOW_MAX_DATA_LEN = USE_ESPNOW_MAX_PAYLOAD_SIZE;
#ifdef ESP_NOW_MAX_DATA_LEN_V2
static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN_V2,
              "espnow max_payload_size cannot exceed the ESP-NOW v2 frame limit");
#else
static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN,
              "espnow max_payload_size beyond 250 bytes requires an ESP-IDF with ESP-NOW v2 support (5.4+)");
#endif

struct WifiPacketRxControl {
  int8_t rssi;         // Received Signal Strength Indicator (RSSI) of packet, unit: dBm
  uint32_t timestamp;  // Timestamp in microseconds when the packet was received, precise only if modem sleep or
                       // light sleep is not enabled
};

struct ESPNowRecvInfo {
  uint8_t src_addr[ESP_NOW_ETH_ALEN]; /**< Source address of ESPNOW packet */
  uint8_t des_addr[ESP_NOW_ETH_ALEN]; /**< Destination address of ESPNOW packet */
  wifi_pkt_rx_ctrl_t *rx_ctrl;        /**< Rx control info of ESPNOW packet */
};

using send_callback_t = std::function<void(esp_err_t)>;

class ESPNowPacket {
 public:
  // NOLINTNEXTLINE(readability-identifier-naming)
  enum esp_now_packet_type_t : uint8_t {
    RECEIVED,
    SENT,
  };

  // Constructor for received data
  ESPNowPacket(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
    this->init_received_data_(info, data, size);
  };

#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
  // Constructor for sent data
  ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) {
    this->init_sent_data_(info->src_addr, status);
  }
#else
  // Constructor for sent data
  ESPNowPacket(const uint8_t *mac_addr, esp_now_send_status_t status) { this->init_sent_data_(mac_addr, status); }
#endif

  // Default constructor for pre-allocation in pool
  ESPNowPacket() {}

  void release() {}

  void load_received_data(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
    this->type_ = RECEIVED;
    this->init_received_data_(info, data, size);
  }

  void load_sent_data(const uint8_t *mac_addr, esp_now_send_status_t status) {
    this->type_ = SENT;
    this->init_sent_data_(mac_addr, status);
  }

  // Disable copy to prevent double-delete
  ESPNowPacket(const ESPNowPacket &) = delete;
  ESPNowPacket &operator=(const ESPNowPacket &) = delete;

  union {
    // NOLINTNEXTLINE(readability-identifier-naming)
    struct received_data {
      ESPNowRecvInfo info;                // Information about the received packet
      uint8_t data[ESPNOW_MAX_DATA_LEN];  // Data received in the packet
      uint16_t size;                      // Size of the received data
      WifiPacketRxControl rx_ctrl;        // Status of the received packet
    } receive;

    // NOLINTNEXTLINE(readability-identifier-naming)
    struct sent_data {
      uint8_t address[ESP_NOW_ETH_ALEN];
      esp_now_send_status_t status;
    } sent;
  } packet_;

  esp_now_packet_type_t type_;

  esp_now_packet_type_t type() const { return this->type_; }
  const ESPNowRecvInfo &get_receive_info() const { return this->packet_.receive.info; }

 private:
  void init_received_data_(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
    memcpy(this->packet_.receive.info.src_addr, info->src_addr, ESP_NOW_ETH_ALEN);
    memcpy(this->packet_.receive.info.des_addr, info->des_addr, ESP_NOW_ETH_ALEN);
    memcpy(this->packet_.receive.data, data, size);
    this->packet_.receive.size = size;

    this->packet_.receive.rx_ctrl.rssi = info->rx_ctrl->rssi;
    this->packet_.receive.rx_ctrl.timestamp = info->rx_ctrl->timestamp;

    this->packet_.receive.info.rx_ctrl = reinterpret_cast<wifi_pkt_rx_ctrl_t *>(&this->packet_.receive.rx_ctrl);
  }

  void init_sent_data_(const uint8_t *mac_addr, esp_now_send_status_t status) {
    memcpy(this->packet_.sent.address, mac_addr, ESP_NOW_ETH_ALEN);
    this->packet_.sent.status = status;
  }
};

class ESPNowSendPacket {
 public:
  ESPNowSendPacket(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &&callback)
      : callback_(callback) {
    this->init_data_(peer_address, payload, size);
  }
  ESPNowSendPacket(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
    this->init_data_(peer_address, payload, size);
  }

  // Default constructor for pre-allocation in pool
  ESPNowSendPacket() {}

  void release() {}

  // Disable copy to prevent double-delete
  ESPNowSendPacket(const ESPNowSendPacket &) = delete;
  ESPNowSendPacket &operator=(const ESPNowSendPacket &) = delete;

  void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &callback) {
    this->init_data_(peer_address, payload, size);
    this->callback_ = callback;
  }

  void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
    this->init_data_(peer_address, payload, size);
    this->callback_ = nullptr;  // Reset callback
  }

  uint8_t address_[ESP_NOW_ETH_ALEN]{0};  // MAC address of the peer to send the packet to
  uint8_t data_[ESPNOW_MAX_DATA_LEN]{0};  // Data to send
  uint16_t size_{0};                      // Size of the data to send, must be <= ESPNOW_MAX_DATA_LEN
  send_callback_t callback_{nullptr};     // Callback to call when the send operation is complete

 private:
  void init_data_(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
    memcpy(this->address_, peer_address, ESP_NOW_ETH_ALEN);
    if (size > ESPNOW_MAX_DATA_LEN) {
      this->size_ = 0;
      return;
    }
    this->size_ = size;
    memcpy(this->data_, payload, this->size_);
  }
};

}  // namespace esphome::espnow

#endif  // USE_ESP32
