/*
  Copyright <2018-2023> <scott.e.graves@protonmail.com>

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.
*/
#if defined(REPERTORY_ENABLE_S3)

#include "providers/s3/s3_provider.hpp"

#include "app_config.hpp"
#include "comm/curl/curl_comm.hpp"
#include "comm/i_http_comm.hpp"
#include "file_manager/i_file_manager.hpp"
#include "types/repertory.hpp"
#include "types/s3.hpp"
#include "types/startup_exception.hpp"
#include "utils/encryption.hpp"
#include "utils/error_utils.hpp"
#include "utils/file_utils.hpp"
#include "utils/path_utils.hpp"
#include "utils/polling.hpp"
#include "utils/rocksdb_utils.hpp"
#include "utils/string_utils.hpp"

namespace repertory {
namespace {
[[nodiscard]] auto
get_object_list(i_http_comm &client, const s3_config &config,
                std::string &response_data, long &response_code,
                std::optional<std::string> delimiter = std::nullopt,
                std::optional<std::string> prefix = std::nullopt) -> bool {
  curl::requests::http_get get{};
  get.allow_timeout = true;
  get.aws_service = "aws:amz:" + config.region + ":s3";
  get.path = '/';
  get.query["list-type"] = "2";
  if (delimiter.has_value() && not delimiter.value().empty()) {
    get.query["delimiter"] = delimiter.value();
  }
  if (prefix.has_value() && not prefix.value().empty()) {
    get.query["prefix"] = prefix.value();
  }
  get.response_handler = [&response_data](const data_buffer &data,
                                          long /*response_code*/) {
    response_data = std::string(data.begin(), data.end());
  };

  stop_type stop_requested{};
  return client.make_request(get, response_code, stop_requested);
}
} // namespace

s3_provider::s3_provider(app_config &config, i_http_comm &comm)
    : config_(config), comm_(comm) {
  comm_.enable_s3_path_style(config.get_s3_config().use_path_style);
}

auto s3_provider::add_if_not_found(api_file &file,
                                   const std::string &object_name) const
    -> api_error {
  api_meta_map meta{};
  if (get_item_meta(file.api_path, meta) == api_error::item_not_found) {
    auto err = create_path_directories(
        file.api_parent, utils::path::get_parent_api_path(object_name));
    if (err != api_error::success) {
      return err;
    }

    api_item_added_(false, file);
  }

  return api_error::success;
}

auto s3_provider::create_api_file(const std::string &path,
                                  const std::string &key, std::uint64_t size)
    -> api_file {
  api_file file{};
  file.accessed_date = utils::get_file_time_now();
  file.api_parent = utils::path::get_parent_api_path(file.api_path);
  file.api_path = utils::path::create_api_path(path);
  file.changed_date = utils::get_file_time_now();
  file.creation_date = utils::get_file_time_now();
  file.file_size = size;
  file.key = key;
  file.modified_date = utils::get_file_time_now();
  return file;
}

auto s3_provider::create_directory(const std::string &api_path,
                                   api_meta_map &meta) -> api_error {

  bool exists{};
  auto res = is_directory(api_path, exists);
  if (res != api_error::success) {
    return res;
  }
  if (exists) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path,
                                       api_error::directory_exists,
                                       "failed to create directory");
    return api_error::directory_exists;
  }

  res = is_file(api_path, exists);
  if (res != api_error::success) {
    return res;
  }
  if (exists) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path,
                                       api_error::item_exists,
                                       "failed to create directory");
    return api_error::item_exists;
  }

  try {
    const auto cfg = config_.get_s3_config();
    const auto is_encrypted = not cfg.encryption_token.empty();

    std::string key;
    if (is_encrypted) {
      res = get_item_meta(api_path, META_KEY, key);
      if (res != api_error::success) {
        return res;
      }
    }

    const auto object_name =
        utils::path::create_api_path(is_encrypted ? key : api_path);

    curl::requests::http_put_file put_file{};
    put_file.allow_timeout = true;
    put_file.aws_service = "aws:amz:" + cfg.region + ":s3";
    put_file.path = object_name + '/';

    stop_type stop_requested{false};
    long response_code{};
    if (not comm_.make_request(put_file, response_code, stop_requested)) {
      utils::error::raise_api_path_error(__FUNCTION__, api_path,
                                         api_error::comm_error,
                                         "failed to create directory");
      return api_error::comm_error;
    }

    if (response_code != http_error_codes::ok) {
      utils::error::raise_api_path_error(__FUNCTION__, api_path, response_code,
                                         "failed to create directory");
      return api_error::comm_error;
    }
  } catch (const std::exception &e) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path, e,
                                       "failed to create directory");
    return api_error::error;
  }

  meta[META_DIRECTORY] = utils::string::from_bool(true);
  return set_item_meta(api_path, meta);
}

