#include "api_server.h"
#ifdef USE_API
#include <cerrno>
#include <cinttypes>
#include "api_connection.h"
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include "esphome/core/version.h"
#ifdef USE_API_HOMEASSISTANT_SERVICES
#include "homeassistant_service.h"
#endif

#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif

#include <algorithm>
#include <utility>

namespace esphome::api {

static const char *const TAG = "api";

// APIServer
APIServer *global_api_server = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

APIServer::APIServer() { global_api_server = this; }

void APIServer::socket_failed_(const LogString *msg) {
  ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
  this->destroy_socket_();
  this->mark_failed();
}

void APIServer::setup() {
  ControllerRegistry::register_controller(this);

#ifdef USE_API_NOISE
  uint32_t hash = 88491486UL;

  this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(hash, true);

#ifndef USE_API_NOISE_PSK_FROM_YAML
  // Only load saved PSK if not set from YAML
  if (this->load_and_apply_noise_psk_()) {
    ESP_LOGD(TAG, "Loaded saved Noise PSK");
  }
#endif
#endif

  this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release();  // monitored for incoming connections
  if (this->socket_ == nullptr) {
    this->socket_failed_(LOG_STR("creation"));
    return;
  }
  int enable = 1;
  int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
  if (err != 0) {
    ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno);
    // we can still continue
  }
  err = this->socket_->setblocking(false);
  if (err != 0) {
    this->socket_failed_(LOG_STR("nonblocking"));
    return;
  }

  struct sockaddr_storage server;

  socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
  if (sl == 0) {
    this->socket_failed_(LOG_STR("set sockaddr"));
    return;
  }

  err = this->socket_->bind((struct sockaddr *) &server, sl);
  if (err != 0) {
    this->socket_failed_(LOG_STR("bind"));
    return;
  }

  err = this->socket_->listen(this->listen_backlog_);
  if (err != 0) {
    this->socket_failed_(LOG_STR("listen"));
    return;
  }

#ifdef USE_LOGGER
  if (logger::global_logger != nullptr) {
    logger::global_logger->add_log_callback(
        this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
          static_cast<APIServer *>(self)->on_log(level, tag, message, message_len);
        });
  }
#endif

#ifdef USE_CAMERA
  if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
    camera::Camera::instance()->add_listener(this);
  }
#endif

  // Initialize last_connected_ for reboot timeout tracking
  this->last_connected_ = App.get_loop_component_start_time();
#if defined(USE_PROVISIONING) && defined(USE_API_NOISE)
  // Register with the provisioning manager (provisioning:) as a source and
  // report our current state (provisioned == an encryption key is set). When the
  // window closes, disconnect any client still attempting to provision so it learns
  // the reason. The manager owns the timeout, window state and on_timeout automation.
  if (provisioning::global_provisioning_manager != nullptr) {
    this->provisioning_source_ = provisioning::global_provisioning_manager->register_source();
    provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_,
                                                                      this->noise_ctx_.has_psk());
    provisioning::global_provisioning_manager->add_on_closed_callback([this]() {
      for (auto &c : this->active_clients()) {
        DisconnectRequest req;
        req.reason = enums::DISCONNECT_REASON_PROVISIONING_CLOSED;
        // Best-effort: if the send buffer is full the reason is dropped, but the
        // client still learns the window is closed when it reconnects (rejected at
        // hello) or via the socket close.
        c->send_message(req);
      }
    });
  }
#endif
  // Set warning status if reboot timeout is enabled (suppressed while provisioning
  // is pending so the device waits to be onboarded instead of rebooting).
  if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
    this->status_set_warning(LOG_STR("waiting for client connection"));
  }
}

void APIServer::loop() {
  // Accept new clients only if the socket exists and has incoming connections
  if (this->socket_ && this->socket_->ready()) {
    this->accept_new_connections_();
  }

  if (this->api_connection_count_ == 0) {
    // Check reboot timeout - done in loop to avoid scheduler heap churn
    // (cancelled scheduler items sit in heap memory until their scheduled time).
    // Suppressed while a provisioning window is pending so the device waits to be
    // onboarded / reset instead of rebooting itself; resumes once provisioned.
    if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
      const uint32_t now = App.get_loop_component_start_time();
      if (now - this->last_connected_ > this->reboot_timeout_) {
        ESP_LOGE(TAG, "No clients; rebooting");
        App.reboot();
      }
    }
    return;
  }

  // Process clients and remove disconnected ones in a single pass
  // Check network connectivity once for all clients
  if (!network::is_connected()) {
    // Network is down - disconnect all clients
    for (auto &client : this->active_clients()) {
      client->on_fatal_error();
      client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
    }
    // Continue to process and clean up the clients below
  }

  uint8_t client_index = 0;
  while (client_index < this->api_connection_count_) {
    auto &client = this->clients_[client_index];

    // Common case: process active client
    if (!client->flags_.remove) {
      client->loop();
    }
    // Handle disconnection promptly - close socket to free LWIP PCB
    // resources and prevent retransmit crashes on ESP8266.
    if (client->flags_.remove) {
      // Rare case: handle disconnection (don't increment - swapped element needs processing)
      this->remove_client_(client_index);
    } else {
      client_index++;
    }
  }
}

