#pragma once

#include "esphome/core/component.h"

#include "esphome/components/modbus/modbus.h"
#include "esphome/components/modbus/modbus_helpers.h"
#include "esphome/core/automation.h"

#include <list>
#include <queue>
#include <set>
#include <utility>
#include <vector>

namespace esphome {
namespace modbus_controller {

class ModbusController;

using modbus::ModbusFunctionCode;
using modbus::ModbusRegisterType;
using modbus::ModbusExceptionCode;
using modbus::helpers::SensorValueType;

// Remove before 2026.10.0 — these helpers have moved to modbus::helpers
ESPDEPRECATED("Use modbus::helpers::value_type_is_float() instead. Removed in 2026.10.0", "2026.4.0")
inline bool value_type_is_float(SensorValueType v) { return modbus::helpers::value_type_is_float(v); }

ESPDEPRECATED("Use modbus::helpers::modbus_register_read_function() instead. Removed in 2026.10.0", "2026.4.0")
inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) {
  return modbus::helpers::modbus_register_read_function(reg_type);
}

ESPDEPRECATED("Use modbus::helpers::modbus_register_write_function() instead. Removed in 2026.10.0", "2026.4.0")
inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type) {
  return modbus::helpers::modbus_register_write_function(reg_type);
}

ESPDEPRECATED("Use modbus::helpers::c_to_hex() instead. Removed in 2026.10.0", "2026.4.0")
inline uint8_t c_to_hex(char c) { return modbus::helpers::c_to_hex(c); }

ESPDEPRECATED("Use modbus::helpers::byte_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
  return modbus::helpers::byte_from_hex_str(value, pos);
}

ESPDEPRECATED("Use modbus::helpers::word_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
  return modbus::helpers::word_from_hex_str(value, pos);
}

ESPDEPRECATED("Use modbus::helpers::dword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
  return modbus::helpers::dword_from_hex_str(value, pos);
}

ESPDEPRECATED("Use modbus::helpers::qword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
  return modbus::helpers::qword_from_hex_str(value, pos);
}

template<typename T>
ESPDEPRECATED("Use modbus::helpers::get_data() instead. Removed in 2026.10.0", "2026.4.0")
T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
  return modbus::helpers::get_data<T>(data, buffer_offset);
}

ESPDEPRECATED("Use modbus::helpers::coil_from_vector() instead. Removed in 2026.10.0", "2026.4.0")
inline bool coil_from_vector(int coil, const std::vector<uint8_t> &data) {
  return modbus::helpers::coil_from_vector(coil, data);
}

template<typename N>
ESPDEPRECATED("Use modbus::helpers::mask_and_shift_by_rightbit() instead. Removed in 2026.10.0", "2026.4.0")
N mask_and_shift_by_rightbit(N data, uint32_t mask) {
  return modbus::helpers::mask_and_shift_by_rightbit(data, mask);
}

ESPDEPRECATED("Use modbus::helpers::number_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
inline void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
  modbus::helpers::number_to_payload(data, value, value_type);
}

ESPDEPRECATED("Use modbus::helpers::payload_to_number() instead. Removed in 2026.10.0", "2026.4.0")
inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
                                 uint32_t bitmask) {
  return modbus::helpers::payload_to_number(data, sensor_value_type, offset, bitmask);
}

ESPDEPRECATED("Use modbus::helpers::float_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
  return modbus::helpers::float_to_payload(value, value_type);
}

class ModbusController;

class SensorItem {
 public:
  virtual void parse_and_publish(const std::vector<uint8_t> &data) = 0;

  void set_custom_data(const std::vector<uint8_t> &data) { custom_data = data; }
  size_t virtual get_register_size() const {
    if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) {
      return 1;
    } else {  // if CONF_RESPONSE_BYTES is used override the default
      return response_bytes > 0 ? response_bytes : register_count * 2;
    }
  }
  // Override register size for modbus devices not using 1 register for one dword
  void set_register_size(uint8_t register_size) { response_bytes = register_size; }
  ModbusRegisterType register_type{ModbusRegisterType::CUSTOM};
  SensorValueType sensor_value_type{SensorValueType::RAW};
  uint16_t start_address{0};
  uint32_t bitmask{0};
  uint8_t offset{0};
  uint8_t register_count{0};
  uint8_t response_bytes{0};
  uint16_t skip_updates{0};
  std::vector<uint8_t> custom_data{};
  bool force_new_range{false};
};

