#include "reverter.h"
#include <maps/wikimap/mapspro/services/editor/src/actions/magic_strings.h>
#include <maps/wikimap/mapspro/services/editor/src/commit.h>
#include <maps/wikimap/mapspro/services/editor/src/exception.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/relations_manager.h>
#include <maps/wikimap/mapspro/services/editor/src/srv_attrs/registry.h>
#include <maps/wikimap/mapspro/services/editor/src/observers/observer.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/relation_object.h>
#include <maps/wikimap/mapspro/services/editor/src/acl_utils.h>
#include <maps/wikimap/mapspro/services/editor/src/magic_strings.h>
#include <maps/wikimap/mapspro/services/editor/src/edit_notes.h>
#include <maps/wikimap/mapspro/services/editor/src/validator.h>

#include <yandex/maps/wiki/common/revert.h>
#include <yandex/maps/wiki/common/string_utils.h>

#include <maps/wikimap/mapspro/libs/acl_utils/include/moderation.h>

#include <boost/lexical_cast.hpp>

namespace maps {
namespace wiki {

namespace {

const StringSet ALLOWED_REVERTING_COMMIT_NON_GROUP_ACTIONS = {
    common::COMMIT_PROPVAL_ACTION_SERVICE,
    common::COMMIT_PROPVAL_OBJECT_CREATED,
    common::COMMIT_PROPVAL_OBJECT_MODIFIED,
    common::COMMIT_PROPVAL_OBJECT_DELETED,
    common::COMMIT_PROPVAL_COMMIT_REVERTED
};

const StringSet ALLOWED_REVERTING_COMMIT_GROUP_ACTIONS = {
    common::COMMIT_PROPVAL_GROUP_MODIFIED_ATTRIBUTES,
    common::COMMIT_PROPVAL_GROUP_MOVED
};

void
checkCommitIds(const TCommitIds& commitIds)
{
    ASSERT(!commitIds.empty());
    for (auto commitId : commitIds) {
        ASSERT(commitId);
    }
}

} // namespace

CommitsReverter::CommitsReverter(
        const ObserverCollection& observers,
        BranchContext& branchCtx,
        UserContext& userContext,
        boost::optional<TId> feedbackTaskId)
    : observers_(observers)
    , branchCtx_(branchCtx)
    , userContext_(userContext)
    , feedbackTaskId_(std::move(feedbackTaskId))
{
    ASSERT(branchCtx_.branch.id() == revision::TRUNK_BRANCH_ID);

    user().checkActiveStatus();
}

EditorRevertData
CommitsReverter::revertCommits(
    const TCommitIds& commitIds,
    const boost::optional<RevertReason>& revertReason) const
{
    checkCommitIds(commitIds);

    auto filter = revision::filters::CommitAttr::id().in(commitIds);
    auto& work = branchCtx_.txnCore();
    revision::CommitManager commitManager(work);
    for (const auto& commit : revision::Commit::load(work, filter)) {
        WIKI_REQUIRE(
            commit.inTrunk(),
            ERR_NOT_FOUND,
            "commit " << commit.id() << " not found in trunk");

        checkRevertedCommit(commit, {commit.id()});
    }

    StringMap commitAttributes{
        {common::COMMIT_PROPKEY_ACTION, common::COMMIT_PROPVAL_COMMIT_REVERTED},
        {common::REVERT_COMMIT_IDS_KEY, common::join(commitIds, ',')}};

    if (revertReason) {
        commitAttributes[common::COMMIT_PROPKEY_REVERT_REASON] =
            boost::lexical_cast<std::string>(*revertReason);
    }

    auto revertData = commitManager.revertCommitsInTrunk(
        commitIds, uid(), commitAttributes);

    TOIds primaryOids;
    TOIds allPrimaryOids;
    TOIds allOidsWithNotes;
    auto revertedCommitFilter =
        revision::filters::CommitAttr::id().in(revertData.revertedCommitIds);
    for (const auto& commit : revision::Commit::load(work, revertedCommitFilter)) {
        checkRevertedCommit(commit, revertData.revertedCommitIds);

        auto primaryOid = primaryObjectId(commit);
        if (primaryOid) {
            if (commitIds.count(commit.id())) {
                primaryOids.insert(*primaryOid);
            }
            allPrimaryOids.insert(*primaryOid);
        }
        auto commitOidsWithNotes = oidsWithNotes(commit);
        allOidsWithNotes.insert(commitOidsWithNotes.begin(), commitOidsWithNotes.end());
    }

    commitAttributes = revertData.createdCommit.attributes();
    auto oldCommitAttributesSize = commitAttributes.size();

    TOid primaryObjectId = primaryOids.size() == 1
        ? *primaryOids.begin()
        : allPrimaryOids.size() == 1 ? *allPrimaryOids.begin() : 0;
    if (primaryObjectId) {
        commitAttributes.insert(
            {primaryObjectKey(primaryObjectId), common::COMMIT_ATTR_TRUE});
    }

    for (auto oid : allOidsWithNotes) {
        commitAttributes.insert({objectEditNotesKey(oid), edit_notes::REVERTED});
    }
    if (oldCommitAttributesSize != commitAttributes.size()) {
        revertData.createdCommit.setAttributes(work, commitAttributes);
    }

    return {
        revertData.createdCommit,
        revertData.revertedCommitIds,
        primaryObjectId ? TOIds{primaryObjectId} : allPrimaryOids
    };
}

Token
CommitsReverter::syncViewAndCommit(const EditorRevertData& revertData)
{
    auto commitId = revertData.createdCommit.id();

    ObjectsCache cache(branchCtx_, commitId);
    cache.setSavedCommit(revertData.createdCommit);

    TOIds objectIds;
    std::map<TId, revision::ObjectRevision> oldRevisions;

    const auto& reader = cache.revisionsFacade().gateway().reader();
    std::vector<revision::RevisionID> prevRevisionIds;
    for (auto& rev : reader.commitRevisions(commitId)) {
        REQUIRE(rev.prevId().valid(),
                "invalid prev revision for revert revision id: " << rev.id());
        prevRevisionIds.push_back(rev.prevId());

        objectIds.insert(rev.id().objectId());
        const auto& relationData = rev.data().relationData;
        if (relationData) {
            objectIds.insert(relationData->masterObjectId());
            objectIds.insert(relationData->slaveObjectId());
        }
    }
    for (auto& rev : reader.loadRevisions(prevRevisionIds)) {
        TId objectId = rev.id().objectId();
        oldRevisions.insert(std::make_pair(objectId, std::move(rev)));
    }
    objectIds.insert(revertData.primaryOids.begin(), revertData.primaryOids.end());

    WIKI_REQUIRE(
        objectIds.size() <= cfg()->editor()->system().revertObjectsLimit()
        || acl_utils::hasRole(user(), REVERT_ROLE),
        ERR_REVERT_TOO_COMPLEX,
        "Too complex revert, should have revert role. Objects: " << objectIds.size());

    for (const auto& obj : cache.get(revertData.primaryOids)) {
        obj->primaryEdit(true);
    }
    auto revertedCollection = cache.get(objectIds);

    GeoObjectCollection modifiedRelations;
    for (auto& objPtr : revertedCollection) {
        auto it = oldRevisions.find(objPtr->id());
        if (it != oldRevisions.end()) {
            ASSERT(objPtr->original());
            objPtr->original()->init(it->second);
            objPtr->calcModified();
        }

        if (is<RelationObject>(objPtr) && objPtr->isModified()) {
            modifiedRelations.add(objPtr);
        }
    }
    cache.relationsManager().updateRelations(modifiedRelations);

    GeoObjectCollection modifiedObjects;

    for (auto& objPtr : revertedCollection) {
        if (objPtr->isModified()) {
            objPtr->requestPropertiesUpdate();
            srv_attrs::affectedDependentObjects(objPtr.get(), cache);
            modifiedObjects.add(objPtr);
        }
    }

    try {
        Validator(uid(), cache, modifiedObjects).validate();
    } catch (const LogicException& ex) {
        THROW_WIKI_LOGIC_ERROR(ERR_REVERT_IMPOSSIBLE, "Revert would break consistency.");
    }
    CommitContext commitContext{feedbackTaskId_};
    return observers_.doCommit(cache, userContext_, commitContext);
}

void
CommitsReverter::checkRevertedCommit(
    const revision::Commit& commit,
    const TCommitIds& allRevertedCommitIds) const
{
    const auto& action = commit.action();
    if (ALLOWED_REVERTING_COMMIT_GROUP_ACTIONS.count(action)) {
        WIKI_REQUIRE(
            allRevertedCommitIds.size() == 1 &&
            *allRevertedCommitIds.begin() == commit.id(),
            ERR_REVERT_IMPOSSIBLE,
            "Can not revert group commit " << commit.id() <<
            ", there are other dependent commits");
    } else {
        WIKI_REQUIRE(
            ALLOWED_REVERTING_COMMIT_NON_GROUP_ACTIONS.count(action),
            ERR_REVERT_IMPOSSIBLE,
            "Can not revert commit " << commit.id() <<
            ", unallowed action: " << action);
    }
}

} // namespace wiki
} // namespace maps
