#include <maps/wikimap/mapspro/services/mrc/eye/lib/playground/include/playground.h>

#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/id.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/txn.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/url.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/detect_sign/include/detect_sign.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/import_mrc/include/import.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/location/include/rotation.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/include/object_manager.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/sync_detection/include/sync_detection.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object_attrs.h>

#include <maps/wikimap/mapspro/services/mrc/libs/common/include/exif.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/feature_gateway.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/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/yt/include/serialization.h>

#include <maps/libs/common/include/exception.h>
#include <maps/libs/json/include/std.h>

#include <library/cpp/iterator/zip.h>

#include <utility>
#include <vector>

namespace maps::mrc::eye {

Playground::Playground(const PlaygroundConfig& config): config_(config) {}

Playground& Playground::uploadMrcFeature(const std::string& featureTableYtPath)
{
    auto reader = config_.yt->CreateTableReader<NYT::TNode>(TString(featureTableYtPath));

    while (reader->IsValid()) {
        db::Features features;
        db::TIds externalIds;

        auto txn = getMasterWriteTxn(*(config_.mrcPool));
        db::FeatureGateway gateway(*txn);

        static constexpr size_t batchSize = 10000;

        for (size_t i = 0; i < batchSize and reader->IsValid(); ++i) {
            auto feature = yt::deserialize<db::Feature>(reader->GetRow());
            externalIds.push_back(feature.id());

            gateway.id(feature) = 0; // reset id to insert into db

            features.push_back(feature);
            reader->Next();
        }

        gateway.insert(features);
        db::updateFeaturesTransaction(features, *txn);

        txn->commit();

        for (const auto& [externalId, feature]: Zip(externalIds, features)) {
            const auto [_, inserted] = externalFeatureIdToFeatureId_.emplace(externalId, feature.id());
            REQUIRE(inserted, "Duplicate feature id " << externalId);

            featureIdToExternalFeatureId_.emplace(feature.id(), externalId);
        }
    }

    return *this;
}

db::TId Playground::getFeatureId(db::TId externalFeatureId) const
{
    const auto it = externalFeatureIdToFeatureId_.find(externalFeatureId);

    REQUIRE(
        it != externalFeatureIdToFeatureId_.end(),
        "Invalid external feature id " << externalFeatureId
    );

    return it->second;
}

db::TId Playground::getExternalFeatureId(db::TId featureId) const
{
    const auto it = featureIdToExternalFeatureId_.find(featureId);

    REQUIRE(
        it != featureIdToExternalFeatureId_.end(),
        "Invalid feature id " << featureId
    );

    return it->second;
}

db::IdTo<db::TId> Playground::loadFrameIdToFeatureId(pqxx::transaction_base& txn)
{
    db::IdTo<db::TId> result;
    for (const auto& pair: db::eye::FeatureToFrameGateway(txn).load()) {
        result.emplace(pair.frameId(), pair.featureId());
    }

    return result;
}

db::IdTo<db::TId> Playground::loadFeatureIdToFrameId(pqxx::transaction_base& txn)
{
    db::IdTo<db::TId> result;
    for (const auto& pair: db::eye::FeatureToFrameGateway(txn).load()) {
        result.emplace(pair.featureId(), pair.frameId());
    }

    return result;
}

db::IdTo<db::eye::Frame> Playground::loadFrameById(pqxx::transaction_base& txn)
{
    return byId(db::eye::FrameGateway(txn).load());
}

db::IdTo<db::TId> Playground::loadDetectionIdToGroupId(pqxx::transaction_base& txn)
{
    db::IdTo<db::TId> result;
    for (const auto& detection: db::eye::DetectionGateway(txn).load()) {
        result.emplace(detection.id(), detection.groupId());
    }

    return result;
}

db::IdTo<db::TId> Playground::loadGroupIdToFrameId(pqxx::transaction_base& txn)
{
    db::IdTo<db::TId> result;
    for (const auto& group: db::eye::DetectionGroupGateway(txn).load()) {
        result.emplace(group.id(), group.frameId());
    }

    return result;
}

namespace {

struct SignDetection {
    db::TId id;
    traffic_signs::TrafficSign type;
    common::ImageBox box;

