#include <yandex/maps/wiki/validator/storage/messages_writer.h>

#include "helpers.h"
#include "magic_strings.h"
#include <maps/wikimap/mapspro/libs/validator/common/magic_strings.h>

#include <yandex/maps/wiki/common/retry_duration.h>
#include <yandex/maps/wiki/common/string_utils.h>

#include <maps/libs/log8/include/log8.h>

namespace maps::wiki::validator::storage {

namespace {

std::string messageAttributesInsertFunctionStr(
    pqxx::transaction_base& txn,
    const Message::Attributes& attributes)
{
    std::ostringstream result;
    result << MESSAGE_ATTRIBUTES_INSERT_FUNCTION << "("
           << static_cast<unsigned int>(attributes.severity) << ","
           << txn.quote(attributes.checkId) << ","
           << txn.quote(attributes.description) << ","
           << (attributes.regionType == RegionType::Important ? "TRUE" : "FALSE") << ")";
    return result.str();
}

std::string taskMessageValuesStr(
    pqxx::transaction_base& txn,
    TTaskId taskId,
    const Message& message)
{
    std::ostringstream result;
    result << "("
           << taskId << ",";

    result << messageAttributesInsertFunctionStr(txn, message.attributes()) << ",";

    result << MESSAGE_CONTENT_INSERT_FUNCTION << "("
           << txn.quote(revisionIdsToPgText(message.revisionIds())) << ",";
    if (message.geomWkb().empty()) {
        result << "NULL";
    } else {
        result << "ST_GeomFromWkb('" + txn.esc_raw(message.geomWkb()) + "')";
    }
    result << "))";

    return result.str();
}

void writeMessagesBatch(
    pqxx::transaction_base& txn,
    TTaskId taskId,
    const std::vector<Message>& messages)
{
    std::ostringstream insertTaskMessageQuery;
    insertTaskMessageQuery
        << "INSERT INTO " << TASK_MESSAGE_TABLE
        << "(" << TASK_ID_COLUMN_NAME << "," << MESSAGE_ID_COLUMNS << ")"
        << " VALUES ";

    std::vector<std::string> valuesStrs;
    valuesStrs.reserve(messages.size());
    for (const Message& message : messages) {
        valuesStrs.push_back(taskMessageValuesStr(txn, taskId, message));
    }
    insertTaskMessageQuery << common::join(valuesStrs, ',');

    txn.exec(insertTaskMessageQuery.str());

    std::map<Message::Attributes, size_t> stats;
    for (const Message& message : messages) {
        stats[message.attributes()] += 1;
    }

    std::ostringstream insertStatsQuery;
    for (const auto& [attributes, count] : stats) {
        insertStatsQuery
            << "SELECT " << TASK_MESSAGE_STATS_UPDATE_FUNCTION << "("
            << taskId << ","
            << messageAttributesInsertFunctionStr(txn, attributes) << ","
            << count << ");";
    }

    txn.exec(insertStatsQuery.str());
}

void execCommitWithRetries(
    pgpool3::Pool& pool,
    const std::function<void(pqxx::transaction_base&)>& f)
{
    size_t retryCounter = 0;
    size_t execCounter = 0;

    common::retryDuration([&] {
        if (retryCounter++) {
            INFO() << "Attempt " << retryCounter;
        }

        try {
            auto conn = pool.getMasterConnection();
            pqxx::work work(conn.get());
            work.exec("SET search_path=public;"); // test connectivity to database

            ++execCounter;
            f(work);
            work.commit();
        } catch (const std::exception& ex) {
            ERROR() << "Attempt " << retryCounter << " failed: " << ex.what();
            if (execCounter < 2) {
                // may be fail connectivity on lost connection, exec(), commit()...
                throw;
            }
            throw common::RetryDurationCancel() << ex.what(); // data error
        }
    });
}

} // namespace

MessagesWriter::MessagesWriter(pgpool3::Pool& pgPool, TTaskId taskId)
    : pgPool_(pgPool)
    , taskId_(taskId)
{ }

bool MessagesWriter::writeMessagesBatchWithRetries(
    const std::vector<Message>& messages)
{
    if (messages.empty()) {
        return true;
    }
    try {
        common::retryDuration([&] {
            execCommitWithRetries(
                pgPool_,
                [&] (pqxx::transaction_base& txn) {
                    writeMessagesBatch(txn, taskId_, messages);
                }
            );
        });
        return true;
    } catch (const std::exception& e) {
        ERROR() << "Failed to insert data to the database: " << e.what();
    } catch (...) {
        ERROR() << "Failed to insert data to the database: unknown error caught";
    }
    return false;
}

} // namespace maps::wiki::validator::storage
