#include "zwave_proxy.h"

#ifdef USE_API

#include "esphome/components/api/api_server.h"

#include <cinttypes>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"

namespace esphome::zwave_proxy {

static const char *const TAG = "zwave_proxy";

// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512)
static constexpr size_t ZWAVE_MAX_LOG_BYTES = 168;

static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20;
// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01;    // Response type field value
static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9;  // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum
static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100;             // Timeout for waiting for home ID during setup
static constexpr uint32_t RECONNECT_DELAY_MS = 500;             // Delay between home ID query attempts after reconnect
static constexpr uint8_t MAX_QUERY_RETRIES = 5;                 // Max attempts to query home ID after reconnect

static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
  // Calculate Z-Wave frame checksum
  // XOR all bytes between SOF and checksum position (exclusive)
  // Initial value is 0xFF per Z-Wave protocol specification
  uint8_t checksum = 0xFF;
  for (uint8_t i = 1; i < length - 1; i++) {
    checksum ^= data[i];
  }
  return checksum;
}

ZWaveProxy::ZWaveProxy() { global_zwave_proxy = this; }

void ZWaveProxy::setup() {
  this->setup_time_ = App.get_loop_component_start_time();
  this->was_connected_ = this->parent_->is_connected();
  if (this->was_connected_) {
    this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
  }
}

float ZWaveProxy::get_setup_priority() const {
  // Set up before API so home ID is ready when API starts
  return setup_priority::BEFORE_CONNECTION;
}

bool ZWaveProxy::can_proceed() {
  // If we already have the home ID, we can proceed
  if (this->home_id_ready_) {
    return true;
  }

  // Handle any pending responses
  if (this->response_handler_()) {
    ESP_LOGV(TAG, "Handled response during setup");
  }

  // Process UART data to check for home ID
  this->process_uart_();

  // Check if we got the home ID after processing
  if (this->home_id_ready_) {
    return true;
  }

  // Wait up to HOME_ID_TIMEOUT_MS for home ID response
  const uint32_t now = App.get_loop_component_start_time();
  if (now - this->setup_time_ > HOME_ID_TIMEOUT_MS) {
    ESP_LOGW(TAG, "Timeout reading Home ID during setup");
    return true;  // Proceed anyway after timeout
  }

  return false;  // Keep waiting
}

void ZWaveProxy::loop() {
  if (this->response_handler_()) {
    ESP_LOGV(TAG, "Handled late response");
  }
  if (this->api_connection_ != nullptr && (!this->api_connection_->is_connection_setup() || !api_is_connected())) {
    ESP_LOGW(TAG, "Subscriber disconnected");
    this->api_connection_ = nullptr;  // Unsubscribe if disconnected
  }

  const bool connected = this->parent_->is_connected();
  if (this->was_connected_ != connected) {
    this->on_connection_changed_(connected);
  }
  if (this->reconnect_time_ != 0) {
    this->retry_home_id_query_();
  }

  this->process_uart_();
  this->status_clear_warning();
}

void ZWaveProxy::process_uart_slow_() {
  // Caller (inline process_uart_) has already confirmed available() > 0, so use do/while to
  // drain bytes — available() is still checked at the tail, but not redundantly on entry.
  do {
    uint8_t byte;
    if (!this->read_byte(&byte)) {
      this->status_set_warning(LOG_STR("UART read failed"));
      return;
    }
    if (this->parse_byte_(byte)) {
      // Check if this is a GET_NETWORK_IDS response frame
      // Frame format: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
      // We verify:
      // - buffer_[0]: Start of frame marker (0x01)
      // - buffer_[1]: Length field must be >= 9 to contain all required data
      // - buffer_[2]: Command type (0x01 for response)
      // - buffer_[3]: Command ID (0x20 for GET_NETWORK_IDS)
      if (this->buffer_[3] == ZWAVE_COMMAND_GET_NETWORK_IDS && this->buffer_[2] == ZWAVE_COMMAND_TYPE_RESPONSE &&
          this->buffer_[1] >= ZWAVE_MIN_GET_NETWORK_IDS_LENGTH && this->buffer_[0] == ZWAVE_FRAME_TYPE_START) {
        // Store the 4-byte Home ID, which starts at offset 4, and notify connected clients if it changed
        // The frame parser has already validated the checksum and ensured all bytes are present
        if (this->set_home_id_(&this->buffer_[4])) {
          this->send_homeid_changed_msg_();
        }
      }
      ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr));
      if (this->api_connection_ != nullptr) {
        // Zero-copy: point directly to our buffer
        this->outgoing_proto_msg_.data = this->buffer_.data();
        if (this->in_bootloader_) {
          this->outgoing_proto_msg_.data_len = this->buffer_index_;
        } else {
          // If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN
          this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1;
        }
        this->api_connection_->send_message(this->outgoing_proto_msg_);
      }
    }
  } while (this->available());
}

