BareGit
#include "subscription_store.hpp"

#include <string>
#include <utility>

#include "database.hpp"
#include "service_error.hpp"

namespace telegrammer
{

namespace
{

mw::E<void> execute(mw::SQLite& db, const std::string& sql)
{
    auto result = db.execute(sql);
    if(!result.has_value())
    {
        return std::unexpected(result.error());
    }
    return {};
}

mw::Error databaseError([[maybe_unused]] const mw::Error& error)
{
    return serviceError(503, "DATABASE_UNAVAILABLE",
                        "The state database is unavailable");
}

mw::E<Subscription> readSubscription(mw::SQLite& db, int64_t id)
{
    auto statement = db.statementFromStr(
        "SELECT id, owner_id, chat_id, callback_url, created_at "
        "FROM subscriptions WHERE id = ?;");
    if(!statement.has_value())
    {
        return std::unexpected(databaseError(statement.error()));
    }
    auto bind_result = statement->bind(id);
    if(!bind_result.has_value())
    {
        return std::unexpected(databaseError(bind_result.error()));
    }
    auto rows = db.eval<int64_t, int64_t, int64_t, std::string, int64_t>(
        std::move(*statement));
    if(!rows.has_value())
    {
        return std::unexpected(databaseError(rows.error()));
    }
    if(rows->empty())
    {
        return std::unexpected(serviceError(
            500, "DATABASE_INCONSISTENT", "Subscription disappeared"));
    }
    const auto& row = (*rows)[0];
    return Subscription{std::get<0>(row), std::get<1>(row),
                        std::get<2>(row), std::get<3>(row),
                        std::get<4>(row)};
}

} // namespace

SubscriptionStore::SubscriptionStore(std::string db_path)
    : db_path_(std::move(db_path))
{}

mw::E<Subscription> SubscriptionStore::add(
    int64_t owner_id, int64_t chat_id, const std::string& callback_url) const
{
    auto db_result = openDatabase(db_path_);
    if(!db_result.has_value())
    {
        return std::unexpected(db_result.error());
    }
    mw::SQLite& db = **db_result;

    auto begin_result = execute(db, "BEGIN IMMEDIATE;");
    if(!begin_result.has_value())
    {
        return std::unexpected(databaseError(begin_result.error()));
    }
    auto rollback = [&db]()
    {
        [[maybe_unused]] auto result = db.execute("ROLLBACK;");
    };

    auto existing = db.statementFromStr(
        "SELECT id FROM subscriptions "
        "WHERE owner_id = ? AND chat_id = ? AND callback_url = ?;");
    if(!existing.has_value())
    {
        rollback();
        return std::unexpected(databaseError(existing.error()));
    }
    auto existing_bind = existing->bind(owner_id, chat_id, callback_url);
    if(!existing_bind.has_value())
    {
        rollback();
        return std::unexpected(databaseError(existing_bind.error()));
    }
    auto existing_rows = db.eval<int64_t>(std::move(*existing));
    if(!existing_rows.has_value())
    {
        rollback();
        return std::unexpected(databaseError(existing_rows.error()));
    }
    if(!existing_rows->empty())
    {
        auto subscription = readSubscription(
            db, std::get<0>((*existing_rows)[0]));
        if(!subscription.has_value())
        {
            rollback();
            return std::unexpected(subscription.error());
        }
        auto commit_result = execute(db, "COMMIT;");
        if(!commit_result.has_value())
        {
            rollback();
            return std::unexpected(databaseError(commit_result.error()));
        }
        return *subscription;
    }

    auto owner_count = db.statementFromStr(
        "SELECT COUNT(*) FROM subscriptions WHERE owner_id = ?;");
    if(!owner_count.has_value())
    {
        rollback();
        return std::unexpected(databaseError(owner_count.error()));
    }
    auto owner_bind = owner_count->bind(owner_id);
    if(!owner_bind.has_value())
    {
        rollback();
        return std::unexpected(databaseError(owner_bind.error()));
    }
    auto owner_rows = db.evalToValue<int64_t>(std::move(*owner_count));
    if(!owner_rows.has_value())
    {
        rollback();
        return std::unexpected(databaseError(owner_rows.error()));
    }
    if(*owner_rows >= 100)
    {
        rollback();
        return std::unexpected(serviceError(
            409, "SUBSCRIPTION_LIMIT", "The key has too many subscriptions"));
    }

    auto total_rows = db.evalToValue<int64_t>(
        "SELECT COUNT(*) FROM subscriptions;");
    if(!total_rows.has_value())
    {
        rollback();
        return std::unexpected(databaseError(total_rows.error()));
    }
    if(*total_rows >= 1000)
    {
        rollback();
        return std::unexpected(serviceError(
            409, "SUBSCRIPTION_LIMIT", "The daemon has too many subscriptions"));
    }

    auto statement = db.statementFromStr(
        "INSERT INTO subscriptions "
        "(owner_id, chat_id, callback_url, created_at) "
        "VALUES (?, ?, ?, ?);");
    if(!statement.has_value())
    {
        rollback();
        return std::unexpected(databaseError(statement.error()));
    }
    auto bind_result = statement->bind(owner_id, chat_id, callback_url,
                                       nowSeconds());
    if(!bind_result.has_value())
    {
        rollback();
        return std::unexpected(databaseError(bind_result.error()));
    }
    auto insert_result = db.execute(std::move(*statement));
    if(!insert_result.has_value())
    {
        rollback();
        return std::unexpected(databaseError(insert_result.error()));
    }
    int64_t id = db.lastInsertRowID();
    auto subscription = readSubscription(db, id);
    if(!subscription.has_value())
    {
        rollback();
        return std::unexpected(subscription.error());
    }
    auto commit_result = execute(db, "COMMIT;");
    if(!commit_result.has_value())
    {
        rollback();
        return std::unexpected(databaseError(commit_result.error()));
    }
    return *subscription;
}

mw::E<std::vector<Subscription>> SubscriptionStore::list(
    int64_t owner_id) const
{
    auto db_result = openDatabase(db_path_);
    if(!db_result.has_value())
    {
        return std::unexpected(db_result.error());
    }
    auto statement = (*db_result)->statementFromStr(
        "SELECT id, owner_id, chat_id, callback_url, created_at "
        "FROM subscriptions WHERE owner_id = ? ORDER BY id;");
    if(!statement.has_value())
    {
        return std::unexpected(databaseError(statement.error()));
    }
    auto bind_result = statement->bind(owner_id);
    if(!bind_result.has_value())
    {
        return std::unexpected(databaseError(bind_result.error()));
    }
    auto rows = (*db_result)->eval<int64_t, int64_t, int64_t, std::string,
                                   int64_t>(std::move(*statement));
    if(!rows.has_value())
    {
        return std::unexpected(databaseError(rows.error()));
    }
    std::vector<Subscription> result;
    result.reserve(rows->size());
    for(const auto& row: *rows)
    {
        result.push_back({std::get<0>(row), std::get<1>(row),
                          std::get<2>(row), std::get<3>(row),
                          std::get<4>(row)});
    }
    return result;
}

mw::E<bool> SubscriptionStore::remove(int64_t owner_id,
                                      int64_t subscription_id) const
{
    auto db_result = openDatabase(db_path_);
    if(!db_result.has_value())
    {
        return std::unexpected(db_result.error());
    }
    auto statement = (*db_result)->statementFromStr(
        "DELETE FROM subscriptions WHERE id = ? AND owner_id = ?;");
    if(!statement.has_value())
    {
        return std::unexpected(databaseError(statement.error()));
    }
    auto bind_result = statement->bind(subscription_id, owner_id);
    if(!bind_result.has_value())
    {
        return std::unexpected(databaseError(bind_result.error()));
    }
    auto delete_result = (*db_result)->execute(std::move(*statement));
    if(!delete_result.has_value())
    {
        return std::unexpected(databaseError(delete_result.error()));
    }
    return (*db_result)->changedRowsCount() > 0;
}

} // namespace telegrammer