/*
 * Copyright 2018 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_FILEUTILS
#define INCLUDED_BUILDBOXCOMMON_FILEUTILS

#include <buildboxcommon_direntwrapper.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_permissions.h>
#include <buildboxcommon_protos.h>
#include <buildboxcommon_tempconstants.h>

#include <chrono>
#include <cstddef>
#include <cstdint>
#include <dirent.h>
#include <functional>
#include <google/protobuf/util/time_util.h>
#include <optional>
#include <string>
#include <sys/stat.h>
#include <unistd.h>

namespace buildboxcommon {

struct FileUtils {
    // Provide a namespace for file utilities.

    /**
     * Return true if the path exists within the given directory.
     */
    static bool existsInDir(int dirFd, const char *path);

    /**
     * Return true if the given path represents a directory.
     */
    static bool isDirectory(const char *path);
    static bool isDirectory(int fd);
    /**
     * Do not follow symlinks
     */
    static bool isDirectoryNoFollow(const char *path);

    /**
     * Return true if the given path represents a regular file.
     */
    static bool isRegularFile(const char *path);
    static bool isRegularFile(int dirfd, const char *path);
    /**
     * Do not follow symlinks
     */
    static bool isRegularFileNoFollow(const char *path);
    static bool isRegularFileNoFollow(int dirfd, const char *path);

    /**
     * Return true if the given path represents a symlink.
     */
    static bool isSymlink(const char *path);

    /**
     * Return true if the directory is empty.
     */
    static bool directoryIsEmpty(const char *path);

    /**
     * Create a directory if it doesn't already exist, including parents.
     * Create the directory with specified mode, by default 0777 -rwxrwxrwx.
     */
    static void createDirectory(int dirfd, const char *path,
                                mode_t mode = PERMISSION_RWXALL);
    static void createDirectory(const char *path,
                                mode_t mode = PERMISSION_RWXALL);

    /**
     * Delete an existing directory.
     */
    static void deleteDirectory(const char *path);
    static void deleteDirectory(int dirfd, const char *path);

    /**
     * Delete the contents of an existing directory.
     */
    static void clearDirectory(const char *path);
    static void clearDirectory(int dirfd, const char *path);

    /**
     * Return true if the given file path is executable.
     */
    static bool isExecutable(const char *path);

    /**
     * Return true if a given file descriptor is executable.
     */
    static bool isExecutable(int fd);

    /**
     * Return the size in bytes of the file specified by the given path.
     */
    static int64_t getFileSize(const char *path);

    /**
     * Return the size in bytes of the file specified by the given file
     * descriptor.
     */
    static int64_t getFileSize(int fd);

    /**
     * Return the Unix mode of the file specified by the given file
     * descriptor.
     */
    static mode_t getUnixMode(int fd);
    /**
     * Return the Unix mode of the file specified by the given file
     * path.
     */
    static mode_t getUnixMode(const char *path);

    /**
     * Return a time point in seconds representing the mtime of the file
     * specified by the given path.
     */
    static std::chrono::system_clock::time_point
    getFileMtime(const char *path);

    /**
     * Return a time point in seconds representing the mtime of the file
     * specified by the given file descriptor.
     */
    static std::chrono::system_clock::time_point getFileMtime(const int fd);

    /**
     * Modify the mtime of an existing file to the time represented by the
     * given time_point. The file is described by the given file
     * descriptor.
     */
    static void setFileMtime(const int fd,
                             std::chrono::system_clock::time_point timepoint);

    /**
     * Modify the mtime of an existing file to the time represented by the
     * given time_point. The file is described by the given path.
     */
    static void setFileMtime(int dirfd, const char *path,
                             std::chrono::system_clock::time_point timepoint);
    static void setFileMtime(const char *path,
                             std::chrono::system_clock::time_point timepoint);

    /**
     * Modify the atime and mtime of an existing file to the time represented
     * by the given time_point. If the value is std::nullopt, it is set to the
     * current time, similar to `touch`.
     */
    static void setFileTimes(
        int dirfd, const char *path,
        const std::optional<std::chrono::system_clock::time_point> &atime,
        const std::optional<std::chrono::system_clock::time_point> &mtime);
    static void setFileTimes(
        int fd,
        const std::optional<std::chrono::system_clock::time_point> &atime,
        const std::optional<std::chrono::system_clock::time_point> &mtime);
    static void setFileTimes(
        const char *path,
        const std::optional<std::chrono::system_clock::time_point> &atime,
        const std::optional<std::chrono::system_clock::time_point> &mtime);
    /**
     * Make the given file executable.
     */
    static void makeExecutable(const char *path);

    /**
     * Gets the contents of a file
     */
    static std::string getFileContents(const char *path);
    static std::string getFileContents(int dirfd, const char *path);
    static std::string getFileContents(int fd);
    static std::string getFileContents(int fd, int64_t offset,
                                       std::optional<int64_t> limit);

    /**
     * Return true if the given path contains any `..` segments.
     */
    static bool containsDotDot(const char *path);

    /**
     * Simplify the given path.
     *
     * The returned path will not contain any empty or `.` segments, and any
     * `..` segments will occur at the start of the path.
     */
    static std::string normalizePath(const char *path);

    /**
     * Return the basename of the given path.
     *
     * The returned entity will be last segment of the path.
     * If no segments found, will an empty string.
     */
    static std::string pathBasename(const char *path);
    static std::string pathBasename(const std::string &path);

    /**
     * Make the given path absolute, using the current working directory.
     *
     * `cwd` must be an absolute path, otherwise it throws an
     * `std::runtime_error` exception.
     */
    static std::string makePathAbsolute(const std::string &path,
                                        const std::string &cwd);

    /**
     * Join two path segments together and return the normalized results.
     * - When the second segment is an absolute path, it will be the only
     * path included in the (normalized) result, similar to other
     * implementations of standard libraries that join paths,
     * unless the `forceSecondSegmentRelative` flag is set to true (Default:
     * false)
     * - Warning: When the paths include `..`, the resulting joined path may
     * escape the first path
     *
     * e.g. `joinPathSegments('/a/', '/b')`       -> '/b'
     *      `joinPathSegments('/a/', '/b', true)` -> '/a/b'
     */
    static std::string
    joinPathSegments(const std::string &firstSegment,
                     const std::string &secondSegment,
                     const bool forceSecondSegmentRelative = false);

    /**
     * Join two path segments together (using FileUtils::joinPathSegments), but
     * throw a `std::runtime_error` if the second path segment makes the joined
     * path escape the first path segment.
     *
     * Accepts the optional flag `forceRelativePathWithinBaseDir` for invoking
     * `joinPathSegments` (Default: false)
     *
     * e.g. `joinPathSegmentsNoEscape('/a/', '/b')`       -> raises
     * `std::runtime_error` due to `/b` escaping '/a/'
     *      `joinPathSegmentsNoEscape('/a/', '/b', true)` -> '/a/b'
     */
    static std::string joinPathSegmentsNoEscape(
        const std::string &basedir, const std::string &pathWithinBasedir,
        const bool forceRelativePathWithinBaseDir = false);

    /**
     * Copy file contents (non-atomically) from the given source path
     * to the given destination path. Additionally attempt to
     * duplicate the file mode.
     */
    static void copyFile(const char *src_path, const char *dest_path);
    static void copyFile(int src_dirfd, const char *src_path, int dest_dirfd,
                         const char *dest_path, mode_t mode = 0,
                         struct stat *srcStat = nullptr,
                         struct stat *destStat = nullptr);

    /**
     * Clone file from `srcFd` to `dstFd` via `copy_file_range` syscall.
     * It returns errno on failure, and returns `ENOSYS` if the OS or
     * kernel does not support the `copy_file_range`.
     */
    static int cloneFile(int srcFd, int dstFd);

    /**
     * Recursively copy the contents of a directory (non-atomically) at the
     * given path into the provided target directory file descriptor.
     */
    static void copyRecursively(int srcfd, int destfd);

    /**
     * Write a file atomically. Note that this only guarantees thread safety
     * if the actual contents to be written to `path` are the same across
     * threads. If the file already exists, it will be replaced atomically.
     *
     * (To guarantee atomicity, create a temporary file, write the data to it
     * and rename the temporary file to the final path.)
     *
     * `mode` allows setting the permissions for the created file; by default
     * 0600 (the default used by `mkstemp()`).
     *
     * If `intermediate_directory` is specified, the temporary file is created
     * in that location. It must be contained in the same filesystem than the
     * output `path` in order for `rename(2)` to work.
     *
     * If `mtime` is specified, the mtime of the file is updated to that value
     *
     * On errors during writing the data or renaming, throw an
     * `std::system_error` exception.
     */
    static void writeFileAtomically(
        const std::string &path, const std::string &data,
        mode_t mode = PERMISSION_RWUSR,
        const std::string &intermediate_directory = "",
        const std::string &prefix = TempDefaults::DEFAULT_TMP_PREFIX,
        const std::optional<std::chrono::time_point<std::chrono::system_clock>>
            mtime = std::nullopt);

    /**
     * Write a digest to a file in the format "hash/size_bytes".
     */
    static void writeDigestToFile(const Digest &digest,
                                  const std::string &path);

    /**
     * Traverse and apply functions on files and directories recursively.
     *
     * If apply_to_root is true, dir_func is applied to the directory stream
     * the function is initally called with.
     *
     * If pass_parent_fd is true, the parent directory of dir will be passed
     * into dir_func instead of dir. This is useful in the case of deletion.
     */
    typedef std::function<void(const char *path, int fd)>
        DirectoryTraversalFnPtr;

    static void fileDescriptorTraverseAndApply(
        DirentWrapper *dir, const DirectoryTraversalFnPtr &dir_func = nullptr,
        const DirectoryTraversalFnPtr &file_func = nullptr,
        bool apply_to_root = false, bool pass_parent_fd = false);

    /**
     * Open a file or directory within the specified directory.
     *
     * This is equivalent to `openat()` except that it will fail with EXDEV
     * when encountering absolute paths, absolute symlinks or a `..` component
     * that would escape the specified directory.
     *
     * To match the interface of `openat()`, this function returns -1 and sets
     * `errno` to indicate an error.
     */
    static int openBeneath(int dirfd, const std::string &path, int flags,
                           bool createDirectories = false);
    static int openBeneath(const std::string &dir, const std::string &path,
                           int flags, bool createDirectories = false);

    /**
     * Open a file or directory within the specified directory.
     *
     * This is equivalent to `openat()` except that the specified directory is
     * treated as the root directory while resolving the specified path, as if
     * the calling process had used `chroot()`. Both absolute and relative
     * paths incl. absolute symlinks are resolved relative to the specified
     * root directory.
     *
     * To match the interface of `openat()`, this function returns -1 and sets
     * `errno` to indicate an error.
     */
    static int openInRoot(int dirfd, const std::string &path, int flags,
                          bool createDirectories = false);
    static int openInRoot(const std::string &dir, const std::string &path,
                          int flags, bool createDirectories = false);

    /**
     * Return the stat of the file at the given open file descriptor.
     */
    static struct stat getFileStat(const int fd);

    /**
     * Return the stat of the file at the given path.
     */
    static struct stat getFileStat(const char *path);
    static struct stat getFileStat(int dirfd, const char *path);

  private:
    /**
     * Return a time point in seconds representing the st_mtim of the filestat.
     */
    static std::chrono::system_clock::time_point
    getMtimeTimepoint(struct stat &result);

    /**
     * Deletes the contents of an existing directory.
     * `delete_parent_directory` allows specifying whether the top-level
     * directory in `path` is to be deleted.
     */
    static void deleteRecursively(int dirfd, const char *path,
                                  const bool delete_parent_directory);

    /**
     * Given a path, create the directory including its parents if necessary.
     * Silently ignore existing directories.
     * PRE: `path` must be normalized.
     */
    static void createDirectoriesInPath(int dirfd, const std::string &path,
                                        const mode_t mode);

    static int openInternal(int dirfd, const std::string &path, int flags,
                            bool dir_is_root, bool createDirectories);