void ZWaveProxy::dump_config() {
  char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)];
  ESP_LOGCONFIG(TAG,
                "Z-Wave Proxy:\n"
                "  Home ID: %s",
                format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size()));
}

void ZWaveProxy::api_connection_authenticated(api::APIConnection *conn) {
  if (this->home_id_ready_) {
    // If a client just authenticated & HomeID is ready, send the current HomeID
    this->send_homeid_changed_msg_(conn);
  }
}

void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type) {
  switch (type) {
    case api::enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE:
      if (this->api_connection_ != nullptr) {
        ESP_LOGE(TAG, "Only one API subscription is allowed at a time");
        return;
      }
      this->api_connection_ = api_connection;
      ESP_LOGV(TAG, "API connection is now subscribed");
      break;

    case api::enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
      if (this->api_connection_ != api_connection) {
        ESP_LOGV(TAG, "API connection is not subscribed");
        return;
      }
      this->api_connection_ = nullptr;
      break;

    default:
      ESP_LOGW(TAG, "Unknown request type: %" PRIu32, static_cast<uint32_t>(type));
      break;
  }
}

void ZWaveProxy::on_connection_changed_(bool connected) {
  this->was_connected_ = connected;
  if (connected) {
    ESP_LOGD(TAG, "Modem reconnected");
    this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START;
    this->buffer_index_ = 0;
    this->last_response_ = 0;
    this->in_bootloader_ = false;
    // Defer the query — the modem needs time to initialize after power is applied
    this->reconnect_time_ = App.get_loop_component_start_time();
    this->query_retries_ = 0;
  } else {
    ESP_LOGW(TAG, "Modem disconnected");
    this->clear_home_id_();
  }
}

void ZWaveProxy::retry_home_id_query_() {
  if (this->home_id_ready_) {
    // Got the home ID, cancel remaining retries
    this->reconnect_time_ = 0;
    return;
  }
  if (App.get_loop_component_start_time() - this->reconnect_time_ <= RECONNECT_DELAY_MS) {
    return;  // Not yet time for next attempt
  }
  this->reconnect_time_ = App.get_loop_component_start_time();  // Reset timer for next retry
  this->query_retries_++;
  if (this->query_retries_ <= MAX_QUERY_RETRIES) {
    ESP_LOGD(TAG, "Querying Home ID (attempt %u)", this->query_retries_);
    this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
  } else {
    ESP_LOGW(TAG, "Failed to read Home ID after %u attempts", MAX_QUERY_RETRIES);
    this->reconnect_time_ = 0;
  }
}

void ZWaveProxy::clear_home_id_() {
  static constexpr uint8_t ZERO_HOME_ID[ZWAVE_HOME_ID_SIZE] = {};
  if (this->set_home_id_(ZERO_HOME_ID)) {
    this->send_homeid_changed_msg_();
  }
  this->home_id_ready_ = false;
  this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START;
  this->buffer_index_ = 0;
  this->last_response_ = 0;
  this->in_bootloader_ = false;
}

bool ZWaveProxy::set_home_id_(const uint8_t *new_home_id) {
  if (std::memcmp(this->home_id_.data(), new_home_id, this->home_id_.size()) == 0) {
    ESP_LOGV(TAG, "Home ID unchanged");
    return false;  // No change
  }
  std::memcpy(this->home_id_.data(), new_home_id, this->home_id_.size());
  char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)];
  ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size()));
  this->home_id_ready_ = true;
  return true;  // Home ID was changed
}

