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

#define FUSE_USE_VERSION 30 // NOLINT (cppcoreguidelines-macro-usage)

#include <assert.h>
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>

// Older versions of glibc required the renameat2 constants
// to be included from <linux/fs.h>
#ifndef RENAME_EXCHANGE
#include <linux/fs.h>
#endif

// Allow building against ancient Linux kernel headers
// (older than Linux 3.15)
#ifndef RENAME_EXCHANGE
#define RENAME_EXCHANGE (1 << 1) /* Exchange source and dest */
#endif
#ifndef RENAME_NOREPLACE
#define RENAME_NOREPLACE (1 << 0) /* Don't overwrite target */
#endif

#include <cstddef>
#include <deque>
#include <fstream>
#include <iostream>
#include <span>
#include <string>

#include <fuse_lowlevel.h>

#include <google/protobuf/util/time_util.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_notify.h>
#include <buildboxcommon_permissions.h>
#include <buildboxcommon_temporarydirectory.h>
#include <buildboxcommon_timeutils.h>
#include <buildboxcommon_version.h>

#include <buildbox.h>

constexpr uint32_t MAX_OPEN_INODES = 512, ENTRY_TIMEOUT = (24 * 3600);
constexpr const char XATTR_CHECKSUM[] = "user.checksum.sha256";
constexpr const char NODE_PROPERTY_SUBTREE_READONLY[] = "SubtreeReadOnly";

// A few Linux constants for consistency
// Emulate a device with 4kB logical blocks matching common storage devices.
constexpr uint32_t LOGICAL_BLOCKSIZE = 4096;
// On Linux stat.st_blocks is the number of 512-byte blocks.
constexpr uint32_t ST_NBLOCKSIZE = 512;
// On Linux NAME_MAX is usually 255
constexpr uint32_t BUILDBOX_NAME_MAX = 255;

template <typename T> constexpr T div_round_up(T n, T d)
{
    return (n + d - 1) / d;
}

// Error codes we return
// This is the general failure code
constexpr uint32_t
    GENERAL_FAILURE = 1,
    // This is used when something todo with fuse caused the failure
    FUSE_FAILURE = 2;

using namespace google::protobuf::util;
using namespace google::protobuf;
using namespace buildboxcommon;

namespace {

const google::protobuf::Timestamp make_timestamp(const struct timespec &ts)
{
    const std::chrono::system_clock::time_point timepoint =
        std::chrono::system_clock::from_time_t(ts.tv_sec) +
        std::chrono::microseconds{ts.tv_nsec / 1000};
    return TimeUtils::make_timestamp(timepoint);
}

} // namespace

class CasDirectory;

class CasInode {
  public:
    CasInode(CasDirectory *parent);
    virtual ~CasInode() {}
    // Delete copy constructor
    CasInode(const CasInode &) = delete;

    // Delete copy assignment operator
    CasInode &operator=(const CasInode &) = delete;

    // Delete move constructor
    CasInode(CasInode &&) = delete;

    // Delete move assignment operator
    CasInode &operator=(CasInode &&) = delete;

    virtual void parse() {}
    virtual void flush();
    void update_timestamps();
    void update_mtime();
    void update_atime();
    void update_ctime();
    void set_modified();
    bool is_modified();

    static CasInode *from_fuse(fuse_ino_t inode_id);

    /* Only set for parent-child links in the input tree
     * as it's not needed for newly created links and can't
     * handle multiple hardlinks. */
    // NOLINTBEGIN (cppcoreguidelines-non-private-member-variables-in-classes)
    CasDirectory *parent;

    struct stat attr;
    Digest digest;
    bool readonly;
    bool mtime_set;

  protected:
    bool modified;        /* modified/created */
    std::string temppath; /* only for modified/created files */
    // NOLINTEND (cppcoreguidelines-non-private-member-variables-in-classes)
};

struct CasDirectoryEntry {
    CasDirectoryEntry(const std::string &name_) : name(name_) {}
    std::string name;
    CasInode *inode = nullptr;
};

class CasDirectory : public CasInode {
  public:
    CasDirectory(CasDirectory *parent);
    CasDirectory(CasDirectory *parent, const Digest &digest);
    ~CasDirectory() override {}
    // Delete copy constructor
    CasDirectory(const CasDirectory &) = delete;

    // Delete copy assignment operator
    CasDirectory &operator=(const CasDirectory &) = delete;

    // Delete move constructor
    CasDirectory(CasDirectory &&) = delete;

    // Delete move assignment operator
    CasDirectory &operator=(CasDirectory &&) = delete;

    void parse() override;
    void flush() override;

    CasDirectoryEntry *get_dentry(const std::string &name);
    CasDirectoryEntry *get_or_create_dentry(const std::string &name);
    CasInode *lookup(const std::string &name);
    void set_dentry(const std::string &name, CasInode *inode);

    void set_size();

    static CasDirectory *open_from_fuse(fuse_ino_t inode_id);
    // NOLINTBEGIN (cppcoreguidelines-non-private-member-variables-in-classes)
    std::vector<CasDirectoryEntry> dentries;
    std::unordered_map<std::string, uint32_t> dentry_map;
    // NOLINTEND (cppcoreguidelines-non-private-member-variables-in-classes)

  private:
    bool parsed;
};

class CasFile : public CasInode {
  public:
    CasFile();
    CasFile(CasDirectory *parent, const Digest &digest, bool is_executable,
            struct timespec *mtime, mode_t *mode);
    ~CasFile() override {}
    // Delete copy constructor
    CasFile(const CasFile &) = delete;

    // Delete copy assignment operator
    CasFile &operator=(const CasFile &) = delete;

    // Delete move constructor
    CasFile(CasFile &&) = delete;

    // Delete move assignment operator
    CasFile &operator=(CasFile &&) = delete;

    void set_size(off_t size);
    void copy_on_write();
    void close();

    static CasFile *open_from_fuse(fuse_ino_t inode_id);

    int fd;
};

class CasSymlink : public CasInode {
  public:
    CasSymlink(const std::string &target);
    CasSymlink(CasDirectory *parent, const std::string &target);
    ~CasSymlink() override {}
    // Delete copy constructor
    CasSymlink(const CasSymlink &) = delete;

    // Delete copy assignment operator
    CasSymlink &operator=(const CasSymlink &) = delete;

    // Delete move constructor
    CasSymlink(CasSymlink &&) = delete;

    // Delete move assignment operator
    CasSymlink &operator=(CasSymlink &&) = delete;

    static CasSymlink *open_from_fuse(fuse_ino_t inode_id);

    std::string target;
};

struct fs_opts {
    const char *local_path;
    const char *input_digest_path;
    const char *input_digest_str;
    const char *output_digest;
    const char *mountpoint;

    const char *remote_url;
    const char *server_cert;
    const char *client_key;
    const char *client_cert;
    const char *output_times;
    int prefetch;

    bool capture_mtime;
    bool capture_mode;
};

class CasFS {
  public:
    fuse_ino_t add_inode(CasInode *inode);

    int mktemp(std::string &temppath);
    int fd_from_digest(const Digest &digest);

    void batch_download_complete();
    void prefetch_batch();
    void prefetch();

    struct fs_opts opts{};
    struct timespec default_timespec{};
    int local_dfd = 0;
    std::unique_ptr<TemporaryDirectory> temp_dir;
    CasDirectory *root_inode = NULL;

    std::vector<CasInode *> inodes;

    std::deque<CasFile *> open_files;

    uid_t uid = 0;
    gid_t gid = 0;

    bool no_opendir = false;

    struct fuse_session *fuse = nullptr;

    Client client;

    std::deque<Digest> prefetch_deque;
    std::deque<Digest> prefetch_next_deque;

    ExecutedActionMetadata executed_metadata;
};

static CasFS fs;

static std::string objdir(const Digest &digest)
{
    std::string path("objects/");
    path.append(digest.hash().substr(0, 2));
    return path;
}

static std::string objpath(const Digest &digest)
{
    std::string path(objdir(digest));
    path.append("/");
    path.append(digest.hash().substr(2));
    return path;
}

fuse_ino_t CasFS::add_inode(CasInode *inode)
{
    this->inodes.push_back(inode);

    /* Return 1-based inode number */
    return this->inodes.size();
}

int CasFS::mktemp(std::string &temppath)
{
    /* work around missing mkstempat() */
    temppath = fs.temp_dir->strname() + "/XXXXXX";
    int fd = mkstemp(&temppath[0]);
    if (fd < 0) {
        throw std::system_error(errno, std::generic_category());
    }
    return fd;
}

