#include "task_consistency.h"
#include "util.h"
#include <maps/wikimap/mapspro/libs/social/helpers.h>
#include <maps/libs/geolib/include/conversion.h>
#include <yandex/maps/wiki/social/feedback/description_serialize.h>
#include <yandex/maps/wiki/social/feedback/history_patch.h>
#include <yandex/maps/wiki/social/feedback/gateway_rw.h>
#include <yandex/maps/wiki/social/feedback/task_patch.h>
#include <yandex/maps/wiki/social/feedback/task_filter.h>


namespace maps::wiki::social::feedback {

using boost::lexical_cast;

namespace {

const double MAX_ACCEPTABLE_LAT = 84.999;
const double MAX_ACCEPTABLE_MERC_LAT = [](){
    return geolib3::convertGeodeticToMercator(geolib3::Point2(0, MAX_ACCEPTABLE_LAT)).y();
}();

geolib3::Point2 fixMercatorPosition(const geolib3::Point2& pos)
{
    return geolib3::Point2(
        pos.x(),
        std::clamp(pos.y(), -MAX_ACCEPTABLE_MERC_LAT, MAX_ACCEPTABLE_MERC_LAT)
    );
}

bool needSetModifiedAt(const HistoryPatch& historyPatch)
{
    for (const auto& historyItem : historyPatch.items()) {
        switch (historyItem.operation()) {
            case TaskOperation::Defer:
            case TaskOperation::Reveal:
            case TaskOperation::Open:
            case TaskOperation::NeedInfo:
            case TaskOperation::Accept:
            case TaskOperation::Reject:
            case TaskOperation::Deploy:
            case TaskOperation::ProcessingLevelUp:
            case TaskOperation::ChangeProcessingLvl:
                return true;

            default:
                break;
        }
    }
    return false;
}

std::string viewedByPatchString(TUid uid)
{
    std::stringstream ss;
    ss << " || '{" << uid << "}'";
    return ss.str();
}

std::string generateSetClause(
    pqxx::transaction_base& socialTxn,
    const TaskPatch& patch,
    const HistoryPatch& historyPatch)
{
    using ColumnValue = std::pair<std::string, std::string>;
    std::vector<ColumnValue> columnValues;

    if (patch.isAcquired()) {
        std::string uidStr = sql::value::NULL_;
        std::string dateStr = sql::value::NULL_;
        if (patch.isAcquired().value()) {
            uidStr = std::to_string(patch.updater());
            if (patch.acquiredAt() == std::nullopt) {
                dateStr = sql::value::NOW;
            } else {
                dateStr = socialTxn.quote(
                    chrono::formatSqlDateTime(patch.acquiredAt().value()));
            }
        }
        columnValues.emplace_back(sql::col::ACQUIRED_BY, std::move(uidStr));
        columnValues.emplace_back(sql::col::ACQUIRED_AT, std::move(dateStr));
    }

    if (patch.resolution()) {
        std::string uidStr = sql::value::NULL_;
        std::string dateStr = sql::value::NULL_;
        std::string resolutionStr = sql::value::NULL_;
        std::string rejectReasonStr = sql::value::NULL_;
        if (patch.resolution().value()) {
            uidStr = std::to_string(patch.updater());
            dateStr = sql::value::NOW;
            resolutionStr = socialTxn.quote(
                boost::lexical_cast<std::string>(
                    patch.resolution().value()->verdict()));
            const auto& reason = patch.resolution().value()->rejectReason();
            if (reason) {
                rejectReasonStr = socialTxn.quote(
                    boost::lexical_cast<std::string>(reason.value()));
            }
        }
        columnValues.emplace_back(sql::col::RESOLVED_BY, std::move(uidStr));
        columnValues.emplace_back(sql::col::RESOLVED_AT, std::move(dateStr));
        columnValues.emplace_back(sql::col::RESOLUTION, std::move(resolutionStr));
        columnValues.emplace_back(sql::col::REJECT_REASON, std::move(rejectReasonStr));
    }

    if (patch.deployedAt()) {
        std::string value = socialTxn.quote(
            chrono::formatSqlDateTime(patch.deployedAt().value()));
        columnValues.emplace_back(sql::col::DEPLOYED_AT, std::move(value));
    }

    if (patch.hidden()) {
        columnValues.emplace_back(sql::col::HIDDEN, toPgValue(*patch.hidden()));
    }

    if (patch.type()) {
        columnValues.emplace_back(sql::col::TYPE,
            socialTxn.quote(boost::lexical_cast<std::string>(*patch.type())));
    }

    if (patch.processingLvl()) {
        columnValues.emplace_back(sql::col::PROCESSING_LVL,
            socialTxn.quote(std::string(toString(*patch.processingLvl()))));
    }

    if (patch.position()) {
        columnValues.emplace_back(sql::col::POSITION,
            makePqxxGeomExpr(socialTxn, fixMercatorPosition(*patch.position())));
    }

    if (patch.bucket()) {
        columnValues.emplace_back(
            sql::col::BUCKET,
            socialTxn.quote(boost::lexical_cast<std::string>(patch.bucket().value())));
    }

    if (patch.duplicateHeadId()) {
        std::string value = sql::value::NULL_;
        if (patch.duplicateHeadId().value()) {
            value = std::to_string(patch.duplicateHeadId().value().value());
        }
        columnValues.emplace_back(sql::col::DUPLICATE_HEAD_ID, std::move(value));
    }

    if (patch.viewedByUpdater()) {
        columnValues.emplace_back(
            sql::col::VIEWED_BY,
            sql::col::VIEWED_BY + viewedByPatchString(patch.updater()));
    }

    if (needSetModifiedAt(historyPatch)) {
        columnValues.emplace_back(
            sql::col::STATE_MODIFIED_AT,
            sql::value::NOW);
    }

    return common::join(columnValues, [](const ColumnValue& colVal) {
        return colVal.first + "=" + colVal.second;
    }, ", ");
}

TIds overdueIdsForUpdate(pqxx::transaction_base& socialTxn)
{
    std::stringstream query;
    query << "SELECT " << sql::col::ID
        << " FROM " << sql::table::FEEDBACK_TASK
        << " WHERE "
        << sql::col::ACQUIRED_BY << " IS NOT NULL"
        << " AND " << sql::col::ACQUIRED_AT << " < NOW() - interval '1 hour'"
        << " ORDER BY " << sql::col::ID
        << " FOR UPDATE";

    auto rows = socialTxn.exec(query.str());
    TIds result;
    for (const auto& row: rows) {
        result.insert(row[sql::col::ID].as<TId>());
    }
    return result;
}

std::string historyParamsJson(const HistoryItemParams& historyParams)
{
    return "{"
        + common::join(
            historyParams,
            [](const HistoryItemParams::value_type& valueType) {
                return "\"" + valueType.first + "\": \"" + valueType.second + "\"";
            },
            ", ")
        + "}";
}

std::string historyItemValueQuery(
    pqxx::transaction_base& socialTxn,
    TId taskId,
    const HistoryItemNew& item)
{
    std::stringstream query;
    query << "(";
    query << taskId << ","
        << item.modifiedBy() << ","
        << "'" << item.operation() << "',"
        << (item.commentId() ? std::to_string(*item.commentId()) : sql::value::NULL_) << ",";

    if (item.params().empty()) {
        query << sql::value::NULL_;
    } else {
        query << "("
            << socialTxn.quote(historyParamsJson(item.params()))
            << ")::jsonb";
    }

    query << ")";
    return query.str();
}

void appendHistory(
    pqxx::transaction_base& socialTxn,
    TId taskId,
    const HistoryPatch& historyPatch)
{
    if (historyPatch.items().empty()) {
        return;
    }

    std::stringstream query;
    std::vector<std::string> fieldNames{
        sql::col::FEEDBACK_TASK_ID,
        sql::col::MODIFIED_BY,
        sql::col::OPERATION,
        sql::col::COMMENT_ID,
        sql::col::PARAMS
    };

    query << "INSERT INTO " << sql::table::FEEDBACK_HISTORY
        << "(" <<  common::join(fieldNames, ',') << ")"
        << " VALUES ";
    query << common::join(
        historyPatch.items(),
        [&](const HistoryItemNew& item) {
            return historyItemValueQuery(socialTxn, taskId, item);
        },
        ", ");

    socialTxn.exec(query.str());
}

} // namespace anonymous

GatewayRW::GatewayRW(pqxx::transaction_base& socialTxn)
    : GatewayRO(socialTxn)
{}

TasksForUpdate GatewayRW::tasksForUpdateByIds(TIds ids)
{
    return tasksForUpdateByFilter(TaskFilter().ids(std::move(ids)));
}

std::optional<TaskForUpdate> GatewayRW::taskForUpdateById(TId id)
{
    auto tasks = tasksForUpdateByFilter(TaskFilter().ids(TIds{id}));
    return tasks.empty() ? std::nullopt :
           std::optional<TaskForUpdate>{std::move(tasks.front())};
}

TasksForUpdate GatewayRW::tasksForUpdateByFilter(const TaskFilter& filter)
{
    std::stringstream query;
    query << "SET LOCAL lock_timeout = '1s';";
    query << tasksByFilterQuery(filter)
        << " ORDER BY " << sql::col::ID
        << " FOR UPDATE";
    return execTaskQuery<TaskForUpdate>(socialTxn_, query.str());
}

void GatewayRW::releaseOverdueTasks(TUid uid)
{
    auto ids = overdueIdsForUpdate(socialTxn_);

    TaskPatch patch(uid);
    patch.setReleased();
    for (const auto id : ids) {
        updateTaskById(id, patch);
    }
}

void GatewayRW::updateDuplicatedHeadId(TUid uid, const TIds& ids, const TId& headIdNew)
{
    if (ids.empty()) {
        return;
    }

    TaskPatch taskPatch(uid);
    taskPatch.setDuplicateHeadId(headIdNew);
    // No HistoryItem for changing duplicateHeadId.
    auto setClause = generateSetClause(socialTxn_, taskPatch, HistoryPatch::patchEmpty());
    auto whereClause = TaskFilter().ids(ids).whereClause(socialTxn_);

    execUpdateTasks(socialTxn_, setClause, whereClause);
}

std::optional<TaskForUpdate> GatewayRW::updateTaskById(TId id, const TaskPatch& patch)
{
    auto task = taskForUpdateById(id);
    if (!task) {
        return std::nullopt;
    }
    return updateTask(*task, patch);
}

TaskForUpdate GatewayRW::updateTask(const TaskForUpdate& task, const TaskPatch& patch)
{
    auto historyPatch = HistoryPatch::patchOnUpdate(task, patch);
    appendHistory(socialTxn_, task.id(), historyPatch);

    auto setClause = generateSetClause(socialTxn_, patch, historyPatch);
    if (setClause.empty()) {
        return task;
    }
    auto whereClause = TaskFilter().ids({task.id()}).whereClause(socialTxn_);
    execUpdateTasks(socialTxn_, setClause, whereClause);
    auto taskUpdated = taskForUpdateById(task.id());
    ASSERT(taskUpdated);
    return TaskForUpdate(std::move(*taskUpdated));
}

TaskForUpdate GatewayRW::addTask(TUid uid, const TaskNew& newTask)
{
    REQUIRE(!newTask.externalReferenceId || !newTask.externalReferenceId->empty(),
            "Empty external reference id");

    requireTaskAndAttrsConsistency(newTask);

    std::stringstream query;

    std::vector<std::string> fieldNames{
        sql::col::POSITION,
        sql::col::TYPE,
        sql::col::SOURCE,
        sql::col::DESCRIPTION,
        sql::col::ATTRS
    };
    std::vector<std::string> fieldValues{
        makePqxxGeomExpr(socialTxn_, fixMercatorPosition(newTask.position)),
        socialTxn_.quote(lexical_cast<std::string>(newTask.type)),
        socialTxn_.quote(newTask.source),
        socialTxn_.quote(toJson(newTask.description)),
        socialTxn_.quote((json::Builder() << newTask.attrs.toJson()).str())
    };

    if (newTask.objectId) {
        ASSERT(newTask.objectId.value() != 0);
        fieldNames.push_back(sql::col::OBJECT_ID);
        fieldValues.push_back(std::to_string(*newTask.objectId));
    }
    if (newTask.hidden) {
        fieldNames.push_back(sql::col::HIDDEN);
        fieldValues.push_back(toPgValue(*newTask.hidden));
    }
    if (newTask.indoorLevel) {
        fieldNames.push_back(sql::col::INDOOR_LEVEL);
        fieldValues.push_back(socialTxn_.quote(*newTask.indoorLevel));
    }
    if (newTask.internalContent) {
        fieldNames.push_back(sql::col::INTERNAL_CONTENT);
        fieldValues.push_back(toPgValue(*newTask.internalContent));
    }

    query << "INSERT INTO " << sql::table::FEEDBACK_TASK_PENDING << " "
        << "(" <<  common::join(fieldNames, ',') << ")"
        << " VALUES (" << common::join(fieldValues, ',') << ")"
        << " RETURNING " << FIELDS_TO_SELECT;

    auto rows = socialTxn_.exec(query.str());
    REQUIRE(!rows.empty(), "Empty pqxx rows after inserting Task");

    TaskForUpdate retVal(rows[0]);
    if (newTask.externalReferenceId) {
        std::stringstream query;
        query << "INSERT INTO " << sql::table::EXTERNAL_REFERENCE_FEEDBACK_TASK << " "
              << "(" << sql::col::EXTERNAL_REFERENCE_ID << "," << sql::col::FEEDBACK_TASK_ID << ")"
              << " VALUES ("
                    << socialTxn_.quote(*newTask.externalReferenceId) << ","
                    << retVal.id() << ")";
        socialTxn_.exec(query.str());
    }

    appendHistory(socialTxn_, retVal.id(), HistoryPatch::patchOnCreate(uid));
    return retVal;
}

} // namespace maps::wiki::social::feedback