    SignDetection(db::TId id, traffic_signs::TrafficSign type, const common::ImageBox& box)
        : id(id)
        , type(type)
        , box(box)
    {}

    SignDetection(const json::Value& value)
        : id(value["object_id"])
        , type(traffic_signs::stringToTrafficSign(value["type"].as<std::string>()))
        , box(common::ImageBox::fromJson(value["bbox"]))
    {}

    void json(json::ObjectBuilder builder) const {
        builder["object_id"] = id;
        builder["type"] = traffic_signs::toString(type);
        builder["bbox"] = box;
    }
};

using SignDetections = std::vector<SignDetection>;

using FeatureIdToSigns = std::unordered_map<db::TId, SignDetections>;

FeatureIdToSigns loadFeaturesSigns(const std::string& detectionJsonPath)
{
    const auto value = json::Value::fromFile(detectionJsonPath);

    FeatureIdToSigns result;

    for (const auto& featureSigns: value["features_objects"]) {
        const auto featureId = featureSigns["feature_id"].as<db::TId>();

        SignDetections detections;
        for (const auto& sign: featureSigns["objects"]) {
            detections.emplace_back(sign);
        }

        result.emplace(featureId, detections);
    }

    return result;
}

} // namespace

Playground& Playground::uploadSignRecognition(const std::string& detectionJsonPath)
{
    const auto featureIdToDetections = loadFeaturesSigns(detectionJsonPath);

    auto txn = getMasterWriteTxn(*(config_.mrcPool));
    const auto featureIdToFrameId = loadFeatureIdToFrameId(*txn);

    const IdMapChain externalFeatureIdToFrameId {
        std::addressof(externalFeatureIdToFeatureId_),
        std::addressof(featureIdToFrameId)
    };

    const auto frameById = loadFrameById(*txn);

    db::eye::Recognitions recognitions;
    for (const auto& [featureId, detections]: featureIdToDetections) {
        db::eye::DetectedSigns signs;

        const db::TId frameId = externalFeatureIdToFrameId.at(featureId);
        const auto& frame = frameById.at(frameId);

        constexpr int16_t version = 0;
        constexpr double typeConfidence = 1.0;
        constexpr bool temporary = false;
        constexpr double temporaryConfidence = 1.0;

        for (const auto& detection: detections) {
            signs.push_back({
                detection.box, // box orientation = 1
                detection.type,
                typeConfidence,
                temporary,
                temporaryConfidence
            });
        }

        recognitions.emplace_back(
            frame.id(),
            frame.orientation(),
            db::eye::RecognitionType::DetectSign,
            db::eye::RecognitionSource::Import,
            version,
            db::eye::DetectedSigns(signs)
        );
    }

    db::eye::RecognitionGateway(*txn).insertx(recognitions);
    txn->commit();

    return *this;
}

db::IdTo<db::TId> Playground::loadExternalFeatureIdToFrameId() {
    auto txn = getMasterWriteTxn(*(config_.mrcPool));

    const auto featureIdToFrameId = loadFeatureIdToFrameId(*txn);

    db::IdTo<db::TId> externalFeatureIdToFrameId;
    for (const auto& [externalFeatureId, featureId] : externalFeatureIdToFeatureId_) {
        db::TId frameId = featureIdToFrameId.at(featureId);

        externalFeatureIdToFrameId[externalFeatureId] = frameId;
    }

    return externalFeatureIdToFrameId;
}

std::map<FeatureObjectId, db::TId> Playground::loadFeatureObjectIdToSignDetectionId(
        const std::string& detectionJsonPath)
{
    auto txn = getMasterWriteTxn(*(config_.mrcPool));

    const auto frameById = loadFrameById(*txn);
    const auto detectionIdToGroupId = loadDetectionIdToGroupId(*txn);
    const auto groupIdToFrameId = loadGroupIdToFrameId(*txn);
    const auto frameIdToFeatureId = loadFrameIdToFeatureId(*txn);

    const IdMapChain detectionIdToFrameId {
        std::addressof(detectionIdToGroupId),
        std::addressof(groupIdToFrameId),
    };

    const IdMapChain frameIdToExternalFeatureId {
        std::addressof(frameIdToFeatureId),
        std::addressof(featureIdToExternalFeatureId_)
    };

    const auto externalFeatureIdToDetections = loadFeaturesSigns(detectionJsonPath);

    auto filter = db::eye::table::Detection::groupId == db::eye::table::DetectionGroup::id
        and db::eye::table::DetectionGroup::type == db::eye::DetectionType::Sign
        and not db::eye::table::Detection::deleted;

    std::map<FeatureObjectId, db::TId> featureObjectIdToDetectionId;
    for (const auto& detection: db::eye::DetectionGateway(*txn).load(filter)) {
        const auto& frame = frameById.at(detectionIdToFrameId.at(detection.id()));
        const db::TId externalFeatureId = frameIdToExternalFeatureId.at(frame.id());

        const auto attrs = detection.attrs<db::eye::DetectedSign>();

        const auto& candidates = externalFeatureIdToDetections.at(externalFeatureId);

        const auto it = std::find_if(
            candidates.begin(), candidates.end(),
            [&](const auto& other) {
                return other.type == attrs.type and other.box == attrs.box;
            }
        );

        REQUIRE(it != candidates.end(), "Missed detection " << detection.id());

        const FeatureObjectId featureObjectId {externalFeatureId, it->id};
        featureObjectIdToDetectionId.emplace(featureObjectId, detection.id());
    }

    return featureObjectIdToDetectionId;
}

Playground& Playground::dumpDetectionsAsJson(const std::string& detectionJsonPath, db::eye::DetectionType type)
{
    auto txn = getMasterWriteTxn(*(config_.mrcPool));

    const auto frameById = loadFrameById(*txn);
    const auto detectionIdToGroupId = loadDetectionIdToGroupId(*txn);
    const auto groupIdToFrameId = loadGroupIdToFrameId(*txn);
    const auto frameIdToFeatureId = loadFrameIdToFeatureId(*txn);

    const IdMapChain detectionIdToFrameId {
        std::addressof(detectionIdToGroupId),
        std::addressof(groupIdToFrameId),
    };

    const IdMapChain frameIdToExternalFeatureId {
        std::addressof(frameIdToFeatureId),
        std::addressof(featureIdToExternalFeatureId_)
    };

    auto filter = db::eye::table::Detection::groupId == db::eye::table::DetectionGroup::id
        and db::eye::table::DetectionGroup::type == type
        and not db::eye::table::Detection::deleted;

    db::IdTo<SignDetections> externalFeatureIdToSigns;
    db::IdTo<int32_t> externalFeatureIdToExifOrientation;
    for (auto&& detection: db::eye::DetectionGateway(*txn).load(filter)) {
        const auto& frame = frameById.at(detectionIdToFrameId.at(detection.id()));
        const db::TId externalFeatureId = frameIdToExternalFeatureId.at(frame.id());

        const auto attrs = detection.attrs<db::eye::DetectedSign>();

        externalFeatureIdToSigns[externalFeatureId].emplace_back(detection.id(), attrs.type, attrs.box);
        externalFeatureIdToExifOrientation[externalFeatureId] = static_cast<int32_t>(frame.orientation());
    }

    std::ofstream out(detectionJsonPath);
    json::Builder builder(out);

    builder << [&](json::ObjectBuilder result) {
        result["features_objects"] << [&](json::ArrayBuilder featuresObjects) {
            for (const auto& pair: externalFeatureIdToSigns) {
                featuresObjects << [&](json::ObjectBuilder featureObjects) {
                    const auto& [externalFeatureId, signs] = pair;
                    featureObjects["feature_id"] = externalFeatureId;
                    featureObjects["orientation"] = externalFeatureIdToExifOrientation.at(externalFeatureId);
                    featureObjects["objects"] << signs;
                };
            }
        };
    };

    return *this;
}

namespace {

static const float GT_MATCH_CONFIDENCE = std::numeric_limits<float>::infinity();

template<class Worker>
void run(Worker& worker, size_t batchSize = 10000)
{
    for (; ;) {
        if (not worker.processBatchInLoopMode(batchSize)) {
            return;
        }
    }
}

MatchedFrameDetections loadDetectionMatches(
    const std::string& path,
    const db::IdTo<db::TId>& featureIdToFrameId,
    const std::map<FeatureObjectId, db::TId>& featureObjectIdToDetectionId)
{
    MatchedFrameDetections matches;
    json::Value value = json::Value::fromFile(path);

    for (const auto& featuresPair: value["features_pairs"]) {
        const auto frameId0 = featureIdToFrameId.at(featuresPair["feature_id_1"].as<db::TId>());
        const auto frameId1 = featureIdToFrameId.at(featuresPair["feature_id_2"].as<db::TId>());

        for (const auto& match : featuresPair["matches"]) {
            const float relevance = match.hasField("confidence")
                ? match["confidence"].as<float>()
                : GT_MATCH_CONFIDENCE;

            std::optional<MatchedData> matchedData;
            if (match.hasField("data")) {
                json::Value data = match["data"];
                matchedData = {
                    .goodPtsCnt = data["good_pts_count"].as<int>(),
                    .sampsonDistance = data["sampson_distance"].as<double>(),
                    .fundMatrix = (cv::Mat_<double>(3, 3) <<
                        data["fund_matrix"][0].as<double>(), data["fund_matrix"][1].as<double>(), data["fund_matrix"][2].as<double>(),
                        data["fund_matrix"][3].as<double>(), data["fund_matrix"][4].as<double>(), data["fund_matrix"][5].as<double>(),
                        data["fund_matrix"][6].as<double>(), data["fund_matrix"][7].as<double>(), data["fund_matrix"][8].as<double>()),
                    .hullDistance0 =  data["hull_distance0"].as<double>(),
                    .hullDistance1 =  data["hull_distance1"].as<double>()
                };
            }

            const auto detectionId0 = featureObjectIdToDetectionId.at(
                {featuresPair["feature_id_1"].as<db::TId>(), match["object_id_1"].as<db::TId>()}
            );
            const auto detectionId1 = featureObjectIdToDetectionId.at(
                {featuresPair["feature_id_2"].as<db::TId>(), match["object_id_2"].as<db::TId>()}
            );

            matches.emplace_back(
                FrameDetectionId{.frameId = frameId0, .detectionId = detectionId0},
                FrameDetectionId{.frameId = frameId1, .detectionId = detectionId1},
                relevance,
                std::nullopt,  // verdict
                std::move(matchedData)
            );
        }
    }
    return matches;
}

class PredefinedDetectionMatcher : public DetectionMatcher {
public:
    PredefinedDetectionMatcher(const MatchedFrameDetections& matches)
    {
        fillMatches(matches);
    }

