/*
 * Copyright (c) 2019, 2025, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is designed to work with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms,
 * as designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have either included with
 * the program or referenced in the documentation.
 *
 * This program is distributed in the hope that it will be useful,  but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
 * the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "mysqlshdk/libs/storage/backend/http.h"

#include <algorithm>
#include <unordered_map>
#include <unordered_set>
#include <utility>

#include "mysqlshdk/libs/db/uri_encoder.h"
#include "mysqlshdk/libs/rest/error_codes.h"
#include "mysqlshdk/libs/rest/response.h"
#include "mysqlshdk/libs/rest/rest_service.h"
#include "mysqlshdk/libs/rest/retry_strategy.h"
#include "mysqlshdk/libs/storage/utils.h"
#include "mysqlshdk/libs/utils/utils_file.h"
#include "mysqlshdk/libs/utils/utils_general.h"
#include "mysqlshdk/libs/utils/utils_path.h"
#include "mysqlshdk/libs/utils/utils_string.h"

using Rest_service = mysqlshdk::rest::Rest_service;
using Headers = mysqlshdk::rest::Headers;
using Response = mysqlshdk::rest::Response;
using Response_error = mysqlshdk::rest::Response_error;

namespace mysqlshdk {
namespace storage {
namespace backend {

namespace {

Rest_service *get_rest_service(const Masked_string &base) {
  static thread_local std::unordered_map<std::string,
                                         std::unique_ptr<Rest_service>>
      services;

  auto &service = services[base.real()];

  if (!service) {
    // copy the values, Rest_service may outlive the original caller
    auto real = base.real();
    auto masked = base.masked();
    Masked_string copy = {std::move(real), std::move(masked)};
    service = std::make_unique<mysqlshdk::rest::Rest_service>(copy, true);

    // Timeout conditions:
    // - 30 seconds for HEAD and DELETE requests
    // - The rest will timeout if less than 1K is received in 60 seconds
    service->set_timeout(30, 1024, 60);
  }

  return service.get();
}

std::size_t span_uri_base(const std::string &uri) {
  if (uri.empty()) {
    throw std::logic_error("URI is empty");
  }

  const auto uri_no_scheme = mysqlshdk::storage::utils::strip_scheme(uri);

  if (uri.length() == uri_no_scheme.length()) {
    throw std::logic_error("URI does not have a scheme");
  }

  const auto pos = uri_no_scheme.rfind('/');

  if (std::string::npos == pos) {
    return uri.size();
  } else {
    return uri.size() - uri_no_scheme.size() + pos;
  }
}

std::string get_uri_base(const std::string &uri) {
  auto base = uri.substr(0, span_uri_base(uri));

  if ('/' != base.back()) {
    base += '/';
  }

  return base;
}

Masked_string get_uri_base(const Masked_string &uri) {
  return {get_uri_base(uri.real()), get_uri_base(uri.masked())};
}

std::string get_uri_path(const std::string &uri) {
  const auto pos = span_uri_base(uri);

  if (pos >= uri.length()) {
    return {};
  } else {
    return uri.substr(pos + 1);
  }
}

std::string get_uri_path(const Masked_string &uri) {
  return get_uri_path(uri.real());
}

inline void throw_on_error(const std::optional<rest::Response_error> &error,
                           const std::string &context,
                           const Masked_string &uri) {
  if (error.has_value()) {
    throw rest::to_exception(*error, context + " '" + uri.masked() + "': ");
  }
}

}  // namespace

Http_request::Http_request(Masked_string path, bool use_retry,
                           rest::Headers headers)
    : Request(std::move(path), std::move(headers)) {
  if (use_retry) {
    m_retry_strategy = mysqlshdk::rest::retry_terminal_errors_strategy();
    retry_strategy = m_retry_strategy.get();
  }
}

Http_object::Http_object(const std::string &full_path, bool use_retry)
    : Http_object(get_uri_base(full_path), get_uri_path(full_path), use_retry) {
}

Http_object::Http_object(const Masked_string &full_path, bool use_retry)
    : Http_object(get_uri_base(full_path), get_uri_path(full_path), use_retry) {
}

Http_object::Http_object(const Masked_string &base, const std::string &path,
                         bool use_retry)
    : m_base(base), m_path(path), m_use_retry(use_retry) {}

void Http_object::open(Mode m) {
  if (Mode::APPEND == m) {
    throw std::logic_error("Http_object::open(): append mode is not supported");
  }

  if (Mode::READ == m) {
    exists();
  }

  m_offset = 0;
  m_open_mode = m;
}

bool Http_object::is_open() const { return m_open_mode.has_value(); }

std::string Http_object::filename() const {
  std::string fname = full_path().real();
  {
    auto p = fname.find("#");
    if (p != std::string::npos) {
      fname = fname.substr(0, p);
    }
  }
  {
    auto p = fname.rfind("?");
    if (p != std::string::npos) {
      fname = fname.substr(0, p);
    }
  }
  {
    auto p = fname.rfind("/");
    if (p != std::string::npos) {
      fname = fname.substr(p + 1);
    }
  }
  return shcore::pctdecode(fname);
}

bool Http_object::exists() const {
  bool result = true;

  if (const auto error = fetch_file_size(); error.has_value()) {
    if (rest::Response::Status_code::NOT_FOUND == error->status_code()) {
      result = false;
    } else {
      throw_if_error(error, "Failed to access object");
    }
  }

  return result;
}

std::unique_ptr<IDirectory> Http_object::parent() const {
  return make_directory(m_base.real(), m_parent_config);
}

void Http_object::flush_on_close() { put(m_buffer.c_str(), m_buffer.length()); }

Rest_service *Http_object::rest_service() const {
  return get_rest_service(m_base);
}

Http_request Http_object::http_request(Masked_string path,
                                       rest::Headers headers) const {
  return Http_request(std::move(path), m_use_retry, std::move(headers));
}

void Http_object::put(const char *data, std::size_t length) {
  auto request =
      http_request(m_path, {{"content-type", "application/octet-stream"}});

  request.body = data;
  request.size = length;

  throw_if_error(rest_service()->put(&request).get_error(),
                 "Failed to put object");
}

void Http_object::close() {
  assert(is_open());

  if (Mode::WRITE == *m_open_mode) {
    flush_on_close();
    m_buffer.clear();
  }

  m_open_mode.reset();
  m_exists = false;
}

size_t Http_object::file_size() const {
  throw_if_error(fetch_file_size(), "Failed to fetch size of object");

  return m_file_size;
}

Masked_string Http_object::full_path() const {
  return {m_base.real() + m_path, m_base.masked() + m_path};
}

off64_t Http_object::seek(off64_t offset) {
  assert(is_open());

  if (Mode::READ == *m_open_mode) {
    m_offset = std::min<off64_t>(offset, file_size());
  }

  return m_offset;
}

ssize_t Http_object::read(void *buffer, size_t length) {
  assert(is_open() && Mode::READ == *m_open_mode);

  if (!(length > 0)) return 0;
  const off64_t fsize = file_size();
  if (m_offset >= fsize) return 0;

  const size_t first = m_offset;
  const size_t last_unbounded = m_offset + length - 1;
  // http range request is both sides inclusive
  const size_t last = std::min(file_size() - 1, last_unbounded);

  auto request =
      http_request(m_path, {{"range", "bytes=" + std::to_string(first) + "-" +
                                          std::to_string(last)}});
  auto response = rest_service()->get(&request);

  if (Response::Status_code::PARTIAL_CONTENT == response.status) {
    const auto &content = response.buffer;
    if (length < content.size()) {
      throw std::runtime_error("Got more data than expected");
    }
    std::copy(content.data(), content.data() + content.size(),
              reinterpret_cast<uint8_t *>(buffer));
    m_offset += content.size();
    return content.size();
  } else if (Response::Status_code::OK == response.status) {
    throw std::runtime_error("Range requests are not supported.");
  } else if (Response::Status_code::RANGE_NOT_SATISFIABLE == response.status) {
    throw std::runtime_error("Range request " + std::to_string(first) + "-" +
                             std::to_string(last) + " is out of bounds.");
  }

  throw_if_error(response.get_error(), "Failed to read object");

  return 0;
}

ssize_t Http_object::write(const void *buffer, size_t length) {
  assert(is_open() && Mode::WRITE == *m_open_mode);

  m_buffer.append(reinterpret_cast<const char *>(buffer), length);
  m_file_size += length;

  return length;
}

void Http_object::remove() {
  auto request =
      http_request(m_path, {{"content-type", "application/octet-stream"}});

  // truncate the file
  char body = 0;
  request.body = &body;
  request.size = 0;

  throw_if_error(rest_service()->put(&request).get_error(),
                 "Failed to truncate object");

  m_exists = false;
  m_file_size = 0;
}

void Http_object::throw_if_error(
    const std::optional<rest::Response_error> &error,
    const std::string &context) const {
  throw_on_error(error, context, full_path());
}

std::optional<rest::Response_error> Http_object::fetch_file_size() const {
  if (!m_exists) {
    auto request = http_request(m_path);
    auto response = rest_service()->head(&request);
    auto error = response.get_error();

    if (error.has_value()) {
      return error;
    } else {
      m_file_size = response.content_length();

      if (response.headers["Accept-Ranges"] != "bytes") {
        throw std::runtime_error(
            "Target server does not support partial requests.");
      }

      m_exists = true;
    }
  }

  return {};
}

void Http_object::set_full_path(const Masked_string &full_path) {
  m_base = get_uri_base(full_path);
  m_path = get_uri_path(full_path);
}

void Http_directory::init_rest(const Masked_string &url) { m_url = url; }

bool Http_directory::exists() const {
  throw std::logic_error("Http_directory::exists() - not implemented");
}

void Http_directory::create() {
  throw std::logic_error("Http_directory::create() - not implemented");
}

void Http_directory::remove() {
  throw std::logic_error("Http_directory::remove() - not implemented");
}

bool Http_directory::is_empty() const {
  const auto list = get_listing(1);
  return list.files.empty() && list.directories.empty();
}

Masked_string Http_directory::full_path() const { return m_url; }

Directory_listing Http_directory::get_listing(size_t limit) const {
  Directory_listing list;
  const auto rest = get_rest_service(m_url);
  const auto context = listing_context(limit);

  do {
    // This may throw but we just let it bubble up
    auto request = Http_request(context->get_url(), m_use_retry);
    auto response = rest->get(&request);

    throw_on_error(response.get_error(), "Could not access", m_url);

    try {
      auto parsed = context->parse(response.buffer.raw());

      list.files.merge(std::move(parsed.files));
      list.directories.merge(std::move(parsed.directories));
    } catch (const shcore::Exception &error) {
      std::string msg = "Error listing directory: ";
      msg.append(error.what());

      log_debug2("%s\n%s", msg.c_str(), response.buffer.data());

      throw shcore::Exception::runtime_error(msg);
    }
  } while (!context->is_complete());

  return list;
}

Directory_listing Http_directory::list(bool /*hidden_files*/) const {
  return get_listing(0);
}

std::unique_ptr<IFile> Http_directory::file(
    const std::string &name, const File_options & /*options*/) const {
  // file may outlive the directory, need to copy the values
  const auto full = full_path();
  auto real = full.real();
  auto masked = full.masked();
  Masked_string copy = {std::move(real), std::move(masked)};
  auto file = std::make_unique<Http_object>(copy, db::uri::pctencode_path(name),
                                            m_use_retry);
  file->set_parent_config(m_config);
  return file;
}

bool Http_directory::is_local() const { return false; }

std::string Http_directory::join_path(const std::string &a,
                                      const std::string &b) const {
  return utils::join_uri_path(a, b, true);
}

}  // namespace backend
}  // namespace storage
}  // namespace mysqlshdk
