/*
 * 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_CASINSTANCE_H
#define INCLUDED_BUILDBOXCASD_CASINSTANCE_H

#include <buildboxcasd_fetchtreecache.h>
#include <buildboxcommon_localcas.h>
#include <buildboxcommon_protos.h>

#include <cstddef>
#include <string>

using namespace build::bazel::remote::execution::v2;
using namespace build::buildgrid;
using namespace google::bytestream;

using grpc::ServerReader;
using grpc::ServerReaderWriter;
using grpc::ServerWriter;
using grpc::Status;

namespace buildboxcasd {

class Server;

class CasInstance {
    /* Defines a common interface for providing the necessary methods to
     * service the calls of the CAS service portion of the Remote Execution
     * API.
     *
     * We distinguish between the methods that operate on multiple
     * blobs (those are defined as virtual) and those that operate on a single
     * blob (implemented here and shared among different specializations of
     * this class). The goal is to allow sharing as much common code as
     * possible between implementations.
     */
  public:
    CasInstance(const std::string &instance_name);
    virtual ~CasInstance() = 0;

    virtual std::shared_ptr<CasInstance> clone(const std::string &instanceName,
                                               const std::string &root) = 0;

    CasInstance(const CasInstance &other, const std::string &instanceName,
                const std::string &root);

    // Delete copy constructor and assignment operator
    CasInstance(const CasInstance &other) = delete;
    CasInstance &operator=(const CasInstance &other) = delete;

    // Delete move constructor and assignment operator
    CasInstance(CasInstance &&other) noexcept = delete;
    CasInstance &operator=(CasInstance &&other) noexcept = delete;

    virtual grpc::Status
    GetLocalServerDetails(const GetLocalServerDetailsRequest &request,
                          LocalServerDetails *response) = 0;

    virtual grpc::Status
    FindMissingBlobs(const FindMissingBlobsRequest &request,
                     FindMissingBlobsResponse *response) = 0;

    virtual grpc::Status
    BatchUpdateBlobs(const BatchUpdateBlobsRequest &request,
                     BatchUpdateBlobsResponse *response) = 0;

    virtual grpc::Status BatchReadBlobs(const BatchReadBlobsRequest &request,
                                        BatchReadBlobsResponse *response) = 0;

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

    // LocalCAS methods
    virtual grpc::Status
    FetchMissingBlobs(const FetchMissingBlobsRequest &request,
                      FetchMissingBlobsResponse *response);

    virtual grpc::Status
    UploadMissingBlobs(const UploadMissingBlobsRequest &request,
                       UploadMissingBlobsResponse *response);

    virtual grpc::Status FetchTree(const FetchTreeRequest &request,
                                   FetchTreeResponse *response);

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

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

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

    virtual grpc::Status ExportFiles(const ExportFilesRequest &request,
                                     ExportFilesResponse *response) = 0;

    virtual grpc::Status CaptureTree(const CaptureTreeRequest &request,
                                     CaptureTreeResponse *response) = 0;

    virtual grpc::Status CaptureFiles(const CaptureFilesRequest &request,
                                      CaptureFilesResponse *response) = 0;

    virtual grpc::Status HashFiles(const HashFilesRequest &request,
                                   HashFilesResponse *response) = 0;

    // These calls can be shared among LocalCAS and CAS proxy instances since
    // they process individual blobs (but may be overridden as optimization):
    virtual grpc::Status GetTree(const GetTreeRequest &request,
                                 ServerWriter<GetTreeResponse> *writer);

    grpc::Status Read(const ReadRequest &request,
                      ServerWriter<ReadResponse> *writer,
                      Digest *requested_digest);

    std::string instanceName() const { return d_instance_name; }

    bool chrooted() const { return d_chrooted; }
    int rootDirfd() const { return d_root_dirfd.get(); }
    virtual google::rpc::Status readBlob(const Digest &digest,
                                         std::string *data, size_t read_offset,
                                         size_t read_limit) = 0;

    /* Given a digest returns a `Directory` object.
     * Precondition: `hasBlob(digest) == true` (which is how `buildTree()`
     * uses this function.)
     */
    Directory getDirectory(const Digest &digest);

  protected:
    /*
     * Helper to process a `ByteStream.Write()` request. It uses a
     * temporary file as a buffer to incrementally store the contents to be
     * added to the CAS as they are received.
     *
     * If the initial request is valid, it will first write the requested
     * digest into `digest`.
     *
     * If `digest` is already present in the storage, it immediately returns
     * `grpc::StatusCode::ALREADY_EXISTS`. Otherwise it starts to incrementally
     * write the data received to `buffer_path`.
     *
     * It checks the size, but does not compute the hash of the data received.
     * (that should be done by the CAS before inserting the blob). If the
     * returned status has code `grpc::StatusCode:OK`, then the expected number
     * of bytes was written to `buffer_path`.
     *
     * The file at `buffer_path` might end with incomplete/invalid data.
     */
    grpc::Status processWriteRequest(WriteRequest *request_message,
                                     ServerReader<WriteRequest> &request,
                                     Digest *digest,
                                     const std::string &buffer_path);

    FetchTreeCache *getTreeCache() const;

    static void recordTreeCacheMetrics(int64_t cache_hits,
                                       int64_t cache_misses);

    int openInRoot(const std::string &root, const std::string &path,
                   int flags);

    /* Parses a `resource_name` string and extracts the Digest it
     * specifies. If the resource name is not relevant to the Bytestream
     * CAS requests that we are servicing, or is invalid, it throws an
     * `std::invalid_argument` exception.
     */
    static Digest
    digestFromUploadResourceName(const std::string &resource_name,
                                 const std::string &instance_name);
    static Digest
    digestFromDownloadResourceName(const std::string &resource_name,
                                   const std::string &instance_name);

  private:
    // Cache the root digests of the trees that were recently fetched in order
    // to avoid scanning the storage to check whether all their blobs are
    // present.
    mutable FetchTreeCache d_tree_cache;

    static Digest
    digestFromPartialResourceName(const std::string &resource_name,
                                  const std::size_t &hash_start);

    /* Builds and writes the tree rooted at a given digest.
     * Returns an `grpc::Status::OK` on success or an `INVALID_ARGUMENT` error
     * status if the `requested_page_token value` is invalid.
     */
    grpc::Status getTree(const Directory &directory, const int page_max_items,
                         const int page_offset,
                         ServerWriter<GetTreeResponse> *writer);

    /* Recursively traverses a tree and writes pages to the stream as it
     * advances.
     *
     * `requested_page_number` allows to skip writing to the stream pages that
     * precede it.
     *
     * `page_max_items` sets the limit of entries in a single `GetTreeResponse`
     * message. If set to 0, the number of entries will not be limited.
     *
     * `current_page_number` must be set to 0 in the initial call. When the
     * function returns it will contain the number of the last page that was
     * populated.
     *
     * `current_page` must point to an empty `GetTreeResponse` in the initial
     * call. That message will contain a partial page when this function
     * returns, which must be written to the stream in order to complete the
     * request.
     *
     * `current_page_size` contains the size of the current page, and is reset
     * when a new page is created.
     */
    void writeTree(const Directory &directory, const int page_max_items,
                   const int requested_page_number,
                   GetTreeResponse *current_page, int *current_page_number,
                   size_t *current_page_size,
                   ServerWriter<GetTreeResponse> *writer);

    /* These functions allow the `buildTree()` algorithm and the
     * ByteStream operations to be shared among the different
     * `ContentAddresableStorageImplementation` instances.
     */
    virtual bool hasBlob(const Digest &digest) = 0;

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

    /* Helper to parse the different arguments provided to the Bytestream
     * calls.
     *
     * If the values are in range and the digest could be parsed properly,
     * it is written into `digest`. In case any error is encountered, `digest`
     * is not modified.
     */
    grpc::Status bytestreamReadArgumentStatus(const ReadRequest &request,
                                              Digest *digest);

    static grpc::Status parsePageToken(const std::string &page_token,
                                       int *starting_page_number);

    const std::string
        d_instance_name; // NOLINT
                         // (cppcoreguidelines-avoid-const-or-ref-data-members)

    // Root directory file descriptor for client-specified paths
    buildboxcommon::FileDescriptor d_root_dirfd;
    bool d_chrooted = false;
};

} // namespace buildboxcasd

#endif // INCLUDED_BUILDBOXCASD_CASINSTANCE_H
