#include <maps/wikimap/ugc/account/src/lib/assignments.h>
#include <maps/wikimap/ugc/account/src/lib/common.h>
#include <maps/wikimap/ugc/libs/common/constants.h>
#include <maps/wikimap/ugc/libs/common/dbqueries.h>
#include <maps/wikimap/ugc/libs/common/helpers.h>

#include <maps/wikimap/mapspro/libs/query_builder/include/select_query.h>
#include <maps/doc/proto/converters/geolib/include/yandex/maps/geolib3/proto.h>
#include <maps/infra/yacare/include/error.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/serialization.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 {

void setAssignmentStatus(
    proto::ugc_account::Assignment& assignment,
    AssignmentStatus status)
{
    switch (status) {
    case AssignmentStatus::Active:
        assignment.set_status(proto::assignment::ACTIVE);
        break;
    case AssignmentStatus::Done:
        assignment.set_status(proto::assignment::DONE);
        break;
    case AssignmentStatus::Skipped:
        assignment.set_status(proto::assignment::SKIPPED);
        break;
    case AssignmentStatus::Expired:
        assignment.set_status(proto::assignment::EXPIRED);
        break;
    }
}

const size_t ASSIGNMENTS_LIMIT = 10000;

std::string bboxToStr(
    pqxx::transaction_base& txn,
    const geolib3::BoundingBox bbox)
{
    return (std::ostringstream()
        << "ST_GeomFromWKB('"
        << txn.esc_raw(geolib3::WKB::toString(bbox.polygon()))
        << "', 4326)" //  Epsg4326, World Geodetic System 1984
    ).str();
}

const auto ALL_ASSIGNMENT_COLUMNS = {
    std::string{"*"},
    columns::GET_X_COORD,
    columns::GET_Y_COORD
};

} // namespace

pqxx::result loadAssignments(
    pqxx::transaction_base& txn,
    Uid uid,
    AssignmentStatus status,
    const std::set<MetadataId>& metadataIds)
{
    const auto whereConditions = qb::WhereConditions()
        .keyInValues(columns::METADATA_ID, metadataIdsToStr(metadataIds))
        .appendQuoted(columns::STATUS, std::string{toString(status)})
        .append(columns::UID, std::to_string(uid.value()))
        .append(
            columns::UPDATED_AT,
            "NOW() - INTERVAL \'6 MONTH\'",
            query_builder::Relation::Greater);
    return qb::SelectQuery(
        tables::ASSIGNMENT,
        ALL_ASSIGNMENT_COLUMNS,
        whereConditions
    )
        .limit(ASSIGNMENTS_LIMIT)
        .exec(txn);
}

pqxx::result loadAssignments(
    pqxx::transaction_base& txn,
    Uid uid,
    AssignmentStatus status,
    const std::set<MetadataId>& metadataIds,
    const geolib3::BoundingBox& bbox,
    size_t limit)
{
    // Do not process limit == 0
    // limit was checked in yacare.cpp
    ASSERT(limit > 0);
    const auto whereConditions = qb::WhereConditions()
        .keyInValues(columns::METADATA_ID, metadataIdsToStr(metadataIds))
        .appendQuoted(columns::STATUS, std::string{toString(status)})
        .append(columns::UID, std::to_string(uid.value()))
        .append(columns::POSITION, bboxToStr(txn, bbox), query_builder::Relation::Intersects)
        .notNull(columns::POSITION);
    return qb::SelectQuery(
        tables::ASSIGNMENT,
        ALL_ASSIGNMENT_COLUMNS,
        whereConditions
    )
        .orderBy(columns::CREATED_AT + " DESC")
        .limit(limit)
        .exec(txn);
}

std::vector<Assignment> makeAssignments(
    pqxx::transaction_base& txn,
    const pqxx::result& rows,
    AssignmentStatus status,
    Uid uid)
{
    std::vector<Assignment> result;
    for (const auto& row : rows) {
        AssignmentId taskId{row[columns::TASK_ID].as<std::string>()};
        Assignment assignment{
            taskId,
            uid,
            status,
            {/*langToMetadata*/},
            maps::chrono::parseSqlDateTime(row[columns::UPDATED_AT].as<std::string>()),
            parsePoint(row)
        };
        auto dataRows = qb::SelectQuery(
            tables::ASSIGNMENT_DATA,
            qb::WhereConditions()
                .append(columns::UID, std::to_string(uid.value()))
                .appendQuoted(columns::TASK_ID, taskId.value())
        ).exec(txn);
        REQUIRE(!dataRows.empty(), "No data for assignment " << taskId << " and uid " << uid);
        for (const auto& dataRow : dataRows) {
            proto::assignment::AssignmentMetadata metadata;
            pqxx::binarystring blob(dataRow[columns::DATA]);
            REQUIRE(
                metadata.ParseFromString(TString{blob.str()}),
                "Cannot parse metadata for assignment " << taskId << " and uid " << uid
            );
            assignment.langToMetadata[dataRow[columns::LOCALE].as<std::string>()] = std::move(metadata);
        }
        result.emplace_back(assignment);
    }
    return result;
}

