/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "cuttlefish/host/libs/command_util/snapshot_utils.h"

#include <unistd.h>
#include <utime.h>

#include <cstdlib>
#include <cstring>
#include <string>
#include <unordered_map>

#include "absl/log/log.h"
#include "absl/strings/strip.h"
#include "android-base/file.h"

#include "cuttlefish/common/libs/fs/shared_fd.h"
#include "cuttlefish/common/libs/utils/environment.h"
#include "cuttlefish/common/libs/utils/files.h"
#include "cuttlefish/common/libs/utils/json.h"
#include "cuttlefish/posix/readlink.h"
#include "cuttlefish/posix/symlink.h"
#include "cuttlefish/result/result.h"

namespace cuttlefish {
namespace {

bool IsFifo(const struct stat& file_stat) {
  return S_ISFIFO(file_stat.st_mode);
}

bool IsSocket(const struct stat& file_stat) {
  return S_ISSOCK(file_stat.st_mode);
}

bool IsSymlink(const struct stat& file_stat) {
  return S_ISLNK(file_stat.st_mode);
}

bool IsRegular(const struct stat& file_stat) {
  return S_ISREG(file_stat.st_mode);
}

// assumes that src_dir_path and dest_dir_path exist and both are
// existing directories or links to the directories. Also they are
// different directories.
Result<void> CopyDirectoryImpl(
    const std::string& src_dir_path, const std::string& dest_dir_path,
    const std::function<bool(const std::string&)>& predicate) {
  // create an empty dest_dir_path with the same permission as src_dir_path
  // and then, recursively copy the contents
  VLOG(0) << "Making sure " << dest_dir_path
          << " exists and is effectively a directory.";
  CF_EXPECTF(EnsureDirectoryExists(dest_dir_path),
             "Directory {} cannot to be created; it does not exist, either.",
             dest_dir_path);
  const auto src_contents = CF_EXPECT(DirectoryContents(src_dir_path));
  for (const auto& src_base_path : src_contents) {
    if (!predicate(src_dir_path + "/" + src_base_path)) {
      continue;
    }
    std::string src_path = src_dir_path + "/" + src_base_path;
    std::string dest_path = dest_dir_path + "/" + src_base_path;

    VLOG(0) << "Handling... " << src_path;

    struct stat src_stat;
    CF_EXPECTF(lstat(src_path.data(), &src_stat) != -1, "Failed in lstat({})",
               src_path);
    if (IsSymlink(src_stat)) {
      std::string target =
          CF_EXPECTF(ReadLink(src_path), "Readlink failed for {}", src_path);
      VLOG(0) << "Creating link from " << dest_path << " to " << target;
      if (FileExists(dest_path, /* follow_symlink */ false)) {
        CF_EXPECTF(RemoveFile(dest_path), "Failed to unlink/remove file \"{}\"",
                   dest_path);
      }
      CF_EXPECT(Symlink(target, dest_path));
      continue;
    }

    if (IsFifo(src_stat) || IsSocket(src_stat)) {
      VLOG(0) << "Ignoring a named pipe or socket " << src_path;
      continue;
    }

    if (DirectoryExists(src_path)) {
      VLOG(0) << "Recursively calling CopyDirectoryImpl(" << src_path << ", "
              << dest_path << ")";
      CF_EXPECT(CopyDirectoryImpl(src_path, dest_path, predicate));
      VLOG(0) << "Returned from Recursive call CopyDirectoryImpl(" << src_path
              << ", " << dest_path << ")";
      continue;
    }

    CF_EXPECTF(IsRegular(src_stat),
               "File {} must be directory, link, socket, pipe or regular."
               "{} is none of those",
               src_path, src_path);

    CF_EXPECTF(Copy(src_path, dest_path), "Copy from {} to {} failed", src_path,
               dest_path);

    auto dest_fd = SharedFD::Open(dest_path, O_RDONLY);
    CF_EXPECT(dest_fd->IsOpen(), "Failed to open \"" << dest_path << "\"");
    // Copy the mtime from the src file. The mtime of the disk image files can
    // be important because we later validate that the disk overlays are not
    // older than the disk components.
    const struct timespec times[2] = {
#if defined(__APPLE__)
      src_stat.st_atimespec,
      src_stat.st_mtimespec
#else
      src_stat.st_atim,
      src_stat.st_mtim,
#endif
    };
    if (dest_fd->Futimens(times) != 0) {
      return CF_ERR("futimens(\""
                    << dest_path << "\", ...) failed: " << dest_fd->StrError());
    }
  }
  return {};
}

/*
 * Returns Realpath(path) if successful, or the absolute path of "path"
 *
 * If emulating absolute path fails, "path" is returned as is.
 */
std::string RealpathOrSelf(const std::string& path) {
  Result<std::string> output = RealPath(path);
  if (output.ok()) {
    return *output;
  }
  auto absolute_path = RealPath(path);
  return absolute_path.ok() ? *absolute_path : path;
}

}  // namespace

Result<void> CopyDirectoryRecursively(
    const std::string& src_dir_path, const std::string& dest_dir_path,
    const bool verify_dest_dir_empty,
    std::function<bool(const std::string&)> predicate) {
  CF_EXPECTF(FileExists(src_dir_path),
             "A file/directory \"{}\" does not exist.", src_dir_path);
  CF_EXPECTF(DirectoryExists(src_dir_path), "\"{}\" is not a directory.",
             src_dir_path);
  if (verify_dest_dir_empty) {
    CF_EXPECTF(!FileExists(dest_dir_path, /* follow symlink */ false),
               "Delete the destination directory \"{}\" first", dest_dir_path);
  }

  std::string dest_final_target = RealpathOrSelf(dest_dir_path);
  std::string src_final_target = RealpathOrSelf(src_dir_path);
  if (dest_final_target == src_final_target) {
    VLOG(0) << "\"" << src_dir_path << "\" and \"" << dest_dir_path
            << "\" are effectively the same.";
    return {};
  }

  LOG(INFO) << "Copy from \"" << src_final_target << "\" to \""
            << dest_final_target << "\"";

  /**
   * On taking snapshot, we should delete dest_dir first. On Restoring,
   * we don't delete the runtime directory, eventually. We could, however,
   * start with deleting it.
   */
  CF_EXPECT(CopyDirectoryImpl(src_final_target, dest_final_target, predicate));
  return {};
}

Result<std::string> InstanceGuestSnapshotPath(const Json::Value& meta_json,
                                              const std::string& instance_id) {
  CF_EXPECTF(meta_json.isMember(kSnapshotPathField),
             "The given json is missing : {}", kSnapshotPathField);
  const std::string snapshot_path = meta_json[kSnapshotPathField].asString();

  const std::vector<std::string> guest_snapshot_path_selectors{
      kGuestSnapshotField, instance_id};
  const auto guest_snapshot_dir = CF_EXPECTF(
      GetValue<std::string>(meta_json, guest_snapshot_path_selectors),
      "root[\"{}\"][\"{}\"] is missing in \"{}\"", kGuestSnapshotField,
      instance_id, kMetaInfoJsonFileName);
  auto snapshot_path_direct_parent = snapshot_path + "/" + guest_snapshot_dir;
  VLOG(0) << "Returning snapshot path : " << snapshot_path_direct_parent;
  return snapshot_path_direct_parent;
}

Result<Json::Value> CreateMetaInfo(const CuttlefishConfig& cuttlefish_config,
                                   const std::string& snapshot_path) {
  Json::Value meta_info;
  meta_info[kSnapshotPathField] = snapshot_path;

  const auto cuttlefish_home = StringFromEnv("HOME", "");
  CF_EXPECT(!cuttlefish_home.empty(),
            "\"HOME\" environment variable must be set.");
  meta_info[kCfHomeField] = cuttlefish_home;

  const auto instances = cuttlefish_config.Instances();
  // "id" -> relative path of instance_dir from cuttlefish_home
  //         + kGuestSnapshotField
  // e.g. "2" -> cuttlefish/instances/cvd-2/guest_snapshot
  std::unordered_map<std::string, std::string>
      id_to_relative_guest_snapshot_dir;
  for (const auto& instance : instances) {
    const std::string instance_snapshot_dir =
        instance.instance_dir() + "/" + kGuestSnapshotField;
    std::string_view sv_relative_path(instance_snapshot_dir);

    CF_EXPECTF(absl::ConsumePrefix(&sv_relative_path, cuttlefish_home),
               "Instance Guest Snapshot Directory \"{}\""
               "is not a subdirectory of \"{}\"",
               instance_snapshot_dir, cuttlefish_home);
    if (!sv_relative_path.empty() && sv_relative_path.at(0) == '/') {
      sv_relative_path.remove_prefix(1);
    }
    id_to_relative_guest_snapshot_dir[instance.id()] =
        std::string(sv_relative_path);
  }

  Json::Value snapshot_mapping;
  // 2 -> cuttlefish/instances/cvd-2
  // relative path to cuttlefish_home
  for (const auto& [id_str, relative_guest_snapshot_dir] :
       id_to_relative_guest_snapshot_dir) {
    snapshot_mapping[id_str] = relative_guest_snapshot_dir;
  }
  meta_info[kGuestSnapshotField] = snapshot_mapping;
  return meta_info;
}

std::string SnapshotMetaJsonPath(const std::string& snapshot_path) {
  return snapshot_path + "/" + kMetaInfoJsonFileName;
}

Result<Json::Value> LoadMetaJson(const std::string& snapshot_path) {
  auto meta_json_path = SnapshotMetaJsonPath(snapshot_path);
  auto meta_json = CF_EXPECT(LoadFromFile(meta_json_path));
  return meta_json;
}

Result<std::vector<std::string>> GuestSnapshotDirectories(
    const std::string& snapshot_path) {
  auto meta_json = CF_EXPECT(LoadMetaJson(snapshot_path));
  CF_EXPECT(meta_json.isMember(kGuestSnapshotField));
  const auto& guest_snapshot_dir_jsons = meta_json[kGuestSnapshotField];
  std::vector<std::string> id_strs = guest_snapshot_dir_jsons.getMemberNames();
  std::vector<std::string> guest_snapshot_paths;
  for (const auto& id_str : id_strs) {
    CF_EXPECT(guest_snapshot_dir_jsons.isMember(id_str));
    std::string path_suffix = guest_snapshot_dir_jsons[id_str].asString();
    guest_snapshot_paths.push_back(snapshot_path + "/" + path_suffix);
  }
  return guest_snapshot_paths;
}

}  // namespace cuttlefish
