#include "get_suggest.h"
#include "maps/wikimap/mapspro/services/editor/src/configs/config.h"
#include "maps/wikimap/mapspro/services/editor/src/format.h"
#include "maps/wikimap/mapspro/services/editor/src/branch_helpers.h"
#include "maps/wikimap/mapspro/services/editor/src/srv_attrs/registry.h"
#include "magic_strings.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/category_traits.h"
#include "maps/wikimap/mapspro/services/editor/src/sync/db_helpers.h"

#include <maps/wikimap/mapspro/libs/views/include/query_builder.h>
#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/wiki/configs/editor/category_template.h>
#include <yandex/maps/wiki/configs/editor/config_holder.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/attrdef.h>
#include <yandex/maps/wiki/common/pg_utils.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <maps/libs/locale/include/find.h>
#include <maps/libs/locale/include/convert.h>
#include <maps/libs/locale/include/codes.h>

namespace maps {
namespace wiki {

namespace {

const size_t GEOMETRY_SEARCH_LIMIT = 200;
const size_t HIERARCHY_SEARCH_LIMIT = 7;
const double DEFAULT_DISTANCE_LIMIT = 2000.0;
const std::string STR_INTERSECTS = "intersects";
const std::string STR_DISTANCE = "distance";
const std::string STR_ID = "id";
const std::string STR_COMMIT_ID = "commit_id";
const std::string STR_OBJECT_ID = "object_id";
const std::string STR_PARENT_ID = "parent_id";
const std::string STR_ATTR_KEYS = "attr_keys";
const std::string STR_TEXT_DATA = "text_data";
const std::string STR_LANG = "lang";
const std::string STR_TITLE = "title";

const locale::Locale DEFAULT_USER_LOCALE(locale::Lang::Ru, locale::Region::Ru);

}//namespace
/**
    Example SQL output for GET /suggest/ad?text=ор б ю, ок
    SELECT suggest_data0.object_id, suggest_data0.commit_id,
    suggest_data0.parent_id, suggest_data0.text_data, 2 as rank_aux ,
    ST_Distance(ST_GeomFromText('POINT(4161469.78533 7483474.4575)', 3395), suggest_data0.the_geom) as distance_to_object ,
    ST_Area(suggest_data0.the_geom) as object_area
    FROM suggest_data suggest_data0 ,suggest_data suggest_data1
    WHERE  suggest_data0.categories ?| ARRAY['cat:ad']
    AND suggest_data0.lang = suggest_data1.lang
    AND suggest_data0.parent_id = suggest_data1.object_id
    AND suggest_data0.suggest_data_fts_russian @@ to_tsquery('russian_without_stopwords','ор:* & б:* & ю:*')
    AND suggest_data1.suggest_data_fts_russian @@ to_tsquery('russian_without_stopwords','ок:*')
    ORDER BY rank_aux ASC, distance_to_object ASC , object_area ASC
    LIMIT 20;
*/
/**
 * search text contains search prefixes delimited with space
 * and separated by commas for each hiearchy path level
 * example:
 * searchText: дер ив, мос обл
 * result:
    {
        {дер, ив},
        {мос, обл}
    }
*/

namespace
{
std::string
sqlArrayFromCategories(const StringSet& categories)
{
    std::ostringstream sqlArrayStr;
    sqlArrayStr << "ARRAY['";
    sqlArrayStr << common::join(categories, plainCategoryIdToCanonical, "','");
    sqlArrayStr << "']";
    return sqlArrayStr.str();
}

template <typename Container>
void eraseEmpty(Container& container) {
    container.erase(std::remove_if(container.begin(),
        container.end(),
        [](const typename Container::value_type& v) {return v.empty();} ),
        container.end());
}

boost::optional<locale::Locale>
tryCastToLocale(const std::string& langName)
{
    if (langName.empty()) {
        return boost::none;
    }
    try {
        return locale::to<locale::Locale>(langName);
    } catch (const locale::LocaleParsingError&) {
    }
    return boost::none;
}

locale::Locale safeLocaleCast(const std::string& langName)
{
    auto result = tryCastToLocale(langName);
    return result ? *result : DEFAULT_USER_LOCALE;
}

} // namespace

class AttributesFilter
{
public:
    explicit AttributesFilter(const StringMap& parsedAttributesAndValues)
    {
        const auto& editorCfg = cfg()->editor();
        for (const auto& pair : parsedAttributesAndValues) {
            const auto& attrId = pair.first;
            WIKI_REQUIRE (
                editorCfg->isAttributeDefined(attrId),
                ERR_BAD_REQUEST,
                "Undefined attribute in filter: " << attrId);
            const auto& attrValue = pair.second;
            const AttrDefPtr& attrDef = editorCfg->attribute(attrId);
            const auto& validValue = attrDef->allowedValue(attrValue);
            if (attrDef->booleanValue()) {
                if (validValue == TRUE_VALUE) {
                    trueValueAttributes_.insert(attrId);
                } else {
                    falseValueAttributes_.insert(attrId);
                }
            } else {
                attributesWithValues_.emplace(attrId, validValue);
            }
        }
    }