int CasFS::fd_from_digest(const Digest &digest)
{
    int fd =
        openat(this->local_dfd, objpath(digest).c_str(), O_RDONLY | O_CLOEXEC);
    if (fd >= 0) {
        return fd;
    }

    if (errno != ENOENT) {
        /* I/O error */
        throw std::system_error(errno, std::generic_category());
    }

    /* File not in local cache, download it from the CAS server */

    if (!this->opts.remote_url) {
        throw std::runtime_error("No CAS server configured");
    }

    /* Get time and set start if not set */
    Timestamp start_time = buildboxcommon::TimeUtils::now();
    if (!fs.executed_metadata.has_input_fetch_start_timestamp()) {
        fs.executed_metadata.mutable_input_fetch_start_timestamp()->CopyFrom(
            start_time);
    }

    /* Download it to temporary location */
    std::string temppath;
    fd = this->mktemp(temppath);
    try {
        this->client.download(fd, digest);

        /* Move it to final location */
        mkdirat(this->local_dfd, objdir(digest).c_str(),
                PERMISSION_RWXUSR_RXALL);
        if (renameat(AT_FDCWD, temppath.c_str(), this->local_dfd,
                     objpath(digest).c_str()) < 0) {
            throw std::system_error(errno, std::generic_category());
        }
    }
    catch (const std::exception &e) {
        unlink(temppath.c_str());
        ::close(fd);
        throw;
    }

    /* Set end time */
    Timestamp end_time = buildboxcommon::TimeUtils::now();
    fs.executed_metadata.mutable_input_fetch_completed_timestamp()->CopyFrom(
        end_time);

    lseek(fd, 0, SEEK_SET);
    return fd;
}

void CasFS::batch_download_complete()
{
    const Digest *digest = nullptr;
    const std::string *data = nullptr;

    while (this->client.batch_download_next(&digest, &data)) {
        if (digest->size_bytes() != (int64_t)data->size()) {
            throw std::runtime_error(
                "Download failed: digest and data size mismatch");
        }

        std::string temppath;
        int fd = this->mktemp(temppath);
        try {
            ssize_t ret = write(fd, data->c_str(), data->size());
            if (ret < 0) {
                throw std::system_error(errno, std::generic_category());
            }
            else if (ret != digest->size_bytes()) {
                throw std::runtime_error("Download failed: short write");
            }

            /* Move it to final location */
            mkdirat(this->local_dfd, objdir(*digest).c_str(),
                    PERMISSION_RWXUSR_RXALL);
            if (renameat(AT_FDCWD, temppath.c_str(), this->local_dfd,
                         objpath(*digest).c_str()) < 0) {
                throw std::system_error(errno, std::generic_category());
            }
        }
        catch (const std::exception &e) {
            ::close(fd);
            unlink(temppath.c_str());
            throw;
        }
    }
}

void CasFS::prefetch_batch()
{
    this->batch_download_complete();

    /* All previously scheduled directories are now locally available,
     * move them to the processing queue. */
    while (fs.prefetch_next_deque.size() > 0) {
        fs.prefetch_deque.push_back(fs.prefetch_next_deque.front());
        fs.prefetch_next_deque.pop_front();
    }
}

void CasFS::prefetch()
{
    fs.prefetch_deque.push_back(fs.root_inode->digest);

    while (fs.prefetch_deque.size() + fs.prefetch_next_deque.size() > 0) {
        if (fs.prefetch_deque.size() == 0) {
            this->prefetch_batch();
        }

        Digest digest = fs.prefetch_deque.front();
        fs.prefetch_deque.pop_front();

        struct stat st{};

        Directory directory;
        {
            int fd = fs.fd_from_digest(digest);
            if (!directory.ParseFromFileDescriptor(fd)) {
                ::close(fd);
                throw std::runtime_error("Failed to parse directory object");
            }
            ::close(fd);
        }

        for (int i = 0; i < directory.directories_size(); i++) {
            auto child_digest = directory.directories(i).digest();
            if (fstatat(this->local_dfd, objpath(child_digest).c_str(), &st,
                        0) == 0) {
                /* Skip download, already in local cache.
                 * Add directory to processing queue. */
                fs.prefetch_deque.push_back(child_digest);
                continue;
            }
            if (child_digest.size_bytes() >=
                fs.client.get_max_batch_total_size_bytes()) {
                /* Too large for batch request, download in independent
                 * request. */
                ::close(fs.fd_from_digest(child_digest));
                /* Add directory to processing queue. */
                fs.prefetch_deque.push_back(child_digest);
            }
            else {
                if (!fs.client.batch_download_add(child_digest)) {
                    /* Not enough space left in batch request.
                     * Complete pending batch first. */
                    this->prefetch_batch();
                    fs.client.batch_download_add(child_digest);
                }
                /* Directory will be available after completing pending batch.
                 * Add directory to deferred processing queue. */
                fs.prefetch_next_deque.push_back(child_digest);
            }
        }

        for (int i = 0; i < directory.files_size(); i++) {
            auto child_digest = directory.files(i).digest();
            if (fstatat(this->local_dfd, objpath(child_digest).c_str(), &st,
                        0) == 0) {
                /* Skip download, already in local cache. */
                continue;
            }
            if (child_digest.size_bytes() >=
                fs.client.get_max_batch_total_size_bytes()) {
                /* Too large for batch request, download in independent
                 * request. */
                ::close(fs.fd_from_digest(child_digest));
            }
            else {
                if (!fs.client.batch_download_add(child_digest)) {
                    /* Not enough space left in batch request.
                     * Complete pending batch first. */
                    this->prefetch_batch();
                    fs.client.batch_download_add(child_digest);
                }
            }
        }
    }

    /* Fetch final batch. */
    this->prefetch_batch();
}

CasInode *CasInode::from_fuse(fuse_ino_t inode_id)
{
    /* Inode number is 1-based */
    CasInode *inode = fs.inodes[inode_id - 1];
    inode->parse();
    return inode;
}

CasInode::CasInode(CasDirectory *parent) : parent(parent), attr()
{

    this->attr.st_ino = fs.add_inode(this);
    this->attr.st_blksize = LOGICAL_BLOCKSIZE;

    this->attr.st_uid = fs.uid;
    this->attr.st_gid = fs.gid;
    this->attr.st_nlink = 1;

    this->attr.st_atim = fs.default_timespec;
    this->attr.st_mtim = fs.default_timespec;
    this->attr.st_ctim = fs.default_timespec;

    if (parent) {
        this->readonly = parent->readonly;
    }
    else {
        this->readonly = false;
    }

    this->modified = false;
    this->mtime_set = false;
}

CasDirectory::CasDirectory(CasDirectory *parent)
    : CasInode(parent), parsed(true)
{
    /* empty directory */

    this->attr.st_mode = S_IFDIR | PERMISSION_RWXUSR_RXALL;

    this->set_dentry(".", this);
    this->set_dentry("..", parent ? parent : this);

    this->update_timestamps();
    this->set_modified();
}

CasDirectory::CasDirectory(CasDirectory *parent, const Digest &digest)
    : CasInode(parent), parsed(false)
{
    this->attr.st_mode = S_IFDIR | PERMISSION_RWXUSR_RXALL;

    this->set_dentry(".", this);
    this->set_dentry("..", parent ? parent : this);

    this->digest = digest;
}

CasDirectoryEntry *CasDirectory::get_dentry(const std::string &name)
{
    uint32_t index = 0;
    auto it = this->dentry_map.find(name);
    if (it == this->dentry_map.end()) {
        return nullptr;
    }

    index = it->second;
    return &this->dentries[index];
}

CasDirectoryEntry *CasDirectory::get_or_create_dentry(const std::string &name)
{
    uint32_t index = 0;
    auto it = this->dentry_map.find(name);
    if (it != this->dentry_map.end()) {
        index = it->second;
    }
    else {
        this->dentries.emplace_back(name);
        index = this->dentries.size() - 1;
        this->dentry_map[name] = index;
        this->set_size();
    }
    return &this->dentries[index];
}

CasInode *CasDirectory::lookup(const std::string &name)
{
    CasDirectoryEntry *dentry = this->get_dentry(name);
    if (dentry == nullptr) {
        return nullptr;
    }

    if (dentry->inode) {
        dentry->inode->parse();
    }

    return dentry->inode;
}

void CasDirectory::set_dentry(const std::string &name, CasInode *inode)
{
    CasDirectoryEntry *dentry = this->get_or_create_dentry(name);
    dentry->inode = inode;
}

CasFile::CasFile() : CasInode(nullptr)
{
    /* empty file */

    this->attr.st_mode = S_IFREG | PERMISSION_RWUSR_RALL;

    int fd = fs.mktemp(this->temppath);
    if (fd < 0) {
        throw std::system_error(errno, std::generic_category());
    }
    ::close(fd);

    this->fd = -1;
    this->update_timestamps();
    this->set_modified();
}

CasFile::CasFile(CasDirectory *parent, const Digest &digest,
                 bool is_executable, struct timespec *mtime, mode_t *mode)
    : CasInode(parent)
{
    this->attr.st_mode = S_IFREG | PERMISSION_RWUSR_RALL;
    this->set_size(digest.size_bytes());

    if (is_executable) {
        this->attr.st_mode |= PERMISSION_XALL;
    }

    if (mtime) {
        this->attr.st_mtim = *mtime;
        this->mtime_set = true;
    }
    if (mode) {
        // Override file mode
        this->attr.st_mode = S_IFREG | *mode;
    }

    this->digest = digest;
    this->fd = -1;
}

