#include "deserialize.h"
#include "factors_erf.h"

#include <saas/util/json/json.h>
#include <saas/util/types/cast.h>

#include <library/cpp/string_utils/base64/base64.h>
#include <util/string/vector.h>

using namespace NSaas;
using namespace NJson;
using namespace NJsonFormat;

template <>
TAttributeValue::TAttributeValueType enum_cast<TAttributeValue::TAttributeValueType, NJsonFormat::TEntity::EType>(NJsonFormat::TEntity::EType value) {
    switch(value) {
    case NJsonFormat::TEntity::atSEARCH_INTEGER:    return TAttributeValue::avtInt;
    case NJsonFormat::TEntity::atSEARCH_LITERAL:    return TAttributeValue::avtLit;
    case NJsonFormat::TEntity::atGROUP:             return TAttributeValue::avtGrp;
    case NJsonFormat::TEntity::atGROUP_LITERAL:     return TAttributeValue::avtGrpLit;
    case NJsonFormat::TEntity::atPROPERTY:          return TAttributeValue::avtProp;
    case NJsonFormat::TEntity::atFSPROPERTY:        return TAttributeValue::avtFSProp;
    case NJsonFormat::TEntity::atSPECKEY:           return TAttributeValue::avtSpecKey;
    case NJsonFormat::TEntity::atTRIGRAM_INDEXING:  return TAttributeValue::avtTIProp;
    default:
        Y_FAIL("incorrect call");
    }
}

void NSaas::TProtobufDeserializer::Deserialize(const NRTYServer::TMessage& source, TAction& target) {
    target.InternalMessage.CopyFrom(source);
    const TRTYMessageBehaviour& behaviour = GetBehaviour(target.GetProtobufActionType());

    if (target.GetProtobufActionType() == NRTYServer::TMessage::DELETE_DOCUMENT &&
        target.InternalDocument.GetBody().StartsWith(NProtobufFormat::QueryDelBodyPrefix)) {
        target.SetRequest(target.InternalDocument.GetBody().c_str() + NProtobufFormat::QueryDelBodyPrefix.length());
    } else if (behaviour.IsContentMessage) {
        TAbstractZone& abstractZone = target.GetDocument();
        ProcessSearchAttributes(target.InternalDocument.GetSearchAttributes(), abstractZone);
        ProcessGroupAttributes(target.InternalDocument.GetGroupAttributes(), abstractZone);
        ProcessProperties(target.InternalDocument.GetDocumentProperties(), abstractZone);
        ProcessSpecialKeys(target.InternalDocument.GetAdditionalKeys(), abstractZone);
        ProcessTIProps(target.InternalDocument.GetTrigramIndexedProps(), abstractZone);
        ProcessFSProperties(target.InternalDocument.GetFSProperties(), abstractZone);
        ProcessErfFactors(target.InternalDocument.GetFactors(), abstractZone);
        ProcessComplexFactors(target.InternalDocument, target.GetDocument());
        const NRTYServer::TZone& rootProtobufZone = target.InternalDocument.GetRootZone();
        if (!rootProtobufZone.HasText()) {
            ProcessSubZones(rootProtobufZone.GetChildren(), abstractZone);
        } else {
            TZone* rootZone = new TZone(const_cast<NRTYServer::TZone&>(rootProtobufZone));
            abstractZone.SubZones.insert(std::make_pair("root_zone", rootZone));
            rootZone->SetText(rootProtobufZone.GetText());
            ProcessSubZones(rootProtobufZone.GetChildren(), *rootZone);
        }
    } else {
        if (source.HasDocument() && source.GetDocument().HasBody())
            throw yexception () << "body in non-content message found";
    }
    if (source.HasReceiveTimestamp()) {
        target.SetReceiveTimestamp(TInstant::Seconds(source.GetReceiveTimestamp()));
    }
}

void NSaas::TProtobufDeserializer::ProcessSubZones(const TProtobufZones& zones, NSaas::TAbstractZone& abstractZone) {
    for (TProtobufZones::const_iterator i = zones.begin(); i != zones.end(); ++i) {
        TZone* newZone = new TZone(const_cast<NRTYServer::TZone&>(*i));
        abstractZone.SubZones.insert(std::make_pair(i->GetName(), newZone));
        ProcessSearchAttributes(i->GetSearchAttributes(), *newZone);
        ProcessSubZones(i->GetChildren(), *newZone);
    }
}

