// Copyright 2018-2022 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 "buildboxcommon_grpcclient.h"
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_remoteexecutionclient.h>
#include <buildboxcommon_stringutils.h>

#include <google/rpc/code.pb.h>
#include <google/rpc/status.pb.h>

#include <chrono>
#include <fcntl.h>
#include <fstream>
#include <functional>
#include <optional>
#include <thread>
#include <unistd.h>
#include <utility>
#include <vector>

#define POLL_WAIT std::chrono::seconds(1)

namespace buildboxcommon {

void RemoteExecutionClient::init(
    std::shared_ptr<Execution::StubInterface> executionStub,
    std::shared_ptr<ActionCache::StubInterface> actionCacheStub,
    std::shared_ptr<Operations::StubInterface> operationsStub)
{
    this->ExecutionClient::init(std::move(actionCacheStub));
    d_executionStub = std::move(executionStub);
    d_operationsStub = std::move(operationsStub);
}

void RemoteExecutionClient::init(
    std::shared_ptr<ActionCache::StubInterface> actionCacheStub)
{
    init(nullptr, actionCacheStub, nullptr);
}

void RemoteExecutionClient::init()
{
    this->ExecutionClient::init();
    d_executionStub =
        d_executionGrpcClient
            ? Execution::NewStub(d_executionGrpcClient->channel())
            : nullptr;
    d_operationsStub =
        d_executionGrpcClient
            ? Operations::NewStub(d_executionGrpcClient->channel())
            : nullptr;
}

void RemoteExecutionClient::enableLogStream(const Digest &digest,
                                            std::ostream *stdoutStream,
                                            std::ostream *stderrStream)
{
    std::lock_guard<std::mutex> lock(d_logStreamInfoMutex);
    LogStreamInfo &info = d_logStreamInfo[digest];
    info.stderrStreamTarget = stderrStream;
    info.stdoutStreamTarget = stdoutStream;
}

void RemoteExecutionClient::enableLogStream(const Digest &digest,
                                            const std::string &stdoutFile,
                                            const std::string &stderrFile)
{
    std::lock_guard<std::mutex> lock(d_logStreamInfoMutex);
    LogStreamInfo &info = d_logStreamInfo[digest];
    info.stderrFileName = stderrFile;
    info.stdoutFileName = stdoutFile;
}

std::optional<LogStreamInfo>
RemoteExecutionClient::getLogStreamInfo(const Digest &digest)
{
    std::lock_guard<std::mutex> lock(d_logStreamInfoMutex);
    try {
        LogStreamInfo info = d_logStreamInfo.at(digest);
        d_logStreamInfo.erase(digest);
        return info;
    }
    catch (const std::out_of_range &e) {
        return {};
    }
}

void RemoteExecutionClient::enableProgressLog()
{
    d_logProgress = true;
    BUILDBOX_LOG_DEBUG("Logging progress...");
};

/**
 * Read the operation into the given pointer using async GRPC so we can
 * properly handle cancellation.
 *
 * cq->AsyncNext() blocks until the specified deadline is reached.
 * No built-in support for a stop signal. This means we need to
 * busy wait and check the signal flag on each iteration.
 *
 * Returns true if no more messages can be read from the operation,
 * false otherwise. Specific Client side channel errors are returned
 * via *errorStatus
 */
bool RemoteExecutionClient::readOperation(
    grpc::ClientContext *context, grpc::CompletionQueue *cq,
    ReaderPointer &readerPtr, OperationPointer &operationPtr,
    WriterPointer writerPtr, std::function<bool()> stopRequested,
    const gpr_timespec &firstRequestDeadline, grpc::Status *errorStatus,
    bool wait, bool cancelOnStop, const std::string &operationNamePrefix)
{
    bool first = true;
    bool streamFinished = false;
    std::optional<std::chrono::time_point<std::chrono::system_clock>>
        finishedTimestamp = std::nullopt;

    auto consumeStream = [&](const std::string &streamName,
                             const std::string &fileName,
                             std::ostream *fallbackStream,
                             const Digest &digest) {
        LogStreamReader reader =
            LogStreamReader(streamName, *d_logstreamConnectionOptions);

        auto shouldStop = [&]() {
            return stopRequested() ||
                   (finishedTimestamp.has_value() &&
                    std::chrono::system_clock::now() - *finishedTimestamp >
                        reader.s_doneOpReadTimeout);
        };

        bool readResult = false;
        if (!fileName.empty()) {
            std::ofstream outStream(fileName);
            auto handler = [&](const std::string &data) { outStream << data; };
            readResult = reader.read(handler, shouldStop);
        }
        else if (fallbackStream != nullptr) {
            auto handler = [&](const std::string &data) {
                *fallbackStream << data;
            };
            readResult = reader.read(handler, shouldStop);
        }

        std::lock_guard<std::mutex> lock(d_logStreamInfoMutex);
        if (d_logStreamInfo.find(digest) != d_logStreamInfo.end()) {
            d_logStreamInfo[digest].streamResults[streamName] = readResult;
        }
    };

    /**
     * Wait for the operation to complete, handling the
     * cancellation flag.
     */
    std::string original_operation_name;
    std::optional<Digest> digest;
    std::vector<std::thread> logstreamReaders;
    readerPtr->Read(operationPtr.get(), nullptr);
    bool receivedQueuedTime = false;
    bool receivedWorkerName = false;
    bool receivedWorkerStartTime = false;
    bool receivedWorkerCompleteTime = false;
    bool receivedInputFetchStartTime = false;
    bool receivedInputFetchCompleteTime = false;
    bool receivedOutputUploadStartTime = false;
    bool receivedOutputUploadCompleteTime = false;
    bool receivedExecutionStartTime = false;
    bool receivedExecutionCompleteTime = false;

    while (wait || first) {
        void *tag = nullptr;
        bool ok = false;
        const auto deadline = std::chrono::system_clock::now() + POLL_WAIT;
        const auto nextStatus = cq->AsyncNext(&tag, &ok, deadline);
        if (nextStatus == grpc::CompletionQueue::GOT_EVENT) {
            if (!ok) {
                /* No more messages in the stream */
                streamFinished = true;
                break;
            }
            if (first) {
                original_operation_name = operationPtr->name();
                if (d_logProgress) {
                    BUILDBOX_LOG_INFO("Received name for the execution. name="
                                      << original_operation_name);
                }
            }
            operationPtr->set_name(operationNamePrefix +
                                   original_operation_name);

            ExecuteOperationMetadata metadata;
            if (wait && operationPtr->metadata().UnpackTo(&metadata)) {
                if (d_logstreamConnectionOptions != nullptr) {
                    std::lock_guard<std::mutex> lock(d_logStreamInfoMutex);
                    digest = metadata.action_digest();
                    if (d_logStreamInfo.find(digest.value()) !=
                        d_logStreamInfo.end()) {
                        LogStreamInfo &info = d_logStreamInfo[digest.value()];

                        // If we're executing the same digest a second time,
                        // clear out the LogStreamInfo from the previous run.
                        if (info.completed) {
                            info.stderrStreamName.reset();
                            info.stdoutStreamName.reset();
                            info.streamResults.clear();
                            info.completed = false;
                        }

                        if (!metadata.stdout_stream_name().empty() &&
                            !info.stdoutStreamName.has_value()) {
                            info.stdoutStreamName =
                                metadata.stdout_stream_name();
                            logstreamReaders.emplace_back(
                                consumeStream, metadata.stdout_stream_name(),
                                info.stdoutFileName, info.stdoutStreamTarget,
                                digest.value());
                        }

                        if (!metadata.stderr_stream_name().empty() &&
                            !info.stderrStreamName.has_value()) {
                            info.stderrStreamName =
                                metadata.stderr_stream_name();
                            logstreamReaders.emplace_back(
                                consumeStream, metadata.stderr_stream_name(),
                                info.stderrFileName, info.stderrStreamTarget,
                                digest.value());
                        }
                    }
                }

                if (d_logProgress) {
                    if (first) {
                        BUILDBOX_LOG_INFO(
                            "Operation created: " << operationPtr->name())
                    }
                    if (!receivedQueuedTime &&
                        metadata.partial_execution_metadata()
                            .has_queued_timestamp()) {
                        const auto queuedTime =
                            metadata.partial_execution_metadata()
                                .queued_timestamp();
                        BUILDBOX_LOG_INFO(
                            "Reached scheduler queue at: "
                            << TimeUtils::timeStampToStr(queuedTime));
                        receivedQueuedTime = true;
                    }

                    if (!receivedWorkerName) {
                        const std::string workerName =
                            metadata.partial_execution_metadata().worker();
                        if (workerName != "") {
                            BUILDBOX_LOG_INFO(
                                "Assigned to worker: " << workerName);
                            receivedWorkerName = true;
                        }
                    }

                    if (!receivedWorkerStartTime &&
                        metadata.partial_execution_metadata()
                            .has_worker_start_timestamp()) {
                        BUILDBOX_LOG_INFO(
                            "Worker started job at: "
                            << TimeUtils::timeStampToStr(
                                   metadata.partial_execution_metadata()
                                       .worker_start_timestamp()));
                        receivedWorkerStartTime = true;
                    }

                    if (!receivedInputFetchStartTime &&
                        metadata.partial_execution_metadata()
                            .has_input_fetch_start_timestamp()) {
                        BUILDBOX_LOG_INFO(
                            "Worker started fetching action inputs at: "
                            << TimeUtils::timeStampToStr(
                                   metadata.partial_execution_metadata()
                                       .input_fetch_start_timestamp()));
                        receivedInputFetchStartTime = true;
                    }

                    if (!receivedInputFetchCompleteTime &&
                        metadata.partial_execution_metadata()
                            .has_input_fetch_completed_timestamp()) {
                        BUILDBOX_LOG_INFO(
                            "Worker finished fetching action inputs at: "
                            << TimeUtils::timeStampToStr(
                                   metadata.partial_execution_metadata()
                                       .input_fetch_completed_timestamp()));
                        receivedInputFetchCompleteTime = true;
                    }

                    if (!receivedExecutionStartTime &&
                        metadata.partial_execution_metadata()
                            .has_execution_start_timestamp()) {
                        BUILDBOX_LOG_INFO(
                            "Worker started execution at: "
                            << TimeUtils::timeStampToStr(
                                   metadata.partial_execution_metadata()
                                       .execution_start_timestamp()));
                        receivedExecutionStartTime = true;
                    }

                    if (!receivedExecutionCompleteTime &&
                        metadata.partial_execution_metadata()
                            .has_execution_completed_timestamp()) {
                        BUILDBOX_LOG_INFO(
                            "Worker completed execution at: "
                            << TimeUtils::timeStampToStr(
                                   metadata.partial_execution_metadata()
                                       .execution_completed_timestamp()));
                        receivedExecutionCompleteTime = true;
                    }

                    if (!receivedOutputUploadStartTime &&
                        metadata.partial_execution_metadata()
                            .has_output_upload_start_timestamp()) {
                        BUILDBOX_LOG_INFO(
                            "Worker started uploading action outputs at: "
                            << TimeUtils::timeStampToStr(
                                   metadata.partial_execution_metadata()
                                       .output_upload_start_timestamp()));
                        receivedOutputUploadStartTime = true;
                    }

                    if (!receivedOutputUploadCompleteTime &&
                        metadata.partial_execution_metadata()
                            .has_output_upload_completed_timestamp()) {
                        BUILDBOX_LOG_INFO(
                            "Worker finished uploading action outputs at: "
                            << TimeUtils::timeStampToStr(
                                   metadata.partial_execution_metadata()
                                       .output_upload_completed_timestamp()));
                        receivedOutputUploadCompleteTime = true;
                    }

                    if (!receivedWorkerCompleteTime &&
                        metadata.partial_execution_metadata()
                            .has_worker_completed_timestamp()) {
                        BUILDBOX_LOG_INFO(
                            "Worker completed job at: "
                            << TimeUtils::timeStampToStr(
                                   metadata.partial_execution_metadata()
                                       .worker_completed_timestamp()));
                        receivedWorkerCompleteTime = true;
                    }
                }
            }

            if (writerPtr) {
                writerPtr->Write(*operationPtr);
            }
            if (first && !operationPtr->name().empty()) {
                BUILDBOX_LOG_DEBUG(
                    "Waiting for Operation: " << operationPtr->name())
            }
            first = false;
            if (operationPtr->done()) {
                BUILDBOX_LOG_DEBUG("Operation done.");
                streamFinished = true;
                break;
            }

            if (wait) {
                /* Previous read is complete, start read of next message */
                readerPtr->Read(operationPtr.get(), nullptr);
            }
        }
        else if (first && gpr_time_cmp(gpr_now(GPR_CLOCK_REALTIME),
                                       firstRequestDeadline) > 0) {
            /*
             * The first Operation response needs to arrive within the request
             * deadline, further updates may take a longer time.
             */
            context->TryCancel();
            GrpcClient::ShutdownAndEmptyQueue(cq);
            *errorStatus =
                grpc::Status(grpc::DEADLINE_EXCEEDED, "The request timed out");
            streamFinished = true;

            break;
        }
        else if (stopRequested()) {
            streamFinished = true;
            context->TryCancel();
            GrpcClient::ShutdownAndEmptyQueue(cq);
            if (cancelOnStop) {
                BUILDBOX_LOG_WARNING("Cancelling job, operation name: "
                                     << original_operation_name);
                /* Cancel the operation if the execution service gave it a name
                 */
                if (!original_operation_name.empty()) {
                    cancelOperation(original_operation_name);
                }
                *errorStatus =
                    grpc::Status(grpc::CANCELLED, "Operation was cancelled");
            }
            else {
                *errorStatus = grpc::Status(
                    grpc::CANCELLED, "Disconnected from Operation stream");
            }

            break;
        }
    }
    finishedTimestamp = std::chrono::system_clock::now();
    if (d_logProgress) {
        BUILDBOX_LOG_INFO("Operation finished: " << TimeUtils::timePointToStr(
                              finishedTimestamp.value()));
    }

    for (auto &thread : logstreamReaders) {
        thread.join();
    }
    if (digest.has_value()) {
        std::lock_guard<std::mutex> lock(d_logStreamInfoMutex);
        if (d_logStreamInfo.find(digest.value()) != d_logStreamInfo.end()) {
            d_logStreamInfo[digest.value()].completed = true;
        }
    }

    return streamFinished;
}

OperationPointer RemoteExecutionClient::performExecuteRequest(
    const ExecuteRequest &request, std::function<bool()> stopRequested,
    bool wait, bool cancelOnStop, grpc::Status *status)
{
    WriterPointer null_writer = nullptr;
    return performExecuteRequest(request, std::move(stopRequested),
                                 null_writer, wait, cancelOnStop, status);
}

grpc::Status RemoteExecutionClient::readOperationStream(
    grpc::ClientContext *context, ReaderPointer &readerPtr,
    WriterPointer writerPtr, OperationPointer &operationPtr,
    grpc::CompletionQueue *cq, gpr_timespec requestDeadline,
    std::function<bool()> stopRequested, bool wait, bool cancelOnStop,
    const std::string &operationNamePrefix)
{
    grpc::Status status;
    void *tag = nullptr;
    bool ok = false;
    const auto nextStatus = cq->AsyncNext(&tag, &ok, requestDeadline);
    if (nextStatus == grpc::CompletionQueue::TIMEOUT) {
        return grpc::Status(grpc::DEADLINE_EXCEEDED,
                            "Sending the Execute request timed out");
    }
    if (nextStatus != grpc::CompletionQueue::GOT_EVENT || !ok) {
        return grpc::Status(grpc::UNAVAILABLE,
                            "Failed to send Execute request to the server");
    }

    /* Read the result of the Execute request into an OperationPointer */
    operationPtr = std::make_shared<Operation>();
    bool operationDone =
        readOperation(context, cq, readerPtr, operationPtr, writerPtr,
                      std::move(stopRequested), requestDeadline, &status, wait,
                      cancelOnStop, operationNamePrefix);
    if (!status.ok()) {
        return status;
    }

    if (wait || operationDone) {
        readerPtr->Finish(&status, nullptr);
        if (!cq->Next(&tag, &ok) || !ok) {
            return grpc::Status(grpc::UNAVAILABLE,
                                "Failed to finish Execute client stream");
        }
    }

    return status;
}

OperationPointer RemoteExecutionClient::performExecuteRequest(
    const ExecuteRequest &request, std::function<bool()> stopRequested,
    WriterPointer writerPtr, bool wait, bool cancelOnStop,
    grpc::Status *statusPtr, const std::string &operationNamePrefix)
{
    if (!(d_executionStub && d_operationsStub)) {
        throw std::runtime_error("Execution Stubs not Configured");
    }

    OperationPointer operationPtr;
    /* Create the lambda to pass to grpc_retry */
    auto execute_lambda = [&](grpc::ClientContext &context) {
        grpc::CompletionQueue cq;

        const auto requestDeadline = context.raw_deadline();

        /*
         * Don't use the configured request timeout as context deadline as that
         * would apply to the whole stream and would thus make it impossible
         * to execute long-running actions. Action timeouts can be set in the
         * `Action` message.
         * The request timeout will still be used for the first response, which
         * provides the operation name and should be returned by the server
         * without waiting for the execution of the action to complete.
         */
        context.set_deadline(gpr_inf_future(GPR_CLOCK_REALTIME));

        // If we are retrying and the initial execute produced an operation,
        // pick up where we left off using WaitExecute
        ReaderPointer readerPtr;
        if (operationPtr && !(operationPtr->name()).empty()) {
            BUILDBOX_LOG_INFO(
                "Execute interrupted, resuming with waitExecution");
            WaitExecutionRequest waitExecReq;
            std::string opName = operationPtr->name();
            if (!operationNamePrefix.empty() &&
                opName.find(operationNamePrefix) == 0) {
                opName = opName.substr(operationNamePrefix.size());
            }
            waitExecReq.set_name(opName);
            readerPtr = d_executionStub->AsyncWaitExecution(
                &context, waitExecReq, &cq, nullptr);
        }
        else {
            readerPtr =
                d_executionStub->AsyncExecute(&context, request, &cq, nullptr);
        }
        grpc::Status status = readOperationStream(
            &context, readerPtr, writerPtr, operationPtr, &cq, requestDeadline,
            stopRequested, wait, cancelOnStop, operationNamePrefix);
        *statusPtr = status;
        return status;
    };

    d_executionGrpcClient->issueRequest(execute_lambda, "Execution.Execute()",
                                        nullptr);
    return operationPtr;
}

OperationPointer RemoteExecutionClient::performWaitExecutionRequest(
    const WaitExecutionRequest &request, std::function<bool()> stopRequested,
    WriterPointer writerPtr, bool wait, bool cancelOnStop,
    grpc::Status *statusPtr, const std::string &operationNamePrefix)
{
    if (!(d_executionStub && d_operationsStub)) {
        throw std::runtime_error("Execution Stubs not Configured");
    }

    OperationPointer operationPtr;
    /* Create the lambda to pass to grpc_retry */
    auto wait_execution_lambda = [&](grpc::ClientContext &context) {
        grpc::CompletionQueue cq;

        const auto requestDeadline = context.raw_deadline();

        /*
         * Don't use the configured request timeout as context deadline as that
         * would apply to the whole stream and would thus make it impossible
         * to execute long-running actions. Action timeouts can be set in the
         * `Action` message.
         * The request timeout will still be used for the first response, which
         * provides the operation name and should be returned by the server
         * without waiting for the execution of the action to complete.
         */
        context.set_deadline(gpr_inf_future(GPR_CLOCK_REALTIME));

        ReaderPointer readerPtr = d_executionStub->AsyncWaitExecution(
            &context, request, &cq, nullptr);
        grpc::Status status = readOperationStream(
            &context, readerPtr, writerPtr, operationPtr, &cq, requestDeadline,
            stopRequested, wait, cancelOnStop, operationNamePrefix);
        *statusPtr = status;
        return status;
    };

    d_executionGrpcClient->issueRequest(wait_execution_lambda,
                                        "Execution.WaitExecution()", nullptr);
    return operationPtr;
}

google::longrunning::Operation
RemoteExecutionClient::waitExecution(const std::string &operationName)
{
    WaitExecutionRequest request;
    request.set_name(operationName);

    grpc::Status status = grpc::Status();
    WriterPointer nullWriter = nullptr;

    auto operation = performWaitExecutionRequest(
        request, [] { return false; }, nullWriter, true, false, &status);

    if (!status.ok()) {
        throw GrpcError(
            "WaitExecution failed: " + std::to_string(status.error_code()) +
                ": " + status.error_message(),
            status);
    }

    return *operation;
}

grpc::Status RemoteExecutionClient::proxyExecuteRequest(
    const ExecuteRequest &request, std::function<bool()> stopRequested,
    WriterPointer writerPtr, const std::string &operationNamePrefix)
{
    grpc::Status status = grpc::Status();
    performExecuteRequest(request, std::move(stopRequested), writerPtr, true,
                          false, &status, operationNamePrefix);
    return status;
}

grpc::Status RemoteExecutionClient::proxyWaitExecutionRequest(
    const WaitExecutionRequest &request, std::function<bool()> stopRequested,
    WriterPointer writerPtr, const std::string &operationNamePrefix)
{
    grpc::Status status = grpc::Status();
    performWaitExecutionRequest(request, std::move(stopRequested), writerPtr,
                                true, false, &status, operationNamePrefix);
    return status;
}

OperationPointer RemoteExecutionClient::submitExecution(
    const Digest &actionDigest, const std::atomic_bool &stopRequested,
    bool skipCache, const ExecutionPolicy *executionPolicy, bool wait)
{
    if (d_executionGrpcClient == nullptr) {
        throw std::runtime_error("Execution Client not configured");
    }

    /* Prepare an asynchronous Execute request */
    ExecuteRequest executeRequest;
    if (executionPolicy != nullptr) {
        executeRequest.mutable_execution_policy()->CopyFrom(*executionPolicy);
    }
    executeRequest.set_instance_name(d_executionGrpcClient->instanceName());
    *executeRequest.mutable_action_digest() = actionDigest;
    executeRequest.set_skip_cache_lookup(skipCache);

    grpc::Status status = grpc::Status();
    std::function<bool()> stop_lambda = [&] { return stopRequested.load(); };
    return performExecuteRequest(executeRequest, stop_lambda, wait, true,
                                 &status);
}

ActionResult RemoteExecutionClient::executeAction(
    const Digest &actionDigest, const std::atomic_bool &stopRequested,
    bool skipCache, const ExecutionPolicy *executionPolicy)
{
    Operation operation = *submitExecution(actionDigest, stopRequested,
                                           skipCache, executionPolicy, true);
    if (!operation.done()) {
        throw std::runtime_error(
            "Server closed stream before Operation finished");
    }

    return getActionResult(operation);
}

google::longrunning::Operation RemoteExecutionClient::asyncExecuteAction(
    const Digest &actionDigest, const std::atomic_bool &stopRequested,
    bool skipCache, const ExecutionPolicy *executionPolicy)
{
    return *submitExecution(actionDigest, stopRequested, skipCache,
                            executionPolicy, false);
}

google::longrunning::Operation
RemoteExecutionClient::getOperation(const std::string &operationName)
{
    GetOperationRequest getRequest;
    getRequest.set_name(operationName);

    google::longrunning::Operation operation;
    proxyGetOperationRequest(getRequest, &operation);

    return operation;
}

bool RemoteExecutionClient::cancelOperation(const std::string &operationName)
{
    CancelOperationRequest cancelRequest;
    cancelRequest.set_name(operationName);

    google::protobuf::Empty empty;
    const auto status = proxyCancelOperationRequest(cancelRequest, &empty);
    if (!status.ok()) {
        if (status.error_code() == grpc::NOT_FOUND) {
            return false;
        }

        throw GrpcError(
            "CancelOperation failed: " + std::to_string(status.error_code()) +
                ": " + status.error_message(),
            status);
    }

    BUILDBOX_LOG_INFO("Cancelled job " << operationName);
    return true;
}

google::longrunning::ListOperationsResponse
RemoteExecutionClient::listOperations(const std::string &name,
                                      const std::string &filter, int page_size,
                                      const std::string &page_token)
{
    google::longrunning::ListOperationsRequest request;
    request.set_name(name);
    request.set_filter(filter);
    request.set_page_size(page_size);
    request.set_page_token(page_token);

    google::longrunning::ListOperationsResponse response;
    proxyListOperationsRequest(request, &response);

    return response;
}

grpc::Status RemoteExecutionClient::proxyGetOperationRequest(
    const GetOperationRequest &request, Operation *response)
{
    if (!(d_executionStub && d_operationsStub)) {
        throw std::runtime_error("Execution Stubs not Configured");
    }

    grpc::Status status;
    auto get_lambda = [&](grpc::ClientContext &context) {
        status = d_operationsStub->GetOperation(&context, request, response);
        return status;
    };

    d_executionGrpcClient->issueRequest(get_lambda,
                                        "Operations.GetOperation()", nullptr);
    return status;
}

grpc::Status RemoteExecutionClient::proxyListOperationsRequest(
    const ListOperationsRequest &request, ListOperationsResponse *response)
{
    if (!(d_executionStub && d_operationsStub)) {
        throw std::runtime_error("Execution Stubs not Configured");
    }

    grpc::Status status;
    auto list_lambda = [&](grpc::ClientContext &context) {
        status = d_operationsStub->ListOperations(&context, request, response);
        return status;
    };

    d_executionGrpcClient->issueRequest(
        list_lambda, "Operations.ListOperations()", nullptr);
    return status;
}

grpc::Status RemoteExecutionClient::proxyCancelOperationRequest(
    const CancelOperationRequest &request, google::protobuf::Empty *response)
{
    if (!(d_executionStub && d_operationsStub)) {
        throw std::runtime_error("Execution Stubs not Configured");
    }

    grpc::Status status;
    auto cancel_lambda = [&](grpc::ClientContext &context) {
        status =
            d_operationsStub->CancelOperation(&context, request, response);
        return status;
    };

    d_executionGrpcClient->issueRequest(
        cancel_lambda, "Operations.CancelOperation()", nullptr);
    return status;
}

#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#elif defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif

Platform *commandMutablePlatformDeprecated(Command &cmd)
{
    return cmd.mutable_platform();
}

const Platform &commandPlatformDeprecated(const Command &cmd)
{
    return cmd.platform();
}

#if defined(__clang__)
#pragma clang diagnostic pop
#elif defined(__GNUC__)
#pragma GCC diagnostic pop
#endif

} // namespace buildboxcommon
