#include <maps/wikimap/mapspro/services/editor/src/actions/social/moderation/dashboard.h>

#include <maps/wikimap/mapspro/services/editor/src/branch_helpers.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/categories_strings.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/exception.h>
#include <maps/wikimap/mapspro/services/editor/src/moderation.h>
#include <maps/wikimap/mapspro/services/editor/src/social_utils.h>

#include <maps/wikimap/mapspro/libs/acl/include/aclgateway.h>
#include <maps/wikimap/mapspro/libs/acl/include/check_context.h>
#include <maps/wikimap/mapspro/libs/acl/include/group.h>
#include <maps/wikimap/mapspro/libs/acl/include/policy.h>
#include <maps/wikimap/mapspro/libs/acl/include/role.h>
#include <maps/wikimap/mapspro/libs/acl/include/subject_path.h>
#include <maps/wikimap/mapspro/libs/configs_editor/include/yandex/maps/wiki/configs/editor/category_groups.h>
#include <maps/wikimap/mapspro/libs/revision/include/yandex/maps/wiki/revision/objectrevision.h>
#include <maps/wikimap/mapspro/libs/revision/include/yandex/maps/wiki/revision/revisionsgateway.h>
#include <maps/wikimap/mapspro/libs/social/include/yandex/maps/wiki/social/gateway.h>

#include <maps/libs/common/include/map_at.h>

#include <sstream>
#include <stack>
#include <unordered_set>
#include <vector>

