#include "create.h"
#include "logging.h"

#include <maps/wikimap/mapspro/services/editor/src/acl_utils.h>
#include <maps/wikimap/mapspro/services/editor/src/branch_helpers.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/categories_strings.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/rate_limiter.h>
#include <maps/wikimap/mapspro/services/editor/src/commit.h>
#include <maps/wikimap/mapspro/services/editor/src/exception.h>
#include <maps/wikimap/mapspro/services/editor/src/objects_cache.h>
#include <maps/wikimap/mapspro/services/editor/src/revision_meta/common.h>
#include <maps/wikimap/mapspro/services/editor/src/social_utils.h>
#include <maps/libs/json/include/value.h>
#include <maps/wikimap/mapspro/libs/acl_utils/include/moderation.h>

#include <maps/wikimap/mapspro/libs/acl/include/policy.h>
#include <maps/wikimap/mapspro/libs/acl/include/restricted_users.h>
#include <maps/wikimap/mapspro/libs/views/include/query_builder.h>

#include <yandex/maps/wiki/configs/editor/category_groups.h>
#include <yandex/maps/wiki/social/gateway.h>
#include <yandex/maps/wiki/social/feedback/agent.h>
#include <yandex/maps/wiki/social/feedback/attribute_names.h>
#include <yandex/maps/wiki/social/feedback/description_producers.h>

#include <boost/lexical_cast.hpp>
#include <boost/optional.hpp>
#include <geos/geom/Point.h>
#include <map>

