#include "zigbee_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52)
#include "esphome/core/log.h"
#include <zephyr/settings/settings.h>
#include <zephyr/storage/flash_map.h>
#include "esphome/core/hal.h"
#include "esphome/core/wake.h"

extern "C" {
#include <zboss_api.h>
#include <zboss_api_addons.h>
#include <zb_nrf_platform.h>
#include <zigbee/zigbee_app_utils.h>
#include <zb_error_to_string.h>
}

namespace esphome::zigbee {

static const char *const TAG = "zigbee";

ZigbeeComponent *global_zigbee = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

const uint8_t IEEE_ADDR_BUF_SIZE = 17;

void ZigbeeComponent::zboss_signal_handler_esphome(zb_bufid_t bufid) {
  zb_zdo_app_signal_hdr_t *sig_hndler = nullptr;
  zb_zdo_app_signal_type_t sig = zb_get_app_signal(bufid, &sig_hndler);
  zb_ret_t status = ZB_GET_APP_SIGNAL_STATUS(bufid);

  switch (sig) {
    case ZB_ZDO_SIGNAL_SKIP_STARTUP:
      ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_SKIP_STARTUP, status: %d", status);
      break;
    case ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY:
      ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY, status: %d", status);
      break;
    case ZB_ZDO_SIGNAL_LEAVE:
      ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_LEAVE, status: %d", status);
      break;
    case ZB_BDB_SIGNAL_DEVICE_REBOOT:
      ESP_LOGD(TAG, "ZB_BDB_SIGNAL_DEVICE_REBOOT, status: %d", status);
      if (status == RET_OK) {
        on_join_(false);
      }
      break;
    case ZB_BDB_SIGNAL_STEERING:
      break;
    case ZB_COMMON_SIGNAL_CAN_SLEEP:
      ESP_LOGV(TAG, "ZB_COMMON_SIGNAL_CAN_SLEEP, status: %d", status);
      break;
    case ZB_BDB_SIGNAL_DEVICE_FIRST_START:
      ESP_LOGD(TAG, "ZB_BDB_SIGNAL_DEVICE_FIRST_START, status: %d", status);
      break;
    case ZB_NLME_STATUS_INDICATION:
      ESP_LOGD(TAG, "ZB_NLME_STATUS_INDICATION, status: %d", status);
      break;
    case ZB_BDB_SIGNAL_TC_REJOIN_DONE:
      ESP_LOGD(TAG, "ZB_BDB_SIGNAL_TC_REJOIN_DONE, status: %d", status);
      break;
    default:
      ESP_LOGD(TAG, "zboss_signal_handler sig: %d, status: %d", sig, status);
      break;
  }

  auto before = millis();
  auto err = zigbee_default_signal_handler(bufid);
  if (err != RET_OK) {
    ESP_LOGE(TAG, "Zigbee_default_signal_handler ERROR %u [%s]", err, zb_error_to_string_get(err));
  }

  if (sig == ZB_COMMON_SIGNAL_CAN_SLEEP) {
    this->sleep_remainder_ += millis() - before;
    uint32_t seconds = this->sleep_remainder_ / 1000;
    this->sleep_remainder_ -= seconds * 1000;
    this->sleep_time_ += seconds;
  }

  switch (sig) {
    case ZB_BDB_SIGNAL_STEERING:
      ESP_LOGD(TAG, "ZB_BDB_SIGNAL_STEERING, status: %d", status);
      if (status == RET_OK) {
        zb_ext_pan_id_t extended_pan_id;
        char ieee_addr_buf[IEEE_ADDR_BUF_SIZE] = {0};
        int addr_len;

        zb_get_extended_pan_id(extended_pan_id);
        addr_len = ieee_addr_to_str(ieee_addr_buf, sizeof(ieee_addr_buf), extended_pan_id);

        for (int i = 0; i < addr_len; ++i) {
          if (ieee_addr_buf[i] != '0') {
            on_join_(true);
            break;
          }
        }
      }
      break;
  }

  /* All callbacks should either reuse or free passed buffers.
   * If bufid == 0, the buffer is invalid (not passed).
   */
  if (bufid) {
    zb_buf_free(bufid);
  }
}

void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
  zb_zcl_device_callback_param_t *p_device_cb_param = ZB_BUF_GET_PARAM(bufid, zb_zcl_device_callback_param_t);
  zb_zcl_device_callback_id_t device_cb_id = p_device_cb_param->device_cb_id;
  zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id;
  zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id;
  auto endpoint = p_device_cb_param->endpoint;

  ESP_LOGI(TAG, "%s id %hd, cluster_id %d, attr_id %d, endpoint: %d", __func__, device_cb_id, cluster_id, attr_id,
           endpoint);