    MatchedFrameDetections makeMatches(
        const DetectionStore& /*store*/,
        const DetectionIdPairSet& detectionPairs,
        const FrameMatcher* = nullptr) const override
    {
        MatchedFrameDetections matches;
        for (const DetectionIdPair& detectionPair : detectionPairs) {
            const auto it = matches_.find(detectionPair);
            if (it != matches_.end()) {
                matches.push_back(it->second);
            } else {
                DetectionIdPair revDetectionPair;
                revDetectionPair.first = detectionPair.second;
                revDetectionPair.second = detectionPair.first;

                const auto revIt = matches_.find(revDetectionPair);
                if (revIt != matches_.end()) {
                    matches.push_back(reverseMatch(revIt->second));
                }
            }
        }

        return matches;
    }

private:
    std::map<std::pair<db::TId, db::TId>, MatchedFrameDetection> matches_;
    void fillMatches(const MatchedFrameDetections& matches) {
        for (size_t i = 0; i < matches.size(); i++) {
            const MatchedFrameDetection& match = matches[i];
            matches_.emplace(
                std::make_pair(match.id0().detectionId, match.id1().detectionId),
                match);
        }
    }
};

class MatchesStore : public DetectionMatcher {
public:
    MatchesStore(std::shared_ptr<DetectionMatcher> matcher)
        : matcher_(std::move(matcher))
    {}

