BareGit
#include "card_service.h"

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

#include "public_id.h"
#include "game_field.h"

namespace
{

inline constexpr int MAX_ID_ATTEMPTS = 128;

mw::E<void> copyAsset(
    const std::filesystem::path& source,
    const std::filesystem::path& destination)
{
    std::error_code filesystem_error;
    std::filesystem::copy_file(
        source,
        destination,
        std::filesystem::copy_options::none,
        filesystem_error);
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to stage an existing card asset: " +
            filesystem_error.message()));
    }
    return {};
}

} // namespace

CardService::CardService(
    DataSourceInterface& data_source,
    NonSecretRandom& random,
    ImageProcessor image_processor,
    AssetStore asset_store)
        : data_source_(data_source),
          random_(random),
          image_processor_(std::move(image_processor)),
          asset_store_(std::move(asset_store))
{}

mw::E<std::string> CardService::createLooseCard(
    std::int64_t actor_user_id, CreateLooseCardInput input)
{
    return createCard(
        actor_user_id, std::move(input), std::nullopt, std::nullopt, {}, {});
}

mw::E<std::string> CardService::createGameCard(
    std::int64_t actor_user_id,
    CreateCardInput input,
    std::string game_short_name,
    std::int64_t expected_game_revision,
    SubmittedGameFields submitted_fields,
    const std::vector<std::int64_t>& series_ids)
{
    return createCard(
        actor_user_id,
        std::move(input),
        std::move(game_short_name),
        expected_game_revision,
        std::move(submitted_fields),
        series_ids);
}

