/*
 * Copyright 2020 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 <rexplorer.grpc.pb.h>
#include <rexplorer_cmdlinespec.h>

#include <buildboxcommon_casclient.h>
#include <buildboxcommon_commandline.h>
#include <buildboxcommon_connectionoptions.h>
#include <buildboxcommon_connectionoptions_commandline.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_logging_commandline.h>
#include <buildboxcommon_protojsonutils.h>
#include <buildboxcommon_protos.h>
#include <buildboxcommon_stringutils.h>
#include <buildboxcommon_version.h>

#include <cstring>
#include <iostream>
#include <regex>

using namespace buildboxcommon;
using namespace rexplorer;

bool digestFromString(const std::string &s, Digest *digest)
{
    // "[hash in hex notation]/[size_bytes]"
    static const std::regex regex("([0-9a-fA-F]+)/(\\d+)");
    std::smatch matches;
    if (std::regex_search(s, matches, regex) && matches.size() == 3) {
        const std::string hash = matches[1];
        const std::string size = matches[2];

        digest->set_hash(hash);
        digest->set_size_bytes(std::stoll(size));
        return true;
    }

    return false;
}

std::string
protoToJson(const google::protobuf::Message &message,
            const buildboxcommon::ProtoJsonUtils::JsonPrintOptions &options)
{
    std::unique_ptr<std::string> jsonPtr =
        buildboxcommon::ProtoJsonUtils::protoToJsonString(message, options);
    if (jsonPtr != nullptr) {
        return std::move(*jsonPtr);
    }

    BUILDBOXCOMMON_THROW_EXCEPTION(
        std::runtime_error,
        "Unable to convert " << message.GetTypeName() << " to json format");
}

void populateFSEntry(FSEntry *entry,
                     const buildboxcommon::Digest &directoryDigest,
                     const int level, buildboxcommon::CASClient &casClient)
{
    // We had a positive level and reached the limit, don't go any deeper
    if (level == 0) {
        return;
    }
    buildboxcommon::Directory dir =
        casClient.fetchMessage<buildboxcommon::Directory>(directoryDigest);
    for (const buildboxcommon::DirectoryNode &subdir : dir.directories()) {
        FSEntry *dirEntry = entry->add_contents();
        dirEntry->set_name(subdir.name());
        dirEntry->set_type("directory");
        dirEntry->set_digest(toString(subdir.digest()));
        populateFSEntry(dirEntry, subdir.digest(), level - 1, casClient);
    }
    for (const buildboxcommon::FileNode &file : dir.files()) {
        FSEntry *fileEntry = entry->add_contents();
        fileEntry->set_type("file");
        fileEntry->set_name(file.name());
        fileEntry->set_digest(toString(file.digest()));
    }

    return;
}

void processRootDigest(const buildboxcommon::Digest &root_digest,
                       const CommandLine &commandLine,
                       buildboxcommon::CASClient &casClient,
                       RexplorerOutput *rexOut)
{
    buildboxcommon::ProtoJsonUtils::JsonPrintOptions jsonOptions;
    buildboxcommon::jsonOptionsAlwaysPrintFieldsWithNoPresence(jsonOptions);

    if (commandLine.exists("pretty")) {
        jsonOptions.add_whitespace = true;
    }

    FSEntry rootEntry;
    populateFSEntry(&rootEntry, root_digest, commandLine.getInt("depth"),
                    casClient);
    *rexOut->mutable_root() = rootEntry.contents();
}

void processActionDigest(const buildboxcommon::Digest &action_digest,
                         const buildboxcommon::ConnectionOptions &d_acServer,
                         const CommandLine &commandLine,
                         buildboxcommon::CASClient &casClient,
                         RexplorerOutput *rexOut)
{
    buildboxcommon::ProtoJsonUtils::JsonPrintOptions jsonOptions;
    buildboxcommon::jsonOptionsAlwaysPrintFieldsWithNoPresence(jsonOptions);

    if (commandLine.exists("pretty")) {
        jsonOptions.add_whitespace = true;
    }

    buildboxcommon::Action action =
        casClient.fetchMessage<buildboxcommon::Action>(action_digest);

    buildboxcommon::Command command =
        casClient.fetchMessage<buildboxcommon::Command>(
            action.command_digest());

    InlinedAction inlineAction;
    *inlineAction.mutable_command() = command;
    *inlineAction.mutable_timeout() = action.timeout();
    inlineAction.set_do_not_cache(action.do_not_cache());
    *inlineAction.mutable_salt() = action.salt();
    *inlineAction.mutable_platform() = action.platform();

    FSEntry rootEntry;
    populateFSEntry(&rootEntry, action.input_root_digest(),
                    commandLine.getInt("depth"), casClient);

    *inlineAction.mutable_input_root() = rootEntry.contents();
    *rexOut->mutable_action() = inlineAction;

    buildboxcommon::GetActionResultRequest actionReq;
    buildboxcommon::ActionResult actionRes;
    actionReq.set_instance_name(d_acServer.d_instanceName);
    *actionReq.mutable_action_digest() = action_digest;
    grpc::ClientContext context;
    std::unique_ptr<buildboxcommon::ActionCache::Stub> actionCache =
        buildboxcommon::ActionCache::NewStub(d_acServer.createChannel());
    const grpc::Status status =
        actionCache->GetActionResult(&context, actionReq, &actionRes);
    if (!status.ok()) {
        if (status.error_code() != grpc::StatusCode::NOT_FOUND) {
            BUILDBOXCOMMON_THROW_EXCEPTION(
                std::runtime_error,
                "GetActionResult returned unexpected error ["
                    << status.error_code() << ": " << status.error_message()
                    << "]");
        }
    }
    else {
        *rexOut->mutable_action_result() = actionRes;
    }
}

int main(int argc, char *argv[])
{
    try {
        CmdLineSpec cmdSpec;
        CommandLine commandLine(cmdSpec.d_spec);
        const bool success = commandLine.parse(argc, argv);

        if (!success) {
            commandLine.usage();
            return 1;
        }

        if (commandLine.exists("help") || commandLine.exists("version")) {
            return 0;
        }

        // Ensure one and only one of "action" or "root-digest" is set
        int optionCount = 0;
        if (commandLine.exists("action")) {
            ++optionCount;
        }
        if (commandLine.exists("root-digest")) {
            ++optionCount;
        }

        if (optionCount != 1) {
            BUILDBOXCOMMON_THROW_EXCEPTION(
                std::runtime_error, "Exactly one of 'action' or 'root-digest' "
                                    " must be specified.");
        }

        buildboxcommon::logging::Logger::getLoggerInstance().initialize(
            commandLine.processName().c_str());

        LogLevel logLevel = buildboxcommon::LogLevel::ERROR;
        if (!buildboxcommon::parseLoggingOptions(commandLine, logLevel)) {
            return 1;
        }
        BUILDBOX_LOG_SET_LEVEL(logLevel);

        // Initialize digest generator

        DigestFunction_Value digestFunctionValue =
            commandLine.exists("digest-function")
                ? buildboxcommon::DigestGenerator::stringToDigestFunction(
                      commandLine.getString("digest-function"))
                : static_cast<buildboxcommon::DigestFunction_Value>(
                      BUILDBOXCOMMON_DIGEST_FUNCTION_VALUE);
        buildboxcommon::DigestGenerator::init(digestFunctionValue);

        // Set the connection channels

        buildboxcommon::ConnectionOptions d_casServer;
        ConnectionOptionsCommandLine::configureChannel(commandLine, "",
                                                       &d_casServer);
        buildboxcommon::ConnectionOptionsCommandLine::updateChannelOptions(
            commandLine, "cas-", &d_casServer);
        auto grpcClient = std::make_shared<buildboxcommon::GrpcClient>();
        grpcClient->init(d_casServer);
        grpcClient->setToolDetails("rexplorer", buildboxcommon::VERSION);
        const std::string toolInvocationId =
            buildboxcommon::StringUtils::getUUIDString();
        const std::string correlatedInvocationsId =
            buildboxcommon::StringUtils::getUUIDString();
        grpcClient->setRequestMetadata("", toolInvocationId,
                                       correlatedInvocationsId);
        buildboxcommon::CASClient casClient(grpcClient);
        casClient.init();

        buildboxcommon::ConnectionOptions d_acServer;
        buildboxcommon::ConnectionOptionsCommandLine::configureChannel(
            commandLine, "", &d_acServer);
        buildboxcommon::ConnectionOptionsCommandLine::updateChannelOptions(
            commandLine, "ac-", &d_acServer);

        RexplorerOutput rexOut;
        if (commandLine.exists("action")) {
            buildboxcommon::Digest action_digest;
            if (!digestFromString(commandLine.getString("action"),
                                  &action_digest)) {
                BUILDBOXCOMMON_THROW_EXCEPTION(
                    std::runtime_error, "Given action ["
                                            << commandLine.getString("action")
                                            << "] is not a valid action");
            }
            processActionDigest(action_digest, d_acServer, commandLine,
                                casClient, &rexOut);
        }
        else if (commandLine.exists("root-digest")) {
            buildboxcommon::Digest root_digest;
            if (!digestFromString(commandLine.getString("root-digest"),
                                  &root_digest)) {
                BUILDBOXCOMMON_THROW_EXCEPTION(
                    std::runtime_error,
                    "Given root [" << commandLine.getString("root-digest")
                                   << "] is not a valid root");
            }
            processRootDigest(root_digest, commandLine, casClient, &rexOut);
        }

        buildboxcommon::ProtoJsonUtils::JsonPrintOptions jsonOptions;

        buildboxcommon::jsonOptionsAlwaysPrintFieldsWithNoPresence(
            jsonOptions);
        if (commandLine.exists("pretty")) {
            jsonOptions.add_whitespace = true;
        }

        std::cout << protoToJson(rexOut, jsonOptions);

        return 0;
    }
    catch (const std::exception &exception) {
        // This catch should not be relied on and is here as a backstop.
        BUILDBOX_LOG_ERROR("Caught exception in main " << exception.what());
        return 1;
    }
}