    bool empty() const {
        return
            attributesWithValues_.empty() &&
            trueValueAttributes_.empty() &&
            falseValueAttributes_.empty();
    }

    std::string formatSQL(Transaction& work, const std::string& hstoreField) const {
        std::ostringstream queryExpression;
        queryExpression << " ( TRUE ";
        if (!attributesWithValues_.empty()) {
            queryExpression <<  " AND " << hstoreField << " @> " <<
                common::attributesToHstore(work, attributesWithValues_);
        }
        if (!trueValueAttributes_.empty()) {
            queryExpression << " AND " << hstoreField << " ?& ARRAY['"
                << common::join(trueValueAttributes_, "','") << "']";
        }
        if (!falseValueAttributes_.empty()) {
            queryExpression << " AND NOT " << hstoreField << " ?| ARRAY['"
                << common::join(falseValueAttributes_, "','") << "']";
        }
        queryExpression << ") ";
        return queryExpression.str();
    }
private:
    StringMap attributesWithValues_;
    StringSet trueValueAttributes_;
    StringSet falseValueAttributes_;
};

class SuggestQueryBuilder
{
public:


    std::string operator ()(const std::string& searchText,
        const StringSet& categories, size_t limit, const std::string& ll, double distance,
        const OidsVector& objectIds, const AttributesFilter& attributesFilter, Transaction& work) const
    {
        const auto searchPrefixes = parseSearchText(searchText);
        if (searchPrefixes.ftsSearchPrefixes.empty() && ll.empty()) {
            return s_emptyString;
        }
        std::stringstream query;
        query.precision(DOUBLE_FORMAT_PRECISION);
        limit *= 10; // expand intermediately result to support suggest for multi lang object's
        if (!searchPrefixes.ftsSearchPrefixes.empty()) {
            bool isUnion = false;
            if (!searchPrefixes.exactMatchPrefix.empty()) {
                query << "SELECT * FROM ((";
                writeExactMatchQuery(query,
                    searchPrefixes.exactMatchPrefix, categories, ll, distance, objectIds,
                        attributesFilter, work);
                writeOrdering(query, ll);
                query << " LIMIT " << limit;
                query << ") UNION ALL (";
                isUnion = true;
            }
            writeFTSMatchQuery(query,
                searchPrefixes.ftsSearchPrefixes, categories, ll, distance, objectIds,
                attributesFilter, work);
            writeOrdering(query, ll);
            query << " LIMIT " << limit;
            if(isUnion) {
                query << ")) all_found ";
                query << " ORDER BY rank_aux ASC";
                writeFinalOrdering(query, ll);
                query << " LIMIT " << limit;
            }
        } else {
            writeBasicQuery(query, categories, ll, distance, objectIds,
                attributesFilter, work);
            writeOrdering(query, ll);
            query << " LIMIT " << limit;
        }
        DEBUG() << BOOST_CURRENT_FUNCTION << "***** SQL:" << query.str();
        return query.str();
    }

private:
    typedef std::list<StringList> FTSSearchPrefixes;
    struct SearchPrefixes
    {
        std::string exactMatchPrefix;
        FTSSearchPrefixes ftsSearchPrefixes;
    };

private:
    void writeBasicQuery(
        std::ostream& query,
        const StringSet& categories,
        const std::string& ll,
        double distance,
        const OidsVector& objectIds,
        const AttributesFilter& attributesFilter,
        Transaction& work) const
    {
        if (fabs(distance) < CALCULATION_TOLERANCE) {
            distance = DEFAULT_DISTANCE_LIMIT;
        }

        writeSELECTFields(query);
        query << ", 0 as rank_aux ";
        writeDistanceExpression(query, ll);
        query << " FROM ";
        if (!attributesFilter.empty()) {
            query << bestViewTable(categories) << " view_attributes, ";
        }
        query << "suggest_data suggest_data0 ";
        writeStartFilter(query, categories, ll, distance, objectIds,
            attributesFilter, work);
    }