mw::E<std::string> CardService::createCard(
    std::int64_t actor_user_id,
    CreateCardInput input,
    std::optional<std::string> game_short_name,
    std::optional<std::int64_t> expected_game_revision,
    SubmittedGameFields submitted_fields,
    const std::vector<std::int64_t>& series_ids)
{
    if(input.short_description)
    {
        auto rendered = markdown_renderer_.render(*input.short_description);
        if(!rendered)
        {
            return std::unexpected(std::move(rendered.error()));
        }
    }
    if(input.long_description)
    {
        auto rendered = markdown_renderer_.render(*input.long_description);
        if(!rendered)
        {
            return std::unexpected(std::move(rendered.error()));
        }
    }
    auto front = image_processor_.process(
        input.front, CardAssetType::FRONT_ART);
    if(!front)
    {
        return std::unexpected(std::move(front.error()));
    }

    std::optional<ProcessedImage> foil;
    if(input.foil)
    {
        auto processed = image_processor_.process(
            *input.foil, CardAssetType::FOIL_CONTROL);
        if(!processed)
        {
            return std::unexpected(std::move(processed.error()));
        }
        foil = std::move(*processed);
    }

    ProcessedImage thumbnail;
    if(foil)
    {
        if(!input.thumbnail)
        {
            return std::unexpected(mw::httpError(
                422,
                "A rendered thumbnail is required for a foil card"));
        }
        auto processed = image_processor_.process(
            *input.thumbnail, CardAssetType::THUMBNAIL);
        if(!processed)
        {
            return std::unexpected(std::move(processed.error()));
        }
        thumbnail = std::move(*processed);
    }
    else
    {
        if(input.thumbnail)
        {
            return std::unexpected(mw::httpError(
                422,
                "A plain card must not include an uploaded thumbnail"));
        }
        auto generated = image_processor_.generatePlainThumbnail(
            *front,
            input.staging_directory / "thumb.avif");
        if(!generated)
        {
            return std::unexpected(std::move(generated.error()));
        }
        thumbnail = std::move(*generated);
    }

    auto transaction = data_source_.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto actor = (*transaction)->getUserForUpdate(actor_user_id);
    if(!actor)
    {
        return std::unexpected(std::move(actor.error()));
    }
    if(!*actor || !authorization_.canCreateCard(**actor))
    {
        return std::unexpected(mw::httpError(403, "Forbidden"));
    }
    if(!(**actor).username)
    {
        return std::unexpected(mw::httpError(
            409, "Username onboarding is required"));
    }
    if((**actor).role == UserRole::CREATOR && input.rarity_was_submitted)
    {
        return std::unexpected(mw::httpError(
            400, "Creators cannot submit card rarity"));
    }

    std::vector<GameFieldValue> field_values;
    if(game_short_name)
    {
        if(!expected_game_revision)
        {
            return std::unexpected(mw::httpError(
                400, "Game revision is required"));
        }
        auto definition = (*transaction)->getGameDefinitionForUpdate(
            *game_short_name);
        if(!definition)
        {
            return std::unexpected(std::move(definition.error()));
        }
        if(!*definition)
        {
            return std::unexpected(mw::httpError(422, "Unknown game"));
        }
        if(!authorization_.canUseGame(**actor, (**definition).game))
        {
            return std::unexpected(mw::httpError(422, "Unknown game"));
        }
        if((**definition).game.revision != *expected_game_revision)
        {
            return std::unexpected(mw::httpError(
                409, "The game definition changed; reload and try again"));
        }
        auto decoded = decodeGameFields(**definition, submitted_fields);
        if(!decoded)
        {
            return std::unexpected(std::move(decoded.error()));
        }
        field_values = std::move(*decoded);
        auto memberships = (*transaction)->seriesBelongToGame(
            *game_short_name, series_ids);
        if(!memberships)
        {
            return std::unexpected(std::move(memberships.error()));
        }
        if(!*memberships)
        {
            return std::unexpected(mw::httpError(
                422, "A selected series belongs to another game"));
        }
    }
    else if(expected_game_revision || !submitted_fields.empty() ||
            !series_ids.empty())
    {
        return std::unexpected(mw::httpError(
            422, "Loose cards cannot have game fields or series"));
    }

    std::uint64_t number = 0;
    if(game_short_name)
    {
        auto allocated = (*transaction)->allocateGameNumber(
            *game_short_name);
        if(!allocated)
        {
            return std::unexpected(std::move(allocated.error()));
        }
        number = *allocated;
    }
    else
    {
        std::optional<std::uint32_t> loose_number;
        for(int attempt = 0; attempt < MAX_ID_ATTEMPTS; ++attempt)
        {
            const std::uint32_t candidate = random_.next();
            auto exists = (*transaction)->looseNumberExists(candidate);
            if(!exists)
            {
                return std::unexpected(std::move(exists.error()));
            }
            if(!*exists)
            {
                loose_number = candidate;
                break;
            }
        }
        if(!loose_number)
        {
            return std::unexpected(mw::runtimeError(
                "Failed to allocate a unique loose-card number"));
        }
        number = *loose_number;
    }

    Card card = {
        0,
        {game_short_name, number},
        std::move(input.name),
        std::move(input.short_description),
        std::move(input.long_description),
        (**actor).role == UserRole::CREATOR ? 0 : input.rarity,
        front->extension,
        foil ? std::optional<std::string>(foil->extension) : std::nullopt,
        thumbnail.extension,
        1,
        actor_user_id,
    };
    auto inserted = (*transaction)->insertCard(
        card, field_values, series_ids);
    if(!inserted)
    {
        return std::unexpected(std::move(inserted.error()));
    }

    auto public_id = formatPublicId(card.identity);
    if(!public_id)
    {
        return std::unexpected(std::move(public_id.error()));
    }
    auto published = asset_store_.publish(
        input.staging_directory, *public_id);
    if(!published)
    {
        return std::unexpected(std::move(published.error()));
    }

    auto committed = (*transaction)->commit();
    if(!committed)
    {
        asset_store_.removePublished(*public_id);
        return std::unexpected(std::move(committed.error()));
    }
    return *public_id;
}

mw::E<std::string> CardService::updateLooseCard(
    std::int64_t actor_user_id, UpdateLooseCardInput input)
{
    return updateCard(
        actor_user_id, std::move(input), std::nullopt, {}, {});
}

mw::E<std::string> CardService::updateGameCard(
    std::int64_t actor_user_id,
    UpdateLooseCardInput input,
    std::int64_t expected_game_revision,
    SubmittedGameFields submitted_fields,
    const std::vector<std::int64_t>& series_ids)
{
    return updateCard(
        actor_user_id,
        std::move(input),
        expected_game_revision,
        std::move(submitted_fields),
        series_ids);
}

