#pragma once

#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/optional.h"
#include "esphome/core/preferences.h"
#include "esphome/core/string_ref.h"
#include "fan_traits.h"

namespace esphome::fan {

#define LOG_FAN(prefix, type, obj) \
  if ((obj) != nullptr) { \
    ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
    (obj)->dump_traits_(TAG, prefix); \
  }

/// Simple enum to represent the direction of a fan.
enum class FanDirection { FORWARD = 0, REVERSE = 1 };

/// Restore mode of a fan.
enum class FanRestoreMode {
  NO_RESTORE,
  ALWAYS_OFF,
  ALWAYS_ON,
  RESTORE_DEFAULT_OFF,
  RESTORE_DEFAULT_ON,
  RESTORE_INVERTED_DEFAULT_OFF,
  RESTORE_INVERTED_DEFAULT_ON,
};

const LogString *fan_direction_to_string(FanDirection direction);

class Fan;

class FanCall {
 public:
  explicit FanCall(Fan &parent) : parent_(parent) {}

  FanCall &set_state(bool binary_state) {
    this->binary_state_ = binary_state;
    return *this;
  }
  FanCall &set_state(optional<bool> binary_state) {
    this->binary_state_ = binary_state;
    return *this;
  }
  optional<bool> get_state() const { return this->binary_state_; }
  FanCall &set_oscillating(bool oscillating) {
    this->oscillating_ = oscillating;
    return *this;
  }
  FanCall &set_oscillating(optional<bool> oscillating) {
    this->oscillating_ = oscillating;
    return *this;
  }
  optional<bool> get_oscillating() const { return this->oscillating_; }
  FanCall &set_speed(int speed) {
    this->speed_ = speed;
    return *this;
  }
  optional<int> get_speed() const { return this->speed_; }
  FanCall &set_direction(FanDirection direction) {
    this->direction_ = direction;
    return *this;
  }
  FanCall &set_direction(optional<FanDirection> direction) {
    this->direction_ = direction;
    return *this;
  }
  optional<FanDirection> get_direction() const { return this->direction_; }
  FanCall &set_preset_mode(const std::string &preset_mode);
  FanCall &set_preset_mode(const char *preset_mode);
  FanCall &set_preset_mode(const char *preset_mode, size_t len);
  const char *get_preset_mode() const { return this->preset_mode_; }
  bool has_preset_mode() const { return this->preset_mode_ != nullptr; }

  void perform();

 protected:
  void validate_();

  Fan &parent_;
  optional<bool> binary_state_;
  optional<bool> oscillating_;
  optional<int> speed_;
  optional<FanDirection> direction_{};
  const char *preset_mode_{nullptr};  // Pointer to string in traits (after validation)
};

struct FanRestoreState {
  static constexpr uint8_t NO_PRESET = UINT8_MAX;

  bool state;
  int speed;
  bool oscillating;
  FanDirection direction;
  uint8_t preset_mode{NO_PRESET};

  /// Convert this struct to a fan call that can be performed.
  FanCall to_call(Fan &fan);
  /// Apply these settings to the fan.
  void apply(Fan &fan);
} __attribute__((packed));

class Fan : public EntityBase {
 public:
  /// The current on/off state of the fan.
  bool state{false};
  /// The current oscillation state of the fan.
  bool oscillating{false};
  /// The current fan speed level
  int speed{0};
  /// The current direction of the fan
  FanDirection direction{FanDirection::FORWARD};

  FanCall turn_on();
  FanCall turn_off();
  FanCall toggle();
  FanCall make_call();

  /// Register a callback that will be called each time the state changes.
  template<typename F> void add_on_state_callback(F &&callback) {
    this->state_callback_.add(std::forward<F>(callback));
  }

  void publish_state();

  virtual FanTraits get_traits() = 0;

  /// Set the supported preset modes (stored on Fan, referenced by FanTraits via pointer).
  void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) {
    this->ensure_preset_modes_().assign(preset_modes.begin(), preset_modes.end());
  }
  void set_supported_preset_modes(const std::vector<const char *> &preset_modes) {
    this->ensure_preset_modes_() = preset_modes;
  }

  /// Set the restore mode of this fan.
  void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }

  /// Get the current preset mode.
  /// Returns a StringRef of the string stored in traits, or empty ref if not set.
  /// The returned ref points to string literals from codegen (static storage).
  /// Traits are set once at startup and valid for the lifetime of the program.
  StringRef get_preset_mode() const { return StringRef::from_maybe_nullptr(this->preset_mode_); }

  /// Check if a preset mode is currently active
  bool has_preset_mode() const { return this->preset_mode_ != nullptr; }

 protected:
  friend FanCall;
  friend struct FanRestoreState;

  virtual void control(const FanCall &call) = 0;

  optional<FanRestoreState> restore_state_();
  void save_state_();

  void dump_traits_(const char *tag, const char *prefix);

  /// Set the preset mode (finds and stores pointer from traits). Returns true if changed.
  /// Passing nullptr or empty string clears the preset mode.
  bool set_preset_mode_(const char *preset_mode, size_t len);
  bool set_preset_mode_(const char *preset_mode);
  bool set_preset_mode_(const std::string &preset_mode);
  bool set_preset_mode_(StringRef preset_mode);
  /// Clear the preset mode
  void clear_preset_mode_();
  /// Apply preset mode from a FanCall (handles speed-clears-preset convention)
  void apply_preset_mode_(const FanCall &call);
  /// Find and return the matching preset mode pointer from traits, or nullptr if not found.
  const char *find_preset_mode_(const char *preset_mode);
  const char *find_preset_mode_(const char *preset_mode, size_t len);

  /// Wire the Fan-owned preset modes pointer into the given traits object.
  void wire_preset_modes_(FanTraits &traits) {
    if (this->supported_preset_modes_) {
      traits.set_supported_preset_modes_(this->supported_preset_modes_);
    }
  }

  LazyCallbackManager<void()> state_callback_{};
  ESPPreferenceObject rtc_;
  FanRestoreMode restore_mode_;

 private:
  /// Lazy-allocate preset modes vector (never freed — entity lives forever).
  std::vector<const char *> &ensure_preset_modes_() {
    if (!this->supported_preset_modes_) {
      this->supported_preset_modes_ = new std::vector<const char *>();  // NOLINT
    }
    return *this->supported_preset_modes_;
  }

  std::vector<const char *> *supported_preset_modes_{nullptr};
  const char *preset_mode_{nullptr};
};

}  // namespace esphome::fan
