#include "build_patch_feed.h"

#include "altay_reader.h"
#include "config.h"
#include "delete_queue.h"
#include "delete_unpublished_pois.h"
#include "db_helpers.h"
#include "helpers.h"
#include "indoor_candidates.h"
#include "message.h"
#include "nmaps_export_result.h"
#include "names_stat.h"
#include "patches_context.h"
#include "queue.h"
#include "stat.h"
#include "sync_geoproduct_flag.h"

#include <maps/wikimap/mapspro/libs/poi_feed/include/helpers.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/json/include/builder.h>
#include <maps/libs/common/include/make_batches.h>
#include <maps/libs/log8/include/log8.h>

#include <pqxx/pqxx>
#include <unordered_set>

namespace maps::wiki::merge_poi {
namespace {
const size_t LON_LAT_QUERY_PRECISION = 12;

void
writeAltayData(const poi_feed::FeedObjectDataVector& objectsData, const MergeTaskId taskId, pqxx::transaction_base& work)
{
    auto objectsDataBatches = maps::common::makeBatches(objectsData, QUERY_BATCH_SIZE);
    INFO() << "Writing batches " << objectsDataBatches.size();
    for (const auto& batch : objectsDataBatches) {
        std::stringstream query;
        query.precision(LON_LAT_QUERY_PRECISION);
        query
            <<
                "INSERT INTO " + MERGE_POI_ALTAY_FEED_DATA +
                " (task_id, object_id, permalink, to_delete, data_json, the_geom, revision) "
            << " VALUES ";

        query <<
            common::join(batch, [&](const auto& objectData) {
                std::stringstream subQuery;
                subQuery << "("
                    << taskId << ","
                    << objectData.nmapsId() << ","
                    << objectData.permalink() << ","
                    << (objectData.toDelete() ? "true" : "false") << ","
                    << work.quote(objectData.toJson()) << ",";
                if (objectData.position()) {
                    subQuery
                        << "ST_Transform(ST_SetSRID(ST_MakePoint("
                        << objectData.position()->lon << ","
                        << objectData.position()->lat << "), 4326), 3395)";
                } else {
                    subQuery << "NULL";
                }
                subQuery
                    << ","
                    << chrono::sinceEpoch<std::chrono::milliseconds>(objectData.actualizationDate())
                    << ")";
                return subQuery.str();
            },
            ',');
        work.exec(query.str());
    }
}

void
dumpNyakMappingUnknownForExport(
    const std::string& nyakMappingUnknownYTURL,
    const MergeTaskId taskId,
    pqxx::transaction_base& socialTxn)
{
    socialTxn.exec("TRUNCATE " + MERGE_POI_ALTAY_FEED_UNKNOWN);
    const auto nyakMappingUnknown = readNyakMappingUnknown(nyakMappingUnknownYTURL);
    INFO() << "Unknown companies mapping size: " << nyakMappingUnknown.size();
    auto dataBatches = maps::common::makeBatches(nyakMappingUnknown, QUERY_BATCH_SIZE);
    for (const auto& batch : dataBatches) {
        std::stringstream query;
        query
            <<
                "INSERT INTO " + MERGE_POI_ALTAY_FEED_UNKNOWN +
                " (task_id, object_id, permalink) "
            << " VALUES ";

        query <<
            common::join(batch, [&](const auto& data) {
                std::stringstream subQuery;
                subQuery << "("
                    << taskId << ","
                    << data.nmapsId << ","
                    << data.permalink
                    << ")";
                return subQuery.str();
            },
            ',');
        socialTxn.exec(query.str());
    }
}

boost::optional<chrono::TimePoint>
lastAltayDataDate(pqxx::transaction_base& work)
{
    auto r = work.exec("SELECT max(altay_data_date) FROM " + MERGE_POI_TASK);
    if (r.empty() || r[0][0].is_null()) {
        return boost::none;
    }
    return chrono::parseSqlDateTime(r[0][0].c_str());
}

std::unordered_set<poi_feed::ObjectId>
loadWorkLimitExceeded(pqxx::transaction_base& work)
{
    std::unordered_set<poi_feed::ObjectId> ids;
    auto rows = work.exec(
        "SELECT object_id FROM " +
        MERGE_POI_PATCH_QUEUE_ARCHIVE +
        " WHERE text(messages)='[\"" + message::WORK_LIMIT_EXCEEDED +"\"]'");
    for (const auto& r : rows) {
        ids.insert(r[0].as<poi_feed::ObjectId>());
    }
    return ids;
}

std::string
json(const Summary& summary)
{
    maps::json::Builder builder;
    builder << [&](maps::json::ObjectBuilder object) {
        object["toDelete"] = summary.toDelete;
        object["toMove"] = summary.toMove;
        object["toRenameOfficial"] = summary.toRenameOfficial;
        object["toRenameShort"] = summary.toRenameShort;
        object["toChangeRubrics"] = summary.toChangeRubrics;
        object["addedObjects"] = summary.addedObjects;
        object["deletedObjects"] = summary.deletedObjects;
    };
    return builder.str();
}

PatchesContext
buildPatches(
    StatData& statistics,
    const poi_feed::FeedObjectDataVector& curObjectsData,
    const poi_feed::FeedObjectDataVector& prevObjectsData,
    const NmapsExportResult& exportData)
{
    std::unordered_set<poi_feed::ObjectId> curObjectsIds;
    for (const auto& curObjectData: curObjectsData) {
        curObjectsIds.emplace(curObjectData.nmapsId());
    }
    auto prevObjectsIdsIndex = createIndex(prevObjectsData);

    PatchesContext patchesContext;
    auto& summary = patchesContext.summary;
    for (const auto& curObject : curObjectsData) {
        const auto nmapsId = curObject.nmapsId();
        if (curObject.toDelete()) {
            summary.toDelete++;
        }
        auto prevIt = prevObjectsIdsIndex.find(nmapsId);
        if (prevIt == prevObjectsIdsIndex.end()) {
            summary.addedObjects++;
            continue;
        }
        const auto& prevObject = prevObjectsData[prevIt->second];
        if (prevObject.toDelete() == curObject.toDelete()) {
            summary.toDelete--;
        }
        if (curObject.toDelete()) {
            continue;
        }
        if (curObject.rubricId() != prevObject.rubricId()) {
            summary.toChangeRubrics++;
        }
        if (curObject.names() != prevObject.names()) {
            summary.toRenameOfficial++;
        }
        if (curObject.shortNames() != prevObject.shortNames()) {
            summary.toRenameShort++;
        }
        if (curObject.position() && prevObject.position() &&
            *curObject.position() != *prevObject.position()) {
            summary.toMove++;
        }
    }

    for (const auto& prevObject : prevObjectsData) {
        if (!curObjectsIds.count(prevObject.nmapsId())) {
            summary.deletedObjects++;
            continue;
        }
    }

    for (const auto& curObject : curObjectsData) {
        const auto exportObject = exportData.objectData(curObject.nmapsId());
        if (!exportObject) {
            WARN() << "Altay feed contains object not present in export: " << curObject.nmapsId();
            continue;
        }
        if (!exportObject->ftTypeId()) {
            continue;
        }
        if (exportObject->actualizationDate() > curObject.actualizationDate()) {
            ++statistics.outdated;
            continue;
        }
        if (
            curObject.toDelete() ||
            curObject.rubricId() != exportObject->rubricId() ||
            curObject.names() != exportObject->names() ||
            curObject.shortNames() != exportObject->shortNames() ||
            *curObject.position() != *exportObject->position() ||
            curObject.permalink() != exportObject->permalink()
        ) {
            patchesContext.patches.push_back(curObject);
            patchesContext.patches.back().setFtTypeId(exportObject->ftTypeId());
            patchesContext.patches.back().setIndoorLevelUniversal(exportObject->indoorLevelUniversal());
        }
    }

    return patchesContext;
}

void
buildPatchesStat(
    StatData& stat,
    const poi_feed::FeedObjectDataVector& patches,
    const NmapsExportResult& exportData,
    const poi_feed::FeedSettingsConfig& feedSettingsCfg)
{
    for (const auto& patch : patches) {
        const auto exportObject = exportData.objectData(patch.nmapsId());
        if (!exportObject) {
            continue;
        }
        if (!exportObject->ftTypeId()) {
            continue;
        }
        if (exportObject->actualizationDate() != patch.actualizationDate()) {
            continue;
        }
        const auto* feedSetting = feedSettingsCfg.feedSettings(*patch.ftTypeId());
        if (!feedSetting) {
            continue;
        }
        if (patch.toDelete()) {
            ++stat.toDelete;
            continue;
        }
        if (patch.rubricId() != exportObject->rubricId()) {
            ++stat.toChangeRubric;
        }
        if (patch.names() != exportObject->names() ||
            patch.shortNames() != exportObject->shortNames()) {
            ++stat.toRename;
        }
        if (*patch.position() != *exportObject->position()) {
            auto distance = patch.position()->distanceMeters(*exportObject->position());

            if (distance > 500) {
                ++stat.toMoveByDistance.over500MetersAway;
            }
            if (distance > 100) {
                ++stat.toMoveByDistance.over100MetersAway;
            }
            if (distance > 50) {
                ++stat.toMoveByDistance.over50MetersAway;
            }
            if (distance > 20) {
                ++stat.toMoveByDistance.over20MetersAway;
            }
            if (distance > 10) {
                ++stat.toMoveByDistance.over10MetersAway;
            }
            if (distance > 5) {
                ++stat.toMoveByDistance.over5MetersAway;
            }
            if (distance > 1) {
                ++stat.toMoveByDistance.over1MetersAway;
            }

            if (distance > feedSetting->minDistance) {
                ++stat.toMove;
            }
        }
    }
}

MergeTaskId
recentTaskId(pqxx::transaction_base& socialTxn)
{
    auto r = socialTxn.exec("SELECT max(task_id) FROM " + MERGE_POI_TASK);
    if (r.empty() || r[0][0].is_null()) {
        return 0;
    }
    return r[0][0].as<MergeTaskId>();
}

poi_feed::FeedObjectDataVector
readRecentTaskObjectsData(pqxx::transaction_base& socialTxn)
{
    poi_feed::FeedObjectDataVector result;
    const auto recentRunId = recentTaskId(socialTxn);
    if (!recentRunId) {
        INFO() << "No previous runs found.";
        return {};
    }
    INFO() << "Found previous run id: " << recentRunId;
    auto rows = socialTxn.exec(
        "SELECT data_json FROM " + MERGE_POI_ALTAY_FEED_DATA +
        " WHERE task_id = "
        + std::to_string(recentRunId));
    poi_feed::FeedObjectDataVector recentData;
    recentData.reserve(rows.size());
    INFO() << "Loading previous run size: " << rows.size();
    for (const auto& row : rows) {
        recentData.emplace_back(row[0].as<std::string>());
    }
    INFO() << "Done loading previous run";
    return recentData;
}

void
truncateRecentTaskObjectsData(pqxx::transaction_base& socialTxn)
{
    socialTxn.exec("TRUNCATE " + MERGE_POI_ALTAY_FEED_DATA);
}

void
writeTaskSummary(const Summary& summary, chrono::TimePoint altayDataRevision,
    MergeTaskId taskId, pqxx::transaction_base& socialTxn)
{
    socialTxn.exec(
        "INSERT INTO " + MERGE_POI_TASK +
        "(task_id, altay_data_date, summary_json, created_at)"
        " VALUES (" + std::to_string(taskId)
            + "," + socialTxn.quote(chrono::formatSqlDateTime(altayDataRevision))
            + ", " + socialTxn.quote(json(summary))
            + ", NOW());");
}

std::unordered_set<poi_feed::ObjectId>
findMoveDuplicatePatches(
    poi_feed::FeedObjectDataVector& patches,
    const NmapsExportResult& exportData,
    const poi_feed::FeedSettingsConfig& feedSettingsCfg)
{
    std::unordered_map<poi_feed::PermalinkId, std::vector<size_t>> patchIdxByPermalink;
    std::unordered_set<poi_feed::ObjectId> moveDuplicateIds;
    for (size_t i = 0; i < patches.size(); ++i) {
        const auto& patch = patches[i];
        if (patch.toDelete()) {
            continue;
        }
        if (!feedSettingsCfg.feedSettings(*patch.ftTypeId())) {
            continue;
        }
        patchIdxByPermalink[patch.permalink()].push_back(i);
    }
    for (const auto& [permalink, patchesIdxWithSamePermalink] : patchIdxByPermalink) {
        if (patchesIdxWithSamePermalink.size() == 1) {
            continue;
        }
        for (const auto& patchIdx : patchesIdxWithSamePermalink) {
            const auto& patch = patches[patchIdx];
            const auto exportObject = exportData.objectData(patch.nmapsId());
            auto distance = patch.position()->distanceMeters(*exportObject->position());
            const auto* feedSetting = feedSettingsCfg.feedSettings(*patch.ftTypeId());
            if (!feedSetting) {
                continue;
            }
            if (distance > feedSetting->minDistance) {
                moveDuplicateIds.insert(patch.nmapsId());
            }
        }
    }
    return moveDuplicateIds;
}

poi_feed::FeedObjectDataVector
filterByIds(
    poi_feed::FeedObjectDataVector& patches,
    std::unordered_set<poi_feed::ObjectId> ids)
{
    if (ids.empty()) {
        return {};
    }
    poi_feed::FeedObjectDataVector rejected;
    rejected.reserve(ids.size());
    for (const auto& patch : patches) {
        if (isFullMergeImplemented(patch)) {
            continue;//TODO skip by config. Weird changes not rejected, but moderated by this
        }
        if (ids.count(patch.nmapsId())) {
            rejected.push_back(patch);
        }
    }
    auto it = std::remove_if(patches.begin(), patches.end(),
            [&](const auto& patch) {
                if (isFullMergeImplemented(patch)) {
                    return false;//TODO skip by config
                }
                return 0 < ids.count(patch.nmapsId());
            });
    patches.erase(it, patches.end());
    return rejected;
}

void
keepByIds(
    poi_feed::FeedObjectDataVector& patches,
    const std::unordered_set<poi_feed::ObjectId>& ids)
{
    ASSERT(!ids.empty());
    auto it = std::remove_if(patches.begin(), patches.end(),
            [&](const auto& patch) {
                return !ids.count(patch.nmapsId());
            });
    patches.erase(it, patches.end());
}

bool isSameLevel(
    poi_feed::ObjectId oid1,
    poi_feed::ObjectId oid2,
    const NmapsExportResult& exportData)
{
    const auto object1 = exportData.objectData(oid1);
    const auto object2 = exportData.objectData(oid2);
    if (!object1 && !object2) {
        return true;
    }
    return
        object1 &&
        object2 &&
        object1->indoorLevelUniversal() == object2->indoorLevelUniversal();
}

std::unordered_set<poi_feed::ObjectId>
findCollisionPatches(
    poi_feed::FeedObjectDataVector& patches,
    const NmapsExportResult& exportData,
    const poi_feed::FeedSettingsConfig& feedSettingsCfg)
{
    std::unordered_set<poi_feed::ObjectId> collisionIds;
    const auto patchesIdsIndex = createIndex(patches);
    PositionIndex patchesGeomIndex;
    for (const auto& patch : patches) {
        if (patch.toDelete() || !patch.position()) {
            continue;
        }
        patchesGeomIndex.insert(&*patch.position(), patch.nmapsId());
    }
    INFO() << "Index feed data by geom.";
    patchesGeomIndex.build();

    size_t progressCounter = 0;
    for (const auto& patch : patches) {
        if ((progressCounter % 100) == 0) {
            INFO()
                << "Checked " << progressCounter
                << "/" << patches.size() << " found "
                << collisionIds.size() << " collisions.";
        }
        ++progressCounter;
        if (collisionIds.count(patch.nmapsId()) || !patch.position()) {
            continue;
        }
        const auto exportObject = exportData.objectData(patch.nmapsId());
        if (!exportObject) {
            collisionIds.insert(patch.nmapsId());
            continue;
        } else {
            const auto* feedSetting = feedSettingsCfg.feedSettings(*patch.ftTypeId());
            if (!feedSetting ||
                patch.position()->distanceMeters(*exportObject->position())
                    < feedSetting->minDistance) {
                continue;
            }
        }
        if (patch.toDelete()) {
            continue;
        }
        auto movedToSamePosition = patchesGeomIndex.find(patch.position()->boundingBox());
        bool foundCollapse = false;
        for (auto otherIt = movedToSamePosition.first;
            otherIt != movedToSamePosition.second; ++otherIt) {
            const auto& otherId = otherIt->value();
            const auto& other = patches[patchesIdsIndex.at(otherId)];
            if (otherId != patch.nmapsId() &&
                patch.permalink() != other.permalink() &&
                !other.toDelete() &&
                isSameLevel(otherId, patch.nmapsId(), exportData))
            {
                collisionIds.insert(otherId);
                collisionIds.insert(patch.nmapsId());
                INFO() << "Collision with altay: " << otherId << " " << patch.nmapsId();
                foundCollapse = true;
            }
        }
        if (foundCollapse) {
            continue;
        }
        auto alreadyAtSamePosition = exportData.geomIndex().find(patch.position()->boundingBox());
        for (auto otherIt = alreadyAtSamePosition.first;
            otherIt != alreadyAtSamePosition.second; ++otherIt) {
            const auto& otherId = otherIt->value();
            if (otherId != patch.nmapsId() && isSameLevel(otherId, patch.nmapsId(), exportData)) {
                collisionIds.insert(patch.nmapsId());
                INFO() << "Collision with exported: " << otherId << " " << patch.nmapsId();
            }
        }
    }
    INFO() << "Collision points count: " << collisionIds.size();
    return collisionIds;
}
} // namespace

poi_feed::FeedObjectDataVector
filterDeleteSignalForMatchDuplicates(poi_feed::FeedObjectDataVector& patches)
{
    std::unordered_map<poi_feed::PermalinkId, size_t> permalinkCounts;
    for (const auto& patch : patches) {
        permalinkCounts[patch.permalink()] += 1;
    }
    std::unordered_set<poi_feed::ObjectId> rejectedIds;
    for (const auto& patch : patches ) {
        if (patch.toDelete() && permalinkCounts[patch.permalink()] > 1) {
            rejectedIds.insert(patch.nmapsId());
        }
    }
    return filterByIds(patches, rejectedIds);
}

poi_feed::FeedObjectDataVector
filterNotImplementedFtTypeObjects(poi_feed::FeedObjectDataVector& patches)
{
    poi_feed::FeedObjectDataVector rejected;
    auto it = std::remove_if(patches.begin(), patches.end(),
        [&](const auto& patch) {
            if (patch.ftTypeId() &&
                isFullMergeImplemented(patch))
            {
                return false;
            }
            rejected.push_back(patch);
            return true;
        });
    patches.erase(it, patches.end());
    return rejected;
}

void removeNotImplementedChanges(
    poi_feed::FeedObjectDataVector& patches,
    const NmapsExportResult& exportData,
    const poi_feed::FeedSettingsConfig& feedSettingsCfg)
{
    auto it = std::remove_if(patches.begin(), patches.end(),
        [&](const auto& patch) {
            if (patch.toDelete()) {//deletion is implemented
                return false;
            }
            const auto exportObject = exportData.objectData(patch.nmapsId());
            ASSERT (exportObject);
            const auto* feedSetting = feedSettingsCfg.feedSettings(*patch.ftTypeId());
            if (!feedSetting) {
                return true;
            }
            if (*patch.position() != *exportObject->position()) {
                auto distance = patch.position()->distanceMeters(*exportObject->position());
                if (distance > feedSetting->minDistance) {
                    return false;//moving object is implemented
                }
            }
            if (patch.permalink() != exportObject->permalink()) {
                return false;//changing permalink is implemented
            }
            if (patch.names() != exportObject->names() ||
                patch.shortNames() != exportObject->shortNames()) {
                return false;//renaming is implemented
            }
            return true;
        });
    patches.erase(it, patches.end());
}

void
buildPatchFeed(const Config& cfg, MergeTaskId taskId)
{
    const auto nyakMappingLocations = getNyakExportLocation();
    const auto& nyakMappingYTURL = nyakMappingLocations.mainUrl;
    if (nyakMappingYTURL.empty()) {
        ERROR() << "Can't get nyak_export snapshot location.";
        return;
    } else {
        INFO() << "Altay nyak_mapping snapshot: " << nyakMappingYTURL;
    }
    auto altayDataRevision = altayDataDateISO(nyakMappingYTURL);
    INFO()
        << "Current altay data date: "
        << chrono::formatIsoDateTime(altayDataRevision);
    auto socialTxn = cfg.socialPool().masterWriteableTransaction();
    auto lastDataDate = lastAltayDataDate(*socialTxn);
    bool workOnLeftByTimeout = false;
    if (lastDataDate) {
        INFO()
            << "Previous altay data date: "
            << chrono::formatIsoDateTime(*lastDataDate);
        if(*lastDataDate >= altayDataRevision) {
            INFO() << "No new data found. Checking with " << message::WORK_LIMIT_EXCEEDED;
            workOnLeftByTimeout = true;
        }
    }
    std::unordered_set<poi_feed::ObjectId> leftByTimeout;
    if (workOnLeftByTimeout) {
        leftByTimeout = loadWorkLimitExceeded(*socialTxn);
        if (leftByTimeout.empty()) {
            INFO() << "Nothing to do.";
            return;
        }
        INFO() << "Resuming work on " << leftByTimeout.size() << " patches";
    }
    INFO() << "Loading recent run altay data.";
    auto prevRunData = readRecentTaskObjectsData(*socialTxn);
    truncateRecentTaskObjectsData(*socialTxn);
    INFO() << "Transfering new altay data.";
    auto altayData = readAltayCompanies(nyakMappingYTURL, cfg.supportedNmapsLangs());
    if (workOnLeftByTimeout) {
        keepByIds(altayData, leftByTimeout);
        if (altayData.empty()) {
            INFO() << "No patches left.";
            return;
        }
    }
    dumpNyakMappingUnknownForExport(nyakMappingLocations.unknownUrl, taskId, *socialTxn);
    INFO() << "Altay read companies number: " << altayData.size();
    writeAltayData(altayData, taskId, *socialTxn);
    INFO() << "Loading recent export_poi_worker feed.";
    NmapsExportResult nmapsData(cfg);
    if (nmapsData.empty()) {
        WARN() << "Empty export_poi_worker feed. Cancel operation.";
        return;
    }
    StatData statistics;
    INFO() << "Recent export_poi_worker read " << nmapsData.size() << " records";
    statistics.exported = nmapsData.size();
    statistics.sprav = altayData.size();
    INFO() << "Build patches";
    auto patchesContext = buildPatches(statistics, altayData, prevRunData, nmapsData);
    statistics.patches = patchesContext.patches.size();
    INFO() << "Previous run compare summary: " << json(patchesContext.summary);
    INFO() << "Patch candidates found: " << patchesContext.patches.size();
    writeTaskSummary(patchesContext.summary, altayDataRevision, taskId, *socialTxn);
    patchesContext.objectIdsForActiveTasks = loadObjectIdsForActiveTasks(*socialTxn);
    INFO() << "Found active feedback tasks: " << patchesContext.objectIdsForActiveTasks.size();
    socialTxn->commit();
    socialTxn.releaseConnection();
    truncateResultsArchive(cfg);
    statistics.forProtected = countPatchesForProtectedObjects(patchesContext.patches);
    INFO() << "Filter delete signals for duplicate sprav matching.";
    auto duplicateMatchesToDelete = filterDeleteSignalForMatchDuplicates(patchesContext.patches);
    INFO() << "Archive rejected duplicate matches to delete patches: " << duplicateMatchesToDelete.size();
    writeResultsToArchive({}, duplicateMatchesToDelete, taskId, cfg,
        { Resolution::Rejected, {message::DELETE_DUPLICATE_MATCH} });
    INFO() << "Filter merge not implemented ft_type objects";
    auto patchesForNotImplementedFtTypeObjects =
        filterNotImplementedFtTypeObjects(patchesContext.patches);
    INFO() << "Archive rejected merge not implemented ft_type objects patches: "
        << patchesForNotImplementedFtTypeObjects.size();
    writeResultsToArchive({}, patchesForNotImplementedFtTypeObjects, taskId, cfg,
        { Resolution::Rejected, {message::MERGE_NOT_IMPLEMENTED} });
    INFO() << "Find move duplicates";
    auto moveDuplicates = findMoveDuplicatePatches(patchesContext.patches, nmapsData, cfg.feedSettingsConfig());
    INFO() << "Filter collision patches";
    patchesContext.movingToSamePositionIds = findCollisionPatches(patchesContext.patches, nmapsData, cfg.feedSettingsConfig());
    INFO() << "Archive rejected move duplicates";
    auto rejectedMoveDupilcates = filterByIds(patchesContext.patches, moveDuplicates);
    writeResultsToArchive({}, rejectedMoveDupilcates, taskId, cfg,
        { Resolution::Rejected, {message::MOVE_DUPLICATE} });
    removeNotImplementedChanges(patchesContext.patches, nmapsData, cfg.feedSettingsConfig());
    INFO() << "Update patches delete queue";
    DeleteQueue deleteQueue(cfg);
    patchesContext.notYetToDelete = deleteQueue.notYetToDelete(patchesContext.patches);
    INFO() << "Objects waiting in delete queue: " << patchesContext.notYetToDelete.size();
    deleteQueue.save();
    INFO() << "Process patches";
    processPatches(
        statistics,
        patchesContext,
        taskId,
        cfg);
    INFO() << "Build patches stat";
    buildPatchesStat(statistics, patchesContext.patches, nmapsData, cfg.feedSettingsConfig());
    statistics.post(cfg);
    statistics.log();
    auto namesStat = buildNamesStat(
        patchesContext.patches, nmapsData, cfg.editorCfg());
    namesStat.post(cfg);
    INFO() << "Start building move rejects stat.";
    RejectsStatData rejectsStatData;
    rejectsStatData.build(cfg);
    INFO() << "Done building move rejects stat.";
    rejectsStatData.post(cfg);
    INFO() << "Indoor candidates processing.";
    createFeedbackForIndoorCandidates(
        nyakMappingLocations.nyakIndoorCandidatesYTURL,
        nmapsData,
        cfg);
    INFO() << "Syncing geoproduct flag.";
    syncGeoproductFlag(nmapsData, cfg);
    INFO() << "Delete unpublished pois.";
    deleteUnpublishedPois(nmapsData, cfg, nyakMappingLocations.unknownUrl);
    INFO() << "Done.";
}

size_t
countPatchesForProtectedObjects(const poi_feed::FeedObjectDataVector& patches)
{
    const auto protectedFtTypes = poi_feed::loadProtectedFtTypes();
    return std::count_if(
        patches.begin(),
        patches.end(),
        [&](const auto& poiData) {
            return poiData.ftTypeId() &&
                protectedFtTypes.count(*poiData.ftTypeId());
        });
}

} // maps::wiki::merge_poi
