#include "objects_update_attributes.h"

#include "maps/wikimap/mapspro/services/editor/src/branch_helpers.h"
#include "maps/wikimap/mapspro/services/editor/src/check_permissions.h"
#include "maps/wikimap/mapspro/services/editor/src/collection.h"
#include "maps/wikimap/mapspro/services/editor/src/commit.h"
#include "maps/wikimap/mapspro/services/editor/src/objects_cache.h"
#include "maps/wikimap/mapspro/services/editor/src/serialize/json_parser.h"
#include "maps/wikimap/mapspro/services/editor/src/serialize/request_data.h"
#include "maps/wikimap/mapspro/services/editor/src/serialize/xml_parser.h"
#include "maps/wikimap/mapspro/services/editor/src/shadow_attributes.h"

namespace maps {
namespace wiki {

namespace {
const std::string TASK_METHOD_NAME = "ObjectsUpdateAttributes";
const std::string ACTION_ID_UPDATE_ATTRIBUTES = "update-attributes";
}

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

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

std::string
ObjectsUpdateAttributes::Request::dump() const
{
    std::stringstream ss;
    ss << " uid: " << userId();
    ss << " branch: " << branchId;
    ss << " body: " << body;
    if (feedbackTaskId) {
        ss << " feedback-task-id: " << *feedbackTaskId;
    }
    return ss.str();
}

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

namespace {

class ObjectsUpdateAttributesRequestData : public RequestData
{
public:
    ObjectsUpdateAttributesRequestData(
        common::FormatType format,
        const std::string& body,
        ObjectsCache& cache)
    {
        parse(format, body, cache);
    }

    const StringMultiMap& attributes() const { return attributes_; }

protected:
    void parseContextDataJson(const json::Value& contextObject, ObjectsCache&) override
    {
        attributes_ = JsonParser::readObjectAttributes(contextObject);
    }

    void parseContextDataXml(const maps::xml3::Node& contextNode, ObjectsCache&) override
    {
        auto attrNodes = contextNode.nodes("wmp:attributes/wmp:attribute");
        for (size_t i = 0; i < attrNodes.size(); ++i) {
            auto attrId = attrNodes[i].attr<std::string>("id");
            auto attrValues = attrNodes[i].nodes("wmp:values/wmp:value");
            for (size_t v = 0; v < attrValues.size(); ++v) {
                attributes_.emplace(attrId, attrValues[v].value<std::string>());
            }
        }
    }

private:
    StringMultiMap attributes_;
};

} // namespace

void
ObjectsUpdateAttributes::control()
{
    auto branchContext = BranchContextFacade::acquireWrite(request_.branchId, request_.userId());
    ObjectsCache cache(branchContext, boost::none);
    ObjectsUpdateAttributesRequestData requestData(request_.format, request_.body, cache);

    GeoObjectCollection objects = cache.get(requestData.oids());
    const auto editorCfg = cfg()->editor();
    cfg()->editor()->objectsUpdateRestrictions().checkObjects(ACTION_ID_UPDATE_ATTRIBUTES, objects);

    StringSet categoryIds;
    for (const auto& oid : requestData.oids()) {
        ObjectPtr object = objects.getById(oid);
        WIKI_REQUIRE(object, ERR_MISSING_OBJECT, "Not found object id: " << oid);
        categoryIds.insert(object->categoryId());
    }
    WIKI_REQUIRE(categoryIds.size() == 1, ERR_MULTIPLE_CATEGORIES_NOT_SUPPORTED,
        "Group attributes edit for multiple categories is not supported");
    CheckPermissions(request_.userId(), branchContext.txnCore()).checkPermissionsForGroupUpdateAttributes(
        *categoryIds.begin(), requestData.attributes());

    StringSet useShadowAttributesToSet;
    StringSet useShadowAttributesToReset;
    for (const auto& [id, value] : requestData.attributes()) {
        if (!isUseShadowAttributeId(id)) {
            continue;
        }
        if (value.empty()) {
            useShadowAttributesToReset.insert(id);
        } else {
            useShadowAttributesToSet.insert(id);
        }
    }
    for (const auto& oid : requestData.oids()) {
        ObjectPtr object = objects.getById(oid);
        std::string curAttrName;
        AttrDefPtr attrDef;
        for (const auto& [id, value] : requestData.attributes()) {
            if (isShadowAttributeId(id)) {
                const auto useShadowId = useShadowAttributeId(id);
                if (useShadowAttributesToReset.count(useShadowId) ||
                    (!useShadowAttributesToSet.count(useShadowId) && object->attributes().value(useShadowId).empty())) {
                    continue;
                }
            }
            if (id != curAttrName) {
                curAttrName = id;
                attrDef = editorCfg->attribute(curAttrName);
                object->attributes().setValue(id,
                    attrDef->allowedValue(value));
            } else {
                object->attributes().addValue(id,
                    attrDef->allowedValue(value));
            }
        }
        object->primaryEdit(true);
        object->calcModified();
    }
    saveAndNotify(objects, cache);
}

void
ObjectsUpdateAttributes::saveAndNotify(
    GeoObjectCollection& /* objectContainer */,
    ObjectsCache& cache)
{
    cache.save(request_.userId(), common::COMMIT_PROPVAL_GROUP_MODIFIED_ATTRIBUTES);
    result_->taskName = taskName();
    CommitContext commitContext{request_.feedbackTaskId};
    result_->token = observers_.doCommit(cache, request_.userContext, commitContext);
    result_->commit = cache.savedCommit();
}

} // namespace wiki
} // namespace maps