auto s3_provider::create_directory_clone_source_meta(
    const std::string &source_api_path, const std::string &api_path)
    -> api_error {
  bool exists{};
  auto res = is_file(source_api_path, exists);
  if (res != api_error::success) {
    return res;
  }
  if (exists) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path,
                                       api_error::item_exists,
                                       "failed to create directory");
    return api_error::item_exists;
  }

  res = is_directory(api_path, exists);
  if (res != api_error::success) {
    return res;
  }
  if (exists) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path,
                                       api_error::directory_exists,
                                       "failed to create directory");
    return api_error::directory_exists;
  }

  res = is_file(api_path, exists);
  if (res != api_error::success) {
    return res;
  }
  if (exists) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path,
                                       api_error::item_exists,
                                       "failed to create directory");
    return api_error::item_exists;
  }

  api_meta_map meta{};
  res = get_item_meta(source_api_path, meta);
  if (res != api_error::success) {
    if (res == api_error::item_not_found) {
      res = api_error::directory_not_found;
    }
    utils::error::raise_api_path_error(__FUNCTION__, api_path, res,
                                       "failed to create directory");
    return res;
  }

  return create_directory(api_path, meta);
}

auto s3_provider::create_file(const std::string &api_path, api_meta_map &meta)
    -> api_error {
  bool exists{};
  auto res = is_directory(api_path, exists);
  if (res != api_error::success && res != api_error::item_not_found) {
    return res;
  }
  if (exists) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path,
                                       api_error::directory_exists,
                                       "failed to create file");
    return api_error::directory_exists;
  }

  res = is_file(api_path, exists);
  if (res != api_error::success && res != api_error::item_not_found) {
    return res;
  }
  if (exists) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path,
                                       api_error::item_exists,
                                       "failed to create file");
    return api_error::item_exists;
  }

  try {
    meta[META_DIRECTORY] = utils::string::from_bool(false);
    res = set_item_meta(api_path, meta);
    if (res != api_error::success) {
      utils::error::raise_api_path_error(__FUNCTION__, api_path, res,
                                         "failed to create file");
      return res;
    }

    stop_type stop_requested{false};
    return upload_file(api_path, meta[META_SOURCE], stop_requested);
  } catch (const std::exception &e) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path, e,
                                       "failed to create file");
  }

  return api_error::error;
}

auto s3_provider::create_path_directories(const std::string &api_path,
                                          const std::string &key) const
    -> api_error {
  if (api_path == "/") {
    return api_error::success;
  }

  const auto encryption_token = config_.get_s3_config().encryption_token;
  const auto is_encrypted = not encryption_token.empty();

  const auto path_parts = utils::string::split(api_path, '/', false);
  const auto key_parts = utils::string::split(key, '/', false);

  if (is_encrypted && key_parts.size() != path_parts.size()) {
    return api_error::error;
  }

  std::string cur_key;
  std::string cur_path;
  for (std::size_t i = 0U; i < path_parts.size(); i++) {
    if (is_encrypted) {
      cur_key = utils::path::create_api_path(
          utils::path::combine(cur_key, {key_parts.at(i)}));
    }
    cur_path = utils::path::create_api_path(
        utils::path::combine(cur_path, {path_parts.at(i)}));

    api_meta_map meta{};
    auto res = get_item_meta(cur_path, meta);
    if (res == api_error::item_not_found) {
      auto dir = create_api_file(cur_path, cur_key, 0U);
      api_item_added_(true, dir);
      continue;
    }

    if (res != api_error::success) {
      return res;
    }
  }

  return api_error::success;
}

