BareGit
#include "game_service.h"

#include <algorithm>
#include <cstdint>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>

#include <mw/utils.hpp>

#include "game_field.h"

namespace
{

mw::E<void> authorizeAdministrator(
    DataSourceTransactionInterface& transaction,
    const AuthorizationService& authorization,
    std::int64_t actor_user_id)
{
    auto actor = transaction.getUserForUpdate(actor_user_id);
    if(!actor)
    {
        return std::unexpected(std::move(actor.error()));
    }
    if(!*actor || !(*actor)->username ||
       !authorization.canAdminister(**actor))
    {
        return std::unexpected(mw::httpError(403, "Administrator required"));
    }
    return {};
}

bool validShortName(const std::string& value)
{
    return !value.empty() && std::ranges::all_of(
        value,
        [](unsigned char character)
        {
            return (character >= 'a' && character <= 'z') ||
                   (character >= '0' && character <= '9');
        });
}

bool validFieldKey(const std::string& value)
{
    if(value.empty() || value.front() < 'a' || value.front() > 'z')
    {
        return false;
    }
    return std::ranges::all_of(
        value,
        [](unsigned char character)
        {
            return (character >= 'a' && character <= 'z') ||
                   (character >= '0' && character <= '9') ||
                   character == '_';
        });
}

mw::E<void> validateDisplayText(
    std::string& display_name,
    const std::string& description,
    MarkdownRenderer& renderer)
{
    display_name = std::string(mw::strip(display_name));
    if(display_name.empty())
    {
        return std::unexpected(mw::Error(GameDefinitionValidationError{
            "display_name", "Game name is required"}));
    }
    if(!validGameText(display_name))
    {
        return std::unexpected(mw::Error(GameDefinitionValidationError{
            "display_name", "Game name must be UTF-8 text"}));
    }
    if(!validGameText(description))
    {
        return std::unexpected(mw::Error(GameDefinitionValidationError{
            "description", "Game description must be UTF-8 text"}));
    }
    auto rendered = renderer.render(description);
    if(!rendered)
    {
        return std::unexpected(mw::Error(GameDefinitionValidationError{
            "description", rendered.error().msg()}));
    }
    return {};
}

mw::E<std::vector<GameChoice>> validateChoices(
    GameFieldType type, const std::vector<std::string>& values)
{
    if(type != GameFieldType::CHOICE && !values.empty())
    {
        return std::unexpected(mw::Error(GameDefinitionValidationError{
            "choice", "Only choice fields can define choices"}));
    }
    if(type == GameFieldType::CHOICE && values.empty())
    {
        return std::unexpected(mw::Error(GameDefinitionValidationError{
            "choice", "A choice field requires at least one choice"}));
    }
    std::unordered_set<std::string> unique;
    std::vector<GameChoice> choices;
    choices.reserve(values.size());
    for(std::size_t position = 0; position < values.size(); ++position)
    {
        const std::string& value = values[position];
        if(value.empty() || !validGameText(value))
        {
            return std::unexpected(mw::Error(GameDefinitionValidationError{
                "choice", "Choices must be nonempty UTF-8 text"}));
        }
        if(!unique.insert(value).second)
        {
            return std::unexpected(mw::httpError(
                409, "Choice values must be unique"));
        }
        choices.push_back({value, static_cast<std::int64_t>(position)});
    }
    return choices;
}

const GameField* findField(
    const GameDefinitionSnapshot& definition, std::int64_t field_id)
{
    const auto field = std::ranges::find_if(
        definition.fields,
        [field_id](const GameField& candidate)
        {
            return candidate.id == field_id;
        });
    return field == definition.fields.end() ? nullptr : &*field;
}

mw::E<GameDefinitionSnapshot> readDefinition(
    DataSourceTransactionInterface& transaction,
    const std::string& short_name,
    std::int64_t expected_revision)
{
    auto definition = transaction.getGameDefinitionForUpdate(short_name);
    if(!definition)
    {
        return std::unexpected(std::move(definition.error()));
    }
    if(!*definition)
    {
        return std::unexpected(mw::httpError(404, "Game not found"));
    }
    if((**definition).game.revision != expected_revision)
    {
        return std::unexpected(mw::httpError(
            409, "The game definition changed; reload and try again"));
    }
    return std::move(**definition);
}

mw::E<void> advanceRevision(
    DataSourceTransactionInterface& transaction,
    const std::string& short_name,
    std::int64_t expected_revision)
{
    auto advanced = transaction.incrementGameRevision(
        short_name, expected_revision);
    if(!advanced)
    {
        return std::unexpected(std::move(advanced.error()));
    }
    if(!*advanced)
    {
        return std::unexpected(mw::httpError(
            409, "The game definition changed; reload and try again"));
    }
    return {};
}

} // namespace

GameService::GameService(DataSourceInterface& data_source)
        : data_source_(data_source)
{}

