/*
 * Copyright 1999-2020 Alibaba Group Holding Ltd.
 *
 * Licensed 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 com.alibaba.nacos.naming.core;

import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.model.Page;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.api.naming.pojo.ServiceInfo;
import com.alibaba.nacos.api.naming.pojo.maintainer.ClusterInfo;
import com.alibaba.nacos.api.naming.pojo.maintainer.ServiceDetailInfo;
import com.alibaba.nacos.api.naming.pojo.maintainer.ServiceView;
import com.alibaba.nacos.common.utils.JacksonUtils;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.core.utils.PageUtil;
import com.alibaba.nacos.naming.constants.FieldsConstants;
import com.alibaba.nacos.naming.core.v2.ServiceManager;
import com.alibaba.nacos.naming.core.v2.index.ServiceStorage;
import com.alibaba.nacos.naming.core.v2.metadata.ClusterMetadata;
import com.alibaba.nacos.naming.core.v2.metadata.NamingMetadataManager;
import com.alibaba.nacos.naming.core.v2.metadata.ServiceMetadata;
import com.alibaba.nacos.naming.core.v2.pojo.Service;
import com.alibaba.nacos.naming.utils.ServiceUtil;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.stream.Collectors;

/**
 * Catalog service for v2.x .
 *
 * @author xiweng.yy
 */
@Component()
public class CatalogServiceV2Impl implements CatalogService {
    
    private final ServiceStorage serviceStorage;
    
    private final NamingMetadataManager metadataManager;
    
    private static final int DEFAULT_PORT = 80;
    
    public CatalogServiceV2Impl(ServiceStorage serviceStorage, NamingMetadataManager metadataManager) {
        this.serviceStorage = serviceStorage;
        this.metadataManager = metadataManager;
    }
    
    @Override
    public ServiceDetailInfo getServiceDetail(String namespaceId, String groupName, String serviceName)
            throws NacosException {
        Service service = Service.newService(namespaceId, groupName, serviceName);
        if (!ServiceManager.getInstance().containSingleton(service)) {
            throw new NacosException(NacosException.NOT_FOUND,
                    String.format("service %s@@%s is not found!", groupName, serviceName));
        }
        service = ServiceManager.getInstance().getSingleton(service);
        Optional<ServiceMetadata> metadata = metadataManager.getServiceMetadata(service);
        ServiceMetadata detailedService = metadata.orElseGet(ServiceMetadata::new);
        
        ServiceDetailInfo result = new ServiceDetailInfo();
        result.setNamespaceId(service.getNamespace());
        result.setGroupName(service.getGroup());
        result.setServiceName(serviceName);
        result.setEphemeral(service.isEphemeral());
        result.setProtectThreshold(detailedService.getProtectThreshold());
        result.setSelector(detailedService.getSelector());
        result.setMetadata(detailedService.getExtendData());
        
        Map<String, ClusterInfo> clusters = new HashMap<>(serviceStorage.getClusters(service).size());
        for (String each : serviceStorage.getClusters(service)) {
            ClusterMetadata clusterMetadata =
                    detailedService.getClusters().containsKey(each) ? detailedService.getClusters().get(each)
                            : new ClusterMetadata();
            ClusterInfo clusterInfo = new ClusterInfo();
            clusterInfo.setClusterName(each);
            clusterInfo.setHealthChecker(clusterMetadata.getHealthChecker());
            clusterInfo.setMetadata(clusterMetadata.getExtendData());
            clusterInfo.setUseInstancePortForCheck(clusterMetadata.isUseInstancePortForCheck());
            clusterInfo.setHealthyCheckPort(clusterMetadata.getHealthyCheckPort());
            clusters.put(each, clusterInfo);
        }
        result.setClusterMap(clusters);
        return result;
    }
    
