#include "common.h"
#include "greedy_merge.h"
#include "params.h"
#include "object_relation.h"
#include "verification.h"
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/metadata.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/location.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/object.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/include/object_manager.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/store.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/batch.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/passage.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/db.h>

#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/id.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/util.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/detection/include/store.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/detection/include/match.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/detection/include/cluster.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/detection/include/greedy_clusterizer.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/detection/include/store_utils.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/detection/include/visibility_predictor.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/common/include/stopwatch.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/frame_gateway.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/wikimap/mapspro/services/mrc/libs/db/include/eye/verified_detection_pair_match.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/verified_detection_missing_on_frame_gateway.h>

#include <maps/libs/common/include/make_batches.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/contains.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/sql_chemistry/include/exists.h>

#include <algorithm>
#include <iterator>
#include <optional>
#include <utility>

namespace maps::mrc::eye {

namespace {

constexpr size_t BATCH_SIZE = 1000;

// возвращает пару массивов id детекций
// first - неудаленные детекции
// second - удаленные детекции
std::pair<db::TIds, db::TIds> splitDeletedDetectionIds(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds)
{
    auto detections = db::eye::DetectionGateway(txn).loadByIds(detectionIds);

    db::TIds notDeletedDetectionIds;
    db::TIds deletedDetectionIds;

    for (const auto& detection : detections) {
        if (detection.deleted()) {
            deletedDetectionIds.push_back(detection.id());
        } else {
            notDeletedDetectionIds.push_back(detection.id());
        }
    }

    return {std::move(notDeletedDetectionIds), std::move(deletedDetectionIds)};
}

db::TIds getDetectionIdsAffectedByDeletedDetections(
    pqxx::transaction_base& txn,
    const db::TIds& deletedDetectionIds)
{
    namespace table = db::eye::table;

    auto primaryRelationWithDeletedDetections = db::eye::PrimaryDetectionRelationGateway(txn).load(
        table::PrimaryDetectionRelation::primaryDetectionId.in(deletedDetectionIds)
        and not table::PrimaryDetectionRelation::deleted
    );

    db::TIds detectionIdsToClusterize;
    for (auto& relation : primaryRelationWithDeletedDetections) {
        detectionIdsToClusterize.push_back(relation.detectionId());
    }

    return db::eye::DetectionGateway(txn).loadIds(
        table::Detection::id.in(detectionIdsToClusterize)
        and not table::Detection::deleted
    );
}


db::IdTo<chrono::TimePoint> loadDetectionIdToDisappearenceDate(
    pqxx::transaction_base& txn, const db::TIds& detectionIds)
{
    namespace table = db::eye::table;
    db::IdTo<chrono::TimePoint> result;
    auto verifiedDetectionMissingWithDateVec =
        db::eye::VerifiedDetectionMissingOnFrameGateway{txn}.loadJoined<chrono::TimePoint>(
            std::make_tuple(db::eye::table::Frame::time),
            table::VerifiedDetectionMissingOnFrame::detectionId.in(detectionIds)
                && table::VerifiedDetectionMissingOnFrame::isVisible ==
                    db::eye::VerifiedDetectionMissingOnFrameIsVisible::No
                && table::VerifiedDetectionMissingOnFrame::missingReason ==
                    db::eye::VerifiedDetectionMissingOnFrameMissingReason::Missing
                && table::VerifiedDetectionMissingOnFrame::frameId ==
                    table::Frame::id
        );

    for (const auto& [disappearedAt, verifiedMissing] : verifiedDetectionMissingWithDateVec) {
        auto it = result.find(verifiedMissing.detectionId());
        if (it != result.end()) {
            if (disappearedAt < it->second) {
                it->second = disappearedAt;
            }
        } else {
            result.emplace(verifiedMissing.detectionId(), disappearedAt);
        }
    }
    INFO() << "Loaded " << result.size() << " verified detection disappearences";
    return result;
}

db::TIds
loadDetectionIdsAffectedByDisappearence(
    pqxx::transaction_base& txn,
    const db::IdTo<chrono::TimePoint>& idToDisappearenceDate
)
{
    auto detectionIds = collectKeys(idToDisappearenceDate);

    auto relations = db::eye::PrimaryDetectionRelationGateway(txn).load(
        db::eye::table::PrimaryDetectionRelation::detectionId.in(detectionIds)
        and not db::eye::table::PrimaryDetectionRelation::deleted
    );

    for (const auto& relation : relations) {
        detectionIds.push_back(relation.primaryDetectionId());
    }

    relations = db::eye::PrimaryDetectionRelationGateway(txn).load(
        db::eye::table::PrimaryDetectionRelation::primaryDetectionId.in(detectionIds)
        and not db::eye::table::PrimaryDetectionRelation::deleted
    );

    for (const auto& relation : relations) {
        detectionIds.push_back(relation.detectionId());
    }
    common::sortUnique(detectionIds);

    return db::eye::DetectionGateway(txn).loadIds(
        db::eye::table::Detection::id.in(detectionIds)
            && !db::eye::table::Detection::deleted
    );
}

std::pair<DetectionStore, std::vector<db::TIdSet>>
makeDetectionStoreWithPassages(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds
)
{
    INFO() << "Making passages";
    DetectionStore detectionStore;
    std::vector<db::TIdSet> passages;
    detectionStore.extendByDetectionIds(txn, detectionIds);
    passages = makePassages(
        txn, &detectionStore,
        SPLIT_PASSAGE_PARAMS, PASSAGE_TIME_PAD
    );
    INFO() << detectionStore.detectionIds().size() << " detections in " << passages.size() << " passages";
    return std::make_pair(std::move(detectionStore), std::move(passages));
}

struct BatchProcessingContext {
    db::TIds deletedDetectionIds;
    db::TIds updatedDetectionIds;
    chrono::TimePoint oldestUpdatedDetectionDate;
    // updated detection grouped in passages
    std::vector<db::TIdSet> passagesDetectionIds;
    DetectionStore detectionStore;
    ObjectStore objectStore;
    db::IdTo<chrono::TimePoint> detectionIdToDisappearenceDate;
    VerificationRequestsIndex verificationRequestsIndex;
};


chrono::TimePoint calcOldestDetectionDate(
    const DetectionStore& detectionStore,
    const db::TIds& updatedDetectionIds)
{
    if (updatedDetectionIds.empty()) {
        return {};
    }
    const db::TId oldestUpdatedDetectionId =
        *std::min_element(
            updatedDetectionIds.begin(),
            updatedDetectionIds.end(),
            [&](db::TId id1, db::TId id2) {
                return detectionStore.frameByDetectionId(id1).time() <
                    detectionStore.frameByDetectionId(id2).time();
            }
        );

    return detectionStore.frameByDetectionId(oldestUpdatedDetectionId).time();
}

BatchProcessingContext
makeProcessingContext(pqxx::transaction_base& txn, const db::TIds& detectionIds)
{
    auto [updatedDetectionIds, deletedDetectionIds] = splitDeletedDetectionIds(txn, detectionIds);

    db::TIds detectionIdsToClusterize =
        getDetectionIdsAffectedByDeletedDetections(txn, deletedDetectionIds);
    updatedDetectionIds.insert(updatedDetectionIds.end(),
        detectionIdsToClusterize.begin(), detectionIdsToClusterize.end()
    );

    auto detectionIdToDisappearenceDate = loadDetectionIdToDisappearenceDate(txn, detectionIds);
    db::TIds detectionIdsAffectedByDisappearence =
        loadDetectionIdsAffectedByDisappearence(txn, detectionIdToDisappearenceDate);
    updatedDetectionIds.insert(updatedDetectionIds.end(),
        detectionIdsAffectedByDisappearence.begin(), detectionIdsAffectedByDisappearence.end()
    );

    auto [detectionStore, passagesDetectionIds] =
        makeDetectionStoreWithPassages(txn, updatedDetectionIds);

    auto oldestUpdatedDetectionDate =
        calcOldestDetectionDate(detectionStore, updatedDetectionIds);

    auto objectStore =
        loadExistingObjectsToMatch(txn, detectionStore, oldestUpdatedDetectionDate);

    const db::TIdSet allDetectionIdSet = detectionStore.detectionIds();
    detectionIdToDisappearenceDate = loadDetectionIdToDisappearenceDate(
        txn, db::TIds{allDetectionIdSet.begin(), allDetectionIdSet.end()});

    const auto referencedDetectionIdSet = detectionStore.detectionIds();
    auto verificationRequestsIndex = loadVerifiedMatches(
            txn,
            db::TIds{referencedDetectionIdSet.begin(), referencedDetectionIdSet.end()});

    return {
        .deletedDetectionIds = std::move(deletedDetectionIds),
        .updatedDetectionIds = std::move(updatedDetectionIds),
        .oldestUpdatedDetectionDate = oldestUpdatedDetectionDate,
        .passagesDetectionIds = std::move(passagesDetectionIds),
        .detectionStore = std::move(detectionStore),
        .objectStore = std::move(objectStore),
        .detectionIdToDisappearenceDate = std::move(detectionIdToDisappearenceDate),
        .verificationRequestsIndex = std::move(verificationRequestsIndex)
    };
}

void deleteObjectsWithDeletedPrimaryDetection(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds)
{
    namespace table = db::eye::table;

    auto objectsToDelete = db::eye::ObjectGateway(txn).load(
        table::Object::primaryDetectionId.in(detectionIds)
        and not table::Object::deleted
    );

    for (auto& object : objectsToDelete) {
        object.setDeleted(true);
    }
    db::eye::ObjectGateway(txn).upsertx(objectsToDelete);
}

void deleteRelationsWithDeletedPrimaryDetection(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds)
{
    namespace table = db::eye::table;

    auto primaryRelationToDelete = db::eye::PrimaryDetectionRelationGateway(txn).load(
        table::PrimaryDetectionRelation::primaryDetectionId.in(detectionIds)
        and not table::PrimaryDetectionRelation::deleted
    );

    for (auto& relation : primaryRelationToDelete) {
        relation.setDeleted(true);
    }
    db::eye::PrimaryDetectionRelationGateway(txn).upsertx(primaryRelationToDelete);
}

// Возвращает набор главных детекций, у которых
// удалились какие-то связи
db::TIds deleteRelationsWithDeletedDetection(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds)
{
    namespace table = db::eye::table;

    auto relationToDelete = db::eye::PrimaryDetectionRelationGateway(txn).load(
        table::PrimaryDetectionRelation::detectionId.in(detectionIds)
        and not table::PrimaryDetectionRelation::deleted
    );

    db::TIdSet touchedPrimaryIds;
    for (auto& relation : relationToDelete) {
        touchedPrimaryIds.insert(relation.primaryDetectionId());
        relation.setDeleted(true);
    }
    db::eye::PrimaryDetectionRelationGateway(txn).upsertx(relationToDelete);

    return {touchedPrimaryIds.begin(), touchedPrimaryIds.end()};
}


std::pair<db::eye::Objects, db::eye::ObjectLocations>
loadObjectsAndLocationsWithPrimaryDetection(
    pqxx::transaction_base& txn,
    const db::TIds& primaryIds)
{
    namespace table = db::eye::table;

    auto objects = db::eye::ObjectGateway(txn).load(
        table::Object::primaryDetectionId.in(primaryIds)
        and not table::Object::deleted
    );

    db::TIds objectIds;
    for (const auto& object : objects) {
        objectIds.push_back(object.id());
    }

    auto locations = db::eye::ObjectLocationGateway(txn).load(
        table::ObjectLocation::objectId.in(objectIds)
    );

    return {std::move(objects), std::move(locations)};
}

std::pair<db::TIds, db::IdTo<db::TIdSet>>
splitDetectionsByObjectId(
    const db::eye::PrimaryDetectionRelations& relations,
    const db::IdTo<db::TId>& objectIdByPrimaryId)
{
    db::TIds detectionIds;
    db::IdTo<db::TIdSet> detectionIdsByObjectId;
    for (const auto& relation : relations) {
        db::TId objectId = objectIdByPrimaryId.at(relation.primaryDetectionId());
        detectionIdsByObjectId[objectId].insert(relation.detectionId());
        detectionIds.push_back(relation.detectionId());
    }
    for (const auto& [primaryId, objectId] : objectIdByPrimaryId) {
        detectionIdsByObjectId[objectId].insert(primaryId);
        detectionIds.push_back(primaryId);
    }

    return {std::move(detectionIds), std::move(detectionIdsByObjectId)};
}

// Пересчитывает положение объектов с указанными главными детекциями
void updateObjectLocationsWithPrimaryDetection(
    pqxx::transaction_base& txn,
    const db::TIds& primaryIds)
{
    namespace table = db::eye::table;

    auto [objects, locations]
        = loadObjectsAndLocationsWithPrimaryDetection(txn, primaryIds);

    db::IdTo<db::TId> objectIdByPrimaryId;
    for (const auto& object : objects) {
        objectIdByPrimaryId[object.primaryDetectionId()] = object.id();
    }

    auto relations = db::eye::PrimaryDetectionRelationGateway(txn).load(
        table::PrimaryDetectionRelation::primaryDetectionId.in(primaryIds)
        and not table::PrimaryDetectionRelation::deleted
    );

    auto [detectionIds, detectionIdsByObjectId]
        = splitDetectionsByObjectId(relations, objectIdByPrimaryId);

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

    db::eye::ObjectLocations diff;
    for (auto& location : locations) {
        const auto& objectDetectionIds = detectionIdsByObjectId.at(location.objectId());

        Location newLocation = makeObjectLocation(store, objectDetectionIds);
        bool update = areDifferent(
            location,
            newLocation,
            ObjectManager::POSITION_TOLERANCE_METERS,
            ObjectManager::ROTATION_TOLERANCE
        );

        if (update) {
            diff.emplace_back(
                location.objectId(),
                newLocation.mercatorPosition, newLocation.rotation
            );
        }
    }

    db::eye::ObjectLocationGateway(txn).upsertx(diff);
}

void processDeletedDetections(
    pqxx::transaction_base& txn,
    const db::TIds& detectionIds)
{
    namespace table = db::eye::table;

    deleteObjectsWithDeletedPrimaryDetection(txn, detectionIds);
    deleteRelationsWithDeletedPrimaryDetection(txn, detectionIds);

    db::TIds touchedPrimaryIds
        = deleteRelationsWithDeletedDetection(txn, detectionIds);

    updateObjectLocationsWithPrimaryDetection(txn, touchedPrimaryIds);
}

db::TIds filterDetectionsWithRelationsBatch(pqxx::transaction_base& txn, const db::TIds& detectionIds)
{
    return db::eye::DetectionGateway(txn).loadIds(
        db::eye::table::Detection::id.in(detectionIds) &&
        sql_chemistry::existsIn<db::eye::table::DetectionRelation>(
            db::eye::table::DetectionRelation::masterDetectionId == db::eye::table::Detection::id ||
            db::eye::table::DetectionRelation::slaveDetectionId == db::eye::table::Detection::id
        )
    );
}

db::TIds filterDetectionsWithRelations(pqxx::transaction_base& txn, const db::TIds& detectionIds)
{

    db::TIds result;
    for (const auto& batch : maps::common::makeBatches(detectionIds, BATCH_SIZE)) {
        db::TIds detectionIdsBatch =
            filterDetectionsWithRelationsBatch(txn, db::TIds{batch.begin(), batch.end()});
        result.insert(result.end(), detectionIdsBatch.begin(), detectionIdsBatch.end());
    }
    return result;
}

db::TIds evalObjectIdsAffectedByDetectionsBatch(pqxx::transaction_base& txn, const db::TIds& detectionIds)
{
    return db::eye::ObjectGateway(txn).loadIds(
        db::eye::table::Object::primaryDetectionId.in(detectionIds) ||
        sql_chemistry::existsIn<db::eye::table::PrimaryDetectionRelation>(
            db::eye::table::PrimaryDetectionRelation::primaryDetectionId
                == db::eye::table::Object::primaryDetectionId &&
            db::eye::table::PrimaryDetectionRelation::detectionId.in(detectionIds)
        )
    );
}

db::TIds evalObjectIdsAffectedByDetections(pqxx::transaction_base& txn, const db::TIds& detectionIds)
{
    db::TIdSet result;
    for (const auto& batch : maps::common::makeBatches(detectionIds, BATCH_SIZE)) {
        db::TIds objectIdsBatch =
            evalObjectIdsAffectedByDetectionsBatch(txn, db::TIds{batch.begin(), batch.end()});
        result.insert(objectIdsBatch.begin(), objectIdsBatch.end());
    }
    return {result.begin(), result.end()};
}

db::TIds evalObjectIdsWithAffectedRelations(pqxx::transaction_base& txn, const db::TIds& detectionIds)
{
    db::TIds detectionIdsWithRelations =
        filterDetectionsWithRelations(txn, detectionIds);
    return evalObjectIdsAffectedByDetections(txn, detectionIdsWithRelations);
}

bool detectionDisappearedBeforeOther(
    const BatchProcessingContext& ctx,
    db::TId detectionId,
    db::TId otherDetectionId)
{
    auto it = ctx.detectionIdToDisappearenceDate.find(detectionId);
    if (it == ctx.detectionIdToDisappearenceDate.end()) {
        return false;
    }
    auto otherDetectionCapturedAt =
        ctx.detectionStore.frameByDetectionId(otherDetectionId).time();
    return it->second < otherDetectionCapturedAt;
};

PatchedMatcher makeVerifiedMatcher(
    const DetectionMatcher* baseMatcher,
    const BatchProcessingContext& ctx)
{
    return PatchedMatcher(
        baseMatcher,
        [&](db::TId id1, db::TId id2) -> std::optional<MatchedFrameDetection::Verdict> {
            if (detectionDisappearedBeforeOther(ctx, id1, id2) ||
                    detectionDisappearedBeforeOther(ctx, id2, id1))
            {
                return MatchedFrameDetection::Verdict::No;
            }

            if (!ctx.verificationRequestsIndex.contains(id1, id2)) {
                return std::nullopt;
            }
            const auto& value = ctx.verificationRequestsIndex.get(id1, id2);

            if (!value.has_value()) {
                return std::nullopt;
            }
            return value.value() ? MatchedFrameDetection::Verdict::Yes
                : MatchedFrameDetection::Verdict::No;
        });
}

struct UpdateDetectionClustersResult {
    db::IdTo<db::TIdSet> detectionIdsByPrimaryId;
    std::vector<MatchedObjects> matchedObjects;
};

void deleteForbiddenMatches(
    ObjectsInPassage& existingObjects,
    const VerificationRequestsIndex& verifiedDetectionMatches)
{
    for (auto& idSetPair : existingObjects.detectionIdsByPrimaryId) {
        auto primaryId = idSetPair.first;
        auto& idSet = idSetPair.second;
        std::erase_if(idSet,
            [&](const auto id) {
                return verifiedDetectionMatches.contains(primaryId, id) &&
                    verifiedDetectionMatches.get(primaryId, id) == false;
            });
    }
}


class FrameSearcher {
public:
    FrameSearcher(const DetectionStore& detectionStore)
    {
        for (const auto& [_, frame] : detectionStore.frameById()) {
            if (frame.deleted()) {
                continue;
            }
            const auto& frameLocation = detectionStore.locationByFrameId(frame.id());
            searcher_.insert(&frameLocation.mercatorPos(), frame.id());
        }
        searcher_.build();
    }

