/*
 * Copyright 2019 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.
 */

#ifndef INCLUDED_BUILDBOXCASD_LOCALCASINSTANCE_H
#define INCLUDED_BUILDBOXCASD_LOCALCASINSTANCE_H

#include <buildboxcasd_casinstance.h>
#include <buildboxcasd_inodecache.h>
#include <buildboxcommon_filestager.h>
#include <buildboxcommon_merklize.h>

#include <cstddef>
#include <expected>
#include <memory>
#include <optional>
#include <ThreadPool.h>

using namespace build::bazel::remote::execution::v2;

using grpc::Status;

namespace buildboxcasd {

class LocalCasInstance
    /* This class implements the logic for methods that service a CAS server,
     * as defined by the Remote Execution API.
     */
    : public CasInstance {
  public:
    explicit LocalCasInstance(
        const std::shared_ptr<buildboxcommon::LocalCas> &storage,
        const std::unordered_map<buildboxcommon::StageTreeRequest::StagingMode,
                                 std::shared_ptr<buildboxcommon::FileStager>>
            &fileStagers,
        const std::shared_ptr<ThreadPool> &digestThreadPool,
        const std::shared_ptr<std::unordered_set<std::string>>
            &allowedPreUnstageCommands,
        const std::string &instance_name);

    std::shared_ptr<CasInstance> clone(const std::string &instanceName,
                                       const std::string &root) override;
    LocalCasInstance(const LocalCasInstance &other,
                     const std::string &instanceName, const std::string &root);

    grpc::Status
    GetLocalServerDetails(const GetLocalServerDetailsRequest &request,
                          LocalServerDetails *response) override;

    grpc::Status FindMissingBlobs(const FindMissingBlobsRequest &request,
                                  FindMissingBlobsResponse *response) override;

    grpc::Status BatchUpdateBlobs(const BatchUpdateBlobsRequest &request,
                                  BatchUpdateBlobsResponse *response) override;

    grpc::Status BatchReadBlobs(const BatchReadBlobsRequest &request,
                                BatchReadBlobsResponse *response) override;

    grpc::Status Write(WriteRequest *request_message,
                       ServerReader<WriteRequest> &request,
                       WriteResponse *response,
                       Digest *requested_digest) override;

    // LocalCAS methods
    grpc::Status FetchTree(const FetchTreeRequest &request,
                           FetchTreeResponse *response) override;

    grpc::Status UploadTree(const UploadTreeRequest &request,
                            UploadTreeResponse *response) override;

    grpc::Status
    StageTree(const StageTreeRequest &stage_request,
              ServerReaderWriter<StageTreeResponse, StageTreeRequest> *stream,
              Server *baseServer) override;

    grpc::Status NestedServer(
        const NestedServerRequest &request,
        ServerReaderWriter<NestedServerResponse, NestedServerRequest> *stream,
        Server *baseServer) override;

    grpc::Status ExportFiles(const ExportFilesRequest &request,
                             ExportFilesResponse *response) override;

    grpc::Status CaptureTree(const CaptureTreeRequest &request,
                             CaptureTreeResponse *response) override;

    grpc::Status CaptureFiles(const CaptureFilesRequest &request,
                              CaptureFilesResponse *response) override;

    grpc::Status HashFiles(const HashFilesRequest &request,
                           HashFilesResponse *response) override;

    void setInodeCache(std::shared_ptr<InodeCache> inodeCache);

  protected:
    /* Writes to the local storage, returning the status code that should be
     * attached to the response to the client.
     */
    google::rpc::Status writeToLocalStorage(const Digest &digest,
                                            const std::string &data);

    /* Moves a file to the local storage, returns a status code to return as a
     * response.
     */
    grpc::Status
    moveTemporaryFileToLocalStorage(const Digest &digest,
                                    buildboxcommon::TemporaryFile &&tempFile);

    void addCapturedFileToLocalStorage(const Digest &digest, int fd, int dirfd,
                                       const std::string &path,
                                       const bool move_file_hint);

    /* Attempts to read from the local storage into `data` returning a gRPC
     * status code that can be embedded into a reply.
     *
     * If the status returned is not successful, `data` remains unmodified.
     */
    google::rpc::Status readFromLocalStorage(const Digest &digest,
                                             std::string *data) const;

    google::rpc::Status readFromLocalStorage(const Digest &digest,
                                             std::string *data, size_t offset,
                                             size_t limit) const;

    /* Copy blob from path to the local storage. */
    void copyToLocalStorage(const Digest &digest, int fd, int dirfd,
                            const std::string &path);

    /*
     * Store digests locally, and upload to remote CAS.
     * Returns Tree digest for constructing the response.
     */
    virtual Digest
    UploadAndStore(int dirfd,
                   const buildboxcommon::digest_string_map &digest_blob_map,
                   const buildboxcommon::digest_string_map &digest_path_map,
                   const std::optional<Tree> &path_tree,
                   const bool bypass_local_cache, const bool move_files_hint);

    // Capture data of open file with known digest
    virtual void captureFileData(const Digest &digest, int fd,
                                 const std::string &path,
                                 const bool bypass_local_cache,
                                 const bool move_file_hint);

    // Walks a tree and makes sure that all its blobs are stored locally.
    // If a CAS client is available, it will try and fetch missing blobs from a
    // remote.
    // It returns an `OK` status if all the blobs contained in the tree are
    // available locally, `FAILED_PRECONDITION` otherwise.
    virtual Status prepareTreeForStaging(const Digest &root_digest) const;

    virtual bool hasRemote() const;

    std::vector<Digest>
    digestsMissingFromDirectory(const Directory &directory,
                                bool file_blobs = true) const;

    std::shared_ptr<buildboxcommon::LocalCas>
        d_storage; // NOLINT
                   // (cppcoreguidelines-non-private-member-variables-in-classes)

  private:
    std::unordered_map<buildboxcommon::StageTreeRequest::StagingMode,
                       std::shared_ptr<buildboxcommon::FileStager>>
        d_fileStagers;
    std::shared_ptr<ThreadPool> d_digestThreadPool = nullptr;
    std::shared_ptr<std::unordered_set<std::string>>
        d_allowedPreUnstageCommands;
    uid_t d_uid;
    std::shared_ptr<InodeCache> d_inodeCache;

    bool hasBlob(const Digest &digest) override;

    std::string createStagingDirectory() const;

    // Returns whether all the contents of given tree are stored locally.
    bool treeIsAvailableLocally(const Digest &root_digest,
                                bool file_blobs = true) const;
    bool treeIsAvailableLocally(const Digest &root_digest, bool file_blobs,
                                int64_t *cache_hits,
                                int64_t *cache_misses) const;

    // Creates a nested server from a RemoteApisSocketConfig.
    // If instance_map is provided, clones each base instance to its mapped
    // name. If instance_name is provided, clones that single instance to "".
    // Returns an error status if both are specified.
    static std::expected<std::shared_ptr<Server>, grpc::Status>
    createNestedServer(const buildboxcommon::RemoteApisSocketConfig &config,
                       Server *baseServer, const std::string &rootPath,
                       const std::shared_ptr<InodeCache> &inodeCache);

    void startServerWithUnixSocket(const std::shared_ptr<Server> &server,
                                   const std::filesystem::path &rootPath,
                                   const std::filesystem::path &socketPath,
                                   std::optional<gid_t> gid,
                                   std::optional<mode_t> mode);

    void
    startServerWithAbsoluteUnixSocket(const std::shared_ptr<Server> &server,
                                      const std::filesystem::path &socketPath,
                                      std::optional<gid_t> gid,
                                      std::optional<mode_t> mode);

    Digest cacheFile(int fd);
    Digest cacheInode(int fd);
    Digest cache(int fd, bool writeBlob);

    Digest cacheFileXattr(int fd, const std::string &xattrName);
    Digest cacheInodeXattr(int fd, const std::string &xattrName);
    Digest cacheXattr(int fd, const std::string &xattrName, bool writeBlob);

    /*
     * Bytestream API
     *
     * We will service resource names that look like:
     *    "{instance_name}/uploads/{uuid}/blobs/{hash}/{size}", or
     *    "{instance_name}/blobs/{hash}/{size}"
     *  with anything after "size" being ignored.
     */
    google::rpc::Status readBlob(const Digest &digest, std::string *data,
                                 size_t read_offset,
                                 size_t read_limit) override;

    google::rpc::Status writeBlob(const Digest &digest,
                                  const std::string &data) override;

    CaptureFilesResponse_Response
    captureFile(const std::string &root, const std::string &path,
                const std::vector<std::string> &capture_properties,
                const bool bypass_local_cache, const bool move_file_hint,
                const bool skipUpload, const mode_t unixModeMask,
                const bool allowChmodToRead);

    HashFilesResponse_Response
    HashFile(const std::string &root, const std::string &path,
             const std::vector<std::string> &capture_properties,
             const mode_t unixModeMask, const bool allowChmodToRead);

    CaptureTreeResponse_Response
    captureDirectory(const std::string &root, const std::string &path,
                     const std::vector<std::string> &capture_properties,
                     const bool bypass_local_cache, const bool move_files_hint,
                     const Command_OutputDirectoryFormat outputDirFormat,
                     const bool skipUpload, const mode_t unixModeMask,
                     const bool allowChmodToRead);

    buildboxcommon::FileStager *
    getFileStager(StageTreeRequest_StagingMode mode,
                  const std::optional<buildboxcommon::ProcessCredentials>
                      &accessCredentials);
};

} // namespace buildboxcasd

#endif // INCLUDED_BUILDBOXCASD_LOCALCASINSTANCE_H