auto s3_provider::decrypt_object_name(std::string &object_name) const
    -> api_error {
  auto parts = utils::string::split(object_name, '/', false);
  for (auto &part : parts) {
    auto err = utils::encryption::decrypt_file_name(
        config_.get_s3_config().encryption_token, part);
    if (err != api_error::success) {
      return err;
    }
  }

  object_name = utils::string::join(parts, '/');
  return api_error::success;
}

auto s3_provider::get_api_path_from_source(const std::string &source_path,
                                           std::string &api_path) const
    -> api_error {
  if (source_path.empty()) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path,
                                       api_error::item_not_found,
                                       "failed to source path from api path");
    return api_error::item_not_found;
  }

  auto iterator = std::unique_ptr<rocksdb::Iterator>(
      db_->NewIterator(rocksdb::ReadOptions()));
  for (iterator->SeekToFirst(); iterator->Valid(); iterator->Next()) {
    std::string current_source_path{};
    if (get_item_meta(iterator->key().ToString(), META_SOURCE,
                      current_source_path) != api_error::success) {
      continue;
    }

    if (current_source_path.empty()) {
      continue;
    }

    if (current_source_path == source_path) {
      api_path = iterator->key().ToString();
      return api_error::success;
    }
  }

  return api_error::item_not_found;
}

auto s3_provider::get_directory_item_count(const std::string &api_path) const
    -> std::uint64_t {}

auto s3_provider::get_directory_items(const std::string &api_path,
                                      directory_item_list &list) const
    -> api_error {}

auto s3_provider::get_file(const std::string &api_path, api_file &file) const
    -> api_error {
  try {
    bool is_encrypted{};
    std::string object_name;
    head_object_result result{};
    auto res = get_object_info(api_path, is_encrypted, object_name, result);
    if (res != api_error::success) {
      return res;
    }

    file.accessed_date = utils::get_file_time_now();
    file.api_path = api_path;
    file.api_parent = utils::path::get_parent_api_path(file.api_path);
    file.changed_date = utils::aws::format_time(result.last_modified);
    file.creation_date = utils::aws::format_time(result.last_modified);
    file.file_size =
        is_encrypted
            ? utils::encryption::encrypting_reader::calculate_decrypted_size(
                  result.content_length)
            : result.content_length;
    file.modified_date = utils::aws::format_time(result.last_modified);
    return add_if_not_found(file, object_name);
  } catch (const std::exception &e) {
    utils::error::raise_error(__FUNCTION__, e, "exception occurred");
  }

  return api_error::error;
}

