#include "db.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/parallel_for_each.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/house_number_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/hypothesis_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/panorama_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/sign_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/traffic_light_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/traffic_light_hypothesis_gateway.h>
#include <maps/libs/common/include/make_batches.h>

#include <boost/lexical_cast.hpp>
#include <boost/range/algorithm/copy.hpp>

namespace maps::mrc::feedback_stat {

namespace {

/// object examples: db::HouseNumber, db::Sign, db::TrafficLight
template <class Table>
constexpr auto& objectId();

template <>
constexpr auto& objectId<db::table::HouseNumberFeature>()
{
    return db::table::HouseNumberFeature::houseNumberId;
}

template <>
constexpr auto& objectId<db::table::SignFeature>()
{
    return db::table::SignFeature::signId;
}

template <>
constexpr auto& objectId<db::table::TrafficLightFeature>()
{
    return db::table::TrafficLightFeature::trafficLightId;
}

TId objectId(const db::HouseNumber& entity)
{
    return entity.id();
}

TId objectId(const db::HouseNumberFeature& entity)
{
    return entity.houseNumberId();
}

TId objectId(const db::HouseNumberPanorama& entity)
{
    return entity.houseNumberId();
}

TId objectId(const db::Hypothesis& entity)
{
    return entity.signId();
}

TId objectId(const db::SignFeature& entity)
{
    return entity.signId();
}

TId objectId(const db::SignPanorama& entity)
{
    return entity.signId();
}

TId objectId(const db::TrafficLightHypothesis& entity)
{
    return entity.id();
}

TId objectId(const db::TrafficLightFeature& entity)
{
    return entity.trafficLightId();
}

TId objectId(const db::TrafficLightPanorama& entity)
{
    return entity.trafficLightId();
}

template <class ObjectTable>
TIdMap loadFeedbackIdToObjectIdMapFrom(pgpool3::Pool& pool)
{
    TIdMap result;
    sql_chemistry::BatchLoad<ObjectTable> batch{
        BATCH_SIZE, ObjectTable::feedbackTaskId > 0};
    while (batch.next(*pool.slaveTransaction())) {
        for (const auto& object : batch) {
            result.insert({object.feedbackTaskId(), objectId(object)});
        }
        INFO() << ObjectTable::name_ << ": " << result.size() << " rows read";
    }
    return result;
}

template <class ObjectFeatureTable>
TIdMultiMap loadObjectIdToFeatureIdMultiMapFrom(pgpool3::Pool& pool,
                                                const TIdSet& objectIds)
{
    TIdMultiMap result;
    auto batches = maps::common::makeBatches(objectIds, BATCH_SIZE);
    common::parallelForEach<MAX_THREADS>(
        batches.begin(),
        batches.end(),
        [&](std::mutex& guard, const auto& batch) {
            auto objectFeatures =
                sql_chemistry::Gateway<ObjectFeatureTable>{
                    *pool.slaveTransaction()}
                    .load(objectId<ObjectFeatureTable>().in(
                        {batch.begin(), batch.end()}));

            TIdMultiMap partialResult;
            for (const auto& objectFeature : objectFeatures) {
                partialResult.insert(
                    {objectId(objectFeature), objectFeature.featureId()});
            }

            std::lock_guard lock{guard};
            result.insert(partialResult.begin(), partialResult.end());
            INFO() << ObjectFeatureTable::name_ << ": " << result.size()
                   << " rows read";
        });
    return result;
}

template <class ObjectPanoramaTable>
TIdMultiMap loadObjectIdToPanoramaIdMultiMapFrom(pgpool3::Pool& pool,
                                                 const TIdSet& objectIds)
{
    TIdMultiMap result;
    sql_chemistry::BatchLoad<ObjectPanoramaTable> batch{BATCH_SIZE};
    while (batch.next(*pool.slaveTransaction())) {
        for (const auto& objectPanorama : batch) {
            if (objectIds.count(objectId(objectPanorama))) {  // no DB index
                result.insert(
                    {objectId(objectPanorama), objectPanorama.panoramaId()});
            }
        }
        INFO() << ObjectPanoramaTable::name_ << ": " << result.size()
               << " rows read";
    }
    return result;
}

void insertOrMinSignal(TId feedbackId,
                       TId objectId,
                       const TIdMultiMap& objectIdToSignalIdMultiMap,
                       const TIdToSignalMap& signals,
                       TIdToSignalMap& feedbackIdToSignalMap)
{
    auto [first, last] = objectIdToSignalIdMultiMap.equal_range(objectId);
    while (first != last) {
        auto [objectId, signalId] = *first++;
        auto signal = signals.at(signalId);
        auto [it, success] = feedbackIdToSignalMap.insert({feedbackId, signal});
        if (!success) {
            it->second = std::min(it->second, signal);
        }
    }
}

class DataDomain {
    TIdMap feedbackIdToObjectIdMap_;
    TIdMultiMap objectIdToFeatureIdMultiMap_;
    TIdMultiMap objectIdToPanoramaIdMultiMap_;

public:
    template <class ObjectTable,
              class ObjectFeatureTable,
              class ObjectPanoramaTable>
    static DataDomain loadFrom(pgpool3::Pool& pool)
    {
        DataDomain result;
        result.feedbackIdToObjectIdMap_ =
            loadFeedbackIdToObjectIdMapFrom<ObjectTable>(pool);
        auto objectIds = mapValues(result.feedbackIdToObjectIdMap_);
        result.objectIdToFeatureIdMultiMap_ =
            loadObjectIdToFeatureIdMultiMapFrom<ObjectFeatureTable>(pool,
                                                                    objectIds);
        result.objectIdToPanoramaIdMultiMap_ =
            loadObjectIdToPanoramaIdMultiMapFrom<ObjectPanoramaTable>(
                pool, objectIds);
        return result;
    }

