#include "db.h"

#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/parallel_for_each.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/utility.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/disqualified_source_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 <atomic>
#include <string>

namespace maps::mrc::red_card {

namespace {

const std::string APP_NAME = "red_card";
const std::string LAST_RUN_TIME = APP_NAME + ".timestamp";

std::optional<db::DisqualifiedSource> loadActiveDisqualifiedSource(
    pqxx::transaction_base& txn,
    db::DisqType disqType,
    const std::string& srcId,
    chrono::TimePoint now)
{
    using Table = db::table::DisqualifiedSource;
    auto disqs = db::DisqualifiedSourceGateway{txn}.load(
        Table::disqType == disqType && Table::sourceId == srcId,
        orderBy(Table::endedAt).desc().limit(1));
    if (disqs.empty() || now > disqs.front().endedAt().value_or(now)) {
        return std::nullopt;
    }
    return disqs.front();
}

std::optional<double> recentRoadProbability(pqxx::transaction_base& txn,
                                            const std::string& srcId,
                                            chrono::TimePoint now)
{
    using Table = db::table::Feature;
    auto features = db::FeatureGateway{txn}.load(
        Table::sourceId == srcId && Table::date >= now - RECENT_TIMES &&
        Table::dataset == db::Dataset::TaxiSignalQ2 &&
        Table::roadProbability.isNotNull());
    if (features.size() < REQUIRED_PHOTOS) {
        return std::nullopt;
    }
    auto nth = static_cast<size_t>(features.size() * REQUIRED_PERCENTILE);
    nth = std::clamp<size_t>(nth, 0, features.size() - 1);
    auto it = std::next(features.begin(), nth);
    std::nth_element(features.begin(),
                     it,
                     features.end(),
                     common::greaterFn(&db::Feature::roadProbability));
    return it->roadProbability();
}

std::string format(size_t n)
{
    auto os = std::ostringstream{};
    os.imbue(std::locale(""));
    os << n;
    return os.str();
}

}  // namespace

void updateLastRunTime(pgpool3::Pool& pool, chrono::TimePoint now)
{
    auto txn = pool.masterWriteableTransaction();
    db::MetadataGateway{*txn}.upsertByKey(LAST_RUN_TIME,
                                          chrono::formatSqlDateTime(now));
    txn->commit();
}

std::optional<chrono::TimePoint> loadLastRunTime(pqxx::transaction_base& txn)
{
    auto result = db::MetadataGateway{txn}.tryLoadByKey(LAST_RUN_TIME);
    if (!result) {
        return std::nullopt;
    }
    return chrono::parseSqlDateTime(*result);
}

db::DisqualifiedSources prepareDisqualifiedSources(pqxx::transaction_base& txn,
                                                   const std::string& srcId,
                                                   chrono::TimePoint now)
{
    if (loadActiveDisqualifiedSource(
            txn, db::DisqType::DisableCapturing, srcId, now)
            .has_value()) {
        return {};
    }
    auto roadProbability = recentRoadProbability(txn, srcId, now);
    if (!roadProbability) {
        return {};
    }
    auto result = db::DisqualifiedSources{};
    auto disablePublishing = loadActiveDisqualifiedSource(
        txn, db::DisqType::DisablePublishing, srcId, now);
    if (*roadProbability < ROAD_PROBABILITY_THRESHOLD) {
        result.emplace_back(
            db::DisqType::DisableCapturing, srcId, now, now + 2 * RECENT_TIMES);
        if (disablePublishing) {
            result.emplace_back(*disablePublishing).resetEndedAt();
        }
        else {
            result.emplace_back(db::DisqType::DisablePublishing, srcId, now);
            INFO() << "sourceId=" << srcId
                   << ", roadProbability=" << *roadProbability;
        }
    }
    else if (disablePublishing) {
        result.emplace_back(*disablePublishing).setEndedAt(now);
        INFO() << "sourceId=" << srcId
               << ", roadProbability=" << *roadProbability;
    }
    return result;
}

void updateDisqualifiedSources(pgpool3::Pool& pool,
                               chrono::TimePoint now,
                               bool dryRun)
{
    auto srcIds = db::FeatureGateway{*pool.slaveTransaction()}.loadAllSources();
    INFO() << "loaded " << format(srcIds.size()) << " sources";
    auto processed = std::atomic_size_t{0};
    common::parallelForEach(
        srcIds.begin(), srcIds.end(), [&](auto& /*guard*/, const auto& srcId) {
            auto disqs = prepareDisqualifiedSources(
                *pool.slaveTransaction(), srcId, now);
            if (!dryRun && !disqs.empty()) {
                auto txn = pool.masterWriteableTransaction();
                db::DisqualifiedSourceGateway{*txn}.upsert(disqs);
                txn->commit();
            }
            if (auto snapshot = ++processed; snapshot % 10000 == 0) {
                INFO() << "processed " << format(snapshot) << " sources";
            }
        });
    INFO() << "processed " << format(processed) << " sources";
}

}  // namespace maps::mrc::red_card