auto s3_provider::get_file_list(api_file_list &list) const -> api_error {
  std::string response_data;
  long response_code{};
  if (not get_object_list(comm_, config_.get_s3_config(), response_data,
                          response_code)) {
    return api_error::comm_error;
  }

  if (response_code != http_error_codes::ok) {
    return api_error::comm_error;
  }

  pugi::xml_document doc;
  auto res = doc.load_string(response_data.c_str());
  if (res.status != pugi::xml_parse_status::status_ok) {
    return api_error::comm_error;
  }

  auto node_list = doc.select_nodes("/ListBucketResult/Contents");
  for (const auto &node : node_list) {
    auto api_path =
        std::string{node.node().select_node("Key").node().text().as_string()};
    if (not utils::string::ends_with(api_path, "/")) {
      auto is_encrypted = not config_.get_s3_config().encryption_token.empty();
      if (is_encrypted) {
        auto err = decrypt_object_name(api_path);
        if (err != api_error::success) {
          return err;
        }
      }

      auto size = node.node().select_node("Size").node().text().as_ullong();

      api_file file{};
      file.api_path = utils::path::create_api_path(api_path);
      file.api_parent = utils::path::get_parent_api_path(file.api_path);
      file.accessed_date = utils::get_file_time_now();
      file.changed_date = utils::convert_api_date(
          node.node().select_node("LastModified").node().text().as_string());
      file.creation_date = file.changed_date;
      file.file_size =
          is_encrypted
              ? utils::encryption::encrypting_reader::calculate_decrypted_size(
                    size)
              : size;
      file.key = is_encrypted ? utils::path::create_api_path(api_path) : "";
      file.modified_date = file.changed_date;

      auto err = add_if_not_found(file, api_path);
      if (err != api_error::success) {
        return err;
      }

      list.push_back(std::move(file));
    }
  }

  return api_error::success;
}

auto s3_provider::get_file_size(const std::string &api_path,
                                std::uint64_t &file_size) const -> api_error {
  bool exists{};
  auto res = is_directory(api_path, exists);
  if (res != api_error::success) {
    return res;
  }
  if (exists) {
    return api_error::directory_exists;
  }

  api_file file{};
  res = get_file(api_path, file);
  if (res != api_error::success) {
    return res;
  }

  file_size = file.file_size;
  return api_error::success;
}

auto s3_provider::get_filesystem_item(const std::string &api_path,
                                      bool directory,
                                      filesystem_item &fsi) const -> api_error {
  bool exists{};
  auto res = is_directory(api_path, exists);
  if (res != api_error::success) {
    return res;
  }
  if (directory && not exists) {
    return api_error::directory_not_found;
  }

  res = is_file(api_path, exists);
  if (res != api_error::success) {
    return res;
  }
  if (not directory && not exists) {
    return api_error::item_not_found;
  }

  api_meta_map meta{};
  res = get_item_meta(api_path, meta);
  if (res != api_error::success) {
    return res;
  }

  fsi.api_parent = utils::path::get_parent_api_path(api_path);
  fsi.api_path = api_path;
  fsi.directory = directory;
  fsi.size = fsi.directory ? 0U : utils::string::to_uint64(meta[META_SIZE]);
  fsi.source_path = meta[META_SOURCE];

  return api_error::success;
}

auto s3_provider::get_filesystem_item_and_file(const std::string &api_path,
                                               api_file &file,
                                               filesystem_item &fsi) const
    -> api_error {
  auto res = get_file(api_path, file);
  if (res != api_error::success) {
    return res;
  }

  api_meta_map meta{};
  res = get_item_meta(api_path, meta);
  if (res != api_error::success) {
    return res;
  }

  fsi.api_parent = utils::path::get_parent_api_path(api_path);
  fsi.api_path = api_path;
  fsi.directory = false;
  fsi.size = utils::string::to_uint64(meta[META_SIZE]);
  fsi.source_path = meta[META_SOURCE];

  return api_error::success;
}

auto s3_provider::get_filesystem_item_from_source_path(
    const std::string &source_path, filesystem_item &fsi) const -> api_error {
  std::string api_path{};
  auto res = get_api_path_from_source(source_path, api_path);
  if (res != api_error::success) {
    return res;
  }

  bool exists{};
  res = is_directory(api_path, exists);
  if (res != api_error::success) {
    return res;
  }
  if (exists) {
    return api_error::directory_exists;
  }

  return get_filesystem_item(api_path, false, fsi);
}

