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

#include <maps/libs/enum_io/include/enum_io.h>
#include <maps/libs/json/include/builder.h>

#include <maps/libs/http/include/http.h>
#include <maps/libs/log8/include/log8.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/magic_strings.h>

#include <algorithm>
#include <vector>

namespace maps {
namespace wiki {
namespace tasks_ng {

namespace {

const auto HTTP_STATUS_OK = 200;

enum_io::Representations<GrinderTaskStatus> GRINDER_TASK_STATUS_REPRESENTATION{
    {GrinderTaskStatus::New, "New"},
    {GrinderTaskStatus::Queued, "Queued"},
    {GrinderTaskStatus::Assigned, "Assigned"},
    {GrinderTaskStatus::Started, "Started"},
    {GrinderTaskStatus::Postponed, "Postponed"},
    {GrinderTaskStatus::Finished, "Finished"},
    {GrinderTaskStatus::Failed, "Failed"},
    {GrinderTaskStatus::Canceled, "Canceled"},
};

std::string performRequest(
    const http::Method& method,
    const std::string& url,
    const std::string& body = std::string{})
{
    http::URL urlWrapper(url);
    http::Client httpClient;
    http::Request request(httpClient, method, urlWrapper);
    if (!body.empty()) {
        request.addHeader("Content-Type", "application/json");
        request.setContent(body);
    }
    auto response = request.perform();
    REQUIRE(
        response.status() == HTTP_STATUS_OK,
        "http status: " << response.status() << "; " <<
        "method: " << method << "; " <<
        "url: " << url);
    return response.readBody();
}

void checkGrinderTaskId(const GrinderTaskId& grinderTaskId)
{
    REQUIRE(!grinderTaskId.empty(), "Empty grinder task id");
}

} // namespace

DEFINE_ENUM_IO(GrinderTaskStatus, GRINDER_TASK_STATUS_REPRESENTATION);

ServiceTaskStatus GrinderTaskResult::serviceTaskStatus() const
{
    switch (status) {
        case GrinderTaskStatus::New:
            return ServiceTaskStatus::Pending;

        case GrinderTaskStatus::Queued:
            return ServiceTaskStatus::Pending;

        case GrinderTaskStatus::Assigned:
            return ServiceTaskStatus::Pending;

        case GrinderTaskStatus::Started:
            return ServiceTaskStatus::Started;

        case GrinderTaskStatus::Postponed:
            return ServiceTaskStatus::Pending;

        case GrinderTaskStatus::Finished:
            return ServiceTaskStatus::Success;

        case GrinderTaskStatus::Failed:
            return ServiceTaskStatus::Failure;

        case GrinderTaskStatus::Canceled:
            return ServiceTaskStatus::Revoked;
    }
}


GrinderGateway::GrinderGateway(const Config& config)
    : grinderHost_("http://" + config.grinderHost())
{}

GrinderTaskResult GrinderGateway::tryGetTaskResult(
    Id taskId,
    const GrinderTaskId& grinderTaskId) const
{
    try {
        return getTaskResult(grinderTaskId);
    } catch (const std::exception& ex) {
        ERROR()
            << "Invalid grinder tasklog result, task: " << taskId
            << " grinder task id: " << grinderTaskId << " : " << ex.what();
    }
    return {};
}

GrinderTaskId GrinderGateway::submitTask(Id taskId, const json::Value& parameters) const
{
    try {
        return submitTask(parameters);
    } catch (const std::exception& ex) {
        ERROR() << "Error on submit task: " << taskId << " : " << ex.what();
        throw;
    }
}

bool GrinderGateway::tryCancelTask(Id taskId, const GrinderTaskId& grinderTaskId) const
{
    try {
        return cancelTask(grinderTaskId);
    } catch (const std::exception& ex) {
        ERROR() << "Error on cancel task: " << taskId
                << " grinder task id: " << grinderTaskId << " : " << ex.what();
    }
    return false;
}

GrinderTaskResult GrinderGateway::getTaskResult(const GrinderTaskId& grinderTaskId) const
{
    checkGrinderTaskId(grinderTaskId);

    auto httpResult = performRequest(
        http::GET,
        grinderHost_ + "/tasklog/" + grinderTaskId);
    if (httpResult.empty()) {
        return {};
    }

    auto jsonData = json::Value::fromString(httpResult);
    REQUIRE(jsonData.isArray(), "Grinder result not array: " << httpResult);

    std::vector<GrinderTaskResult> results;
    results.reserve(jsonData.size());
    for (const auto& value : jsonData) {
        ASSERT(value.isObject());

        results.emplace_back(GrinderTaskResult{
            boost::lexical_cast<int64_t>(value["ts"].toString()),
            boost::lexical_cast<GrinderTaskStatus>(value["status"].toString()),
            value["pid"].toString(),
            value["msg"].toString()
        });
    }
    if (results.empty()) {
        return {};
    }
    std::sort(
        results.begin(), results.end(),
        [](const GrinderTaskResult& a, const GrinderTaskResult& b) {
            return a.timestampMs < b.timestampMs;
        }
    );
    return std::move(results.back());
}

GrinderTaskId GrinderGateway::submitTask(const json::Value& parameters) const
{
    REQUIRE(parameters.isObject(), "Invalid task arguments");
    REQUIRE(
        parameters.hasField(TYPE),
        "Task arguments must contain 'type' field");

    json::Builder builder;
    builder << parameters;

    auto httpResult = performRequest(
        http::POST, grinderHost_ + "/tasks", builder.str());

    auto jsonData = json::Value::fromString(httpResult);
    REQUIRE(
        jsonData.isObject(),
        "Grinder result not object: " << httpResult);

    auto grinderTaskId = jsonData["id"].toString();
    checkGrinderTaskId(grinderTaskId);
    return grinderTaskId;
}

bool GrinderGateway::cancelTask(const GrinderTaskId& grinderTaskId) const
{
    checkGrinderTaskId(grinderTaskId);

    auto httpResult = performRequest(
        http::PUT,
        grinderHost_ + "/tasks/" + grinderTaskId + "/status",
        "\"cancelled\"");

    auto jsonData = json::Value::fromString(httpResult);
    return jsonData["canceled"].as<bool>();
}

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