#include "worker.h"

#include <maps/libs/log8/include/log8.h>
#include "maps/libs/csv/include/csv.h"
#include <yandex/maps/wiki/social/feedback/agent.h>
#include <yandex/maps/wiki/social/feedback/description.h>
#include <yandex/maps/wiki/tasks/task_logger.h>
#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <maps/libs/geolib/include/conversion.h>

namespace maps::wiki::import_feedback {

namespace sql {

namespace fields {

const std::string ID = "id";
const std::string SOURCE = "source";
const std::string FEEDBACK_TYPE = "feedback_type";
const std::string WORKFLOW = "workflow";
const std::string HIDDEN = "hidden";
const std::string FILE = "file";
const std::string CREATED = "created";

} // fields

const std::string TABLE_NAME = "service.import_feedback_task";
const std::string BASE_TABLE_NAME = "service.task";

} // sql

namespace sf = sql::fields;

namespace {

const std::string EXPERIMENT_SOURCE_PREFIX = "experiment-";

} // namespace

TaskParams::TaskParams(
        revision::DBID dbTaskId,
        std::string source,
        social::feedback::Type type,
        social::feedback::Workflow workflow,
        bool hidden,
        std::string data)
    : dbTaskId_(dbTaskId)
    , source_(std::move(source))
    , type_(type)
    , workflow_(workflow)
    , hidden_(hidden)
    , data_(std::move(data))
{}


TaskParams TaskParams::fromTxn(
    pqxx::transaction_base& txnCore,
    revision::DBID dbTaskId)
{
    std::ostringstream query;
    auto fields = common::join(std::vector<std::string>{
        sf::SOURCE,
        sf::FEEDBACK_TYPE,
        sf::WORKFLOW,
        sf::HIDDEN,
        sf::FILE
    }, ',');
    query << " SELECT " << fields
          << " FROM " << sql::TABLE_NAME
          << " WHERE " << sf::ID << " = " << dbTaskId;
    auto result = txnCore.exec(query.str());
    REQUIRE(!result.empty(), "Task hasn't been prepared yet");

    const auto& row = result[0];

    pqxx::binarystring binaryData(row[sf::FILE]);
    std::stringstream stream;
    stream << binaryData.str();

    return TaskParams(
        dbTaskId,
        row[sf::SOURCE].as<std::string>(),
        boost::lexical_cast<social::feedback::Type>(row[sf::FEEDBACK_TYPE].as<std::string>()),
        boost::lexical_cast<social::feedback::Workflow>(row[sf::WORKFLOW].as<std::string>()),
        row[sf::HIDDEN].as<bool>(),
        stream.str()
    );
}

revision::DBID TaskParams::dbTaskId() const
{
    return dbTaskId_;
}

std::string TaskParams::source() const
{
    return source_;
}

social::feedback::Type TaskParams::type() const
{
    return type_;
}

social::feedback::Workflow TaskParams::workflow() const
{
    return workflow_;
}

bool TaskParams::hidden() const
{
    return hidden_;
}

std::string TaskParams::data() const
{
    return data_;
}

namespace {

std::vector<social::feedback::TaskNew> makeNewFeedbackTasks(const TaskParams& taskParams)
{
    std::vector<social::feedback::TaskNew> result;

    std::stringstream stream(taskParams.data());
    csv::InputStream csvStream(stream, '\t');
    csv::Header header(csvStream);
    for (const std::string field: {"lon", "lat", "description"}) {
        if (header.indexOf(field) == csv::Header::npos) {
            throw RuntimeError() << "Obligatory field " << field << " is absent";
        }
    }
    bool hasObjectId = (header.indexOf("object_id") != csv::Header::npos);

    csv::MapRow row(header);
    while (csvStream) {
        auto currentLine = csvStream.line();
        try {
            row.fromCsv(csvStream);

            auto lon = row["lon"].as<double>();
            auto lat = row["lat"].as<double>();

            auto position = geolib3::convertGeodeticToMercator(geolib3::Point2(lon, lat));

            social::feedback::TaskNew newTask(
                position,
                taskParams.type(),
                EXPERIMENT_SOURCE_PREFIX + taskParams.source(),
                social::feedback::Description(std::string(row["description"]))
            );
            newTask.hidden = taskParams.hidden();
            if (hasObjectId) {
                auto objectId = row["object_id"];
                if (!objectId.isEmpty()) {
                    newTask.objectId = objectId.as<social::TId>();
                }
            }

            result.push_back(newTask);
        } catch (std::exception&) {
            throw RuntimeError() << "Can't parse row number " << currentLine << "; abort";
        }
    }

    return result;
}

} // namespace


size_t importRoutine(
    const TaskParams& taskParams,
    pqxx::transaction_base& txnSocial)
{
    social::feedback::Agent agent(txnSocial, common::ROBOT_UID);
    auto newFeedbackTasks = makeNewFeedbackTasks(taskParams);

    social::feedback::Tasks insertedTasks;
    for (const auto& newFeedbackTask: newFeedbackTasks) {
        insertedTasks.push_back(
            agent.addTask(newFeedbackTask)
        );
    }

    return insertedTasks.size();
}


void cleanupOldFiles(pqxx::transaction_base& txnCore)
{
    std::ostringstream query;
    query << "UPDATE " << sql::TABLE_NAME << std::endl
          << "SET " << sf::FILE << "=NULL" << std::endl
          << "WHERE " << sf::ID << " IN " << std::endl
                << "(SELECT " << sf::ID << std::endl
                << "FROM " << sql::TABLE_NAME << " INNER JOIN " << sql::BASE_TABLE_NAME << std::endl
                << "USING (" << sf::ID << ")" << std::endl
                << "WHERE " << sf::CREATED << " < now() - '30 days'::interval" << std::endl
                << "AND " << sf::FILE << " IS NOT NULL)";

    txnCore.exec(query.str());
}


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");

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

    try {
        auto coreTxn = corePool.pool().masterWriteableTransaction();
        auto taskParams = TaskParams::fromTxn(*coreTxn, dbTaskId);
        cleanupOldFiles(*coreTxn);
        coreTxn->commit();

        auto socialTxn = socialPool.pool().masterWriteableTransaction();
        auto importedTasksCount = importRoutine(taskParams, *socialTxn);
        socialTxn->commit();

        logger.logInfo() << "Successfully imported " << importedTasksCount << " 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::import_feedback
