BareGit
#include "collection.h"

#include <algorithm>
#include <chrono>
#include <cstdint>
#include <limits>
#include <utility>

CollectionService::CollectionService(
    DataSourceInterface& data_source,
    ClockInterface& clock,
    mw::CryptoInterface& crypto,
    std::uint32_t maximum_accumulated_pulls)
    : data_source_(data_source),
      clock_(clock),
      crypto_(crypto),
      maximum_accumulated_pulls_(maximum_accumulated_pulls)
{}

std::pair<std::uint32_t, std::int64_t>
CollectionService::effectivePullState(
    const User& user, std::int64_t current_day) const
{
    const std::int64_t elapsed = std::max<std::int64_t>(
        0, current_day - user.pull_refresh_day);
    const std::uint64_t available = std::min<std::uint64_t>(
        maximum_accumulated_pulls_,
        static_cast<std::uint64_t>(user.stored_pulls) +
            static_cast<std::uint64_t>(elapsed));
    return {
        static_cast<std::uint32_t>(available),
        std::max(current_day, user.pull_refresh_day)};
}

mw::E<User> CollectionService::refresh(std::int64_t user_id)
{
    const std::int64_t current_day = utcDay(clock_.now());
    auto transaction = data_source_.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto user = (*transaction)->getUserForUpdate(user_id);
    if(!user)
    {
        return std::unexpected(std::move(user.error()));
    }
    if(!*user)
    {
        return std::unexpected(mw::httpError(404, "User not found"));
    }
    if(!(**user).username)
    {
        return std::unexpected(mw::httpError(
            409, "Username onboarding is required"));
    }
    const auto [available, refresh_day] = effectivePullState(
        **user, current_day);
    auto updated = (*transaction)->updatePullState(
        user_id, available, refresh_day);
    if(!updated)
    {
        return std::unexpected(std::move(updated.error()));
    }
    auto committed = (*transaction)->commit();
    if(!committed)
    {
        return std::unexpected(std::move(committed.error()));
    }
    (**user).stored_pulls = available;
    (**user).pull_refresh_day = refresh_day;
    return std::move(**user);
}

mw::E<PullResult> CollectionService::pull(std::int64_t user_id)
{
    const std::int64_t current_day = utcDay(clock_.now());
    auto transaction = data_source_.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto user = (*transaction)->getUserForUpdate(user_id);
    if(!user)
    {
        return std::unexpected(std::move(user.error()));
    }
    if(!*user)
    {
        return std::unexpected(mw::httpError(404, "User not found"));
    }
    if(!(**user).username)
    {
        return std::unexpected(mw::httpError(
            409, "Username onboarding is required"));
    }
    const auto [available, refresh_day] = effectivePullState(
        **user, current_day);
    auto refreshed = (*transaction)->updatePullState(
        user_id, available, refresh_day);
    if(!refreshed)
    {
        return std::unexpected(std::move(refreshed.error()));
    }
    if(available == 0)
    {
        auto committed = (*transaction)->commit();
        if(!committed)
        {
            return std::unexpected(std::move(committed.error()));
        }
        return std::unexpected(mw::httpError(
            409, "No pulls are available"));
    }
    auto cards = (*transaction)->getPoolCardsForUpdate();
    if(!cards)
    {
        return std::unexpected(std::move(cards.error()));
    }
    auto entries = card_pool_.calculate(*cards);
    if(entries.empty())
    {
        auto committed = (*transaction)->commit();
        if(!committed)
        {
            return std::unexpected(std::move(committed.error()));
        }
        return std::unexpected(mw::httpError(409, "Card pool is empty"));
    }
    auto selected = card_pool_.select(entries, crypto_);
    if(!selected)
    {
        return std::unexpected(std::move(selected.error()));
    }
    auto decremented = (*transaction)->updatePullState(
        user_id, available - 1, refresh_day);
    if(!decremented)
    {
        return std::unexpected(std::move(decremented.error()));
    }
    auto quantity = (*transaction)->incrementHolding(
        user_id, selected->card.id);
    if(!quantity)
    {
        return std::unexpected(std::move(quantity.error()));
    }
    auto committed = (*transaction)->commit();
    if(!committed)
    {
        return std::unexpected(std::move(committed.error()));
    }
    return PullResult{std::move(selected->card), *quantity};
}