#include "social_utils.h"
#include "branch_helpers.h"
#include "geom.h"
#include "configs/config.h"
#include "configs/categories_strings.h"
#include "objects/object.h"
#include "configs/categories.h"
#include "objects/category_traits.h"
#include <maps/wikimap/mapspro/libs/views/include/query_builder.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/common/pg_utils.h>
#include <yandex/maps/wiki/common/string_utils.h>

#include <boost/lexical_cast.hpp>

namespace maps {
namespace wiki {

namespace {

enum class FromTable { ObjectsAreal, ContourObjectsGeom };

const StringSet IMPORTANT_CATEGORIES_FOR_BIG_AREA {
        CATEGORY_HYDRO,
        CATEGORY_HYDRO_FC,
        CATEGORY_HYDRO_FC_EL,
        CATEGORY_HYDRO_FC_JC,
        CATEGORY_VEGETATION,
        CATEGORY_VEGETATION_EL,
        CATEGORY_VEGETATION_FC,
        CATEGORY_VEGETATION_JC };

const StringSet IMPORTANT_CATEGORIES_IN_IMPORTANT_REGIONS {
        CATEGORY_HYDRO,
        CATEGORY_HYDRO_FC,
        CATEGORY_HYDRO_FC_EL,
        CATEGORY_HYDRO_FC_JC,
        CATEGORY_HYDRO_LN,
        CATEGORY_HYDRO_LN_EL,
        CATEGORY_HYDRO_LN_JC,
        CATEGORY_HYDRO_POINT,
        CATEGORY_RELIEF,
        CATEGORY_RELIEF_EL,
        CATEGORY_RELIEF_FC,
        CATEGORY_RELIEF_JC,
        CATEGORY_RELIEF_POINT,
        CATEGORY_VEGETATION,
        CATEGORY_VEGETATION_EL,
        CATEGORY_VEGETATION_FC,
        CATEGORY_VEGETATION_JC };

const StringSet DIFFALERT_IMPORTANT_MESSAGES {
    "feature-big-area-change",
    "feature-big-area-deleted" };

const std::string STR_CAT_AOI = plainCategoryIdToCanonical(CATEGORY_AOI);
const std::string STR_CAT_DIFFALERT_REGION = plainCategoryIdToCanonical(CATEGORY_DIFFALERT_REGION);

const geos::geom::Envelope ZERO_ENVELOPE {-10.0, -10.0, 10.0, 10.0};

const std::string CONTOUR_OBJECTS_IDS =
    "DISTINCT (SELECT ARRAY_AGG(SUBSTR(skeys,23))"
    " FROM skeys(a.service_attrs) WHERE skeys LIKE 'srv:is_part_of_region_%') AS ids";
const std::string CONTOUR_OBJECTS_WHERE_CLAUSE =
    "a.service_attrs ? 'srv:is_part_of_region'";

struct RegionCategoryIds
{
    std::string included;
    std::string excluded;
};

const std::vector<RegionCategoryIds> AOI_ORDERED_REGION_CATEGORIES = {
    { CATEGORY_AOI, "" }
};

const std::vector<RegionCategoryIds> DIFFALERT_ORDERED_REGION_CATEGORIES = {
    { CATEGORY_DIFFALERT_REGION, "" }
};

const std::vector<RegionCategoryIds> CONTOUR_OBJECTS_ORDERED_REGION_CATEGORIES = {
    { "", CATEGORY_AD_NEUTRAL }, // check without ad_neutral (skip included ad for optimization)
    { CATEGORY_AD_NEUTRAL, "" }  // skip check category ad
};

TOIds
pqxxResultToOidsFromArrays(const pqxx::result& r)
{
    TOIds ids;
    for (const auto& row : r) {
        std::string s = row[0].c_str();
        if (s.size() <= 2) {
            continue;
        }
        for (const auto& value : common::parseSqlArray(s)) {
            ids.insert(boost::lexical_cast<TOid>(value));
        }
    }
    return ids;
}

TOIds
pqxxResultToOids(const pqxx::result& r)
{
    TOIds ids;
    for (const auto& row : r) {
        ids.insert(row[0].as<TOid>());
    }
    return ids;
}

TOIds
pqxxResultToOids(const pqxx::result& r, FromTable fromTable)
{
    switch (fromTable) {
        case FromTable::ContourObjectsGeom:
            return pqxxResultToOidsFromArrays(r);
        case FromTable::ObjectsAreal:
            return pqxxResultToOids(r);
    }
}

views::QueryBuilder
makeQueryBuilder(const BranchContext& branchCtx, FromTable fromTable)
{
    views::QueryBuilder qb(branchCtx.branch.id());
    switch (fromTable) {
        case FromTable::ObjectsAreal:
            qb.fromTable(views::TABLE_OBJECTS_A, "a");
            break;
        case FromTable::ContourObjectsGeom:
            qb.fromTable(views::TABLE_CONTOUR_OBJECTS_GEOM, "a");
            break;
    }
    return qb;
}

std::string
regionClause(FromTable fromTable, const RegionCategoryIds& regionCategoryIds)
{
    StringVec clauses;
    if (fromTable == FromTable::ContourObjectsGeom) {
        clauses.push_back(CONTOUR_OBJECTS_WHERE_CLAUSE);
    }

    auto matchCategory = [&] (const auto& categoryId) {
        return "a.domain_attrs ? '" + plainCategoryIdToCanonical(categoryId) + "'";
    };

    if (!regionCategoryIds.included.empty()) {
        clauses.emplace_back(matchCategory(regionCategoryIds.included));
    }
    if (!regionCategoryIds.excluded.empty()) {
        clauses.emplace_back("(NOT " + matchCategory(regionCategoryIds.excluded) + ")");
    }

    ASSERT(!clauses.empty());
    return common::join(clauses, " AND ");
}

std::string
selectFields(FromTable fromTable)
{
    switch (fromTable) {
        case FromTable::ObjectsAreal:
            return "DISTINCT a.id";
        case FromTable::ContourObjectsGeom:
            return CONTOUR_OBJECTS_IDS;
    }
}

TOIds
getRegionIdsByEnvelope(
    const BranchContext& branchCtx,
    FromTable fromTable,
    const RegionCategoryIds& regionCategoryIds,
    const geos::geom::Envelope& env)
{
    const auto& envelope = env.isNull()
        ? ZERO_ENVELOPE
        : env;

    // expand envelope because polygon without interior (e.g. created
    // from envelope with zero area) does not intersect with anything.
    auto expanded = envelope;
    expanded.expandBy(CALCULATION_TOLERANCE);

    std::stringstream whereClause;
    whereClause.precision(DOUBLE_FORMAT_PRECISION);
    whereClause
        << "ST_Intersects(the_geom, "
            << "ST_SetSRID(ST_MakeBox2D("
            << "ST_Point(" << expanded.getMinX() << "," << expanded.getMinY() << "),"
            << "ST_Point(" << expanded.getMaxX() << "," << expanded.getMaxY() << ")), 3395)"
        << ") AND " + regionClause(fromTable, regionCategoryIds);

    auto qb = makeQueryBuilder(branchCtx, fromTable);
    switch (fromTable) {
        case FromTable::ObjectsAreal:
            qb.selectFields("id");
            if (cfg()->viewPartsEnabled()) {
                whereClause
                    << " AND a.part=ANY(get_parts("
                    << expanded.getMinX() << "," << expanded.getMinY() << ","
                    << expanded.getMaxX() << "," << expanded.getMaxY() << "))";
            }
            break;
        case FromTable::ContourObjectsGeom:
            qb.selectFields(CONTOUR_OBJECTS_IDS);
            break;
    }
    qb.whereClause(whereClause.str());

    auto r = branchCtx.txnView().exec(qb.query());
    return pqxxResultToOids(r, fromTable);
}

TOIds
getRegionIdsByGeoms(
    const BranchContext& branchCtx,
    FromTable fromTable,
    const RegionCategoryIds& regionCategoryIds,
    const std::vector<Geom>& geoms)
{
    ASSERT(!geoms.empty());

    auto& work = branchCtx.txnView();
    std::string geomsQuery;
    for (const auto& geom : geoms) {
        ASSERT(!geom.isNull());
        geomsQuery += (geomsQuery.empty() ? "" : ",");
        geomsQuery += "(ST_GeomFromWKB("
            "'" + work.esc_raw(geom.wkb()) + "', 3395))";
    }

    auto qb = makeQueryBuilder(branchCtx, fromTable);
    qb.with("ggg", "the_geom", "VALUES " + geomsQuery);
    qb.selectFields(selectFields(fromTable));
    qb.fromWith("ggg");
    qb.whereClause(
        "ST_Intersects(a.the_geom, ggg.the_geom)"
        " AND " + regionClause(fromTable, regionCategoryIds));
    return pqxxResultToOids(work.exec(qb.query()), fromTable);
}

TOIds
getRegionIdsByComplexObjectsCategory(
    const BranchContext& branchCtx,
    FromTable fromTable,
    const RegionCategoryIds& regionCategoryIds,
    TOIds&& objectIds,
    const std::string& categoryId)
{
    ASSERT(!objectIds.empty());
    const auto& categories = cfg()->editor()->categories();
    const auto& category = categories[categoryId];
    if (!category.complex()) {
        return {};
    }

    StringVec geomSlaveRoleIds;
    StringSet geomSlaveCategoryIds;
    StringVec geomComplexRoleIds;
    for (const auto& roleId : category.geomSlaveRoleIds()) {
        const auto& slaveCategoryId = category.slaveRole(roleId).categoryId();
        const auto& slaveCategory = categories[slaveCategoryId];
        if (slaveCategory.complex()) {
            geomComplexRoleIds.push_back(roleId);
        } else {
            geomSlaveRoleIds.push_back(roleId);
            geomSlaveCategoryIds.insert(slaveCategoryId);
        }
    }

    auto& work = branchCtx.txnView();
    TOIds regionIds;
    if (!geomSlaveRoleIds.empty()) {
        auto checkMasterId = objectIds.size() > 1 && !geomComplexRoleIds.empty();

        std::string extraColumn = checkMasterId ? ", r.master_id" : "";
        auto qb = makeQueryBuilder(branchCtx, fromTable);
        qb.selectFields(selectFields(fromTable) + extraColumn);

        qb.fromTable(views::TABLE_OBJECTS_R, "r");
        qb.fromTable(bestViewTable(geomSlaveCategoryIds), "g");
        qb.whereClause(
            " r.master_id IN (" + common::join(objectIds, ',') + ")"
            " AND r.domain_attrs->'rel:role' IN ('" + common::join(geomSlaveRoleIds, "','") + "')"
            " AND g.id=r.slave_id"
            " AND ST_Intersects(g.the_geom, a.the_geom)"
            " AND " + regionClause(fromTable, regionCategoryIds));

        auto pqxxResult = work.exec(qb.query());
        regionIds = pqxxResultToOids(pqxxResult, fromTable);
        if (checkMasterId) {
            for (const auto& row : pqxxResult) {
                objectIds.erase(row[1].as<TOid>());
            }
        } else if (!regionIds.empty()) {
            return regionIds;
        }
    }
    if (!geomComplexRoleIds.empty() && !objectIds.empty()) {
        views::QueryBuilder qb1(branchCtx.branch.id());
        qb1.selectFields("g.the_geom");
        qb1.fromTable(views::TABLE_OBJECTS_R, "r", "r_next");
        qb1.fromTable(views::TABLE_OBJECTS_L, "g");
        qb1.whereClause(
            " r.master_id IN (" + common::join(objectIds, ',') + ")"
            " AND r.domain_attrs->'rel:role' IN ('" + common::join(geomComplexRoleIds, "','") + "')"
            " AND r.slave_id=r_next.master_id"
            " AND g.id=r_next.slave_id");

        auto qb = makeQueryBuilder(branchCtx, fromTable);
        qb.with("ggg", qb1.query());
        qb.selectFields(selectFields(fromTable));
        qb.fromWith("ggg");
        qb.whereClause(
            "ST_Intersects(ggg.the_geom, a.the_geom)"
            " AND " + regionClause(fromTable, regionCategoryIds));
        auto regionIds2 = pqxxResultToOids(work.exec(qb.query()), fromTable);
        regionIds.insert(regionIds2.begin(), regionIds2.end());
    }
    return regionIds;
}

TOIds
getRegionIdsFromObjects(
    const BranchContext& branchCtx,
    FromTable fromTable,
    const RegionCategoryIds& regionCategoryIds,
    const std::vector<ObjectPtr>& objects)
{
    std::vector<Geom> geoms;
    std::map<std::string, TOIds> nonGeomCategories;
    for (const auto& obj : objects) {
        if (!obj->geom().isNull()) {
            geoms.push_back(obj->geom());
        } else {
            nonGeomCategories[obj->categoryId()].insert(obj->id());
        }
    }

    TOIds regionIds;
    for (auto& cat2oids : nonGeomCategories) {
        auto ids = getRegionIdsByComplexObjectsCategory(
            branchCtx, fromTable, regionCategoryIds, std::move(cat2oids.second), cat2oids.first);
        regionIds.insert(ids.begin(), ids.end());
    }

    if (!geoms.empty()) {
        auto geomRegionIds = getRegionIdsByGeoms(branchCtx, fromTable, regionCategoryIds, geoms);
        regionIds.insert(geomRegionIds.begin(), geomRegionIds.end());
    }
    return regionIds;
}

TOIds
getRegionIds(
    const BranchContext& branchCtx,
    FromTable fromTable,
    const std::vector<RegionCategoryIds>& regionCategories,
    const std::vector<ObjectPtr>& objects,
    const geos::geom::Envelope& env)
{
    if (!objects.empty()) {
        for (const auto& regionCategoryIds : regionCategories) {
            auto regionIds = getRegionIdsFromObjects(
                branchCtx, fromTable, regionCategoryIds, objects);
            if (!regionIds.empty()) {
                return regionIds;
            }
        }
    }

    for (const auto& regionCategoryIds : regionCategories) {
        auto regionIds = getRegionIdsByEnvelope(
            branchCtx, fromTable, regionCategoryIds, env);
        if (!regionIds.empty()) {
            return regionIds;
        }
    }
    return {};
}

} // namespace

revision::filters::ProxyFilterExpr
existingAoiFilter(const TOIds& aoiIds)
{
    namespace rf = revision::filters;

    return
        rf::Attr(plainCategoryIdToCanonical(CATEGORY_AOI)).defined() &&
        rf::ObjRevAttr::isNotRelation() &&
        rf::ObjRevAttr::isNotDeleted() &&
        rf::ObjRevAttr::objectId().in(aoiIds) &&
        rf::Geom::defined();
}

TOIds getAoiIds(
    const BranchContext& branchCtx,
    const std::vector<ObjectPtr>& objects,
    const geos::geom::Envelope& envelope)
{
    return getRegionIds(
        branchCtx, FromTable::ObjectsAreal,
        AOI_ORDERED_REGION_CATEGORIES, objects, envelope);
}

TOIds getImportantRegionIds(
    const BranchContext& branchCtx,
    const std::vector<ObjectPtr>& objects,
    const geos::geom::Envelope& envelope)
{
    return getRegionIds(
        branchCtx, FromTable::ObjectsAreal,
        DIFFALERT_ORDERED_REGION_CATEGORIES, objects, envelope);
}

TOIds getPublishRegionIds(
    const BranchContext& branchCtx,
    const std::vector<ObjectPtr>& objects,
    const geos::geom::Envelope& envelope)
{
    return getRegionIds(
        branchCtx, FromTable::ContourObjectsGeom,
        CONTOUR_OBJECTS_ORDERED_REGION_CATEGORIES, objects, envelope);
}

bool isCommitInImportantRegion(
    const BranchContext& branchCtx,
    const std::string& categoryId,
    const std::vector<ObjectPtr>& geomDiffObjects,
    const geos::geom::Envelope& envelope)
{
    if (IMPORTANT_CATEGORIES_IN_IMPORTANT_REGIONS.count(categoryId)) {
        auto importantRegionIds = getImportantRegionIds(branchCtx, geomDiffObjects, envelope);
        if (!importantRegionIds.empty()) {
            return true;
        }
    }
    return false;
}

bool isCommitInImportantRegion(
    const BranchContext& branchCtx,
    const std::string& categoryId,
    const geolib3::BoundingBox& bbox)
{
    if (IMPORTANT_CATEGORIES_IN_IMPORTANT_REGIONS.count(categoryId)) {
        auto importantRegionIds = getImportantRegionIds(branchCtx, std::vector<ObjectPtr>(),
            geos::geom::Envelope(bbox.minX(), bbox.maxX(), bbox.minY(), bbox.maxY()));
        if (!importantRegionIds.empty()) {
            return true;
        }
    }
    return false;
};

bool isCommitWithBigArea(
    const std::string& categoryId,
    const std::vector<diffalert::Message>& diffalertMessages)
{
    if (IMPORTANT_CATEGORIES_FOR_BIG_AREA.count(categoryId)) {
        for (const auto& message : diffalertMessages) {
            if (DIFFALERT_IMPORTANT_MESSAGES.count(message.description()) > 0) {
                return true;
            }
        }
    }
    return false;
}

bool isCommitWithBigArea(
    const std::string& categoryId,
    const social::Gateway& socialGateway,
    TId taskId)
{
    if (IMPORTANT_CATEGORIES_FOR_BIG_AREA.count(categoryId)) {
        auto alerts = socialGateway.loadEventAlerts({taskId});
        for (const auto& alert : alerts) {
            if (DIFFALERT_IMPORTANT_MESSAGES.count(alert.description()) > 0) {
                return true;
            }
        }
    }
    return false;
};

social::EventAlerts
loadPermittedAlerts(
    social::TId taskId,
    const social::Gateway& socialGateway,
    CheckPermissions& permissionsChecker)
{
    auto eventAlerts = socialGateway.loadEventAlerts({taskId});
    eventAlerts.erase(
        std::remove_if(
            eventAlerts.begin(),
            eventAlerts.end(),
            [&](const auto& alert) {
                const auto categoryId = socialGateway.loadEventsByIds({alert.eventId()})
                    .front().getPrimaryObjectCategory();
                return categoryId &&
                    !permissionsChecker.isUserHasAccessToEventAlert(
                        alert, *categoryId);
            }),
        eventAlerts.end());
    return eventAlerts;
}

std::map<social::TId, social::EventAlerts>
fillAlertsByTaskId(
    const social::Tasks& tasks,
    social::Gateway& socialGw,
    CheckPermissions& permissionsChecker)
{
    social::TIds taskIds;
    for (const auto& task : tasks) {
        taskIds.insert(task.id());
    }

    std::map<social::TId, social::EventAlerts> result;
    for (auto&& alert : socialGw.loadEventAlerts(taskIds)) {
        const auto categoryId = socialGw.loadEventsByIds({alert.eventId()})
            .front().getPrimaryObjectCategory();
        if (categoryId &&
            !permissionsChecker.isUserHasAccessToEventAlert(
                alert, *categoryId))
        {
            continue;
        }
        const auto taskId = alert.eventId();
        result[taskId].push_back(std::move(alert));
    }
    return result;
}

} // namespace wiki
} // namespace maps