    MatchedFrameDetections makeMatches(
        const DetectionStore& store,
        const DetectionIdPairSet& detectionPairs,
        const FrameMatcher* frameMatcherPtr) const override
    {
        MatchedFrameDetections matches = matcher_->makeMatches(store, detectionPairs, frameMatcherPtr);

        for (size_t i = 0; i < matches.size(); i++) {
            const MatchedFrameDetection& match = matches[i];
            const std::pair<db::TId, db::TId> pairId = {match.id0().detectionId, match.id1().detectionId};
            if (detectionPairMatched_.count(pairId)) {
                continue;
            }
            detectionPairMatched_.insert(pairId);
            matches_.push_back(match);
        }

        return matches;
    }

    const MatchedFrameDetections& getStoredMatches() const {
        return matches_;
    }

private:
    std::shared_ptr<DetectionMatcher> matcher_;

    mutable std::set<std::pair<db::TId, db::TId>> detectionPairMatched_;
    mutable MatchedFrameDetections matches_;
};

class PredefinedMatchesObjectManager: public ObjectManager
{
public:
    PredefinedMatchesObjectManager(
        const ObjectManagerConfig& config,
        const MatchedFrameDetections& matches)
        : ObjectManager(config)
    {
        // Подменяем матчер, на матчер с загруженными матчами
        detectionMatcher_ = std::make_shared<MatchesStore>(std::make_shared<PredefinedDetectionMatcher>(matches));
    }

