BareGit
#include "asset_store.h"

#include <charconv>
#include <filesystem>
#include <optional>
#include <string>
#include <system_error>
#include <utility>

#include <spdlog/spdlog.h>

#include "data.h"
#include "public_id.h"

namespace
{

struct RecoveryName
{
    std::string public_id;
    std::int64_t revision;
};

std::optional<RecoveryName> parseRecoveryName(
    const std::string& name,
    std::string_view prefix)
{
    if(!name.starts_with(prefix))
    {
        return std::nullopt;
    }
    const std::size_t begin = prefix.size();
    std::size_t marker = name.find("-r", begin);
    while(marker != std::string::npos)
    {
        const std::size_t revision_begin = marker + 2;
        const std::size_t suffix = name.find('-', revision_begin);
        if(suffix != std::string::npos)
        {
            const std::string public_id = name.substr(begin, marker - begin);
            const std::string revision_text = name.substr(
                revision_begin, suffix - revision_begin);
            auto identity = parsePublicId(public_id);
            std::int64_t revision = 0;
            const auto parsed = std::from_chars(
                revision_text.data(),
                revision_text.data() + revision_text.size(),
                revision);
            if(identity && !revision_text.empty() &&
               parsed.ec == std::errc{} &&
               parsed.ptr == revision_text.data() + revision_text.size() &&
               revision > 0)
            {
                return RecoveryName{public_id, revision};
            }
        }
        marker = name.find("-r", marker + 2);
    }
    return std::nullopt;
}

mw::E<void> removeDirectory(const std::filesystem::path& path)
{
    std::error_code filesystem_error;
    std::filesystem::remove_all(path, filesystem_error);
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to remove abandoned card assets: " +
            filesystem_error.message()));
    }
    return {};
}

} // namespace

AssetStore::AssetStore(std::filesystem::path card_storage_root)
        : published_root_(
              std::move(card_storage_root) / "published")
{}

std::filesystem::path AssetStore::directory(
    const std::string& public_id) const
{
    return published_root_ / public_id;
}

mw::E<void> AssetStore::publish(
    const std::filesystem::path& staging_directory,
    const std::string& public_id) const
{
    std::error_code filesystem_error;
    std::filesystem::create_directories(
        published_root_, filesystem_error);
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to create the published asset root: " +
            filesystem_error.message()));
    }

    const std::filesystem::path destination = published_root_ / public_id;
    if(std::filesystem::exists(destination, filesystem_error))
    {
        return std::unexpected(mw::runtimeError(
            "The destination card asset directory already exists"));
    }
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to inspect the published asset directory: " +
            filesystem_error.message()));
    }

    std::filesystem::rename(
        staging_directory, destination, filesystem_error);
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to publish the card assets: " +
            filesystem_error.message()));
    }
    return {};
}

mw::E<AssetReplacement> AssetStore::replace(
    const std::filesystem::path& staging_directory,
    const std::string& public_id,
    std::int64_t previous_revision) const
{
    const std::filesystem::path destination = published_root_ / public_id;
    const std::filesystem::path previous = staging_directory.parent_path() /
        ("backup-" + public_id + "-r" +
         std::to_string(previous_revision) + "-" +
         staging_directory.filename().string());
    std::error_code filesystem_error;
    if(!std::filesystem::is_directory(destination, filesystem_error))
    {
        return std::unexpected(mw::runtimeError(
            "The existing card asset directory is missing"));
    }
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to inspect the existing card assets: " +
            filesystem_error.message()));
    }

    std::filesystem::rename(destination, previous, filesystem_error);
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to retain the existing card assets: " +
            filesystem_error.message()));
    }
    std::filesystem::rename(
        staging_directory, destination, filesystem_error);
    if(filesystem_error)
    {
        std::error_code restore_error;
        std::filesystem::rename(previous, destination, restore_error);
        return std::unexpected(mw::runtimeError(
            "Failed to publish the edited card assets: " +
            filesystem_error.message()));
    }
    return AssetReplacement{public_id, previous};
}

void AssetStore::restore(const AssetReplacement& replacement) const
{
    const std::filesystem::path destination =
        published_root_ / replacement.public_id;
    std::error_code filesystem_error;
    std::filesystem::remove_all(destination, filesystem_error);
    if(!filesystem_error)
    {
        std::filesystem::rename(
            replacement.previous_directory,
            destination,
            filesystem_error);
    }
    if(filesystem_error)
    {
        spdlog::critical(
            "Failed to restore rolled-back assets for card {}: {}",
            replacement.public_id,
            filesystem_error.message());
    }
}

void AssetStore::finish(const AssetReplacement& replacement) const
{
    std::error_code filesystem_error;
    std::filesystem::remove_all(
        replacement.previous_directory, filesystem_error);
    if(filesystem_error)
    {
        spdlog::warn(
            "Failed to remove previous assets for card {}: {}",
            replacement.public_id,
            filesystem_error.message());
    }
}

mw::E<TrashedAssets> AssetStore::trash(
    const std::string& public_id,
    std::int64_t revision,
    const std::string& suffix) const
{
    const std::filesystem::path source = published_root_ / public_id;
    std::error_code filesystem_error;
    if(!std::filesystem::exists(source, filesystem_error))
    {
        if(filesystem_error)
        {
            return std::unexpected(mw::runtimeError(
                "Failed to inspect card assets for deletion: " +
                filesystem_error.message()));
        }
        return TrashedAssets{public_id, {}};
    }
    const std::filesystem::path trash_root =
        published_root_.parent_path() / ".trash";
    std::filesystem::create_directories(trash_root, filesystem_error);
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to create the private trash directory: " +
            filesystem_error.message()));
    }
    const std::filesystem::path destination = trash_root /
        (public_id + "-r" + std::to_string(revision) + "-" + suffix);
    std::filesystem::rename(source, destination, filesystem_error);
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to move card assets into private trash: " +
            filesystem_error.message()));
    }
    return TrashedAssets{public_id, destination};
}