void ZWaveProxy::send_frame(const uint8_t *data, size_t length) {
  // Safety: validate pointer before any access
  if (data == nullptr) {
    ESP_LOGE(TAG, "Null data pointer");
    return;
  }
  if (length == 0) {
    ESP_LOGE(TAG, "Length 0");
    return;
  }

  // Skip duplicate single-byte responses (ACK/NAK/CAN)
  if (length == 1 && data[0] == this->last_response_) {
    ESP_LOGV(TAG, "Response already sent: 0x%02X", data[0]);
    return;
  }

#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
  char hex_buf[format_hex_pretty_size(ZWAVE_MAX_LOG_BYTES)];
#endif
  ESP_LOGVV(TAG, "Sending: %s", format_hex_pretty_to(hex_buf, data, length));

  this->write_array(data, length);
}

void ZWaveProxy::send_homeid_changed_msg_(api::APIConnection *conn) {
  api::ZWaveProxyRequest msg;
  msg.type = api::enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE;
  msg.data = this->home_id_.data();
  msg.data_len = this->home_id_.size();
  if (conn != nullptr) {
    // Send to specific connection
    conn->send_message(msg);
  } else if (api::global_api_server != nullptr) {
    // We could add code to manage a second subscription type, but, since this message is
    //  very infrequent and small, we simply send it to all clients
    api::global_api_server->on_zwave_proxy_request(msg);
  }
}

void ZWaveProxy::send_simple_command_(const uint8_t command_id) {
  // Send a simple Z-Wave command with no parameters
  // Frame format: [SOF][LENGTH][TYPE][CMD][CHECKSUM]
  // Where LENGTH=0x03 (3 bytes: TYPE + CMD + CHECKSUM)
  uint8_t cmd[] = {0x01, 0x03, 0x00, command_id, 0x00};
  cmd[4] = calculate_frame_checksum(cmd, sizeof(cmd));
  this->send_frame(cmd, sizeof(cmd));
}

bool ZWaveProxy::parse_byte_(uint8_t byte) {
  bool frame_completed = false;
  // Basic parsing logic for received frames
  switch (this->parsing_state_) {
    case ZWAVE_PARSING_STATE_WAIT_START:
      this->parse_start_(byte);
      break;
    case ZWAVE_PARSING_STATE_WAIT_LENGTH:
      if (!byte) {
        ESP_LOGW(TAG, "Invalid LENGTH: %u", byte);
        this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_NAK;
        return false;
      }
      ESP_LOGVV(TAG, "Received LENGTH: %u", byte);
      this->end_frame_after_ = this->buffer_index_ + byte;
      ESP_LOGVV(TAG, "Calculated EOF: %u", this->end_frame_after_);
      this->buffer_[this->buffer_index_++] = byte;
      this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_TYPE;
      break;
    case ZWAVE_PARSING_STATE_WAIT_TYPE:
      this->buffer_[this->buffer_index_++] = byte;
      ESP_LOGVV(TAG, "Received TYPE: 0x%02X", byte);
      this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_COMMAND_ID;
      break;
    case ZWAVE_PARSING_STATE_WAIT_COMMAND_ID:
      this->buffer_[this->buffer_index_++] = byte;
      ESP_LOGVV(TAG, "Received COMMAND ID: 0x%02X", byte);
      this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_PAYLOAD;
      break;
    case ZWAVE_PARSING_STATE_WAIT_PAYLOAD:
      this->buffer_[this->buffer_index_++] = byte;
      ESP_LOGVV(TAG, "Received PAYLOAD: 0x%02X", byte);
      if (this->buffer_index_ >= this->end_frame_after_) {
        this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_CHECKSUM;
      }
      break;
    case ZWAVE_PARSING_STATE_WAIT_CHECKSUM: {
      this->buffer_[this->buffer_index_++] = byte;
      auto checksum = calculate_frame_checksum(this->buffer_.data(), this->buffer_index_);
      ESP_LOGVV(TAG, "CHECKSUM Received: 0x%02X - Calculated: 0x%02X", byte, checksum);
      if (checksum != byte) {
        ESP_LOGW(TAG, "Bad checksum: expected 0x%02X, got 0x%02X", checksum, byte);
        this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_NAK;
      } else {
        this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_ACK;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
        char hex_buf[format_hex_pretty_size(ZWAVE_MAX_LOG_BYTES)];
#endif
        ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_index_));
        frame_completed = true;
      }
      this->response_handler_();
      break;
    }
    case ZWAVE_PARSING_STATE_READ_BL_MENU:
      if (this->buffer_index_ >= this->buffer_.size()) {
        this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START;
        break;
      }
      this->buffer_[this->buffer_index_++] = byte;
      if (!byte) {
        this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START;
        frame_completed = true;
      }
      break;
    case ZWAVE_PARSING_STATE_SEND_ACK:
    case ZWAVE_PARSING_STATE_SEND_NAK:
      break;  // Should not happen, handled in loop()
    default:
      ESP_LOGW(TAG, "Bad parsing state; resetting");
      this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START;
      break;
  }
  return frame_completed;
}

