#include "review_utils.h"
#include "serialize.h"

#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/serialization.h>
#include <yandex/maps/wiki/revision/objectrevision.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <yandex/maps/wiki/revision/snapshot.h>
#include <yandex/maps/wiki/social/feedback/gateway_ro.h>
#include <maps/wikimap/mapspro/services/social/src/libs/yacare/error.h>
#include <maps/wikimap/mapspro/libs/acl_utils/include/feedback.h>

#include <maps/libs/json/include/exception.h>
#include <maps/libs/json/include/value.h>
#include <maps/infra/yacare/include/error.h>

using namespace maps::wiki::social::feedback;

namespace maps::wiki::socialsrv {

namespace {

const std::string REGION_CATEGORY = "mrc_pedestrian_region";
const std::string REGION_CATEGORY_LABEL = "{{categories:mrc_pedestrian_region}}";
const std::string REGION_CANONICAL_CATEGORY = "cat:" + REGION_CATEGORY;
const std::string REGION_NAME_ATTRIBUTE = REGION_CATEGORY + ":name";
const std::string REGION_ASSIGNEE_UID_ATTRIBUTE = REGION_CATEGORY + ":pedestrian_login_uid";
const std::string REGION_ASSIGNEE_LOGIN_ATTRIBUTE = REGION_CATEGORY + ":pedestrian_login";

const std::string REL_ROLE = "rel:role";
const std::string ROLE_MRC_PEDESTRIAN_REGION_ASSOCIATED_WITH = "mrc_pedestrian_region_associated_with";
const std::string ROLE_OFFICIAL = "official";
const std::string ATTR_AD_NM_NAME = "ad_nm:name";
const std::string ATTR_AD_NM_LANG = "ad_nm:lang";
const std::string ATTR_AD_NM_IS_LOCAL = "ad_nm:is_local";

TId
getParentAdId(
    const revision::Snapshot& snapshot,
    TId regionId)
{
    const auto parentAdRelations = snapshot.loadMasterRelations({regionId},
        revision::filters::Attr(REL_ROLE) == ROLE_MRC_PEDESTRIAN_REGION_ASSOCIATED_WITH &&
        revision::filters::ObjRevAttr::isNotDeleted());
    if (parentAdRelations.empty()) {
        return 0;
    }
    return parentAdRelations.front().data().relationData->masterObjectId();
}

std::string
getAdNameValue(
    const revision::Snapshot& snapshot,
    const revision::Revisions& nameRelations,
    bool needLocal)
{
    for (const auto& nameRelation : nameRelations) {
        const auto nameId = nameRelation.data().relationData->slaveObjectId();
        ASSERT(nameId);
        const auto nameRev = snapshot.objectRevision(nameId);
        ASSERT(nameRev);
        const auto& nameAttrs = *(nameRev->data().attributes);
        ASSERT(nameAttrs.contains(ATTR_AD_NM_LANG) && nameAttrs.contains(ATTR_AD_NM_NAME));
        if (!needLocal || nameAttrs.contains(ATTR_AD_NM_IS_LOCAL)) {
            return nameAttrs.at(ATTR_AD_NM_NAME);
        }
    }
    return {};
}

std::string
getParentAdName(
    const revision::Snapshot& snapshot,
    TId regionId)
{
    const auto parentAdId = getParentAdId(snapshot, regionId);
    if (!parentAdId) {
        return {};
    }

    const auto nameRelations = snapshot.loadSlaveRelations({parentAdId},
        revision::filters::Attr(REL_ROLE) == ROLE_OFFICIAL &&
        revision::filters::ObjRevAttr::isNotDeleted());

    std::string result = getAdNameValue(snapshot, nameRelations, true /* needLocal */);
    if (!result.empty()) {
        return result;
    }
    return getAdNameValue(snapshot, nameRelations, false /* needLocal */);
}

feedback::Tasks getReviewTasks(
    const feedback::Review& review,
    pqxx::transaction_base& socialReadTxn,
    IncludeTasksWithoutComments includeWithoutComments)
{
    const auto& tasksComments = review.tasksComments();
    TIds tasksIds;
    for (const auto& [taskId, taskComment] : tasksComments) {
        if (includeWithoutComments == IncludeTasksWithoutComments::Yes || taskComment.topic) {
            tasksIds.insert(taskId);
        }
    }
    GatewayRO gatewayRo(socialReadTxn);
    return gatewayRo.tasksByIds(tasksIds);
}

serialize::TasksForReviewUI
orderedReviewTasks(
    const Review& review,
    pqxx::transaction_base& socialReadTxn,
    IncludeTasksWithoutComments includeWithoutComments)
{
    serialize::TasksForReviewUI result;
    auto tasks = getReviewTasks(review, socialReadTxn, includeWithoutComments);
    result.reserve(tasks.size());
    for (const auto& task : tasks) {
        result.emplace_back(std::move(task));
    }
    auto lessByTypeAndId =
        [](
            const serialize::TaskForReviewUI& t1,
            const serialize::TaskForReviewUI& t2)
    {
        return std::make_tuple(t1.type(), t1.id()) <
            std::make_tuple(t2.type(), t2.id());
    };
    std::sort(result.begin(), result.end(), lessByTypeAndId);
    return result;
}

} // namespace

RegionData
getRegionData(
    pqxx::transaction_base& coreTxn,
    TId id,
    TId commitId)
{
    revision::RevisionsGateway revGateway(coreTxn);
    if (!commitId) {
        commitId = revGateway.headCommitId();
    }
    const auto snapshot = revGateway.snapshot(commitId);
    const auto objRev = snapshot.objectRevision(id);
    REQUIRE(objRev,
        yacare::errors::BadRequest() << "Can't get " <<
        REGION_CATEGORY << " at revision " << id << ":" << commitId);
    const auto& data = objRev->data();
    ASSERT(data.attributes && data.geometry);
    const auto& attrs = *data.attributes;
    REQUIRE(attrs.contains(REGION_CANONICAL_CATEGORY),
        yacare::errors::BadRequest() << id <<
        " Is not " << REGION_CATEGORY);
    REQUIRE(
        attrs.contains(REGION_ASSIGNEE_UID_ATTRIBUTE) &&
        attrs.contains(REGION_ASSIGNEE_LOGIN_ATTRIBUTE),
        yacare::errors::BadRequest() << id << " Region assignee not set.");

    auto title = attrs.contains(REGION_NAME_ATTRIBUTE)
        ? attrs.at(REGION_NAME_ATTRIBUTE)
        : REGION_CATEGORY_LABEL;
    title += " (" + std::to_string(id);
    const auto parentAdName = getParentAdName(snapshot, id);
    if (!parentAdName.empty()) {
        title += ", " + parentAdName;
    }
    title += ")";

    return RegionData {
        .id = objRev->id().objectId(),
        .commitId = commitId,
        .categoryId = REGION_CATEGORY,
        .title = title,
        .pedestrianUid = std::atoll(attrs.at(REGION_ASSIGNEE_UID_ATTRIBUTE).c_str()),
        .pedestrianLogin = attrs.at(REGION_ASSIGNEE_LOGIN_ATTRIBUTE),
        .polygon = geolib3::WKB::read<geolib3::Polygon2>(*data.geometry)
    };
}

ReviewExternData
getReviewExternData(const Review& review,
    pqxx::transaction_base& coreReadTxn,
    pqxx::transaction_base& socialReadTxn,
    IncludeTasksWithoutComments includeWithoutComments)
{
    return {
        .region = getRegionData(coreReadTxn, review.regionId(), review.regionCommitId()),
        .tasks = orderedReviewTasks(review, socialReadTxn, includeWithoutComments)
    };
}

void updateReviewFromJson(Review& review, const std::string& updateReviewJson)
{
    try {
        const auto parsedJson = maps::json::Value::fromString(updateReviewJson);
        review.setComment(parsedJson["comment"].toString());
        const auto oldComments = review.tasksComments();
        for (const auto& taskCommentJson : parsedJson["items"]) {
            const auto taskId = std::atoll(taskCommentJson["taskId"].toString().c_str());
            REQUIRE(taskId,
                yacare::errors::BadRequest() << "Can't parse taskId");
            REQUIRE(oldComments.contains(taskId),
                yacare::errors::BadRequest() << "Adding tasks to existing review not supported.");
            review.setTaskComment(
                taskId,
                taskCommentJson["comment"].toString(),
                taskCommentJson["topic"].isNull()
                    ? std::optional<ReviewTaskComment::Topic>()
                    : std::optional<ReviewTaskComment::Topic>(
                        enum_io::fromString<ReviewTaskComment::Topic>(taskCommentJson["topic"].toString())));
        }
    } catch (const maps::json::Error& ex) {
        throw yacare::errors::BadRequest() << ex.what();
    }
}

} // namespace maps::wiki::socialsrv
