#pragma once

#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
#include <concepts>
#include <functional>
#include <utility>
#include <vector>

namespace esphome {

// C++20 std::index_sequence is now used for tuple unpacking
// Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility
// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971
// Remove before 2026.6.0
// NOLINTBEGIN(readability-identifier-naming)
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif

template<int...> struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {};
template<int N, int... S>
struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens
    : gens<N - 1, N - 1, S...> {};
template<int... S> struct gens<0, S...> { using type = seq<S...>; };

#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
// NOLINTEND(readability-identifier-naming)

#define TEMPLATABLE_VALUE_(type, name) \
 protected: \
  TemplatableValue<type, Ts...> name##_{}; \
\
 public: \
  template<typename V> void set_##name(V name) { this->name##_ = name; }

#define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name)

template<typename T, typename... X> class TemplatableValue {
  // For std::string, store pointer to heap-allocated string to keep union pointer-sized.
  // For other types, store value inline.
  static constexpr bool USE_HEAP_STORAGE = std::same_as<T, std::string>;

 public:
  TemplatableValue() : type_(NONE) {}

  // For const char* when T is std::string: store pointer directly, no heap allocation
  // String remains in flash and is only converted to std::string when value() is called
  TemplatableValue(const char *str) requires std::same_as<T, std::string> : type_(STATIC_STRING) {
    this->static_str_ = str;
  }

#ifdef USE_ESP8266
  // On ESP8266, __FlashStringHelper* is a distinct type from const char*.
  // ESPHOME_F(s) expands to F(s) which returns __FlashStringHelper* pointing to PROGMEM.
  // Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions
  // to access the PROGMEM pointer safely.
  TemplatableValue(const __FlashStringHelper *str) requires std::same_as<T, std::string> : type_(FLASH_STRING) {
    this->static_str_ = reinterpret_cast<const char *>(str);
  }
#endif

  template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) {
    if constexpr (USE_HEAP_STORAGE) {
      this->value_ = new T(std::move(value));
    } else {
      new (&this->value_) T(std::move(value));
    }
  }

  // For stateless lambdas (convertible to function pointer): use function pointer
  template<typename F>
  TemplatableValue(F f) requires std::invocable<F, X...> && std::convertible_to<F, T (*)(X...)>
      : type_(STATELESS_LAMBDA) {
    this->stateless_f_ = f;  // Implicit conversion to function pointer
  }

  // For stateful lambdas (not convertible to function pointer): use std::function
  template<typename F>
  TemplatableValue(F f) requires std::invocable<F, X...> &&(!std::convertible_to<F, T (*)(X...)>) : type_(LAMBDA) {
    this->f_ = new std::function<T(X...)>(std::move(f));
  }

