#include "objects_update_relation.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/relations_manager.h"
#include "maps/wikimap/mapspro/services/editor/src/relations_processor.h"
#include "maps/wikimap/mapspro/services/editor/src/revisions_facade.h"
#include "maps/wikimap/mapspro/services/editor/src/serialize/json_parser.h"
#include "maps/wikimap/mapspro/services/editor/src/serialize/xml_parser.h"
#include "maps/wikimap/mapspro/services/editor/src/serialize/request_data.h"

#include <yandex/maps/wiki/configs/editor/categories.h>

namespace maps::wiki {

namespace
{

const std::string TASK_METHOD_NAME = "ObjectsUpdateRelation";
const std::string NODE_ID_ATTR = "id";
const std::string ACTION_ID_UPDATE_RELATION = "update-relation";

const StringSet MUTUAL_EXCLUSIVE_ROLES =
{
    "associated_with",
    "addr_associated_with"
};

using SlaveOidsByRoleId = std::map<std::string, std::set<TOid>>;

class UpdateRelationRequestData : public RequestData
{
public:
    UpdateRelationRequestData(
        common::FormatType format,
        const std::string& body,
        ObjectsCache& cache)
    {
        parse(format, body, cache);
    }

    const SlaveOidsByRoleId& slavesToSet() const
    { return slavesToSet_; }

    const SlaveOidsByRoleId& slavesToRemove() const
    { return slavesToRemove_; }

protected:
    void parseContextDataJson(const json::Value& contextObject, ObjectsCache& cache) override
    {
        ASSERT(contextObject.hasField(STR_SLAVES));
        auto slavesObject = contextObject[STR_SLAVES];

        RevisionIds revisionIds;
        for (const auto& roleId : slavesObject.fields()) {
            auto roleObject = slavesObject[roleId];
            if (roleObject.hasField(STR_SET_FOR)) {
                ASSERT(roleObject[STR_SET_FOR].isArray());
                for (const auto& objValue : roleObject[STR_SET_FOR]) {
                    slavesToSet_[roleId].insert(
                        boost::lexical_cast<TOid>(objValue[STR_ID].toString()));
                    revisionIds.insert(JsonParser::readObjectRevision(objValue));
                }
            }
            if (roleObject.hasField(STR_REMOVE_FROM)) {
                ASSERT(roleObject[STR_REMOVE_FROM].isArray());
                for (const auto& objValue : roleObject[STR_REMOVE_FROM]) {
                    slavesToRemove_[roleId].insert(
                        boost::lexical_cast<TOid>(objValue[STR_ID].toString()));
                    revisionIds.insert(JsonParser::readObjectRevision(objValue));
                }
            }
        }

        cache.revisionsFacade().gateway().checkConflicts(revisionIds);

        for (const auto& [roleId, setSlaveOids] : slavesToSet_) {
            if (slavesToRemove_.count(roleId)) {
                for (const auto oid : setSlaveOids) {
                    WIKI_REQUIRE(
                        !slavesToRemove_[roleId].count(oid),
                        ERR_BAD_REQUEST,
                        "Intersection between slave objects to set and remove: " << oid);
                }
            }
        }
    }

