/*
 * Copyright (C) 2022 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/commands/cvd/instances/instance_manager.h"

#include <chrono>
#include <iostream>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include <android-base/file.h>
#include <fmt/format.h>
#include "absl/log/log.h"
#include "absl/strings/match.h"

#include "cuttlefish/common/libs/utils/files.h"
#include "cuttlefish/host/commands/cvd/cli/commands/host_tool_target.h"
#include "cuttlefish/host/commands/cvd/instances/local_instance.h"
#include "cuttlefish/host/commands/cvd/instances/local_instance_group.h"
#include "cuttlefish/host/commands/cvd/instances/lock/instance_lock.h"
#include "cuttlefish/host/commands/cvd/instances/lock/lock_file.h"
#include "cuttlefish/host/commands/cvd/instances/stop.h"
#include "cuttlefish/host/commands/cvd/utils/common.h"
#include "cuttlefish/host/libs/config/config_utils.h"
#include "cuttlefish/host/libs/metrics/device_metrics_orchestration.h"
#include "cuttlefish/posix/symlink.h"
#include "cuttlefish/result/result.h"

namespace cuttlefish {
namespace {

Result<void> RemoveGroupDirectory(const LocalInstanceGroup& group) {
  std::string per_user_dir = PerUserDir();
  if (!absl::StartsWith(group.HomeDir(), per_user_dir)) {
    LOG(WARNING)
        << "Instance group home directory not under user specific directory("
        << per_user_dir << "), artifacts not deleted";
    return {};
  }
  std::string group_directory = CF_EXPECT(GroupDirFromHome(group.HomeDir()));
  if (DirectoryExists(group_directory)) {
    CF_EXPECT(RecursivelyRemoveDirectory(group_directory),
              "Failed to remove group directory");
  }
  return {};
}

Result<void> LinkOrMakeDir(std::optional<std::string> target,
                           const std::string& path) {
  if (target.has_value()) {
    CF_EXPECT(DirectoryExists(*target));
    CF_EXPECT(Symlink(*target, path));
  } else {
    CF_EXPECTF(EnsureDirectoryExists(path), "Failed to create directory: {}",
               path);
  }
  return {};
}

Result<void> CreateOrLinkGroupDirectories(
    const LocalInstanceGroup& group,
    InstanceManager::GroupDirectories directories) {
  CF_EXPECT(
      LinkOrMakeDir(std::move(directories.base_directory), group.BaseDir()));
  CF_EXPECT(LinkOrMakeDir(std::move(directories.home), group.HomeDir()));
  CF_EXPECT(EnsureDirectoryExists(group.ArtifactsDir()));
  CF_EXPECT(LinkOrMakeDir(std::move(directories.host_artifacts_path),
                          group.HostArtifactsPath()));
  for (size_t i = 0; i < directories.product_out_paths.size(); ++i) {
    CF_EXPECT(LinkOrMakeDir(std::move(directories.product_out_paths[i]),
                            group.ProductDir(i)));
  }
  return {};
}

Result<std::string> StopBin(const std::string& host_android_out) {
  return CF_EXPECT(HostToolTarget(host_android_out).GetStopBinName());
}

}  // namespace

InstanceManager::InstanceManager(InstanceLockFileManager& lock_manager,
                                 InstanceDatabase& instance_db)
    : lock_manager_(lock_manager), instance_db_(instance_db) {}

Result<std::pair<LocalInstance, LocalInstanceGroup>>
InstanceManager::FindInstanceWithGroup(
    const InstanceDatabase::Filter& filter) const {
  return CF_EXPECT(instance_db_.FindInstanceWithGroup(filter));
}

Result<std::vector<std::pair<LocalInstanceGroup, std::vector<LocalInstance>>>>
InstanceManager::FindInstances(const InstanceDatabase::Filter& filter) const {
  return CF_EXPECT(instance_db_.FindInstances(filter));
}

Result<bool> InstanceManager::HasInstanceGroups() const {
  return !CF_EXPECT(instance_db_.IsEmpty());
}

Result<std::vector<InstanceManager::InternalInstanceDesc>>
InstanceManager::AllocateAndLockInstanceIds(
    std::vector<InstanceParams> instances) {
  std::set<unsigned> requested;
  std::vector<InstanceLockFile> requested_lock_files;
  // Acquire requested locks first
  for (const auto& instance : instances) {
    if (instance.instance_id.has_value()) {
      unsigned id = instance.instance_id.value();
      auto [_, inserted] = requested.insert(id);
      // This check avoids a possible deadlock when trying to acquire the lock a
      // second time
      CF_EXPECTF(std::move(inserted),
                 "Requested instance ids must be distinct, but {} is repeated",
                 id);
      requested_lock_files.emplace_back(
          CF_EXPECT(lock_manager_.AcquireLock(id)));
    }
  }
  std::vector<InternalInstanceDesc> ret;
  ret.reserve(instances.size());
  auto requested_it = requested_lock_files.begin();
  for (auto& instance : instances) {
    if (instance.instance_id.has_value()) {
      CF_EXPECT(requested_it != requested_lock_files.end());
      ret.emplace_back(InternalInstanceDesc{
          .lock_file = std::move(*requested_it),
          .name = std::move(instance.per_instance_name),
      });
      ++requested_it;
    } else {
      ret.emplace_back(InternalInstanceDesc{
          .lock_file = CF_EXPECT(lock_manager_.AcquireUnusedLock()),
          .name = std::move(instance.per_instance_name),
      });
    }
  }
  return ret;
}

Result<LocalInstanceGroup> InstanceManager::CreateInstanceGroup(
    InstanceGroupParams group_params, GroupDirectories directories) {
  CF_EXPECT_EQ(
      group_params.instances.size(), directories.product_out_paths.size(),
      "Number of product directories doesn't match number of instances");

  std::vector<InternalInstanceDesc> instance_descs =
      CF_EXPECT(AllocateAndLockInstanceIds(std::move(group_params.instances)));

  LocalInstanceGroup::Builder group_builder(std::move(group_params.group_name));
  for (const auto& instance_desc : instance_descs) {
    if (instance_desc.name.has_value()) {
      group_builder.AddInstance(instance_desc.lock_file.Instance(),
                                instance_desc.name.value());
    } else {
      group_builder.AddInstance(instance_desc.lock_file.Instance());
    }
  }
  LocalInstanceGroup group = CF_EXPECT(group_builder.Build());

  // The base and other directories are always set to the default location, if
  // the user provides custom directories the ones in the default locations
  // become symbolic links to those.
  CF_EXPECT(CreateOrLinkGroupDirectories(group, std::move(directories)));

  CF_EXPECT(instance_db_.AddInstanceGroup(group));
  for (auto& instance_desc : instance_descs) {
    CF_EXPECT(instance_desc.lock_file.Status(InUseState::kInUse));
  }

  GatherVmInstantiationMetrics(group);

  return group;
}

Result<bool> InstanceManager::RemoveInstanceGroup(LocalInstanceGroup group) {
  CF_EXPECT(!group.HasActiveInstances(),
            "Group still contains active instances");
  for (auto& instance : group.Instances()) {
    if (instance.Id() == 0) {
      continue;
    }
    if (auto res = lock_manager_.RemoveLockFile(instance.Id()); !res.ok()) {
      LOG(ERROR) << "Failed to remove instance id lock: " << res.error();
    }
  }
  CF_EXPECT(RemoveGroupDirectory(group));

  return CF_EXPECT(instance_db_.RemoveInstanceGroup(group.GroupName()));
}

Result<void> InstanceManager::UpdateInstanceGroup(
    const LocalInstanceGroup& group) {
  CF_EXPECT(instance_db_.UpdateInstanceGroup(group));
  return {};
}

Result<void> InstanceManager::StopInstanceGroup(
    LocalInstanceGroup& group,
    std::optional<std::chrono::seconds> launcher_timeout,
    InstanceDirActionOnStop instance_dir_action,
    const std::vector<unsigned>& instance_nums) {
  // Validate that the requested instances actually belong to this group
  std::set<unsigned> valid_ids;
  for (const auto& instance : group.Instances()) {
    valid_ids.insert(instance.Id());
  }
  std::set<unsigned> ids_to_stop;
  for (unsigned num : instance_nums) {
    CF_EXPECTF(valid_ids.count(num) != 0,
               "Instance ID {} does not belong to group '{}'", num,
               group.GroupName());
    ids_to_stop.insert(num);
  }

  const auto stop_bin = CF_EXPECT(StopBin(group.HostArtifactsPath()));
  const auto stop_bin_path = group.HostArtifactsPath() + "/bin/" + stop_bin;
  int wait_for_launcher_secs = 0;
  if (launcher_timeout.has_value()) {
    wait_for_launcher_secs = launcher_timeout->count();
  }
  Result<void> cmd_result = RunStopCvd(StopCvdParams{
      .bin_path = stop_bin_path,
      .home_dir = group.HomeDir(),
      .wait_for_launcher_secs = wait_for_launcher_secs,
      .clear_runtime_dirs =
          instance_dir_action == InstanceDirActionOnStop::Clear,
      .instance_nums = instance_nums,
  });
  if (!cmd_result.ok()) {
    LOG(WARNING)
        << "Warning: error stopping instances for dir \"" + group.HomeDir() +
               "\".\nThis can happen if instances are already stopped.\n";
    return cmd_result;
  }
  if (instance_nums.empty()) {
    group.SetAllStates(cvd::INSTANCE_STATE_STOPPED);
  } else {
    for (auto& instance : group.Instances()) {
      if (ids_to_stop.count(instance.Id()) > 0) {
        instance.SetState(cvd::INSTANCE_STATE_STOPPED);
      }
    }
  }
  // TODO: b/471069557 - diagnose unused
  Result<void> unused = instance_db_.UpdateInstanceGroup(group);
  return {};
}

Result<void> InstanceManager::Clear() {
  const std::string config_json_name =
      android::base::Basename(GetGlobalConfigFileLink());
  auto instance_groups =
      CF_EXPECT(instance_db_.Clear(), "Failed to clear instance database");
  for (auto& group : instance_groups) {
    // Only stop running instances.
    if (group.HasActiveInstances()) {
      auto stop_result = StopInstanceGroup(group, std::chrono::seconds(5),
                                           InstanceDirActionOnStop::Clear);
      if (!stop_result.ok()) {
        LOG(ERROR) << stop_result.error();
      }
    }
    for (auto instance : group.Instances()) {
      if (instance.Id() <= 0) {
        continue;
      }
      auto res = lock_manager_.RemoveLockFile(instance.Id());
      if (!res.ok()) {
        LOG(ERROR) << "Failed to remove lock file for instance: "
                   << res.error();
      }
    }
    std::string runtime_link = group.HomeDir() + "/cuttlefish_runtime";
    if (Result<void> res = RemoveFile(runtime_link);!res.ok()) {
      LOG(ERROR) << res.error();
    }
    std::string config_link = group.HomeDir() + "/" + config_json_name;
    if (Result<void> res = RemoveFile(config_link);!res.ok()) {
      LOG(ERROR) << res.error();
    }
    // TODO: b/471069557 - diagnose unused
    Result<void> unused = RemoveGroupDirectory(group);
  }
  std::cerr << "Stopped all known instances\n";
  return {};
}

Result<void> InstanceManager::Reset() {
  CF_EXPECT(Clear());
  CF_EXPECT(KillAllCuttlefishInstances(false));
  return {};
}

Result<void> InstanceManager::ResetAndClearInstanceDirs() {
  CF_EXPECT(Clear());
  CF_EXPECT(KillAllCuttlefishInstances(true));
  return {};
}

Result<std::vector<LocalInstanceGroup>> InstanceManager::FindGroups(
    const InstanceDatabase::Filter& filter) const {
  return CF_EXPECT(instance_db_.FindGroups(filter));
}

Result<LocalInstanceGroup> InstanceManager::FindGroup(
    const InstanceDatabase::Filter& filter) const {
  auto output = CF_EXPECT(instance_db_.FindGroups(filter));
  CF_EXPECT_EQ(output.size(), 1ul);
  return *(output.begin());
}

}  // namespace cuttlefish
