#include <maps/wikimap/mapspro/services/social/src/api/globals.h>

#include <maps/wikimap/mapspro/services/social/src/libs/common/common.h>
#include <maps/wikimap/mapspro/services/social/src/libs/feedback/common.h>
#include <maps/wikimap/mapspro/services/social/src/libs/feedback/serialize.h>
#include <maps/wikimap/mapspro/services/social/src/libs/feedback/user_processing_level.h>
#include <maps/wikimap/mapspro/services/social/src/libs/yacare/helpers.h>

#include <maps/wikimap/mapspro/services/social/src/libs/feedback-actions/find_nearby.h>
#include <maps/wikimap/mapspro/services/social/src/libs/feedback-actions/meta.h>
#include <maps/wikimap/mapspro/services/social/src/libs/feedback-actions/tasks_feed.h>
#include <maps/wikimap/mapspro/services/social/src/libs/feedback-actions/tasks_from_json.h>
#include <maps/wikimap/mapspro/services/social/src/libs/feedback-actions/tasks_get.h>
#include <maps/wikimap/mapspro/services/social/src/libs/feedback-actions/tasks_helpers.h>
#include <maps/wikimap/mapspro/services/social/src/libs/feedback-actions/tasks_resolve.h>

#include <maps/wikimap/mapspro/libs/acl_utils/include/feedback.h>
#include <maps/wikimap/mapspro/libs/acl_utils/include/user_feed_access.h>

#include <yandex/maps/wiki/common/aoi.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/social/feedback/agent.h>
#include <yandex/maps/wiki/social/feedback/enums.h>
#include <yandex/maps/wiki/social/feedback/gateway_rw.h>
#include <yandex/maps/wiki/social/feedback/mv_source_type.h>
#include <yandex/maps/wiki/social/feedback/task.h>
#include <yandex/maps/wiki/social/feedback/task_filter.h>
#include <yandex/maps/wiki/social/feedback/task_patch.h>
#include <yandex/maps/wiki/social/gateway.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/common/include/profiletimer.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/log8/include/log8.h>

#include <boost/lexical_cast.hpp>

