#include "objects_query.h"
#include <maps/wikimap/mapspro/services/editor/src/sync/db_helpers.h>

#include <maps/wikimap/mapspro/libs/views/include/query_builder.h>
#include <yandex/maps/wiki/common/pg_utils.h>
#include <yandex/maps/wiki/common/string_utils.h>

namespace maps::wiki::views {

OidsCondition::OidsCondition(TOIds oids)
    : oids_(std::move(oids))
{
    ASSERT(!oids_.empty());
}

std::string
OidsCondition::sqlExpression() const
{
    return common::whereClause("id", oids_);
}

AnyOfDomainAttributesCondition::AnyOfDomainAttributesCondition(Transaction& txn, StringSet ids)
    : txn_(txn)
    , attrIds_(std::move(ids))
{
    ASSERT(!attrIds_.empty());
}

std::string
AnyOfDomainAttributesCondition::sqlExpression() const
{
    return "domain_attrs ?| ARRAY[" +
        common::join(
            attrIds_,
            [this](const std::string& attrId) { return txn_.quote(attrId); },
            ',')
        + "]";
}

DomainAttributeCondition::DomainAttributeCondition(
    Transaction& txn,
    const std::string& id,
    const std::string& value)
    : txn_(txn)
    , id_(std::move(id))
    , value_(std::move(value))
{
}

std::string
DomainAttributeCondition::sqlExpression() const
{
    return
        "domain_attrs @> hstore(" +
        txn_.quote(id_) + ", " +
        txn_.quote(value_) + ")";
}

NoneOfDomainAttributesCondition::NoneOfDomainAttributesCondition(Transaction& txn, StringSet ids)
    : txn_(txn)
    , attrIds_(std::move(ids))
{
    ASSERT(!attrIds_.empty());
}

std::string
NoneOfDomainAttributesCondition::sqlExpression() const
{
    return "(NOT domain_attrs ?| ARRAY[" +
        common::join(
            attrIds_,
            [this](const std::string& attrId) { return txn_.quote(attrId); },
            ',')
        + "])";
}

AllOfServiceAttributesCondition::AllOfServiceAttributesCondition(Transaction& txn, StringMap values)
    : txn_(txn)
    , attrValues_(std::move(values))
{
    ASSERT(!attrValues_.empty());
}

std::string
AllOfServiceAttributesCondition::sqlExpression() const
{
    return "service_attrs @> hstore(ARRAY[" +
        common::join(
            attrValues_,
            [this](const std::pair<std::string, std::string>& kvp) {
                return txn_.quote(kvp.first) + ", " + txn_.quote(kvp.second);
            },
            ',')
        + "])";
}

NoneOfServiceAttributesCondition::NoneOfServiceAttributesCondition(Transaction& txn, StringSet ids)
    : txn_(txn)
    , attrIds_(std::move(ids))
{
    ASSERT(!attrIds_.empty());
}

std::string
NoneOfServiceAttributesCondition::sqlExpression() const
{
    return "(service_attrs IS NULL OR NOT service_attrs ?| ARRAY[" +
        common::join(
            attrIds_,
            [this](const std::string& attrId) { return txn_.quote(attrId); },
            ',')
        + "])";
}


CategoriesCondition::CategoriesCondition(Transaction& txn, const StringSet& ids)
    : AnyOfDomainAttributesCondition(txn, plainCategoryIdsToCanonical(ids))
{
}


CoveredByGeometryCondition::CoveredByGeometryCondition(Transaction& txn, Geom geom)
    : txn_(txn)
    , geom_(std::move(geom))
{
    ASSERT(!geom_.isNull());
}

std::string
CoveredByGeometryCondition::sqlExpression() const
{
    return "ST_CoveredBy(the_geom, ST_GeomFromWKB('"
            + txn_.esc_raw(geom_.wkb())
            + "',3395))";
}

IntersectsGeometryCondition::IntersectsGeometryCondition(Transaction& txn, Geom geom)
    : txn_(txn)
    , geom_(std::move(geom))
{
    ASSERT(!geom_.isNull());
}

std::string
IntersectsGeometryCondition::sqlExpression() const
{
    return "ST_Intersects(the_geom, ST_GeomFromWKB('"
            + txn_.esc_raw(geom_.wkb())
            + "',3395))";
}

WithinGeometryCondition::WithinGeometryCondition(Transaction& txn, Geom geom, double distance)
    : txn_(txn)
    , geom_(std::move(geom))
    , distance_(distance)
{
    ASSERT(!geom_.isNull());
}

std::string
WithinGeometryCondition::sqlExpression() const
{
    return "ST_DWithin(the_geom, ST_GeomFromWKB('"
            + txn_.esc_raw(geom_.wkb())
            + "',3395), " + std::to_string(distance_) + ")";
}


std::string
EnvelopeGeometryCondition::sqlExpression() const
{
    std::ostringstream clauseStream;
    clauseStream.precision(DOUBLE_FORMAT_PRECISION);
    clauseStream << "the_geom && ST_SetSRID(ST_MakeBox2D("
        << "ST_Point(" << envelope_.getMinX() << ',' << envelope_.getMinY() << "),"
        << "ST_Point(" << envelope_.getMaxX() << ',' << envelope_.getMaxY() << ")),"
        << "3395)";
    return clauseStream.str();
}

GenericSqlCondition::GenericSqlCondition(std::string sql)
    : sql_(std::move(sql))
{
    ASSERT(!sql_.empty());
}


std::string
ObjectsQuery::whereClause() const
{
    ASSERT(!conds_.empty());
    return common::join(
        conds_,
        [](const std::unique_ptr<Condition>& cond) { return cond->sqlExpression(); },
        " AND ");
}

bool ObjectsQuery::hasGeometry() const
{
    for (const auto& cond : conds_) {
        const auto* geomCond = dynamic_cast<const GeometryCondition*>(cond.get());
        if (geomCond) {
            return true;
        }
    }
    return false;
}

const std::string ObjectsQuery::VIEW_TABLE_ALIAS = "o";

ViewObjects
ObjectsQuery::exec(
    Transaction& work,
    TBranchId branchId,
    boost::optional<size_t> limit) const
{
    QueryBuilder qb(branchId);
    std::string fields;
    if (mode_ == Mode::Objects) {
        fields = "id, ";
    } else {
        fields = " DISTINCT ON (object_id) object_id AS id,";
    }
    fields += "commit_id, hstore_to_array(domain_attrs) AS domain, "
        "hstore_to_array(service_attrs) AS service, the_geom";
    qb.selectFields(fields);
    qb.fromTable(
        mode_ == Mode::Objects
            ? (hasGeometry() ? TABLE_OBJECTS_G : TABLE_OBJECTS)
            : TABLE_CONTOUR_OBJECTS_GEOM,
        VIEW_TABLE_ALIAS);
    qb.whereClause(whereClause());

    auto query = qb.query();
    if (limit) {
        query += " LIMIT " + std::to_string(*limit + 1);
    }
    DEBUG() << BOOST_CURRENT_FUNCTION << " QUERY: " << query;

    auto rows = work.exec(query);
    if (limit && rows.size() > *limit) {
        THROW_WIKI_LOGIC_ERROR(ERR_LIMIT_EXCEEDED, " Query limit " << *limit << " exceeded");
    }

    ViewObjects objectsFound;
    objectsFound.reserve(rows.size());
    for (const auto& dbtuple : rows) {
        objectsFound.emplace_back(dbtuple);
    }
    return objectsFound;
}

} // namespace maps::wiki::views
