#include "poi.h"
#include <maps/libs/geolib/include/conversion.h>
#include <yandex/maps/wiki/common/string_utils.h>

using namespace std::string_literals;

namespace maps::wiki::socialsrv {

namespace {

const auto DOUBLE_FORMAT_PRECISION = 12;

const auto TABLE_TRUNK_OBJECTS_A = "vrevisions_trunk.objects_a_view"s;
const auto TABLE_TRUNK_OBJECTS_P = "vrevisions_trunk.objects_p_view"s;
const auto TABLE_TRUNK_OBJECTS_R = "vrevisions_trunk.objects_r"s;

const auto SELECT_VIEW_FIELDS = "id, ST_X(the_geom), ST_Y(the_geom)"s;
const auto SELECT_VIEW_FIELDS_WITH_INDOOR =
    SELECT_VIEW_FIELDS + ", service_attrs -> 'srv:indoor_level_id'"s;

enum ViewFields {
    ObjectId,
    X,
    Y,
    IndoorLevelId,
};

PoiData makePoiData(const pqxx::row& row)
{
    return PoiData{
        .objectId = row[ViewFields::ObjectId].as<social::TId>(0),
        .positionMercator = geolib3::Point2(
            row[ViewFields::X].as<double>(),
            row[ViewFields::Y].as<double>())
    };
}

BusinessData makeBusinessData(const pqxx::row& row)
{
    BusinessData result;
    static_cast<PoiData&>(result) = makePoiData(row);
    result.feedbackType = row[ViewFields::IndoorLevelId].is_null()
        ? social::feedback::Type::Poi
        : social::feedback::Type::Indoor;
    return result;
}

std::vector<PoiData> makePoiDatas(const pqxx::result& rows)
{
    std::vector<PoiData> result;
    result.reserve(rows.size());
    for (const auto& row : rows) {
        result.push_back(makePoiData(row));
    }
    return result;
}

std::vector<PoiData> loadEntrances(
    pqxx::transaction_base& txnViewTrunk,
    social::TId objectId)
{
    std::ostringstream query;
    query <<
        "SELECT " << SELECT_VIEW_FIELDS <<
        " FROM " << TABLE_TRUNK_OBJECTS_P <<
        " WHERE id IN ("
            "SELECT slave_id FROM " << TABLE_TRUNK_OBJECTS_R <<
            " WHERE domain_attrs @> hstore('rel:role', 'entrance_assigned')"
            " AND master_id = " << objectId << ")";

    return makePoiDatas(txnViewTrunk.exec(query.str()));
}

std::string makeQueryPosition(const geolib3::Point2& positionMercator)
{
    std::ostringstream query;
    query.precision(DOUBLE_FORMAT_PRECISION);
    query << "ST_SetSRID(ST_Point("
          << positionMercator.x() << ", " << positionMercator.y() << "), 3395)";
    return query.str();
}

bool isIndoorPoi(
    pqxx::transaction_base& txnViewTrunk,
    const geolib3::Point2& positionMercator)
{
    std::ostringstream query;
    query <<
        "SELECT "
        " FROM " << TABLE_TRUNK_OBJECTS_A <<
        " WHERE domain_attrs ? 'cat:indoor_level'"
        " AND the_geom && " << makeQueryPosition(positionMercator) <<
        " LIMIT 1";
    return !txnViewTrunk.exec(query.str()).empty();
}

social::feedback::Type detectFeedbackType(
    pqxx::transaction_base& txnViewTrunk,
    const BusinessData& businessData,
    const geolib3::Point2& positionMercator)
{
    if (businessData.feedbackType == social::feedback::Type::Indoor ||
        isIndoorPoi(txnViewTrunk, businessData.positionMercator))
    {
        return social::feedback::Type::Indoor;
    }
    return (businessData.positionMercator != positionMercator) &&
            isIndoorPoi(txnViewTrunk, positionMercator)
        ? social::feedback::Type::Indoor
        : social::feedback::Type::Poi;
}

} // namespace

std::optional<CompanyData> findCompany(
    DbPoolsWithViewTrunk& dbPools,
    const SpravData& spravData,
    double distance)
{
    const auto positionMercator = geolib3::geoPoint2Mercator(spravData.positionGeo);
    auto positionStr = makeQueryPosition(positionMercator);

    auto makeQuery = [&] (const auto& clause) {
        return
            "SELECT " + SELECT_VIEW_FIELDS_WITH_INDOOR +
            " FROM " + TABLE_TRUNK_OBJECTS_P +
            " WHERE " + clause +
            " ORDER BY ST_Distance(the_geom, " + positionStr + ")"
            " LIMIT 1";
    };

    auto clauseByPermalinkIds = [&] {
        ASSERT(!spravData.clusterPermalinkIds.empty()); // businessId already included

        auto businessIds = common::join(
            spravData.clusterPermalinkIds,
            [](const auto& id) { return "'" + std::to_string(id) + "'"; },
            ',');
        auto distanceMercator = geolib3::toMercatorUnits(distance, positionMercator);

        std::ostringstream query;
        query.precision(DOUBLE_FORMAT_PRECISION);
        query <<
            "ST_DWithin(the_geom, " << positionStr << ", " << distanceMercator << ")"
            " AND domain_attrs ? 'poi:business_id'"
            " AND domain_attrs -> 'poi:business_id' IN (" << businessIds << ")";
        return query.str();
    };

    auto txn = dbPools.viewTrunkTxn();

    auto makeResult = [&](const auto& row) {
        auto businessData = makeBusinessData(row);
        businessData.feedbackType = detectFeedbackType(*txn, businessData, positionMercator);
        auto entrances = loadEntrances(*txn, businessData.objectId);

        return CompanyData{
            .business = std::move(businessData),
            .entrances = std::move(entrances)
        };
    };

    auto rows = txn->exec(makeQuery(clauseByPermalinkIds()));
    if (!rows.empty()) {
        return makeResult(rows[0]);
    }
    if (!spravData.objectIds.empty()) {
        auto rows = txn->exec(makeQuery(
            "id IN (" + common::join(spravData.objectIds, ',') + ")"));
        if (!rows.empty()) {
            return makeResult(rows[0]);
        }
    }
    return std::nullopt;
}

std::optional<CompanyData> findCompanyByObjectId(
    DbPoolsWithViewTrunk& dbPools,
    social::TId objectId)
{
    std::ostringstream query;
    query <<
        "SELECT " << SELECT_VIEW_FIELDS_WITH_INDOOR <<
        " FROM " << TABLE_TRUNK_OBJECTS_P <<
        " WHERE id = " << objectId;

    auto txn = dbPools.viewTrunkTxn();
    auto rows = txn->exec(query.str());
    if (rows.empty()) {
        return std::nullopt;
    }
    return CompanyData{
        .business = makeBusinessData(rows[0]),
        .entrances = loadEntrances(*txn, objectId)
    };
}

} // namespace maps::wiki::socialsrv
