#pragma once

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/common.h>

#include <maps/libs/sql_chemistry/include/gateway.h>
#include <maps/libs/sql_chemistry/include/system_information.h>

#include <util/generic/algorithm.h>
#include <util/generic/array_ref.h>

#include <utility>
#include <vector>

namespace maps::mrc::db {

template<typename Entity>
class TxnIdAccess {
public:
    static void setTxnId(TId txnId, Entity& entity)
    {
        entity.setTxnId(txnId);
    }
};

namespace detail {

template<typename Entity>
void setTxnIdImpl(TId txnId, std::vector<Entity>& entities)
{
    for (auto& entity: entities) {
        TxnIdAccess<Entity>::setTxnId(txnId, entity);
    }
}

template<typename Entity>
void setTxnIdImpl(TId txnId, TArrayRef<Entity>& entities)
{
    for (auto& entity: entities) {
        TxnIdAccess<Entity>::setTxnId(txnId, entity);
    }
}

template<typename Entity>
void setTxnIdImpl(TId txnId, Entity& entity)
{
    TxnIdAccess<Entity>::setTxnId(txnId, entity);
}

} // namespace detail

template<typename ...Entities>
TId setTxnId(pqxx::transaction_base& txn, Entities& ...entities)
{
    const TId txnId = sql_chemistry::SystemInformation(txn).getCurrentTransactionId();

    ForEach(
        std::tie(entities...),
        [txnId](auto& entities) {
            detail::setTxnIdImpl(txnId, entities);
        }
    );

    return txnId;
}

template <typename Table>
class TxnIdGatewayBase: public sql_chemistry::Gateway<Table> {
public:
    using Base = typename sql_chemistry::Gateway<Table>;
    using Base::Base;
    using Entity = typename Base::Entity;
    using Entities = typename Base::Entities;
    using EntitiesRef = typename Base::EntitiesRef;

    using Id = typename Base::Id;
    using Ids = typename Base::Ids;

    struct Batch {
        Ids ids;
        TId beginTxnId;
        TId endTxnId; // excluded
    };

    template<class T>
    TId insertx(T&& arg) {
        const TId txnId = setTxnId(Base::txn(), arg);
        Base::insert(std::forward<T>(arg));
        return txnId;
    }

    template<class T>
    TId updatex(T&& arg) {
        const TId txnId = setTxnId(Base::txn(), arg);
        Base::update(std::forward<T>(arg));
        return txnId;
    }

    template<class T>
    TId upsertx(T&& arg) {
        const TId txnId = setTxnId(Base::txn(), arg);
        Base::upsert(std::forward<T>(arg));
        return txnId;
    }

    template<class...Clauses>
    inline TIds loadTxnIds(const Clauses& ...clauses)
    {
        return Base::loadColumn(Table::txnId, clauses...);
    }

    Batch loadBatch(TId beginTxnId, size_t limit, const sql_chemistry::Filter& filter)
    {
        const auto result = loadTxnIds(
            Table::txnId >= beginTxnId and filter,
            sql_chemistry::limit(limit).orderBy(Table::txnId)
        );

        if (result.empty()) {
            return {{}, beginTxnId, beginTxnId};
        }

        const TId endTxnId = result.back() + 1;

        return {
            Base::loadIds(filter and Table::txnId >= beginTxnId and Table::txnId < endTxnId),
            beginTxnId,
            endTxnId
        };
    }

    inline Batch loadBatch(TId beginTxnId, size_t limit)
    {
        return loadBatch(beginTxnId, limit, sql_chemistry::AnyFilter());
    }

    inline size_t queueSize(db::TId txnId) { return Base::count(Table::txnId >= txnId); }

    bool queueSizeIsGreaterThan(db::TId txnId, size_t count)
    {
        auto result = loadTxnIds(
            Table::txnId >= txnId,
            sql_chemistry::limit(1)
                .orderBy(Table::txnId)
                .offset(count)
            );
        return !result.empty();
    }

    void upsert(auto&&...) = delete;
    void update(auto&&...) = delete;
    void insert(auto&&...) = delete;

    template <class... Ts>
    void updateWithoutX(Ts&&... args)
    {
        Base::update(std::forward<Ts>(args)...);
    }
};

} // namespace maps::mrc::db
