#include <buildboxcommon_fusestager.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_systemutils.h>

#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace buildboxcommon;

const std::string FuseStager::s_buildboxFuseBinaryName = "buildbox-fuse";

FuseStager::FuseStager(LocalCas *cas_storage) : FileStager(cas_storage)
{
    d_buildboxFusePath = buildboxcommon::SystemUtils::getPathToCommand(
        s_buildboxFuseBinaryName);

    if (d_buildboxFusePath == "") {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            StagerBinaryNotAvailable, "Could not find FUSE stager binary \""
                                          << s_buildboxFuseBinaryName << "\"");
    }

    BUILDBOX_LOG_DEBUG("Found FUSE stager binary at: " << d_buildboxFusePath);
}

FuseStager::FuseStagedDirectory::FuseStagedDirectory(
    FuseStager *stager, buildboxcommon::TemporaryFile *digest_file,
    const std::string &path, const pid_t stager_pid, const dev_t fuse_dev)
    : d_stager(stager), d_staged_path(path), d_digest_file(digest_file),
      d_stager_pid(stager_pid), d_fuse_dev(fuse_dev)
{
}

FuseStager::FuseStagedDirectory::~FuseStagedDirectory()
{
    BUILDBOX_LOG_DEBUG("Unstaging directory " << d_staged_path)

    // 1) Mark FUSE device as inactive
    d_stager->removeActiveDevice(d_fuse_dev);

    // 2) Terminate the `buildbox-fuse` process:
    BUILDBOX_LOG_TRACE(
        "Terminating FUSE stager process with pid: " << d_stager_pid);
    const auto kill_status = kill(d_stager_pid, SIGTERM);

    if (kill_status == -1) {
        const auto kill_error = errno;
        BUILDBOX_LOG_ERROR(
            "Error sending signal to shutdown FUSE stager process "
            << strerror(kill_error));
        return;
    }

    int status = 0;
    const auto wait_status = waitpid(d_stager_pid, &status, 0);

    if (wait_status == -1) {
        const auto wait_error = errno;
        BUILDBOX_LOG_ERROR(
            "Error waiting for FUSE stager process after issuing SIGTERM: "
            << strerror(wait_error));
    }

    // 3) Delete the digest file:
    // (Automatically done by the destructor of TemporaryFile)
}

std::unique_ptr<FileStager::StagedDirectory> FuseStager::stage(
    const Digest &root_digest, const std::string &path,
    const std::optional<ProcessCredentials> & /*accessCredentials*/,
    const FileStagedCallback & /*callback*/)
{
    // (buildbox-fuse currently reads the digest from a file)
    std::unique_ptr<buildboxcommon::TemporaryFile> digest_file =
        writeDigestToTemporaryFile(root_digest);

    BUILDBOX_LOG_DEBUG("Digest file written to: " << digest_file->name());

    struct stat st{};
    const pid_t stager_pid = launchStager(digest_file->name(), path, &st);

    addActiveDevice(st.st_dev);

    const auto file_pointer = digest_file.release();
    return std::make_unique<FuseStagedDirectory>(this, file_pointer, path,
                                                 stager_pid, st.st_dev);
}

pid_t FuseStager::launchStager(const std::string &digest_file_path,
                               const std::string &stage_path, struct stat *st)
{
    struct stat old_stat{};
    struct stat new_stat{};

    if (stat(stage_path.c_str(), &old_stat) != 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "error in stat() for FUSE mountpoint " << stage_path);
    }

    const auto buildbox_fuse_command =
        prepareCommand(digest_file_path, stage_path);

    BUILDBOX_LOG_DEBUG("Launching " << buildbox_fuse_command[0]);

    const auto pid = fork();
    if (pid == -1) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category, "error in fork()");
    }
    else if (pid == 0) {
        // Unblock signals in child process
        sigset_t signal_mask;
        sigemptyset(&signal_mask);
        if (sigprocmask(SIG_SETMASK, &signal_mask, nullptr) < 0) {
            perror("Failed to unblock signals");
            _Exit(1);
        }

        const auto exit_code =
            buildboxcommon::SystemUtils::executeCommand(buildbox_fuse_command);

        perror(nullptr);
        _Exit(exit_code);
    }
    else {
        BUILDBOX_LOG_DEBUG("FUSE stager running with pid: " << pid);

        // Wait for buildbox-fuse to initialize and mount the filesystem
        while (true) {
            int status = 0;
            if (waitpid(pid, &status, WNOHANG) == pid) {
                int exit_code = 0;
                if (WIFEXITED(status)) {
                    exit_code = WEXITSTATUS(status);
                }
                else if (WIFSIGNALED(status)) {
                    exit_code = -WTERMSIG(status);
                }

                BUILDBOXCOMMON_THROW_EXCEPTION(
                    std::runtime_error,
                    "The FUSE stager child process unexpectedly died with "
                    "exit code "
                        << std::to_string(exit_code));
            }

            if (stat(stage_path.c_str(), &new_stat) != 0) {
                BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                    std::system_error, errno, std::system_category,
                    "error in stat() for FUSE mountpoint " << stage_path);
            }

            if (new_stat.st_dev != old_stat.st_dev) {
                // Mount was successful
                *st = new_stat;
                break;
            }

            const int sleepDuration = 10000;
            usleep(sleepDuration);
        }

        return pid;
    }
}

std::vector<std::string>
FuseStager::prepareCommand(const std::string &digest_file_path,
                           const std::string &stage_path) const
{
    std::vector<std::string> command = {this->d_buildboxFusePath};

    // Digest file:
    command.push_back("--input-digest=" + std::string(digest_file_path));

    // Path to the CAS:
    command.push_back("--local=" +
                      std::string(this->d_cas_storage->path().c_str()));

    // Digest function
    command.push_back("--digest-function=" +
                      buildboxcommon::DigestFunction_Value_Name(
                          buildboxcommon::DigestGenerator::digestFunction()));

    // Stage path:
    command.push_back(stage_path);

    return command;
}

std::unique_ptr<buildboxcommon::TemporaryFile>
FuseStager::writeDigestToTemporaryFile(const Digest &digest) const
{
    auto file = std::make_unique<buildboxcommon::TemporaryFile>();

    const bool write_status = digest.SerializeToFileDescriptor(file->fd());

    if (!write_status) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error writing digest to \"" << file->name() << "\"");
    }

    BUILDBOX_LOG_DEBUG("Wrote digest to " << file->name());
    file->close();

    return file;
}

std::string FuseStager::getHashXattrName(int dirfd)
{
    struct stat st{};
    if (fstat(dirfd, &st) < 0) {
        BUILDBOX_LOG_WARNING(
            "FuseStager::checksumXattrSupported: Failed to stat directory fd "
            << dirfd << ": " << strerror(errno));
        return "";
    }

    const std::lock_guard<std::mutex> lock(d_activeDevicesMutex);
    if (d_activeDevices.count(st.st_dev) == 0) {
        // Not a FuseStager directory
        return "";
    }

    // buildbox-fuse always uses this xattr name
    return "user.checksum.sha256";
}

void FuseStager::addActiveDevice(dev_t dev)
{
    const std::lock_guard<std::mutex> lock(d_activeDevicesMutex);
    d_activeDevices.insert(dev);
}

void FuseStager::removeActiveDevice(dev_t dev)
{
    const std::lock_guard<std::mutex> lock(d_activeDevicesMutex);
    const auto it = d_activeDevices.find(dev);
    if (it != d_activeDevices.end()) {
        d_activeDevices.erase(it);
    }
}