void APIServer::remove_client_(uint8_t client_index) {
  auto &client = this->clients_[client_index];

#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
  this->unregister_active_action_calls_for_connection(client.get());
#endif
  ESP_LOGV(TAG, "Remove connection %s", client->get_name());

#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
  // Save client info before closing socket and removal for the trigger
  char peername_buf[socket::SOCKADDR_STR_LEN];
  std::string client_name(client->get_name());
  std::string client_peername(client->get_peername_to(peername_buf));
#endif

  // Close socket now (was deferred from on_fatal_error to allow getpeername)
  client->helper_->close();

  // Swap-and-reset: move the removed client to the trailing slot and null it out so slots
  // [api_connection_count_, N) remain nullptr.
  const uint8_t last_index = this->api_connection_count_ - 1;
  if (client_index < last_index) {
    std::swap(this->clients_[client_index], this->clients_[last_index]);
  }
  // Drop the count before resetting the slot. reset() runs ~APIConnection(), which can reenter the
  // server (e.g. voice_assistant unsubscribes in its disconnect trigger, publishing entity state ->
  // on_*_update iterating active_clients()). Excluding the dying slot from the active range first
  // keeps that reentrant iteration from dereferencing the now-null slot.
  this->api_connection_count_--;
  this->clients_[last_index].reset();

  // Last client disconnected - set warning and start tracking for reboot timeout
  // (suppressed while provisioning is pending - see loop()).
  if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
    this->status_set_warning(LOG_STR("waiting for client connection"));
    this->last_connected_ = App.get_loop_component_start_time();
  }

#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
  // Fire trigger after client is removed so api.connected reflects the true state
  this->client_disconnected_trigger_.trigger(client_name, client_peername);
#endif
}

void __attribute__((flatten)) APIServer::accept_new_connections_() {
  while (true) {
    struct sockaddr_storage source_addr;
    socklen_t addr_len = sizeof(source_addr);

    auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
    if (!sock)
      break;

    char peername[socket::SOCKADDR_STR_LEN];
    sock->getpeername_to(peername);

    // Check if we're at the connection limit
    if (this->api_connection_count_ >= MAX_API_CONNECTIONS) {
      ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
      // Immediately close - socket destructor will handle cleanup
      sock.reset();
      continue;
    }

    ESP_LOGD(TAG, "Accept %s", peername);

    auto *conn = new APIConnection(std::move(sock), this);
    this->clients_[this->api_connection_count_++].reset(conn);
    conn->start();

    // First client connected - clear warning and update timestamp
    if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
      this->status_clear_warning();
      this->last_connected_ = App.get_loop_component_start_time();
    }
  }
}