auto s3_provider::get_object_info(const std::string &api_path,
                                  bool &is_encrypted, std::string &object_name,
                                  head_object_result &result) const
    -> api_error {
  try {
    const auto cfg = config_.get_s3_config();
    is_encrypted = not cfg.encryption_token.empty();

    std::string key;
    if (is_encrypted) {
      auto res = get_item_meta(api_path, META_KEY, key);
      if (res != api_error::success) {
        return res;
      }
    }

    object_name = utils::path::create_api_path(is_encrypted ? key : api_path);

    curl::requests::http_head head{};
    head.allow_timeout = true;
    head.aws_service = "aws:amz:" + cfg.region + ":s3";
    head.path = object_name;
    head.response_headers = http_headers{};

    stop_type stop_requested{false};
    long response_code{};
    if (not comm_.make_request(head, response_code, stop_requested)) {
      return api_error::comm_error;
    }

    if (response_code == http_error_codes::not_found) {
      return api_error::item_not_found;
    }

    if (response_code != http_error_codes::ok) {
      return api_error::comm_error;
    }

    result.from_headers(head.response_headers.value());

    return api_error::success;
  } catch (const std::exception &e) {
    utils::error::raise_error(__FUNCTION__, e, "exception occurred");
  }

  return api_error::error;
}

auto s3_provider::get_pinned_files() const -> std::vector<std::string> {
  std::vector<std::string> ret{};

  auto iterator = std::unique_ptr<rocksdb::Iterator>(
      db_->NewIterator(rocksdb::ReadOptions()));
  for (iterator->SeekToFirst(); iterator->Valid(); iterator->Next()) {
    std::string pinned{};
    if (get_item_meta(iterator->key().ToString(), META_PINNED, pinned) !=
        api_error::success) {
      continue;
    }
    if (pinned.empty() || not utils::string::to_bool(pinned)) {
      continue;
    }
    ret.emplace_back(iterator->key().ToString());
  }

  return ret;
}

auto s3_provider::get_item_meta(const std::string &api_path,
                                api_meta_map &meta) const -> api_error {
  std::string meta_value{};
  db_->Get(rocksdb::ReadOptions(), api_path, &meta_value);
  if (meta_value.empty()) {
    return api_error::item_not_found;
  }

  try {
    meta = json::parse(meta_value).get<api_meta_map>();

    return api_error::success;
  } catch (const std::exception &e) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path, e,
                                       "failed to get item meta");
  }

  return api_error::error;
}

auto s3_provider::get_item_meta(const std::string &api_path,
                                const std::string &key,
                                std::string &value) const -> api_error {
  std::string meta_value{};
  db_->Get(rocksdb::ReadOptions(), api_path, &meta_value);
  if (meta_value.empty()) {
    return api_error::item_not_found;
  }

  try {
    value = json::parse(meta_value)[key];
    return api_error::success;
  } catch (const std::exception &e) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path, e,
                                       "failed to get item meta");
  }

  return api_error::error;
}

auto s3_provider::get_total_drive_space() const -> std::uint64_t {}

auto s3_provider::get_total_item_count() const -> std::uint64_t {}

auto s3_provider::get_used_drive_space() const -> std::uint64_t {}

auto s3_provider::is_directory(const std::string &api_path, bool &exists) const
    -> api_error {
  if (api_path == "/") {
    exists = true;
    return api_error::success;
  }

  exists = false;

  try {
    bool is_encrypted{};
    std::string object_name;
    head_object_result result{};
    auto res = get_object_info(utils::path::create_api_path(api_path) + '/',
                               is_encrypted, object_name, result);
    if (res != api_error::item_not_found && res != api_error::success) {
      return res;
    }
    exists = res != api_error::item_not_found;
    return api_error::success;
  } catch (const std::exception &e) {
    utils::error::raise_error(__FUNCTION__, e, "exception occurred");
  }

  return api_error::error;
}

auto s3_provider::is_file(const std::string &api_path, bool &exists) const
    -> api_error {
  if (api_path == "/") {
    return api_error::success;
  }

  exists = false;

  try {
    bool is_encrypted{};
    std::string object_name;
    head_object_result result{};
    auto res = get_object_info(utils::path::create_api_path(api_path),
                               is_encrypted, object_name, result);
    if (res != api_error::item_not_found && res != api_error::success) {
      return res;
    }
    exists = res != api_error::item_not_found;
    return api_error::success;
  } catch (const std::exception &e) {
    utils::error::raise_error(__FUNCTION__, e, "exception occurred");
  }

  return api_error::error;
}

