/**
 * 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.glossary;

import org.apache.atlas.AtlasErrorCode;
import org.apache.atlas.bulkimport.BulkImportResponse;
import org.apache.atlas.bulkimport.BulkImportResponse.ImportInfo;
import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.model.glossary.AtlasGlossary;
import org.apache.atlas.model.glossary.AtlasGlossaryTerm;
import org.apache.atlas.model.glossary.relations.AtlasGlossaryHeader;
import org.apache.atlas.model.glossary.relations.AtlasRelatedTermHeader;
import org.apache.atlas.model.glossary.relations.AtlasTermCategorizationHeader;
import org.apache.atlas.model.instance.AtlasObjectId;
import org.apache.atlas.model.instance.AtlasRelatedObjectId;
import org.apache.atlas.model.instance.AtlasRelationship;
import org.apache.atlas.model.instance.AtlasStruct;
import org.apache.atlas.repository.graphdb.AtlasVertex;
import org.apache.atlas.repository.ogm.DataAccess;
import org.apache.atlas.repository.store.graph.AtlasRelationshipStore;
import org.apache.atlas.repository.store.graph.v2.AtlasGraphUtilsV2;
import org.apache.atlas.type.AtlasRelationshipType;
import org.apache.atlas.type.AtlasTypeRegistry;
import org.apache.atlas.util.FileUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.Objects.requireNonNull;
import static org.apache.atlas.bulkimport.BulkImportResponse.ImportStatus.FAILED;
import static org.apache.atlas.repository.graph.GraphHelper.updateModificationMetadata;

public class GlossaryTermUtils extends GlossaryUtils {
    private static final Logger LOG = LoggerFactory.getLogger(GlossaryTermUtils.class);

    private static final int INDEX_FOR_GLOSSARY_AT_RECORD = 0;
    private static final int INDEX_FOR_TERM_AT_RECORD     = 1;

    private static final ThreadLocal<Map<String, String>>  GLOSSARY_NAME_GUID_CACHE        = ThreadLocal.withInitial(LinkedHashMap::new);
    private static final ThreadLocal<Map<String, Integer>> GLOSSARY_TERM_ORDER_CACHE       = ThreadLocal.withInitial(HashMap::new);
    private static final ThreadLocal<Map<String, String>>  GLOSSARY_TERM_Q_NAME_GUID_CACHE = ThreadLocal.withInitial(HashMap::new);

    protected GlossaryTermUtils(AtlasRelationshipStore relationshipStore, AtlasTypeRegistry typeRegistry, DataAccess dataAccess) {
        super(relationshipStore, typeRegistry, dataAccess);
    }

    public static String getGlossaryTermHeaders() {
        List<String> ret = new ArrayList<>();

        ret.add("GlossaryName");
        ret.add("TermName");
        ret.add("ShortDescription");
        ret.add("LongDescription");
        ret.add("Examples");
        ret.add("Abbreviation");
        ret.add("Usage");
        ret.add("AdditionalAttributes");
        ret.add("TranslationTerms");
        ret.add("ValidValuesFor");
        ret.add("Synonyms");
        ret.add("ReplacedBy");
        ret.add("ValidValues");
        ret.add("ReplacementTerms");
        ret.add("SeeAlso");
        ret.add("TranslatedTerms");
        ret.add("IsA");
        ret.add("Antonyms");
        ret.add("Classifies");
        ret.add("PreferredToTerms");
        ret.add("PreferredTerms");

        return String.join(", ", ret);
    }

    public void processTermRelations(AtlasGlossaryTerm storeObject, AtlasGlossaryTerm updatedTerm, RelationshipOperation op) throws AtlasBaseException {
        LOG.debug("==> GlossaryTermUtils.processTermRelations({}, {}, {})", storeObject, updatedTerm, op);

        processTermAnchor(storeObject, updatedTerm, op);
        processRelatedTerms(storeObject, updatedTerm, op);
        processAssociatedCategories(storeObject, updatedTerm, op);

        LOG.debug("<== GlossaryTermUtils.processTermRelations()");
    }

    public void processTermAssignments(AtlasGlossaryTerm glossaryTerm, Collection<AtlasRelatedObjectId> relatedObjectIds) throws AtlasBaseException {
        LOG.debug("==> GlossaryTermUtils.processTermAssignments({}, {})", glossaryTerm, relatedObjectIds);

        requireNonNull(glossaryTerm);

        Set<AtlasRelatedObjectId> assignedEntities = glossaryTerm.getAssignedEntities();

        for (AtlasRelatedObjectId objectId : relatedObjectIds) {
            if (CollectionUtils.isNotEmpty(assignedEntities) && assignedEntities.contains(objectId)) {
                LOG.debug("Skipping already assigned entity {}", objectId);

                continue;
            }

            LOG.debug("Assigning term guid={}, to entity guid = {}", glossaryTerm.getGuid(), objectId.getGuid());

            createRelationship(defineTermAssignment(glossaryTerm.getGuid(), objectId));
            AtlasVertex vertex = AtlasGraphUtilsV2.findByGuid(objectId.getGuid());
            updateModificationMetadata(vertex);
        }

        LOG.debug("<== GlossaryTermUtils.processTermAssignments()");
    }

    public void processTermDissociation(AtlasGlossaryTerm glossaryTerm, Collection<AtlasRelatedObjectId> relatedObjectIds) throws AtlasBaseException {
        LOG.debug("==> GlossaryTermUtils.processTermDissociation({}, {}, {})", glossaryTerm.getGuid(), relatedObjectIds, glossaryTerm);

        requireNonNull(glossaryTerm);

        Set<AtlasRelatedObjectId>         assignedEntities  = glossaryTerm.getAssignedEntities();
        Map<String, AtlasRelatedObjectId> assignedEntityMap = new HashMap<>();

        if (CollectionUtils.isNotEmpty(assignedEntities)) {
            for (AtlasRelatedObjectId relatedObjectId : assignedEntities) {
                assignedEntityMap.put(relatedObjectId.getGuid(), relatedObjectId);
            }
        }

        if (CollectionUtils.isNotEmpty(relatedObjectIds)) {
            for (AtlasRelatedObjectId relatedObjectId : relatedObjectIds) {
                LOG.debug("Removing term guid={}, from entity guid = {}", glossaryTerm.getGuid(), relatedObjectId.getGuid());

                if (isNull(relatedObjectId.getRelationshipGuid())) {
                    throw new AtlasBaseException(AtlasErrorCode.TERM_DISSOCIATION_MISSING_RELATION_GUID);
                }

                AtlasRelatedObjectId existingTermRelation = assignedEntityMap.get(relatedObjectId.getGuid());

                if (CollectionUtils.isNotEmpty(assignedEntities) && isRelationshipGuidSame(existingTermRelation, relatedObjectId)) {
                    relationshipStore.deleteById(relatedObjectId.getRelationshipGuid(), true);
                    AtlasVertex vertex = AtlasGraphUtilsV2.findByGuid(relatedObjectId.getGuid());
                    updateModificationMetadata(vertex);
                } else {
                    throw new AtlasBaseException(AtlasErrorCode.INVALID_TERM_DISSOCIATION, relatedObjectId.getRelationshipGuid(), glossaryTerm.getGuid(), relatedObjectId.getGuid());
                }
            }
        }

        LOG.debug("<== GlossaryTermUtils.processTermDissociation()");
    }

    public void clearImportCache() {
        GLOSSARY_NAME_GUID_CACHE.get().clear();
        GLOSSARY_TERM_ORDER_CACHE.get().clear();
        GLOSSARY_TERM_Q_NAME_GUID_CACHE.get().clear();
    }

    public void updateGlossaryTermRelations(AtlasGlossaryTerm updatedGlossaryTerm) {
        if (GLOSSARY_TERM_Q_NAME_GUID_CACHE.get().containsKey(updatedGlossaryTerm.getQualifiedName())) {
            try {
                AtlasGlossaryTerm glossaryTermFromDB = getGlossaryTem(GLOSSARY_TERM_Q_NAME_GUID_CACHE.get().get(updatedGlossaryTerm.getQualifiedName()));

                copyRelations(updatedGlossaryTerm, glossaryTermFromDB);
            } catch (AtlasBaseException e) {
                LOG.debug("Error occurred while loading glossary Term", e);
            }
        }
    }

    protected List<AtlasGlossaryTerm> getGlossaryTermDataWithoutRelations(List<String[]> fileData, BulkImportResponse bulkImportResponse) throws AtlasBaseException {
        List<AtlasGlossaryTerm> glossaryTerms = new ArrayList<>();
        int                     rowCount      = 1;

        for (String[] record : fileData) {
            List<String>      failedTermMsgs = new ArrayList<>();
            AtlasGlossaryTerm glossaryTerm   = new AtlasGlossaryTerm();
            String            glossaryName   = StringUtils.EMPTY;

            if (ArrayUtils.isEmpty(record) || StringUtils.isBlank(record[INDEX_FOR_GLOSSARY_AT_RECORD])) {
                LOG.error("The GlossaryName is blank for the record : {}", Arrays.toString(record));

                failedTermMsgs.add("The GlossaryName is blank for the record : " + Arrays.toString(record));
            } else {
                glossaryName = record[INDEX_FOR_GLOSSARY_AT_RECORD];

                String glossaryGuid = GLOSSARY_NAME_GUID_CACHE.get().get(glossaryName);

                if (StringUtils.isEmpty(glossaryGuid)) {
                    glossaryGuid = getGlossaryGUIDFromGraphDB(glossaryName);

                    if (StringUtils.isEmpty(glossaryGuid)) {
                        glossaryGuid = createGlossary(glossaryName, failedTermMsgs);
                    }

                    GLOSSARY_NAME_GUID_CACHE.get().put(glossaryName, glossaryGuid);
                }

                if (StringUtils.isNotEmpty(glossaryGuid)) {
                    glossaryTerm = populateGlossaryTermObject(failedTermMsgs, record, glossaryGuid, false);

                    glossaryTerm.setQualifiedName(getGlossaryTermQualifiedName(glossaryTerm.getName(), glossaryName));

                    GLOSSARY_TERM_ORDER_CACHE.get().put(glossaryTerm.getQualifiedName(), rowCount);

                    glossaryTerms.add(glossaryTerm);
                }
            }

            if (!failedTermMsgs.isEmpty()) {
                String failedTermMsg    = StringUtils.join(failedTermMsgs, System.lineSeparator());
                String glossaryTermName = glossaryTerm.getName();

                bulkImportResponse.addToFailedImportInfoList(new ImportInfo(glossaryName, glossaryTermName, FAILED, failedTermMsg, rowCount));
            }

            rowCount++;
        }

        return glossaryTerms;
    }

    protected List<AtlasGlossaryTerm> getGlossaryTermDataWithRelations(List<String[]> fileData, BulkImportResponse bulkImportResponse) {
        List<AtlasGlossaryTerm> glossaryTerms = new ArrayList<>();
        int                     rowCount      = 1;

        for (String[] record : fileData) {
            List<String> failedTermMsgs = new ArrayList<>();

            if (ArrayUtils.isNotEmpty(record) && StringUtils.isNotBlank(record[INDEX_FOR_GLOSSARY_AT_RECORD])) {
                AtlasGlossaryTerm glossaryTerm = new AtlasGlossaryTerm();
                String            glossaryName = record[INDEX_FOR_GLOSSARY_AT_RECORD];
                String            glossaryGuid = GLOSSARY_NAME_GUID_CACHE.get().get(glossaryName);

                if (StringUtils.isNotEmpty(glossaryGuid)) {
                    glossaryTerm = populateGlossaryTermObject(failedTermMsgs, record, glossaryGuid, true);

                    glossaryTerm.setQualifiedName(getGlossaryTermQualifiedName(glossaryTerm.getName(), glossaryName));

                    glossaryTerms.add(glossaryTerm);
                }

                if (!failedTermMsgs.isEmpty()) {
                    String failedTermMsg    = StringUtils.join(failedTermMsgs, System.lineSeparator());
                    String glossaryTermName = glossaryTerm.getName();

                    bulkImportResponse.addToFailedImportInfoList(new ImportInfo(glossaryName, glossaryTermName, FAILED, failedTermMsg, rowCount));
                }
            }

            rowCount++;
        }

        return glossaryTerms;
    }

    protected Map<String, Object> getMapValue(String csvRecord, List<String> failedTermMsgs, boolean populateRelations) {
        Map<String, Object> ret = null;

        if (StringUtils.isNotBlank(csvRecord)) {
            ret = new HashMap<>();

            String[] csvRecordArray = csvRecord.split(FileUtils.ESCAPE_CHARACTER + FileUtils.PIPE_CHARACTER);

            for (String record : csvRecordArray) {
                String[] recordArray = record.split(FileUtils.COLON_CHARACTER);

                if ((recordArray.length % 2) == 0) {
                    ret.put(recordArray[0], recordArray[1]);
                } else if (!populateRelations) {
                    failedTermMsgs.add("The Data in the uploaded file is incorrectly specified  : " + csvRecord + System.lineSeparator() + "AdditionalAttributes needs to be a key:value pair");
                }
            }
        }

        return ret;
    }

    protected List<String> getListValue(String csvRecord) {
        List<String> ret = null;

        if (StringUtils.isNotBlank(csvRecord)) {
            ret = Arrays.asList(csvRecord.split(FileUtils.ESCAPE_CHARACTER + FileUtils.PIPE_CHARACTER));
        }

        return ret;
    }

    protected Set<AtlasRelatedTermHeader> getAtlasRelatedTermHeaderSet(String csvRecord, String termName, String glossaryName, List<String> failedTermMsgs) {
        Set<AtlasRelatedTermHeader> ret = null;

        if (StringUtils.isNotBlank(csvRecord)) {
            ret = new HashSet<>();

            String[] csvRecordArray = csvRecord.split(FileUtils.ESCAPE_CHARACTER + FileUtils.PIPE_CHARACTER);

            AtlasRelatedTermHeader relatedTermHeader;

            for (String data : csvRecordArray) {
                String[] dataArray = data.split(FileUtils.ESCAPE_CHARACTER + FileUtils.COLON_CHARACTER);

                if (dataArray.length == 2) {
                    String relatedTermQualifiedName = dataArray[1] + INVALID_NAME_CHARS[0] + dataArray[0];
                    String currTermQualifiedName    = termName + INVALID_NAME_CHARS[0] + glossaryName;

                    if (relatedTermQualifiedName.equalsIgnoreCase(currTermQualifiedName)) {
                        failedTermMsgs.add("Invalid relationship specified for Term. Term cannot have a relationship with self");
                    } else {
                        AtlasVertex vertex = AtlasGraphUtilsV2.findByTypeAndUniquePropertyName(GlossaryUtils.ATLAS_GLOSSARY_TERM_TYPENAME, GlossaryUtils.ATLAS_GLOSSARY_TERM_TYPENAME + INVALID_NAME_CHARS[1] + QUALIFIED_NAME_ATTR, relatedTermQualifiedName);

                        if (vertex != null) {
                            String glossaryTermGuid = AtlasGraphUtilsV2.getIdFromVertex(vertex);

                            relatedTermHeader = new AtlasRelatedTermHeader();

                            relatedTermHeader.setTermGuid(glossaryTermGuid);

                            cacheRelatedTermQNameGuid(currTermQualifiedName, relatedTermQualifiedName, glossaryTermGuid);

                            ret.add(relatedTermHeader);
                        } else {
                            failedTermMsgs.add("The provided Reference " + dataArray[1] + "@" + dataArray[0] + " does not exist at Atlas referred at record with TermName  : " + termName + " and GlossaryName : " + glossaryName);
                        }
                    }
                } else {
                    failedTermMsgs.add("Incorrect relation data specified for the term : " + termName + "@" + glossaryName);
                }
            }
        }

        return ret;
    }

    protected AtlasGlossaryTerm populateGlossaryTermObject(List<String> failedTermMsgList, String[] record, String glossaryGuid, boolean populateRelations) {
        int               length = record.length;
        int               i      = INDEX_FOR_TERM_AT_RECORD;
        AtlasGlossaryTerm ret    = new AtlasGlossaryTerm();

        if (length > i) {
            ret.setName(record[i]);
        }

        if (StringUtils.isBlank(ret.getName())) {
            if (!populateRelations) {
                failedTermMsgList.add("The TermName is blank for provided record: " + Arrays.toString(record));
            }
        } else {
            ret.setAnchor(new AtlasGlossaryHeader(glossaryGuid));

            ret.setShortDescription((length > ++i) ? record[i] : null);

            ret.setLongDescription((length > ++i) ? record[i] : null);

            ret.setExamples((length > ++i) ? getListValue(record[i]) : null);

            ret.setAbbreviation((length > ++i) ? record[i] : null);

            ret.setUsage((length > ++i) ? record[i] : null);

            ret.setAdditionalAttributes(((length > ++i) ? getMapValue(record[i], failedTermMsgList, populateRelations) : null));

            if (populateRelations) {
                ret.setTranslationTerms((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);

                ret.setValidValuesFor((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);

                ret.setSynonyms((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);

                ret.setReplacedBy((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);

                ret.setValidValues((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);

                ret.setReplacementTerms((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);

                ret.setSeeAlso((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);

                ret.setTranslatedTerms((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);

                ret.setIsA((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);

                ret.setAntonyms((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);

                ret.setClassifies((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);

                ret.setPreferredToTerms((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);

                ret.setPreferredTerms((length > ++i) ? getAtlasRelatedTermHeaderSet(record[i], ret.getName(), record[INDEX_FOR_GLOSSARY_AT_RECORD], failedTermMsgList) : null);
            }
        }

        return ret;
    }

    private boolean isRelationshipGuidSame(AtlasRelatedObjectId storeObject, AtlasRelatedObjectId relatedObjectId) {
        return StringUtils.equals(relatedObjectId.getRelationshipGuid(), storeObject.getRelationshipGuid());
    }

    private void processTermAnchor(AtlasGlossaryTerm currentTerm, AtlasGlossaryTerm updatedTerm, RelationshipOperation op) throws AtlasBaseException {
        if (isNull(updatedTerm.getAnchor()) && op != RelationshipOperation.DELETE) {
            throw new AtlasBaseException(AtlasErrorCode.MISSING_MANDATORY_ANCHOR);
        }

        AtlasGlossaryHeader currentTermGlossary     = currentTerm.getAnchor(); // glossary_g1
        AtlasGlossaryHeader updatedTermGlossary     = updatedTerm.getAnchor(); // glossary_g2
        String              updatedTermGlossaryGuid = updatedTermGlossary.getGlossaryGuid();
        String              currentTermGlossaryGuid = currentTermGlossary.getGlossaryGuid();

        switch (op) {
            case CREATE:
                if (isNull(updatedTermGlossaryGuid)) {
                    throw new AtlasBaseException(AtlasErrorCode.INVALID_NEW_ANCHOR_GUID);
                } else {
                    LOG.debug("Creating new term anchor, category = {}, glossary = {}", currentTerm.getGuid(), updatedTerm.getAnchor().getGlossaryGuid());

                    if (!StringUtils.equals(updatedTermGlossaryGuid, currentTermGlossaryGuid)) {
                        createRelationship(defineTermAnchorRelation(updatedTermGlossaryGuid, currentTerm.getGuid()));
                    }
                }
                break;
            case UPDATE:
                if (!Objects.equals(updatedTermGlossary, currentTermGlossary)) {
                    if (isNull(updatedTermGlossaryGuid)) {
                        throw new AtlasBaseException(AtlasErrorCode.INVALID_NEW_ANCHOR_GUID);
                    }

                    LOG.debug("Updating term anchor, currAnchor = {}, newAnchor = {} and term = {}", currentTermGlossaryGuid, updatedTermGlossaryGuid, currentTerm.getName());

                    relationshipStore.deleteById(currentTermGlossary.getRelationGuid(), true);

                    // Derive the qualifiedName when anchor changes
                    String        anchorGlossaryGuid = updatedTermGlossaryGuid;
                    AtlasGlossary glossary           = dataAccess.load(getGlossarySkeleton(anchorGlossaryGuid));
                    currentTerm.setQualifiedName(currentTerm.getName() + "@" + glossary.getQualifiedName());

                    LOG.debug("Derived qualifiedName = {}", currentTerm.getQualifiedName());

                    createRelationship(defineTermAnchorRelation(updatedTermGlossaryGuid, currentTerm.getGuid()));
                }
                break;
            case DELETE:
                if (nonNull(currentTermGlossary)) {
                    LOG.debug("Deleting term anchor");

                    relationshipStore.deleteById(currentTermGlossary.getRelationGuid(), true);
                }
                break;
        }
    }

    private void processRelatedTerms(AtlasGlossaryTerm storeObject, AtlasGlossaryTerm updatedTerm, RelationshipOperation op) throws AtlasBaseException {
        Map<AtlasGlossaryTerm.Relation, Set<AtlasRelatedTermHeader>> newRelatedTerms      = updatedTerm.getRelatedTerms();
        Map<AtlasGlossaryTerm.Relation, Set<AtlasRelatedTermHeader>> existingRelatedTerms = storeObject.getRelatedTerms();

        switch (op) {
            case CREATE:
                for (Map.Entry<AtlasGlossaryTerm.Relation, Set<AtlasRelatedTermHeader>> entry : newRelatedTerms.entrySet()) {
                    AtlasGlossaryTerm.Relation  relation = entry.getKey();
                    Set<AtlasRelatedTermHeader> terms    = entry.getValue();

                    if (nonNull(terms)) {
                        LOG.debug("{} relation {} for term = {}", op, relation, storeObject.getGuid());
                        LOG.debug("Related Term count = {}", terms.size());

                        createTermRelationships(storeObject, relation, terms);
                    }
                }
                break;
            case UPDATE:
                for (AtlasGlossaryTerm.Relation relation : AtlasGlossaryTerm.Relation.values()) {
                    Map<String, AtlasRelatedTermHeader> existingTermHeaders = getRelatedTermHeaders(existingRelatedTerms, relation);
                    Map<String, AtlasRelatedTermHeader> newTermHeaders      = getRelatedTermHeaders(newRelatedTerms, relation);

                    // No existing term relations, create all
                    if (MapUtils.isEmpty(existingTermHeaders)) {
                        LOG.debug("Creating new term relations, relation = {}, terms = {}", relation, nonNull(newTermHeaders) ? newTermHeaders.size() : "none");

                        createTermRelationships(storeObject, relation, newTermHeaders.values());
                        continue;
                    }

                    // Existing term relations but nothing in updated object, remove all
                    if (MapUtils.isEmpty(newTermHeaders)) {
                        LOG.debug("Deleting existing term relations, relation = {}, terms = {}", relation, existingTermHeaders.size());

                        deleteTermRelationships(relation, existingTermHeaders.values());
                        continue;
                    }

                    // Determine what to update, delete or create
                    Set<AtlasRelatedTermHeader> toCreate = newTermHeaders
                            .values()
                            .stream()
                            .filter(t -> !existingTermHeaders.containsKey(t.getTermGuid()))
                            .collect(Collectors.toSet());

                    Set<AtlasRelatedTermHeader> toUpdate = newTermHeaders
                            .values()
                            .stream()
                            .filter(t -> updatedExistingTermRelation(existingTermHeaders, t))
                            .collect(Collectors.toSet());

                    Set<AtlasRelatedTermHeader> toDelete = existingTermHeaders
                            .values()
                            .stream()
                            .filter(t -> !newTermHeaders.containsKey(t.getTermGuid()))
                            .collect(Collectors.toSet());

                    createTermRelationships(storeObject, relation, toCreate);
                    updateTermRelationships(relation, toUpdate);
                    deleteTermRelationships(relation, toDelete);
                }
                break;
            case DELETE:
                for (AtlasGlossaryTerm.Relation relation : AtlasGlossaryTerm.Relation.values()) {
                    // No existing term relations, create all
                    Set<AtlasRelatedTermHeader> existingTermHeaders = existingRelatedTerms.get(relation);

                    deleteTermRelationships(relation, existingTermHeaders);
                }
                break;
        }
    }

    private Map<String, AtlasRelatedTermHeader> getRelatedTermHeaders(Map<AtlasGlossaryTerm.Relation, Set<AtlasRelatedTermHeader>> relatedTerms, AtlasGlossaryTerm.Relation relation) {
        if (nonNull(relatedTerms.get(relation))) {
            Map<String, AtlasRelatedTermHeader> map = new HashMap<>();

            for (AtlasRelatedTermHeader t : relatedTerms.get(relation)) {
                AtlasRelatedTermHeader header = map.get(t.getTermGuid());

                if (header == null || (StringUtils.isEmpty(header.getRelationGuid()) && StringUtils.isNotEmpty(t.getRelationGuid()))) {
                    map.put(t.getTermGuid(), t);
                }
            }

            return map;
        } else {
            return Collections.emptyMap();
        }
    }

    private boolean updatedExistingTermRelation(Map<String, AtlasRelatedTermHeader> existingTermHeaders, AtlasRelatedTermHeader header) {
        return nonNull(header.getRelationGuid()) && !header.equals(existingTermHeaders.get(header.getTermGuid()));
    }

    private void processAssociatedCategories(AtlasGlossaryTerm storeObject, AtlasGlossaryTerm updatedTerm, RelationshipOperation op) throws AtlasBaseException {
        Map<String, AtlasTermCategorizationHeader> newCategories      = getAssociatedCategories(updatedTerm);
        Map<String, AtlasTermCategorizationHeader> existingCategories = getAssociatedCategories(storeObject);

        switch (op) {
            case CREATE:
                if (nonNull(newCategories)) {
                    LOG.debug("Creating new term categorization, term = {}, categories = {}", storeObject.getGuid(), newCategories.size());

                    createTermCategorizationRelationships(storeObject, newCategories.values());
                }
                break;
            case UPDATE:
                // If no existing categories are present then create all existing ones
                if (MapUtils.isEmpty(existingCategories)) {
                    LOG.debug("Creating new term categorization, term = {}, categories = {}", storeObject.getGuid(), nonNull(newCategories) ? newCategories.size() : "none");

                    createTermCategorizationRelationships(storeObject, newCategories.values());
                    break;
                }

                // If no new categories are present then delete all existing ones
                if (MapUtils.isEmpty(newCategories)) {
                    LOG.debug("Deleting term categorization, term = {}, categories = {}", storeObject.getGuid(), existingCategories.size());

                    deleteCategorizationRelationship(existingCategories.values());
                    break;
                }

                Set<AtlasTermCategorizationHeader> toCreate = newCategories
                        .values()
                        .stream()
                        .filter(c -> !existingCategories.containsKey(c.getCategoryGuid()))
                        .collect(Collectors.toSet());

                createTermCategorizationRelationships(storeObject, toCreate);

                Set<AtlasTermCategorizationHeader> toUpdate = newCategories
                        .values()
                        .stream()
                        .filter(c -> updatedExistingCategorizationRelation(existingCategories, c))
                        .collect(Collectors.toSet());

                updateTermCategorizationRelationships(storeObject, toUpdate);

                Set<AtlasTermCategorizationHeader> toDelete = existingCategories
                        .values()
                        .stream()
                        .filter(c -> !newCategories.containsKey(c.getCategoryGuid()))
                        .collect(Collectors.toSet());

                deleteCategorizationRelationship(toDelete);
                break;
            case DELETE:
                deleteCategorizationRelationship(existingCategories.values());
                break;
        }
    }

    private boolean updatedExistingCategorizationRelation(Map<String, AtlasTermCategorizationHeader> existingCategories, AtlasTermCategorizationHeader header) {
        return nonNull(header.getRelationGuid()) && !header.equals(existingCategories.get(header.getCategoryGuid()));
    }

    private Map<String, AtlasTermCategorizationHeader> getAssociatedCategories(AtlasGlossaryTerm term) {
        if (nonNull(term.getCategories())) {
            Map<String, AtlasTermCategorizationHeader> map = new HashMap<>();

            for (AtlasTermCategorizationHeader c : term.getCategories()) {
                AtlasTermCategorizationHeader header = map.get(c.getCategoryGuid());

                if (header == null || (StringUtils.isEmpty(header.getRelationGuid()) && StringUtils.isNotEmpty(c.getRelationGuid()))) {
                    map.put(c.getCategoryGuid(), c);
                }
            }

            return map;
        } else {
            return Collections.emptyMap();
        }
    }

    private void createTermCategorizationRelationships(AtlasGlossaryTerm storeObject, Collection<AtlasTermCategorizationHeader> categories) throws AtlasBaseException {
        if (CollectionUtils.isNotEmpty(categories)) {
            Set<AtlasTermCategorizationHeader> existingCategories = storeObject.getCategories();

            for (AtlasTermCategorizationHeader categorizationHeader : categories) {
                if (nonNull(existingCategories) && existingCategories.contains(categorizationHeader)) {
                    LOG.debug("Skipping existing category guid={}", categorizationHeader.getCategoryGuid());

                    continue;
                }

                LOG.debug("Creating relation between term = {} and category = {}", storeObject.getGuid(), categorizationHeader.getDisplayText());

                createRelationship(defineCategorizedTerm(categorizationHeader, storeObject.getGuid()));
            }
        }
    }

    private void updateTermCategorizationRelationships(AtlasGlossaryTerm storeObject, Collection<AtlasTermCategorizationHeader> toUpdate) throws AtlasBaseException {
        if (CollectionUtils.isNotEmpty(toUpdate)) {
            for (AtlasTermCategorizationHeader categorizationHeader : toUpdate) {
                LOG.debug("Updating relation between term = {} and category = {}", storeObject.getGuid(), categorizationHeader.getDisplayText());

                AtlasRelationship relationship = relationshipStore.getById(categorizationHeader.getRelationGuid());

                updateRelationshipAttributes(relationship, categorizationHeader);

                relationshipStore.update(relationship);
            }
        }
    }

    private void deleteCategorizationRelationship(Collection<AtlasTermCategorizationHeader> existingCategories) throws AtlasBaseException {
        if (CollectionUtils.isNotEmpty(existingCategories)) {
            for (AtlasTermCategorizationHeader categorizationHeader : existingCategories) {
                LOG.debug("Deleting relation guid = {}, text = {}", categorizationHeader.getRelationGuid(), categorizationHeader.getDisplayText());

                relationshipStore.deleteById(categorizationHeader.getRelationGuid(), true);
            }
        }
    }

    private void createTermRelationships(AtlasGlossaryTerm storeObject, AtlasGlossaryTerm.Relation relation, Collection<AtlasRelatedTermHeader> terms) throws AtlasBaseException {
        if (CollectionUtils.isNotEmpty(terms)) {
            Map<String, AtlasRelatedTermHeader> existingRelations;

            if (nonNull(storeObject.getRelatedTerms()) && nonNull(storeObject.getRelatedTerms().get(relation))) {
                Map<String, AtlasRelatedTermHeader> map = new HashMap<>();

                for (AtlasRelatedTermHeader t : storeObject.getRelatedTerms().get(relation)) {
                    AtlasRelatedTermHeader header = map.get(t.getTermGuid());

                    if (header == null || (StringUtils.isEmpty(header.getRelationGuid()) && StringUtils.isNotEmpty(t.getRelationGuid()))) {
                        map.put(t.getTermGuid(), t);
                    }
                }

                existingRelations = map;
            } else {
                existingRelations = Collections.emptyMap();
            }

            for (AtlasRelatedTermHeader term : terms) {
                if (existingRelations.containsKey(term.getTermGuid())) {
                    LOG.debug("Skipping existing term relation termGuid={}", term.getTermGuid());
                    continue;
                }

                if (storeObject.getGuid().equals(term.getTermGuid())) {
                    throw new AtlasBaseException(AtlasErrorCode.INVALID_TERM_RELATION_TO_SELF);
                }

                LOG.debug("Creating new term relation = {}, terms = {}", relation, term.getDisplayText());

                createRelationship(defineTermRelation(relation, storeObject.getGuid(), term));
            }
        }
    }

    private void updateTermRelationships(AtlasGlossaryTerm.Relation relation, Collection<AtlasRelatedTermHeader> terms) throws AtlasBaseException {
        if (CollectionUtils.isNotEmpty(terms)) {
            for (AtlasRelatedTermHeader term : terms) {
                LOG.debug("Updating term relation = {}, terms = {}", relation, term.getDisplayText());

                AtlasRelationship relationship = relationshipStore.getById(term.getRelationGuid());
                updateRelationshipAttributes(relationship, term);
                relationshipStore.update(relationship);
            }
        }
    }

    private void deleteTermRelationships(AtlasGlossaryTerm.Relation relation, Collection<AtlasRelatedTermHeader> terms) throws AtlasBaseException {
        if (CollectionUtils.isNotEmpty(terms)) {
            for (AtlasRelatedTermHeader termHeader : terms) {
                LOG.debug("Deleting term relation = {}, terms = {}", relation, termHeader.getDisplayText());

                relationshipStore.deleteById(termHeader.getRelationGuid(), true);
            }
        }
    }

    private AtlasRelationship defineTermAnchorRelation(String glossaryGuid, String termGuid) {
        AtlasRelationshipType relationshipType = typeRegistry.getRelationshipTypeByName(TERM_ANCHOR);
        AtlasStruct           defaultAttrs     = relationshipType.createDefaultValue();

        return new AtlasRelationship(TERM_ANCHOR, new AtlasObjectId(glossaryGuid), new AtlasObjectId(termGuid), defaultAttrs.getAttributes());
    }

    private AtlasRelationship defineTermRelation(AtlasGlossaryTerm.Relation relation, String end1TermGuid, AtlasRelatedTermHeader end2RelatedTerm) {
        AtlasRelationshipType relationshipType = typeRegistry.getRelationshipTypeByName(relation.getName());
        AtlasStruct           defaultAttrs     = relationshipType.createDefaultValue();
        AtlasRelationship     relationship;

        // End1 and End2 ObjectIds depend on the attribute
        if (relation.isEnd2Attr()) {
            relationship = new AtlasRelationship(relation.getName(), new AtlasObjectId(end2RelatedTerm.getTermGuid()), new AtlasObjectId(end1TermGuid), defaultAttrs.getAttributes());
        } else {
            relationship = new AtlasRelationship(relation.getName(), new AtlasObjectId(end1TermGuid), new AtlasObjectId(end2RelatedTerm.getTermGuid()), defaultAttrs.getAttributes());
        }

        updateRelationshipAttributes(relationship, end2RelatedTerm);

        return relationship;
    }

    private AtlasRelationship defineCategorizedTerm(AtlasTermCategorizationHeader relatedCategoryId, String termId) {
        AtlasRelationshipType relationshipType = typeRegistry.getRelationshipTypeByName(TERM_CATEGORIZATION);
        AtlasStruct           defaultAttrs     = relationshipType.createDefaultValue();
        AtlasRelationship     relationship     = new AtlasRelationship(TERM_CATEGORIZATION, new AtlasObjectId(relatedCategoryId.getCategoryGuid()), new AtlasObjectId(termId), defaultAttrs.getAttributes());

        updateRelationshipAttributes(relationship, relatedCategoryId);

        return relationship;
    }

    private AtlasRelationship defineTermAssignment(String termGuid, AtlasRelatedObjectId relatedObjectId) {
        AtlasRelationshipType relationshipType = typeRegistry.getRelationshipTypeByName(TERM_ASSIGNMENT);
        AtlasStruct           defaultAttrs     = relationshipType.createDefaultValue();
        AtlasObjectId         end1             = new AtlasObjectId(termGuid);
        AtlasRelationship     relationship     = new AtlasRelationship(TERM_ASSIGNMENT, end1, relatedObjectId, defaultAttrs.getAttributes());

        updateRelationshipAttributes(relationship, relatedObjectId);

        return relationship;
    }

    private void updateRelationshipAttributes(AtlasRelationship relationship, AtlasTermCategorizationHeader categorizationHeader) {
        if (nonNull(relationship)) {
            relationship.setAttribute(TERM_RELATION_ATTR_DESCRIPTION, categorizationHeader.getDescription());

            if (nonNull(categorizationHeader.getStatus())) {
                relationship.setAttribute(TERM_RELATION_ATTR_STATUS, categorizationHeader.getStatus().name());
            }
        }
    }

    private void updateRelationshipAttributes(AtlasRelationship relationship, AtlasRelatedObjectId relatedObjectId) {
        AtlasStruct relationshipAttributes = relatedObjectId.getRelationshipAttributes();

        if (nonNull(relationshipAttributes)) {
            for (Map.Entry<String, Object> attrEntry : relationshipAttributes.getAttributes().entrySet()) {
                relationship.setAttribute(attrEntry.getKey(), attrEntry.getValue());
            }
        }
    }

    private String getGlossaryTermQualifiedName(String glossaryTermName, String glossaryName) {
        return glossaryTermName + "@" + glossaryName;
    }

    private String getGlossaryGUIDFromGraphDB(String glossaryName) {
        AtlasVertex vertex = AtlasGraphUtilsV2.findByTypeAndUniquePropertyName(GlossaryUtils.ATLAS_GLOSSARY_TYPENAME, GlossaryUtils.ATLAS_GLOSSARY_TYPENAME + "." + QUALIFIED_NAME_ATTR, glossaryName);

        return (vertex != null) ? AtlasGraphUtilsV2.getIdFromVertex(vertex) : null;
    }

    private String createGlossary(String glossaryName, List<String> failedTermMsgs) throws AtlasBaseException {
        String ret = null;

        if (GlossaryService.isNameInvalid(glossaryName)) {
            LOG.error("The provided Glossary Name is invalid : {}", glossaryName);

            failedTermMsgs.add("The provided Glossary Name {" + glossaryName + "} is invalid : " + AtlasErrorCode.INVALID_DISPLAY_NAME.getFormattedErrorMessage());
        } else {
            AtlasGlossary glossary = new AtlasGlossary();

            glossary.setQualifiedName(glossaryName);
            glossary.setName(glossaryName);

            glossary = dataAccess.save(glossary);
            ret      = glossary.getGuid();
        }

        return ret;
    }

    private void cacheRelatedTermQNameGuid(String currTermQualifiedName, String relatedTermQualifiedName, String termGuid) {
        if (!GLOSSARY_TERM_Q_NAME_GUID_CACHE.get().containsKey(relatedTermQualifiedName) &&
                GLOSSARY_TERM_ORDER_CACHE.get().containsKey(currTermQualifiedName) &&
                GLOSSARY_TERM_ORDER_CACHE.get().containsKey(relatedTermQualifiedName) &&
                GLOSSARY_TERM_ORDER_CACHE.get().get(currTermQualifiedName) < GLOSSARY_TERM_ORDER_CACHE.get().get(relatedTermQualifiedName)) {
            GLOSSARY_TERM_Q_NAME_GUID_CACHE.get().put(relatedTermQualifiedName, termGuid);
        }
    }

    private AtlasGlossaryTerm getGlossaryTem(String termGuid) throws AtlasBaseException {
        AtlasGlossaryTerm ret = null;

        LOG.debug("==> GlossaryTemUtils.getGlossaryTem({})", termGuid);

        if (!isNull(termGuid)) {
            AtlasGlossaryTerm atlasGlossary = getAtlasGlossaryTermSkeleton(termGuid);

            ret = dataAccess.load(atlasGlossary);

            LOG.debug("<== GlossaryTemUtils.getGlossaryTem() : {}", ret);
        }
        return ret;
    }

    private void copyRelations(AtlasGlossaryTerm toGlossaryTerm, AtlasGlossaryTerm fromGlossaryTerm) {
        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getSeeAlso())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getSeeAlso())) {
                toGlossaryTerm.getSeeAlso().addAll(fromGlossaryTerm.getSeeAlso());
            } else {
                toGlossaryTerm.setSeeAlso(fromGlossaryTerm.getSeeAlso());
            }
        }

        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getSynonyms())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getSynonyms())) {
                toGlossaryTerm.getSynonyms().addAll(fromGlossaryTerm.getSynonyms());
            } else {
                toGlossaryTerm.setSynonyms(fromGlossaryTerm.getSynonyms());
            }
        }

        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getAntonyms())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getAntonyms())) {
                toGlossaryTerm.getAntonyms().addAll(fromGlossaryTerm.getAntonyms());
            } else {
                toGlossaryTerm.setAntonyms(fromGlossaryTerm.getAntonyms());
            }
        }

        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getPreferredTerms())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getPreferredTerms())) {
                toGlossaryTerm.getPreferredTerms().addAll(fromGlossaryTerm.getPreferredTerms());
            } else {
                toGlossaryTerm.setPreferredTerms(fromGlossaryTerm.getPreferredTerms());
            }
        }

        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getPreferredToTerms())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getPreferredToTerms())) {
                toGlossaryTerm.getPreferredToTerms().addAll(fromGlossaryTerm.getPreferredToTerms());
            } else {
                toGlossaryTerm.setPreferredToTerms(fromGlossaryTerm.getPreferredToTerms());
            }
        }

        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getReplacementTerms())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getReplacementTerms())) {
                toGlossaryTerm.getReplacementTerms().addAll(fromGlossaryTerm.getReplacementTerms());
            } else {
                toGlossaryTerm.setReplacementTerms(fromGlossaryTerm.getReplacementTerms());
            }
        }

        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getReplacedBy())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getReplacedBy())) {
                toGlossaryTerm.getReplacedBy().addAll(fromGlossaryTerm.getReplacedBy());
            } else {
                toGlossaryTerm.setReplacedBy(fromGlossaryTerm.getReplacedBy());
            }
        }

        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getTranslationTerms())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getTranslationTerms())) {
                toGlossaryTerm.getTranslationTerms().addAll(fromGlossaryTerm.getTranslationTerms());
            } else {
                toGlossaryTerm.setTranslationTerms(fromGlossaryTerm.getTranslationTerms());
            }
        }

        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getTranslatedTerms())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getTranslatedTerms())) {
                toGlossaryTerm.getTranslatedTerms().addAll(fromGlossaryTerm.getTranslatedTerms());
            } else {
                toGlossaryTerm.setTranslatedTerms(fromGlossaryTerm.getTranslatedTerms());
            }
        }

        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getIsA())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getIsA())) {
                toGlossaryTerm.getIsA().addAll(fromGlossaryTerm.getIsA());
            } else {
                toGlossaryTerm.setIsA(fromGlossaryTerm.getIsA());
            }
        }

        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getClassifies())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getClassifies())) {
                toGlossaryTerm.getClassifies().addAll(fromGlossaryTerm.getClassifies());
            } else {
                toGlossaryTerm.setClassifies(fromGlossaryTerm.getClassifies());
            }
        }

        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getValidValues())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getValidValues())) {
                toGlossaryTerm.getValidValues().addAll(fromGlossaryTerm.getValidValues());
            } else {
                toGlossaryTerm.setValidValues(fromGlossaryTerm.getValidValues());
            }
        }

        if (CollectionUtils.isNotEmpty(fromGlossaryTerm.getValidValuesFor())) {
            if (CollectionUtils.isNotEmpty(toGlossaryTerm.getValidValuesFor())) {
                toGlossaryTerm.getValidValuesFor().addAll(fromGlossaryTerm.getValidValuesFor());
            } else {
                toGlossaryTerm.setValidValuesFor(fromGlossaryTerm.getValidValuesFor());
            }
        }
    }
}
