#include <maps/wikimap/ugc/backoffice/src/lib/assignments/modify.h>
#include <maps/wikimap/ugc/backoffice/src/lib/common.h>
#include <maps/wikimap/ugc/libs/common/constants.h>
#include <maps/wikimap/ugc/libs/common/dbqueries.h>
#include <maps/wikimap/mapspro/libs/query_builder/include/compound_query.h>
#include <maps/wikimap/mapspro/libs/query_builder/include/delete_query.h>
#include <maps/wikimap/mapspro/libs/query_builder/include/update_query.h>

#include <maps/doc/proto/converters/geolib/include/yandex/maps/geolib3/proto.h>
#include <maps/infra/yacare/include/error.h>
#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/libs/query_builder/include/select_query.h>

#include <iomanip>
#include <map>
#include <queue>

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

namespace qb = maps::wiki::query_builder;

namespace {

std::string makePositionStr(const geolib3::Point2& point)
{
    return (std::ostringstream()
        << std::setprecision(9) // 1cm
        << "ST_SetSRID(ST_MakePoint("
        << point.x()
        << ", "
        << point.y()
        << "), 4326)"
    ).str();
}

qb::InsertQuery makeInsertAssignmentQuery(
    const AssignmentId& assignmentId,
    const Uid& uid,
    const MetadataId& metadataId,
    const std::optional<geolib3::Point2>& position,
    std::chrono::seconds ttl)
{
    auto now = maps::chrono::formatSqlDateTime(maps::chrono::TimePoint::clock::now());
    static const std::string ACTIVE_STATUS = std::string{toString(AssignmentStatus::Active)};
    std::optional<std::string> strPosition = position
        ? std::optional<std::string>{makePositionStr(*position)}
        : std::optional<std::string>{};
    qb::UpdateOnConflict update;
    update
        .appendQuoted(columns::UPDATED_AT, now)
        .appendQuoted(columns::STATUS, ACTIVE_STATUS)
        .append(columns::POSITION, strPosition)
        .appendQuoted(columns::TTL, std::to_string(ttl.count()) + " SECONDS");

    qb::InsertQuery insertQuery(tables::ASSIGNMENT);
    insertQuery
        .append(columns::UID, std::to_string(uid.value()))
        .appendQuoted(columns::TASK_ID, assignmentId.value())
        .append(columns::METADATA_ID, std::to_string(metadataId.value()))
        .appendQuoted(columns::STATUS, ACTIVE_STATUS)
        .append(columns::POSITION, strPosition)
        .appendQuoted(columns::TTL, std::to_string(ttl.count()) + " SECONDS")
        .updateOnConflict(
            qb::OnConflict::fromConstraint(constraints::ASSIGNMENT, std::move(update)));
    return insertQuery;
}

qb::DeleteQuery makeDeleteAssignmentDataQuery(
    const AssignmentId& assignmentId,
    const Uid& uid)
{
    return qb::DeleteQuery(
        tables::ASSIGNMENT_DATA,
        qb::WhereConditions()
            .append(columns::UID, std::to_string(uid.value()))
            .appendQuoted(columns::TASK_ID, assignmentId.value())
    );
}

qb::InsertQuery makeInsertAssignmentDataQuery(
    const AssignmentId& assignmentId,
    const Uid& uid,
    const Lang& lang,
    const std::string& data)
{
    return std::move(qb::InsertQuery(tables::ASSIGNMENT_DATA)
        .append(columns::UID, std::to_string(uid.value()))
        .appendQuoted(columns::TASK_ID, assignmentId.value())
        .appendQuoted(columns::LOCALE, lang.value())
        .appendRawQuoted(columns::DATA, data));
}

MetadataId extractMetadataId(
    const proto::backoffice::Task& task,
    const RequestValidator& validator,
    maps::auth::TvmId tvmId)
{
    auto metadataId = getMetadataId(
        task.lang_to_metadata(),
        [] (const proto::assignment::AssignmentMetadata& c) { return c.assignment_case(); }
    );
    validator.checkMetadataId(tvmId, metadataId);
    validator.checkGeometry(task, metadataId);
    return metadataId;
}

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

void execQueries(
    const std::map<pgpool3::Pool*, qb::CompoundQuery>& poolQueries,
    bool dryRun)
{
    std::queue<pgpool3::TransactionHandle> queue;
    for (auto& [poolPtr, queries] : poolQueries) {
        auto txn = poolPtr->masterWriteableTransaction();
        queries.execNotEmpty(*txn);
        queue.push(std::move(txn));
    }
    while (!queue.empty()) {
        auto& txn = queue.front();
        finishTxn(*txn, dryRun);
        queue.pop();
    }
}

} // namespace

