#include "data_access.h"

#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/sql_chemistry/include/batch_load.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/collection.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/metadata_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/object_in_photo_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/walk_object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/export_gen/lib/tools.h>

#include <boost/lexical_cast.hpp>

namespace maps::mrc::browser {

namespace {

uint64_t getUserId(const db::Feature& feature)
{
    return !feature.userId().has_value() ||
                   feature.gdprDeleted().value_or(false) ||
                   !feature.showAuthorship().value_or(false)
               ? YANDEX_UID
               : boost::lexical_cast<uint64_t>(feature.userId().value());
}

}  // namespace

DbDataAccess::DbDataAccess(common::SharedPool pgPool)
    : pgPool_(std::move(pgPool))
{
    INFO() << "DB data access created";
}

db::Feature DbDataAccess::getFeatureById(db::TId featureId)
{
    return db::FeatureGateway(*pgPool_->pool().slaveTransaction())
        .loadOne(db::table::Feature::id == featureId &&
                 db::publishingFeatures());
}

db::Features DbDataAccess::getFeaturesByIds(const db::TIds& featureIds)
{
    if (featureIds.empty()) {
        return {};
    }
    return db::FeatureGateway(*pgPool_->pool().slaveTransaction())
        .load(db::table::Feature::id.in(featureIds) &&
              db::publishingFeatures());
}

std::optional<db::Feature> DbDataAccess::tryGetFeatureById(db::TId featureId)
{
    return db::FeatureGateway(*pgPool_->pool().slaveTransaction())
        .tryLoadOne(db::table::Feature::id == featureId &&
                    db::publishingFeatures());
}

db::Features DbDataAccess::getFeaturesByBbox(
    const geolib3::BoundingBox& geoBbox)
{
    return db::FeatureGateway(*pgPool_->pool().slaveTransaction())
        .load(db::table::Feature::pos.intersects(
                  geolib3::convertGeodeticToMercator(geoBbox)) &&
              db::publishingFeatures());
}

db::Features DbDataAccess::getWalkFeaturesByWalkObjectsBbox(
    const geolib3::BoundingBox& geoBbox)
{
    auto mercatorBbox = geolib3::convertGeodeticToMercator(geoBbox);
    return db::FeatureGateway{*pgPool_->pool().slaveTransaction()}.load(
        db::table::Feature::walkObjectId == db::table::WalkObject::id &&
        db::table::WalkObject::geometry.intersects(mercatorBbox) &&
        db::publishingFeatures());
}

db::Features DbDataAccess::getNearestFeatures(
    const geolib3::Point2& /* geoPos */,
    size_t /* limit */)
{
    throw LogicError() << "Not implemented";
}

bool DbDataAccess::existFeaturesInBbox(const geolib3::BoundingBox& geoBbox)
{
    return db::FeatureGateway(*pgPool_->pool().slaveTransaction()).exists(
        db::table::Feature::isPublished &&
        db::table::Feature::pos.intersects(
            geolib3::convertGeodeticToMercator(geoBbox)));
}

db::Features DbDataAccess::getFeaturesSequence(
    db::TId baseFeatureId,
    size_t beforeLimit,
    size_t afterLimit)
{
    return db::FeatureGateway(*pgPool_->pool().slaveTransaction())
            .loadSequence(baseFeatureId, beforeLimit, afterLimit);
}

db::ObjectsInPhoto DbDataAccess::getObjectsInPhotoByFeatureId(db::TId featureId)
{
    return db::ObjectInPhotoGateway(*pgPool_->pool().slaveTransaction())
        .load(db::table::ObjectInPhotoTable::featureId == featureId);
}

DbDataAccess::IdToPointMap DbDataAccess::getWalkObjectsByFeatures(
    const db::Features& walkPhotos)
{
    IdToPointMap result;

    db::TIds walkObjectIds;
    walkObjectIds.reserve(walkPhotos.size());
    for (const auto& walkPhoto : walkPhotos) {
        if (!walkPhoto.isPublished() && !walkPhoto.shouldBePublished()) {
            continue;
        }
        if (walkPhoto.walkObjectId().has_value()) {
            walkObjectIds.push_back(walkPhoto.walkObjectId().value());
        }
    }

    auto walkObjects = db::WalkObjectGateway{*pgPool_->pool().slaveTransaction()}.load(
        db::table::WalkObject::id.in(walkObjectIds));
    auto walkObjectsById = common::byId(std::move(walkObjects));

    for (const auto& walkPhoto : walkPhotos) {
        if (walkPhoto.walkObjectId().has_value()) {
            auto walkObjectIt = walkObjectsById.find(walkPhoto.walkObjectId().value());
            if (walkObjectIt != walkObjectsById.end()) {
                const auto& object = walkObjectIt->second;
                if (object.geometryType() == geolib3::GeometryType::Point) {
                    result.emplace(
                        walkPhoto.id(),
                        std::get<geolib3::Point2>(object.geodeticGeometry()));
                }
            }
        }
    }
    return result;
}

bool DbDataAccess::isUnpublished(db::TId featureId) const
{
    auto txn = pgPool_->pool().slaveTransaction();
    const auto feature = db::FeatureGateway(*txn).tryLoadById(featureId);
    return feature && !feature->shouldBePublished() && !feature->isPublished();
}

uint64_t DbDataAccess::getUid(const db::Feature& feature)
{
    return getUserId(feature);
}

DbDataAccess::IdToUidMap DbDataAccess::getUids(const db::Features& features)
{
    IdToUidMap result;
    result.reserve(features.size());
    for (const auto& feature : features) {
        result.emplace(feature.id(), getUserId(feature));
    }
    return result;
}

std::string DbDataAccess::getVersion()
{
    static const std::string DEFAULT_VERSION = "0";

    auto txn = pgPool_->pool().slaveTransaction();
    db::MetadataGateway gtw(*txn);
    return gtw.tryLoadByKey(export_gen::LAST_RUN_TIME, DEFAULT_VERSION);
}

} // namespace maps::mrc::browser
