#include "worker.h"
#include "fbapi/processing.h"
#include "fbapi/traits.h"
#include "schedule_stat.h"
#include "util.h"

#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/libs/geolocks/include/yandex/maps/wiki/geolocks/geolocks.h>
#include <yandex/maps/wiki/common/aoi.h>
#include <yandex/maps/wiki/common/pg_advisory_lock_ids.h>
#include <yandex/maps/wiki/common/moderation.h>
#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/social/feedback/agent.h>
#include <yandex/maps/wiki/social/feedback/gateway_rw.h>
#include <yandex/maps/wiki/social/feedback/task.h>
#include <yandex/maps/wiki/social/feedback/task_aoi.h>
#include <yandex/maps/wiki/social/feedback/task_filter.h>
#include <yandex/maps/wiki/social/feedback/task_patch.h>
#include <yandex/maps/pgpool3utils/pg_advisory_mutex.h>

#include <functional>
#include <future>

namespace maps::wiki::schedule_feedback {

namespace sf = social::feedback;

namespace {

constexpr size_t INCOMING_FEEDBACK_REMAINS_THRESHOLD = 10000;

void generatePendingAoiFeed(
    pqxx::transaction_base& txnSocialWrite,
    pqxx::transaction_base& txnViewTrunkRead,
    const sf::TaskForUpdate& task)
{
    if (task.source() == "experiment-recommendations") {
        return;
    }

    auto aoiIds = common::calculateAoisContainingPosition(
        task.position(), txnViewTrunkRead);

    sf::addTaskToAoiFeed(txnSocialWrite,
        task.id(), aoiIds, sf::Partition::Pending);
}

ScheduleStatOne scheduleTask(
    pqxx::transaction_base& txnCoreRead,
    pqxx::transaction_base& txnSocialWrite,
    pqxx::transaction_base& txnViewTrunkRead,
    sf::TaskForUpdate task)
{
    generatePendingAoiFeed(txnSocialWrite, txnViewTrunkRead, task);

    ScheduleStatOne statOne;
    sf::Agent agent(txnSocialWrite, common::ROBOT_UID);
    if (geolocks::isLocked(
        txnCoreRead,
        revision::TRUNK_BRANCH_ID,
        task.position(),
        geolocks::GeolockType::Manual)
    ) {
        auto optTask = agent.hideTask(task);
        REQUIRE(optTask, "Cannot hide task with id " << task.id());
        task = *optTask;
        statOne.setHide();
    }

    if (isFbapiTask(task)) {
        statOne += processIncomingFbapiTasks(txnCoreRead, agent, task);
    } else {
        /* Trivially shift it to outgoing bucket */
        agent.revealTaskByIdCascade(task.id());
    }
    return statOne;
}

} // anonymous

void Worker::releaseOverdueRoutine()
{
    auto hndlSocialWrite = socialPool_.masterWriteableTransaction();
    auto& txnSocialWrite = hndlSocialWrite.get();

    sf::GatewayRW(txnSocialWrite).releaseOverdueTasks(common::ROBOT_UID);

    txnSocialWrite.commit();
    INFO() << "Overdue tasks released.";
}

void Worker::rejectOverdueNeedInfoRoutine()
{
    const auto NEEDINFO_OVERDUE_PERIOD = std::chrono::hours(24*90);

    auto hndlSocialWrite = socialPool_.masterWriteableTransaction();
    auto& txnSocialWrite = hndlSocialWrite.get();
    sf::GatewayRW gatewayRw(txnSocialWrite);
    sf::Agent agent(txnSocialWrite, common::ROBOT_UID);

    auto now = chrono::TimePoint::clock::now();
    auto tasks = gatewayRw.tasksForUpdateByFilter(
        sf::TaskFilter()
            .bucket(sf::Bucket::NeedInfo)
            .stateModifiedAt(social::DateTimeCondition(std::nullopt, now - NEEDINFO_OVERDUE_PERIOD)));
    INFO() << "Found " << tasks.size() << " overdue feedback in NeedInfo bucket.";

    for (auto& task : tasks) {
        if (auto taskOpened = agent.openTask(task)) {
            agent.resolveTaskCascade(*taskOpened, sf::Resolution::createRejected(sf::RejectReason::NoInfo));
        }
    }

    txnSocialWrite.commit();
    INFO() << "Overdue tasks rejected.";
}

void Worker::scheduleRoutine()
{
    auto txnCoreReadHandle = corePool_.slaveTransaction();
    auto txnViewTrunkReadHandle = viewTrunkPool_.slaveTransaction();

    auto& txnCoreRead = txnCoreReadHandle.get();
    auto& txnViewTrunkRead = txnViewTrunkReadHandle.get();

    auto getIncomingTasks = [&]() {
        auto txnSocialReadHandle = socialPool_.slaveTransaction();
        sf::GatewayRO gatewayRo(txnSocialReadHandle.get());
        return gatewayRo.tasksByFilter(
            sf::TaskFilter().bucket(sf::Bucket::Incoming).createdBeforeNow());
    };
    auto incomingTasks = getIncomingTasks();
    INFO() << "Found " << incomingTasks.size() << " feedback tasks in Incoming bucket.";

    ScheduleStat stat;
    for (auto& task: incomingTasks) try {
        auto txnSocialWriteHandle = socialPool_.masterWriteableTransaction();
        sf::GatewayRW gatewayRw(txnSocialWriteHandle.get());

        auto taskForUpdate = gatewayRw.taskForUpdateById(task.id());
        if (!taskForUpdate || (taskForUpdate->bucket() != sf::Bucket::Incoming)) {
            continue;
        }
        stat += scheduleTask(txnCoreRead, txnSocialWriteHandle.get(), txnViewTrunkRead, *taskForUpdate);
            txnSocialWriteHandle.get().commit();
    } catch (const Exception& ex) {
        stat.incError();
        ERROR() << "Scheduling feedback '" << task.id() << "' failed" << ex;
    } catch (const std::exception& ex) {
        stat.incError();
        ERROR() << "Scheduling feedback '" << task.id() << "' failed" << ex.what();
    }
    stat.printInfo();

    if (getIncomingTasks().size() > INCOMING_FEEDBACK_REMAINS_THRESHOLD) {
        static const std::string INCOMING_OVERFLOW_ERROR_MSG(
            "Too many tasks left as Incoming after scheduling.");
        ERROR() << INCOMING_OVERFLOW_ERROR_MSG;
        statusWriter_.err(INCOMING_OVERFLOW_ERROR_MSG);
    }
}

Worker::Worker(
        pgpool3::Pool& socialPool,
        pgpool3::Pool& corePool,
        pgpool3::Pool& viewTrunkPool,
        tasks::StatusWriter& statusWriter)
    : socialPool_(socialPool)
    , corePool_(corePool)
    , viewTrunkPool_(viewTrunkPool)
    , statusWriter_(statusWriter)
{}

void Worker::doTask()
{
    statusWriter_.reset();
    INFO() << "Task started.";

    //locking database
    pgp3utils::PgAdvisoryXactMutex dbLocker(
        socialPool_,
        static_cast<int64_t>(common::AdvisoryLockIds::SCHEDULE_FEEDBACK));
    if (!dbLocker.try_lock()) {
        INFO() << "Database is already locked. Task interrupted.";
        return;
    }

    using Routine = void (Worker::*)();
    std::map<std::string, Routine> namedRoutines;
    namedRoutines.emplace("schedule", &Worker::scheduleRoutine);
    namedRoutines.emplace("release overdue", &Worker::releaseOverdueRoutine);
    namedRoutines.emplace("reject overdue", &Worker::rejectOverdueNeedInfoRoutine);

    auto constructRoutineError =
        [](const std::string& routineName, const std::string& error) {
            return routineName + " failed. " + error;
        };

    std::vector<std::string> errs;

    auto noExceptCall = [&](const std::string& name, const auto& routine) {
        try {
            INFO() << "Routine " << name << " started.";
            routine();
            INFO() << "Routine " << name << " finished.";
        } catch (const Exception& ex) {
            auto exStr = (std::stringstream() << ex).str();
            errs.emplace_back(constructRoutineError(name, exStr));
        } catch (const std::exception& ex) {
            errs.emplace_back(constructRoutineError(name, ex.what()));
        } catch (...) {
            errs.emplace_back(constructRoutineError(name,"unknown error"));
        }
    };

    for (const auto& namedRoutine : namedRoutines) {
        noExceptCall(namedRoutine.first, [&](){ std::invoke(namedRoutine.second, *this); });
    }

    for (const auto& err : errs) {
        statusWriter_.err(err);
        ERROR() << err;
    }

    statusWriter_.flush();
}

} // namespace maps::wiki::schedule_feedback