    const MatchedFrameDetections& getMatches() const {
        MatchesStore* store = dynamic_cast<MatchesStore*>(detectionMatcher_.get());
        REQUIRE(store != nullptr, "Corrupted matches store");
        return store->getStoredMatches();
    }
};

class PlaygroundObjectManager : public ObjectManager
{
public:
    PlaygroundObjectManager(const ObjectManagerConfig& config)
        : ObjectManager(config)
    {
        detectionMatcher_ = std::make_shared<MatchesStore>(std::move(detectionMatcher_));
    }

    const MatchedFrameDetections& getMatches() const {
        MatchesStore* store = dynamic_cast<MatchesStore*>(detectionMatcher_.get());
        REQUIRE(store != nullptr, "Corrupted matches store");
        return store->getStoredMatches();
    }
};

} // namespace

Playground& Playground::runImportMrc()
{
    ImportMrc worker(makeImportMrcConfig(config_));
    run(worker);
    return *this;
}

Playground& Playground::runDetectSign()
{
    DetectSign worker(makeDetectSignConfig(config_));
    run(worker);
    return *this;
}

Playground& Playground::runSyncDetection()
{
    SyncDetection worker(makeSyncDetectionConfig(config_));
    run(worker);
    return *this;
}

Playground& Playground::runObjectManager()
{
    PlaygroundObjectManager worker(makeObjectManagerConfig(config_));
    run(worker);
    matches_ = worker.getMatches();
    return *this;
}

Playground& Playground::runObjectManager(
        const std::string& importDetectionsJsonPath,
        const std::string& importMatchesJsonPath)
{
    const db::IdTo<db::TId> featureIdToFrameId = loadExternalFeatureIdToFrameId();

    const std::map<FeatureObjectId, db::TId> featureObjectIdToDetectionId = \
            loadFeatureObjectIdToSignDetectionId(importDetectionsJsonPath);

    MatchedFrameDetections matches = loadDetectionMatches(
        importMatchesJsonPath, featureIdToFrameId, featureObjectIdToDetectionId
    );

    PredefinedMatchesObjectManager worker(
        makeObjectManagerConfig(config_),
        matches
    );
    run(worker);
    matches_ = worker.getMatches();
    return *this;
}

Playground& Playground::dumpClustersAsJson(const std::string& path, db::eye::DetectionType type)
{
    auto txn = getMasterWriteTxn(*(config_.mrcPool));

    const auto detectionIdToGroupId = loadDetectionIdToGroupId(*txn);
    const auto groupIdToFrameId = loadGroupIdToFrameId(*txn);
    const auto frameIdToFeatureId = loadFrameIdToFeatureId(*txn);

    const IdMapChain detectionIdToExternalFeatureId {
        std::addressof(detectionIdToGroupId),
        std::addressof(groupIdToFrameId),
        std::addressof(frameIdToFeatureId),
        std::addressof(featureIdToExternalFeatureId_)
    };

    const auto objects = db::eye::ObjectGateway(*txn).load(
        db::eye::table::DetectionGroup::type == type
            and db::eye::table::DetectionGroup::id == db::eye::table::Detection::groupId
            and not db::eye::table::Detection::deleted
            and db::eye::table::Detection::id == db::eye::table::Object::primaryDetectionId
            and not db::eye::table::Object::deleted
    );

    db::IdTo<db::TIds> primaryIdToDetectionIds;
    for (const auto& object: objects) {
        primaryIdToDetectionIds[object.primaryDetectionId()].emplace_back(object.primaryDetectionId());
    }

    const auto pairs = db::eye::PrimaryDetectionRelationGateway(*txn).load(
        db::eye::table::PrimaryDetectionRelation::primaryDetectionId.in(collectIds(primaryIdToDetectionIds))
            and not db::eye::table::PrimaryDetectionRelation::deleted
    );

    for (const auto& pair: pairs) {
        primaryIdToDetectionIds[pair.primaryDetectionId()].emplace_back(pair.detectionId());
    }

    std::ofstream out(path);
    json::Builder builder(out);

    builder << [&](json::ObjectBuilder result) {
        result["clusters"] << [&](json::ArrayBuilder clusters) {
            for (auto pair: primaryIdToDetectionIds) {
                clusters << [&](json::ObjectBuilder cluster) {
                    const auto primaryId = pair.first;
                    const auto& detectionIds = pair.second;

                    cluster["cluster_id"] = primaryId;
                    cluster["objects"] << [&](json::ArrayBuilder detections) {
                        for (auto detectionId: detectionIds) {
                            detections << [&](json::ObjectBuilder object) {
                                const db::TId externalFeatureId = detectionIdToExternalFeatureId.at(detectionId);
                                object["feature_id"] = externalFeatureId;
                                object["object_id"] = detectionId;
                            };
                        }
                    };
                };
            }
        };
    };


    return *this;
}

Playground& Playground::dumpMatchesAsJson(const std::string& path)
{
    auto txn = getMasterWriteTxn(*(config_.mrcPool));

    const auto detectionIdToGroupId = loadDetectionIdToGroupId(*txn);
    const auto groupIdToFrameId = loadGroupIdToFrameId(*txn);
    const auto frameIdToFeatureId = loadFrameIdToFeatureId(*txn);

    const IdMapChain detectionIdToExternalFeatureId {
        std::addressof(detectionIdToGroupId),
        std::addressof(groupIdToFrameId),
        std::addressof(frameIdToFeatureId),
        std::addressof(featureIdToExternalFeatureId_)
    };

    std::map<std::pair<db::TId, db::TId>, MatchedFrameDetections> matchesByFeatureIdPairs;
    for (size_t i = 0; i < matches_.size(); i++) {
        MatchedFrameDetection match = matches_[i];
        // match.id0.frameId = detectionIdToExternalFeatureId.at(match.id0().detectionId);
        // match.id1.frameId = detectionIdToExternalFeatureId.at(match.id1().detectionId);
        matchesByFeatureIdPairs[{match.id0().frameId, match.id1().frameId}].emplace_back(match);
    }

    std::ofstream ofs(path);
    json::Builder builder(ofs);

    builder << [&](json::ObjectBuilder b) {
        b["features_pairs"] << [&](json::ArrayBuilder b) {
            for (const auto& featureIdPairsAndMatches : matchesByFeatureIdPairs) {
                const std::pair<db::TId, db::TId>& featureIdPairs = featureIdPairsAndMatches.first;
                const MatchedFrameDetections& matches = featureIdPairsAndMatches.second;
                b << [&](json::ObjectBuilder b) {
                    b["feature_id_1"] = featureIdPairs.first;
                    b["feature_id_2"] = featureIdPairs.second;
                    b["matches"] << [&](json::ArrayBuilder b) {
                        for (const auto& match : matches) {
                            b << [&](json::ObjectBuilder b) {
                                b["object_id_1"] = match.id0().detectionId;
                                b["object_id_2"] = match.id1().detectionId;
                                if (match.relevance() != GT_MATCH_CONFIDENCE) {
                                    b["confidence"] = match.relevance();
                                }
                                if (match.data()) {
                                    b["data"] << [&](json::ObjectBuilder b) {
                                        b["good_pts_count"] = match.data()->goodPtsCnt;
                                        b["sampson_distance"] = match.data()->sampsonDistance;
                                        b["fund_matrix"] << [&](json::ArrayBuilder b) {
                                            REQUIRE(match.data()->fundMatrix.cols == 3 &&
                                                    match.data()->fundMatrix.rows == 3 &&
                                                    match.data()->fundMatrix.depth() == CV_64F,
                                                    "Invalid fundamental matrix");
                                            for (int row = 0; row < 3; row++) {
                                                const double *ptr = match.data()->fundMatrix.ptr<double>(row);
                                                for (int col = 0; col < 3; col++) {
                                                    b << ptr[col];
                                                }
                                            }
                                        };
                                        b["hull_distance0"] = match.data()->hullDistance0;
                                        b["hull_distance1"] = match.data()->hullDistance1;
                                    };
                                }
                            };
                        }
                    };
                };
            }
        };
    };

    return *this;
}

Playground& Playground::dumpClustersAsGeoJson(const std::string& path, db::eye::DetectionType type)
{
    auto txn = getMasterWriteTxn(*(config_.mrcPool));

    const auto frameById = loadFrameById(*txn);
    const auto detectionIdToGroupId = loadDetectionIdToGroupId(*txn);
    const auto groupIdToFrameId = loadGroupIdToFrameId(*txn);
    const auto frameIdToFeatureId = loadFrameIdToFeatureId(*txn);

    db::IdTo<db::eye::FrameLocation> frameLocationById;
    for (const auto& frameLocation : db::eye::FrameLocationGateway(*txn).load()) {
        frameLocationById.emplace(frameLocation.frameId(), frameLocation);
    }

    auto detectionsFilter = db::eye::table::Detection::groupId == db::eye::table::DetectionGroup::id
        and db::eye::table::DetectionGroup::type == type
        and not db::eye::table::Detection::deleted;

    db::IdTo<db::eye::Detection> detectionById;
    for (const auto& detection : db::eye::DetectionGateway(*txn).load(detectionsFilter)) {
        detectionById.emplace(detection.id(), detection);
    }

    const IdMapChain detectionIdToExternalFeatureId {
        std::addressof(detectionIdToGroupId),
        std::addressof(groupIdToFrameId),
        std::addressof(frameIdToFeatureId),
        std::addressof(featureIdToExternalFeatureId_)
    };

    const IdMapChain detectionIdToFrameId {
        std::addressof(detectionIdToGroupId),
        std::addressof(groupIdToFrameId),
    };

    const auto objects = db::eye::ObjectGateway(*txn).load(
        db::eye::table::DetectionGroup::type == type
            and db::eye::table::DetectionGroup::id == db::eye::table::Detection::groupId
            and not db::eye::table::Detection::deleted
            and db::eye::table::Detection::id == db::eye::table::Object::primaryDetectionId
            and not db::eye::table::Object::deleted
    );

    db::IdTo<db::eye::ObjectLocation> objectLocationById;
    for (const auto& objectLocation : db::eye::ObjectLocationGateway(*txn).load()) {
        objectLocationById.emplace(objectLocation.objectId(), objectLocation);
    }

    db::IdTo<db::TIds> primaryIdToDetectionIds;
    for (const auto& object: objects) {
        primaryIdToDetectionIds[object.primaryDetectionId()].emplace_back(object.primaryDetectionId());
    }

    const auto pairs = db::eye::PrimaryDetectionRelationGateway(*txn).load(
        db::eye::table::PrimaryDetectionRelation::primaryDetectionId.in(collectIds(primaryIdToDetectionIds))
            and not db::eye::table::PrimaryDetectionRelation::deleted
    );

    for (const auto& pair: pairs) {
        primaryIdToDetectionIds[pair.primaryDetectionId()].emplace_back(pair.detectionId());
    }

    std::ofstream out(path);
    json::Builder builder(out);

    builder << [&](json::ObjectBuilder b) {
        b["data"] << [&](json::ObjectBuilder b) {
            b["type"] = "FeatureCollection";
            b["features"] << [&](json::ArrayBuilder b) {
                for (const auto& object : objects) {
                    db::eye::SignAttrs signAttrs = object.attrs<db::eye::SignAttrs>();
                    const auto& objectLocation = objectLocationById.at(object.id());
                    b << [&](json::ObjectBuilder b) {
                        b["type"] = "Feature";
                        b["id"] = object.id();
                        b["geometry"] << geolib3::geojson(objectLocation.geodeticPos());
                        b["properties"] = [&](json::ObjectBuilder b) {
                            b["type"] = toString(signAttrs.type);
                            b["heading"] = eye::decomposeRotation(objectLocation.rotation()).heading.value();
                            b["photos"] << [&](json::ArrayBuilder b) {
                                const auto& detectionIds = primaryIdToDetectionIds.at(object.primaryDetectionId());
                                for (auto detectionId : detectionIds) {
                                    const auto& frame = frameById.at(detectionIdToFrameId.at(detectionId));
                                    const auto& frameLocation = frameLocationById.at(frame.id());
                                    const db::TId externalFeatureId = detectionIdToExternalFeatureId.at(detectionId);
                                    const auto& detection = detectionById.at(detectionId);
                                    const auto attrs = detection.attrs<db::eye::DetectedSign>();
                                    const auto box =  common::transformByImageOrientation(
                                        attrs.box,
                                        frame.originalSize(),
                                        frame.orientation()
                                    );
                                    b << [&](json::ObjectBuilder b) {
                                        b["geometry"] << geolib3::geojson(frameLocation.geodeticPos());
                                        b["heading"] = eye::decomposeRotation(frameLocation.rotation()).heading.value();
                                        b["date"] = chrono::formatIsoDateTime(frame.time());
                                        std::ostringstream os;
                                        os << "https://core-nmaps-mrc-browser.maps.yandex.ru/feature/"
                                            << externalFeatureId
                                            << "/image?boxes=" << box.minX() << "," << box.minY()
                                            << "," << box.maxX() << "," << box.maxY();
                                        b["url"] = os.str();
                                    };
                                }
                            };
                        };
                    };
                }
            };
        };
    };

    return *this;
}

} // namespace maps::mrc::eye
