#include <maps/wikimap/mapspro/services/tasks_realtime/src/user_edits_metrics/lib/commit_loader.h>
#include <maps/wikimap/mapspro/services/tasks_realtime/src/user_edits_metrics/lib/load_helpers.h>
#include <maps/wikimap/mapspro/services/tasks_realtime/src/user_edits_metrics/lib/metrics.h>
#include <maps/wikimap/mapspro/services/tasks_realtime/src/user_edits_metrics/lib/metrics_calculator.h>
#include <maps/wikimap/mapspro/services/tasks_realtime/src/user_edits_metrics/lib/options.h>
#include <maps/wikimap/mapspro/services/tasks_realtime/src/user_edits_metrics/lib/st_notifier.h>

#include <maps/wikimap/mapspro/services/tasks_realtime/src/user_edits_metrics/lib/stat_utils/consolidated_deployment_time.h>
#include <maps/wikimap/mapspro/services/tasks_realtime/src/user_edits_metrics/lib/stat_utils/deployment_sla.h>
#include <maps/wikimap/mapspro/services/tasks_realtime/src/user_edits_metrics/lib/stat_utils/stat_util.h>
#include <maps/wikimap/mapspro/services/tasks_realtime/src/user_edits_metrics/lib/stat_utils/user_edits_processing_times.h>

#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/pgpool3_helpers.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/pg_advisory_lock_ids.h>

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

#include <filesystem>

