#include <yandex/maps/wiki/social/feedback/gateway_ro.h>

#include <maps/wikimap/mapspro/libs/social/feedback/stats.h>
#include <maps/wikimap/mapspro/libs/social/feedback/util.h>
#include <maps/wikimap/mapspro/libs/social/magic_strings.h>

#include <yandex/maps/wiki/common/pg_utils.h>

namespace maps::wiki::social::feedback {

namespace {

std::string byFilterQuery(
    const std::string& fields,
    const TaskFilter& filter,
    pqxx::transaction_base& txn,
    std::optional<size_t> limit)
{
    std::stringstream query;
    query << "SELECT " << fields
        << " FROM " << sql::table::FEEDBACK_TASK
        << " " << filter.joinClause()
        << " WHERE " << filter.whereClause(txn);
    if (limit) {
        query << " LIMIT " << *limit;
    }
    return query.str();
}

} // namespace

GatewayRO::GatewayRO(pqxx::transaction_base& socialTxn)
    : socialTxn_(socialTxn)
{}

TaskIdsToHistory GatewayRO::history(const TIds& taskIds)
{
    static const std::string FIELDS_TO_SELECT = common::join(
        std::vector<std::string>{
            sql::col::FEEDBACK_TASK_ID,
            sql::col::MODIFIED_AT,
            sql::col::MODIFIED_BY,
            sql::col::OPERATION,
            sql::col::PARAMS,
            sql::col::COMMENT_ID,
        },
        ","
    );

    std::stringstream query;
    query << "SELECT " << FIELDS_TO_SELECT
        << " FROM " << sql::table::FEEDBACK_HISTORY
        << " WHERE " << sql::col::FEEDBACK_TASK_ID << " " << common::sqlInCondition(taskIds)
        << " ORDER BY " << sql::col::FEEDBACK_HISTORY_ID;

    std::map<TId, HistoryItems> idToHistoryItems;
    for (const auto& row : socialTxn_.exec(query)) {
        HistoryItemParams params;
        if (!row[sql::col::PARAMS].is_null()) {
            params = json::Value::fromString(row[sql::col::PARAMS].as<std::string>())
                .as<HistoryItemParams>();
        }
        std::optional<TId> commentId;
        if (!row[sql::col::COMMENT_ID].is_null()) {
            commentId = row[sql::col::COMMENT_ID].as<TId>();
        }
        auto taskId = row[sql::col::FEEDBACK_TASK_ID].as<TId>();
        TaskOperation operation = TaskOperation::Unrecognized;
        tryFromString(
            row[sql::col::OPERATION].as<std::string>(),
            operation);
        idToHistoryItems[taskId].emplace_back(
            chrono::parseSqlDateTime(row[sql::col::MODIFIED_AT].as<std::string>()),
            row[sql::col::MODIFIED_BY].as<TUid>(),
            operation,
            std::move(params),
            commentId
        );
    }

    TaskIdsToHistory idsToHistory;
    for(auto& [taskId, historyItems] : idToHistoryItems) {
        idsToHistory.emplace(taskId, std::move(historyItems));
    }
    return idsToHistory;
}

History GatewayRO::history(TId taskId)
{
    auto idToHistory = history(TIds{taskId});
    if (idToHistory.empty()) {
        return History(HistoryItems{});
    } else {
        return std::move(idToHistory.begin()->second);
    }
}

std::optional<Task> GatewayRO::taskById(TId id)
{
    auto tasks = tasksByIds({id});
    return tasks.empty() ? std::nullopt :
           std::optional<Task>{std::move(tasks.front())};
}

Tasks GatewayRO::tasksByIds(TIds ids)
{
    return tasksByFilter(TaskFilter().ids(std::move(ids)));
}

std::string GatewayRO::tasksByFilterQuery(const TaskFilter& filter, std::optional<size_t> limit)
{
    return byFilterQuery(FIELDS_TO_SELECT, filter, socialTxn_, limit);
}

std::string GatewayRO::taskIdsByFilterQuery(const TaskFilter& filter)
{
    return byFilterQuery(sql::col::ID, filter, socialTxn_, std::nullopt);
}

TIds GatewayRO::taskIdsByFilter(const TaskFilter& filter)
{
    const auto rows = socialTxn_.exec(taskIdsByFilterQuery(filter));
    TIds feedbackIds;
    for (const auto& row: rows) {
        feedbackIds.insert(row[sql::col::ID].as<TId>());
    }
    return feedbackIds;
}

Tasks GatewayRO::tasksByFilter(const TaskFilter& filter)
{
    return execTaskQuery<Task>(socialTxn_, tasksByFilterQuery(filter));
}

TasksBriefResult GatewayRO::tasksBriefByFilter(const TaskFilter& filter, std::optional<size_t> limit)
{
    auto limitForQuery = limit;
    if (limitForQuery) {
        limitForQuery.value()++;
    }
    auto tasks = execTaskQuery<TaskBrief>(socialTxn_, tasksByFilterQuery(filter, limitForQuery));
    return constructTasksBriefResult(std::move(tasks), limit);
}

TaskFeed GatewayRO::tasksFeed(
    const TaskFilter& filter,
    const ITaskFeedParams& feedParams)
{
    std::stringstream query;
    query <<
        " SELECT " << FIELDS_TO_SELECT <<
        " FROM " << sql::table::FEEDBACK_TASK <<
        " " << filter.joinClause() <<
        " WHERE " <<
        putInBrackets(filter.whereClause(socialTxn_)) <<
        " AND" <<
        putInBrackets(feedParams.sqlFeedWhereClause());

    if (feedParams.limit()) {
        /*
         * This is some kind of optimization. If feedParams's limit is equal to zero,
         * client just wants to know if there are any feedback in feed (HasMore::Yes or HasMore::No).
         * So there is no need to execute ORDER BY statement and we can gain:
         * -- 7x   speedup if there is no such feedback
         * -- 100x speedup otherwise
         */
        query << " ORDER BY " << feedParams.sqlFeedOrderByIdClause();
    }

    query << " LIMIT " << feedParams.limit() + 1;

    return constructTasksFeed(execTaskQuery<Task>(socialTxn_, query.str()), feedParams);
}

TaskFeedWithCount GatewayRO::tasksFeedWithCount(
    const TaskFilter& filter,
    const ITaskFeedParams& feedParams)
{
    auto taskFeed = tasksFeed(filter, feedParams);
    return TaskFeedWithCount(
        std::move(taskFeed.tasks),
        taskFeed.hasMore,
        getTotalCountOfTasks(filter)
    );
}

size_t GatewayRO::getTotalCountOfTasks(const TaskFilter& filter)
{
    std::stringstream query;
    query <<
        " SELECT COUNT(1) " <<
        " FROM " << sql::table::FEEDBACK_TASK <<
        " " << filter.joinClause() <<
        " WHERE " << filter.whereClause(socialTxn_);

    return socialTxn_.exec(query.str()).at(0).at(0).as<size_t>();
}

std::vector<AggregatedCounter>
GatewayRO::getAggregatedCountOfTasks(
    const TaskFilter& filter,
    const Columns& aggregationColumns)
{
    if (aggregationColumns.empty()) {
        AggregatedCounter value;
        value.count = getTotalCountOfTasks(filter);
        return {value};
    }
    const auto query = getAggregatedCountQuery(
        filter.whereClause(socialTxn_), aggregationColumns);
    const auto rows = socialTxn_.exec(query);

    std::vector<AggregatedCounter> aggregatedCounters;
    for (const auto& row: rows) {
        aggregatedCounters.push_back(rowToAggregatedCounter(row));
    }
    return aggregatedCounters;
}

std::optional<Task> GatewayRO::taskByCommitId(TId commitId)
{
    std::stringstream query;
    query << "SELECT " << FIELDS_TO_SELECT << " FROM " << sql::table::FEEDBACK_TASK << " "
        << "JOIN " << sql::table::COMMIT_FEEDBACK_TASK << " "
        << "ON " << sql::col::FEEDBACK_TASK_ID << " = " << sql::col::ID << " "
        << "WHERE " << sql::col::COMMIT_ID << " = " << commitId;

    auto tasks = execTaskQuery<Task>(socialTxn_, query.str());
    if (tasks.empty()) {
        return std::nullopt;
    }
    return std::move(tasks[0]);
}

std::optional<Task> GatewayRO::taskByExternalReferenceId(const ExternalReferenceId& id)
{
    std::stringstream query;
    query << "SELECT " << FIELDS_TO_SELECT << " FROM " << sql::table::FEEDBACK_TASK << " "
        << "JOIN " << sql::table::EXTERNAL_REFERENCE_FEEDBACK_TASK << " "
        << "ON " << sql::col::FEEDBACK_TASK_ID << " = " << sql::col::ID << " "
        << "WHERE " << sql::col::EXTERNAL_REFERENCE_ID << " = " << socialTxn_.quote(id);

    auto tasks = execTaskQuery<Task>(socialTxn_, query.str());
    if (tasks.empty()) {
        return std::nullopt;
    }
    return std::move(tasks[0]);
}

size_t GatewayRO::countOperationsToday(
    const TaskOperations &operations,
    TUid uid,
    int tzOffsetInMinutes)
{
    return stats::countOperationsToday(socialTxn_, operations, uid, tzOffsetInMinutes);
}


} // namespace maps::wiki::social::feedback
