#include "objects_update_merge.h"

#include "maps/wikimap/mapspro/services/editor/src/exception.h"
#include "maps/wikimap/mapspro/services/editor/src/commit.h"
#include "maps/wikimap/mapspro/services/editor/src/branch_helpers.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/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 {
namespace wiki {

namespace {

const std::string TASK_METHOD_NAME = "ObjectsUpdateMerge";

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

protected:
    void parseContextDataJson(const json::Value&, ObjectsCache&) override {}
    void parseContextDataXml(const maps::xml3::Node&, ObjectsCache&) override {}
};

} // namespace

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

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

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

void ObjectsUpdateMerge::control()
{
    auto branchContext = BranchContextFacade::acquireWrite(request_.branchId, request_.userId());
    ObjectsCache cache(branchContext, boost::none);

    ObjectsUpdateMergeRequestData requestData(request_.format, request_.body, cache);
    WIKI_REQUIRE(requestData.oids().size() > 1, ERR_BAD_REQUEST, "nothing to merge");

    TOid primaryOid = requestData.oids()[0];
    auto primaryObj = cache.getExisting(primaryOid);
    WIKI_REQUIRE(
            !primaryObj->isDeleted(), ERR_MISSING_OBJECT, "primary object is deleted");

    const Category& category = primaryObj->category();
    WIKI_REQUIRE(
            category.complex(), ERR_ACTION_NOT_ALLOWED_FOR_CATEGORY,
            "category " << category.id() << " is not complex, cannot merge");
    primaryObj->primaryEdit(true);

    auto roleIdsToMerge =
            primaryObj->category().slaveRoleIds(roles::filters::IsNotTable);

    TOIds oidsToMerge(requestData.oids().begin() + 1, requestData.oids().end());
    WIKI_REQUIRE(
            !oidsToMerge.count(primaryOid), ERR_BAD_REQUEST,
            "request to merge object id: " << primaryOid << " into itself");

    auto objectsToMerge = cache.get(oidsToMerge);
    WIKI_REQUIRE(
            objectsToMerge.size() == oidsToMerge.size(),
            ERR_MISSING_OBJECT, "Some objects were not loaded");

    cache.relationsManager().loadRelations(RelationType::Master, oidsToMerge);
    cache.relationsManager().loadRelations(RelationType::Slave, oidsToMerge);

    RelationsProcessor relProcessor(cache);
    for (const auto& mergedObj : objectsToMerge) {
        WIKI_REQUIRE(
                !mergedObj->isDeleted(),
                ERR_MISSING_OBJECT, "object " << mergedObj->id() << " is deleted");
        WIKI_REQUIRE(
                mergedObj->category().id() == category.id(),
                ERR_ACTION_NOT_ALLOWED_FOR_CATEGORY,
                "trying to merge objects of different categories");

        mergedObj->setState(GeoObject::State::Deleted);
        relProcessor.replaceWithMerged(
                RelationType::Slave, mergedObj.get(), primaryObj.get(), roleIdsToMerge);
        relProcessor.deleteAllRelations(mergedObj.get());
    }

    saveAndNotify(cache);
}

void ObjectsUpdateMerge::saveAndNotify(ObjectsCache& cache)
{
    cache.save(request_.userId(), common::COMMIT_PROPVAL_GROUP_MERGED);
    result_->taskName = taskName();
    result_->token = observers_.doCommit(cache, request_.userContext);
    result_->commit = cache.savedCommit();
}


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

} // namespace wiki
} // namespace maps
