#include <yandex/maps/wiki/common/default_config.h>
#include <yandex/maps/wiki/common/extended_xml_doc.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>
#include <yandex/maps/wiki/common/pg_advisory_lock_ids.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/tasks/status_writer.h>
#include <yandex/maps/wiki/social/published_commits.h>
#include <maps/wikimap/mapspro/libs/revision_meta/include/commit_regions.h>

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

#include <maps/wikimap/mapspro/services/tasks_realtime/src/publication_check_worker/lib/branch_events.h>
#include <maps/wikimap/mapspro/services/tasks_realtime/src/publication_check_worker/lib/export.h>
#include <maps/wikimap/mapspro/services/tasks_realtime/src/publication_check_worker/lib/garden.h>
#include <maps/libs/cmdline/include/cmdline.h>

namespace maps {
namespace wiki {

namespace {

using TCommitId = revision::DBID;
using TCommitIds = revision::DBIDSet;
using RegionsData = std::map<std::string, revision::DBID>;

const std::string LOAD_REGIONS_DATA_QUERY =
    "SELECT text_data, object_id"
    " FROM vrevisions_trunk.suggest_data"
    " WHERE categories ? 'cat:region'";

RegionsData loadRegionsData(pgpool3::Pool& viewTrunkPool)
{
    RegionsData result;

    auto txn = viewTrunkPool.masterReadOnlyTransaction();
    for (const auto& row : txn->exec(LOAD_REGIONS_DATA_QUERY)) {
        result.emplace(row[0].as<std::string>(), row[1].as<revision::DBID>());
    }
    return result;
}

void processPublishedRegions(
    const PublishedRegions& publishedRegions,
    pqxx::transaction_base& coreTxn,
    pqxx::transaction_base& socialTxn,
    pgpool3::Pool& viewTrunkPool)
{
    auto regionsData = loadRegionsData(viewTrunkPool);

    social::PublishedCommits pcg(socialTxn);

    for (const auto& pair : publishedRegions) {
        const auto& region = pair.first;
        const auto& branchIds = pair.second;
        ASSERT(!branchIds.empty());
        auto branches = common::join(branchIds, ',');

        auto it = regionsData.find(region);
        if (it == regionsData.end()) {
            ERROR() << "Published region: " << region << " (unknown id)"
                    << " : branches: " << branches;
            continue;
        }

        auto regionId = it->second;
        INFO() << "Published region: " << region << " (" << regionId << ")"
               << " : branches: " << branches;

        revision_meta::CommitRegions commitRegions(coreTxn);
        auto commitIds = commitRegions.getNotPublished(regionId, branchIds);
        if (commitIds.empty()) {
            continue;
        }

        commitRegions.publish(commitIds);
        pcg.push(commitIds);
    }
}

void doWork(
    pqxx::transaction_base& coreTxn,
    pqxx::transaction_base& socialTxn,
    pgpool3::Pool& viewTrunkPool)
{
    BranchStore branchStore(coreTxn);
    if (branchStore.empty()) {
        WARN() << "No stable or archive or deleted branch found";
        return;
    }

    branchStore.mergeEvents(
        loadBranchExportedTimes(coreTxn));

    garden::HttpClient httpClient;
    auto fromTime = chrono::TimePoint::clock::now() - garden::TIME_OFFSET;

    branchStore.mergeEvents(
        garden::loadBranchDeployedTimes(httpClient, EVENT_DEPLOYED_RENDERER, fromTime));
    branchStore.mergeEvents(
        garden::loadBranchDeployedTimes(httpClient, EVENT_DEPLOYED_CAMS, fromTime));
    branchStore.mergeEvents(
        garden::loadBranchDeployedTimes(httpClient, EVENT_DEPLOYED_CARPARKS, fromTime));
    branchStore.mergeEvents(
        garden::loadBranchDeployedTimes(httpClient, EVENT_DEPLOYED_GEOCODER, fromTime));
    branchStore.mergeEvents(
        garden::loadBranchDeployedTimes(httpClient, EVENT_DEPLOYED_MTR_EXPORT, fromTime));
    branchStore.mergeEvents(
        garden::loadBranchDeployedTimes(httpClient, EVENT_DEPLOYED_GRAPH, fromTime));
    branchStore.mergeEvents(
        garden::loadBranchDeployedTimes(httpClient, EVENT_DEPLOYED_BICYCLE_GRAPH, fromTime));
    branchStore.mergeEvents(
        garden::loadBranchDeployedTimes(httpClient, EVENT_DEPLOYED_PEDESTRIAN_GRAPH, fromTime));

    branchStore.writeAttributes(coreTxn);

    auto publishedRegions = branchStore.publishedRegions();
    if (!publishedRegions.empty()) {
        processPublishedRegions(publishedRegions, coreTxn, socialTxn, viewTrunkPool);
    }

    revision_meta::CommitRegions commitRegions(coreTxn);
    auto removedCommitsCount = commitRegions.removeOldPublishedCommits();
    INFO() << "Removed " << removedCommitsCount << " old published commits";
}

void updateStatusFile(std::optional<std::string> filepath, std::function<void()> func)
{
    maps::wiki::tasks::StatusWriter statusWriter(std::move(filepath));

    try {
        func();
        statusWriter.flush();
    } catch (const std::exception& e) {
        statusWriter.err(e.what());
        statusWriter.flush();
        throw;
    }
}

} // anonymous
} // wiki
} // maps