  /* Set default response value. */
  p_device_cb_param->status = RET_OK;

  esphome::wake_loop_threadsafe();

  // endpoints are enumerated from 1
  if (global_zigbee->callbacks_.size() >= endpoint) {
    const auto &cb = global_zigbee->callbacks_[endpoint - 1];
    if (cb) {
      cb(bufid);
      return;
    }
  }
  p_device_cb_param->status = RET_NOT_IMPLEMENTED;
}

void ZigbeeComponent::on_join_(bool factory_new) {
  this->defer([this, factory_new]() {
    ESP_LOGD(TAG, "Joined the network");
    this->join_cb_.call(factory_new);
  });
}

#ifdef USE_ZIGBEE_WIPE_ON_BOOT
void ZigbeeComponent::erase_flash_(int area) {
  const struct flash_area *fap;
  flash_area_open(area, &fap);
  flash_area_erase(fap, 0, fap->fa_size);
  flash_area_close(fap);
}
#endif

void ZigbeeComponent::setup() {
  global_zigbee = this;
  auto err = settings_subsys_init();
  if (err) {
    ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err);
    return;
  }

#ifdef USE_ZIGBEE_WIPE_ON_BOOT
  bool wipe = true;
#ifdef USE_ZIGBEE_WIPE_ON_BOOT_MAGIC
  // unique hash to store preferences for this component
  uint32_t hash = 88498616UL;
  uint32_t wipe_value = 0;
  auto wipe_pref = global_preferences->make_preference<uint32_t>(hash, true);
  if (wipe_pref.load(&wipe_value)) {
    wipe = wipe_value != USE_ZIGBEE_WIPE_ON_BOOT_MAGIC;
    ESP_LOGD(TAG, "Wipe value in preferences %u, in firmware %u", wipe_value, USE_ZIGBEE_WIPE_ON_BOOT_MAGIC);
  }
#endif
  if (wipe) {
    erase_flash_(FIXED_PARTITION_ID(ZBOSS_NVRAM));
    erase_flash_(FIXED_PARTITION_ID(ZBOSS_PRODUCT_CONFIG));
    erase_flash_(FIXED_PARTITION_ID(SETTINGS_STORAGE));
#ifdef USE_ZIGBEE_WIPE_ON_BOOT_MAGIC
    wipe_value = USE_ZIGBEE_WIPE_ON_BOOT_MAGIC;
    wipe_pref.save(&wipe_value);
#endif
  }
#endif

  ZB_ZCL_REGISTER_DEVICE_CB(zcl_device_cb);
  err = settings_load();
  if (err) {
    ESP_LOGE(TAG, "Cannot load settings, err: %d", err);
    return;
  }
#ifdef CONFIG_ZIGBEE_ROLE_END_DEVICE
  zigbee_configure_sleepy_behavior(this->sleepy_);
#endif
  zigbee_enable();
}

#ifdef ESPHOME_LOG_HAS_CONFIG
static const char *role() {
  switch (zb_get_network_role()) {
    case ZB_NWK_DEVICE_TYPE_COORDINATOR:
      return "coordinator";
    case ZB_NWK_DEVICE_TYPE_ROUTER:
      return "router";
    case ZB_NWK_DEVICE_TYPE_ED:
      return "end device";
  }
  return "unknown";
}

static const char *get_wipe_on_boot() {
#ifdef USE_ZIGBEE_WIPE_ON_BOOT
#ifdef USE_ZIGBEE_WIPE_ON_BOOT_MAGIC
  return "ONCE";
#else
  return "YES";
#endif
#else
  return "NO";
#endif
}
#endif

void ZigbeeComponent::dump_config() {
  char ieee_addr_buf[IEEE_ADDR_BUF_SIZE] = {0};
  zb_ieee_addr_t addr;
  zb_get_long_address(addr);
  ieee_addr_to_str(ieee_addr_buf, sizeof(ieee_addr_buf), addr);
  zb_ext_pan_id_t extended_pan_id;
  char extended_pan_id_buf[IEEE_ADDR_BUF_SIZE] = {0};
  zb_get_extended_pan_id(extended_pan_id);
  ieee_addr_to_str(extended_pan_id_buf, sizeof(extended_pan_id_buf), extended_pan_id);
  ESP_LOGCONFIG(TAG,
                "Zigbee\n"
                "  Wipe on boot: %s\n"
                "  Device is joined to the network: %s\n"
                "  Sleep time: %us\n"
                "  RX ON when idle: %s\n"
                "  Current channel: %d\n"
                "  Current page: %d\n"
                "  Sleep threshold: %ums\n"
                "  Role: %s\n"
                "  Long addr: 0x%s\n"
                "  Short addr: 0x%04X\n"
                "  Long pan id: 0x%s\n"
                "  Short pan id: 0x%04X",
                get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, YESNO(zb_get_rx_on_when_idle()),
                zb_get_current_channel(), zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf,
                zb_get_short_address(), extended_pan_id_buf, zb_get_pan_id());
  dump_reporting_();
}