#if defined(__linux__)
    static int openat2(int dirfd, const char *path, uint64_t flags,
                       mode_t mode, uint64_t resolve);
    static constexpr uint64_t OPENAT2_RESOLVE_BENEATH = 0x08;
    static constexpr uint64_t OPENAT2_RESOLVE_IN_ROOT = 0x10;
#endif

    /* Allow access to openat2 */
    friend class PlatformInfo;
};

/**
 * RAII helper class for file descriptors. Automatically closes the fd as it
 * goes out of scope (unless disabled). Is movable but not copyable.
 */
class FileDescriptor final {
  public:
    FileDescriptor() noexcept : d_fd(-1), d_close(false) {}
    explicit FileDescriptor(int fd, bool close = true) noexcept
        : d_fd(fd), d_close(close)
    {
    }
    ~FileDescriptor()
    {
        if (this->d_fd >= 0 && this->d_close) {
            if (close(this->d_fd) != 0) {
                BUILDBOX_LOG_WARNING("Failed to close fd "
                                     << d_fd
                                     << ", errno: " << std::strerror(errno));
            }
        }
    }

    FileDescriptor(FileDescriptor &&other) noexcept
        : d_fd(other.d_fd), d_close(other.d_close)
    {
        other.d_fd = -1;
    }
    FileDescriptor &operator=(FileDescriptor &&other) noexcept
    {
        if (this != &other) {
            this->~FileDescriptor();
            this->d_fd = other.d_fd;
            this->d_close = other.d_close;
            other.d_fd = -1;
        }
        return *this;
    }
    // Delete copy constructor and copy assignment operator
    FileDescriptor(const FileDescriptor &) = delete;
    FileDescriptor &operator=(const FileDescriptor &) = delete;

    int get() const { return d_fd; }

  private:
    int d_fd;
    bool d_close;
};

/**
 * RAII helper class for lock files. Automatically closes and deletes the lock
 * file as it goes out of scope. Is movable but not copyable.
 */
class LockFile final {
  public:
    LockFile(int dirfd, std::string pathname);
    ~LockFile();

  private:
    FileDescriptor d_dirfd;
    std::string d_pathname;
    FileDescriptor d_lockfd;
};

} // namespace buildboxcommon

#endif
