/*
 * Copyright (C) 2024 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/local_instance.h"

#include <android-base/file.h>
#include <fmt/format.h>

#include <fstream>
#include <memory>
#include <string>
#include <utility>

#include "absl/log/log.h"
#include "cuttlefish/common/libs/utils/contains.h"
#include "cuttlefish/common/libs/utils/files.h"
#include "cuttlefish/common/libs/utils/subprocess.h"
#include "cuttlefish/common/libs/utils/subprocess_managed_stdio.h"
#include "cuttlefish/host/commands/cvd/cli/commands/host_tool_target.h"
#include "cuttlefish/host/commands/cvd/cli/utils.h"
#include "cuttlefish/host/commands/cvd/instances/status_fetcher.h"
#include "cuttlefish/host/commands/cvd/utils/common.h"
#include "cuttlefish/host/libs/command_util/runner/defs.h"
#include "cuttlefish/host/libs/command_util/util.h"
#include "cuttlefish/host/libs/config/cuttlefish_config.h"
#include "cuttlefish/host/libs/screen_recording_controls/screen_recording_controls.h"

namespace cuttlefish {

namespace {
constexpr int BASE_ADB_PORT = 6520;
constexpr int BASE_INSTANCE_ID = 1;

void AddEnvironmentForInstance(Command& cmd, const LocalInstance& instance) {
  cmd.AddEnvironmentVariable("HOME", instance.HomeDirectory());
  cmd.AddEnvironmentVariable(kAndroidHostOut, instance.HostArtifactsPath());
  cmd.AddEnvironmentVariable(kAndroidSoongHostOut,
                             instance.HostArtifactsPath());
}

}  // namespace

LocalInstance::LocalInstance(std::shared_ptr<cvd::InstanceGroup> group_proto,
                             cvd::Instance* instance_proto)
    : group_proto_(group_proto), instance_proto_(instance_proto) {}

void LocalInstance::SetState(cvd::InstanceState state) {
  instance_proto_->set_state(state);
}

std::string LocalInstance::InstanceDirectory() const {
  std::string to_ret = fmt::format("{}/cuttlefish/instances/cvd-{}",
                                   group_proto_->home_directory(), Id());
  if (!FileExists(to_ret)) {
    // Legacy launchers create cuttlefish_runtime.{$ID}
    to_ret = fmt::format("{}/cuttlefish_runtime.{}",
                         group_proto_->home_directory(), Id());
  }
  return to_ret;
}

int LocalInstance::AdbPort() const {
  // The instance id is zero for a very short time between the load and create
  // commands. The adb_port property should not be accessed during that time,
  // but return an invalid port number just in case.
  if (Id() == 0) {
    return 0;
  }
  // run_cvd picks this port from the instance id and doesn't provide a flag
  // to change in cvd_internal_flag
  return BASE_ADB_PORT + Id() - BASE_INSTANCE_ID;
}

std::string LocalInstance::AssemblyDirectory() const {
  return AssemblyDirFromHome(HomeDirectory());
}

bool LocalInstance::IsActive() const {
  switch (State()) {
    case cvd::INSTANCE_STATE_RUNNING:
    case cvd::INSTANCE_STATE_STARTING:
    case cvd::INSTANCE_STATE_STOPPING:
    case cvd::INSTANCE_STATE_PREPARING:
    case cvd::INSTANCE_STATE_UNREACHABLE:
      return true;
    case cvd::INSTANCE_STATE_UNSPECIFIED:
    case cvd::INSTANCE_STATE_STOPPED:
    case cvd::INSTANCE_STATE_PREPARE_FAILED:
    case cvd::INSTANCE_STATE_BOOT_FAILED:
    case cvd::INSTANCE_STATE_CANCELLED:
      return false;
    // Include these just to avoid the warning
    default:
      LOG(FATAL) << "Invalid instance state: " << State();
  }
  return false;
}

Result<Json::Value> LocalInstance::FetchStatus(std::chrono::seconds timeout) {
  return CF_EXPECT(FetchInstanceStatus(*this, timeout));
}

Result<void> LocalInstance::PressPowerBtn() {
  auto bin_check = HostToolTarget(HostArtifactsPath()).GetPowerBtnBinPath();
  if (bin_check.ok()) {
    return PressPowerBtnLegacy();
  }

  std::unique_ptr<const CuttlefishConfig> config =
      CuttlefishConfig::GetFromFile(InstanceDirectory() + "/cuttlefish_config.json");
  CF_EXPECT_EQ(config->vm_manager(), VmmMode::kCrosvm,
               "powerbtn not supported in vm manager " << config->vm_manager());
  auto instance = config->ForInstance(Id());

  Command command = Command(instance.crosvm_binary())
                        .AddParameter("powerbtn")
                        .AddParameter(instance.CrosvmSocketPath());

  LOG(INFO) << "Pressing power button";
  CF_EXPECT(RunAndCaptureStdout(std::move(command)));
  return {};
}

Result<void> LocalInstance::PressPowerBtnLegacy() {
  Command cmd(
      CF_EXPECT(HostToolTarget(HostArtifactsPath()).GetPowerBtnBinPath()));

  cmd.AddParameter("--instance_num=", Id());
  cmd.SetEnvironment({});
  AddEnvironmentForInstance(cmd, *this);

  VLOG(0) << "Executing: " << cmd.ToString();

  siginfo_t infop;
  cmd.Start().Wait(&infop, WEXITED);
  CF_EXPECT(CheckProcessExitedNormally(infop));

  return {};
}

Result<void> LocalInstance::Restart(std::chrono::seconds launcher_timeout,
                                    std::chrono::seconds boot_timeout) {
  SharedFD monitor_socket = CF_EXPECT(GetLauncherMonitor(launcher_timeout));

  LOG(INFO) << "Requesting restart";
  CF_EXPECT(RunLauncherAction(monitor_socket, LauncherAction::kRestart,
                              launcher_timeout.count()));

  LOG(INFO) << "Waiting for device to boot up again";
  CF_EXPECT(WaitForRead(monitor_socket, boot_timeout.count()));
  RunnerExitCodes boot_exit_code = CF_EXPECT(ReadExitCode(monitor_socket));
  CF_EXPECT(boot_exit_code != RunnerExitCodes::kVirtualDeviceBootFailed,
            "Boot failed");
  CF_EXPECT(boot_exit_code == RunnerExitCodes::kSuccess,
            "Unknown response" << static_cast<int>(boot_exit_code));

  LOG(INFO) << "Restart successful";
  return {};
}

Result<void> LocalInstance::PowerWash(std::chrono::seconds launcher_timeout,
                                      std::chrono::seconds boot_timeout) {
  SharedFD monitor_socket = CF_EXPECT(GetLauncherMonitor(launcher_timeout));

  LOG(INFO) << "Requesting powerwash";
  CF_EXPECT(RunLauncherAction(monitor_socket, LauncherAction::kPowerwash,
                              launcher_timeout.count()));

  LOG(INFO) << "Waiting for device to boot up again";
  CF_EXPECT(WaitForRead(monitor_socket, boot_timeout.count()));
  RunnerExitCodes boot_exit_code = CF_EXPECT(ReadExitCode(monitor_socket));
  CF_EXPECT(boot_exit_code != RunnerExitCodes::kVirtualDeviceBootFailed,
            "Boot failed");
  CF_EXPECT(boot_exit_code == RunnerExitCodes::kSuccess,
            "Unknown response" << static_cast<int>(boot_exit_code));

  LOG(INFO) << "Powerwash successful";

  return {};
}

Result<std::vector<std::string>> LocalInstance::ListRecordings() {
  std::string recordings_dir = fmt::format("{}/recording", InstanceDirectory());
  std::vector<std::string> files = CF_EXPECT(DirectoryContents(recordings_dir));
  for (std::string& file : files) {
    file = fmt::format("{}/{}", recordings_dir, file);
  }
  return files;
}

Result<void> LocalInstance::StartRecording(
    std::chrono::seconds launcher_timeout) {
  CF_EXPECT(State() == cvd::INSTANCE_STATE_STARTING ||
                State() == cvd::INSTANCE_STATE_RUNNING,
            "Instance must be running to be recorded");
  CuttlefishConfig::InstanceSpecific instance_config =
      CF_EXPECT(GetInstanceConfig(), "Failed to load instance config");
  CF_EXPECT(StartScreenRecording(instance_config, launcher_timeout));
  return {};
}

Result<void> LocalInstance::StopRecording(
    std::chrono::seconds launcher_timeout) {
  CF_EXPECT(State() == cvd::INSTANCE_STATE_STARTING ||
                State() == cvd::INSTANCE_STATE_RUNNING,
            "Instance must be running to be recorded");
  CuttlefishConfig::InstanceSpecific instance_config =
      CF_EXPECT(GetInstanceConfig(), "Failed to load instance config");
  CF_EXPECT(StopScreenRecording(instance_config, launcher_timeout));
  return {};
}

std::string LocalInstance::config_file_path() const {
  return HomeDirectory() + "/.cuttlefish_config.json";
}

Result<Json::Value> LocalInstance::ReadJsonConfig() const {
  Json::CharReaderBuilder builder;
  std::string config_file = config_file_path();
  std::ifstream ifs(config_file);
  std::string errorMessage;
  Json::Value config;
  bool parsed = Json::parseFromStream(builder, ifs, &config, &errorMessage);
  CF_EXPECTF(std::move(parsed), "Could not read config file {}: {}",
             config_file, errorMessage);
  return config;
}

Result<const CuttlefishConfig*> LocalInstance::LoadConfig() {
  if (!UNSAFE_config_) {
    UNSAFE_config_ = CuttlefishConfig::GetFromFile(config_file_path());
    CF_EXPECT(UNSAFE_config_ != nullptr, "Failed to load config from file");
  }
  return UNSAFE_config_.get();
}

Result<const CuttlefishConfig::InstanceSpecific>
LocalInstance::GetInstanceConfig() {
  const CuttlefishConfig* config = CF_EXPECT(LoadConfig());
  return config->ForInstance(Id());
}

Result<SharedFD> LocalInstance::GetLauncherMonitor(
    std::chrono::seconds timeout) const {
  // Newer cuttlefish instances put launcher monitor socket in a directory
  // under /tmp, and store this path in the config. Older instances just put
  // them in the instance directory.
  std::string uds_dir = InstanceDirectory();
  Json::Value config = CF_EXPECT(ReadJsonConfig());
  if (config.isMember("instances_uds_dir") &&
      config["instances_uds_dir"].isString()) {
    uds_dir =
        fmt::format("{}/cvd-{}", config["instances_uds_dir"].asString(), Id());
  }
  std::string monitor_path = uds_dir + "/launcher_monitor.sock";
  SharedFD monitor = SharedFD::SocketLocalClient(monitor_path, false,
                                                 SOCK_STREAM, timeout.count());
  CF_EXPECTF(monitor->IsOpen(),
             "Failed to connect to instance monitor socket ({}): {}",
             monitor_path, monitor->StrError());
  return monitor;
}

Result<std::vector<std::string>> LocalInstance::LogsFilenames() const {
  if (!FileExists(InstanceDirectory())) {
    VLOG(0) << "Instance directory \"" << InstanceDirectory() << "\" does not exist";
    return {};
  }
  std::string logs_dir = InstanceDirectory() + "/logs";
  if (!FileExists(logs_dir)) {
    VLOG(0) << "Instance logs directory \"" << logs_dir << "\" does not exist";
    return {};
  }
  std::vector<std::string> result = {};
  auto callback = [&result](const std::string& filename) -> Result<void> {
    result.push_back(filename);
    return {};
  };
  CF_EXPECT(WalkDirectory(logs_dir, callback));
  return result;
}

}  // namespace cuttlefish