void NSaas::TProtobufDeserializer::ProcessSearchAttributes(const TProtobufAttributes& attributes, NSaas::TAbstractZone& zone) {
    for (TProtobufAttributes::const_iterator i = attributes.begin(); i != attributes.end(); ++i) {
        TAttributeValue::TAttributeValueType type = i->GetType() == NRTYServer::TAttribute::INTEGER_ATTRIBUTE ? TAttributeValue::avtInt : TAttributeValue::avtLit;
        zone.RegisterAttribute(i->GetName(), i->GetValue(), type);
    }
}

void NSaas::TProtobufDeserializer::ProcessGroupAttributes(const TProtobufAttributes& attributes, NSaas::TAbstractZone& zone) {
    for (TProtobufAttributes::const_iterator i = attributes.begin(); i != attributes.end(); ++i)
        zone.RegisterAttribute(i->GetName(), i->GetValue(), i->GetType() == NRTYServer::TAttribute::INTEGER_ATTRIBUTE
                                                            ? TAttributeValue::avtGrp
                                                            : TAttributeValue::avtGrpLit);
}

void NSaas::TProtobufDeserializer::ProcessProperties(const TProtobufProperties& properties, NSaas::TAbstractZone& zone) {
    for (TProtobufProperties::const_iterator i = properties.begin(); i != properties.end(); ++i)
        zone.RegisterAttribute(i->GetName(), i->GetValue(), TAttributeValue::avtProp);
}

void NSaas::TProtobufDeserializer::ProcessSpecialKeys(const TProtobufProperties& properties, NSaas::TAbstractZone& zone) {
    for (TProtobufProperties::const_iterator i = properties.begin(); i != properties.end(); ++i)
        zone.RegisterAttribute(i->GetName(), i->GetValue(), TAttributeValue::avtSpecKey);
}

void NSaas::TProtobufDeserializer::ProcessTIProps(const TProtobufProperties& properties, NSaas::TAbstractZone& zone) {
    for (TProtobufProperties::const_iterator i = properties.begin(); i != properties.end(); ++i)
        zone.RegisterAttribute(i->GetName(), i->GetValue(), TAttributeValue::avtTIProp);
}

void NSaas::TProtobufDeserializer::ProcessFSProperties(const TProtobufProperties& properties, NSaas::TAbstractZone& zone) {
    for (TProtobufProperties::const_iterator i = properties.begin(); i != properties.end(); ++i)
        zone.RegisterAttribute(i->GetName(), i->GetValue(), TAttributeValue::avtFSProp);
}

void NSaas::TProtobufDeserializer::ProcessErfFactors(const NRTYServer::TMessage::TErfInfo& erf, NSaas::TAbstractZone& zone) {
    if (erf.NamesSize() != erf.GetValues().ValuesSize()) {
        ythrow yexception() << "invalid erf: it has " << erf.NamesSize() << " names and " << erf.GetValues().ValuesSize() << " values";
    }
    for (ui64 i = 0; i < NSaas::FactorsSize(erf); ++i) {
        const TString& name = erf.GetNames(i);
        const TString value = NSaas::ValueToString(erf.GetValues().GetValues(i));
        zone.RegisterAttribute(name, value, TAttributeValue::avtFactor);
    }
}

void NSaas::TProtobufDeserializer::ProcessComplexFactors(const NRTYServer::TMessage::TDocument& document, NSaas::TDocument& zone) {
    for (ui64 i = 0; i < document.CSInfoSize(); ++i)
        zone.CSBlocks.push_back(new TCSBlock(const_cast<NRTYServer::TMessage::TCSInfo&>(document.GetCSInfo(i))));
    for (ui64 i = 0; i < document.QSInfoSize(); ++i)
        zone.QSBlocks.push_back(new TQSBlock(const_cast<NRTYServer::TMessage::TQSInfo&>(document.GetQSInfo(i))));

    if (document.HasAnnData()) {
        zone.InternalDocument.MutableAnnData()->CopyFrom(document.GetAnnData());
        zone.AddAnnotations();
    }
    if (document.HasGeoData()) {
        zone.InternalDocument.MutableGeoData()->CopyFrom(zone.GetGeoData());
        zone.AddGeo();
    }
}