void APIServer::dump_config() {
  char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
  ESP_LOGCONFIG(TAG,
                "Server:\n"
                "  Address: %s:%u\n"
                "  Listen backlog: %u\n"
                "  Max connections: %u",
                network::get_use_address_to(addr_buf), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
#ifdef USE_API_NOISE
  ESP_LOGCONFIG(TAG, "  Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
  if (!this->noise_ctx_.has_psk()) {
    ESP_LOGCONFIG(TAG, "  Supports encryption: YES");
  }
#else
  ESP_LOGCONFIG(TAG, "  Noise encryption: NO");
#endif
}

void APIServer::handle_disconnect(APIConnection *conn) {}

// Macro for controller update dispatch
#define API_DISPATCH_UPDATE(entity_type, entity_name) \
  void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
    if (obj->is_internal()) \
      return; \
    for (auto &c : this->active_clients()) { \
      if (c->flags_.state_subscription) \
        c->send_##entity_name##_state(obj); \
    } \
  }

#ifdef USE_BINARY_SENSOR
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
#endif

#ifdef USE_COVER
API_DISPATCH_UPDATE(cover::Cover, cover)
#endif

#ifdef USE_FAN
API_DISPATCH_UPDATE(fan::Fan, fan)
#endif

#ifdef USE_LIGHT
API_DISPATCH_UPDATE(light::LightState, light)
#endif

#ifdef USE_SENSOR
API_DISPATCH_UPDATE(sensor::Sensor, sensor)
#endif

#ifdef USE_SWITCH
API_DISPATCH_UPDATE(switch_::Switch, switch)
#endif

#ifdef USE_TEXT_SENSOR
API_DISPATCH_UPDATE(text_sensor::TextSensor, text_sensor)
#endif

#ifdef USE_CLIMATE
API_DISPATCH_UPDATE(climate::Climate, climate)
#endif

#ifdef USE_NUMBER
API_DISPATCH_UPDATE(number::Number, number)
#endif

#ifdef USE_DATETIME_DATE
API_DISPATCH_UPDATE(datetime::DateEntity, date)
#endif

#ifdef USE_DATETIME_TIME
API_DISPATCH_UPDATE(datetime::TimeEntity, time)
#endif

#ifdef USE_DATETIME_DATETIME
API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
#endif

#ifdef USE_TEXT
API_DISPATCH_UPDATE(text::Text, text)
#endif

#ifdef USE_SELECT
API_DISPATCH_UPDATE(select::Select, select)
#endif

#ifdef USE_LOCK
API_DISPATCH_UPDATE(lock::Lock, lock)
#endif

#ifdef USE_VALVE
API_DISPATCH_UPDATE(valve::Valve, valve)
#endif

#ifdef USE_MEDIA_PLAYER
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif

#ifdef USE_WATER_HEATER
API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
#endif

#ifdef USE_EVENT
void APIServer::on_event(event::Event *obj) {
  if (obj->is_internal())
    return;
  for (auto &c : this->active_clients()) {
    if (c->flags_.state_subscription)
      c->send_event(obj);
  }
}
#endif

#ifdef USE_UPDATE
// Update is a special case - the method is called on_update, not on_update_update
void APIServer::on_update(update::UpdateEntity *obj) {
  if (obj->is_internal())
    return;
  for (auto &c : this->active_clients()) {
    if (c->flags_.state_subscription)
      c->send_update_state(obj);
  }
}
#endif

#ifdef USE_ZWAVE_PROXY
void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
  // We could add code to manage a second subscription type, but, since this message type is
  //  very infrequent and small, we simply send it to all clients
  for (auto &c : this->active_clients())
    c->send_message(msg);
}
#endif

#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
                                               const std::vector<int32_t> *timings) {
  InfraredRFReceiveEvent resp{};
#ifdef USE_DEVICES
  resp.device_id = device_id;
#endif
  resp.key = key;
  resp.timings = timings;

  for (auto &c : this->active_clients())
    c->send_infrared_rf_receive_event(resp);
}
#endif

#ifdef USE_ALARM_CONTROL_PANEL
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
#endif

float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }

void APIServer::set_port(uint16_t port) { this->port_ = port; }

void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }

#ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
  bool has_subscriber = false;
  for (auto &client : this->active_clients()) {
    has_subscriber |= client->send_homeassistant_action(call);
  }
  if (!has_subscriber) {
    // Home Assistant subscribes to actions shortly *after* authenticating, so actions
    // fired right at connection time (on_client_connected, on_time_sync, ...) can
    // arrive before the subscription and are lost - warn instead of failing silently.
    ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s", call.is_event ? "event" : "action", call.service.c_str(),
             this->is_connected() ? "client has not subscribed to actions (yet)" : "no client connected");
  }
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) {
  this->action_response_callbacks_.push_back({call_id, std::move(callback)});
}

void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message) {
  for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
    if (it->call_id == call_id) {
      auto callback = std::move(it->callback);
      this->action_response_callbacks_.erase(it);
      ActionResponse response(success, error_message);
      callback(response);
      return;
    }
  }
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message,
                                       const uint8_t *response_data, size_t response_data_len) {
  for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
    if (it->call_id == call_id) {
      auto callback = std::move(it->callback);
      this->action_response_callbacks_.erase(it);
      ActionResponse response(success, error_message, response_data, response_data_len);
      callback(response);
      return;
    }
  }
}
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif  // USE_API_HOMEASSISTANT_SERVICES

#ifdef USE_API_HOMEASSISTANT_STATES
// Helper to add subscription (reduces duplication)
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
                                        std::function<void(StringRef)> &&f, bool once) {
  this->state_subs_.push_back(HomeAssistantStateSubscription{
      .entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
      // entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
  });
}