static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) {
  ESP_LOGD(TAG, "Force zboss scheduler to wake and send attribute report");
  zb_buf_free(bufid);
}

void ZigbeeComponent::force_report() { this->force_report_ = true; }

void ZigbeeComponent::loop() {
  if (this->force_report_) {
    this->force_report_ = false;
    zb_buf_get_out_delayed_ext(send_attribute_report, 0, 0);
  }
}

void ZigbeeComponent::factory_reset() {
  ESP_LOGD(TAG, "Factory reset");
  ZB_SCHEDULE_APP_CALLBACK(zb_bdb_reset_via_local_action, 0);
}

static void log_reporting_info(zb_zcl_reporting_info_t *rep_info) {
  auto now = millis();
  ESP_LOGD(TAG, "Reporting: endpoint %d, cluster_id 0x%04X, attr_id 0x%04X, flags 0x%02X, report in %ums", rep_info->ep,
           rep_info->cluster_id, rep_info->attr_id, rep_info->flags,
           ZB_ZCL_GET_REPORTING_FLAG(rep_info, ZB_ZCL_REPORT_TIMER_STARTED)
               ? ZB_TIME_BEACON_INTERVAL_TO_MSEC(rep_info->run_time) - now
               : 0);
  ESP_LOGD(TAG, "  min_interval %ds, max_interval %ds, def_min_interval %ds, def_max_interval %ds",
           rep_info->u.send_info.min_interval, rep_info->u.send_info.max_interval,
           rep_info->u.send_info.def_min_interval, rep_info->u.send_info.def_max_interval);
}

void ZigbeeComponent::dump_reporting_() {
#ifdef ESPHOME_LOG_HAS_VERBOSE
  for (zb_uint8_t j = 0; j < ZCL_CTX().device_ctx->ep_count; j++) {
    if (ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info) {
      zb_zcl_reporting_info_t *rep_info = ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info;
      for (zb_uint8_t i = 0; i < ZCL_CTX().device_ctx->ep_desc_list[j]->rep_info_count; i++) {
        log_reporting_info(rep_info);
        rep_info++;
      }
    }
  }
#endif
}

void ZigbeeComponent::after_reporting_info(zb_zcl_configure_reporting_req_t *config_rep_req,
                                           zb_zcl_attr_addr_info_t *attr_addr_info) {
#ifdef ESPHOME_LOG_HAS_DEBUG
  auto *rep_info =
      zb_zcl_find_reporting_info_manuf(attr_addr_info->src_ep, attr_addr_info->cluster_id, attr_addr_info->cluster_role,
                                       config_rep_req->attr_id, attr_addr_info->manuf_code);
  if (rep_info == nullptr) {
    ESP_LOGE(TAG,
             "Failed to resolve reporting info (src_ep=%u cluster_id=0x%04x role=%u attr_id=0x%04x manuf_code=0x%04x)",
             attr_addr_info->src_ep, attr_addr_info->cluster_id, attr_addr_info->cluster_role, config_rep_req->attr_id,
             attr_addr_info->manuf_code);
    return;
  }
  log_reporting_info(rep_info);
#endif
}

}  // namespace esphome::zigbee

extern "C" {
void zboss_signal_handler(zb_uint8_t param) { esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(param); }
void zb_osif_serial_put_bytes(const zb_uint8_t *buf, zb_short_t len) {
  (void) buf;
  (void) len;
}
void zb_osif_serial_flush() {}
void zb_osif_serial_init() {}

// NOLINTBEGIN(readability-identifier-naming,bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
extern zb_ret_t __real_zb_zcl_put_reporting_info_from_req(zb_zcl_configure_reporting_req_t *config_rep_req,
                                                          zb_zcl_attr_addr_info_t *attr_addr_info);

zb_ret_t __wrap_zb_zcl_put_reporting_info_from_req(zb_zcl_configure_reporting_req_t *config_rep_req,
                                                   zb_zcl_attr_addr_info_t *attr_addr_info) {
  zb_ret_t ret = __real_zb_zcl_put_reporting_info_from_req(config_rep_req, attr_addr_info);
  esphome::zigbee::global_zigbee->after_reporting_info(config_rep_req, attr_addr_info);
  return ret;
}
// NOLINTEND(readability-identifier-naming,bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
}
#endif