    @Override
    public List<? extends Instance> listInstances(String namespaceId, String groupName, String serviceName,
            String clusterName) throws NacosException {
        Service service = Service.newService(namespaceId, groupName, serviceName);
        if (!ServiceManager.getInstance().containSingleton(service)) {
            throw new NacosException(NacosException.NOT_FOUND,
                    String.format("service %s@@%s is not found!", groupName, serviceName));
        }
        if (!serviceStorage.getClusters(service).contains(clusterName)) {
            throw new NacosException(NacosException.NOT_FOUND, "cluster " + clusterName + " is not found!");
        }
        ServiceInfo serviceInfo = serviceStorage.getData(service);
        ServiceInfo result = ServiceUtil.selectInstances(serviceInfo, clusterName);
        return result.getHosts();
    }
    
    @Override
    public List<? extends Instance> listAllInstances(String namespaceId, String groupName, String serviceName) {
        Service service = Service.newService(namespaceId, groupName, serviceName);
        if (!ServiceManager.getInstance().containSingleton(service)) {
            return Collections.EMPTY_LIST;
        }
        
        ServiceInfo serviceInfo = serviceStorage.getData(service);
        
        return serviceInfo.getHosts();
    }
    
    @Override
    public Object pageListService(String namespaceId, String groupName, String serviceName, int pageNo, int pageSize,
            String instancePattern, boolean ignoreEmptyService) throws NacosException {
        ObjectNode result = JacksonUtils.createEmptyJsonNode();
        List<ServiceView> serviceViews = new LinkedList<>();
        Collection<Service> services = patternServices(namespaceId, groupName, serviceName);
        if (ignoreEmptyService) {
            services = services.stream().filter(each -> 0 != serviceStorage.getData(each).ipCount())
                    .collect(Collectors.toList());
        }
        result.put(FieldsConstants.COUNT, services.size());
        services = doPage(services, pageNo - 1, pageSize);
        for (Service each : services) {
            ServiceMetadata serviceMetadata = metadataManager.getServiceMetadata(each).orElseGet(ServiceMetadata::new);
            ServiceView serviceView = new ServiceView();
            serviceView.setName(each.getName());
            serviceView.setGroupName(each.getGroup());
            serviceView.setClusterCount(serviceStorage.getClusters(each).size());
            serviceView.setIpCount(serviceStorage.getData(each).ipCount());
            serviceView.setHealthyInstanceCount(countHealthyInstance(serviceStorage.getData(each)));
            serviceView.setTriggerFlag(isProtectThreshold(serviceView, serviceMetadata) ? "true" : "false");
            serviceViews.add(serviceView);
        }
        result.set(FieldsConstants.SERVICE_LIST, JacksonUtils.transferToJsonNode(serviceViews));
        return result;
    }
    
    @Override
    public Page<ServiceView> listService(String namespaceId, String groupName, String serviceName, int pageNo,
            int pageSize, boolean ignoreEmptyService) throws NacosException {
        Page<ServiceView> serviceViews = new Page<>();
        Collection<Service> services = patternServices(namespaceId, groupName, serviceName);
        if (ignoreEmptyService) {
            services = services.stream().filter(each -> 0 != serviceStorage.getData(each).ipCount()).toList();
        }
        Page<Service> page = PageUtil.subPage(services.stream().toList(), pageNo, pageSize);
        serviceViews.setTotalCount(page.getTotalCount());
        serviceViews.setPageNumber(page.getPageNumber());
        serviceViews.setPagesAvailable(page.getPagesAvailable());
        for (Service each : page.getPageItems()) {
            ServiceMetadata serviceMetadata = metadataManager.getServiceMetadata(each).orElseGet(ServiceMetadata::new);
            ServiceView serviceView = new ServiceView();
            serviceView.setName(each.getName());
            serviceView.setGroupName(each.getGroup());
            serviceView.setClusterCount(serviceStorage.getClusters(each).size());
            serviceView.setIpCount(serviceStorage.getData(each).ipCount());
            serviceView.setHealthyInstanceCount(countHealthyInstance(serviceStorage.getData(each)));
            serviceView.setTriggerFlag(isProtectThreshold(serviceView, serviceMetadata) ? "true" : "false");
            serviceViews.getPageItems().add(serviceView);
        }
        return serviceViews;
    }
    
