#include "audio_reader.h"

#ifdef USE_ESP32

#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"

#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
#include "esp_crt_bundle.h"
#endif

namespace esphome {
namespace audio {

static const uint32_t READ_WRITE_TIMEOUT_MS = 20;

static const uint32_t CONNECTION_TIMEOUT_MS = 5000;
static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6;

static const size_t HTTP_STREAM_BUFFER_SIZE = 2048;

static const uint8_t MAX_REDIRECTIONS = 5;

static const char *const TAG = "audio_reader";

// Some common HTTP status codes - borrowed from http_request component accessed 20241224
enum HttpStatus {
  HTTP_STATUS_OK = 200,
  HTTP_STATUS_NO_CONTENT = 204,
  HTTP_STATUS_PARTIAL_CONTENT = 206,

  /* 3xx - Redirection */
  HTTP_STATUS_MULTIPLE_CHOICES = 300,
  HTTP_STATUS_MOVED_PERMANENTLY = 301,
  HTTP_STATUS_FOUND = 302,
  HTTP_STATUS_SEE_OTHER = 303,
  HTTP_STATUS_NOT_MODIFIED = 304,
  HTTP_STATUS_TEMPORARY_REDIRECT = 307,
  HTTP_STATUS_PERMANENT_REDIRECT = 308,

  /* 4XX - CLIENT ERROR */
  HTTP_STATUS_BAD_REQUEST = 400,
  HTTP_STATUS_UNAUTHORIZED = 401,
  HTTP_STATUS_FORBIDDEN = 403,
  HTTP_STATUS_NOT_FOUND = 404,
  HTTP_STATUS_METHOD_NOT_ALLOWED = 405,
  HTTP_STATUS_NOT_ACCEPTABLE = 406,
  HTTP_STATUS_LENGTH_REQUIRED = 411,

  /* 5xx - Server Error */
  HTTP_STATUS_INTERNAL_ERROR = 500
};

AudioReader::~AudioReader() { this->cleanup_connection_(); }

esp_err_t AudioReader::add_sink(const std::weak_ptr<RingBuffer> &output_ring_buffer) {
  if (current_audio_file_ != nullptr) {
    // A transfer buffer isn't ncessary for a local file
    this->file_ring_buffer_ = output_ring_buffer.lock();
    return ESP_OK;
  }

  if (this->output_transfer_buffer_ != nullptr) {
    this->output_transfer_buffer_->set_sink(output_ring_buffer);
    return ESP_OK;
  }

  return ESP_ERR_INVALID_STATE;
}

esp_err_t AudioReader::start(AudioFile *audio_file, AudioFileType &file_type) {
  file_type = AudioFileType::NONE;

  this->current_audio_file_ = audio_file;

  this->file_current_ = audio_file->data;
  file_type = audio_file->file_type;

  return ESP_OK;
}

esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
  file_type = AudioFileType::NONE;

  this->cleanup_connection_();

  if (uri.empty()) {
    return ESP_ERR_INVALID_ARG;
  }

  esp_http_client_config_t client_config = {};

  client_config.url = uri.c_str();
  client_config.cert_pem = nullptr;
  client_config.disable_auto_redirect = false;
  client_config.max_redirection_count = MAX_REDIRECTIONS;
  client_config.event_handler = http_event_handler;
  client_config.user_data = this;
  client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE;
  client_config.keep_alive_enable = true;
  client_config.timeout_ms = CONNECTION_TIMEOUT_MS;  // Shouldn't trigger watchdog resets if caller runs in a task

#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
  if (uri.find("https:") != std::string::npos) {
    client_config.crt_bundle_attach = esp_crt_bundle_attach;
  }
#endif

  this->client_ = esp_http_client_init(&client_config);

  if (this->client_ == nullptr) {
    return ESP_FAIL;
  }

  esp_err_t err = esp_http_client_open(this->client_, 0);

  if (err != ESP_OK) {
    ESP_LOGE(TAG, "Failed to open URL");
    this->cleanup_connection_();
    return err;
  }

  int64_t header_length = esp_http_client_fetch_headers(this->client_);
  uint8_t reattempt_count = 0;
  while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) {
    this->cleanup_connection_();
    if (header_length != -ESP_ERR_HTTP_EAGAIN) {
      // Serious error, no recovery
      return ESP_FAIL;
    } else {
      // Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available
      this->client_ = esp_http_client_init(&client_config);
      esp_http_client_open(this->client_, 0);
      header_length = esp_http_client_fetch_headers(this->client_);
      ++reattempt_count;
    }
  }

