#include "config.h"
#include "logger_proxy.h"
#include "tools.h"

#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/common/include/exception.h>
#include <maps/tools/grinder/worker/include/api.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/mongo/include/init.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>
#include <yandex/maps/wiki/common/retry_duration.h>
#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/common/secrets.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/tasks/task_logger.h>
#include <yandex/maps/wiki/revision/common.h>
#include <yandex/maps/wiki/tasks/task_manager.h>
#include <yandex/maps/wiki/validator/storage/results_gateway.h>
#include <yandex/maps/wiki/validator/storage/message_attributes_filter.h>

#include <maps/libs/st/include/st.h>

#include <chrono>
#include <memory>
#include <string>
#include <thread>

namespace log8 = maps::log8;
namespace release = maps::wiki::release;
namespace tasks = maps::wiki::tasks;
namespace worker = maps::grinder::worker;
namespace revision = maps::wiki::revision;
namespace validator = maps::wiki::validator;
namespace mwc = maps::wiki::common;

namespace {

constexpr size_t WORKER_CONCURRENCY = 1;
const std::string TASK_ID_ARG = "taskId";
const std::string UID_ARG = "uid";
constexpr auto POLLING_INTERVAL = std::chrono::seconds(5);
constexpr auto WAIT_TASKS_DURATION = std::chrono::hours(9);

const std::string CONFIG_PATH_ST = "/config/common/st";
const std::string CONFIG_ATTR_ST_API_URL = "api-base-url";

const std::string CONFIG_PATH_BASE = "/config/services/tasks/" + release::TASK_NAME;
const std::string CONFIG_PATH_ST_QUEUE = CONFIG_PATH_BASE + "/validation-issue-queue";
const std::string CONFIG_PATH_ST_ASSIGNEE = CONFIG_PATH_BASE + "/validation-issue-assignee";
const std::string CONFIG_PATH_ST_PARENT = CONFIG_PATH_BASE + "/validation-issue-parent";

const std::string ST_ISSUE_SUMMARY = "Фатальные ошибки в релизной ветке ";
const std::string FATAL_TASK_MESSAGE = "Задача завершилась аварийно: ";

std::unique_ptr<release::Config> globalCfg;
std::unique_ptr<maps::wiki::common::PoolHolder> globalCorePool;
std::unique_ptr<maps::wiki::common::PoolHolder> globalValidationPool;
std::unique_ptr<maps::wiki::common::PoolHolder> globalViewPool;
std::unique_ptr<maps::wiki::common::PoolHolder> globalLabelsPool;

revision::Branch waitForNewStableBranch(
    const worker::Task& grinderTask,
    const tasks::TaskManager& taskManager,
    const tasks::TaskInfo& createStableBranchTaskInfo)
{
    while (true) {
        if (grinderTask.isCanceled()) {
            throw worker::TaskCanceledException{};
        }

        auto branch = release::getStableBranch(globalCorePool->pool());
        if (branch) {
            return *branch;
        }

        auto updatedTaskInfo = mwc::retryDuration([&] {
            return taskManager.taskInfo(
                createStableBranchTaskInfo.id(), createStableBranchTaskInfo.token());
        });
        REQUIRE(updatedTaskInfo.status() != tasks::TaskStatus::Failed, "Create stable branch task has failed");

        std::this_thread::sleep_for(POLLING_INTERVAL);
    }
}

bool taskSuccessOrFrozen(
    const tasks::TaskManager::TaskWaitResult& waitResult,
    const tasks::TaskInfo& taskInfo)
{
    auto it = waitResult.taskStatuses.find(taskInfo.id());
    return it != waitResult.taskStatuses.end() &&
        (it->second == tasks::TaskStatus::Success || it->second == tasks::TaskStatus::Frozen);
}

bool hasFatalMessages(const tasks::TaskInfo& task)
{
    validator::storage::MessageAttributesFilter filter;
    filter.severity = validator::Severity::Max;

    return mwc::retryDuration([&] {
        auto validationTxn = globalValidationPool->pool().masterReadOnlyTransaction();

        validator::storage::ResultsGateway validatorGateway(*validationTxn, task.id());
        return validatorGateway.messageCount(filter);
    });
}

tasks::TaskInfos findChildTasks(
    tasks::TaskId parentTaskId,
    const tasks::TaskManager& taskManager)
{
    return mwc::retryDuration([&] {
        auto txn = globalCorePool->pool().masterReadOnlyTransaction();
        auto rows = txn->exec(
            "SELECT id FROM service.task WHERE parent_id = " +
                std::to_string(parentTaskId));

        tasks::TaskInfos taskInfos;
        for (const auto& row : rows) {
            auto childTaskId = row[0].as<tasks::TaskId>();
            taskInfos.push_back(taskManager.taskInfo(childTaskId));
        }
        return taskInfos;
    });
}

std::string nproFatalMessagesLink(
    revision::DBID branchId,
    release::TaskId taskId)
{
    std::ostringstream link;
    link << "https://"
         << globalCfg->configDoc().get<std::string>("/config/common/npro/host", {})
         << "/#!/tools/longtasks/"
         << taskId
         << "?branch=" << branchId
         << "&severity=fatal";
    return link.str();
}

std::vector<std::string> nproFatalMessagesLinks(
    revision::DBID branchId,
    tasks::TaskInfos validationTasks,
    const tasks::TaskManager& taskManager,
    tasks::TaskManager::CheckCancelCallback checkCanceled)
{
    std::vector<std::string> messages;
    while (!validationTasks.empty()) {
        auto waitResult = taskManager.waitForTasks(
            validationTasks, WAIT_TASKS_DURATION, checkCanceled);

        tasks::TaskInfos childTasks;
        for (const auto& task : validationTasks) {
            if (!waitResult.isSuccess(task.id())) {
                messages.push_back(FATAL_TASK_MESSAGE + std::to_string(task.id()));
            } else if (hasFatalMessages(task)) {
                messages.push_back(nproFatalMessagesLink(branchId, task.id()));
            }
            childTasks.splice(childTasks.end(),
                findChildTasks(task.id(), taskManager));
        }
        validationTasks = std::move(childTasks);
    }
    return messages;
}

void run(const worker::Task& grinderTask)
{
    const auto& args = grinderTask.args();
    const auto taskId = args[TASK_ID_ARG].as<release::TaskId>();
    const auto uid = args[UID_ARG].as<tasks::UserId>();
    tasks::TaskPgLogger logger(globalCorePool->pool(), taskId);
    DUAL_INFO(logger) << "Task " << taskId
                      << " started (Grinder task ID: " << grinderTask.id()
                      << ")";

    try {
        tasks::TaskManager taskManager(globalCfg->tasksUrl(), uid);

        if (uid == mwc::ROBOT_UID && !release::isTaskScheduled(globalCorePool->pool())) {
            DUAL_INFO(logger) << "Scheduled task start disabled";
            return;
        }

        auto publishedBranch = release::publishStableBranch(
            globalCorePool->pool(), globalLabelsPool->pool(), uid);
        if (publishedBranch) {
            DUAL_INFO(logger) << "Published branch " << publishedBranch->id();
        }

        auto deletedBranch = release::deleteExceedingArchiveBranch(
            globalCorePool->pool(), globalViewPool->pool(), globalLabelsPool->pool(), uid);
        if (deletedBranch) {
            DUAL_INFO(logger) << "Deleted branch " << deletedBranch->id();
        }

        // Make sure that stable branch does not exist
        REQUIRE(!release::getStableBranch(globalCorePool->pool()), "Stable branch should not exist");

        auto approvedBranch = release::getApprovedBranch(globalCorePool->pool());
        REQUIRE(approvedBranch.state() == revision::BranchState::Normal,
            "Approved branch should be in normal state. Current state " << approvedBranch.state());
        auto approvedBranchId = approvedBranch.id();

        // Create new stable branch
        auto createStableBranchTaskInfo
            = taskManager.startCreateStableBranch(approvedBranchId);
        DUAL_INFO(logger) << "Creating stable branch task ID: "
                          << createStableBranchTaskInfo.id();
        auto stableBranch = waitForNewStableBranch(grinderTask, taskManager, createStableBranchTaskInfo);
        REQUIRE(stableBranch.state() != revision::BranchState::Progress, "Unexpected progress state");
        auto stableBranchId = stableBranch.id();

        // Start shadow attributes application
        auto applyShadowAttributesTaskInfo
            = taskManager.startApplyShadowAttributes(stableBranchId);
        DUAL_INFO(logger) << "Apply shadow attributes task ID: "
                          << applyShadowAttributesTaskInfo.id();

        auto checkCanceled = [&grinderTask]() {
                if (grinderTask.isCanceled()) {
                    throw mwc::RetryDurationCancel();
                }
            };

        auto waitResult = taskManager.waitForTasks(
            {applyShadowAttributesTaskInfo}, WAIT_TASKS_DURATION, checkCanceled);
        REQUIRE(taskSuccessOrFrozen(waitResult, applyShadowAttributesTaskInfo),
            "Shadow attributes task has failed");
        DUAL_INFO(logger) << "Shadow attributes are applied";

        // Start validation
        tasks::TaskInfos validationTaskInfos;
        for (const auto& param : globalCfg->validationParams()) {
            auto aoiId = param.aoiId
                ? boost::optional<tasks::ObjectId>(param.aoiId)
                : tasks::NO_AOI;
            auto regionId = param.regionId
                ? boost::optional<tasks::ObjectId>(param.regionId)
                : tasks::NO_REGION;
            validationTaskInfos.push_back(taskManager.startValidation(
                stableBranchId, param.presetId, aoiId, regionId));
        }

        for (const auto& taskInfo : validationTaskInfos) {
            DUAL_INFO(logger) << "Subtask ID: " << taskInfo.id() << " " << taskInfo.type();
        }

        // Wait for stable branch view syncronization
        waitResult = taskManager.waitForTasks(
            {createStableBranchTaskInfo}, WAIT_TASKS_DURATION, checkCanceled);
        REQUIRE(waitResult.isSuccess(createStableBranchTaskInfo.id()),
            "Create stable branch task has failed");
        DUAL_INFO(logger) << "Stable branch is created: " << stableBranchId;

        // Unfreeze shadow attributes task if it is necessary
        if (release::isTaskFrozen(globalCorePool->pool(), applyShadowAttributesTaskInfo.id())) {
            DUAL_INFO(logger) << "Resume shadow attributes task";
            applyShadowAttributesTaskInfo = taskManager.resumeTask(applyShadowAttributesTaskInfo.id());
            waitResult = taskManager.waitForTasks(
                {applyShadowAttributesTaskInfo}, WAIT_TASKS_DURATION, checkCanceled);
            REQUIRE(waitResult.isSuccess(applyShadowAttributesTaskInfo.id()),
                "Shadow attributes task has failed");
        }

        // Start diffalert
        tasks::TaskInfos diffalertTaskInfos;
        auto archiveBranchId =
            release::mostRecentArchiveBranchId(globalCorePool->pool());
        if (archiveBranchId) {
            diffalertTaskInfos.push_back(taskManager.startDiffalert(
                *archiveBranchId, stableBranchId));
            const auto& taskInfo = diffalertTaskInfos.back();
            DUAL_INFO(logger) << "Subtask ID: " << taskInfo.id() << " " << taskInfo.type();
        }

        // Create issue for validations with fatal errors
        auto messagesLinks = nproFatalMessagesLinks(
            stableBranchId,
            validationTaskInfos,
            taskManager,
            checkCanceled);
        if (!messagesLinks.empty()) {
            maps::st::Gateway stGateway(maps::st::Configuration(
                globalCfg->configDoc().getAttr<std::string>(
                    CONFIG_PATH_ST, CONFIG_ATTR_ST_API_URL),
                mwc::secrets::tokenByKey(mwc::secrets::Key::RobotWikimapStToken)));

            maps::st::IssuePatch issuePatch;
            issuePatch.summary().set(ST_ISSUE_SUMMARY + std::to_string(stableBranchId));
            issuePatch.description().set(maps::wiki::common::join(messagesLinks, '\n'));

            auto assigneeLogin = globalCfg->configDoc().get<std::string>(
                CONFIG_PATH_ST_ASSIGNEE, {});
            if (!assigneeLogin.empty()) {
                issuePatch.assignee().set(assigneeLogin);
            }

            auto parentKey = globalCfg->configDoc().get<std::string>(
                CONFIG_PATH_ST_PARENT, {});
            if (!parentKey.empty()) {
                issuePatch.parent().set(parentKey);
            }

            auto issueQueue = globalCfg->configDoc().get<std::string>(
                CONFIG_PATH_ST_QUEUE, {});
            REQUIRE(!issueQueue.empty(), "No StarTrek queue key in config");

            auto issue = stGateway.createIssue(issueQueue, issuePatch);
            DUAL_INFO(logger) << "Fatal messages issue created " << issue.key();
        }

        // Wait for the remaining executing tasks
        waitResult = taskManager.waitForTasks(
            diffalertTaskInfos, WAIT_TASKS_DURATION, checkCanceled);
        for (const auto& taskInfo : diffalertTaskInfos) {
            REQUIRE(waitResult.isSuccess(taskInfo.id()),
                "Task " << taskInfo.id() << " has failed");
        }
        release::saveResult(globalCorePool->pool(), taskId, stableBranchId);
        DUAL_INFO(logger) << "Task finished. Branch ID: " << stableBranchId;
    }
    catch (const mwc::RetryDurationCancel&) {
        DUAL_WARN(logger) << "Task canceled.";
        throw worker::TaskCanceledException();
    }
    catch (const worker::TaskCanceledException&) {
        DUAL_WARN(logger) << "Task canceled";
        throw;
    }
    catch (const std::exception& e) {
        DUAL_ERROR(logger) << "Task failed: " << e.what();
        throw;
    }
}

} // namespace

