#include <maps/wikimap/mapspro/libs/social_serv_serialize/include/jsonize_feedback_task.h>

#include <maps/wikimap/mapspro/libs/social_serv_serialize/include/jsonize_comment.h>
#include <maps/wikimap/mapspro/libs/gdpr/include/user.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/serialization.h>
#include <yandex/maps/wiki/common/json_helpers.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.h>
#include <yandex/maps/wiki/social/feedback/description_serialize.h>

namespace maps::wiki::socialsrv::serialize {

namespace sf  = social::feedback;
namespace sfa = social::feedback::attrs;
using boost::lexical_cast;

namespace {

namespace jsids {

const std::string ACQUIRED = "acquired";
const std::string AGE_TYPE = "ageType";
const std::string CREATED_AT = "createdAt";
const std::string DATE = "date";
const std::string DEPLOYED_AT = "deployedAt";
const std::string DESCRIPTION = "description";
const std::string DUPLICATE_HEAD_ID = "duplicateHeadId";
const std::string DUPLICATES_COUNT = "duplicatesCount";
const std::string EVENT_COMMENT = "comment";
const std::string EVENT_MODIFIED_AT = "modifiedAt";
const std::string EVENT_MODIFIED_BY = "modifiedBy";
const std::string EVENT_OPERATION = "operation";
const std::string EVENT_REASON = "reason";
const std::string EVENT_OPERATION_TYPE = "type";
const std::string EVENT_OPERATION_PARAMS = "params";
const std::string FBAPI_ISSUE_ID = "fbApiTaskId";
const std::string HIDDEN = "hidden";
const std::string HISTORY = "history";
const std::string ID = "id";
const std::string INTERNAL_CONTENT = "internalContent";
const std::string LINK = "link";
const std::string OBJECT_ID = "objectId";
const std::string PERMALINK = "permalink";
const std::string POSITION = "position";
const std::string INDOOR_LEVEL = "indoorLevel";
const std::string RESOLUTION = "resolution";
const std::string REJECT_REASON = "rejectReason";
const std::string RESOLVED = "resolved";
const std::string SOURCE = "source";
const std::string OBJECT_URI = "objectUri";
const std::string STATE_MODIFIED_AT = "stateModifiedAt";
const std::string STATE = "state";
const std::string TYPE_CATEGORY = "typeCategory";
const std::string TYPE = "type";
const std::string UID = "uid";
const std::string USER_ATTRS = "userAttrs";
const std::string USER_DATA = "userData";
const std::string USER_DATA_COMMENT = "comment";
const std::string USER_DATA_SEARCH_REQUEST = "searchRequest";
const std::string USER_DATA_PHOTO_URLS = "photoUrls";
const std::string VALID_OPERATIONS = "validOperations";
const std::string VALID_REJECT_REASONS = "validRejectReasons";
const std::string VALID_NEW_TYPES = "validNewTypes";
const std::string VALID_NEW_PROCESSING_LVLS = "validNewProcessingLvls";
const std::string VIEWED = "viewed";
const std::string WORKFLOW = "workflow";
const std::string PROCESSING_LEVEL = "processingLevel";

} // namespace jsids
/*
 * Frontend guys say that they need double curly brackets around i18n keys,
 * because they have some parser which works only when bla-bla-bla ...
*/
sf::Description putKeysInCurlBrackets(const sf::Description& description)
{
    if (description.isNonTranslatable()) {
        return description;
    }

    const auto& descriptionI18n = description.asTranslatable();

    std::string keyCurled = "{{" + descriptionI18n.i18nKey() + "}}";

    sf::ParamToDescription paramsCurled;
    for (const auto& [param, description] : descriptionI18n.i18nParams()) {
        paramsCurled[param] = putKeysInCurlBrackets(description);
    }

    return sf::DescriptionI18n(std::move(keyCurled), std::move(paramsCurled));
}

void jsonizeParams(sf::HistoryItemParams params, json::ObjectBuilder& builder)
{
    builder[jsids::EVENT_OPERATION_PARAMS] =
        [&](json::ObjectBuilder builder) {
            for (const auto& [key, value]: params) {
                builder[key] = value;
            }
        };
}

void jsonizeHistory(
    json::ArrayBuilder& builder,
    const HistoryEventsUI& eventsUI)
{
    for (auto eventIt = eventsUI.rbegin(); eventIt != eventsUI.rend(); ++eventIt) {
        const auto& item = eventIt->historyItem;
        builder << [&](json::ObjectBuilder builder) {
            REQUIRE(
                item.id(),
                "History item ID is missing"
            );
            builder[jsids::ID] = std::to_string(*item.id());
            builder[jsids::EVENT_MODIFIED_AT] = chrono::formatIsoDateTime(item.modifiedAt());
            builder[jsids::EVENT_MODIFIED_BY] = std::to_string(gdpr::User(item.modifiedBy()).uid());
            builder[jsids::EVENT_OPERATION] = [&](json::ObjectBuilder builder) {
                builder[jsids::EVENT_OPERATION_TYPE] = sf::toString(item.operation());

                static const sf::TaskOperations OPERATIONS_WITH_PARAMS {
                    sf::TaskOperation::ChangeType,
                    sf::TaskOperation::ProcessingLevelUp,
                    sf::TaskOperation::ChangeProcessingLvl,
                    sf::TaskOperation::NeedInfo,
                };
                if (OPERATIONS_WITH_PARAMS.count(item.operation())) {
                    jsonizeParams(item.params(), builder);
                }

                const auto it = item.params().find(jsids::EVENT_REASON);
                if (it != item.params().end()) {
                    builder[jsids::EVENT_REASON] = it->second;
                }
            };
            if (eventIt->comment) {
                builder[jsids::EVENT_COMMENT] = [&](json::ObjectBuilder builder) {
                    socialsrv::serialize::jsonize(builder, eventIt->comment.value());
                };
            }
        };
    }
}

// Remove this function (and send all objectUri to UI)
// when MAPSUI-21003 will be done.
bool isUriLinkable(const std::string& uri)
{
    static const std::vector<std::string> LINKABLE_URI_PREFIXES = {
        "ymapsbm1://route/driving",
        "ymapsbm1://route/transit",
        "ymapsbm1://route/pedestrian",
        "ymapsbm1://route/bicycle",
        "ymapsbm1://transit/stop",
        "ymapsbm1://transit/line",
        "ymapsbm1://org",
        "ymapsbm1://geo",
    };
    for (const auto& prefix : LINKABLE_URI_PREFIXES) {
        if (uri.starts_with(prefix)) {
            return true;
        }
    }
    return false;
}

void userDataToJson(json::ObjectBuilder& builder, const TaskUserData& userData)
{
    if (!userData.comment &&
        !userData.searchRequest &&
        !userData.photoUrls)
    {
        return;
    }
    builder[jsids::USER_DATA] = [&](json::ObjectBuilder builder) {
        if (userData.comment) {
            builder[jsids::USER_DATA_COMMENT] = *userData.comment;
        }

        if (userData.searchRequest) {
            builder[jsids::USER_DATA_SEARCH_REQUEST] = *userData.searchRequest;
        }

        if (userData.photoUrls) {
            builder[jsids::USER_DATA_PHOTO_URLS] = *userData.photoUrls;
        }
    };
}

void taskForUIBriefToJson(
    json::ObjectBuilder& builder,
    const sf::Task& task,
    social::TUid uid)
{
    taskBriefToJson(builder, task, uid);

    if (!task.indoorLevel().empty()) {
        builder[jsids::INDOOR_LEVEL] = task.indoorLevel();
    }

    builder[jsids::DESCRIPTION] = putKeysInCurlBrackets(task.description());

    if (task.acquired()) {
        builder[jsids::ACQUIRED] << [&](json::ObjectBuilder builder) {
            builder[jsids::UID] = common::idToJson(task.acquired()->uid);
            builder[jsids::DATE] = chrono::formatIsoDateTime(task.acquired()->date);
        };
    } else {
        builder[jsids::ACQUIRED] << json::null;
    }
    builder[jsids::HIDDEN] = task.hidden();
    builder[jsids::INTERNAL_CONTENT] = task.internalContent();
    if (task.objectId()) {
        builder[jsids::OBJECT_ID] = common::idToJson(*task.objectId());
    }
    if (task.duplicateHeadId()) {
        builder[jsids::DUPLICATE_HEAD_ID] = common::idToJson(*task.duplicateHeadId());
    } else {
        builder[jsids::DUPLICATE_HEAD_ID] = json::null;
    }

    const auto& attrs = task.attrs();

    if (attrs.exist(sf::AttrType::ObjectDiff)) {
        builder[sfa::OBJECT_DIFF] = attrs.get(sf::AttrType::ObjectDiff);
    }

    if (attrs.exist(sf::AttrType::SourceContext)) {
        builder[sfa::SOURCE_CONTEXT] =
            transformSourceContext(attrs.get(sf::AttrType::SourceContext));
    }

    if (attrs.exist(sf::AttrType::UserData)) {
        builder[jsids::USER_ATTRS] = attrs.get(sf::AttrType::UserData);
    }

    auto addCustomAttrIfExist = [&](const std::string& attrName) {
        if (attrs.existCustom(attrName)) {
            builder[attrName] = attrs.getCustom(attrName);
        }
    };

    addCustomAttrIfExist(jsids::PERMALINK);
    addCustomAttrIfExist(jsids::LINK);
}

} // namespace

/**
 * https://st.yandex-team.ru/NMAPS-8853
 *
 * TODO: remove this function as soon as
 * all the context values in the db are transformed to a new format
 */
json::Value transformSourceContext(const json::Value& oldContext)
{
    // TODO: check if you really need it
    if (!oldContext.isObject()) {
        return oldContext;
    }

    if (oldContext.hasField("content")) {
        return oldContext;
    }

    std::string type = "mrc";

    json::repr::ObjectRepr featuresReplaced;
    for (const auto& fieldName : oldContext.fields()) {
        if (fieldName == "features") {
            featuresReplaced.emplace("imageFeatures", oldContext[fieldName]);
        } else {
            featuresReplaced.emplace(fieldName, oldContext[fieldName]);
        }
    }

    return json::Value(json::repr::ObjectRepr{
        {"type", json::Value(std::move(type))},
        {"content", json::Value(std::move(featuresReplaced))}
    });
}

void taskBriefToJson(
    json::ObjectBuilder& builder,
    const social::feedback::TaskBrief& task,
    social::TUid uid)
{
    builder[jsids::ID] = common::idToJson(task.id());
    builder[jsids::CREATED_AT] = chrono::formatIsoDateTime(task.createdAt());
    builder[jsids::AGE_TYPE] = lexical_cast<std::string>(task.ageType());
    builder[jsids::SOURCE] = task.source();
    builder[jsids::TYPE] = lexical_cast<std::string>(task.type());
    builder[jsids::TYPE_CATEGORY] = lexical_cast<std::string>(sf::typeCategoryByType(task.type()));
    builder[jsids::WORKFLOW] = lexical_cast<std::string>(task.workflow());
    builder[jsids::STATE_MODIFIED_AT] = chrono::formatIsoDateTime(task.stateModifiedAt());

    if (task.processingLevel()) {
        builder[jsids::PROCESSING_LEVEL] = task.processingLevel();
    }

    if (task.resolved()) {
        builder[jsids::RESOLVED] << [&](json::ObjectBuilder builder) {
            builder[jsids::UID] = common::idToJson(gdpr::User(task.resolved()->uid).uid());
            builder[jsids::DATE] = chrono::formatIsoDateTime(task.resolved()->date);
            builder[jsids::RESOLUTION] = lexical_cast<std::string>(task.resolved()->resolution.verdict());
            if (task.resolved()->resolution.rejectReason()) {
                builder[jsids::REJECT_REASON] = lexical_cast<std::string>(*task.resolved()->resolution.rejectReason());
            }
        };
    } else {
        builder[jsids::RESOLVED] << json::null;
    }

    if (task.deployedAt()) {
        builder[jsids::DEPLOYED_AT] = chrono::formatIsoDateTime(*task.deployedAt());
    }

    builder[jsids::VIEWED] = task.viewedBy().count(uid) > 0;

    auto geoPoint = geolib3::convertMercatorToGeodetic(task.position());
    builder[jsids::POSITION] = geolib3::geojson(geoPoint);

    builder[jsids::STATE] = toString(task.state());
}

void taskForReviewUIToJson(
    json::ObjectBuilder& builder,
    const TaskForReviewUI& task,
    social::TUid uid)
{
    taskForUIBriefToJson(builder, task, uid);
    userDataToJson(builder, task.userData());
}

void taskToJson(
    json::ObjectBuilder& builder,
    const TaskForUI& task,
    social::TUid uid)
{
    taskForUIBriefToJson(builder, task, uid);
    if (!task.objectId() && task.nearbyObjectId()) {
        builder[jsids::OBJECT_ID] = common::idToJson(*task.nearbyObjectId());
    }

    builder[jsids::VALID_OPERATIONS] = [&](json::ArrayBuilder builder) {
        for (auto operation : task.validOperations()) {
            builder << toString(operation);
        }
    };

    builder[jsids::VALID_REJECT_REASONS] = [&](json::ArrayBuilder builder) {
        for (auto reason : task.validRejectReasons()) {
            builder << toString(reason);
        }
    };

    builder[jsids::HISTORY] = [&](json::ArrayBuilder builder) {
        jsonizeHistory(
            builder,
            task.historyEventsUI());
    };

    const auto validNewTypes = sf::Agent::validNewTypes(task);
    if (!validNewTypes.empty()) {
        builder[jsids::VALID_NEW_TYPES] = [&](json::ArrayBuilder builder) {
            for (auto type : validNewTypes) {
                builder << toString(type);
            }
        };
    }

    const auto validNewProcessingLvls = sf::Agent::validNewProcessingLvls(task);
    if (!validNewProcessingLvls.empty()) {
        builder[jsids::VALID_NEW_PROCESSING_LVLS] = [&](json::ArrayBuilder builder) {
            for (auto processingLvl : validNewProcessingLvls) {
                builder << toString(processingLvl);
            }
        };
    }
}

void taskToJson(
    json::ObjectBuilder& builder,
    const TaskExtended& taskExtended,
    social::TUid uid)
{
    taskToJson(builder, taskExtended.taskForUI, uid);

    if (taskExtended.objectUri && isUriLinkable(*taskExtended.objectUri)) {
        builder[jsids::OBJECT_URI] = *taskExtended.objectUri;
    }

    if (taskExtended.duplicatesCount) {
        builder[jsids::DUPLICATES_COUNT] = *taskExtended.duplicatesCount;
    }

    if (taskExtended.fbapiIssueId) {
        builder[jsids::FBAPI_ISSUE_ID] = *taskExtended.fbapiIssueId;
    }
    userDataToJson(builder, taskExtended.userData);
}

} // namespace maps::wiki::socialsrv::serialize