    void writeExactMatchQuery(
        std::ostream& query, const std::string& pattern,
        const StringSet& categories,
        const std::string& ll, double distance,
        const OidsVector& objectIds,
        const AttributesFilter& attributesFilter,
        Transaction& work) const
    {
        writeSELECTFields(query);
        query << ", 1 as rank_aux ";
        writeDistanceExpression(query, ll);
        query << " FROM ";
        if (!attributesFilter.empty()) {
            query << bestViewTable(categories) << " view_attributes, ";
        }
        query << "suggest_data suggest_data0 ";
        writeStartFilter(query, categories, ll, distance, objectIds,
            attributesFilter, work);
        query << " AND suggest_data0.lower_text_data=lower("
                    "'" << pattern << "' COLLATE \"C.UTF-8\") ";
    }
    void writeFTSMatchQuery(
        std::ostream& query, const FTSSearchPrefixes& ftsSearchPrefixes,
        const StringSet& categories,
        const std::string& ll, double distance,
        const OidsVector& objectIds,
        const AttributesFilter& attributesFilter,
        Transaction& work) const
    {
        writeSELECTFields(query);
        query << ", 2 as rank_aux ";
        writeDistanceExpression(query, ll);
        writeFromClause(query, categories, ftsSearchPrefixes.size(), attributesFilter);
        writeStartFilter(query, categories, ll, distance, objectIds,
            attributesFilter, work);
        writeJoinClause(query, ftsSearchPrefixes.size());
        writeTextSearchCondition(query, ftsSearchPrefixes);
    }
    void writeSELECTFields(std::ostream& query) const
    {
        query << "SELECT suggest_data0.object_id, suggest_data0.commit_id,"
                 " suggest_data0.parent_id, suggest_data0.text_data, "
                 "akeys(suggest_data0.categories) attr_keys, suggest_data0.lang ";
    }
    void writeFromClause(
        std::ostream& out,
        const StringSet& categories,
        size_t jointTablesCount,
        const AttributesFilter& attributesFilter) const
    {
        out << " FROM ";
        if (!attributesFilter.empty()) {
            out << bestViewTable(categories) << " view_attributes,  ";
        }
        for (size_t i = 0; i < jointTablesCount; ++i) {
            if (0 != i) {
                out << " ,";
            }
            out << "suggest_data suggest_data" << i;
        }
    }

    void writeJoinClause(std::ostream& out, size_t jointTablesCount) const
    {
        ASSERT(jointTablesCount);
        for (size_t i = 0; i < jointTablesCount - 1; ++i) {
            out << " AND "
                << "suggest_data" << i << ".parent_id = "
                << "suggest_data" << i + 1 << ".object_id"
                << " AND "
                << "suggest_data" << i << ".lang = "
                << "suggest_data" << i + 1 << ".lang";
        }
    }

    void writeTextSearchCondition(std::ostream& out,
        const FTSSearchPrefixes& ftsSearchPrefixes) const
    {
        size_t i = 0;
        for (const auto& levelSearchPrefixes : ftsSearchPrefixes) {
            out << " AND suggest_data" << i << ".suggest_data_fts_russian @@ to_tsquery('russian_without_stopwords', lower('";
            bool bFirst = true;
            for (const auto& searchPrefix : levelSearchPrefixes) {
                if (!bFirst) {
                    out << " & ";
                }
                bFirst = false;
                out << searchPrefix << ":*";
            }
            out << "' COLLATE \"C.UTF-8\"))";
            ++i;
        }
    }

    void writePoint(std::ostream& out, const std::string& ll) const
    {
        ASSERT(!ll.empty());
        std::vector<double> geodeticLL = splitCast<std::vector<double>>(ll, ',');
        WIKI_REQUIRE(geodeticLL.size() == 2, ERR_BAD_REQUEST, "Can't parse ll for suggest.");
        TMercatorPoint mercatorLL = common::geodeticTomercator(geodeticLL[0], geodeticLL[1]);
        out << "ST_GeomFromText('POINT("
            << mercatorLL.x() << " " << mercatorLL.y()
            << ")', 3395)";
    }

    void writeDistanceExpression(std::ostream& out, const std::string& ll) const
    {
        if (ll.empty()) {
            return;
        }
        out << ", ";
        writePoint(out, ll);
        out << "<-> suggest_data0.the_geom as distance_to_object "
               ", ST_Area(suggest_data0.the_geom) as object_area ";
    }