struct ServerCourtesyResponse {
  bool enabled{false};
  uint16_t register_last_address{0xFFFF};
  uint16_t register_value{0};
};

class ServerRegister {
  using ReadLambda = std::function<int64_t()>;
  using WriteLambda = std::function<bool(int64_t value)>;

 public:
  ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) {
    this->address = address;
    this->value_type = value_type;
    this->register_count = register_count;
  }

  template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
    this->read_lambda = [this, user_read_lambda]() -> int64_t {
      T user_value = user_read_lambda(this->address);
      if constexpr (std::is_same_v<T, float>) {
        return bit_cast<uint32_t>(user_value);
      } else {
        return static_cast<int64_t>(user_value);
      }
    };
  }

  template<typename T>
  void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) {
    this->write_lambda = [this, user_write_lambda](int64_t number) {
      if constexpr (std::is_same_v<T, float>) {
        float float_value = bit_cast<float>(static_cast<uint32_t>(number));
        return user_write_lambda(this->address, float_value);
      }
      return user_write_lambda(this->address, static_cast<T>(number));
    };
  }

  // Formats a raw value into a string representation based on the value type for debugging
  std::string format_value(int64_t value) const {
    // max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
    // plus null terminator = 43, rounded to 44 for 4-byte alignment
    char buf[44];
    switch (this->value_type) {
      case SensorValueType::U_WORD:
      case SensorValueType::U_DWORD:
      case SensorValueType::U_DWORD_R:
      case SensorValueType::U_QWORD:
      case SensorValueType::U_QWORD_R:
        buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
        return buf;
      case SensorValueType::S_WORD:
      case SensorValueType::S_DWORD:
      case SensorValueType::S_DWORD_R:
      case SensorValueType::S_QWORD:
      case SensorValueType::S_QWORD_R:
        buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
        return buf;
      case SensorValueType::FP32_R:
      case SensorValueType::FP32:
        buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
        return buf;
      default:
        buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
        return buf;
    }
  }

  uint16_t address{0};
  SensorValueType value_type{SensorValueType::RAW};
  uint8_t register_count{0};
  ReadLambda read_lambda;
  WriteLambda write_lambda;
};

// ModbusController::create_register_ranges_ tries to optimize register range
// for this the sensors must be ordered by register_type, start_address and bitmask
class SensorItemsComparator {
 public:
  bool operator()(const SensorItem *lhs, const SensorItem *rhs) const {
    // first sort according to register type
    if (lhs->register_type != rhs->register_type) {
      return lhs->register_type < rhs->register_type;
    }

    // ensure that sensor with force_new_range set are before the others
    if (lhs->force_new_range != rhs->force_new_range) {
      return lhs->force_new_range > rhs->force_new_range;
    }

    // sort by start address
    if (lhs->start_address != rhs->start_address) {
      return lhs->start_address < rhs->start_address;
    }

    // sort by offset (ensures update of sensors in ascending order)
    if (lhs->offset != rhs->offset) {
      return lhs->offset < rhs->offset;
    }

    // The pointer to the sensor is used last to ensure that
    // multiple sensors with the same values can be added with a stable sort order.
    return lhs < rhs;
  }
};

using SensorSet = std::set<SensorItem *, SensorItemsComparator>;

struct RegisterRange {
  uint16_t start_address;
  ModbusRegisterType register_type;
  uint8_t register_count;
  uint16_t skip_updates;          // the config value
  SensorSet sensors;              // all sensors of this range
  uint16_t skip_updates_counter;  // the running value
};

class ModbusCommandItem {
 public:
  static const size_t MAX_PAYLOAD_BYTES = 240;
  ModbusController *modbusdevice{nullptr};
  uint16_t register_address{0};
  uint16_t register_count{0};
  ModbusFunctionCode function_code{ModbusFunctionCode::CUSTOM};
  ModbusRegisterType register_type{ModbusRegisterType::CUSTOM};
  std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
      on_data_func;
  std::vector<uint8_t> payload = {};
  bool send();
  /// Check if the command should be retried based on the max_retries parameter
  bool should_retry(uint8_t max_retries) { return this->send_count_ <= max_retries; };

