#include "gettile.h"

#include "maps/wikimap/mapspro/services/editor/src/actions/magic_strings.h"
#include "maps/wikimap/mapspro/services/editor/src/branch_helpers.h"
#include "maps/wikimap/mapspro/services/editor/src/srv_attrs/calc.h"
#include "maps/wikimap/mapspro/services/editor/src/srv_attrs/registry.h"
#include "maps/wikimap/mapspro/services/editor/src/configs/config.h"

#include <maps/wikimap/mapspro/libs/acl/include/deleted_users_cache.h>
#include <maps/wikimap/mapspro/libs/dbutils/include/parser.h>
#include <maps/wikimap/mapspro/libs/views/include/query_builder.h>
#include <yandex/maps/wiki/filters/stored_expression.h>
#include <yandex/maps/wiki/configs/editor/category_template.h>
#include <yandex/maps/wiki/configs/editor/interactivity.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/topology_groups.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <maps/libs/tile/include/utils.h>

namespace maps::wiki {

namespace {
UserAclDataCache g_userAclDataCache;

const double TILE_OVERLAP = 0.5;//Mercator
const std::string STR_CONTOUR_ID_FIELD = "contour_id";
const std::string SQL_HOTSPOT_LABEL_CASE = \
    " CASE WHEN s.service_attrs ? '" + srv_attrs::SRV_HOTSPOT_LABEL + "' THEN " \
    " s.service_attrs->'" + srv_attrs::SRV_HOTSPOT_LABEL + "' ELSE" \
    " s.service_attrs->'" + srv_attrs::SRV_SCREEN_LABEL + "' END as \""
    + srv_attrs::SRV_HOTSPOT_LABEL +"\",";
const std::string EXCLUDE_INDOOR_CLAUSE = " AND (s.service_attrs IS NULL OR NOT s.service_attrs ? 'srv:indoor_level_id')";

const TZoom MIN_ZOOM_TO_IGNORE_ZMIN = 14;

std::string
partsClause(const geolib3::Point2& point1, const geolib3::Point2& point2)
{
    if (!cfg()->viewPartsEnabled()) {
        return {};
    }

    std::stringstream ss;
    ss.precision(DOUBLE_FORMAT_PRECISION);
    ss << point1.x() << "," << point1.y() << "," << point2.x() << "," << point2.y();
    return " AND s.part=ANY(get_parts(" + ss.str() + "))";
}

std::string
formatCategoriesRangeQuery(const std::optional<StringSet>& categoriesSet)
{
    if (!categoriesSet) {
        return std::string();
    }
    ASSERT(!categoriesSet->empty());

    return " AND domain_attrs ?| ARRAY['" +
        common::join(*categoriesSet, plainCategoryIdToCanonical, "','") + "']";
}


std::string
indoorClause(TOid indoorLevelId)
{
    if (!indoorLevelId) {
        return EXCLUDE_INDOOR_CLAUSE;
    }

    return " AND s.service_attrs @> hstore('srv:indoor_level_id', '" +
        std::to_string(indoorLevelId) + "')";
}

std::optional<StringSet>
filterCategoriesByGeomType(
    const std::optional<StringSet>& categoriesSet,
    const std::string& geomType)
{
    if (!categoriesSet) {
        return categoriesSet;
    }
    const auto* cfgEditor = cfg()->editor();
    const auto& cfgCategories = cfgEditor->categories();
    StringSet filtered;
    for (const auto& catId : *categoriesSet) {
        const auto& cat = cfgCategories[catId];
        const auto& tmpl = cat.categoryTemplate();
        if (tmpl.isGeometryTypeValid(geomType)) {
            filtered.insert(catId);
        }
    }
    return filtered;
}

std::optional<StringSet>
filterNonTopoCategories(const std::optional<StringSet>& categoriesSet)
{
    if (!categoriesSet) {
        return categoriesSet;
    }
    const auto& topoGroups = cfg()->editor()->topologyGroups();
    StringSet filtered;
    for (const auto& catId : *categoriesSet) {
        if (!topoGroups.findGroup(catId)) {
            filtered.insert(catId);
        }
    }
    return filtered;
}

} // namespace

std::string GetObjectsByTile::TileQueryParams::bboxStr() const
{
    std::stringstream ss;
    ss.precision(DOUBLE_FORMAT_PRECISION);
    ss
        << "ST_SetSRID(ST_MakeBox2D("
        << "ST_Point(" << point1.x() << "," << point1.y() << "),"
        << "ST_Point(" << point2.x() << "," << point2.y() << ")), 3395)";
    return ss.str();
}

std::string GetObjectsByTile::TileQueryParams::zoomFilter(
    const std::string& zminName,
    const std::string& zmaxName) const
{
    return
        zminName + " <= " + std::to_string(zminLimit) +
        " AND " +
        std::to_string(zmaxLimit) + " <= " + zmaxName;
}


GetObjectsByTile::GetObjectsByTile(const Request& request)
    : controller::BaseController<GetObjectsByTile>(BOOST_CURRENT_FUNCTION)
    , request_(request)
    , categoriesSet_(
        request_.categoriesRangeList
        ? (request_.categoriesRangeList->empty()
            ? StringSet()
            : removeUndefinedCategories(split<StringSet>(*request_.categoriesRangeList, ',')))
        : std::optional<StringSet>()
    )
{
    tile::Tile tile(
        tile::TileCoord(request_.tileX, request_.tileY), request_.zoom);
    tile::RealBox bbox = tile.realBox();
    ltTile_ = bbox.lt().toMapCS();
    rbTile_ = bbox.rb().toMapCS();
    tileGeodetic_ = common::mercatorToGeodetic(ltTile_.x(), ltTile_.y());
    tileQueryParams_ = getTileQueryParams();
}

std::string
GetObjectsByTile::printRequest() const
{
    std::stringstream ss;
    ss << " tileX: " << request_.tileX;
    ss << " tileY: " << request_.tileY;
    ss << " zoom: " << request_.zoom;
    ss << " user: " << request_.uid;
    ss << " object-id: " << request_.objectId;
    if (request_.categoriesRangeList && categoriesSet_) {
        ss << " categories[" << *request_.categoriesRangeList
        << "(" << categoriesSet_->size() << ")]";
    }
    ss << " token: " << request_.dbToken;
    ss << " branch: " << request_.branchId;
    ss << " dead-ends: " << request_.deadEnds;
    ss << " ignore-zmax: " << (request_.zMaxPolicy == ZMaxPolicy::Ignore);
    ss << " ignore-zmin: " << (request_.zMinPolicy == ZMinPolicy::Ignore);
    return ss.str();
}

void
GetObjectsByTile::control()
{
    if (!cfg()->deletedUsersCache().isAllowed(request_.uid)) {
        return;
    }
    const auto editorCfg = cfg()->editor();

    WIKI_REQUIRE(request_.zMinPolicy == ZMinPolicy::Compare ||
        ( request_.zMinPolicy == ZMinPolicy::Ignore &&
            request_.zoom >= MIN_ZOOM_TO_IGNORE_ZMIN && categoriesSet_ &&
            categoriesSet_->size() == 1),
        ERR_BAD_REQUEST,
        "Malformed request.");

    userAclData_ = g_userAclDataCache.get(request_.uid, request_.dbToken);
    ASSERT(userAclData_);
    if (categoriesSet_) {
        categoriesSet_ = userAclData_->filterRequestCategories(*categoriesSet_);
    }

    if (editorCfg->minGeneralizationZmin() > request_.zoom ||
        (categoriesSet_ && categoriesSet_->empty())) {
        return;
    }
    if (request_.expressionId) {
        auto workCore = cfg()->poolCore().slaveTransaction(request_.dbToken);
        auto expression = filters::StoredExpression::load(*workCore, *request_.expressionId);
        filterViewSql_ = " AND (" + expression.parsed().viewFilterClause(*workCore, "s.") + ") ";
    }
    auto work = BranchContextFacade::acquireWorkReadViewOnly(
        request_.branchId, request_.dbToken);
    if (request_.objectId) {
        loadContourLinearElements(*work);
        return;
    }
    loadArealObjects(*work);

    //! For topologycal there is no sense limit hotspots by zmax, so only zmin is used
    StringSet linearCategoriesToLoad;
    size_t totalTopoCategories = 0;
    for (const auto& topoGrp : editorCfg->topologyGroups()) {
        TZoom highlightZoom = topoGrp.interactivity().highlightingZoom().zmin();
        totalTopoCategories += topoGrp.linearElementsCategories().size();
        if (request_.zMinPolicy == ZMinPolicy::Compare && request_.zoom < highlightZoom) {
            continue;
        }
        for (const auto& category : topoGrp.linearElementsCategories()) {
            if (!categoriesSet_
                || categoriesSet_->find(category) != categoriesSet_->end()) {
                    linearCategoriesToLoad.insert(category);
            }
        }
    }
    const auto& nonTopoLinearCategories =
        filterNonTopoCategories(
            filterCategoriesByGeomType(categoriesSet_, Geom::geomTypeNameLine));
    if (nonTopoLinearCategories) {
        linearCategoriesToLoad.insert(
            nonTopoLinearCategories->begin(),
            nonTopoLinearCategories->end());
    }
    if (!linearCategoriesToLoad.empty()) {
        loadLinearElements(*work, linearCategoriesToLoad);
    }
    loadPointObjects(*work);

    if (request_.indoorLevelId) {
        loadArealObjects(*work, request_.indoorLevelId);
        loadLineObjects(*work, request_.indoorLevelId);
        loadPointObjects(*work, request_.indoorLevelId);
    }
}

GetObjectsByTile::TileQueryParams
GetObjectsByTile::getTileQueryParams() const
{
    return TileQueryParams{
        .point1 = geolib3::Point2(
            ltTile_.x() - TILE_OVERLAP,
            rbTile_.y() - TILE_OVERLAP),
        .point2 = geolib3::Point2(
            rbTile_.x() + TILE_OVERLAP,
            ltTile_.y() + TILE_OVERLAP),
        .zminLimit = (request_.zMinPolicy == ZMinPolicy::Ignore ? MAX_ZOOM : request_.zoom),
        .zmaxLimit = (request_.zMaxPolicy == ZMaxPolicy::Ignore ? (TZoom)0 : request_.zoom)
    };
}

void
GetObjectsByTile::loadContourLinearElements(Transaction& work)
{
    WIKI_REQUIRE(categoriesSet_ && categoriesSet_->size(),
        ERR_BAD_REQUEST, "Linear element category required");
    const std::string& linearElementCategory = *categoriesSet_->begin();
    WIKI_REQUIRE(cfg()->editor()->contourObjectsDefs().partType(linearElementCategory) ==
        ContourObjectsDefs::PartType::LinearElement,
        ERR_BAD_REQUEST, linearElementCategory << " is not contour part category.");;

    views::QueryBuilder qb(request_.branchId);
    qb.selectFields(
        "s.id, s.commit_id, s.the_geom, "
        " hstore_to_array(s.domain_attrs) as domain_attrs, "
        + SQL_HOTSPOT_LABEL_CASE +
        " akeys(s.domain_attrs) as attrs,"
        " s.service_attrs, s.zmin, s.zmax,"
        " r_next.master_id as " + STR_CONTOUR_ID_FIELD +
        ", " + SQL_EXTRACT_FT_TYPE_ID_PRETABLE + "s." + SQL_EXTRACT_FT_TYPE_ID_POSTTABLE);
    qb.fromTable(views::TABLE_OBJECTS_R, "r", "r_next");
    qb.fromTable(views::TABLE_OBJECTS_L, "s");
    qb.whereClause(
        " r.master_id = " + std::to_string(request_.objectId) +
        " AND r.slave_id = r_next.master_id AND r_next.slave_id = s.id"
        " AND s.domain_attrs ? '" + plainCategoryIdToCanonical(linearElementCategory) + "'"
        " AND ST_Intersects(" + tileQueryParams_.bboxStr() + ", s.the_geom)"
        " AND " + tileQueryParams_.zoomFilter("s.zmin", "s.zmax") + " "
        + partsClause(tileQueryParams_.point1, tileQueryParams_.point2)
        + filterViewSql_);
    auto rLines = work.exec(qb.query());
    collectTileObjects(rLines, {STR_CONTOUR_ID_FIELD});
}

void
GetObjectsByTile::loadLinearElements(
    Transaction& work,
    const std::optional<StringSet>& categoriesSet)
{
    views::QueryBuilder qb(request_.branchId);
    qb.selectFields(
        "id, commit_id, the_geom, "
        " hstore_to_array(domain_attrs) as domain_attrs, "
        + SQL_HOTSPOT_LABEL_CASE +
        " akeys(domain_attrs) as attrs" +
        ", " + SQL_EXTRACT_FT_TYPE_ID);
    qb.fromTable(views::TABLE_OBJECTS_L, "s");
    qb.whereClause(
        " ST_Intersects(" + tileQueryParams_.bboxStr() + ", the_geom)"
        " AND " + tileQueryParams_.zoomFilter("zmin", "zmax") + " "
        + formatCategoriesRangeQuery(categoriesSet)
        + EXCLUDE_INDOOR_CLAUSE
        + partsClause(tileQueryParams_.point1, tileQueryParams_.point2)
        + filterViewSql_);
    auto rLines = work.exec(qb.query());
    collectTileObjects(rLines);
}

void
GetObjectsByTile::loadLineObjects(
    Transaction& work, TOid indoorLevelId)
{
    const auto& lineCategories =
        filterCategoriesByGeomType(categoriesSet_, Geom::geomTypeNameLine);
    if (lineCategories && lineCategories->empty()) {
        return;
    }
    views::QueryBuilder qb(request_.branchId);
    qb.selectFields(
        "id, commit_id, the_geom, "
        " hstore_to_array(domain_attrs) as domain_attrs, "
        + SQL_HOTSPOT_LABEL_CASE +
        " akeys(domain_attrs) as attrs" +
        ", " + SQL_EXTRACT_FT_TYPE_ID);
    qb.fromTable(views::TABLE_OBJECTS_L, "s");
    qb.whereClause(
        " ST_Intersects(" + tileQueryParams_.bboxStr() + ", the_geom)"
        " AND " + tileQueryParams_.zoomFilter("zmin", "zmax") + " "
        + formatCategoriesRangeQuery(lineCategories)
        + indoorClause(indoorLevelId)
        + partsClause(tileQueryParams_.point1, tileQueryParams_.point2)
        + filterViewSql_);
    auto rLines = work.exec(qb.query());
    collectTileObjects(rLines);
}

void
GetObjectsByTile::loadArealObjects(
    Transaction& work, TOid indoorLevelId)
{
    const auto& arealCategories =
        filterCategoriesByGeomType(categoriesSet_, Geom::geomTypeNamePolygon);
    if (arealCategories && arealCategories->empty()) {
        return;
    }

    views::QueryBuilder qb(request_.branchId);
    qb.selectFields(
        "id, commit_id, the_geom, "
        " hstore_to_array(domain_attrs) as domain_attrs, "
        + SQL_HOTSPOT_LABEL_CASE +
        " akeys(domain_attrs) as attrs" +
        ", " + SQL_EXTRACT_FT_TYPE_ID);
    qb.fromTable(views::TABLE_OBJECTS_A, "s");
    qb.whereClause(
        " ST_Intersects(" + tileQueryParams_.bboxStr() + ", the_geom)"
        " AND " + tileQueryParams_.zoomFilter("zmin", "zmax") + " "
        + formatCategoriesRangeQuery(arealCategories)
        + indoorClause(indoorLevelId)
        + partsClause(tileQueryParams_.point1, tileQueryParams_.point2)
        + filterViewSql_);

    auto rAreal = work.exec(qb.query() + " ORDER BY area DESC");
    collectTileObjects(rAreal);
}


void
GetObjectsByTile::loadPointObjects(
    Transaction& work, TOid indoorLevelId)
{
    const auto& pointCategories =
        filterCategoriesByGeomType(categoriesSet_, Geom::geomTypeNamePoint);
    if (pointCategories && pointCategories->empty()) {
        return;
    }

    views::QueryBuilder qb(request_.branchId);
    qb.selectFields(
        "id, commit_id, the_geom, "
        " hstore_to_array(domain_attrs) as domain_attrs, "
        + SQL_HOTSPOT_LABEL_CASE +
        " akeys(domain_attrs) as attrs" +
        ", " + SQL_EXTRACT_FT_TYPE_ID +
        ", hstore_to_array(domain_attrs) as domain_attrs ");
    qb.fromTable(views::TABLE_OBJECTS_P, "s");

    auto whereClause =
        tileQueryParams_.bboxStr() + " && the_geom "
        " AND " + tileQueryParams_.zoomFilter("zmin", "zmax") + " "
        + indoorClause(indoorLevelId)
        + partsClause(tileQueryParams_.point1, tileQueryParams_.point2)
        + filterViewSql_;
    if (!request_.deadEnds) {
        whereClause +=
        " AND (service_attrs IS NULL "
        " OR service_attrs ?| ARRAY[ " +
        common::join(
            srv_attrs::ALL_RD_JC_IS_INVOLVE_IN_COND,
            [&](const auto& srvAttr) {
                return work.quote(srvAttr);
            },
            ",") +
        "] OR (NOT service_attrs ? '" + srv_attrs::SRV_VALENCY + "')"
        + " OR service_attrs->'" + srv_attrs::SRV_VALENCY + "' = ''"
        + " OR cast(service_attrs->'" + srv_attrs::SRV_VALENCY + "' as int) > 1)";
    }
    whereClause += formatCategoriesRangeQuery(pointCategories);

    qb.whereClause(std::move(whereClause));
    auto rPoint = work.exec(qb.query());
    collectTileObjects(rPoint);
}

void
GetObjectsByTile::collectTileObjects(
    const pqxx::result& result,
    const StringSet& customFields/* = StringSet()*/)
{
    const auto* editorCfg = cfg()->editor();
    DEBUG() << BOOST_CURRENT_FUNCTION << " Num rows to collect: " << result.size();
    REQUIRE(userAclData_, "User ACL data not ready.");
    for (const auto& row : result) {
        if (!userAclData_->isAllowedObjectByAttributes(
            dbutils::parsePGHstore(row["domain_attrs"].c_str())))
        {
            continue;
        }
        auto tileObject = make_unique<TileObject>(
            row,
            request_.zoom,
            customFields,
            request_.simpificationPolicy);

        const auto& categoryTemplate =
            cfg()->editor()->categories()[tileObject->categoryId()].categoryTemplate();
        const auto& interactivity =
            editorCfg->interactivity(
                categoryTemplate.interactivityId());

        tileObject->setHotspot(
            make_unique<Hotspot>(tileGeodetic_,
                tileObject->id(),
                tileObject->geom(),
                request_.zoom,
                interactivity.hotspotSimplificationTolerance(),
                request_.hotspotWidth ? *request_.hotspotWidth : interactivity.hotspotWidth(),
                request_.polygonBufferPolicy)
            );
        result_->objects.emplace_back(std::move(tileObject));
    }
}

} // namespace maps::wiki