// Helper to add subscription with heap-allocated strings (reduces duplication)
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
                                        std::function<void(StringRef)> &&f, bool once) {
  HomeAssistantStateSubscription sub;
  // Allocate heap storage for the strings
  sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
  sub.entity_id = sub.entity_id_dynamic_storage->c_str();

  if (attribute.has_value()) {
    sub.attribute_dynamic_storage = std::make_unique<std::string>(std::move(attribute.value()));
    sub.attribute = sub.attribute_dynamic_storage->c_str();
  } else {
    sub.attribute = nullptr;
  }

  sub.callback = std::move(f);
  sub.once = once;
  this->state_subs_.push_back(std::move(sub));
}

// New const char* overload (for internal components - zero allocation)
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
                                               std::function<void(StringRef)> &&f) {
  this->add_state_subscription_(entity_id, attribute, std::move(f), false);
}

void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
                                         std::function<void(StringRef)> &&f) {
  this->add_state_subscription_(entity_id, attribute, std::move(f), true);
}

// std::string overload with StringRef callback (zero-allocation callback)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
                                               std::function<void(StringRef)> &&f) {
  this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}

void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
                                         std::function<void(StringRef)> &&f) {
  this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}

// Legacy helper: wraps std::string callback and delegates to StringRef version
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
                                        std::function<void(const std::string &)> &&f, bool once) {
  // Wrap callback to convert StringRef -> std::string, then delegate
  this->add_state_subscription_(std::move(entity_id), std::move(attribute),
                                std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
                                once);
}

// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
                                               std::function<void(const std::string &)> &&f) {
  this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}

void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
                                         std::function<void(const std::string &)> &&f) {
  this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}

const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
  return this->state_subs_;
}
#endif

uint16_t APIServer::get_port() const { return this->port_; }

void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }

#ifdef USE_API_NOISE
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
                                  const LogString *fail_log_msg, bool make_active) {
  if (!this->noise_pref_.save(&new_psk)) {
    ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
    return false;
  }
  // ensure it's written immediately
  if (!global_preferences->sync()) {
    ESP_LOGW(TAG, "Failed to sync preferences");
    return false;
  }
  ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
  if (make_active) {
    this->set_timeout(100, [this]() {
      // Re-read the PSK from preferences rather than capturing the 32-byte array
      // in the lambda (which would exceed std::function SBO and heap-allocate).
      if (!this->load_and_apply_noise_psk_()) {
        ESP_LOGW(TAG, "Failed to load saved PSK for activation");
        return;
      }
      ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
      for (auto &c : this->active_clients()) {
        DisconnectRequest req;
        c->send_message(req);
      }
    });
  }
  return true;
}

bool APIServer::load_and_apply_noise_psk_() {
  SavedNoisePsk saved{};
  if (!this->noise_pref_.load(&saved))
    return false;
  this->set_noise_psk(saved.psk);
  return true;
}

bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
#ifdef USE_API_NOISE_PSK_FROM_YAML
  // When PSK is set from YAML, this function should never be called
  // but if it is, reject the change
  ESP_LOGW(TAG, "Key set in YAML");
  return false;
#else
  auto &old_psk = this->noise_ctx_.get_psk();
  if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
    ESP_LOGW(TAG, "New PSK matches old");
    return true;
  }

  SavedNoisePsk new_saved_psk{psk};
  bool result = this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"),
                                        make_active);
#ifdef USE_PROVISIONING
  // The device now has a key; report provisioned so the provisioning window is
  // satisfied and the reboot timeout resumes normal operation.
  if (result && provisioning::global_provisioning_manager != nullptr) {
    provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_, true);
  }
#endif
  return result;
#endif
}
bool APIServer::clear_noise_psk(bool make_active) {
#ifdef USE_API_NOISE_PSK_FROM_YAML
  // When PSK is set from YAML, this function should never be called
  // but if it is, reject the change
  ESP_LOGW(TAG, "Key set in YAML");
  return false;
#else
  SavedNoisePsk empty_psk{};
  bool result = this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
                                        make_active);
#ifdef USE_PROVISIONING
  // The key was cleared; report unprovisioned so a subsequent reboot reopens the
  // provisioning window.
  if (result && provisioning::global_provisioning_manager != nullptr) {
    provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_, false);
  }
#endif
  return result;
#endif
}
#endif

#ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() {
  for (auto &client : this->active_clients()) {
    if (!client->flags_.remove && client->is_authenticated()) {
      client->send_time_request();
      return;  // Only request from one client to avoid clock conflicts
    }
  }
}
#endif