    auto featureIds() const
    {
        return objectIdToFeatureIdMultiMap_ | boost::adaptors::map_values;
    }

    auto panoramaIds() const
    {
        return objectIdToPanoramaIdMultiMap_ | boost::adaptors::map_values;
    }

    void fillFeedbackIdToSignalMap(const TIdToSignalMap& features,
                                   const TIdToSignalMap& panoramas,
                                   TIdToSignalMap& result) const
    {
        for (auto [feedbackId, objectId] : feedbackIdToObjectIdMap_) {
            insertOrMinSignal(feedbackId,
                              objectId,
                              objectIdToFeatureIdMultiMap_,
                              features,
                              result);
            insertOrMinSignal(feedbackId,
                              objectId,
                              objectIdToPanoramaIdMultiMap_,
                              panoramas,
                              result);
        }
    }
};

TIdToSignalMap loadFeatureIdToSignalMap(pgpool3::Pool& pool,
                                        const TIdSet& featureIds)
{
    TIdToSignalMap result;
    auto batches = maps::common::makeBatches(featureIds, BATCH_SIZE);
    common::parallelForEach<MAX_THREADS>(
        batches.begin(),
        batches.end(),
        [&](std::mutex& guard, const auto& batch) {
            auto features = db::FeatureGateway{*pool.slaveTransaction()}.load(
                db::table::Feature::id.in({batch.begin(), batch.end()}));

            TIdToSignalMap partialResult;
            for (const auto& feature : features) {
                auto signal = Signal{.createdAt = feature.timestamp(),
                                     .source = convertToSource(feature.dataset())};
                partialResult.insert({feature.id(), signal});
            }

            std::lock_guard lock{guard};
            result.insert(partialResult.begin(), partialResult.end());
            INFO() << db::table::Feature::name_ << ": " << result.size()
                   << " rows read";
        });
    return result;
}

TIdToSignalMap loadPanoramaIdToSignalMap(pgpool3::Pool& pool,
                                         const TIdSet& panoramaIds)
{
    TIdToSignalMap result;
    auto batches = maps::common::makeBatches(panoramaIds, BATCH_SIZE);
    common::parallelForEach<MAX_THREADS>(
        batches.begin(),
        batches.end(),
        [&](std::mutex& guard, const auto& batch) {
            auto panoramas = db::PanoramaGateway{*pool.slaveTransaction()}.load(
                db::table::Panorama::panoramaId.in(
                    {batch.begin(), batch.end()}));

            TIdToSignalMap partialResult;
            for (const auto& panorama : panoramas) {
                auto signal = Signal{.createdAt = panorama.date(),
                                     .source = Source::Panorama};
                partialResult.insert({panorama.panoramaId(), signal});
            }

            std::lock_guard lock{guard};
            result.insert(partialResult.begin(), partialResult.end());
            INFO() << db::table::Panorama::name_ << ": " << result.size()
                   << " rows read";
        });
    return result;
}

}  // namespace

TIdToSignalMap loadFeedbackIdToSignalMap(pgpool3::Pool& pool)
{
    auto dataDomains = {
        DataDomain::loadFrom<db::table::HouseNumber,
                             db::table::HouseNumberFeature,
                             db::table::HouseNumberPanorama>(pool),
        DataDomain::loadFrom<db::table::Hypothesis,
                             db::table::SignFeature,
                             db::table::SignPanorama>(pool),
        DataDomain::loadFrom<db::table::TrafficLightHypothesis,
                             db::table::TrafficLightFeature,
                             db::table::TrafficLightPanorama>(pool)};

    TIdSet featureIds;
    TIdSet panoramaIds;
    for (const auto& dataDomain : dataDomains) {
        boost::range::copy(dataDomain.featureIds(),
                           std::inserter(featureIds, featureIds.end()));
        boost::range::copy(dataDomain.panoramaIds(),
                           std::inserter(panoramaIds, panoramaIds.end()));
    }
    auto features = loadFeatureIdToSignalMap(pool, featureIds);
    auto panoramas = loadPanoramaIdToSignalMap(pool, panoramaIds);

    TIdToSignalMap result;
    for (const auto& dataDomain : dataDomains) {
        dataDomain.fillFeedbackIdToSignalMap(features, panoramas, result);
    }
    return result;
}

}  // namespace maps::mrc::feedback_stat
