#include "objects_query_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/libs/geolib/include/static_geometry_searcher.h>

namespace maps::wiki {
namespace {
const std::string TASK_NAME = "ObjectsQueryPoisConflicts";
} // namespace

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

std::string
ObjectsQueryPoisConflicts::Request::dump() const
{
    std::stringstream ss;
    ss << " token: " << dbToken;
    ss << " branch: " << branchId;
    if (indoorLevelId) {
        ss << " indoor-level-id: " << *indoorLevelId;
    }
    return ss.str();
}

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

namespace {

views::ViewObject
fakeGeom(const views::ViewObject& original, const Geom& geom)
{
    return views::ViewObject(
        original.revision(),
        original.domainAttrs(),
        original.serviceAttrs(),
        geom);
}

std::map<TOid, Geom>
parseRequest(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::map<TOid, Geom> result;
    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(
                boost::lexical_cast<TOid>(objectJson[STR_ID].toString()),
                createGeomFromJson(objectJson[STR_GEOMETRY]));
        revisionIds.insert(
            boost::lexical_cast<TRevisionId>(objectJson[STR_REVISION_ID].as<std::string>()));
    }
    cache.revisionsFacade().gateway().checkConflicts(revisionIds);
    return result;
}

std::map<TOid, views::ViewObject>
viewObjectsByIds(const TOIds& ids, Transaction& workView, TBranchId branchId)
{
    if (ids.empty()) {
        return {};
    }
    views::ObjectsQuery objectsQuery;
    objectsQuery.addCondition(views::OidsCondition(ids));
    auto viewObjects = objectsQuery.exec(workView, branchId);
    std::map<TOid, views::ViewObject> result;
    for (const auto& viewObject : viewObjects) {
        result.emplace(viewObject.id(), viewObject);
    }
    return result;
}

views::ViewObjects
findCurrentObjectsInTargetArea(
    const std::map<TOid, Geom>& requestObjects,
    const StringSet& categories,
    const std::optional<TOid>& indoorLevelId,
    double searchRadius,
    Transaction& workView,
    TBranchId branchId)
{
    views::ObjectsQuery objectsQuery;
    objectsQuery.addCondition(views::CategoriesCondition(workView, categories));
    if (indoorLevelId) {
        objectsQuery.addCondition(views::AllOfServiceAttributesCondition(
            workView,
            {{srv_attrs::SRV_INDOOR_LEVEL_ID, std::to_string(*indoorLevelId)}}));
    } else {
        objectsQuery.addCondition(views::NoneOfServiceAttributesCondition(
            workView, {srv_attrs::SRV_INDOOR_LEVEL_ID}));
    }
    geos::geom::Envelope envelope;
    for (const auto& [id, geom] : requestObjects ) {
        WIKI_REQUIRE(!geom.isNull() && geom->getGeometryTypeId() == geos::geom::GEOS_POINT,
            ERR_BAD_REQUEST, "Point type geometry required: " << id);
        envelope.expandToInclude(*geom->getCoordinate());
    }
    envelope.expandBy(searchRadius);
    objectsQuery.addCondition(views::EnvelopeGeometryCondition(envelope));
    return objectsQuery.exec(workView, branchId);
}

std::map<TOid, views::ViewObject>
prepareMapIdToViewObjectWithIncomingGeometry(
    const std::map<TOid, Geom>& requestObjects,
    const views::ViewObjects& currentObjects,
    Transaction& workView,
    TBranchId branchId)
{
    std::map<TOid, views::ViewObject> result;
    for (const auto& viewObject : currentObjects) {
        auto it = requestObjects.find(viewObject.id());
        if (it != requestObjects.end()) {
            result.emplace(viewObject.id(), fakeGeom(viewObject, it->second));
        } else {
            result.emplace(viewObject.id(), viewObject);
        }
    }
    TOIds remainingIds;
    for (const auto& [id, geom] : requestObjects) {
        if (result.count(id)) {
            continue;
        }
        remainingIds.insert(id);
    }
    auto remaining = viewObjectsByIds(remainingIds, workView, branchId);
    ASSERT(remaining.size() == remainingIds.size());
    for (const auto& [id, viewObject] : remaining) {
        result.emplace(id, fakeGeom(viewObject, requestObjects.at(id)));
    }
    return result;
}

struct BboxHolder
{
    BboxHolder(double radius, const Geom& geom)
    {
        const auto buffer = geom.createBuffer(radius);
        auto envelope = buffer->getEnvelopeInternal();
        bbox = geolib3::BoundingBox(
            {
                envelope->getMinX(),
                envelope->getMinY()
            },
            {
                envelope->getMaxX(),
                envelope->getMaxY()
            });
    };
    geolib3::BoundingBox boundingBox() const
    {
        return bbox;
    }
    geolib3::BoundingBox bbox;
};

struct BboxHolders
{
    BboxHolders(double radius)
        : radius_(radius)
    {
    }

