#include <maps/wikimap/mapspro/services/tasks_social/src/stats_updater/lib/commits_processor.h>

#include <maps/libs/chrono/include/time_point.h>
#include <maps/wikimap/mapspro/libs/gdpr/include/user.h>
#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/revision/commit.h>
#include <yandex/maps/wiki/social/event.h>
#include <yandex/maps/wiki/social/gateway.h>
#include <yandex/maps/wiki/social/edits_stat.h>
#include <yandex/maps/wiki/social/edits_stat_gateway.h>

#include <chrono>
#include <map>
#include <set>

namespace maps::wiki::stats_updater {

namespace {

// Artificial categories group to count group edits
//
const std::string CAT_GROUP_GROUPEDITS = "groupedits_group";
const std::string CAT_GROUP_SERVICE = "service_group";
const std::string NEW_OBJECT_ACTION = "object-created";
const std::string GROUPEDIT_ACTION_PREFIX = "group";


std::map<revision::UserID, size_t>
getCommitsCountByAuthor(const std::list<revision::Commit>& commits)
{
    std::map<revision::UserID, size_t> result;
    for (const auto& commit: commits) {
        const gdpr::User user(commit.createdBy());
        if (!user.hidden()) {
            ++result[user.uid()];
        }
    }
    return result;
}

using CatGroup = std::string;

struct EditsStatDiff
{
    size_t totalCount{};
    size_t createdCount{};
};

using CatGroupsEditsStatDiff = std::map<CatGroup, EditsStatDiff>;

std::map<revision::UserID, CatGroupsEditsStatDiff>
getCommitsGroupCountByAuthor(
    const std::set<revision::DBID>& commitIds,
    pqxx::transaction_base& socialTxn,
    const configs::editor::CategoryGroups& categoryGroups)
{
    social::Gateway gtw(socialTxn);
    const auto events = gtw.loadTrunkEditEventsByCommitIds(commitIds);

    std::map<revision::UserID, CatGroupsEditsStatDiff> stat;
    for (const auto& event : events) {
        const gdpr::User user(event.createdBy());
        if (user.hidden()) {
            continue;
        }
        if (auto category = event.getPrimaryObjectCategory(); category) {
            auto group = categoryGroups.findGroupByCategoryId(category.value());
            if (!group) {
                continue;
            }
            auto& editsStatDiff = stat[user.uid()][group->id()];
            editsStatDiff.totalCount++;
            if (event.action() == NEW_OBJECT_ACTION) {
                editsStatDiff.createdCount++;
            }
        } else if (event.action().starts_with(GROUPEDIT_ACTION_PREFIX)) {
            stat[user.uid()][CAT_GROUP_GROUPEDITS].totalCount++;
        }
    }
    return stat;
}

std::map<revision::UserID, size_t>
getCommitsCountByAuthorV2(
    const std::map<revision::UserID, CatGroupsEditsStatDiff> usersGroupCounts)
{
    std::map<revision::UserID, size_t> res;
    for (const auto& [uid, groupsStat] : usersGroupCounts) {
        size_t totalCount = 0;
        for (const auto& [group, statDiff] : groupsStat) {
            if (group != CAT_GROUP_SERVICE) {
                totalCount += statDiff.totalCount;
            }
        }
        if (totalCount) {
            res[uid] = totalCount;
        }
    }
    return res;
}

void updateUserCatGroupEditsStat(
    revision::UserID uid,
    const CatGroupsEditsStatDiff& groupsStatsDiff,
    pqxx::transaction_base& socialTxn)
{
    ASSERT(!groupsStatsDiff.empty());

    social::CatGroupEditsStatGateway gtw(socialTxn);

    const std::map<CatGroup, social::CatGroupEditsStat> groupsStatsExisting = [&]{
        std::vector<CatGroup> groupsDiff;
        for (const auto& [group, statDiff] : groupsStatsDiff) {
            groupsDiff.push_back(group);
        }

        auto statsExisting = gtw.load(
            social::table::CatGroupEditsStatTbl::uid == uid &&
            social::table::CatGroupEditsStatTbl::categoryGroup.in(groupsDiff)
        );

        std::map<CatGroup, social::CatGroupEditsStat> groupsStat;
        for (const auto& stat : statsExisting) {
            groupsStat[stat.categoryGroup] = stat;
        }
        return groupsStat;
    }();

    std::vector<social::CatGroupEditsStat> statsForInsert;
    for (const auto& [group, editsStatDiff] : groupsStatsDiff) {
        social::CatGroupEditsStat stat{
            0,
            uid,
            group,
            editsStatDiff.totalCount,
            editsStatDiff.createdCount
        };
        if (auto statExisting = groupsStatsExisting.find(group); statExisting != groupsStatsExisting.end()) {
            stat.id = statExisting->second.id;
            stat.totalCount   += statExisting->second.totalCount;
            stat.createdCount += statExisting->second.createdCount;
        }
        statsForInsert.push_back(stat);
    }

    gtw.upsert(statsForInsert);
}

std::map<revision::UserID, chrono::TimePoint>
getFirstCommitAtByAuthor(const std::list<revision::Commit>& commits)
{
    using namespace chrono;

    std::map<revision::UserID, TimePoint> result;
    for (const auto& commit: commits) {
        const gdpr::User user(commit.createdBy());
        if (user.hidden()) {
            continue;
        }
        const auto createdAt = parseSqlDateTime(commit.createdAt());

        auto [uidToTimeIt, inserted] = result.emplace(user.uid(), createdAt);
        if (inserted) {
            continue;
        }

        uidToTimeIt->second = std::min(uidToTimeIt->second, createdAt);
    }
    return result;
}

std::set<revision::DBID>
extractCommitIds(const std::list<revision::Commit>& commits)
{
    std::set<revision::DBID> ids;
    for (const auto& commit : commits) {
        ids.insert(commit.id());
    }
    return ids;
}

std::set<revision::UserID>
extractAuthorsIds(const std::list<revision::Commit>& commits)
{
    std::set<revision::UserID> uids;
    for (const auto& commit : commits) {
        const gdpr::User user(commit.createdBy());
        if (!user.hidden()) {
            uids.insert(user.uid());
        }
    }
    return uids;
}

} // anon namespace

CommitsProcessor::CommitsProcessor(
        pqxx::transaction_base& socialTxn,
        const configs::editor::CategoryGroups& categoryGroups)
    : socialTxn_(socialTxn)
    , categoryGroups_(categoryGroups)
{}

void CommitsProcessor::processCommits(const std::list<revision::Commit>& commits)
{
    auto authorsUids = extractAuthorsIds(commits);

    auto commitsCountByAuthor = getCommitsCountByAuthor(commits);
    auto firstCommitAtByAuthor = getFirstCommitAtByAuthor(commits);
    auto commitsGroupCountByAuthor = getCommitsGroupCountByAuthor(
        extractCommitIds(commits), socialTxn_, categoryGroups_);
    auto commitsCountByAuthorV2 = getCommitsCountByAuthorV2(commitsGroupCountByAuthor);

    for (const auto& authorUid : authorsUids) {
        // It is possible that there is no categories group stat for user while
        // total edits count is not zero - because commit_event is not created
        // for some kind of edits - `service`, `import` edits
        // And these edits are not important for categories group user statistics.
        // So it's absolutely fine.
        //
        if (auto groupToStat = commitsGroupCountByAuthor.find(authorUid); groupToStat != commitsGroupCountByAuthor.end()) {
            updateUserCatGroupEditsStat(authorUid, groupToStat->second, socialTxn_);
        }

        if (commitsCountByAuthorV2.count(authorUid)) {
            const auto& totalCommitsCount = commitsCountByAuthorV2.at(authorUid);

            auto totalEditsUpdateResultV2 = updateTotalEditsInStatV2(
                authorUid,
                totalCommitsCount,
                firstCommitAtByAuthor.at(authorUid));

            auto newBadgesV2 = computeNewEditsCountBadge(
                authorUid,
                totalEditsUpdateResultV2.oldTotalEdits,
                totalEditsUpdateResultV2.newTotalEdits
            );

            for (const auto& badge : newBadgesV2) {
                saveBadgeToSocial(badge);
                editsBadges_.push_back(badge);
            }
        }

        const auto& totalCommitsCount = commitsCountByAuthor.at(authorUid);
        auto totalEditsUpdateResult = updateTotalEditsInStat(authorUid, totalCommitsCount);

        if (!totalEditsUpdateResult.statExists) {
            createNewStat(authorUid, totalCommitsCount, firstCommitAtByAuthor.at(authorUid));
        }
    }
}

const std::vector<EditsCountBadge>&
CommitsProcessor::getEditsBadges() const
{
    return editsBadges_;
}

void CommitsProcessor::createNewStat(
    UserId user, size_t editsCountDelta, chrono::TimePoint firstCommitAt)
{
    std::ostringstream query;
    query
        << "INSERT INTO social.stats (uid, total_edits, first_commit_at) "
        << "VALUES ("
            << user << ", "
            << editsCountDelta << ", "
            << socialTxn_.quote(chrono::formatSqlDateTime(firstCommitAt))
        << ")";

    socialTxn_.exec(query.str());
}

CommitsProcessor::UpdateUserTotalEdits
CommitsProcessor::updateTotalEditsInStat(UserId user, size_t editsCountDelta)
{
    std::ostringstream query;
    query <<
        "UPDATE social.stats"
        " SET total_edits = total_edits + " << editsCountDelta <<
        " WHERE uid = " << user <<
        " RETURNING total_edits";
    auto result = socialTxn_.exec(query.str());

    if (!result.affected_rows()) {
        return UpdateUserTotalEdits{editsCountDelta, 0, false};
    }

    size_t newTotalEdits = result[0][0].as<size_t>();
    ASSERT(newTotalEdits >= editsCountDelta);
    size_t oldTotalEdits = newTotalEdits - editsCountDelta;

    return UpdateUserTotalEdits{newTotalEdits, oldTotalEdits, true};
}

CommitsProcessor::UpdateUserTotalEdits
CommitsProcessor::updateTotalEditsInStatV2(
    UserId user, size_t editsCountDelta, chrono::TimePoint firstCommitAt)
{
    social::TotalEditsStatGateway gtw(socialTxn_);

    if (auto statExisting = gtw.tryLoadOne(social::table::TotalEditsStatTbl::uid == user); statExisting) {
        auto statForUpdate = statExisting.value();
        statForUpdate.totalEditsV2 += editsCountDelta;
        gtw.update(statForUpdate);

        return UpdateUserTotalEdits{
            statForUpdate.totalEditsV2,
            statExisting.value().totalEditsV2,
            true
        };
    } else {
        social::TotalEditsStat statForInsert;
        statForInsert.uid = user;
        statForInsert.totalEditsV2 = editsCountDelta;
        statForInsert.firstCommitAt = firstCommitAt;
        gtw.insert(statForInsert);

        return UpdateUserTotalEdits{
            editsCountDelta,
            0,
            false
        };
    }
}

void CommitsProcessor::saveBadgeToSocial(const EditsCountBadge& badge)
{
    std::ostringstream query;
    query
        << "INSERT INTO social.badge (uid, badge_id, level, awarded_by) "
        << "VALUES ("
            << badge.user << ", "
            << "'edits', "
            << badge.level << ", "
            << common::ROBOT_UID
        << ")";

    socialTxn_.exec(query.str());
}

} // namespace maps::wiki::stats_updater
