#include <maps/wikimap/mapspro/libs/revision_meta/include/commit_regions.h>

#include <yandex/maps/wiki/common/batch.h>
#include <yandex/maps/wiki/common/pg_utils.h>
#include <yandex/maps/wiki/common/string_utils.h>

#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>

#include <map>
#include <sstream>

using namespace std::string_literals;

namespace maps::wiki::revision_meta {

namespace {

const size_t COMMIT_IDS_BATCH_SIZE = 1000;
const auto TABLE_COMMIT_REGIONS = "revision_meta.commit_regions"s;

const auto COMMIT_ID = "commit_id"s;
const auto PUBLISHED = "published"s;
const auto REGIONS = "regions"s;
const auto STABLE_BRANCH_ID = "stable_branch_id"s;
const auto CREATED_AT = "created_at"s;

const auto SELECT_UNSTABLE_COMMITS_QUERY =
    "SELECT " + COMMIT_ID +
    " FROM " + TABLE_COMMIT_REGIONS +
    " WHERE " + STABLE_BRANCH_ID + " IS NULL";

TCommitIds readCommitIds(const pqxx::result& rows)
{
    TCommitIds commitIds;
    for (const auto& row : rows) {
        commitIds.insert(row[0].as<TCommitId>());
    }
    return commitIds;
}

common::Hstore
regionsToHstore(
    pqxx::transaction_base& txn_,
    const TOIds& regionIds)
{
    if (regionIds.empty()) {
        return "NULL";
    }

    common::Attributes regions;
    for (const auto id: regionIds) {
        ASSERT(id);
        regions.emplace("region:"s + std::to_string(id), "1"s);
    }

    return common::attributesToHstore(txn_, regions);
}

} // namespace

void
CommitRegions::push(
    TCommitId commitId,
    const TOIds& regionIds) const
{
    ASSERT(commitId);
    txn_.exec(
        "INSERT INTO " + TABLE_COMMIT_REGIONS + " (" + COMMIT_ID + ',' + REGIONS + ") "
        "VALUES (" + std::to_string(commitId)  + ',' + regionsToHstore(txn_, regionIds) + ")"
    );
}

TCommitToRegions
CommitRegions::getCommitRegions(const TCommitIds& commitIds) const
{
    TCommitToRegions result;

    common::applyBatchOp<TCommitIds>(
        commitIds,
        COMMIT_IDS_BATCH_SIZE,
        [&, this](const TCommitIds& batch) {
            auto query =
                "SELECT commit_id, (SELECT ARRAY_AGG(SUBSTR(skeys,8)) FROM skeys(regions)) AS region_ids"s +
                " FROM "s + TABLE_COMMIT_REGIONS +
                " WHERE regions IS NOT NULL"s +
                "   AND "s + common::whereClause(COMMIT_ID, batch);

            for (const auto& row : txn_.exec(query)) {
                auto commitId = row[0].as<revision::DBID>();
                auto regionIds = common::parseSqlArray<TOId>(row[1].as<std::string>());

                result[commitId].insert(regionIds.begin(), regionIds.end());
            }
        });

    return result;
}

void
CommitRegions::toStable(
    const revision::Branch& branch,
    const TCommitIds& commitIds) const
{
    ASSERT(branch.type() == revision::BranchType::Stable);

    auto updateQueryWhere =
        "UPDATE "s + TABLE_COMMIT_REGIONS +
        " SET "s + STABLE_BRANCH_ID + " = "s + std::to_string(branch.id()) +
        " WHERE "s;

    common::applyBatchOp<TCommitIds>(
        commitIds,
        COMMIT_IDS_BATCH_SIZE,
        [&, this](const TCommitIds& batch) {
            auto query =
                SELECT_UNSTABLE_COMMITS_QUERY +
                " AND "s + common::whereClause(COMMIT_ID, batch) +
                " ORDER BY 1 FOR UPDATE"s;

            auto selectedCommitIds = readCommitIds(txn_.exec(query));
            if (!selectedCommitIds.empty()) {
                auto query =
                    updateQueryWhere +
                    common::whereClause(COMMIT_ID, selectedCommitIds);

                txn_.exec(query);
            }
        });
}

TCommitIds
CommitRegions::loadNotPublished(const TCommitIds& commitIds) const
{
    TCommitIds result;

    common::applyBatchOp<TCommitIds>(
        commitIds,
        COMMIT_IDS_BATCH_SIZE,
        [&](const TCommitIds& batch) {
            auto query =
                "SELECT "s + COMMIT_ID + " FROM "s + TABLE_COMMIT_REGIONS +
                " WHERE "s + common::whereClause(COMMIT_ID, batch) +
                " AND NOT "s + PUBLISHED;

            for (const auto& row : txn_.exec(query)) {
                result.insert(row[0].as<TCommitId>());
            }
        });
    return result;
}

TCommitIds
CommitRegions::getNotPublished(
    TOId regionId,
    const TBranchIds& branchIds) const
{
    auto query =
        "SELECT "s + COMMIT_ID + " FROM "s + TABLE_COMMIT_REGIONS +
        " WHERE "s + REGIONS + " ? 'region:"s + std::to_string(regionId) + "'"
        " AND "s + common::whereClause(STABLE_BRANCH_ID, branchIds) +
        " AND NOT "s + PUBLISHED +
        " ORDER BY 1 FOR UPDATE"s;

    return readCommitIds(txn_.exec(query));
}

void
CommitRegions::publish(const TCommitIds& commitIds) const
{
    common::applyBatchOp<TCommitIds>(
        commitIds,
        COMMIT_IDS_BATCH_SIZE,
        [&](const TCommitIds& batch) {
            auto query =
                "UPDATE "s + TABLE_COMMIT_REGIONS +
                " SET "s + PUBLISHED + "=TRUE"s
                " WHERE "s + common::whereClause(COMMIT_ID, batch);

            txn_.exec(query);
        });
}

size_t
CommitRegions::removeOldPublishedCommits(chrono::Days days) const
{
    auto query ="DELETE FROM "s + TABLE_COMMIT_REGIONS +
        " WHERE "s + PUBLISHED +
        " AND "s + CREATED_AT + " < (NOW() - INTERVAL '"s + std::to_string(days.count()) + " days')"s;

    return txn_.exec(query).affected_rows();
}

} // namespace maps::wiki::revision_meta
