/*
 * 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_casclient.h>
#include <buildboxcommon_commandline.h>
#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_permissions.h>
#include <buildboxcommon_protojsonutils.h>
#include <buildboxcommon_protos.h>
#include <buildboxcommon_remoteexecutionclient.h>
#include <buildboxcommon_stringutils.h>
#include <buildboxcommon_version.h>

#include "processargs.h"

#include <chrono>
#include <fcntl.h>
#include <iomanip>
#include <iostream>
#include <libgen.h> // dirname
#include <memory>
#include <span>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

using buildboxcommon::Digest;

bool actionResultFromAction(const Digest &action,
                            const buildboxcommon::ConnectionOptions &options,
                            buildboxcommon::GrpcClient *grpcClient,
                            buildboxcommon::CASClient *client,
                            buildboxcommon::ActionResult *actionResult)
{
    std::unique_ptr<buildboxcommon::ActionCache::Stub> actionCache =
        buildboxcommon::ActionCache::NewStub(options.createChannel());

    buildboxcommon::GetActionResultRequest actionRequest;
    actionRequest.set_instance_name(grpcClient->instanceName());

    actionRequest.set_inline_stdout(false);
    actionRequest.set_inline_stderr(false);
    *actionRequest.mutable_action_digest() = action;

    grpc::ClientContext context;
    const grpc::Status status =
        actionCache->GetActionResult(&context, actionRequest, actionResult);

    if (!status.ok()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error, "Action cache returned error "
                                    << std::to_string(status.error_code())
                                    << ": \""
                                    << status.error_message() + "\"");
    }

    return true;
}

std::string calculateDirname(const std::string &path)
{
    // some dirname implementations modify the input so we have to copy
    std::vector<char> cpath(path.cbegin(), path.cend());
    cpath.push_back('\0');
    return std::string(dirname(&cpath[0]));
}
void createDirectoryForPath(const std::string &path)
{
    const std::string dir = calculateDirname(path);
    buildboxcommon::FileUtils::createDirectory(dir.c_str());
}

void writeExitCode(const std::string &file, int code)
{
    std::ostringstream rc;
    rc << code << "\n";
    buildboxcommon::FileUtils::writeFileAtomically(file, rc.str());
    std::cout << "Wrote rc=" << code << " to " << file << '\n';
}

void writeProtoAsJson(const google::protobuf::Message &message,
                      const std::string &path)
{
    buildboxcommon::ProtoJsonUtils::JsonPrintOptions jsonOptions;
    jsonOptions.add_whitespace = true; // make the output human-readable

    const std::unique_ptr<std::string> json =
        buildboxcommon::ProtoJsonUtils::protoToJsonString(message,
                                                          jsonOptions);

    if (json) {
        buildboxcommon::FileUtils::writeFileAtomically(path, *json);
    }
    else {
        std::cout << "Error converting message to JSON.\n";
    }
}

void fetchAndWriteExecutionStatistics(
    const buildboxcommon::ActionResult &actionResult,
    const std::string &executionStatsDest,
    buildboxcommon::CASClient *remoteCasClient)
{
    if (actionResult.execution_metadata().auxiliary_metadata().empty()) {
        return;
    }

    Digest d;
    if (!actionResult.execution_metadata().auxiliary_metadata(0).UnpackTo(
            &d)) {
        return;
    }

    BUILDBOX_LOG_DEBUG("ActionResult.execution_metadata.auxiliary_metadata "
                       "contains Digest: ["
                       << d << "], fetching message...");

    google::protobuf::Any anyWrapper;
    try {
        anyWrapper = remoteCasClient->fetchMessage<google::protobuf::Any>(d);
    }
    catch (const std::runtime_error &e) {
        BUILDBOX_LOG_ERROR("Failed to fetch blob referred to by Digest in "
                           "auxiliary metadata ["
                           << d << "]: " << e.what());
        return;
    }

    buildboxcommon::ExecutionStatistics stats;
    if (anyWrapper.UnpackTo(&stats)) {
        BUILDBOX_LOG_DEBUG("Writing `ExecutionStatistics` message to ["
                           << executionStatsDest << "]");
        writeProtoAsJson(stats, executionStatsDest);
    }
    else {
        BUILDBOX_LOG_DEBUG(
            "ActionResult.execution_metadata.auxiliary_metadata did "
            "not point to an `ExecutionStatistics` message, ignoring "
            "it.");
    }
}

void writeActionResultToDisk(const buildboxcommon::ActionResult &actionResult,
                             const std::string &destinationDirectory,
                             const std::string &metadataDir,
                             buildboxcommon::CASClient *remoteCasClient)

{
    buildboxcommon::CASClient::OutputMap blobs;
    std::vector<Digest> digests;

    buildboxcommon::FileDescriptor destination_dirfd(open(
        destinationDirectory.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC));
    if (destination_dirfd.get() < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening destination directory at path \""
                << destinationDirectory << "\"");
    }

    // Create an empty RemoteExecutionClient to make use of the downloadOutputs
    // method
    std::shared_ptr<buildboxcommon::GrpcClient> grpcClient =
        std::make_shared<buildboxcommon::GrpcClient>();
    std::shared_ptr<buildboxcommon::RemoteExecutionClient> reclient =
        std::make_shared<buildboxcommon::RemoteExecutionClient>(grpcClient,
                                                                grpcClient);
    reclient->downloadOutputs(remoteCasClient, actionResult,
                              destination_dirfd.get(), nullptr);

    const std::string stdoutDest = metadataDir + "/stdout";
    const std::string stderrDest = metadataDir + "/stderr";
    const std::string exitCodeDest = metadataDir + "/exit_code";
    const std::string metadataDest = metadataDir + "/metadata";
    const std::string executionStatsDest = metadataDir + "/execution_stats";

    if (actionResult.stdout_raw().size()) {
        buildboxcommon::FileUtils::writeFileAtomically(
            stdoutDest, std::string(actionResult.stdout_raw()));
        std::cout << "Wrote inline stdout to: " << metadataDir << '\n';
    }
    else if (actionResult.stdout_digest() != Digest()) {
        blobs.emplace(actionResult.stdout_digest().hash(),
                      buildboxcommon::CASClient::OutputMap::mapped_type(
                          stdoutDest, buildboxcommon::FileNode()));

        digests.push_back(actionResult.stdout_digest());
    }
    if (actionResult.stderr_raw().size()) {
        buildboxcommon::FileUtils::writeFileAtomically(
            stderrDest, actionResult.stderr_raw());
        std::cout << "Wrote inline stderr to: " << stderrDest << '\n';
    }
    else if (actionResult.stderr_digest() != Digest()) {
        blobs.emplace(actionResult.stderr_digest().hash(),
                      buildboxcommon::CASClient::OutputMap::mapped_type(
                          stderrDest, buildboxcommon::FileNode()));

        digests.push_back(actionResult.stderr_digest());
    }
    remoteCasClient->downloadBlobs(digests, blobs);

    writeProtoAsJson(actionResult.execution_metadata(), metadataDest);

    fetchAndWriteExecutionStatistics(actionResult, executionStatsDest,
                                     remoteCasClient);

    writeExitCode(exitCodeDest, actionResult.exit_code());
}

int main(int argc, char *argv[])
{
    auto cliArgs = std::span(argv, argc);
    buildboxcommon::logging::Logger::getLoggerInstance().initialize(
        cliArgs[0]);

    auto args = casdownload::processArgs(argc, argv);
    if (args.d_processed) {
        return 0;
    }
    if (!args.d_valid) {
        return 2;
    }

    BUILDBOX_LOG_SET_LEVEL(args.d_logLevel);

    buildboxcommon::DigestGenerator::init(args.d_digestFunctionValue);

    try {
        // connect to remote CAS server
        std::cout << "CAS client connecting to "
                  << args.d_casConnectionOptions.d_url << '\n';

        auto remoteGrpcClient = std::make_shared<buildboxcommon::GrpcClient>();
        remoteGrpcClient->init(args.d_casConnectionOptions);
        remoteGrpcClient->setToolDetails("casdownload",
                                         buildboxcommon::VERSION);
        const std::string toolInvocationId =
            buildboxcommon::StringUtils::getUUIDString();
        const std::string correlatedInvocationsId =
            buildboxcommon::StringUtils::getUUIDString();
        remoteGrpcClient->setRequestMetadata("", toolInvocationId,
                                             correlatedInvocationsId);
        buildboxcommon::CASClient remoteCasClient(remoteGrpcClient);
        remoteCasClient.init();

        auto downloadDirectory = args.d_destinationDir;

        if (!buildboxcommon::FileUtils::isDirectory(
                downloadDirectory.c_str())) {
            if (buildboxcommon::FileUtils::isRegularFile(
                    downloadDirectory.c_str())) {
                std::cerr << "Error: [" << downloadDirectory
                          << "] was specified "
                          << "as destination download directory but already "
                             "exists "
                          << "as file.\n";
                return 1;
            }
            const std::string d = calculateDirname(downloadDirectory);
            if (buildboxcommon::FileUtils::isRegularFile(d.c_str())) {
                std::cerr << "Error: [" << downloadDirectory
                          << "] was specified "
                          << "as destination download directory but " << d
                          << " is a file.\n";
                return 1;
            }
        }

        buildboxcommon::FileUtils::createDirectory(downloadDirectory.c_str());
        std::cout << "Starting to download " << args.d_digest << " to \""
                  << downloadDirectory << "\"\n";

        auto start = std::chrono::high_resolution_clock::now();

        if (args.d_digestType == casdownload::DIGEST_ACTION) {
            buildboxcommon::ActionResult actionResult;
            if (!actionResultFromAction(
                    args.d_digest, args.d_casConnectionOptions,
                    remoteGrpcClient.get(), &remoteCasClient, &actionResult)) {
                std::cerr << "Failed to download action result with digest "
                          << args.d_digest << '\n';
                return 1;
            }
            const std::string metadataDir =
                downloadDirectory + "/" + args.d_digest.hash() + "-out";
            buildboxcommon::FileUtils::createDirectory(metadataDir.c_str());
            writeActionResultToDisk(actionResult, downloadDirectory,
                                    metadataDir, &remoteCasClient);
        }
        else if (args.d_digestType == casdownload::DIGEST_ROOT) {
            remoteCasClient.downloadDirectory(args.d_digest,
                                              args.d_destinationDir);
        }
        else if (args.d_digestType == casdownload::DIGEST_TREE) {
            remoteCasClient.downloadTree(args.d_digest, args.d_destinationDir);
        }
        else {
            try {
                const std::string blob =
                    remoteCasClient.fetchString(args.d_digest);
                buildboxcommon::FileUtils::writeFileAtomically(
                    downloadDirectory + "/blob", blob);

                std::cout << "Downloaded blob to "
                          << downloadDirectory + "/blob\n";
            }
            catch (const std::exception &) {
                std::cerr << "Could not download blob with digest "
                          << args.d_digest << '\n';
                return 1;
            }
        }

        auto end = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> elapsed = end - start;

        std::cout << "Finished downloading " << args.d_digest << " to \""
                  << downloadDirectory << "\" in " << std::fixed
                  << std::setprecision(3) << elapsed.count() << " second(s)\n";
        return 0;
    }
    catch (const std::exception &e) {
        std::cerr << "ERROR: Caught exception [" << e.what()
                  << "] while running " << cliArgs[0] << '\n';
        return 1;
    }
}