    private int countHealthyInstance(ServiceInfo data) {
        int result = 0;
        for (Instance each : data.getHosts()) {
            if (each.isHealthy()) {
                result++;
            }
        }
        return result;
    }
    
    private boolean isProtectThreshold(ServiceView serviceView, ServiceMetadata metadata) {
        return (serviceView.getHealthyInstanceCount() * 1.0 / serviceView.getIpCount())
                <= metadata.getProtectThreshold();
    }
    
    @Override
    public Page<ServiceDetailInfo> pageListServiceDetail(String namespaceId, String groupName, String serviceName,
            int pageNo, int pageSize) throws NacosException {
        Collection<Service> services = patternServices(namespaceId, groupName, serviceName);
        Page<Service> servicePage = PageUtil.subPage(services.stream().toList(), pageNo, pageSize);
        Page<ServiceDetailInfo> result = new Page<>();
        result.setPagesAvailable(servicePage.getPagesAvailable());
        result.setPageNumber(servicePage.getPageNumber());
        result.setTotalCount(servicePage.getTotalCount());
        List<ServiceDetailInfo> pagedItem = new LinkedList<>();
        for (Service each : servicePage.getPageItems()) {
            ServiceDetailInfo serviceDetailInfo = new ServiceDetailInfo();
            serviceDetailInfo.setServiceName(each.getName());
            serviceDetailInfo.setGroupName(each.getGroup());
            ServiceMetadata serviceMetadata = metadataManager.getServiceMetadata(each).orElseGet(ServiceMetadata::new);
            serviceDetailInfo.setMetadata(serviceMetadata.getExtendData());
            serviceDetailInfo.setClusterMap(getClusterMap(each));
            pagedItem.add(serviceDetailInfo);
        }
        result.setPageItems(pagedItem);
        return result;
    }
    
    private Map<String, ClusterInfo> getClusterMap(Service service) {
        Map<String, ClusterInfo> result = new HashMap<>(1);
        for (Instance each : serviceStorage.getData(service).getHosts()) {
            if (!result.containsKey(each.getClusterName())) {
                ClusterInfo clusterInfo = new ClusterInfo();
                clusterInfo.setHosts(new LinkedList<>());
                result.put(each.getClusterName(), clusterInfo);
            }
            result.get(each.getClusterName()).getHosts().add(each);
        }
        return result;
    }
    
    private Collection<Service> patternServices(String namespaceId, String group, String serviceName) {
        boolean noFilter = StringUtils.isBlank(serviceName) && StringUtils.isBlank(group);
        if (noFilter) {
            return ServiceManager.getInstance().getSingletons(namespaceId);
        }
        Collection<Service> result = new LinkedList<>();
        StringJoiner regex = new StringJoiner(Constants.SERVICE_INFO_SPLITER);
        regex.add(getRegexString(group));
        regex.add(getRegexString(serviceName));
        String regexString = regex.toString();
        for (Service each : ServiceManager.getInstance().getSingletons(namespaceId)) {
            if (each.getGroupedServiceName().matches(regexString)) {
                result.add(each);
            }
        }
        return result;
    }
    
    private String getRegexString(String target) {
        return StringUtils.isBlank(target) ? Constants.ANY_PATTERN
                : Constants.ANY_PATTERN + target + Constants.ANY_PATTERN;
    }
    
    private Collection<Service> doPage(Collection<Service> services, int pageNo, int pageSize) {
        if (pageNo == 0 && services.size() < pageSize) {
            return services;
        }
        int start = pageNo * pageSize;
        if (start > services.size()) {
            return Collections.emptyList();
        }
        Collection<Service> result = new LinkedList<>();
        int i = 0;
        for (Service each : services) {
            if (i++ < start) {
                continue;
            }
            result.add(each);
            if (result.size() >= pageSize) {
                break;
            }
        }
        return result;
    }
}
