/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.atlas.repository.store.graph.v2;

import com.google.common.annotations.VisibleForTesting;
import org.apache.atlas.AtlasErrorCode;
import org.apache.atlas.RequestContext;
import org.apache.atlas.authorize.AtlasAuthorizationUtils;
import org.apache.atlas.authorize.AtlasPrivilege;
import org.apache.atlas.authorize.AtlasTypeAccessRequest;
import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.model.typedef.AtlasStructDef;
import org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef;
import org.apache.atlas.model.typedef.AtlasStructDef.AtlasConstraintDef;
import org.apache.atlas.repository.Constants;
import org.apache.atlas.repository.graphdb.AtlasCardinality;
import org.apache.atlas.repository.graphdb.AtlasGraphManagement;
import org.apache.atlas.repository.graphdb.AtlasVertex;
import org.apache.atlas.type.AtlasRelationshipType;
import org.apache.atlas.type.AtlasStructType;
import org.apache.atlas.type.AtlasStructType.AtlasAttribute;
import org.apache.atlas.type.AtlasType;
import org.apache.atlas.type.AtlasTypeRegistry;
import org.apache.atlas.type.AtlasTypeUtil;
import org.apache.atlas.typesystem.types.DataTypes.TypeCategory;
import org.apache.atlas.v1.model.typedef.AttributeDefinition;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.apache.atlas.type.AtlasStructType.AtlasAttribute.encodePropertyKey;

/**
 * StructDef store in v1 format.
 */
public class AtlasStructDefStoreV2 extends AtlasAbstractDefStoreV2<AtlasStructDef> {
    private static final Logger LOG = LoggerFactory.getLogger(AtlasStructDefStoreV2.class);

    public AtlasStructDefStoreV2(AtlasTypeDefGraphStoreV2 typeDefStore, AtlasTypeRegistry typeRegistry) {
        super(typeDefStore, typeRegistry);
    }

    public static void updateVertexPreCreate(AtlasStructDef structDef, AtlasStructType structType, AtlasVertex vertex, AtlasTypeDefGraphStoreV2 typeDefStore) throws AtlasBaseException {
        List<String> attrNames = new ArrayList<>(structDef.getAttributeDefs().size());

        createPropertyKeys(structDef, typeDefStore);

        for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
            String propertyKey = AtlasGraphUtilsV2.getTypeDefPropertyKey(structDef, attributeDef.getName());

            vertex.setProperty(AtlasGraphUtilsV2.encodePropertyKey(propertyKey), toJsonFromAttribute(structType.getAttribute(attributeDef.getName())));

            attrNames.add(attributeDef.getName());
        }

        String typeNamePropertyKey = AtlasGraphUtilsV2.getTypeDefPropertyKey(structDef);