void AssetStore::restore(const TrashedAssets& trashed) const
{
    if(trashed.trash_directory.empty())
    {
        return;
    }
    std::error_code filesystem_error;
    std::filesystem::rename(
        trashed.trash_directory,
        published_root_ / trashed.public_id,
        filesystem_error);
    if(filesystem_error)
    {
        spdlog::critical(
            "Failed to restore deleted assets for card {}: {}",
            trashed.public_id,
            filesystem_error.message());
    }
}

void AssetStore::finish(const TrashedAssets& trashed) const
{
    if(trashed.trash_directory.empty())
    {
        return;
    }
    std::error_code filesystem_error;
    std::filesystem::remove_all(trashed.trash_directory, filesystem_error);
    if(filesystem_error)
    {
        spdlog::warn(
            "Failed to remove committed trash for card {}: {}",
            trashed.public_id,
            filesystem_error.message());
    }
}

void AssetStore::removePublished(const std::string& public_id) const
{
    std::error_code filesystem_error;
    std::filesystem::remove_all(
        published_root_ / public_id, filesystem_error);
    if(filesystem_error)
    {
        spdlog::error(
            "Failed to remove rolled-back assets for card {}: {}",
            public_id,
            filesystem_error.message());
    }
}

mw::E<void> AssetStore::reconcile(DataSourceInterface& data_source) const
{
    const std::filesystem::path storage_root = published_root_.parent_path();
    const std::filesystem::path staging_root = storage_root / ".staging";
    const std::filesystem::path trash_root = storage_root / ".trash";
    std::error_code filesystem_error;
    std::filesystem::create_directories(staging_root, filesystem_error);
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to inspect staging during recovery: " +
            filesystem_error.message()));
    }

    for(const std::filesystem::directory_entry& entry :
        std::filesystem::directory_iterator(staging_root))
    {
        const std::string name = entry.path().filename().string();
        auto recovery = parseRecoveryName(name, "backup-");
        if(!recovery)
        {
            if(!name.starts_with("backup-"))
            {
                auto removed = removeDirectory(entry.path());
                if(!removed)
                {
                    return removed;
                }
            }
            else
            {
                spdlog::warn("Leaving malformed recovery entry {}", name);
            }
            continue;
        }
        auto identity = parsePublicId(recovery->public_id);
        auto card = data_source.getCard(
            *identity, GameContentScope::INCLUDE_INTERNAL);
        if(!card)
        {
            return std::unexpected(std::move(card.error()));
        }
        const std::filesystem::path canonical =
            published_root_ / recovery->public_id;
        if(*card && (**card).revision == recovery->revision)
        {
            auto removed = removeDirectory(canonical);
            if(!removed)
            {
                return removed;
            }
            std::filesystem::rename(
                entry.path(), canonical, filesystem_error);
            if(filesystem_error)
            {
                return std::unexpected(mw::runtimeError(
                    "Failed to restore interrupted card edit: " +
                    filesystem_error.message()));
            }
        }
        else if(*card && (**card).revision > recovery->revision)
        {
            auto removed = removeDirectory(entry.path());
            if(!removed)
            {
                return removed;
            }
        }
        else if(!*card)
        {
            auto removed = removeDirectory(entry.path());
            if(!removed)
            {
                return removed;
            }
        }
        else
        {
            return std::unexpected(mw::runtimeError(
                "A card revision predates its recovery backup"));
        }
    }

    std::filesystem::create_directories(trash_root, filesystem_error);
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to inspect trash during recovery: " +
            filesystem_error.message()));
    }
    for(const std::filesystem::directory_entry& entry :
        std::filesystem::directory_iterator(trash_root))
    {
        auto recovery = parseRecoveryName(
            entry.path().filename().string(), "");
        if(!recovery)
        {
            spdlog::warn(
                "Leaving malformed trash entry {}",
                entry.path().filename().string());
            continue;
        }
        auto identity = parsePublicId(recovery->public_id);
        auto card = data_source.getCard(
            *identity, GameContentScope::INCLUDE_INTERNAL);
        if(!card)
        {
            return std::unexpected(std::move(card.error()));
        }
        const std::filesystem::path canonical =
            published_root_ / recovery->public_id;
        if(*card && !std::filesystem::exists(canonical))
        {
            std::filesystem::rename(
                entry.path(), canonical, filesystem_error);
            if(filesystem_error)
            {
                return std::unexpected(mw::runtimeError(
                    "Failed to restore interrupted card deletion: " +
                    filesystem_error.message()));
            }
        }
        else if(!*card || std::filesystem::exists(canonical))
        {
            auto removed = removeDirectory(entry.path());
            if(!removed)
            {
                return removed;
            }
        }
    }

    for(const std::filesystem::directory_entry& entry :
        std::filesystem::directory_iterator(published_root_))
    {
        const std::string public_id = entry.path().filename().string();
        auto identity = parsePublicId(public_id);
        if(!identity)
        {
            spdlog::warn("Leaving non-card published entry {}", public_id);
            continue;
        }
        auto card = data_source.getCard(
            *identity, GameContentScope::INCLUDE_INTERNAL);
        if(!card)
        {
            return std::unexpected(std::move(card.error()));
        }
        if(!*card)
        {
            auto removed = removeDirectory(entry.path());
            if(!removed)
            {
                return removed;
            }
        }
    }
    return {};
}