int main(int argc, char* argv[]) try {
    maps::cmdline::Parser parser;
    auto workerConfig
        = parser.string("config").help("path to worker configuration");
    auto syslogTag = parser.string("syslog-tag").help(
        "redirect log output to syslog with given tag");
    auto grinderConfig = parser.string("grinder-config")
                             .help("path to grinder configuration");
    parser.parse(argc, argv);

    std::string cfgPath;
    if (workerConfig.defined()) {
        cfgPath = workerConfig;
    }

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

    maps::mongo::init();

    worker::Options workerOpts;
    if (grinderConfig.defined()) {
        workerOpts.setConfigLocation(grinderConfig);
    }

    globalCfg.reset(new release::Config(cfgPath));
    globalCorePool.reset(new maps::wiki::common::PoolHolder(
        globalCfg->configDoc(), "core", "grinder"));
    globalValidationPool.reset(new maps::wiki::common::PoolHolder(
        globalCfg->configDoc(), "validation", "grinder"));
    globalViewPool.reset(new maps::wiki::common::PoolHolder(
        globalCfg->configDoc(), "view-stable", "editor-tool"));
    globalLabelsPool.reset(new maps::wiki::common::PoolHolder(
        globalCfg->configDoc(), "view-stable-labels", "editor-tool"));

    workerOpts.on(release::TASK_NAME, [](const worker::Task& grinderTask) {
                      run(grinderTask);
                  }).setConcurrencyLevel(WORKER_CONCURRENCY);
    worker::run(std::move(workerOpts));
    return EXIT_SUCCESS;
}
catch (const maps::Exception& e) {
    ERROR() << "Worker failed: " << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    ERROR() << "Worker failed: " << e.what();
    return EXIT_FAILURE;
}
