/*
 * 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_BUILDBOXCOMMON_HARDLINKSTAGER_H
#define INCLUDED_BUILDBOXCOMMON_HARDLINKSTAGER_H

#include <buildboxcommon_filestager.h>
#include <buildboxcommon_localcas.h>

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

namespace buildboxcommon {

class HardLinkStager final : public FileStager {

  public:
    /* Allows staging the contents of directories stored in a LocalCAS
     * instance using hard links, so it only supports staging inside the
     * filesystem of the LocalCAS.
     *
     * Note: Support for staging symbolic links with absolute paths is not
     * currently implemented (causes `stage()` to abort).
     */
    HardLinkStager(LocalCas *cas_storage,
                   const std::shared_ptr<ThreadPool> &threadPool);

    /* Stage the contents under directory specified by `root_digest`.
     *
     * The optional `path` argument allows to specify where the directory
     * should be staged, but it must point to a non-existent or empty
     * directory that is in the same filesystem than the LocalCAS containing
     * the data.
     *
     * If the directory given is not empty, is in a different filesystem, or if
     * encounters a non-supported symlink, it throws `std::invalid_argument`.
     *
     * Any filesystem errors encountered during the staging
     * throw `std::system_error`, aborting the staging and restoring back
     * the staging directory to its previous state (empty or non-existent).
     */
    std::unique_ptr<StagedDirectory>
    stage(const Digest &root_digest, const std::string &path,
          const std::optional<ProcessCredentials> &accessCredentials =
              std::nullopt,
          const FileStagedCallback &callback = std::nullopt) override;

    class HardLinkStagedDirectory : public StagedDirectory {
        /* Represents a staged directory. It's destructor will
         * delete the directory or clear its contents depending on whether it
         * existed beforehand.
         */

      public:
        explicit HardLinkStagedDirectory(const std::string &path);

        ~HardLinkStagedDirectory() override;

        // delete copy and move constructors
        HardLinkStagedDirectory(const HardLinkStagedDirectory &) = delete;
        HardLinkStagedDirectory &
        operator=(const HardLinkStagedDirectory &) = delete;
        HardLinkStagedDirectory(HardLinkStagedDirectory &&) = delete;
        HardLinkStagedDirectory &
        operator=(HardLinkStagedDirectory &&) = delete;

      private:
        const std::string d_staged_path;
    };

  private:
    std::shared_ptr<ThreadPool> d_threadPool;
    uid_t d_euid;

    // Use the same timestamp for copied or new files to minimize
    // non-deterministic behavior.
    google::protobuf::Timestamp d_deterministicTime;

    static void unstage(const std::string &path);

    void stageFile(const build::bazel::remote::execution::v2::FileNode &file,
                   int dirfd, const std::string &path, bool access_by_owner,
                   const std::optional<gid_t> &accessByGroup,
                   const FileStagedCallback &callback);

    void stageDirectory(const Digest &digest, const std::string &path,
                        bool access_by_owner,
                        const std::optional<gid_t> &accessByGroup,
                        const FileStagedCallback &callback);

    /* Helper that recursively stages a tree.
     *
     * It must be initially invoked with `is_root_directory` set, and `path`
     * pointing to an existing empty directory. (The former makes the function
     * skip the creation of the latter in the initial call.)
     *
     * The `readonly` argument will be set accordingly for recursive calls
     * after checking the node properties of that level's `Directory`.
     */
    void stageDirectoriesRecursively(const Digest &digest,
                                     const bool is_root_directory,
                                     const std::string &path,
                                     bool access_by_owner,
                                     const std::optional<gid_t> &accessByGroup,
                                     const FileStagedCallback &callback,
                                     bool readonly = false);

    void stageSymLink(const std::string &target, const std::string &link_path);

    void hardLinkAt(const int target_dir_fd, const std::string &target,
                    const int link_dir_fd, const std::string &link_path);

    buildboxcommon::Directory getDirectory(const Digest &digest);
};
} // namespace buildboxcommon

#endif // INCLUDED_BUILDBOXCOMMON_HARDLINKSTAGER_H
