#include <maps/libs/common/include/exception.h>
#include <maps/libs/enum_io/include/enum_io.h>
#include <maps/wikimap/mapspro/libs/social/feedback/workflow_logic.h>
#include <maps/wikimap/mapspro/libs/social/helpers.h>
#include <maps/wikimap/mapspro/libs/social/magic_strings.h>
#include <yandex/maps/wiki/common/pg_utils.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/social/feedback/consts.h>
#include <yandex/maps/wiki/social/feedback/task_aoi.h>

#include "util.h"
#include <algorithm>

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

namespace {

const std::string TOTAL = "total";
const std::string TOTAL_HIDDEN = "total_hidden";
const std::string OLD = "old";
const std::string OLD_HIDDEN = "old_hidden";

std::string whereAoiInCondition(const TIds& aoiIds)
{
    return sql::col::AOI_ID + " " + common::sqlInCondition(aoiIds);
}

std::string aoiFeedJoinFeedbackTask(Partition partition)
{
    return
        aoiFeedbackFeedPartitionTable(partition) +
        " JOIN " + feedbackPartitionTable(partition) +
        " ON " + sql::col::FEEDBACK_TASK_ID + " = " + sql::col::ID;
}

enum class CounterType
{
    Total,
    TotalHidden,
    Old,
    OldHidden
};

enum_io::Representations<CounterType>
COUNTER_TYPE_STRINGS {
    {CounterType::Total, TOTAL},
    {CounterType::TotalHidden, TOTAL_HIDDEN},
    {CounterType::Old, OLD},
    {CounterType::OldHidden, OLD_HIDDEN}
};

DEFINE_ENUM_IO(CounterType, COUNTER_TYPE_STRINGS);

std::string
counterFilterCondition(
    pqxx::transaction_base& socialTxn,
    CounterType type)
{
    switch (type) {
        case CounterType::Total:
            return "TRUE";
        case CounterType::TotalHidden:
            return TaskFilter().hidden(true).whereClause(socialTxn);
        case CounterType::Old:
            return ageTypeSqlCondition(AgeType::Old);
        case CounterType::OldHidden:
            return ageTypeSqlCondition(AgeType::Old) + " AND " +
                TaskFilter().hidden(true).whereClause(socialTxn);
    }
}

std::string
counterSqlFilterMvExpr(
    CounterType type)
{
    std::stringstream expr;
    expr << "SUM(" << type << ") AS " << type;
    return expr.str();
}

std::string
counterSqlFilterExpr(
    pqxx::transaction_base& socialTxn,
    CounterType type)
{
    std::stringstream expr;
    expr << "COUNT(*) FILTER (WHERE "
         << counterFilterCondition(socialTxn, type)
         << ") AS " << type;
    return expr.str();
}

uint64_t
counterValFromPqxxRow(
    const pqxx::row& row,
    CounterType counter)
{
    return row[std::string(toString(counter))].as<uint64_t>();
}

using CounterToVal = std::map<CounterType, uint64_t>;

std::string aoiToCounterValsQuery(
    pqxx::transaction_base& socialTxn,
    const TIds& aoiIds,
    const std::set<CounterType>& counters,
    const TaskFilter& filter,
    Partition partition)
{
    // Construct counters filter expressions
    //
    std::vector<std::string> counterExprs;
    std::transform(
        counters.begin(), counters.end(),
        std::back_inserter(counterExprs),
        [&](auto type) {
            return counterSqlFilterExpr(socialTxn, type);
        }
    );

    // Construct SQL query
    //
    std::stringstream query;
    query <<
        " SELECT " <<
            sql::col::AOI_ID << "," <<
            common::join(counterExprs, ",") <<
        " FROM " <<
            aoiFeedJoinFeedbackTask(partition) <<
        " WHERE " <<
            "(" << filter.whereClause(socialTxn) << ")" <<
            " AND " <<
            whereAoiInCondition(aoiIds) <<
        " GROUP BY " << sql::col::AOI_ID;
    return query.str();
}

std::string aoiToCounterValsQueryMv(
    const TIds& aoiIds,
    const std::set<CounterType>& counters,
    const AoiTaskFilter& filter)
{
    // Construct counters filter expressions
    //
    std::vector<std::string> counterExprs;
    std::transform(
        counters.begin(), counters.end(),
        std::back_inserter(counterExprs),
        [&](auto type) {
            return counterSqlFilterMvExpr(type);
        }
    );

    // Construct SQL query
    //
    std::stringstream query;
    query <<
        " SELECT " <<
            sql::col::AOI_ID << "," <<
            common::join(counterExprs, ",") <<
        " FROM " <<
            sql::table::FEEDBACK_AOI_OPENED_TASK_STAT_MV <<
        " WHERE " <<
            "(" << filter.whereClause() << ")" <<
            " AND " <<
            whereAoiInCondition(aoiIds) <<
        " GROUP BY " << sql::col::AOI_ID;
    return query.str();
}

std::map<TId, CounterToVal> aoiToCounterVals(
    pqxx::transaction_base& socialTxn,
    const TIds& aoiIds,
    const std::set<CounterType>& counters,
    const std::string& query)
{
    // Extract data structures from query result
    //
    std::map<TId, CounterToVal> res;
    for (const auto& row : socialTxn.exec(query)) {
        TId aoiId = row[sql::col::AOI_ID].as<TId>();

        CounterToVal counterVals;
        for (auto counter : counters) {
            counterVals[counter] = counterValFromPqxxRow(row, counter);
        }

        res[aoiId] = std::move(counterVals);
    }

    // Some AOIs may not be present in query result.
    // It means that all their counters are zero.
    //
    CounterToVal zeroCounterVals;
    for (auto counter : counters) {
        zeroCounterVals[counter] = 0;
    }
    for (const auto& aoiId : aoiIds) {
        if (!res.count(aoiId)) {
            res[aoiId] = zeroCounterVals;
        }
    }

    return res;
}

void defaultInitializeForEachTask(const TIds& taskIds, TaskIdToAoiIds& out)
{
    for (auto taskId: taskIds) {
        out[taskId];
    }
}

} // namespace anonymous