    void parseContextDataXml(const maps::xml3::Node&, ObjectsCache&) override
    {
        UNSUPPORTED_FORMAT(common::FormatType::XML, TASK_METHOD_NAME);
    }

private:
    SlaveOidsByRoleId slavesToSet_;
    SlaveOidsByRoleId slavesToRemove_;
};

void checkMaster(TOid newMasterId, const std::string& roleId, ObjectsCache& cache)
{
    ObjectPtr newMaster = cache.getExisting(newMasterId);
    const auto& category = newMaster->category();
    const auto& slavesRoles = category.slavesRoles();
    if (slavesRoles.findByKey(roleId) == slavesRoles.end()) {
        THROW_WIKI_LOGIC_ERROR(ERR_BAD_REQUEST,
            "Category " << category.id() <<
            " has no role " << roleId << " for slaves");
    }
}

} // namespace

ObjectsUpdateRelation::ObjectsUpdateRelation(
        const ObserverCollection& observers,
        const Request& request,
        taskutils::TaskID asyncTaskID)
    : controller::BaseController<ObjectsUpdateRelation>(BOOST_CURRENT_FUNCTION, asyncTaskID)
    , request_(request)
    , observers_(observers)
{
    result_->taskName = ObjectsUpdateRelation::taskName();
}

std::string
ObjectsUpdateRelation::Request::dump() const
{
    std::stringstream ss;
    ss << " uid: " << userId();
    ss << " branch: " << branchId;
    ss << " body: " << body;
    return ss.str();
}

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

void
ObjectsUpdateRelation::control()
{
    auto branchContext = BranchContextFacade::acquireWrite(request_.branchId, request_.userId());
    ObjectsCache cache(
        branchContext,
        boost::none);
    CheckPermissions(request_.userId(), branchContext.txnCore())
        .checkPermissionsForGroupUpdateRelation();
    UpdateRelationRequestData requestData(request_.format, request_.body, cache);
    RelationsManager& relationsManager = cache.relationsManager();
    RelationsProcessor relationsProcessor(cache);

    std::set<TOid> slaveOids;
    for (const auto& [_, setSlaveOids] : requestData.slavesToSet()) {
        for (const auto& slaveOid : setSlaveOids) {
            slaveOids.insert(slaveOid);
        }
    }
    for (const auto& [_, removeSlaveOids] : requestData.slavesToRemove()) {
        for (const auto& slaveOid : removeSlaveOids) {
            slaveOids.insert(slaveOid);
        }
    }
    GeoObjectCollection objects = cache.get(slaveOids);
    cfg()->editor()->objectsUpdateRestrictions().checkObjects(ACTION_ID_UPDATE_RELATION, objects);

    REQUIRE(requestData.oids().size() == 1, "Operation not allowed for multiple master objects");
    TOid masterObjectId = requestData.oids().front();

    auto cleanRelationsToMaster = [&](TOid slaveOid, const std::string& roleId) {
        ObjectPtr object = objects.getById(slaveOid);
        if (!object){
            THROW_WIKI_LOGIC_ERROR(ERR_MISSING_OBJECT, " Not found object id:" << slaveOid);
        }
        const StringSet dropRoleIds =
            MUTUAL_EXCLUSIVE_ROLES.count(roleId)
            ? MUTUAL_EXCLUSIVE_ROLES
            : StringSet({ roleId });
        relationsProcessor.deleteRelations(
            RelationType::Master,
            object.get(),
            dropRoleIds);
    };

    for (const auto& [roleId, setSlaveOids]: requestData.slavesToSet()) {
        checkMaster(masterObjectId, roleId, cache);
        for (const auto& slaveOid : setSlaveOids) {
            cleanRelationsToMaster(slaveOid, roleId);
            relationsManager.createRelation(masterObjectId, slaveOid, roleId);
        }
    }
    for (const auto& [roleId, removeSlaveOids]: requestData.slavesToRemove()) {
        checkMaster(masterObjectId, roleId, cache);
        for (const auto& slaveOid : removeSlaveOids) {
            relationsManager.deleteRelation(masterObjectId, slaveOid, roleId);
        }
    }

    {
        ObjectPtr masterObject = cache.getExisting(masterObjectId);
        masterObject->calcModified();
        masterObject->primaryEdit(true);
    }
    for (const auto& slaveOid : slaveOids) {
        ObjectPtr object = objects.getById(slaveOid);
        object->calcModified();
    }

    saveAndNotify(objects, cache);
}

void
ObjectsUpdateRelation::saveAndNotify(GeoObjectCollection& /*objectContainer*/, ObjectsCache& cache)
{
    cache.save(request_.userId(), common::COMMIT_PROPVAL_GROUP_MODIFIED_RELATION);
    result_->token = observers_.doCommit(cache, request_.userContext);
    result_->commit = cache.savedCommit();
}

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

} // namespace maps::wiki
