/*
 * Copyright 2019 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 <build/bazel/remote/execution/v2/remote_execution.pb.h>
#include <buildboxcommon_stageddirectory.h>

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

#include <cstdint>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

using namespace buildboxcommon;

void StagedDirectory::captureAllOutputs(const Command &command,
                                        ActionResult *result) const
{
    CaptureFileCallback capture_file_function = [&](const char *path) {
        return this->captureFile(path, command);
    };

    CaptureDirectoryCallback capture_directory_function =
        [&](const char *path, int64_t *directoryTotalSize) {
            return this->captureDirectory(path, command, directoryTotalSize);
        };

    return captureAllOutputs(command, result, capture_file_function,
                             capture_directory_function);
}

void StagedDirectory::captureAllOutputs(
    const Command &command, ActionResult *result,
    StagedDirectory::CaptureFileCallback captureFileFunction,
    StagedDirectory::CaptureDirectoryCallback captureDirectoryFunction) const
{
    const std::string workingDirectory = getWorkingDirectory(command);

    int64_t resultSizeBytes = 0;
    resultSizeBytes += result->stdout_digest().size_bytes();
    resultSizeBytes += result->stderr_digest().size_bytes();

    const auto captureFileAndAddToResult =
        [&captureFileFunction, &result, &resultSizeBytes](
            const std::string &name, const std::string &pathInInputRoot) {
            const OutputFile outputFile =
                captureFile(name, pathInInputRoot, captureFileFunction);

            if (!outputFile.path().empty()) {
                OutputFile *fileEntry = result->add_output_files();
                resultSizeBytes += fileEntry->digest().size_bytes();
                *fileEntry = outputFile;
            }
        };

    const auto captureDirectoryAndAddToResult =
        [&captureDirectoryFunction, &result, &resultSizeBytes](
            const std::string &name, const std::string &pathInInputRoot) {
            int64_t directoryTotalSize = 0;
            const OutputDirectory outputDirectory =
                captureDirectory(name, pathInInputRoot, &directoryTotalSize,
                                 captureDirectoryFunction);
            if (!outputDirectory.path().empty()) {
                OutputDirectory *dirEntry = result->add_output_directories();
                *dirEntry = outputDirectory;
                resultSizeBytes += directoryTotalSize;
            }
        };

    for (const auto &outputPath : command.output_paths()) {
        const auto relative_path =
            pathInInputRoot(outputPath, workingDirectory);

        mode_t st_mode = 0;
        const auto pathExists =
            getStatMode(this->d_path, relative_path, &st_mode);
        if (!pathExists) {
            continue; // Do not fail with paths that do not exist (#30).
        }

        if (S_ISDIR(st_mode)) {
            captureDirectoryAndAddToResult(outputPath, relative_path);
        }
        else if (S_ISREG(st_mode)) {
            captureFileAndAddToResult(outputPath, relative_path);
        }
        else {
            std::stringstream errorMessage;
            errorMessage << "Output path \"" << relative_path
                         << "\" is not a file or directory: st_mode == "
                         << st_mode;
            throw std::invalid_argument(errorMessage.str());
        }
    }

    BUILDBOX_LOG_INFO("Action output size " << resultSizeBytes << " bytes");
}

std::string StagedDirectory::getWorkingDirectory(const Command &command)
{
    if (command.working_directory().empty()) {
        return "";
    }

    // According to the REAPI, `Command.working_directory()` can be empty.
    // In that case, we want to avoid adding leading slashes to paths:
    // that would make them absolute. To simplify handling this later, we
    // add the trailing slash here.
    std::string working_directory =
        FileUtils::normalizePath(command.working_directory().c_str()) + "/";

    if (working_directory.front() == '/') {
        const auto error_message = "`working_directory` path in `Command` "
                                   "must be relative. It is \"" +
                                   working_directory + "\"";
        BUILDBOX_LOG_ERROR(error_message);
        throw std::invalid_argument(error_message);
    }

    if (working_directory.substr(0, 3) == "../") {
        const auto error_message =
            "The `working_directory` path in `Command` is "
            "outside of input root \"" +
            working_directory + "\"";
        BUILDBOX_LOG_ERROR(error_message);
        throw std::invalid_argument(error_message);
    }

    return working_directory;
}

OutputFile StagedDirectory::captureFile(
    const std::string &name, const std::string &pathInInputRoot,
    const StagedDirectory::CaptureFileCallback &captureFileFunction)
{
    OutputFile outputFile = captureFileFunction(pathInInputRoot.c_str());
    if (!outputFile.path().empty()) {
        outputFile.set_path(name);
    }
    return outputFile;
}

OutputDirectory StagedDirectory::captureDirectory(
    const std::string &name, const std::string &pathInInputRoot,
    int64_t *directoryTotalSize,
    const StagedDirectory::CaptureDirectoryCallback &captureDirectoryFunction)
{

    OutputDirectory outputDirectory =
        captureDirectoryFunction(pathInInputRoot.c_str(), directoryTotalSize);
    if (!outputDirectory.path().empty()) {
        outputDirectory.set_path(name);
    }
    return outputDirectory;
}

std::string
StagedDirectory::pathInInputRoot(const std::string &name,
                                 const std::string &workingDirectory)
{
    assertNoInvalidSlashes(name);
    return workingDirectory + name;
}

bool StagedDirectory::getStatMode(const std::string &root,
                                  const std::string &path, mode_t *st_mode)
{
    struct stat s{};
    const FileDescriptor fd(FileUtils::openInRoot(
        root, path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC));

    if (fd.get() < 0) {
        if (errno == ENOENT) {
            return false;
        }
        else if (errno == ELOOP) {
            // Symlink can't be opened.
            // (In Linux it's possible with O_PATH but that flag is not
            // available on other systems)
            *st_mode = S_IFLNK;
            return true;
        }
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening \"" << path << "\" in \"" << root << "\"");
    }

    if (fstat(fd.get(), &s) < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error on fstat for \"" << path << "\" in \"" << root << "\"");
    }

    *st_mode = s.st_mode;
    return true;
}

void StagedDirectory::assertNoInvalidSlashes(const std::string &path)
{
    //  According to the REAPI:
    // "The paths are relative to the working directory of the action
    // execution. [...] The path MUST NOT include a trailing slash, nor a
    // leading slash, being a relative path."
    if (!path.empty() && (path.front() == '/' || path.back() == '/')) {
        const auto error_message = "Output path in `Command` has "
                                   "leading or trailing slashes: \"" +
                                   path + "\"";
        BUILDBOX_LOG_ERROR(error_message);
        throw std::invalid_argument(error_message);
    }
}

int StagedDirectoryUtils::openFileInInputRoot(const int root_dir_fd,
                                              const std::string &relative_path)
{
    int file_fd = FileUtils::openInRoot(root_dir_fd, relative_path,
                                        O_RDONLY | O_NOFOLLOW | O_CLOEXEC);

    if (file_fd >= 0 && FileUtils::isDirectory(file_fd)) {
        close(file_fd);
        file_fd = -1;
        errno = EISDIR;
    }

    if (file_fd < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening \"" << relative_path << "\"");
    }

    return file_fd;
}

int StagedDirectoryUtils::openDirectoryInInputRoot(const int root_dir_fd,
                                                   const std::string &path)
{
    const int subdir_fd = FileUtils::openInRoot(
        root_dir_fd, path, O_DIRECTORY | O_RDONLY | O_NOFOLLOW | O_CLOEXEC);

    if (subdir_fd < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening directory \"" << path << "\"");
    }

    return subdir_fd;
}

bool StagedDirectoryUtils::fileInInputRoot(const int root_dir_fd,
                                           const std::string &path)
{
    try {
        const int fd = openFileInInputRoot(root_dir_fd, path);
        close(fd);
        return true;
    }
    catch (const std::system_error &) {
        return false;
    }
}

bool StagedDirectoryUtils::directoryInInputRoot(const int root_dir_fd,
                                                const std::string &path)
{
    if (path.empty()) {
        return true;
    }

    try {
        const int fd = openDirectoryInInputRoot(root_dir_fd, path);
        close(fd);
        return true;
    }
    catch (const std::system_error &) {
        return false;
    }
}
