#include "worker.h"

#include <maps/libs/enum_io/include/enum_io.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/variant.h>
#include <yandex/maps/wiki/common/batch.h>
#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/social/common.h>
#include <yandex/maps/wiki/social/feedback/agent.h>
#include <yandex/maps/wiki/social/feedback/gateway_rw.h>
#include <yandex/maps/wiki/tasks/task_logger.h>

#include <tuple>

namespace maps::wiki::tasks_feedback::reject_fb {

using social::feedback::Bucket;
using social::feedback::Workflow;
using social::feedback::Type;
using social::feedback::RejectReason;
namespace sf = maps::wiki::social::feedback;

namespace {

const std::string REJECT_FEEDBACK_TASK_TABLE_NAME = "service.reject_feedback_task";
const std::string ID = "id";
const std::string AOI = "aoi";
const std::string WORKFLOWS = "workflows";
const std::string SOURCES = "sources";
const std::string TYPES = "types";
const std::string REJECT_REASON = "reject_reason";

const std::string RESULT_TABLE_NAME = "service.reject_feedback_result";
const std::string TASK_ID = "task_id";
const std::string REJECTED_FEEDBACK_ID = "rejected_feedback_id";

template<class T>
T fromString(const std::string& str)
{
    return enum_io::fromString<T>(str);
}

template<>
std::string fromString(const std::string& str)
{
    return str;
}

template<class T>
std::vector<T> parseArray(const pqxx::field& field)
{
    std::vector<T> result;
    if (field.is_null()) {
        return result;
    }
    auto arrayParser = field.as_array();

    for (auto [juncture, valueStr] = arrayParser.get_next();
        juncture != pqxx::array_parser::juncture::done;
        std::tie(juncture, valueStr) = arrayParser.get_next())
    {
        if (juncture == pqxx::array_parser::string_value) {
            result.emplace_back(fromString<T>(valueStr));
        }
    }

    return result;
}

geolib3::Polygon2 parsePolygon(const pqxx::field& field)
{
    const std::string wkbStr = pqxx::binarystring(field).str();
    const auto geometryVariant =
        geolib3::WKB::read<geolib3::SimpleGeometryVariant>(wkbStr);
    REQUIRE(
        geometryVariant.geometryType() == geolib3::GeometryType::Polygon,
        "Polygon expected here");
    return geometryVariant.get<geolib3::Polygon2>();
}

social::TIds resolveTasks(
    sf::Agent& agent,
    sf::RejectReason rejectReason,
    const sf::TasksForUpdate& tasksForUpdate)
{
    social::TIds resolvedIds;
    for (const auto& task: tasksForUpdate) {
        try {
            REQUIRE(!task.resolved().has_value(), "Only open tasks expected");
            const auto resolvedTask = agent.resolveTaskCascade(
                task, sf::Resolution::createRejected(rejectReason));
            REQUIRE(
                resolvedTask.has_value(),
                "Function Agent::resolveTaskCascade returned null");

            resolvedIds.insert(resolvedTask->id());
        } catch (std::exception& ex) {
            ERROR() << "Failed processing task " << task.id();
            throw;
        }
    }
    return resolvedIds;
}

sf::TaskFilter toFilter(const TaskParams& params)
{
    sf::TaskFilter filter;
    filter.boundary(params.polygon());
    if (!params.workflows().empty()) {
        filter.workflows(params.workflows());
    }
    if (!params.sources().empty()) {
        filter.sources(params.sources());
    }
    if (!params.types().empty()) {
        filter.types(params.types());
    }
    return filter;
}

} // namespace

namespace impl {

[[nodiscard]] social::TIds
rejectTasks(pqxx::transaction_base& socialTxn, const TaskParams& params)
{
    const auto filter = toFilter(params)
        .resolved(false)
        .bucket(Bucket::Outgoing);
    sf::GatewayRW gw{socialTxn};
    sf::Agent agent{socialTxn, common::ROBOT_UID};

    social::TIds rejectedIds;
    auto batchClose = [&] (const social::TIds& ids) {
        const auto resolvedIds = resolveTasks(
            agent, params.rejectReason(), gw.tasksForUpdateByIds(ids));
        rejectedIds.insert(resolvedIds.begin(), resolvedIds.end());
    };
    common::applyBatchOp(
        gw.taskIdsByFilter(filter), params.batchSize(), batchClose);

    return rejectedIds;
}

void saveRejectedTaskIds(
    const revision::DBID dbTaskId,
    const social::TIds& rejectedTaskIds,
    pqxx::transaction_base& coreTxn,
    const size_t batchSize)
{
    const std::string dbTaskIdStr = std::to_string(dbTaskId);
    const std::string queryStart =
        " INSERT INTO " + RESULT_TABLE_NAME
        + '(' + TASK_ID + ',' + REJECTED_FEEDBACK_ID + ") VALUES (" + dbTaskIdStr + ',';
    const std::string delimiter = "),(" + dbTaskIdStr + ',';
    auto batchSave = [&] (const social::TIds& ids) {
        coreTxn.exec0(queryStart + common::join(ids, delimiter) + ')');
    };

    common::applyBatchOp(rejectedTaskIds, batchSize, batchSave);
}

} // namespace impl

TaskParams::TaskParams(
        revision::DBID dbTaskId,
        geolib3::Polygon2 polygon,
        std::vector<Workflow> workflows,
        std::vector<std::string> sources,
        std::vector<Type> types,
        RejectReason rejectReason)
    : dbTaskId_(dbTaskId)
    , polygon_(std::move(polygon))
    , workflows_(std::move(workflows))
    , sources_(std::move(sources))
    , types_(std::move(types))
    , rejectReason_(rejectReason)
{}


TaskParams TaskParams::fromTxn(
    pqxx::transaction_base& txnCore,
    revision::DBID dbTaskId)
{
    const auto fields = common::join(std::vector<std::string>{
            "ST_ASBINARY(" + AOI + ") AS aoi",
            WORKFLOWS,
            SOURCES,
            TYPES,
            REJECT_REASON
        }, ',');

    std::ostringstream query;
    query << " SELECT " << fields
          << " FROM " << REJECT_FEEDBACK_TASK_TABLE_NAME
          << " WHERE " << ID << " = " << dbTaskId;
    auto result = txnCore.exec(query.str());
    REQUIRE(!result.empty(), "Task hasn't been prepared yet");

    const auto& row = result[0];

    return TaskParams(
        dbTaskId,
        parsePolygon(row[AOI]),
        parseArray<Workflow>(row[WORKFLOWS]),
        parseArray<std::string>(row[SOURCES]),
        parseArray<Type>(row[TYPES]),
        enum_io::fromString<RejectReason>(row[REJECT_REASON].as<std::string>())
    );
}

Worker::Worker(std::unique_ptr<maps::wiki::common::ExtendedXmlDoc> cfg)
    : cfg_(std::move(cfg))
{}


void Worker::doTask(const grinder::worker::Task& task)
try {
    common::PoolHolder socialPool(*cfg_, "social", "grinder");
    common::PoolHolder corePool(*cfg_, "core", "grinder");

    const auto dbTaskId = task.args()["taskId"].as<revision::DBID>();
    tasks::TaskPgLogger logger(corePool.pool(), dbTaskId);
    logger.logInfo() << "Reject feedback started; task id = " << dbTaskId;

    try {
        auto coreTxnHolder = corePool.pool().masterWriteableTransaction();
        auto taskParams = TaskParams::fromTxn(*coreTxnHolder, dbTaskId);

        auto socialTxnHolder = socialPool.pool().masterWriteableTransaction();
        const auto rejectedTaskIds = impl::rejectTasks(*socialTxnHolder, taskParams);

        impl::saveRejectedTaskIds(dbTaskId, rejectedTaskIds, *coreTxnHolder, taskParams.batchSize());

        socialTxnHolder->commit();
        coreTxnHolder->commit();

        logger.logInfo() << "Successfully rejected " << rejectedTaskIds.size() << " tasks";
    } catch (const std::exception& ex) {
        logger.logError() << ex.what();
        throw;
    }
} catch (const maps::Exception& ex) {
    ERROR() << "Task ERROR: " << ex;
    throw;
} catch (const std::exception& ex) {
    ERROR() << "Task ERROR: " << ex.what();
    throw;
}

} // namespace maps::wiki::tasks_feedback::reject_fb