mw::E<std::string> CardService::updateCard(
    std::int64_t actor_user_id,
    UpdateLooseCardInput input,
    std::optional<std::int64_t> expected_game_revision,
    SubmittedGameFields submitted_fields,
    const std::vector<std::int64_t>& series_ids)
{
    auto stored = data_source_.getCard(
        input.current_card.identity,
        GameContentScope::INCLUDE_INTERNAL);
    if(!stored)
    {
        return std::unexpected(std::move(stored.error()));
    }
    if(!*stored)
    {
        return std::unexpected(mw::httpError(404, "Card not found"));
    }
    Card card = std::move(**stored);
    if(input.short_description)
    {
        auto rendered = markdown_renderer_.render(*input.short_description);
        if(!rendered)
        {
            return std::unexpected(std::move(rendered.error()));
        }
    }
    if(input.long_description)
    {
        auto rendered = markdown_renderer_.render(*input.long_description);
        if(!rendered)
        {
            return std::unexpected(std::move(rendered.error()));
        }
    }
    const bool is_game_card = card.identity.game_short_name.has_value();
    if(card.id <= 0 || is_game_card != expected_game_revision.has_value())
    {
        return std::unexpected(mw::httpError(
            400, "Card identity does not match its game definition"));
    }
    if(input.expected_revision != card.revision)
    {
        return std::unexpected(mw::httpError(
            409, "The card was changed in another request"));
    }

    const bool rendering_changed =
        input.front_action == FrontAssetAction::REPLACE ||
        input.foil_action != FoilAssetAction::KEEP;
    if((input.front_action == FrontAssetAction::REPLACE) !=
       input.front.has_value())
    {
        return std::unexpected(mw::httpError(
            422, "Front artwork does not match the requested action"));
    }
    if((input.foil_action == FoilAssetAction::REPLACE) !=
       input.foil.has_value())
    {
        return std::unexpected(mw::httpError(
            422, "Foil control does not match the requested action"));
    }
    if(!rendering_changed && input.thumbnail)
    {
        return std::unexpected(mw::httpError(
            422, "A metadata-only edit must not include a thumbnail"));
    }

    auto public_id = formatPublicId(card.identity);
    if(!public_id)
    {
        return std::unexpected(std::move(public_id.error()));
    }

    std::optional<AssetReplacement> replacement;
    if(rendering_changed)
    {
        const std::filesystem::path published =
            asset_store_.directory(*public_id);
        ProcessedImage front;
        if(input.front_action == FrontAssetAction::REPLACE)
        {
            auto processed = image_processor_.process(
                *input.front, CardAssetType::FRONT_ART);
            if(!processed)
            {
                return std::unexpected(std::move(processed.error()));
            }
            front = std::move(*processed);
        }
        else
        {
            const std::string name =
                "front-art." + card.front_extension;
            auto copied = copyAsset(
                published / name, input.staging_directory / name);
            if(!copied)
            {
                return std::unexpected(std::move(copied.error()));
            }
            front = {
                input.staging_directory / name,
                card.front_extension,
                0,
                0,
                false,
            };
        }

        std::optional<ProcessedImage> foil;
        if(input.foil_action == FoilAssetAction::REPLACE)
        {
            auto processed = image_processor_.process(
                *input.foil, CardAssetType::FOIL_CONTROL);
            if(!processed)
            {
                return std::unexpected(std::move(processed.error()));
            }
            foil = std::move(*processed);
        }
        else if(input.foil_action == FoilAssetAction::KEEP &&
                card.foil_extension)
        {
            const std::string name = "foil." + *card.foil_extension;
            auto copied = copyAsset(
                published / name, input.staging_directory / name);
            if(!copied)
            {
                return std::unexpected(std::move(copied.error()));
            }
            foil = ProcessedImage{
                input.staging_directory / name,
                *card.foil_extension,
                0,
                0,
                false,
            };
        }

        ProcessedImage thumbnail;
        if(foil)
        {
            if(!input.thumbnail)
            {
                return std::unexpected(mw::httpError(
                    422,
                    "A rendered thumbnail is required for this edit"));
            }
            auto processed = image_processor_.process(
                *input.thumbnail, CardAssetType::THUMBNAIL);
            if(!processed)
            {
                return std::unexpected(std::move(processed.error()));
            }
            thumbnail = std::move(*processed);
        }
        else
        {
            if(input.thumbnail)
            {
                return std::unexpected(mw::httpError(
                    422,
                    "A plain card must not include an uploaded thumbnail"));
            }
            auto generated = image_processor_.generatePlainThumbnail(
                front, input.staging_directory / "thumb.avif");
            if(!generated)
            {
                return std::unexpected(std::move(generated.error()));
            }
            thumbnail = std::move(*generated);
        }
        card.front_extension = std::move(front.extension);
        card.foil_extension = foil
            ? std::optional<std::string>(std::move(foil->extension))
            : std::nullopt;
        card.thumbnail_extension = std::move(thumbnail.extension);
    }

    card.name = std::move(input.name);
    card.short_description = std::move(input.short_description);
    card.long_description = std::move(input.long_description);
    ++card.revision;

    auto transaction = data_source_.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto current = (*transaction)->getCardForUpdate(card.id);
    if(!current)
    {
        return std::unexpected(std::move(current.error()));
    }
    if(!*current)
    {
        return std::unexpected(mw::httpError(404, "Card not found"));
    }
    if((**current).revision != input.expected_revision)
    {
        return std::unexpected(mw::httpError(
            409, "The card was changed in another request"));
    }
    auto actor = (*transaction)->getUserForUpdate(actor_user_id);
    if(!actor)
    {
        return std::unexpected(std::move(actor.error()));
    }
    if(!*actor || !authorization_.canEditCard(**actor, **current))
    {
        return std::unexpected(mw::httpError(404, "Card not found"));
    }
    if(!(**actor).username)
    {
        return std::unexpected(mw::httpError(
            409, "Username onboarding is required"));
    }
    if((**actor).role == UserRole::CREATOR && input.rarity_was_submitted)
    {
        return std::unexpected(mw::httpError(
            400, "Creators cannot submit card rarity"));
    }
    card.id = (**current).id;
    card.identity = (**current).identity;
    card.creator_user_id = (**current).creator_user_id;
    card.rarity = (**actor).role == UserRole::CREATOR
        ? (**current).rarity
        : input.rarity;

    std::vector<GameFieldValue> field_values;
    if(card.identity.game_short_name)
    {
        auto definition = (*transaction)->getGameDefinitionForUpdate(
            *card.identity.game_short_name);
        if(!definition)
        {
            return std::unexpected(std::move(definition.error()));
        }
        if(!*definition)
        {
            return std::unexpected(mw::runtimeError(
                "Card references a missing game"));
        }
        if(!authorization_.canUseGame(**actor, (**definition).game))
        {
            return std::unexpected(mw::httpError(404, "Card not found"));
        }
        if((**definition).game.revision != *expected_game_revision)
        {
            return std::unexpected(mw::httpError(
                409, "The game definition changed; reload and try again"));
        }
        auto decoded = decodeGameFields(**definition, submitted_fields);
        if(!decoded)
        {
            return std::unexpected(std::move(decoded.error()));
        }
        field_values = std::move(*decoded);
        auto memberships = (*transaction)->seriesBelongToGame(
            *card.identity.game_short_name, series_ids);
        if(!memberships)
        {
            return std::unexpected(std::move(memberships.error()));
        }
        if(!*memberships)
        {
            return std::unexpected(mw::httpError(
                422, "A selected series belongs to another game"));
        }
    }
    else if(!submitted_fields.empty() || !series_ids.empty())
    {
        return std::unexpected(mw::httpError(
            422, "Loose cards cannot have game fields or series"));
    }

    auto updated = (*transaction)->updateCard(
        card, field_values, series_ids);
    if(!updated)
    {
        return std::unexpected(std::move(updated.error()));
    }
    if(rendering_changed)
    {
        auto replaced = asset_store_.replace(
            input.staging_directory,
            *public_id,
            input.expected_revision);
        if(!replaced)
        {
            return std::unexpected(std::move(replaced.error()));
        }
        replacement = std::move(*replaced);
    }

    auto committed = (*transaction)->commit();
    if(!committed)
    {
        if(replacement)
        {
            asset_store_.restore(*replacement);
        }
        return std::unexpected(std::move(committed.error()));
    }
    if(replacement)
    {
        asset_store_.finish(*replacement);
    }
    return *public_id;
}