auto s3_provider::is_file_writeable(const std::string &api_path) const -> bool {
  bool exists{};
  auto res = is_directory(api_path, exists);
  if (res != api_error::success) {
    return false;
  }

  return not exists;
}

auto s3_provider::is_online() const -> bool {
  // TODO implement this
  return true;
}

auto s3_provider::read_file_bytes(const std::string &api_path, std::size_t size,
                                  std::uint64_t offset, data_buffer &data,
                                  stop_type &stop_requested) -> api_error {
  try {
    const auto cfg = config_.get_s3_config();
    const auto is_encrypted = not cfg.encryption_token.empty();
    std::string key;
    if (is_encrypted) {
      auto res = get_item_meta(api_path, META_KEY, key);
      if (res != api_error::success) {
        return res;
      }
    }

    const auto object_name =
        utils::path::create_api_path(is_encrypted ? key : api_path);

    const auto read_bytes = [this, &cfg, &object_name, &stop_requested](
                                std::size_t read_size, std::size_t read_offset,
                                data_buffer &read_buffer) -> api_error {
      curl::requests::http_get get{};
      get.aws_service = "aws:amz:" + cfg.region + ":s3";
      get.headers["response-content-type"] = "binary/octet-stream";
      get.path = object_name;
      get.range = {{
          read_offset,
          read_offset + read_size - 1U,
      }};
      get.response_handler = [&read_buffer](const data_buffer &response_data,
                                            long /*response_code*/) {
        read_buffer = response_data;
      };

      long response_code{};
      if (not comm_.make_request(get, response_code, stop_requested)) {
        return api_error::comm_error;
      }

      if (response_code == http_error_codes::not_found) {
        return api_error::item_not_found;
      }

      if (response_code < http_error_codes::ok ||
          response_code >= http_error_codes::multiple_choices) {
        return api_error::comm_error;
      }

      return api_error::success;
    };

    if (is_encrypted) {
      std::string temp;
      auto res = get_item_meta(api_path, META_SIZE, temp);
      if (res != api_error::success) {
        return res;
      }
      const auto total_size = utils::string::to_uint64(temp);

      return utils::encryption::read_encrypted_range(
          {offset, offset + size - 1U},
          utils::encryption::generate_key(cfg.encryption_token),
          [&](data_buffer &ct_buffer, std::uint64_t start_offset,
              std::uint64_t end_offset) -> api_error {
            return read_bytes((end_offset - start_offset + 1U), start_offset,
                              ct_buffer);
          },
          total_size, data);
    }

    return read_bytes(size, offset, data);
  } catch (const std::exception &e) {
    utils::error::raise_error(__FUNCTION__, e, "exception occurred");
  }

  return api_error::error;
}

