#include "util.h"
#include "common.h"
#include "eye/hypothesis.h"
#include "eye/hypothesis_gateway.h"
#include "eye/object.h"
#include "eye/object_gateway.h"

#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/for_each_batch.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/hypothesis_gateway.h>

#include <maps/wikimap/mapspro/libs/social/include/yandex/maps/wiki/social/feedback/enums.h>

#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/geolib/include/test_tools/io_operations.h>
#include <maps/libs/sql_chemistry/include/gateway.h>
#include <maps/libs/sql_chemistry/include/column.h>
#include <maps/libs/sql_chemistry/include/order.h>
#include <maps/libs/introspection/include/stream_output.h>

#include <tuple>


namespace maps::geolib3 {
using maps::geolib3::io::operator<<;
}

namespace maps::mrc::db::eye {

using maps::introspection::operator<<;

} // namespace maps::mrc::db::eye

namespace maps::mrc::eye::qa {

namespace {

constexpr size_t DB_BATCH_SIZE = 1000;

struct RevisionCommit {
    db::TId id{};
    bool trunk;
    chrono::TimePoint createdAt;
    std::int64_t createdBy;

    RevisionCommit() = default;

    template<class T>
    static auto introspect(T& t)
    {
        return std::tie(t.id, t.trunk, t.createdAt, t.createdBy);
    }
};

struct RevisionCommitTable : sql_chemistry::Table<RevisionCommit> {
    static constexpr std::string_view name_{"revision.commit"sv};

    static constexpr sql_chemistry::BigSerialKey id{"id"sv, name_};
    static constexpr sql_chemistry::BooleanColumn trunk{"trunk"sv, name_};
    static constexpr sql_chemistry::TimePointColumn createdAt{"created"sv, name_};
    static constexpr sql_chemistry::Int64Column createdBy{"created_by"sv, name_};

    auto columns_() const { return std::tie(id, trunk, createdAt, createdBy); }
};

struct CommitFeedbackTask {
    db::TId commitId;
    db::TId feedbackTaskId;

    CommitFeedbackTask() = default;

    template<class T>
    static auto introspect(T& t)
    {
        return std::tie(t.commitId, t.feedbackTaskId);
    }
};

struct CommitFeedbackTaskTable : sql_chemistry::Table<CommitFeedbackTask> {
    static constexpr std::string_view name_{"social.commit_feedback_task"sv};

    static constexpr sql_chemistry::Int64Column commitId{"commit_id"sv, name_};
    static constexpr sql_chemistry::Int64Column feedbackTaskId{"feedback_task_id"sv, name_};

    auto columns_() const { return std::tie(commitId, feedbackTaskId); }
};

struct FeedbackTask {
    db::TId id;
    std::optional<wiki::social::feedback::Verdict> verdict;
    std::optional<chrono::TimePoint> resolvedAt;

    template<class T>
    static auto introspect(T& t)
    {
        return std::tie(t.id, t.verdict, t.resolvedAt);
    }
};

struct FeedbackTaskTable : sql_chemistry::Table<FeedbackTask> {
    static constexpr std::string_view name_{"social.feedback_task"sv};

    static constexpr sql_chemistry::Int64Column id{"id"sv, name_};
    static constexpr sql_chemistry::NullableEnumColumn<wiki::social::feedback::Verdict> resolution{"resolution"sv, name_};
    static constexpr sql_chemistry::NullableTimePointColumn resolvedAt{"resolved_at"sv, name_};