void TJsonDeserializer::Deserialize(const NJson::TJsonValue& source, TAction& target) {
    const TJsonValue::TMapType& map = NUtil::GetMap(source, "message");
    for (TJsonValue::TMapType::const_iterator i = map.begin(); i != map.end(); ++i) {
        if (i->first == NJsonFormat::ActionLable) {
            const TString& actionString = NUtil::GetString(i->second, NJsonFormat::ActionLable);
            ParsingContext.Action = GetBehaviour(actionString);
            if (!ParsingContext.Action)
                throw yexception() << "unknown action '" + actionString + "'";
            target.SetProtobufActionType(ParsingContext.Action->MessageType);
            continue;
        }
        if (i->first == NJsonFormat::PrefixLable) {
            target.SetPrefix(NUtil::GetUInteger(i->second, NJsonFormat::PrefixLable));
            ParsingContext.HasPrefix = true;
            continue;
        }
        if (i->first == NJsonFormat::RequestLable) {
            ParsingContext.Request = NUtil::GetString(i->second, NJsonFormat::RequestLable);
            continue;
        }
        if (i->first == NJsonFormat::DocsLable) {
            ParsingContext.HasDocumentSection = true;
            continue;
        }
        if (i->first == NJsonFormat::ExternalShardLable) {
            target.SetExternalShard(NUtil::GetUInteger(i->second, NJsonFormat::ExternalShardLable));
            continue;
        }
        throw yexception() << "unknown field " << i->first;
    }

    if (!ParsingContext.Action)
        throw yexception() << "message must have action field";
    if (!ParsingContext.HasPrefix && ParsingContext.Action->MessageType == NRTYServer::TMessage::SWITCH_PREFIX)
        throw yexception() << "message must have prefix field";

    if (ParsingContext.Action->MessageType == NRTYServer::TMessage::DELETE_DOCUMENT &&
        !ParsingContext.HasDocumentSection && !ParsingContext.Request)
        throw yexception() << "delete must have either request or docs";

    if (!ParsingContext.Action->IsContentMessage) {
        if (ParsingContext.HasDocumentSection)
            throw yexception() << ParsingContext.Action->Name << " action cannot have docs section";
        target.GetDocument().SetUrl(ParsingContext.Action->Name);
    } else if (ParsingContext.HasDocumentSection) {
        const TJsonValue::TArray& docs = NUtil::GetArray(source[NJsonFormat::DocsLable], "document section");
        if (docs.size() > 1)
            throw yexception() << "only one document should be present";
        if (docs.size())
            ProcessDoc(docs[0], target);
        else
            throw yexception() << "docs section is present, but empty";
    } else if (ParsingContext.Action->MessageType == NRTYServer::TMessage::DELETE_DOCUMENT) {
        if (!ParsingContext.Request)
            throw yexception() << "delete action has neither an URL to delete nor a delete request";
        target.SetRequest(ParsingContext.Request);
    }
}

void TJsonDeserializer::ProcessDoc(const NJson::TJsonValue& jsonDocument, TAction& target) {
    const TJsonValue::TMapType& docMap = NUtil::GetMap(jsonDocument, "document section");
    TDocument& document = target.GetDocument();
    for (TJsonValue::TMapType::const_iterator i = docMap.begin(); i != docMap.end(); ++i) {
        if (i->first == NJsonFormat::OptionsLable) {
            ProcessDocOptions(i->second, document);
        }
        else if (i->second.IsMap() || i->second.IsString()) {
            ProcessDocZone(i->first, i->second, document);
        }
        else if (i->second.IsArray()) {
            const TJsonValue::TArray& zoneArray = NUtil::GetArray(i->second);
            for (TJsonValue::TArray::const_iterator arrVal = zoneArray.begin(); arrVal != zoneArray.end(); ++arrVal)
                ProcessDocZone(i->first, *arrVal, document);
        }
        else {
            throw yexception() << "Document zone must be map or array of maps";
        }
    }
    if (!DocParsingContext.HasURL)
        throw yexception() << "Document must contain url";
}

