#include "esphome/core/defines.h"
#ifdef USE_OPENTHREAD
#include "openthread.h"

#include <openthread/cli.h>
#include <openthread/instance.h>
#include <openthread/logging.h>
#include <openthread/netdata.h>
#include <openthread/tasklet.h>

#include <cstring>
#include <utility>

#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"

static const char *const TAG = "openthread";

namespace esphome::openthread {

OpenThreadComponent *global_openthread_component =  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
    nullptr;                                        // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

OpenThreadComponent::OpenThreadComponent() { global_openthread_component = this; }

void OpenThreadComponent::dump_config() {
  ESP_LOGCONFIG(TAG, "Open Thread:");
#if CONFIG_OPENTHREAD_FTD
  ESP_LOGCONFIG(TAG, "  Device Type: FTD");
#elif CONFIG_OPENTHREAD_MTD
  ESP_LOGCONFIG(TAG, "  Device Type: MTD");
  // TBD: Synchronized Sleepy End Device
  if (this->poll_period_ > 0) {
    ESP_LOGCONFIG(TAG, "  Device is configured as Sleepy End Device (SED)");
    uint32_t duration = this->poll_period_ / 1000;
    ESP_LOGCONFIG(TAG, "  Poll Period: %" PRIu32 "s", duration);
  } else {
    ESP_LOGCONFIG(TAG, "  Device is configured as Minimal End Device (MED)");
  }
#endif
  if (this->output_power_.has_value()) {
    ESP_LOGCONFIG(TAG, "  Output power: %" PRId8 "dBm", *this->output_power_);
  }
}

void OpenThreadComponent::on_state_changed(otChangedFlags flags, void *context) {
  if (flags & OT_CHANGED_THREAD_ROLE) {
    auto *self = static_cast<OpenThreadComponent *>(context);
    // This runs on the OpenThread task thread with the OT lock held,
    // so we can safely call otThreadGetDeviceRole directly.
    otInstance *instance = self->get_openthread_instance_();
    otDeviceRole role = otThreadGetDeviceRole(instance);
    self->connected_ = role >= OT_DEVICE_ROLE_CHILD;
  }
}

// Gets the off-mesh routable address
std::optional<otIp6Address> OpenThreadComponent::get_omr_address() {
  InstanceLock lock = InstanceLock::acquire();
  return this->get_omr_address_(lock);
}

std::optional<otIp6Address> OpenThreadComponent::get_omr_address_(InstanceLock &lock) {
  otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT;
  otInstance *instance = nullptr;

  instance = lock.get_instance();

  otBorderRouterConfig config;
  if (otNetDataGetNextOnMeshPrefix(instance, &iterator, &config) != OT_ERROR_NONE) {
    return std::nullopt;
  }

  const otIp6Prefix *omr_prefix = &config.mPrefix;
  const otNetifAddress *unicast_addresses = otIp6GetUnicastAddresses(instance);
  for (const otNetifAddress *addr = unicast_addresses; addr; addr = addr->mNext) {
    const otIp6Address *local_ip = &addr->mAddress;
    if (otIp6PrefixMatch(&omr_prefix->mPrefix, local_ip)) {
      return *local_ip;
    }
  }
  return {};
}

void OpenThreadComponent::defer_factory_reset_external_callback() {
  ESP_LOGD(TAG, "Defer factory_reset_external_callback_");
  this->defer([this]() { this->factory_reset_external_callback_(); });
}

void OpenThreadSrpComponent::srp_callback(otError err, const otSrpClientHostInfo *host_info,
                                          const otSrpClientService *services,
                                          const otSrpClientService *removed_services, void *context) {
  if (err != 0) {
    ESP_LOGW(TAG, "SRP client reported an error: %s", otThreadErrorToString(err));
    for (const otSrpClientHostInfo *host = host_info; host; host = nullptr) {
      ESP_LOGW(TAG, "  Host: %s", host->mName);
    }
    for (const otSrpClientService *service = services; service; service = service->mNext) {
      ESP_LOGW(TAG, "  Service: %s", service->mName);
    }
  }
}

void OpenThreadSrpComponent::srp_start_callback(const otSockAddr *server_socket_address, void *context) {
  ESP_LOGI(TAG, "SRP client has started");
}

void OpenThreadSrpComponent::srp_factory_reset_callback(otError err, const otSrpClientHostInfo *host_info,
                                                        const otSrpClientService *services,
                                                        const otSrpClientService *removed_services, void *context) {
  OpenThreadComponent *obj = (OpenThreadComponent *) context;
  if (err == OT_ERROR_NONE && removed_services != NULL && host_info != NULL &&
      host_info->mState == OT_SRP_CLIENT_ITEM_STATE_REMOVED) {
    ESP_LOGD(TAG, "Successful Removal SRP Host and Services");
  } else if (err != OT_ERROR_NONE) {
    // Handle other SRP client events or errors
    ESP_LOGW(TAG, "SRP client event/error: %s", otThreadErrorToString(err));
  }
  obj->defer_factory_reset_external_callback();
}

void OpenThreadSrpComponent::setup() {
  otError error;
  InstanceLock lock = InstanceLock::acquire();
  otInstance *instance = lock.get_instance();

  otSrpClientSetCallback(instance, OpenThreadSrpComponent::srp_callback, nullptr);

  // set the host name
  uint16_t size;
  char *existing_host_name = otSrpClientBuffersGetHostNameString(instance, &size);
  const auto &host_name = App.get_name();
  uint16_t host_name_len = host_name.size();
  if (host_name_len >= size) {
    ESP_LOGW(TAG, "Hostname is too long, choose a shorter project name");
    return;
  }
  memset(existing_host_name, 0, size);
  memcpy(existing_host_name, host_name.c_str(), host_name_len);

  error = otSrpClientSetHostName(instance, existing_host_name);
  if (error != 0) {
    ESP_LOGW(TAG, "Could not set host name");
    return;
  }

  error = otSrpClientEnableAutoHostAddress(instance);
  if (error != 0) {
    ESP_LOGW(TAG, "Could not enable auto host address");
    return;
  }

  // Get mdns services and copy their data (strdup on ESP32, pool_alloc_ on Zephyr)
  const auto &mdns_services = this->mdns_->get_services();
  ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", mdns_services.size());
  for (const auto &service : mdns_services) {
    otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance);
    if (!entry) {
      ESP_LOGW(TAG, "Failed to allocate service entry");
      continue;
    }

    // Set service name
    char *string = otSrpClientBuffersGetServiceEntryServiceNameString(entry, &size);
    std::string full_service = std::string(MDNS_STR_ARG(service.service_type)) + "." + MDNS_STR_ARG(service.proto);
    if (full_service.size() >= size) {
      ESP_LOGW(TAG, "Service name too long: %s", full_service.c_str());
      continue;
    }
    memcpy(string, full_service.c_str(), full_service.size() + 1);

    // Set instance name (using host_name)
    string = otSrpClientBuffersGetServiceEntryInstanceNameString(entry, &size);
    if (host_name_len >= size) {
      ESP_LOGW(TAG, "Instance name too long: %s", host_name.c_str());
      continue;
    }
    memset(string, 0, size);
    memcpy(string, host_name.c_str(), host_name_len);

    // Set port
    entry->mService.mPort = service.port.value();

    otDnsTxtEntry *txt_entries =
        reinterpret_cast<otDnsTxtEntry *>(this->pool_alloc_(sizeof(otDnsTxtEntry) * service.txt_records.size()));
    // Set TXT records
    entry->mService.mNumTxtEntries = service.txt_records.size();
    for (size_t i = 0; i < service.txt_records.size(); i++) {
      const auto &txt = service.txt_records[i];
      // Value is either a compile-time string literal in flash or a pointer to dynamic_txt_values_
      // OpenThread SRP client expects the data to persist, so we copy it
      const char *value_str = MDNS_STR_ARG(txt.value);
      txt_entries[i].mKey = MDNS_STR_ARG(txt.key);
#ifndef USE_ZEPHYR
      txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(strdup(value_str));
      txt_entries[i].mValueLength = strlen(value_str);
#else
      // strdup is not available on zephyr
      // https:// github.com/zephyrproject-rtos/zephyr/issues/22464
      size_t value_len = strlen(value_str);
      char *value_copy = reinterpret_cast<char *>(this->pool_alloc_(value_len + 1));
      memcpy(value_copy, value_str, value_len + 1);
      txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(value_copy);
      txt_entries[i].mValueLength = value_len;
#endif
    }
    entry->mService.mTxtEntries = txt_entries;
    entry->mService.mNumTxtEntries = service.txt_records.size();

    // Add service
    error = otSrpClientAddService(instance, &entry->mService);
    if (error != OT_ERROR_NONE) {
      ESP_LOGW(TAG, "Failed to add service: %s", otThreadErrorToString(error));
    }
    ESP_LOGD(TAG, "Added service: %s", full_service.c_str());
  }