namespace maps::wiki::socialsrv {

namespace mws = social;
namespace mwsf = social::feedback;

namespace {

std::optional<mwsf::DateAndId> optionalDateAndId(const std::optional<std::string>& str)
{
    if (!str) {
        return std::nullopt;
    }
    auto splittedString = common::split(*str, ",");
    try {
        return mwsf::DateAndId(
            maps::chrono::parseIsoDateTime(splittedString.at(0)),
            boost::lexical_cast<social::TId>(splittedString.at(1))
        );
    } catch (const std::exception& e) {
        throw yacare::errors::BadRequest()
            << "Invalid parameter value: '" << *str << "'. ERROR: " << e.what();
    }
}

void makeResponseByUpdatedTask(
    yacare::Response& response,
    const serialize::TaskExtended& taskExtended,
    const std::string& token,
    mws::TUid uid)
{
    makeJsonResponse(response, toJson(taskExtended, token, uid));
}

void validateNeedInfoRequestTemplate(const std::string& requestTemplate)
{
    static const std::set<std::string> VALID_REQUEST_TEMPLATES {
        "toponym-need-proof",
        "need-full-address",
        "obstacle-need-proof",
        "remove-need-proof",
        "need-more-info"
    };
    REQUIRE(VALID_REQUEST_TEMPLATES.count(requestTemplate), yacare::errors::BadRequest());
}

yacare::ThreadPool feedbackTasksThreadPool("feedbackTasksThreadPool", 2, 2048);
yacare::ThreadPool feedbackTasksHeavyThreadPool("feedbackTasksHeavyThreadPool", 3, 2048);

} // namespace

YCR_USE(feedbackTasksHeavyThreadPool) {

YCR_RESPOND_TO("PUT /feedback/tasks-feed", uid, token = "")
{
    ProfileTimer timer;
    const auto bodyJson = json::Value::fromString(request.body());
    auto createdBy = parseOptional<mws::TUid>(bodyJson["createdBy"]);
    if (createdBy) {
        SOCIAL_REQUIRE(
            *createdBy == uid,
            Forbidden,
            "User with uid " << uid << " is not allowed to view "
                << "feedback feed of user " << *createdBy);
    }

    TasksFeedParams params;
    params.beforeId = queryParam<ID>(request, "before", 0);
    params.afterId = queryParam<ID>(request, "after", 0);
    params.perPage = queryParam<size_t>(request, "per-page", params.perPage);

    params.hidden = parseOptional<bool>(bodyJson["hidden"]);
    params.status = parseOptional<mwsf::UIFilterStatus>(bodyJson["status"]);
    params.resolved = parseOptional<bool>(bodyJson["resolved"]);
    params.types = parseOptionalVector<mwsf::Type>(bodyJson["types"]);
    params.objectId = parseOptional<mws::TId>(bodyJson["objectId"]);
    params.bboxGeo = parseOptional<std::string>(bodyJson["bboxGeo"]);
    params.feedRegionId = parseOptional<mws::TId>(bodyJson["feedRegionId"]);
    params.createdBy = createdBy;
    params.acquiredBy = parseOptional<mws::TUid>(bodyJson["acquiredBy"]);
    params.resolvedBy = parseOptional<mws::TUid>(bodyJson["resolvedBy"]);
    params.resolvedByRobots = parse<bool>(bodyJson["resolvedByRobots"], params.resolvedByRobots);
    params.sources = parseOptionalVector<std::string>(bodyJson["sources"]);
    params.workflows = parseOptionalVector<mwsf::Workflow>(bodyJson["workflows"]);
    params.duplicateHeadId = parseOptional<mws::TId>(bodyJson["duplicateHeadId"]);

    auto feedForUI = getTasksFeed(
        Globals::dbPools(),
        Globals::feedbackChecker(),
        params,
        uid,
        token);

    makeJsonResponse(response, toJson(feedForUI, uid));

    auto elapsed = timer.getElapsedTimeNumber();
    if (elapsed > 1) {
        WARN() << "Slow request /feedback/tasks-feed uid: " << uid << " body: " << request.body();
    }
}

} // YCR_USE(feedbackTasksHeavyThreadPool)


YCR_USE(feedbackTasksThreadPool) {

YCR_RESPOND_TO("GET /feedback/tasks/personal/$", uid, token = "")
{
    auto coreTxn = Globals::dbPools().coreReadTxn();
    auto socialTxn = Globals::dbPools().socialReadTxn(token);
    mwsf::GatewayRO gatewayRo(*socialTxn);

    auto modifiedBy = positionalParam<acl::UID>(argv, 0);
    auto before = optionalDateAndId(optionalQueryParam<std::string>(request, "before"));
    auto after = optionalDateAndId(optionalQueryParam<std::string>(request, "after"));
    auto perPage = queryParam<size_t>(request, "per-page", 10);

    // ACL
    //
    if (!acl_utils::isUserAllowedToReadFeed(*coreTxn, uid, modifiedBy)) {
        throw Error(Error::Status::Forbidden)
            << "User with uid " << uid << " is not allowed to view "
            << "feedback feed of user " << modifiedBy;
    }

    // Construct task filter
    //
    mwsf::TaskFilter filter;
    filter.buckets(mwsf::allRevealedBuckets());
    filter.modifiedBy(modifiedBy);

    // Get tasks feed
    //
    mwsf::TaskFeedParamsDateAndId feedParams(before, after, perPage);
    auto tasksFeed = gatewayRo.tasksFeed(filter, feedParams);

    auto feedForUI = makeFeedForUI(
        gatewayRo,
        std::move(tasksFeed),
        getSubstitutionStrategy(Globals::aclChecker(), uid));

    makeJsonResponse(response, toJson(feedForUI, uid));
}

YCR_RESPOND_TO("GET /feedback/tasks/$", uid, token = "")
{
    auto taskId = positionalParam<social::TId>(argv, 0);

    auto taskExtended = getTask(
        Globals::dbPools(),
        Globals::feedbackChecker(),
        taskId,
        uid,
        token);

    findNearbyObject(Globals::dbPools(), taskExtended.taskForUI);

    makeJsonResponse(response, toJson(taskExtended, uid));
}

YCR_RESPOND_TO("POST /feedback/tasks/$/mark-viewed", uid)
{
    auto taskId = positionalParam<uint64_t>(argv, 0);

    auto writeContext = Globals::dbPools().writeContext();
    auto& socialWriteTxn = writeContext.socialTxn();
    auto& coreTxn = writeContext.coreTxn();
    mwsf::Agent agent(socialWriteTxn, uid);

    auto task = getTaskForUpdate(agent, taskId);
    checkPermissionForTask(
        Globals::feedbackChecker(),
        uid, task,
        agent.gatewayRo().history(taskId), mwsf::TaskOperation::MarkViewed);

    auto taskExtended = makeTaskExtended(
        agent.markViewedTask(task),
        Globals::feedbackChecker(),
        coreTxn,
        socialWriteTxn,
        uid);

    auto token = writeContext.commit();
    makeResponseByUpdatedTask(response, taskExtended, token, uid);
}

YCR_RESPOND_TO("POST /feedback/tasks", uid)
{
    auto body = json::Value::fromString(request.body());

    auto newTask = createNewTaskFromJson(body);

    auto result = postNewTask(Globals::dbPools(), newTask);
    makeResponseByUpdatedTask(response, result.taskExt, result.token, uid);
}

YCR_RESPOND_TO("POST /feedback/tasks/$/acquire", uid)
{
    auto taskId = positionalParam<uint64_t>(argv, 0);

    auto writeContext = Globals::dbPools().writeContext();
    auto& coreTxn = writeContext.coreTxn();
    auto& socialTxn = writeContext.socialTxn();
    mwsf::Agent agent(socialTxn, uid);

    auto task = getTaskForUpdate(agent, taskId);
    checkPermissionForTask(Globals::feedbackChecker(), uid, task,
        agent.gatewayRo().history(taskId), mwsf::TaskOperation::Acquire);

    {
        auto acquiredTasks = agent.gatewayRo().tasksByFilter(
            mwsf::TaskFilter().acquiredBy(uid));

        SOCIAL_REQUIRE(
            acquiredTasks.empty(),
            Forbidden,
            "Another task has been already acquired"
        );
    }

    auto taskExtended = makeTaskExtended(
        agent.acquireTask(task),
        Globals::feedbackChecker(),
        coreTxn,
        socialTxn,
        uid);

    auto token = writeContext.commit();
    makeResponseByUpdatedTask(response, taskExtended, token, uid);
}

YCR_RESPOND_TO("POST /feedback/tasks/$/release", uid)
{
    auto taskId = positionalParam<uint64_t>(argv, 0);

    auto writeContext = Globals::dbPools().writeContext();
    auto& socialTxn = writeContext.socialTxn();
    auto& coreTxn = writeContext.coreTxn();
    mwsf::Agent agent(socialTxn, uid);

    auto task = getTaskForUpdate(agent, taskId);
    checkPermissionForTask(Globals::feedbackChecker(), uid, task,
        agent.gatewayRo().history(taskId), mwsf::TaskOperation::Release);

    auto taskExtended = makeTaskExtended(
        agent.releaseTask(task),
        Globals::feedbackChecker(),
        coreTxn,
        socialTxn,
        uid);

    auto token = writeContext.commit();
    makeResponseByUpdatedTask(response, taskExtended, token, uid);
}

YCR_RESPOND_TO("PATCH /feedback/tasks/$", uid)
{
    auto taskId = positionalParam<uint64_t>(argv, 0);

    auto writeContext = Globals::dbPools().writeContext();
    auto& socialTxn = writeContext.socialTxn();
    auto& coreTxn = writeContext.coreTxn();
    mwsf::Agent agent(socialTxn, uid);

    auto task = getTaskForUpdate(agent, taskId);
    auto requestBody = maps::json::Value::fromString(request.body());

    const auto& positionJson = requestBody[jsids::POSITION];
    if (positionJson.exists()) {
        auto newPositionMerc = geolib3::convertGeodeticToMercator(pointFromGeojson(positionJson));

        checkPermissionForTask(Globals::feedbackChecker(), uid, task,
            agent.gatewayRo().history(taskId), mwsf::TaskOperation::ChangePosition);

        auto txnViewTrunkRead = Globals::dbPools().viewTrunkTxn();
        auto aoiIds = common::calculateAoisContainingPosition(
            newPositionMerc, txnViewTrunkRead.get());

        auto updatedTask = agent.changeTaskPosition(task, newPositionMerc, aoiIds);
        requireExists(updatedTask);
        task = std::move(*updatedTask);
    }

    if (requestBody.hasField(jsids::TYPE)) {
        auto newType = enum_io::fromString<mwsf::Type>(
            requestBody[jsids::TYPE].as<std::string>());

        checkPermissionForTask(Globals::feedbackChecker(), uid, task,
            agent.gatewayRo().history(taskId), mwsf::TaskOperation::ChangeType);

        auto updatedTask = agent.changeTaskType(task, newType);
        requireExists(updatedTask);
        task = std::move(*updatedTask);
    }

    auto taskExtended = makeTaskExtended(
        std::move(task),
        Globals::feedbackChecker(),
        coreTxn,
        socialTxn,
        uid);

    auto token = writeContext.commit();
    makeResponseByUpdatedTask(response, taskExtended, token, uid);
}

YCR_RESPOND_TO("POST /feedback/tasks/$/resolve", uid)
{
    const auto taskId = positionalParam<social::TId>(argv, 0);
    const auto verdict = queryParam<mwsf::Verdict>(request, "resolution");
    const auto rejectReason = optionalQueryParam<mwsf::RejectReason>(request, "reject-reason");

    auto result = resolveTask(
        Globals::dbPools(), Globals::feedbackChecker(), Globals::socialRateLimiter(),
        taskId, uid, verdict, rejectReason, request.body());

    makeResponseByUpdatedTask(response, result.taskExt, result.token, uid);
}

YCR_RESPOND_TO("POST /feedback/tasks/$/open", uid)
{
    auto taskId = positionalParam<uint64_t>(argv, 0);

    auto writeContext = Globals::dbPools().writeContext();
    auto& socialTxn = writeContext.socialTxn();
    auto& coreTxn = writeContext.coreTxn();
    mwsf::Agent agent(socialTxn, uid);

    auto task = getTaskForUpdate(agent, taskId);
    checkPermissionForTask(Globals::feedbackChecker(), uid, task,
        agent.gatewayRo().history(taskId), mwsf::TaskOperation::Open);

    std::optional<social::TId> commentId;
    const auto& message = request.body();
    if (!message.empty()) {
        social::Gateway socialGtw(socialTxn);
        auto comment = socialGtw.createComment(
            uid, social::CommentType::Info, message, 0, 0, taskId, {});
        commentId = comment.id();
    }

    std::optional<mwsf::Task> modifiedTask;
    if (task.state() == mwsf::TaskState::Opened && task.processingLevel() > 0) {
        modifiedTask = agent.processingLevelDown(task);
    } else {
        modifiedTask = agent.openTask(task, commentId);
    }
    auto taskExtended = makeTaskExtended(
        std::move(modifiedTask), Globals::feedbackChecker(), coreTxn, socialTxn, uid);

    auto token = writeContext.commit();
    makeResponseByUpdatedTask(response, taskExtended, token, uid);
}

YCR_RESPOND_TO("POST /feedback/tasks/$/need-info", uid)
{
    auto taskId = positionalParam<uint64_t>(argv, 0);
    auto requestTemplate = optionalQueryParam<std::string>(request, "template");
    if (requestTemplate) {
        validateNeedInfoRequestTemplate(*requestTemplate);
    }

    const auto& message = request.body();
    REQUIRE(!message.empty(), yacare::errors::BadRequest() << "message in body required");

    auto writeContext = Globals::dbPools().writeContext();
    auto& socialTxn = writeContext.socialTxn();
    auto& coreTxn = writeContext.coreTxn();
    mwsf::Agent agent(socialTxn, uid);

    auto task = getTaskForUpdate(agent, taskId);
    checkPermissionForTask(Globals::feedbackChecker(), uid, task,
        agent.gatewayRo().history(taskId), mwsf::TaskOperation::NeedInfo);

    social::Gateway socialGtw(socialTxn);
    auto comment = socialGtw.createComment(
        uid, social::CommentType::Info, message, 0, 0, taskId, {});

    auto taskExtended = makeTaskExtended(
        agent.needInfoTask(task, comment.id(), requestTemplate),
        Globals::feedbackChecker(),
        coreTxn,
        socialTxn,
        uid);

    auto token = writeContext.commit();
    makeResponseByUpdatedTask(response, taskExtended, token, uid);
}

YCR_RESPOND_TO("POST /feedback/tasks/$/hide", uid)
{
    auto taskId = positionalParam<uint64_t>(argv, 0);

    auto writeContext = Globals::dbPools().writeContext();
    auto& coreTxn = writeContext.coreTxn();
    auto& socialTxn = writeContext.socialTxn();
    mwsf::Agent agent(socialTxn, uid);

    auto task = getTaskForUpdate(agent, taskId);
    checkPermissionForTask(Globals::feedbackChecker(), uid, task,
        agent.gatewayRo().history(taskId), mwsf::TaskOperation::Hide);

    auto taskExtended = makeTaskExtended(
        agent.hideTask(task),
        Globals::feedbackChecker(),
        coreTxn,
        socialTxn,
        uid);

    auto token = writeContext.commit();
    makeResponseByUpdatedTask(response, taskExtended, token, uid);
}

// @forbidden by Agent::validOperations();  NMAPS-13497
YCR_RESPOND_TO("POST /feedback/tasks/$/show", uid)
{
    auto taskId = positionalParam<uint64_t>(argv, 0);

    auto writeContext = Globals::dbPools().writeContext();
    auto& coreTxn = writeContext.coreTxn();
    mwsf::Agent agent(writeContext.socialTxn(), uid);

    auto task = getTaskForUpdate(agent, taskId);
    checkPermissionForTask(Globals::feedbackChecker(), uid, task,
        agent.gatewayRo().history(taskId), mwsf::TaskOperation::Show);

    auto taskExtended = makeTaskExtended(
        agent.showTask(task),
        Globals::feedbackChecker(),
        coreTxn,
        writeContext.socialTxn(),
        uid);

    auto token = writeContext.commit();
    makeResponseByUpdatedTask(response, taskExtended, token, uid);
}

YCR_RESPOND_TO("PUT /feedback/tasks/stats/opened", uid)
{
    Globals::aclChecker().checkPermission(uid, FEEDBACK_GROUP_REJECT);

    const auto jsonBody = json::Value::fromString(request.body());
    const auto polygon = [&]() {
        try {
            return geolib3::convertGeodeticToMercator(
                geolib3::readGeojson<geolib3::Polygon2>(jsonBody["polygon"]));
        } catch (std::exception& e) {
            throw yacare::errors::BadRequest()
                << "Error parsing polygon '" << request.body() << "': " << e.what();
        }
    }();

    auto filter = mwsf::TaskFilter().resolved(false).boundary(polygon);
    filter.types(parseOptionalVector<mwsf::Type>(jsonBody["types"]));
    filter.sources(parseOptionalVector<std::string>(jsonBody["sources"]));
    filter.workflows(parseOptionalVector<mwsf::Workflow>(jsonBody["workflows"]));

    auto socialTxnHandle = Globals::dbPools().socialReadTxn();
    mwsf::GatewayRO gatewayRo(*socialTxnHandle);
    const auto aggregationColumns =
        mwsf::Columns{mwsf::Column::Workflow, mwsf::Column::Source, mwsf::Column::Type};
    const auto counters =
        gatewayRo.getAggregatedCountOfTasks(filter, aggregationColumns);

    makeJsonResponse(response, toJson(counters));
}

YCR_RESPOND_TO("GET /feedback/tasks/stats/paid-operations", uid)
{
    Globals::aclChecker().checkPermission(uid, PAID_OPERATIONS_COUNTER_PERMISSION);

    const auto tzOffsetInMinutes = queryParam<int>(request, "tz");

    auto socialTxn = Globals::dbPools().socialReadTxn();
    auto socialGw = mwsf::GatewayRO(*socialTxn);

    PaidOperationCounters paidOperationCounters = {
        .today = socialGw.countOperationsToday(PAID_OPERATIONS, uid, tzOffsetInMinutes)
    };
    makeJsonResponse(response, toJson(paidOperationCounters));
}

namespace {

mwsf::Types feedbackTypesNoExcept(
    pqxx::transaction_base& txn,
    const mwsf::MvSourceTypeFilter& filter)
{
    const std::string errPrefix = "Unable to retrieve feedback types. ";

    try {
        return mwsf::mvTypesByFilter(txn, filter);
    } catch (const Exception& e) {
        ERROR() << errPrefix << e;
    } catch (const std::exception& e) {
        ERROR() << errPrefix << e.what();
    } catch (...) {
        ERROR() << errPrefix;
    }

    return {};
}

} // namespace anonymous

YCR_RESPOND_TO("PUT /feedback/tasks/types", uid, token = "")
{
    // Create filter
    //
    const auto bodyJson = json::Value::fromString(request.body());
    mwsf::MvSourceTypeFilter filter;
    filter.status(parseOptional<mwsf::UIFilterStatus>(bodyJson["status"]));
    filter.hidden(parseOptional<bool>(bodyJson["hidden"]));
    filter.sources(parseOptionalVector<std::string>(bodyJson["sources"]));
    filter.ageTypes(parseOptionalVector<mwsf::AgeType>(bodyJson["ageTypes"]));
    filter.workflows(parseOptionalVector<mwsf::Workflow>(bodyJson["workflows"]));
    Globals::feedbackChecker().fbPresetChecker().restrictFilterByGlobalPresets(filter, uid);

    // Calc types
    //
    auto socialTxn = Globals::dbPools().socialReadTxn(token);
    auto taskTypes = feedbackTypesNoExcept(*socialTxn, filter);

    // Form response
    //
    makeJsonResponse(response, toJson(taskTypes));
}

namespace {

std::set<std::string> feedbackSourcesNoExcept(
    pqxx::transaction_base& txn,
    const mwsf::MvSourceTypeFilter& filter)
{
    const std::string errPrefix = "Unable to retrieve feedback sources. ";

    try {
        return mwsf::mvSourcesByFilter(txn, filter);
    } catch (const Exception& e) {
        ERROR() << errPrefix << e;
    } catch (const std::exception& e) {
        ERROR() << errPrefix << e.what();
    } catch (...) {
        ERROR() << errPrefix;
    }

    return {};
}

} //namespace anonymous

YCR_RESPOND_TO("GET /feedback/tasks/sources", uid, token = "")
{
    Globals::aclChecker().checkPermission(uid, SOURCE_PERMISSION);

    // Create filter
    mwsf::MvSourceTypeFilter filter;
    filter.status(optionalQueryParam<mwsf::UIFilterStatus>(request, "status"));
    filter.hidden(optionalQueryParam<bool>(request, "hidden"));
    filter.types(optionalVectorQueryParam<mwsf::Type>(request, "type"));
    filter.ageTypes(optionalVectorQueryParam<mwsf::AgeType>(request, "age-type"));
    filter.workflows(optionalVectorQueryParam<mwsf::Workflow>(request, "workflow"));
    Globals::feedbackChecker().fbPresetChecker().restrictFilterByGlobalPresets(filter, uid);

    // Load sources
    //
    auto socialTxn = Globals::dbPools().socialReadTxn(token);
    auto sources = feedbackSourcesNoExcept(*socialTxn, filter);

    // Form response
    //
    makeJsonResponse(response, toJson(sources));
}

YCR_RESPOND_TO("GET /feedback/meta")
{
    makeJsonResponse(response, feedbackMeta(Globals::dbPools()));
}

} // YCR_USE(feedbackTasksThreadPool)

} // namespace maps::wiki::socialsrv
