#include "get_commit_diff.h"

#include "maps/wikimap/mapspro/services/editor/src/objects/object.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/junction.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/configs/config.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/category_traits.h"
#include "maps/wikimap/mapspro/services/editor/src/views/objects_query.h"
#include "maps/wikimap/mapspro/services/editor/src/acl_utils.h"

#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/revision/commit.h>
#include <maps/wikimap/mapspro/libs/acl/include/aclgateway.h>

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

namespace maps {
namespace wiki {

namespace {

const CachePolicy CACHE_POLICY
{
    TableAttributesLoadPolicy::Load,
    ServiceAttributesLoadPolicy::Skip, // default: Load
    DanglingRelationsPolicy::Ignore // default: Check
};

} // namespace

GetCommitDiff::GetCommitDiff(const Request& request)
    : controller::BaseController<GetCommitDiff>(BOOST_CURRENT_FUNCTION)
    , request_(request)
{
    CHECK_REQUEST_PARAM(request.commitId);
}

GetCommitDiff::~GetCommitDiff()
{}

std::string
GetCommitDiff::printRequest() const
{
    std::stringstream ss;

    ss << " commit-id:" << request_.commitId;
    ss << " object:" << request_.objectId;
    ss << " user: " << request_.user;
    ss << " token: " << request_.dbToken;
    ss << " branch: " << request_.branchId;
    return ss.str();
}

namespace {
struct UserStatus
{
    bool isModerator;
    bool isCartographer;
};

UserStatus
getUserStatus(pqxx::transaction_base& work, const acl::User& user)
{
    acl::ACLGateway gateway(work);
    auto status = acl_utils::moderationStatus(
        gateway,
        user);
    return UserStatus {
        acl_utils::isModerator(status),
        acl_utils::isCartographer(status)
    };
}

bool isCommitRevertible(
    const BranchContext& branchCtx,
    TCommitId commitId,
    TOid objectId,
    const acl::User& user)
{
    const auto userStatus = getUserStatus(branchCtx.txnCore(), user);
    if (userStatus.isCartographer) {
        return true;
    }
    std::set<revision::DBID> commitIds;
    const auto userId = user.uid();
    revision::RevisionsGateway gateway(branchCtx.txnCore(), branchCtx.branch);
    auto snapshot = gateway.historicalSnapshot(gateway.headCommitId());
    auto revsFilterSelf =
        (
            revision::filters::ObjRevAttr::objectId() == objectId &&
            revision::filters::ObjRevAttr::isNotRelation() &&
            revision::filters::ObjRevAttr::commitId() >= commitId
        );
    auto revisionIdsSelf = snapshot.revisionIdsByFilter(revsFilterSelf);
    for (const auto& revId : revisionIdsSelf) {
        commitIds.insert(revId.commitId());
    }
    auto revsFilterRel =
        (
            revision::filters::ObjRevAttr::commitId() >= commitId &&
            revision::filters::ObjRevAttr::isRelation() &&
            (revision::filters::ObjRevAttr::slaveObjectId() == objectId ||
             revision::filters::ObjRevAttr::masterObjectId() == objectId)
         );
    auto revisionIdsRel = snapshot.revisionIdsByFilter(revsFilterRel);
    for (const auto& revId : revisionIdsRel) {
        commitIds.insert(revId.commitId());
    }
    StringVec commitAttrs {
            objectEditNotesKey(objectId),
            primaryObjectKey(objectId)
        };
    auto commitsFilter =
        commitIds.empty()
        ? (
            revision::filters::CommitAttribute::definedAny(commitAttrs) &&
            revision::filters::CommitAttr::isVisible(branchCtx.branch) &&
            revision::filters::CommitAttr::id() >= commitId
          )
        : (
            revision::filters::CommitAttribute::definedAny(commitAttrs) &&
            revision::filters::CommitAttr::isVisible(branchCtx.branch) &&
            revision::filters::CommitAttr::id() >= commitId
          ) || revision::filters::CommitAttr::id().in(commitIds);
    auto commits = revision::Commit::load(branchCtx.txnCore(), commitsFilter);
    std::map<TCommitId, revision::Commit> idToCommit;
    for (const auto& commit : commits) {
        idToCommit.emplace(commit.id(), commit);
    }
    if (idToCommit.empty() || idToCommit.begin()->first != commitId) {
        return false;
    }
    if (idToCommit.size() == 1) {
        const auto& commit = idToCommit.begin()->second;
        if (commit.approveOrder() == 0 && commit.createdBy() == userId) {
            return true;
        }
    }
    for (const auto& commitPair : idToCommit) {
        const auto& commit = commitPair.second;
        if (commit.approveOrder()) {
            return false;
        }
        if (!userStatus.isModerator && commit.createdBy() != userId) {
            return false;
        }
    }
    return true;
}

void
prepareRelativesViewData(DiffDetails& details, ObjectsCache& curCache, TBranchId branchId)
{
    TOIds addedRelativesOids;
    TOIds removedRelativesOids;
    for (const auto& [_, relativesIdsDiff] : details.slavesDiff) {
        addedRelativesOids.insert(
                relativesIdsDiff.added.begin(), relativesIdsDiff.added.end());
        removedRelativesOids.insert(
                relativesIdsDiff.deleted.begin(), relativesIdsDiff.deleted.end());
    }
    for (const auto& [_, relativesIdsDiff] : details.mastersDiff) {
        addedRelativesOids.insert(
                relativesIdsDiff.added.begin(), relativesIdsDiff.added.end());
        removedRelativesOids.insert(
                relativesIdsDiff.deleted.begin(), relativesIdsDiff.deleted.end());
    }

    auto allRelativesOids = addedRelativesOids;
    allRelativesOids.insert(removedRelativesOids.begin(), removedRelativesOids.end());
    if (!allRelativesOids.empty()) {
        views::ObjectsQuery objectsQuery;
        objectsQuery.addCondition(views::OidsCondition(allRelativesOids));
        auto objectsInView = objectsQuery.exec(
            curCache.workView(),
            branchId);
        for (const auto& obj : objectsInView) {
            if (addedRelativesOids.count(obj.id())) {
                details.addedRelatives.insert({obj.id(), obj});
            }
            if (removedRelativesOids.count(obj.id())) {
                details.removedRelatives.insert({obj.id(), obj});
            }
            allRelativesOids.erase(obj.id());
        }
        auto notInViewObjects = curCache.get(allRelativesOids);
        for (const auto& obj : notInViewObjects) {
            if (addedRelativesOids.count(obj->id())) {
                details.addedRelatives.insert(
                     {obj->id(), views::ViewObject(obj)});
            }
            if (removedRelativesOids.count(obj->id())) {
                details.removedRelatives.insert(
                     {obj->id(), views::ViewObject(obj)});
            }
        }
    }
}

} // namespace

void
GetCommitDiff::control()
{
    auto branchCtx = BranchContextFacade::acquireRead(
        request_.branchId, request_.dbToken);
    ObjectsCache curCache(branchCtx, request_.commitId, CACHE_POLICY);
    RevisionsFacade& tdsGw = curCache.revisionsFacade();
    result_->commitModel = make_unique<CommitModel>(tdsGw.loadHeadCommit());
    const auto& commitModel = result_->commitModel;
    commitModel->setupState(branchCtx);
    commitModel->setupFeedbackTaskId(branchCtx);
    if (request_.user && request_.objectId &&
        request_.branchId == revision::TRUNK_BRANCH_ID)
    {
        auto user = getUser(branchCtx, request_.user);
        commitModel->setIsRevertible(
            isCommitRevertible(
                branchCtx, request_.commitId,
                request_.objectId, user));
    } else {
        commitModel->setIsRevertible(false);
    }

    const auto& action = commitModel->commit().action();
    if ((!common::COMMIT_NON_GROUP_ACTIONS.count(action) &&
        !common::COMMIT_GROUP_ACTIONS.count(action))) {
        return;
    }
    if (action == common::COMMIT_PROPVAL_ACTION_IMPORT) {
        if (!request_.objectId) {
            return;
        }
        result_->details = DiffDetails();
        auto& details = *result_->details;
        details.toObjectVersion = curCache.getExisting(request_.objectId);
        details.envelope = details.toObjectVersion->envelope();
        details.geomDiff.tryAddAfter(details.toObjectVersion->geom());
        return;
    }
    result_->details = DiffDetails();
    auto& details = *result_->details;

    std::unique_ptr<ObjectsCache> prevCache;
    if (request_.commitId > 1) {
        prevCache.reset(new ObjectsCache(
            branchCtx, request_.commitId - 1, CACHE_POLICY));
    }

    auto diffScope = commitDiffScope(commitModel->commit(), request_.objectId);

    auto commitDiff = revision::commitDiff(
            branchCtx.txnCore(), request_.commitId,
            diffScope == DiffScope::Commit ? 0 : request_.objectId);

    if (diffScope == DiffScope::GroupUpdateAttributes) {
        for (const auto& objDiff : commitDiff) {
            const auto& attributes = objDiff.second.data().attributes;
            if (attributes) {
                for (const auto& attrDiff : *attributes) {
                    details.attributesDiff.emplace(attrDiff.first,
                        revision::StringDiff("", attrDiff.second.after));
                }
            }
        }
        result_->categoryId = curCache.getExisting(commitDiff.begin()->first)->categoryId();
    }

    if (diffScope == DiffScope::GroupUpdateRelations) {
        tdsGw.headCommitRelativesDiff(
            RelationType::Master).swap(details.mastersDiff);
        prepareRelativesViewData(details, curCache, request_.branchId);
        const auto& firstRelationDiff = commitDiff.begin()->second.data();
        TOid slaveOid =
            firstRelationDiff.newRelationData
            ? firstRelationDiff.newRelationData->slaveObjectId()
            : firstRelationDiff.oldRelationData->slaveObjectId();
        result_->categoryId = curCache.getExisting(slaveOid)->categoryId();
    }

    if (prevCache) {
        details.geomDiff = objectGeomDiff(
            prevCache,
            curCache,
            commitDiff,
            request_.objectId,
            affectedObjectIds(commitModel->commit()),
            diffScope);

        const auto& primaryObjectId = commitModel->contextPrimaryObjectId();
        if (primaryObjectId && isLinearElement(curCache.getExisting(*primaryObjectId)->categoryId())) {
            details.envelope = curCache.getExisting(*primaryObjectId)->envelope();
        } else {
            details.envelope = changesEnvelope(details.geomDiff);
        }
    }

    if (!request_.objectId) {
        return;
    }
    commitModel->setCustomContextObjectId(request_.objectId);

    if (prevCache) {
        auto fromObjectVersion = prevCache->get(request_.objectId);
        if (fromObjectVersion) {
            details.fromObjectVersion = *fromObjectVersion;
        }
    }
    details.toObjectVersion = curCache.getExisting(request_.objectId);

    if (details.toObjectVersion->state() != GeoObject::State::Deleted) {
        auto objDiffIt = commitDiff.find(request_.objectId);
        if (objDiffIt != commitDiff.end() && objDiffIt->second.data().attributes) {
            details.attributesDiff = std::move(*objDiffIt->second.data().attributes);
        }
    }

    fillCategoryDiff(details);

    if (details.envelope.isNull()) {
        details.envelope = changesEnvelope(
            details.fromObjectVersion.get(),
            details.toObjectVersion.get());
    }

    calcModified(tdsGw, curCache, request_.branchId);
    if (prevCache && details.toObjectVersion &&
        isTransportThread(details.toObjectVersion->categoryId())) {
        details.threadStopDiff = threadsDiff(details.toObjectVersion, details.fromObjectVersion, &curCache, prevCache.get());
    }

    if (details.anyObject()) {
        result_->categoryId = details.anyObject().get()->categoryId();
    }
}

void
GetCommitDiff::calcModified(
    RevisionsFacade& revisionsFacade,
    ObjectsCache& curCache,
    TBranchId branchId)
{
    auto& details = *result_->details;
    if (!details.toObjectVersion) {
        return;
    }

    if (details.toObjectVersion->category().showSlavesDiff()) {
        revisionsFacade.headCommitRelativesDiff(
            RelationType::Slave, details.toObjectVersion->id(),
            details.toObjectVersion->category().slaveRoleIds(roles::filters::IsNotTable)).swap(details.slavesDiff);

    }
    revisionsFacade.headCommitRelativesDiff(
        RelationType::Master, details.toObjectVersion->id(),
        details.toObjectVersion->category().masterRoleIds(roles::filters::All)).swap(details.mastersDiff);
    prepareRelativesViewData(details, curCache, branchId);
}

} // namespace wiki
} // namespace maps
