/*
 * Copyright 2021 Bloomberg Finance LP
 *
 * 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 <buildboxrun_container_host.h>

#include <buildboxcommon_logging.h>
#include <buildboxcommon_remoteexecutionclient.h>

#include <nlohmann/json.hpp>

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

#include <chrono>
#include <fstream>
#include <string>
#include <utility>
#include <vector>

namespace buildboxcommon {
namespace buildboxrun {
namespace oci {
using namespace build::bazel::remote::execution::v2;
namespace {
static const std::string k_tempFolderPrefix = "buildbox_ocirunner";
}

static std::filesystem::path buildTemporaryDirectoryPath()
{
    return std::filesystem::temp_directory_path() / "buildbox_" /
           std::to_string(getpid()) / "_" /
           std::to_string(
               std::chrono::duration_cast<std::chrono::seconds>(
                   std::chrono::steady_clock::now().time_since_epoch())
                   .count());
}

OCIRunner::ContainerHost::ContainerHost(
    OCIRunner *r, const Command &cmdValue, const Digest &workdirDigest,
    const std::shared_ptr<CASClient> &casClient,
    StageTreeRequest_StagingMode stagingMode)
    : d_runner(r),
      d_workdir(workdirDigest, buildTemporaryDirectoryPath().string(),
                casClient, stagingMode),
      d_workdirPath(std::filesystem::absolute(d_workdir.getPath())),
      d_cmd(cmdValue)
{
}

ActionResult OCIRunner::ContainerHost::execute()
{
    prepareFilesystem();
    stageSpec();
    return executeCommand();
}

ActionResult OCIRunner::ContainerHost::executeCommand()
{
    ActionResult result;
    // This directory is where runc will store its runtime information when
    // running in rootless mode. The current user must be able to write to it.
    TemporaryDirectory runcWorkdir(k_tempFolderPrefix.c_str());
    constexpr mode_t mode = S_IRWXU | S_IRWXG | S_IRWXO;
    Runner::recursively_chmod_directories(runcWorkdir.name(), mode);

    d_runner->executeAndStore(
        {"/usr/sbin/runc", "--root", runcWorkdir.name(), "run", "ct"}, &result,
        d_workdirPath.string());

    // We need to create this new command, which is a copy of d_cmd except for
    // the working_directory property, which is adjusted be relative to the
    // root workdir, so d_workdir can properly capture the output.
    Command rootPathCommand;
    rootPathCommand.CopyFrom(d_cmd);
    rootPathCommand.set_working_directory(d_cmd.working_directory().empty()
                                              ? "rootfs"
                                              : d_cmd.working_directory() +
                                                    "/rootfs");
    d_workdir.captureAllOutputs(rootPathCommand, &result);
    return result;
}

void OCIRunner::ContainerHost::prepareFilesystem() const
{
    // The path of the working directory of the command on the worker machine.
    const std::filesystem::path bundleWorkdirPath =
        d_workdirPath / "rootfs" / d_cmd.working_directory();

    // Creating directories for output paths and their parent paths
    for (auto &outputDir : d_cmd.output_paths()) {
        try {
            std::filesystem::path outputPath =
                std::filesystem::path(outputDir);
            if (!outputPath.has_parent_path())
                continue;

            std::filesystem::create_directories(bundleWorkdirPath /
                                                outputPath.parent_path());
        }
        catch (...) {
            BUILDBOXCOMMON_THROW_EXCEPTION(
                std::runtime_error,
                "Error staging directory "
                    << bundleWorkdirPath /
                           std::filesystem::path(outputDir).parent_path());
        }
    }
}

void OCIRunner::ContainerHost::stageSpec() const
{
    std::vector<std::string> command = {"runc", "spec", "--rootless"};

    const int exitCode = SystemUtils::executeCommandWithResult(
                             command, false, false, d_workdirPath)
                             .d_exitCode;

    if (exitCode != 0)
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Command failed to execute: " << command.data()
                                          << " with status code " << exitCode);

    const std::filesystem::path specPath = d_workdirPath / "config.json";

    nlohmann::json jsonSpec;
    std::ifstream(specPath) >> jsonSpec;

    // Changes readonly properties.
    jsonSpec["root"]["readonly"] = false;

    // Changes default user to UID 1000.
    const int DEFAULT_UID = 1000;
    jsonSpec["user"]["uid"] = DEFAULT_UID;

    // Changes working directory.
    if (d_cmd.working_directory().empty()) {
        jsonSpec["process"]["cwd"] = "/";
    }
    else {
        jsonSpec["process"]["cwd"] = d_cmd.working_directory();
    }

    // Changes command.
    jsonSpec["process"]["args"] = std::vector<std::string>(
        d_cmd.arguments().begin(), d_cmd.arguments().end());

    // Changes environment variables.
    std::vector<std::string> envVars;
    envVars.reserve(d_cmd.environment_variables_size());
    for (const Command_EnvironmentVariable &envVar :
         d_cmd.environment_variables()) {
        envVars.emplace_back(envVar.name() + "=" + envVar.value());
    }
    jsonSpec["process"]["env"] = std::move(envVars);

    // Disables active terminal (i.e. open passthrough I/O mode)
    jsonSpec["process"]["terminal"] = false;

    // Adds resource limits.
    std::vector<nlohmann::json> limits;
    limits.reserve(d_runner->d_resourceLimits.size());
    for (const auto &limitPair : d_runner->d_resourceLimits) {
        limits.emplace_back(
            nlohmann::json::object({{"type", limitPair.first},
                                    {"soft", limitPair.second},
                                    {"hard", limitPair.second}}));
    }
    jsonSpec["process"]["rlimits"] = std::move(limits);

    std::ofstream(specPath, std::ios::trunc) << jsonSpec;
}
} // namespace oci
} // namespace buildboxrun
} // namespace buildboxcommon
