#include <maps/wikimap/mapspro/libs/acl/include/check_context.h>
#include <maps/wikimap/mapspro/libs/acl/include/aclgateway.h>
#include <maps/wikimap/mapspro/libs/acl/include/exception.h>

#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>

#include <unordered_map>

namespace maps::wiki::acl {

namespace {

std::unordered_multimap<std::string, ID>
getPathToAoiIds(const User& user, Transaction& work, std::set<User::Status> allowedUserStatuses)
{
    const auto userStatus = user.status();
    if (!allowedUserStatuses.count(userStatus)) {
        throw AccessDenied(user.uid()) << " is " << userStatus;
    }

    std::vector<ID> agentIds;
    agentIds.push_back(user.id());
    for (const auto& group : user.groups()) {
        agentIds.push_back(group.id());
    }

    std::string query = ""
        "WITH RECURSIVE"
        "  perm(id, path) AS"
        "    (SELECT id, name FROM acl.permission perm WHERE parent_id = 0"
        "       UNION ALL"
        "     SELECT next.id, perm.path || '/' || next.name"
        "       FROM perm, acl.permission next"
        "       WHERE next.parent_id = perm.id) "
        "SELECT path, aoi_id"
        "  FROM perm"
        "  JOIN acl.role_permission rp ON (perm.id = rp.permission_id)"
        "  JOIN acl.policy p USING (role_id)"
        "  WHERE agent_id in (" + common::join(agentIds, ',') + ")";

    const auto rows = work.exec(query);
    if (rows.empty()) {
        return {};
    }
    std::unordered_multimap<std::string, ID> pathToAoiIds;
    pathToAoiIds.reserve(rows.size());
    for (const auto& row : rows) {
        auto aoiId = row["aoi_id"].as<ID>();
        auto path = row["path"].as<std::string>();
        pathToAoiIds.emplace(path, aoiId);
    }
    return pathToAoiIds;
}

bool
isInside(const common::Geom& geom, const common::Geom& aoiGeom)
{
    if (geom.geometryTypeName() == common::Geom::geomTypeNameLine) {
        return geom->intersects(aoiGeom.geosGeometryPtr());
    } else if (geom.geometryTypeName() == common::Geom::geomTypeNamePoint) {
        return aoiGeom->contains(geom.geosGeometryPtr());
    } else if (geom.geometryTypeName() == common::Geom::geomTypeNamePolygon) {
        common::GeometryPtr diff(geom->difference(aoiGeom.geosGeometryPtr()));
        return diff && diff->getArea() / geom->getArea() <= 0.2;
    }
    return false;
}

} // namespace

CheckContext::CheckContext(
    UID uid,
    const std::vector<std::string>& wkbs,
    Transaction& work,
    std::set<User::Status> allowedUserStatuses)
    : uid_(uid)
    , isGlobal_(true)
    , aoiGeomsLoaded_(false)
{
    ACLGateway gw(work);
    init(gw.user(uid_), wkbs, work, std::move(allowedUserStatuses));
}

CheckContext::CheckContext(
    const User& user,
    const std::vector<std::string>& wkbs,
    Transaction& work,
    std::set<User::Status> allowedUserStatuses)
    : uid_(user.uid())
    , isGlobal_(true)
    , aoiGeomsLoaded_(false)
{
    init(user, wkbs, work, std::move(allowedUserStatuses));
}

void
CheckContext::init(
    const User& user,
    const std::vector<std::string>& wkbs,
    Transaction& work,
    std::set<User::Status> allowedUserStatuses)
{
    const auto pathToAoiIds = getPathToAoiIds(user, work, std::move(allowedUserStatuses));
    for (const auto& [path, aoiId] : pathToAoiIds) {
        if (aoiId == 0) {
            allowedPaths_.insert(path);
            pathToAoiIds_.erase(path);
        } else if (!allowedPaths_.count(path)) {
            pathToAoiIds_[path].insert(aoiId);
        }
    }
    if (!wkbs.empty()) {
        *this = narrow(wkbs, work);
    }
}

CheckContext::CheckContext(
    UID uid,
    std::set<std::string> allowedPaths)
    : uid_(uid)
    , allowedPaths_(std::move(allowedPaths))
    , isGlobal_(false)
{ }

CheckContext
CheckContext::narrow(const std::vector<std::string>& wkbs, Transaction& work)
{
    REQUIRE(isGlobal_, "Context already narrowed");

    if (!aoiGeomsLoaded_ && !wkbs.empty()) {
        loadAoiGeoms(work);
        aoiGeomsLoaded_ = true;
    }

    struct GeomInfo {
        common::Geom geom;
        std::map<ID, bool> isInsideByAoi;
    };

    std::vector<GeomInfo> geomInfos;
    geomInfos.reserve(wkbs.size());
    for (const auto& wkb : wkbs) {
        geomInfos.push_back({common::Geom(wkb), {}});
    }

    auto newAllowedPaths = allowedPaths_;
    if (!geomInfos.empty()) {
        for (const auto& pair : pathToAoiIds_) {
            const auto& path = pair.first;
            const auto& aoiIds = pair.second;

            auto isInsideSomeAoi = [&](GeomInfo& geomInfo)
            {
                for (ID aoiId : aoiIds) {
                    auto aoiGeomIt = aoiGeoms_.find(aoiId);
                    if (aoiGeomIt == aoiGeoms_.end()) {
                        continue;
                    }
                    if (!geomInfo.isInsideByAoi.count(aoiId)) {
                        geomInfo.isInsideByAoi[aoiId] =
                            isInside(geomInfo.geom, aoiGeomIt->second);
                    }
                    if (geomInfo.isInsideByAoi[aoiId]) {
                        return true;
                    }
                }
                return false;
            };

            if (std::all_of(geomInfos.begin(), geomInfos.end(), isInsideSomeAoi)) {
                newAllowedPaths.insert(path);
            }
        }
    }

    return CheckContext(uid_, std::move(newAllowedPaths));
}

CheckContext
CheckContext::inflate() const
{
    REQUIRE(isGlobal_, "Context already narrowed/inflated");
    auto newAllowedPaths = allowedPaths_;
    for (const auto& [path, _] : pathToAoiIds_) {
        newAllowedPaths.insert(path);
    }
    return CheckContext(uid_, std::move(newAllowedPaths));
}

void
CheckContext::loadAoiGeoms(Transaction& work)
{
    std::set<ID> aoiIds;
    for (const auto& pair : pathToAoiIds_) {
        aoiIds.insert(pair.second.begin(), pair.second.end());
    }
    if (aoiIds.empty()) {
        return;
    }

    namespace rf = revision::filters;
    revision::RevisionsGateway rg(work); // trunk
    auto revisions = rg.snapshot(rg.headCommitId()).objectRevisionsByFilter(
        rf::ObjRevAttr::isNotDeleted() &&
        rf::Geom::defined() &&
        rf::ObjRevAttr::objectId().in(aoiIds));

    for (const auto& rev : revisions) {
        REQUIRE(rev.data().geometry, "Revision " << rev.id() << " without geometry");
        aoiGeoms_.insert({rev.id().objectId(), common::Geom(*rev.data().geometry)});
    }
}

} // namespace maps::wiki::acl