    db::TIds find(
        const geolib3::Point2& pos,
        double radiusMercMeters) const
    {
        const geolib3::BoundingBox searchBox(pos, 2 * radiusMercMeters, 2 * radiusMercMeters);
        const auto searchResult = searcher_.find(searchBox);

        db::TIds result;
        for (auto it = searchResult.first; it != searchResult.second; it++) {
            result.push_back(it->value());
        }

        return result;
    }

private:
    geolib3::StaticGeometrySearcher<geolib3::Point2, db::TId> searcher_;
};

std::optional<chrono::TimePoint> evalMinDisappearenceDate(
    const db::IdTo<chrono::TimePoint>& detectionIdToDisappearenceDate,
    const db::TIdSet& detectionIds)
{
    std::optional<chrono::TimePoint> date;
    for (auto detectionId: detectionIds) {
        if (detectionIdToDisappearenceDate.contains(detectionId)) {
            if (date.has_value()) {
                date = std::min(
                    date.value(), detectionIdToDisappearenceDate.at(detectionId));
            } else {
                date = detectionIdToDisappearenceDate.at(detectionId);
            }
        }
    }
    return date;
}

chrono::TimePoint
evalMaxFrameDate(
    const DetectionStore& store,
    const db::TIdSet& detectionIds)
{
    chrono::TimePoint result{};
    for (auto detectionId : detectionIds) {
        result = std::max(result, store.frameByDetectionId(detectionId).time());
    }
    return result;
}

bool firstDetectionsDisappearedBeforeOther(
    const BatchProcessingContext& ctx,
    const db::TIdSet& firstDetectionIds,
    const db::TIdSet& otherDetectionIds)
{
    if (auto disappearedAt = evalMinDisappearenceDate(ctx.detectionIdToDisappearenceDate, firstDetectionIds)) {
        return disappearedAt.value() <
            evalMaxFrameDate(ctx.detectionStore, otherDetectionIds);
    }
    return false;
}

bool containsNegativeVerification(
    const VerificationRequestsIndex& verifiedRequestsIndex,
    const db::TIds& ids1,
    const db::TIds& ids2)
{
    for (auto id1 : ids1) {
        for (auto id2 : ids2) {
            if (verifiedRequestsIndex.contains(id1, id2)) {
                const auto& value = verifiedRequestsIndex.get(id1, id2);
                if (value.has_value() && value.value() == false) {
                    return true;
                }
            }
        }
    }
    return false;
}

std::function<bool(const db::TIdSet&, const db::TIdSet&)>
makeDetectionsMergeVerifier(const BatchProcessingContext& ctx)
{
    const db::TIdSet restrictedDetections =
        ctx.verificationRequestsIndex.referencedDetections();

    return [&, restrictedDetections](const db::TIdSet& detectionIds1,
               const db::TIdSet& detectionIds2) {
        if (hasDifferentDetectionsOnSameFrame(
                ctx.detectionStore, detectionIds1, detectionIds2)) {
            return false;
        }

        const auto restrictedDetections1 =
            intersect(restrictedDetections, detectionIds1);
        const auto restrictedDetections2 =
            intersect(restrictedDetections, detectionIds2);
        if (!restrictedDetections1.empty() && !restrictedDetections2.empty() &&
            containsNegativeVerification(
                ctx.verificationRequestsIndex,
                restrictedDetections1,
                restrictedDetections2)) {
            return false;
        }

        if (firstDetectionsDisappearedBeforeOther(ctx, detectionIds1, detectionIds2) ||
                firstDetectionsDisappearedBeforeOther(ctx, detectionIds2, detectionIds1))
        {
            return false;
        }
        return true;
    };
}

UpdateDetectionClustersResult
evalUpdatedDetectionClusters(
    const FrameMatcher& frameMatcher,
    const DetectionMatcher& detectionMatcher,
    const DetectionClusterizer& clusterizer,
    const BatchProcessingContext& ctx)
{
    if (ctx.passagesDetectionIds.empty()) {
        return {};
    }

    auto existingObjectsPassage =
        makeFakeObjectsInPassage(ctx.detectionStore, ctx.objectStore);

    // To recluster objects affected by negative verified detection pair matches
    // we explicitly exclude wrongly matched detections from existing clusters
    deleteForbiddenMatches(existingObjectsPassage, ctx.verificationRequestsIndex);
    PatchedMatcher verifiedMatcher =
        makeVerifiedMatcher(&detectionMatcher, ctx);
    auto areDetectionsAllowedToMerge = makeDetectionsMergeVerifier(ctx);

    INFO() << "Creating objects in passages";
    std::vector<ObjectsInPassage> objectsByPassages = makeObjectsByPassages(
        ctx.detectionStore, frameMatcher, verifiedMatcher, clusterizer, ctx.passagesDetectionIds);

    objectsByPassages.push_back(std::move(existingObjectsPassage));

    INFO() << "Merging objects from passages";

    auto matchedObjects =
        matchObjectsByPassages(ctx.detectionStore, frameMatcher, verifiedMatcher, objectsByPassages);

    db::IdTo<db::TIdSet> detectionIdsByPrimaryId = mergeObjectsByPassages(
        matchedObjects,
        ctx.detectionStore,
        ctx.objectStore,
        objectsByPassages,
        frameMatcher,
        verifiedMatcher,
        areDetectionsAllowedToMerge
    );

    return UpdateDetectionClustersResult {
        .detectionIdsByPrimaryId = std::move(detectionIdsByPrimaryId),
        .matchedObjects = std::move(matchedObjects)
    };
}

/// Finds primaryDetectionIds wich clusters does not have one of
/// @param detectionIds
db::TIds findUntouchedPrimaryDetections(
    const db::IdTo<db::TIdSet>& detectionIdsByPrimaryId,
    const BatchProcessingContext& ctx)
{
    if(ctx.updatedDetectionIds.empty()) {
        return {};
    }
    db::TIds result;
    db::TIdSet detectionIdSet(ctx.updatedDetectionIds.begin(), ctx.updatedDetectionIds.end());

    for (const auto& [primaryDetectionId, clusterDetectionIds] : detectionIdsByPrimaryId) {
        if (detectionIdSet.count(primaryDetectionId)) {
            continue;
        }

        if (ctx.detectionIdToDisappearenceDate.count(primaryDetectionId) &&
                ctx.detectionIdToDisappearenceDate.at(primaryDetectionId)
                    < ctx.oldestUpdatedDetectionDate)
        {
            continue;
        }

        bool found = false;
        for (const auto clusterDetectionId : clusterDetectionIds) {
            if (detectionIdSet.count(clusterDetectionId)) {
                found = true;
                break;
            }
        }
        if (!found) {
            result.push_back(primaryDetectionId);
        }
    }
    return result;
}

/// Returns map frameId -> frame matches with cluster detections
db::IdTo<std::vector<DetectionMatchData>>
collectFrameMatchesForCluster(
    const DetectionStore& detectionStore,
    const MatchedFramesPairs& frameMatches,
    const std::pair<db::TId, db::TIdSet>& detectionCluster,
    const db::TIdSet& frameIds)
{
    db::IdTo<std::vector<DetectionMatchData>> result;
    using MatchedFramesPairCRef = std::reference_wrapper<const MatchedFramesPair>;
    std::map<db::TId, std::vector<MatchedFramesPairCRef>> frameIdToMatchedFramePairsRefs;

    for (const auto& frameMatch : frameMatches) {
        frameIdToMatchedFramePairsRefs[frameMatch.id0].push_back(std::ref(frameMatch));
        frameIdToMatchedFramePairsRefs[frameMatch.id1].push_back(std::ref(frameMatch));
    }
    auto collectDetectionMatches =
        [&](db::TId detectionId) {
            const auto& frame = detectionStore.frameByDetectionId(detectionId);
            if (!frameIdToMatchedFramePairsRefs.count(frame.id())) {
                return;
            }

            const auto& frameLocation = detectionStore.locationByDetectionId(detectionId);
            const auto frameHeading = eye::decomposeRotation(frameLocation.rotation()).heading;
            const auto detectionBbox = transformByImageOrientation(
                detectionStore.detectionById(detectionId).box(),
                frame.originalSize(),
                frame.orientation());

            for (const auto& frameMatchRef : frameIdToMatchedFramePairsRefs.at(frame.id()))
            {
                const MatchedFramesPair& frameMatch = frameMatchRef.get();
                db::TId otherFrameId = frameMatch.id0 == frame.id() ? frameMatch.id1 : frameMatch.id0;
                if (!frameIds.count(otherFrameId)) {
                    continue;
                }

                auto matchDirection = otherFrameId == frameMatch.id0
                    ? FrameMatchPlace::First
                    : FrameMatchPlace::Second;

                result[otherFrameId].push_back(DetectionMatchData{
                    .matchPlace = matchDirection,
                    .match = frameMatch.match,
                    .detectionFrameMercPosition = frameLocation.mercatorPos(),
                    .detectionFrameHeading = frameHeading,
                    .detectionBbox = detectionBbox});
            }
        };
    collectDetectionMatches(detectionCluster.first);
    for (db::TId detectionId : detectionCluster.second) {
        collectDetectionMatches(detectionId);
    }
    return result;
}

/// @returns pairs <oldFrameId, newFrameId> of which objects visible in oldFrame
/// might be visible in newFrame
std::vector<std::pair<db::TId, db::TId>>
generateFramePairs(const DetectionStore& detectionStore,
                   const db::TIds& oldFrameIds,
                   const db::TIdSet& newFrameIds)
{
    std::vector<std::pair<db::TId, db::TId>> result;
    const FrameSearcher frameSearcher(detectionStore);

    for (auto oldFrameId : oldFrameIds) {
        if (newFrameIds.count(oldFrameId)) {
            continue;
        }
        const auto& oldFrameLocation = detectionStore.locationByFrameId(oldFrameId);
        const double mercObjectVisibility = geolib3::toMercatorUnits(DISAPPEARANCE_CANDIDATE_PARAMS.distanceMeters, oldFrameLocation.mercatorPos());
        const auto decomposedOldFrameRotation =
            decomposeRotation(oldFrameLocation.rotation());
        for (auto foundFrameId : frameSearcher.find(oldFrameLocation.mercatorPos(), mercObjectVisibility)) {
            if (!newFrameIds.count(foundFrameId)) {
                continue;
            }
            const auto& foundFrameLocation = detectionStore.locationByFrameId(foundFrameId);
            const auto decomposedFoundFrameRotation = decomposeRotation(foundFrameLocation.rotation());
            if (geolib3::distance(oldFrameLocation.mercatorPos(), foundFrameLocation.mercatorPos()) < mercObjectVisibility &&
                geolib3::angleBetween(decomposedOldFrameRotation.heading, decomposedFoundFrameRotation.heading) < DISAPPEARANCE_CANDIDATE_PARAMS.angleEpsilon)
            {
                result.emplace_back(oldFrameId, foundFrameId);
            }
        }
    }

    return result;
}

db::TIds
collectClusterFrames(const DetectionStore& detectionStore,
                     const db::IdTo<db::TIdSet>& detectionIdsByPrimaryId,
                     const db::TIds& primaryDetectionIds)
{
    db::TIds result;
    for (auto primaryDetectionId : primaryDetectionIds) {
        result.push_back(detectionStore.frameByDetectionId(primaryDetectionId).id());
        for (auto detectionId : detectionIdsByPrimaryId.at(primaryDetectionId)) {
            result.push_back(detectionStore.frameByDetectionId(detectionId).id());
        }
    }
    return result;
}

/// returns [<detectionId, frameIds>]
db::IdTo<db::TIds>
findMissingDetections(
    const ClusterVisibilityPredictor& visibilityPredictor,
    const BatchProcessingContext& ctx,
    const CachingFrameMatcher& frameMatcher,
    const db::IdTo<db::TIdSet>& detectionIdsByPrimaryId
)
{
    INFO() << "Finding missing detections";
    db::IdTo<db::TIds> result;
    db::TIds untouchedPrimaryDetectionIds = findUntouchedPrimaryDetections(detectionIdsByPrimaryId, ctx);
    db::TIdSet newFrameIds;
    for (db::TId detectionId : ctx.updatedDetectionIds) {
        newFrameIds.insert(ctx.detectionStore.frameByDetectionId(detectionId).id());
    }

    const db::TIds oldFrameIds = collectClusterFrames(ctx.detectionStore, detectionIdsByPrimaryId, untouchedPrimaryDetectionIds);

    auto frameIdsPairsToMatch = generateFramePairs(ctx.detectionStore, oldFrameIds, newFrameIds);
    INFO() << "There are " << frameIdsPairsToMatch.size() << " frames to match";
    const auto frameMatches = frameMatcher.makeMatches(ctx.detectionStore, frameIdsPairsToMatch);

    for (db::TId detectionId : untouchedPrimaryDetectionIds) {
        auto clusterIt = detectionIdsByPrimaryId.find(detectionId);
        ASSERT(clusterIt != detectionIdsByPrimaryId.end());
        for (const auto& [frameId, matches] : collectFrameMatchesForCluster(ctx.detectionStore, frameMatches, *clusterIt, newFrameIds))
        {
            const auto& frame = ctx.detectionStore.frameById(frameId);
            const auto& frameLocation = ctx.detectionStore.locationByFrameId(frameId);
            const auto frameHeading = eye::decomposeRotation(frameLocation.rotation()).heading;

            if (visibilityPredictor.isVisible(frame.size(), frameLocation.mercatorPos(), frameHeading, matches)) {
                result[detectionId].push_back(frameId);
            }
        }
    }
    INFO() << "Found " << result.size() << " missing detection candidates";

    return result;
}

} // namespace

pgpool3::TransactionHandle ObjectManager::processDetectionGroups(
    const std::vector<struct TxnIdDetectionGroupId>& originalDetectionGroupIds)
{
    INFO() << "processDetectionGroups " << originalDetectionGroupIds.size();
    if (originalDetectionGroupIds.empty()) {
        return getMasterWriteTxn(*(config_.mrc.pool));
    }

    auto txnDetectionGroupIds = originalDetectionGroupIds;

    const auto originalDetectionIds = extractDetectionIds(txnDetectionGroupIds);

    if (!config_.rework) {
        removeProcessedDetectionGroupIds(*getSlaveTxn(), &txnDetectionGroupIds);
    }

    const auto processingContext = makeProcessingContext(
        *getSlaveTxn(), extractDetectionIds(txnDetectionGroupIds));

    INFO() << processingContext.updatedDetectionIds.size() << " detections need to be processed";
    INFO() << processingContext.deletedDetectionIds.size() << " detections are deleted";

    CachingFrameMatcher cachingFrameMatcher(*frameMatcher_);

    auto updatedClusters = evalUpdatedDetectionClusters(
        cachingFrameMatcher, *detectionMatcher_, *clusterizer_, processingContext);

    auto missingDetectionCandidates = findMissingDetections(
        *visibilityPredictor_, processingContext, cachingFrameMatcher,
        updatedClusters.detectionIdsByPrimaryId);

    auto writeTxn = getMasterWriteTxn(*(config_.mrc.pool));

    INFO() << "Processing deleted detections";
    processDeletedDetections(*writeTxn, processingContext.deletedDetectionIds);
    INFO() << "Saving objects";
    const auto objects = saveObjects(*writeTxn, updatedClusters.detectionIdsByPrimaryId,
        processingContext.detectionIdToDisappearenceDate);

    INFO() << "Creating verification requests";
    createVerificationRequests(
        *writeTxn,
        config_.verificationRules,
        *config_.geoIdProvider,
        processingContext.detectionStore,
        processingContext.verificationRequestsIndex,
        updatedClusters.matchedObjects,
        updatedClusters.detectionIdsByPrimaryId,
        objects,
        missingDetectionCandidates);

    db::TIds objectsAffectedByRelations =
        evalObjectIdsWithAffectedRelations(*writeTxn, originalDetectionIds);
    INFO() << "Found " << objectsAffectedByRelations.size()
        << " objects whose relations must be recalculated";
    updateObjectRelations(*writeTxn, objectsAffectedByRelations);

    return writeTxn;
}

void ObjectManager::processBatch(const db::TIds& detectionGroupIds)
{
    const auto lock = lockIfNeed();

    INFO() << "Batch size " << detectionGroupIds.size();
    std::vector<struct TxnIdDetectionGroupId> txnDetectionGroupIds;
    txnDetectionGroupIds.reserve(detectionGroupIds.size());

    auto readTxn = getSlaveTxn();
    const auto groupIdToDetectionIds = loadGroupIdToDetectionIdsMap(*readTxn, detectionGroupIds);
    for (auto groupId : detectionGroupIds) {
        std::vector<TxnIdDetectionId> txnDetectionIds;
        if (groupIdToDetectionIds.count(groupId)) {
            for(auto detectionId : groupIdToDetectionIds.at(groupId)) {
                txnDetectionIds.push_back({.txnId = 0, .detectionId = detectionId});
            }
        }
        txnDetectionGroupIds.push_back(TxnIdDetectionGroupId{
            .detectionGroupId = groupId,
            .txnDetectionIds = std::move(txnDetectionIds)});
    }

    auto txn = processDetectionGroups(txnDetectionGroupIds);
    commitIfNeed(*txn);
}

bool ObjectManager::processBatchInLoopMode(size_t batchSize)
{
    const auto lock = lockIfNeed();

    Batch batch;
    { // read input batch
        auto readTxn = getSlaveTxn();
        auto metadata = objectManagerMetadata(*readTxn);

        batch = getNewBatch(*readTxn, metadata.getTxnId(), batchSize, config_.detectionTypes);
        INFO() << "Batch size " << batch.detectionGroupIds.size()
               << " [" << batch.beginTxnId << ", " << batch.endTxnId << "]";
    }

    auto txn = processDetectionGroups(batch.detectionGroupIds);
    auto metadata = objectManagerMetadata(*txn);
    metadata.updateTxnId(batch.endTxnId);
    metadata.updateTime();
    commitIfNeed(*txn);

    return batch.beginTxnId != batch.endTxnId;
}

ObjectManager::ObjectManager(const ObjectManagerConfig& config)
    : BaseMrcWorkerWithConfig(config)
    , frameMatcher_(config.frameMatcher)
    , detectionMatcher_(config.detectionMatcher)
    , clusterizer_(config.clusterizer)
    , visibilityPredictor_(config.visibilityPredictor)
{}

} // namespace maps::mrc::eye