void ZWaveProxy::parse_start_(uint8_t byte) {
  this->buffer_index_ = 0;
  this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START;
  switch (byte) {
    case ZWAVE_FRAME_TYPE_START:
      ESP_LOGV(TAG, "Received START");
      if (this->in_bootloader_) {
        ESP_LOGD(TAG, "Exited bootloader mode");
        this->in_bootloader_ = false;
      }
      this->buffer_[this->buffer_index_++] = byte;
      this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_LENGTH;
      return;
    case ZWAVE_FRAME_TYPE_BL_MENU:
      ESP_LOGV(TAG, "Received BL_MENU");
      if (!this->in_bootloader_) {
        ESP_LOGD(TAG, "Entered bootloader mode");
        this->in_bootloader_ = true;
      }
      this->buffer_[this->buffer_index_++] = byte;
      this->parsing_state_ = ZWAVE_PARSING_STATE_READ_BL_MENU;
      return;
    case ZWAVE_FRAME_TYPE_BL_BEGIN_UPLOAD:
      ESP_LOGV(TAG, "Received BL_BEGIN_UPLOAD");
      break;
    case ZWAVE_FRAME_TYPE_ACK:
      ESP_LOGV(TAG, "Received ACK");
      break;
    case ZWAVE_FRAME_TYPE_NAK:
      ESP_LOGV(TAG, "Received NAK");
      break;
    case ZWAVE_FRAME_TYPE_CAN:
      ESP_LOGV(TAG, "Received CAN");
      break;
    default:
      ESP_LOGW(TAG, "Unrecognized START: 0x%02X", byte);
      return;
  }
  // Forward response (ACK/NAK/CAN) back to client for processing
  if (this->api_connection_ != nullptr) {
    // Store single byte in buffer and point to it
    this->buffer_[0] = byte;
    this->outgoing_proto_msg_.data = this->buffer_.data();
    this->outgoing_proto_msg_.data_len = 1;
    this->api_connection_->send_message(this->outgoing_proto_msg_);
  }
}

bool ZWaveProxy::response_handler_slow_() {
  switch (this->parsing_state_) {
    case ZWAVE_PARSING_STATE_SEND_ACK:
      this->last_response_ = ZWAVE_FRAME_TYPE_ACK;
      break;
    case ZWAVE_PARSING_STATE_SEND_CAN:
      this->last_response_ = ZWAVE_FRAME_TYPE_CAN;
      break;
    case ZWAVE_PARSING_STATE_SEND_NAK:
      this->last_response_ = ZWAVE_FRAME_TYPE_NAK;
      break;
    default:
      return false;  // No response handled
  }

  ESP_LOGVV(TAG, "Sending %s (0x%02X)", this->last_response_ == ZWAVE_FRAME_TYPE_ACK ? "ACK" : "NAK/CAN",
            this->last_response_);
  this->write_byte(this->last_response_);
  this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START;
  return true;
}

ZWaveProxy *global_zwave_proxy = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

}  // namespace esphome::zwave_proxy

#endif  // USE_API
