#include "worker.h"
#include "last_deployed_branch.h"

#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/wiki/common/pg_advisory_lock_ids.h>
#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/social/feedback/commits.h>
#include <yandex/maps/wiki/social/feedback/gateway_rw.h>
#include <yandex/maps/wiki/social/feedback/task_filter.h>
#include <yandex/maps/wiki/social/feedback/agent.h>
#include <yandex/maps/wiki/revision/commit.h>
#include <yandex/maps/wiki/revision/branch_manager.h>
#include <yandex/maps/pgpool3utils/pg_advisory_mutex.h>

#include <boost/algorithm/string.hpp>
#include <boost/range/algorithm_ext/erase.hpp>
#include <optional>


namespace maps::wiki::social::feedback {

namespace {

const std::string ATTR_DEPLOYED_RENDERER = "deployed_renderer";

std::optional<chrono::TimePoint> getBranchDeployedTime(const revision::Branch& branch)
{
    auto& branchAttr = branch.attributes();
    auto it = branchAttr.find(ATTR_DEPLOYED_RENDERER);
    if (it != branchAttr.end()) {
        return chrono::parseSqlDateTime(it->second);
    }
    return std::nullopt;
}

struct BranchesInfo
{
    revision::DBID lastDeployedBranchId;
    chrono::TimePoint lastDeployedBranchTime;