    const BboxHolder* wrap(const Geom& geom)
    {
        storage_.emplace_back(radius_, geom);
        return &storage_.back();
    }
    std::list<BboxHolder> storage_;
    const double radius_;
};

} // namespace

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

void
ObjectsQueryPoisConflicts::control()
{
    static const PoiConflictingCategories poiCategories;
    static const poi_conflicts::PoiConflicts poiConflicts;

    const CachePolicy policy {
        TableAttributesLoadPolicy::Skip,
        ServiceAttributesLoadPolicy::Load,
        DanglingRelationsPolicy::Ignore
    };
    ObjectsCache cache(
        BranchContextFacade::acquireRead(request_.branchId, request_.dbToken),
        boost::none,
        policy);
    CheckPermissions(request_.uid, cache.workCore()).checkAccessToPoisConflictsTool();
    const auto requestObjects = parseRequest(request_.requestBody, cache);
    WIKI_REQUIRE(!requestObjects.empty(),
        ERR_BAD_REQUEST,
        "Minimum one poi expected");

    auto& workView = cache.workView();
    const auto searchRadius = poiConflicts.maxConflictDistanceMercator();
    const auto objectsInArea = findCurrentObjectsInTargetArea(
        requestObjects,
        poiCategories(),
        request_.indoorLevelId,
        searchRadius,
        workView,
        request_.branchId);
    const auto idToObject = prepareMapIdToViewObjectWithIncomingGeometry(
        requestObjects,
        objectsInArea,
        workView,
        request_.branchId);

    BboxHolders bboxHolders(searchRadius);
    geolib3::StaticGeometrySearcher<BboxHolder, TOid> geomSearch;
    for (const auto& [_, viewObject] : idToObject) {
        geomSearch.insert(bboxHolders.wrap(viewObject.geom()), viewObject.id());
    }
    geomSearch.build();

    const auto escalateGeoproduct = userHasAccessToGeoproducts(
        request_.uid,
        poiCategories,
        cache.workCore());

    for (const auto& [id, geom] : requestObjects) {
        auto itRange = geomSearch.find(bboxHolders.wrap(geom)->boundingBox());
        const bool thisIsGeoproduct = isGeoproduct(idToObject.at(id));
        auto& conflicts = result_->perPoiConflicts[id];
        for (auto otherIt = itRange.first; otherIt != itRange.second; ++otherIt) {
            const auto& otherId = otherIt->value();
            if (otherId == id) {
                continue;
            }
            const auto& otherViewObject = idToObject.at(otherId);
            const auto zoomConflictKind = conflictKind(thisIsGeoproduct, isGeoproduct(otherViewObject));
            const auto zoom = poiConflicts.conflictZoom(geom.distance(otherViewObject.geom()), zoomConflictKind);
            if (!zoom) {
                continue;
            }
            conflicts[*zoom]
                [conflictSeverity(*zoom, escalateGeoproduct, zoomConflictKind)].push_back(otherViewObject);
        }
    }
}
} // namespace maps::wiki