CasSymlink::CasSymlink(const std::string &target)
    : CasInode(nullptr), target(target)
{
    this->attr.st_mode = S_IFLNK | PERMISSION_RWXALL;
    this->attr.st_size = static_cast<off_t>(target.size());

    this->set_modified();
}

CasSymlink::CasSymlink(CasDirectory *parent, const std::string &target)
    : CasInode(parent), target(target)
{
    this->attr.st_mode = S_IFLNK | PERMISSION_RWXALL;
    this->attr.st_size = static_cast<off_t>(target.size());
}

CasDirectory *CasDirectory::open_from_fuse(fuse_ino_t inode_id)
{
    CasInode *inode = CasInode::from_fuse(inode_id);
    if (!S_ISDIR(inode->attr.st_mode)) {
        throw std::runtime_error("Inode is not a directory");
    }

    CasDirectory *dir = dynamic_cast<CasDirectory *>(inode);
    if (dir == nullptr) {
        throw std::runtime_error("Failed to get directory from inode");
    }
    return dir;
}

void CasDirectory::parse()
{
    if (this->parsed) {
        return;
    }

    /* Populate directory entries from CAS directory object */

    int fd = fs.fd_from_digest(this->digest);

    Directory directory;
    if (!directory.ParseFromFileDescriptor(fd)) {
        ::close(fd);
        throw std::runtime_error("Failed to parse directory object");
    }
    ::close(fd);

    auto dir_node_properties = directory.node_properties();

    /* Parse optional mtime */
    if (dir_node_properties.has_mtime()) {
        auto timepoint =
            TimeUtils::parse_timestamp(dir_node_properties.mtime());
        this->attr.st_mtim = TimeUtils::make_timespec(timepoint);
        this->mtime_set = true;
    }
    /* Parse optional directory mode */
    if (dir_node_properties.has_unix_mode()) {
        mode_t mode = dir_node_properties.unix_mode().value();
        if (mode & ~PERMISSION_RWXALL_SPECIAL) {
            throw std::runtime_error("Invalid unix mode for directory node");
        }
        this->attr.st_mode = S_IFDIR | mode;
    }

    for (const auto &property : dir_node_properties.properties()) {
        if (property.name() == NODE_PROPERTY_SUBTREE_READONLY) {
            this->readonly = property.value() == "true";
        }
    }

    for (int i = 0; i < directory.files_size(); i++) {
        const FileNode &file_node = directory.files(i);

        struct timespec mtime{};
        struct timespec *pmtime = nullptr;
        mode_t mode = 0;
        mode_t *pmode = nullptr;

        const auto &node_properties = file_node.node_properties();

        /* Parse optional mtime */
        if (node_properties.has_mtime()) {
            auto timepoint =
                TimeUtils::parse_timestamp(node_properties.mtime());
            mtime = TimeUtils::make_timespec(timepoint);
            pmtime = &mtime;
        }
        /* Parse optional file mode */
        if (node_properties.has_unix_mode()) {
            mode = node_properties.unix_mode().value();
            if (mode & ~PERMISSION_RWXALL_SPECIAL) {
                throw std::runtime_error("Invalid unix mode for file node");
            }
            pmode = &mode;
        }
        // NOLINTBEGIN (cppcoreguidelines-owning-memory)
        this->set_dentry(file_node.name(),
                         new CasFile(this, file_node.digest(),
                                     file_node.is_executable(), pmtime,
                                     pmode));
    }

    for (int i = 0; i < directory.directories_size(); i++) {
        const DirectoryNode &directory_node = directory.directories(i);
        this->set_dentry(directory_node.name(),
                         new CasDirectory(this, directory_node.digest()));
    }

    for (int i = 0; i < directory.symlinks_size(); i++) {
        const SymlinkNode &symlink_node = directory.symlinks(i);
        this->set_dentry(symlink_node.name(),
                         new CasSymlink(this, symlink_node.target()));
    }
    // NOLINTEND (cppcoreguidelines-owning-memory)

    this->parsed = true;
}

void CasFile::set_size(off_t size)
{
    this->attr.st_size = size;
    blkcnt_t logical_blocks =
        div_round_up<long>(this->attr.st_size, LOGICAL_BLOCKSIZE);
    this->attr.st_blocks =
        logical_blocks * (LOGICAL_BLOCKSIZE / ST_NBLOCKSIZE);
}

CasFile *CasFile::open_from_fuse(fuse_ino_t inode_id)
{
    CasInode *inode = CasInode::from_fuse(inode_id);
    if (!S_ISREG(inode->attr.st_mode)) {
        throw std::runtime_error("Inode is not a regular file");
    }

    CasFile *file = dynamic_cast<CasFile *>(inode);
    if (file == nullptr) {
        throw std::runtime_error("Failed to get cas file from inode");
    }

    if (file->fd >= 0) {
        /* already open */
        return file;
    }

    if (fs.open_files.size() >= MAX_OPEN_INODES) {
        CasFile *open_file = fs.open_files.back();
        open_file->close();
        fs.open_files.pop_back();
    }

    if (file->modified) {
        file->fd = open(file->temppath.c_str(), O_RDWR | O_CLOEXEC);
        if (file->fd < 0) {
            throw std::system_error(errno, std::generic_category());
        }
    }
    else {
        file->fd = fs.fd_from_digest(file->digest);
    }

    fs.open_files.push_front(file);

    return file;
}

void CasFile::close()
{
    assert(this->fd >= 0);

    ::close(this->fd);
    this->fd = -1;
}

void CasInode::set_modified()
{
    if (!this->modified) {
        if (this->parent) {
            this->parent->set_modified();
        }
        this->modified = true;
    }
}

bool CasInode::is_modified() { return this->modified; }

void CasFile::copy_on_write()
{
    if (this->modified) {
        /* new file or already copied */
        return;
    }

    std::string temppath;
    int fd = fs.mktemp(temppath);

    std::vector<char> buffer(BUFFER_SIZE);
    ssize_t count = 0;
    for (off_t offset = 0; offset < this->attr.st_size; offset += count) {
        count = pread(this->fd, &buffer[0], buffer.size(), offset);
        if (count < 0 || write(fd, &buffer[0], count) < 0) {
            ::close(fd);
            throw std::system_error(errno, std::generic_category());
        }
    }

    ::close(this->fd);
    this->fd = fd;

    this->temppath = temppath;

    this->set_modified();
}

void CasInode::update_timestamps()
{
    struct timespec ts{};
    if (clock_gettime(CLOCK_REALTIME, &ts) < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(std::system_error, errno,
                                              std::system_category,
                                              "Failed to get current time");
    }

    this->attr.st_mtim = ts;
    this->attr.st_atim = ts;
    this->attr.st_ctim = ts;
    this->mtime_set = true;
}

void CasInode::update_mtime()
{
    struct timespec ts{};
    if (clock_gettime(CLOCK_REALTIME, &ts) < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(std::system_error, errno,
                                              std::system_category,
                                              "Failed to get current time");
    }

    this->attr.st_mtim = ts;
    this->attr.st_ctim = ts;
    this->mtime_set = true;
}

void CasInode::update_atime()
{
    struct timespec ts{};
    if (clock_gettime(CLOCK_REALTIME, &ts) < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(std::system_error, errno,
                                              std::system_category,
                                              "Failed to get current time");
    }

    this->attr.st_atim = ts;
}

void CasInode::update_ctime()
{
    struct timespec ts{};
    if (clock_gettime(CLOCK_REALTIME, &ts) < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(std::system_error, errno,
                                              std::system_category,
                                              "Failed to get current time");
    }

    this->attr.st_ctim = ts;
}

void CasDirectory::set_size()
{
    /* Report an arbitrary but plausible directory size to improve application
     * compatibility. */
    constexpr uint32_t DENTRY_SIZE = 256;

    this->attr.st_size =
        static_cast<off_t>(this->dentry_map.size()) * DENTRY_SIZE;
    blkcnt_t logical_blocks =
        div_round_up<long>(this->attr.st_size, LOGICAL_BLOCKSIZE);
    this->attr.st_blocks =
        logical_blocks * (LOGICAL_BLOCKSIZE / ST_NBLOCKSIZE);
}

