/*
 * Copyright 2013-2018 the original author or authors.
 *
 * 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
 *
 *      https://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.cloud.dubbo.registry;

import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository;
import com.alibaba.cloud.dubbo.registry.event.ServiceInstancesChangedEvent;
import com.alibaba.cloud.dubbo.service.DubboGenericServiceFactory;
import com.alibaba.cloud.dubbo.service.DubboMetadataService;
import com.alibaba.cloud.dubbo.service.DubboMetadataServiceProxy;
import com.alibaba.cloud.dubbo.util.JSONUtils;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.registry.NotifyListener;
import org.apache.dubbo.registry.RegistryFactory;
import org.apache.dubbo.registry.support.FailbackRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.CollectionUtils;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.apache.dubbo.common.URLBuilder.from;
import static org.apache.dubbo.common.constants.CommonConstants.GROUP_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.PROTOCOL_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER;
import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER_SIDE;
import static org.apache.dubbo.common.constants.CommonConstants.SIDE_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.VERSION_KEY;
import static org.apache.dubbo.common.constants.RegistryConstants.CATEGORY_KEY;
import static org.apache.dubbo.common.constants.RegistryConstants.EMPTY_PROTOCOL;
import static org.apache.dubbo.registry.Constants.ADMIN_PROTOCOL;
import static org.springframework.util.StringUtils.hasText;

/**
 * Abstract Dubbo {@link RegistryFactory} uses Spring Cloud Service Registration
 * abstraction, whose protocol is "spring-cloud".
 *
 * @author <a href="mailto:[email protected]">Mercy</a>
 */
public abstract class AbstractSpringCloudRegistry extends FailbackRegistry {

	/**
	 * The parameter name of {@link #servicesLookupInterval}.
	 */
	public static final String SERVICES_LOOKUP_INTERVAL_PARAM_NAME = "dubbo.services.lookup.interval";

	protected static final String DUBBO_METADATA_SERVICE_CLASS_NAME = DubboMetadataService.class
			.getName();

	/**
	 * Caches the IDs of {@link ApplicationListener}.
	 */
	private static final Set<String> registerListeners = new HashSet<>();

	protected final Logger logger = LoggerFactory.getLogger(getClass());

	/**
	 * The interval in second of lookup service names(only for Dubbo-OPS).
	 */
	private final long servicesLookupInterval;

	private final DiscoveryClient discoveryClient;

	private final DubboServiceMetadataRepository repository;

	private final DubboMetadataServiceProxy dubboMetadataConfigServiceProxy;

	private final JSONUtils jsonUtils;

	private final DubboGenericServiceFactory dubboGenericServiceFactory;

	private final ConfigurableApplicationContext applicationContext;

	public AbstractSpringCloudRegistry(URL url, DiscoveryClient discoveryClient,
			DubboServiceMetadataRepository dubboServiceMetadataRepository,
			DubboMetadataServiceProxy dubboMetadataConfigServiceProxy,
			JSONUtils jsonUtils, DubboGenericServiceFactory dubboGenericServiceFactory,
			ConfigurableApplicationContext applicationContext) {
		super(url);
		this.servicesLookupInterval = url
				.getParameter(SERVICES_LOOKUP_INTERVAL_PARAM_NAME, 60L);
		this.discoveryClient = discoveryClient;
		this.repository = dubboServiceMetadataRepository;
		this.dubboMetadataConfigServiceProxy = dubboMetadataConfigServiceProxy;
		this.jsonUtils = jsonUtils;
		this.dubboGenericServiceFactory = dubboGenericServiceFactory;
		this.applicationContext = applicationContext;
	}

	protected boolean shouldRegister(URL url) {
		String side = url.getParameter(SIDE_KEY);

		boolean should = PROVIDER_SIDE.equals(side); // Only register the Provider.

		if (!should) {
			if (logger.isDebugEnabled()) {
				logger.debug("The URL[{}] should not be registered.", url.toString());
			}
		}

		return should;
	}

	@Override
	public final void doRegister(URL url) {
		if (!shouldRegister(url)) {
			return;
		}
		doRegister0(url);
	}

	/**
	 * The sub-type should implement to register.
	 * @param url {@link URL}
	 */
	protected abstract void doRegister0(URL url);

	@Override
	public final void doUnregister(URL url) {
		if (!shouldRegister(url)) {
			return;
		}
		doUnregister0(url);
	}

	/**
	 * The sub-type should implement to unregister.
	 * @param url {@link URL}
	 */
	protected abstract void doUnregister0(URL url);

