#include <maps/wikimap/mapspro/services/tasks_social/src/involvement_stats_worker/lib/filtration.h>

#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/string_utils.h>

#include <yandex/maps/wiki/configs/editor/config_holder.h>

#include <yandex/maps/wiki/social/involvement.h>
#include <yandex/maps/wiki/social/involvement_filter.h>
#include <yandex/maps/wiki/social/involvement_stat.h>

#include <yandex/maps/wiki/pubsub/commit_consumer.h>

#include <yandex/maps/wiki/revision/filters.h>
#include <yandex/maps/wiki/revision/objectrevision.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <yandex/maps/wiki/revision/reader.h>

#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/multipolygon.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/pgpool/include/pgpool3.h>

#include <pqxx/pqxx>

#include <algorithm>
#include <atomic>
#include <condition_variable>
#include <chrono>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <thread>

namespace rev = maps::wiki::revision;
namespace filter = maps::wiki::revision::filters;
namespace pgpool3 = maps::pgpool3;
namespace cfgeditor = maps::wiki::configs::editor;

namespace {

const auto EDITOR_CONFIG_XPATH = "/config/services/editor/config";

const std::string CORE_DB_ID = "core";
const std::string SOCIAL_DB_ID = "social";
const std::string MRC_DB_ID = "mrc";
const std::string POOL_ID = "grinder";
const std::chrono::seconds WORKER_SLEEP_PERIOD{5};
//automatically disable involvements finished more than this period ago
using Days = std::chrono::duration<int64_t, std::ratio<24 * 60 * 60>>;
const Days INVOLVEMENT_OBSOLENCE_PERIOD{3};

const std::chrono::hours OBSOLETER_SLEEP_PERIOD{1};

} //anonymous namespace

