/*
 * 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 <buildboxcommon_direntwrapper.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_localcasstageddirectory.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_protos.h>
#include <buildboxcommon_timeutils.h>

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

using namespace buildboxcommon;

LocalCasStagedDirectory::LocalCasStagedDirectory(
    const Digest &digest, const std::string &path,
    const std::shared_ptr<CASClient> &cas_client,
    const StageTreeRequest_StagingMode stagingMode,
    const ProcessCredentialsGetter &processCredentialsGetter,
    const std::vector<std::string> &postActionCommands,
    bool outputByPassedLocalCache,
    const std::optional<RemoteApisSocketConfig> &remoteApisSocket)
    : LocalCasStagedDirectory(digest, path, cas_client, stagingMode, geteuid(),
                              processCredentialsGetter, postActionCommands,
                              outputByPassedLocalCache, remoteApisSocket)
{
}

LocalCasStagedDirectory::LocalCasStagedDirectory(
    const Digest &digest, const std::string &path,
    const std::shared_ptr<CASClient> &cas_client,
    const StageTreeRequest_StagingMode stagingMode, const uid_t rootOwnerUid,
    const ProcessCredentialsGetter &processCredentialsGetter,
    const std::vector<std::string> &postActionCommands,
    bool outputByPassedLocalCache,
    const std::optional<RemoteApisSocketConfig> &remoteApisSocket)
    : d_cas_client(cas_client),
      d_cas_client_staged_directory(cas_client->stage(
          digest, stagingMode, path, processCredentialsGetter,
          postActionCommands, remoteApisSocket)),
      d_rootOwnerUid(rootOwnerUid),
      d_outputByPassedLocalCache(outputByPassedLocalCache)
{
    this->d_path = d_cas_client_staged_directory->path();

    this->d_staged_directory_fd =
        open(d_cas_client_staged_directory->path().c_str(),
             O_DIRECTORY | O_RDONLY | O_CLOEXEC);

    if (d_staged_directory_fd == -1) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening staged directory: \"" << d_path << "\"");
    }
}

LocalCasStagedDirectory::~LocalCasStagedDirectory()
{
    // As directories can be written with arbitrary permissions
    // and may be a different user than the stage directory,
    // chmod all the directories to be globally removable so unstaging
    // will succeed.
    struct stat statResult{};
    auto rc = fstat(d_staged_directory_fd, &statResult);
    if (rc < 0) {
        BUILDBOX_LOG_ERROR("Error calling stat on the staged directory"
                           << std::string(strerror(errno)));
    }
    else if (statResult.st_uid != d_rootOwnerUid) {
        try {
            DirentWrapper dwrap = DirentWrapper(d_staged_directory_fd, ".");
            FileUtils::DirectoryTraversalFnPtr dir_chmod_func =
                [](const char * /* dir_path */, int fd) {
                    fchmod(fd, PERMISSION_RWXALL);
                };
            FileUtils::fileDescriptorTraverseAndApply(&dwrap, dir_chmod_func);
        }
        catch (const std::exception &e) {
            BUILDBOX_LOG_WARNING(
                "Error chmodding staged directory contents: " << e.what());
        }
    }
    close(d_staged_directory_fd);
}

OutputFile LocalCasStagedDirectory::captureFile(const char *relative_path,
                                                const Command &command) const
{
    if (!StagedDirectoryUtils::fileInInputRoot(this->d_staged_directory_fd,
                                               relative_path)) {
        return {};
    }

    const std::vector<std::string> properties(
        command.output_node_properties().cbegin(),
        command.output_node_properties().cend());

    const auto responses = this->d_cas_client->captureFiles(
        this->d_path, {relative_path}, properties, d_outputByPassedLocalCache);

    if (responses.empty()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Error capturing \"" << relative_path << "\" in \"" << this->d_path
                                 << "\": server returned empty response");
    }

    const auto &captured_file = responses.at(0);

    if (captured_file.status().code() != grpc::StatusCode::OK) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Error capturing \""
                << relative_path << "\" in \"" << this->d_path
                << "\": " << captured_file.status().message());
    }

    OutputFile output_file;
    output_file.set_path(relative_path);
    output_file.mutable_digest()->CopyFrom(captured_file.digest());
    output_file.set_is_executable(captured_file.is_executable());
    if (captured_file.has_node_properties()) {
        *output_file.mutable_node_properties() =
            captured_file.node_properties();
    }
    return output_file;
}

OutputDirectory LocalCasStagedDirectory::captureDirectory(
    const char *relative_path, const Command &command,
    int64_t *directoryTotalSize = nullptr) const
{
    if (!StagedDirectoryUtils::directoryInInputRoot(
            this->d_staged_directory_fd, relative_path)) {
        // If the directory does not exist or is outside the input root, we
        // just ignore it:
        return OutputDirectory();
    }

    const std::vector<std::string> properties(
        command.output_node_properties().cbegin(),
        command.output_node_properties().cend());

    const CaptureTreeResponse capture_response =
        this->d_cas_client->captureTree(this->d_path, {relative_path},
                                        properties, d_outputByPassedLocalCache,
                                        0, nullptr,
                                        command.output_directory_format());

    if (capture_response.responses().empty()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Error capturing \"" << relative_path << "\" in \"" << this->d_path
                                 << "\": server returned empty response");
    }

    const auto &captured_directory = capture_response.responses(0);
    if (directoryTotalSize != nullptr) {
        *directoryTotalSize = captured_directory.total_size_bytes();
    }

    if (captured_directory.status().code() != grpc::StatusCode::OK) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Error capturing \""
                << relative_path << "\" in \"" << this->d_path
                << "\": " << captured_directory.status().message());
    }

    OutputDirectory output_directory;
    output_directory.set_path(relative_path);
    if (captured_directory.has_tree_digest()) {
        output_directory.mutable_tree_digest()->CopyFrom(
            captured_directory.tree_digest());
    }
    if (captured_directory.has_root_directory_digest()) {
        output_directory.mutable_root_directory_digest()->CopyFrom(
            captured_directory.root_directory_digest());
    }

    return output_directory;
}