namespace maps {
namespace wiki {

namespace {

const acl::SubjectPath ACL_PATH_PREFIX("mpro/social/moderation-dashboard");

const TOid REGION_HIERARCHY_ROOT_ID = 0;

void checkDashboardPermissions(
        pqxx::transaction_base& work,
        TUid uid,
        social::ModerationMode mode)
{
    ACL_PATH_PREFIX(moderation::toAclRoleName(mode)).check(
        acl::CheckContext(uid, {}, work, {acl::User::Status::Active}));
}

} // namespace

SocialModerationDashboard::Request::Request(
        TUid uid, social::ModerationMode mode, OutputType outputType, Token token,
        boost::optional<std::string> categoryGroup,
        boost::optional<std::string> eventType_)
    : uid(uid)
    , mode(mode)
    , outputType(outputType)
    , token(token)
    , categoryGroup(categoryGroup)
{
    CHECK_REQUEST_PARAM(uid);
    WIKI_REQUIRE(!categoryGroup || cfg()->editor()->categoryGroups().isGroupExists(*categoryGroup),
                 ERR_BAD_REQUEST,
                 "Unknown group: " << *categoryGroup);

    if (eventType_) {
        social::EventType eventTypeValue;
        WIKI_REQUIRE(
            tryFromString(*eventType_, eventTypeValue),
            ERR_BAD_REQUEST,
            "Unknown event type: " << *eventType_);
        eventType = eventTypeValue;
    }
}

SocialModerationDashboard::SocialModerationDashboard(const Request& request)
    : controller::BaseController<SocialModerationDashboard>(BOOST_CURRENT_FUNCTION)
    , request_(request)
{ }

std::string SocialModerationDashboard::printRequest() const
{
    std::ostringstream os;
    os << " uid: " << request_.uid
       << " token: " << request_.token
       << " mode: " << moderation::toAclRoleName(request_.mode);
    if (request_.categoryGroup) {
        os << " category-group: " << *request_.categoryGroup;
    }
    if (request_.eventType) {
        os << " event-type: " << *request_.eventType;
    }
    return os.str();
}

void SocialModerationDashboard::control()
{
    auto branchCtx = CheckedTrunkBranchContextFacade()
        .acquireReadCoreSocial(request_.token);
    auto& txnCore = branchCtx.txnCore();

    checkDashboardPermissions(txnCore, request_.uid, request_.mode);

    result_->mode = request_.mode;

    loadPolicies(txnCore);
    loadRegions(txnCore);
    if (request_.outputType == OutputType::Tree) {
        buildRegionsHierarchy();
    }
    calcModCounts(txnCore);
    fillStats(branchCtx.txnSocial());
}

void SocialModerationDashboard::loadPolicies(pqxx::transaction_base& work)
{
    acl::ACLGateway aclGateway(work);
    auto roles = aclGateway.roles(
        acl::InclusionType::StartsWith,
        moderation::toAclRoleName(request_.mode));
    for (const acl::Role& role : roles) {
        for (const acl::Policy& policy : role.policies()) {
            policies_.emplace(policy);
        }
    }
}

void SocialModerationDashboard::loadRegions(pqxx::transaction_base& work)
{
    TOIds moderatedAoiIds;
    for (const auto& policy : policies_) {
        if (policy.aoiId) {
            moderatedAoiIds.insert(policy.aoiId);
        }
    }

    if (moderatedAoiIds.empty()) {
        return;
    }

    revision::RevisionsGateway revGateway(work); // trunk
    auto snapshot = revGateway.snapshot(revGateway.headCommitId());

    auto aoiObjRevs =
        snapshot.objectRevisionsByFilter(existingAoiFilter(moderatedAoiIds));
    std::unordered_map<TOid, Geom> aoiGeomById;
    for (const auto& aoiObjRev : aoiObjRevs) {
        TOid aoiId = aoiObjRev.id().objectId();
        const auto& data = aoiObjRev.data();
        ASSERT(data.geometry);
        aoiGeomById.insert({aoiId, Geom(*data.geometry)});

        ASSERT(data.attributes);
        const auto& attrs = *data.attributes;
        auto nameIt = attrs.find(ATTR_AOI_NAME);
        result_->regionStatsByOid.insert({
            aoiId,
            nameIt != std::end(attrs)
                ? nameIt->second
                : std::string()});
    }

    for (const auto& [currOid, currGeom] : aoiGeomById) {
        auto& nestedAois = nestedAoisById_[currOid];
        for (const auto& [otherOid, otherGeom] : aoiGeomById) {
            if (currOid == otherOid) {
                continue;
            }
            if (currGeom->contains(otherGeom.geosGeometryPtr())) {
                nestedAois.push_back(otherOid);
            }
        }
    }
}

void SocialModerationDashboard::buildRegionsHierarchy()
{
    enum class DfsState
    {
        New,
        Entered,
        Exited,
        ParentAssigned
    };

    std::unordered_map<TOid, DfsState> aoiDfsState;
    std::stack<TOid> dfsStack;
    for (const auto& currRoot : nestedAoisById_) {
        if (aoiDfsState[currRoot.first] != DfsState::New) {
            ASSERT(aoiDfsState[currRoot.first] != DfsState::Entered);
            continue;
        }

        dfsStack.push(currRoot.first);
        while (!dfsStack.empty()) {
            TOid curr = dfsStack.top();
            auto& childAois = result_->regionsHierarchy[curr];

            switch (aoiDfsState[curr]) {
                case DfsState::New:
                    aoiDfsState[curr] = DfsState::Entered;
                    for (TOid child : nestedAoisById_[curr]) {
                        if (aoiDfsState[child] == DfsState::New) {
                            dfsStack.push(child);
                        }
                    }
                    break;
                case DfsState::Entered:
                    for (TOid child : nestedAoisById_[curr]) {
                        if (aoiDfsState[child] == DfsState::Exited) {
                            childAois.push_back(child);
                            aoiDfsState[child] = DfsState::ParentAssigned;
                        }
                    }
                    aoiDfsState[curr] = DfsState::Exited;
                    // fall through
                case DfsState::Exited:
                case DfsState::ParentAssigned:
                    dfsStack.pop();
            }
        }
    }

    auto& rootAois = result_->regionsHierarchy[REGION_HIERARCHY_ROOT_ID];
    for (const auto& currRoot : nestedAoisById_) {
        if (aoiDfsState[currRoot.first] == DfsState::ParentAssigned) {
            continue;
        }
        ASSERT(aoiDfsState[currRoot.first] == DfsState::Exited);
        rootAois.push_back(currRoot.first);
    }
}

void SocialModerationDashboard::calcModCounts(pqxx::transaction_base& work)
{
    std::vector<acl::ID> agentIds;
    for (const auto& policy : policies_) {
        agentIds.push_back(policy.agentId);
    }

    acl::ACLGateway aclGateway(work);
    std::unordered_map<acl::ID, std::vector<acl::User>> groupUsersById;
    for (const auto& group : aclGateway.groups(agentIds)) {
        groupUsersById.insert({group.id(), group.users()});
    }

    std::unordered_map<acl::ID, acl::User> usersById;
    for (auto& user : aclGateway.usersByIds(agentIds)) {
        usersById.insert({user.id(), std::move(user)});
    }

    std::unordered_map<TOid, std::unordered_set<acl::ID>> modsByAoiId;
    for (const auto& policy : policies_) {
        if (!nestedAoisById_.count(policy.aoiId)) {
            // deleted aoi or 0
            continue;
        }

        auto addAffectedUser = [&](const acl::User& user)
        {
            if (user.status() != acl::User::Status::Active) {
                return;
            }
            modsByAoiId[policy.aoiId].insert(user.id());
            for (auto nestedAoiId : nestedAoisById_.at(policy.aoiId)) {
                modsByAoiId[nestedAoiId].insert(user.id());
            }
        };

        auto groupIt = groupUsersById.find(policy.agentId);
        if (groupIt != std::end(groupUsersById)) {
            for (const auto& user : groupIt->second) {
                addAffectedUser(user);
            }
        } else if (usersById.count(policy.agentId)) {
            addAffectedUser(usersById.at(policy.agentId));
        }
    }

    for (const auto& [oid, userIds] : modsByAoiId) {
        result_->regionStatsByOid.at(oid).modsAssigned = userIds.size();
    }
}

void SocialModerationDashboard::fillStats(pqxx::transaction_base& work)
{
    social::Gateway socialGw(work);

    auto statsConsole = socialGw.taskStatsConsole(request_.mode);
    if (request_.categoryGroup) {
        auto categories = cfg()->editor()->categoryGroups().categoryIdsByGroup(*request_.categoryGroup);
        statsConsole.setFilterByCategories(categories);
    }
    if (request_.eventType) {
        statsConsole.setFilterByEventType(*request_.eventType);
    }

    auto activeTaskStats = statsConsole.activeTaskStatsByAoi(cfg()->moderationTimeIntervals());
    auto recentNewCounts = statsConsole.recentNewTaskCountsByAoi(cfg()->moderationTimeIntervals());
    auto recentProcessedCounts = statsConsole.recentProcessedTaskCountsByAoi(cfg()->moderationTimeIntervals());

    for (auto& oidStatsPair : result_->regionStatsByOid) {
        auto oid = oidStatsPair.first;
        auto& stats = oidStatsPair.second;

        auto atStatsIt = activeTaskStats.find(oid);
        if (atStatsIt != std::end(activeTaskStats)) {
            stats.activeTaskCount = atStatsIt->second.count;
            stats.oldestActiveTaskActiveSince =
                std::move(atStatsIt->second.oldestTaskActiveSince);
        }

        stats.recentNewTaskCount = mapAt(recentNewCounts, oid, 0);
        stats.recentProcessedTaskCount = mapAt(recentProcessedCounts, oid, 0);
    }
}

} // namespace wiki
} // namespace maps