void TJsonDeserializer::ProcessDocOptions(const TJsonValue& options, NSaas::TDocument& document) {
    const TJsonValue::TMapType& opMap = options.GetMap();
    for (TJsonValue::TMapType::const_iterator i = opMap.begin(); i != opMap.end(); ++i) {
        if (i->first == NJsonFormat::CheckOnlyBeforeReplyLable)
            document.SetCheckOnlyBeforeReply(i->second.GetBooleanRobust());
        else if (i->first == NJsonFormat::RealtimeLable)
            document.SetRealtime(i->second.GetBooleanRobust());
        else if (i->first == NJsonFormat::VersionLable)
            document.SetVersion(i->second.GetIntegerRobust());
        else if (i->first == NJsonFormat::FilterRankLable)
            document.SetFilterRank(i->second.GetDoubleRobust());
        else if (i->first == NJsonFormat::DatabaseVersionLable)
            document.SetDatabaseVersion(i->second.GetIntegerRobust());
        else if (i->first == NJsonFormat::DeadlineLable)
            document.SetDeadlineMinutesUTC(i->second.GetIntegerRobust());
        else if (i->first == NJsonFormat::MimeTypeLable)
            document.SetMimeType(i->second.GetStringRobust());
        else if (i->first == NJsonFormat::CharsetLable)
            document.SetCharset(i->second.GetString());
        else if (i->first == NJsonFormat::LanguageLable)
            document.SetLang(i->second.GetString());
        else if (i->first == NJsonFormat::Language2Lable)
            document.SetLang2(i->second.GetString());
        else if (i->first == NJsonFormat::UpdateTypeLable) {
            NRTYServer::TMessage::TUpdateType type;
            if (!NRTYServer::TMessage::TUpdateType_Parse(i->second.GetString(), &type)) {
                throw yexception() << "cannot parse " << NJsonFormat::UpdateTypeLable << ": " << i->second.GetString();
            }
            document.SetUpdateType(type);
        } else if (i->first == NJsonFormat::ModificationTimestampLable) {
            ui64 value = i->second.GetUIntegerRobust();
            if (value > Max<ui32>())
                throw yexception() << "incorrect timestamp " << value << ": expecting unix timestamp";
            document.SetTimestamp(value);
        } else if (i->first == NJsonFormat::LanguageDefaultLable)
            document.SetLangDef(i->second.GetString());
        else if (i->first == NJsonFormat::LanguageDefault2Lable ||
                 i->first == NJsonFormat::GroupLable)
            Y_UNUSED(i->second); // deprecated
        else
            throw yexception() << "unknown document option " << i->first;
    }
}

void NSaas::TJsonDeserializer::ProcessZoneOptions(const NJson::TJsonValue& options, TSet<NJsonFormat::TEntity::EType>& types) {
    const TJsonValue::TMapType& optionsMap = options.GetMap();
    for (TJsonValue::TMapType::const_iterator i = optionsMap.begin(); i != optionsMap.end(); ++i) {
        if (i->first == NJsonFormat::TypeLable) {
            ParseType(i->second, types);
            continue;
        }
        throw yexception() << "unknown zone option " << i->first;
    }
}

void TJsonDeserializer::ParseType(const TJsonValue& type, TSet<NJsonFormat::TEntity::EType>& types) const {
    if (type.IsString()) {
        if (!!type.GetString() && (type.GetString().data())[0] == '#') {
            for (const char* p = type.GetString().data() + 1; *p; ++p)
                types.insert(THashedEntities::GetByShortName(*p).Type);
        } else {
            TVector<TString> typeV = SplitString(type.GetString(), ",");
            for (TVector<TString>::const_iterator i = typeV.begin(); i != typeV.end(); ++i)
                types.insert(THashedEntities::GetByLongName(*i).Type);
        }
    } else if (type.IsArray()) {
        for (TJsonValue::TArray::const_iterator i = type.GetArray().begin(); i != type.GetArray().end(); ++i) {
            const TString& zoneType = NUtil::GetString(*i, "zone type");
            types.insert(THashedEntities::GetByLongName(zoneType).Type);
        }
    } else {
        throw yexception() << "type must be string or array of strings";
    }
}

