#include "utils.h"

#include <maps/wikimap/mapspro/libs/acl/include/aclgateway.h>
#include <maps/wikimap/mapspro/libs/acl_utils/include/moderation.h>
#include <maps/wikimap/mapspro/libs/gdpr/include/user.h>

#include <yandex/maps/wiki/common/geom.h>
#include <yandex/maps/wiki/common/moderation.h>

#include <maps/libs/geolib/include/multipolygon.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/spatial_relation.h>

namespace rev = maps::wiki::revision;
namespace filter = maps::wiki::revision::filters;

namespace maps::wiki::tasks::involvement {

namespace {

RevisionIdToRevision
loadPreviousRevisions(
    const rev::RevisionsGateway& gateway,
    const rev::Revisions& revisions
)
{
    rev::RevisionIds prevRevisionIds;
    prevRevisionIds.reserve(revisions.size());
    for (const auto& revision: revisions) {
        if (revision.prevId().valid()) {
            prevRevisionIds.emplace_back(revision.prevId());
        }
    }
    auto prevRevisions = gateway.reader().loadRevisions(prevRevisionIds);

    RevisionIdToRevision result;
    for (auto& revision: prevRevisions) {
        result.insert({revision.id(), std::move(revision)});
    }
    return result;
}

bool hasAttribute(
    const rev::ObjectRevision& revision,
    const std::string& attribute)
{
    return revision.data().attributes->count(attribute) > 0;
}

bool isPedestrianFunctionalClass(const rev::ObjectRevision& revision)
{
    const auto& attrs = revision.data().attributes.value();
    return attrs.at(ATTR_FUNCTIONAL_CLASS) == FUNCTIONAL_CLASS_PEDESTRIAN;
}

EAttributeChangeType getAttributeChangeType(
    const rev::ObjectRevision& revision,
    const RevisionIdToRevision& previousRevisions,
    const std::string& attribute)
{
    const bool isSet = (
        !revision.data().deleted &&
         hasAttribute(revision, attribute)
    );

    bool wasSet = false;
    if (revision.prevId().valid()) {
        /**
         * Object was altered. Previous revision is required.
         *
         * WARN: Even if deleted object was restored
         * during revert operation, it won't be counted.
         */
        const auto& previousRevision =
            previousRevisions.at(revision.prevId());
        wasSet = (
            !previousRevision.data().deleted &&
             hasAttribute(previousRevision, attribute)
        );
    }

    if (isSet == wasSet) {
        return EAttributeChangeType::UNCHANGED;
    } else if (wasSet and not isSet) {
        return EAttributeChangeType::UNSET;
    }
    return EAttributeChangeType::SET;
}

bool isNonYandexUserStatus(const std::string& modStatus) {
    return modStatus == common::MODERATION_STATUS_COMMON ||
           modStatus == common::MODERATION_STATUS_EXPERT ||
           modStatus == common::MODERATION_STATUS_MODERATOR;
}

[[maybe_unused]]
int64_t processAll(
    pqxx::transaction_base&,
    const std::vector<rev::DBID>& commitIds,
    const geolib3::MultiPolygon2&)
{
    return commitIds.size();
}

[[maybe_unused]]
int64_t processAllForNonYandexUsers(
    pqxx::transaction_base& coreTxn,
    const std::vector<rev::DBID>& commitIds,
    const geolib3::MultiPolygon2&)
{
    auto commits = revision::Commit::load(
        coreTxn,
        revision::filters::CommitAttr::id().in(commitIds)
    );

    const auto authorToModStatus = [&](){
        std::set<revision::UserID> authors;
        for (const auto& commit : commits) {
            const gdpr::User user(commit.createdBy());
            if (!user.hidden()) {
                authors.emplace(user.uid());
            }
        }

        acl::ACLGateway aclGateway(coreTxn);
        return acl_utils::moderationStatuses(aclGateway, authors);
    }();

    size_t count = 0;
    for (const auto& commit : commits) {
        auto it = authorToModStatus.find(gdpr::User(commit.createdBy()).realUid());
        if (it != authorToModStatus.end() && isNonYandexUserStatus(it->second)) {
            ++count;
        }
    }
    return count;
}

} // namespace

size_t countObjectsInsideRegion(
    const rev::Revisions& revisions,
    std::shared_ptr<const geos::geom::MultiPolygon> geosMultiPolygon)
{
    if (!geosMultiPolygon) {
        return revisions.size();
    }

    return std::count_if(
        revisions.begin(), revisions.end(),
        [&](const auto& rev) {
            const auto& geometry = rev.data().geometry;
            ASSERT(geometry);
            const common::Geom geom(*geometry);
            return geosMultiPolygon->intersects(geom.geosGeometryPtr());
        }
    );
}

int64_t countLengthOnAnyChangeOfAttribute(
    const rev::ObjectRevision& revision,
    const EAttributeChangeType pavedChangeType)
{
    const auto revisionGeom = common::Geom(revision.data().geometry.value());
    const double rdElLength = revisionGeom.realLength();

    if (pavedChangeType != EAttributeChangeType::UNCHANGED) {
        return std::llround(rdElLength);
    }
    return 0;
}

int64_t countLengthAvoidingCheating(
    const rev::ObjectRevision& revision,
    const EAttributeChangeType speedLimitChangeType)
{
    const auto revisionGeom = common::Geom(revision.data().geometry.value());
    const double rdElLength = revisionGeom.realLength();

    if (speedLimitChangeType == EAttributeChangeType::SET) {
        return std::llround(rdElLength);
    } else if (speedLimitChangeType == EAttributeChangeType::UNSET) {
        return std::llround(-rdElLength);
    }
    return 0;
}

social::Events
filterCommitEventsByNonYaUsers(
    pqxx::transaction_base& coreTxn,
    const social::Events& commitEvents)
{
    std::set<social::TUid> authorsUids;
    for (const auto& commitEvent : commitEvents) {
        const gdpr::User user(commitEvent.createdBy());
        if (!user.hidden()) {
            authorsUids.emplace(user.uid());
        }
    }

    acl::ACLGateway aclGateway(coreTxn);
    auto authorToModStatus = acl_utils::moderationStatuses(aclGateway, authorsUids);

    social::Events filteredCommitEvents;
    for (const auto& commitEvent : commitEvents) {
        const gdpr::User user(commitEvent.createdBy());
        if (user.hidden()) {
            continue;
        }
        auto it = authorToModStatus.find(user.uid());
        if (it != authorToModStatus.end() && isNonYandexUserStatus(it->second)) {
            filteredCommitEvents.push_back(commitEvent);
        }
    }

    return filteredCommitEvents;
}

std::optional<geolib3::BoundingBox>
extractBoundingBoxMerc(const social::Event& event)
{
    if (!event.commitData()) {
        return std::nullopt;
    }
    return event.commitData().value().bbox();
}

bool isMultipolygonIntersectsBoundingBox(
    const geolib3::MultiPolygon2& multipolygon,
    const std::optional<geolib3::BoundingBox>& boundingBox)
{
    if (multipolygon.polygonsNumber() == 0) {
        return true;
    }
    if (!boundingBox) {
        return false;
    }

    if (boundingBox.value().isDegenerate()) {
        return geolib3::spatialRelation(
            multipolygon,
            boundingBox.value().center(),
            geolib3::SpatialRelation::Intersects
        );
    } else {
        return geolib3::spatialRelation(
            multipolygon,
            boundingBox.value().polygon(),
            geolib3::SpatialRelation::Intersects
        );
    }
}

rev::filters::BinaryFilterExpr
getFilterForAnyRdElRevisions(const std::vector<rev::DBID>& commitIDs)
{
    return (
        filter::Attr(CAT_RD_EL).defined() &&
        filter::Geom::defined() &&
        filter::CommitAttr::id().in(commitIDs)
    );
}

rev::filters::BinaryFilterExpr
getFilterForRdElModifyingRevisions(const std::vector<rev::DBID>& commitIDs)
{
    return (
        filter::Attr(CAT_RD_EL).defined() &&
        filter::Geom::defined() &&
        filter::CommitAttribute(ACTION).in(
            {OBJECT_MODIFIED, GROUP_MODIFY_ATTRIBUTES}) &&
        filter::CommitAttr::id().in(commitIDs));
}

int64_t countLengthOfChangedRdElements(
    const rev::RevisionsGateway& gateway,
    const std::vector<rev::DBID>& commitIds,
    const geolib3::MultiPolygon2& polygons,
    const RevisionFilterGetter& filterGetter,
    const RdElRevisionCounter& lengthCounter,
    const std::string& attribute)
{
    const auto geosMultiPolygon = polygons.polygonsNumber() ?
        geolib3::internal::geolib2geosGeometry(polygons) :
        nullptr;

    const auto filter = filterGetter(commitIds);
    const auto revisions = gateway.reader().loadRevisions(filter);

    const auto prevRevisions = loadPreviousRevisions(gateway, revisions);

    int64_t result = 0;
    for (const auto& revision: revisions) {
        if (isPedestrianFunctionalClass(revision)) {
            continue;
        }

        const auto revisionGeom = common::Geom(revision.data().geometry.value());
        if (geosMultiPolygon && !geosMultiPolygon->intersects(revisionGeom.geosGeometryPtr())) {
            continue;
        }

        const auto attributeChangeType = getAttributeChangeType(
            revision,
            prevRevisions,
            attribute);
        result += lengthCounter(revision, attributeChangeType);
    }
    return result;
}

} // namespace maps::wiki::tasks::involvement