    void writeStartFilter(
        std::ostream& query,
        const StringSet& categories,
        const std::string& ll,
        double distance,
        const OidsVector& objectIds,
        const AttributesFilter& attributesFilter,
        Transaction& work) const
    {
        query << " WHERE ";
        if (!attributesFilter.empty()) {
            query << " view_attributes.id=suggest_data0.object_id AND " << attributesFilter.formatSQL(work, "view_attributes.domain_attrs")
                << " AND ";
        }
        if (objectIds.empty()) {
            query << " suggest_data0.categories ?| "
                  << sqlArrayFromCategories(categories);
            writeDistanceFilter(query, ll, distance);
        } else {
            query << " suggest_data0.object_id IN ("
                  << common::join(objectIds, ',') << ")";
        }
    }

    void writeDistanceFilter(std::ostream& out,
        const std::string& ll,
        double distance) const
    {
        if (ll.empty()
            || fabs(distance) < CALCULATION_TOLERANCE) {
            return;
        }
        out << " AND (suggest_data0.the_geom IS NULL  OR ST_DWithin(";
        writePoint(out, ll);
        out << ", suggest_data0.the_geom, " << distance << ")) ";
    }

    SearchPrefixes parseSearchText(const std::string& searchText) const
    {
        const char space = ' ';
        auto cleanSearchText = searchText;
        std::for_each(cleanSearchText.begin(), cleanSearchText.end(),
            [](char& c) { if (c > 0 && strchr("()|&:*!\t", c)) { c = space;}});
        for (size_t i = 1; i < cleanSearchText.size(); ++i) {
            if (cleanSearchText[i] == '\'' && cleanSearchText[i - 1] == space) {
                cleanSearchText[i] = space;
            }
        }
        SearchPrefixes searchPrefixes;
        auto hierarchySplit = split<StringList>(boost::trim_copy(cleanSearchText), ',');
        if (hierarchySplit.size() == 1) {
            searchPrefixes.exactMatchPrefix = hierarchySplit.front();
        }
        WIKI_REQUIRE(hierarchySplit.size() <= HIERARCHY_SEARCH_LIMIT,
            ERR_BAD_REQUEST,
            "Too many hiearchy levels in request "
            << hierarchySplit.size() << " > " << HIERARCHY_SEARCH_LIMIT);


        for (auto& pathPart : hierarchySplit) {
            boost::trim(pathPart);
            if (!pathPart.empty()) {
                auto searchPrefixesPart = split<StringList>(pathPart, ' ');
                eraseEmpty(searchPrefixesPart);
                searchPrefixes.ftsSearchPrefixes.push_back(searchPrefixesPart);
            }
        }
        eraseEmpty(searchPrefixes.ftsSearchPrefixes);
        return searchPrefixes;
    }

    void writeOrdering(std::ostream& query, const std::string& ll) const
    {
        query << " ORDER BY ";
        if (!ll.empty()) {
            writePoint(query, ll);
            query << "<-> suggest_data0.the_geom ASC NULLS LAST"
                    ", object_area ASC NULLS LAST , ";
        }
        query << " object_id ASC ";
    }