  otSrpClientEnableAutoStartMode(instance, OpenThreadSrpComponent::srp_start_callback, nullptr);
  ESP_LOGD(TAG, "Finished SRP setup");
}

void *OpenThreadSrpComponent::pool_alloc_(size_t size) {
  uint8_t *ptr = new uint8_t[size];
  this->memory_pool_.emplace_back(std::unique_ptr<uint8_t[]>(ptr));
  return ptr;
}

void OpenThreadSrpComponent::set_mdns(esphome::mdns::MDNSComponent *mdns) { this->mdns_ = mdns; }

bool OpenThreadComponent::teardown() {
  if (!this->teardown_started_) {
    this->teardown_started_ = true;
    ESP_LOGD(TAG, "Clear Srp");
    auto lock = InstanceLock::try_acquire(100);
    if (!lock) {
      ESP_LOGW(TAG, "Failed to acquire OpenThread lock during teardown, leaking memory");
      return true;
    }
    otInstance *instance = lock.get_instance();
    otSrpClientClearHostAndServices(instance);
    otSrpClientBuffersFreeAllServices(instance);
    global_openthread_component = nullptr;
    ESP_LOGD(TAG, "Exit main loop ");
    int error = this->openthread_stop_();
    if (error != 0) {
      ESP_LOGW(TAG, "Failed attempt to stop main loop %d", error);
      this->teardown_complete_ = true;
    }
  }
  return this->teardown_complete_;
}

