#include "db.h"
#include "utility.h"

#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/collection.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/pg_locks.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/hypothesis_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/metadata_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ride_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/walk_object_gateway.h>
#include <yandex/maps/pgpool3utils/pg_advisory_mutex.h>

namespace maps::mrc::ugc_uploader {

using namespace db;

namespace {

// customization (type traits) for queueX() functions
template <class Entity>
struct At;

template <>
struct At<Ride> {
    using Gateway = RideGateway;
    static constexpr auto txnIdKey = "ugc-uploader.txn_id";
    static constexpr auto txnIdCol = table::Ride::txnId;
    static constexpr auto idKey = "ugc-uploader.ride_id";
    static constexpr auto idCol = table::Ride::rideId;
    static constexpr auto getId = &Ride::rideId;
};

template <>
struct At<WalkObject> {
    using Gateway = WalkObjectGateway;
    static constexpr auto txnIdKey = "ugc-uploader.walk_object.txn_id";
    static constexpr auto txnIdCol = table::WalkObject::txnId;
    static constexpr auto idKey = "ugc-uploader.walk_object.id";
    static constexpr auto idCol = table::WalkObject::id;
    static constexpr auto getId = &WalkObject::id;
};

template <class Entity>
constexpr auto queueFilterHead(TId txnId, TId id)
{
    return At<Entity>::txnIdCol == txnId && At<Entity>::idCol > id;
}

template <class Entity>
constexpr auto queueFilterTail(TId txnId)
{
    return At<Entity>::txnIdCol > txnId;
}

template <class Entity>
constexpr auto queueFilter(TId txnId, TId id)
{
    return (queueFilterHead<Entity>(txnId, id)) ||
           (queueFilterTail<Entity>(txnId));
}

sql_chemistry::FiltersCollection featureFilter()
{
    auto result =
        sql_chemistry::FiltersCollection{sql_chemistry::op::Logical::And};
    result.add(!table::Feature::deletedByUser.is(true));
    result.add(!table::Feature::gdprDeleted.is(true));
    result.add(!table::Feature::width.isNull());
    result.add(table::Feature::width > 0);
    result.add(!table::Feature::height.isNull());
    result.add(table::Feature::height > 0);
    return result;
}

}  // namespace

template <class Entity>
bool queuePop(pgpool3::Pool& pool, const Consumer<Entity>& fn)
try {
    auto mutex = pgp3utils::PgAdvisoryXactMutex{
        pool, static_cast<int64_t>(common::LockId::UgcUploader)};
    if (!mutex.try_lock()) {
        return false;
    }
    auto& txn = mutex.writableTxn();
    auto txnId = MetadataGateway{txn}.tryLoadByKey(At<Entity>::txnIdKey, TId{});
    auto id = MetadataGateway{txn}.tryLoadByKey(At<Entity>::idKey, TId{});
    auto entities = typename At<Entity>::Gateway{txn}.load(
        queueFilterHead<Entity>(txnId, id),
        orderBy(At<Entity>::txnIdCol).orderBy(At<Entity>::idCol).limit(1));
    if (entities.empty()) {
        entities = typename At<Entity>::Gateway{txn}.load(
            queueFilterTail<Entity>(txnId),
            orderBy(At<Entity>::txnIdCol).orderBy(At<Entity>::idCol).limit(1));
    }
    if (entities.empty()) {
        return false;
    }
    auto& entity = entities.front();
    fn(entity);
    MetadataGateway{txn}.upsertByKey(At<Entity>::txnIdKey, entity.txnId());
    MetadataGateway{txn}.upsertByKey(At<Entity>::idKey,
                                     std::invoke(At<Entity>::getId, entity));
    txn.commit();
    return true;
}
catch (const maps::Exception& e) {
    WARN() << e;
    return false;
}

template <class Entity>
size_t queueSize(sql_chemistry::Transaction& txn)
{
    auto txnId = MetadataGateway{txn}.tryLoadByKey(At<Entity>::txnIdKey, TId{});
    auto id = MetadataGateway{txn}.tryLoadByKey(At<Entity>::idKey, TId{});
    return
        typename At<Entity>::Gateway{txn}.count(queueFilter<Entity>(txnId, id));
}

// explicit instantiation
template bool queuePop<Ride>(pgpool3::Pool&, const Consumer<Ride>&);
template size_t queueSize<Ride>(sql_chemistry::Transaction&);
template bool queuePop<WalkObject>(pgpool3::Pool&, const Consumer<WalkObject>&);
template size_t queueSize<WalkObject>(sql_chemistry::Transaction&);

auto tryLoadSizedPhoto(sql_chemistry::Transaction& txn, const db::Ride& ride)
    -> std::optional<db::Feature>
{
    if (ride.isDeleted()) {
        return std::nullopt;
    }
    auto allOf = featureFilter();
    allOf.add(table::Feature::dataset == Dataset::Rides);
    allOf.add(table::Feature::userId == ride.userId());
    allOf.add(table::Feature::sourceId == ride.sourceId());
    if (ride.clientId()) {
        allOf.add(table::Feature::clientRideId == *ride.clientId());
    }
    else {
        allOf.add(table::Feature::clientRideId.isNull());
    }
    allOf.add(table::Feature::date.between(ride.startTime(), ride.endTime()));
    return FeatureGateway{txn}.tryLoadOne(
        allOf, orderBy(table::Feature::date).limit(1));
}

auto loadPhotos(sql_chemistry::Transaction& txn, const db::WalkObject& obj)
    -> db::Features
{
    if (!obj.hasUserId()) {
        return {};
    }
    auto allOf = featureFilter();
    allOf.add(table::Feature::userId == obj.userId());
    allOf.add(table::Feature::walkObjectId == obj.id());
    allOf.add(table::Feature::pos.isNotNull());
    allOf.add(table::Feature::heading.isNotNull());
    return FeatureGateway{txn}.load(allOf);
}

}  // namespace maps::mrc::ugc_uploader