void TJsonDeserializer::ProcessZoneZone(const TString& name, const TJsonValue& zone, NSaas::TZone& parent) {
    TZoneParsingContext zpc;
    TSet<NJsonFormat::TEntity::EType> types;
    ProcessZoneDirectives(name, zone, zpc, types);
    if (types.empty())
        DeduceZoneZoneType(name, zone, types);

    for (TSet<NJsonFormat::TEntity::EType>::const_iterator i = types.begin(); i != types.end(); ++i) {
        switch(*i) {
        case TEntity::atURL:
        case TEntity::atBODY:
        case TEntity::atGROUP:
        case TEntity::atGROUP_LITERAL:
        case TEntity::atPROPERTY:
        case TEntity::atSPECKEY:
        case TEntity::atTRIGRAM_INDEXING:
        case TEntity::atFSPROPERTY:
        case TEntity::atMINGEO:
        case TEntity::atFACTOR:
        case TEntity::atINT_FACTOR:
        case TEntity::atEMBEDDING:
            throw yexception() << "zones can contain only search attributes and other zones";
            break;
        case TEntity::atSEARCH_INTEGER:
        case TEntity::atSEARCH_LITERAL: {
            if (!name)
                throw yexception() << "attribute with empty name found in zone " << parent.GetName();
            if (!zpc.HasValue)
                throw yexception() << "search attribute " << name << " must have a value";
            const TString& value = NUtil::ConvertToString(zone[NJsonFormat::ValueLable], "value of zone " + name);
            parent.AddAttribute(name).AddValue(value).AddType(enum_cast<TAttributeValue::TAttributeValueType>(*i));
            break;
        }
        case TEntity::atZONE: {
            NSaas::TZone& docZone = parent.AddZone(name);
            if (zpc.HasValue)
                docZone.SetText(NUtil::ConvertToString(zone[NJsonFormat::ValueLable], "value of zone " + name));
            if (zpc.HasChildren)
                ProcessZoneTree(zone[NJsonFormat::ChildrenLable], docZone);
            break;
        }
        default:
            Y_FAIL("Invalid usage");
        }
    }
}

void TJsonDeserializer::ProcessZoneTree(const TJsonValue& zone, NSaas::TZone& parent) {
    const TJsonValue::TMapType& map = NUtil::GetMap(zone, "zone children");
    for (TJsonValue::TMapType::const_iterator i = map.begin(); i != map.end(); ++i) {
        if (i->second.IsMap()) {
            ProcessZoneZone(i->first, i->second, parent);
        } else if (i->second.IsArray()) {
            const TJsonValue::TArray& zoneArray = i->second.GetArray();
            for (TJsonValue::TArray::const_iterator arrVal = zoneArray.begin(); arrVal != zoneArray.end(); ++arrVal)
                ProcessZoneZone(i->first, *arrVal, parent);
        } else {
            throw yexception() << "Document zone must be map or array of maps";
        }
    }
}

void TJsonDeserializer::ProcessQsFactors(const NJson::TJsonValue& qsFactByKey, NRTYServer::TMessage::TQSInfo& parent, EFactorsType factorsType){
    for (const auto& i: NUtil::GetMap(qsFactByKey, "qs_info.factors")){
        NRTYServer::TMessage::TFactorsByKey* qsFactor= parent.AddFactors();
        qsFactor->SetKey(i.first);
        for (const auto& j: NUtil::GetArray(i.second, "qs_info.factors." + i.first)) {
            switch (factorsType) {
            case EFactorsType::Int:
                AddSimpleIntFactorValue(j.GetStringRobust(), *qsFactor->MutableValues());
                break;
            case EFactorsType::Float:
                AddSimpleFactorValue(j.GetStringRobust(), *qsFactor->MutableValues());
                break;
            }
        }
    }
}

void TJsonDeserializer::ProcessQsFactorNames(const NJson::TJsonValue& qsFactorNames, NRTYServer::TMessage::TQSInfo& parent){
    const TJsonValue::TArray& arr = NUtil::GetArray(qsFactorNames, "qs_info.factor_names");
    for (TJsonValue::TArray::const_iterator i = arr.begin(); i != arr.end(); ++i)
        parent.AddFactorNames(NUtil::GetString(*i, "elements of qs_info.factor_names"));
}

