#include "tasks_resolve.h"

#include "maps/wikimap/mapspro/services/editor/src/approved_commits/approver.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/moderation.h"
#include "maps/wikimap/mapspro/services/editor/src/moderation_log.h"
#include "maps/wikimap/mapspro/services/editor/src/objects_cache.h"
#include "maps/wikimap/mapspro/services/editor/src/observers/observer.h"
#include "maps/wikimap/mapspro/services/editor/src/social_utils.h"
#include "maps/wikimap/mapspro/services/editor/src/actions/commits/reverter.h"
#include "maps/wikimap/mapspro/services/editor/src/actions/objects_deleter.h"
#include "helpers.h"

#include "maps/wikimap/mapspro/services/editor/src/check_permissions.h"
#include <maps/wikimap/mapspro/libs/acl_utils/include/moderation.h>

#include <maps/libs/common/include/profiletimer.h>
#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/configs/editor/categories.h>

#include <utility>

namespace maps::wiki {

namespace {

const std::string TASK_METHOD_NAME = "SocialModerationTasksResolve";

social::Tasks
loadNonResolvedTasks(
    const social::Gateway& socialGateway,
    const social::TIds& taskIds,
    TUid uid,
    moderation::ResolveLockedByOther resolveLockedByOther)
{
    auto tasks = moderation::loadTasks(socialGateway, taskIds, uid, resolveLockedByOther);
    for (const auto& task : tasks) {
        WIKI_REQUIRE(
            !task.isResolved(), ERR_MODERATION_CONFLICT,
            "Moderation task already resolved: " << task.id());
    }
    return tasks;
}

void
checkTasksResolvedByCurrentUser(
    social::ResolveResolution resolution, TUid uid, const social::Tasks& tasks)
{
    for (const auto& task : tasks) {
        WIKI_REQUIRE(
            task.resolved().uid() == uid && task.resolved().resolution() == resolution,
            ERR_MODERATION_CONFLICT,
            "Moderation task " << task.id() << " "
            "already resolved with resolution " << task.resolved().resolution() << " "
            "by " << task.resolved().uid()
        );
    }
}

revision::DBIDSet
getDepCommitsForResolveResolutionCalc(
    pqxx::transaction_base& txn,
    const revision::DBIDSet& commitIds)
{
    const auto limits = cfg()->editor()->system().resolveResolutionCalcLimits();
    const auto timeoutSec = static_cast<double>(limits.timeoutMs.count()) / 1000;
    const auto commitsLimit = limits.commitsLimit;

    ProfileTimer timer;
    return revision::CommitManager(txn).findDependentCommitsInTrunk(
        commitIds,
        [&timer, timeoutSec, commitsLimit](const auto& commitIds) {
            return
                (commitIds.size() > commitsLimit || timer.getElapsedTimeNumber() > timeoutSec)
                ? revision::SearchPredicateResult::Stop
                : revision::SearchPredicateResult::Continue;
        }
    );
}

social::TIds
resolveCommitsWithAcceptOrEdit(
    const social::Gateway& socialGw,
    const social::TIds& commitsToResolve,
    social::TUid resolveBy,
    std::optional<social::TId> mostRecentUserCommitId)
{
    using namespace social;

    const auto firstCommitToAcceptIt =
        mostRecentUserCommitId
        ? commitsToResolve.upper_bound(*mostRecentUserCommitId)
        : commitsToResolve.cbegin();

    auto console = socialGw.superModerationConsole(resolveBy);
    auto result =
        console.resolveEditTasksByCommitIds(
            ResolveResolution::Edit,
            {commitsToResolve.cbegin(), firstCommitToAcceptIt}
        );
    result.merge(
        console.resolveEditTasksByCommitIds(
            ResolveResolution::Accept,
            {firstCommitToAcceptIt, commitsToResolve.cend()}
        )
    );
    return result;
}

social::TIds
resolveEditTasks(
    pqxx::transaction_base& coreTxn,
    const social::Gateway& socialGw,
    const social::TIds& commitsToResolve,
    const social::TIds& contributingCommits,
    const social::TUid& moderator)
{
    auto allCommits = commitsToResolve;
    allCommits.insert(contributingCommits.cbegin(), contributingCommits.cend());

    const auto dependentCommitIds = getDepCommitsForResolveResolutionCalc(coreTxn, allCommits);
    const auto recentModCommitId = socialGw.getRecentEditCommitMadeBy(moderator, dependentCommitIds);

    auto result =
        resolveCommitsWithAcceptOrEdit(socialGw, commitsToResolve, moderator, recentModCommitId);
    result.merge(
        resolveCommitsWithAcceptOrEdit(socialGw, contributingCommits, common::ROBOT_UID, recentModCommitId)
    );
    return result;
}

} // namespace

SocialModerationTasksResolve::SocialModerationTasksResolve(
        const ObserverCollection& observers,
        const Request& request,
        taskutils::TaskID asyncTaskID)
    : SocialModerationCommentTasksProcessor<SocialModerationTasksResolve, social::ResolveResolution>(observers, request, asyncTaskID)
{}

void
SocialModerationTasksResolve::control()
{
    auto resolvedTaskIds = resolveTasks();
    for (auto taskId : result_->taskIds) {
        if (resolvedTaskIds.count(taskId)) {
            logModerationEvent(taskId, ModerationLogger::Action::Resolved);
        } else {
            logModerationEvent(taskId, ModerationLogger::Action::Closed);
        }
    }
}

social::TaskIds
SocialModerationTasksResolve::resolveTasks()
{
    social::TaskIds resolvedTaskIds;
    auto branchCtx = BranchContextFacade::acquireWrite(
        revision::TRUNK_BRANCH_ID, request_.userId());

    const auto& user = request_.userContext.aclUser(branchCtx);
    user.checkActiveStatus();

    const auto& moderationStatus = request_.userContext.moderationStatus(branchCtx);
    const bool isUserCartographer = acl_utils::isCartographer(moderationStatus);

    social::Gateway socialGateway(branchCtx.txnSocial());
    auto tasks = loadNonResolvedTasks(
        socialGateway,
        request_.taskIds,
        request_.userId(),
        moderation::canResolveLockedByOther(isUserCartographer)
    );
    moderation::checkTasks(tasks);

    if (moderation::hasComments(tasks)) {
        processComments(branchCtx, tasks);
    } else {
        if (request_.resolution == social::ResolveResolution::Revert) {
            processEditsRevert(branchCtx, socialGateway, tasks, isUserCartographer);
        } else {
            processEditsResolve(branchCtx, socialGateway, user, tasks, isUserCartographer)
                .swap(resolvedTaskIds);
        }
    }

    if (result_->token.empty()) {
        result_->token = branchCtx.commit();
    }
    return resolvedTaskIds;
}

social::TaskIds
SocialModerationTasksResolve::processEditsResolve(
    BranchContext& branchCtx,
    const social::Gateway& socialGateway,
    const acl::User& user,
    const social::Tasks& tasks,
    bool isUserCartographer)
{
    auto commitIdsToResolve = moderation::commitIdsFromEditTasks(tasks);
    auto contributingCommitIds = revision::CommitManager(branchCtx.txnCore())
        .findDraftContributingCommits(commitIdsToResolve, {});

    result_->taskIds = resolveEditTasks(
        branchCtx.txnCore(), socialGateway,
        commitIdsToResolve, contributingCommitIds, user.uid());

    TCommitIds commitIdsToApprove;
    if (isUserCartographer) {
        commitIdsToApprove = std::move(commitIdsToResolve);
        commitIdsToApprove.merge(contributingCommitIds);
    } else if (!request_.aoiId) {
        CheckPermissions(
            user.uid(),
            branchCtx.txnCore()).checkPermissionsToBlockingTasks();

        commitIdsToApprove = moderation::getAutoApproveCommitIds(
            branchCtx,
            socialGateway,
            tasks,
            user,
            {});
    } else {
        commitIdsToApprove = moderation::getAutoApproveCommitIds(
            branchCtx,
            socialGateway,
            tasks,
            user,
            {*request_.aoiId});
    }

    social::TaskIds approvedTaskIds;
    if (!commitIdsToApprove.empty()) {
        approved_commits::CommitsApprover approver(branchCtx, user, commitIdsToApprove);
        approvedTaskIds = approver.closeTasksWithContributing(
            common::ROBOT_UID,
            social::CloseResolution::Approve);
        result_->taskIds.insert(approvedTaskIds.begin(), approvedTaskIds.end());
    }
    checkResult(request_.resolution, request_.taskIds, result_->taskIds);

    social::TaskIds resolvedTaskIds;
    for (auto taskId : result_->taskIds) {
        if (!approvedTaskIds.count(taskId)) {
            resolvedTaskIds.insert(taskId);
        }
    }
    return resolvedTaskIds;
}

void
SocialModerationTasksResolve::processEditsRevert(
    BranchContext& branchCtx,
    const social::Gateway& socialGateway,
    const social::Tasks& tasks,
    bool isUserCartographer)
{
    if (!isUserCartographer && !request_.aoiId) {
        CheckPermissions(
            request_.userId(),
            branchCtx.txnCore()).checkPermissionsToBlockingTasks();
    }

    CommitsReverter reverter(observers_, branchCtx, request_.userContext);

    const auto commitIds = moderation::commitIdsFromEditTasks(tasks);
    const auto revertData = reverter.revertCommits(commitIds, request_.revertReason);

    result_->taskIds = moderation::closeOrResolveRevertedTasks(
        branchCtx,
        socialGateway,
        reverter.user(),
        revertData.revertedCommitIds,
        request_.aoiId ? TOIds{*request_.aoiId} : TOIds{},
        commitIds,
        isUserCartographer
    );

    checkResult(social::ResolveResolution::Revert, request_.taskIds, result_->taskIds);

    checkTasksResolvedByCurrentUser(
        social::ResolveResolution::Revert,
        request_.userId(),
        socialGateway.loadTasksByTaskIds(request_.taskIds));

    result_->token = reverter.syncViewAndCommit(revertData);
    result_->revertingCommitModel = CommitModel(revertData.createdCommit);
}

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

} // namespace maps::wiki
