#include <yandex/maps/wiki/social/feedback/reviews_gateway.h>

#include <yandex/maps/wiki/social/feedback/gateway_ro.h>
#include <yandex/maps/wiki/social/feedback/task_filter.h>
#include <yandex/maps/wiki/common/string_utils.h>

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

namespace {
const std::vector<std::string> REVIEW_TASK_SOURCES {
    "partner-pedestrian-indoor",
    "partner-pedestrian-onfoot",
};

void
saveTaskComments(TId reviewId, const std::map<TId, ReviewTaskComment>& tasksComments, pqxx::transaction_base& txn)
{
    ASSERT(reviewId);
    if (tasksComments.empty()) {
        return;
    }
    std::string query =
        "INSERT INTO social.feedback_review_task (review_id, task_id, task_comment, topic)"
        " VALUES ";
    bool first = true;
    for (const auto& [taskId, comment] : tasksComments) {
        if (!first) {
            query += ",";
        }
        query +=
            "(" + std::to_string(reviewId) +
            ", " + std::to_string(taskId) +
            ", " + txn.quote(comment.comment) +
            ", " + (comment.topic
                ? txn.quote(std::string(toString(*comment.topic)))
                : std::string("NULL")) +
            ")";
        first = false;
    }
    query += " ON CONFLICT (review_id, task_id) DO "
            " UPDATE SET task_comment = EXCLUDED.task_comment, topic = EXCLUDED.topic;";
    txn.exec(query);
}

TId
maxTaskId(TId regionId, pqxx::transaction_base& txn)
{
    const auto rows  = txn.exec(
        "SELECT max(task_id) FROM "
        "social.feedback_review_task INNER JOIN "
        "social.feedback_review USING (review_id) "
        "WHERE region_id = " + std::to_string(regionId));
    return rows.empty() || rows[0][0].is_null()
        ? 0
        : rows[0][0].as<TId>();
}

} // namespace

ReviewsGateway::ReviewsGateway(pqxx::transaction_base& socialTxn)
    : socialTxn_(socialTxn)
{
}

Review
ReviewsGateway::createReview(
    TId regionId,
    TId regionCommitId,
    TUid reviewee,
    TUid author,
    const geolib3::Polygon2& regionGeom,
    CreationPolicy creationPolicy)
{
    const auto recent = getRecentRegionReviews(regionId, 1);
    const auto lastTaskId =
        creationPolicy == CreationPolicy::AllTasks
            ? 0
            : maxTaskId(regionId, socialTxn_);

    if (!recent.empty() && recent.front().state() != ReviewState::Published) {
        throw AnotherReviewInProgress() <<
            "Region: " << regionId <<
            " review in progress: " << recent.front().id();
    }
    TaskFilter taskFilter;
    // TODO: .createdBy(reviewee), for now task author is robot.
    taskFilter
        .boundary(regionGeom)
        .sources(REVIEW_TASK_SOURCES)
        .uiFilterStatus(UIFilterStatus::Opened);
    if (lastTaskId) {
        taskFilter.idGreaterThan(lastTaskId);
    }
    GatewayRO gw(socialTxn_);
    const auto newReviewTasksIds = gw.taskIdsByFilter(taskFilter);
    if (newReviewTasksIds.empty()) {
        throw NothingToReview() <<
            "Region: " << regionId <<
            " doesn't contain open tasks from [" <<
            common::join(REVIEW_TASK_SOURCES, ", ") << "]";
    }
    Review newReview(0, ReviewState::Draft, author, reviewee, regionId, regionCommitId);
    for (const auto taskId : newReviewTasksIds) {
        newReview.setTaskComment(taskId, {}, std::nullopt);
    }
    newReview = saveReview(newReview);
    return newReview;
}

namespace {

TId updateReview(const Review& review, pqxx::transaction_base& socialTxn)
{
    ASSERT(review.id());
    std::string updateQuery =
        "UPDATE social.feedback_review SET"
        " review_comment = " + socialTxn.quote(review.comment());
    if (review.state() == ReviewState::Published) {
        updateQuery +=
            ", published_at = now()"
            ", report_data = " + socialTxn.quote(review.reportData());
    }
    updateQuery += " WHERE review_id = " + std::to_string(review.id());
    socialTxn.exec(updateQuery);
    saveTaskComments(review.id(), review.tasksComments(), socialTxn);
    return review.id();
}

TId initReview(const Review& review, pqxx::transaction_base& socialTxn)
{
    ASSERT(!review.id());
    std::string insertQuery =
        "INSERT INTO social.feedback_review (created_by, reviewee, region_id, region_commit_id)"
        "VALUES "
        "(" + std::to_string(review.createdBy()) +
        ", " + std::to_string(review.reviewee()) +
        ", " + std::to_string(review.regionId()) +
        "," + std::to_string(review.regionCommitId()) + ")"
        " RETURNING review_id";
    const auto result = socialTxn.exec(insertQuery);
    ASSERT(result.size() == 1);
    const auto newId = result[0][0].as<TId>();
    saveTaskComments(newId, review.tasksComments(), socialTxn);
    return newId;
}

} // namespace

Review
ReviewsGateway::saveReview(const Review& review)
{
    return getReview(review.id()
            ? updateReview(review, socialTxn_)
            : initReview(review, socialTxn_));
}

Review
ReviewsGateway::getReview(TId reviewId)
{
    const auto rows = socialTxn_.exec(
        "SELECT * FROM social.feedback_review WHERE review_id = " +
        std::to_string(reviewId));
    if (rows.empty()) {
        throw ReviewDoesntExist() << "Review: " << reviewId << " doesn't exists.";
    }
    ASSERT(rows.size() == 1);
    const auto row = rows[0];
    Review review(row);
    const auto taskRows = socialTxn_.exec(
        "SELECT task_id, task_comment, topic FROM social.feedback_review_task WHERE review_id = " +
        std::to_string(reviewId));
    for (const auto& taskRow : taskRows) {
        review.setTaskComment(
            taskRow[0].as<TId>(),
            taskRow[1].as<std::string>(),
            taskRow[2].is_null()
                ? std::optional<ReviewTaskComment::Topic>()
                : std::optional<ReviewTaskComment::Topic>(
                    enum_io::fromString<ReviewTaskComment::Topic>(taskRow[2].as<std::string>())));
    }
    return review;
}

std::vector<ReviewBrief>
ReviewsGateway::getRecentRegionReviews(TId regionId, size_t limit)
{
    const auto rows = socialTxn_.exec(
        "SELECT * FROM social.feedback_review WHERE region_id = " +
        std::to_string(regionId) +
        " ORDER BY review_id DESC " +
        (limit ? ("LIMIT " + std::to_string(limit)) : std::string()));
    std::vector<ReviewBrief> reviews;
    reviews.reserve(limit);
    for (const auto& row : rows) {
        ReviewBrief review(row);
        reviews.emplace_back(std::move(review));
    }
    return reviews;
}

void
ReviewsGateway::deleteReview(TId reviewId)
{
    const std::string reviewIdStr = std::to_string(reviewId);
    socialTxn_.exec(
        "DELETE FROM social.feedback_review_task WHERE review_id = " + reviewIdStr +
        "; DELETE FROM social.feedback_review WHERE review_id = " + reviewIdStr);
}

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