#include "edit_options.h"

#include "check_permissions.h"
#include "configs/categories.h"
#include "configs/categories_strings.h"
#include "configs/config.h"
#include "exception.h"
#include "objects/junction.h"
#include "objects/linear_element.h"
#include "objects/relation_object.h"
#include "objects_cache.h"
#include "topo_storage.h"

#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/slave_role.h>
#include <yandex/maps/wiki/topo/cache.h>
#include <maps/wikimap/mapspro/libs/acl/include/exception.h>

namespace maps::wiki {

namespace {
const StringSet CANT_DELETE_CATEGORIES =
    {
        CATEGORY_TRANSPORT_THREAD_STOP
    };

const std::map<std::string, StringSet> CANT_DELETE_CATEGORIES_WITH_SLAVES_ROLES =
    {
        {CATEGORY_TRANSPORT_METRO_STATION, StringSet{ROLE_ASSIGNED_THREAD_STOP}},
        {CATEGORY_TRANSPORT_STOP, StringSet{ROLE_ASSIGNED_THREAD_STOP}},
        {CATEGORY_TRANSPORT_WATERWAY_STOP, StringSet{ROLE_ASSIGNED_THREAD_STOP}},
        {CATEGORY_RD, StringSet{ROLE_ASSOCIATED_WITH}}
    };

bool
isLastContour(const GeoObject* obj)
{
    const auto& contourDefs = cfg()->editor()->contourObjectsDefs();
    if (contourDefs.partType(obj->categoryId()) !=
            ContourObjectsDefs::PartType::Contour) {
        return false;
    }
    const auto& contourDef = contourDefs.contourDef(obj->categoryId());
    auto masterRelations = obj->masterRelations().range(contourDef.contour.roleId);
    RelativesLimit relativesLimit{ 1, RelativesLimit::PerRole};
    for (const auto& masterRelation : masterRelations) {
        auto* contourObject = masterRelation.relative();
        auto slaveRelations = contourObject->
            slaveRelations().range(contourDef.contour.roleId, relativesLimit);
        if (slaveRelations && slaveRelations->size() ==  1) {
            return true;
        }
    }
    return false;
}

bool
canDeleteWithSlaves(const GeoObject* obj)
{
    auto it = CANT_DELETE_CATEGORIES_WITH_SLAVES_ROLES.find(obj->categoryId());
    if (it == CANT_DELETE_CATEGORIES_WITH_SLAVES_ROLES.end()) {
        return true;
    }
    return obj->slaveRelations().range(it->second).empty();
}

bool
isCondWithVR(const GeoObject* obj)
{
    if (obj->categoryId() != CATEGORY_COND) {
        return false;
    }
    const auto& attrs = obj->attributes();
    return std::any_of(attrs.begin(), attrs.end(),
        [](const auto& attr) {
            return
                attr.id().starts_with(CATEGORY_VEHICLE_RESTRICTION) &&
                !attr.value().empty();
        });
}

} // namespace

bool
canEdit(const GeoObject* obj, TUid userId) try {
    if (obj->isDeleted()) {
        return false;
    }
    if (obj->categoryId() == CATEGORY_PEDESTRIAN_REGION) {
        return obj->attributes().value(ATTR_PEDESTRIAN_REGION_TASK_ID).empty();
    }
    if (!userId) {
        return true;
    }
    auto& txn = obj->cache().workCore();
    acl::CheckContext globalContext(
        userId,
        std::vector<std::string>{},
        txn,
        {acl::User::Status::Active});
    if (obj->categoryId() == CATEGORY_INDOOR_LEVEL) {
        auto objectGeomContext = globalContext.narrow({obj->geom().wkb()}, txn);
        acl::SubjectPath aclPath(SERVICE_ACL_BASE);
        if (!aclPath(obj->category().aclPermissionName())
                (STR_EDIT_PERMISSION)
                (STR_ATTRIBUTES_PERMISSION).isAllowed(objectGeomContext) &&
            !aclPath(obj->category().aclPermissionName())
                (STR_EDIT_PERMISSION)
                (STR_GEOMETRY_PERMISSION).isAllowed(objectGeomContext))
        {
            return false;
        }
    } else if (isCondWithVR(obj)) {
        auto junctions = obj->slaveRelations().range(ROLE_VIA);
        ASSERT(!junctions.empty());
        const auto* junction = junctions.begin()->relative();
        auto objectGeomContext = globalContext.narrow({junction->geom().wkb()}, txn);
        acl::SubjectPath aclPath(SERVICE_ACL_BASE);
        auto attrsAclPath = aclPath(obj->category().aclPermissionName())
            (STR_EDIT_PERMISSION)
            (STR_ATTRIBUTES_PERMISSION);
        for (const auto& attr : obj->attributes()) {
            if (!attr.id().starts_with(CATEGORY_VEHICLE_RESTRICTION) ||
                attr.value().empty())
            {
                continue;
            }
            if (!attrsAclPath(attr.id()).isAllowed(objectGeomContext)) {
                return false;
            }
        }
    }
    CheckPermissions(userId, txn).checkPermissionsToEditObject(obj);
    return true;
} catch (const acl::AccessDenied&) {
    return false;
}

bool
canDelete(const GeoObject* obj, TUid userId)
{
    ASSERT(obj);
    if (obj->isDeleted()) {
        return false;
    }
    if (CANT_DELETE_CATEGORIES.count(obj->categoryId())) {
        return false;
    }
    if (!canDeleteWithSlaves(obj)) {
        return false;
    }
    const auto masters = obj->masterRelations().range();
    if (!masters.empty()) {
        if (isLastContour(obj)) {
            return false;
        }
        std::unique_ptr<CheckPermissions> checkPermissions;
        if (userId) {
            checkPermissions = make_unique<CheckPermissions>(userId, obj->cache().workCore());
        }
        for (const auto& masterInfo : obj->masterRelations().range()) {
            const auto& masterCategory = masterInfo.relative()->category();
            const SlaveRole& role = masterCategory.slaveRole(masterInfo.roleId());
            if (!role.allowDelete() &&
                (role.maxOccurs() == 1 ||
                    (!checkPermissions || !checkPermissions->allowToIgnoreRestrictions(masterCategory))))
            {
                return false;
            }
        }
    }
    if (userId && isCondWithVR(obj)) {
        return canEdit(obj, userId);
    }
    if (!is<Junction>(obj)) {
        return true;
    }
    const Junction* j = as<const Junction>(obj);
    auto valency = j->valency();
    if (!valency) {
        return true;
    }
    if (valency != 2) {
        DEBUG() << BOOST_CURRENT_FUNCTION <<
        " Can't merge because junction " << obj->id() <<
        " valency is: " << valency;
        return false;
    }
    auto topoRelations = j->topologicalRelations();
    auto it = topoRelations.begin();
    auto firstElement = it->relative();
    auto secondElement = (++it)->relative();
    return as<const LinearElement>(firstElement)->canMerge(
        as<const LinearElement>(secondElement));
}

} // namespace maps::wiki