  /// factory methods
  /** Create modbus read command
   *  Function code 02-04
   * @param modbusdevice pointer to the device to execute the command
   * @param function_code modbus function code for the read command
   * @param start_address modbus address of the first register to read
   * @param register_count number of registers to read
   * @param handler function called when the response is received
   * @return ModbusCommandItem with the prepared command
   */
  static ModbusCommandItem create_read_command(
      ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count,
      std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
          &&handler);
  /** Create modbus read command
   *  Function code 02-04
   * @param modbusdevice pointer to the device to execute the command
   * @param function_code modbus function code for the read command
   * @param start_address modbus address of the first register to read
   * @param register_count number of registers to read
   * @return ModbusCommandItem with the prepared command
   */
  static ModbusCommandItem create_read_command(ModbusController *modbusdevice, ModbusRegisterType register_type,
                                               uint16_t start_address, uint16_t register_count);
  /** Create modbus read command
   *  Function code 02-04
   * @param modbusdevice pointer to the device to execute the command
   * @param function_code modbus function code for the read command
   * @param start_address modbus address of the first register to read
   * @param register_count number of registers to read
   * @param handler function called when the response is received
   * @return ModbusCommandItem with the prepared command
   */
  static ModbusCommandItem create_write_multiple_command(ModbusController *modbusdevice, uint16_t start_address,
                                                         uint16_t register_count, const std::vector<uint16_t> &values);
  /** Create modbus write multiple registers command
   *  Function 16 (10hex) Write Multiple Registers
   * @param modbusdevice pointer to the device to execute the command
   * @param start_address modbus address of the first register to read
   * @param register_count number of registers to read
   * @param value uint16_t single register value to write
   * @return ModbusCommandItem with the prepared command
   */
  static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address,
                                                       uint16_t value);
  /** Create modbus write single registers command
   *  Function 05 (05hex) Write Single Coil
   * @param modbusdevice pointer to the device to execute the command
   * @param start_address modbus address of the first register to read
   * @param value uint16_t data to be written to the registers
   * @return ModbusCommandItem with the prepared command
   */
  static ModbusCommandItem create_write_single_coil(ModbusController *modbusdevice, uint16_t address, bool value);

  /** Create modbus write multiple registers command
   *  Function 15 (0Fhex) Write Multiple Coils
   * @param modbusdevice pointer to the device to execute the command
   * @param start_address modbus address of the first register to read
   * @param value bool vector of values to be written to the registers
   * @return ModbusCommandItem with the prepared command
   */
  static ModbusCommandItem create_write_multiple_coils(ModbusController *modbusdevice, uint16_t start_address,
                                                       const std::vector<bool> &values);
  /** Create custom modbus command
   * @param modbusdevice pointer to the device to execute the command
   * @param values byte vector of data to be sent to the device. The complete payload must be provided with the
   * exception of the crc codes
   * @param handler function called when the response is received. Default is just logging a response
   * @return ModbusCommandItem with the prepared command
   */
  static ModbusCommandItem create_custom_command(
      ModbusController *modbusdevice, const std::vector<uint8_t> &values,
      std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
          &&handler = nullptr);

  /** Create custom modbus command
   * @param modbusdevice pointer to the device to execute the command
   * @param values word vector of data to be sent to the device. The complete payload must be provided with the
   * exception of the crc codes
   * @param handler function called when the response is received. Default is just logging a response
   * @return ModbusCommandItem with the prepared command
   */
  static ModbusCommandItem create_custom_command(
      ModbusController *modbusdevice, const std::vector<uint16_t> &values,
      std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
          &&handler = nullptr);

  bool is_equal(const ModbusCommandItem &other);

 protected:
  // wrong commands (esp. custom commands) can block the send queue, limit the number of repeats.
  /// How many times this command has been sent
  uint8_t send_count_{0};
};

/** Modbus controller class.
 *   Each instance handles the modbus commuinication for all sensors with the same modbus address
 *
 * all sensor items (sensors, switches, binarysensor ...) are parsed in modbus address ranges.
 * when esphome calls ModbusController::Update the commands for each range are created and sent
 * Responses for the commands are dispatched to the modbus sensor items.
 */

class ModbusController : public PollingComponent, public modbus::ModbusDevice {
 public:
  void dump_config() override;
  void loop() override;
  void setup() override;
  void update() override;