void createAssignments(
    const IDbPools& pools,
    bool dryRun,
    const AssignmentId& assignmentId,
    const proto::backoffice::Task& task,
    maps::auth::TvmId tvmId,
    std::chrono::seconds ttl,
    const RequestValidator& validator)
try {
    validator.checkId(tvmId, assignmentId.value());
    const auto& langToMetadata = task.lang_to_metadata();
    REQUIRE(
        !task.uid().empty(),
        yacare::errors::BadRequest() << "No uids in creating assignment " << assignmentId
    );
    REQUIRE(
        !langToMetadata.empty(),
        yacare::errors::BadRequest()
            << "Empty langToMetadata map for assignment " << assignmentId
    );
    MetadataId metadataId = extractMetadataId(task, validator, tvmId);

    std::map<pgpool3::Pool*, qb::CompoundQuery> poolQueries;
    std::optional<geolib3::Point2> position;
    if (task.has_point()) {
        position = geolib3::proto::decode(task.point());
    }

    std::set<Uid> usedUids;
    for (int i = 0; i < task.uid_size(); ++i) {
        Uid uid{std::stoull(task.uid(i))};
        if (!usedUids.insert(uid).second) {
            continue;
        }
        poolQueries[&pools.at(uid)].append(
            makeInsertAssignmentQuery(
                assignmentId, uid, metadataId, position, ttl
            )
        );
        poolQueries[&pools.at(uid)].append(
            makeDeleteAssignmentDataQuery(
                assignmentId, uid
            )
        );
    }
    for (auto it = langToMetadata.cbegin(); it != langToMetadata.cend(); ++it) {
        TString data;
        REQUIRE(
            it->second.SerializeToString(&data),
            "Problem occured while serializing proto data");
        std::string strData{data};
        for (const Uid& uid : usedUids) {
            poolQueries[&pools.at(uid)].append(
                makeInsertAssignmentDataQuery(
                    assignmentId, uid, Lang{it->first}, strData
                )
            );
        }
    }

    execQueries(poolQueries, dryRun);
} catch (std::exception& e) {
    ERROR() << "Exception " << e.what()
            << " caused by create assignments request: "
            << task.DebugString();
    throw;
}

void deleteAssignment(
    pqxx::transaction_base& txn,
    Uid uid,
    const AssignmentId& assignmentId)
{
    auto now = maps::chrono::TimePoint::clock::now();
    const auto rows = updateAssignmentStatus(
        txn, uid, AssignmentStatus::Expired, assignmentId, now);
    REQUIRE(
        rows.affected_rows() != 0,
        yacare::errors::NotFound()
            << "Cannot delete assignment " << assignmentId
            << ": unknown id for uid " << uid
    );
}

void doneAssignment(
    pqxx::transaction_base& txn,
    Uid uid,
    const AssignmentId& assignmentId)
{
    auto now = maps::chrono::TimePoint::clock::now();
    const auto rows = updateAssignmentStatus(
        txn, uid, AssignmentStatus::Done, assignmentId, now);
    REQUIRE(
        rows.affected_rows() != 0,
        yacare::errors::NotFound()
            << "Cannot mark assignment " << assignmentId
            << " as done: unknown id for uid " << uid
    );
}

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