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

#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/hitman_client/include/client.h>

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

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

using namespace std::string_view_literals;

namespace maps::wiki::autocart::pipeline::hitman {

namespace {

const std::string FIELD_ID = "id";
const std::string FIELD_STATUS = "status";
const std::string FIELD_REQUESTER = "requester";
const std::string FIELD_PROPERTIES = "properties";
const std::string FIELD_WORKFLOW = "workflow";
const std::string FIELD_EXECUTIONS = "executions";

const std::string PARAM_STATUS = "status";

const std::string AUTH_HEADER_NAME = "Authorization";
const std::string CONTENT_TYPE_HEADER_NAME = "Content-Type";
const std::string JSON_CONTENT_TYPE = "application/json";

constexpr enum_io::Representations<HitmanJobStatus> HITMAN_JOB_STATUS_STRINGS {
    {HitmanJobStatus::NEW, "NEW"},
    {HitmanJobStatus::RUNNING, "RUNNING"},
    {HitmanJobStatus::START_FAILED, "START_FAILED"},
    {HitmanJobStatus::FAILED, "FAILED"},
    {HitmanJobStatus::CANCELED, "CANCELED"},
    {HitmanJobStatus::SUCCEEDED, "SUCCEEDED"},
    {HitmanJobStatus::FIXED, "FIXED"},
    {HitmanJobStatus::RESUMING, "RESUMING"},
    {HitmanJobStatus::IN_CANCELLATION, "IN_CANCELLATION"}
};

} // namespace

DEFINE_ENUM_IO(HitmanJobStatus, HITMAN_JOB_STATUS_STRINGS);

HitmanClient::HitmanClient(const std::string& host, const std::string& authToken)
    : host_(host),
      authToken_(authToken),
      schema_("https://"),
      httpClient_(new http::Client())
{
    httpClient_->setTimeout(timeout_);
}


const std::string& HitmanClient::schema() const {
    return schema_;
}

HitmanClient& HitmanClient::setSchema(std::string schema) {
    schema_ = std::move(schema);
    return *this;
}


std::chrono::milliseconds HitmanClient::timeout() const {
    return timeout_;
}

HitmanClient& HitmanClient::setTimeout(std::chrono::milliseconds timeout) {
    REQUIRE(timeout > std::chrono::milliseconds::zero(),
            "Timeout should be greater than zero");
    timeout_ = timeout;
    httpClient_->setTimeout(timeout_);
    return *this;
}


size_t HitmanClient::maxRequestAttempts() const {
    return maxRequestAttempts_;
}

HitmanClient& HitmanClient::setMaxRequestAttempts(size_t maxRequestAttempts) {
    maxRequestAttempts_ = maxRequestAttempts;
    return *this;
}


std::chrono::milliseconds HitmanClient::retryInitialTimeout() const {
    return retryInitialTimeout_;
}

HitmanClient& HitmanClient::setRetryInitialTimeout(std::chrono::milliseconds timeout) {
    retryInitialTimeout_ = timeout;
    return *this;
}


double HitmanClient::retryTimeoutBackoff() const {
    return retryTimeoutBackoff_;
}

HitmanClient& HitmanClient::setRetryTimeoutBackoff(double retryTimeoutBackoff) {
    retryTimeoutBackoff_ = retryTimeoutBackoff;
    return *this;
}

std::string HitmanClient::processParamsToJson(
    const std::string& requester, const HitmanProcessProperties& properties) const
{
    std::stringstream ss;
    json::Builder builder(ss);
    builder << [&](json::ObjectBuilder b) {
        b[FIELD_REQUESTER] = requester;
        b[FIELD_PROPERTIES] = [&](json::ObjectBuilder b) {
            for (const auto& [name, value] : properties) {
                b[name] = value;
            }
        };
    };
    return ss.str();
}

http::Response HitmanClient::performRequestChecked(http::Request& request) const {
    http::Response response = [&](){
        try {
            return request.perform();
        } catch (http::Error& e) {
            throw ServerError() << "http request failed: " << e.what()
                                 << ", url: " << request.url();
        }
    }();

    if (response.status() >= 500) {
        throw ServerError() << "Unexpected status: " << response.status()
                            << ", url: " << request.url();
    } else if (response.status() == 412) {
        throw ParallelRunsLimit() << "Number of parallel runs was exceeded";
    } else if (response.status() / 100 != 2) {
        throw ClientError() << "Unexpected status: " << response.status()
                            << ", url: " << request.url();
    }

    return response;
}

HitmanJobId HitmanClient::runProcess(
    const std::string& processCode,
    const std::string& requester,
    const HitmanProcessProperties& properties) const
{
    http::URL url = schema_ + host_ + "/api/v1/execution/start/" + processCode;
    std::string paramsJson = processParamsToJson(requester, properties);

    http::Response response = retry([&](){
        http::Request request(*httpClient_, http::POST, url);
        request.addHeader(AUTH_HEADER_NAME, "OAuth " + authToken_);
        request.addHeader(CONTENT_TYPE_HEADER_NAME, JSON_CONTENT_TYPE);
        std::stringstream ss;
        ss << paramsJson;
        request.setContent(ss);

        return performRequestChecked(request);
    });

    json::Value jsonBody = json::Value::fromStream(response.body());

    if (jsonBody.hasField(FIELD_ID)) {
        return std::stoull(jsonBody[FIELD_ID].as<std::string>());
    } else {
        throw RuntimeError() << "Failed to parse server response, url: " << url
                             << ", jobId does not exist";
    }
}

HitmanJobId HitmanClient::waitAvailableRunsAndRunProcess(
    const std::string& processCode,
    const std::string& requester,
    const HitmanProcessProperties& properties,
    const std::chrono::minutes& waitTimeout,
    const std::chrono::minutes& recheckInterval) const
{
    std::chrono::time_point<std::chrono::system_clock> start
        = std::chrono::system_clock::now();
    std::chrono::time_point<std::chrono::system_clock> now = start;
    while (std::chrono::duration_cast<std::chrono::minutes>(now - start) < waitTimeout) {
        try {
            return runProcess(processCode, requester, properties);
        } catch (const ParallelRunsLimit& e) {
            INFO() << e;
            std::this_thread::sleep_for(recheckInterval);
            now = std::chrono::system_clock::now();
        }
    }
    return runProcess(processCode, requester, properties);
}

HitmanJobStatus HitmanClient::getJobStatus(const HitmanJobId& jobId) const {
    http::URL url = schema_ + host_ + "/api/v1/execution/" + std::to_string(jobId);

    http::Response response = retry([&](){
        http::Request request(*httpClient_, http::GET, url);
        request.addHeader(AUTH_HEADER_NAME, "OAuth " + authToken_);

        return performRequestChecked(request);
    });

    json::Value jsonBody = json::Value::fromStream(response.body());

    if (jsonBody.hasField(FIELD_STATUS)) {
        HitmanJobStatus status;
        fromString(jsonBody[FIELD_STATUS].as<std::string>(), status);
        return status;
    } else {
        throw RuntimeError() << "Failed to parse server response, url: " << url
                             << ", jod status does not exist";
    }
}

/*
* Get all Hitman jobs with specified status
* Request example:
*   https://sandbox.hitman.yandex-team.ru/api/v2/process/example/jobs&status=RUNNING
*
* Response example (json array):
* [
*   {"id": "1", ..., "executions": ["id": "4", "workflow": "a23df/werq", ...]},
*   {"id": "2", ..., "executions": ["id": "5", "workflow": "qer23/daf3", ...]},
*   ...
* ]
*
* Field "executions" contains information about all executions of the job.
* Job status is status of last execution (with the largest id).
* Field "workflow" contains id of Nirvana graph in the following formats:
*   * "<workflowId>/<instanceId>" - if all jobs run in one workflow
*   * "<workflowId>" - if new workflow is created for each job
*/
std::vector<HitmanJob> HitmanClient::getJobsByStatus(
    const std::string& processCode, const HitmanJobStatus& status) const
{
    http::URL url = schema_ + host_ + "/api/v2/process/" + processCode + "/jobs";

    http::Response response = retry([&](){
        http::Request request(*httpClient_, http::GET, url);
        request.addHeader(AUTH_HEADER_NAME, "OAuth " + authToken_);
        request.addParam(PARAM_STATUS, toString(status));

        return performRequestChecked(request);
    });

    json::Value jsonBody = json::Value::fromStream(response.body());

    if (jsonBody.isArray()) {
        std::vector<HitmanJob> jobs;
        for (const json::Value& jobJson : jsonBody) {
            HitmanJobId jobId = std::stoull(jobJson[FIELD_ID].as<std::string>());
            std::string workflow;
            uint64_t lastExecId = 0;
            for (const json::Value& execJson : jobJson[FIELD_EXECUTIONS]) {
                uint64_t execId = std::stoull(execJson[FIELD_ID].as<std::string>());
                if (execId > lastExecId) {
                    workflow = execJson[FIELD_WORKFLOW].as<std::string>();
                    lastExecId = execId;
                }
            }
            size_t delimPos = workflow.find("/");
            std::string workflowId;
            std::string instanceId;
            if (delimPos != std::string::npos) {
                workflowId = workflow.substr(0, delimPos);
                instanceId = workflow.substr(delimPos + 1);
            } else {
                workflowId = workflow;
            }
            jobs.push_back({jobId, workflowId, instanceId});
        }
        return jobs;
    } else {
        throw RuntimeError() << "Failed to parse server response, url: " << url
                             << ", server response is not json array";
    }
}

HitmanProcessProperties HitmanClient::getProcessProperties(const HitmanJobId& jobId) const {
    const std::string FIELD_START_PROPERTIES = "startProperties";
    http::URL url = schema_ + host_ + "/api/v1/spec/job/" + std::to_string(jobId);

    http::Response response = retry([&](){
        http::Request request(*httpClient_, http::GET, url);
        request.addHeader(AUTH_HEADER_NAME, "OAuth " + authToken_);

        return performRequestChecked(request);
    });

    json::Value jsonBody = json::Value::fromStream(response.body());

    if (jsonBody.hasField(FIELD_START_PROPERTIES)) {
        const json::Value& propertiesJson = jsonBody[FIELD_START_PROPERTIES];
        HitmanProcessProperties properties;
        for (const std::string& fieldName : propertiesJson.fields()) {
            properties[fieldName] = propertiesJson[fieldName].as<std::string>();
        }
        return properties;
    } else {
        throw RuntimeError() << "Failed to parse server response, url: " << url
                             << ", server response does not have "
                             << "\"" << FIELD_START_PROPERTIES << "\" field";
    }
}

template <typename RequestFunctor>
http::Response HitmanClient::retry(RequestFunctor&& requestFunc) const {
    return common::retry(
        requestFunc,
        common::RetryPolicy()
            .setInitialCooldown(retryInitialTimeout_)
            .setTryNumber(maxRequestAttempts_)
            .setCooldownBackoff(retryTimeoutBackoff_),
        [](const Expected<http::Response>& maybeResponse) {
            if (maybeResponse.hasException<ClientError>() ||
                maybeResponse.hasException<ServerError>() ||
                maybeResponse.hasException<ParallelRunsLimit>())
            {
                maybeResponse.rethrowException();
            }
            return maybeResponse.valid();
        }
    );
}

} // namespace maps::wiki::autocart::pipeline::hitman
