#include <yandex/maps/wiki/social/gateway.h>

#include "aoi_feed.h"
#include "comment_impl.h"
#include "factory.h"
#include "helpers.h"
#include "magic_strings.h"
#include "skills_impl.h"
#include "suspicious_users.h"
#include "tables_columns.h"
#include "tasks/create.h"
#include "tasks/get.h"
#include "tasks/load.h"
#include "tasks/stats.h"
#include "feed_helpers.h"
#include "users.h"

#include <maps/libs/common/include/exception.h>
#include <yandex/maps/wiki/common/batch.h>
#include <yandex/maps/wiki/common/pg_utils.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/social/task.h>
#include <yandex/maps/wiki/social/feed.h>
#include <yandex/maps/wiki/social/region_feed.h>

#include <memory>

namespace maps::wiki::social {

namespace {

enum class LoadEventsExtraData {No, Yes};

const size_t TRUNK_BRANCH_ID = 0;

const size_t COMMENT_IDS_BATCH_SIZE = 100;

const std::string CLOSED_FEEDBACK_ACTION = "closed-feedback";

std::string joinExtraDataClause(LoadEventsExtraData loadExtraData)
{
    switch (loadExtraData) {
        case LoadEventsExtraData::No:
            return "";
        case LoadEventsExtraData::Yes:
            return " LEFT JOIN " + sql::table::EVENT_EXTRA_DATA +
                   " USING (" + sql::col::EVENT_ID + ")";
    }
}

std::optional<EventExtraData>
getExtraData(
    const pqxx::row& row,
    LoadEventsExtraData loadExtraData)
{
    if (loadExtraData == LoadEventsExtraData::No) {
        return NO_EVENT_EXTRA_DATA;
    }

    EventExtraData result;
    bool extraDataPresent = false;

    if (!row[sql::col::FT_TYPE_ID].is_null()) {
        result.ftTypeId = row[sql::col::FT_TYPE_ID].as<FtTypeId>();
        extraDataPresent = true;
    }
    if (!row[sql::col::BUSINESS_RUBRIC_ID].is_null()) {
        result.businessRubricId = row[sql::col::BUSINESS_RUBRIC_ID].as<BusinessRubricId>();
        extraDataPresent = true;
    }

    if (!extraDataPresent) {
        return NO_EVENT_EXTRA_DATA;
    }

    return result;
}

Events
loadEditEventsByCommitIds(
    pqxx::transaction_base& txn,
    const TIds& commitIds,
    LoadEventsExtraData loadExtraData,
    std::optional<TId> branchId = std::nullopt)
{
    if (commitIds.empty()) {
        return {};
    }

    auto query =
        "SELECT * FROM " + sql::table::COMMIT_EVENT + "\n" +
        joinExtraDataClause(loadExtraData) + "\n"
        "WHERE " + sql::col::COMMIT_ID + " IN (" + common::join(commitIds, ',') + ")"
        " AND " + sql::col::TYPE + "='" + sql::value::EVENT_TYPE_EDIT + "'";

    if (branchId) {
        query += " AND " + sql::col::BRANCH_ID + " = " + std::to_string(branchId.value());
    }

    Events events;

    for (const auto& row : txn.exec(query)) {
        events.push_back(
            Factory::component<Event>(
                row,
                getExtraData(row, loadExtraData),
                Event::Kind::Commit
            )
        );
    }

    return events;
}

Events
loadEditEventsByCommitRange(
    pqxx::transaction_base& txn,
    TId from,
    TId to,
    std::optional<TId> branchId = std::nullopt)
{
    REQUIRE(to >= from, "Wrong commit range [" << from << "; " << to << "]");

    auto query =
        "SELECT * FROM " + sql::table::COMMIT_EVENT + "\n" +
        "WHERE " + sql::col::COMMIT_ID + " >= " + std::to_string(from) + "\n" +
        "  AND " + sql::col::COMMIT_ID + " <= " + std::to_string(to) + "\n" +
        "  AND " + sql::col::TYPE + "='" + sql::value::EVENT_TYPE_EDIT + "'";

    if (branchId) {
        query += " AND " + sql::col::BRANCH_ID + " = " + std::to_string(branchId.value());
    }

    Events events;
    for (const auto& row : txn.exec(query)) {
        events.push_back(
            Factory::component<Event>(
                row,
                NO_EVENT_EXTRA_DATA,
                Event::Kind::Commit
            )
        );
    }
    return events;
}

} // namespace

Gateway::Gateway(pqxx::transaction_base& txn)
    : txn_(txn)
{}

TaskFeed
Gateway::loadTasks(const TaskFeedParams& params, const TaskFilter& filter) const
{
    return tasks::load(txn_, params, filter);
}

Tasks
Gateway::loadAllActiveEditTasks() const
{
    return tasks::loadAllActiveEditTasks(txn_);
}

TIds
Gateway::getAllActiveEditTasksCommitIds() const
{
    return tasks::getAllActiveEditTasksCommitIds(txn_);
}

TIds
Gateway::getTaskIds(const TaskFilter& filter) const
{
    return tasks::getTaskIds(txn_, filter);
}

TIds
Gateway::getTaskIdsResolvedAt(DateTimeCondition resolvedAt) const
{
    return tasks::getTaskIdsResolvedAt(txn_, resolvedAt);
}

TIds
Gateway::getTaskIdsClosedAt(DateTimeCondition closedAt) const
{
    return tasks::getTaskIdsClosedAt(txn_, closedAt);
}

Tasks
Gateway::loadTasksByTaskIds(const TaskIds& taskIds) const
{
    return tasks::loadAllByTaskIds(
        txn_,
        taskIds,
        tasks::LoadingTaskMode::All);
}

Tasks
Gateway::loadActiveTasksByTaskIds(const TaskIds& taskIds) const
{
    return tasks::loadAllByTaskIds(
        txn_,
        taskIds,
        tasks::LoadingTaskMode::Active);
}

Tasks
Gateway::loadEditTasksByCommitIds(const TaskIds& commitIds) const
{
    return tasks::loadEditsByCommitIds(
        txn_,
        commitIds,
        tasks::LoadingTaskMode::All);
}

Tasks
Gateway::loadActiveEditTasksByCommitIds(const TIds& commitIds) const
{
    return tasks::loadEditsByCommitIds(
        txn_,
        commitIds,
        tasks::LoadingTaskMode::Active);
}

Feed
Gateway::feed(
    TId branchId,
    TId subscriberId,
    FeedType feedType) const
{
    return Feed(txn_, branchId, subscriberId, feedType);
}

Feed
Gateway::feed(
    TId branchId,
    TId subscriberId,
    FeedType feedType,
    FeedFilter filter) const
{
    return Feed(txn_, branchId, subscriberId, feedType, std::move(filter));
}

RegionFeedPtr
Gateway::regionFeed(
    TId branchId,
    std::string geometryMercatorWkb
) const
{
    return std::make_unique<RegionFeed>(
        txn_, branchId, std::move(geometryMercatorWkb));
}

Feed
Gateway::suspiciousFeed(TId branchId, FeedFilter filter) const
{
    return Feed(
        txn_, branchId, FeedType::Suspicious, std::move(filter));
}

void
Gateway::removeOldSuspiciousUsers(chrono::Days olderThan) const
{
    suspicious_users::removeOld(txn_, olderThan);
}

SubscriptionConsole
Gateway::subscriptionConsole(TUid uid) const
{
    return Factory::component<SubscriptionConsole>(txn_, uid);
}

ModerationConsole
Gateway::moderationConsole(TUid uid) const
{
    return Factory::component<ModerationConsole>(txn_, uid);
}

SuperModerationConsole
Gateway::superModerationConsole(TUid uid) const
{
    return Factory::component<SuperModerationConsole>(txn_, uid);
}

TaskStatsConsole
Gateway::taskStatsConsole(ModerationMode mode) const
{
    return Factory::component<TaskStatsConsole>(txn_, mode);
}

Event
Gateway::createCommitEvent(
        TUid uid,
        const CommitData& commitData,
        const std::optional<PrimaryObjectData>& primaryObjectData,
        const TIds& aoiIds) const
{
    return createCommitEvent(
        uid, commitData, primaryObjectData, aoiIds, NO_EVENT_EXTRA_DATA
    );
}

Event
Gateway::createCloseFeedbackEvent(
    TUid uid,
    const feedback::Task& feedbackTask,
    const TIds& aoiIds) const
{
    return Factory::componentCreate<Event>(
        txn_, uid, CLOSED_FEEDBACK_ACTION, feedbackTask, aoiIds
    );
}

Event
Gateway::createCommitEvent(
        TUid uid,
        const CommitData& commitData,
        const std::optional<PrimaryObjectData>& primaryObjectData,
        const TIds& aoiIds,
        const std::optional<EventExtraData>& extraData) const
{
    return Factory::componentCreate<Event>(
        txn_, EventType::Edit, uid, commitData, primaryObjectData, aoiIds, extraData
    );
}

Events
Gateway::loadEditEventsByCommitIds(const TIds& commitIds) const
{
    return social::loadEditEventsByCommitIds(txn_, commitIds, LoadEventsExtraData::No);
}

Events
Gateway::loadTrunkEditEventsByCommitIds(const TIds& commitIds) const
{
    return social::loadEditEventsByCommitIds(txn_, commitIds, LoadEventsExtraData::No, TRUNK_BRANCH_ID);
}

Events
Gateway::loadEditEventsWithExtraDataByCommitIds(const TIds& commitIds) const
{
    return social::loadEditEventsByCommitIds(txn_, commitIds, LoadEventsExtraData::Yes);
}

Events
Gateway::loadEditEventsByCommitRange(TId from, TId to) const
{
    return social::loadEditEventsByCommitRange(txn_, from, to);
}

Events
Gateway::loadTrunkEditEventsByCommitRange(TId from, TId to) const
{
    return social::loadEditEventsByCommitRange(txn_, from, to, TRUNK_BRANCH_ID);
}

Events Gateway::loadTrunkEditEventsByCommitIdsCreatedBefore(
    const TIds& commitIds,
    chrono::TimePoint createdBefore) const
{
    if (commitIds.empty()) {
        return {};
    }

    auto query =
        "SELECT * FROM " + sql::table::COMMIT_EVENT + " " +
        " WHERE " + sql::col::COMMIT_ID + " IN (" + common::join(commitIds, ',') + ")"
        " AND " + sql::col::TYPE + "='" + sql::value::EVENT_TYPE_EDIT + "'" +
        " AND " + sql::col::BRANCH_ID + " = " + std::to_string(TRUNK_BRANCH_ID) +
        " AND " + sql::col::CREATED_AT + " < " + txn_.quote(chrono::formatSqlDateTime(createdBefore));

    Events events;

    for (const auto& row : txn_.exec(query)) {
        events.push_back(
            Factory::component<Event>(
                row,
                getExtraData(row, LoadEventsExtraData::No),
                Event::Kind::Commit
            )
        );
    }

    return events;
}

Events
Gateway::loadEditEventsByCreationInterval(const DateTimeCondition& createdAt) const
{
    const auto query =
        "SELECT * FROM " +
            sql::table::COMMIT_EVENT + " "
        "WHERE " +
            createdAt.sqlComparison(txn_, sql::col::CREATED_AT) + " AND " +
            sql::col::TYPE + "='" + sql::value::EVENT_TYPE_EDIT + "'";

    Events events;
    for (const auto& row : txn_.exec(query)) {
        events.push_back(
            Factory::component<Event>(
                row,
                NO_EVENT_EXTRA_DATA,
                Event::Kind::Commit
            )
        );
    }
    return events;
}

Events
Gateway::loadNoTaskEditsByCreationInterval(const DateTimeCondition& createdAt) const
{
    const auto query =
        "SELECT ce.* FROM " +
            sql::table::COMMIT_EVENT + " as ce "
            "LEFT JOIN " + sql::table::TASK + " as t "
            "USING (" + sql::col::EVENT_ID + ") "
        "WHERE " +
            createdAt.sqlComparison(txn_, "ce." + sql::col::CREATED_AT) + " AND "
            "ce." + sql::col::TYPE + "='" + sql::value::EVENT_TYPE_EDIT + "' AND "
            "t." + sql::col::EVENT_ID + " IS NULL";

    Events events;
    for (const auto& row : txn_.exec(query)) {
        events.push_back(
            Factory::component<Event>(
                row,
                NO_EVENT_EXTRA_DATA,
                Event::Kind::Commit
            )
        );
    }
    return events;
}

Events
Gateway::loadEventsByIds(const TIds& eventIds) const
{
    if (eventIds.empty()) {
        return {};
    }

    auto query = "SELECT * FROM " + sql::table::COMMIT_EVENT +
        " WHERE " + sql::col::EVENT_ID + " IN (" + common::join(eventIds, ',') + ")";

    Events events;
    for (const auto& row : txn_.exec(query)) {
        events.push_back(
            Factory::component<Event>(
                row,
                NO_EVENT_EXTRA_DATA,
                Event::Kind::Commit
            )
        );
    }
    return events;
}

std::optional<TId>
Gateway::getRecentEditCommitMadeBy(TUid userId, const TIds& commitIds) const
{
    if (commitIds.empty()) {
        return std::nullopt;
    }

    auto query =
        "SELECT " + sql::col::COMMIT_ID + "\n"
        "FROM " + sql::table::COMMIT_EVENT + "\n"
        "WHERE " + common::whereClause(sql::col::COMMIT_ID, commitIds) + "\n"
        "  AND " + sql::col::CREATED_BY + " = " + std::to_string(userId) + "\n"
        "  AND " + sql::col::TYPE + " = '" + sql::value::EVENT_TYPE_EDIT + "'\n"
        "ORDER BY " + sql::col::COMMIT_ID + " DESC\n"
        "LIMIT 1";

    const auto rows = txn_.exec(query);

    if (rows.empty()) {
        return std::nullopt;
    }

    return rows[0][0].as<TId>();
}

Task
Gateway::createTask(const Event& event, const std::string& userCreatedOrUnbannedAt) const
{
    return tasks::create(txn_, event, userCreatedOrUnbannedAt, tasks::Accepted::No);
}

Task
Gateway::createAcceptedTask(const Event& event, const std::string& userCreatedOrUnbannedAt) const
{
    return tasks::create(txn_, event, userCreatedOrUnbannedAt, tasks::Accepted::Yes);
}

Comment
Gateway::createComment(
    TUid uid, CommentType type, const std::string& data,
    TId commitId, TId objectId, std::optional<TId> feedbackTaskId,
    const TIds& aoiIds,
    Comment::Internal internal) const
{
    auto comment = Factory::componentCreate<Comment>(
        txn_, uid, type, data, commitId, objectId, feedbackTaskId, 0, internal);
    bindCommentForAoi(txn_, comment.id(), aoiIds);

    return comment;
}

CommentsFeed
Gateway::commentsFeed(const CommentsFeedParams& params) const
{
    return Factory::component<CommentsFeed>(txn_, params);
}

Comments
Gateway::loadComments(const TIds& commentIds) const
{
    if (commentIds.empty()) {
        return {};
    }

    auto query =
            " SELECT * FROM " + sql::table::COMMENT +
            " WHERE " + sql::col::ID + " IN (" + common::join(commentIds, ',') + ")";
    return Factory::comments(txn_.exec(query));
}

Comments
Gateway::clearUserComments(
    TUid createdBy,
    TUid deletedBy) const
{
    auto query =
        " SELECT " + sql::col::ID + " FROM " + sql::table::COMMENT +
        " WHERE "  + sql::col::CREATED_BY + "=" + std::to_string(createdBy) +
        " AND " + sql::col::DELETED_BY + "=0 " +
        " ORDER BY " + sql::col::ID + " FOR UPDATE";
    auto rows = txn_.exec(query);

    std::vector<TId> commentIds;
    for (const auto& row : rows) {
        commentIds.push_back(row[sql::col::ID].as<TId>());
    }

    Comments clearedComments;

    common::applyBatchOp<std::vector<TId>>(
        commentIds,
        COMMENT_IDS_BATCH_SIZE,
        [&](const auto& batchCommentIds) {
            auto query =
                "UPDATE " + sql::table::COMMENT +
                " SET " + sql::col::DELETED_BY + "=" + std::to_string(deletedBy) +
                "," + sql::col::DELETED_AT + "=" + sql::value::NOW +
                " WHERE " + sql::col::ID + " IN (" + common::join(batchCommentIds, ',') + ")" +
                "RETURNING *";

            auto comments = Factory::comments(txn_.exec(query));
            clearedComments.insert(
                clearedComments.end(),
                comments.begin(),
                comments.end()
            );
        });

    return clearedComments;
}

void
Gateway::saveEventAlerts(const EventAlerts& alerts) const
{
    if (alerts.empty()) {
        return;
    }

    std::vector<std::string> columns {
        sql::col::EVENT_ID,
        sql::col::PRIORITY,
        sql::col::DESCRIPTION,
        sql::col::OBJECT_ID
    };

    auto valuesMatrix = [&]() {
        std::vector<std::vector<std::string>> result;
        std::for_each(
            alerts.begin(), alerts.end(),
            [&](const auto& alert) {
                result.push_back({
                    std::to_string(alert.eventId()),
                    std::to_string(alert.priority()),
                    txn_.quote(alert.description()),
                    std::to_string(alert.objectId())
                });
            }
        );
        return result;
    }();

    txn_.exec(
        "INSERT INTO " + sql::table::EVENT_ALERT + " (" +
        common::join(columns, ',') + ") VALUES " +
        multirowValuesInsertStatement(valuesMatrix)
    );
}

EventAlerts
Gateway::loadEventAlerts(const TIds& eventIds) const
{
    if (eventIds.empty()) {
        return {};
    }

    auto query = "SELECT * FROM " + sql::table::EVENT_ALERT
        + " WHERE " + sql::col::EVENT_ID + " IN ("
        + common::join(eventIds, ',')
        + ")";
    auto result = Factory::eventAlerts(txn_.exec(query));
    std::sort(
            result.begin(), result.end(),
            [](const EventAlert& left, const EventAlert& right) {
                if (left.eventId() == right.eventId()) {
                    return left.priority() < right.priority();
                }
                return left.eventId() < right.eventId();
            });
    return result;
}

UserActivity
Gateway::getUserActivity(
    TUid uid,
    const std::vector<std::chrono::seconds>& timeIntervals,
    ActivityType type) const
{
    return impl::getUserActivity(txn_, uid, timeIntervals, type);
}

std::vector<TUid> Gateway::getActiveUserIds(
    chrono::TimePoint activityTimeBegin,
    chrono::TimePoint activityTimeEnd)
{
    using namespace sql;

    auto timeBegin = txn_.quote(chrono::formatSqlDateTime(activityTimeBegin));
    auto timeEnd = txn_.quote(chrono::formatSqlDateTime(activityTimeEnd));
    auto query =
        "SELECT " + col::CREATED_BY + "\n"
        "FROM " + table::TASK + "\n"
        "LEFT JOIN " + table::COMMIT_EVENT + " USING(" + col::EVENT_ID + ")\n"
        "WHERE\n"
            + table::COMMIT_EVENT + "." + col::TYPE + " = " + txn_.quote(value::EVENT_TYPE_EDIT) + " AND\n"
            + table::COMMIT_EVENT + "." + col::CREATED_AT + " >= " + timeBegin + " AND\n"
            + table::COMMIT_EVENT + "." + col::CREATED_AT + " < " + timeEnd + "\n"
        "GROUP BY " + col::CREATED_BY;

    std::vector<TUid> result;
    for (const auto& row : txn_.exec(query)) {
        result.emplace_back(row[0].as<TUid>());
    }
    return result;
}


void
Gateway::updateSkills(const std::vector<TUid>& uids)
{
    for (const auto uid: uids) {
        social::updateSkills(txn_, uid);
    }
}


SkillsByUid
Gateway::getSkills(const std::vector<TUid>& uids)
{
    return social::getSkills(txn_, uids);
}


TIds
Gateway::getAoiIdsOfActiveEditTask(TId commitId)
{
    return impl::getAoiIdsOfActiveEditTask(txn_, commitId);
}

std::optional<UserData>
Gateway::getUserData(TUid uid) const
{
    return impl::getUserData(txn_, uid);
}

UserData
Gateway::setUserData(TUid uid, const std::string& data) const
{
    return impl::setUserData(txn_, uid, data);
}

void
Gateway::saveUserActivity(
    TUid uid,
    const std::string& ip,
    std::optional<uint16_t> port,
    UserActivityAction action,
    const std::optional<TId>& entityId) const
{
    impl::saveUserActivity(txn_, uid, ip, port, action, entityId);
}

void Gateway::saveUserActivityAlert(TUid uid, const std::string& reason) const
{
    impl::saveUserActivityAlert(txn_, uid, reason);
}

} // namespace maps::wiki::social