	@Override
	public final void doSubscribe(URL url, NotifyListener listener) {

		if (isAdminURL(url)) {
			// TODO in future
		}
		else if (isDubboMetadataServiceURL(url)) { // for DubboMetadataService
			subscribeDubboMetadataServiceURLs(url, listener);
			if (from(url).getParameter(CATEGORY_KEY) != null
					&& from(url).getParameter(CATEGORY_KEY).contains(PROVIDER)) {
				// Fix #1259 and #753 Listene meta service change events to remove useless
				// clients
				registerServiceInstancesChangedEventListener(url, listener);
			}

		}
		else { // for general Dubbo Services
			subscribeDubboServiceURLs(url, listener);
		}
	}

	protected void subscribeDubboServiceURLs(URL url, NotifyListener listener) {

		doSubscribeDubboServiceURLs(url, listener);

		registerServiceInstancesChangedEventListener(url, listener);
	}

	/**
	 * Register a {@link ApplicationListener listener} for
	 * {@link ServiceInstancesChangedEvent}.
	 * @param url {@link URL}
	 * @param listener {@link NotifyListener}
	 */
	private void registerServiceInstancesChangedEventListener(URL url,
			NotifyListener listener) {
		String listenerId = generateId(url);
		if (registerListeners.add(listenerId)) {
			applicationContext.addApplicationListener(
					new ApplicationListener<ServiceInstancesChangedEvent>() {
						@Override
						public void onApplicationEvent(
								ServiceInstancesChangedEvent event) {
							String serviceName = event.getServiceName();
							Collection<ServiceInstance> serviceInstances = event
									.getServiceInstances();
							subscribeDubboServiceURL(url, listener, serviceName,
									s -> serviceInstances);
						}
					});
		}
	}

	private void doSubscribeDubboServiceURLs(URL url, NotifyListener listener) {

		Set<String> subscribedServices = repository.getSubscribedServices();
		// Sync
		subscribedServices.forEach(service -> subscribeDubboServiceURL(url, listener,
				service, this::getServiceInstances));
	}

	protected void subscribeDubboServiceURL(URL url, NotifyListener listener,
			String serviceName,
			Function<String, Collection<ServiceInstance>> serviceInstancesFunction) {

		if (logger.isInfoEnabled()) {
			logger.info(
					"The Dubbo Service URL[ID : {}] is being subscribed for service[name : {}]",
					generateId(url), serviceName);
		}

		List<URL> allSubscribedURLs = new LinkedList<>();

		Collection<ServiceInstance> serviceInstances = serviceInstancesFunction
				.apply(serviceName);

		// issue : ReStarting a consumer and then starting a provider does not
		// automatically discover the registration
		// fix https://github.com/alibaba/spring-cloud-alibaba/issues/753
		// Re-obtain the latest list of available metadata address here, ip or port may
		// change.
		// by https://github.com/wangzihaogithub
		// When the last service provider is closed, 【fix 1259】while close the
		// channel,when up a new provider then repository.initializeMetadata(serviceName)
		// will throw Exception.
		// dubboMetadataConfigServiceProxy.removeProxy(serviceName);
		// repository.removeMetadataAndInitializedService(serviceName);
		// dubboGenericServiceFactory.destroy(serviceName);
		// repository.initializeMetadata(serviceName);
		if (CollectionUtils.isEmpty(serviceInstances)) {
			if (logger.isWarnEnabled()) {
				logger.warn(
						"There is no instance from service[name : {}], and then Dubbo Service[key : {}] will not be "
								+ "available , please make sure the further impact",
						serviceName, url.getServiceKey());
			}
			if (isDubboMetadataServiceURL(url)) {
				// if meta service change, and serviceInstances is zero, will clean up
				// information about this client
				dubboMetadataConfigServiceProxy.removeProxy(serviceName);
				repository.removeMetadataAndInitializedService(serviceName, url);
				dubboGenericServiceFactory.destroy(serviceName);
				String listenerId = generateId(url);
				// The metaservice will restart the new listener. It needs to be optimized
				// to see whether the original listener can be reused.
				this.registerListeners.remove(listenerId);
			}

			/**
			 * URLs with {@link RegistryConstants#EMPTY_PROTOCOL}
			 */
			allSubscribedURLs.addAll(emptyURLs(url));
			if (logger.isDebugEnabled()) {
				logger.debug("The subscribed URL[{}] will notify all URLs : {}", url,
						allSubscribedURLs);
			}
			listener.notify(allSubscribedURLs);
			return;
		}
		if (isDubboMetadataServiceURL(url)) {
			// Prevent duplicate generation of DubboMetadataService
			return;
		}
		repository.initializeMetadata(serviceName);

		DubboMetadataService dubboMetadataService = dubboMetadataConfigServiceProxy
				.getProxy(serviceName);
		if (dubboMetadataService == null) { // It makes sure not-found, return immediately
			if (logger.isWarnEnabled()) {
				logger.warn(
						"The metadata of Dubbo service[key : {}] still can't be found, it could effect the further "
								+ "Dubbo service invocation",
						url.getServiceKey());
			}
			return;
		}

		List<URL> exportedURLs = getExportedURLs(dubboMetadataService, url);
		for (URL exportedURL : exportedURLs) {
			String protocol = exportedURL.getProtocol();
			List<URL> subscribedURLs = new LinkedList<>();
			serviceInstances.forEach(serviceInstance -> {
				Integer port = repository.getDubboProtocolPort(serviceInstance, protocol);
				String host = serviceInstance.getHost();
				if (port == null) {
					if (logger.isWarnEnabled()) {
						logger.warn(
								"The protocol[{}] port of Dubbo  service instance[host : {}] "
										+ "can't be resolved",
								protocol, host);
					}
				}
				else {
					URL subscribedURL = new URL(protocol, host, port,
							exportedURL.getParameters());
					subscribedURLs.add(subscribedURL);
				}
			});

			allSubscribedURLs.addAll(subscribedURLs);
		}

		if (logger.isDebugEnabled()) {
			logger.debug("The subscribed URL[{}] will notify all URLs : {}", url,
					allSubscribedURLs);
		}

		listener.notify(allSubscribedURLs);
	}

