#include "tools_resolve_pois_conflicts.h"

#include "poi_conflicts_common.h"

#include "maps/wikimap/mapspro/services/editor/src/branch_helpers.h"
#include "maps/wikimap/mapspro/services/editor/src/check_permissions.h"
#include "maps/wikimap/mapspro/services/editor/src/configs/config.h"
#include "maps/wikimap/mapspro/services/editor/src/serialize/common.h"
#include "maps/wikimap/mapspro/services/editor/src/srv_attrs/registry.h"
#include "maps/wikimap/mapspro/services/editor/src/objects_cache.h"
#include "maps/wikimap/mapspro/services/editor/src/revisions_facade.h"
#include "maps/wikimap/mapspro/services/editor/src/utils.h"
#include "maps/wikimap/mapspro/services/editor/src/views/objects_query.h"

#include <maps/wikimap/mapspro/libs/poi_feed/include/helpers.h>
#include <maps/poi/libs/conflict_solver/include/conflict_solver.h>
#include <maps/libs/geolib/include/distance.h>

#include <geos/geom/Point.h>
#include <geos/geom/Polygon.h>

using namespace maps::geolib3::internal;
using namespace maps::poi::conflict_solver;

namespace maps::wiki {
namespace {
const std::string TASK_NAME = "ToolsResolvePoisConflicts";
const std::set<std::string> VERIFIED_POSITION_QUALITY_VALUES {
    "user",
    "precise"
};
} // namespace

ToolsResolvePoisConflicts::ToolsResolvePoisConflicts(const Request& request)
    : controller::BaseController<ToolsResolvePoisConflicts>(BOOST_CURRENT_FUNCTION)
    , request_(request)
{
}

std::string
ToolsResolvePoisConflicts::Request::dump() const
{
    std::stringstream ss;
    ss << " token: " << dbToken;
    ss << " branch: " << branchId;
    return ss.str();
}

std::string
ToolsResolvePoisConflicts::printRequest() const
{
    return request_.dump();
}

namespace {

enum class ObjectIsMoveable
{
    No,
    Yes
};

struct InputObject
{
    TOid id;
    Geom geom;
    ObjectIsMoveable isMoveable = ObjectIsMoveable::No;
};

ObjectIsMoveable
objectIsMoveable(const json::Value& objectJson)
{
    if (!objectJson.hasField(STR_ATTRS)) {
        return ObjectIsMoveable::Yes;
    }
    if (!objectJson[STR_ATTRS].hasField(ATTR_POI_POSITION_QUALITY)) {
        return ObjectIsMoveable::Yes;
    }
    return
        objectJson[STR_ATTRS][ATTR_POI_POSITION_QUALITY].isString() &&
        poi_feed::isVerifiedPositionQuality(
                objectJson[STR_ATTRS][ATTR_POI_POSITION_QUALITY].toString())
            ? ObjectIsMoveable::No
            : ObjectIsMoveable::Yes;
}

std::vector<InputObject>
extractObjectsFromRequest(const std::string_view& requestBodyJson, ObjectsCache& cache)
{
    if (requestBodyJson.empty()) {
        return {};
    }
    const auto jsonObjects = json::Value::fromString(requestBodyJson);
    WIKI_REQUIRE(jsonObjects.isArray(),
        ERR_BAD_REQUEST, "Array expected as root: " << requestBodyJson);
    RevisionIds revisionIds;
    std::vector<InputObject> result;
    result.reserve(jsonObjects.size());
    for (const auto& objectJson : jsonObjects) {
        WIKI_REQUIRE(objectJson.hasField(STR_ID) &&
            objectJson.hasField(STR_REVISION_ID) &&
            objectJson.hasField(STR_GEOMETRY),
            ERR_BAD_REQUEST, "Object should contain id/revisionId/geometry " << requestBodyJson);
        result.emplace_back(InputObject {
                boost::lexical_cast<TOid>(objectJson[STR_ID].toString()),
                Geom(createGeomFromJson(objectJson[STR_GEOMETRY])),
                objectIsMoveable(objectJson)});
        revisionIds.insert(
            boost::lexical_cast<TRevisionId>(objectJson[STR_REVISION_ID].as<std::string>()));
    }
    cache.revisionsFacade().gateway().checkConflicts(revisionIds);
    return result;
}

views::ViewObjects
findBldsInTargetArea(
    const std::vector<InputObject>& inputObjects,
    Transaction& workView,
    TBranchId branchId)
{
    views::ObjectsQuery objectsQuery;
    objectsQuery.addCondition(views::CategoriesCondition(workView, {CATEGORY_BLD}));
    geos::geom::Envelope envelope;
    for (const auto& inputObject : inputObjects) {
        WIKI_REQUIRE(!inputObject.geom.isNull() &&
            inputObject.geom->getGeometryTypeId() == geos::geom::GEOS_POINT,
            ERR_BAD_REQUEST, "Point type geometry required: " << inputObject.id);
        envelope.expandToInclude(*inputObject.geom->getCoordinate());
    }
    objectsQuery.addCondition(views::EnvelopeGeometryCondition(envelope));
    auto blds = objectsQuery.exec(workView, branchId);
    std::erase_if(
        blds,
        [&](const auto& bld) {
            for (const auto& inputObject : inputObjects) {
                if (bld.geom()->intersects(inputObject.geom.geosGeometryPtr())) {
                    return false;
                }
            }
            return true;
        });
    return blds;
}

views::ViewObjects
findPOIsInTargetBlds(
    const views::ViewObjects& blds,
    Transaction& workView,
    TBranchId branchId)
{
    static const PoiConflictingCategories poiCategories;
    views::ViewObjects pois;
    std::set<TOid> alreadyFound;
    for (const auto& bld : blds) {
        views::ObjectsQuery objectsQuery;
        objectsQuery.addCondition(views::CategoriesCondition(workView, poiCategories()));
        objectsQuery.addCondition(views::IntersectsGeometryCondition(workView, bld.geom()));
        objectsQuery.addCondition(views::NoneOfServiceAttributesCondition(
            workView, {srv_attrs::SRV_INDOOR_LEVEL_ID}));
        auto bldPois = objectsQuery.exec(workView, branchId);
        for (const auto& bldPoi : bldPois) {
            if (!alreadyFound.contains(bldPoi.id())) {
                alreadyFound.insert(bldPoi.id());
                pois.emplace_back(bldPoi);
            }
        }
    }
    return pois;
}

Polygon2
geomToPolygon(const Geom& point)
{
    return geos2geolibGeometry(
        dynamic_cast<const geos::geom::Polygon*>(point.geosGeometryPtr()));
}

} // namespace

const std::string&
ToolsResolvePoisConflicts::taskName()
{
    return TASK_NAME;
}

void
ToolsResolvePoisConflicts::control()
{
    static const poi_conflicts::PoiConflicts poiConflicts;
    const auto poiConflictDistance = poiConflicts.maxConflictDistanceMercator();
    const CachePolicy policy {
        TableAttributesLoadPolicy::Skip,
        ServiceAttributesLoadPolicy::Load,
        DanglingRelationsPolicy::Ignore
    };
    ObjectsCache cache(
        BranchContextFacade::acquireRead(request_.branchId, request_.dbToken),
        boost::none,
        policy);
    const auto inputObjects = extractObjectsFromRequest(request_.requestBody, cache);
    WIKI_REQUIRE(!inputObjects.empty(),
        ERR_BAD_REQUEST,
        "Minimum one poi expected");

    TOIds inputIds;
    std::vector<TOid> moveableInputIds;
    std::vector<PointWithConflictDistance> moveablePoints;
    std::vector<PointWithConflictDistance> fixedPoints;

    for (const auto& inputObject : inputObjects) {
        inputIds.insert(inputObject.id);
        if (inputObject.isMoveable == ObjectIsMoveable::Yes) {
            moveableInputIds.emplace_back(inputObject.id);
            moveablePoints.emplace_back(
                PointWithConflictDistance {
                    geomToPoint(inputObject.geom),
                    poiConflictDistance});
        } else {
            fixedPoints.emplace_back(
                PointWithConflictDistance {
                    geomToPoint(inputObject.geom),
                    poiConflictDistance});
        }
    }
    if (moveableInputIds.empty()) {
        return;
    }
    auto& workView = cache.workView();
    auto blds = findBldsInTargetArea(inputObjects, workView, request_.branchId);
    WIKI_REQUIRE(!blds.empty(),
        ERR_NO_BUILDINGS,
        "Target positions are outside of buildings");
    auto pois = findPOIsInTargetBlds(blds, workView, request_.branchId);
    for (const auto& poi : pois) {
        if (!inputIds.contains(poi.id())) {
            fixedPoints.emplace_back(
                PointWithConflictDistance {
                    geomToPoint(poi.geom()),
                    poiConflictDistance});
        }
    }
    std::vector<Polygon2> polygons;
    for (const auto& bld : blds) {
        polygons.emplace_back(geomToPolygon(bld.geom()));
    }
    const auto solution = solvePointConflicts(
        moveablePoints,
        fixedPoints,
        polygons,
        CALCULATION_TOLERANCE);
    for (size_t i = 0; i < solution.size(); ++i) {
        if (geolib3::distance(solution[i], moveablePoints[i].mercPoint) >
            CALCULATION_TOLERANCE)
        {
            result_->movedPois.emplace(
                moveableInputIds[i],
                geolib2geosGeometry(solution[i]));
        }
    }
}
} // namespace maps::wiki
