#include "client_impl.h"
#include "magic_strings.h"
#include "task_impl.h"
#include <yandex/maps/mrc/toloka_client/filter.h>
#include <yandex/maps/mrc/toloka_client/task.h>
#include <maps/libs/json/include/exception.h>

namespace maps {
namespace mrc {
namespace toloka {
namespace io {

namespace {

constexpr size_t DEFAULT_PAGE_LIMIT = 50;

} // anonymous namespace

json::Value
TolokaClient::Impl::requestTasksPage(const Filter& filter, size_t limit) const
{
    auto httpClient = makeHttpClient();
    http::URL url = schema_ + host_ + "/api/v1/tasks";

    auto response = retry([&](){
        http::Request request(*httpClient, http::GET, url);
        addAuthHeader(request);
        request.addParam(PARAM_SORT, FIELD_ID);
        request.addParam(PARAM_LIMIT, std::to_string(limit));
        filter.apply<Task>(request);

        return performRequestChecked(request, url);
    });

    try {
       return json::Value::fromStream(response.body());
    } catch (const maps::json::ParserError& e) {
        throw ServerError() << "Failed to parse server response, url: " << url
                << ", " << e.what();
    }
}



TasksResponse TolokaClient::Impl::getTasks(Filter filter) const
{
    bool hasMore = true;
    std::string lastId;
    Tasks tasks;

    while (hasMore) {
        if (!lastId.empty()) {
            filter.idGreaterThan(lastId);
        }

        auto jsonBody = requestTasksPage(filter, DEFAULT_PAGE_LIMIT);
        hasMore = jsonBody[FIELD_HAS_MORE].as<bool>();

        for (const auto& item : jsonBody[FIELD_ITEMS]) {
            tasks.push_back(PImplFactory::create<Task>(item));
        }
        if (!tasks.empty()) {
            lastId = tasks.back().id();
        }
    }
    return PImplFactory::create<TasksResponse>(std::move(tasks), false);
}

TasksResponse TolokaClient::Impl::getTasks(const Filter& filter,
                                           size_t limit) const
{
    Tasks tasks;
    auto jsonBody = requestTasksPage(filter, limit);
    bool hasMore = jsonBody[FIELD_HAS_MORE].as<bool>();
    for (const auto& item : jsonBody[FIELD_ITEMS]) {
        tasks.push_back(PImplFactory::create<Task>(item));
    }
    return PImplFactory::create<TasksResponse>(std::move(tasks), hasMore);
}

Task TolokaClient::Impl::getTask(const std::string& taskId) const
{
    auto httpClient = makeHttpClient();
    http::URL url = schema_ + host_ + "/api/v1/tasks/" + taskId;

    auto response = retry([&](){
        http::Request request(*httpClient, http::GET, url);
        addAuthHeader(request);

        return performRequestChecked(request, url);
    });

    try {
        auto jsonBody = json::Value::fromStream(response.body());
        return PImplFactory::create<Task>(jsonBody);
    } catch (const maps::json::ParserError& e) {
        throw ServerError() << "Failed to parse server response, url: " << url
                << ", " << e.what();
    }
}

Task TolokaClient::Impl::postTask(const TaskCreationParams& taskCreationParams,
                                  const StringMap& params) const
{
    auto httpClient = makeHttpClient();
    http::URL url = schema_ + host_ + "/api/v1/tasks";

    auto response = retry([&](){
        http::Request request(*httpClient, http::POST, url);
        addAuthHeader(request);
        addContentTypeHeader(request);
        for (const auto& param : params) {
            request.addParam(param.first, param.second);
        }
        std::stringstream ss;
        ss << taskCreationParams;
        request.setContent(ss);

        return performRequestChecked(request, url);
    });

    try {
        auto jsonBody = json::Value::fromStream(response.body());
        return PImplFactory::create<Task>(jsonBody);
    } catch (const maps::json::ParserError& e) {
        throw ServerError() << "Failed to parse server response, url: " << url
                << ", " << e.what();
    }
}

TasksPostResponse TolokaClient::Impl::postTasks(
    const std::vector<TaskCreationParams>& taskCreationParamsVec,
    const StringMap& params) const
{
    auto httpClient = makeHttpClient();
    http::URL url = schema_ + host_ + "/api/v1/tasks";

    auto response = retry([&](){
        http::Request request(*httpClient, http::POST, url);
        addAuthHeader(request);
        addContentTypeHeader(request);
        for (const auto& param : params) {
            request.addParam(param.first, param.second);
        }
        std::stringstream ss;
        ss << taskCreationParamsVec;
        request.setContent(ss);

        return performRequestChecked(request, url);
    });

    try {
        auto jsonBody = json::Value::fromStream(response.body());
        std::unordered_map<size_t, Task> tasks;
        std::unordered_map<size_t, json::Value> errors;

        const auto& items = jsonBody[FIELD_ITEMS];
        const auto& validationErrors = jsonBody[FIELD_VALIDATION_ERRORS];

        for (const auto& key : items.fields()) {
            tasks.emplace(boost::lexical_cast<size_t>(key),
                          PImplFactory::create<Task>(items[key]));
        }
        for (const auto& key : validationErrors.fields()) {
            errors.emplace(boost::lexical_cast<size_t>(key),
                           validationErrors[key]);
        }

        return PImplFactory::create<TasksPostResponse>(
            std::move(tasks), std::move(errors));
    } catch (const maps::json::ParserError& e) {
        throw ServerError() << "Failed to parse server response, url: " << url
                << ", " << e.what();
    }
}


Task TolokaClient::Impl::patchTask(const std::string& taskId,
                                   const std::string& data) const
{
    auto httpClient = makeHttpClient();
    http::URL url = schema_ + host_ + "/api/v1/tasks/" + taskId;

    auto response = retry([&](){
        http::Request request(*httpClient, http::PATCH, url);
        addAuthHeader(request);
        addContentTypeHeader(request);
        request.setContent(data);
        return performRequestChecked(request, url);
    });

    try {
        auto jsonBody = json::Value::fromStream(response.body());
        return PImplFactory::create<Task>(jsonBody);
    } catch (const maps::json::ParserError& e) {
        throw ServerError() << "Failed to parse server response, url: " << url
                << ", " << e.what();
    }
}

Task TolokaClient::Impl::stopAssigningTask(const std::string& taskId) const
{
    auto httpClient = makeHttpClient();
    http::URL url = schema_ + host_ + "/api/v1/tasks/" + taskId + "/set-overlap-or-min";

    json::Builder builder;
    builder << [=](json::ObjectBuilder b) {
        b[FIELD_OVERLAP] = 0;
        b[FIELD_INFINITE_OVERLAP] = false;
    };

    auto response = retry([&](){
        http::Request request(*httpClient, http::PATCH, url);
        addAuthHeader(request);
        addContentTypeHeader(request);
        request.setContent(builder.str());
        return performRequestChecked(request, url);
    });

    try {
        auto jsonBody = json::Value::fromStream(response.body());
        return PImplFactory::create<Task>(jsonBody);
    } catch (const maps::json::ParserError& e) {
        throw ServerError() << "Failed to parse server response, url: " << url
                << ", " << e.what();
    }
}

} // namespace io
} // namespace toloka
} // namespace mrc
} // namespace maps