    void writeFinalOrdering(std::ostream& query, const std::string& ll) const
    {
        query << ",";
        if (!ll.empty()) {
            query << " distance_to_object ASC NULLS LAST"
                    ", object_area ASC NULLS LAST, ";
        }
        query << " object_id ASC ";
    }
}; // SuggestQueryBuilder

namespace
{
std::string
compileSuggestPath(Transaction& work, TOid startId, const std::string& lang)
{
    if (!startId) {
        return s_emptyString;
    }
    std::stringstream queryStream;
    queryStream <<
    "WITH recursive parents(object_id, text_data, parent_id, lang) AS ("
    "SELECT object_id, text_data, parent_id, lang "
    "FROM suggest_data "
    "WHERE object_id=" << startId << " AND lang= " << work.quote(lang) <<
    " UNION ALL "
    "SELECT d.object_id, d.text_data, d.parent_id, d.lang "
    "FROM parents pr, suggest_data d "
    "WHERE d.object_id = pr.parent_id and d.lang = pr.lang) "
    "SELECT text_data FROM parents LIMIT " << MAX_PARENT_PATH_LEVELS;
    auto queryResult = work.exec(queryStream.str());
    return common::join(
        queryResult.begin(), queryResult.end(),
        [](const pqxx::row& row) { return row[0].c_str(); },
        ", "
    );
}

OidsVector
collectOids(Transaction& work, const std::string& query)
{
    DEBUG() << "Suggest::collectOids query:" << query;
    auto resultRows = work.exec(query);
    OidsVector oids;
    oids.reserve(resultRows.size());
    for (const auto& row : resultRows) {
        oids.push_back(row[0].as<TOid>());
    }
    return oids;
}

std::string
geometryCondition(
    double distanceToGeom,
    const std::string& geomWkb,
    GetSuggest::GeomPredicate predicate)
{
    std::ostringstream qstr;
    qstr.precision(DOUBLE_FORMAT_PRECISION);
    switch(predicate) {
        case GetSuggest::GeomPredicate::Intersects :
            qstr << " ST_Intersects(s.the_geom, ST_GeomFromWKB('"
                << geomWkb << "',3395)) ";
            return qstr.str();
        case GetSuggest::GeomPredicate::Distance :
            qstr << " ST_DWithin(s.the_geom, ST_GeomFromWKB('"
                << geomWkb << "',3395), " << distanceToGeom << ") ";
            return qstr.str();
    }
    ASSERT(false);
}

OidsVector
oidsOfComplexObjectsWithinDistance(
    Transaction& work, TBranchId branchId, double distanceToGeom,
    const std::string& geomWkb, const StringSet& categories,
    GetSuggest::GeomPredicate predicate,
    const AttributesFilter& attributesFilter)
{
    const auto& editorCfg = cfg()->editor();
    const auto& allCategories = editorCfg->categories();

    StringSet masterCategories;
    StringSet geomSlaveCategories;
    for (const auto& categoryId : categories) {
        const auto& category = allCategories[categoryId];
        if (!category.complex()) {
            continue;
        }
        for (const auto& slaveRole : category.slavesRoles()) {
            if (slaveRole.tableRow()) {
                continue;
            }
            const auto& slaveCategoryId = slaveRole.categoryId();
            if (geomSlaveCategories.count(slaveCategoryId)) {
                continue;
            }
            const auto& slaveCategory = allCategories[slaveCategoryId];
            const auto& tmpl = slaveCategory.categoryTemplate();
            if (tmpl.hasGeometryType()) {
                geomSlaveCategories.insert(slaveCategoryId);
                masterCategories.insert(categoryId);
            }
        }
    }

    if (geomSlaveCategories.empty()) {
        return {};
    }

    views::QueryBuilder qb(branchId);
    qb.selectFields(
        "master_id, "
        "min(ST_Distance(s.the_geom, ST_GeomFromWKB('" + geomWkb + "',3395))) as distance");
    qb.fromTable(views::TABLE_OBJECTS_R, "r");
    qb.fromTable(bestViewTable(geomSlaveCategories), "s");
    if (!attributesFilter.empty()) {
        qb.fromTable(views::TABLE_OBJECTS_C, "view_attributes");
    }

    std::ostringstream whereClause;
    whereClause
            << geometryCondition(distanceToGeom, geomWkb, predicate);
    if (!attributesFilter.empty()) {
        whereClause
            << " AND master_id = view_attributes.id AND "
            << attributesFilter.formatSQL(work, "view_attributes.domain_attrs");
    }
    whereClause
            << " AND r.slave_id = s.id"
            << " AND s.domain_attrs ?| ARRAY['"
            << common::join(plainCategoryIdsToCanonical(geomSlaveCategories), "','") << "']"
            << " AND r.domain_attrs -> 'rel:master' IN ('" << common::join(masterCategories, "','") << "')";

    qb.whereClause(whereClause.str());

    auto query =
        qb.query() +
        " GROUP BY 1 ORDER BY 2 LIMIT " + std::to_string(GEOMETRY_SEARCH_LIMIT);
    return collectOids(work, query);
}

OidsVector
oidsOfContourObjectsWithinDistance(
    Transaction& work, TBranchId branchId, double distanceToGeom,
    const std::string& geomWkb, const StringSet& categories,
    GetSuggest::GeomPredicate predicate)
{
    const auto& editorCfg = cfg()->editor();
    const auto& contourDefs = editorCfg->contourObjectsDefs();
    StringSet elementsCats;
    StringSet contourObjsCats;
    for (const auto& c : categories) {
        if (contourDefs.partType(c) != ContourObjectsDefs::PartType::Object) {
            continue;
        }
        const auto& contourDef = contourDefs.contourDef(c);
        elementsCats.insert(contourDef.contour.linearElement.categoryId);
        contourObjsCats.insert(c);
    }
    if (elementsCats.empty()) {
        return {};
    }

    views::QueryBuilder qb(branchId);
    qb.selectFields(
        "r.master_id, "
        "min(ST_Distance(the_geom, ST_GeomFromWKB('" + geomWkb +  "',3395))) as distance");
    qb.fromTable(views::TABLE_OBJECTS_L, "s");
    qb.fromTable(views::TABLE_OBJECTS_R, "r", "r_next");
    qb.whereClause(
        geometryCondition(distanceToGeom, geomWkb, predicate) +
        " AND r.slave_id = r_next.master_id AND r_next.slave_id = s.id"
        " AND s.domain_attrs ?| ARRAY['" +
            common::join(plainCategoryIdsToCanonical(elementsCats), "','") + "']"
        " AND r.domain_attrs -> 'rel:master' IN ('" + common::join(contourObjsCats, "','") + "')");

    auto query =
        qb.query() +
        " GROUP BY 1 ORDER BY 2 LIMIT " + std::to_string(GEOMETRY_SEARCH_LIMIT);
    return collectOids(work, query);
}

OidsVector
oidsOfObjectsWithinDistance(
    Transaction& work, TBranchId branchId, double distanceToGeom,
    const std::string& geomWkb, const StringSet& categories,
    GetSuggest::GeomPredicate predicate,
    const AttributesFilter& attributesFilter)
{
    if (categories.empty()) {
        return {};
    }

    views::QueryBuilder qb(branchId);
    qb.selectFields(
        "id, "
        "min(ST_Distance(the_geom, ST_GeomFromWKB('" + geomWkb + "',3395))) as distance");
    qb.fromTable(bestViewTable(categories), "s");

    std::ostringstream whereClause;
    whereClause << geometryCondition(distanceToGeom, geomWkb, predicate);
    if (!attributesFilter.empty()) {
        whereClause << " AND " <<  attributesFilter.formatSQL(work, "domain_attrs");
    }
    whereClause
        << " AND domain_attrs ?| ARRAY['"
        << common::join(plainCategoryIdsToCanonical(categories), "','") << "']";
    qb.whereClause(whereClause.str());

    auto query =
        qb.query() +
        " GROUP BY 1 ORDER BY 2 LIMIT " + std::to_string(GEOMETRY_SEARCH_LIMIT);
    return collectOids(work, query);
}


void
orderSuggestedObjects(
    std::vector<controller::ResultType<GetSuggest>::SuggestedObject>& suggestedObjects,
    const OidsVector& objectIds)
{
    if (objectIds.empty()) {
        return;
    }
    std::unordered_map<TOid, size_t> orderOfIds;
    for (size_t i = 0; i < objectIds.size(); ++i) {
        orderOfIds.insert({objectIds[i], i});
    }
    std::sort(suggestedObjects.begin(), suggestedObjects.end(),
        [&orderOfIds](
                const controller::ResultType<GetSuggest>::SuggestedObject& obj1,
                const controller::ResultType<GetSuggest>::SuggestedObject& obj2)
                {
                    return orderOfIds[obj1.id] < orderOfIds[obj2.id];
                });
}

std::string
queryNoSuggestObjectsData(TBranchId branchId, const OidsVector& oids)
{
    views::QueryBuilder qb(branchId);
    qb.selectFields(
        "id, commit_id, akeys(domain_attrs) as attr_keys, "
        " CASE WHEN service_attrs ? '" + srv_attrs::SRV_SCREEN_LABEL + "' THEN " \
        " service_attrs->'" + srv_attrs::SRV_SCREEN_LABEL + "' ELSE" \
        " '' END as title, "
        + SQL_EXTRACT_FT_TYPE_ID);
    qb.fromTable(views::TABLE_OBJECTS, "o");
    qb.whereClause(common::whereClause("id", oids));
    return qb.query();
}

std::map<TOid, TCommitId>
oidsViewRevisions(
    Transaction& workView,
    TBranchId branchId,
    const std::vector<controller::ResultType<GetSuggest>::SuggestedObject>& suggestedObjects)
{
    if (suggestedObjects.empty()) {
        return {};
    }
    TOIds oids;
    for (auto& object : suggestedObjects) {
        oids.insert(object.id);
    }

    views::QueryBuilder qb(branchId);
    qb.selectFields("id, commit_id");
    qb.fromTable(views::TABLE_OBJECTS, "o");
    qb.whereClause(common::whereClause("id", oids));

    std::map<TOid, TCommitId> idToCommit;
    for (const auto& row : workView.exec(qb.query())) {
        idToCommit.insert({row[0].as<TId>(), row[1].as<TCommitId>()});
    }
    return idToCommit;
}

} // namespace

GetSuggest::GetSuggest(const maps::wiki::GetSuggest::Request &request)
    : controller::BaseController<GetSuggest>(BOOST_CURRENT_FUNCTION)
    , request_(request)
{
    request_.limit = request_.limit
            ? std::min(request_.limit, cfg()->suggestCount())
            : cfg()->suggestCount();
    result_->categories = removeUndefinedCategories(split<StringSet>(request_.categories, ','));
}

std::string
GetSuggest::printRequest() const
{
    std::stringstream ss;
    ss << " categories: " << request_.categories;
    ss << " searchText: " << request_.searchText;
    ss << " limit: " << request_.limit;
    ss << " token: " << request_.token;
    ss << " branch: " << request_.branchId;
    return ss.str();
}

void
GetSuggest::collectOidsWithinGeometryRequest(Transaction& work, const AttributesFilter& attributesFilter)
{
    if (request_.geometry.isNull()) {
        return;
    }
    geomWkb_ = work.esc_raw(request_.geometry.wkb());
    double distanceToGeom = request_.distance;
    if (fabs(distanceToGeom) < CALCULATION_TOLERANCE) {
        distanceToGeom = DEFAULT_DISTANCE_LIMIT;
    }

    const auto& editorCfg = cfg()->editor();
    StringSet noGeomCats;
    StringSet geomCats;
    StringSet noSuggestGeomCats;
    for (const auto& catId : result_->categories) {
        const Category& cat = editorCfg->categories()[catId];
        const auto& tmpl = cat.categoryTemplate();
        if (!tmpl.hasGeometryType()) {
            WIKI_REQUIRE(cat.complex() && cat.suggest(), ERR_BAD_REQUEST,
                "Category " << catId << " neither complex suggest neither geometry.");
            noGeomCats.insert(catId);
        } else if (cat.suggest()) {
            geomCats.insert(catId);
        } else {
            noSuggestGeomCats.insert(catId);
        }
    }
    auto noOwnGeometryObjectsOids = oidsOfComplexObjectsWithinDistance(
        work, request_.branchId, distanceToGeom, geomWkb_,
        noGeomCats, request_.predicate, attributesFilter);
    auto contourObjectsOids = oidsOfContourObjectsWithinDistance(
        work, request_.branchId, distanceToGeom, geomWkb_,
        noGeomCats, request_.predicate);
    noOwnGeometryObjectsOids.insert(noOwnGeometryObjectsOids.end(),
        contourObjectsOids.begin(), contourObjectsOids.end());
    auto ownGeometryObjectsOids = oidsOfObjectsWithinDistance(
        work, request_.branchId, distanceToGeom, geomWkb_,
        geomCats, request_.predicate, attributesFilter);
    if (request_.searchText.empty() && !noSuggestGeomCats.empty()) {
        oidsOfObjectsWithinDistance(
            work, request_.branchId, distanceToGeom, geomWkb_,
            noSuggestGeomCats, request_.predicate,
            attributesFilter).swap(noSuggestOwnGeometryObjectsOids_);
    }
    objectIdsWithSuggest_.swap(noOwnGeometryObjectsOids);

    objectIdsWithSuggest_.insert(objectIdsWithSuggest_.end(),
        ownGeometryObjectsOids.begin(), ownGeometryObjectsOids.end());
    if (request_.searchText.empty()) {
        noSuggestOwnGeometryObjectsOids_.insert(noSuggestOwnGeometryObjectsOids_.end(),
            ownGeometryObjectsOids.begin(), ownGeometryObjectsOids.end());
    }
}

void
GetSuggest::performSuggestSearch(Transaction& work, const AttributesFilter& attributesFilter)
{
    if (!request_.geometry.isNull()
        && objectIdsWithSuggest_.empty()) {
        return;
    }

    std::string query = SuggestQueryBuilder()(
        work.esc(request_.searchText),
        result_->categories,
        request_.limit,
        request_.ll,
        request_.distance,
        objectIdsWithSuggest_,
        attributesFilter,
        work);

    if (query.empty()) {
        return;
    }
    std::map<TOid, std::pair<controller::ResultType<GetSuggest>::SuggestedObject, std::string>> suggestedObjectsLangById;

    pqxx::result queryResult = work.exec(query);
    result_->suggestedObjects.reserve(queryResult.size());
    std::vector<TOid> idsOrderedAsResult;
    idsOrderedAsResult.reserve(queryResult.size());
    const auto& userLocale = safeLocaleCast(request_.lang);

    locale::DistanceComparator distanceComp(userLocale);

    for (const auto& row : queryResult) {
        TOid oid = row[STR_OBJECT_ID].as<TOid>();
        idsOrderedAsResult.push_back(oid);
        const std::string& rowLang = row[STR_LANG].c_str();
        auto rowLocale = tryCastToLocale(rowLang);
        if (!rowLocale) {
            continue;
        }
        auto it = suggestedObjectsLangById.find(oid);
        if (it == suggestedObjectsLangById.end()
            ||
            distanceComp.less(*rowLocale, locale::to<locale::Locale>(it->second.second)))
        {
            controller::ResultType<GetSuggest>::SuggestedObject
                suggestedObject {
                    oid,
                    row[STR_COMMIT_ID].as<TCommitId>(),
                    row[STR_TEXT_DATA].c_str(),
                    row[STR_PARENT_ID].is_null()
                        ? s_emptyString
                        : compileSuggestPath(
                            work,
                            row[STR_PARENT_ID].as<TOid>(),
                            rowLang),
                    categoryFromAttributes(row[STR_ATTR_KEYS].c_str())
                };
            if (it == suggestedObjectsLangById.end()) {
                suggestedObjectsLangById.insert({oid, std::make_pair(suggestedObject, rowLang)});
            } else {
                it->second = std::make_pair(suggestedObject, rowLang);
            }
        }
    }

    for (const auto&  oid : idsOrderedAsResult) {
        if (foundIds_.size() >= request_.limit) {
            break;
        }
        if (!foundIds_.insert(oid).second) {
            continue;
        }
        auto it = suggestedObjectsLangById.find(oid);
        result_->suggestedObjects.push_back(it->second.first);
    }
    orderSuggestedObjects(result_->suggestedObjects, objectIdsWithSuggest_);
}

void
GetSuggest::collectNoSuggestOwnGeometryObjectsOids(Transaction& work)
{
    if (noSuggestOwnGeometryObjectsOids_.empty()) {
        return;
    }
    auto query = queryNoSuggestObjectsData(request_.branchId, noSuggestOwnGeometryObjectsOids_);
    for (const auto& row : work.exec(query)) {
        if (foundIds_.size() >= request_.limit) {
            break;
        }
        TOid oid = row[STR_ID].as<TOid>();
        if (!foundIds_.insert(oid).second) {
            continue;
        }
        const auto& categoryId = categoryFromAttributes(row[STR_ATTR_KEYS].c_str());
        std::string title = row[STR_TITLE].c_str();
        if (title.empty() && !row[FT_TYPE_ID].is_null()) {
            std::string ftTypeIdValue = row[FT_TYPE_ID].c_str();
            if (!ftTypeIdValue.empty()) {
                title = valueLabel(categoryId + ":" + FT_TYPE_ID, ftTypeIdValue);
            }
        }
        result_->suggestedObjects.push_back(
            controller::ResultType<GetSuggest>::SuggestedObject
                {
                    oid,
                    row[STR_COMMIT_ID].as<TCommitId>(),
                    title.empty() ? cfg()->editor()->categories()[categoryId].label() : title,
                    s_emptyString,
                    categoryId
                }
        );
    }
}

void
GetSuggest::control()
{
    auto work = BranchContextFacade::acquireWorkReadViewOnly(request_.branchId, request_.token);
    const AttributesFilter attributesFilter(request_.parsedAttributesAndValues);
    collectOidsWithinGeometryRequest(*work, attributesFilter);
    performSuggestSearch(*work, attributesFilter);
    collectNoSuggestOwnGeometryObjectsOids(*work);

    auto itToCommitId = oidsViewRevisions(*work, request_.branchId, result_->suggestedObjects);
    for (auto& obj : result_->suggestedObjects) {
        auto it = itToCommitId.find(obj.id);
        if (it != itToCommitId.end()) {
            obj.commitId = it->second;
        }
    }
}

std::ostream& operator << (std::ostream& os, GetSuggest::GeomPredicate predicate)
{
    switch (predicate) {
        case GetSuggest::GeomPredicate::Intersects:
            os << STR_INTERSECTS;
            return os;
        case GetSuggest::GeomPredicate::Distance:
            os << STR_DISTANCE;
            return os;
    }
    ASSERT(false);
}

std::istream&
operator >> (std::istream& is, GetSuggest::GeomPredicate& predicate)
{
    std::string s;
    is >> s;
    if (s == STR_INTERSECTS) {
        predicate = GetSuggest::GeomPredicate::Intersects;
    } else if (s == STR_DISTANCE) {
        predicate = GetSuggest::GeomPredicate::Distance;
    } else {
        THROW_WIKI_LOGIC_ERROR(ERR_BAD_DATA, "Invalid geometry predicate: " << s);
    }
    return is;
}

} // namespace wiki
} // namespace maps
