/*
 * 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
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.skywalking.oap.server.core.query;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.oap.server.core.CoreModule;
import org.apache.skywalking.oap.server.core.CoreModuleConfig;
import org.apache.skywalking.oap.server.core.analysis.IDManager;
import org.apache.skywalking.oap.server.core.analysis.Layer;
import org.apache.skywalking.oap.server.core.config.HierarchyDefinitionService;
import org.apache.skywalking.oap.server.core.hierarchy.instance.InstanceHierarchyRelationTraffic;
import org.apache.skywalking.oap.server.core.hierarchy.service.ServiceHierarchyRelationTraffic;
import org.apache.skywalking.oap.server.core.query.type.Attribute;
import org.apache.skywalking.oap.server.core.query.type.HierarchyInstanceRelation;
import org.apache.skywalking.oap.server.core.query.type.HierarchyRelatedInstance;
import org.apache.skywalking.oap.server.core.query.type.HierarchyRelatedService;
import org.apache.skywalking.oap.server.core.query.type.HierarchyServiceRelation;
import org.apache.skywalking.oap.server.core.query.type.InstanceHierarchy;
import org.apache.skywalking.oap.server.core.query.type.LayerLevel;
import org.apache.skywalking.oap.server.core.query.type.ServiceHierarchy;
import org.apache.skywalking.oap.server.core.query.type.ServiceInstance;
import org.apache.skywalking.oap.server.core.storage.StorageModule;
import org.apache.skywalking.oap.server.core.storage.query.IHierarchyQueryDAO;
import org.apache.skywalking.oap.server.core.storage.query.IMetadataQueryDAO;
import org.apache.skywalking.oap.server.library.module.ModuleManager;
import org.apache.skywalking.oap.server.library.module.Service;

@Slf4j
public class HierarchyQueryService implements Service {
    private final ModuleManager moduleManager;
    private final boolean isEnableHierarchy;
    private IHierarchyQueryDAO hierarchyQueryDAO;
    private IMetadataQueryDAO metadataQueryDAO;
    private Map<String, Map<String, HierarchyDefinitionService.MatchingRule>> hierarchyDefinition;
    private Map<String, Integer> layerLevels;
    private LoadingCache<Boolean, Map<HierarchyRelatedService/*self*/, ServiceRelations/*upper and lower service list*/>> serviceHierarchyCache;

    public HierarchyQueryService(ModuleManager moduleManager, CoreModuleConfig moduleConfig) {
        this.moduleManager = moduleManager;
        this.isEnableHierarchy = moduleConfig.isEnableHierarchy();
        if (moduleConfig.isEnableHierarchy()) {
            this.serviceHierarchyCache =
                CacheBuilder.newBuilder()
                            .maximumSize(1)
                            .refreshAfterWrite(moduleConfig.getServiceCacheRefreshInterval(), TimeUnit.SECONDS)
                            .build(
                                new CacheLoader<>() {
                                    @Override
                                    public Map<HierarchyRelatedService, ServiceRelations> load(final Boolean key) throws Exception {
                                        return mapServiceHierarchy();
                                    }
                                });
        }
    }

    private IHierarchyQueryDAO getHierarchyQueryDAO() {
        if (hierarchyQueryDAO == null) {
            this.hierarchyQueryDAO = moduleManager.find(StorageModule.NAME)
                                                  .provider()
                                                  .getService(IHierarchyQueryDAO.class);
        }
        return hierarchyQueryDAO;
    }

    private IMetadataQueryDAO getMetadataQueryDAO() {
        if (metadataQueryDAO == null) {
            this.metadataQueryDAO = moduleManager.find(StorageModule.NAME)
                                                     .provider()
                                                     .getService(IMetadataQueryDAO.class);
        }
        return metadataQueryDAO;
    }

    private Map<String, Map<String, HierarchyDefinitionService.MatchingRule>> getHierarchyDefinition() {
        if (hierarchyDefinition == null) {
            hierarchyDefinition = moduleManager.find(CoreModule.NAME)
                                               .provider()
                                               .getService(HierarchyDefinitionService.class).getHierarchyDefinition();
        }
        return hierarchyDefinition;
    }

    private Map<String, Integer> getLayerLevels() {
        if (layerLevels == null) {
            layerLevels = moduleManager.find(CoreModule.NAME)
                                               .provider()
                                               .getService(HierarchyDefinitionService.class).getLayerLevels();
        }
        return layerLevels;
    }

    private Map<HierarchyRelatedService, ServiceRelations> mapServiceHierarchy() throws Exception {
        List<ServiceHierarchyRelationTraffic> traffics = getHierarchyQueryDAO().readAllServiceHierarchyRelations();
        Map<HierarchyRelatedService, ServiceRelations> serviceRelationsMap = new HashMap<>();

        for (ServiceHierarchyRelationTraffic traffic : traffics) {
            HierarchyRelatedService service = new HierarchyRelatedService();
            IDManager.ServiceID.ServiceIDDefinition serviceIdDef = IDManager.ServiceID.analysisId(
                traffic.getServiceId());
            service.setId(traffic.getServiceId());
            service.setName(serviceIdDef.getName());
            service.setLayer(traffic.getServiceLayer().name());
            service.setNormal(serviceIdDef.isReal());
            HierarchyRelatedService relatedService = new HierarchyRelatedService();
            IDManager.ServiceID.ServiceIDDefinition relatedServiceIdDef = IDManager.ServiceID.analysisId(
                traffic.getRelatedServiceId());
            relatedService.setId(traffic.getRelatedServiceId());
            relatedService.setName(relatedServiceIdDef.getName());
            relatedService.setLayer(traffic.getRelatedServiceLayer().name());
            relatedService.setNormal(relatedServiceIdDef.isReal());

            ServiceRelations serviceRelations = serviceRelationsMap.computeIfAbsent(
                service, k -> new ServiceRelations());
            ServiceRelations relationServiceRelations = serviceRelationsMap.computeIfAbsent(
                relatedService, k -> new ServiceRelations());
            Map<String, HierarchyDefinitionService.MatchingRule> lowerLayers = getHierarchyDefinition().getOrDefault(
                traffic.getServiceLayer().name(), new HashMap<>());
            if (lowerLayers.containsKey(traffic.getRelatedServiceLayer().name())) {
                serviceRelations.getLowerServices().add(relatedService);
                relationServiceRelations.getUpperServices().add(service);
            }
        }
        return serviceRelationsMap;
    }

    private void buildServiceRelation(Map<HierarchyRelatedService, ServiceRelations> serviceRelationsMap, ServiceHierarchy hierarchy, HierarchyRelatedService self, int maxDepth, HierarchyDirection direction) throws ExecutionException {
        if (maxDepth < 1) {
            return;
        }
        maxDepth--;
        ServiceRelations serviceRelations = serviceRelationsMap.getOrDefault(self, new ServiceRelations());

        if (serviceRelations.getLowerServices().isEmpty() && serviceRelations.getUpperServices().isEmpty()) {
            return;
        }

        if (direction == HierarchyDirection.LOWER || direction == HierarchyDirection.All) {
            for (HierarchyRelatedService lowerService : serviceRelations.getLowerServices()) {
                HierarchyServiceRelation relation = new HierarchyServiceRelation(self, lowerService);
                if (!hierarchy.getRelations().add(relation)) {
                    continue;
                }
                buildServiceRelation(serviceRelationsMap, hierarchy, lowerService, maxDepth, direction);
            }
        }
        if (direction == HierarchyDirection.UPPER || direction == HierarchyDirection.All) {
            for (HierarchyRelatedService upperService : serviceRelations.getUpperServices()) {
                HierarchyServiceRelation relation = new HierarchyServiceRelation(upperService, self);
                if (!hierarchy.getRelations().add(relation)) {
                    continue;
                }
                buildServiceRelation(serviceRelationsMap, hierarchy, upperService, maxDepth, direction);
            }
        }
    }

    private ServiceHierarchy getServiceHierarchy(String serviceId, String layer, int maxDepth, HierarchyDirection direction) throws Exception {
        ServiceHierarchy hierarchy = new ServiceHierarchy();
        HierarchyRelatedService self = new HierarchyRelatedService();
        self.setId(serviceId);
        self.setName(IDManager.ServiceID.analysisId(serviceId).getName());
        self.setLayer(layer);
        self.setNormal(Layer.nameOf(layer).isNormal());
        buildServiceRelation(serviceHierarchyCache.get(true), hierarchy, self, maxDepth, direction);
        return hierarchy;
    }

    /**
     * @return return the related service hierarchy recursively, e.g. A-B-C, query A will return A-B, B-C
     * If the relation could be conjectured will be removed, e.g. A-B-C-D and A-D, query A will return A-B, B-C, C-D because A-D could be conjectured.
     */
    public ServiceHierarchy getServiceHierarchy(String serviceId, String layer) throws Exception {
        if (!this.isEnableHierarchy) {
            log.warn("CoreModuleConfig config {enableHierarchy} is false, return empty ServiceHierarchy.");
            return new ServiceHierarchy();
        }
        int maxDepth = 10;
        //build relation recursively, set max depth to 10
        ServiceHierarchy hierarchy = getServiceHierarchy(serviceId, layer, maxDepth, HierarchyDirection.All);
        return filterConjecturableRelations(serviceHierarchyCache.get(true), hierarchy, maxDepth);
    }

    public InstanceHierarchy getInstanceHierarchy(String instanceId, String layer) throws Exception {
        if (!this.isEnableHierarchy) {
            log.warn("CoreModuleConfig config {enableHierarchy} is false, return empty InstanceHierarchy.");
            return new InstanceHierarchy();
        }
        ServiceInstance self = getMetadataQueryDAO().getInstance(instanceId);
        if (self == null) {
            return new InstanceHierarchy();
        }

        List<HierarchyInstanceRelation> relations = new ArrayList<>();
        //build from service hierarchy and instance traffic
        IDManager.ServiceInstanceID.InstanceIDDefinition idDefinition = IDManager.ServiceInstanceID.analysisId(
            instanceId);
        IDManager.ServiceID.ServiceIDDefinition serviceIdDefinition = IDManager.ServiceID.analysisId(
            idDefinition.getServiceId());
        //instance is only query 1 depth of service hierarchy, set max depth to 1
        ServiceHierarchy serviceHierarchy = getServiceHierarchy(idDefinition.getServiceId(), layer, 1, HierarchyDirection.All);

        Optional<Attribute> host = self.getAttributes()
                                           .stream()
                                           .filter(hostAttrFilter())
                                           .findFirst();
        Optional<ServiceInstance> lower;
        Optional<ServiceInstance> upper;

        for (HierarchyServiceRelation serviceRelation : serviceHierarchy.getRelations()) {
            //if the service relation is lower/upper, then the instance relation is upper/lower
            List<ServiceInstance> lowerCandidates = getMetadataQueryDAO().listInstances(
                null,
                serviceRelation.getLowerService()
                               .getId()
            );
            List<ServiceInstance> upperCandidates = getMetadataQueryDAO().listInstances(
                null,
                serviceRelation.getUpperService()
                               .getId()
            );
            lower = lowerCandidates.stream()
                                   .filter(relatedInstanceFilter(self, host))
                                   .findFirst();
            upper = upperCandidates.stream()
                                   .filter(relatedInstanceFilter(self, host))
                                   .findFirst();
            HierarchyRelatedInstance instance = new HierarchyRelatedInstance();
            instance.setId(self.getId());
            instance.setName(self.getName());
            instance.setServiceId(idDefinition.getServiceId());
            instance.setServiceName(serviceIdDefinition.getName());
            instance.setNormal(serviceIdDefinition.isReal());
            instance.setLayer(layer);
            //The instances could be same but the service layer is different
            if (lower.isPresent() && !layer.equals(serviceRelation.getLowerService().getLayer())) {
                HierarchyRelatedInstance relatedInstance = new HierarchyRelatedInstance();
                relatedInstance.setId(lower.get().getId());
                relatedInstance.setName(lower.get().getName());
                relatedInstance.setServiceId(serviceRelation.getLowerService().getId());
                relatedInstance.setServiceName(serviceRelation.getLowerService().getName());
                relatedInstance.setLayer(serviceRelation.getLowerService().getLayer());
                relatedInstance.setNormal(serviceRelation.getLowerService().isNormal());
                relations.add(new HierarchyInstanceRelation(instance, relatedInstance));
            }

            if (upper.isPresent() && !layer.equals(serviceRelation.getUpperService().getLayer())) {
                HierarchyRelatedInstance relatedInstance = new HierarchyRelatedInstance();
                relatedInstance.setId(upper.get().getId());
                relatedInstance.setName(upper.get().getName());
                relatedInstance.setServiceId(serviceRelation.getUpperService().getId());
                relatedInstance.setServiceName(serviceRelation.getUpperService().getName());
                relatedInstance.setLayer(serviceRelation.getUpperService().getLayer());
                relatedInstance.setNormal(serviceRelation.getUpperService().isNormal());
                relations.add(new HierarchyInstanceRelation(relatedInstance, instance));
            }
        }

        //build from storage directly
        List<InstanceHierarchyRelationTraffic> traffics = getHierarchyQueryDAO().readInstanceHierarchyRelations(
            instanceId, layer);

        for (InstanceHierarchyRelationTraffic traffic : traffics) {
            HierarchyRelatedInstance instance = new HierarchyRelatedInstance();
            IDManager.ServiceInstanceID.InstanceIDDefinition idDef = IDManager.ServiceInstanceID.analysisId(
                instanceId);
            IDManager.ServiceID.ServiceIDDefinition serviceIdDef = IDManager.ServiceID.analysisId(
                idDefinition.getServiceId());
            instance.setId(traffic.getInstanceId());
            instance.setName(idDef.getName());
            instance.setServiceId(idDef.getServiceId());
            instance.setServiceName(serviceIdDef.getName());
            instance.setLayer(traffic.getServiceLayer().name());
            instance.setNormal(serviceIdDef.isReal());
            HierarchyRelatedInstance relatedInstance = new HierarchyRelatedInstance();
            IDManager.ServiceInstanceID.InstanceIDDefinition relatedIdDef = IDManager.ServiceInstanceID.analysisId(
                traffic.getRelatedInstanceId());
            IDManager.ServiceID.ServiceIDDefinition relatedServiceIdDef = IDManager.ServiceID.analysisId(
                relatedIdDef.getServiceId());
            relatedInstance.setId(traffic.getRelatedInstanceId());
            relatedInstance.setName(relatedIdDef.getName());
            relatedInstance.setServiceId(relatedIdDef.getServiceId());
            relatedInstance.setServiceName(relatedServiceIdDef.getName());
            relatedInstance.setLayer(traffic.getRelatedServiceLayer().name());
            relatedInstance.setNormal(relatedServiceIdDef.isReal());
            Map<String, HierarchyDefinitionService.MatchingRule> lowerLayers = getHierarchyDefinition().get(
                traffic.getServiceLayer().name());
            if (lowerLayers != null && lowerLayers.containsKey(traffic.getRelatedServiceLayer().name())) {
                relations.add(new HierarchyInstanceRelation(instance, relatedInstance));
            }
        }
        InstanceHierarchy hierarchy = new InstanceHierarchy();
        hierarchy.setRelations(relations.stream().distinct().collect(Collectors.toList()));
        return hierarchy;
    }

    public List<LayerLevel> listLayerLevels() {
        return getLayerLevels().entrySet()
                               .stream()
                               .map(entry -> new LayerLevel(entry.getKey(), entry.getValue()))
                               .collect(Collectors.toList());
    }

    private Predicate<Attribute> hostAttrFilter() {
        return attribute -> attribute.getName().equalsIgnoreCase("pod")
            || attribute.getName().equalsIgnoreCase("hostname");
    }

    private Predicate<ServiceInstance> relatedInstanceFilter(ServiceInstance instance, Optional<Attribute> hostAttr) {
        return candidate -> {
            // both names equals
            if (candidate.getName().equals(instance.getName())) {
                return true;
            }
            if (hostAttr.isPresent()) {
                // both hosts equals
                if (candidate.getAttributes().contains(hostAttr.get())) {
                    return true;
                }
                // name equals host
                if (candidate.getName().equals(hostAttr.get().getValue())) {
                    return true;
                }
                // host equals name
                return candidate.getAttributes()
                                .stream()
                                .filter(hostAttrFilter())
                                .anyMatch(attr -> attr.getValue().equals(instance.getName()));
            }
            return false;
        };
    }

    //If the lower service relation could be found from other lower relations, then it could be conjectured.
    private ServiceHierarchy filterConjecturableRelations(Map<HierarchyRelatedService, ServiceRelations> serviceRelationsMap,
                                                          ServiceHierarchy hierarchy,
                                                          int maxDepth) {
        Set<HierarchyServiceRelation> relations = hierarchy.getRelations();
        List<HierarchyServiceRelation> relationList = new ArrayList<>(relations);
        for (HierarchyServiceRelation relation : relationList) {
            HierarchyRelatedService upperService = relation.getUpperService();
            HierarchyRelatedService lowerService = relation.getLowerService();
            ServiceRelations serviceRelations = serviceRelationsMap.get(upperService);
            // if only one lower service, keep the relation
            if (serviceRelations.lowerServices.size() > 1) {
                if (checkIfConjecturable(serviceRelationsMap, serviceRelations.lowerServices, lowerService, maxDepth)) {
                    // if the lower service is conjecturable, remove the relation
                    relations.remove(relation);
                }
            }
        }
        return hierarchy;
    }

    private boolean checkIfConjecturable(Map<HierarchyRelatedService, ServiceRelations> serviceRelationsMap,
                                         List<HierarchyRelatedService> services,
                                         HierarchyRelatedService conjecturalService,
                                         int maxDepth) {
        if (maxDepth < 1) {
            return false;
        }
        maxDepth--;
        for (HierarchyRelatedService service : services) {
            if (!service.equals(conjecturalService)) {
                List<HierarchyRelatedService> lowerServices = serviceRelationsMap.get(service).lowerServices;
                if (lowerServices.contains(conjecturalService)) {
                    return true;
                } else {
                    return checkIfConjecturable(serviceRelationsMap, lowerServices, conjecturalService, maxDepth);
                }
            }
        }
        return false;
    }

    /**
     * Record the all upper and lower services of the specified service.
     */
    @Data
    public static class ServiceRelations {
        private List<HierarchyRelatedService> upperServices = new ArrayList<>();
        private List<HierarchyRelatedService> lowerServices = new ArrayList<>();
    }

    public enum HierarchyDirection {
        All,
        UPPER,
        LOWER
    }
}