namespace maps::wiki::tasks::involvement {

namespace {

const std::string PUBSUB_CONSUMER_ID = "InvolvementStats";
const rev::DBID PUBSUB_BRANCH_ID = rev::TRUNK_BRANCH_ID;
const std::string CATEGORY_PREFIX = "cat:";
const size_t PUBSUB_CONSUMER_BATCH_SIZE = 1000;

} //anonymous namespace

using CommitCreationTimes = std::map<rev::DBID, chrono::TimePoint>;

struct InvolvementDatum
{
    social::Involvement involvement;
    social::InvolvementStat stat;
    ICounterPtr counter;
};
using InvolvementData = std::vector<InvolvementDatum>;

social::Involvements loadEnabledInvolvements(pqxx::transaction_base& socialTxn)
{
    //Do not consider current time during this query
    //since current time has no relation
    //with the commits that will be processed
    //
    return social::Involvement::byFilter(
        socialTxn,
        social::InvolvementFilter().enabled(social::Enabled::Yes)
    );
}

InvolvementData
loadInvolvementData(
    pqxx::transaction_base& socialTxn,
    const social::Involvements& enabledInvolvements)
{
    auto statMap = social::loadInvolvementStatMap(socialTxn, enabledInvolvements);

    InvolvementData result;
    for (const auto& [involvement, stats] : statMap) {
        for (const auto& stat : stats) {
            InvolvementDatum involvementDatum;
            involvementDatum.involvement = involvement;
            involvementDatum.stat = stat;
            result.push_back(std::move(involvementDatum));
        }
    }
    return result;
}

using InvolvementIdToCommitIds = std::map<social::TId, std::vector<revision::DBID>>;
void fillInvolvementsCounters(
    const CommitsCounterFactory& factory,
    const InvolvementIdToCommitIds& invToCommits,
    InvolvementData& involvementData)
{
    for (auto& involvementDatum: involvementData) {
        const auto& inv = involvementDatum.involvement;
        involvementDatum.counter = factory.createCounter(
            involvementDatum.stat.type(),
            invToCommits.count(inv.id()) ? invToCommits.at(inv.id())
                                         : std::vector<revision::DBID>(),
            inv.start(),
            inv.finish(),
            involvementDatum.stat.value());
    }
}

CommitCreationTimes loadCommitCreationTimes(
    const std::vector<rev::DBID>& commitIds,
    pqxx::transaction_base& coreTxn
)
{
    CommitCreationTimes result;
    if (commitIds.empty()) {
        return result;
    }
    auto commits = rev::Commit::load(
        coreTxn,
        rev::filters::CommitAttr::id().in(commitIds)
    );
    for (const auto& commit: commits) {
        result.insert({
            commit.id(),
            commit.createdAtTimePoint()
        });
    }
    return result;
}

void writeStatsToDatabase(
    InvolvementData& involvementData,
    pqxx::transaction_base& socialTxn
)
{
    for (auto& datum: involvementData) {
        datum.stat.writeToDatabase(socialTxn);
    }
}

bool involvementMatchesCommit(const social::Involvement& inv, chrono::TimePoint commitTime)
{
    if (inv.start() > commitTime) {
        //commit was created before the involvement has begun
        return false;
    }
    if (inv.finish() && (inv.finish().value() < commitTime)) {
        //commit was created after the involvement has ended
        return false;
    }
    return true;
}

std::vector<rev::DBID> getRelevantCommitIds(
    const CommitCreationTimes& commitCreationTimes,
    const social::Involvement& involvement)
{
    std::vector<rev::DBID> commitIds;
    for (const auto& [commitId, creationTime]: commitCreationTimes) {
        if (involvementMatchesCommit(involvement, creationTime)) {
            commitIds.push_back(commitId);
        }
    }
    return commitIds;
}

InvolvementIdToCommitIds
involvmentsRelevantCommitIds(
    const CommitCreationTimes& commitCreationTimes,
    const social::Involvements& involvements)
{
    InvolvementIdToCommitIds result;
    for (const auto& inv : involvements) {
        result[inv.id()] = getRelevantCommitIds(commitCreationTimes, inv);
    }
    return result;
}


//returns number of commits processed during routine
//
size_t updateStats(
    pgpool3::Pool& corePool,
    pgpool3::Pool& socialPool,
    pgpool3::Pool& mrcPool,
    const cfgeditor::ConfigHolder& editorConfig) try
{
    auto socialTxnHandle = socialPool.masterWriteableTransaction();
    auto& socialTxn = socialTxnHandle.get();

    const auto enabledInvolvements = loadEnabledInvolvements(socialTxn);

    pubsub::CommitConsumer consumer(socialTxn, PUBSUB_CONSUMER_ID, PUBSUB_BRANCH_ID);
    consumer.setBatchSizeLimit(PUBSUB_CONSUMER_BATCH_SIZE);
    auto coreTxnHandle = corePool.slaveTransaction();
    auto& coreTxn = coreTxnHandle.get();
    auto commitIds = consumer.consumeBatch(coreTxn);
    INFO() << "Consumed batch of " << commitIds.size() << " commits";

    if (enabledInvolvements.empty()) {
        INFO() << "No active involvements found. Skip commits";
    } else {
        auto involvementData = loadInvolvementData(socialTxn, enabledInvolvements);
        auto commitCreationTimes = loadCommitCreationTimes(commitIds, coreTxn);
        auto involvementIdToCommitIds =
            involvmentsRelevantCommitIds(commitCreationTimes, enabledInvolvements);

        auto mrcTxnHandle = mrcPool.slaveTransaction();
        auto& mrcTxn = mrcTxnHandle.get();
        CommitsCounterFactory countersFactory(coreTxn, socialTxn, mrcTxn, editorConfig);
        fillInvolvementsCounters(countersFactory, involvementIdToCommitIds, involvementData);

        for (auto& datum : involvementData) {
            datum.stat += datum.counter->count(
                datum.involvement.polygons()
            );
        }
        writeStatsToDatabase(involvementData, socialTxn);
    }
    INFO() << "Committing";
    socialTxn.commit();

    return commitIds.size();
} catch (const pubsub::AlreadyLockedException&) {
    INFO() << "Another instance has already locked pubsub queue";
    return 0;
}

class Obsoleter
{
public:
    explicit Obsoleter(pgpool3::Pool& socialPool)
        : socialPool_(socialPool)
        , running_(true)
        , thread_(&Obsoleter::routine, this)
    {
    }