std::vector<Assignment> getAssignmentsSlice(
    std::vector<Assignment> assignments,
    const Paging& paging)
{
    if (!paging.baseId()) {
        assignments.resize(std::min(assignments.size(), paging.afterLimit()));
        return assignments;
    }
    auto it = std::find_if(
        assignments.begin(),
        assignments.end(),
        [&paging] (const Assignment& obj) { return obj.id.value() == *paging.baseId(); }
    );
    REQUIRE(
        it != assignments.end(),
        yacare::errors::NotFound() << "Unknown assignment id " << *paging.baseId()
    );
    if (paging.beforeLimit() == 0 && paging.afterLimit() == 0) {
        return {std::move(*it)};
    }
    auto first = paging.beforeLimit() == 0
        ? std::next(it)
        : static_cast<size_t>(std::distance(assignments.begin(), it)) > paging.beforeLimit()
            ? it - paging.beforeLimit()
            : assignments.begin();
    auto last = paging.afterLimit() == 0
        ? it
        : static_cast<size_t>(std::distance(it, assignments.end())) > paging.afterLimit() + 1
            ? it + paging.afterLimit() + 1
            : assignments.end();
    if (std::distance(first, last) <= 0) {
        return {};
    }
    return {std::make_move_iterator(first), std::make_move_iterator(last)};
}

proto::ugc_account::Assignments convertAssignments(
    const std::vector<Assignment>& assignments,
    const maps::locale::Locale& locale)
{
    proto::ugc_account::Assignments result;
    for (const Assignment& assignment : assignments) {
        proto::ugc_account::Assignment protoAssignment;
        *protoAssignment.mutable_task_id() = assignment.id.value();
        if (assignment.position) {
            *protoAssignment.mutable_point() = geolib3::proto::encode(*assignment.position);
        }
        setAssignmentStatus(protoAssignment, assignment.status);
        REQUIRE(
            !assignment.langToMetadata.empty(),
            "Empty langToMetadata map for assignment " << assignment.id
        );
        auto it = maps::locale::findBest(
            assignment.langToMetadata.cbegin(),
            assignment.langToMetadata.cend(),
            locale,
            [] (const std::pair<std::string, proto::assignment::AssignmentMetadata>& pair) {
                maps::locale::Locale locale;
                std::istringstream(pair.first) >> locale;
                return locale;
            }
        );

        *protoAssignment.mutable_metadata() = it->second;
        *result.add_assignment() = protoAssignment;
    }
    return result;
}

proto::ugc_account::Assignments findAssignments(
    pqxx::transaction_base& txn,
    Uid uid,
    AssignmentStatus status,
    const std::set<MetadataId>& metadataIds,
    const Paging& paging,
    const maps::locale::Locale& locale)
{
    auto rows = loadAssignments(txn, uid, status, metadataIds);
    auto assignments = makeAssignments(txn, rows, status, uid);

    // somehow range assignments: from new to old
    std::sort(
        assignments.begin(),
        assignments.end(),
        [] (const Assignment& first, const Assignment& second)
        {
            return first.updated > second.updated;
        }
    );
    // choose corresponding to paging
    assignments = getAssignmentsSlice(std::move(assignments), paging);

    // convert to protobuf
    return convertAssignments(assignments, locale);
}

proto::ugc_account::Assignments findAssignments(
    pqxx::transaction_base& txn,
    Uid uid,
    AssignmentStatus status,
    const std::set<MetadataId>& metadataIds,
    const geolib3::BoundingBox& bbox,
    size_t limit,
    const maps::locale::Locale& locale)
{
    auto rows = loadAssignments(txn, uid, status, metadataIds, bbox, limit);
    auto assignments = makeAssignments(txn, rows, status, uid);

    // convert to protobuf
    return convertAssignments(assignments, locale);
}


void skipAssignment(
    pqxx::transaction_base& txn,
    Uid uid,
    const AssignmentId& assignmentId)
{
    const auto now = maps::chrono::TimePoint::clock::now();
    const auto rows = updateAssignmentStatus(
        txn, uid, AssignmentStatus::Skipped, assignmentId, now);

    REQUIRE(
        rows.affected_rows() != 0,
        yacare::errors::NotFound()
            << "Cannot skip assignment " << assignmentId
            << ": unknown id for uid " << uid
    );
}

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