#include "get_feed.h"

#include "get_feed_helper.h"

#include <maps/wikimap/mapspro/services/editor/src/actions/social/moderation/helpers.h>
#include <maps/wikimap/mapspro/services/editor/src/check_permissions.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/rate_limiter.h>
#include <maps/wikimap/mapspro/services/editor/src/moderation.h>
#include <maps/wikimap/mapspro/services/editor/src/serialize/common.h>
#include <maps/wikimap/mapspro/services/editor/src/social_utils.h>

#include <maps/wikimap/mapspro/libs/acl_utils/include/user_feed_access.h>
#include <maps/wikimap/mapspro/libs/revision_meta/include/approved_queue.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/deleted_users_cache.h>
#include <maps/wikimap/mapspro/libs/acl/include/subject_path.h>

#include <yandex/maps/wiki/common/paged_result.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/category_groups.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <yandex/maps/wiki/social/gateway.h>
#include <yandex/maps/wiki/social/i_feed.h>

#include <boost/optional.hpp>
#include <boost/optional/optional_io.hpp>

#include <memory>

namespace maps::wiki {

namespace {

revision::Wkb
getRegionGeometry(
    const BranchContext& branchCtx, TOid regionId)
{
    revision::RevisionsGateway rg(branchCtx.txnCore());
    auto revision = rg.snapshot(rg.headCommitId()).objectRevision(regionId);
    WIKI_REQUIRE(revision && !revision->data().deleted, ERR_NOT_FOUND,
                 "object id: " << regionId << " not found");
    WIKI_REQUIRE(
            revision->data().attributes
            && revision->data().attributes->count(
                    plainCategoryIdToCanonical(CATEGORY_FEED_REGION))
            && revision->data().geometry,
            ERR_NOT_FOUND, "bad revision id: " << revision->id());

    return *revision->data().geometry;
}

social::CategoryIdsFilter
getRegionFeedCategoriesFilter(boost::optional<std::string> includedCategoryGroup)
{
    if (includedCategoryGroup) {
        return {cfg()->editor()->categoryGroups().categoryIdsByGroup(
                    *includedCategoryGroup),
                social::InclusionPolicy::Including};
    }

    return {cfg()->editor()->categoryGroups().categoryIdsByGroup(
                CATEGORY_GROUP_SERVICE),
            social::InclusionPolicy::Excluding};
}

social::IFeedPtr createRegionFeed(
    const BranchContext& branchCtx,
    const GetRegularSocialFeed::Request& request)
{
    // WARNING: request.subscriber means regionId when FeedType is region.
    const auto regionId = request.subscriber;

    const auto geometry = getRegionGeometry(branchCtx, regionId);
    auto regionFeedPtr = social::Gateway(branchCtx.txnSocial())
                             .regionFeed(request.branchId, geometry);

    regionFeedPtr->setCategoryIdsFilter(
        getRegionFeedCategoriesFilter(request.categoryGroup));

    auto& cache = cfg()->userGroupsUidsCache();
    regionFeedPtr->setSkippedUids(
        acl_utils::userKindsToUids(request.userKinds, request.uid, cache));

    return regionFeedPtr;
}

social::IFeedPtr
createFeed(
    const BranchContext& branchCtx,
    const GetRegularSocialFeed::Request& request)
{
    if (request.feedType == social::FeedType::Region) {
        return createRegionFeed(branchCtx, request);
    }

    social::FeedFilter filter;

    if (request.categoryGroup) {
        filter.categoryIds(
            {cfg()->editor()->categoryGroups().categoryIdsByGroup(
                 *request.categoryGroup),
             social::InclusionPolicy::Including});
    }
    if (request.since) {
        filter.createdAfter(*request.since);
    }
    if (request.till) {
        filter.createdBefore(*request.till);
    }
    if (request.preApprovedOnly == GetRegularSocialFeed::PreApprovedOnly::Yes ||
        request.feedType == social::FeedType::PreApproved)
    {
        const auto preApprovedCommits =
            revision_meta::PreApprovedQueue(branchCtx.txnCore()).readAllCommitIds();

        if (preApprovedCommits.empty()) {
            return social::IFeedPtr();
        }

        filter.commitIds(preApprovedCommits);
    }

    return std::make_unique<social::Feed>(
        branchCtx.txnSocial(),
        request.branchId,
        request.subscriber,
        request.feedType,
        filter);
}

void
checkUserReadCapabilities(
    const BranchContext& branchCtx,
    const GetRegularSocialFeed::Request& request)
{
    switch (request.feedType) {
        case social::FeedType::Aoi: {
            auto user = acl::ACLGateway(branchCtx.txnCore()).user(request.uid);
            user.checkActiveStatus();
            if (moderation::createRegionPolicies(user, request.subscriber, boost::none).empty())
            {
                CheckPermissions(
                    request.uid,
                    branchCtx.txnCore()).checkAccessToEditsFeed();
            }
            break;
        }
        case social::FeedType::Region: break;
        case social::FeedType::Suspicious: break;
        case social::FeedType::User:
            WIKI_REQUIRE(
                acl_utils::isUserAllowedToReadFeed(
                    branchCtx.txnCore(),
                    request.uid,
                    request.subscriber
                ),
                ERR_FORBIDDEN,
                "Reading user feed is denied"
                " (" << request.subscriber << "), uid: " << request.uid);
            break;
        case social::FeedType::PreApproved:
            CheckPermissions(
                request.uid,
                branchCtx.txnCore()).checkPermissionsToBlockingTasks();
    }
}


bool isCategoryGroupAllowed(const CategoryGroup& categoryGroup, const acl::CheckContext& context)
{
    const auto& editorCfg = cfg()->editor();
    const acl::SubjectPath aclPath(SERVICE_ACL_BASE);
    for (const auto& categoryId : categoryGroup.categoryIds()) {
        const auto& category = editorCfg->categories()[categoryId];
        if (aclPath(category.aclPermissionName()).isAllowedPartially(context)) {
            return true;
        }
    }
    return false;
}

void requireNonDeletedUser(TUid uid)
{
    WIKI_REQUIRE(
        cfg()->deletedUsersCache().isAllowed(uid),
        ERR_FORBIDDEN,
        "Reading feed is denied, uid: " << uid << " deleted");
}

} // namespace

GetSocialFeed::GetSocialFeed()
    : controller::BaseController<GetSocialFeed>(BOOST_CURRENT_FUNCTION)
{}

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

std::string
GetRegularSocialFeedMeta::printRequest() const
{
    std::stringstream ss;
    ss << " uid: " << request_.uid
       << " token: " << request_.token;
    return ss.str();
}

void
GetRegularSocialFeedMeta::control()
{
    WIKI_REQUIRE(request_.uid, ERR_FORBIDDEN, "Authorization required.");
    requireNonDeletedUser(request_.uid);
    const auto& editorCfg = cfg()->editor();
    auto work = cfg()->poolCore().slaveTransaction(request_.token);
    acl::CheckContext context(
        request_.uid,
        {},
        *work,
        {acl::User::Status::Active, acl::User::Status::Banned});
    auto limitlessContext = context.inflate();

    for (const auto& [groupId, _] : editorCfg->categoryGroups().allGroups()) {
        if (groupId == CATEGORY_GROUP_SERVICE) {
            continue;
        }
        const auto group = editorCfg->categoryGroups().groupById(groupId);
        if (isCategoryGroupAllowed(*group, limitlessContext)) {
            result_->categoryGroups.insert(groupId);
        }
    }
}

GetRegularSocialFeed::GetRegularSocialFeed(const Request& request)
    : request_(request)
{
    CHECK_REQUEST_PARAM(request.perPage);
    if (!request_.page) {
        WIKI_REQUIRE(
            !request_.afterEventId || !request_.beforeEventId,
            ERR_BAD_REQUEST,
            "non-empty after,before request parameters");
    }

    if (request_.feedType == social::FeedType::User) {
        CHECK_REQUEST_PARAM(request_.subscriber);
    }

    if (request_.categoryGroup) {
        WIKI_REQUIRE(
            cfg()->editor()->categoryGroups().isGroupExists(*request_.categoryGroup),
            ERR_BAD_REQUEST,
            "unknown category group: " << *request_.categoryGroup);
    }

    result_->branchId = request_.branchId;
    result_->feedType = request_.feedType;
    result_->subscriber = request_.subscriber;
    result_->page = request_.page;
    result_->perPage = request_.perPage;
}

std::string
GetRegularSocialFeed::printPreApprovedOnly() const
{
    switch (request_.preApprovedOnly) {
        case PreApprovedOnly::No:  return "No";
        case PreApprovedOnly::Yes: return "Yes";
    }
}

std::string
GetRegularSocialFeed::printRequest() const
{
    std::stringstream ss;
    ss << " uid: " << request_.uid
        << " branch: " << request_.branchId
        << " feed: " << request_.feedType
        << " subscriber: " << request_.subscriber
        << " category-group: " << request_.categoryGroup
        << " page: " << request_.page
        << " per-page: " << request_.perPage
        << " after-event-id: " << request_.afterEventId
        << " before-event-id: " << request_.beforeEventId;
    ss << " since: ";
    if (request_.since) {
        ss << chrono::formatIsoDateTime(*request_.since);
    } else {
        ss << "none";
    }
    ss << " till: ";
    if (request_.till) {
        ss << chrono::formatIsoDateTime(*request_.till);
    } else {
        ss << "none";
    }
    ss << " preapproved-only: " << printPreApprovedOnly()
        << " token: " << request_.token;
    return ss.str();
}

void
GetRegularSocialFeed::control()
{
    requireNonDeletedUser(request_.uid);
    if (request_.uid && !request_.branchId && cfg()->rateLimiter().areRpsChecksEnabled()) {
        static const std::string HANDLE = "/social/feeds/";
        std::ostringstream params;
        params << request_.feedType << "/" << request_.subscriber;
        cfg()->rateLimiter().checkUserReadActivityDeleteAndRestrict(
            request_.uid, HANDLE, params.str());
    }

    auto branchCtx = BranchContextFacade(request_.branchId)
        .acquireReadCoreSocial(request_.token);

    checkUserReadCapabilities(branchCtx, request_);

    auto feed = createFeed(branchCtx, request_);
    if (!feed) {
        return;
    }

    if (request_.page || request_.withTotal == WithTotal::Yes) {
        result_->totalCount = feed->count();
    }
    if (request_.page) {
        if (result_->totalCount > 0) {
            common::Pager pager(*result_->totalCount, request_.page, request_.perPage);
            fillWithEvents(
                branchCtx,
                *result_,
                feed->events(pager.offset(), pager.limit())
            );
        }
        return;
    }

    auto processEventsMore = [&](std::pair<social::Events, social::HasMore>&& pair)
    {
        fillWithEvents(
            branchCtx,
            *result_,
            std::move(pair.first)
        );
        result_->hasMore = pair.second == social::HasMore::Yes;
    };

    if (request_.beforeEventId) {
        processEventsMore(feed->eventsBefore(request_.beforeEventId, request_.perPage));
    } else if (request_.afterEventId) {
        processEventsMore(feed->eventsAfter(request_.afterEventId, request_.perPage));
    } else {
        processEventsMore(feed->eventsHead(request_.perPage));
    }
}

} // namespace maps::wiki