bool APIServer::is_connected_with_state_subscription() const {
  for (uint8_t i = 0; i < this->api_connection_count_; i++) {
    if (this->clients_[i]->flags_.state_subscription) {
      return true;
    }
  }
  return false;
}

#ifdef USE_LOGGER
void APIServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
  if (this->shutting_down_) {
    // Don't try to send logs during shutdown
    // as it could result in a recursion and
    // we would be filling a buffer we are trying to clear
    return;
  }
  for (auto &c : this->active_clients()) {
    if (!c->flags_.remove && c->get_log_subscription_level() >= level)
      c->try_send_log_message(level, tag, message, message_len);
  }
}
#endif

#ifdef USE_CAMERA
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
  for (auto &c : this->active_clients()) {
    if (!c->flags_.remove)
      c->set_camera_state(image);
  }
}
#endif

void APIServer::on_shutdown() {
  this->shutting_down_ = true;

  // Close the listening socket to prevent new connections
  this->destroy_socket_();

  // Change batch delay to 5ms for quick flushing during shutdown
  this->batch_delay_ = 5;

  // Send disconnect requests to all connected clients
  for (auto &c : this->active_clients()) {
    DisconnectRequest req;
    if (!c->send_message(req)) {
      // If we can't send the disconnect request directly (tx_buffer full),
      // schedule it at the front of the batch so it will be sent with priority
      c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE);
    }
  }
}

bool APIServer::teardown() {
  // If network is disconnected, no point trying to flush buffers
  if (!network::is_connected()) {
    return true;
  }
  this->loop();

  // Return true only when all clients have been torn down
  return this->api_connection_count_ == 0;
}

#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Timeout for action calls - matches aioesphomeapi client timeout (default 30s)
// Can be overridden via USE_API_ACTION_CALL_TIMEOUT_MS define for testing
#ifndef USE_API_ACTION_CALL_TIMEOUT_MS
#define USE_API_ACTION_CALL_TIMEOUT_MS 30000  // NOLINT
#endif

uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConnection *conn) {
  uint32_t action_call_id = this->next_action_call_id_++;
  // Handle wraparound (skip 0 as it means "no call")
  if (this->next_action_call_id_ == 0) {
    this->next_action_call_id_ = 1;
  }
  this->active_action_calls_.push_back({action_call_id, client_call_id, conn});

  // Schedule automatic cleanup after timeout (client will have given up by then)
  // Uses numeric ID overload to avoid heap allocation from str_sprintf
  this->set_timeout(action_call_id, USE_API_ACTION_CALL_TIMEOUT_MS, [this, action_call_id]() {
    ESP_LOGD(TAG, "Action call %" PRIu32 " timed out", action_call_id);
    this->unregister_active_action_call(action_call_id);
  });

  return action_call_id;
}

void APIServer::unregister_active_action_call(uint32_t action_call_id) {
  // Cancel the timeout for this action call (uses numeric ID overload)
  this->cancel_timeout(action_call_id);

  // Swap-and-pop is more efficient than remove_if for unordered vectors
  for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
    if (this->active_action_calls_[i].action_call_id == action_call_id) {
      std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
      this->active_action_calls_.pop_back();
      return;
    }
  }
}

void APIServer::unregister_active_action_calls_for_connection(APIConnection *conn) {
  // Remove all active action calls for disconnected connection using swap-and-pop
  for (size_t i = 0; i < this->active_action_calls_.size();) {
    if (this->active_action_calls_[i].connection == conn) {
      // Cancel the timeout for this action call (uses numeric ID overload)
      this->cancel_timeout(this->active_action_calls_[i].action_call_id);

      std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
      this->active_action_calls_.pop_back();
      // Don't increment i - need to check the swapped element
    } else {
      i++;
    }
  }
}

void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message) {
  for (auto &call : this->active_action_calls_) {
    if (call.action_call_id == action_call_id) {
      call.connection->send_execute_service_response(call.client_call_id, success, error_message);
      return;
    }
  }
  ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %" PRIu32, action_call_id);
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message,
                                     const uint8_t *response_data, size_t response_data_len) {
  for (auto &call : this->active_action_calls_) {
    if (call.action_call_id == action_call_id) {
      call.connection->send_execute_service_response(call.client_call_id, success, error_message, response_data,
                                                     response_data_len);
      return;
    }
  }
  ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %" PRIu32, action_call_id);
}
#endif  // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif  // USE_API_USER_DEFINED_ACTION_RESPONSES

}  // namespace esphome::api
#endif