void CasDirectory::flush()
{
    if (!this->modified) {
        return;
    }

    Directory directory;

    if (fs.opts.capture_mtime && this->mtime_set) {
        auto node_properties = directory.mutable_node_properties();
        node_properties->mutable_mtime()->CopyFrom(
            make_timestamp(this->attr.st_mtim));
    }

    if (fs.opts.capture_mode) {
        auto node_properties = directory.mutable_node_properties();
        node_properties->mutable_unix_mode()->set_value(
            this->attr.st_mode & PERMISSION_RWXALL_SPECIAL);
    }

    for (auto it = this->dentries.begin(); it != this->dentries.end(); it++) {
        if (it->name == "." || it->name == "..") {
            continue;
        }

        CasInode *child_inode = it->inode;
        if (child_inode) {
            child_inode->flush();

            if (S_ISDIR(child_inode->attr.st_mode)) {
                auto subdir_node = directory.add_directories();
                subdir_node->set_name(it->name);
                subdir_node->mutable_digest()->CopyFrom(child_inode->digest);
            }
            else if (S_ISREG(child_inode->attr.st_mode)) {
                auto file_node = directory.add_files();
                file_node->set_name(it->name);
                file_node->mutable_digest()->CopyFrom(child_inode->digest);
                file_node->set_is_executable(
                    (child_inode->attr.st_mode & PERMISSION_XUSR) != 0);

                if (fs.opts.capture_mtime && child_inode->mtime_set) {
                    auto node_properties =
                        file_node->mutable_node_properties();
                    node_properties->mutable_mtime()->CopyFrom(
                        make_timestamp(child_inode->attr.st_mtim));
                }

                if (fs.opts.capture_mode) {
                    auto node_properties =
                        file_node->mutable_node_properties();
                    node_properties->mutable_unix_mode()->set_value(
                        child_inode->attr.st_mode & PERMISSION_RWXALL_SPECIAL);
                }
            }
            else if (S_ISLNK(child_inode->attr.st_mode)) {
                CasSymlink *symlink = dynamic_cast<CasSymlink *>(child_inode);
                if (symlink == nullptr) {
                    throw std::runtime_error(
                        "Failed to get symlink from inode");
                }
                auto symlink_node = directory.add_symlinks();
                symlink_node->set_name(it->name);
                symlink_node->set_target(symlink->target);
            }
            else if (S_ISFIFO(child_inode->attr.st_mode) ||
                     S_ISSOCK(child_inode->attr.st_mode)) {
                /* ignore FIFOs and sockets */
            }
            else {
                assert(0);
            }
        }
    }

    std::string temppath;
    int fd = fs.mktemp(temppath);
    if (!directory.SerializeToFileDescriptor(fd)) {
        ::close(fd);
        throw std::runtime_error("Failed to serialize directory");
    }
    ::close(fd);

    this->temppath = temppath;

    CasInode::flush();
}

void CasInode::flush()
{
    if (!this->modified) {
        return;
    }

    if (S_ISREG(this->attr.st_mode) || S_ISDIR(this->attr.st_mode)) {
        /* directory or regular file */

        /* Checksum file */

        int fd = open(this->temppath.c_str(), O_RDONLY | O_CLOEXEC);
        if (fd < 0) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Failed to open " << this->temppath);
        }

        auto digestContext = DigestGenerator::createDigestContext();

        std::vector<char> buffer(BUFFER_SIZE);

        ssize_t bytes_read = 0;
        while ((bytes_read = read(fd, &buffer[0], buffer.size())) > 0) {
            digestContext.update(&buffer[0], bytes_read);
        }

        this->digest = digestContext.finalizeDigest();

        /* Move it to the right place */

        mkdirat(fs.local_dfd, objdir(this->digest).c_str(),
                PERMISSION_RWXUSR_RXALL);
        if (renameat(AT_FDCWD, this->temppath.c_str(), fs.local_dfd,
                     objpath(this->digest).c_str()) < 0) {
            ::close(fd);
            throw std::system_error(errno, std::generic_category());
        }

        try {
            /* Upload to CAS server */

            if (fs.opts.remote_url) {
                int digest_size = static_cast<int>(this->digest.size_bytes());
                if (digest_size > BUFFER_SIZE) {
                    fs.client.upload(fd, this->digest);
                }
                else {
                    if (!fs.client.batch_upload_add(this->digest, buffer)) {
                        // Not enough space, create new batch
                        fs.client.batch_upload();
                        fs.client.batch_upload_add(this->digest, buffer);
                    }
                }
            }

            this->modified = false;
        }
        catch (const std::exception &e) {
            ::close(fd);
            throw;
        }

        ::close(fd);
    }
}

static void cas_fuse_init(void *userdata, struct fuse_conn_info *conn)
{
    /* Enable optimization: handle file open requests in the kernel.
     * Unconditionally set this as it's currently required.
     * libfuse will verify that the kernel supports it. */
    conn->want |= FUSE_CAP_NO_OPEN_SUPPORT;
    /* Atomic open and truncate is incompatible with FUSE_CAP_NO_OPEN_SUPPORT
     */
    conn->want &= ~FUSE_CAP_ATOMIC_O_TRUNC;

    /* Enable optimization: handle opendir requests in the kernel,
     * if possible */
    if (conn->capable & FUSE_CAP_NO_OPENDIR_SUPPORT) {
        fs.no_opendir = true;
        conn->want |= FUSE_CAP_NO_OPENDIR_SUPPORT;
    }

    /* Enable optimization: move data in kernelspace, if possible */
    if (conn->capable & FUSE_CAP_SPLICE_MOVE) {
        conn->want |= FUSE_CAP_SPLICE_MOVE;
    }
    if (conn->capable & FUSE_CAP_SPLICE_WRITE) {
        conn->want |= FUSE_CAP_SPLICE_WRITE;
    }
}

static int cas_fuse_do_lookup(fuse_req_t req, fuse_ino_t parent_id,
                              const char *name, struct fuse_entry_param *e)
{
    if (strlen(name) > BUILDBOX_NAME_MAX) {
        return -ENAMETOOLONG;
    }

    try {
        CasDirectory *parent = CasDirectory::open_from_fuse(parent_id);

        /* Set high timeout as no changes can happen to the filesystem
         * behind the kernel's back. */
        e->attr_timeout = ENTRY_TIMEOUT;
        e->entry_timeout = ENTRY_TIMEOUT;

        CasInode *inode = parent->lookup(name);
        if (inode == nullptr) {
            /* No matching directory entry
             * Returning entry with inode 0 instead of ENOENT
             * enables negative dentry caching. */
            return 0;
        }

        memcpy(&e->attr, &inode->attr, sizeof(e->attr));
        e->ino = inode->attr.st_ino;

        return 0;
    }
    catch (const std::exception &e) {
        return -EIO;
    }
}

static void cas_fuse_lookup(fuse_req_t req, fuse_ino_t parent_id,
                            const char *name)
{
    struct fuse_entry_param e = {0};

    int ret = cas_fuse_do_lookup(req, parent_id, name, &e);

    if (ret == 0) {
        fuse_reply_entry(req, &e);
    }
    else {
        fuse_reply_err(req, -ret);
    }
}

static void cas_fuse_getattr(fuse_req_t req, fuse_ino_t ino,
                             struct fuse_file_info *fi)
{
    CasInode *inode = CasInode::from_fuse(ino);

    fuse_reply_attr(req, &inode->attr, ENTRY_TIMEOUT);
}

static void cas_fuse_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name,
                              size_t size)
{
    CasInode *inode = CasInode::from_fuse(ino);

    if (std::string(name) == XATTR_CHECKSUM) {
        /* SHA256 checksum for regular files */

        if (S_ISREG(inode->attr.st_mode) && !inode->is_modified()) {
            /* Unmodified regular file, digest is available */

            std::string hash = inode->digest.hash();

            if (size == 0) {
                /* Return size of xattr value */
                fuse_reply_xattr(req, hash.size());
                return;
            }

            if (size < hash.size()) {
                /* Not enough space */
                fuse_reply_err(req, E2BIG);
                return;
            }

            fuse_reply_buf(req, hash.c_str(), hash.size());
        }
        else {
            fuse_reply_err(req, ENODATA);
        }
    }
    else {
        fuse_reply_err(req, ENODATA);
    }
}

static void cas_fuse_listxattr(fuse_req_t req, fuse_ino_t ino, size_t size)
{
    /* No extended attributes. Don't advertise the checksum xattr to reduce
     * differences to regular filesystems. */
    fuse_reply_err(req, ENOSYS);
}