mw::E<std::string> GameService::createGame(
    std::int64_t actor_user_id,
    std::string short_name,
    std::string display_name,
    std::string description,
    GameVisibility visibility)
{
    if(!validShortName(short_name))
    {
        return std::unexpected(mw::Error(GameDefinitionValidationError{
            "short_name",
            "Short name must contain lowercase letters and digits"}));
    }
    auto valid = validateDisplayText(
        display_name, description, markdown_renderer_);
    if(!valid)
    {
        return std::unexpected(std::move(valid.error()));
    }
    auto transaction = data_source_.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto authorized = authorizeAdministrator(
        **transaction, authorization_, actor_user_id);
    if(!authorized)
    {
        return std::unexpected(std::move(authorized.error()));
    }
    auto existing = (*transaction)->getGameDefinitionForUpdate(short_name);
    if(!existing)
    {
        return std::unexpected(std::move(existing.error()));
    }
    if(*existing)
    {
        return std::unexpected(mw::httpError(
            409, "A game with this short name already exists"));
    }
    auto inserted = (*transaction)->insertGame(
        {short_name, std::move(display_name), std::move(description),
         visibility, 1});
    if(!inserted)
    {
        return std::unexpected(std::move(inserted.error()));
    }
    auto committed = (*transaction)->commit();
    if(!committed)
    {
        return std::unexpected(std::move(committed.error()));
    }
    return short_name;
}

mw::E<void> GameService::updateGame(
    std::int64_t actor_user_id,
    const std::string& short_name,
    std::int64_t expected_revision,
    std::string display_name,
    std::string description,
    GameVisibility visibility)
{
    auto valid = validateDisplayText(
        display_name, description, markdown_renderer_);
    if(!valid)
    {
        return std::unexpected(std::move(valid.error()));
    }
    auto transaction = data_source_.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto authorized = authorizeAdministrator(
        **transaction, authorization_, actor_user_id);
    if(!authorized)
    {
        return std::unexpected(std::move(authorized.error()));
    }
    auto definition = readDefinition(
        **transaction, short_name, expected_revision);
    if(!definition)
    {
        return std::unexpected(std::move(definition.error()));
    }
    auto updated = (*transaction)->updateGame(
        short_name, display_name, description, visibility,
        expected_revision);
    if(!updated)
    {
        return std::unexpected(std::move(updated.error()));
    }
    if(!*updated)
    {
        return std::unexpected(mw::httpError(409, "Game update conflict"));
    }
    return (*transaction)->commit();
}

mw::E<void> GameService::removeGame(
    std::int64_t actor_user_id,
    const std::string& short_name,
    std::int64_t expected_revision)
{
    auto transaction = data_source_.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto authorized = authorizeAdministrator(
        **transaction, authorization_, actor_user_id);
    if(!authorized)
    {
        return std::unexpected(std::move(authorized.error()));
    }
    auto definition = readDefinition(
        **transaction, short_name, expected_revision);
    if(!definition)
    {
        return std::unexpected(std::move(definition.error()));
    }
    auto usage = (*transaction)->getGameUsage(short_name);
    if(!usage)
    {
        return std::unexpected(std::move(usage.error()));
    }
    if(usage->card_count != 0 || usage->series_count != 0 ||
       usage->last_number != 0)
    {
        return std::unexpected(mw::httpError(
            409,
            "This game cannot be deleted because it has " +
                std::to_string(usage->card_count) + " card(s), " +
                std::to_string(usage->series_count) + " series, and has " +
                "issued card number " + std::to_string(usage->last_number)));
    }
    auto deleted = (*transaction)->deleteGame(short_name);
    if(!deleted)
    {
        return std::unexpected(std::move(deleted.error()));
    }
    return (*transaction)->commit();
}

mw::E<std::int64_t> GameService::createField(
    std::int64_t actor_user_id,
    const std::string& short_name,
    std::int64_t expected_revision,
    std::string key,
    std::string label,
    GameFieldType type,
    std::vector<std::string> choice_values)
{
    label = std::string(mw::strip(label));
    if(!validFieldKey(key))
    {
        return std::unexpected(mw::Error(GameDefinitionValidationError{
            "key", "Field key is invalid"}));
    }
    if(label.empty() || !validGameText(label))
    {
        return std::unexpected(mw::Error(GameDefinitionValidationError{
            "label", "Field label is invalid"}));
    }
    auto choices = validateChoices(type, choice_values);
    if(!choices)
    {
        return std::unexpected(std::move(choices.error()));
    }
    auto transaction = data_source_.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto authorized = authorizeAdministrator(
        **transaction, authorization_, actor_user_id);
    if(!authorized)
    {
        return std::unexpected(std::move(authorized.error()));
    }
    auto definition = readDefinition(
        **transaction, short_name, expected_revision);
    if(!definition)
    {
        return std::unexpected(std::move(definition.error()));
    }
    if(std::ranges::any_of(
           definition->fields,
           [&key](const GameField& field) { return field.key == key; }))
    {
        return std::unexpected(mw::httpError(
            409, "A field with this key already exists"));
    }
    GameField field{
        0, short_name, std::move(key), std::move(label), type,
        static_cast<std::int64_t>(definition->fields.size()), {}};
    auto inserted = (*transaction)->insertGameField(field, *choices);
    if(!inserted)
    {
        return std::unexpected(std::move(inserted.error()));
    }
    auto advanced = advanceRevision(
        **transaction, short_name, expected_revision);
    if(!advanced)
    {
        return std::unexpected(std::move(advanced.error()));
    }
    auto committed = (*transaction)->commit();
    if(!committed)
    {
        return std::unexpected(std::move(committed.error()));
    }
    return *inserted;
}

