// Copyright 2020 Bloomberg Finance L.P
//
// 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 <actionbuilder.h>

#include <env.h>
#include <fileutils.h>
#include <iterator>
#include <linkdeps.h>
#include <reccdefaults.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_merklize.h>
#include <buildboxcommon_remoteexecutionclient.h>
#include <buildboxcommonmetrics_durationmetrictimer.h>
#include <buildboxcommonmetrics_metricteeguard.h>

#include <filesystem>
#include <set>
#include <system_error>
#include <thread>
#include <vector>

static const std::string TIMER_NAME_COMPILER_DEPS = "recc.compiler_deps";
static const std::string TIMER_NAME_LINKER_DEPS = "recc.linker_deps";
static const std::string TIMER_NAME_BUILD_MERKLE_TREE =
    "recc.build_merkle_tree";

namespace recc {

namespace {

// Do path replacement and normalize
const std::string normalize_replace_root(const std::string &path)
{
    // If the path matches any in RECC_PATH_PREFIX, replace it if
    // necessary, and normalize path.
    const std::string replacedRoot =
        FileUtils::resolvePathPrefixFromLocalToRemote(path);

    // Get the relativePath from the current PROJECT_ROOT.
    std::string relativePath =
        FileUtils::rewritePathToRelative(replacedRoot, RECC_PROJECT_ROOT);

    // Prepend the RECC_WORKING_DIR_PREFIX if relative and normalize path
    if (relativePath[0] != '/' && !RECC_WORKING_DIR_PREFIX.empty()) {
        relativePath.insert(0, RECC_WORKING_DIR_PREFIX + "/");
    }

    return buildboxcommon::FileUtils::normalizePath(relativePath.c_str());
}

} // unnamed namespace

std::mutex LogWriteMutex;

proto::Command ActionBuilder::populateCommandProto(
    const std::vector<std::string> &command,
    const std::set<std::string> &outputFiles,
    const std::set<std::string> &outputDirectories,
    const std::map<std::string, std::string> &remoteEnvironment,
    const std::map<std::string, std::string> &platformProperties,
    const std::string &workingDirectory)
{
    proto::Command commandProto;

    for (const auto &arg : command) {
        commandProto.add_arguments(arg);
    }

    for (const auto &envIter : remoteEnvironment) {
        auto envVar = commandProto.add_environment_variables();
        envVar->set_name(envIter.first);
        envVar->set_value(envIter.second);
    }

    for (const auto &file : outputFiles) {
        commandProto.add_output_paths(file);
    }

    for (const auto &directory : outputDirectories) {
        commandProto.add_output_paths(directory);
    }

    for (const auto &platformIter : platformProperties) {
        if (!platformIter.second.empty()) {
            auto property =
                buildboxcommon::commandMutablePlatformDeprecated(commandProto)
                    ->add_properties();
            property->set_name(platformIter.first);
            property->set_value(platformIter.second);
        }
    }

    commandProto.set_working_directory(workingDirectory);

    return commandProto;
}

std::string
ActionBuilder::commonAncestorPath(const DependencyPairs &dependencies,
                                  const std::set<std::string> &products,
                                  const std::string &workingDirectory)
{
    int parentsNeeded = 0;
    for (const auto &dep : dependencies) {
        parentsNeeded = std::max(parentsNeeded,
                                 FileUtils::parentDirectoryLevels(dep.second));
    }

    for (const auto &product : products) {
        parentsNeeded =
            std::max(parentsNeeded, FileUtils::parentDirectoryLevels(product));
    }

    return FileUtils::lastNSegments(workingDirectory, parentsNeeded);
}

std::string
ActionBuilder::prefixWorkingDirectory(const std::string &workingDirectory,
                                      const std::string &prefix)
{
    if (prefix.empty()) {
        return workingDirectory;
    }

    return prefix + "/" + workingDirectory;
}

std::string getMerklePath(const std::string &path, const std::string &cwd,
                          buildboxcommon::NestedDirectory *nestedDirectory)
{
    // If this path is relative, prepend the remote cwd to it
    // and normalize it, getting rid of any '../' present
    std::filesystem::path merklePath(path);
    if (merklePath.is_relative() && !cwd.empty()) {
        merklePath = std::filesystem::path(cwd) / merklePath;
    }

    // Remove the trailing slash of a directory path
    if (!merklePath.has_filename()) {
        merklePath = merklePath.parent_path();
    }

    std::filesystem::path resolvedMerklePath;

    std::deque<std::string> pathSegments(merklePath.begin(), merklePath.end());

    while (!pathSegments.empty()) {
        const auto segment = pathSegments.front();
        pathSegments.pop_front();

        if (segment == "/") {
            resolvedMerklePath = "/";
        }
        else if (segment == ".") {
            // `.` segments can simply be skipped
        }
        else if (segment == "..") {
            if (resolvedMerklePath.empty()) {
                // Skip paths outside the working directory if `cwd` is empty.
                // This can happen for unused include directories if path
                // rewriting is enabled.
                return "";
            }
            // If the path contains `..` segments before normalization, we need
            // to ensure that the directories preceding the `..` exist in the
            // merkle tree. Otherwise, the OS will fail to resolve the
            // non-normalized path during action execution with `ENOENT`.
            nestedDirectory->addDirectory(resolvedMerklePath.c_str());
            resolvedMerklePath = resolvedMerklePath.parent_path();
        }
        else {
            const auto segmentPath = resolvedMerklePath / segment;

            if (RECC_NO_PATH_REWRITE) {
                std::error_code ec;
                const auto target =
                    std::filesystem::read_symlink(segmentPath, ec);
                if (!ec) {
                    // Current path segment is a symlink. Prepend the path
                    // segments of the symlink target to the processing queue
                    // and add the symlink to the merkle tree.
                    auto it = pathSegments.begin();
                    for (const auto &targetSegment : target) {
                        it = std::next(pathSegments.insert(it, targetSegment));
                    }

                    nestedDirectory->addSymlink(target, segmentPath.c_str());
                    continue;
                }
            }

            resolvedMerklePath = segmentPath;
        }
    }

    // don't include a dependency if:
    // 1. It's an absolute path outside the current working directory and
    //    RECC_DEPS_GLOBAL_PATHS is not specified
    // 2. It's has a prefix of an entry specified in RECC_DEPS_EXCLUDE_PATHS
    if ((resolvedMerklePath.is_absolute() && !RECC_DEPS_GLOBAL_PATHS &&
         !FileUtils::hasPathPrefix(resolvedMerklePath, cwd)) ||
        FileUtils::hasPathPrefixes(resolvedMerklePath,
                                   RECC_DEPS_EXCLUDE_PATHS)) {
        const std::lock_guard<std::mutex> lock(LogWriteMutex);
        BUILDBOX_LOG_DEBUG("Skipping \"" << resolvedMerklePath << "\"");
        return "";
    }

    return resolvedMerklePath;
}

void addFileToMerkleTreeHelper(
    const PathRewritePair &dep_paths, const std::string &cwd,
    buildboxcommon::NestedDirectory *nestedDirectory)
{
    const std::string merklePath =
        getMerklePath(dep_paths.second, cwd, nestedDirectory);
    if (merklePath.empty()) {
        // Path is excluded
        return;
    }

    // This follows symlinks
    nestedDirectory->addFilePath(dep_paths.first, merklePath.c_str());
}

void addDirectoryToMerkleTreeHelper(
    const std::string &path, const std::string &cwd,
    buildboxcommon::NestedDirectory *nestedDirectory)
{
    const std::string merklePath = getMerklePath(path, cwd, nestedDirectory);
    if (merklePath.empty()) {
        // Path is excluded
        return;
    }

    nestedDirectory->addDirectory(merklePath.c_str());
}

void addSymlinkToMerkleTreeHelper(
    const PathRewritePair &paths, const std::string &cwd,
    buildboxcommon::NestedDirectory *nestedDirectory)
{
    const std::string merklePath =
        getMerklePath(paths.second, cwd, nestedDirectory);
    if (merklePath.empty()) {
        // Path is excluded
        return;
    }

    struct stat st = FileUtils::getStat(paths.first.c_str(), false);
    const std::string target =
        FileUtils::getSymlinkContents(paths.first.c_str(), st);

    nestedDirectory->tryAddSymlink(target, merklePath.c_str());
}

void ActionBuilder::buildMerkleTree(
    DependencyPairs &dependency_paths, const std::string &cwd,
    buildboxcommon::NestedDirectory *nestedDirectory,
    buildboxcommon::digest_string_map *digest_to_filepaths)
{ // Timed function
    buildboxcommon::buildboxcommonmetrics::MetricTeeGuard<
        buildboxcommon::buildboxcommonmetrics::DurationMetricTimer>
        mt(TIMER_NAME_BUILD_MERKLE_TREE, d_durationMetricCallback);

    BUILDBOX_LOG_DEBUG("Building Merkle tree");

    for (const auto &path : dependency_paths) {
        addFileToMerkleTreeHelper(path, cwd, nestedDirectory);
    }

    if (d_casClient) {
        if (RECC_CACHE_ONLY) {
            nestedDirectory->hashFiles(d_casClient.get(), digest_to_filepaths);
        }
        else {
            nestedDirectory->captureFiles(d_casClient.get(),
                                          digest_to_filepaths);
        }
    }
    else {
        const auto numThreads = RECC_MAX_THREADS > 0
                                    ? RECC_MAX_THREADS
                                    : std::thread::hardware_concurrency();
        ThreadPool threadPool(numThreads, "digest");

        nestedDirectory->hashFiles(&threadPool, digest_to_filepaths);
    }
}

/*
 * Stuff the entire environment into the provided map, aside from variables
 * starting with "RECC_".
 */
void ActionBuilder::populateRemoteEnvWithNonReccVars(
    const char *const *env, std::map<std::string, std::string> *remoteEnv)
{
    // NOLINTBEGIN (cppcoreguidelines-pro-bounds-pointer-arithmetic)
    for (int i = 0; env[i] != nullptr; ++i) {
        std::string envItem(env[i]);
        if (envItem.rfind("RECC_", 0) == 0) {
            continue;
        }
        size_t equalsPosition = envItem.find("=");
        if (equalsPosition == std::string::npos) {
            throw std::runtime_error("Could not parse environment setting " +
                                     envItem);
        }

        std::string key = envItem.substr(0, equalsPosition);
        std::string value = envItem.substr(equalsPosition + 1);

        (*remoteEnv)[key] = value;
    }
    // NOLINTEND (cppcoreguidelines-pro-bounds-pointer-arithmetic)
}

/**
 * Prepare the remote environment from the configuration.
 */
std::map<std::string, std::string>
ActionBuilder::prepareRemoteEnv(const ParsedCommand &command)
{
    std::map<std::string, std::string> remoteEnv;
    if (RECC_PRESERVE_ENV) {
        populateRemoteEnvWithNonReccVars(environ, &remoteEnv);
    }
    else if (RECC_ENV_TO_READ.empty()) {
        // https://gcc.gnu.org/onlinedocs/gcc/Environment-Variables.html
        // https://gcc.gnu.org/onlinedocs/libgomp/Environment-Variables.html
        // https://clang.llvm.org/docs/CommandGuide/clang.html
        // https://docs.oracle.com/cd/E37069_01/html/E54439/cc-1.html
        // https://www.openmp.org/spec-html/5.0/openmpch6.html

        RECC_ENV_TO_READ.insert({"PATH", "LD_LIBRARY_PATH", "LANG", "LC_CTYPE",
                                 "LC_MESSAGES", "LC_ALL"});

        if (command.is_gcc() || command.is_clang()) {
            RECC_ENV_TO_READ.insert({"CPATH", "C_INCLUDE_PATH",
                                     "CPLUS_INCLUDE_PATH", "OBJC_INCLUDE_PATH",
                                     "OBJCPLUS_INCLUDE_PATH",
                                     "SOURCE_DATE_EPOCH"});
        }

        if (command.is_gcc()) {
            RECC_ENV_TO_READ.insert(
                {"GCC_COMPARE_DEBUG", "GCC_EXEC_PREFIX", "COMPILER_PATH",
                 "LIBRARY_PATH", "GCC_EXTRA_DIAGNOSTIC_OUTPUT",
                 "DEPENDENCIES_OUTPUT", "GOMP_CPU_AFFINITY", "GOMP_DEBUG",
                 "GOMP_STACKSIZE", "GOMP_SPINCOUNT",
                 "GOMP_RTEMS_THREAD_POOLS"});
        }

        if (command.is_gcc() || command.is_sun_studio()) {
            RECC_ENV_TO_READ.insert({"SUNPRO_DEPENDENCIES"});
        }
        if (command.is_sun_studio()) {
            RECC_ENV_TO_READ.insert({"PARALLEL", "STACKSIZE"});
        }

        RECC_ENV_TO_READ.insert(
            {"OMP_CANCELLATION", "OMP_DISPLAY_ENV", "OMP_DYNAMIC",
             "OMP_MAX_ACTIVE_LEVELS", "OMP_MAX_TASK_PRIORITY", "OMP_NESTED",
             "OMP_NUM_TEAMS", "OMP_NUM_THREADS", "OMP_PROC_BIND", "OMP_PLACES",
             "OMP_STACKSIZE", "OMP_SCHEDULE", "OMP_TARGET_OFFLOAD",
             "OMP_TEAMS_THREAD_LIMIT", "OMP_THREAD_LIMIT", "OMP_WAIT_POLICY"});
    }

    // Environment variables which have path-like segments delimited by ':'
    std::unordered_set<std::string> pathLikeEnv = {"PATH",
                                                   "LD_LIBRARY_PATH",
                                                   "CPATH",
                                                   "C_INCLUDE_PATH",
                                                   "CPLUS_INCLUDE_PATH",
                                                   "OBJC_INCLUDE_PATH",
                                                   "OBJCPLUS_INCLUDE_PATH",
                                                   "COMPILER_PATH",
                                                   "LIBRARY_PATH",
                                                   "LIB_PATH"};
    for (const std::string &envVar : RECC_ENV_TO_READ) {
        const char *envVal = getenv(envVar.c_str());
        if (envVal != nullptr) {
            if (pathLikeEnv.find(envVar) != pathLikeEnv.end() &&
                strcmp(envVal, "") != 0) {
                // Split the string on ':' to get the individual paths
                // and run each through prefix map resolution, joining them
                // back up afterwords
                const std::string pathPart = envVal;
                std::string pathMapEnv;
                size_t delim_pos = std::string::npos;
                size_t delim_prev = 0;
                while ((delim_pos = pathPart.find(':', delim_prev)) !=
                       std::string::npos) {
                    const std::string pathPiece =
                        pathPart.substr(delim_prev, delim_pos - delim_prev);
                    if (!pathPiece.empty()) {
                        pathMapEnv +=
                            FileUtils::resolvePathPrefixFromLocalToRemote(
                                pathPiece) +
                            ":";
                    }
                    else {
                        pathMapEnv += ":";
                    }
                    delim_prev = delim_pos + 1;
                }
                // Grab the last path
                const std::string pathPiece = pathPart.substr(delim_prev);
                if (!pathPiece.empty()) {
                    pathMapEnv +=
                        FileUtils::resolvePathPrefixFromLocalToRemote(
                            pathPiece);
                }
                remoteEnv[envVar] = pathMapEnv;
            }
            else {
                remoteEnv[envVar] = envVal;
            }
        }
    }
    for (std::pair<std::string, std::string> overrideElem : RECC_REMOTE_ENV) {
        remoteEnv[overrideElem.first] = overrideElem.second;
    }
    return remoteEnv;
}

void ActionBuilder::getDependencies(const ParsedCommand &command,
                                    std::set<std::string> *dependencies,
                                    std::set<std::string> *products)
{

    CommandFileInfo fileInfo;
    if (command.is_linker_command()) {
        buildboxcommon::buildboxcommonmetrics::MetricTeeGuard<
            buildboxcommon::buildboxcommonmetrics::DurationMetricTimer>
            mt(TIMER_NAME_LINKER_DEPS, d_durationMetricCallback);
        fileInfo = LinkDeps::get_file_info(command);
    }
    else {
        buildboxcommon::buildboxcommonmetrics::MetricTeeGuard<
            buildboxcommon::buildboxcommonmetrics::DurationMetricTimer>
            mt(TIMER_NAME_COMPILER_DEPS, d_durationMetricCallback);
        fileInfo = Deps::get_file_info(command, d_counterMetricCallback);
    }

    *dependencies = fileInfo.d_dependencies;

    if (RECC_OUTPUT_DIRECTORIES_OVERRIDE.empty() &&
        RECC_OUTPUT_FILES_OVERRIDE.empty()) {
        *products = fileInfo.d_possibleProducts;
    }
}

std::shared_ptr<proto::Action> ActionBuilder::BuildAction(
    const ParsedCommand &command, const std::string &cwd,
    buildboxcommon::digest_string_map *blobs,
    buildboxcommon::digest_string_map *digest_to_filepaths,
    std::set<std::string> *productsPtr)
{
    if (!command.is_compiler_command() && !command.is_linker_command() &&
        !RECC_FORCE_REMOTE) {
        return nullptr;
    }

    std::string commandWorkingDirectory;
    buildboxcommon::NestedDirectory nestedDirectory;

    std::set<std::string> products = RECC_OUTPUT_FILES_OVERRIDE;
    if (!RECC_DEPS_DIRECTORY_OVERRIDE.empty()) {
        BUILDBOX_LOG_DEBUG("Building Merkle tree using directory override");
        // When RECC_DEPS_DIRECTORY_OVERRIDE is set, we will not follow
        // symlinks to help us avoid getting into an endless loop.
        {
            buildboxcommon::Merklizer merklizer(false /*no follow*/);
            buildboxcommon::FileDescriptor dirfd(
                open(RECC_DEPS_DIRECTORY_OVERRIDE.c_str(),
                     O_RDONLY | O_DIRECTORY | O_CLOEXEC));
            if (dirfd.get() < 0) {
                throw std::runtime_error(
                    "Failed to open deps directory override: " +
                    RECC_DEPS_DIRECTORY_OVERRIDE);
            }

            auto [nestedDir, filePaths] = merklizer.makeNestedDirectory(
                dirfd.get(), RECC_DEPS_DIRECTORY_OVERRIDE.c_str());
            nestedDirectory = std::move(nestedDir);
            digest_to_filepaths->merge(std::move(filePaths));
        }

        auto replacedRoot =
            normalize_replace_root(RECC_DEPS_DIRECTORY_OVERRIDE);

        BUILDBOX_LOG_DEBUG("Mapping local file path: ["
                           << RECC_DEPS_DIRECTORY_OVERRIDE
                           << "] to normalized-relative (if)updated: ["
                           << replacedRoot << "]");

        // Create parent directory structure based on the normalized/replaced
        // path.
        const auto pathComponents = FileUtils::parseDirectories(replacedRoot);
        for (auto it = pathComponents.rbegin(); it != pathComponents.rend();
             it++) {
            auto parentDirectory = buildboxcommon::NestedDirectory();
            parentDirectory.d_subdirs->emplace(*it,
                                               std::move(nestedDirectory));
            nestedDirectory = std::move(parentDirectory);
        }

        commandWorkingDirectory = RECC_WORKING_DIR_PREFIX;
    }
    else {
        std::set<std::string> deps;
        if (RECC_DEPS_OVERRIDE.empty() && !RECC_FORCE_REMOTE) {
            try {
                getDependencies(command, &deps, &products);

                // If no dependencies are found, there won't be any input files
                // in the remote, so run the build locally.
                if (deps.empty()) {
                    BUILDBOX_LOG_INFO("No deps found. Running locally.");
                    return nullptr;
                }

                // Similarly if no output files are found we aren't actually
                // compiling anything that recc understands, run the build
                // locally
                if (products.empty()) {
                    BUILDBOX_LOG_INFO("No products found. Running locally.");
                    return nullptr;
                }
            }
            catch (const subprocess_failed_error &) {
                BUILDBOX_LOG_DEBUG("Running locally to display the error.");
                return nullptr;
            }
            catch (const std::exception &e) {
                BUILDBOX_LOG_INFO(e.what());
                return nullptr;
            }
        }
        else {
            deps = RECC_DEPS_OVERRIDE;
        }

        // Go through all the dependencies and apply any required path
        // transformations, constructing DependencyPairs
        // corresponding to filesystem path -> transformed merkle tree path
        DependencyPairs dep_path_pairs;
        for (const auto &dep : deps) {
            std::string modifiedDep(dep);
            if (modifiedDep[0] == '/') {
                modifiedDep =
                    FileUtils::resolvePathPrefixFromLocalToRemote(modifiedDep);
                // Make path relative if needed
                modifiedDep =
                    FileUtils::rewritePathToRelative(modifiedDep, cwd);
                if (dep != modifiedDep) {
                    BUILDBOX_LOG_DEBUG("Mapping local path: ["
                                       << dep << "] to remote path: ["
                                       << modifiedDep << "]");
                }
            }
            dep_path_pairs.emplace_back(dep, modifiedDep);
        }

        // Iterate through d_tmpRspMerklePathMap to add response files to
        // dep_path_pairs
        for (const auto &rspFilePair : command.d_tmpRspMerklePathMap) {
            dep_path_pairs.emplace_back(rspFilePair.first, rspFilePair.second);
            BUILDBOX_LOG_DEBUG("Mapping local rsp file path: ["
                               << rspFilePair.first << "] to remote path: ["
                               << rspFilePair.second << "]");
        }

        if (RECC_NO_PATH_REWRITE && RECC_WORKING_DIR_PREFIX.empty()) {
            commandWorkingDirectory = cwd;
        }
        else {
            const auto commonAncestor =
                commonAncestorPath(dep_path_pairs, products, cwd);
            commandWorkingDirectory = prefixWorkingDirectory(
                commonAncestor, RECC_WORKING_DIR_PREFIX);
        }

        buildMerkleTree(dep_path_pairs, commandWorkingDirectory,
                        &nestedDirectory, digest_to_filepaths);
    }

    if (!commandWorkingDirectory.empty()) {
        commandWorkingDirectory = buildboxcommon::FileUtils::normalizePath(
            commandWorkingDirectory.c_str());
        nestedDirectory.addDirectory(commandWorkingDirectory.c_str());
    }
    for (const auto &include_dir : command.d_includeDirs) {
        getMerklePath(include_dir, commandWorkingDirectory, &nestedDirectory);
    }

    if (command.d_upload_all_include_dirs) {
        for (const auto &include_dir : command.d_includeDirs) {
            addDirectoryToMerkleTreeHelper(
                include_dir, commandWorkingDirectory, &nestedDirectory);
        }
    }

    for (const auto &symlinkPath : RECC_DEPS_EXTRA_SYMLINKS) {
        if (buildboxcommon::FileUtils::isSymlink(symlinkPath.c_str())) {
            const std::string replacedPath =
                FileUtils::modifyPathForRemote(symlinkPath, cwd);
            addSymlinkToMerkleTreeHelper(
                std::make_pair(symlinkPath, replacedPath),
                commandWorkingDirectory, &nestedDirectory);
        }
    }

    for (const auto &product : products) {
        if (!product.empty() && product[0] == '/') {
            BUILDBOX_LOG_DEBUG(
                "Command produces file in a location unrelated "
                "to the current directory, so running locally.");
            BUILDBOX_LOG_DEBUG(
                "(use RECC_OUTPUT_[FILES|DIRECTORIES]_OVERRIDE to "
                "override)");
            return nullptr;
        }
    }

    const auto directoryDigest = nestedDirectory.to_digest(blobs);

    if (RECC_LINK_METRICS_ONLY && command.is_linker_command() &&
        !RECC_FORCE_REMOTE) {
        // ActionCache entry only for metric collection, do not upload
        // linker output
        products.clear();
    }

    std::map<std::string, std::string> remoteEnv = prepareRemoteEnv(command);
    const proto::Command commandProto = generateCommandProto(
        command.get_command(), products, RECC_OUTPUT_DIRECTORIES_OVERRIDE,
        remoteEnv, RECC_REMOTE_PLATFORM, commandWorkingDirectory);
    BUILDBOX_LOG_DEBUG("Command: " << commandProto.ShortDebugString());

    const auto commandDigest =
        buildboxcommon::DigestGenerator::hash(commandProto);
    (*blobs)[commandDigest] = commandProto.SerializeAsString();

    proto::Action action;
    action.mutable_command_digest()->CopyFrom(commandDigest);
    action.mutable_input_root_digest()->CopyFrom(directoryDigest);
    action.set_do_not_cache(RECC_ACTION_UNCACHEABLE);
    if (!RECC_ACTION_SALT.empty()) {
        action.set_salt(RECC_ACTION_SALT);
    }

    // REAPI v2.2 allows setting the platform property list in the `Action`
    // message, which allows servers to immediately read it without having to
    // dereference the corresponding `Command`.
    if (Env::configured_reapi_version_equal_to_or_newer_than("2.2")) {
        action.mutable_platform()->CopyFrom(
            buildboxcommon::commandPlatformDeprecated(commandProto));
    }

    if (productsPtr != nullptr) {
        *productsPtr = products;
    }

    return std::make_shared<proto::Action>(action);
}

build::bazel::remote::execution::v2::Command
ActionBuilder::generateCommandProto(
    const std::vector<std::string> &command,
    const std::set<std::string> &products,
    const std::set<std::string> &outputDirectories,
    const std::map<std::string, std::string> &remoteEnvironment,
    const std::map<std::string, std::string> &platformProperties,
    const std::string &workingDirectory)
{
    // If dependency paths aren't absolute, they are made absolute by
    // having the CWD prepended, and then normalized and replaced. If that
    // is the case, and the CWD contains a replaced prefix, then replace
    // it.
    std::string resolvedWorkingDirectory =
        FileUtils::resolvePathPrefixFromLocalToRemote(workingDirectory);

    // `Command.working_directory` is required to be relative
    while (FileUtils::isAbsolutePath(resolvedWorkingDirectory)) {
        resolvedWorkingDirectory = resolvedWorkingDirectory.substr(1);
    }

    return ActionBuilder::populateCommandProto(
        command, products, outputDirectories, remoteEnvironment,
        platformProperties, resolvedWorkingDirectory);
}

ActionBuilder::ActionBuilder(
    const DurationMetricCallback &duration_metric_callback,
    const CounterMetricCallback &counter_metric_callback,
    std::shared_ptr<buildboxcommon::CASClient> casClient)
    : d_casClient(std::move(casClient))
{
    if (duration_metric_callback == nullptr) {
        // no-op callback
        d_durationMetricCallback = [](auto &&...) {};
    }
    else {
        d_durationMetricCallback = duration_metric_callback;
    }

    if (counter_metric_callback == nullptr) {
        // no-op callback
        d_counterMetricCallback = [](auto &&...) {};
    }
    else {
        d_counterMetricCallback = counter_metric_callback;
    }
}

} // namespace recc
