// Copyright 2022 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

#include "storage/v2/durability/durability.hpp"

#include <pwd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <cerrno>
#include <cstring>

#include <algorithm>
#include <tuple>
#include <utility>
#include <vector>

#include "storage/v2/durability/paths.hpp"
#include "storage/v2/durability/snapshot.hpp"
#include "storage/v2/durability/wal.hpp"
#include "utils/logging.hpp"
#include "utils/memory_tracker.hpp"
#include "utils/message.hpp"

namespace memgraph::storage::durability {

void VerifyStorageDirectoryOwnerAndProcessUserOrDie(const std::filesystem::path &storage_directory) {
  // Get the process user ID.
  auto process_euid = geteuid();

  // Get the data directory owner ID.
  struct stat statbuf;
  auto ret = stat(storage_directory.c_str(), &statbuf);
  if (ret != 0 && errno == ENOENT) {
    // The directory doesn't currently exist.
    return;
  }
  MG_ASSERT(ret == 0, "Couldn't get stat for '{}' because of: {} ({})", storage_directory, strerror(errno), errno);
  auto directory_owner = statbuf.st_uid;

  auto get_username = [](auto uid) {
    auto info = getpwuid(uid);
    if (!info) return std::to_string(uid);
    return std::string(info->pw_name);
  };

  auto user_process = get_username(process_euid);
  auto user_directory = get_username(directory_owner);
  MG_ASSERT(process_euid == directory_owner,
            "The process is running as user {}, but the data directory is "
            "owned by user {}. Please start the process as user {}!",
            user_process, user_directory, user_directory);
}

std::vector<SnapshotDurabilityInfo> GetSnapshotFiles(const std::filesystem::path &snapshot_directory,
                                                     const std::string_view uuid) {
  std::vector<SnapshotDurabilityInfo> snapshot_files;
  std::error_code error_code;
  if (utils::DirExists(snapshot_directory)) {
    for (const auto &item : std::filesystem::directory_iterator(snapshot_directory, error_code)) {
      if (!item.is_regular_file()) continue;
      try {
        auto info = ReadSnapshotInfo(item.path());
        if (uuid.empty() || info.uuid == uuid) {
          snapshot_files.emplace_back(item.path(), std::move(info.uuid), info.start_timestamp);
        }
      } catch (const RecoveryFailure &) {
        continue;
      }
    }
    MG_ASSERT(!error_code, "Couldn't recover data because an error occurred: {}!", error_code.message());
  }

  return snapshot_files;
}

std::optional<std::vector<WalDurabilityInfo>> GetWalFiles(const std::filesystem::path &wal_directory,
                                                          const std::string_view uuid,
                                                          const std::optional<size_t> current_seq_num) {
  if (!utils::DirExists(wal_directory)) return std::nullopt;

  std::vector<WalDurabilityInfo> wal_files;
  std::error_code error_code;
  for (const auto &item : std::filesystem::directory_iterator(wal_directory, error_code)) {
    if (!item.is_regular_file()) continue;
    try {
      auto info = ReadWalInfo(item.path());
      if ((uuid.empty() || info.uuid == uuid) && (!current_seq_num || info.seq_num < *current_seq_num))
        wal_files.emplace_back(info.seq_num, info.from_timestamp, info.to_timestamp, std::move(info.uuid),
                               std::move(info.epoch_id), item.path());
    } catch (const RecoveryFailure &e) {
      spdlog::warn("Failed to read {}", item.path());
      continue;
    }
  }
  MG_ASSERT(!error_code, "Couldn't recover data because an error occurred: {}!", error_code.message());

  std::sort(wal_files.begin(), wal_files.end());
  return std::move(wal_files);
}

// Function used to recover all discovered indices and constraints. The
// indices and constraints must be recovered after the data recovery is done
// to ensure that the indices and constraints are consistent at the end of the
// recovery process.
void RecoverIndicesAndConstraints(const RecoveredIndicesAndConstraints &indices_constraints, Indices *indices,
                                  Constraints *constraints, utils::SkipList<Vertex> *vertices) {
  spdlog::info("Recreating indices from metadata.");
  // Recover label indices.
  spdlog::info("Recreating {} label indices from metadata.", indices_constraints.indices.label.size());
  for (const auto &item : indices_constraints.indices.label) {
    if (!indices->label_index.CreateIndex(item, vertices->access()))
      throw RecoveryFailure("The label index must be created here!");
    spdlog::info("A label index is recreated from metadata.");
  }
  spdlog::info("Label indices are recreated.");

  // Recover label+property indices.
  spdlog::info("Recreating {} label+property indices from metadata.",
               indices_constraints.indices.label_property.size());
  for (const auto &item : indices_constraints.indices.label_property) {
    if (!indices->label_property_index.CreateIndex(item.first, item.second, vertices->access()))
      throw RecoveryFailure("The label+property index must be created here!");
    spdlog::info("A label+property index is recreated from metadata.");
  }
  spdlog::info("Label+property indices are recreated.");
  spdlog::info("Indices are recreated.");

  spdlog::info("Recreating constraints from metadata.");
  // Recover existence constraints.
  spdlog::info("Recreating {} existence constraints from metadata.", indices_constraints.constraints.existence.size());
  for (const auto &item : indices_constraints.constraints.existence) {
    auto ret = CreateExistenceConstraint(constraints, item.first, item.second, vertices->access());
    if (ret.HasError() || !ret.GetValue()) throw RecoveryFailure("The existence constraint must be created here!");
    spdlog::info("A existence constraint is recreated from metadata.");
  }
  spdlog::info("Existence constraints are recreated from metadata.");

  // Recover unique constraints.
  spdlog::info("Recreating {} unique constraints from metadata.", indices_constraints.constraints.unique.size());
  for (const auto &item : indices_constraints.constraints.unique) {
    auto ret = constraints->unique_constraints.CreateConstraint(item.first, item.second, vertices->access());
    if (ret.HasError() || ret.GetValue() != UniqueConstraints::CreationStatus::SUCCESS)
      throw RecoveryFailure("The unique constraint must be created here!");
    spdlog::info("A unique constraint is recreated from metadata.");
  }
  spdlog::info("Unique constraints are recreated from metadata.");
  spdlog::info("Constraints are recreated from metadata.");
}

std::optional<RecoveryInfo> RecoverData(const std::filesystem::path &snapshot_directory,
                                        const std::filesystem::path &wal_directory, std::string *uuid,
                                        std::string *epoch_id,
                                        std::deque<std::pair<std::string, uint64_t>> *epoch_history,
                                        utils::SkipList<Vertex> *vertices, utils::SkipList<Edge> *edges,
                                        std::atomic<uint64_t> *edge_count, NameIdMapper *name_id_mapper,
                                        Indices *indices, Constraints *constraints, Config::Items items,
                                        uint64_t *wal_seq_num) {
  utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception;
  spdlog::info("Recovering persisted data using snapshot ({}) and WAL directory ({}).", snapshot_directory,
               wal_directory);
  if (!utils::DirExists(snapshot_directory) && !utils::DirExists(wal_directory)) {
    spdlog::warn(utils::MessageWithLink("Snapshot or WAL directory don't exist, there is nothing to recover.",
                                        "https://memgr.ph/durability"));
    return std::nullopt;
  }

  auto snapshot_files = GetSnapshotFiles(snapshot_directory);

  RecoveryInfo recovery_info;
  RecoveredIndicesAndConstraints indices_constraints;
  std::optional<uint64_t> snapshot_timestamp;
  if (!snapshot_files.empty()) {
    spdlog::info("Try recovering from snapshot directory {}.", snapshot_directory);
    // Order the files by name
    std::sort(snapshot_files.begin(), snapshot_files.end());

    // UUID used for durability is the UUID of the last snapshot file.
    *uuid = snapshot_files.back().uuid;
    std::optional<RecoveredSnapshot> recovered_snapshot;
    for (auto it = snapshot_files.rbegin(); it != snapshot_files.rend(); ++it) {
      const auto &[path, file_uuid, _] = *it;
      if (file_uuid != *uuid) {
        spdlog::warn("The snapshot file {} isn't related to the latest snapshot file!", path);
        continue;
      }
      spdlog::info("Starting snapshot recovery from {}.", path);
      try {
        recovered_snapshot = LoadSnapshot(path, vertices, edges, epoch_history, name_id_mapper, edge_count, items);
        spdlog::info("Snapshot recovery successful!");
        break;
      } catch (const RecoveryFailure &e) {
        spdlog::warn("Couldn't recover snapshot from {} because of: {}.", path, e.what());
        continue;
      }
    }
    MG_ASSERT(recovered_snapshot,
              "The database is configured to recover on startup, but couldn't "
              "recover using any of the specified snapshots! Please inspect them "
              "and restart the database.");
    recovery_info = recovered_snapshot->recovery_info;
    indices_constraints = std::move(recovered_snapshot->indices_constraints);
    snapshot_timestamp = recovered_snapshot->snapshot_info.start_timestamp;
    *epoch_id = std::move(recovered_snapshot->snapshot_info.epoch_id);

    if (!utils::DirExists(wal_directory)) {
      RecoverIndicesAndConstraints(indices_constraints, indices, constraints, vertices);
      return recovered_snapshot->recovery_info;
    }
  } else {
    spdlog::info("No snapshot file was found, collecting information from WAL directory {}.", wal_directory);
    std::error_code error_code;
    if (!utils::DirExists(wal_directory)) return std::nullopt;
    // We use this smaller struct that contains only a subset of information
    // necessary for the rest of the recovery function.
    // Also, the struct is sorted primarily on the path it contains.
    struct WalFileInfo {
      explicit WalFileInfo(std::filesystem::path path, std::string uuid, std::string epoch_id)
          : path(std::move(path)), uuid(std::move(uuid)), epoch_id(std::move(epoch_id)) {}
      std::filesystem::path path;
      std::string uuid;
      std::string epoch_id;

      auto operator<=>(const WalFileInfo &) const = default;
    };
    std::vector<WalFileInfo> wal_files;
    for (const auto &item : std::filesystem::directory_iterator(wal_directory, error_code)) {
      if (!item.is_regular_file()) continue;
      try {
        auto info = ReadWalInfo(item.path());
        wal_files.emplace_back(item.path(), std::move(info.uuid), std::move(info.epoch_id));
      } catch (const RecoveryFailure &e) {
        continue;
      }
    }
    MG_ASSERT(!error_code, "Couldn't recover data because an error occurred: {}!", error_code.message());
    if (wal_files.empty()) {
      spdlog::warn(utils::MessageWithLink("No snapshot or WAL file found.", "https://memgr.ph/durability"));
      return std::nullopt;
    }
    std::sort(wal_files.begin(), wal_files.end());
    // UUID used for durability is the UUID of the last WAL file.
    // Same for the epoch id.
    *uuid = std::move(wal_files.back().uuid);
    *epoch_id = std::move(wal_files.back().epoch_id);
  }

  auto maybe_wal_files = GetWalFiles(wal_directory, *uuid);
  if (!maybe_wal_files) {
    spdlog::warn(
        utils::MessageWithLink("Couldn't get WAL file info from the WAL directory.", "https://memgr.ph/durability"));
    return std::nullopt;
  }

  // Array of all discovered WAL files, ordered by sequence number.
  auto &wal_files = *maybe_wal_files;

  // By this point we should have recovered from a snapshot, or we should have
  // found some WAL files to recover from in the above `else`. This is just a
  // sanity check to circumvent the following case: The database didn't recover
  // from a snapshot, the above `else` triggered to find the recovery UUID from
  // a WAL file. The above `else` has an early exit in case there are no WAL
  // files. Because we reached this point there must have been some WAL files
  // and we must have some WAL files after this second WAL directory iteration.
  MG_ASSERT(snapshot_timestamp || !wal_files.empty(),
            "The database didn't recover from a snapshot and didn't find any WAL "
            "files that match the last WAL file!");

  if (!wal_files.empty()) {
    spdlog::info("Checking WAL files.");
    {
      const auto &first_wal = wal_files[0];
      if (first_wal.seq_num != 0) {
        // We don't have all WAL files. We need to see whether we need them all.
        if (!snapshot_timestamp) {
          // We didn't recover from a snapshot and we must have all WAL files
          // starting from the first one (seq_num == 0) to be able to recover
          // data from them.
          LOG_FATAL(
              "There are missing prefix WAL files and data can't be "
              "recovered without them!");
        } else if (first_wal.from_timestamp >= *snapshot_timestamp) {
          // We recovered from a snapshot and we must have at least one WAL file
          // that has at least one delta that was created before the snapshot in order to
          // verify that nothing is missing from the beginning of the WAL chain.
          LOG_FATAL(
              "You must have at least one WAL file that contains at least one "
              "delta that was created before the snapshot file!");
        }
      }
    }
    std::optional<uint64_t> previous_seq_num;
    auto last_loaded_timestamp = snapshot_timestamp;
    spdlog::info("Trying to load WAL files.");
    for (auto &wal_file : wal_files) {
      if (previous_seq_num && (wal_file.seq_num - *previous_seq_num) > 1) {
        LOG_FATAL("You are missing a WAL file with the sequence number {}!", *previous_seq_num + 1);
      }
      previous_seq_num = wal_file.seq_num;

      if (wal_file.epoch_id != *epoch_id) {
        // This way we skip WALs finalized only because of role change.
        // We can also set the last timestamp to 0 if last loaded timestamp
        // is nullopt as this can only happen if the WAL file with seq = 0
        // does not contain any deltas and we didn't find any snapshots.
        if (last_loaded_timestamp) {
          epoch_history->emplace_back(wal_file.epoch_id, *last_loaded_timestamp);
        }
        *epoch_id = std::move(wal_file.epoch_id);
      }
      try {
        auto info = LoadWal(wal_file.path, &indices_constraints, last_loaded_timestamp, vertices, edges, name_id_mapper,
                            edge_count, items);
        recovery_info.next_vertex_id = std::max(recovery_info.next_vertex_id, info.next_vertex_id);
        recovery_info.next_edge_id = std::max(recovery_info.next_edge_id, info.next_edge_id);
        recovery_info.next_timestamp = std::max(recovery_info.next_timestamp, info.next_timestamp);

        recovery_info.last_commit_timestamp = info.last_commit_timestamp;
      } catch (const RecoveryFailure &e) {
        LOG_FATAL("Couldn't recover WAL deltas from {} because of: {}", wal_file.path, e.what());
      }

      if (recovery_info.next_timestamp != 0) {
        last_loaded_timestamp.emplace(recovery_info.next_timestamp - 1);
      }
    }
    // The sequence number needs to be recovered even though `LoadWal` didn't
    // load any deltas from that file.
    *wal_seq_num = *previous_seq_num + 1;

    spdlog::info("All necessary WAL files are loaded successfully.");
  }

  RecoverIndicesAndConstraints(indices_constraints, indices, constraints, vertices);
  return recovery_info;
}

}  // namespace memgraph::storage::durability