void TJsonDeserializer::ProcessQsMap(const TJsonValue& qsFactors, NRTYServer::TMessage::TQSInfo& parent, EFactorsType factorsType) {
    const TJsonValue::TMapType& map = NUtil::GetMap(qsFactors, "qs_info");
    for (TJsonValue::TMapType::const_iterator i = map.begin(); i != map.end(); ++i) {
        if (i->first == NJsonFormat::QSFactorNamesLable) {
            ProcessQsFactorNames(i->second, parent);
        } else if (i->first == NJsonFormat::QSFactorsLable) {
            ProcessQsFactors(i->second, parent, factorsType);
        } else {
            throw yexception() << "Unknown field in qs_info: " << i->first;
        }
    }
}

void TJsonDeserializer::ProcessCsMap(const TJsonValue& csFactors, NSaas::TCSBlock& parent) {
    const TJsonValue::TMapType& map = NUtil::GetMap(csFactors, "cs_info");
    for (TJsonValue::TMapType::const_iterator i = map.begin(); i != map.end(); ++i)
        parent.AddFactor(i->first, NUtil::GetDouble(i->second, "value of cs_factor " + i->first));
}

static void ParseRegionData(const TJsonValue& regionsArray, TAnnBlock::TSentence& sent) {
    for (auto& regionJson : regionsArray.GetArray()) {
        if (!regionJson.IsMap())
            throw yexception() << "Region data shoul be in map";
        ui32 region = 0;
        const NJson::TJsonValue* streams  = nullptr;
        for (auto& i : regionJson.GetMap()) {
            if (i.first == NJsonFormat::AnnSentenceRegionLable) {
                region = i.second.GetInteger();
            } else if (i.first == NJsonFormat::AnnStreamsLable) {
                if (!i.second.IsMap())
                    throw yexception() << "Sentence streams shoul be in map";
                streams = &(i.second);
            } else
                throw yexception() << "Unknown annotation field" << i.first;
        }

        if (!streams)
            throw yexception() << "There is no streams data found";

        TAnnBlock::TRegion docRegion = sent.AddRegion(region);
        for (auto& stream : streams->GetMap()) {
            docRegion.AddStream(stream.first, stream.second.GetStringRobust());
        }
    }
}

void TJsonDeserializer::ProcessAnnotations(const TJsonValue& annotations, NSaas::TAnnBlock& result) {
    if (!annotations.IsArray()) {
        throw yexception() << "Document annotation must be array of sentences maps";
    }

    for (const auto& sent: annotations.GetArray()) {
        if (!sent.IsMap())
            throw yexception() << "Document annotation must be array of sentences maps";

        const NJson::TJsonValue* regions  = nullptr;
        TString text;
        TMaybe<ELanguage> lang;
        for (auto& i : sent.GetMap()) {
            if (i.first == NJsonFormat::AnnSentenceLable) {
                text = i.second.GetStringRobust();
            } else if (i.first == NJsonFormat::AnnSentenceLangLable) {
                lang = static_cast<ELanguage>(i.second.GetInteger());
            } else if (i.first == NJsonFormat::AnnSentenceRegionsArrayLable) {
                if (!i.second.IsArray())
                    throw yexception() << "Regions shoul be in array";
                regions = &(i.second);
            } else
                throw yexception() << "Unknown annotation field" << i.first;
        }

        if (!regions)
            throw yexception() << "There is no regions data found";

        NSaas::TAnnBlock::TSentence docSent = result.AddSentence(text, lang);
        ParseRegionData(*regions, docSent);
    }
}

void TJsonDeserializer::ProcessGeo(const TString& name, const TJsonValue& geodata, NSaas::TGeoBlock& result) {
    const TJsonValue& jCoords = geodata[NJsonFormat::CoordsLable];
    const TJsonValue& jKind = geodata[NJsonFormat::GeoLayerKindLable];
    EGeoObjectKind layerKind = EGeoObjectKind::PointSet;
    if (jKind.IsString()) {
        Y_ENSURE(TryFromString<EGeoObjectKind>(jKind.GetString(), layerKind), "EGeoObjectKind");
    }

    if (layerKind == EGeoObjectKind::RectSet) {
        auto rect = result.AddRectSet(name);
        if (jCoords.IsDefined()) {
            const TJsonValue::TArray& arr = NUtil::GetArray(jCoords, name);
            const size_t cnt = (arr.size() / 4) * 4;
            for (size_t i = 0; i < cnt; i += 4) {
                rect.AddRect(arr[i].GetDouble(), arr[i + 1].GetDouble(), arr[i + 2].GetDouble(), arr[i + 3].GetDouble());
            }
        }
    } else {
        const bool isGeoCoords = (layerKind != EGeoObjectKind::SpecialPointSet);
        auto row = result.AddPointSet(name, isGeoCoords);
        if (jCoords.IsDefined()) {
            const TJsonValue::TArray& arr = NUtil::GetArray(jCoords, name);
            const size_t cnt = (arr.size() / 2) * 2;
            for (size_t i = 0; i < cnt; i += 2) {
                row.AddPoint(arr[i].GetDouble(), arr[i + 1].GetDouble());
            }
        }
    }
}