  // Copy constructor
  TemplatableValue(const TemplatableValue &other) : type_(other.type_) {
    if (this->type_ == VALUE) {
      if constexpr (USE_HEAP_STORAGE) {
        this->value_ = new T(*other.value_);
      } else {
        new (&this->value_) T(other.value_);
      }
    } else if (this->type_ == LAMBDA) {
      this->f_ = new std::function<T(X...)>(*other.f_);
    } else if (this->type_ == STATELESS_LAMBDA) {
      this->stateless_f_ = other.stateless_f_;
    } else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
      this->static_str_ = other.static_str_;
    }
  }

  // Move constructor
  TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) {
    if (this->type_ == VALUE) {
      if constexpr (USE_HEAP_STORAGE) {
        this->value_ = other.value_;
        other.value_ = nullptr;
      } else {
        new (&this->value_) T(std::move(other.value_));
      }
    } else if (this->type_ == LAMBDA) {
      this->f_ = other.f_;
      other.f_ = nullptr;
    } else if (this->type_ == STATELESS_LAMBDA) {
      this->stateless_f_ = other.stateless_f_;
    } else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
      this->static_str_ = other.static_str_;
    }
    other.type_ = NONE;
  }

  // Assignment operators
  TemplatableValue &operator=(const TemplatableValue &other) {
    if (this != &other) {
      this->~TemplatableValue();
      new (this) TemplatableValue(other);
    }
    return *this;
  }

  TemplatableValue &operator=(TemplatableValue &&other) noexcept {
    if (this != &other) {
      this->~TemplatableValue();
      new (this) TemplatableValue(std::move(other));
    }
    return *this;
  }

  ~TemplatableValue() {
    if (this->type_ == VALUE) {
      if constexpr (USE_HEAP_STORAGE) {
        delete this->value_;
      } else {
        this->value_.~T();
      }
    } else if (this->type_ == LAMBDA) {
      delete this->f_;
    }
    // STATELESS_LAMBDA/STATIC_STRING/FLASH_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
  }

  bool has_value() const { return this->type_ != NONE; }

  T value(X... x) const {
    switch (this->type_) {
      case STATELESS_LAMBDA:
        return this->stateless_f_(x...);  // Direct function pointer call
      case LAMBDA:
        return (*this->f_)(x...);  // std::function call
      case VALUE:
        if constexpr (USE_HEAP_STORAGE) {
          return *this->value_;
        } else {
          return this->value_;
        }
      case STATIC_STRING:
        // if constexpr required: code must compile for all T, but STATIC_STRING
        // can only be set when T is std::string (enforced by constructor constraint)
        if constexpr (std::same_as<T, std::string>) {
          return std::string(this->static_str_);
        }
        __builtin_unreachable();
#ifdef USE_ESP8266
      case FLASH_STRING:
        // PROGMEM pointer — must use _P functions to access on ESP8266
        if constexpr (std::same_as<T, std::string>) {
          size_t len = strlen_P(this->static_str_);
          std::string result(len, '\0');
          memcpy_P(result.data(), this->static_str_, len);
          return result;
        }
        __builtin_unreachable();
#endif
      case NONE:
      default:
        return T{};
    }
  }

  optional<T> optional_value(X... x) {
    if (!this->has_value()) {
      return {};
    }
    return this->value(x...);
  }

  T value_or(X... x, T default_value) {
    if (!this->has_value()) {
      return default_value;
    }
    return this->value(x...);
  }

  /// Check if this holds a static string (const char* stored without allocation)
  /// The pointer is always directly readable (RAM or flash-mapped).
  /// Returns false for FLASH_STRING (PROGMEM on ESP8266, requires _P functions).
  bool is_static_string() const { return this->type_ == STATIC_STRING; }

  /// Get the static string pointer (only valid if is_static_string() returns true)
  /// The pointer is always directly readable — FLASH_STRING uses a separate type.
  const char *get_static_string() const { return this->static_str_; }

  /// Check if the string value is empty without allocating (for std::string specialization).
  /// For NONE, returns true. For STATIC_STRING/VALUE, checks without allocation.
  /// For LAMBDA/STATELESS_LAMBDA, must call value() which may allocate.
  bool is_empty() const requires std::same_as<T, std::string> {
    switch (this->type_) {
      case NONE:
        return true;
      case STATIC_STRING:
        return this->static_str_ == nullptr || this->static_str_[0] == '\0';
#ifdef USE_ESP8266
      case FLASH_STRING:
        // PROGMEM pointer — must use progmem_read_byte on ESP8266
        return this->static_str_ == nullptr ||
               progmem_read_byte(reinterpret_cast<const uint8_t *>(this->static_str_)) == '\0';
#endif
      case VALUE:
        return this->value_->empty();
      default:  // LAMBDA/STATELESS_LAMBDA - must call value()
        return this->value().empty();
    }
  }

  /// Get a StringRef to the string value without heap allocation when possible.
  /// For STATIC_STRING/VALUE, returns reference to existing data (no allocation).
  /// For FLASH_STRING (ESP8266 PROGMEM), copies to provided buffer via _P functions.
  /// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer.
  /// @param lambda_buf Buffer used only for copy cases (must remain valid while StringRef is used).
  /// @param lambda_buf_size Size of the buffer.
  /// @return StringRef pointing to the string data.
  StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as<T, std::string> {
    switch (this->type_) {
      case NONE:
        return StringRef();
      case STATIC_STRING:
        if (this->static_str_ == nullptr)
          return StringRef();
        return StringRef(this->static_str_, strlen(this->static_str_));
#ifdef USE_ESP8266
      case FLASH_STRING:
        if (this->static_str_ == nullptr)
          return StringRef();
        {
          // PROGMEM pointer — copy to buffer via _P functions
          size_t len = strlen_P(this->static_str_);
          size_t copy_len = std::min(len, lambda_buf_size - 1);
          memcpy_P(lambda_buf, this->static_str_, copy_len);
          lambda_buf[copy_len] = '\0';
          return StringRef(lambda_buf, copy_len);
        }
#endif
      case VALUE:
        return StringRef(this->value_->data(), this->value_->size());
      default: {  // LAMBDA/STATELESS_LAMBDA - must call value() and copy
        std::string result = this->value();
        size_t copy_len = std::min(result.size(), lambda_buf_size - 1);
        memcpy(lambda_buf, result.data(), copy_len);
        lambda_buf[copy_len] = '\0';
        return StringRef(lambda_buf, copy_len);
      }
    }
  }

 protected : enum : uint8_t {
   NONE,
   VALUE,
   LAMBDA,
   STATELESS_LAMBDA,
   STATIC_STRING,  // For const char* when T is std::string - avoids heap allocation
   FLASH_STRING,   // PROGMEM pointer on ESP8266; never set on other platforms
 } type_;
  // For std::string, use heap pointer to minimize union size (4 bytes vs 12+).
  // For other types, store value inline as before.
  using ValueStorage = std::conditional_t<USE_HEAP_STORAGE, T *, T>;
  union {
    ValueStorage value_;  // T for inline storage, T* for heap storage
    std::function<T(X...)> *f_;
    T (*stateless_f_)(X...);
    const char *static_str_;  // For STATIC_STRING and FLASH_STRING types
  };
};

/** Base class for all automation conditions.
 *
 * @tparam Ts The template parameters to pass when executing.
 */
