#include "sprav_tasks_updater.h"

#include <maps/wikimap/mapspro/services/editor/src/sync/lock_helpers.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>

#include <maps/wikimap/mapspro/libs/http/http_utils.h>

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

using namespace std::chrono_literals;

namespace maps::wiki {

namespace {
const auto WORKER_NAME = "feedback api tasks status updater"s;
constexpr size_t MAX_TASKS_PER_RUN = 10000;
const auto MERGE_POI_ALTAY_FEED_DATA_TABLE = "sprav.merge_poi_altay_feed_data"s;//TODO to poi_feed lib

const auto RETRY_POLICY = maps::common::RetryPolicy()
        .setTryNumber(5)
        .setInitialCooldown(1s)
        .setCooldownBackoff(2.0);
const std::chrono::milliseconds REQUEST_INTERVAL = 100ms;
} // namespace


SpravTasksUpdater::SpravTasksUpdater(
    pgpool3::Pool& pgpool,
    std::chrono::seconds waitTimeout,
    SpravTask::Status statusToProcess,
    common::AdvisoryLockIds lockId
    )
    : ThreadObserver<SpravTasksUpdater>(waitTimeout)
    , socialPool_(pgpool)
    , statusToProcess_(statusToProcess)
    , lockId_(lockId)
{
    start();
}


constexpr const std::string&
SpravTasksUpdater::name() const noexcept
{
    return WORKER_NAME;
}

std::string
SpravTasksUpdater::logHeader() const
{
    return
        "SpravTasksUpdater[" +
        boost::lexical_cast<std::string>(statusToProcess_) + "]";
}

void
SpravTasksUpdater::onStart()
{
    INFO() << name() << " started for " << statusToProcess_;
}


void
SpravTasksUpdater::onStop()
{
    INFO() << name() << " stopped";
}

namespace {

const std::string TVM_ALIAS_FEEDBACK_API = "feedback-api";

void
updateTaskPermalink(pqxx::transaction_base& socialTxn, const SpravTask& task)
{
    auto rows = socialTxn.exec(
        "SELECT permalink FROM " + MERGE_POI_ALTAY_FEED_DATA_TABLE + " WHERE object_id="
        + std::to_string(task.objectId));
    if (rows.empty()) {
        DEBUG() << "SpravTasksUpdater still no permalink for : " << task.objectId;
        return;
    }
    auto originalTask = OriginalTask::fromJsonString(task.originalTaskJson);
    originalTask.setPermalink(rows[0][0].as<TOid>());
    socialTxn.exec(
        "UPDATE " + SPRAV_TASKS_TABLE + " SET permalink=" +
        std::to_string(originalTask.permalink()) +
        ", original_task=" + socialTxn.quote(originalTask.toJsonString()) +
        " WHERE task_id=" + std::to_string(task.id));
}

void
submitTask(
    http::Client& client,
    pqxx::transaction_base &socialTxn,
    const SpravTask& task,
    const std::string& logHeader)
{
    ASSERT(task.status == SpravTask::Status::NotSubmitted);
    http::URL url = cfg()->feedbackApiUrl() + "/v1/tasks";

    http::Request request(client, http::POST, url);
    auto tvmTicket = cfg()->getTvmTicket(TVM_ALIAS_FEEDBACK_API);
    if (!tvmTicket.empty()) {
        request.addHeader(auth::SERVICE_TICKET_HEADER, std::move(tvmTicket));
    }
    request.addHeader("Content-Type", "application/json");
    request.setContent(task.originalTaskJson);
    auto response = request.perform();
    if (response.status() != http_status::CREATED) {
        WARN() << logHeader << " Response " << response.status() << " while submiting: " << task.id
            << " rejecting (" << response.readBody() << ") [[[" << task.originalTaskJson << "]]]";
        socialTxn.exec(
            "UPDATE " + SPRAV_TASKS_TABLE + " SET sprav_status=" +
            socialTxn.quote(boost::lexical_cast<std::string>(SpravTask::Status::Rejected)) +
            " WHERE task_id=" + std::to_string(task.id));
    } else {
        const auto responseParsed = json::Value::fromString(response.readBody());
        if (!responseParsed.hasField("id")) {
            WARN() << logHeader << " No id in reponse while submitting task: " << task.id;
        } else {
            socialTxn.exec(
                "UPDATE " + SPRAV_TASKS_TABLE + " SET sprav_status=" +
                socialTxn.quote(boost::lexical_cast<std::string>(SpravTask::Status::New)) +
                ", sprav_task_id=" + socialTxn.quote(responseParsed["id"].as<std::string>()) +
                " WHERE task_id=" + std::to_string(task.id));
        }
    }
}

void
updateTaskStatus(
    http::Client& client,
    pqxx::transaction_base &socialTxn,
    const SpravTask& task,
    const std::string& logHeader)
{
    http::URL url = cfg()->feedbackApiUrl() + "/v1/tasks/" + task.spravTaskId;
    http::HeaderMap headers;
    auto tvmTicket = cfg()->getTvmTicket(TVM_ALIAS_FEEDBACK_API);
    if (!tvmTicket.empty()) {
        headers[auth::SERVICE_TICKET_HEADER] = std::move(tvmTicket);
    }
    auto [responseBody, status] = client.get(url, headers, RETRY_POLICY);
    if (status != http_status::OK) {
        WARN() << logHeader << " failed to get task status task: " << task.id;
        return;
    }
    const auto responseParsed = json::Value::fromString(responseBody);
    if (!responseParsed.isObject() || !responseParsed.hasField("status")) {
        WARN() << logHeader << " unexpected response while trying to get task: " << task.id;
        return;
    }
    auto spravStatus = boost::lexical_cast<SpravTask::Status>(responseParsed["status"].as<std::string>());
    if (task.status == spravStatus) {
        DEBUG() << logHeader << " task " << task.id
            << " status didn't change (" << spravStatus <<")";
        return;
    }
    socialTxn.exec(
        "UPDATE " + SPRAV_TASKS_TABLE + " SET sprav_status="
        + socialTxn.quote(responseParsed["status"].as<std::string>())
        + " WHERE task_id=" + std::to_string(task.id));
}

std::vector<SpravTask> loadTasks(
    pqxx::transaction_base &socialTxn,
    SpravTask::Status status)
{
    auto query = "SELECT * FROM " + SPRAV_TASKS_TABLE + " WHERE "
        " sprav_status=" + socialTxn.quote(boost::lexical_cast<std::string>(status))+
        " ORDER BY task_id DESC LIMIT " + std::to_string(MAX_TASKS_PER_RUN);
    auto rows = socialTxn.exec(query);
    std::vector<SpravTask> tasks;
    tasks.reserve(rows.size());
    for (const auto& row : rows) {
        tasks.emplace_back(row);
    }
    return tasks;
}

} // namespace

void
SpravTasksUpdater::doWork()
{
    DEBUG() << logHeader() << " woke up";
    try {
        auto socialTxnLockHandle = socialPool_.masterWriteableTransaction();
        if (!sync::tryLockDb(*socialTxnLockHandle, lockId_, sync::LockType::Exclusive)) {
            DEBUG() << logHeader() << " failed to acquire lock";
            return;
        }
        const auto tasks = loadTasks(*socialTxnLockHandle, statusToProcess_);
        if (tasks.empty()) {
            DEBUG() << logHeader() << " nothing to do";
            return;
        }
        http::Client client;
        DEBUG() << logHeader() << " tasks found " << tasks.size();
        for (const auto& task : tasks) {
            auto socialTxnTaskHandle = socialPool_.masterWriteableTransaction();
            if (statusToProcess_ == SpravTask::Status::NotSubmitted) {
                if (!task.permalink) {
                    updateTaskPermalink(*socialTxnTaskHandle, task);
                    socialTxnTaskHandle->commit();
                    continue;
                }
                submitTask(client, *socialTxnTaskHandle, task, logHeader());
            } else {
                updateTaskStatus(client, *socialTxnTaskHandle, task, logHeader());
            }
            socialTxnTaskHandle->commit();
            std::this_thread::sleep_for(REQUEST_INTERVAL);
        }
    } catch (const maps::Exception& ex) {
        WARN() << logHeader() << " exception: " << ex;
    } catch (const std::exception& ex) {
        WARN() << logHeader() << " exception: " << ex.what();
    } catch (...) {
        WARN() << logHeader() << " unknown exception";
    }
    DEBUG() << logHeader() << " fell to sleep";
}

} // namespace maps::wiki
