#include "import_entrances.h"
#include "yt_reader.h"

#include <yandex/maps/wiki/common/batch.h>
#include <yandex/maps/wiki/revision/filters.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <maps/libs/log8/include/log8.h>

#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/dynamic_geometry_searcher.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/spatial_relation.h>

#include <map>
#include <set>
#include <string>
#include <vector>

namespace rf = maps::wiki::revision::filters;

namespace maps::wiki::business_entrance_import_tool {

namespace {

// Constants are from https://epsg.io/3395
constexpr double MERCATOR_MIN_X = -20026376.39;
constexpr double MERCATOR_MIN_Y = -15496570.74;
constexpr double MERCATOR_MAX_X = 20026376.39;
constexpr double MERCATOR_MAX_Y = 18764656.23;

const geolib3::BoundingBox MERCATOR_BBOX = {
    geolib3::Point2(MERCATOR_MIN_X, MERCATOR_MIN_Y),
    geolib3::Point2(MERCATOR_MAX_X, MERCATOR_MAX_Y)
};

constexpr size_t GEOM_SEARCHER_SIZE_X = 10000;
constexpr size_t GEOM_SEARCHER_SIZE_Y = 10000;

const std::string REL_ROLE_ATTR = "rel:role";
const std::string ROLE_ENTRANCE_ASSIGNED = "entrance_assigned";
const std::string CATEGORY_KEY_PREFIX = "cat:";

constexpr size_t REVISION_BATCH_SIZE = 1000;
constexpr double TOLERANCE_METERS = 0.5;

const std::set<std::string> POI_CATEGORIES = {
    "poi_medicine",
    "poi_edu",
    "poi_finance",
    "poi_shopping",
    "poi_goverment",
    "poi_religion",
    "poi_food",
    "poi_auto",
    "poi_sport",
    "poi_leisure",
    "poi_urban",
    "poi_service"
};

std::vector<geolib3::Polygon2>
loadAreaGeoms(
    std::set<revision::DBID> areaIds,
    revision::RevisionsGateway& gateway,
    revision::DBID headCommitId)
{
    auto filter = rf::ProxyFilterExpr(
        rf::ObjRevAttr::isNotRelation() &&
        rf::ObjRevAttr::isNotDeleted() &&
        rf::Geom::defined());

    auto snapshot = gateway.snapshot(headCommitId);
    auto revIds = snapshot.revisionIdsByFilter(areaIds, filter);
    auto revisions = gateway.reader().loadRevisions(revIds);

    std::vector<geolib3::Polygon2> result;
    for (const auto& rev : revisions) {
        auto geom = rev.data().geometry;
        REQUIRE(geom, "Object " << rev.id() << " has no geometry");
        REQUIRE(geolib3::WKB::getGeometryType(*geom) == geolib3::GeometryType::Polygon,
            "Object " << rev.id() << " has non-polygon geometry");
        result.push_back(
            geolib3::WKB::read<geolib3::Polygon2>(*geom));
    }
    return result;
}

bool isPointInsideAnyArea(
    const geolib3::Point2& point,
    const std::vector<geolib3::Polygon2>& areas)
{
    for (const auto& area : areas) {
        if (geolib3::spatialRelation(
            point, area, geolib3::SpatialRelation::Within))
        {
            return true;
        }
    }
    return false;
}

YtOrganizations filterOrgsByAreas(
    const YtOrganizations& orgs,
    const std::vector<geolib3::Polygon2>& areaGeoms)
{
    YtOrganizations result;
    for (const auto& org : orgs) {
        if (isPointInsideAnyArea(org.pos, areaGeoms)) {
            result.push_back(org);
        }
    }
    return result;
}

std::string getCategoryId(const revision::ObjectRevision& rev)
{
    REQUIRE(rev.data().attributes, "Object " << rev.id() << " has no attributes");
    std::string result;
    for (const auto& [attrKey, _] : *rev.data().attributes) {
        if (attrKey.starts_with(CATEGORY_KEY_PREFIX)) {
            result = attrKey.substr(CATEGORY_KEY_PREFIX.length());
            break;
        }
    }
    REQUIRE(!result.empty(), "Object " << rev.id() << " has no category");
    return result;
}

NkData loadPoiDataFromNk(
    const YtOrganizations& orgs,
    revision::RevisionsGateway& gateway,
    revision::DBID headCommitId)
{
    auto snapshot = gateway.snapshot(headCommitId);

    auto relFilter = rf::ProxyFilterExpr(
        rf::Attr(REL_ROLE_ATTR).equals(ROLE_ENTRANCE_ASSIGNED) &&
        rf::ObjRevAttr::isNotDeleted());

    INFO() << "Detecting POIs with entrances started";

    std::set<revision::DBID> poiWithEntrancesIds;
    common::applyBatchOp(
        orgs,
        REVISION_BATCH_SIZE,
        [&](const YtOrganizations& batch) {
            std::set<revision::DBID> ftIds;
            for (const auto& org : batch) {
                ftIds.insert(org.ftId);
            }
            auto relations = snapshot.loadSlaveRelations(ftIds, relFilter);
            for (const auto& relation : relations) {
                poiWithEntrancesIds.insert(
                    relation.data().relationData->masterObjectId());
            }
        });

    INFO() << "Detecting POIs with entrances finished";
    INFO() << "POIs with entrances count: " << poiWithEntrancesIds.size();

    auto poiFilter = rf::ProxyFilterExpr(
        rf::ObjRevAttr::isNotRelation() &&
        rf::ObjRevAttr::isNotDeleted() &&
        rf::Geom::defined());

    INFO() << "Loading POI data from NK started";

    NkData result;
    common::applyBatchOp(
        orgs,
        REVISION_BATCH_SIZE,
        [&](const YtOrganizations& batch) {
            std::set<revision::DBID> ftIds;
            for (const auto& org : batch) {
                if (!poiWithEntrancesIds.contains(org.ftId)) {
                    ftIds.insert(org.ftId);
                }
            }
            auto revIds = snapshot.revisionIdsByFilter(ftIds, poiFilter);
            auto revisions = gateway.reader().loadRevisions(revIds);
            for (const auto& rev : revisions) {
                auto ftId = rev.id().objectId();
                auto catId = getCategoryId(rev);
                if (POI_CATEGORIES.contains(catId)) {
                    result[ftId] = NkPoi{ftId, getCategoryId(rev)};
                }
            }
        });

    INFO() << "Loading POI data from NK finished";
    INFO() << "POIs loaded: " << result.size();

    return result;
}

Entrances deduplicateEntrances(const YtData& ytData, const NkData& nkData)
{
    INFO() << "Entrances deduplicating started";

    Entrances result;
    geolib3::DynamicGeometrySearcher<geolib3::Point2, size_t>
        entrancesSearcher(MERCATOR_BBOX, GEOM_SEARCHER_SIZE_X, GEOM_SEARCHER_SIZE_Y);

    for (const auto& org : ytData.orgs) {
        auto isLocatedAtMall = false;
        for (const auto& permalink : org.locatedAt) {
            if (ytData.mallPermalinks.contains(permalink))
            {
                isLocatedAtMall = true;
                break;
            }
        }
        if (isLocatedAtMall) {
            continue;
        }

        auto iter = nkData.find(org.ftId);
        if (iter == nkData.end()) {
            continue;
        }
        const auto& nkPoi = iter->second;

        for (const auto& newEntrance : org.entrancePos) {
            auto bbox = geolib3::BoundingBox(
                newEntrance, 2 * TOLERANCE_METERS, 2 * TOLERANCE_METERS);
            auto closeToExisting = entrancesSearcher.find(bbox);
            if (closeToExisting.first != closeToExisting.second) {
                auto& entrance = result[closeToExisting.first->value()];
                entrance.pois.push_back(nkPoi);
            } else {
                entrancesSearcher.insert(&newEntrance, result.size());
                result.push_back({newEntrance, {nkPoi}});
            }
        }
    }

    INFO() << "Entrances deduplicating finished";
    INFO() << "Deduplicated entrances count: " << result.size();

    return result;
}

} // namespace

Entrances getEntrancesToImport(
    const std::set<revision::DBID>& areaIds,
    pgpool3::Pool& pgPool)
{
    auto ytData = readOrganizationsDataFromYt();

    auto txn = pgPool.slaveTransaction();
    auto gateway = revision::RevisionsGateway(*txn);
    auto headCommitId = gateway.headCommitId();
    INFO() << "Head commit: " << headCommitId;

    auto areaGeoms = loadAreaGeoms(areaIds, gateway, headCommitId);
    ytData.orgs = filterOrgsByAreas(ytData.orgs, areaGeoms);
    auto nkData = loadPoiDataFromNk(ytData.orgs, gateway, headCommitId);
    auto entrances = deduplicateEntrances(ytData, nkData);

    return entrances;
}

} // maps::wiki::business_entrance_import_tool
