#include <maps/wikimap/ugc/account/src/lib/contributions.h>
#include <maps/wikimap/ugc/account/src/lib/common.h>
#include <maps/wikimap/ugc/libs/common/constants.h>
#include <maps/wikimap/mapspro/libs/query_builder/include/count_query.h>
#include <maps/wikimap/mapspro/libs/query_builder/include/select_query.h>
#include <maps/infra/yacare/include/error.h>
#include <maps/libs/locale/include/find.h>
#include <maps/libs/locale/include/convert.h>

namespace maps::wiki::ugc::account {

namespace qb = maps::wiki::query_builder;

namespace {

pqxx::result::const_iterator getLocalizedRow(
    const pqxx::result& rows,
    const maps::locale::Locale& locale)
{
    std::vector<std::pair<maps::locale::Locale, size_t>> localesIndex;
    for (size_t i = 0; i < rows.size(); ++i) {
        maps::locale::Locale locale;
        std::istringstream(rows[i][columns::LOCALE].as<std::string>()) >> locale;
        localesIndex.emplace_back(std::move(locale), i);
    }

    ASSERT(!rows.empty());
    auto it = maps::locale::findBest(
        localesIndex.cbegin(),
        localesIndex.cend(),
        locale,
        [] (const std::pair<maps::locale::Locale, size_t>& pair) {
            return pair.first;
        }
    );
    return rows.begin() + it->second;
}

} // namespace

proto::contribution::ContributionMetadata getContributionMetadata(
    pqxx::transaction_base& txn,
    ContributionId id,
    Uid uid,
    const maps::locale::Locale& locale)
{
    const std::string order = columns::LOCALE + " DESC";
    auto rows = qb::SelectQuery(
        tables::CONTRIBUTION_DATA,
        qb::WhereConditions()
            .append(columns::UID, std::to_string(uid.value()))
            .appendQuoted(columns::CONTRIBUTION_ID, id.value())
    ).orderBy(order).exec(txn);
    REQUIRE(!rows.empty(), "No data for contribution " << id << " and uid " << uid);

    const auto& row = *getLocalizedRow(rows, locale);

    proto::contribution::ContributionMetadata data;
    pqxx::binarystring blob(row[columns::DATA]);
    REQUIRE(
        data.ParseFromString(TString{blob.str()}),
        "Cannot parse metadata for contribution " << id << " and uid " << uid
    );
    return data;
}

void appendContributions(
    pqxx::transaction_base& txn,
    proto::ugc_account::Contributions& contributions,
    Uid uid,
    const pqxx::result& rows,
    const maps::locale::Locale& locale)
{
    for (const auto& row : rows) {
        auto id = ContributionId{row[columns::CONTRIBUTION_ID].as<ContributionId::ValueType>()};
        auto metadata = getContributionMetadata(txn, id, uid, locale);

        proto::ugc_account::Contribution contribution;
        contribution.set_contribution_id(TString{id.value()});
        *contribution.mutable_metadata() = metadata;

        maps::chrono::TimePoint tp = maps::chrono::parseSqlDateTime(row[columns::CREATED_AT].as<std::string>());
        auto time = contribution.mutable_time();
        *time->mutable_text() = maps::chrono::formatIsoDateTime(tp);
        time->set_value(maps::chrono::convertToUnixTime(tp));
        time->set_tz_offset(0);

        *contributions.add_contribution() = contribution;
    }
}

proto::ugc_account::Contributions findContributions(
    pqxx::transaction_base& txn,
    Uid uid,
    const std::set<MetadataId>& metadataIds,
    const Paging& paging,
    const maps::locale::Locale& locale)
{
    const auto whereConditions = qb::WhereConditions()
        .keyInValues(columns::METADATA_ID, metadataIdsToStr(metadataIds))
        .append(columns::UID, std::to_string(uid.value()));
    // sort by contribution_id to make consitent feed for contributions with equal timestamp
    const std::string order = columns::CREATED_AT + " DESC, " + columns::CONTRIBUTION_ID + " DESC";
    if (!paging.baseId()) {
        auto rows = qb::SelectQuery(tables::CONTRIBUTION, whereConditions)
            .orderBy(order)
            .limit(paging.afterLimit())
            .exec(txn);

        proto::ugc_account::Contributions contributions;
        appendContributions(txn, contributions, uid, rows, locale);
        return contributions;
    }
    auto rows = qb::SelectQuery(
        tables::CONTRIBUTION,
        qb::WhereConditions{whereConditions}
            .appendQuoted(columns::CONTRIBUTION_ID, *paging.baseId()))
        .exec(txn);
    REQUIRE(
        !rows.empty(),
        yacare::errors::NotFound() << "Unknown contribution id " << *paging.baseId()
    );
    REQUIRE(rows.size() == 1, "More than 1 contribution found for id " << *paging.baseId());
    const auto timestamp = rows[0][columns::CREATED_AT].as<std::string>();

    proto::ugc_account::Contributions contributions;
    if (paging.beforeLimit() > 0) {
        const auto beforeRows = qb::SelectQuery(
            tables::CONTRIBUTION,
            qb::WhereConditions{whereConditions}
                .and_(qb::SubConditions(query_builder::AppendType::Or)
                    .appendQuoted(columns::CREATED_AT, timestamp, qb::Relation::Greater)
                        .or_(qb::SubConditions()
                        .appendQuoted(columns::CREATED_AT, timestamp)
                        // be careful with contribution_id order: it depends on feed sorting order
                        .appendQuoted(columns::CONTRIBUTION_ID, *paging.baseId(), qb::Relation::Greater)
                    )
                )
            )
            .orderBy(order)
            .limit(paging.beforeLimit())
            .exec(txn);
        appendContributions(txn, contributions, uid, beforeRows, locale);
    }
    if ((paging.beforeLimit() == 0) == (paging.afterLimit() == 0)) {
        appendContributions(txn, contributions, uid, rows, locale);
    }
    if (paging.afterLimit() > 0) {
        const auto afterRows = qb::SelectQuery(
            tables::CONTRIBUTION,
            qb::WhereConditions{whereConditions}
                .and_(qb::SubConditions(query_builder::AppendType::Or)
                    .appendQuoted(columns::CREATED_AT, timestamp, qb::Relation::Less)
                    .or_(qb::SubConditions()
                        .appendQuoted(columns::CREATED_AT, timestamp)
                        // be careful with contribution_id order: it depends on feed sorting order
                        .appendQuoted(columns::CONTRIBUTION_ID, *paging.baseId(), qb::Relation::Less)
                    )
                )
            )
            .orderBy(order)
            .limit(paging.afterLimit())
            .exec(txn);
        appendContributions(txn, contributions, uid, afterRows, locale);
    }
    return contributions;
}

proto::ugc_account::ContributionsStat getContributionsStat(
    pqxx::transaction_base& txn,
    Uid uid,
    const std::set<MetadataId>& metadataIds)
{
    proto::ugc_account::ContributionsStat stat;

    const auto whereConditions = qb::WhereConditions()
        .keyInValues(columns::METADATA_ID, metadataIdsToStr(metadataIds))
        .append(columns::UID, std::to_string(uid.value()));

    auto result = qb::CountQuery(tables::CONTRIBUTION, whereConditions).exec(txn);
    stat.set_count(result);

    return stat;
}

} // namespace maps::wiki::ugc::account