void OpenThreadComponent::on_factory_reset(std::function<void()> callback) {
  this->factory_reset_external_callback_ = std::move(callback);
  ESP_LOGD(TAG, "Start Removal SRP Host and Services");
  otError error;
  InstanceLock lock = InstanceLock::acquire();
  otInstance *instance = lock.get_instance();
  otSrpClientSetCallback(instance, OpenThreadSrpComponent::srp_factory_reset_callback, this);
  error = otSrpClientRemoveHostAndServices(instance, true, true);
  if (error != OT_ERROR_NONE) {
    ESP_LOGW(TAG, "Failed to Remove SRP Host and Services");
    return;
  }
  ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services");
}

void OpenThreadComponent::apply_linkmode_(otInstance *instance) {
  otLinkModeConfig link_mode_config{};
#if CONFIG_OPENTHREAD_FTD
  link_mode_config.mRxOnWhenIdle = true;
  link_mode_config.mDeviceType = true;
  link_mode_config.mNetworkData = true;
#elif CONFIG_OPENTHREAD_MTD
  if (this->poll_period_ > 0) {
    if (otLinkSetPollPeriod(instance, this->poll_period_) != OT_ERROR_NONE) {
      ESP_LOGE(TAG, "Failed to set pollperiod");
    }
    ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, otLinkGetPollPeriod(instance));
  }
  link_mode_config.mRxOnWhenIdle = this->poll_period_ == 0;
  link_mode_config.mDeviceType = false;
  link_mode_config.mNetworkData = false;
#endif

  if (otThreadSetLinkMode(instance, link_mode_config) != OT_ERROR_NONE) {
    ESP_LOGE(TAG, "Failed to set linkmode");
  }
#ifdef ESPHOME_LOG_HAS_DEBUG  // Fetch link mode from OT only when DEBUG
  link_mode_config = otThreadGetLinkMode(instance);
  ESP_LOGD(TAG, "Link Mode Device Type: %s, Network Data: %s, RX On When Idle: %s",
           TRUEFALSE(link_mode_config.mDeviceType), TRUEFALSE(link_mode_config.mNetworkData),
           TRUEFALSE(link_mode_config.mRxOnWhenIdle));
#endif
}

}  // namespace esphome::openthread
#endif
