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

namespace esphome {
namespace mqtt {

static const char *TAG = "mqtt.component";

void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; }

std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const {
  std::string sanitized_name = sanitize_string_whitelist(App.get_name(), HOSTNAME_CHARACTER_WHITELIST);
  return discovery_info.prefix + "/" + this->component_type() + "/" + sanitized_name + "/" +
         this->get_default_object_id_() + "/config";
}

std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
  return global_mqtt_client->get_topic_prefix() + "/" + this->component_type() + "/" + this->get_default_object_id_() +
         "/" + suffix;
}

const std::string MQTTComponent::get_state_topic_() const {
  if (this->custom_state_topic_.empty())
    return this->get_default_topic_for_("state");
  return this->custom_state_topic_;
}

const std::string MQTTComponent::get_command_topic_() const {
  if (this->custom_command_topic_.empty())
    return this->get_default_topic_for_("command");
  return this->custom_command_topic_;
}

bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
  if (topic.empty())
    return false;
  return global_mqtt_client->publish(topic, payload, 0, this->retain_);
}

bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) {
  if (topic.empty())
    return false;
  return global_mqtt_client->publish_json(topic, f, 0, this->retain_);
}

bool MQTTComponent::send_discovery_() {
  const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();

  if (discovery_info.clean) {
    ESP_LOGV(TAG, "'%s': Cleaning discovery...", this->friendly_name().c_str());
    return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, 0, true);
  }

  ESP_LOGV(TAG, "'%s': Sending discovery...", this->friendly_name().c_str());

  return global_mqtt_client->publish_json(
      this->get_discovery_topic_(discovery_info),
      [this](JsonObject &root) {
        SendDiscoveryConfig config;
        config.state_topic = true;
        config.command_topic = true;

        this->send_discovery(root, config);

        std::string name = this->friendly_name();
        root["name"] = name;
        if (config.state_topic)
          root["state_topic"] = this->get_state_topic_();
        if (config.command_topic)
          root["command_topic"] = this->get_command_topic_();

        if (this->availability_ == nullptr) {
          if (!global_mqtt_client->get_availability().topic.empty()) {
            root["availability_topic"] = global_mqtt_client->get_availability().topic;
            if (global_mqtt_client->get_availability().payload_available != "online")
              root["payload_available"] = global_mqtt_client->get_availability().payload_available;
            if (global_mqtt_client->get_availability().payload_not_available != "offline")
              root["payload_not_available"] = global_mqtt_client->get_availability().payload_not_available;
          }
        } else if (!this->availability_->topic.empty()) {
          root["availability_topic"] = this->availability_->topic;
          if (this->availability_->payload_available != "online")
            root["payload_available"] = this->availability_->payload_available;
          if (this->availability_->payload_not_available != "offline")
            root["payload_not_available"] = this->availability_->payload_not_available;
        }

        const std::string &node_name = App.get_name();
        std::string unique_id = this->unique_id();
        if (!unique_id.empty()) {
          root["unique_id"] = unique_id;
        } else {
          // default to almost-unique ID. It's a hack but the only way to get that
          // gorgeous device registry view.
          root["unique_id"] = "ESP" + this->component_type() + this->get_default_object_id_();
        }

        JsonObject &device_info = root.createNestedObject("device");
        device_info["identifiers"] = get_mac_address();
        device_info["name"] = node_name;
        device_info["sw_version"] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time();
#ifdef ARDUINO_BOARD
        device_info["model"] = ARDUINO_BOARD;
#endif
        device_info["manufacturer"] = "espressif";
      },
      0, discovery_info.retain);
}

bool MQTTComponent::get_retain() const { return this->retain_; }

bool MQTTComponent::is_discovery_enabled() const {
  return this->discovery_enabled_ && global_mqtt_client->is_discovery_enabled();
}

std::string MQTTComponent::get_default_object_id_() const {
  return sanitize_string_whitelist(to_lowercase_underscore(this->friendly_name()), HOSTNAME_CHARACTER_WHITELIST);
}

void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) {
  global_mqtt_client->subscribe(topic, std::move(callback), qos);
}

void MQTTComponent::subscribe_json(const std::string &topic, mqtt_json_callback_t callback, uint8_t qos) {
  global_mqtt_client->subscribe_json(topic, std::move(callback), qos);
}

MQTTComponent::MQTTComponent() = default;

float MQTTComponent::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
void MQTTComponent::disable_discovery() { this->discovery_enabled_ = false; }
void MQTTComponent::set_custom_state_topic(const std::string &custom_state_topic) {
  this->custom_state_topic_ = custom_state_topic;
}
void MQTTComponent::set_custom_command_topic(const std::string &custom_command_topic) {
  this->custom_command_topic_ = custom_command_topic;
}

void MQTTComponent::set_availability(std::string topic, std::string payload_available,
                                     std::string payload_not_available) {
  delete this->availability_;
  this->availability_ = new Availability();
  this->availability_->topic = std::move(topic);
  this->availability_->payload_available = std::move(payload_available);
  this->availability_->payload_not_available = std::move(payload_not_available);
}
void MQTTComponent::disable_availability() { this->set_availability("", "", ""); }
void MQTTComponent::call_setup() {
  // Call component internal setup.
  this->setup_internal_();

  if (this->is_internal())
    return;

  this->setup();

  global_mqtt_client->register_mqtt_component(this);

  if (!this->is_connected_())
    return;

  if (this->is_discovery_enabled()) {
    if (!this->send_discovery_()) {
      this->schedule_resend_state();
    }
  }
  if (!this->send_initial_state()) {
    this->schedule_resend_state();
  }
}

void MQTTComponent::call_loop() {
  this->loop_internal_();

  if (this->is_internal())
    return;

  this->loop();

  if (!this->resend_state_ || !this->is_connected_()) {
    return;
  }

  this->resend_state_ = false;
  if (this->is_discovery_enabled()) {
    if (!this->send_discovery_()) {
      this->schedule_resend_state();
    }
  }
  if (!this->send_initial_state()) {
    this->schedule_resend_state();
  }
}
void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; }
std::string MQTTComponent::unique_id() { return ""; }
bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); }

}  // namespace mqtt
}  // namespace esphome