        vertex.setProperty(AtlasGraphUtilsV2.encodePropertyKey(typeNamePropertyKey), attrNames);
    }

    public static void updateVertexPreUpdate(AtlasStructDef structDef, AtlasStructType structType, AtlasVertex vertex, AtlasTypeDefGraphStoreV2 typeDefStore) throws AtlasBaseException {
        List<String> attrNames = new ArrayList<>();

        if (CollectionUtils.isNotEmpty(structDef.getAttributeDefs())) {
            for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
                attrNames.add(attributeDef.getName());
            }
        }

        String       structDefPropertyKey        = AtlasGraphUtilsV2.getTypeDefPropertyKey(structDef);
        String       encodedStructDefPropertyKey = encodePropertyKey(structDefPropertyKey);
        Object       names                       = vertex.getProperty(encodedStructDefPropertyKey, Object.class);
        List<String> currAttrNames               = names instanceof List ? (List<String>) names : new ArrayList<>();

        // delete attributes that are not present in updated structDef
        if (CollectionUtils.isNotEmpty(currAttrNames)) {
            List<String> removedAttributes = null;

            createPropertyKeys(structDef, typeDefStore);

            for (String currAttrName : currAttrNames) {
                if (!attrNames.contains(currAttrName)) {
                    if (RequestContext.get().isInTypePatching()) {
                        String propertyKey = AtlasGraphUtilsV2.getTypeDefPropertyKey(structDef, currAttrName);

                        AtlasGraphUtilsV2.setProperty(vertex, propertyKey, null);

                        if (removedAttributes == null) {
                            removedAttributes = new ArrayList<>();
                        }

                        removedAttributes.add(currAttrName);

                        LOG.warn("REMOVED ATTRIBUTE: {}.{}", structDef.getName(), currAttrName);
                    } else {
                        throw new AtlasBaseException(AtlasErrorCode.ATTRIBUTE_DELETION_NOT_SUPPORTED, structDef.getName(), currAttrName);
                    }
                }
            }

            if (removedAttributes != null) {
                currAttrNames.removeAll(removedAttributes);

                vertex.setListProperty(encodedStructDefPropertyKey, currAttrNames);

                for (String removedAttribute : removedAttributes) {
                    String propertyKey = AtlasGraphUtilsV2.getTypeDefPropertyKey(structDef, removedAttribute);

                    vertex.removeProperty(propertyKey);
                }
            }
        } else {
            if (names == null) {
                LOG.warn("failed to load attribute names for type {}", structDef.getName());
            }
        }

        typeDefStore.updateTypeVertex(structDef, vertex);

        // Load up current struct definition for matching attributes
        AtlasStructDef currentStructDef = toStructDef(vertex, new AtlasStructDef(), typeDefStore);

        // add/update attributes that are present in updated structDef
        if (CollectionUtils.isNotEmpty(structDef.getAttributeDefs())) {
            for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
                if (CollectionUtils.isEmpty(currAttrNames) || !currAttrNames.contains(attributeDef.getName())) {
                    // this could have been an attribute removed by a REMOVE_LEGACY_REF_ATTRIBUTE patch
                    // in such case, don't add this attribute; ignore and continue
                    AtlasRelationshipType relationship = AtlasTypeUtil.findRelationshipWithLegacyRelationshipEnd(structDef.getName(), attributeDef.getName(), typeDefStore.getTypeRegistry());

                    if (relationship != null) {
                        attrNames.remove(attributeDef.getName());

                        LOG.warn("Ignoring attempt to add legacy attribute {}.{}, which is already present in relationship {}", structDef.getName(), attributeDef.getName(), relationship.getTypeName());

                        continue;
                    }

                    // new attribute - allow optional by default or allow mandatory only with typedef patch ADD_MANDATORY_ATTRIBUTE
                    if (!attributeDef.getIsOptional() && !isInAddMandatoryAttributePatch()) {
                        throw new AtlasBaseException(AtlasErrorCode.CANNOT_ADD_MANDATORY_ATTRIBUTE, structDef.getName(), attributeDef.getName());
                    }
                }

                // Validate the mandatory features of an attribute (compatibility with legacy type system)
                if (StringUtils.isEmpty(attributeDef.getName())) {
                    throw new AtlasBaseException(AtlasErrorCode.MISSING_MANDATORY_ATTRIBUTE, structDef.getName(), "name");
                }

                if (StringUtils.isEmpty(attributeDef.getTypeName())) {
                    throw new AtlasBaseException(AtlasErrorCode.MISSING_MANDATORY_ATTRIBUTE, structDef.getName(), "typeName");
                }

                AtlasAttributeDef existingAttribute = currentStructDef.getAttribute(attributeDef.getName());

                if (null != existingAttribute && !attributeDef.getTypeName().equals(existingAttribute.getTypeName())) {
                    throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST, "Data type update for attribute is not supported");
                }

                String propertyKey = AtlasGraphUtilsV2.getTypeDefPropertyKey(structDef, attributeDef.getName());

                AtlasGraphUtilsV2.setProperty(vertex, propertyKey, toJsonFromAttribute(structType.getAttribute(attributeDef.getName())));
            }
        }

        AtlasGraphUtilsV2.setEncodedProperty(vertex, encodedStructDefPropertyKey, attrNames);
    }

    public static boolean isInAddMandatoryAttributePatch() {
        return RequestContext.get().isInTypePatching() && StringUtils.equals(Constants.TYPEDEF_PATCH_ADD_MANDATORY_ATTRIBUTE, RequestContext.get().getCurrentTypePatchAction());
    }

    public static void updateVertexAddReferences(AtlasStructDef structDef, AtlasVertex vertex, AtlasTypeDefGraphStoreV2 typeDefStore) throws AtlasBaseException {
        for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
            addReferencesForAttribute(vertex, attributeDef, typeDefStore);
        }
    }

    public static AtlasStructDef toStructDef(AtlasVertex vertex, AtlasStructDef structDef, AtlasTypeDefGraphStoreV2 typeDefStore) {
        AtlasStructDef ret = (structDef != null) ? structDef : new AtlasStructDef();

        typeDefStore.vertexToTypeDef(vertex, ret);

        List<AtlasAttributeDef> attributeDefs          = new ArrayList<>();
        String                  typePropertyKey        = AtlasGraphUtilsV2.getTypeDefPropertyKey(ret);
        String                  encodedTypePropertyKey = AtlasGraphUtilsV2.encodePropertyKey(typePropertyKey);
        Object                  names                  = vertex.getProperty(encodedTypePropertyKey, Object.class);
        List<String>            attrNames              = names instanceof List ? (List<String>) names : new ArrayList<>();

        if (CollectionUtils.isNotEmpty(attrNames)) {
            for (String attrName : attrNames) {
                String attrPropertyKey        = AtlasGraphUtilsV2.getTypeDefPropertyKey(ret, attrName);
                String encodedAttrPropertyKey = AtlasGraphUtilsV2.encodePropertyKey(attrPropertyKey);
                String attrJson               = vertex.getProperty(encodedAttrPropertyKey, String.class);

                if (StringUtils.isEmpty(attrJson)) {
                    LOG.warn("attribute not found {}.{}. Ignoring..", ret.getName(), attrName);

                    continue;
                }

                attributeDefs.add(toAttributeDefFromJson(structDef, AtlasType.fromJson(attrJson, Map.class), typeDefStore));
            }
        } else {
            if (names == null) {
                LOG.warn("failed to load attribute names for type {}", structDef);
            }
        }

        ret.setAttributeDefs(attributeDefs);

        return ret;
    }

    @VisibleForTesting
    public static String toJsonFromAttribute(AtlasAttribute attribute) {
        AtlasAttributeDef   attributeDef = attribute.getAttributeDef();
        Map<String, Object> attribInfo   = new HashMap<>();

        attribInfo.put("name", attributeDef.getName());
        attribInfo.put("dataType", attributeDef.getTypeName());
        attribInfo.put("isUnique", attributeDef.getIsUnique());
        attribInfo.put("isIndexable", attributeDef.getIsIndexable());
        attribInfo.put("includeInNotification", attributeDef.getIncludeInNotification());
        attribInfo.put("isComposite", attribute.isOwnedRef());
        attribInfo.put("reverseAttributeName", attribute.getInverseRefAttributeName());
        attribInfo.put("defaultValue", attributeDef.getDefaultValue());
        attribInfo.put("description", attributeDef.getDescription());
        attribInfo.put("searchWeight", attributeDef.getSearchWeight());
        attribInfo.put("indexType", attributeDef.getIndexType());

        if (attributeDef.getOptions() != null) {
            attribInfo.put("options", AtlasType.toJson(attributeDef.getOptions()));
        }

        attribInfo.put("displayName", attributeDef.getDisplayName());

        final int lower;
        final int upper;

        if (attributeDef.getCardinality() == AtlasAttributeDef.Cardinality.SINGLE) {
            lower = attributeDef.getIsOptional() ? 0 : 1;
            upper = 1;
        } else {
            if (attributeDef.getIsOptional()) {
                lower = 0;
            } else {
                lower = attributeDef.getValuesMinCount() < 1 ? 1 : attributeDef.getValuesMinCount();
            }

            upper = attributeDef.getValuesMaxCount() < 2 ? Integer.MAX_VALUE : attributeDef.getValuesMaxCount();
        }

        Map<String, Object> multiplicity = new HashMap<>();

        multiplicity.put("lower", lower);
        multiplicity.put("upper", upper);
        multiplicity.put("isUnique", AtlasAttributeDef.Cardinality.SET.equals(attributeDef.getCardinality()));

        attribInfo.put("multiplicity", AtlasType.toJson(multiplicity));

        return AtlasType.toJson(attribInfo);
    }

    @VisibleForTesting
    public static AtlasAttributeDef toAttributeDefFromJson(AtlasStructDef structDef, Map<?, ?> attribInfo, AtlasTypeDefGraphStoreV2 typeDefStore) {
        AtlasAttributeDef ret = new AtlasAttributeDef();

        ret.setName((String) attribInfo.get("name"));
        ret.setTypeName((String) attribInfo.get("dataType"));
        ret.setIsUnique((Boolean) attribInfo.get("isUnique"));
        ret.setIsIndexable((Boolean) attribInfo.get("isIndexable"));
        ret.setIncludeInNotification((Boolean) attribInfo.get("includeInNotification"));
        ret.setDefaultValue((String) attribInfo.get("defaultValue"));
        ret.setDescription((String) attribInfo.get("description"));

        if (attribInfo.get("options") != null) {
            ret.setOptions(AtlasType.fromJson((String) attribInfo.get("options"), Map.class));
        }

        ret.setDisplayName((String) attribInfo.get("displayName"));

        if ((Boolean) attribInfo.get("isComposite")) {
            ret.addConstraint(new AtlasConstraintDef(AtlasConstraintDef.CONSTRAINT_TYPE_OWNED_REF));
        }

        final String reverseAttributeName = (String) attribInfo.get("reverseAttributeName");

        if (StringUtils.isNotBlank(reverseAttributeName)) {
            Map<String, Object> attributes = new HashMap<>();

            attributes.put(AtlasConstraintDef.CONSTRAINT_PARAM_ATTRIBUTE, reverseAttributeName);

            ret.addConstraint(new AtlasConstraintDef(AtlasConstraintDef.CONSTRAINT_TYPE_INVERSE_REF, attributes));
        }

        Map<?, ?> multiplicity = AtlasType.fromJson((String) attribInfo.get("multiplicity"), Map.class);
        Number    minCount     = (Number) multiplicity.get("lower");
        Number    maxCount     = (Number) multiplicity.get("upper");
        Boolean   isUnique     = (Boolean) multiplicity.get("isUnique");

        if (minCount == null || minCount.intValue() == 0) {
            ret.setIsOptional(true);
            ret.setValuesMinCount(0);
        } else {
            ret.setIsOptional(false);
            ret.setValuesMinCount(minCount.intValue());
        }

        if (maxCount == null || maxCount.intValue() < 2) {
            ret.setCardinality(AtlasAttributeDef.Cardinality.SINGLE);
            ret.setValuesMaxCount(1);
        } else {
            if (isUnique == null || isUnique == Boolean.FALSE) {
                ret.setCardinality(AtlasAttributeDef.Cardinality.LIST);
            } else {
                ret.setCardinality(AtlasAttributeDef.Cardinality.SET);
            }

            ret.setValuesMaxCount(maxCount.intValue());
        }

        Number searchWeight = (Number) attribInfo.get("searchWeight");

        if (searchWeight != null) {
            ret.setSearchWeight(searchWeight.intValue());
        } else {
            ret.setSearchWeight(-1);
        }

        String indexType = (String) attribInfo.get("indexType");

        if (!StringUtils.isEmpty(indexType)) {
            ret.setIndexType(AtlasAttributeDef.IndexType.valueOf(indexType));
        }

        return ret;
    }

    public static AttributeDefinition toAttributeDefinition(AtlasAttribute attribute) {
        final AtlasAttributeDef attrDef = attribute.getAttributeDef();

        AttributeDefinition ret = new AttributeDefinition();

        ret.setName(attrDef.getName());
        ret.setDataTypeName(attrDef.getTypeName());
        ret.setMultiplicity(AtlasTypeUtil.getMultiplicity(attrDef));
        ret.setIsComposite(attribute.isOwnedRef());
        ret.setIsUnique(attrDef.getIsUnique());
        ret.setIsIndexable(attrDef.getIsIndexable());
        ret.setReverseAttributeName(attribute.getInverseRefAttributeName());
        ret.setDescription(attrDef.getDescription());
        ret.setDefaultValue(attrDef.getDefaultValue());
        ret.setSearchWeight(attrDef.getSearchWeight());
        ret.setIndexType(attrDef.getIndexType());

        return ret;
    }

    @Override
    public AtlasVertex preCreate(AtlasStructDef structDef) throws AtlasBaseException {
        LOG.debug("==> AtlasStructDefStoreV1.preCreate({})", structDef);

        validateType(structDef);

        AtlasType type = typeRegistry.getType(structDef.getName());

        if (type.getTypeCategory() != org.apache.atlas.model.TypeCategory.STRUCT) {
            throw new AtlasBaseException(AtlasErrorCode.TYPE_MATCH_FAILED, structDef.getName(), TypeCategory.STRUCT.name());
        }

        AtlasAuthorizationUtils.verifyAccess(new AtlasTypeAccessRequest(AtlasPrivilege.TYPE_CREATE, structDef), "create struct-def ", structDef.getName());

        AtlasVertex ret = typeDefStore.findTypeVertexByName(structDef.getName());

        if (ret != null) {
            throw new AtlasBaseException(AtlasErrorCode.TYPE_ALREADY_EXISTS, structDef.getName());
        }

        ret = typeDefStore.createTypeVertex(structDef);

        AtlasStructDefStoreV2.updateVertexPreCreate(structDef, (AtlasStructType) type, ret, typeDefStore);

        LOG.debug("<== AtlasStructDefStoreV1.preCreate({}): {}", structDef, ret);

        return ret;
    }

    @Override
    public AtlasStructDef create(AtlasStructDef structDef, AtlasVertex preCreateResult) throws AtlasBaseException {
        LOG.debug("==> AtlasStructDefStoreV1.create({}, {})", structDef, preCreateResult);

        verifyAttributeTypeReadAccess(structDef.getAttributeDefs());

        if (CollectionUtils.isEmpty(structDef.getAttributeDefs())) {
            throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST, "Missing attributes for structdef");
        }

        AtlasVertex vertex = (preCreateResult == null) ? preCreate(structDef) : preCreateResult;

        AtlasStructDefStoreV2.updateVertexAddReferences(structDef, vertex, typeDefStore);

        AtlasStructDef ret = toStructDef(vertex);

        LOG.debug("<== AtlasStructDefStoreV1.create({}, {}): {}", structDef, preCreateResult, ret);

        return ret;
    }

    @Override
    public List<AtlasStructDef> getAll() throws AtlasBaseException {
        LOG.debug("==> AtlasStructDefStoreV1.getAll()");

        List<AtlasStructDef> ret = new ArrayList<>();

        Iterator<AtlasVertex> vertices = typeDefStore.findTypeVerticesByCategory(TypeCategory.STRUCT);

        while (vertices.hasNext()) {
            ret.add(toStructDef(vertices.next()));
        }

        LOG.debug("<== AtlasStructDefStoreV1.getAll(): count={}", ret.size());

        return ret;
    }

    @Override
    public AtlasStructDef getByName(String name) throws AtlasBaseException {
        LOG.debug("==> AtlasStructDefStoreV1.getByName({})", name);

        AtlasVertex vertex = typeDefStore.findTypeVertexByNameAndCategory(name, TypeCategory.STRUCT);

        if (vertex == null) {
            throw new AtlasBaseException(AtlasErrorCode.TYPE_NAME_NOT_FOUND, name);
        }

        vertex.getProperty(Constants.TYPE_CATEGORY_PROPERTY_KEY, String.class);

        AtlasStructDef ret = toStructDef(vertex);

        LOG.debug("<== AtlasStructDefStoreV1.getByName({}): {}", name, ret);

        return ret;
    }

    @Override
    public AtlasStructDef getByGuid(String guid) throws AtlasBaseException {
        LOG.debug("==> AtlasStructDefStoreV1.getByGuid({})", guid);

        AtlasVertex vertex = typeDefStore.findTypeVertexByGuidAndCategory(guid, TypeCategory.STRUCT);

        if (vertex == null) {
            throw new AtlasBaseException(AtlasErrorCode.TYPE_GUID_NOT_FOUND, guid);
        }

        AtlasStructDef ret = toStructDef(vertex);

        LOG.debug("<== AtlasStructDefStoreV1.getByGuid({}): {}", guid, ret);

        return ret;
    }

    @Override
    public AtlasStructDef update(AtlasStructDef structDef) throws AtlasBaseException {
        LOG.debug("==> AtlasStructDefStoreV1.update({})", structDef);

        verifyAttributeTypeReadAccess(structDef.getAttributeDefs());

        validateType(structDef);

        AtlasStructDef ret = StringUtils.isNotBlank(structDef.getGuid()) ? updateByGuid(structDef.getGuid(), structDef)
                : updateByName(structDef.getName(), structDef);

        LOG.debug("<== AtlasStructDefStoreV1.update({}): {}", structDef, ret);

        return ret;
    }

    @Override
    public AtlasStructDef updateByName(String name, AtlasStructDef structDef) throws AtlasBaseException {
        LOG.debug("==> AtlasStructDefStoreV1.updateByName({}, {})", name, structDef);

        AtlasStructDef existingDef = typeRegistry.getStructDefByName(name);

        AtlasAuthorizationUtils.verifyAccess(new AtlasTypeAccessRequest(AtlasPrivilege.TYPE_UPDATE, existingDef), "update struct-def ", name);

        validateType(structDef);

        AtlasType type = typeRegistry.getType(structDef.getName());

        if (type.getTypeCategory() != org.apache.atlas.model.TypeCategory.STRUCT) {
            throw new AtlasBaseException(AtlasErrorCode.TYPE_MATCH_FAILED, structDef.getName(), TypeCategory.STRUCT.name());
        }

        AtlasVertex vertex = typeDefStore.findTypeVertexByNameAndCategory(name, TypeCategory.STRUCT);

        if (vertex == null) {
            throw new AtlasBaseException(AtlasErrorCode.TYPE_NAME_NOT_FOUND, name);
        }

        AtlasStructDefStoreV2.updateVertexPreUpdate(structDef, (AtlasStructType) type, vertex, typeDefStore);
        AtlasStructDefStoreV2.updateVertexAddReferences(structDef, vertex, typeDefStore);

        AtlasStructDef ret = toStructDef(vertex);

        LOG.debug("<== AtlasStructDefStoreV1.updateByName({}, {}): {}", name, structDef, ret);

        return ret;
    }

    @Override
    public AtlasStructDef updateByGuid(String guid, AtlasStructDef structDef) throws AtlasBaseException {
        LOG.debug("==> AtlasStructDefStoreV1.updateByGuid({})", guid);

        AtlasStructDef existingDef = typeRegistry.getStructDefByGuid(guid);

        AtlasAuthorizationUtils.verifyAccess(new AtlasTypeAccessRequest(AtlasPrivilege.TYPE_UPDATE, existingDef), "update struct-def ", (existingDef != null ? existingDef.getName() : guid));

        validateType(structDef);

        AtlasType type = typeRegistry.getTypeByGuid(guid);

        if (type.getTypeCategory() != org.apache.atlas.model.TypeCategory.STRUCT) {
            throw new AtlasBaseException(AtlasErrorCode.TYPE_MATCH_FAILED, structDef.getName(), TypeCategory.STRUCT.name());
        }

        AtlasVertex vertex = typeDefStore.findTypeVertexByGuidAndCategory(guid, TypeCategory.STRUCT);

        if (vertex == null) {
            throw new AtlasBaseException(AtlasErrorCode.TYPE_GUID_NOT_FOUND, guid);
        }

        AtlasStructDefStoreV2.updateVertexPreUpdate(structDef, (AtlasStructType) type, vertex, typeDefStore);
        AtlasStructDefStoreV2.updateVertexAddReferences(structDef, vertex, typeDefStore);

        AtlasStructDef ret = toStructDef(vertex);

        LOG.debug("<== AtlasStructDefStoreV1.updateByGuid({}): {}", guid, ret);

        return ret;
    }

    @Override
    public AtlasVertex preDeleteByName(String name) throws AtlasBaseException {
        LOG.debug("==> AtlasStructDefStoreV1.preDeleteByName({})", name);

        AtlasStructDef existingDef = typeRegistry.getStructDefByName(name);

        AtlasAuthorizationUtils.verifyAccess(new AtlasTypeAccessRequest(AtlasPrivilege.TYPE_DELETE, existingDef), "delete struct-def ", name);

        AtlasVertex ret = typeDefStore.findTypeVertexByNameAndCategory(name, TypeCategory.STRUCT);

        if (AtlasGraphUtilsV2.typeHasInstanceVertex(name)) {
            throw new AtlasBaseException(AtlasErrorCode.TYPE_HAS_REFERENCES, name);
        }

        if (ret == null) {
            throw new AtlasBaseException(AtlasErrorCode.TYPE_NAME_NOT_FOUND, name);
        }

        typeDefStore.deleteTypeVertexOutEdges(ret);

        LOG.debug("<== AtlasStructDefStoreV1.preDeleteByName({}): {}", name, ret);

        return ret;
    }

    @Override
    public AtlasVertex preDeleteByGuid(String guid) throws AtlasBaseException {
        LOG.debug("==> AtlasStructDefStoreV1.preDeleteByGuid({})", guid);

        AtlasStructDef existingDef = typeRegistry.getStructDefByGuid(guid);

        AtlasAuthorizationUtils.verifyAccess(new AtlasTypeAccessRequest(AtlasPrivilege.TYPE_DELETE, existingDef), "delete struct-def ", (existingDef != null ? existingDef.getName() : guid));

        AtlasVertex ret = typeDefStore.findTypeVertexByGuidAndCategory(guid, TypeCategory.STRUCT);

        String typeName = AtlasGraphUtilsV2.getEncodedProperty(ret, Constants.TYPENAME_PROPERTY_KEY, String.class);

        if (AtlasGraphUtilsV2.typeHasInstanceVertex(typeName)) {
            throw new AtlasBaseException(AtlasErrorCode.TYPE_HAS_REFERENCES, typeName);
        }

        if (ret == null) {
            throw new AtlasBaseException(AtlasErrorCode.TYPE_GUID_NOT_FOUND, guid);
        }

        typeDefStore.deleteTypeVertexOutEdges(ret);

        LOG.debug("<== AtlasStructDefStoreV1.preDeleteByGuid({}): {}", guid, ret);

        return ret;
    }

    private AtlasStructDef toStructDef(AtlasVertex vertex) {
        AtlasStructDef ret = null;

        if (vertex != null && typeDefStore.isTypeVertex(vertex, TypeCategory.STRUCT)) {
            ret = toStructDef(vertex, new AtlasStructDef(), typeDefStore);
        }

        return ret;
    }

    private static void addReferencesForAttribute(AtlasVertex vertex, AtlasAttributeDef attributeDef, AtlasTypeDefGraphStoreV2 typeDefStore) throws AtlasBaseException {
        Set<String> referencedTypeNames = AtlasTypeUtil.getReferencedTypeNames(attributeDef.getTypeName());
        String      typeName            = vertex.getProperty(Constants.TYPENAME_PROPERTY_KEY, String.class);

        for (String referencedTypeName : referencedTypeNames) {
            if (!AtlasTypeUtil.isBuiltInType(referencedTypeName)) {
                AtlasVertex referencedTypeVertex = typeDefStore.findTypeVertexByName(referencedTypeName);

                if (referencedTypeVertex == null) {
                    throw new AtlasBaseException(AtlasErrorCode.UNKNOWN_TYPE, referencedTypeName, typeName, attributeDef.getName());
                }

                String label = AtlasGraphUtilsV2.getEdgeLabel(typeName, attributeDef.getName());

                typeDefStore.getOrCreateEdge(vertex, referencedTypeVertex, label);
            }
        }
    }

    private static void createPropertyKeys(AtlasStructDef structDef, AtlasTypeDefGraphStoreV2 typeDefStore) throws AtlasBaseException {
        try (AtlasGraphManagement management = typeDefStore.atlasGraph.getManagementSystem()) {
            for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
                // Validate the mandatory features of an attribute (compatibility with legacy type system)
                if (StringUtils.isEmpty(attributeDef.getName())) {
                    throw new AtlasBaseException(AtlasErrorCode.MISSING_MANDATORY_ATTRIBUTE, structDef.getName(), "name");
                }

                if (StringUtils.isEmpty(attributeDef.getTypeName())) {
                    throw new AtlasBaseException(AtlasErrorCode.MISSING_MANDATORY_ATTRIBUTE, structDef.getName(), "typeName");
                }

                String propertyKey = AtlasGraphUtilsV2.getTypeDefPropertyKey(structDef, attributeDef.getName());

                createPropertyKey(AtlasGraphUtilsV2.encodePropertyKey(propertyKey), String.class, AtlasCardinality.SINGLE, management);
            }

            String typeNamePropertyKey = AtlasGraphUtilsV2.getTypeDefPropertyKey(structDef);

            createPropertyKey(AtlasGraphUtilsV2.encodePropertyKey(typeNamePropertyKey), Object.class, AtlasCardinality.SINGLE, management);

            management.setIsSuccess(true);
        } catch (Exception e) {
            LOG.error("PropertyKey creation failed for structDef: {}", structDef, e);

            throw e instanceof AtlasBaseException ? (AtlasBaseException) e : new AtlasBaseException(e);
        }
    }
}
