#include "db.h"
#include "verification.h"
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/db.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/object.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/location.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/include/object_manager.h>

#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/util.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/recognition_gateway.h>

#include <maps/libs/common/include/make_batches.h>

#include <maps/libs/log8/include/log8.h>

#include <util/generic/iterator_range.h>

namespace maps::mrc::eye {

namespace {

template <typename T>
void makeUnique(std::vector<T>* dataPtr) {
    std::sort(dataPtr->begin(), dataPtr->end(),
        [](const auto& lhs, const auto& rhs) {
            return getId(lhs) < getId(rhs);
        }
    );
    dataPtr->erase(
        std::unique(dataPtr->begin(), dataPtr->end(),
            [](const auto& lhs, const auto& rhs) {
                return getId(lhs) == getId(rhs);
            }
        ),
        dataPtr->end()
    );
}

db::eye::PrimaryDetectionRelations loadTouchedRelations(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds)
{
    using RelationTable = db::eye::table::PrimaryDetectionRelation;

    static const size_t BATCH_SIZE = 10000;

    db::eye::PrimaryDetectionRelations relations;

    for (const auto& batch : maps::common::makeBatches(detectionIds, BATCH_SIZE)) {
        db::TIds batchDetectionIds{batch.begin(), batch.end()};

        auto masterRelations = db::eye::PrimaryDetectionRelationGateway(txn).load(
            RelationTable::primaryDetectionId.in(batchDetectionIds)
        );

        auto slaveRelations = db::eye::PrimaryDetectionRelationGateway(txn).load(
            RelationTable::detectionId.in(batchDetectionIds)
        );

        relations.insert(relations.end(),
            masterRelations.begin(), masterRelations.end()
        );
        relations.insert(relations.end(),
            slaveRelations.begin(), slaveRelations.end()
        );
    }

    makeUnique(&relations);

    return relations;
}

db::eye::Objects loadTouchedObjects(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds)
{
    using ObjectTable = db::eye::table::Object;

    static const size_t BATCH_SIZE = 10000;

    db::eye::Objects objects;

    for (const auto& batch : maps::common::makeBatches(detectionIds, BATCH_SIZE)) {
        db::TIds batchDetectionIds{batch.begin(), batch.end()};

        auto batchObjects = db::eye::ObjectGateway(txn).load(
            ObjectTable::primaryDetectionId.in(batchDetectionIds)
        );

        objects.insert(objects.end(),
            batchObjects.begin(), batchObjects.end()
        );
    }

    makeUnique(&objects);

    return objects;
}

db::eye::ObjectLocations loadTouchedLocations(
    pqxx::transaction_base& txn,
    const db::TIds& objectIds)
{
    using LocationTable = db::eye::table::ObjectLocation;

    static const size_t BATCH_SIZE = 10000;

    db::eye::ObjectLocations locations;

    for (const auto& batch : maps::common::makeBatches(objectIds, BATCH_SIZE)) {
        db::TIds batchObjectIds{batch.begin(), batch.end()};

        auto batchLocations = db::eye::ObjectLocationGateway(txn).load(
            LocationTable::objectId.in(batchObjectIds)
        );

        locations.insert(locations.end(),
            batchLocations.begin(), batchLocations.end()
        );
    }

    makeUnique(&locations);

    return locations;
}

// Проставляет всем связям значение deleted = true и сохраняет их в базу
void removeRelations(
    pqxx::transaction_base& txn,
    db::eye::PrimaryDetectionRelations& relations)
{
    for (db::eye::PrimaryDetectionRelation& relation : relations) {
        relation.setDeleted(true);
    }

    db::eye::PrimaryDetectionRelationGateway(txn).upsertx(relations);
}

// Проставляет всем связям значение deleted = false и сохраняет их в базу
void restoreRelations(
    pqxx::transaction_base& txn,
    db::eye::PrimaryDetectionRelations& relations)
{
    for (db::eye::PrimaryDetectionRelation& relation : relations) {
        relation.setDeleted(false);
    }

    db::eye::PrimaryDetectionRelationGateway(txn).upsertx(relations);
}

// Сохраняет все связи в базу без изменений
void createRelations(
    pqxx::transaction_base& txn,
    db::eye::PrimaryDetectionRelations& relations)
{
    db::eye::PrimaryDetectionRelationGateway(txn).insertx(relations);
}

// Обновляет связи из множества relations соответсвенно внесенным изменениям:
//  1) updatedRelationIds - список id связей, которые были изменены
//  2) relationsToRemove - множество связей, которые были удалены
//  3) relationsToRestore - множесто связей, которые были восстановлены
//  4) relationsToCreate - множество связей, которые были созданы вновь
//
// Алгоритм:
//  1) Удаляем из relations все связи с id из updatedRelationIds
//  2) Добавляем в relations все связи из relationsToRemove,
//     relationsToRestore, relationsToCreate
//
// Возвращает множество связей в актуальном состоянии
db::eye::PrimaryDetectionRelations updateRelations(
    db::eye::PrimaryDetectionRelations relations,
    const db::TIdSet& updatedRelationIds,
    const db::eye::PrimaryDetectionRelations& relationsToRemove,
    const db::eye::PrimaryDetectionRelations& relationsToRestore,
    const db::eye::PrimaryDetectionRelations& relationsToCreate)
{
    relations.erase(
        std::remove_if(relations.begin(), relations.end(),
            [&](const auto& relation) {
                return updatedRelationIds.count(relation.id());
            }
        ),
        relations.end()
    );
    relations.insert(relations.end(),
        relationsToRemove.begin(), relationsToRemove.end()
    );
    relations.insert(relations.end(),
        relationsToRestore.begin(), relationsToRestore.end()
    );
    relations.insert(relations.end(),
        relationsToCreate.begin(), relationsToCreate.end()
    );

    return relations;
}

// Обновляет все связи согласно данным из detectionIdsByPrimaryId
// Возвращает множество связей в актуальном состоянии
db::eye::PrimaryDetectionRelations updateRelations(
    pqxx::transaction_base& txn,
    db::eye::PrimaryDetectionRelations relations,
    const db::IdTo<db::TIdSet>& detectionIdsByPrimaryId)
{
    db::eye::PrimaryDetectionRelations relationsToRemove;
    db::eye::PrimaryDetectionRelations relationsToRestore;
    db::eye::PrimaryDetectionRelations relationsToCreate;

    INFO() << "Searching relations for remove and restore";
    db::TIdSet updatedRelationIds;
    std::set<DetectionIdPair> existingRelations;
    for (const auto& relation : relations) {
        auto detectionIdsIt = detectionIdsByPrimaryId.find(relation.primaryDetectionId());
        if (detectionIdsByPrimaryId.end() == detectionIdsIt) {
            // нет связей с такой главной детекцией
            if (!relation.deleted()) {
                updatedRelationIds.insert(relation.id());
                relationsToRemove.push_back(relation);
            }
            continue;
        }
        const db::TIdSet& detectionIds = detectionIdsIt->second;
        if (detectionIds.count(relation.detectionId()) == 0) {
            // у этой детекции теперь нет связи со старой главной детекцией
            if (!relation.deleted()) {
                updatedRelationIds.insert(relation.id());
                relationsToRemove.push_back(relation);
            }
            continue;
        }
        // связь primaryId -> detectionId есть в старом и новом варианте
        // необходимо ее восстановить, если она была ранее удалена
        if (relation.deleted()) {
            updatedRelationIds.insert(relation.id());
            relationsToRestore.push_back(relation);
        }
        // если дошли до сюда, то это значит, что связь есть в старом
        // и новом варианте, и она не удалена, значит надо все оставить
        // как есть. В любом случае (удалена связь или нет) связь уже есть
        // в базе, поэтому не надо ее создавать заново
        existingRelations.insert({relation.primaryDetectionId(), relation.detectionId()});
    }

    INFO() << "Making new relations";
    for (const auto& [primaryId, detectionIds] : detectionIdsByPrimaryId) {
        for (db::TId detectionId : detectionIds) {
            if (primaryId == detectionId) {
                // связи такого типа не хранятся в базе
                continue;
            }
            if (existingRelations.count({primaryId, detectionId}) != 0) {
                // связь уже есть в базе
                continue;
            }

            relationsToCreate.emplace_back(primaryId, detectionId);
        }
    }

    INFO() << "Removing " << relationsToRemove.size() << " relations";
    removeRelations(txn, relationsToRemove);
    INFO() << "Restoring " << relationsToRestore.size() << " relations";
    restoreRelations(txn, relationsToRestore);
    INFO() << "Creating " << relationsToCreate.size() << " relations";
    createRelations(txn, relationsToCreate);

    return updateRelations(
        std::move(relations), updatedRelationIds,
        relationsToRemove, relationsToRestore, relationsToCreate
    );
}

std::optional<chrono::TimePoint>
evalObjectDisappearenceDate(
    const db::IdTo<db::TIdSet>& detectionIdsByPrimaryId,
    const db::IdTo<chrono::TimePoint>& detectionIdToDisappearenceDate,
    const db::eye::Object& object)
{
    std::optional<chrono::TimePoint> disappearenceDate;
    auto updateDisappearenceDateByDetection = [&](db::TId detectionId) {
        auto it = detectionIdToDisappearenceDate.find(detectionId);
        if (it != detectionIdToDisappearenceDate.end()) {
            if (!disappearenceDate.has_value()) {
                disappearenceDate = it->second;
            } else {
                disappearenceDate =
                    std::min(it->second, disappearenceDate.value());
            }
        }
    };

    updateDisappearenceDateByDetection(object.primaryDetectionId());
    if (detectionIdsByPrimaryId.count(object.primaryDetectionId())) {
        for (auto detectionId : detectionIdsByPrimaryId.at(object.primaryDetectionId())) {
            updateDisappearenceDateByDetection(detectionId);
        }
    }
    return disappearenceDate;
}

/// Returns object in actual state and flag that it has been changed
std::pair<db::eye::Object, bool>
updateObject(
    const db::IdTo<db::TIdSet>& detectionIdsByPrimaryId,
    const db::IdTo<chrono::TimePoint>& detectionIdToDisappearenceDate,
    db::eye::Object&& object)
{
    bool wasUpdated = false;
    if (detectionIdsByPrimaryId.count(object.primaryDetectionId()) == 0) {
        // New clusters does not have object's primary detection as
        // a cluster head. So delete the object
        if (!object.deleted()) {
            object.setDeleted(true);
            wasUpdated = true;
        }
    } else if (object.deleted()) {
        object.setDeleted(false);
        wasUpdated = true;
    }

    const auto disappearedAt =
        evalObjectDisappearenceDate(detectionIdsByPrimaryId,
            detectionIdToDisappearenceDate,
            object);
    if (object.disappearedAt() != disappearedAt) {
        object.setDisappearedAt(disappearedAt);
        wasUpdated = true;
    }

    return std::make_pair(std::move(object), wasUpdated);
}

// Обновляет состояние объектов в базе соответственно detectionIdsByPrimaryId
// Возвращает множество объектов в актуальном состоянии
db::eye::Objects updateObjects(
    pqxx::transaction_base& txn,
    db::eye::Objects objects,
    const DetectionStore& detectionStore,
    const db::IdTo<db::TIdSet>& detectionIdsByPrimaryId,
    const db::IdTo<chrono::TimePoint>& detectionIdToDisappearenceDate)
{
    db::eye::Objects changedObjects;
    db::eye::Objects unchangedObjects;

    INFO() << "Searching objects to remove and restore";
    db::TIdSet updatedObjectIds;
    db::TIdSet existingPrimaryIds;
    for (db::eye::Object& object : objects) {
        // в любом случае объект уже есть в базе и создавать новый не надо
        existingPrimaryIds.insert(object.primaryDetectionId());
        auto [updatedObject, wasUpdated] = updateObject(
            detectionIdsByPrimaryId,
            detectionIdToDisappearenceDate,
            std::move(object));

        if (wasUpdated) {
            changedObjects.push_back(std::move(updatedObject));
        } else {
            unchangedObjects.push_back(std::move(updatedObject));
        }
    }

    INFO() << "Making new objects";
    for (const auto& [primaryId, _] : detectionIdsByPrimaryId) {
        if (existingPrimaryIds.count(primaryId) != 0) {
            // объект уже есть в базе
            continue;
        }
        changedObjects.push_back(makeObject(detectionStore, primaryId));
    }

    INFO() << "Saving " << changedObjects.size() << " to database";
    db::eye::ObjectGateway{txn}.upsertx(changedObjects);

    changedObjects.insert(
        changedObjects.end(), unchangedObjects.begin(), unchangedObjects.end());
    return changedObjects;
}

void updateLocations(
    pqxx::transaction_base& txn,
    db::eye::ObjectLocations& locations)
{
    db::eye::ObjectLocationGateway(txn).upsertx(locations);
}

void createLocations(
    pqxx::transaction_base& txn,
    db::eye::ObjectLocations& locations)
{
    db::eye::ObjectLocationGateway(txn).insertx(locations);
}

template <typename Items>
void removeDeleted(Items* items) {
    items->erase(
        std::remove_if(items->begin(), items->end(),
            [](const auto& item) {
                return item.deleted();
            }
        ),
        items->end()
    );
}

// Обновляет положение неудаленных объектов из objects
// соответственно неудаленным связям из relations
void updateLocations(
    pqxx::transaction_base& txn,
    const db::eye::PrimaryDetectionRelations& relations,
    const db::eye::Objects& objects,
    const DetectionStore& detectionStore)
{
    db::IdTo<db::TIdSet> relationMap;
    for (const auto& relation : relations) {
        relationMap[relation.primaryDetectionId()].insert(relation.primaryDetectionId());
        relationMap[relation.primaryDetectionId()].insert(relation.detectionId());
    }

    db::TIds objectIds;
    db::IdTo<db::TId> objectIdToPrimaryId;
    for (const auto& object : objects) {
        objectIds.push_back(object.id());
        objectIdToPrimaryId[object.id()] = object.primaryDetectionId();

        relationMap[object.primaryDetectionId()].insert(object.primaryDetectionId());
    }

    db::eye::ObjectLocations locations = loadTouchedLocations(txn, objectIds);

    db::eye::ObjectLocations locationsToUpdate;
    db::eye::ObjectLocations locationsToCreate;

    INFO() << "Searching updated locations";
    db::TIdSet existsLocationObjectIds;
    for (const db::eye::ObjectLocation& location : locations) {
        const db::TId primaryId = objectIdToPrimaryId.at(location.objectId());
        const db::TIdSet& detectionIds = relationMap.at(primaryId);

        Location newLocation = makeObjectLocation(detectionStore, detectionIds);

        if (areDifferent(
                location, newLocation,
                ObjectManager::POSITION_TOLERANCE_METERS,
                ObjectManager::ROTATION_TOLERANCE))
        {
            db::eye::ObjectLocation newObjectLocation = location;
            newObjectLocation.setMercatorPos(newLocation.mercatorPosition);
            newObjectLocation.setRotation(newLocation.rotation);

            locationsToUpdate.push_back(newObjectLocation);
        }

        existsLocationObjectIds.insert(location.objectId());
    }

    INFO() << "Making new locations";
    for (const auto& [objectId, primaryId] : objectIdToPrimaryId) {
        if (existsLocationObjectIds.count(objectId) != 0) {
            // позиция для этого объекта уже есть в базе
            continue;
        }

        const db::TIdSet& detectionIds = relationMap.at(primaryId);
        Location location = makeObjectLocation(detectionStore, detectionIds);
        locationsToCreate.emplace_back(
            objectId,
            location.mercatorPosition,
            location.rotation
        );
    }

    INFO() << "Updating " << locationsToUpdate.size() << " locations";
    updateLocations(txn, locationsToUpdate);
    INFO() << "Creating " << locationsToCreate.size() << " locations";
    createLocations(txn, locationsToCreate);
}

} // namespace

db::eye::Objects saveObjects(
    pqxx::transaction_base& txn,
    const db::IdTo<db::TIdSet>& detectionIdsByPrimaryId,
    const db::IdTo<chrono::TimePoint>& detectionIdToDisappearenceDate)
{
    db::TIds detectionIds;
    for (const auto& [primaryId, relatedIds] : detectionIdsByPrimaryId) {
        detectionIds.push_back(primaryId);
        detectionIds.insert(detectionIds.end(),
            relatedIds.begin(), relatedIds.end()
        );
    }

    DetectionStore detectionStore;
    detectionStore.extendByDetectionIds(txn, detectionIds);

    auto relations = loadTouchedRelations(txn, detectionIds);
    auto objects = loadTouchedObjects(txn, detectionIds);

    INFO() << "Updating relations";
    relations = updateRelations(txn, std::move(relations), detectionIdsByPrimaryId);
    INFO() << "Updating objects";
    objects = updateObjects(
        txn,
        std::move(objects),
        detectionStore,
        detectionIdsByPrimaryId,
        detectionIdToDisappearenceDate);
    INFO() << "Updating locations";
    removeDeleted(&relations);
    removeDeleted(&objects);
    updateLocations(txn, relations, objects, detectionStore);
    return objects;
}

namespace {

std::vector<std::pair<db::TId, db::TId>>
loadDetectionPrimaryTxn(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds)
{
    namespace table = db::eye::table;
    std::vector<std::pair<db::TId, db::TId>> detectionIdTxnIdPairs;

    auto objects = db::eye::ObjectGateway(txn).load(
        table::Object::primaryDetectionId.in(detectionIds)
            && !table::Object::deleted
            && table::Object::primaryDetectionId == table::Detection::id
            && !table::Detection::deleted
    );
    for (const auto& object : objects) {
        detectionIdTxnIdPairs.emplace_back(object.primaryDetectionId(), object.txnId());
    }
    return detectionIdTxnIdPairs;
}

std::vector<std::pair<db::TId, db::TId>>
loadDetectionSlaveTxn(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds)
{
    namespace table = db::eye::table;
    std::vector<std::pair<db::TId, db::TId>> detectionIdTxnIdPairs;

    auto relations = db::eye::PrimaryDetectionRelationGateway(txn).load(
        table::PrimaryDetectionRelation::detectionId.in(detectionIds)
            && !table::PrimaryDetectionRelation::deleted
            && table::PrimaryDetectionRelation::primaryDetectionId == table::Detection::id
            && !table::Detection::deleted
            && table::Detection::id == table::Object::primaryDetectionId
            && !table::Object::deleted
    );

    for (const auto& relation : relations) {
        detectionIdTxnIdPairs.emplace_back(relation.detectionId(), relation.txnId());
        detectionIdTxnIdPairs.emplace_back(relation.primaryDetectionId(), relation.txnId());
    }

    return detectionIdTxnIdPairs;
}

std::vector<std::pair<db::TId, db::TId>>
loadDetectionRelationTxn(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds)
{
    namespace table = db::eye::table;
    std::vector<std::pair<db::TId, db::TId>> detectionIdTxnIdPairs;

    auto relations = db::eye::DetectionRelationGateway(txn).load(
        (table::DetectionRelation::masterDetectionId.in(detectionIds)
            || table::DetectionRelation::slaveDetectionId.in(detectionIds))
            && !table::DetectionRelation::deleted
    );

    for (const auto& relation : relations) {
        detectionIdTxnIdPairs.emplace_back(relation.masterDetectionId(), relation.txnId());
        detectionIdTxnIdPairs.emplace_back(relation.slaveDetectionId(), relation.txnId());
    }

    return detectionIdTxnIdPairs;
}

void mergePreservingMaxValue(const std::vector<std::pair<db::TId, db::TId>>& src, db::IdTo<db::TId>& dst)
{
    for (auto [id, value] : src) {
        auto it = dst.find(id);
        if (it == dst.end()) {
            dst.emplace(id, value);
        } else if (it->second < value) {
            it->second = value;
        }
    }
}

db::IdTo<db::TId> calculateDetectionIdToMaxOutputTxnId(
    pqxx::transaction_base& txn,
    const std::vector<TxnIdDetectionGroupId>& txnDetectionGroupIds)
{
    constexpr size_t BATCH_SIZE = 1000;
    db::IdTo<db::TId> detectionIdToTxnId;
    for (const auto& batch : maps::common::makeBatches(txnDetectionGroupIds, BATCH_SIZE)) {
        db::TIds batchDetectionIds;
        for (const auto& txnDetectionGroupId : MakeIteratorRange(batch.begin(), batch.end())) {
            const auto detectionIds = extractDetectionIds(txnDetectionGroupId.txnDetectionIds);
            batchDetectionIds.insert(batchDetectionIds.end(),
                detectionIds.begin(), detectionIds.end());
        }
        mergePreservingMaxValue(loadDetectionPrimaryTxn(txn, batchDetectionIds), detectionIdToTxnId);
        mergePreservingMaxValue(loadDetectionSlaveTxn(txn, batchDetectionIds), detectionIdToTxnId);
        mergePreservingMaxValue(loadDetectionRelationTxn(txn, batchDetectionIds), detectionIdToTxnId);
    }
    return detectionIdToTxnId;
}

std::vector<std::pair<db::TId, db::TId>>
loadExistingClusteredDetectionPairs(
    pqxx::transaction_base& txn,
    const std::vector<std::pair<db::TId, db::TId>>& detectionIdPairs
)
{
    constexpr size_t BATCH_SIZE = 500;
    std::vector<std::pair<db::TId, db::TId>> result;
    for (const auto& batch : maps::common::makeBatches(detectionIdPairs, BATCH_SIZE)) {
        sql_chemistry::FiltersCollection filterByDetectionId(sql_chemistry::op::Logical::Or);
        for (const auto& [id1, id2] : MakeIteratorRange(batch.begin(), batch.end())) {
            filterByDetectionId.add(
                db::eye::table::PrimaryDetectionRelation::primaryDetectionId == id1 &&
                db::eye::table::PrimaryDetectionRelation::detectionId == id2
            );
            filterByDetectionId.add(
                db::eye::table::PrimaryDetectionRelation::primaryDetectionId == id2 &&
                db::eye::table::PrimaryDetectionRelation::detectionId == id1
            );
        }
        const auto primaryDetectionRelations =
            db::eye::PrimaryDetectionRelationGateway{txn}.load(
                filterByDetectionId &&
                ! db::eye::table::PrimaryDetectionRelation::deleted
            );
        for (const auto& relation : primaryDetectionRelations) {
            result.emplace_back(relation.primaryDetectionId(), relation.detectionId());
        }
    }
    return result;
}

db::TIdSet
findDetectionIdsWithVerificationResultsCoherentToClusters(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds
)
{
    db::TIdSet result;
    db::TIdSet incoherentDetectionIds;
    const auto verificationIndex = loadVerifiedMatches(txn, detectionIds);
    const auto verifiedDetectionPairs = verificationIndex.referencedPairs();
    const auto existingClusteredDetectionPairs = loadExistingClusteredDetectionPairs(txn, verifiedDetectionPairs);

    for (const auto& [id1, id2] : existingClusteredDetectionPairs) {
        if (auto verificationResult = verificationIndex.get(id1, id2);
                verificationResult.has_value() && verificationResult.value() == false)
        {
            incoherentDetectionIds.insert(id1);
            incoherentDetectionIds.insert(id2);
        }
    }

    std::set<std::pair<db::TId, db::TId>> existingClusteredDetectionPairsSet(
        existingClusteredDetectionPairs.begin(),
        existingClusteredDetectionPairs.end());
    for (const auto& [id1, id2] : verifiedDetectionPairs) {
        if (auto verificationResult = verificationIndex.get(id1, id2);
                verificationResult.has_value() && verificationResult.value() == true)
        {
            if (!existingClusteredDetectionPairsSet.contains(std::make_pair(id1, id2)) &&
                    !existingClusteredDetectionPairsSet.contains(std::make_pair(id2, id1)))
            {
                incoherentDetectionIds.insert(id1);
                incoherentDetectionIds.insert(id2);
            }
        }
    }

    for (const auto detectionId : detectionIds) {
        if (!incoherentDetectionIds.count(detectionId)) {
            result.insert(detectionId);
        }
    }

    return result;
}


} // namespace

db::IdTo<db::TIds> loadGroupIdToDetectionIdsMap(
    pqxx::transaction_base& txn, const db::TIds& detectionGroupIds)
{
    db::IdTo<db::TIds> result;
    const auto detections = db::eye::DetectionGateway(txn).load(
        db::eye::table::Detection::groupId.in(detectionGroupIds)
    );
    for (const auto& detection : detections) {
        result[detection.groupId()].push_back(detection.id());
    }
    return result;
}

db::TIdSet
loadDeletedDetectionIds(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds)
{
    db::TIdSet result;

    constexpr size_t BATCH_SIZE = 1000;
    for (const auto& batch : maps::common::makeBatches(detectionIds, BATCH_SIZE)) {
        auto ids = db::eye::DetectionGateway(txn).loadIds(
            db::eye::table::Detection::id.in({batch.begin(), batch.end()})
                && db::eye::table::Detection::deleted
        );
        result.insert(ids.begin(), ids.end());
    }
    return result;
}


void removeProcessedDetectionGroupIds(
    pqxx::transaction_base& txn,
    std::vector<TxnIdDetectionGroupId>* detectionGroupIdsPtr)
{
    INFO() << "Removing already processed detection groups";

    const auto detectionIdToTxnId = calculateDetectionIdToMaxOutputTxnId(txn, *detectionGroupIdsPtr);
    const auto detectionIds = collectKeys(detectionIdToTxnId);
    const db::TIdSet deletedDetections = loadDeletedDetectionIds(txn, detectionIds);
    const db::TIdSet coherentClusteredDetectionIds =
        findDetectionIdsWithVerificationResultsCoherentToClusters(txn, detectionIds);

    detectionGroupIdsPtr->erase(
        std::remove_if(detectionGroupIdsPtr->begin(), detectionGroupIdsPtr->end(),
            [&] (const auto& txnDetectionGroupId) {
                if (txnDetectionGroupId.txnDetectionIds.empty()) {
                    return false;
                }

                bool allDetectionsProcessed =
                    std::all_of(
                        txnDetectionGroupId.txnDetectionIds.begin(),
                        txnDetectionGroupId.txnDetectionIds.end(),
                        [&](const auto& txnDetectionId) {
                            if (deletedDetections.count(txnDetectionId.detectionId)) {
                                return false;
                            }
                            if (coherentClusteredDetectionIds.count(txnDetectionId.detectionId)) {
                                return true;
                            }
                            auto detectionTxnIt = detectionIdToTxnId.find(txnDetectionId.detectionId);
                            return detectionTxnIt != detectionIdToTxnId.end() &&
                                detectionTxnIt->second > txnDetectionId.txnId;
                        });
                return allDetectionsProcessed;
            }
        ),
        detectionGroupIdsPtr->end()
    );


    INFO() << detectionGroupIdsPtr->size() << " detection groups left";
}

} // namespace maps::mrc::eye