mw::E<void> CardService::deleteCard(
    std::int64_t actor_user_id, const Card& card)
{
    auto transaction = data_source_.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto current = (*transaction)->getCardForUpdate(card.id);
    if(!current)
    {
        return std::unexpected(std::move(current.error()));
    }
    if(!*current)
    {
        return std::unexpected(mw::httpError(404, "Card not found"));
    }
    auto actor = (*transaction)->getUserForUpdate(actor_user_id);
    if(!actor)
    {
        return std::unexpected(std::move(actor.error()));
    }
    if(!*actor || !authorization_.canDeleteCard(**actor))
    {
        return std::unexpected(mw::httpError(403, "Forbidden"));
    }
    if(!(**actor).username)
    {
        return std::unexpected(mw::httpError(
            409, "Username onboarding is required"));
    }
    auto public_id = formatPublicId((**current).identity);
    if(!public_id)
    {
        return std::unexpected(std::move(public_id.error()));
    }
    auto trashed = asset_store_.trash(
        *public_id, (**current).revision, random_.hex(16));
    if(!trashed)
    {
        return std::unexpected(std::move(trashed.error()));
    }
    auto deleted = (*transaction)->deleteCard((**current).id);
    if(!deleted)
    {
        asset_store_.restore(*trashed);
        return std::unexpected(std::move(deleted.error()));
    }
    auto committed = (*transaction)->commit();
    if(!committed)
    {
        asset_store_.restore(*trashed);
        return std::unexpected(std::move(committed.error()));
    }
    asset_store_.finish(*trashed);
    return {};
}