mw::E<void> GameService::updateField(
    std::int64_t actor_user_id,
    const std::string& short_name,
    std::int64_t field_id,
    std::int64_t expected_revision,
    std::string label,
    std::vector<std::string> choice_values)
{
    label = std::string(mw::strip(label));
    if(label.empty() || !validGameText(label))
    {
        return std::unexpected(mw::Error(GameDefinitionValidationError{
            "label", "Field label is invalid"}));
    }
    auto transaction = data_source_.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto authorized = authorizeAdministrator(
        **transaction, authorization_, actor_user_id);
    if(!authorized)
    {
        return std::unexpected(std::move(authorized.error()));
    }
    auto definition = readDefinition(
        **transaction, short_name, expected_revision);
    if(!definition)
    {
        return std::unexpected(std::move(definition.error()));
    }
    const GameField* field = findField(*definition, field_id);
    if(field == nullptr)
    {
        return std::unexpected(mw::httpError(404, "Game field not found"));
    }
    auto choices = validateChoices(field->type, choice_values);
    if(!choices)
    {
        return std::unexpected(std::move(choices.error()));
    }
    for(const GameChoice& existing : field->choices)
    {
        const bool retained = std::ranges::any_of(
            *choices,
            [&existing](const GameChoice& choice)
            {
                return choice.value == existing.value;
            });
        if(!retained)
        {
            auto count = (*transaction)->countChoiceUsage(
                field_id, existing.value);
            if(!count)
            {
                return std::unexpected(std::move(count.error()));
            }
            if(*count != 0)
            {
                return std::unexpected(mw::httpError(
                    409,
                    "This choice cannot be deleted because " +
                        std::to_string(*count) + " card(s) use it"));
            }
        }
    }
    auto updated = (*transaction)->updateGameField(
        field_id, label, *choices);
    if(!updated)
    {
        return std::unexpected(std::move(updated.error()));
    }
    auto advanced = advanceRevision(
        **transaction, short_name, expected_revision);
    if(!advanced)
    {
        return std::unexpected(std::move(advanced.error()));
    }
    return (*transaction)->commit();
}

mw::E<void> GameService::removeField(
    std::int64_t actor_user_id,
    const std::string& short_name,
    std::int64_t field_id,
    std::int64_t expected_revision)
{
    auto transaction = data_source_.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto authorized = authorizeAdministrator(
        **transaction, authorization_, actor_user_id);
    if(!authorized)
    {
        return std::unexpected(std::move(authorized.error()));
    }
    auto definition = readDefinition(
        **transaction, short_name, expected_revision);
    if(!definition)
    {
        return std::unexpected(std::move(definition.error()));
    }
    if(findField(*definition, field_id) == nullptr)
    {
        return std::unexpected(mw::httpError(404, "Game field not found"));
    }
    auto count = (*transaction)->countFieldUsage(field_id);
    if(!count)
    {
        return std::unexpected(std::move(count.error()));
    }
    if(*count != 0)
    {
        return std::unexpected(mw::httpError(
            409,
            "This field cannot be deleted because " +
                std::to_string(*count) + " card(s) use it"));
    }
    auto deleted = (*transaction)->deleteGameField(field_id);
    if(!deleted)
    {
        return std::unexpected(std::move(deleted.error()));
    }
    auto advanced = advanceRevision(
        **transaction, short_name, expected_revision);
    if(!advanced)
    {
        return std::unexpected(std::move(advanced.error()));
    }
    return (*transaction)->commit();
}

mw::E<void> GameService::reorderFields(
    std::int64_t actor_user_id,
    const std::string& short_name,
    std::int64_t expected_revision,
    const std::vector<std::int64_t>& field_ids)
{
    auto transaction = data_source_.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto authorized = authorizeAdministrator(
        **transaction, authorization_, actor_user_id);
    if(!authorized)
    {
        return std::unexpected(std::move(authorized.error()));
    }
    auto definition = readDefinition(
        **transaction, short_name, expected_revision);
    if(!definition)
    {
        return std::unexpected(std::move(definition.error()));
    }
    std::unordered_set<std::int64_t> requested(
        field_ids.begin(), field_ids.end());
    if(requested.size() != field_ids.size() ||
       field_ids.size() != definition->fields.size() ||
       std::ranges::any_of(
           definition->fields,
           [&requested](const GameField& field)
           {
               return !requested.contains(field.id);
           }))
    {
        return std::unexpected(mw::httpError(422, "Invalid field order"));
    }
    auto reordered = (*transaction)->updateGameFieldOrder(
        short_name, field_ids);
    if(!reordered)
    {
        return std::unexpected(std::move(reordered.error()));
    }
    auto advanced = advanceRevision(
        **transaction, short_name, expected_revision);
    if(!advanced)
    {
        return std::unexpected(std::move(advanced.error()));
    }
    return (*transaction)->commit();
}