#include "strong_connectivity_checks_common.h"

#include <yandex/maps/wiki/validator/categories.h>
#include <yandex/maps/wiki/validator/check.h>
#include <yandex/maps/wiki/common/string_utils.h>

#include <optional>
#include <regex>
#include <unordered_map>

namespace maps::wiki::validator::checks {

using categories::RD_EL;
using categories::RD_JC;
using categories::COND;
using categories::VEHICLE_RESTRICTION;

namespace {

const std::string RESIDENTIAL = "residential";
const std::regex REGEX_WEIGHT_SUFFIX = std::regex("\\-([0-9_]+)t$");

const int WEIGHTED_TRUCK_MAX_FC = 7;
const int GENERAL_TRUCK_MAX_FC = 10; // 8

// RD_EL --> RD_JC from which to try to move
using TruckRestrictedMoves = std::unordered_multimap<TId, TId>;

std::string getMessageKey(const Message& message)
{
    std::stringstream ss;
    ss << message.attributes().severity
       << '-'
       << common::join(message.revisionIds(), '-');
    return ss.str();
}

double getWeightFromMessage(const Message& message)
{
    const auto& description = message.attributes().description;
    std::smatch match;
    if (std::regex_search(description, match, REGEX_WEIGHT_SUFFIX)) {
        auto suffix = match.str(1);
        std::replace(suffix.begin(), suffix.end(), '_', '.');
        return std::stod(suffix);
    } else {
        return 0.0;
    }
}

bool compareMessages(const Message& lhs, const Message& rhs)
{
    // Message with less weight (which is more strict condition) should be saved.
    // Empty suffix generates zero weight, so message from common truck check
    // consumes any message from specific weight check
    return getWeightFromMessage(lhs) < getWeightFromMessage(rhs);
}

bool
isTruckWeightRestriction(
    const VehicleRestriction* vr,
    std::optional<double> truckWeight)
{
    if (!truckWeight) {
        return false;
    }

    if (vr->universalId() == RESIDENTIAL) {
        return true;
    }

    if (!common::isSet(common::AccessId::Truck, vr->accessId())) {
        return false;
    }

    const auto& vrParams = vr->vehicleRestrictionParameters();
    return vrParams
        && vrParams->maxWeightLimit
        && *(vrParams->maxWeightLimit) < *truckWeight;
}

void checkStrongConnectivityTrucks(
    CheckContext* context,
    int maxFc,
    std::optional<double> truckWeight,
    const std::string& descrSuffix)
{
    auto viewRdEl = context->objects<RD_EL>();

    TruckRestrictedMoves truckRestrictedMoves;
    context->objects<VEHICLE_RESTRICTION>().visit([&](const VehicleRestriction* vr) {
        if (!isTruckWeightRestriction(vr, truckWeight)) {
            return;
        }
        for (auto rdElId : vr->restrictsRoadElements()) {
            if (!viewRdEl.loaded(rdElId)) {
                continue;
            }
            auto rdEl = viewRdEl.byId(rdElId);
            truckRestrictedMoves.insert({rdElId, rdEl->startJunction()});
            truckRestrictedMoves.insert({rdElId, rdEl->endJunction()});
        }
        for (auto rdElId : vr->restrictsRoadElementsFrom()) {
            if (!viewRdEl.loaded(rdElId)) {
                continue;
            }
            auto rdEl = viewRdEl.byId(rdElId);
            truckRestrictedMoves.insert({rdElId, rdEl->startJunction()});
        }
        for (auto rdElId : vr->restrictsRoadElementsTo()) {
            if (!viewRdEl.loaded(rdElId)) {
                continue;
            }
            auto rdEl = viewRdEl.byId(rdElId);
            truckRestrictedMoves.insert({rdElId, rdEl->endJunction()});
        }
    });

    auto prohibitedContinuations = utils::findProhibitedContinuations(
        context, common::AccessId::Truck);

    auto truckRdElFilter = [&](const RoadElement* rdEl) -> bool
    {
        return rdEl->fc() <= maxFc
               && common::isSet(common::AccessId::Truck, rdEl->accessId())
               && (!truckWeight || !rdEl->residential())
               && truckRestrictedMoves.count(rdEl->id()) < 2;
    };

    auto truckRdElDirectionFilter = [&](const RoadElement* rdEl, TId startRdJcId) -> bool
    {
        auto iter = truckRestrictedMoves.find(rdEl->id());
        return iter == truckRestrictedMoves.end() ||
               truckRestrictedMoves.find(rdEl->id())->second != startRdJcId;
    };

    RoadNetwork roadNetwork(
        context,
        truckRdElFilter,
        prohibitedContinuations,
        RoadNetwork::Directed::Yes,
        [](const RoadElement*) { return false; },
        truckRdElDirectionFilter);

    checkStrongConnectivity(
        context,
        roadNetwork,
        Severity::Critical,
        descrSuffix,
        getMessageKey,
        compareMessages);
}

} // namespace

VALIDATOR_CHECK_PART( strong_connectivity_truck, weight_any, RD_EL, RD_JC, COND, VEHICLE_RESTRICTION )
{
    checkStrongConnectivityTrucks(context, GENERAL_TRUCK_MAX_FC, std::nullopt, "");
}

VALIDATOR_CHECK_PART( strong_connectivity_truck, weight_2t, RD_EL, RD_JC, COND, VEHICLE_RESTRICTION )
{
    checkStrongConnectivityTrucks(context, WEIGHTED_TRUCK_MAX_FC, 2.5, "-weight-2_5t");
}

VALIDATOR_CHECK_PART( strong_connectivity_truck, weight_3t, RD_EL, RD_JC, COND, VEHICLE_RESTRICTION )
{
    checkStrongConnectivityTrucks(context, WEIGHTED_TRUCK_MAX_FC, 3.5, "-weight-3_5t");
}

VALIDATOR_CHECK_PART( strong_connectivity_truck, weight_5t, RD_EL, RD_JC, COND, VEHICLE_RESTRICTION )
{
    checkStrongConnectivityTrucks(context, WEIGHTED_TRUCK_MAX_FC, 5, "-weight-5t");
}

VALIDATOR_CHECK_PART( strong_connectivity_truck, weight_6t, RD_EL, RD_JC, COND, VEHICLE_RESTRICTION )
{
    checkStrongConnectivityTrucks(context, WEIGHTED_TRUCK_MAX_FC, 6, "-weight-6t");
}

VALIDATOR_CHECK_PART( strong_connectivity_truck, weight_8t, RD_EL, RD_JC, COND, VEHICLE_RESTRICTION )
{
    checkStrongConnectivityTrucks(context, WEIGHTED_TRUCK_MAX_FC, 8, "-weight-8t");
}

VALIDATOR_CHECK_PART( strong_connectivity_truck, weight_12t, RD_EL, RD_JC, COND, VEHICLE_RESTRICTION )
{
    checkStrongConnectivityTrucks(context, WEIGHTED_TRUCK_MAX_FC, 12, "-weight-12t");
}

VALIDATOR_CHECK_PART( strong_connectivity_truck, weight_15t, RD_EL, RD_JC, COND, VEHICLE_RESTRICTION )
{
    checkStrongConnectivityTrucks(context, WEIGHTED_TRUCK_MAX_FC, 15, "-weight-15t");
}

} // namespace maps::wiki::validator::checks