namespace maps::wiki {

namespace {

const std::string TASK_METHOD_NAME = "CommentsCreate";

const std::string STR_COMMENT_TYPE = "type";
const std::string STR_OBJECT_ID = "objectId";
const std::string STR_FEEDBACK_TASK_ID = "feedbackTaskId";
const std::string STR_COMMIT_ID = "commitId";
const std::string STR_DATA = "data";

const auto COMMENT_DATA_MAX_SIZE = 5000;

const CachePolicy CACHE_POLICY =
{
    TableAttributesLoadPolicy::Load,
    ServiceAttributesLoadPolicy::Load,
    DanglingRelationsPolicy::Ignore
};

struct FeedbackInfo {
    social::feedback::SuggestedAction suggestedAction;
    std::string source;
};

const std::map<social::CommentType, FeedbackInfo> COMMENT_TYPE_TO_FEEDBACK_INFO {
    {
        social::CommentType::Complaint,
        { social::feedback::SuggestedAction::Modify, "nmaps-complaint" }
    },
    {
        social::CommentType::RequestForDeletion,
        { social::feedback::SuggestedAction::Delete, "nmaps-request-for-deletion" }
    },
};

using StringToFeedbackType = std::map<std::string, social::feedback::Type>;

const std::map<std::string, StringToFeedbackType> CATEGORY_ATTR_TO_FEEDBACK_TYPES {
    {
        ATTR_COND_COND_TYPE, {
            { COND_TYPE_BARRIER, social::feedback::Type::Barrier },
        },
    },
};

const StringToFeedbackType CATEGORY_TO_FEEDBACK_TYPE {
    { CATEGORY_COND_ANNOTATION,         social::feedback::Type::RoadAnnotation },
    { CATEGORY_COND_ANNOTATION_PHRASE,  social::feedback::Type::RoadAnnotation },
    { CATEGORY_COND_DS,                 social::feedback::Type::RoadDirection },
    { CATEGORY_COND_CLOSURE,            social::feedback::Type::RoadClosure },
    { CATEGORY_COND_LANE,               social::feedback::Type::TrafficLaneSign },
    { CATEGORY_COND_TRAFFIC_LIGHT,      social::feedback::Type::TrafficLight },
    { CATEGORY_TRANSPORT_STOP,          social::feedback::Type::PublicTransportStop },
};

const StringToFeedbackType CATEGORY_GROUP_TO_FEEDBACK_TYPE {
    { CATEGORY_GROUP_ADDR,                      social::feedback::Type::Address },
    { CATEGORY_GROUP_BLD,                       social::feedback::Type::Building },
    { CATEGORY_GROUP_COND,                      social::feedback::Type::Maneuver },
    { CATEGORY_GROUP_ENTRANCE,                  social::feedback::Type::Entrance },
    { CATEGORY_GROUP_FENCE,                     social::feedback::Type::Fence },
    { CATEGORY_GROUP_INDOOR,                    social::feedback::Type::Indoor },
    { CATEGORY_GROUP_PARKING,                   social::feedback::Type::Parking },
    { CATEGORY_GROUP_POI,                       social::feedback::Type::Poi },
    { CATEGORY_GROUP_RD,                        social::feedback::Type::Road },
    { CATEGORY_GROUP_TRANSPORT,                 social::feedback::Type::MtRoute },
    { CATEGORY_GROUP_TRANSPORT_AIRPORT,         social::feedback::Type::MtRoute },
    { CATEGORY_GROUP_TRANSPORT_METRO,           social::feedback::Type::MtRoute },
    { CATEGORY_GROUP_TRANSPORT_RAILWAY,         social::feedback::Type::MtRoute },
    { CATEGORY_GROUP_TRANSPORT_WATERWAY,        social::feedback::Type::MtRoute },
};


template <typename Type>
Type
loadFromJsonValue(
    const json::Value& jsonObject, const std::string& key, Type defValue)
{
    if (!jsonObject.hasField(key)) {
        return defValue;
    }

    const auto& jsonValue = jsonObject[key];
    return boost::lexical_cast<Type>(jsonValue.toString());
}

template <typename Type>
std::optional<Type>
loadOptionalFromJsonValue(
    const json::Value& jsonObject, const std::string& key)
{
    if (!jsonObject.hasField(key)) {
        return std::nullopt;
    }

    const auto& jsonValue = jsonObject[key];
    return boost::lexical_cast<Type>(jsonValue.toString());
}

social::Comment::Internal
isInternalComment(social::CommentType type, const std::string& moderationStatus)
{
    if (type != social::CommentType::Complaint &&
        type != social::CommentType::Annotation)
    {
        return social::Comment::Internal::No;
    }

    if (acl_utils::isYandexModerator(moderationStatus) ||
        acl_utils::isCartographer(moderationStatus))
    {
        return social::Comment::Internal::Yes;
    }

    return social::Comment::Internal::No;
}

geolib3::Point2 calculateCenterByMinimalAoi(BranchContext& branchCtx, const TOIds& aoiIds)
{
    ASSERT(!aoiIds.empty());

    views::QueryBuilder qb(branchCtx.branch.id());
    qb.selectFields("ST_X(ST_Centroid(the_geom)), ST_Y(ST_Centroid(the_geom))");
    enum { FIELD_X, FIELD_Y };

    qb.fromTable(views::TABLE_OBJECTS_A, "a");
    auto aoiIdsStr = common::join(aoiIds, ',');
    qb.whereClause("id IN (" + aoiIdsStr + ")");
    auto query = qb.query() + " ORDER BY area ASC LIMIT 1";

    auto rows = branchCtx.txnView().exec(query);
    REQUIRE(!rows.empty(), "Can not calculate center by aoi: " << aoiIdsStr);
    const auto& row = rows[0];
    return { row[FIELD_X].as<double>(), row[FIELD_Y].as<double>() };
}

geolib3::Point2 calculateCenter(
    BranchContext& branchCtx,
    const geos::geom::Envelope& envelope,
    const common::Geom& center,
    const TOIds& aoiIds)
{
    if (!center.isNull()) {
        auto point =
            dynamic_cast<const geos::geom::Point*>(center.geosGeometryPtr());
        ASSERT(point);
        return { point->getX(), point->getY() };
    }
    if (!envelope.isNull()) {
        return {(envelope.getMinX() + envelope.getMaxX()) / 2,
                (envelope.getMinY() + envelope.getMaxY()) / 2};
    }
    return calculateCenterByMinimalAoi(branchCtx, aoiIds);
}

social::feedback::Type calculateFeedbackType(const GeoObject& object)
{
    const auto& attributes = object.attributes();
    for (const auto& [attrId, data] : CATEGORY_ATTR_TO_FEEDBACK_TYPES) {
        for (const auto& attrValue : attributes.values(attrId)) {
            auto it = data.find(attrValue);
            if (it != data.end()) {
                return it->second;
            }
        }
    }

    const auto& categoryId = object.category().id();
    auto it = CATEGORY_TO_FEEDBACK_TYPE.find(categoryId);
    if (it != CATEGORY_TO_FEEDBACK_TYPE.end()) {
        return it->second;
    }

    auto group = cfg()->editor()->categoryGroups().findGroupByCategoryId(categoryId);
    if (group) {
        auto it = CATEGORY_GROUP_TO_FEEDBACK_TYPE.find(group->id());
        if (it != CATEGORY_GROUP_TO_FEEDBACK_TYPE.end()) {
            return it->second;
        }
    }
    return social::feedback::Type::Other;
}

void createFeedbackTask(
    BranchContext& branchCtx,
    const CommentsCreate::Request& request,
    const FeedbackInfo& feedbackInfo,
    TCommitId commitId,
    const GeoObject& object,
    const common::Geom& center,
    const geos::geom::Envelope& envelope,
    const TOIds& aoiIds)
{
    WIKI_REQUIRE(
        !aoiIds.empty(), ERR_FORBIDDEN,
        "Can not create feedback without aoi,"
        " uid: " << request.userId());

    social::feedback::TaskNew task(
        calculateCenter(branchCtx, envelope, center, aoiIds),
        calculateFeedbackType(object),
        feedbackInfo.source,
        social::feedback::SuggestedActionDescr{feedbackInfo.suggestedAction}.toDescription());

    task.attrs.addCustom(
        social::feedback::attrs::SUGGESTED_ACTION,
        std::string(toString(feedbackInfo.suggestedAction)));

    task.hidden = true;
    task.internalContent = false;
    task.objectId = object.id();
    task.attrs.addCustom(social::feedback::attrs::COMMIT_ID, std::to_string(commitId));
    task.attrs.addCustom(social::feedback::attrs::USER_DATA_COMMENT, request.data);

    social::feedback::Agent agent(branchCtx.txnSocial(), request.userId());
    auto createdTask = agent.addTask(task);
    DEBUG() << "Created feedback task, id: " << createdTask.id();
}

void checkPermisisonToAddComment(
    social::CommentType type,
    TUid commentBy,
    TUid commitBy,
    pqxx::transaction_base& txn)
{
    if (type != social::CommentType::Annotation) {
        return;
    }
    WIKI_REQUIRE(
        commentBy == commitBy ||
        CheckPermissions(commentBy, txn).canUserAnnotateOtherUserCommit(),
        ERR_FORBIDDEN,
        "User can not comment commit of the other user: " << commitBy);
}

} // namespace

CommentsCreate::Request::Request(
        UserContext userContext,
        social::CommentType type,
        const std::string& data,
        TCommitId commitId,
        TOid objectId,
        std::optional<TId> feedbackTaskId)
    : userContext(std::move(userContext))
    , type(type)
    , data(data)
    , commitId(commitId)
    , objectId(objectId)
    , feedbackTaskId(feedbackTaskId)
{
    CHECK_NON_EMPTY_REQUEST_PARAM(data);

    WIKI_REQUIRE(
        objectId || feedbackTaskId || commitId,
        ERR_BAD_REQUEST,
        "Invalid request parameters: at least one of " <<
        STR_OBJECT_ID << " / " << STR_FEEDBACK_TASK_ID << " / "
        << STR_COMMIT_ID << " is required."
    );

    WIKI_REQUIRE(
        data.size() <= COMMENT_DATA_MAX_SIZE,
        ERR_COMMENT_SIZE_LIMIT_EXCEEDED,
        "Comment size limit exceeded, size: " << data.size();
    );
}

CommentsCreate::Request
CommentsCreate::Request::parseJson(UserContext userContext, const std::string& json)
{
    auto jsonObject = maps::json::Value::fromString(json);
    WIKI_REQUIRE(jsonObject.isObject(), ERR_BAD_REQUEST, "json body is not object");

    const auto& jsonType = jsonObject[STR_COMMENT_TYPE];
    auto commentType = boost::lexical_cast<social::CommentType>(jsonType.toString());

    const auto& jsonData = jsonObject[STR_DATA];
    WIKI_REQUIRE(jsonData.exists(), ERR_BAD_REQUEST, "Comment data not exists");

    return {
        std::move(userContext),
        commentType,
        jsonData.toString(),
        loadFromJsonValue<TCommitId>(jsonObject, STR_COMMIT_ID, 0),
        loadFromJsonValue<TOid>(jsonObject, STR_OBJECT_ID, 0),
        loadOptionalFromJsonValue<TId>(jsonObject, STR_FEEDBACK_TASK_ID)
    };
}

std::string
CommentsCreate::Request::dump() const
{
    std::stringstream ss;
    ss << " uid: " << userId()
       << " type: " << type
       << " commit-id: " << commitId
       << " object-id: " << objectId;
    if (feedbackTaskId){
        ss << " feedback-task-id: " << *feedbackTaskId;
    }
    return ss.str();
}


CommentsCreate::CommentsCreate(
        const ObserverCollection&,
        const Request& request,
        taskutils::TaskID asyncTaskID)
    : controller::BaseController<CommentsCreate>(BOOST_CURRENT_FUNCTION, asyncTaskID)
    , request_(request)
{}

std::string
CommentsCreate::printRequest() const
{
    return request_.dump();
}

void
CommentsCreate::control()
{
    auto branchCtx =
        CheckedTrunkBranchContextFacade().acquireWrite();

    WIKI_REQUIRE(
        !acl::isUserRestricted(branchCtx.txnCore(), request_.userId()),
        ERR_FORBIDDEN,
        "User " << request_.userId() << " is restricted");

    const auto& user = request_.userContext.aclUser(branchCtx);
    user.checkActiveStatus();

    const auto& moderationStatus = request_.userContext.moderationStatus(branchCtx);

    cfg()->rateLimiter().checkUserActivityAndRestrict(
        branchCtx,
        request_.userId(),
        social::ActivityType::Comments,
        moderationStatus);

    if (!request_.objectId) {
        if (request_.commitId) {
            auto commit = revision::Commit::load(
                branchCtx.txnCore(), request_.commitId);

            checkPermisisonToAddComment(
                request_.type,
                request_.userId(),
                commit.createdBy(),
                branchCtx.txnCore());
        }

        createComment(branchCtx, moderationStatus, request_.commitId, {});
        return;
    }

    ObjectsCache cache(
        branchCtx,
        request_.commitId
            ? boost::optional<TCommitId>(request_.commitId)
            : boost::optional<TCommitId>(),
        CACHE_POLICY
    );
    auto objectPtr = cache.getExisting(request_.objectId);
    auto envelope = objectPtr->envelope();
    auto center = objectPtr->center();

    if (request_.commitId && envelope.isNull()) {
        ObjectsCache cache(
            branchCtx, boost::none, REVISION_META_CACHE_POLICY);
        auto objectPtr = cache.getExisting(request_.objectId);
        envelope = objectPtr->envelope();
        if (center.isNull()) {
            center = objectPtr->center();
        }
    }

    auto loadCommit = [&] {
        if (!request_.commitId) {
            return recentAffectingCommit(cache, objectPtr->revision());
        }

        auto commit = revision::Commit::load(branchCtx.txnCore(), request_.commitId);

        WIKI_REQUIRE(
            commit.inTrunk(), ERR_INVALID_OPERATION,
            "Can not create comment for non-trunk commit id " << commit.id());
        checkPermisisonToAddComment(
            request_.type,
            request_.userId(),
            commit.createdBy(),
            branchCtx.txnCore());

        return commit;
    };
    auto commit = loadCommit();

    auto aoiIds = getAoiIds(branchCtx, {objectPtr}, envelope);

    auto it = COMMENT_TYPE_TO_FEEDBACK_INFO.find(request_.type);
    if (it != COMMENT_TYPE_TO_FEEDBACK_INFO.end()) {
        createFeedbackTask(
            branchCtx, request_, it->second,
            commit.id(), *objectPtr, center, envelope, aoiIds);
    }
    createComment(branchCtx, moderationStatus, commit.id(), aoiIds);
}

void
CommentsCreate::createComment(
    BranchContext& branchCtx,
    const std::string& moderationStatus,
    TCommitId commitId,
    const TOIds& aoiIds)
{
    social::Gateway socialGateway(branchCtx.txnSocial());
    result_->comment = socialGateway.createComment(
        request_.userId(), request_.type, request_.data,
        commitId, request_.objectId, request_.feedbackTaskId, aoiIds,
        isInternalComment(request_.type, moderationStatus));

    if (request_.feedbackTaskId) {
        social::feedback::Agent agent(branchCtx.txnSocial(), request_.userId());
        agent.commentTask(
            request_.feedbackTaskId.value(),
            result_->comment->id());
    }

    finalizeComment(branchCtx);
}

void
CommentsCreate::finalizeComment(BranchContext& branchCtx)
{
    ASSERT(result_->comment);

    request_.userContext.saveActivity(
        branchCtx, social::UserActivityAction::CreateComment, result_->comment->id());

    logCreateComment(*result_->comment);

    // view txn used for read data only
    // skip creating section "trunk" on build compound token
    branchCtx.releaseViewTxn();
    result_->token = branchCtx.commit();
}

const std::string&
CommentsCreate::taskName()
{
    return TASK_METHOD_NAME;
}

} // namespace maps::wiki
