#include <maps/wikimap/ugc/backoffice/src/lib/contributions/modify.h>
#include <maps/wikimap/ugc/backoffice/src/lib/common.h>
#include <maps/wikimap/ugc/libs/common/constants.h>

#include <maps/infra/yacare/include/error.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/libs/query_builder/include/compound_query.h>
#include <maps/wikimap/mapspro/libs/query_builder/include/select_query.h>

namespace maps::wiki::ugc::backoffice {

namespace qb = maps::wiki::query_builder;

namespace {

qb::DeleteQuery makeDeleteQuery(
    const ContributionId& contributionId,
    const Uid& uid,
    const std::string& table)
{
    return qb::DeleteQuery(
        table,
        qb::WhereConditions()
            .append(columns::UID, std::to_string(uid.value()))
            .appendQuoted(columns::CONTRIBUTION_ID, contributionId.value()));
}

MetadataId extractMetadataId(
    const proto::backoffice::ModifyContribution::Upsert& upsert)
{
    return getMetadataId(
        upsert.contribution().lang_to_metadata(),
        [] (const proto::contribution::ContributionMetadata& c) { return c.contribution_case(); }
    );
}

[[maybe_unused]] void checkNewMetadataId(
    pqxx::transaction_base& txn,
    MetadataId newMetadataId,
    const ContributionId& contributionId)
{
    auto oldMetadataId = loadMetadataId(
        txn,
        tables::CONTRIBUTION,
        columns::CONTRIBUTION_ID,
        contributionId.value()
    );
    if (oldMetadataId && newMetadataId != oldMetadataId) {
        throw yacare::errors::BadRequest()
            << "Got metadata_id=" << newMetadataId
            << " not equal for existing " << *oldMetadataId
            << " for contribution_id=" << contributionId;
    }
}

} // namespace

qb::InsertQuery makeInsertContributionQuery(
    const ContributionId& contributionId,
    const Uid& uid,
    const MetadataId& metadataId,
    std::optional<uint64_t> timestamp)
{
    qb::InsertQuery insertQuery(tables::CONTRIBUTION);
    insertQuery
        .append(columns::UID, std::to_string(uid.value()))
        .appendQuoted(columns::CONTRIBUTION_ID, contributionId.value())
        .append(columns::METADATA_ID, std::to_string(metadataId.value()))
        .updateOnConflict(qb::OnConflict{});
    if (timestamp) {
        insertQuery.append(
            columns::CREATED_AT,
            "to_timestamp(" + std::to_string(*timestamp) + ")");
    }
    return insertQuery;
}

qb::InsertQuery makeInsertContributionDataQuery(
    const ContributionId& contributionId,
    const Uid& uid,
    const Lang& lang,
    const proto::contribution::ContributionMetadata& metadata)
{
    TString data;
    REQUIRE(
        metadata.SerializeToString(&data),
        "Problem occured while serializing proto data");
    return std::move(qb::InsertQuery(tables::CONTRIBUTION_DATA)
        .append(columns::UID, std::to_string(uid.value()))
        .appendQuoted(columns::CONTRIBUTION_ID, contributionId.value())
        .appendQuoted(columns::LOCALE, lang.value())
        .appendRawQuoted(columns::DATA, data));
}

void updateDb(
    pqxx::transaction_base& txn,
    const proto::backoffice::ModifyContributions& modifyContributions,
    const Uid& uid,
    maps::auth::TvmId tvmId,
    const RequestValidator& validator)
try {
    qb::CompoundQuery query;

    for (int i = 0; i < modifyContributions.modify_contribution_size(); ++i) {
        const auto& modify = modifyContributions.modify_contribution(i);
        if (modify.has_delete_()) {
            ContributionId id{modify.delete_().id()};
            validator.checkId(tvmId, id.value());
            DEBUG() << "Delete contribution " << id;
            query.append(makeDeleteQuery(id, uid, tables::CONTRIBUTION));
        } else if (modify.has_upsert()) {
            const auto& upsert = modify.upsert();
            if (!upsert.has_contribution()) {
                throw yacare::errors::BadRequest() << "Upsert message without contribution";
            }
            MetadataId metadataId = extractMetadataId(upsert);
            validator.checkMetadataId(tvmId, metadataId);
            std::optional<uint64_t> timestamp = upsert.contribution().has_timestamp()
                ? std::optional{upsert.contribution().timestamp()}
                : std::nullopt;
            ContributionId id{upsert.id()};
            validator.checkId(tvmId, id.value());
            DEBUG() << "Insert contribution " << id;
            query.append(makeInsertContributionQuery(id, uid, metadataId, timestamp));
            query.append(makeDeleteQuery(id, uid, tables::CONTRIBUTION_DATA));
            const auto& langToMetadata = upsert.contribution().lang_to_metadata();
            REQUIRE(
                !langToMetadata.empty(),
                yacare::errors::BadRequest()
                    << "Empty langToMetadata map for contribution " << id
            );
            for (auto it = langToMetadata.cbegin(); it != langToMetadata.cend(); ++it) {
                query.append(makeInsertContributionDataQuery(id, uid, Lang{it->first}, it->second));
            }
        } else {
            throw yacare::errors::BadRequest() << "Bad request proto";
        }
    }
    query.execNotEmpty(txn);
} catch (std::exception& e) {
    ERROR() << "Exception " << e.what()
            << " caused by modify contributions request: "
            << modifyContributions.DebugString();
    throw;
}

} // maps::wiki::ugc::backoffice
