#include <yandex/maps/wiki/diffalert/storage/results_writer.h>
#include <yandex/maps/wiki/diffalert/diff_context.h>
#include <yandex/maps/wiki/diffalert/message.h>

#include "magic_strings.h"
#include <yandex/maps/wiki/common/geom.h>
#include <yandex/maps/wiki/common/retry_duration.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>

#include <geos/geom/GeometryFactory.h>

#include <iomanip>
#include <iterator>
#include <sstream>
#include <string>

namespace maps {
namespace wiki {
namespace diffalert {

namespace {

const size_t MESSAGE_BATCH_SIZE = 100;

std::string attributesIdClause(
    const std::string& categoryId,
    const std::string& description,
    const pqxx::transaction_base& txn)
{
    return funcs::GET_ATTRIBUTES_ID + '(' +
        txn.quote(categoryId) + ',' +
        txn.quote(description) +
        ')';
}

std::string taskMessageValuesStr(
    TaskId taskId,
    const StoredMessage& message,
    RegionPriority regionPriority,
    pqxx::transaction_base& txn)
{
    std::ostringstream oss;
    oss.precision(common::MERCATOR_OSTREAM_PRECISION);

    oss << '('
        << taskId << ','
        << message.objectId() << ','
        << message.priority().major << ','
        << message.priority().minor << ','
        << message.priority().sort << ','
        << attributesIdClause(message.categoryId(), message.description(), txn) << ',';

    const auto& envelope = message.envelope();
    if (!envelope.isNull()) {
        oss << "ST_MakeEnvelope("
            << envelope.getMinX() << ',' << envelope.getMinY() << ','
            << envelope.getMaxX() << ',' << envelope.getMaxY() << ','
            << MERCATOR_SRID << "),";
    } else {
        oss << "NULL,";
    }
    oss << static_cast<uint32_t>(regionPriority) << ',';
    oss << txn.quote(message.objectLabel()) << ',';

    oss << (message.hasOwnName() == HasOwnName::Yes ? "TRUE" : "FALSE");
    oss << ')';

    return oss.str();
}

} // namespace

ResultsWriter::ResultsWriter(TaskId taskId, pgpool3::Pool& connPool)
    : taskId_(taskId)
    , connPool_(connPool)
    , writer_(std::async(std::launch::async, [this]{ writerLoop(); }))
{ }

ResultsWriter::ResultsWriter(
        TaskId taskId,
        pgpool3::Pool& connPool,
        std::vector<DiffalertRegion> sortedRegions)
    : taskId_(taskId)
    , connPool_(connPool)
    , sortedRegions_(std::move(sortedRegions))
    , failed_(false)
    , writer_(std::async(std::launch::async, [this]{ writerLoop(); }))
{
}

ResultsWriter::~ResultsWriter()
{
    stop();
}

void ResultsWriter::put(std::list<StoredMessage> messages)
{
    messagesQueue_.pushAll(std::move(messages));
}

void ResultsWriter::finish()
{
    stop();
    writer_.get();
}

void ResultsWriter::stop() noexcept
{
    messagesQueue_.finish();
    if (writer_.valid()) {
        writer_.wait();
    }
}

void ResultsWriter::writerLoop()
{
    std::list<StoredMessage> batch;
    std::list<StoredMessage> buffer;
    while (!messagesQueue_.finished() || messagesQueue_.pendingItemsCount()) {
        messagesQueue_.popAll(buffer);

        while (batch.size() + buffer.size() >= MESSAGE_BATCH_SIZE) {
            auto spliceIt = std::begin(buffer);
            std::advance(spliceIt, MESSAGE_BATCH_SIZE - batch.size());
            batch.splice(std::end(batch), buffer, std::begin(buffer), spliceIt);

            writeBatchWithRetries(batch);
            batch.clear();

            if (failed_) {
                return;
            }
        }

        batch.splice(std::end(batch), buffer);
    }
    if (!batch.empty()) {
        writeBatchWithRetries(batch);
    }
}

void ResultsWriter::writeBatch(
        const std::list<StoredMessage>& messages)
{
    auto txn = connPool_.masterWriteableTransaction();

    std::string query =
        "INSERT INTO " + tables::MESSAGES + " (" +
                columns::TASK_ID + ',' +
                columns::OBJECT_ID + ',' +
                columns::MAJOR_PRIORITY + ',' +
                columns::MINOR_PRIORITY + ',' +
                columns::SORT_PRIORITY + ',' +
                columns::ATTRIBUTES_ID + ',' +
                columns::THE_GEOM + ',' +
                columns::REGION_PRIORITY + ',' +
                columns::OBJECT_LABEL + ',' +
                columns::HAS_OWN_NAME +
            ") VALUES ";
    std::vector<std::string> messageValueStrs;
    for (const auto& message : messages) {
        auto regionPriority = findRegionPriority(message.envelope());
        messageValueStrs.push_back(taskMessageValuesStr(taskId_, message, regionPriority, *txn));
    }
    query += common::join(messageValueStrs, ',');
    txn->exec(query);

    txn->commit();
};

void ResultsWriter::writeBatchWithRetries(
        const std::list<StoredMessage>& messages)
{
    try {
        common::retryDuration([&] { writeBatch(messages); });
    } catch (const std::exception& e) {
        failed_ = true;
        ERROR() << "Failed to insert data to the database: " << e.what();
    } catch (...) {
        failed_ = true;
        ERROR() << "Failed to insert data to the database: unknown error caught";
    }
}

RegionPriority ResultsWriter::findRegionPriority(const Envelope& envelope) const
{
    if (sortedRegions_.empty()) {
        return RegionPriority::Unimportant;
    }

    common::Geom geom(geos::geom::GeometryFactory::getDefaultInstance()->toGeometry(&envelope));
    for (const auto& region : sortedRegions_) {
        if (region.geometry->getEnvelopeInternal()->intersects(&envelope)
                && region.geometry->intersects(geom.geosGeometryPtr())) {
            return region.priority;
        }
    }

    return RegionPriority::Unimportant;
}

} // namespace diffalert
} // namespace wiki
} // namespace maps