    auto columns_() const { return std::tie(id, resolution, resolvedAt); }
};

} // namespace

db::eye::Objects
loadObjects(pqxx::transaction_base& txn, const geolib3::BoundingBox& mercBbox)
{
    return db::eye::ObjectGateway(txn).load(
        db::eye::table::ObjectLocation::position.intersects(mercBbox) &&
        db::eye::table::ObjectLocation::objectId ==
            db::eye::table::Object::id &&
        ! db::eye::table::Object::deleted
    );
}

db::TId evalWikiRevisionCommitIdByDate(pqxx::transaction_base& txn, chrono::TimePoint date)
{
    constexpr std::int64_t WIKIMAPS_BLD_UID = 684818229;
    constexpr std::int64_t WIKIMAPS_SPRAV_UID = 685892448;
    const auto commits = sql_chemistry::Gateway<RevisionCommitTable>{txn}
        .load(
            RevisionCommitTable::createdBy.in({WIKIMAPS_BLD_UID, WIKIMAPS_SPRAV_UID}) &&
                RevisionCommitTable::createdAt < date,
            sql_chemistry::orderBy(RevisionCommitTable::createdAt)
                .desc()
                .limit(1));
    ASSERT(!commits.empty());
    INFO() << "Selected wiki commit " << commits.front().id << " " << chrono::formatSqlDateTime(commits.front().createdAt);
    return commits.front().id;
}

std::unordered_map<db::TId, FeedbackResult> loadFeedbackResult(
    pqxx::transaction_base& txn, const db::TIds& feedbackTaskIds)
{
    std::unordered_map<db::TId, FeedbackResult> result;
    sql_chemistry::Gateway<FeedbackTaskTable> feedbackTaskGtw{txn};
    sql_chemistry::Gateway<CommitFeedbackTaskTable> commitFeedbackTaskGtw{txn};
    common::forEachBatch(feedbackTaskIds, DB_BATCH_SIZE,
        [&](auto begin, auto end){
            db::TIds batch(begin, end);
            auto feedbackTasks = feedbackTaskGtw.load(FeedbackTaskTable::id.in(batch));
            for (const auto& feedbackTask: feedbackTasks) {
                result.emplace(
                    feedbackTask.id,
                    FeedbackResult{
                        .isFeedbackProcessed = feedbackTask.verdict.has_value(),
                        .processedAt = feedbackTask.resolvedAt});
            }

            auto feedbackCommits = commitFeedbackTaskGtw.load(CommitFeedbackTaskTable::feedbackTaskId.in(batch));
            for (const auto& feedbackCommit : feedbackCommits) {
                result.at(feedbackCommit.feedbackTaskId).hasCommits = true;
            }
        });
    return result;
}

std::vector<HypothesisWithFeedback> loadHypothesesWithFeedback(
    pqxx::transaction_base& mrcTxn,
    pqxx::transaction_base& socialTxn,
    const db::TIds& objectIds)
{
    std::vector<HypothesisWithFeedback> result;
    db::eye::HypothesisGateway gtw{mrcTxn};
    common::forEachBatch(objectIds, DB_BATCH_SIZE,
        [&](auto begin, auto end){
            db::TIds batch(begin, end);
            auto hypotheses = gtw.loadJoined<db::TId, db::TId>(
                std::make_tuple(
                    db::eye::table::HypothesisFeedback::feedbackId,
                    db::eye::table::HypothesisObject::objectId),
                db::eye::table::HypothesisFeedback::hypothesisId == db::eye::table::Hypothesis::id &&
                db::eye::table::HypothesisObject::hypothesisId == db::eye::table::Hypothesis::id &&
                db::eye::table::Hypothesis::id == db::eye::table::HypothesisObject::hypothesisId &&
                db::eye::table::HypothesisObject::objectId.in(batch));
            result.reserve(result.size() + hypotheses.size());
            for (auto& [feedbackId, objectId, hypothesis] : hypotheses) {
                result.push_back(HypothesisWithFeedback{
                    .hypothesis = std::move(hypothesis),
                    .objectId = objectId,
                    .feedbackTaskId = feedbackId});
            }
        });

    db::TIds feedbackIds;
    feedbackIds.reserve(result.size());
    for (const auto& hypothesis : result) {
        feedbackIds.push_back(hypothesis.feedbackTaskId);
    }

    auto feedbackIdToResult = loadFeedbackResult(socialTxn, feedbackIds);
    for (auto& hypothesisWithResult : result) {
        hypothesisWithResult.result = feedbackIdToResult.at(hypothesisWithResult.feedbackTaskId);
    }

    return result;
}

db::TIdSet objectIdsSet(const std::vector<HypothesisWithFeedback>& hypotheses)
{
    db::TIdSet result;
    for (const auto& hypothesis : hypotheses) {
        result.insert(hypothesis.objectId);
    }
    return result;
}

HypothesesStatistics calculateStatistics(
    const std::vector<HypothesisWithFeedback>& oldHypotheses,
    const std::unordered_map<db::TId, db::eye::Hypotheses>& objectToNewHypotheses)
{
    HypothesesStatistics result{};

    for (const auto& oldHypothesis: oldHypotheses) {
        ++result.oldCnt;
        if (oldHypothesis.result.isFeedbackProcessed) {
            ++result.oldProcessedCnt;
            if (oldHypothesis.result.hasCommits) {
                ++result.oldProcessedWithCommitsCnt;
            }
        }

        if (objectToNewHypotheses.contains(oldHypothesis.objectId)) {
            for (const auto& newHypothesis:
                 objectToNewHypotheses.at(oldHypothesis.objectId)) {

                if (oldHypothesis.hypothesis.hasEqualAttrs(newHypothesis)) {
                    ++result.newMatchedCnt;
                    if (oldHypothesis.result.isFeedbackProcessed) {
                        ++result.newMatchedProcessedCnt;
                        if (oldHypothesis.result.hasCommits) {
                            ++result.newMatchedProcessedWithCommitsCnt;
                        }
                    }
                }
            }
        }
    }

    for (const auto& [_, newHypotheses]: objectToNewHypotheses) {
        result.newCnt += newHypotheses.size();
    }

    return result;
}

db::eye::Objects filterObjects(ObjectSelector objectSelector, db::eye::Objects objects)
{
    objects.erase(
        std::remove_if(objects.begin(), objects.end(),
            [objectSelector](const db::eye::Object& object) {
                return !objectSelector(object);
            }
        ),
        objects.end()
    );
    return objects;
}


} // namespace maps::mrc:eye::qa