void s3_provider::remove_deleted_files() {
  struct removed_item {
    std::string api_path{};
    bool directory{};
    std::string source_path{};
  };

  api_file_list list{};
  auto res = get_file_list(list);
  if (res != api_error::success) {
    utils::error::raise_error(__FUNCTION__, res, "failed to get file list");
    return;
  }

  std::vector<removed_item> removed_list{};
  auto iterator = std::unique_ptr<rocksdb::Iterator>(
      db_->NewIterator(rocksdb::ReadOptions()));
  for (iterator->SeekToFirst(); iterator->Valid(); iterator->Next()) {
    api_meta_map meta{};
    if (get_item_meta(iterator->key().ToString(), meta) == api_error::success) {
      if (utils::string::to_bool(meta[META_DIRECTORY])) {
        bool exists{};
        if (is_directory(iterator->key().ToString(), exists) !=
            api_error::success) {
          continue;
        }
        if (not exists) {
          removed_list.emplace_back(
              removed_item{iterator->key().ToString(), true, ""});
        }
        continue;
      }

      bool exists{};
      if (is_file(iterator->key().ToString(), exists) != api_error::success) {
        continue;
      }
      if (not exists) {
        removed_list.emplace_back(
            removed_item{iterator->key().ToString(), false, meta[META_SOURCE]});
      }
    }
  }

  for (const auto &item : removed_list) {
    if (not item.directory) {
      if (utils::file::is_file(item.source_path)) {
        const auto orphaned_directory =
            utils::path::combine(config_.get_data_directory(), {"orphaned"});
        if (utils::file::create_full_directory_path(orphaned_directory)) {
          const auto parts = utils::string::split(item.api_path, '/', false);
          const auto orphaned_file = utils::path::combine(
              orphaned_directory,
              {utils::path::strip_to_file_name(item.source_path) + '_' +
               parts[parts.size() - 1U]});

          event_system::instance().raise<orphaned_file_detected>(
              item.source_path);
          if (utils::file::reset_modified_time(item.source_path) &&
              utils::file::copy_file(item.source_path, orphaned_file)) {
            event_system::instance().raise<orphaned_file_processed>(
                item.source_path, orphaned_file);
          } else {
            event_system::instance().raise<orphaned_file_processing_failed>(
                item.source_path, orphaned_file,
                std::to_string(utils::get_last_error_code()));
          }
        } else {
          utils::error::raise_error(
              __FUNCTION__, std::to_string(utils::get_last_error_code()),
              "failed to create orphaned director|sp|" + orphaned_directory);
          continue;
        }
      }

      if (fm_->evict_file(item.api_path)) {
        db_->Delete(rocksdb::WriteOptions(), item.api_path);
        event_system::instance().raise<file_removed_externally>(
            item.api_path, item.source_path);
      }
    }
  }

  for (const auto &item : removed_list) {
    if (item.directory) {
      db_->Delete(rocksdb::WriteOptions(), item.api_path);
      event_system::instance().raise<directory_removed_externally>(
          item.api_path, item.source_path);
    }
  }
}

auto s3_provider::remove_directory(const std::string &api_path) -> api_error {}

auto s3_provider::remove_file(const std::string &api_path) -> api_error {}

auto s3_provider::remove_item_meta(const std::string &api_path,
                                   const std::string &key) -> api_error {
  api_meta_map meta{};
  auto res = get_item_meta(api_path, meta);
  if (res != api_error::success) {
    return res;
  }

  meta.erase(key);

  auto status = db_->Put(rocksdb::WriteOptions(), api_path, json(meta).dump());
  if (not status.ok()) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path, status.code(),
                                       "failed to remove item meta");
    return api_error::error;
  }

  return api_error::success;
}

auto s3_provider::rename_file(const std::string & /* from_api_path */,
                              const std::string & /* to_api_path */)
    -> api_error {
  return api_error::not_implemented;
}

auto s3_provider::set_item_meta(const std::string &api_path,
                                const std::string &key,
                                const std::string &value) -> api_error {
  json meta_json{};
  std::string meta_value{};
  db_->Get(rocksdb::ReadOptions(), api_path, &meta_value);
  if (not meta_value.empty()) {
    try {
      meta_json = json::parse(meta_value);
    } catch (const std::exception &e) {
      utils::error::raise_api_path_error(__FUNCTION__, api_path, e,
                                         "failed to set item meta");
      return api_error::error;
    }
  }

  meta_json[key] = value;

  auto status = db_->Put(rocksdb::WriteOptions(), api_path, meta_json.dump());
  if (not status.ok()) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path, status.code(),
                                       "failed to set item meta");
    return api_error::error;
  }

  return api_error::success;
}

