#include <maps/wikimap/mapspro/services/social/src/libs/feedback-actions/tasks_pins.h>

#include <maps/infra/yacare/include/error.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/tile/include/geometry.h>
#include <maps/libs/tile/include/range.h>
#include <maps/libs/tile/include/tile.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/json_helpers.h>
#include <maps/wikimap/mapspro/libs/social/include/yandex/maps/wiki/social/feedback/gateway_ro.h>
#include <maps/wikimap/mapspro/libs/social_serv_serialize/include/jsonize_feedback_task.h>
#include <maps/wikimap/mapspro/services/social/src/libs/feedback/common.h>
#include <maps/wikimap/mapspro/services/social/src/libs/feedback-actions/presets.h>

#include <boost/range/algorithm_ext/erase.hpp>

namespace maps::wiki::socialsrv {

namespace sf = social::feedback;

namespace {

const int MIN_SCALE_TO_APPEND_MULTIPLE_TASKS = 16;
const double MIN_BOUNDS_TO_APPEND_MULTIPLE_TASKS = 0.000001;

bool coveredBy(const sf::TaskBrief& task, const tile::Tile& tile)
{
    auto x = task.position().x();
    auto y = task.position().y();
    auto mBox = tile.mercatorBox();
    return (
        x > mBox.lt().x() and x <= mBox.rb().x() and
            y < mBox.lt().y() and y >= mBox.rb().y());
}

std::vector<sf::TasksBrief> separateToPins(
    const sf::TasksBrief & tasks, const tile::Tile& tile)
{
    std::vector<sf::TasksBrief> pins;

    tile::TileRange tileRange(tile, tile.z() + 2u);
    for (const tile::Tile& tile: tileRange) {
        sf::TasksBrief pin;
        for (auto& task: tasks) {
            if (coveredBy(task, tile)) {
                pin.emplace_back(task);
            }
        }
        if (!pin.empty()) {
            pins.emplace_back(std::move(pin));
        }
    }
    return pins;
}

void summaryToJson(
    json::ArrayBuilder& builder,
    const sf::TasksBrief& tasks)
{
    auto unresolvedStr = "";

    std::map<sf::AgeType, std::map<std::string, int>> ageResolutionMatr;

    for (const auto& task : tasks) {
        auto age = task.ageType();
        if (task.resolved()) {
            auto resolutionVerdict = task.resolved()->resolution.verdict();
            std::string resolutionStr(toString(resolutionVerdict));
            ageResolutionMatr[age][resolutionStr]++;
        } else {
            ageResolutionMatr[age][unresolvedStr]++;
        }
    }

    auto summaryItemToJson = [&builder](
        int taskCount, sf::AgeType ageType, const std::string& resolution)
    {
        builder << [&](json::ObjectBuilder builder) {
            builder[jsids::TASK_COUNT] = taskCount;
            builder[jsids::AGE_TYPE] = std::string(toString(ageType));
            if (resolution.empty()) {
                builder[jsids::RESOLUTION] = json::null;
            } else {
                builder[jsids::RESOLUTION] = resolution;
            }
        };
    };

    for (const auto& [age, resolutionToCount] : ageResolutionMatr) {
        for (const auto& [resolution, count] : resolutionToCount) {
            summaryItemToJson(count, age, resolution);
        }
    }
}

int numberOfTasksViewedByUser(
    const sf::TasksBrief& tasks,
    social::TUid uid)
{
    return std::count_if(tasks.begin(), tasks.end(),
        [&](const sf::TaskBrief& task){
            return task.viewedBy().count(uid) > 0;
        });
}

void pinToJson(
    json::ObjectBuilder& builder,
    const sf::TasksBrief& tasks,
    const tile::Zoom& zoom,
    social::TUid uid,
    social::HasMore hasMore)
{
    builder[jsids::TASK_COUNT] = tasks.size();
    builder[jsids::TASK_VIEWED_COUNT] = numberOfTasksViewedByUser(tasks, uid);
    if (hasMore == social::HasMore::Yes) {
        builder[jsids::HAS_MORE] = true;
    }

    // Calculating geo bounding_box of tasks
    // and mean position of pin
    //
    std::vector<double> posX, posY;
    for (const auto& task : tasks) {
        auto posGeo = geolib3::mercator2GeoPoint(task.position());
        posX.push_back(posGeo.x());
        posY.push_back(posGeo.y());
    }

    builder[jsids::POSITION] = geolib3::geojson(
        geolib3::Point2(
            std::accumulate(posX.begin(), posX.end(), 0.) / tasks.size(),
            std::accumulate(posY.begin(), posY.end(), 0.) / tasks.size()
        )
    );

    const geolib3::BoundingBox bboxGeo = [&](){
        auto [minX, maxX] = std::minmax_element(posX.begin(), posX.end());
        auto [minY, maxY] = std::minmax_element(posY.begin(), posY.end());
        return geolib3::BoundingBox({*minX, *minY}, {*maxX, *maxY});
    }();

    builder[jsids::TASK_BOUNDS] << [&](json::ArrayBuilder builder) {
        builder << bboxGeo.minX();
        builder << bboxGeo.minY();
        builder << bboxGeo.maxX();
        builder << bboxGeo.maxY();
    };

    builder[jsids::SUMMARY] << [&](json::ArrayBuilder builder) {
        summaryToJson(builder, tasks);
    };

    bool boundsAreSmall = (
        bboxGeo.maxX() - bboxGeo.minX() < MIN_BOUNDS_TO_APPEND_MULTIPLE_TASKS &&
            bboxGeo.maxY() - bboxGeo.minY() < MIN_BOUNDS_TO_APPEND_MULTIPLE_TASKS
    );
    bool appendTasks = (
        zoom >= MIN_SCALE_TO_APPEND_MULTIPLE_TASKS
            || tasks.size() == 1
            || boundsAreSmall
    );

    if (appendTasks) {
        builder[jsids::TASKS] << [&](json::ArrayBuilder builder) {
            for (const auto& task: tasks) {
                builder << [&](json::ObjectBuilder builder) {
                    serialize::taskBriefToJson(builder, task, uid);
                };
            }
        };
    }
}

const int MIN_SCALE_TO_SHOW_RECOMENDATIONS_PINS = 16;

std::optional<sf::Types>
transformTypesNoRecommendations(
    const std::optional<sf::Types>& types,
    const tile::Zoom& zoom)
{
    if (zoom >= MIN_SCALE_TO_SHOW_RECOMENDATIONS_PINS) {
        return types;
    }

    auto typesNoRecomend = types == std::nullopt ? sf::allTypes() : *types;

    boost::remove_erase(typesNoRecomend, sf::Type::AddressExperiment);
    boost::remove_erase(typesNoRecomend, sf::Type::EntranceExperiment);

    return typesNoRecomend;
}

} // namespace

sf::TaskFilter makeTaskFilter(
    const acl_utils::FeedbackChecker& feedbackChecker,
    UserId uid,
    tile::Zoom zoom,
    std::optional<sf::Types> typesRaw,
    std::optional<sf::Workflows> workflows,
    std::optional<std::vector<std::string>> sources,
    std::optional<bool> hidden)
{
    /*
     * Idea here is not to show recommendations on small scales,
     * otherwise it'll be mess. The way we control it - by editing
     * types that we'll pass to task filter
    */
    auto types = transformTypesNoRecommendations(
        typesRaw,
        zoom
    );

    REQUIRE(!(types && types->empty()), yacare::errors::BadRequest() << "Empty types list.");
    REQUIRE(!(workflows && workflows->empty()), yacare::errors::BadRequest() << "Empty workflows list.");
    REQUIRE(!(sources && sources->empty()), yacare::errors::BadRequest() << "Empty sources list.");

    sf::TaskFilter filter;
    filter.duplicateHeadId(std::nullopt);

    filter.hidden(hidden);
    filter.types(std::move(types));
    filter.workflows(std::move(workflows));
    filter.sources(std::move(sources));

    if (!feedbackChecker.aclChecker().userHasPermission(uid, HIDDEN_TASK_PERMISSION)) {
        filter.hidden(false);
    }
    return filter;
}

void addFilterParams(
    sf::TaskFilter& filter,
    acl_utils::CachingAclChecker& aclChecker,
    UserId uid,
    const tile::Tile& tile,
    const std::optional<sf::UIFilterStatus>& uiFilterStatus,
    std::optional<sf::AgeTypes> ageTypes,
    std::optional<std::string> indoorLevel)
{
    /* There is no access rights check here.
     * The reason is to reduce database traffic.
     * Tasks in pins are only brief; so it is not big deal.
    */
    REQUIRE(tile.z() >= MIN_SCALE_TO_SHOW_PINS, yacare::errors::BadRequest() << "Zoom is too large.");
    REQUIRE(!(ageTypes && ageTypes->empty()), yacare::errors::BadRequest() << "Empty age-types list.");

    filter.boxBoundary(tile::mercatorBBox(tile));
    filter.duplicateHeadId(std::nullopt);

    setStatusForPins(filter, uiFilterStatus, tile.z());
    filter.ageTypes(std::move(ageTypes));
    filter.indoorLevel(std::move(indoorLevel));

    if (!aclChecker.userHasPermission(uid, INTERNAL_CONTENT_TASK_PERMISSION)) {
        filter.internalContent(false);
    }
    if (!aclChecker.userHasPermission(uid, PROCESSING_LVL1_PERMISSION)) {
        filter.processingLvls({{sf::ProcessingLvl::Level0}});
    }
}

sf::TaskFilter makeTaskFilter(
    acl_utils::CachingAclChecker& aclChecker,
    UserId uid,
    const tile::Tile& tile,
    const social::feedback::Preset& preset,
    const social::TId aoiId,
    const std::optional<std::string>& indoorLevel,
    const std::optional<sf::UIFilterStatus>& uiFilterStatus)
{
    auto filter = assignedPresetFilter(preset, aoiId);
    addFilterParams(
        filter,
        aclChecker,
        uid,
        tile,
        uiFilterStatus,
        std::nullopt, // all ageTypes
        indoorLevel);

    return filter;
}

std::string getPinsJsonString(
    const sf::TasksBriefResult& result,
    UserId uid,
    const tile::Tile& tile)
{
    std::vector<sf::TasksBrief> pins;
    if (result.hasMore == social::HasMore::Yes) {
        pins.emplace_back(result.tasks);
    } else {
        pins = separateToPins(result.tasks, tile);
    }

    json::Builder builder;
    builder << [&](json::ArrayBuilder builder) {
        for (const auto& pin: pins) {
            builder << [&](json::ObjectBuilder builder) {
                pinToJson(builder, pin, tile.z(), uid, result.hasMore);
            };
        }
    };
    return builder.str();
}

} // namespace maps::wiki::socialsrv