template<typename... Ts> class Condition {
 public:
  /// Check whether this condition passes. This condition check must be instant, and not cause any delays.
  virtual bool check(const Ts &...x) = 0;

  /// Call check with a tuple of values as parameter.
  bool check_tuple(const std::tuple<Ts...> &tuple) {
    return this->check_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
  }

 protected:
  template<size_t... S> bool check_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
    return this->check(std::get<S>(tuple)...);
  }
};

template<typename... Ts> class Automation;

template<typename... Ts> class Trigger {
 public:
  /// Inform the parent automation that the event has triggered.
  void trigger(const Ts &...x) {
    if (this->automation_parent_ == nullptr)
      return;
    this->automation_parent_->trigger(x...);
  }
  void set_automation_parent(Automation<Ts...> *automation_parent) { this->automation_parent_ = automation_parent; }

  /// Stop any action connected to this trigger.
  void stop_action() {
    if (this->automation_parent_ == nullptr)
      return;
    this->automation_parent_->stop();
  }
  /// Returns true if any action connected to this trigger is running.
  bool is_action_running() {
    if (this->automation_parent_ == nullptr)
      return false;
    return this->automation_parent_->is_running();
  }

 protected:
  Automation<Ts...> *automation_parent_{nullptr};
};

template<typename... Ts> class ActionList;

template<typename... Ts> class Action {
 public:
  virtual void play_complex(const Ts &...x) {
    this->num_running_++;
    this->play(x...);
    this->play_next_(x...);
  }
  virtual void stop_complex() {
    if (num_running_) {
      this->stop();
      this->num_running_ = 0;
    }
    this->stop_next_();
  }
  /// Check if this or any of the following actions are currently running.
  virtual bool is_running() { return this->num_running_ > 0 || this->is_running_next_(); }

  /// The total number of actions that are currently running in this plus any of
  /// the following actions in the chain.
  int num_running_total() {
    int total = this->num_running_;
    if (this->next_ != nullptr)
      total += this->next_->num_running_total();
    return total;
  }

 protected:
  friend ActionList<Ts...>;
  template<typename... Us> friend class ContinuationAction;

  virtual void play(const Ts &...x) = 0;
  void play_next_(const Ts &...x) {
    if (this->num_running_ > 0) {
      this->num_running_--;
      if (this->next_ != nullptr) {
        this->next_->play_complex(x...);
      }
    }
  }
  template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
    this->play_next_(std::get<S>(tuple)...);
  }
  void play_next_tuple_(const std::tuple<Ts...> &tuple) {
    this->play_next_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
  }

  virtual void stop() {}
  void stop_next_() {
    if (this->next_ != nullptr) {
      this->next_->stop_complex();
    }
  }

  bool is_running_next_() {
    if (this->next_ == nullptr)
      return false;
    return this->next_->is_running();
  }

  Action<Ts...> *next_{nullptr};

  /// The number of instances of this sequence in the list of actions
  /// that is currently being executed.
  int num_running_{0};
};

template<typename... Ts> class ActionList {
 public:
  void add_action(Action<Ts...> *action) {
    if (this->actions_end_ == nullptr) {
      this->actions_begin_ = action;
    } else {
      this->actions_end_->next_ = action;
    }
    this->actions_end_ = action;
  }
  void add_actions(const std::initializer_list<Action<Ts...> *> &actions) {
    for (auto *action : actions) {
      this->add_action(action);
    }
  }
  void play(const Ts &...x) {
    if (this->actions_begin_ != nullptr)
      this->actions_begin_->play_complex(x...);
  }
  void play_tuple(const std::tuple<Ts...> &tuple) {
    this->play_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
  }
  void stop() {
    if (this->actions_begin_ != nullptr)
      this->actions_begin_->stop_complex();
  }
  bool empty() const { return this->actions_begin_ == nullptr; }

  /// Check if any action in this action list is currently running.
  bool is_running() {
    if (this->actions_begin_ == nullptr)
      return false;
    return this->actions_begin_->is_running();
  }
  /// Return the number of actions in this action list that are currently running.
  int num_running() {
    if (this->actions_begin_ == nullptr)
      return 0;
    return this->actions_begin_->num_running_total();
  }

 protected:
  template<size_t... S> void play_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
    this->play(std::get<S>(tuple)...);
  }

  Action<Ts...> *actions_begin_{nullptr};
  Action<Ts...> *actions_end_{nullptr};
};

template<typename... Ts> class Automation {
 public:
  explicit Automation(Trigger<Ts...> *trigger) : trigger_(trigger) { this->trigger_->set_automation_parent(this); }

  void add_action(Action<Ts...> *action) { this->actions_.add_action(action); }
  void add_actions(const std::initializer_list<Action<Ts...> *> &actions) { this->actions_.add_actions(actions); }

  void stop() { this->actions_.stop(); }

  void trigger(const Ts &...x) { this->actions_.play(x...); }

  bool is_running() { return this->actions_.is_running(); }

  /// Return the number of actions in the action part of this automation that are currently running.
  int num_running() { return this->actions_.num_running(); }

 protected:
  Trigger<Ts...> *trigger_;
  ActionList<Ts...> actions_;
};

}  // namespace esphome