auto s3_provider::set_item_meta(const std::string &api_path,
                                const api_meta_map &meta) -> api_error {
  json meta_json{};
  std::string meta_value{};
  db_->Get(rocksdb::ReadOptions(), api_path, &meta_value);
  if (not meta_value.empty()) {
    try {
      meta_json = json::parse(meta_value);
    } catch (const std::exception &e) {
      utils::error::raise_api_path_error(__FUNCTION__, api_path, e,
                                         "failed to set item meta");
      return api_error::error;
    }
  }

  for (const auto &item : meta) {
    meta_json[item.first] = item.second;
  }

  auto status = db_->Put(rocksdb::WriteOptions(), api_path, meta_json.dump());
  if (not status.ok()) {
    utils::error::raise_api_path_error(__FUNCTION__, api_path, status.code(),
                                       "failed to set item meta");
    return api_error::error;
  }

  return api_error::success;
}

auto s3_provider::start(api_item_added_callback api_item_added,
                        i_file_manager *fm) -> bool {
  event_system::instance().raise<service_started>("s3_provider");
  utils::db::create_rocksdb(config_, DB_NAME, db_);

  api_item_added_ = api_item_added;
  fm_ = fm;

  api_meta_map meta{};
  if (get_item_meta("/", meta) == api_error::item_not_found) {
    auto dir = create_api_file("/", "", 0U);
    api_item_added_(true, dir);
  }

  auto online{false};
  auto unmount_requested{false};
  {
    repertory::event_consumer consumer(
        "unmount_requested",
        [&unmount_requested](const event &) { unmount_requested = true; });
    for (std::uint16_t i = 0U; not online && not unmount_requested &&
                               (i < config_.get_online_check_retry_secs());
         i++) {
      online = is_online();
      if (not online) {
        auto cfg = curl_comm::create_host_config(
            config_.get_s3_config(), config_.get_s3_config().use_path_style);
        event_system::instance().raise<provider_offline>(cfg.host_name_or_ip,
                                                         cfg.api_port);
        std::this_thread::sleep_for(1s);
      }
    }
  }

  if (online && not unmount_requested) {
    polling::instance().set_callback({"check_deleted", polling::frequency::low,
                                      [this]() { remove_deleted_files(); }});
    return true;
  }

  return false;
}

void s3_provider::stop() {
  event_system::instance().raise<service_shutdown_begin>("s3_provider");
  polling::instance().remove_callback("check_deleted");
  db_.reset();
  event_system::instance().raise<service_shutdown_end>("s3_provider");
}

auto s3_provider::upload_file(const std::string &api_path,
                              const std::string &source_path,
                              stop_type &stop_requested) -> api_error {
  event_system::instance().raise<provider_upload_begin>(api_path, source_path);

  const auto notify_end = [&api_path,
                           &source_path](api_error error) -> api_error {
    event_system::instance().raise<provider_upload_end>(api_path, source_path,
                                                        error);
    return error;
  };
  try {
    const auto cfg = config_.get_s3_config();
    const auto is_encrypted = not cfg.encryption_token.empty();

    std::string key;
    if (is_encrypted) {
      auto res = get_item_meta(api_path, META_KEY, key);
      if (res != api_error::success) {
        return notify_end(res);
      }
    }

    const auto object_name =
        utils::path::create_api_path(is_encrypted ? key : api_path);

    curl::requests::http_put_file put_file{};
    put_file.aws_service = "aws:amz:" + cfg.region + ":s3";
    put_file.path = object_name;
    put_file.source_path = source_path;

    if (is_encrypted) {
      static stop_type no_stop{false};

      put_file.reader = std::make_shared<utils::encryption::encrypting_reader>(
          object_name, source_path, no_stop, cfg.encryption_token, -1);
    }

    long response_code{};
    if (not comm_.make_request(put_file, response_code, stop_requested)) {
      return notify_end(api_error::comm_error);
    }

    if (response_code != http_error_codes::ok) {
      return notify_end(api_error::comm_error);
    }

    return notify_end(api_error::success);
  } catch (const std::exception &e) {
    utils::error::raise_error(__FUNCTION__, e, "exception occurred");
  }

  return notify_end(api_error::error);
}
} // namespace repertory

#endif // REPERTORY_ENABLE_S3