  /// queues a modbus command in the send queue
  void queue_command(const ModbusCommandItem &command);
  /// Registers a sensor with the controller. Called by esphomes code generator
  void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
  /// Registers a server register with the controller. Called by esphomes code generator
  void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
  /// called when a modbus response was parsed without errors
  void on_modbus_data(const std::vector<uint8_t> &data) override;
  /// called when a modbus error response was received
  void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
  /// called when a modbus request (function code 0x03 or 0x04) was parsed without errors
  void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
  /// called when a modbus request (function code 0x06 or 0x10) was parsed without errors
  void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
  /// default delegate called by process_modbus_data when a response has retrieved from the incoming queue
  void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
  /// default delegate called by process_modbus_data when a response for a write response has retrieved from the
  /// incoming queue
  void on_write_register_response(ModbusRegisterType register_type, uint16_t start_address,
                                  const std::vector<uint8_t> &data);
  /// Allow a duplicate command to be sent
  void set_allow_duplicate_commands(bool allow_duplicate_commands) {
    this->allow_duplicate_commands_ = allow_duplicate_commands;
  }
  /// get if a duplicate command can be sent
  bool get_allow_duplicate_commands() { return this->allow_duplicate_commands_; }
  /// called by esphome generated code to set the command_throttle period
  void set_command_throttle(uint16_t command_throttle) { this->command_throttle_ = command_throttle; }
  /// called by esphome generated code to set the offline_skip_updates
  void set_offline_skip_updates(uint16_t offline_skip_updates) { this->offline_skip_updates_ = offline_skip_updates; }
  /// get the number of queued modbus commands (should be mostly empty)
  size_t get_command_queue_length() { return command_queue_.size(); }
  /// get if the module is offline, didn't respond the last command
  bool get_module_offline() { return module_offline_; }
  /// Set callback for commands
  template<typename F> void add_on_command_sent_callback(F &&callback) {
    this->command_sent_callback_.add(std::forward<F>(callback));
  }
  /// Set callback for online changes
  template<typename F> void add_on_online_callback(F &&callback) {
    this->online_callback_.add(std::forward<F>(callback));
  }
  /// Set callback for offline changes
  template<typename F> void add_on_offline_callback(F &&callback) {
    this->offline_callback_.add(std::forward<F>(callback));
  }
  /// called by esphome generated code to set the max_cmd_retries.
  void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
  /// get how many times a command will be (re)sent if no response is received
  uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
  /// Called by esphome generated code to set the server courtesy response object
  void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
    this->server_courtesy_response_ = server_courtesy_response;
  }
  /// Get the server courtesy response object
  ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; }

 protected:
  /// parse sensormap_ and create range of sequential addresses
  size_t create_register_ranges_();
  // find register in sensormap. Returns iterator with all registers having the same start address
  SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const;
  /// submit the read command for the address range to the send queue
  void update_range_(RegisterRange &r);
  /// parse incoming modbus data
  void process_modbus_data_(const ModbusCommandItem *response);
  /// send the next modbus command from the send queue
  bool send_next_command_();
  /// dump the parsed sensormap for diagnostics
  void dump_sensors_();
  /// Collection of all sensors for this component
  SensorSet sensorset_;
  /// Collection of all server registers for this component
  std::vector<ServerRegister *> server_registers_{};
  /// Continuous range of modbus registers
  std::vector<RegisterRange> register_ranges_{};
  /// Hold the pending requests to be sent
  std::list<std::unique_ptr<ModbusCommandItem>> command_queue_;
  /// modbus response data waiting to get processed
  std::queue<std::unique_ptr<ModbusCommandItem>> incoming_queue_;
  /// if duplicate commands can be sent
  bool allow_duplicate_commands_{false};
  /// when was the last send operation
  uint32_t last_command_timestamp_{0};
  /// min time in ms between sending modbus commands
  uint16_t command_throttle_{0};
  /// if module didn't respond the last command
  bool module_offline_{false};
  /// how many updates to skip if module is offline
  uint16_t offline_skip_updates_{0};
  /// How many times we will retry a command if we get no response
  uint8_t max_cmd_retries_{4};
  /// Command sent callback
  CallbackManager<void(int, int)> command_sent_callback_{};
  /// Server online callback
  CallbackManager<void(int, int)> online_callback_{};
  /// Server offline callback
  CallbackManager<void(int, int)> offline_callback_{};
  /// Server courtesy response
  ServerCourtesyResponse server_courtesy_response_{
      .enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
};

/** Convert vector<uint8_t> response payload to float.
 * @param data payload with data
 * @param item SensorItem object
 * @return float value of data
 */
inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem &item) {
  int64_t number = modbus::helpers::payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask);

  float float_value;
  if (modbus::helpers::value_type_is_float(item.sensor_value_type)) {
    float_value = bit_cast<float>(static_cast<uint32_t>(number));
  } else {
    float_value = static_cast<float>(number);
  }

  return float_value;
}

}  // namespace modbus_controller
}  // namespace esphome
