#include <maps/wikimap/mapspro/services/tasks-ng/lib/task.h>

#include <maps/wikimap/mapspro/services/tasks-ng/lib/config.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/exception.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/grinder.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/magic_strings.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/task_module.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/task_module_registry.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/query.h>

#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/wiki/common/date_time.h>

namespace maps {
namespace wiki {
namespace tasks_ng {

namespace {
const std::string COMMON_FIELDS = "type,created_by";
const std::string GET_FIELDS = "id,created,parent_id,grinder_task_id,frozen," + COMMON_FIELDS;
const std::string MODIFIED_FIELDS = "modified,modified_by";
}

BaseTask::BaseTask(const pqxx::row& row)
    : id(row["id"].as<Id>())
    , type(row["type"].as<std::string>())
    , created(row["created"].as<std::string>())
    , createdBy(row["created_by"].as<Uid>())
    , parentId(row["parent_id"].as<Id>(0))
    , grinderTaskId(row["grinder_task_id"].as<GrinderTaskId>({}))
    , frozen(row["frozen"].as<bool>({}))
{}

std::unique_ptr<BaseTask> BaseTask::create(
    DbContext& ctx,
    const std::string& type,
    Uid uid,
    Id parentId)
{
    auto& txnCore = ctx.txnCore();

    auto fields = Query()
        << COMMON_FIELDS << "," << MODIFIED_FIELDS;
    if (parentId) {
        fields << ", parent_id";
    }
    auto values = Query()
        << txnCore.quote(type) << ", " << uid << ", NOW(), " << uid;
    if (parentId) {
        values << ", " << parentId;
    }

    auto query = Query()
        .insertInto(sql::table::TASK)
        .columns(fields)
        .values(values)
        .returning(GET_FIELDS);

    auto rows = txnCore.exec(query.str());
    ASSERT(rows.size() == 1);
    return std::make_unique<BaseTask>(rows[0]);
}

const std::string& BaseTask::fields()
{
    return GET_FIELDS;
}

pqxx::result BaseTask::resultFromRow(
    pqxx::transaction_base& txn,
    const std::string& table,
    const std::string& fields,
    Id taskId)
{
    auto query = Query()
        .select(fields)
        .from(table)
        .where() << "id=" << taskId;
    auto res = txn.exec(query.str());
    ASSERT(res.size() <= 1);
    return res;
}

void BaseTask::writeProperties(XmlWriter& writer) const
{
    writer
        .addAttribute("id", id)
        .addAttribute("uid", createdBy)
        .addAttribute("type", type)
        .addAttribute("created",
            common::canonicalDateTimeString(created, common::WithTimeZone::Yes));
    if (parentId) {
        writer.addAttribute("parent-id", parentId);
    }
}


Task::Task(const pqxx::row& row)
{
    data_.baseTask = std::make_unique<BaseTask>(row);
}

Task::Task(
    DbContext& ctx,
    const std::string& type,
    Uid uid,
    Id parentId,
    const RequestParameters& parameters)
{
    const auto& module = TaskModuleRegistry::get().module(type);
    data_.baseTask = BaseTask::create(ctx, type, uid, parentId);
    module.onCreate(ctx, *this, parameters);
}

void Task::launch(DbContext& ctx, Uid uid)
{
    REQUIRE(data_.baseTask, "invalid task");
    auto taskId = id();

    const auto& module = TaskModuleRegistry::get().module(data_.baseTask->type);
    auto params = module.launchParameters(*this);

    GrinderGateway gtw(cfg());
    auto grinderTaskId = gtw.submitTask(taskId, params);

    try {
        auto& txnCore = ctx.txnCore();
        auto query = Query()
            .update(sql::table::TASK)
            .set(Query()
                << "grinder_task_id=" << txnCore.quote(grinderTaskId) << ','
                << "modified=NOW()" << ','
                << "modified_by=" << uid)
            .where(Query() << "id=" << taskId)
            .returning(GET_FIELDS);

        auto rows = txnCore.exec(query.str());
        ASSERT(rows.size() == 1);

        data_.baseTask = std::make_unique<BaseTask>(rows[0]);

    } catch (...) {
        ERROR() << "Task " << taskId << " wrong. "
                << "Try cancel grinder task id: " << grinderTaskId;
        gtw.tryCancelTask(taskId, grinderTaskId);

        throw;
    }
}

void Task::revoke(DbContext& ctx, Uid uid)
{
    auto taskId = id();
    TASKS_REQUIRE(
        revokable(), TASK_NOT_REVOCABLE, "Task " << taskId << " not revocable");

    const auto& grinderTaskId = baseTask().grinderTaskId;
    if (grinderTaskId.empty()) {
        return;
    }

    GrinderGateway gtw(cfg());
    INFO() << "Try cancel grinder task " << taskId << ", id: " << grinderTaskId;
    if (!gtw.tryCancelTask(taskId, grinderTaskId)) {
        return;
    }

    data_.status = ServiceTaskStatus::Revoked;

    auto& txnCore = ctx.txnCore();

    auto query = Query()
        .update(sql::table::TASK)
        .set(Query() << "modified=NOW(), modified_by=" << uid)
        .where(Query() << "id=" << taskId)
        .returning(GET_FIELDS);

    auto rows = txnCore.exec(query.str());
    ASSERT(rows.size() == 1);

    data_.baseTask = std::make_unique<BaseTask>(rows[0]);
}

void Task::resume(DbContext& ctx, Uid uid)
{
    auto taskId = id();
    TASKS_REQUIRE(
        resumable(), TASK_NOT_RESUMABLE, "Task " << taskId << " not resumable");

    auto grinderTaskId = baseTask().grinderTaskId;
    if (grinderTaskId.empty()) {
        return;
    }

    data_.status = ServiceTaskStatus::Pending;
    launch(ctx, uid);
}

bool Task::revokable() const
{
    if (!data_.context) {
        return false;
    }
    if (data_.status == ServiceTaskStatus::Started) {
        return data_.context->revokable();
    }
    return data_.status == ServiceTaskStatus::Pending;
}

bool Task::resumable() const
{
    if (!data_.context) {
        return false;
    }
    return data_.status == ServiceTaskStatus::Frozen;
}

const BaseTask& Task::baseTask() const
{
    ASSERT(data_.baseTask);
    return *data_.baseTask;
}

void Task::load(DbContext& ctx)
{
    updateStatus();

    const auto& base = baseTask();
    auto module = TaskModuleRegistry::get().modulePtr(base.type);
    if (!module) {
        WARN() << "Skip loading task data id: "
               << base.id << " type: " << base.type;
        return;
    }

    module->loadContext(ctx, *this);

    if (status() == ServiceTaskStatus::Success) {
        module->loadResult(ctx, *this);
    }
}

void Task::write(XmlWriter& writer, TaskWriteMode mode) const
{
    writer.addTag("task", [&] {
        writeProperties(writer);
        if (data_.context) {
            writer.addTag("context", [&] { data_.context->write(writer, mode); });
        }
        bool isError = status() == ServiceTaskStatus::Failure;
        if (isError || data_.result) {
            writer.addTag("result", [&] {
                if (isError) {
                    writer.addTag("error-result", [&] {
                        writer.addAttribute("status", INTERNAL_ERROR);
                        if (!data_.error.empty()) {
                            writer.addCdata(data_.error);
                        }
                    });
                } else {
                    data_.result->write(writer, mode);
                }
            });
        }
    });
}

void Task::updateStatus()
{
    const auto& grinderTaskId = baseTask().grinderTaskId;
    if (grinderTaskId.empty()) {
        data_.status = ServiceTaskStatus::Failure;
        data_.error = "Task not launched";
        return;
    }

    GrinderGateway gtw(cfg());
    auto result = gtw.tryGetTaskResult(id(), grinderTaskId);
    auto newStatus = result.serviceTaskStatus();
    if (data_.baseTask->frozen && newStatus == ServiceTaskStatus::Failure) {
        data_.status = ServiceTaskStatus::Frozen;
    } else {
        data_.status = newStatus;
    }
    if (newStatus == ServiceTaskStatus::Failure) {
        data_.error = result.msg;
    }
}

void Task::writeProperties(XmlWriter& writer) const
{
    baseTask().writeProperties(writer);
    writer
        .addAttribute("status", status())
        .addAttribute("revocable", revokable())
        .addAttribute("resumable", resumable());
}

} // namespace tasks_ng
} // namespace wiki
} // namespace maps
