#include "tile.h"

#include <maps/infra/yacare/include/yacare.h>
#include <maps/libs/enum_io/include/enum_io.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/tile/include/geometry.h>
#include <maps/wikimap/mapspro/libs/acl/include/aclgateway.h>
#include <maps/wikimap/mapspro/libs/acl_utils/include/user_kind.h>
#include <maps/wikimap/mapspro/libs/gdpr/include/user.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/common/include/profiletimer.h>
#include <maps/libs/geolib/include/algorithm.h>
#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/contains.h>
#include <maps/libs/geolib/include/polyline.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/variant.h>
#include <yandex/maps/wiki/configs/editor/category_groups.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <yandex/maps/wiki/social/event.h>
#include <yandex/maps/wiki/social/gateway.h>
#include <yandex/maps/wiki/social/region_feed.h>

namespace aclu = maps::wiki::acl_utils;

namespace maps::wiki::socialsrv {

namespace {

class UserStatusCache {
public:
    explicit UserStatusCache(IDbPools& dbPools)
        : dbPools_(dbPools)
    {}

    bool isActive(social::TUid uid, const DbToken& token)
    {
        if (!data_.count(uid)) {
            auto coreTxnHandle = dbPools_.coreReadTxn(token);
            data_[uid] = acl::ACLGateway(*coreTxnHandle).user(uid).status();
        }
        return acl::User::Status::Active == data_.at(uid);
    }

private:
    IDbPools& dbPools_;
    std::map<social::TId, acl::User::Status> data_;
};

// clang-format off
const std::vector<aclu::UserKind> DEFAULT_IGNORED_USER_KINDS{
    aclu::UserKind::Robots,
    aclu::UserKind::Yandex
};
// clang-format on

constexpr double HANDLE_THRESHOLD_SECONDS = 1.0;

constexpr size_t ADDITIONAL_EVENTS_COUNT = 50; // was 100, GEOMONITORINGS-17319

const std::string CATEGORY_GROUP_SERVICE = "service_group";

std::vector<aclu::UserKind> getIgnoredUserKinds(
    std::vector<std::string> skipCreatedBy)
{
    std::vector<aclu::UserKind> ignoredUserKinds;
    for (const auto& str: skipCreatedBy) {
        auto enumValue = enum_io::tryFromString<aclu::UserKind>(str);
        REQUIRE(
            enumValue,
            yacare::errors::BadRequest()
                << "Invalid skip-created-by parameter value: '" << str << "'");
        ignoredUserKinds.push_back(*enumValue);
    }
    return ignoredUserKinds;
}

social::RegionFeed getRegionFeedForTile(
    const configs::editor::ConfigHolder& editorConfig,
    pqxx::transaction_base& socialReadTxn,
    const geolib3::Polygon2& tilePolygon,
    const social::TUids& skippedUids)
{
    const size_t branchId = 0;
    social::RegionFeed regionFeed{
        socialReadTxn,
        branchId,
        geolib3::WKB::toString<geolib3::Polygon2>(tilePolygon),
        social::EventBoundsPredicate::IntersectsRegion};

    regionFeed.setSkippedUids(skippedUids);
    regionFeed.setCategoryIdsFilter(
        {editorConfig.categoryGroups().categoryIdsByGroup(
             CATEGORY_GROUP_SERVICE),
         social::InclusionPolicy::Excluding});

    return regionFeed;
}

geolib3::Point2 getEventPosition(const std::string& wkb)
{
    const auto variant =
        geolib3::WKB::read<geolib3::SimpleGeometryVariant>(wkb);
    switch (variant.geometryType()) {
        case geolib3::GeometryType::Polygon:
            return geolib3::findCentroid(variant.get<geolib3::Polygon2>());
        case geolib3::GeometryType::Point:
            return variant.get<geolib3::Point2>();
        case geolib3::GeometryType::LineString:
            return geolib3::findMiddle(variant.get<geolib3::Polyline2>());
        default:
            throw maps::Exception("Unexpected variant GeometryType: ")
                << variant.geometryType();
    }
}

std::map<revision::RevisionID, revision::ObjectRevision> getRevisions(
    pqxx::transaction_base& txn,
    const std::set<social::TId>& commitIds,
    const std::set<social::TId>& objectIds)
{
    std::map<revision::RevisionID, revision::ObjectRevision> revisionsById;
    if (commitIds.empty() || objectIds.empty()) {
        return revisionsById;
    }

    revision::RevisionsGateway rg(txn);
    auto reader = rg.reader();
    auto filter = revision::filters::ObjRevAttr::nextCommitId() == 0 &&
                  revision::filters::ObjRevAttr::commitId().in(commitIds) &&
                  revision::filters::ObjRevAttr::objectId().in(objectIds) &&
                  revision::filters::Geom::defined();
    auto revisions = reader.loadRevisions(filter);
    for (const auto& r: revisions) {
        revisionsById.try_emplace(r.id(), r);
    }
    return revisionsById;
}

std::set<social::TId> getFilteredCommitIds(
    pqxx::transaction_base& txn, const std::set<social::TId>& commitIds)
{
    std::set<social::TId> result;
    if (commitIds.empty()) {
        return result;
    }

    const auto commitsFilter =
        revision::filters::CommitAttr::id().in(commitIds);
    const auto commits = revision::Commit::load(txn, commitsFilter);

    for (const auto& commit: commits) {
        if (!commit.revertingCommitIds().empty() ||
            !commit.revertedCommitIds().empty()) {
            continue;
        }
        result.insert(commit.id());
    }
    return result;
}

std::vector<EventData> getEventsData(
    IDbPools& dbPools,
    const social::Events& events, const DbToken& token)
{
    std::set<social::TId> commitIds;
    std::set<social::TId> objectIds;
    social::Events filteredEvents;
    for (const auto& event: events) {
        if (event.primaryObjectData() && event.commitData()) {
            filteredEvents.push_back(event);
            commitIds.insert(event.commitData()->commitId());
            objectIds.insert(event.primaryObjectData()->id());
        }
    }

    std::map<revision::RevisionID, revision::ObjectRevision> revisionsById;
    {
        auto coreTxnHandle = dbPools.coreReadTxn(token);
        revisionsById = getRevisions(
            *coreTxnHandle,
            getFilteredCommitIds(*coreTxnHandle, commitIds),
            objectIds);
    }

    std::vector<EventData> result;
    for (const auto& event: filteredEvents) {
        const auto it = revisionsById.find(
            {event.primaryObjectData()->id(),
             event.commitData()->commitId()});
        if (it == revisionsById.end()) {
            continue;
        }
        const auto& rev = it->second;
        ASSERT(rev.data().geometry);
        result.emplace_back(
            EventData{event, getEventPosition(*rev.data().geometry)});
    }

    return result;
}

int64_t elapsedMs(const ProfileTimer& timer)
{
    return timer.getElapsedTimeNumber() * 1000;
}

int64_t elapsedSince(int64_t prevElapsedMs, const ProfileTimer& timer)
{
    return elapsedMs(timer) - prevElapsedMs;
}

} // namespace

social::TUids getIngnoredUids(
    const std::optional<std::vector<std::string>>& skipCreatedBy,
    acl::UID uid,
    const acl_utils::UserGroupsUidsCache& uidsCache)
{
    // clang-format off
    const auto ignoredUserKinds = skipCreatedBy
        ? getIgnoredUserKinds(*skipCreatedBy)
        : DEFAULT_IGNORED_USER_KINDS;
    // clang-format on
    return aclu::userKindsToUids(ignoredUserKinds, uid, uidsCache);
}

std::list<EventData> getEventsForTile(
    IDbPools& dbPools,
    const configs::editor::ConfigHolder& editorConfig,
    const tile::Tile& tile,
    size_t limit,
    social::TUids skippedUids,
    const std::string& token)
{
    const auto tilePolygon = tile::mercatorBBox(tile).polygon();
    const auto tileBbox = tilePolygon.boundingBox();
    std::list<EventData> result;

    UserStatusCache userStatusCache(dbPools);
    auto appendChosenEventsToResult = [&](const social::Events& events) {
        auto eventsData = getEventsData(dbPools, events, token);
        social::TUids addedUids;
        for (auto& eventData: eventsData) {
            if (result.size() >= limit) {
                break;
            }
            auto uid = eventData.event.createdBy();
            if (gdpr::User(uid).hidden()) {
                continue;
            }
            if (addedUids.count(uid)) {
                continue;
            }
            if (geolib3::contains(tileBbox, eventData.position) &&
                userStatusCache.isActive(uid, token))
            {
                result.push_back(std::move(eventData));
                addedUids.insert(uid);
            }
        }
        return addedUids;
    };
    ProfileTimer profileTimer;
    auto txnHandle = dbPools.socialReadTxn(token);
    auto regionFeed =
        getRegionFeedForTile(editorConfig, *txnHandle, tilePolygon, skippedUids);

    int64_t queryStartMs = elapsedMs(profileTimer);
    auto [events, hasMore] = regionFeed.eventsHead(limit);
    int64_t queryTimeMs = elapsedSince(queryStartMs, profileTimer);

    auto addedUids = appendChosenEventsToResult(events);

    size_t additionalQueriesCounter = 0;
    auto logQuery = [&] (size_t eventsCount){
        INFO() << "DB query: queryNumber=" << additionalQueriesCounter
               << " timerMs=" << queryStartMs
               << " queryTimeMs=" << queryTimeMs << " x=" << tile.x()
               << " y=" << tile.y() << " z=" << tile.z()
               << " eventsCount=" << eventsCount;
    };
    logQuery(events.size());
    while (result.size() < limit && hasMore == social::HasMore::Yes &&
           profileTimer.getElapsedTimeNumber() < HANDLE_THRESHOLD_SECONDS) {
        ++additionalQueriesCounter;
        if (!addedUids.empty()) {
            skippedUids.insert(addedUids.begin(), addedUids.end());
            regionFeed.setSkippedUids(skippedUids);
        }

        queryStartMs = elapsedMs(profileTimer);
        auto [eventsTmp, hasMoreTmp] = regionFeed.eventsBefore(
            events.back().id(), ADDITIONAL_EVENTS_COUNT);
        events = std::move(eventsTmp);
        hasMore = hasMoreTmp;

        queryTimeMs = elapsedSince(queryStartMs, profileTimer);

        addedUids = appendChosenEventsToResult(events);
        logQuery(events.size());
    }

    return result;
}

} // namespace maps::wiki::socialsrv
