#include "load.h"

#include <maps/wikimap/mapspro/libs/social/factory.h>
#include <maps/wikimap/mapspro/libs/social/helpers.h>
#include <maps/wikimap/mapspro/libs/social/magic_strings.h>

#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/social/task.h>

namespace maps::wiki::social::tasks {

namespace {

const std::string& taskTableName(LoadingTaskMode taskMode)
{
    if (taskMode == LoadingTaskMode::Active) {
        return sql::table::TASK_ACTIVE;
    } else if (taskMode == LoadingTaskMode::Closed) {
        return sql::table::TASK_CLOSED;
    } else {
        return sql::table::TASK;
    }
}

std::string actionAtColumnName(TaskFeedParams::OrderBy orderBy)
{
    switch (orderBy) {
        case TaskFeedParams::OrderBy::ClosedAt:
            return sql::col::CLOSED_AT;
        case TaskFeedParams::OrderBy::ResolvedAt:
            return sql::col::RESOLVED_AT;
    }
}

Tasks pqxxRowsToTasks(
    const pqxx::result& rows,
    std::function<Task(const pqxx::row& row)> converter)
{
    Tasks tasks;
    tasks.reserve(rows.size());
    for (const auto& row : rows) {
        tasks.push_back(converter(row));
    }
    return tasks;
}

Tasks
loadEditsTasks(
    pqxx::transaction_base& work,
    LoadingTaskMode taskMode,
    const std::optional<TIds>& commitIds = std::nullopt)
{
    if (commitIds && commitIds->empty()) {
        return {};
    }

    std::string query =
        " SELECT * FROM " + taskTableName(taskMode) + " t"
        " JOIN " + sql::table::COMMIT_EVENT + " ce USING (" + sql::col::EVENT_ID + ")"
        " WHERE ce." + sql::col::TYPE + " = '" + sql::value::EVENT_TYPE_EDIT + "'";

    if (commitIds && !commitIds->empty()) {
        query += " AND ce." + sql::col::COMMIT_ID + " IN (" + common::join(*commitIds, ',') + ")";
    }

    return pqxxRowsToTasks(work.exec(query), Factory::taskWithCommitEvent);
}

} // namespace

Tasks
loadAllByTaskIds(
    pqxx::transaction_base& work,
    const TaskIds& taskIds,
    LoadingTaskMode taskMode)
{
    if (taskIds.empty()) {
        return {};
    }

    const std::string taskIdsFilter =
        "t." + sql::col::EVENT_ID + " IN (" + common::join(taskIds, ',') + ")";

    auto commitEventTasks = [&](){
        std::string query =
            " SELECT t.*, ce.*, " + joinCommentFields() +
            " FROM " + taskTableName(taskMode) + " t" +
            " JOIN " + sql::table::COMMIT_EVENT + " ce USING (" + sql::col::EVENT_ID + ") " +
            joinCommentTable("t.") +
            " WHERE " + taskIdsFilter;

        return pqxxRowsToTasks(work.exec(query), Factory::taskWithCommitEventAndComment);
    }();

    auto feedbackEventTasks = [&](){
        std::string query =
            " SELECT t.*,fe.*, " + feedbackTaskFields() +
            " FROM " + taskTableName(taskMode) + " t"
            " JOIN " + sql::table::FEEDBACK_EVENT + " fe USING (" + sql::col::EVENT_ID + ")"
            " JOIN " + sql::table::FEEDBACK_TASK + " ft ON" +
                " ft." + sql::col::ID + " = fe." + sql::col::FEEDBACK_TASK_ID +
            " WHERE " + taskIdsFilter;

        return pqxxRowsToTasks(work.exec(query), Factory::taskWithFeedbackEventAndFeedback);
    }();

    std::move(
        feedbackEventTasks.begin(), feedbackEventTasks.end(),
        std::back_inserter(commitEventTasks)
    );

    return commitEventTasks;
}

Tasks
loadEditsByCommitIds(
    pqxx::transaction_base& work,
    const TIds& commitIds,
    LoadingTaskMode taskMode)
{
    return loadEditsTasks(work, taskMode, commitIds);
}

Tasks
loadAllActiveEditTasks(pqxx::transaction_base& work)
{
    return loadEditsTasks(work, LoadingTaskMode::Active);
}

namespace {

std::optional<std::string>
getStringValue(pqxx::transaction_base& work, TId taskId, const std::string& column)
{
    const auto result = work.exec(
        " SELECT " + column +
        " FROM " + sql::table::TASK +
        " WHERE " + sql::col::EVENT_ID + " = " + std::to_string(taskId));

    if (result.empty()) {
        return {}; // Something's wrong. Fallback to no offset.
    }
    return result[0][0].as<std::string>();
}

std::string
loadParamsWhereClause(
    pqxx::transaction_base& work,
    const TaskFeedParams& params)
{
    const auto actionAtColumn = actionAtColumnName(params.orderBy());
    if (params.before()) {
        const auto actionAt = getStringValue(work, params.before(), actionAtColumn);
        if (actionAt) {
           return
                "t." + actionAtColumn + " > '" + *actionAt + "' OR "
                "t." + actionAtColumn + " = '" + *actionAt + "' AND "
                "t." + sql::col::EVENT_ID + " > " + std::to_string(params.before());
        }
    } else if (params.after()) {
        const auto actionAt = getStringValue(work, params.after(), actionAtColumn);
        if (actionAt) {
            return
                "t." + actionAtColumn + " < '" + *actionAt + "' OR " +
                "t." + actionAtColumn + " = '" + *actionAt + "' AND "
                "t." + sql::col::EVENT_ID + " < " + std::to_string(params.after());
        }
    }
    return "TRUE";
}

std::string
loadOrderByClause(const TaskFeedParams& params)
{
    // reverse order to select last perPage() records prior to before()
    const auto order = params.before() ? " ASC" : " DESC";
    return
        "t." + actionAtColumnName(params.orderBy()) + order + ", " +
        "t." + sql::col::EVENT_ID + order;
}

} // namespace

TaskFeed
load(
    pqxx::transaction_base& work,
    const TaskFeedParams& params,
    const TaskFilter& filter)
{
    const auto query =
        " SELECT t.*, ce.*, " + joinCommentFields() +
        " FROM " + sql::table::TASK + " t" +
        " JOIN " + sql::table::COMMIT_EVENT + " ce USING (" + sql::col::EVENT_ID + ")" +
        joinCommentTable("t.") +
        " WHERE " +
            filter.whereClause(work, "t") + " AND " +
            loadParamsWhereClause(work, params) +
        " ORDER BY " + loadOrderByClause(params) +
        " LIMIT " + std::to_string(params.perPage() + 1);  // `+ 1` to fill hasMore

    const auto tasks = pqxxRowsToTasks(work.exec(query), Factory::taskWithCommitEventAndComment);
    const auto hasMore = tasks.size() > params.perPage();
    if (params.before()) {
        return TaskFeed(Tasks(tasks.crbegin() + hasMore, tasks.crend()), HasMore(hasMore));
    }
    return TaskFeed(Tasks(tasks.cbegin(), tasks.cend() - hasMore), HasMore(hasMore));
}

} // namespace maps::wiki::social::tasks
