#include <maps/wikimap/mapspro/services/mrc/eye/lib/detection/include/store_utils.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/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/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/intersection.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>
#include <maps/libs/sql_chemistry/include/exists.h>

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

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

namespace maps::mrc::eye {

namespace {

void moveByDeviceId(
    db::IdTo<db::eye::Device> deviceById,
    db::IdTo<db::IdTo<db::eye::Device>>* deviceByIdByDeviceIdPtr)
{
    auto& deviceByIdByDeviceId = *deviceByIdByDeviceIdPtr;

    for (auto it = deviceById.begin(); it != deviceById.end();) {
        db::TId deviceId = it->second.id();

        auto nh = deviceById.extract(it++);
        deviceByIdByDeviceId[deviceId].insert(std::move(nh));
    }
}

db::IdTo<db::TId> moveByDeviceId(
    db::IdTo<db::eye::Frame> frameById,
    db::IdTo<db::IdTo<db::eye::Frame>>* frameByIdByDeviceIdPtr)
{
    auto& frameByIdByDeviceId = *frameByIdByDeviceIdPtr;

    db::IdTo<db::TId> deviceIdByFrameId;
    for (auto it = frameById.begin(); it != frameById.end();) {
        db::TId frameId = it->second.id();
        db::TId deviceId = it->second.deviceId();
        deviceIdByFrameId[frameId] = deviceId;

        auto nh = frameById.extract(it++);
        frameByIdByDeviceId[deviceId].insert(std::move(nh));
    }

    return deviceIdByFrameId;
}

void moveByDeviceId(
    db::IdTo<db::eye::FrameLocation> locationById,
    db::IdTo<db::IdTo<db::eye::FrameLocation>>* locationByIdByDeviceIdPtr,
    const db::IdTo<db::TId> deviceIdByFrameId)
{
    auto& locationByIdByDeviceId = *locationByIdByDeviceIdPtr;

    for (auto it = locationById.begin(); it != locationById.end();) {
        db::TId frameId = it->second.frameId();
        db::TId deviceId = deviceIdByFrameId.at(frameId);

        auto nh = locationById.extract(it++);
        locationByIdByDeviceId[deviceId].insert(std::move(nh));
    }
}

void moveByDeviceId(
    db::IdTo<db::eye::FramePrivacy> privacyById,
    db::IdTo<db::IdTo<db::eye::FramePrivacy>>* privacyByIdByDeviceIdPtr,
    const db::IdTo<db::TId> deviceIdByFrameId)
{
    auto& privacyByIdByDeviceId = *privacyByIdByDeviceIdPtr;

    for (auto it = privacyById.begin(); it != privacyById.end();) {
        db::TId frameId = it->second.frameId();
        db::TId deviceId = deviceIdByFrameId.at(frameId);

        auto nh = privacyById.extract(it++);
        privacyByIdByDeviceId[deviceId].insert(std::move(nh));
    }
}

db::IdTo<db::TId> moveByDeviceId(
    db::IdTo<db::eye::DetectionGroup> groupById,
    db::IdTo<db::IdTo<db::eye::DetectionGroup>>* groupByIdByDeviceIdPtr,
    const db::IdTo<db::TId> deviceIdByFrameId)
{
    auto& groupByIdByDeviceId = *groupByIdByDeviceIdPtr;

    db::IdTo<db::TId> deviceIdByGroupId;
    for (auto it = groupById.begin(); it != groupById.end();) {
        db::TId groupId = it->second.id();
        db::TId frameId = it->second.frameId();
        db::TId deviceId = deviceIdByFrameId.at(frameId);
        deviceIdByGroupId[groupId] = deviceId;

        auto nh = groupById.extract(it++);
        groupByIdByDeviceId[deviceId].insert(std::move(nh));
    }

    return deviceIdByGroupId;
}

void moveByDeviceId(
    db::IdTo<db::eye::Detection> detectionById,
    db::IdTo<db::IdTo<db::eye::Detection>>* detectionByIdByDeviceIdPtr,
    const db::IdTo<db::TId>& deviceIdByGroupId)
{
    auto& detectionByIdByDeviceId = *detectionByIdByDeviceIdPtr;

    for (auto it = detectionById.begin(); it != detectionById.end();) {
        db::TId groupId = it->second.groupId();
        db::TId deviceId = deviceIdByGroupId.at(groupId);

        auto nh = detectionById.extract(it++);
        detectionByIdByDeviceId[deviceId].insert(std::move(nh));
    }
}

} // namespace

std::vector<DetectionStore> splitByDeviceId(DetectionStore store) {
    const size_t deviceCnt = store.deviceById_.size();
    if (deviceCnt == 0) {
        return {};
    } else if (deviceCnt == 1) {
        return {std::move(store)};
    };

    // move devices
    db::IdTo<db::IdTo<db::eye::Device>> deviceByIdByDeviceId;
    deviceByIdByDeviceId.reserve(deviceCnt);
    moveByDeviceId(std::move(store.deviceById_), &deviceByIdByDeviceId);

    // move frame and locations
    db::IdTo<db::IdTo<db::eye::Frame>> frameByIdByDeviceId;
    frameByIdByDeviceId.reserve(deviceCnt);
    const auto deviceIdByFrameId = moveByDeviceId(
        std::move(store.frameById_),
        &frameByIdByDeviceId
    );
    db::IdTo<db::IdTo<db::eye::FrameLocation>> locationByIdByDeviceId;
    locationByIdByDeviceId.reserve(deviceCnt);
    moveByDeviceId(
        std::move(store.frameLocationByFrameId_),
        &locationByIdByDeviceId,
        deviceIdByFrameId
    );
    db::IdTo<db::IdTo<db::eye::FramePrivacy>> privacyByIdByDeviceId;
    privacyByIdByDeviceId.reserve(deviceCnt);
    moveByDeviceId(
        std::move(store.framePrivacyByFrameId_),
        &privacyByIdByDeviceId,
        deviceIdByFrameId
    );

    // move groups
    db::IdTo<db::IdTo<db::eye::DetectionGroup>> groupByIdByDeviceId;
    groupByIdByDeviceId.reserve(deviceCnt);
    const auto deviceIdByGroupId = moveByDeviceId(
        std::move(store.groupById_),
        &groupByIdByDeviceId,
        deviceIdByFrameId
    );

    // move detections
    db::IdTo<db::IdTo<db::eye::Detection>> detectionByIdByDeviceId;
    detectionByIdByDeviceId.reserve(deviceCnt);
    moveByDeviceId(
        std::move(store.detectionById_),
        &detectionByIdByDeviceId,
        deviceIdByGroupId
    );

    std::vector<DetectionStore> stores;
    stores.reserve(deviceCnt);
    for (auto&& [deviceId, deviceById] : deviceByIdByDeviceId) {
        stores.emplace_back(
            std::move(groupByIdByDeviceId[deviceId]),
            std::move(detectionByIdByDeviceId[deviceId]),
            std::move(frameByIdByDeviceId[deviceId]),
            std::move(locationByIdByDeviceId[deviceId]),
            std::move(privacyByIdByDeviceId[deviceId]),
            std::move(deviceById)
        );
    }

    return std::move(stores);
};

namespace {

std::vector<typename db::IdTo<db::eye::Frame>::node_type>
extractFrameNodes(db::IdTo<db::eye::Frame> frameById) {
    std::vector<typename db::IdTo<db::eye::Frame>::node_type> frameNhs;
    for (auto it = frameById.begin(); it != frameById.end();) {
        auto nh = frameById.extract(it++);
        frameNhs.push_back(std::move(nh));
    }

    return frameNhs;
}

void sortByTime(std::vector<typename db::IdTo<db::eye::Frame>::node_type>* frameNhsPtr) {
    auto& frameNhs = *frameNhsPtr;

    std::sort(frameNhs.begin(), frameNhs.end(),
        [](const auto& lhs, const auto& rhs) {
            return lhs.mapped().time() < rhs.mapped().time();
        }
    );
}

// Возвращает отображение фреймов в проезды и количество проездов
std::pair<db::IdTo<size_t>, size_t> moveByPassage(
    db::IdTo<db::eye::Frame> frameById,
    db::IdTo<db::eye::FrameLocation> locationById,
    db::IdTo<db::eye::FramePrivacy> privacyById,
    std::vector<db::IdTo<db::eye::Frame>>* frameByIdByPassagePtr,
    std::vector<db::IdTo<db::eye::FrameLocation>>* locationByIdByPassagePtr,
    std::vector<db::IdTo<db::eye::FramePrivacy>>* privacyByIdByPassagePtr,
    const SplitPassageParams& params)
{
    auto& frameByIdByPassage = *frameByIdByPassagePtr;
    frameByIdByPassage = {{}};
    auto& locationByIdByPassage = *locationByIdByPassagePtr;
    locationByIdByPassage = {{}};
    auto& privacyByIdByPassage = *privacyByIdByPassagePtr;
    privacyByIdByPassage = {{}};

    auto frameNhs = extractFrameNodes(std::move(frameById));
    sortByTime(&frameNhs);

    size_t passageIndx = 0;
    db::IdTo<size_t> passageIndxByFrameId;

    auto lastTime = frameNhs.begin()->mapped().time();
    auto lastPos = locationById.at(frameNhs.begin()->key()).mercatorPos();

    for (auto&& frameNh : frameNhs) {
        const db::TId frameId = frameNh.key();

        auto locationIt = locationById.find(frameNh.key());
        REQUIRE(locationIt != locationById.end(), "Not found location " << frameId);
        auto locationNh = locationById.extract(locationIt);

        auto privacyIt = privacyById.find(frameNh.key());
        REQUIRE(privacyIt != privacyById.end(), "Not found privacy " << frameId);
        auto privacyNth = privacyById.extract(privacyIt);

        const auto time = frameNh.mapped().time();
        const auto pos = locationNh.mapped().mercatorPos();

        auto secondsDiff = std::chrono::duration_cast<std::chrono::seconds>(time - lastTime);

        double mercDiff = geolib3::distance(pos, lastPos);
        double mercEpsilon = geolib3::toMercatorUnits(params.metersEpsilon, pos);

        if (secondsDiff > params.secondsEpsilon || mercDiff > mercEpsilon) {
            frameByIdByPassage.push_back({});
            locationByIdByPassage.push_back({});
            privacyByIdByPassage.push_back({});
            passageIndx++;
        }
        frameByIdByPassage.back().insert(std::move(frameNh));
        locationByIdByPassage.back().insert(std::move(locationNh));
        privacyByIdByPassage.back().insert(std::move(privacyNth));
        passageIndxByFrameId[frameId] = passageIndx;

        lastTime = time;
        lastPos = pos;
    }

    return {std::move(passageIndxByFrameId), passageIndx + 1};
}

void moveByPassage(
    db::IdTo<db::eye::Detection> detectionById,
    db::IdTo<db::eye::DetectionGroup> groupById,
    std::vector<db::IdTo<db::eye::Detection>>* detectionByIdByPassagePtr,
    std::vector<db::IdTo<db::eye::DetectionGroup>>* groupByIdByPassagePtr,
    const db::IdTo<size_t>& passageIndxByFrameId)
{
    auto& detectionByIdByPassage = *detectionByIdByPassagePtr;
    auto& groupByIdByPassage = *groupByIdByPassagePtr;

    db::IdTo<size_t> passageIndxByGroupId;
    for (auto detectionIt = detectionById.begin(); detectionIt != detectionById.end();) {
        const db::TId groupId = detectionIt->second.groupId();
        auto detectionNh = detectionById.extract(detectionIt++);

        auto groupIt = groupById.find(groupId);
        REQUIRE(groupIt != groupById.end(), "Not found group " << groupId);

        const db::TId frameId = groupIt->second.frameId();
        size_t passageIndx = passageIndxByFrameId.at(frameId);

        passageIndxByGroupId[groupId] = passageIndx;
        detectionByIdByPassage[passageIndx].insert(std::move(detectionNh));
    }

    for (auto groupIt = groupById.begin(); groupIt != groupById.end();) {
        auto groupNh = groupById.extract(groupIt++);
        size_t passageIndx = passageIndxByGroupId.at(groupNh.mapped().id());

        groupByIdByPassage[passageIndx].insert(std::move(groupNh));
    }
}


std::vector<DetectionStore> splitByPassagesFromUniqueDeviceId(
    db::IdTo<db::eye::DetectionGroup> groupById,
    db::IdTo<db::eye::Detection> detectionById,
    db::IdTo<db::eye::Frame> frameById,
    db::IdTo<db::eye::FrameLocation> locationById,
    db::IdTo<db::eye::FramePrivacy> privacyById,
    db::IdTo<db::eye::Device> deviceById,
    const SplitPassageParams& params)
{
    if (frameById.empty()) {
        return {};
    }

    std::vector<db::IdTo<db::eye::Frame>> frameByIdByPassage;
    std::vector<db::IdTo<db::eye::FrameLocation>> locationByIdByPassage;
    std::vector<db::IdTo<db::eye::FramePrivacy>> privacyByIdPassage;

    const auto [passageIndxByFrameId, passagesCnt] = moveByPassage(
        std::move(frameById), std::move(locationById), std::move(privacyById),
        &frameByIdByPassage, &locationByIdByPassage,
        &privacyByIdPassage, params
    );

    std::vector<db::IdTo<db::eye::DetectionGroup>> groupByIdByPassage;
    groupByIdByPassage.resize(passagesCnt);
    std::vector<db::IdTo<db::eye::Detection>> detectionByIdByPassage;
    detectionByIdByPassage.resize(passagesCnt);
    moveByPassage(
        std::move(detectionById), std::move(groupById),
        &detectionByIdByPassage, &groupByIdByPassage,
        passageIndxByFrameId
    );

    std::vector<DetectionStore> stores;
    stores.reserve(passagesCnt);
    for (size_t i = 0; i < passagesCnt; i++) {
        stores.emplace_back(
            std::move(groupByIdByPassage[i]),
            std::move(detectionByIdByPassage[i]),
            std::move(frameByIdByPassage[i]),
            std::move(locationByIdByPassage[i]),
            std::move(privacyByIdPassage[i]),
            deviceById
        );
    }

    return std::move(stores);
}

} // namespace

std::vector<DetectionStore> splitByPassages(
    DetectionStore store,
    const SplitPassageParams& params)
{
    std::vector<DetectionStore> stores;
    for (auto&& storeUniqueDeviceId : splitByDeviceId(std::move(store))) {
        auto tmpStores = splitByPassagesFromUniqueDeviceId(
            std::move(storeUniqueDeviceId.groupById_),
            std::move(storeUniqueDeviceId.detectionById_),
            std::move(storeUniqueDeviceId.frameById_),
            std::move(storeUniqueDeviceId.frameLocationByFrameId_),
            std::move(storeUniqueDeviceId.framePrivacyByFrameId_),
            std::move(storeUniqueDeviceId.deviceById_),
            params
        );

        stores.insert(stores.end(),
            std::make_move_iterator(tmpStores.begin()),
            std::make_move_iterator(tmpStores.end())
        );
    }

    return stores;
}

namespace {

std::vector<geolib3::BoundingBox> mergeIntersectedBoxes(
    const std::vector<geolib3::BoundingBox>& boxes)
{
    if (boxes.size() < 2) {
        return boxes;
    }

    std::list<geolib3::BoundingBox> mergedBoxes{boxes[0]};
    for (size_t i = 1; i < boxes.size(); i++) {
        if (intersects(mergedBoxes.back(), boxes[i])) {
            mergedBoxes.back() = geolib3::expand(mergedBoxes.back(), boxes[i]);
        } else {
            mergedBoxes.push_back(boxes[i]);
        }
    }

    bool merged = false;
    while (!merged) {
        merged = true;
        for (auto it1 = mergedBoxes.begin(); it1 != mergedBoxes.end(); it1++) {
            auto it2 = std::next(it1);
            while (it2 != mergedBoxes.end()) {
                if (intersects(*it1, *it2)) {
                    merged = false;
                    *it1 = geolib3::expand(*it1, *it2);
                    auto tmp = it2;
                    it2++;
                    mergedBoxes.erase(tmp);
                } else {
                    it2++;
                }
            }
        }
    }

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

db::eye::ObjectLocations filterByBoxes(
    db::eye::ObjectLocations&& locations,
    const std::vector<geolib3::BoundingBox>& boxes)
{
    geolib3::StaticGeometrySearcher<geolib3::BoundingBox, size_t> searcher;
    for (size_t i = 0; i < boxes.size(); i++) {
        searcher.insert(&(boxes[i]), i);
    }
    searcher.build();

    locations.erase(
        std::remove_if(locations.begin(), locations.end(),
            [&](const auto& location) {
                auto searchResult = searcher.find(location.mercatorPos().boundingBox());
                return searchResult.first == searchResult.second;
            }
        ),
        locations.end()
    );

    return locations;
}

struct ObjectFilter {
    std::vector<geolib3::BoundingBox> bboxes;
    db::eye::ObjectType objectType;
};

std::vector<ObjectFilter>
makeObjectFilters(
    const DetectionStore& store,
    double radiusMeters)
{
    std::vector<ObjectFilter> result;
    std::map<db::eye::ObjectType, std::vector<geolib3::BoundingBox>> typeToBboxes;

    for (const auto& [_, group] : store.groupById()) {
        const auto& location = store.locationByFrameId(group.frameId());
        const geolib3::Point2& pos = location.mercatorPos();
        double radiusMerc = geolib3::toMercatorUnits(radiusMeters, pos);

        typeToBboxes[toObjectType(group.type())].push_back(
            geolib3::BoundingBox(pos, 2 * radiusMerc, 2 * radiusMerc));
    }

    result.reserve(typeToBboxes.size());
    for (auto& [type, bboxes] : typeToBboxes) {
        result.push_back(
            ObjectFilter{.bboxes = std::move(bboxes), .objectType = type}
        );
    }

    return result;
}

db::TIds loadNeighborsObjectIds(
    pqxx::transaction_base& txn,
    const std::vector<ObjectFilter>& filters)
{
    static constexpr size_t BATCH_SIZE = 1000;
    db::TIdSet objectIds;

    INFO() << "Loading ids of neighboring object";
    std::vector<geolib3::BoundingBox> allBboxes;
    db::eye::ObjectLocations objectLocations;

    for (const auto& filter : filters)
    {
        auto mergedBboxes = mergeIntersectedBoxes(filter.bboxes);
        allBboxes.insert(allBboxes.end(), filter.bboxes.begin(), filter.bboxes.end());
        for (const auto& bboxBatch : maps::common::makeBatches(mergedBboxes, BATCH_SIZE))
        {
            sql_chemistry::FiltersCollection bufferFilter(sql_chemistry::op::Logical::Or);

            for (const auto& bbox: bboxBatch) {
                bufferFilter.add(
                    db::eye::table::ObjectLocation::position.intersects(
                        bbox));
            }


            auto objectLocationsBatch =
                db::eye::ObjectLocationGateway(txn).load(bufferFilter
                        && db::eye::table::Object::id == db::eye::table::ObjectLocation::objectId
                        && db::eye::table::Object::type == filter.objectType);

            objectLocations.insert(
                objectLocations.end(),
                std::make_move_iterator(objectLocationsBatch.begin()),
                std::make_move_iterator(objectLocationsBatch.end())
            );
        }
    }

    objectLocations = filterByBoxes(std::move(objectLocations), allBboxes);

    for (const auto& location : objectLocations) {
        objectIds.insert(location.objectId());
    }

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

} // namespace

db::eye::Objects loadNeighboringObjects(
    pqxx::transaction_base& txn,
    const DetectionStore& store,
    double radiusMeters)
{
    const auto objectFilters = makeObjectFilters(store, radiusMeters);
    const auto objectIds = loadNeighborsObjectIds(txn, objectFilters);

    return db::eye::ObjectGateway(txn).loadByIds(objectIds);
}

DetectionStore mergeDetectionStores(std::vector<DetectionStore> stores) {
    DetectionStore store;
    for (size_t i = 0; i < stores.size(); i++) {
        store.groupById_.merge(std::move(stores[i].groupById_));
        store.detectionById_.merge(std::move(stores[i].detectionById_));
        store.frameById_.merge(std::move(stores[i].frameById_));
        store.frameLocationByFrameId_.merge(std::move(stores[i].frameLocationByFrameId_));
        store.deviceById_.merge(std::move(stores[i].deviceById_));
    }
    return store;
}

namespace {

std::pair<chrono::TimePoint, chrono::TimePoint>
getTimeRange(const db::IdTo<db::eye::Frame>& frameById) {
    auto minTime = frameById.begin()->second.time();
    auto maxTime = frameById.begin()->second.time();
    for (const auto& [_, frame] : frameById) {
        minTime = std::min(minTime, frame.time());
        maxTime = std::max(maxTime, frame.time());
    }

    return {minTime, maxTime};
}

geolib3::BoundingBox getMercatorBbox(const db::IdTo<db::eye::FrameLocation>& frameLocationById)
{
    geolib3::BoundingBox bbox;
    bool bboxInitialized = false;

    for (const auto& [_, frameLocation] : frameLocationById) {
        if (bboxInitialized) {
            bbox = geolib3::expand(bbox, frameLocation.mercatorPos());
        } else {
            bbox = frameLocation.mercatorPos().boundingBox();
            bboxInitialized = true;
        }
    }

    return bbox;
}

struct PassageFilterParams {
    size_t passageIndx;
    db::TId deviceId;
    chrono::TimePoint minTime;
    chrono::TimePoint maxTime;
    geolib3::BoundingBox mercatorBbox;
    std::vector<db::eye::DetectionType> detectionTypes;
};

db::IdTo<db::eye::Frame> loadFrames(
    pqxx::transaction_base& txn,
    const db::TIds& frameIds)
{
    namespace table = db::eye::table;
    static const size_t BATCH_SIZE = 1000;

    db::IdTo<db::eye::Frame> frameById;

    INFO() << "Loading frames";

    for (const auto& batch : maps::common::makeBatches(frameIds, BATCH_SIZE)) {
        frameById.merge(
            byId(db::eye::FrameGateway(txn).loadByIds({batch.begin(), batch.end()}))
        );
    }

    return std::move(frameById);
}

db::IdTo<db::eye::DetectionGroup>
loadGroups(
    pqxx::transaction_base& txn,
    const std::vector<PassageFilterParams>& passageFilters)
{
    namespace table = db::eye::table;
    static const size_t BATCH_SIZE = 1000;

    db::IdTo<db::eye::DetectionGroup> groupById;

    INFO() << "Loading groups by " << passageFilters.size() << " filters";

    for (const auto& batch : maps::common::makeBatches(passageFilters, BATCH_SIZE)) {
        sql_chemistry::FiltersCollection rangesFilter(sql_chemistry::op::Logical::Or);

        for (const auto& params : batch) {
            rangesFilter.add(
                table::Frame::time.between(params.minTime, params.maxTime)
                    && table::Frame::deviceId == params.deviceId
                    && table::FrameLocation::position.intersects(params.mercatorBbox)
                    && table::DetectionGroup::type.in(params.detectionTypes)
                    && sql_chemistry::existsIn<table::Detection>(
                        table::Detection::groupId == table::DetectionGroup::id
                    )
            );
        }

        groupById.merge(
            byId(db::eye::DetectionGroupGateway(txn).load(
                rangesFilter
                    && table::Frame::id == table::FrameLocation::frameId
                    && table::DetectionGroup::frameId == table::Frame::id
            ))
        );
        INFO() << "Loaded " << groupById.size();
    }

    return groupById;
}

db::IdTo<db::eye::FrameLocation> loadLocationsByFrameIds(
    pqxx::transaction_base& txn,
    const db::TIds& frameIds)
{
    namespace table = db::eye::table;
    static const size_t BATCH_SIZE = 10000;

    db::IdTo<db::eye::FrameLocation> locationByFrameId;

    INFO() << "Loading locations";

    for (const auto& batch : maps::common::makeBatches(frameIds, BATCH_SIZE)) {
        locationByFrameId.merge(
            byId(db::eye::FrameLocationGateway(txn).loadByIds({batch.begin(), batch.end()}))
        );
    }

    return locationByFrameId;
}

db::IdTo<db::eye::FramePrivacy> loadPrivaciesByFrameIds(
    pqxx::transaction_base& txn,
    const db::TIds& frameIds)
{
    namespace table = db::eye::table;

    static const size_t BATCH_SIZE = 10000;

    db::IdTo<db::eye::FramePrivacy> privacyByFrameId;

    for (const auto& batch : maps::common::makeBatches(frameIds, BATCH_SIZE)) {
        privacyByFrameId.merge(
            byId(db::eye::FramePrivacyGateway(txn).loadByIds({batch.begin(), batch.end()}))
        );
    }

    return privacyByFrameId;
}


db::IdTo<db::eye::Detection> loadDetectionsByGroupIds(
    pqxx::transaction_base& txn,
    const db::TIds& groupIds)
{
    namespace table = db::eye::table;

    static const size_t BATCH_SIZE = 10000;

    db::IdTo<db::eye::Detection> detectionById;

    for (const auto& batch : maps::common::makeBatches(groupIds, BATCH_SIZE)) {
        detectionById.merge(
            byId(db::eye::DetectionGateway(txn).load(
                table::Detection::groupId.in({batch.begin(), batch.end()})
                    && !table::Detection::deleted
            ))
        );
    }

    return detectionById;
}

db::TIds
collectFrameIds(const db::IdTo<db::eye::DetectionGroup>& groupById)
{
    db::TIdSet frameIds;
    for (const auto& [_, group] : groupById) {
        frameIds.insert(group.frameId());
    }
    return db::TIds(frameIds.begin(), frameIds.end());
}

std::vector<db::eye::DetectionType>
evalDetectionTypes(const DetectionStore& store)
{
    std::vector<db::eye::DetectionType> result;
    for (const auto& [_, group] : store.groupById()) {
        if (std::find(result.begin(), result.end(), group.type()) == result.end()) {
            result.push_back(group.type());
        }
    }
    return result;
}

} // namespace

std::vector<db::TIdSet> makePassages(
    pqxx::transaction_base& txn,
    DetectionStore* storePtr,
    const SplitPassageParams& splitParams,
    const std::chrono::seconds timePad)
{
    DetectionStore& store = *storePtr;
    db::IdTo<db::eye::Device> deviceById = store.deviceById_;

    INFO() << "Splitting detections by passages";
    std::vector<DetectionStore> passageStores
        = splitByPassages(std::move(store), splitParams);
    INFO() << "Detections has been splitted into " << passageStores.size() << " passages";

    std::vector<PassageFilterParams> passageFilters;
    for (size_t passageIndx = 0; passageIndx < passageStores.size(); passageIndx++) {
        REQUIRE(passageStores[passageIndx].deviceById_.size() == 1u,
                "Store for one passage must have one device");
        const auto& passageStore = passageStores[passageIndx];
        const db::TId deviceId = passageStore.deviceById_.begin()->first;
        const auto [minTime, maxTime] = getTimeRange(passageStore.frameById_);
        const auto bbox = getMercatorBbox(passageStore.frameLocationByFrameId_);
        const auto expandedBbox = geolib3::resizeByValue(bbox,
            geolib3::toMercatorUnits(splitParams.metersEpsilon, bbox.center()));
        const auto detectionTypes = evalDetectionTypes(passageStore);

        passageFilters.push_back({
            passageIndx, deviceId,
            minTime - timePad, maxTime + timePad,
            expandedBbox,
            detectionTypes
        });
    }

    auto groupById = loadGroups(txn, passageFilters);
    auto frameIds = collectFrameIds(groupById);
    auto frameById = loadFrames(txn, frameIds);
    auto groupIds = collectKeys(groupById);

    INFO() << "Loaded frames in time ranges: " << frameById.size();
    INFO() << "Loaded groups in time ranges: " << groupById.size();

    store = DetectionStore(
        std::move(groupById),
        loadDetectionsByGroupIds(txn, groupIds),
        std::move(frameById),
        loadLocationsByFrameIds(txn, frameIds),
        loadPrivaciesByFrameIds(txn, frameIds),
        std::move(deviceById)
    );

    std::vector<db::TIdSet> passages(passageStores.size());

    db::IdTo<std::vector<PassageFilterParams>> passageFiltersByDeviceId;
    for (const PassageFilterParams& filter : passageFilters) {
        passageFiltersByDeviceId[filter.deviceId].push_back(filter);
    }

    for (db::TId detectionId : store.detectionIds()) {
        const auto& frame = store.frameByDetectionId(detectionId);
        const auto& passageFilters = passageFiltersByDeviceId.at(frame.deviceId());

        for (const auto& filter : passageFilters) {
            if (filter.minTime <= frame.time() && frame.time() <= filter.maxTime) {
                passages[filter.passageIndx].insert(detectionId);
            }
        }
    }

    return passages;
}

} // namespace maps::mrc::eye