int main(int argc, char** argv) try {
    maps::cmdline::Parser parser;

    auto workerConfig = parser.file("config")
        .help("path to worker configuration");
    auto syslogTag = parser.string("syslog-tag")
        .help("redirect log output to syslog with given tag");
    auto dryRun = parser.flag("dry-run")
        .help("Skip commit to the database");
    auto statusDir = parser.string("status-dir")
        .help("path to status dir");

    parser.parse(argc, argv);

    if (syslogTag.defined()) {
        maps::log8::setBackend(maps::log8::toSyslog(syslogTag));
    }

    std::unique_ptr<maps::wiki::common::ExtendedXmlDoc> configDocPtr;
    if (workerConfig.defined()) {
        configDocPtr.reset(new maps::wiki::common::ExtendedXmlDoc(workerConfig));
    } else {
        configDocPtr = maps::wiki::common::loadDefaultConfig();
    }

    maps::wiki::common::PoolHolder corePoolHolder(*configDocPtr, "core", "grinder");

    maps::pgp3utils::PgAdvisoryXactMutex locker(
        corePoolHolder.pool(),
        static_cast<int64_t>(maps::wiki::common::AdvisoryLockIds::PUBLICATION_CHECKER));
    if (!locker.try_lock()) {
        INFO() << "Database is already locked. Task interrupted.";
        return EXIT_SUCCESS;
    }

    maps::wiki::common::PoolHolder viewTrunkPoolHolder(*configDocPtr, "view-trunk", "editor-tool");
    maps::wiki::common::PoolHolder socialPoolHolder(*configDocPtr, "social", "grinder");
    auto socialTxn = socialPoolHolder.pool().masterWriteableTransaction();

    std::optional<std::string> statusFileName;
    if (statusDir.defined()) {
        statusFileName = statusDir + "/wiki-publication-check-worker.status";
    }

    INFO() << "Publication check started";

    maps::wiki::updateStatusFile(
        statusFileName,
        [&]() {
            maps::wiki::doWork(
                locker.writableTxn(),
                *socialTxn,
                viewTrunkPoolHolder.pool());

            if (!dryRun) {
                locker.writableTxn().commit();
                socialTxn->commit();
            }
        });

    INFO() << "Publication check finished";
    return EXIT_SUCCESS;
} catch (const maps::Exception& ex) {
    FATAL() << "Worker failed: " << ex;
    return EXIT_FAILURE;
} catch (const std::exception& ex) {
    FATAL() << "Worker failed: " << ex.what();
    return EXIT_FAILURE;
}
