#include "social.h"
#include "geolocks_checker.h"

#include "diffalert_runner_proxy.h"
#include "envelope_info.h"
#include "trust_level_calculator.h"

#include <maps/wikimap/mapspro/services/editor/src/acl_role_info.h>
#include <maps/wikimap/mapspro/services/editor/src/acl_utils.h>
#include <maps/wikimap/mapspro/services/editor/src/approved_commits/updater.h>
#include <maps/wikimap/mapspro/services/editor/src/collection.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/serialize/title.h>
#include <maps/wikimap/mapspro/services/editor/src/social_utils.h>

#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/libs/acl/include/policy.h>
#include <maps/wikimap/mapspro/libs/acl/include/role.h>
#include <yandex/maps/wiki/common/moderation.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/category_groups.h>
#include <yandex/maps/wiki/social/gateway.h>

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

namespace maps {
namespace wiki {

struct SocialObserver::SocialContextData : public Observer::ContextData
{
    enum class IsAccepted { No, Yes };

    SocialContextData()
        : taskId(0)
        , isAccepted(false)
    { }

    SocialContextData(social::TId taskId_, IsAccepted isAccepted_)
        : taskId(taskId_)
        , isAccepted(isAccepted_ == IsAccepted::Yes)
    { }

    const social::TId taskId;
    const bool isAccepted;
};

namespace {

const StringSet SKIP_COMMIT_ACTIONS = {
    common::COMMIT_PROPVAL_ACTION_SERVICE
};

/**
Runs diffalert checks on request or returns from the cache
Use only from one thread (not thread-safe)
*/

social::EventAlerts
toEventAlerts(const TId eventId, const std::vector<diffalert::Message>& messages)
{
    social::EventAlerts eventAlerts;
    std::map<std::string, std::pair<size_t, size_t>> descriptionToSourceAndInserted;
    auto messageToEventAlert = [&](const diffalert::Message& message) {
        return social::EventAlert {
                    eventId,
                    message.priority().major,
                    message.description(),
                    message.objectId()
                };
    };
    for (size_t sourceIdx = 0; sourceIdx < messages.size(); ++sourceIdx) {
        const auto& message = messages[sourceIdx];
        auto alreadyInserted = descriptionToSourceAndInserted.find(message.description());
        if (alreadyInserted == descriptionToSourceAndInserted.end()) {
            descriptionToSourceAndInserted[message.description()] = std::make_pair(sourceIdx, eventAlerts.size());
            eventAlerts.emplace_back(messageToEventAlert(message));
        } else {
            auto& oldSourceIdx = alreadyInserted->second.first;
            const auto insertedLocation = alreadyInserted->second.second;
            const auto& oldMessage = messages[oldSourceIdx];
            if (message.priority() < oldMessage.priority()) {
                oldSourceIdx = sourceIdx;
                eventAlerts[insertedLocation] = messageToEventAlert(message);
            }
        }
    }
    return eventAlerts;
}

std::optional<social::EventExtraData>
createEventExtraData(const ObjectPtr& object)
{
    if (!object) {
        return social::NO_EVENT_EXTRA_DATA;
    }

    bool extraDataPresent = false;
    social::EventExtraData result;
    const auto ftTypeId =
        object->attributes().value(object->categoryId() + SUFFIX_FT_TYPE_ID);
    if (!ftTypeId.empty()) {
        result.ftTypeId = boost::lexical_cast<social::FtTypeId>(
            ftTypeId
        );
        extraDataPresent = true;
    }
    const auto rubricId = object->attributes().value(ATTR_POI_BUSINESS_RUBRIC_ID);
    if (!rubricId.empty()) {
        result.businessRubricId = boost::lexical_cast<social::BusinessRubricId>(
            rubricId
        );
        extraDataPresent = true;
    }

    if (!extraDataPresent) {
        return social::NO_EVENT_EXTRA_DATA;
    }

    return result;
}

void
pushCommitRegions(
    const revision::Commit& commit,
    const BranchContext& branchCtx,
    const EnvelopeInfo& envelopeInfo)
{
    ASSERT(commit.inTrunk());
    const auto regionIds = getPublishRegionIds(
        branchCtx,
        envelopeInfo.geomDiffObjects(),
        envelopeInfo.envelope()
    );
    revision_meta::CommitRegions(branchCtx.txnCore()).push(commit.id(), regionIds);
}

} // namespace

Observer::ContextDataPtr
SocialObserver::beforeCommit(
    ObjectsCache& cache,
    const GeoObjectCollection& modifiedObjects,
    UserContext& userContext,
    const CommitContext& commitContext) const
{
    auto& branchCtx = cache.branchContext();
    const auto& user = userContext.aclUser(branchCtx);
    user.checkActiveStatus();

    const auto& commit = cache.savedCommit();
    userContext.saveActivity(
        branchCtx, social::UserActivityAction::CreateCommit, commit.id());

    GeoObjectCollection collection = modifiedObjects;
    collection.append(
        cache.find([](const GeoObject* object) { return object->primaryEdit(); })
    );
    DEBUG() << BOOST_CURRENT_FUNCTION << " GeoCollection size: " << collection.size();

    const EnvelopeInfo envelopeInfo(collection, cache);
    const auto primaryObj = collection.primaryObject();
    if (!primaryObj || !primaryObj->isPrivate()) {
        checkLockedByGeolocks(branchCtx, userContext, envelopeInfo.envelope());
    }

    const auto& action = commit.action();
    if (SKIP_COMMIT_ACTIONS.count(action)) {
        return {};
    }

    auto addForApprove = [&]() -> ContextDataPtr
    {
        revision_meta::enqueueCommitsForApprovedBranch(branchCtx.txnCore(), {commit.id()});

        // awake approver after commit transaction
        return std::make_shared<SocialContextData>();
    };

    std::optional<social::PrimaryObjectData> primaryObjData;
    if (primaryObj) {
        if (primaryObj->isPrivate()) {
            return addForApprove();
        }

        primaryObjData = social::PrimaryObjectData {
            primaryObj->id(),
            primaryObj->categoryId(),
            title(primaryObj.get()),
            commitAttributesObjectEditNotes(commit.attributes(), primaryObj->id())
        };
    }

    auto aoiIds = getAoiIds(
        branchCtx, envelopeInfo.geomDiffObjects(), envelopeInfo.envelope());

    DiffAlertRunnerProxy diffAlertRunnerProxy(diffAlertRunner_, cache, modifiedObjects, action);
    const auto trustLevel = TrustLevelCalculator(branchCtx, user, aoiIds, envelopeInfo,
        commit, diffAlertRunnerProxy).calculate(collection);
    WIKI_REQUIRE(
        trustLevel == moderation::TrustLevel::SkipModeration || !aoiIds.empty(),
        ERR_FORBIDDEN,
        "Can not moderate "
            << (trustLevel == moderation::TrustLevel::NotTrusted ? "non-" : "")
            << "trusted task without aoi, uid: " << user.uid());

    social::CommitData commitData {
        branchCtx.branch.id(),
        commit.id(),
        action,
        json(envelopeInfo.envelope())
    };

    social::Gateway socialGw(branchCtx.txnSocial());
    auto event = socialGw.createCommitEvent(
        commit.createdBy(), commitData, primaryObjData, aoiIds,
        createEventExtraData(primaryObj)
    );

    DEBUG() << BOOST_CURRENT_FUNCTION << ": generated event id: " << event.id();

    if (!commit.inTrunk()) {
        return {};
    }

    pushCommitRegions(commit, branchCtx, envelopeInfo);

    if (trustLevel == moderation::TrustLevel::SkipModeration) {
        return addForApprove();
    }
    auto messages = diffAlertRunnerProxy.messages();
    for (const auto& [oid, predefinedDiffAlerts] : commitContext.predefinedDiffAlerts) {
        for (const auto& predefinedDiffAlert : predefinedDiffAlerts) {
            messages.emplace_back(
                oid,
                diffalert::Priority(predefinedDiffAlert.priority, 0),
                predefinedDiffAlert.message,
                diffalert::Message::Scope::WholeObject
            );
        }
    }
    socialGw.saveEventAlerts(toEventAlerts(event.id(), messages));

    if (trustLevel == moderation::TrustLevel::AcceptAndModerate) {
        auto task = socialGw.createAcceptedTask(event, acl_utils::createdOrUnbannedAt(user));
        return std::make_shared<SocialContextData>(
            task.id(), SocialContextData::IsAccepted::Yes);
    }
    auto task = socialGw.createTask(event, acl_utils::createdOrUnbannedAt(user));

    return std::make_shared<SocialContextData>(
        task.id(), SocialContextData::IsAccepted::No);
}

void
SocialObserver::afterCommit(TBranchId, Observer::ContextData& contextData) const
{
    auto socialContextData = dynamic_cast<const SocialContextData*>(&contextData);
    REQUIRE(socialContextData, "Invalid context data type");

    if (socialContextData->taskId) {
        logModerationEvent(
            socialContextData->taskId,
            ModerationLogger::Action::Created);
        if (socialContextData->isAccepted) {
            logModerationEvent(
                socialContextData->taskId,
                ModerationLogger::Action::Resolved);
        }
    } else {
        auto updater = cfg()->approvedCommitsUpdater();
        if (updater) {
            updater->awake();
        }
    }
}

} // namespace wiki
} // namespace maps