namespace maps::wiki::user_edits_metrics {

namespace {

namespace fs = std::filesystem;

namespace filename {
const auto USER_EDITS_PROCESSING_TIMES_REPORT = "user_edits_processing_times.csv";
const auto CONSOLIDATED_DEPLOYMENT_TIME_REPORT = "consolidated_deployment_time.csv";
const auto DEPLOYMENT_SLA_REPORT = "deployment_sla.csv";
}

void calcSlidingWindowMetricsAndUpdateReport(
    const RegionUserTypeToMetricVec& regionUserToMetrics,
    DeploymentSlaReport& deploymentSlaReport,
    Event event,
    const Options::DeploymentDaysOutOfSlaOptions& dds,
    bool inTrunk,
    chrono::TimePoint calculationDate)
{
    deploymentSlaReport.add(
        daysOutOfSlaInSlidingWindow(
            regionUserToMetrics, event, dds.window, calculationDate, dds.quantile, inTrunk),
        dds.window,
        dds.brokenSlaBudget
    );

    deploymentSlaReport.add(
        achievableSlaInSlidingWindow(
            regionUserToMetrics, event, dds.window, dds.brokenSlaBudget, dds.quantile, inTrunk),
        dds.window,
        dds.brokenSlaBudget
    );
}

void saveReports(
    const std::optional<fs::path>& dir,
    const UserEditsProcessingTimesReport& userEditsProcessingTimesReport,
    const ConsolidatedDeploymentTimeReport& consolidatedDeploymentTimeReport,
    const DeploymentSlaReport& deploymentSlaReport)
{
    if (dir) {
        using namespace filename;
        saveReport(userEditsProcessingTimesReport,   *dir / USER_EDITS_PROCESSING_TIMES_REPORT);
        saveReport(consolidatedDeploymentTimeReport, *dir / CONSOLIDATED_DEPLOYMENT_TIME_REPORT);
        saveReport(deploymentSlaReport,              *dir / DEPLOYMENT_SLA_REPORT);
    }
}

void uploadReports(
    pgpool3::Pool& statPool,
    const UserEditsProcessingTimesReport& userEditsProcessingTimesReport,
    const ConsolidatedDeploymentTimeReport& consolidatedDeploymentTimeReport,
    const DeploymentSlaReport& deploymentSlaReport)
{
    INFO() << "Stat upload to db";

    auto txn = statPool.masterWriteableTransaction();
    uploadReportToStatDb(*txn, userEditsProcessingTimesReport);
    uploadReportToStatDb(*txn, consolidatedDeploymentTimeReport);
    uploadReportToStatDb(*txn, deploymentSlaReport);
    txn->commit();

    INFO() << "Stat reports uploaded";
}

void run(int argc, char** argv)
{
    auto options = Options::parse(argc, argv);

    common::ExtendedXmlDoc config(options.configFname);

    common::PoolHolder viewTrunkPoolHolder(config, "view-trunk", "view-trunk");
    pgp3utils::PgAdvisoryXactMutex guard(
        viewTrunkPoolHolder.pool(),
        static_cast<int64_t>(common::AdvisoryLockIds::USER_EDITS_METRICS));
    if (!guard.try_lock()) {
        INFO() << "User edits metrics worker is already running. (DB is locked)";
        return;
    }

    common::PoolHolder statPoolHolder(config, "stat", "tasks");
    common::PoolHolder longReadPoolHolder(config, "long-read", "long-read");
    common::PoolHolder socialPoolHolder(config, "social", "grinder");

    auto slaveCoreTxn = longReadPoolHolder.pool().slaveTransaction();
    auto slaveSocialTxn = socialPoolHolder.pool().slaveTransaction();
    auto slaveViewTrunkTxn = viewTrunkPoolHolder.pool().slaveTransaction();

    const auto metricsConfig = loadMetricsConfig(config);
    const auto aoiRegionsData = loadAoiRegions(*slaveCoreTxn, metricsConfig);
    const auto regionsData = loadRegionsData(*slaveViewTrunkTxn);

    const auto loaderStartTime = beginningOfTimeInterval(options.calculationDate, options.window) + options.window;

    CommitLoader loader(
        *slaveCoreTxn,
        *slaveSocialTxn,
        *slaveViewTrunkTxn,
        regionsData,
        aoiRegionsData,
        loaderStartTime,
        options.window);

    const auto calculationStartTime = beginningOfTimeInterval(options.calculationDate, options.window * options.totalDepth);
    const auto eventToRegions = calcEventToRegions(
        loader.branchesData(),
        aoiRegionsData,
        calculationStartTime);
    const auto eventToDeployedRegions = calcEventToDeployedRegions(
        loader.branchesData(),
        calculationStartTime);
    MetricsCalculator metricsCalculator(
        options.window,
        options.singleMetricDepth,
        options.quantiles,
        eventToRegions,
        eventToDeployedRegions,
        loader);

    UserEditsProcessingTimesReport userEditsProcessingTimesReport;
    ConsolidatedDeploymentTimeReport consolidatedDeploymentTimeReport;
    DeploymentSlaReport deploymentSlaReport;

    // Metrics: Approved, Exported, InStable, Resolved, Deployed<Component>
    const auto quantileMetrics = metricsCalculator.calculate(options.totalDepth);

    slaveViewTrunkTxn.releaseConnection();
    slaveSocialTxn.releaseConnection();
    slaveCoreTxn.releaseConnection();

    userEditsProcessingTimesReport.add(quantileMetrics);

    const auto regionToDeployedEvents = calcRegionToDeployedEvents(eventToRegions);

    checkMetricsAndNotifySt(
        quantileMetrics,
        regionToDeployedEvents,
        options.calculationDate,
        options.upload
    );

    for (const auto& dds: options.dds) {
        for (const bool inTrunk: {false, true}) {
            // Maximum of deployed to production events
            // Metrics: DeployedTotal[.estimated]
            auto deployedMetrics = getConsolidatedDeployedMetricsWithEstimations(
                quantileMetrics,
                options.calculationDate,
                dds.quantile,
                inTrunk,
                regionToDeployedEvents);
            consolidatedDeploymentTimeReport.add(deployedMetrics);

            // Metrics: DeployedTotalBrokenSla, DeployedTotalRealSla
            calcSlidingWindowMetricsAndUpdateReport(
                deployedMetrics,
                deploymentSlaReport,
                Event::DeployedTotal,
                dds,
                inTrunk,
                options.calculationDate);

            for (const auto& event: DEPLOYED_TO_PRODUCTION_EVENTS) {
                // Metrics: Deployed<Component>[.estimated]
                deployedMetrics = getConsolidatedMetricsWithEstimations(
                    quantileMetrics,
                    options.calculationDate,
                    event,
                    dds.quantile,
                    inTrunk,
                    eventToRegions.at(event));

                // Metrics: Deployed<Component>BrokenSla, Deployed<Component>RealSla
                calcSlidingWindowMetricsAndUpdateReport(
                    deployedMetrics,
                    deploymentSlaReport,
                    event,
                    dds,
                    inTrunk,
                    options.calculationDate);
            }
        }
    }

    deploymentSlaReport.removeIncompleteDimensions();
    saveReports(
        options.dumpDir,
        userEditsProcessingTimesReport,
        consolidatedDeploymentTimeReport,
        deploymentSlaReport
    );

    if (options.upload) {
        uploadReports(
            statPoolHolder.pool(),
            userEditsProcessingTimesReport,
            consolidatedDeploymentTimeReport,
            deploymentSlaReport
        );
    }
}

} // namespace

} // namespace maps::wiki::user_edits_metrics

int main(int argc, char** argv)
try {
    maps::wiki::user_edits_metrics::run(argc, argv);
    return EXIT_SUCCESS;
} catch (const maps::Exception& ex) {
    ERROR() << argv[0] << " failed: " << ex;
    return EXIT_FAILURE;
} catch (const std::exception& ex) {
    ERROR() << argv[0] << " failed: " << ex.what();
    return EXIT_FAILURE;
}
