/*
 * Copyright 2018 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 "buildboxcommon_systemutils.h"
#include <buildboxcommon_fallbackstageddirectory.h>

#include <buildboxcommon_exception.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_timeutils.h>

#include <cstring>
#include <dirent.h>
#include <exception>
#include <fcntl.h>
#include <filesystem>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <utility>

namespace buildboxcommon {

FallbackStagedDirectory::FallbackStagedDirectory() {}

FallbackStagedDirectory::FallbackStagedDirectory(
    const Digest &digest, const std::string &path,
    std::shared_ptr<CASClient> cas_client,
    const std::vector<std::string> &preUnstageCommands)
    : d_casClient(std::move(cas_client)),
      d_stage_directory(path.c_str(), "buildboxrun"),
      d_preUnstageCommands(preUnstageCommands)
{
    this->d_path = d_stage_directory.name();

    // Using `AT_FDCWD` as placeholder. Since `d_path` is absolute, `openat()`
    // will ignore it.
    this->d_stage_directory_fd = // NOLINT
                                 // (cppcoreguidelines-prefer-member-initializer)
        openat(AT_FDCWD, this->d_path.c_str(),
               O_DIRECTORY | O_RDONLY | O_CLOEXEC);
    // (It will be closed by the destructor.)

    if (this->d_stage_directory_fd == -1) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening directory path \"" << path << "\"");
    }

    BUILDBOX_LOG_DEBUG("Downloading to " << this->d_path);
    this->d_casClient->downloadDirectory(digest, this->d_path);
}

FallbackStagedDirectory::~FallbackStagedDirectory()
{
    runPreUnstageCommands();
    BUILDBOX_LOG_DEBUG("Unstaging " << this->d_path);
    close(this->d_stage_directory_fd);
}

void FallbackStagedDirectory::runPreUnstageCommands() const noexcept
{
    for (const auto &command : this->d_preUnstageCommands) {
        try {
            if (!std::filesystem::path(command).is_absolute()) {
                BUILDBOX_LOG_WARNING("Pre-unstage command [" << command
                                                             << "] is not "
                                                                "absolute");
                continue;
            }
            BUILDBOX_LOG_DEBUG("Running pre-unstage command=" << command);
            // `executeCommandAndWait` throws if it fails to fork
            const int rc = SystemUtils::executeCommandAndWait(
                {command, this->d_path}, true);
            if (rc != 0) {
                BUILDBOX_LOG_WARNING(
                    "Pre-unstage command failed with exit code "
                    << rc << ": command=" << command);
            }
        }
        catch (std::exception &e) {
            BUILDBOX_LOG_ERROR("Failed to launch pre-unstage command="
                               << command << " error=" << e.what());
        }
    }
}

OutputFile FallbackStagedDirectory::captureFile(const char *relative_path,
                                                const Command &command) const
{

    const auto upload_file_function = [this](const int fd,
                                             const Digest &digest) {
        this->d_casClient->upload(fd, digest);
    };

    bool capture_mtime = false;
    for (const auto &property : command.output_node_properties()) {
        if (property == "mtime") {
            capture_mtime = true;
        }
    }

    return captureFile(relative_path, upload_file_function, capture_mtime);
}

OutputDirectory
FallbackStagedDirectory::captureDirectory(const char *relative_path,
                                          const Command &command,
                                          int64_t *directoryTotalSize) const
{

    const auto upload_directory_function =
        [this, &command, &directoryTotalSize](const std::string &path) {
            return this->uploadDirectory(
                path, command.output_directory_format(), directoryTotalSize);
        };

    const auto captured_directory = this->captureDirectory(
        relative_path, command, upload_directory_function);

    return captured_directory;
}

OutputDirectory FallbackStagedDirectory::captureDirectory(
    const char *relative_path, const Command &command,
    const std::function<std::pair<Digest, Digest>(const std::string &path)>
        &upload_directory_function) const
{

    if (!StagedDirectoryUtils::directoryInInputRoot(this->d_stage_directory_fd,
                                                    relative_path)) {
        // The directory does not exist.
        return {};
    }

    const auto [tree_digest, directory_digest] =
        upload_directory_function(relative_path);

    OutputDirectory output_directory;
    output_directory.set_path(relative_path);
    if (command.output_directory_format() ==
            Command_OutputDirectoryFormat_TREE_ONLY ||
        command.output_directory_format() ==
            Command_OutputDirectoryFormat_TREE_AND_DIRECTORY) {
        output_directory.mutable_tree_digest()->CopyFrom(tree_digest);
    }
    if (command.output_directory_format() ==
            Command_OutputDirectoryFormat_DIRECTORY_ONLY ||
        command.output_directory_format() ==
            Command_OutputDirectoryFormat_TREE_AND_DIRECTORY) {
        output_directory.mutable_root_directory_digest()->CopyFrom(
            directory_digest);
    }

    return output_directory;
}

std::pair<Digest, Digest> FallbackStagedDirectory::uploadDirectory(
    const std::string &path,
    const Command_OutputDirectoryFormat outputDirFormat,
    int64_t *directoryTotalSize) const
{
    BUILDBOX_LOG_DEBUG("Uploading directory " << path);

    int64_t total_size = 0;
    Digest root_directory_digest;
    Tree tree;
    FileDescriptor fd(FileUtils::openInRoot(
        this->d_stage_directory_fd, path, O_RDONLY | O_DIRECTORY | O_CLOEXEC));
    if (fd.get() < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening directory \"" << path << "\"");
    }

    const auto failed_uploads = this->d_casClient->uploadDirectory(
        fd.get(), &root_directory_digest, &tree);

    if (failed_uploads.size() > 0) {
        const auto &status = failed_uploads[0].status;
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error, "Failed to upload "
                                    << failed_uploads.size()
                                    << " blobs to CAS:" << status.error_code()
                                    << ": " << status.error_message());
    }

    Digest tree_digest;
    if (outputDirFormat != Command_OutputDirectoryFormat::
                               Command_OutputDirectoryFormat_DIRECTORY_ONLY) {
        tree_digest = this->d_casClient->uploadMessage(tree);
    }

    if (directoryTotalSize != nullptr) {
        for (const auto &file_node : tree.root().files()) {
            total_size += file_node.digest().size_bytes();
        }
        for (const auto &child_directory : tree.children()) {
            for (const auto &file_node : child_directory.files()) {
                total_size += file_node.digest().size_bytes();
            }
        }
        *directoryTotalSize = total_size;
    }

    return std::make_pair<Digest, Digest>(std::move(tree_digest),
                                          std::move(root_directory_digest));
}

OutputFile FallbackStagedDirectory::captureFile(
    const char *relative_path,
    const std::function<void(const int fd, const Digest &digest)>
        &upload_file_function,
    const bool capture_mtime) const
{
    FileDescriptor fd(FileUtils::openInRoot(
        this->d_stage_directory_fd, relative_path, O_RDONLY | O_CLOEXEC));
    if (fd.get() < 0) {
        if (errno == ENOENT || errno == ENOTDIR) {
            return {};
        }

        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening path \"" << relative_path << "\"");
    }

    const Digest digest = DigestGenerator::hash(fd.get());

    upload_file_function(fd.get(), digest);

    OutputFile output_file;
    output_file.set_path(relative_path);
    output_file.mutable_digest()->CopyFrom(digest);
    output_file.set_is_executable(FileUtils::isExecutable(fd.get()));

    if (capture_mtime) {
        const auto mtime = FileUtils::getFileMtime(fd.get());
        const auto mtime_timestamp = TimeUtils::make_timestamp(mtime);
        output_file.mutable_node_properties()->mutable_mtime()->CopyFrom(
            mtime_timestamp);
    }

    return output_file;
}

} // namespace buildboxcommon
