#include "objects_update_sync_geometry.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/collection.h"
#include "maps/wikimap/mapspro/services/editor/src/commit.h"
#include "maps/wikimap/mapspro/services/editor/src/objects_cache.h"
#include "maps/wikimap/mapspro/services/editor/src/serialize/request_data.h"
#include "maps/wikimap/mapspro/services/editor/src/serialize/xml_parser.h"

#include <maps/libs/geolib/include/vector.h>

#include <geos/geom/CoordinateFilter.h>

namespace maps {
namespace wiki {

namespace {
const std::string TASK_METHOD_NAME = "ObjectsUpdateSyncGeometry";
const std::string ACTION_ID_SYNC_GEOMETRY = "sync-geometry";
const std::string STR_SYNC_WITH = "syncWith";
}

ObjectsUpdateSyncGeometry::ObjectsUpdateSyncGeometry(
        const ObserverCollection& observers,
        const Request& request,
        taskutils::TaskID asyncTaskID)
    : controller::BaseController<ObjectsUpdateSyncGeometry>(BOOST_CURRENT_FUNCTION, asyncTaskID)
    , request_(request)
    , observers_(observers)
{}

const std::string&
ObjectsUpdateSyncGeometry::taskName()
{
    return TASK_METHOD_NAME;
}

std::string
ObjectsUpdateSyncGeometry::Request::dump() const
{
    std::stringstream ss;
    ss << " uid: " << userId();
    ss << " branch: " << branchId;
    ss << " body: " << body;
    if (feedbackTaskId) {
        ss << " feedback-task-id: " << *feedbackTaskId;
    }
    return ss.str();
}

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

namespace {

class ObjectsUpdateSyncGeometryRequestData : public RequestData
{
public:
    ObjectsUpdateSyncGeometryRequestData(common::FormatType format, const std::string& body, ObjectsCache& cache)
        : RequestData(RequestData::VersionCheckPolicy::Optional)
        , syncWith_(0)
    {
        parse(format, body, cache);
    }

    TOid syncWith() const { return syncWith_; }

protected:
    void parseContextDataJson(const json::Value& contextObject, ObjectsCache&) override
    {
        WIKI_REQUIRE(
            contextObject.hasField(STR_SYNC_WITH),
            ERR_BAD_REQUEST,
            "sync-with argument is missing");
        syncWith_ = boost::lexical_cast<TOid>(contextObject[STR_SYNC_WITH].as<std::string>());
    }

    void parseContextDataXml(const maps::xml3::Node& contextNode, ObjectsCache&) override
    {
        syncWith_ = contextNode.node("wmp:sync_with").value<TOid>();
    }

private:
    TOid syncWith_;

};

std::string
ftTypeIdString(const ObjectPtr& object)
{
    for (const auto& attr : object->attributes()) {
        if (attr.id().ends_with(SUFFIX_FT_TYPE_ID)) {
            return attr.value();
        }
    }
    return {};
}

StringMap
createActionNotes(const ObjectsUpdateSyncGeometryRequestData& requestData)
{
    StringMap actionNotes;
    actionNotes[common::COMMIT_PROPKEY_GROUP_MOVED_OBJECTS_COUNT] =
        std::to_string(requestData.oids().size());
    return actionNotes;
}

} // namespace

void
ObjectsUpdateSyncGeometry::control()
{
    auto branchContext = BranchContextFacade::acquireWrite(request_.branchId, request_.userId());
    ObjectsCache cache(
        branchContext,
        boost::none);
    ObjectsUpdateSyncGeometryRequestData requestData(request_.format, request_.body, cache);

    GeoObjectCollection objects = cache.get(requestData.oids());
    cfg()->editor()->objectsUpdateRestrictions().checkObjects(ACTION_ID_SYNC_GEOMETRY, objects);
    const auto syncWith = cache.getExisting(requestData.syncWith());
    WIKI_REQUIRE(!syncWith->geom().isNull(),
        ERR_BAD_REQUEST,
        "Sync target object doesn't have geometry" << requestData.syncWith());
    auto ftTypeId = ftTypeIdString(syncWith);
    std::unordered_set<TOid> masterIds;
    auto checkMasters = [&](const ObjectPtr& object) {
        const auto masters = object->masterRelations();
        for (const auto& master : masters.range()) {
            WIKI_REQUIRE(!masterIds.count(master.id()),
                ERR_BAD_REQUEST,
                "Same master objects update is prohibited.");
            masterIds.insert(master.id());
        }
    };
    checkMasters(syncWith);
    for (const auto& oid : requestData.oids()) {
        WIKI_REQUIRE(
            oid != requestData.syncWith(),
            ERR_BAD_REQUEST,
            "Objets to move shouldn't contain sync target " << requestData.syncWith());
        auto object = objects.getById(oid);
        checkMasters(object);
        WIKI_REQUIRE(object, ERR_MISSING_OBJECT, "Not found object id: " << oid);
        WIKI_REQUIRE(!object->geom().isNull(), ERR_BAD_REQUEST, "Object " << oid << " has no geometry");
        WIKI_REQUIRE(
            object->categoryId() == syncWith->categoryId() &&
            ftTypeIdString(object) == ftTypeId,
            ERR_BAD_REQUEST,
            "Object " << oid << " is not of same type with sync tagret " << requestData.syncWith());
    }
    CheckPermissions(request_.userId(), branchContext.txnCore()).checkPermissionsForGroupSyncGeometry(
        syncWith->categoryId());

    for (const auto& oid : requestData.oids()) {
        ObjectPtr object = objects.getById(oid);
        object->setGeometry(syncWith->geom());
        object->primaryEdit(true);
        object->calcModified();
    }
    saveAndNotify(objects, createActionNotes(requestData), cache);
}

void
ObjectsUpdateSyncGeometry::saveAndNotify(
    GeoObjectCollection& /* objectContainer */,
    const StringMap& actionNotes,
    ObjectsCache& cache)
{
    cache.save(request_.userId(), common::COMMIT_PROPVAL_GROUP_MOVED, actionNotes);
    result_->taskName = taskName();
    CommitContext commitContext{request_.feedbackTaskId};
    result_->token = observers_.doCommit(cache, request_.userContext, commitContext);
    result_->commit = cache.savedCommit();
}

} // namespace wiki
} // namespace maps
