#include "hypotheses.h"
#include "utility.h"

#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/collection.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/hypothesis_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/recognition_gateway.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/ride_gateway.h>

namespace maps::mrc::ride_inspector {
namespace {

const std::string APP_NAME = "ride-inspector";
const std::string HYPOTHESIS_ID_KEY = APP_NAME + ".hypothesis_id";
const std::string HYPOTHESIS_FEEDBACK_TXN_ID_KEY =
    APP_NAME + ".hypothesis_feedback.txn_id";

auto hypothesesQueueFilter(db::TId txnId, db::TId hypothesisId)
{
    return (db::eye::table::HypothesisFeedback::txnId == txnId &&
            db::eye::table::HypothesisFeedback::hypothesisId > hypothesisId) ||
           (db::eye::table::HypothesisFeedback::txnId > txnId);
}

sql_chemistry::FiltersCollection featureFilter(const db::Ride& ride)
{
    auto result =
        sql_chemistry::FiltersCollection{sql_chemistry::op::Logical::And};
    if (ride.clientId()) {
        result.add(db::table::Feature::clientRideId == *ride.clientId());
    }
    else {
        result.add(db::table::Feature::clientRideId.isNull());
        result.add(db::table::Feature::userId == ride.userId());
        result.add(db::table::Feature::sourceId == ride.sourceId());
        result.add(
            db::table::Feature::date.between(ride.startTime(), ride.endTime()));
    }
    result.add(db::table::Feature::dataset == db::Dataset::Rides);
    result.add(!db::table::Feature::deletedByUser.is(true));
    result.add(!db::table::Feature::gdprDeleted.is(true));
    return result;
}

sql_chemistry::FiltersCollection rideFilter(const db::Feature& feature)
{
    ASSERT(feature.dataset() == db::Dataset::Rides);
    auto result =
        sql_chemistry::FiltersCollection{sql_chemistry::op::Logical::And};
    if (feature.clientRideId()) {
        result.add(db::table::Ride::clientId == *feature.clientRideId());
    }
    else {
        result.add(db::table::Ride::clientId.isNull());
        result.add(db::table::Ride::userId == getUserId(feature));
        result.add(db::table::Ride::sourceId == getSourceId(feature));
        result.add(db::table::Ride::startTime <=
                   feature.timestamp() + MIN_TIME_GAP_BETWEEN_RIDES);
        result.add(db::table::Ride::endTime >=
                   feature.timestamp() - MIN_TIME_GAP_BETWEEN_RIDES);
    }
    result.add(!db::table::Ride::isDeleted.is(true));
    return result;
}

std::optional<db::TId> tryLoadFeatureIdByHypothesisId(
    sql_chemistry::Transaction& txn,
    db::TId hypothesisId)
{
    auto hypothesisObjects = db::eye::HypothesisObjectGateway{txn}.load(
        db::eye::table::HypothesisObject::hypothesisId == hypothesisId &&
        db::eye::table::HypothesisObject::deleted.is(false));
    auto objectIds =
        invokeForEach(&db::eye::HypothesisObject::objectId, hypothesisObjects);
    auto objects = db::eye::ObjectGateway{txn}.load(
        db::eye::table::Object::id.in(objectIds) &&
        db::eye::table::Object::deleted.is(false));
    auto primaryDetectionIds =
        invokeForEach(&db::eye::Object::primaryDetectionId, objects);
    auto primaryDetectionRelations =
        db::eye::PrimaryDetectionRelationGateway{txn}.load(
            db::eye::table::PrimaryDetectionRelation::primaryDetectionId.in(
                primaryDetectionIds) &&
            db::eye::table::PrimaryDetectionRelation::deleted.is(false));
    auto detectionIds =
        invokeForEach(&db::eye::PrimaryDetectionRelation::detectionId,
                      primaryDetectionRelations);
    detectionIds.insert(detectionIds.end(),
                        primaryDetectionIds.begin(),
                        primaryDetectionIds.end());
    common::sortUnique(detectionIds);
    auto detections = db::eye::DetectionGateway{txn}.load(
        db::eye::table::Detection::id.in(detectionIds) &&
        db::eye::table::Detection::deleted.is(false));
    auto groupIds = invokeForEach(&db::eye::Detection::groupId, detections);
    auto detectionGroups = db::eye::DetectionGroupGateway{txn}.load(
        db::eye::table::DetectionGroup::id.in(groupIds));
    auto frameIds =
        invokeForEach(&db::eye::DetectionGroup::frameId, detectionGroups);
    auto featureToFrames = db::eye::FeatureToFrameGateway{txn}.load(
        db::eye::table::FeatureToFrame::frameId.in(frameIds));
    auto featureIds =
        invokeForEach(&db::eye::FeatureToFrame::featureId, featureToFrames);
    featureIds = db::FeatureGateway{txn}.loadIds(
        db::table::Feature::id.in(featureIds) &&
            !db::table::Feature::deletedByUser.is(true) &&
            !db::table::Feature::gdprDeleted.is(true),
        orderBy(db::table::Feature::id).limit(1));
    return featureIds.empty() ? std::nullopt
                              : std::optional{featureIds.front()};
}

db::TIds loadHypothesisIdsByFeatureIds(sql_chemistry::Transaction& txn,
                                       const db::TIds& featureIds)
{
    auto featureToFrames = db::eye::FeatureToFrameGateway{txn}.load(
        db::eye::table::FeatureToFrame::featureId.in(featureIds));
    auto frameIds =
        invokeForEach(&db::eye::FeatureToFrame::frameId, featureToFrames);
    auto groupIds = db::eye::DetectionGroupGateway{txn}.loadIds(
        db::eye::table::DetectionGroup::frameId.in(frameIds));
    auto detectionIds = db::eye::DetectionGateway{txn}.loadIds(
        db::eye::table::Detection::groupId.in(groupIds) &&
        db::eye::table::Detection::deleted.is(false));
    auto primaryDetectionRelations =
        db::eye::PrimaryDetectionRelationGateway{txn}.load(
            db::eye::table::PrimaryDetectionRelation::detectionId.in(
                detectionIds) &&
            db::eye::table::PrimaryDetectionRelation::deleted.is(false));
    auto primaryDetectionIds =
        invokeForEach(&db::eye::PrimaryDetectionRelation::primaryDetectionId,
                      primaryDetectionRelations);
    detectionIds.insert(detectionIds.end(),
                        primaryDetectionIds.begin(),
                        primaryDetectionIds.end());
    common::sortUnique(detectionIds);
    auto objectIds = db::eye::ObjectGateway{txn}.loadIds(
        db::eye::table::Object::primaryDetectionId.in(detectionIds) &&
        db::eye::table::Object::deleted.is(false));
    auto hypothesisObjects = db::eye::HypothesisObjectGateway{txn}.load(
        db::eye::table::HypothesisObject::objectId.in(objectIds) &&
        db::eye::table::HypothesisObject::deleted.is(false));
    auto hypothesisIds = invokeForEach(&db::eye::HypothesisObject::hypothesisId,
                                       hypothesisObjects);
    hypothesisIds = db::eye::HypothesisGateway{txn}.loadIds(
        db::eye::table::Hypothesis::id.in(hypothesisIds) &&
        db::eye::table::Hypothesis::deleted.is(false));
    auto hypothesisFeedbacks = db::eye::HypothesisFeedbackGateway{txn}.load(
        db::eye::table::HypothesisFeedback::hypothesisId.in(hypothesisIds));
    return invokeForEach(&db::eye::HypothesisFeedback::hypothesisId,
                         hypothesisFeedbacks);
}

}  // namespace

bool hypothesesQueuePop(pgpool3::Pool& pool,
                        const HypothesisIdConsumer& consume)
try {
    auto txn = pool.masterWriteableTransaction();
    auto txnId = db::MetadataGateway{*txn}.tryLoadByKey(
        HYPOTHESIS_FEEDBACK_TXN_ID_KEY, db::TId{});
    auto hypothesisId =
        db::MetadataGateway{*txn}.tryLoadByKey(HYPOTHESIS_ID_KEY, db::TId{});
    auto hypothesisFeedbacks = db::eye::HypothesisFeedbackGateway{*txn}.load(
        hypothesesQueueFilter(txnId, hypothesisId),
        orderBy(db::eye::table::HypothesisFeedback::txnId)
            .orderBy(db::eye::table::HypothesisFeedback::hypothesisId)
            .limit(1));
    if (hypothesisFeedbacks.empty()) {
        return false;
    }
    auto& hypothesisFeedback = hypothesisFeedbacks.front();
    consume(*txn, hypothesisFeedback.hypothesisId());
    db::MetadataGateway{*txn}.upsertByKey(HYPOTHESIS_FEEDBACK_TXN_ID_KEY,
                                          hypothesisFeedback.txnId());
    db::MetadataGateway{*txn}.upsertByKey(HYPOTHESIS_ID_KEY,
                                          hypothesisFeedback.hypothesisId());
    txn->commit();
    return true;
}
catch (const maps::Exception& e) {
    WARN() << e;
    return false;
}

size_t hypothesesQueueSize(sql_chemistry::Transaction& txn)
{
    auto txnId = db::MetadataGateway{txn}.tryLoadByKey(
        HYPOTHESIS_FEEDBACK_TXN_ID_KEY, db::TId{});
    auto hypothesisId =
        db::MetadataGateway{txn}.tryLoadByKey(HYPOTHESIS_ID_KEY, db::TId{});
    return db::eye::HypothesisFeedbackGateway{txn}.count(
        hypothesesQueueFilter(txnId, hypothesisId));
}

std::optional<db::RideHypothesis> tryMakeRideHypothesis(
    sql_chemistry::Transaction& txn,
    db::TId hypothesisId)
{
    auto featureId = tryLoadFeatureIdByHypothesisId(txn, hypothesisId);
    if (!featureId) {
        return std::nullopt;
    }
    auto feature = db::FeatureGateway{txn}.tryLoadById(*featureId);
    if (!feature || feature->dataset() != db::Dataset::Rides) {
        return std::nullopt;
    }
    auto rideIds = db::RideGateway{txn}.loadIds(rideFilter(*feature));
    if (rideIds.size() != 1) {
        WARN() << "feature " << feature->id() << " belongs to "
               << rideIds.size() << " rides";
        return std::nullopt;
    }
    return db::RideHypothesis{rideIds.front(), hypothesisId};
}

db::RideHypotheses makeRideHypotheses(sql_chemistry::Transaction& txn,
                                      const db::Ride& ride)
{
    if (ride.isDeleted()) {
        return {};
    }
    auto featureIds = db::FeatureGateway{txn}.loadIds(featureFilter(ride));
    auto featureIdSet = db::TIdSet{featureIds.begin(), featureIds.end()};
    auto hypothesisIds = loadHypothesisIdsByFeatureIds(txn, featureIds);
    auto result = db::RideHypotheses{};
    for (auto hypothesisId : hypothesisIds) {
        auto featureId = tryLoadFeatureIdByHypothesisId(txn, hypothesisId);
        if (featureId && featureIdSet.contains(*featureId)) {
            result.emplace_back(ride.rideId(), hypothesisId);
        }
    }
    return result;
}

}  // namespace maps::mrc::ride_inspector
