#include "clone_object.h"

#include "routing/helper.h"

#include "maps/wikimap/mapspro/services/editor/src/common.h"
#include "maps/wikimap/mapspro/services/editor/src/branch_helpers.h"
#include "maps/wikimap/mapspro/services/editor/src/commit.h"
#include "maps/wikimap/mapspro/services/editor/src/configs/config.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/object.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/category_traits.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/revisions_facade.h"
#include "maps/wikimap/mapspro/services/editor/src/factory.h"
#include "maps/wikimap/mapspro/services/editor/src/check_permissions.h"

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

namespace maps {
namespace wiki {

namespace {

static const std::string TASK_METHOD_NAME = "CloneObject";

ObjectPtr
cloneObjectAndUpdateTableAttributesRelations(const ObjectPtr& object, ObjectsCache& cache)
{
    auto clone = GeoObjectFactory(cache).cloneObject(object);
    writeTableAttributesToSlaveInfos(cache, clone->id());
    return clone;
}

} // namespace

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

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

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

namespace {
bool isRelationsDataUpdateOnly(const ObjectUpdateData& data)
{
    return
        !data.revision().valid() &&
        !data.isGeometryDefined() &&
        data.attributes().empty() &&
        data.tableAttributes().empty() &&
        data.richContent() && data.richContent()->empty();
}

void
checkInput(const ObjectsUpdateDataCollection& dataCollection)
{
    WIKI_REQUIRE(
        dataCollection.empty() ||
        isRelationsDataUpdateOnly(*dataCollection.begin()),
        ERR_BAD_DATA,
        "Expected single object update data with relations only");
    if (dataCollection.size() <= 1) {
        return;
    }
    std::for_each(++dataCollection.begin(), dataCollection.end(),
        [](const ObjectUpdateData& data) {
            WIKI_REQUIRE(data.isEmpty(),
                ERR_BAD_DATA,
                "Related objects update not supported");
        });
}
}//namespace

void
CloneObject::control()
{
    checkInput(request_.objectsUpdateData);
    auto branchCtx = BranchContextFacade::acquireWrite(request_.branchId, request_.userId());
    ASSERT(result_);
    std::make_shared<ObjectsCache>(branchCtx, boost::none).swap(result_->cache);
    Context context(*result_->cache, request_.userId());
    doWork(context);
    context.cache().saveWithContextSkipPermissionsCheck(
        context.editContexts(),
        request_.userId(),
        common::COMMIT_PROPVAL_OBJECT_CREATED);
    CommitContext commitContext{request_.feedbackTaskId};
    result_->token = observers_.doCommit(*result_->cache, request_.userContext, commitContext);
}

void
CloneObject::checkObjectRevisions(Context& context)
{
    std::set<TRevisionId> revisionIds;
    for (const auto& objectUpdateData : request_.objectsUpdateData) {
        const auto& revisionId = objectUpdateData.revision();
        if (revisionId.valid()) {
            revisionIds.insert(revisionId);
        }
    }
    context.cache().revisionsFacade().gateway().checkConflicts(revisionIds);
}

namespace {

TOIds
findAddedThreadRoutes(const ObjectsUpdateDataCollection& objectsUpdateData)
{
    if (objectsUpdateData.empty()) {
        return {};
    }
    const auto& diff = objectsUpdateData.begin()->mastersDiff();
    TOIds routes;
    for (const auto& roleDiff : diff) {
        const auto& roleId = roleDiff.first;
        if (roleId != ROLE_ASSIGNED_THREAD) {
            continue;
        }
        const auto& diffIds = roleDiff.second;
        for (auto addedId : diffIds.added()) {
            const auto relativeId = addedId.objectId();
            if (relativeId) {
                routes.insert(relativeId);
            }
        }
    }
    return routes;
}

ObjectPtr
cloneTransportThread(
    const ObjectPtr& originalThread,
    ObjectsCache& cache,
    const ObjectsUpdateDataCollection& objectsUpdateData)
{
    auto newThread = cloneObjectAndUpdateTableAttributesRelations(originalThread, cache);
    StringSet geomPartsRoles = originalThread->category().slaveRoleIds(roles::filters::IsGeom);
    for (const auto& geomPart : originalThread->slaveRelations().range(geomPartsRoles)) {
        cache.relationsManager().createRelation(
            newThread->id(), geomPart.id(),  geomPart.roleId());
    }
    auto threadStopIds = orderedThreadStopIds(originalThread);
    if (threadStopIds.empty()) {
        return newThread;
    }
    TId prevThreadStopCloneId = 0;
    const auto addedRoutes = findAddedThreadRoutes(objectsUpdateData);
    for (auto threadStopId : threadStopIds) {
        auto threadStop = cache.getExisting(threadStopId);
        auto threadStopClone = cloneObjectAndUpdateTableAttributesRelations(threadStop, cache);
        cache.relationsManager().createRelation(
            newThread->id(), threadStopClone->id(), ROLE_PART);
        auto stopRelations = threadStop->masterRelations().range(ROLE_ASSIGNED_THREAD_STOP);
        ASSERT(stopRelations.size() == 1);
        const auto stopId = stopRelations.begin()->id();
        cache.relationsManager().createRelation(
            stopId, threadStopClone->id(), ROLE_ASSIGNED_THREAD_STOP);
        for (const auto addedRouteId : addedRoutes) {
            cache.relationsManager().createRelation(
                addedRouteId, stopId, ROLE_ASSIGNED);
        }
        if (prevThreadStopCloneId) {
            cache.relationsManager().createRelation(
                threadStopClone->id(), prevThreadStopCloneId, ROLE_PREVIOUS);
        }
        prevThreadStopCloneId = threadStopClone->id();
    }
    return newThread;
}

void
saveObjectRelations(
    TOid oid,
    const RelativesDiffByRoleMap& diff,
    RelationType relationType,
    RelationsManager& relationsManager)
{
    for (const auto& roleDiff : diff) {
        const auto& roleId = roleDiff.first;
        const RelativesDiff& diffIds = roleDiff.second;
        for (auto addedId : diffIds.added()) {
            const auto& relativeId = addedId.objectId();
            if (!relativeId) {
                continue;
            }
            relationType == RelationType::Master
                ? relationsManager.createRelation(relativeId, oid, roleId)
                : relationsManager.createRelation(oid, relativeId, roleId);
        }
    }
}

}//namespace

void
CloneObject::doWork(Context& context)
{
    checkObjectRevisions(context);
    auto originalObject = context.cache().getExisting(request_.objectId);
    WIKI_REQUIRE(!originalObject->isDeleted(),
        ERR_MISSING_OBJECT,
        "Cloning deleted objects is not allowed operation." << request_.objectId);
    const auto& categoryId = originalObject->categoryId();
    WIKI_REQUIRE(
        request_.objectsUpdateData.empty() ||
        categoryId == request_.objectsUpdateData.begin()->categoryId(),
        ERR_CATEGORY_MISMATCH,
        "Original object category [" <<  categoryId
            << "] doesn't match update category ["
            << request_.objectsUpdateData.begin()->categoryId() << "]");
    CheckPermissions(request_.userId(), context.cache().workCore())
        .checkPermissionsForCloneOperation(originalObject.get());
    if (isTransportThread(categoryId)) {
        result_->clone = cloneTransportThread(
            originalObject, context.cache(), request_.objectsUpdateData);
    } else {
        result_->clone =
            cloneObjectAndUpdateTableAttributesRelations(
                originalObject, context.cache());
    }
    result_->clone->primaryEdit(true);
    if (!request_.objectsUpdateData.empty()) {
        const auto& objectUpdateData = *request_.objectsUpdateData.begin();
        RelationsManager& relationsManager = context.cache().relationsManager();
        saveObjectRelations(result_->clone->id(), objectUpdateData.mastersDiff(),
            RelationType::Master, relationsManager);
        saveObjectRelations(result_->clone->id(), objectUpdateData.slavesDiff(),
            RelationType::Slave, relationsManager);
    }
}

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

} // namespace wiki
} // namespace maps
