#include "acquire.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 <maps/wikimap/mapspro/libs/social/tables_columns.h>
#include "helpers.h"

#include <maps/libs/common/include/exception.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/social/event_filter.h>

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

namespace {

const std::string TASK_TABLE_ALIAS = "t.";
const std::string COMMIT_EVENT_TABLE_ALIAS = "ce.";
const std::string TASK_ORDER =
    "ORDER BY " + TASK_TABLE_ALIAS + sql::col::EVENT_ID + " DESC";
const std::string REVERSED_TASK_ORDER =
    "ORDER BY " + TASK_TABLE_ALIAS + sql::col::EVENT_ID + " ASC";
const std::string FOR_UPDATE = " FOR UPDATE OF t";


bool commitModerationTasksCanBeAcquired(const EventFilter& filter)
{
    bool commonTasksPermitted = !filter.commonTasksPermitted() || *filter.commonTasksPermitted();
    bool someCategoryIdsPermitted = !filter.categoryIds() || !filter.categoryIds()->empty();
    return commonTasksPermitted || someCategoryIdsPermitted;
}


std::string
selectCommitEventTasksPrefix()
{
    return
        "SELECT t.*,ce.*, " + joinCommentFields() +
        " FROM " + sql::table::TASK_ACTIVE + " t" +
        " JOIN " + sql::table::COMMIT_EVENT + " ce USING(" + sql::col::EVENT_ID + ")" +
            joinCommentTable(TASK_TABLE_ALIAS) +
        " WHERE TRUE";
}

const boost::format ACQUIRE_TASKS(
    "UPDATE " + sql::table::TASK_ACTIVE + " SET " +
        sql::col::LOCKED_AT + "=NOW(), " +
        sql::col::LOCKED_BY + "=%1%"
    " WHERE " + sql::col::EVENT_ID + " IN (%2%)"
    " RETURNING " + sql::col::EVENT_ID + "," +
                    sql::col::LOCKED_BY + "," + sql::col::LOCKED_AT);

struct AcquireParams
{
    TUid uid;
    const EventFilter& eventFilter;
    std::optional<size_t> limit;
};

enum class AcquireMode { Own, Free, Any };

std::string
taskIdsClause(const TaskIds& taskIds)
{
    return " AND " + TASK_TABLE_ALIAS + sql::col::EVENT_ID +
        " IN (" + common::join(taskIds, ',') + ") ";
}

std::string
lockClause(AcquireMode acquireMode, TUid uid)
{
    switch (acquireMode) {
        case AcquireMode::Any:  return {};
        case AcquireMode::Own:  return lockedClause(uid);
        case AcquireMode::Free: return nonLockedClause();
    }
}

std::string
commitEventTaskClauses(
    pqxx::transaction_base& work,
    const AcquireParams& params,
    AcquireMode acquireMode,
    const ModerationTimeIntervals& moderationTimeIntervals)
{
    return
        deferredClause(params.eventFilter.deferred(), params.uid, TASK_TABLE_ALIAS)
        + aoiTaskActiveClause(params.eventFilter.aoiId(), TASK_TABLE_ALIAS)
        + commitEventTaskClause(params.eventFilter.moderationMode(),
                                TASK_TABLE_ALIAS, moderationTimeIntervals)
        + lockClause(acquireMode, params.uid)
        + filterParamClause(params.eventFilter.eventType(),
                            TASK_TABLE_ALIAS, sql::col::TYPE, work)
        + filterParamClause(params.eventFilter.createdBy(),
                            COMMIT_EVENT_TABLE_ALIAS, sql::col::CREATED_BY, work)
        + filterParamClause(params.eventFilter.resolvedBy(),
                            TASK_TABLE_ALIAS, sql::col::RESOLVED_BY, work)
        + filterParamClause(params.eventFilter.objectId(),
                            COMMIT_EVENT_TABLE_ALIAS, sql::col::PRIMARY_OBJECT_ID, work)
        + primaryObjectCategoryIdClause(params.eventFilter,
                                        TASK_TABLE_ALIAS, work)
        + suspiciousUsersClause(params.eventFilter.suspiciousUsers(), COMMIT_EVENT_TABLE_ALIAS)
        + noviceUsersClause(params.eventFilter.noviceUsers(), TASK_TABLE_ALIAS)
        + filterParamClause(params.eventFilter.commitIds(),
                            TASK_TABLE_ALIAS, sql::col::COMMIT_ID, work)
        + " ";
}

std::string
limitClause(std::optional<size_t> limit)
{
    if (!limit) {
        return {};
    }

    return " LIMIT " + std::to_string(*limit);
}


std::string
selectForUpdateCommitEventTasksNewestQuery(
    pqxx::transaction_base& work,
    const AcquireParams& params,
    AcquireMode acquireMode,
    const ModerationTimeIntervals& moderationTimeIntervals)
{
    return selectCommitEventTasksPrefix()
        + commitEventTaskClauses(work, params, acquireMode, moderationTimeIntervals)
        + TASK_ORDER + FOR_UPDATE
        + limitClause(params.limit);
}

std::string
selectCommitEventTasksOldestQuery(
    pqxx::transaction_base& work,
    const AcquireParams& params,
    AcquireMode acquireMode,
    const ModerationTimeIntervals& moderationTimeIntervals)
{
    return selectCommitEventTasksPrefix()
        + commitEventTaskClauses(work, params, acquireMode, moderationTimeIntervals)
        + REVERSED_TASK_ORDER
        // @warning There is no `FOR UPDATE` statement to avoid a deadlock. The
        // deadlock is possible if same rows are locked in the opposite order in
        // different transactions.
        + limitClause(params.limit);
}

std::string
selectForUpdateCommitEventTasksByIdsQuery(const TaskIds& taskIds)
{
    ASSERT(!taskIds.empty());
    return selectCommitEventTasksPrefix()
        + taskIdsClause(taskIds)
        + TASK_ORDER + FOR_UPDATE;
}

Tasks
loadCommitEventTasks(const pqxx::result& rows)
{
    Tasks tasks;
    for (const auto& row : rows) {
        tasks.push_back(Factory::taskWithCommitEventAndComment(row));
    }
    return tasks;
}

Tasks
selectOwnTasks(
    pqxx::transaction_base& work,
    const AcquireParams& params,
    TasksOrder order,
    const ModerationTimeIntervals& moderationTimeIntervals)
{
    auto query = selectForUpdateCommitEventTasksNewestQuery(
        work,
        params,
        AcquireMode::Own,
        moderationTimeIntervals);

    auto tasks = loadCommitEventTasks(work.exec(query));

    if (order == TasksOrder::OldestFirst) {
        std::reverse(tasks.begin(), tasks.end());
    }
    return tasks;
}

AcquireMode
guessAcquireMode(const EventFilter& filter)
{
    return
        filter.createdBy() || filter.objectId() || filter.commitIds()
        ? AcquireMode::Any
        : AcquireMode::Free;
}

Tasks
selectNewestTasks(
    pqxx::transaction_base& work,
    const AcquireParams& params,
    AcquireMode acquireMode,
    const ModerationTimeIntervals& moderationTimeIntervals)
{
    auto query = selectForUpdateCommitEventTasksNewestQuery(
        work,
        params,
        acquireMode,
        moderationTimeIntervals);

    return loadCommitEventTasks(work.exec(query));
}

TaskIds
selectCommitEventOldestTaskIds(
    pqxx::transaction_base& work,
    const AcquireParams& params,
    AcquireMode acquireMode,
    const ModerationTimeIntervals& moderationTimeIntervals)
{
    auto query = selectCommitEventTasksOldestQuery(
        work,
        params,
        acquireMode,
        moderationTimeIntervals);

    return loadTaskIds(work.exec(query));
}

Tasks
selectOldestTasks(
    pqxx::transaction_base& work,
    const AcquireParams& params,
    AcquireMode acquireMode,
    const ModerationTimeIntervals& moderationTimeIntervals)
{
    auto taskIds = selectCommitEventOldestTaskIds(
        work,
        params,
        acquireMode,
        moderationTimeIntervals);

    if (taskIds.empty()) {
        return {};
    }

    auto query = selectForUpdateCommitEventTasksByIdsQuery(taskIds);
    auto tasks = loadCommitEventTasks(work.exec(query));

    std::reverse(tasks.begin(), tasks.end());  // reverse order
    return tasks;
}

Tasks
selectTasks(
    pqxx::transaction_base& work,
    TUid uid,
    const EventFilter& eventFilter,
    std::optional<size_t> limit,
    TasksOrder order,
    const ModerationTimeIntervals& moderationTimeIntervals)
{
    AcquireParams params = {uid, eventFilter, limit};

    const auto acquireMode = guessAcquireMode(eventFilter);

    if (acquireMode != AcquireMode::Any) {
        // Let's check if there are already acquired tasks.
        auto ownTasks = selectOwnTasks(work, params, order, moderationTimeIntervals);
        if (!ownTasks.empty()) {
            return ownTasks;
        }
    }

    return order == TasksOrder::NewestFirst
        ? selectNewestTasks(work, params, acquireMode, moderationTimeIntervals)
        : selectOldestTasks(work, params, acquireMode, moderationTimeIntervals);
}

void
acquireTasks(pqxx::transaction_base& work, TUid uid, Tasks& tasks)
{
    if (tasks.empty()) {
        return;
    }

    std::map<TId, Task*> id2taskPtr;
    for (auto& task : tasks) {
        ASSERT(id2taskPtr.insert({task.id(), &task}).second);
    }

    auto taskIdsStr = common::join(
        id2taskPtr,
        [](const std::pair<TId, Task*>& pair) { return pair.first; },
        ',');

    boost::format query(ACQUIRE_TASKS);
    query % uid % taskIdsStr;

    for (const auto& row : work.exec(query.str())) {
        auto taskId = row[sql::col::EVENT_ID].as<TId>();
        auto it = id2taskPtr.find(taskId);
        ASSERT(it != id2taskPtr.end());

        Task& task = *(it->second);
        auto locked = Factory::component<Locked>(row);
        task.setLocked(locked);
    }
}

} // namespace

Tasks
acquire(
    pqxx::transaction_base& work,
    TUid uid,
    const EventFilter& eventFilter,
    std::optional<size_t> limit,
    TasksOrder order,
    const ModerationTimeIntervals& moderationTimeIntervals)
{
    if (!commitModerationTasksCanBeAcquired(eventFilter)) {
        return {};
    }

    auto tasks = selectTasks(
        work,
        uid,
        eventFilter,
        limit,
        order,
        moderationTimeIntervals);

    acquireTasks(work, uid, tasks);
    return tasks;
}

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