#include "entrance_heuristics.h"

#include "consts.h"
#include "traits.h"
#include "../revision/building.h"
#include "../revision/entrance.h"

#include <maps/libs/log8/include/log8.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/distance.h>
#include <yandex/maps/wiki/geom_tools/conversion.h>
#include <yandex/maps/wiki/social/feedback/attribute_names.h>
#include <yandex/maps/wiki/social/feedback/task_filter.h>

#include <maps/libs/chrono/include/time_point.h>

#include <algorithm>
#include <stdexcept>

namespace maps::wiki::schedule_feedback {

namespace sf = social::feedback;
namespace sfa = social::feedback::attrs;

namespace {

const uint64_t MAX_ALLOWED_ENTR_NUMBER = 20;
const double EXISTING_ENTR_VICINITY = 15.; // m
const double EXISTING_FEEBACK_VICINITY = 10.; // m
const double MAX_DIST_TO_BUILDING = 20.; // m

std::optional<EntranceFb>
socialTaskToEntranceNoExcept(const sf::Task& task) try
{
    return socialTaskToEntrance(task);
} catch (...) {
    return std::nullopt;
}

} // namespace anonymous

EntranceFb socialTaskToEntrance(const sf::Task& task)
{
    ASSERT(isFbapiTask(task));
    ASSERT(isAddEntranceTask(task));

    if (task.objectId() == std::nullopt) {
        throw EntranceObjectIdNotExist();
    }

    const auto &attrs = task.attrs();
    if (!attrs.existCustom(sfa::ENTRANCE_NAME)) {
        throw ExtractEntranceNameError();
    }

    uint64_t entranceNumber;
    try {
        entranceNumber = std::stoull(attrs.getCustom(sfa::ENTRANCE_NAME));
    } catch (const std::logic_error&) {
        throw ExtractEntranceNameError();
    }

    return EntranceFb{
        task.id(),
        task.createdAt(),
        task.position(),
        *task.objectId(),
        entranceNumber
    };
}

bool entranceNumberIsTooBig(
    const EntranceFb& entrance,
    uint64_t maxAllowedNumber)
{
    return entrance.number > maxAllowedNumber;
}

bool entranceExistsInVicinity(
    const EntranceFb& entrance,
    double vicinityRadiusGeo,
    pqxx::transaction_base& coreReadTxn)
{
    auto bbox = geom_tools::makeVicinityBboxMerc(
        entrance.positionMerc, vicinityRadiusGeo);

    auto names = revisionEntranceNames(bbox, coreReadTxn);
    return names.count(std::to_string(entrance.number)) > 0;
}

bool isDuplicateWithPreviousSocialFeedback(
    const EntranceFb& entrance,
    sf::GatewayRO& gatewayRo)
{
    auto isDuplicateWithOld = [](const EntranceFb& cur, const EntranceFb& old) {
        return cur.feedbackId != old.feedbackId &&
               cur.createdAt >= old.createdAt &&
               cur.number == old.number;
    };

    sf::TaskFilter filter;

    filter.source(FBAPI);
    filter.type(sf::Type::Entrance);
    filter.objectId(entrance.addressObjectId);
    filter.boxBoundary(geom_tools::makeVicinityBboxMerc(
        entrance.positionMerc, EXISTING_FEEBACK_VICINITY));

    auto socialTasks = gatewayRo.tasksByFilter(filter);

    for (const auto& task : socialTasks) {
        auto socialEntrance = socialTaskToEntranceNoExcept(task);
        if (socialEntrance && isDuplicateWithOld(entrance, *socialEntrance)) {
            return true;
        }
    }

    return false;
}

bool farawayFromAnyBuilding(
    const EntranceFb& entrance,
    double maxDistToBuildingGeo,
    pqxx::transaction_base& coreReadTxn)
{
    auto bboxMerc = geom_tools::makeVicinityBboxMerc(
        entrance.positionMerc, maxDistToBuildingGeo);
    auto buildingsGeom = revisionBuildingsGeom(bboxMerc, coreReadTxn);

    double minDist = std::numeric_limits<double>::max();
    for (auto geom : buildingsGeom) {
        double distMerc = geolib3::distance(geom, entrance.positionMerc);
        double distGeo = geom_tools::correctMercDistanceToGeo(
            distMerc, entrance.positionMerc);
        minDist = std::min(minDist, distGeo);
    }

    return minDist > maxDistToBuildingGeo;
}

EntranceAction entranceTaskHeuristicsAction(
    const sf::Task& task,
    pqxx::transaction_base& coreReadTxn,
    sf::GatewayRO& gatewayRo)
{
    EntranceFb entrance;
    try {
        entrance = socialTaskToEntrance(task);
    } catch (const ExtractEntranceNameError&) {
        return EntranceRejectAction{EntranceRejectAction::Reason::NameNotInteger};
    } catch (const EntranceObjectIdNotExist&) {
        return EntranceNoneAction{};
    }

    if (entranceNumberIsTooBig(entrance, MAX_ALLOWED_ENTR_NUMBER)) {
        return EntranceRejectAction{EntranceRejectAction::Reason::NumberTooBig};
    }

    if (entranceExistsInVicinity(entrance, EXISTING_ENTR_VICINITY, coreReadTxn)) {
        return EntranceRejectAction{EntranceRejectAction::Reason::EntranceExistNearby};
    }

    if (isDuplicateWithPreviousSocialFeedback(entrance, gatewayRo)) {
        return EntranceRejectAction{EntranceRejectAction::Reason::DuplicateFeedback};
    }

    if (farawayFromAnyBuilding(entrance, MAX_DIST_TO_BUILDING, coreReadTxn)) {
        return EntranceRejectAction{EntranceRejectAction::Reason::FarawayFromBuilding};
    }

    return EntranceNoneAction{};
}

} // namespace maps::wiki::schedule_feedback
