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

import org.apache.atlas.AtlasException;
import org.apache.atlas.model.discovery.AtlasAggregationEntry;
import org.apache.atlas.model.discovery.SearchParameters;
import org.apache.atlas.repository.Constants;
import org.apache.atlas.repository.graphdb.AggregationContext;
import org.apache.atlas.repository.graphdb.AtlasGraph;
import org.apache.atlas.repository.graphdb.AtlasGraphIndexClient;
import org.apache.atlas.type.AtlasEntityType;
import org.apache.atlas.type.AtlasStructType.AtlasAttribute;
import org.apache.atlas.type.AtlasTypeRegistry;
import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public class SearchAggregatorImpl implements SearchAggregator {
    private static final Logger LOG = LoggerFactory.getLogger(SearchAggregatorImpl.class);

    private final SearchContext searchContext;

    public SearchAggregatorImpl(SearchContext searchContext) {
        this.searchContext = searchContext;
    }

    public Map<String, List<AtlasAggregationEntry>> getAggregatedMetrics(Set<String> aggregationFields, Set<AtlasAttribute> aggregationAttributes) {
        SearchParameters    searchParameters = searchContext.getSearchParameters();
        AtlasGraph          graph            = searchContext.getGraph();
        AtlasTypeRegistry   typeRegistry     = searchContext.getTypeRegistry();
        String              queryString      = searchParameters.getQuery();
        List<PostProcessor> postProcessors   = new ArrayList<>();

        postProcessors.add(new ServiceTypeAggregator(searchContext.getTypeRegistry()));

        try {
            AtlasGraphIndexClient graphIndexClient    = graph.getGraphIndexClient();
            Set<AtlasEntityType>  searchForEntityType = searchContext.getEntityTypes();

            Map<String, String> indexFieldNameCache = new HashMap<>();

            for (String fieldName : aggregationFields) {
                String indexFieldName = getIndexFieldNameForCommonFieldName(typeRegistry, fieldName);

                indexFieldNameCache.put(fieldName, indexFieldName);
            }

            for (AtlasAttribute attribute : aggregationAttributes) {
                String indexFieldName = attribute.getIndexFieldName();

                if (indexFieldName == null) {
                    //there is no index field name.
                    indexFieldName = attribute.getQualifiedName();
                }

                indexFieldNameCache.put(attribute.getQualifiedName(), indexFieldName);
            }

            AggregationContext aggregatorContext = new AggregationContext(queryString, searchParameters.getEntityFilters(), searchForEntityType,
                    aggregationFields, aggregationAttributes, indexFieldNameCache, searchParameters.getExcludeDeletedEntities(), searchParameters.getIncludeSubTypes());

            Map<String, List<AtlasAggregationEntry>> aggregatedMetrics = graphIndexClient.getAggregatedMetrics(aggregatorContext);

            for (String aggregationMetricName : aggregatedMetrics.keySet()) {
                for (PostProcessor postProcessor : postProcessors) {
                    if (postProcessor.needsProcessing(aggregationMetricName)) {
                        postProcessor.prepareForMetric(aggregationMetricName);

                        for (AtlasAggregationEntry aggregationEntry : aggregatedMetrics.get(aggregationMetricName)) {
                            postProcessor.process(aggregationEntry);
                        }

                        postProcessor.handleMetricCompletion(aggregationMetricName);
                    }
                }
            }

            for (PostProcessor postProcessor : postProcessors) {
                postProcessor.handleCompletion(aggregatedMetrics);
            }

            return aggregatedMetrics;
        } catch (AtlasException e) {
            LOG.error("Error encountered in post processing stage of aggrgation metrics collection. Empty metrics will be returned.", e);

            return Collections.emptyMap();
        }
    }

    private String getIndexFieldNameForCommonFieldName(AtlasTypeRegistry typeRegistry, String fieldName) {
        String indexFieldName = typeRegistry.getIndexFieldName(fieldName);

        if (indexFieldName != null) {
            return indexFieldName;
        }

        LOG.debug("Could not find index field name from type registry for attribute {}. Will use use the field name as is.", fieldName);

        return fieldName;
    }

    interface PostProcessor {
        boolean needsProcessing(String metricName);

        void prepareForMetric(String metricName);

        void process(AtlasAggregationEntry aggregationEntry);

        void handleMetricCompletion(String metricName);

        void handleCompletion(Map<String, List<AtlasAggregationEntry>> aggregatedMetrics);
    }

    static class ServiceTypeAggregator implements PostProcessor {
        private static final String SERVICE_TYPE = "ServiceType";

        private final AtlasTypeRegistry                  typeRegistry;
        private final List<AtlasAggregationEntry>        entries               = new ArrayList<>();
        private final Map<String, AtlasAggregationEntry> entityType2MetricsMap = new HashMap<>();

        public ServiceTypeAggregator(AtlasTypeRegistry typeRegistry) {
            this.typeRegistry = typeRegistry;
        }

        @Override
        public boolean needsProcessing(String metricName) {
            return Constants.ENTITY_TYPE_PROPERTY_KEY.equals(metricName);
        }

        @Override
        public void prepareForMetric(String metricName) {
            Map<String, AtlasAggregationEntry> serviceName2MetricsMap = new HashMap<>();

            //prepare the service map to aggregations
            for (String serviceName : typeRegistry.getAllServiceTypes()) {
                AtlasAggregationEntry serviceMetrics = new AtlasAggregationEntry(serviceName, 0);

                serviceName2MetricsMap.put(serviceName, serviceMetrics);

                entries.add(serviceMetrics);
            }

            //prepare the map from entity type to aggregations
            for (AtlasEntityType entityType : typeRegistry.getAllEntityTypes()) {
                String serviceName = entityType.getServiceType();

                entityType2MetricsMap.put(entityType.getTypeName(), serviceName2MetricsMap.get(serviceName));
            }
        }

        @Override
        public void process(AtlasAggregationEntry aggregationEntryForType) {
            String                entityType                      = aggregationEntryForType.getName();
            AtlasAggregationEntry atlasAggregationEntryForService = entityType2MetricsMap.get(entityType);

            //atlasAggregationEntryForService can be null--classifications for e.g.
            if (atlasAggregationEntryForService != null) {
                atlasAggregationEntryForService.setCount(atlasAggregationEntryForService.getCount() + aggregationEntryForType.getCount());
            }
        }

        @Override
        public void handleMetricCompletion(String metricName) {
            //do nothing
        }

        @Override
        public void handleCompletion(Map<String, List<AtlasAggregationEntry>> aggregatedMetrics) {
            //remove all zero count entries.
            for (int i = entries.size() - 1; i >= 0; i--) {
                if (entries.get(i).getCount() == 0) {
                    entries.remove(i);
                }
            }

            if (CollectionUtils.isNotEmpty(entries)) {
                aggregatedMetrics.put(SERVICE_TYPE, entries);
            }
        }
    }
}
