// Copyright 2018-2023 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.

#ifndef INCLUDED_BUILDBOXCOMMON_LOCALEXECUTIONCLIENT
#define INCLUDED_BUILDBOXCOMMON_LOCALEXECUTIONCLIENT

#include <buildboxcommon_connectionoptions.h>
#include <buildboxcommon_executionclient.h>
#include <buildboxcommon_protos.h>
#include <buildboxcommon_temporarydirectory.h>

#include <map>
#include <mutex>
#include <optional>
#include <vector>

namespace buildboxcommon {

/**
 * This class provides a mechanism to execute REAPI actions locally by spawning
 * a BuildBox runner. By subclassing `ExecutionClient` it provides a common API
 * with `RemoteExecutionClient` such that applications can share code paths for
 * local and remote execution.
 */
class LocalExecutionClient : public ExecutionClient {
  public:
    /**
     * Constructs a `LocalExecutionClient`. The required CAS server should
     * typically be a local buildbox-casd instance to allow the runner to use
     * the LocalCAS protocol to stage the input tree. If it is a generic CAS
     * server, `--disable-localcas` must be passed as extra runner argument.
     *
     * The action cache client parameter is optional to support cache lookups
     * and updates.
     */
    explicit LocalExecutionClient(const ConnectionOptions &casServer,
                                  std::shared_ptr<buildboxcommon::GrpcClient>
                                      actionCacheGrpcClient = nullptr)
        : ExecutionClient(actionCacheGrpcClient), d_casServer(casServer)
    {
    }

    explicit LocalExecutionClient(const ConnectionOptions &casServer,
                                  const ConnectionOptions &assetServer,
                                  std::shared_ptr<buildboxcommon::GrpcClient>
                                      actionCacheGrpcClient = nullptr)
        : ExecutionClient(actionCacheGrpcClient), d_casServer(casServer),
          d_assetServer(assetServer)
    {
    }

    ~LocalExecutionClient() override = default;

    LocalExecutionClient(const LocalExecutionClient &other) = delete;
    LocalExecutionClient &
    operator=(const LocalExecutionClient &other) = delete;

    LocalExecutionClient(LocalExecutionClient &&other) noexcept = delete;

    LocalExecutionClient &
    operator=(LocalExecutionClient &&other) noexcept = delete;

    /**
     * This configures the BuildBox runner to spawn. If `runnerCommand` does
     * not contain a slash (/) character, a path lookup is performed. Extra
     * runner arguments are optional, e.g., to pass `--disable-localcas`.
     *
     * This method should be called before `init()`.
     */
    void setRunner(const std::string &runnerCommand,
                   const std::vector<std::string> &extraRunArgs = {});

    /**
     * Configure whether to cache action results with a non-zero exit code.
     */
    void setCacheFailures(bool cacheFailures);

    /**
     * Complete initialization. If no runner command has been configured,
     * perform a path lookup of `buildbox-run`.
     */
    void
    init(std::shared_ptr<ActionCache::StubInterface> actionCacheStub) override;
    void init() override;

    /**
     * Invoke the configured runner with `--validate-parameters` to allow early
     * detection of configuration issues. Returns `true` if validation is
     * successful.
     */
    bool validateRunner();

    /**
     * Spawn the configured runner with the given REAPI action. This does not
     * wait for the execution to complete. The returned `Operation` can be used
     * to check the status of or wait for the completion of the action.
     *
     * Multiple actions may be executed concurrently using a single instance of
     * `LocalExecutionClient`.
     *
     * Optionally, paths for stdout and stderr logs may be specified. This
     * enables live monitoring.
     */
    google::longrunning::Operation asyncExecuteAction(
        const Action &action, const std::string &stdoutFile = "",
        const std::string &stderrFile = "",
        const std::string &partialExecutionMetadataFile = "",
        const std::optional<std::string> &jobInstanceName = std::nullopt);

    google::longrunning::Operation asyncExecuteAction(
        const Digest &actionDigest, const std::atomic_bool &stop_requested,
        bool skipCache = false,
        const ExecutionPolicy *executionPolicy = nullptr) override;

    ActionResult
    executeAction(const Digest &actionDigest,
                  const std::atomic_bool &stopRequested,
                  bool skipCache = false,
                  const ExecutionPolicy *executionPolicy = nullptr) override;

    google::longrunning::Operation
    getOperation(const std::string &operationName) override;

    /**
     * Wait for the specified operation to complete. This will return the final
     * `Operation` message with `done` set to `true`, or throw an exception if
     * there was an unexpected error (e.g., misbehaving runner).
     */
    google::longrunning::Operation
    waitExecution(const std::string &operationName) override;

    /**
     * Cancel the specified operation by sending the SIGTERM signal to the
     * corresponding runner process. Use `waitExecution()` to wait for the
     * signalled runner to terminate. Depending on timing, it is possible that
     * the runner returns an `ActionResult` without error despite the call to
     * `cancelOperation()`.
     */
    bool cancelOperation(const std::string &operationName) override;

    /**
     * Cancel all currently running operations. The behaviour is equivalent to
     * cancelling each operation individually with `cancelOperation()`.
     */
    void cancelAllOperations();

    /**
     * This is not currently supported and will always throw an exception.
     */
    google::longrunning::ListOperationsResponse
    listOperations(const std::string &name, const std::string &filter,
                   int page_size, const std::string &page_token) override;

  private:
    google::longrunning::Operation
    getOperationInternal(const std::string &operationName, bool wait);
    CASClient *getCASClient();

    struct InternalOperation {
        Action action;
        Digest actionDigest;
        pid_t pid{};
        bool cancelled = false;
    };

    std::mutex d_mutex;

    TemporaryDirectory d_tempDirectory;
    std::string d_runnerPath;
    std::vector<std::string> d_extraRunArgs;
    std::unordered_map<std::string, InternalOperation> d_runningOperations;

    ConnectionOptions d_casServer;
    ConnectionOptions d_assetServer;
    std::unique_ptr<CASClient> d_casClient;

    bool d_cacheFailures = true;
};
} // namespace buildboxcommon
#endif