    ~Obsoleter()
    {
        running_ = false;
        cv_.notify_all();
        thread_.join();
    }

private:
    void disableObsoleteInvolvements()
    {
        auto socialTxnHandle = socialPool_.masterWriteableTransaction();
        auto& socialTxn = socialTxnHandle.get();
        auto obsolenceTime = chrono::TimePoint::clock::now() - INVOLVEMENT_OBSOLENCE_PERIOD;

        auto obsoleteInvolvements = social::Involvement::byFilter(
            socialTxn,
            social::InvolvementFilter()
                .finishedBefore(obsolenceTime)
                .enabled(social::Enabled::Yes)
        );
        if (obsoleteInvolvements.empty()) {
            return;
        }

        for (auto& involvement: obsoleteInvolvements) {
            INFO() << "Disabling obsolete involvement with id " << involvement.id();
            involvement.setEnabled(social::Enabled::No);
            involvement.writeToDatabase(socialTxn);
        }
        socialTxn.commit();
    }

    void routine()
    {
        while (running_) {
            try {
                disableObsoleteInvolvements();
            } catch (const maps::Exception& ex) {
                WARN() << "Got exception in obsoleterRoutines: " << ex;
            } catch (const std::exception& ex) {
                WARN() << "Got exception in obsoleterRoutine: " << ex.what();
            }

            std::unique_lock<std::mutex> lock(mutex_);
            cv_.wait_for(lock, OBSOLETER_SLEEP_PERIOD);
        }
    }

private:
    pgpool3::Pool& socialPool_;
    std::atomic<bool> running_;
    std::mutex mutex_;
    std::condition_variable cv_;
    std::thread thread_;
};

} //namespace maps::wiki::tasks::involvement

namespace inv = maps::wiki::tasks::involvement;

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

    auto syslogTag = parser.string("syslog-tag").help(
        "redirect log output to syslog with given tag");
    auto workerConfig = parser.file("config").help(
        "path to services configuration");

    parser.parse(argc, argv);

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

    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_DB_ID,
        POOL_ID
    );
    maps::wiki::common::PoolHolder socialPoolHolder(
        *configDocPtr,
        SOCIAL_DB_ID,
        POOL_ID
    );
    maps::wiki::common::PoolHolder mrcPoolHolder(
        *configDocPtr,
        MRC_DB_ID,
        POOL_ID
    );

    maps::wiki::configs::editor::ConfigHolder editorConfig(
        configDocPtr->get<std::string>(EDITOR_CONFIG_XPATH)
    );

    inv::Obsoleter obsoleter(socialPoolHolder.pool());

    while (true) {
        size_t commitsProcessed = 0;
        try {
            INFO() << "Starting involvement statistics update routine";

            commitsProcessed = inv::updateStats(
                corePoolHolder.pool(),
                socialPoolHolder.pool(),
                mrcPoolHolder.pool(),
                editorConfig
            );
        } catch (const maps::Exception& ex) {
            WARN() << "Got exception while updating statistics: " << ex;
        } catch (const std::exception& ex) {
            WARN() << "Got exception while updating statistics: " << ex.what();
        }

        //sleeping only if consumed batch is lesser in size than requested
        //this is a probably-good approximation of no-new-commits-pending state
        if (commitsProcessed < inv::PUBSUB_CONSUMER_BATCH_SIZE) {
            INFO() <<
                "Worker is going to sleep for " <<
                WORKER_SLEEP_PERIOD.count() << " seconds"
            ;
            std::this_thread::sleep_for(WORKER_SLEEP_PERIOD);
        }
    }
    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;
}
