/*
 * Copyright 2022 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_connectionoptions_toml.h>
#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_logging_commandline.h>
#include <buildboxcommon_merklize.h>
#include <buildboxcommon_stringutils.h>
#include <buildboxcommon_tomlutils.h>
#include <cstddef>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <trexe_executionoptions.h>

#include <functional>
#include <vector>

namespace trexe {

using namespace buildboxcommon;

std::pair<std::string, std::string> splitKeyValuePair(const std::string &s)
{
    const auto splitIndex = s.find_first_of('=');
    if (splitIndex == std::string::npos) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error, "Incorrect argument format: "
                                    << s << ". Please use KEY=VALUE syntax");
    }
    return {s.substr(0, splitIndex), s.substr(splitIndex + 1)};
}

InputPathOption InputPathOption::fromCLI(const std::string &s)
{

    std::vector<std::string> parts = StringUtils::split(s, ":");
    InputPathOption option(parts[0]);
    if (parts.size() > 1) {
        option.d_remote = parts[1];
    }
    if (parts.size() > 2) {
        const auto &properties = StringUtils::split(parts[2], ",");
        for (const auto &property : properties) {
            if (property == "mtime") {
                option.d_captureMtime = true;
            }
            else {
                // Other supported properties are related to file permission
                if (option.d_permission !=
                    InputPathOption::FilePermission::NONE) {
                    BUILDBOXCOMMON_THROW_EXCEPTION(
                        std::invalid_argument, "Input path file permission "
                                               "cannot be set more than once");
                }
                option.d_permission = filePermissionFromString(property);
            }
        }
    }
    return option;
}

InputPathOption::FilePermission
InputPathOption::filePermissionFromString(const std::string &s)
{
    if (s == "capture" || s == "unix_mode") {
        return InputPathOption::FilePermission::CAPTURE;
    }
    else if (s == "ro") {
        return InputPathOption::FilePermission::READ_ONLY;
    }
    else if (s == "rw") {
        return InputPathOption::FilePermission::READ_WRITE;
    }
    BUILDBOXCOMMON_THROW_EXCEPTION(
        std::invalid_argument, "Invalid input path file permission: " << s);
}

bool InputPathOption::operator==(const InputPathOption &other) const
{
    return d_local == other.d_local && d_remote == other.d_remote &&
           d_permission == other.d_permission &&
           d_captureMtime == other.d_captureMtime &&
           d_nodeProperties == other.d_nodeProperties &&
           ((d_ignoreMatcher == nullptr && other.d_ignoreMatcher == nullptr) ||
            (d_ignoreMatcher != nullptr && other.d_ignoreMatcher != nullptr &&
             *d_ignoreMatcher == *other.d_ignoreMatcher));
}

std::ostream &operator<<(std::ostream &os,
                         const InputPathOption &inputPathOption)
{
    const std::string &remoteStr = inputPathOption.d_remote.has_value()
                                       ? inputPathOption.d_remote.value()
                                       : inputPathOption.d_local;
    os << "InputPath=["
       << "local=" << inputPathOption.d_local << ", remote=" << remoteStr
       << "]";
    return os;
}

std::vector<std::pair<std::string, std::string>>
pairStringToVector(const std::string &sourcePairString,
                   const std::string &argName)
{
    std::vector<std::pair<std::string, std::string>> pairVector = {};
    if (!sourcePairString.empty()) {
        std::istringstream pairStream(sourcePairString);
        while (pairStream) {
            std::string s;
            std::getline(pairStream, s, ',');
            if (s.empty()) { // stream adds an extra blank character we need to
                             // skip
                continue;
            }
            pairVector.emplace_back(splitKeyValuePair(s));
        }
    }
    return pairVector;
};

void ExecutionOptions::validate() const
{
    switch (mode()) {
        case TrexeMode::CANCEL:
        case TrexeMode::DOWNLOAD:
        case TrexeMode::WAIT_DOWNLOAD:
            // Impossible code path for now, just for understandability
            if (d_operation.empty()) {
                throw std::runtime_error("An operation-id is required for "
                                         "cancellation or download");
            }
            break;
        case TrexeMode::CACHE_ONLY_EXECUTION:
        case TrexeMode::REMOTE_EXECUTION_BLOCKING:
        case TrexeMode::REMOTE_EXECUTION_ASYNC:
            if (d_argv.empty()) {
                throw std::runtime_error(
                    "A command is required to be executed");
            }
            break;
    }
}

TrexeMode ExecutionOptions::mode() const
{
    if (!d_operation.empty()) {
        if (d_cancelMode) {
            return TrexeMode::CANCEL;
        }
        return isBlocking() ? TrexeMode::WAIT_DOWNLOAD : TrexeMode::DOWNLOAD;
    }
    if (d_cacheOnly) {
        return TrexeMode::CACHE_ONLY_EXECUTION;
    }
    return isBlocking() ? TrexeMode::REMOTE_EXECUTION_BLOCKING
                        : TrexeMode::REMOTE_EXECUTION_ASYNC;
}

bool ExecutionOptions::isBlocking() const
{
    if (d_blockingExecute.has_value()) {
        return d_blockingExecute.value();
    }
    // For backward compatibility, if the operation is set (usually for
    // downloading outputs), we default to non-blocking
    return d_operation.empty();
}

ExecutionOptions
ExecutionOptions::fromCommandLine(const std::vector<std::string> &argv,
                                  const buildboxcommon::CommandLine &cmd)
{
    ExecutionOptions options;
    if (cmd.exists("log-level") || cmd.exists("verbose")) {
        if (!buildboxcommon::parseLoggingOptions(cmd, options.d_logLevel)) {
            throw std::invalid_argument("Invalid logging option");
        }
    }

    if (cmd.exists("config-file")) {
        toml::table configTable =
            toml::parse_file(cmd.getString("config-file"));
        updateFromTOML(configTable, options);
    }
    if (cmd.exists("cache-only")) {
        options.d_cacheOnly = true;

        if (cmd.exists("buildbox-run")) {
            options.d_runnerCommand = cmd.getString("buildbox-run");
        }
        if (cmd.exists("runner-arg")) {
            options.d_extraRunArgs = cmd.getVS("runner-arg");
        }
    }
    if (cmd.exists("cancel")) {
        options.d_cancelMode = true;
    }
    if (cmd.exists("d")) {
        options.d_downloadResultsPath = cmd.getString("d");
    }
    if (cmd.exists("stdout-file")) {
        options.d_stdoutFile = cmd.getString("stdout-file");
    }
    if (cmd.exists("stderr-file")) {
        options.d_stderrFile = cmd.getString("stderr-file");
    }
    if (!argv.empty()) {
        options.d_argv = argv;
    }

    if (cmd.exists("working-dir")) {
        options.d_workingDir = cmd.getString("working-dir");
    }

    if (cmd.exists("input-path")) {
        const auto inputPaths = cmd.getVS("input-path");
        for (const auto &path : inputPaths) {
            options.d_inputPaths.push_back(InputPathOption::fromCLI(path));
        }
    }

    if (cmd.exists("input-root-digest")) {
        options.d_inputRootDigest = cmd.getString("input-root-digest");
    }

    if (cmd.exists("output-path")) {
        const auto outputPaths = cmd.getVS("output-path");
        for (const auto &path : outputPaths) {
            options.d_outputPaths.emplace(path);
        }
    }

    if (cmd.exists("output-node-properties")) {
        const auto outputNodeProperties = cmd.getVS("output-node-properties");
        for (const auto &property : outputNodeProperties) {
            options.d_outputNodeProperties.emplace(property);
        }
    }

    if (cmd.exists("platform-properties")) {
        const auto pairs = pairStringToVector(
            cmd.getString("platform-properties"), "platform-properties");
        for (const auto &pair : pairs) {
            options.d_platform.insert(pair);
        }
    }

    if (cmd.exists("environment")) {

        const auto pairs =
            pairStringToVector(cmd.getString("environment"), "environment");
        for (const auto &pair : pairs) {
            options.d_environment[pair.first] = pair.second;
        }
    }
    if (cmd.exists("env")) {
        for (const auto &envPair : cmd.getVS("env")) {
            const auto pair = splitKeyValuePair(envPair);
            options.d_environment[pair.first] = pair.second;
        }
    }

    options.d_digestFunction =
        cmd.exists("digest-function")
            ? buildboxcommon::DigestGenerator::stringToDigestFunction(
                  cmd.getString("digest-function"))
            : static_cast<buildboxcommon::DigestFunction_Value>(
                  BUILDBOXCOMMON_DIGEST_FUNCTION_VALUE);

    if (cmd.exists("exec-timeout")) {
        options.d_execTimeout = cmd.getInt("exec-timeout");
    }
    if (cmd.exists("skip-cache-lookup")) {
        options.d_skipCacheLookup = cmd.getBool("skip-cache-lookup");
    }
    if (cmd.exists("do-not-cache")) {
        options.d_doNotCache = cmd.getBool("do-not-cache");
    }

    {
        // Default behavior to follow symlinks
        if (cmd.exists("follow-symlinks") &&
            cmd.exists("do-not-follow-symlinks")) {
            throw std::runtime_error(
                "Cannot set --follow-symlinks and --do-not-follow-symlinks at "
                "the same time");
        }
        if (cmd.exists("follow-symlinks")) {
            options.d_followSymlinks = true;
        }
        if (cmd.exists("do-not-follow-symlinks")) {
            options.d_followSymlinks = false;
        }
    }

    if (cmd.exists("salt")) {
        options.d_salt = cmd.getString("salt");
    }
    if (cmd.exists("priority")) {
        options.d_priority = std::make_shared<int>(cmd.getInt("priority"));
    }

    if (cmd.exists("no-wait") && cmd.exists("wait")) {
        throw std::runtime_error("Requests cannot be both blocking and async");
    }
    if (cmd.exists("wait")) {
        options.d_blockingExecute = true;
    }
    if (cmd.exists("no-wait")) {
        options.d_blockingExecute = false;
    }

    if (cmd.exists("log-progress")) {
        options.d_logProgress = cmd.getBool("log-progress");
    }

    if (cmd.exists("operation")) {
        options.d_operation = cmd.getString("operation");
    }

    if (cmd.exists("result-metadata-file")) {
        options.d_resultMetadataFile = cmd.getString("result-metadata-file");
    }
    if (cmd.exists("action-result-json")) {
        options.d_actionResultJsonFile = cmd.getString("action-result-json");
    }
    if (cmd.exists("stream-logs")) {
        options.d_streamLogs = true;
    }

    if (cmd.exists("use-localcas")) {
        options.d_useLocalCas = true;
    }

    if (cmd.exists("localcas-skip-upload")) {
        options.d_localCasSkipUpload = true;
    }

    if (cmd.exists("num-digest-threads")) {
        int numDigestThreads = cmd.getInt("num-digest-threads");
        if (numDigestThreads < 0) {
            throw std::invalid_argument(
                "Invalid value for --num-digest-threads " +
                std::to_string(numDigestThreads));
        }
        options.d_numDigestThreads = numDigestThreads;
    }

    if (cmd.exists("num-io-threads")) {
        int numIOThreads = cmd.getInt("num-io-threads");
        if (numIOThreads < 0) {
            throw std::invalid_argument("Invalid value for --num-io-threads " +
                                        std::to_string(numIOThreads));
        }
        options.d_numIOThreads = numIOThreads;
    }

    // Tool name and version are set by CMAKE, and constructed using additional
    // input if provided
    if (cmd.exists("tool-name")) {
        options.d_metadata["tool-name"] = TREXE_METADATA_TOOL_NAME +
                                          std::string(":") +
                                          cmd.getString("tool-name");
    }
    if (cmd.exists("tool-version")) {
        options.d_metadata["tool-version"] = buildboxcommon::VERSION +
                                             std::string(":") +
                                             cmd.getString("tool-version");
    }

    // "" is the proto default for string, so they are set as "" in the
    // constructor to allow us to treat the arguments as default if they aren't
    // present, yet still send the action_id
    if (cmd.exists("tool-invocation-id")) {
        options.d_metadata["tool-invocation-id"] =
            cmd.getString("tool-invocation-id");
    }
    if (cmd.exists("correlated-invocations-id")) {
        options.d_metadata["correlated-invocations-id"] =
            cmd.getString("correlated-invocations-id");
    }
    if (cmd.exists("action-mnemonic")) {
        options.d_metadata["action-mnemonic"] =
            cmd.getString("action-mnemonic");
    }
    if (cmd.exists("target-id")) {
        options.d_metadata["target-id"] = cmd.getString("target-id");
    }
    if (cmd.exists("configuration-id")) {
        options.d_metadata["configuration-id"] =
            cmd.getString("configuration-id");
    }

    if (cmd.exists("locality-hint")) {
        *options.d_schedulingMetadata.mutable_locality_hint() =
            cmd.getString("locality-hint");
    }

    if (cmd.exists("metrics-mode")) {
        options.d_metricMode = cmd.getString("metrics-mode");
    }
    if (cmd.exists("metrics-publish-interval")) {
        options.d_metricPublishInterval =
            cmd.getInt("metrics-publish-interval");
    }
    if (cmd.exists("metrics-prefix")) {
        options.d_metricPrefix = cmd.getString("metrics-prefix");
    }

    // Update connections from common CLI options
    ConnectionOptionsCommandLine::updateChannelOptions(cmd, "",
                                                       &options.d_execConn);
    ConnectionOptionsCommandLine::updateChannelOptions(cmd, "",
                                                       &options.d_casConn);
    ConnectionOptionsCommandLine::updateChannelOptions(cmd, "",
                                                       &options.d_acConn);
    ConnectionOptionsCommandLine::updateChannelOptions(cmd, "",
                                                       &options.d_lsConn);

    // Update connections from individual CLI options
    ConnectionOptionsCommandLine::updateChannelOptions(cmd, "exec-",
                                                       &options.d_execConn);
    ConnectionOptionsCommandLine::updateChannelOptions(cmd, "cas-",
                                                       &options.d_casConn);
    ConnectionOptionsCommandLine::updateChannelOptions(cmd, "ac-",
                                                       &options.d_acConn);
    ConnectionOptionsCommandLine::updateChannelOptions(cmd, "logstream-",
                                                       &options.d_lsConn);

    return options;
};

std::ostream &operator<<(std::ostream &os, const ExecutionOptions &eo)
{
    os << "ExecutionOptions:"
       << "Command=[";
    for (const auto &argv : eo.d_argv) {
        os << "'" << argv << "' ";
    }
    os << "]"
       << ", DownloadResults=[" << eo.d_downloadResultsPath << "]"
       << ", WorkingDirectory=[" << eo.d_workingDir << "]"
       << ", InputPaths=[";
    for (const auto &path : eo.d_inputPaths) {
        os << path << ";";
    }
    os << "]"
       << ", OutputPaths=[";
    for (const auto &path : eo.d_outputPaths) {
        os << path << ";";
    }
    os << "]"
       << ", Platform=[";
    for (const auto &pair : eo.d_platform) {
        os << "(" << pair.first << "," << pair.second << ");";
    }
    os << "]"
       << ", Environment=[";
    for (const auto &pair : eo.d_environment) {
        os << "(" << pair.first << "," << pair.second << ");";
    }
    os << "]"
       << ", BlockingExecute=[" << eo.isBlocking() << "]"
       << ", ExecutionTimeout=[" << eo.d_execTimeout << "]"
       << ", SkipCacheLookup=[" << eo.d_skipCacheLookup << "]"
       << ", DoNotCache=[" << !eo.d_doNotCache << "]";
    if (eo.d_priority != nullptr) {
        os << ", Priority=[" << *eo.d_priority << "]";
    }
    os << ", Metadata=[ "
       << "tool-name=[" << eo.d_metadata.at("tool-name") << "], "
       << "tool-version=[" << eo.d_metadata.at("tool-version") << "], "
       << "tool-invocation-id=[" << eo.d_metadata.at("tool-invocation-id")
       << "], "
       << "correlated-invocations-id=["
       << eo.d_metadata.at("correlated-invocations-id") << "], "
       << "GetCompletedOperation=[" << eo.d_operation << "]";

    return os;
};

std::map<std::string, std::string> readNodeProperties(const toml::node &node,
                                                      AccessPath &path)
{
    if (const auto table = node.as_table()) {
        std::map<std::string, std::string> result;
        for (const auto &pair : *table) {
            const auto &name = pair.first.str();
            path.emplace_back(std::string(name));
            result.emplace(
                name, TOMLUtils::readValue<std::string>(pair.second, path));
            path.pop_back();
        }
        return result;
    }
    TOMLUtils::throwTOMLTypeError(node, toml::node_type::table, path,
                                  "node-property");
}

InputPathOption readInputPath(const toml::node &node, AccessPath &path)
{
    if (const auto table = node.as_table()) {
        InputPathOption::FilePermission permission =
            InputPathOption::FilePermission::NONE;
        const auto permissionNode = table->at_path("file-permission");
        if (permissionNode.is_string()) {
            permission = InputPathOption::filePermissionFromString(
                permissionNode.as_string()->get());
        }
        bool captureMtime = false;
        const auto mtimeNode = table->at_path("mtime");
        if (mtimeNode.is_string()) {
            if (mtimeNode.as_string()->get() == "capture") {
                captureMtime = true;
            }
            else {
                BUILDBOXCOMMON_THROW_EXCEPTION(
                    std::invalid_argument,
                    "Invalid input path mtime: '"
                        << mtimeNode.as_string()->get()
                        << "'. Supported values are: [capture]");
            }
        }

        std::map<std::string, std::string> nodeProperties;
        if (const auto optNodeProperties = TOMLUtils::readOptionalFromTable<
                std::map<std::string, std::string>>(readNodeProperties, *table,
                                                    "node-property", path)) {
            nodeProperties = optNodeProperties.value();
        }

        std::shared_ptr<std::vector<buildboxcommon::IgnorePattern>>
            ignorePatterns;
        if (const auto ignorePatternStrs =
                TOMLUtils::readOptionalFromTable<std::vector<std::string>>(
                    std::bind(TOMLUtils::readFromArray<std::string>,
                              TOMLUtils::readValue<std::string>,
                              std::placeholders::_1, std::placeholders::_2),
                    *table, "ignore", path)) {
            ignorePatterns =
                IgnoreMatcher::parseIgnorePatterns(ignorePatternStrs.value());
        }

        // no path remapping
        const auto path = table->at_path("path");
        if (path.is_string()) {
            return {path.as_string()->get(),
                    {},
                    permission,
                    captureMtime,
                    nodeProperties,
                    ignorePatterns};
        }
        // path remapping
        const auto local = table->at_path("local");
        const auto remote = table->at_path("remote");
        if (local.is_string() && remote.is_string()) {
            return {local.as_string()->get(),
                    remote.as_string()->get(),
                    permission,
                    captureMtime,
                    nodeProperties,
                    ignorePatterns};
        }
    }
    TOMLUtils::throwTOMLTypeError(node, toml::node_type::table, path,
                                  "{local,remote} or {path}");
}

std::vector<std::pair<std::string, std::string>>
readPlatformProperties(const toml::node &node, AccessPath &path)
{
    if (const auto table = node.as_table()) {
        std::vector<std::pair<std::string, std::string>> result;
        for (const auto &pair : *table) {
            const auto &name = pair.first.str();
            if (pair.second.is_string()) {
                // single string value
                result.emplace_back(name, pair.second.as_string()->get());
            }
            else {
                // array of strings
                path.emplace_back(std::string(name));
                for (const auto &property :
                     TOMLUtils::readFromArray<std::string>(
                         TOMLUtils::readValue<std::string>, pair.second,
                         path)) {
                    result.emplace_back(name, property);
                }
                path.pop_back();
            }
        }
        return result;
    }
    TOMLUtils::throwTOMLTypeError(node, toml::node_type::table, path,
                                  "platform");
}

std::vector<std::pair<std::string, std::string>>
readEnviroment(const toml::node &node, AccessPath &path)
{
    if (const auto table = node.as_table()) {
        std::vector<std::pair<std::string, std::string>> result;
        for (const auto &pair : *table) {
            const auto &name = pair.first.str();
            path.emplace_back(std::string(name));
            result.emplace_back(
                name, TOMLUtils::readValue<std::string>(pair.second, path));
            path.pop_back();
        }
        return result;
    }
    TOMLUtils::throwTOMLTypeError(node, toml::node_type::table, path,
                                  "environment");
}

LogLevel readLogLevel(const toml::node &node, AccessPath &path)
{
    if (const auto s = node.as_string()) {
        const auto &stringToLog = logging::stringToLogLevelMap();
        if (stringToLog.find(s->get()) != stringToLog.end()) {
            return stringToLog.at(s->get());
        }
        TOMLUtils::throwTOMLTypeError(node, toml::node_type::string, path,
                                      logging::stringifyLogLevels());
    }
    TOMLUtils::throwTOMLTypeError(node, toml::node_type::string, path,
                                  logging::stringifyLogLevels());
}

ExecutionOptions ExecutionOptions::newFromTOML(const toml::table &table)
{
    ExecutionOptions result;
    updateFromTOML(table, result);
    return result;
}

void ExecutionOptions::updateFromTOML(const toml::table &table,
                                      ExecutionOptions &options)
{
    AccessPath path;
    if (const auto logLevel = TOMLUtils::readOptionalFromTable<LogLevel>(
            readLogLevel, table, "log-level", path)) {
        options.d_logLevel = logLevel.value();
    }
    if (const auto cacheOnly = TOMLUtils::readOptionalFromTable<bool>(
            TOMLUtils::readValue<bool>, table, "cache-only", path)) {
        options.d_cacheOnly = cacheOnly.value();
    }
    if (const auto buildboxRun = TOMLUtils::readOptionalFromTable<std::string>(
            TOMLUtils::readValue<std::string>, table, "buildbox-run", path)) {
        options.d_runnerCommand = buildboxRun.value();
    }
    if (const auto runnerArgs =
            TOMLUtils::readOptionalFromTable<std::vector<std::string>>(
                std::bind(TOMLUtils::readFromArray<std::string>,
                          TOMLUtils::readValue<std::string>,
                          std::placeholders::_1, std::placeholders::_2),
                table, "runner-args", path)) {
        options.d_extraRunArgs = runnerArgs.value();
    }
    if (const auto cancel = TOMLUtils::readOptionalFromTable<bool>(
            TOMLUtils::readValue<bool>, table, "cancel", path)) {
        options.d_cancelMode = cancel.value();
    }
    if (const auto downloadPath =
            TOMLUtils::readOptionalFromTable<std::string>(
                TOMLUtils::readValue<std::string>, table, "download-path",
                path)) {
        options.d_downloadResultsPath = downloadPath.value();
    }
    if (const auto stdoutFile = TOMLUtils::readOptionalFromTable<std::string>(
            TOMLUtils::readValue<std::string>, table, "stdout-file", path)) {
        options.d_stdoutFile = stdoutFile.value();
    }
    if (const auto stderrFile = TOMLUtils::readOptionalFromTable<std::string>(
            TOMLUtils::readValue<std::string>, table, "stderr-file", path)) {
        options.d_stderrFile = stderrFile.value();
    }
    if (const auto command =
            TOMLUtils::readOptionalFromTable<std::vector<std::string>>(
                std::bind(TOMLUtils::readFromArray<std::string>,
                          TOMLUtils::readValue<std::string>,
                          std::placeholders::_1, std::placeholders::_2),
                table, "command", path)) {
        options.d_argv = command.value();
    }
    if (const auto workingDir = TOMLUtils::readOptionalFromTable<std::string>(
            TOMLUtils::readValue<std::string>, table, "working-dir", path)) {
        options.d_workingDir = workingDir.value();
    }
    if (const auto inputPaths =
            TOMLUtils::readOptionalFromTable<std::vector<InputPathOption>>(
                std::bind(TOMLUtils::readFromArray<InputPathOption>,
                          readInputPath, std::placeholders::_1,
                          std::placeholders::_2),
                table, "input-path", path)) {
        for (const auto &path : inputPaths.value()) {
            options.d_inputPaths.emplace_back(path);
        }
    }
    if (const auto inputRootDigest =
            TOMLUtils::readOptionalFromTable<std::string>(
                TOMLUtils::readValue<std::string>, table, "input-root-digest",
                path)) {
        options.d_inputRootDigest = inputRootDigest.value();
    }
    if (const auto outputPaths =
            TOMLUtils::readOptionalFromTable<std::vector<std::string>>(
                std::bind(TOMLUtils::readFromArray<std::string>,
                          TOMLUtils::readValue<std::string>,
                          std::placeholders::_1, std::placeholders::_2),
                table, "output-path", path)) {
        for (const auto &path : outputPaths.value()) {
            options.d_outputPaths.insert(path);
        }
    }
    if (const auto outputNodeProperties =
            TOMLUtils::readOptionalFromTable<std::vector<std::string>>(
                std::bind(TOMLUtils::readFromArray<std::string>,
                          TOMLUtils::readValue<std::string>,
                          std::placeholders::_1, std::placeholders::_2),
                table, "output-node-property", path)) {
        for (const auto &property : outputNodeProperties.value()) {
            options.d_outputNodeProperties.insert(property);
        }
    }
    if (const auto platform = TOMLUtils::readOptionalFromTable<
            std::vector<std::pair<std::string, std::string>>>(
            readPlatformProperties, table, "platform", path)) {
        for (const auto &pair : platform.value()) {
            options.d_platform.insert(pair);
        }
    }
    if (const auto environment = TOMLUtils::readOptionalFromTable<
            std::vector<std::pair<std::string, std::string>>>(
            readEnviroment, table, "environment", path)) {
        for (const auto &pair : environment.value()) {
            options.d_environment[pair.first] = pair.second;
        }
    }
    if (const auto execTimeout = TOMLUtils::readOptionalFromTable<int64_t>(
            TOMLUtils::readValue<int64_t>, table, "exec-timeout", path)) {
        options.d_execTimeout = static_cast<int>(execTimeout.value());
    }
    if (const auto skipCacheLookup = TOMLUtils::readOptionalFromTable<bool>(
            TOMLUtils::readValue<bool>, table, "skip-cache-lookup", path)) {
        options.d_skipCacheLookup = skipCacheLookup.value();
    }
    if (const auto doNotCache = TOMLUtils::readOptionalFromTable<bool>(
            TOMLUtils::readValue<bool>, table, "do-not-cache", path)) {
        options.d_doNotCache = doNotCache.value();
    }
    if (const auto followSymlinks = TOMLUtils::readOptionalFromTable<bool>(
            TOMLUtils::readValue<bool>, table, "follow-symlinks", path)) {
        options.d_followSymlinks = followSymlinks.value();
    }
    if (const auto salt = TOMLUtils::readOptionalFromTable<std::string>(
            TOMLUtils::readValue<std::string>, table, "salt", path)) {
        options.d_salt = salt.value();
    }
    if (const auto priority = TOMLUtils::readOptionalFromTable<int64_t>(
            TOMLUtils::readValue<int64_t>, table, "priority", path)) {
        options.d_priority = std::make_shared<int>(priority.value());
    }
    if (table.contains("wait") && table.contains("no-wait")) {
        throw std::runtime_error("Requests cannot be both blocking and async");
    }
    if (const auto noWait = TOMLUtils::readOptionalFromTable<bool>(
            TOMLUtils::readValue<int64_t>, table, "no-wait", path)) {
        options.d_blockingExecute = !noWait.value();
    }
    if (const auto wait = TOMLUtils::readOptionalFromTable<bool>(
            TOMLUtils::readValue<int64_t>, table, "wait", path)) {
        // d_blockingExecute is already optional so we don't need to extract
        // the value, otherwise clang-tidy complains
        options.d_blockingExecute = wait;
    }
    if (const auto logProgress = TOMLUtils::readOptionalFromTable<bool>(
            TOMLUtils::readValue<bool>, table, "log-progress", path)) {
        options.d_logProgress = logProgress.value();
    }
    if (const auto operation = TOMLUtils::readOptionalFromTable<std::string>(
            TOMLUtils::readValue<std::string>, table, "operation", path)) {
        options.d_operation = operation.value();
    }
    if (const auto resultMetadataFile =
            TOMLUtils::readOptionalFromTable<std::string>(
                TOMLUtils::readValue<std::string>, table,
                "result-metadata-file", path)) {
        options.d_resultMetadataFile = resultMetadataFile.value();
    }
    if (const auto actionResultJSON =
            TOMLUtils::readOptionalFromTable<std::string>(
                TOMLUtils::readValue<std::string>, table, "action-result-json",
                path)) {
        options.d_actionResultJsonFile = actionResultJSON.value();
    }
    if (const auto streamLogs = TOMLUtils::readOptionalFromTable<bool>(
            TOMLUtils::readValue<bool>, table, "stream-logs", path)) {
        options.d_streamLogs = streamLogs.value();
    }
    if (const auto useLocalCas = TOMLUtils::readOptionalFromTable<bool>(
            TOMLUtils::readValue<bool>, table, "use-localcas", path)) {
        options.d_useLocalCas = useLocalCas.value();
    }
    if (const auto localCasSkipUpload = TOMLUtils::readOptionalFromTable<bool>(
            TOMLUtils::readValue<bool>, table, "localcas-skip-upload", path)) {
        options.d_localCasSkipUpload = localCasSkipUpload.value();
    }
    if (const auto numDigestThreads =
            TOMLUtils::readOptionalFromTable<int64_t>(
                TOMLUtils::readValue<int64_t>, table, "num-digest-threads",
                path)) {
        if (numDigestThreads.value() < 0) {
            throw std::invalid_argument(
                "Invalid value for num-digest-threads " +
                std::to_string(numDigestThreads.value()));
        }
        options.d_numDigestThreads =
            static_cast<size_t>(numDigestThreads.value());
    }
    if (const auto numIOThreads = TOMLUtils::readOptionalFromTable<int64_t>(
            TOMLUtils::readValue<int64_t>, table, "num-io-threads", path)) {
        if (numIOThreads.value() < 0) {
            throw std::invalid_argument("Invalid value for num-io-threads " +
                                        std::to_string(numIOThreads.value()));
        }
        options.d_numIOThreads = numIOThreads.value();
    }

    // TODO: refactor how CLI works with tool metadata so they are compatible
    if (const auto toolName = TOMLUtils::readOptionalFromTable<std::string>(
            TOMLUtils::readValue<std::string>, table, "tool-name", path)) {
        options.d_metadata.emplace("tool-name", TREXE_METADATA_TOOL_NAME +
                                                    ":" + toolName.value());
    }
    if (const auto toolVersion = TOMLUtils::readOptionalFromTable<std::string>(
            TOMLUtils::readValue<std::string>, table, "tool-version", path)) {
        options.d_metadata.emplace("tool-version", buildboxcommon::VERSION +
                                                       ":" +
                                                       toolVersion.value());
    }
    // TODO: refactor how CLI works with request metadata so they are
    // compatible
    if (const auto toolInvocationId =
            TOMLUtils::readOptionalFromTable<std::string>(
                TOMLUtils::readValue<std::string>, table, "tool-invocation-id",
                path)) {
        options.d_metadata["tool-invocation-id"] = toolInvocationId.value();
    }
    if (const auto correlatedInvocationsId =
            TOMLUtils::readOptionalFromTable<std::string>(
                TOMLUtils::readValue<std::string>, table,
                "correlated-invocations-id", path)) {
        options.d_metadata["correlated-invocations-id"] =
            correlatedInvocationsId.value();
    }
    if (const auto actionMnemonic =
            TOMLUtils::readOptionalFromTable<std::string>(
                TOMLUtils::readValue<std::string>, table, "action-mnemonic",
                path)) {
        options.d_metadata.emplace("action-mnemonic", actionMnemonic.value());
    }
    if (const auto targetId = TOMLUtils::readOptionalFromTable<std::string>(
            TOMLUtils::readValue<std::string>, table, "target-id", path)) {
        options.d_metadata.emplace("target-id", targetId.value());
    }
    if (const auto configurationId =
            TOMLUtils::readOptionalFromTable<std::string>(
                TOMLUtils::readValue<std::string>, table, "configuration-id",
                path)) {
        options.d_metadata.emplace("configuration-id",
                                   configurationId.value());
    }
    if (auto localityHint = TOMLUtils::readOptionalFromTable<std::string>(
            TOMLUtils::readValue<std::string>, table, "locality-hint", path)) {
        options.d_schedulingMetadata.set_locality_hint(
            std::move(localityHint.value()));
    }

    if (table["connection"].is_table()) {
        const auto commonConnection = ConnectionOptionsTOML::configureChannel(
            *table["connection"].as_table());
        options.d_execConn = commonConnection;
        options.d_casConn = commonConnection;
        options.d_acConn = commonConnection;
        options.d_lsConn = commonConnection;
    }
    if (table["connection"]["exec"].is_table()) {
        ConnectionOptionsTOML::updateChannel(
            *table["connection"]["exec"].as_table(), options.d_execConn);
    }
    if (table["connection"]["cas"].is_table()) {
        ConnectionOptionsTOML::updateChannel(
            *table["connection"]["cas"].as_table(), options.d_casConn);
    }
    if (table["connection"]["ac"].is_table()) {
        ConnectionOptionsTOML::updateChannel(
            *table["connection"]["ac"].as_table(), options.d_acConn);
    }
    if (table["connection"]["logstream"].is_table()) {
        ConnectionOptionsTOML::updateChannel(
            *table["connection"]["logstream"].as_table(), options.d_lsConn);
    }

    // metrics
    if (const auto metricMode = TOMLUtils::readOptionalFromTable<std::string>(
            TOMLUtils::readValue<std::string>, table, "metrics-mode", path)) {
        options.d_metricMode = metricMode.value();
    }
    if (const auto metricPublishInterval =
            TOMLUtils::readOptionalFromTable<int64_t>(
                TOMLUtils::readValue<int64_t>, table,
                "metrics-publish-interval", path)) {
        options.d_metricPublishInterval = int(metricPublishInterval.value());
    }
    if (const auto metricPrefix =
            TOMLUtils::readOptionalFromTable<std::string>(
                TOMLUtils::readValue<std::string>, table, "metrics-prefix",
                path)) {
        options.d_metricPrefix = metricPrefix.value();
    }
}

} // namespace trexe