  if (header_length < 0) {
    ESP_LOGE(TAG, "Failed to fetch headers");
    this->cleanup_connection_();
    return ESP_FAIL;
  }

  int status_code = esp_http_client_get_status_code(this->client_);

  if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
    this->cleanup_connection_();
    return ESP_FAIL;
  }

  ssize_t redirect_count = 0;

  while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) {
    err = esp_http_client_open(this->client_, 0);
    if (err != ESP_OK) {
      this->cleanup_connection_();
      return ESP_FAIL;
    }

    header_length = esp_http_client_fetch_headers(this->client_);
    if (header_length < 0) {
      this->cleanup_connection_();
      return ESP_FAIL;
    }

    status_code = esp_http_client_get_status_code(this->client_);

    if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
      this->cleanup_connection_();
      return ESP_FAIL;
    }

    ++redirect_count;
  }

  if (this->audio_file_type_ == AudioFileType::NONE) {
    // Failed to determine the file type from the header, fallback to using the url
    char url[500];
    err = esp_http_client_get_url(this->client_, url, 500);
    if (err != ESP_OK) {
      this->cleanup_connection_();
      return err;
    }

    file_type = detect_audio_file_type(nullptr, url);
    if (file_type == AudioFileType::NONE) {
      this->cleanup_connection_();
      return ESP_ERR_NOT_SUPPORTED;
    }
  } else {
    file_type = this->audio_file_type_;
  }

  this->last_data_read_ms_ = millis();

  this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(this->buffer_size_);
  if (this->output_transfer_buffer_ == nullptr) {
    return ESP_ERR_NO_MEM;
  }

  return ESP_OK;
}

AudioReaderState AudioReader::read() {
  if (this->client_ != nullptr) {
    return this->http_read_();
  } else if (this->current_audio_file_ != nullptr) {
    return this->file_read_();
  }

  return AudioReaderState::FAILED;
}

esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
  // Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224
  AudioReader *this_reader = (AudioReader *) evt->user_data;

  switch (evt->event_id) {
    case HTTP_EVENT_ON_HEADER:
      if (strcasecmp(evt->header_key, "Content-Type") == 0) {
        this_reader->audio_file_type_ = detect_audio_file_type(evt->header_value, nullptr);
      }
      break;
    default:
      break;
  }
  return ESP_OK;
}

AudioReaderState AudioReader::file_read_() {
  size_t remaining_bytes = this->current_audio_file_->length - (this->file_current_ - this->current_audio_file_->data);
  if (remaining_bytes > 0) {
    size_t bytes_written = this->file_ring_buffer_->write_without_replacement(this->file_current_, remaining_bytes,
                                                                              pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
    this->file_current_ += bytes_written;

    return AudioReaderState::READING;
  }

  return AudioReaderState::FINISHED;
}

AudioReaderState AudioReader::http_read_() {
  this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);

  if (esp_http_client_is_complete_data_received(this->client_)) {
    if (this->output_transfer_buffer_->available() == 0) {
      this->cleanup_connection_();
      return AudioReaderState::FINISHED;
    }
  } else if (this->output_transfer_buffer_->free() > 0) {
    int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(),
                                            this->output_transfer_buffer_->free());

    if (received_len > 0) {
      this->output_transfer_buffer_->increase_buffer_length(received_len);
      this->last_data_read_ms_ = millis();
      return AudioReaderState::READING;
    } else if (received_len <= 0) {
      // HTTP read error
      if (received_len == -1) {
        // A true connection error occured, no chance at recovery
        this->cleanup_connection_();
        return AudioReaderState::FAILED;
      }

      // Read timed out, manually verify if it has been too long since the last successful read
      if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) {
        ESP_LOGE(TAG, "Timed out");
        this->cleanup_connection_();
        return AudioReaderState::FAILED;
      }

      delay(READ_WRITE_TIMEOUT_MS);
    }
  }

  return AudioReaderState::READING;
}

void AudioReader::cleanup_connection_() {
  if (this->client_ != nullptr) {
    esp_http_client_close(this->client_);
    esp_http_client_cleanup(this->client_);
    this->client_ = nullptr;
  }
}

}  // namespace audio
}  // namespace esphome

#endif