static void cas_fuse_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr,
                             int valid, struct fuse_file_info *fi)
{
    try {
        CasInode *inode = CasInode::from_fuse(ino);

        if (inode->readonly) {
            fuse_reply_err(req, EROFS);
            return;
        }

        if (valid & FUSE_SET_ATTR_MODE) {
            if (S_ISDIR(inode->attr.st_mode)) {
                inode->attr.st_mode &= ~PERMISSION_RWXALL_SPECIAL;
                inode->attr.st_mode |=
                    attr->st_mode & PERMISSION_RWXALL_SPECIAL;

                if (fs.opts.capture_mode) {
                    inode->set_modified();
                }
            }
            else {
                /* By default, only the executable bit of regular files is
                 * stored, however, the full mode bits are kept in memory. */

                bool old_executable =
                    (inode->attr.st_mode & PERMISSION_XUSR) != 0;
                bool new_executable = (attr->st_mode & PERMISSION_XUSR) != 0;

                inode->attr.st_mode &= ~PERMISSION_RWXALL_SPECIAL;
                inode->attr.st_mode |=
                    attr->st_mode & PERMISSION_RWXALL_SPECIAL;

                if (inode->parent && (fs.opts.capture_mode ||
                                      old_executable != new_executable)) {
                    /* The mode bits are stored in the parent directory */
                    inode->parent->set_modified();
                }
            }
        }

        if (valid & FUSE_SET_ATTR_SIZE) {
            if (S_ISREG(inode->attr.st_mode)) {
                CasFile *file = CasFile::open_from_fuse(ino);

                file->copy_on_write();

                if (ftruncate(file->fd, attr->st_size) < 0) {
                    fuse_reply_err(req, errno);
                    return;
                }
                file->set_size(attr->st_size);

                // Always set the mtime when setting the size
                if ((valid & FUSE_SET_ATTR_MTIME) == 0) {
                    valid |= FUSE_SET_ATTR_MTIME_NOW;
                }
            }
        }

        if (valid & FUSE_SET_ATTR_MTIME_NOW) {
            inode->update_mtime();
        }
        else if (valid & FUSE_SET_ATTR_MTIME) {
            inode->attr.st_mtim = attr->st_mtim;
            inode->mtime_set = true;
        }

        if (fs.opts.capture_mtime && inode->mtime_set) {
            if (S_ISDIR(inode->attr.st_mode)) {
                inode->set_modified();
            }
            else if (inode->parent) {
                /* The mtime is stored in the parent directory */
                inode->parent->set_modified();
            }
        }

        if (valid & FUSE_SET_ATTR_ATIME_NOW) {
            if (valid & FUSE_SET_ATTR_MTIME_NOW) {
                // Use the same timestamp as for mtime
                inode->attr.st_atim = inode->attr.st_mtim;
            }
            else {
                inode->update_atime();
            }
        }
        else if (valid & FUSE_SET_ATTR_ATIME) {
            inode->attr.st_atim = attr->st_atim;
        }

        if (valid & FUSE_SET_ATTR_CTIME) {
            inode->attr.st_ctim = attr->st_ctim;
        }
        else if ((valid & FUSE_SET_ATTR_MTIME_NOW) == 0) {
            inode->update_ctime();
        }

        cas_fuse_getattr(req, ino, fi);
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_fallocate(fuse_req_t req, fuse_ino_t ino, int mode,
                               off_t offset, off_t len,
                               struct fuse_file_info *fi)
{
    try {
        if (mode != 0) {
            fuse_reply_err(req, EOPNOTSUPP);
            return;
        }

        CasFile *file = CasFile::open_from_fuse(ino);

        if (file->readonly) {
            fuse_reply_err(req, EROFS);
            return;
        }

        file->copy_on_write();

        int err = posix_fallocate(file->fd, offset, len);
        if (err) {
            fuse_reply_err(req, err);
        }

        if (offset + len > file->attr.st_size) {
            file->set_size(offset + len);
        }

        file->update_mtime();

        fuse_reply_err(req, 0);
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_readlink(fuse_req_t req, fuse_ino_t ino)
{
    try {
        CasInode *inode = CasInode::from_fuse(ino);

        if (!S_ISLNK(inode->attr.st_mode)) {
            /* not a symlink */
            fuse_reply_err(req, EINVAL);
            return;
        }

        CasSymlink *symlink = dynamic_cast<CasSymlink *>(inode);
        if (symlink == nullptr) {
            throw std::runtime_error("Failed to get symlink from inode");
        }
        else {
            fuse_reply_readlink(req, symlink->target.c_str());
        }
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_opendir(fuse_req_t req, fuse_ino_t ino,
                             struct fuse_file_info *fi)
{
    if (fs.no_opendir) {
        /* We let the kernel handle opendir requests to reduce round-trips */
        fuse_reply_err(req, ENOSYS);
        return;
    }

    try {
        CasDirectory::open_from_fuse(ino);

        /* Cache readdir results in the kernel */
        fi->keep_cache = 1;
        fi->cache_readdir = 1;

        fuse_reply_open(req, fi);
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_readdir(fuse_req_t req, fuse_ino_t ino, size_t size,
                             off_t offset, struct fuse_file_info *fi,
                             bool plus)
{
    try {
        char *p = nullptr;
        size_t rem = 0;

        CasDirectory *inode = CasDirectory::open_from_fuse(ino);
        if (!inode) {
            fuse_reply_err(req, errno);
            return;
        }

        const off_t dentriesLen = static_cast<off_t>(inode->dentries.size());

        if (offset < 0 || offset > dentriesLen) {
            /* Invalid offset */
            fuse_reply_err(req, EIO);
            return;
        }

        std::vector<char> buf(size);

        p = &buf[0];
        rem = size;
        for (; offset < dentriesLen; offset++) {
            const auto &dentry = inode->dentries[offset];
            const char *name = dentry.name.c_str();
            CasInode *child_inode = dentry.inode;
            off_t nextoff = offset + 1;
            size_t entsize = 0;

            if (child_inode == nullptr) {
                /* Deleted entry, continue */
                continue;
            }

            if (plus) {
                struct fuse_entry_param e = {0};

                int ret = cas_fuse_do_lookup(req, ino, name, &e);
                if (ret) {
                    fuse_reply_err(req, -ret);
                    return;
                }

                entsize =
                    fuse_add_direntry_plus(req, p, rem, name, &e, nextoff);
            }
            else {
                /* Only bits 12-15 of the st_mode field are used, which means
                 * that the permission bits do not need to be populated. */
                entsize = fuse_add_direntry(req, p, rem, name,
                                            &child_inode->attr, nextoff);
            }
            if (entsize > rem) {
                /* Insufficient space in buffer for next entry */
                break;
            }

            p += entsize; // NOLINT
                          // (cppcoreguidelines-pro-bounds-pointer-arithmetic)
            rem -= entsize;
        }

        inode->update_atime();

        fuse_reply_buf(req, &buf[0], size - rem);
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_readdir(fuse_req_t req, fuse_ino_t ino, size_t size,
                             off_t offset, struct fuse_file_info *fi)
{
    cas_fuse_readdir(req, ino, size, offset, fi, false);
}

static void cas_fuse_readdirplus(fuse_req_t req, fuse_ino_t ino, size_t size,
                                 off_t offset, struct fuse_file_info *fi)
{
    cas_fuse_readdir(req, ino, size, offset, fi, true);
}

static void cas_fuse_releasedir(fuse_req_t req, fuse_ino_t ino,
                                struct fuse_file_info *fi)
{
    fuse_reply_err(req, 0);
}

static void cas_fuse_read(fuse_req_t req, fuse_ino_t ino, size_t size,
                          off_t offset, struct fuse_file_info *fi)
{
    /* Read file block without moving data between kernel and userspace
     * (splice) */

    try {
        struct fuse_bufvec buf = FUSE_BUFVEC_INIT(size);

        CasFile *inode = CasFile::open_from_fuse(ino);

        buf.buf[0].flags = fuse_buf_flags(FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK);
        buf.buf[0].fd = inode->fd;
        buf.buf[0].pos = offset;

        inode->update_atime();

        fuse_reply_data(req, &buf, FUSE_BUF_SPLICE_MOVE);
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_write_buf(fuse_req_t req, fuse_ino_t ino,
                               struct fuse_bufvec *in_buf, off_t offset,
                               struct fuse_file_info *fi)
{
    /* Write file block without moving data between kernel and userspace
     * (splice) */

    try {
        struct fuse_bufvec out_buf = FUSE_BUFVEC_INIT(fuse_buf_size(in_buf));

        CasFile *inode = CasFile::open_from_fuse(ino);

        if (inode->readonly) {
            fuse_reply_err(req, EROFS);
            return;
        }

        inode->copy_on_write();

        out_buf.buf[0].flags =
            fuse_buf_flags(FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK);
        out_buf.buf[0].fd = inode->fd;
        out_buf.buf[0].pos = offset;

        ssize_t res = fuse_buf_copy(&out_buf, in_buf, fuse_buf_copy_flags(0));
        if (res < 0) {
            fuse_reply_err(req, static_cast<int>(-res));
        }
        else {
            if (offset + res > inode->attr.st_size) {
                /* Kernel implicitly extends file, if needed.
                 * Keep track on our side as well. */
                inode->set_size(offset + res);
            }

            inode->update_mtime();
            fuse_reply_write(req, (size_t)res);
        }
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_access(fuse_req_t req, fuse_ino_t ino, int mask)
{
    /* Check user's permission for a file */

    const struct fuse_ctx *ctx = fuse_req_ctx(req);

    try {
        CasInode *inode = CasInode::from_fuse(ino);

        if (inode->readonly && (mask & W_OK)) {
            /* Write permission was requested for a read-only file. */
            fuse_reply_err(req, EROFS);
            return;
        }

        /* Extract the mode bits for the current user */
        int user_mode = 0;
        if (ctx->uid == inode->attr.st_uid) {
            /* The user is the owner of the inode */
            const int USER_PERMISSION_SHIFT = 6;
            user_mode = static_cast<int>((inode->attr.st_mode & S_IRWXU) >>
                                         USER_PERMISSION_SHIFT);
        }
        else if (ctx->gid == inode->attr.st_gid) {
            const int GROUP_PERMISSION_SHIFT = 3;
            /* The user is in the group of the inode */
            user_mode = static_cast<int>((inode->attr.st_mode & S_IRWXG) >>
                                         GROUP_PERMISSION_SHIFT);
        }
        else {
            user_mode = static_cast<int>(inode->attr.st_mode & S_IRWXO);
        }

        /* Check whether `mask` asks for anything not allowed by the
         * mode bits for the current user. */
        if ((mask & ~user_mode) == 0) {
            fuse_reply_err(req, 0);
        }
        else {
            fuse_reply_err(req, EACCES);
        }
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_create(fuse_req_t req, fuse_ino_t parent_id,
                            const char *name, mode_t mode,
                            struct fuse_file_info *fi)
{
    /* Create new file */

    if (strlen(name) > BUILDBOX_NAME_MAX) {
        fuse_reply_err(req, ENAMETOOLONG);
        return;
    }

    try {
        CasDirectory *parent = CasDirectory::open_from_fuse(parent_id);

        if (parent->lookup(name) != nullptr) {
            fuse_reply_err(req, EEXIST);
            return;
        }

        if (parent->readonly) {
            fuse_reply_err(req, EROFS);
            return;
        }

        CasInode *inode = new CasFile();

        /* Store specified mode */
        inode->attr.st_mode &= ~PERMISSION_RWXALL_SPECIAL;
        inode->attr.st_mode |= mode & PERMISSION_RWXALL_SPECIAL;

        parent->update_mtime();
        parent->set_modified();
        parent->set_dentry(name, inode);

        struct fuse_entry_param e = {0};
        int ret = cas_fuse_do_lookup(req, parent_id, name, &e);

        if (ret == 0) {
            fuse_reply_create(req, &e, fi);
        }
        else {
            fuse_reply_err(req, -ret);
        }
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_mkdir(fuse_req_t req, fuse_ino_t parent_id,
                           const char *name, mode_t mode)
{
    if (strlen(name) > BUILDBOX_NAME_MAX) {
        fuse_reply_err(req, ENAMETOOLONG);
        return;
    }

    try {
        CasDirectory *parent = CasDirectory::open_from_fuse(parent_id);

        if (parent->lookup(name) != nullptr) {
            fuse_reply_err(req, EEXIST);
            return;
        }

        if (parent->readonly) {
            fuse_reply_err(req, EROFS);
            return;
        }

        CasInode *inode = new CasDirectory(parent);

        /* Store specified mode */
        inode->attr.st_mode &= ~PERMISSION_RWXALL_SPECIAL;
        inode->attr.st_mode |= mode & PERMISSION_RWXALL_SPECIAL;

        parent->update_mtime();
        parent->set_modified();
        parent->set_dentry(name, inode);

        struct fuse_entry_param e = {0};
        int ret = cas_fuse_do_lookup(req, parent_id, name, &e);

        if (ret == 0) {
            fuse_reply_entry(req, &e);
        }
        else {
            fuse_reply_err(req, -ret);
        }
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_symlink(fuse_req_t req, const char *link,
                             fuse_ino_t parent_id, const char *name)
{
    if (strlen(name) > BUILDBOX_NAME_MAX) {
        fuse_reply_err(req, ENAMETOOLONG);
        return;
    }

    try {
        CasDirectory *parent = CasDirectory::open_from_fuse(parent_id);

        if (parent->lookup(name) != nullptr) {
            fuse_reply_err(req, EEXIST);
            return;
        }

        if (parent->readonly) {
            fuse_reply_err(req, EROFS);
            return;
        }

        CasSymlink *inode = new CasSymlink(link);

        inode->update_mtime();
        parent->update_mtime();
        parent->set_modified();
        inode->set_modified();

        parent->set_dentry(name, inode);

        struct fuse_entry_param e = {0};
        int ret = cas_fuse_do_lookup(req, parent_id, name, &e);

        if (ret == 0) {
            fuse_reply_entry(req, &e);
        }
        else {
            fuse_reply_err(req, -ret);
        }
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_mknod(fuse_req_t req, fuse_ino_t parent_id,
                           const char *name, mode_t mode, dev_t rdev)
{
    if (strlen(name) > BUILDBOX_NAME_MAX) {
        fuse_reply_err(req, ENAMETOOLONG);
        return;
    }

    try {
        CasDirectory *parent = CasDirectory::open_from_fuse(parent_id);

        if (parent->lookup(name) != nullptr) {
            fuse_reply_err(req, EEXIST);
            return;
        }

        if (parent->readonly) {
            fuse_reply_err(req, EROFS);
            return;
        }

        if (!S_ISFIFO(mode) && !S_ISSOCK(mode)) {
            /* Device node creation is not supported */
            fuse_reply_err(req, EPERM);
            return;
        }

        CasInode *inode = // NOLINT (cppcoreguidelines-owning-memory)
            new CasInode(nullptr);
        /* Store specified mode, this includes the file type */
        inode->attr.st_mode = mode;

        parent->update_mtime();
        parent->set_modified();
        parent->set_dentry(name, inode);

        struct fuse_entry_param e = {0};
        int ret = cas_fuse_do_lookup(req, parent_id, name, &e);

        if (ret == 0) {
            fuse_reply_entry(req, &e);
        }
        else {
            fuse_reply_err(req, -ret);
        }
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_unlink(fuse_req_t req, fuse_ino_t parent_id,
                            const char *name)
{
    try {
        CasDirectory *parent = CasDirectory::open_from_fuse(parent_id);

        auto dentry = parent->get_dentry(name);
        if (dentry == nullptr || dentry->inode == nullptr) {
            fuse_reply_err(req, ENOENT);
            return;
        }

        if (S_ISDIR(dentry->inode->attr.st_mode)) {
            fuse_reply_err(req, EISDIR);
            return;
        }

        if (parent->readonly) {
            fuse_reply_err(req, EROFS);
            return;
        }

        dentry->inode->attr.st_nlink--;
        dentry->inode = nullptr;
        parent->update_mtime();
        parent->set_modified();
        fuse_reply_err(req, 0);
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_rmdir(fuse_req_t req, fuse_ino_t parent_id,
                           const char *name)
{
    try {
        CasDirectory *parent = CasDirectory::open_from_fuse(parent_id);

        auto dentry = parent->get_dentry(name);
        if (dentry == nullptr || dentry->inode == nullptr) {
            fuse_reply_err(req, ENOENT);
            return;
        }

        if (!S_ISDIR(dentry->inode->attr.st_mode)) {
            fuse_reply_err(req, ENOTDIR);
            return;
        }

        if (parent->readonly) {
            fuse_reply_err(req, EROFS);
            return;
        }

        CasDirectory *dir =
            CasDirectory::open_from_fuse(dentry->inode->attr.st_ino);

        for (auto child_it = dir->dentries.begin();
             child_it != dir->dentries.end(); child_it++) {
            if (child_it->name != "." && child_it->name != ".." &&
                child_it->inode != nullptr) {
                fuse_reply_err(req, ENOTEMPTY);
                return;
            }
        }

        dentry->inode->attr.st_nlink--;
        dentry->inode = nullptr;
        parent->update_mtime();
        parent->set_modified();
        fuse_reply_err(req, 0);
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_rename(fuse_req_t req, fuse_ino_t parent_id,
                            const char *name, fuse_ino_t newparent_id,
                            const char *newname, unsigned int flags)
{
    if (strlen(newname) > BUILDBOX_NAME_MAX) {
        fuse_reply_err(req, ENAMETOOLONG);
        return;
    }

    if (flags & ~(RENAME_EXCHANGE | RENAME_NOREPLACE)) {
        fuse_reply_err(req, EINVAL);
        return;
    }

    /* RENAME_NOREPLACE can't be employed together with RENAME_EXCHANGE */
    if ((flags & RENAME_EXCHANGE) && (flags & RENAME_NOREPLACE)) {
        fuse_reply_err(req, EINVAL);
        return;
    }

    try {
        CasDirectory *parent = CasDirectory::open_from_fuse(parent_id);
        CasDirectory *newparent = CasDirectory::open_from_fuse(newparent_id);

        auto dentry = parent->get_dentry(name);
        if (dentry == nullptr || dentry->inode == nullptr) {
            fuse_reply_err(req, ENOENT);
            return;
        }

        if (parent->readonly || newparent->readonly) {
            fuse_reply_err(req, EROFS);
            return;
        }

        if (flags & RENAME_EXCHANGE) {
            auto newdentry = newparent->get_dentry(newname);

            /* Exchange the two directory entries */
            if (newdentry == nullptr || newdentry->inode == nullptr) {
                /* No entry with the new name */
                fuse_reply_err(req, ENOENT);
                return;
            }

            /* Detach from CAS parents */
            dentry->inode->parent = nullptr;
            newdentry->inode->parent = nullptr;

            /* Swap inodes */
            auto tmpinode = newdentry->inode;
            newdentry->inode = dentry->inode;
            dentry->inode = tmpinode;

            /* Link subdirectories to the new parents */
            if (S_ISDIR(dentry->inode->attr.st_mode)) {
                auto dir = dynamic_cast<CasDirectory *>(dentry->inode);
                if (dir == nullptr) {
                    throw std::runtime_error(
                        "Failed to get directory from inode");
                }
                dir->set_dentry("..", parent);
            }
            if (S_ISDIR(newdentry->inode->attr.st_mode)) {
                auto dir = dynamic_cast<CasDirectory *>(newdentry->inode);
                if (dir == nullptr) {
                    throw std::runtime_error(
                        "Failed to get directory from inode");
                }
                dir->set_dentry("..", newparent);
            }
        }
        else {
            auto newdentry = newparent->get_or_create_dentry(newname);

            // Creating a new dentry in the same directory may invalidate
            // previous dentry pointers.
            dentry = parent->get_dentry(name);
            if (dentry == nullptr || dentry->inode == nullptr) {
                fuse_reply_err(req, ENOENT);
                return;
            }

            /* Regular rename */
            if (newdentry->inode == nullptr) {
                /* No entry with the new name yet, add it */
                newdentry->inode = dentry->inode;
            }
            else if (flags & RENAME_NOREPLACE) {
                /* New name already exists */
                fuse_reply_err(req, EEXIST);
                return;
            }
            else {
                /* New name already exists, replace it */
                newdentry->inode->attr.st_nlink--;
                newdentry->inode = dentry->inode;
            }
            dentry->inode->parent = nullptr;
            dentry->inode = nullptr;

            /* Link subdirectory to the new parent */
            if (S_ISDIR(newdentry->inode->attr.st_mode)) {
                auto casdir = dynamic_cast<CasDirectory *>(newdentry->inode);
                if (casdir == nullptr) {
                    throw std::runtime_error(
                        "Failed to get directory from inode");
                }
                casdir->set_dentry("..", newparent);
            }
        }

        parent->update_mtime();
        parent->set_modified();
        newparent->update_mtime();
        newparent->set_modified();
        fuse_reply_err(req, 0);
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_link(fuse_req_t req, fuse_ino_t ino,
                          fuse_ino_t newparent_id, const char *newname)
{
    if (strlen(newname) > BUILDBOX_NAME_MAX) {
        fuse_reply_err(req, ENAMETOOLONG);
        return;
    }

    try {
        CasInode *inode = CasInode::from_fuse(ino);
        if (S_ISDIR(inode->attr.st_mode)) {
            /* Can't hardlink directories */
            fuse_reply_err(req, EPERM);
            return;
        }

        CasDirectory *newparent = CasDirectory::open_from_fuse(newparent_id);

        if (newparent->readonly) {
            fuse_reply_err(req, EROFS);
            return;
        }

        auto newdentry = newparent->get_or_create_dentry(newname);

        if (newdentry->inode == nullptr) {
            /* No entry with the new name yet, add it */
            newdentry->inode = inode;
        }
        else {
            /* New name already exists, do not replace it */
            fuse_reply_err(req, EEXIST);
            return;
        }
        newparent->update_mtime();
        newparent->set_modified();

        inode->attr.st_nlink++;

        struct fuse_entry_param e = {0};
        int ret = cas_fuse_do_lookup(req, newparent_id, newname, &e);

        if (ret == 0) {
            fuse_reply_entry(req, &e);
        }
        else {
            fuse_reply_err(req, -ret);
        }
    }
    catch (const std::exception &e) {
        fuse_reply_err(req, EIO);
    }
}

static void cas_fuse_open(fuse_req_t req, fuse_ino_t ino,
                          struct fuse_file_info *fi)
{
    /* We let the kernel handle file open requests to reduce round-trips */
    fuse_reply_err(req, ENOSYS);
}

static void cas_fuse_statfs(fuse_req_t req, fuse_ino_t ino)
{
    struct statvfs stbuf{};
    int ret = fstatvfs(fs.local_dfd, &stbuf);

    if (ret == 0) {
        fuse_reply_statfs(req, &stbuf);
    }
    else {
        fuse_reply_err(req, errno);
    }
}

static void cas_fuse_ioctl(fuse_req_t req, fuse_ino_t ino, int cmd, void *arg,
                           struct fuse_file_info *fi, unsigned flags,
                           const void *in_buf, size_t in_bufsz,
                           size_t out_bufsz)
{
    fuse_reply_err(req, ENOTTY);
}

constexpr static struct fuse_lowlevel_ops fs_oper = {
    .init = cas_fuse_init,
    .destroy = NULL,
    .lookup = cas_fuse_lookup,
    .forget = NULL,
    .getattr = cas_fuse_getattr,
    .setattr = cas_fuse_setattr,
    .readlink = cas_fuse_readlink,
    .mknod = cas_fuse_mknod,
    .mkdir = cas_fuse_mkdir,
    .unlink = cas_fuse_unlink,
    .rmdir = cas_fuse_rmdir,
    .symlink = cas_fuse_symlink,
    .rename = cas_fuse_rename,
    .link = cas_fuse_link,
    .open = cas_fuse_open,
    .read = cas_fuse_read,
    .write = NULL,
    .flush = NULL,
    .release = NULL,
    .fsync = NULL,
    .opendir = cas_fuse_opendir,
    .readdir = cas_fuse_readdir,
    .releasedir = cas_fuse_releasedir,
    .fsyncdir = NULL,
    .statfs = cas_fuse_statfs,
    .setxattr = NULL,
    .getxattr = cas_fuse_getxattr,
    .listxattr = cas_fuse_listxattr,
    .removexattr = NULL,
    .access = cas_fuse_access,
    .create = cas_fuse_create,
    .getlk = NULL,
    .setlk = NULL,
    .bmap = NULL,
    .ioctl = cas_fuse_ioctl,
    .poll = NULL,
    .write_buf = cas_fuse_write_buf,
    .retrieve_reply = NULL,
    .forget_multi = NULL,
    .flock = NULL,
    .fallocate = cas_fuse_fallocate,
    .readdirplus = cas_fuse_readdirplus,
};

static void usage(const std::string &name)
{
    std::cerr << "usage: " << name << " [OPTIONS] MOUNTPOINT\n";
    std::cerr << "    --help                 Print this message and exit\n";
    std::cerr
        << "    --version              Print version information and exit\n";
    std::cerr << "    --local=PATH           Local CAS cache directory\n";
    std::cerr << "    --input-digest=PATH    Path to input directory digest\n";
    std::cerr << "    --input-digest-value=DIGEST    Input directory digest "
                 "with DIGEST specified as <hash>/<size>\n";
    std::cerr
        << "    --output-digest=PATH   Path to output directory digest\n";
    std::cerr
        << "    --digest-function=FUNCTION    Digest function (default: " +
               buildboxcommon::DigestFunction_Value_Name(
                   BUILDBOXCOMMON_DIGEST_FUNCTION_VALUE) +
               ", options"
        << buildboxcommon::DigestGenerator::supportedDigestFunctionsList()
        << ")\n";
    std::cerr << "    --remote=URL           URL for remote CAS server\n";
    std::cerr << "    --server-cert=PATH     Public server certificate for "
                 "TLS (PEM-encoded)\n";
    std::cerr << "    --client-key=PATH      Private client key for TLS "
                 "(PEM-encoded)\n";
    std::cerr << "    --client-cert=PATH     Public client certificate for "
                 "TLS (PEM-encoded)\n";
    std::cerr << "    --prefetch             Prefetch input directory from "
                 "CAS server\n";
    std::cerr << "    --output-times=PATH    Path for timestamp output file\n";
    std::cerr
        << "    --capture-mtime        Capture last modification timestamps\n";
    std::cerr << "    --capture-mode         Capture file modes\n";
}

int main(int argc, char *argv[])
{
    try {
        auto cliArgs = std::span(argv, argc);
        int index = 0;
        const std::string prgname = cliArgs[0];
        index++;
        argc--;

        DigestFunction_Value digestFunction =
            static_cast<buildboxcommon::DigestFunction_Value>(
                BUILDBOXCOMMON_DIGEST_FUNCTION_VALUE);

        while (argc > 0) {
            std::string_view arg = cliArgs[index];
            size_t equal_pos = arg.find('=');
            std::string_view key = arg.substr(0, equal_pos);
            std::string_view value = (equal_pos != std::string_view::npos)
                                         ? arg.substr(equal_pos + 1)
                                         : "";

            if (key.size() > 2 && key[0] == '-' && key[1] == '-') {
                arg.remove_prefix(2);
                key.remove_prefix(2); // Remove the leading "--"
                if (!value.empty()) {
                    if (key == "local") {
                        fs.opts.local_path = value.data();
                    }
                    else if (key == "input-digest") {
                        fs.opts.input_digest_path = value.data();
                    }
                    else if (key == "input-digest-value") {
                        fs.opts.input_digest_str = value.data();
                    }
                    else if (key == "output-digest") {
                        fs.opts.output_digest = value.data();
                    }
                    else if (key == "remote") {
                        fs.opts.remote_url = value.data();
                    }
                    else if (key == "server-cert") {
                        fs.opts.server_cert = value.data();
                    }
                    else if (key == "client-key") {
                        fs.opts.client_key = value.data();
                    }
                    else if (key == "client-cert") {
                        fs.opts.client_cert = value.data();
                    }
                    else if (key == "output-times") {
                        fs.opts.output_times = value.data();
                    }
                    else if (key == "digest-function") {
                        digestFunction = buildboxcommon::DigestGenerator::
                            stringToDigestFunction(std::string(value));
                    }
                    else {
                        std::cerr << "Invalid option " << cliArgs[index]
                                  << "\n";
                        usage(prgname);
                        return GENERAL_FAILURE;
                    }
                }
                else {
                    if (arg == "help") {
                        usage(prgname);
                        return 0;
                    }
                    else if (arg == "version") {
                        std::cout << prgname << " " << buildboxcommon::VERSION
                                  << '\n';
                        return 0;
                    }
                    else if (arg == "prefetch") {
                        fs.opts.prefetch = 1;
                    }
                    else if (arg == "capture-mtime") {
                        fs.opts.capture_mtime = true;
                    }
                    else if (arg == "capture-mode") {
                        fs.opts.capture_mode = true;
                    }
                    else {
                        std::cerr << "Invalid option " << cliArgs[index]
                                  << "\n";
                        usage(prgname);
                        return GENERAL_FAILURE;
                    }
                }
            }
            else {
                if (!fs.opts.mountpoint) {
                    fs.opts.mountpoint = arg.data();
                }
                else {
                    std::cerr << "Invalid argument " << cliArgs[index] << "\n";
                    usage(prgname);
                    return GENERAL_FAILURE;
                }
            }
            index++;
            argc--;
        }

        DigestGenerator::init(digestFunction);

        /* Use the same timestamp for all input files by default to minimize
         * non-deterministic behavior. */
        google::protobuf::Timestamp gtime;
        google::protobuf::util::TimeUtil::FromString("2011-11-11T11:11:11Z",
                                                     &gtime);
        auto default_timepoint = TimeUtils::parse_timestamp(gtime);
        fs.default_timespec = TimeUtils::make_timespec(default_timepoint);

        fs.uid = getuid();
        fs.gid = getgid();

        if (!fs.opts.mountpoint) {
            std::cerr << "Mountpoint is missing\n\n";
            usage(prgname);
            return GENERAL_FAILURE;
        }

        if (!fs.opts.local_path) {
            std::cerr << "Local CAS cache directory is missing\n\n";
            usage(prgname);
            return GENERAL_FAILURE;
        }

        if (fs.opts.input_digest_str && fs.opts.input_digest_path) {
            std::cerr << "Cannot specify both input-digest and "
                         "input-digest-value\n\n";
            usage(prgname);
            return GENERAL_FAILURE;
        }

        fs.local_dfd =
            open(fs.opts.local_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
        if (fs.local_dfd < 0) {
            std::cerr << "Failed to open " << fs.opts.local_path << ": "
                      << std::strerror(errno) << "\n";
            return GENERAL_FAILURE;
        }
        mkdirat(fs.local_dfd, "objects", PERMISSION_RWXUSR_RXALL);
        mkdirat(fs.local_dfd, "tmp", PERMISSION_RWXUSR_RXALL);

        /* Use unique subdirectory in tmp for this buildbox-fuse instance */
        fs.temp_dir = std::make_unique<TemporaryDirectory>(fs.opts.local_path,
                                                           "tmp/fuse-");

        if (fs.opts.remote_url) {
            fs.client.init(fs.opts.remote_url, fs.opts.server_cert,
                           fs.opts.client_key, fs.opts.client_cert);
        }

        /* read digest of sandbox root directory (input tree) */
        Digest digest;
        if (fs.opts.input_digest_path || fs.opts.input_digest_str) {
            if (fs.opts.input_digest_str) {
                const std::string dstr(fs.opts.input_digest_str);
                const size_t idx = dstr.find('/');
                if (idx == 0 || idx == std::string::npos ||
                    idx + 1 >= dstr.size()) {
                    std::cerr << "Invalid digest specified, expected: "
                                 "<hash>/<size>, got ["
                              << dstr << "]\n\n";
                    usage(prgname);
                    return GENERAL_FAILURE;
                }
                digest.set_hash(dstr.substr(0, idx));
                digest.set_size_bytes(std::stoll(dstr.substr(idx + 1)));
            }
            else {
                int fd = open(fs.opts.input_digest_path, O_RDONLY | O_CLOEXEC);
                if (fd < 0) {
                    std::cerr << "Failed to open " << fs.opts.input_digest_path
                              << ": " << std::strerror(errno) << "\n";
                    return GENERAL_FAILURE;
                }
                if (!digest.ParseFromFileDescriptor(fd) ||
                    digest.hash().empty()) {
                    ::close(fd);
                    std::cerr << "Failed to parse input digest\n";
                    return GENERAL_FAILURE;
                }
                ::close(fd);
            }
            fs.root_inode = // NOLINT(cppcoreguidelines-owning-memory)
                new CasDirectory(nullptr, digest);

            if (fs.opts.prefetch) {
                if (!fs.opts.remote_url) {
                    std::cerr << "--prefetch requires --remote\n";
                    return GENERAL_FAILURE;
                }

                fs.executed_metadata.mutable_input_fetch_start_timestamp()
                    ->CopyFrom(buildboxcommon::TimeUtils::now());
                fs.prefetch();
                fs.executed_metadata.mutable_input_fetch_completed_timestamp()
                    ->CopyFrom(buildboxcommon::TimeUtils::now());
            }
        }
        else {
            /* empty input tree */
            fs.root_inode = // NOLINT(cppcoreguidelines-owning-memory)
                new CasDirectory(nullptr);
        }

        assert(fs.root_inode->attr.st_ino == FUSE_ROOT_ID);

        struct fuse_args args = FUSE_ARGS_INIT(0, nullptr);

        /* -o default_permissions enable normal permission checking based on
         * file modes */
        if (fuse_opt_add_arg(&args, prgname.c_str()) ||
            fuse_opt_add_arg(&args, "-o") ||
            fuse_opt_add_arg(&args, "default_permissions")) {
            return FUSE_FAILURE;
        }

        fs.fuse = fuse_session_new(&args, &fs_oper, sizeof(fs_oper), &fs);
        if (fs.fuse == nullptr) {
            return FUSE_FAILURE;
        }

        if (fuse_set_signal_handlers(fs.fuse) != 0) {
            return FUSE_FAILURE;
        }

        if (fuse_session_mount(fs.fuse, fs.opts.mountpoint) != 0) {
            return FUSE_FAILURE;
        }

        int output_digest_fd = -1;

        if (fs.opts.output_digest) {
            output_digest_fd = open(fs.opts.output_digest,
                                    O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,
                                    PERMISSION_RWALL);
            if (output_digest_fd < 0) {
                std::cerr << "Failed to open " << fs.opts.output_digest << ": "
                          << std::strerror(errno) << "\n";
                return GENERAL_FAILURE;
            }
        }

        buildboxcommon::systemd_notify_socket_send_ready();

        fuse_session_loop(fs.fuse);

        if (fs.opts.output_digest) {
            fs.executed_metadata.mutable_output_upload_start_timestamp()
                ->CopyFrom(buildboxcommon::TimeUtils::now());
            try {
                /* Recalculate digests and push changes to CAS */
                fs.root_inode->flush();
                // upload any remaining batch
                if (fs.opts.remote_url) {
                    fs.client.batch_upload();
                }
            }
            catch (const std::exception &e) {
                ::close(output_digest_fd);
                std::cerr << "Failed to flush changes to CAS: " << e.what()
                          << "\n";
                return GENERAL_FAILURE;
            }
            fs.executed_metadata.mutable_output_upload_completed_timestamp()
                ->CopyFrom(buildboxcommon::TimeUtils::now());

            /* Output new digest of root directory */
            if (!fs.root_inode->digest.SerializeToFileDescriptor(
                    output_digest_fd)) {
                ::close(output_digest_fd);
                std::cerr << "Failed to serialize output digest\n";
                return GENERAL_FAILURE;
            }
            ::close(output_digest_fd);
        }

        /* Delete temporary directory here to avoid I/O outside main() */
        fs.temp_dir.reset();

        fuse_session_unmount(fs.fuse);
        fuse_session_destroy(fs.fuse);
        fuse_opt_free_args(&args);

        if (fs.opts.output_times) {
            std::fstream fstream;
            fstream.open(fs.opts.output_times, std::fstream::out);
            fs.executed_metadata.SerializeToOstream(&fstream);
            fstream.close();
        }

        return 0;
    }
    catch (const std::exception &exception) {
        // This catch should not be relied on and is here as a backstop.
        BUILDBOX_LOG_ERROR("Caught exception in main " << exception.what());
        return 1;
    }
}