	private String generateId(URL url) {
		return url.toString(VERSION_KEY, GROUP_KEY, PROTOCOL_KEY);
	}

	private List<URL> emptyURLs(URL url) {
		// issue : When the last service provider is closed, the client still periodically
		// connects to the last provider.n
		// fix https://github.com/alibaba/spring-cloud-alibaba/issues/1259
		return asList(from(url).setProtocol(EMPTY_PROTOCOL).removeParameter(CATEGORY_KEY)
				.build());
	}

	private List<ServiceInstance> getServiceInstances(String serviceName) {
		return hasText(serviceName) ? doGetServiceInstances(serviceName) : emptyList();
	}

	private List<ServiceInstance> doGetServiceInstances(String serviceName) {
		List<ServiceInstance> serviceInstances = emptyList();
		try {
			serviceInstances = discoveryClient.getInstances(serviceName);
		}
		catch (Exception e) {
			if (logger.isErrorEnabled()) {
				logger.error(e.getMessage(), e);
			}
		}
		return serviceInstances;
	}

	private List<URL> getExportedURLs(DubboMetadataService dubboMetadataService,
			URL url) {
		String serviceInterface = url.getServiceInterface();
		String group = url.getParameter(GROUP_KEY);
		String version = url.getParameter(VERSION_KEY);
		// The subscribed protocol may be null
		String subscribedProtocol = url.getParameter(PROTOCOL_KEY);
		String exportedURLsJSON = dubboMetadataService.getExportedURLs(serviceInterface,
				group, version);
		return jsonUtils.toURLs(exportedURLsJSON).stream()
				.filter(exportedURL -> subscribedProtocol == null
						|| subscribedProtocol.equalsIgnoreCase(exportedURL.getProtocol()))
				.collect(Collectors.toList());
	}

	private void subscribeDubboMetadataServiceURLs(URL url, NotifyListener listener) {
		String serviceInterface = url.getServiceInterface();
		String group = url.getParameter(GROUP_KEY);
		String version = url.getParameter(VERSION_KEY);
		String protocol = url.getParameter(PROTOCOL_KEY);
		List<URL> urls = repository.findSubscribedDubboMetadataServiceURLs(
				serviceInterface, group, version, protocol);
		listener.notify(urls);
	}

	@Override
	public final void doUnsubscribe(URL url, NotifyListener listener) {
		// if (isAdminURL(url)) {
		// }
	}

	@Override
	public boolean isAvailable() {
		return !discoveryClient.getServices().isEmpty();
	}

	protected boolean isAdminURL(URL url) {
		return ADMIN_PROTOCOL.equals(url.getProtocol());
	}

	protected boolean isDubboMetadataServiceURL(URL url) {
		return DUBBO_METADATA_SERVICE_CLASS_NAME.equals(url.getServiceInterface());
	}

}