    /// We consider that all feedback closed before the timeX are already in production.
    chrono::TimePoint timeX;
};

BranchesInfo investigateBranches(pqxx::transaction_base& txnCoreRead)
{
    chrono::TimePoint lastDeployedBranchCreationTime
            = std::chrono::system_clock::now() - std::chrono::hours(24*365);
    chrono::TimePoint lastDeployedBranchTime = lastDeployedBranchCreationTime;
    revision::DBID lastDeployedBranchId = 0;

    revision::BranchManager branchManager(txnCoreRead);
    revision::BranchManager::BranchLimits branchLimits {
            {revision::BranchType::Stable, revision::BranchManager::UNLIMITED},
            {revision::BranchType::Archive, revision::BranchManager::UNLIMITED},
            {revision::BranchType::Deleted, 100} // 100 - just not to select all branches unlimited
    };
    for (auto& branch : branchManager.load(branchLimits)) {
        auto branchDeployedTime = getBranchDeployedTime(branch);
        if (branchDeployedTime) {
            auto branchCreationTime = chrono::parseSqlDateTime(branch.createdAt());
            if (branchCreationTime > lastDeployedBranchCreationTime) {
                lastDeployedBranchId = branch.id();
                lastDeployedBranchCreationTime = branchCreationTime;
                lastDeployedBranchTime = *branchDeployedTime;
                REQUIRE(lastDeployedBranchTime >= lastDeployedBranchCreationTime,
                        "unexpected creationTime && deployedTime of branch " << lastDeployedBranchId);
            }
        }
    }

    // At branch creating moment some commits are approved,
    // BUT not in Approved branch (they are in queue for moved to Approved branch)
    chrono::TimePoint timeX = lastDeployedBranchCreationTime - std::chrono::hours(1);

    // At feedback closing moment cartographer see that everything is ok.
    // BUT he see Trunk branch; while Release branch created based on Approved branch.
    // So, it'll take some time (maximum 4 days for now) all commits (that he is seeing)
    // will be approved and moved to Approved branch.
    timeX -= std::chrono::hours(24*4);

    return BranchesInfo{lastDeployedBranchId, lastDeployedBranchTime, timeX};
}

} // anonymous namespace

Worker::Worker(const common::ExtendedXmlDoc& cfg,
        tasks::StatusWriter& statusWriter)
    : socialPool_(cfg, "social", "grinder")
    , corePool_(cfg, "core", "grinder")
    , statusWriter_(statusWriter)
{
}

size_t Worker::setDeployed(
    pqxx::transaction_base& txnCore,
    const Tasks& feedbacks)
{
    // get txn
    auto socialReadHandle = socialPool_.pool().slaveTransaction();
    auto& txnSocialRead = socialReadHandle.get();

    auto branchesInfo = investigateBranches(txnCore);

    // find feedback to commit binding
    TIds allFeedbackIds;
    for (const auto& feedback : feedbacks) {
        allFeedbackIds.insert(feedback.id());
    }

    INFO() << "Selecting commit ids for "
        << allFeedbackIds.size() << " feedbacks started.";
    const auto feedbackIdToCommitIds = commitIdsByTaskIds(txnSocialRead, allFeedbackIds);

    std::set<social::TId> allCommitIds;
    for (const auto& [fbId, commitIds] : feedbackIdToCommitIds) {
        allCommitIds.insert(commitIds.begin(), commitIds.end());
    }

    // find all necessary commits
    INFO() << "Selecting " << allCommitIds.size() << " commits started.";
    std::unordered_map<social::TId, revision::Commit> idToCommit;
    std::set<revision::DBID> allBranchIds;
    if (!allCommitIds.empty()) {
        for (auto& commit : revision::Commit::load(
                txnCore, revision::filters::CommitAttr::id().in(allCommitIds))) {
            if (commit.inStable()) {
                allBranchIds.insert(commit.stableBranchId());
            }
            idToCommit.emplace(commit.id(), std::move(commit));
        }
    }
    INFO() << idToCommit.size() << " commits selected.";

    // find all necessary branches
    std::unordered_map<revision::DBID, revision::Branch> idToBranch;
    revision::BranchManager branchManager(txnCore);
    for (auto& branch : branchManager.load(allBranchIds)) {
        idToBranch.emplace(branch.id(), std::move(branch));
    }
    INFO() << idToBranch.size() << " branches selected.";

    // check every founded feedback and mark it deployed if necessary
    size_t deployedCount = 0;
    for (auto& feedback : feedbacks) {
        bool isDeployed = false;
        auto feedbackCommitIdsIt = feedbackIdToCommitIds.find(feedback.id());
        if (feedbackCommitIdsIt == feedbackIdToCommitIds.end()) {
            if (feedback.resolved() && feedback.resolved()->date < branchesInfo.timeX) {
                isDeployed = true;
            }
        } else {
            auto& feedbackCommitIds = feedbackCommitIdsIt->second;
            isDeployed = true;
            for (auto& commitId : feedbackCommitIds) {
                auto idToCommitIt = idToCommit.find(commitId);
                REQUIRE(idToCommitIt != idToCommit.end(),
                        "Commit " << commitId << " not found for feedback-task " << feedback.id());
                auto& commit = idToCommitIt->second;

                if (!commit.inStable()) {
                    isDeployed = false;
                    break;
                }

                auto idToBranchIt = idToBranch.find(commit.stableBranchId());
                REQUIRE(idToBranchIt != idToBranch.end(),
                        "Branch " << commit.stableBranchId() << " not found for commit " << commit.id());
                auto& branch = idToBranchIt->second;

                if (!getBranchDeployedTime(branch)) {
                    isDeployed = false;
                    break;
                }
            }
        }

        if (isDeployed) {
            // Database constraint failing expected.
            // Due task might be changed from outside
            // since SELECT.
            try {
                auto socialWriteHandle = socialPool_.pool().masterWriteableTransaction();
                auto& txnSocialWrite = socialWriteHandle.get();
                Agent agent(txnSocialWrite, common::ROBOT_UID);
                agent.deployTaskByIdCascade(
                        feedback.id(),
                        branchesInfo.lastDeployedBranchTime);
                txnSocialWrite.commit();

                INFO() << "Feedback-task " << feedback.id() << " marked deployed.";
                ++deployedCount;
            } catch (const Exception& ex) {
                ERROR() << "Deploying feedback '" << feedback.id() << "' failed." << ex;
            }
        }
    }
    return deployedCount;
}

void Worker::doWork()
{
    // Transactions
    //
    auto socialReadHandle = socialPool_.pool().slaveTransaction();
    auto coreWriteHandle  = corePool_.pool().masterWriteableTransaction();
    auto& txnSocialRead = socialReadHandle.get();
    GatewayRO gatewayRo(txnSocialRead);
    auto& txnCoreWrite  = coreWriteHandle.get();

    auto branchesInfo = investigateBranches(txnCoreWrite);

    auto savedDeployedBranchId = social::feedback::lastDeployedBranchId(txnCoreWrite);
    if (savedDeployedBranchId && branchesInfo.lastDeployedBranchId <= *savedDeployedBranchId) {
        INFO() << "No new deployed branch after " << *savedDeployedBranchId
                    << " found. There is no need to do anything.";
        return;
    }

    INFO() << "Setting deploy status...";
    INFO() << "We believe that all feedbacks closed before '"
           << chrono::formatSqlDateTime(branchesInfo.timeX) << "' are in production.";

    // find head feedbacks for check
    {
        TaskFilter filter;
        filter.resolved(true);
        filter.deployed(false);
        filter.verdict(Verdict::Accepted);
        filter.duplicateHeadId(std::nullopt);
        auto feedbacks = gatewayRo.tasksByFilter(filter);
        INFO() << feedbacks.size() << " head feedback tasks for check found.";

        auto deployedCount = setDeployed(txnCoreWrite, feedbacks);
        INFO() << "Deploy status set for " << deployedCount << " head feedback tasks.";
    }

    // find leaf feedbacks for check
    {
        TaskFilter filter;
        filter.resolved(true);
        filter.deployed(false);
        filter.verdict(Verdict::Accepted);
        auto feedbacks = gatewayRo.tasksByFilter(filter);
        boost::remove_erase_if(feedbacks,
            [](const Task& task) {
                return task.duplicateHeadId() == std::nullopt;
            });
        INFO() << feedbacks.size() << " leaf feedback tasks for check found.";

        auto deployedCount = setDeployed(txnCoreWrite, feedbacks);
        INFO() << "Deploy status set for " << deployedCount << " leaf feedback tasks.";
    }

    social::feedback::setLastDeployedBranchId(
        txnCoreWrite,
        branchesInfo.lastDeployedBranchId);

    txnCoreWrite.commit();
}

void Worker::doTask()
{
    statusWriter_.reset();

    std::optional<std::string> err;
    try {
        // Exclusive execution between hosts
        //
        pgp3utils::PgAdvisoryXactMutex dbCoreLocker(
        corePool_.pool(),
        static_cast<int64_t>(common::AdvisoryLockIds::FEEDBACK_SET_DEPLOYED));
        if (!dbCoreLocker.try_lock()) {
            INFO() << "Database is already locked. Task interrupted.";
            return;
        }

        doWork();

    } catch (const maps::Exception& ex) {
        err = (std::stringstream() << ex).str();
    } catch (const std::exception& ex) {
        err = ex.what();
    } catch (...) {
        err = "unknown exception";
    }

    if (err) {
        ERROR() << err.value();
        statusWriter_.err(err.value());
    }

    statusWriter_.flush();
}

} // namespace maps::wiki::social::feedback
