/*
 * Copyright 2020 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_BUILDBOXCOMMON_FSLOCALCAS_H
#define INCLUDED_BUILDBOXCOMMON_FSLOCALCAS_H

#include <array>
#include <chrono>
#include <memory>
#include <optional>
#include <sys/stat.h>

#include <buildboxcommon_localcas.h>
#include <buildboxcommon_objectsubdirsfdstore.h>

#include <buildboxcommon_protos.h>
#include <buildboxcommon_temporarydirectory.h>
#include <buildboxcommon_temporaryfile.h>

namespace buildboxcommon {

class FsLocalCas final : public LocalCas {
    /* Implements a local, content-addressable storage in which the blobs are
     * stored in a tree directory structure indexed by their Digests.
     *
     * The first two most significant characters of the hashes are used
     * for a top-level directory name and the rest as a filename.
     *
     * For example, the contents of a blob with Digest
     * `ec75218cfaaebdd03d1c135c96c7576a323ffb7bb63bb456a8e6da5bd5dbc612/58`
     * would be stored in:
     * d_storage_root/cas/objects/ec/
     *   |- 75218cfaaebdd03d1c135c96c7576a323ffb7bb63bb456a8e6da5bd5dbc612
     */
  public:
    // Create a new LocalCAS using the given directory to store files:
    explicit FsLocalCas(const std::string &root_path,
                        const bool allow_external_file_moves = false,
                        const bool storeWritableObjects = false,
                        const bool trackDiskUsage = true);

    ~FsLocalCas() override;

    // disallow move and copy
    FsLocalCas(const FsLocalCas &) = delete;
    FsLocalCas &operator=(const FsLocalCas &) = delete;
    FsLocalCas(FsLocalCas &&) = delete;
    FsLocalCas &operator=(FsLocalCas &&) = delete;

    bool hasBlob(const Digest &digest) override;

    bool writeBlob(const Digest &digest, const std::string &data) override;
    bool writeBlob(int fd, Digest *digest) override;

    // Adds a blob from a file in `d_temp_directory` (created with
    // `createTemporaryFile()`).
    // If the `path` is outside that directory, or `digest` does not match the
    // actual contents of the file, throws an `invalid_argument` exception.
    //
    // This method is responsible for carrying out the actual write into CAS.
    // It moves a file (which could be a temporary buffer or regular existing
    // file) into the storage.
    // In consequence, it will be in charge of updating these metrics:
    //    * `MetricNames::COUNTER_NUM_BLOBS_WRITTEN_TO_LOCAL`
    //    * `MetricNames::COUNTER_NAME_BYTES_WRITE`
    bool
    moveBlobFromTemporaryFile(const Digest &digest,
                              buildboxcommon::TemporaryFile tempFile) override;
    bool moveBlobFromTemporaryFile(
        const Digest &digest,
        const buildboxcommon::TemporaryDirectory &directory,
        const std::string &path) override;

    std::unique_ptr<std::string> readBlob(const Digest &digest, int64_t offset,
                                          size_t length) override;

    std::unique_ptr<std::string> readBlob(const Digest &digest) override;

    bool copyBlob(const Digest &digest, int dirfd, const char *path,
                  mode_t mode = 0, struct stat *destStat = nullptr) override;

    bool deleteBlob(const Digest &digest) override;

    bool deleteBlob(const Digest &digest,
                    const blob_time_type &if_unmodified_since) override;

    void listBlobs(digest_time_callback_type callback) override;

    int64_t getDiskUsage(bool force_scan = false) override;

    std::optional<int64_t> getDiskQuota() override;

    std::string path(const Digest &digest) override;

    std::string pathUnchecked(const Digest &digest) override;

    FdNamePair pathAt(const Digest &digest) override;

    FdNamePair pathAtUnchecked(const Digest &digest) override;

    const std::string &path() override;

    static int pathHashPrefixLength() { return s_HASH_PREFIX_LENGTH; };

    // Create staging directories in `d_staging_directory` and
    // temporary files inside `d_temp_directory`.
    // (From that location it is guaranteed that hard links can reach the CAS'
    // files.)
    buildboxcommon::TemporaryDirectory createStagingDirectory() override;
    buildboxcommon::TemporaryFile createTemporaryFile() override;
    buildboxcommon::TemporaryDirectory createTemporaryDirectory() override;

    int64_t getAvailableDiskSpace() override;

    // Returns whether the `moveBlobFromExternalFile()` can be used in order to
    // move files to CAS in order to avoid copies.
    bool externalFileMovesAllowed() const override
    {
        return d_external_file_moves_allowed;
    }

    bool objectFilesWritable() const override
    {
        return d_storeWritableObjects;
    }

    // Moves the file into the storage.
    // For that, `path` must be:
    //  1) Owned by the user that is running this instance
    //  2) In the same filesystem as `d_storage_root`.
    //
    // If any of those conditions is not met, returns false.
    // If the blob was successfully moved, returns true. In that case `path` is
    // unlinked.
    //
    // If `externalFileMovesAllowed()` returns false, throws an
    // `invalid_argument` exception. I/O errors throw `std::system_error`.
    //
    bool moveBlobFromExternalFile(const Digest &digest, int dirfd,
                                  const std::string &path) override;

  private:
    const std::string d_storage_root;

    const std::string d_objects_directory;
    dev_t d_objects_directory_st_dev;
    // (The device ID allows determining whether we can hardlink an external
    // file to the CAS.)

    const std::string d_temp_directory;
    const std::string d_staging_directory;

    // When the instance gets destroyed, the disk usage at the time will be
    // written to this metadata file
    // That way a new instance initialized with an already-populated CAS
    // directory (if it is safe to assume that files haven't been touched) can
    // read the value and avoid scanning the directory.
    const std::string d_disk_usage_metadata_filepath;

    // Whether we trust clients to move files straight to the CAS storage.
    // That will only be possible for files stored in the same filesystem as
    // `d_storage_root` and owned by the same user that is running this
    // instance.
    const bool d_external_file_moves_allowed;

    // Store object files with u=rw permission instead of u=r
    const bool d_storeWritableObjects;

    // User that is running this process. This will allow to ensure that files
    // moved into the storage are owned by us.
    const uid_t d_process_uid;

    // ObjectSubDirsFDStore will create the object subdirectories.
    // It will also store open file descriptors to these directories.
    std::unique_ptr<ObjectSubDirsFDStore> d_object_subdir_fd_store;

    // Stores whether the metadata file containing the size of the CAS was
    // deleted.
    bool d_disk_usage_metadata_file_deleted = false;

    // stores the current estimated disk usage
    std::atomic<int64_t> d_disk_usage = 0;

    const bool d_trackDiskUsage;

    /* Given a Digest, returns the corresponding path to the store content in
     * the storage.
     *
     * The `create_parent_directory` parameter, if set, ensures that the top
     * level directory will be present in the CAS `objects/` directory.
     *
     * For example, for digest "xy...abc/123", directory `cas/objects/xy` will
     * be created and the path returned:
     * "d_storage_root/cas/objects/xy/...abc".
     */
    std::string filePath(const Digest &digest,
                         bool create_parent_directory = false) const;

    FdNamePair filePathAt(const Digest &digest) const;

    /* Given a Digest and some data, checks that the digest matches the actual
     * data.
     *
     * On success it returns. Otherwise it throws an `std::invalid_argument`
     * exception.
     */
    void validateBlobDigest(const Digest &digest,
                            const std::string &data) const;

    /*
     * Compares two digests.
     * On success it returns. Otherwise it throws an `std::invalid_argument`
     * exception.
     */
    static void validateDigests(const Digest &given_digest,
                                const Digest &computed_digest);

    /* Given a Digest, attempts to open its corresponding file.
     * If the file does not exist, returns an unassociated `ifstream` object
     * (i.e. `std::ifstream()`).
     * To determine whether the operation suceeded, call `is_open()` on the
     * returned object.
     * Other errors throw a `system_error` exception.
     */
    std::ifstream openFile(const Digest &digest);

    bool digestFromFile(const std::string &dirname,
                        const std::string &filename, Digest *digest,
                        struct stat *st) const;

    // Number of most significant hash characters to take for top-level
    // directory names:
    static const int s_HASH_PREFIX_LENGTH;

    const Digest d_emptyDigest;
    const std::string::size_type d_hashLength;

    static int hardLink(int source_dirfd, const std::string &source_path,
                        int destination_dirfd,
                        const std::string &destination_path);

    bool linkTemporaryFileToCas(const Digest &digest, int dirfd,
                                const std::string &path);

    static bool deleteFile(int dirfd, const std::string &path);

    static struct stat getStat(int dirfd, const std::string &path);

    /*
     * Extract and return the last modification time reported by `stat()` with
     * microsecond precision.
     */
    static blob_time_type getMtime(const struct stat &s);
    static blob_time_type getMtime(const std::string &path);

    void writeDiskUsageMetadataFile();

    void deleteDiskUsageMetadataFile();

    /* Attempt to read the disk usage from the metadata file in the CAS root.
     * If a valid value cannot be read from the file, returns -1.
     */
    int64_t getDiskUsageFromMetadataFile() const;
};

} // namespace buildboxcommon

#endif // INCLUDED_BUILDBOXCOMMON_FSLOCALCAS_H