void TJsonDeserializer::ProcessDocZone(const TString& name, const TJsonValue& zone, NSaas::TDocument& document) {
    const TJsonValue* jValue = nullptr;
    TZoneParsingContext zpc;
    TSet<NJsonFormat::TEntity::EType> types;
    if (zone.IsMap()) {
        ProcessZoneDirectives(name, zone, zpc, types);
        zone.GetValuePointer(NJsonFormat::ValueLable, &jValue);
    } else {
        jValue = &zone;
    }
    if (types.empty())
        DeduceDocZoneType(name, zone, types);

    for (TSet<NJsonFormat::TEntity::EType>::const_iterator i = types.begin(); i != types.end(); ++i) {
        switch (*i) {
        case TEntity::atURL: {
            if (DocParsingContext.HasURL)
                throw yexception() << "there can be only one url in document";
            DocParsingContext.HasURL = true;
            const TString& url = NUtil::GetString(jValue, "url");
            if (!url)
                throw yexception() << "document url cannot be empty";
            document.SetUrl(url);
            break;
        }
        case TEntity::atBODY: {
            if (DocParsingContext.HasBody)
                throw yexception() << "can be only one body in document";
            if (DocParsingContext.HasZone)
                throw yexception() << "in one document can be zone or body, but not both";
            if (zpc.HasChildren)
                throw yexception() << "body section cannot have children";
            DocParsingContext.HasBody = true;
            document.SetBody(NUtil::GetString(jValue, "body"));
            break;
        }
        case TEntity::atSEARCH_INTEGER:
        case TEntity::atSEARCH_LITERAL:
        case TEntity::atGROUP:
        case TEntity::atGROUP_LITERAL:
        case TEntity::atFSPROPERTY:
        case TEntity::atPROPERTY:
        case TEntity::atSPECKEY:
        case TEntity::atTRIGRAM_INDEXING:
        {
            if (!name)
                throw yexception() << "section with empty name found in the document";

            const TString& value = NUtil::ConvertToString(jValue, "value of " + name);
            document.AddAttribute(name).AddValue(value).AddType(enum_cast<TAttributeValue::TAttributeValueType>(*i));
            break;
        }
        case TEntity::atMINGEO:
        {
            if (!name)
                throw yexception() << "section with empty name found in the document";

            TGeoBlock& geoBlock = document.AddGeo();
            ProcessGeo(name, *jValue, geoBlock);
            break;
        }
        case TEntity::atEMBEDDING:
        {
            if (!name) {
                throw yexception() << "section with empty name found in the document";
            }

            TString value, version;
            TMaybe<TString> tag;
            if (jValue->IsMap()) {
                NJson::TJsonValue valueJson, versionJson, tagJson;
                Y_ENSURE(jValue->GetValue("value", &valueJson));
                value = Base64Decode(NUtil::ConvertToString(valueJson, "value of " + name));
                if (jValue->GetValue("version", &versionJson)) {
                    version = NUtil::ConvertToString(versionJson, "version of " + name);
                }
                if (jValue->GetValue("tag", &tagJson)) {
                    tag = NUtil::ConvertToString(tagJson, "tag for " + name);
                }
            } else {
                value = Base64Decode(NUtil::ConvertToString(jValue, "value of " + name));
            }
            if (tag.Defined()) {
                document.AddEmbedding(name, value, version, tag.GetRef());
            } else {
                document.AddEmbedding(name, value, version);
            }

            break;
        }
        case TEntity::atZONE: {
            if (DocParsingContext.HasBody)
                throw yexception() << "in one document can be zone or body, but not both";
            DocParsingContext.HasZone = true;
            NSaas::TZone& docZone = document.AddZone(name);
            if (jValue && (jValue->IsInteger() || jValue->IsString()))
                docZone.SetText(jValue->GetStringRobust());
            if (zpc.HasChildren)
                ProcessZoneTree(zone[NJsonFormat::ChildrenLable], docZone);
            break;
        }
        case TEntity::atFACTOR: {
            const TString& value = NUtil::ConvertToString(jValue, "factor " + name + " value");
            document.AddAttribute(name).AddValue(value).AddType(TAttributeValue::avtFactor);
            break;
        }
        case TEntity::atINT_FACTOR:
        {
            const TString& value = NUtil::ConvertToString(jValue, "factor " + name + " value");
            document.AddAttribute(name).AddValue(value).AddType(TAttributeValue::avtIntFactor);
            break;
        }
        case TEntity::atQS_INFO: {
            NRTYServer::TMessage::TQSInfo& qsi = document.AddQSInfo();
            qsi.SetName(name);
            ProcessQsMap(*jValue, qsi, EFactorsType::Float);
            break;
        }
        case TEntity::atINT_QS_INFO: {
            NRTYServer::TMessage::TQSInfo& qsi = document.AddQSInfo();
            qsi.SetName(name);
            ProcessQsMap(*jValue, qsi, EFactorsType::Int);
            break;
        }
        case TEntity::atCS_INFO: {
            TCSBlock& block = document.AddCS(name);
            ProcessCsMap(*jValue, block);
            break;
        }
        case TEntity::atINDDOC: {
            if (DocParsingContext.HasIndDoc)
                throw yexception() << "can be only one indexed doc in document";
            DocParsingContext.HasIndDoc = true;
            const TString& serializedIndexedDoc = Base64Decode(NUtil::GetString(jValue, "indexed doc"));
            if (!document.MutableIndexedDoc()->ParseFromString(serializedIndexedDoc))
                throw yexception() << "cannot parse IndexedDoc";
            break;
        }
        case TEntity::atANNOTATION: {
            TAnnBlock& annotations = document.AddAnnotations();
            ProcessAnnotations(*jValue, annotations);
            break;
        }
        default:
            Y_FAIL("Invalid usage");
        }
    }
}