std::string AoiTaskFilter::whereClause() const
{
    std::stringstream query;

    query << "TRUE";
    if (types_) {
        query << " AND " << sql::col::TYPE << " "
              << common::sqlInCondition(*types_);
    }

    if (workflows_) {
        query << " AND " << feedback::whereClause(*workflows_);
    }

    if (sources_) {
        query << " AND " << sql::col::SOURCE << " "
              << common::sqlInCondition(*sources_);
    }

    return query.str();
}

AoiTaskFilter& AoiTaskFilter::types(std::optional<Types> types)
{
    types_ = std::move(types);
    return *this;
}

AoiTaskFilter& AoiTaskFilter::workflows(std::optional<Workflows> workflows)
{
    workflows_ = std::move(workflows);
    return *this;
}

AoiTaskFilter& AoiTaskFilter::sources(std::optional<std::vector<std::string>> sources)
{
    sources_ = std::move(sources);
    return *this;
}

void removeTaskFromAoiFeed(pqxx::transaction_base& socialTxn, TId taskId)
{
    std::stringstream query;
    query <<
        " DELETE FROM " << sql::table::FEEDBACK_AOI_FEED <<
        " WHERE " << sql::col::FEEDBACK_TASK_ID << " = " << taskId;

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

void addTaskToAoiFeed(
    pqxx::transaction_base& socialTxn,
    TId taskId,
    const TIds& aoiIds,
    Partition partition)
{
    if (aoiIds.empty()) {
        return;
    }

    std::stringstream query;
    query <<
        " INSERT INTO " << aoiFeedbackFeedPartitionTable(partition) << " " <<
            "(" <<
                sql::col::AOI_ID << "," <<
                sql::col::FEEDBACK_TASK_ID <<
            ")" <<
        " SELECT " <<
            sql::col::AOI_ID << "," << taskId <<
        " FROM " <<
            unnestArray(socialTxn, aoiIds) << " " << sql::col::AOI_ID;

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

AoiToTaskCounters
calcAoiTaskCounters(
    pqxx::transaction_base& socialTxn,
    const TIds& aoiIds,
    const TaskFilter& filter,
    Partition partition)
{
    const std::set<CounterType> counters = {
        CounterType::Old,
        CounterType::Total
    };

    const auto aoiToCounters = aoiToCounterVals(
        socialTxn,
        aoiIds,
        counters,
        aoiToCounterValsQuery(socialTxn, aoiIds, counters, filter, partition)
    );

    AoiToTaskCounters res;
    for (const auto& [aoi, counterVals] : aoiToCounters) {
        res[aoi] = AoiTaskCounters {
            counterVals.at(CounterType::Old),
            counterVals.at(CounterType::Total),
        };
    }

    return res;
}

AoiToTaskStatCounters
calcAoiOpenedTaskStatCounters(
    pqxx::transaction_base& socialTxn,
    const TIds& aoiIds,
    const AoiTaskFilter& filter)
{
    const std::set<CounterType> counters = {
        CounterType::Old,
        CounterType::OldHidden,
        CounterType::Total,
        CounterType::TotalHidden
    };

    const auto aoiToCounters = aoiToCounterVals(
        socialTxn,
        aoiIds,
        counters,
        aoiToCounterValsQueryMv(aoiIds, counters, filter)
    );

    AoiToTaskStatCounters res;
    for (const auto& [aoi, counterVals] : aoiToCounters) {
        res[aoi] = AoiTaskStatCounters {
            counterVals.at(CounterType::Old),
            counterVals.at(CounterType::OldHidden),
            counterVals.at(CounterType::Total),
            counterVals.at(CounterType::TotalHidden),
        };
    }

    return res;
}

AoiToOptionalTask calcAoiOldestOpenedTask(
    pqxx::transaction_base& socialTxn,
    const TIds& aoiIds,
    const AoiTaskFilter& filter)
{
    std::stringstream query;
    query <<
        " WITH sub AS (SELECT * FROM " << sql::table::FEEDBACK_AOI_OLDEST_OPENED_TASK_MV <<
            " WHERE " <<
                "(" << filter.whereClause() << ")" <<
                    " AND " <<
                whereAoiInCondition(aoiIds) <<
        ") " <<
        " SELECT " << sql::col::AOI_ID << "," << FIELDS_TO_SELECT <<
        " FROM (" <<
            "SELECT DISTINCT ON (lhs.aoi_id) " <<
                "lhs.aoi_id as aoi_id, " <<
                "rhs.id as feedback_task_id " <<
            "FROM (" <<
                "SELECT aoi_id, MIN(min_created_at) as min_created_at " <<
                "FROM sub " <<
                "GROUP BY aoi_id " <<
            ") AS lhs JOIN sub AS rhs " <<
                "ON lhs.min_created_at = rhs.min_created_at " <<
            "ORDER BY aoi_id" <<
        ") AS aoi_oldest " <<
        "JOIN " << sql::table::FEEDBACK_TASK_OUTGOING_OPENED << " AS feedback_task " <<
        "ON aoi_oldest.feedback_task_id = feedback_task.id";

    AoiToOptionalTask res;
    for (const auto& row : socialTxn.exec(query.str())) {
        TId aoiId = row[sql::col::AOI_ID].as<TId>();
        res.emplace(aoiId, Task(row));
    }

    // Some AOIs may not be present in query result.
    // It means that there is no task in those AOIs.
    //
    for (const auto& aoiId : aoiIds) {
        if (!res.count(aoiId)) {
            res[aoiId] = std::nullopt;
        }
    }

    return res;
}

TaskFeed aoiTaskFeed(
    pqxx::transaction_base& socialTxn,
    TId aoiId,
    const ITaskFeedParams& feedParams,
    const TaskFilter& filter,
    const std::set<Partition>& partitions)
{
    if (partitions.empty()) {
        throw LogicError("Partitions should not be empty");
    };

    std::vector<std::string> partitionQueries;
    for (auto partition: partitions) {
        partitionQueries.emplace_back(
            " SELECT " + FIELDS_TO_SELECT + " FROM " +
                aoiFeedJoinFeedbackTask(partition) +
            " WHERE " +
                putInBrackets(filter.whereClause(socialTxn)) +
                    " AND " +
                putInBrackets(whereAoiInCondition({aoiId})) +
                    " AND " +
                putInBrackets(feedParams.sqlFeedWhereClause()));
    }

    std::string query =
        "(" + common::join(partitionQueries, ") UNION ALL (") + ")"
        " ORDER BY " + feedParams.sqlFeedOrderByIdClause() +
        " LIMIT " + std::to_string(feedParams.limit() + 1);

    return constructTasksFeed(execTaskQuery<Task>(socialTxn, query), feedParams);
}

void refreshAoiOldestTaskMv(pqxx::transaction_base& socialTxn)
{
    socialTxn.exec(
        "REFRESH MATERIALIZED VIEW " +
            sql::table::FEEDBACK_AOI_OLDEST_OPENED_TASK_DATA_MV + "; "
        "REFRESH MATERIALIZED VIEW " +
            sql::table::FEEDBACK_AOI_OLDEST_OPENED_TASK_MV
    );
}

void refreshAoiTaskStatMv(pqxx::transaction_base& socialTxn)
{
    socialTxn.exec(
        "REFRESH MATERIALIZED VIEW " +
            sql::table::FEEDBACK_AOI_OPENED_TASK_STAT_DATA_MV + "; "
        "REFRESH MATERIALIZED VIEW " +
            sql::table::FEEDBACK_AOI_OPENED_TASK_STAT_MV
    );
}

TaskIdToAoiIds getTasksAoiIds(
    pqxx::transaction_base& socialTxn, const TIds& taskIds)
{
    TaskIdToAoiIds result;
    if (taskIds.empty()) {
        return result;
    }

    defaultInitializeForEachTask(taskIds, result);

    // clang-format off
    const std::string query =
        " SELECT " + sql::col::FEEDBACK_TASK_ID + "," + sql::col::AOI_ID +
        " FROM " + sql::table::FEEDBACK_AOI_FEED +
        " WHERE " + sql::col::FEEDBACK_TASK_ID + " IN (" + common::join(taskIds, ',') + ")";
    // clang-format on

    for (const auto& row: socialTxn.exec(query)) {
        result[row[sql::col::FEEDBACK_TASK_ID].as<TId>()].insert(
            row[sql::col::AOI_ID].as<TId>());
    }

    return result;
}

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