void NSaas::TJsonDeserializer::DeduceDocZoneType(const TString& name, const NJson::TJsonValue& /*value*/, TSet<NJsonFormat::TEntity::EType>& types) const {
    if (name == NJsonFormat::BodyLable)
        types.insert(TEntity::atBODY);
    else if (name == NJsonFormat::UrlLable)
        types.insert(TEntity::atURL);
    else if (name == NJsonFormat::IndexedDocLable)
        types.insert(TEntity::atINDDOC);
    else
        types.insert(TEntity::atZONE);
}

void NSaas::TJsonDeserializer::DeduceZoneZoneType(const TString& /*name*/, const NJson::TJsonValue& /*value*/, TSet<NJsonFormat::TEntity::EType>& types) const {
    types.insert(TEntity::atZONE);
}

void NSaas::TJsonDeserializer::ProcessZoneDirectives(const TString& name, const TJsonValue& zone, TZoneParsingContext &zpc, TSet<NJsonFormat::TEntity::EType>& types) {
    const TJsonValue::TMapType& map = NUtil::GetMap(zone, "value of zone " + name);
    for (TJsonValue::TMapType::const_iterator i = map.begin(); i != map.end(); ++i) {
        if (i->first == NJsonFormat::OptionsLable) {
            if (zpc.HasType)
                throw yexception() << "zone " << name << " cannot have both options and type";
            zpc.HasOptions = true;
            ProcessZoneOptions(i->second, types);
            continue;
        }
        if (i->first == NJsonFormat::TypeLable) {
            if (zpc.HasOptions)
                throw yexception() << "zone " << name << " cannot have both options and type";
            zpc.HasType = true;
            ParseType(i->second, types);
            continue;
        }
        if (i->first == NJsonFormat::ValueLable) {
            zpc.HasValue = true;
            continue;
        }
        if (i->first == NJsonFormat::ChildrenLable) {
            zpc.HasChildren = true;
            continue;
        }
        throw yexception() << "unknown section " << i->first << " in zone " << name;
    }
}
