/**
 * Copyright 2014-2017 yangming.liu<[email protected]>.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, see <http://www.gnu.org/licenses/>.
 */
package org.bytesoft.bytejta.supports.springcloud.config;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.transaction.UserTransaction;

import org.apache.commons.lang3.StringUtils;
import org.bytesoft.bytejta.supports.resource.properties.ConnectorResourcePropertySourceFactory;
import org.bytesoft.bytejta.supports.springcloud.SpringCloudBeanRegistry;
import org.bytesoft.bytejta.supports.springcloud.feign.TransactionClientRegistry;
import org.bytesoft.bytejta.supports.springcloud.feign.TransactionFeignBeanPostProcessor;
import org.bytesoft.bytejta.supports.springcloud.feign.TransactionFeignContract;
import org.bytesoft.bytejta.supports.springcloud.feign.TransactionFeignDecoder;
import org.bytesoft.bytejta.supports.springcloud.feign.TransactionFeignErrorDecoder;
import org.bytesoft.bytejta.supports.springcloud.feign.TransactionFeignInterceptor;
import org.bytesoft.bytejta.supports.springcloud.hystrix.TransactionHystrixBeanPostProcessor;
import org.bytesoft.bytejta.supports.springcloud.loadbalancer.TransactionLoadBalancerRuleImpl;
import org.bytesoft.bytejta.supports.springcloud.property.TransactionPropertySourceFactory;
import org.bytesoft.bytejta.supports.springcloud.web.TransactionHandlerInterceptor;
import org.bytesoft.bytejta.supports.springcloud.web.TransactionRequestInterceptor;
import org.bytesoft.common.utils.CommonUtils;
import org.bytesoft.transaction.TransactionManager;
import org.bytesoft.transaction.aware.TransactionEndpointAware;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.TransactionManagementConfigurer;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import feign.codec.ErrorDecoder;

@PropertySource(value = "bytejta:loadbalancer.config", factory = TransactionPropertySourceFactory.class)
@PropertySource(value = "bytejta:connector.config", factory = ConnectorResourcePropertySourceFactory.class)
@ImportResource({ "classpath:bytejta-supports-springcloud.xml" })
@EnableAspectJAutoProxy(proxyTargetClass = true)
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })
@EnableTransactionManagement
public class SpringCloudConfiguration implements WebMvcConfigurer, TransactionManagementConfigurer, BeanFactoryPostProcessor,
		InitializingBean, SmartInitializingSingleton, TransactionEndpointAware, EnvironmentAware, ApplicationContextAware {
	static final String CONSTANT_INCLUSIONS = "org.bytesoft.bytejta.feign.inclusions";
	static final String CONSTANT_EXCLUSIONS = "org.bytesoft.bytejta.feign.exclusions";
	static final String FEIGN_FACTORY_CLASS = "org.springframework.cloud.openfeign.FeignClientFactoryBean";

	private ApplicationContext applicationContext;
	private String identifier;
	private Environment environment;
	private transient final Set<String> transientClientSet = new HashSet<String>();

	public void afterSingletonsInstantiated() /* Check if the rule is set correctly */ {
		com.netflix.loadbalancer.IRule loadBalancerRule = null;
		try {
			loadBalancerRule = this.applicationContext.getBean(com.netflix.loadbalancer.IRule.class);
		} catch (NoSuchBeanDefinitionException ex) {
			return; // return quietly
		}

		if (TransactionLoadBalancerRuleImpl.class.isInstance(loadBalancerRule)) {
			return; // return quietly
		}

		throw new IllegalStateException("TransactionLoadBalancerRuleImpl is disabled!");
	}

	public void afterPropertiesSet() throws Exception {
		String host = CommonUtils.getInetAddress();
		String name = this.environment.getProperty("spring.application.name");
		String port = this.environment.getProperty("server.port");
		this.identifier = String.format("%s:%s:%s", host, name, port);
	}

	public PlatformTransactionManager annotationDrivenTransactionManager() {
		org.springframework.transaction.jta.JtaTransactionManager jtaTransactionManager //
				= new org.springframework.transaction.jta.JtaTransactionManager();
		jtaTransactionManager.setTransactionManager(this.applicationContext.getBean(TransactionManager.class));
		jtaTransactionManager.setUserTransaction(this.applicationContext.getBean(UserTransaction.class));
		return jtaTransactionManager;
	}

	@org.springframework.context.annotation.Bean
	@ConditionalOnProperty(name = "feign.hystrix.enabled", havingValue = "false", matchIfMissing = true)
	public TransactionFeignBeanPostProcessor feignPostProcessor() {
		return new TransactionFeignBeanPostProcessor();
	}

	@org.springframework.context.annotation.Bean
	@ConditionalOnProperty(name = "feign.hystrix.enabled")
	@ConditionalOnClass(feign.hystrix.HystrixFeign.class)
	public TransactionHystrixBeanPostProcessor hystrixPostProcessor() {
		return new TransactionHystrixBeanPostProcessor();
	}

	@org.springframework.context.annotation.Bean
	public TransactionFeignInterceptor transactionFeignInterceptor() {
		TransactionFeignInterceptor interceptor = new TransactionFeignInterceptor();
		interceptor.setEndpoint(this.identifier);
		return interceptor;
	}

	@org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean(feign.Contract.class)
	@org.springframework.context.annotation.Bean
	public feign.Contract feignContract() {
		return new SpringMvcContract();
	}

	@org.springframework.context.annotation.Primary
	@org.springframework.context.annotation.Bean
	public feign.Contract transactionFeignContract(@Autowired feign.Contract contract) {
		return new TransactionFeignContract(contract);
	}

	@org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean(feign.codec.Decoder.class)
	@org.springframework.context.annotation.Bean
	public feign.codec.Decoder feignDecoder(@Autowired ObjectFactory<HttpMessageConverters> messageConverters) {
		return new ResponseEntityDecoder(new SpringDecoder(messageConverters));
	}

	@org.springframework.context.annotation.Primary
	@org.springframework.context.annotation.Bean
	public feign.codec.Decoder transactionFeignDecoder(@Autowired ObjectFactory<HttpMessageConverters> objectFactory,
			@Autowired feign.codec.Decoder decoder) {
		TransactionFeignDecoder feignDecoder = new TransactionFeignDecoder(decoder);
		feignDecoder.setObjectFactory(objectFactory);
		return feignDecoder;
	}

	@org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean(feign.codec.ErrorDecoder.class)
	@org.springframework.context.annotation.Bean
	public feign.codec.ErrorDecoder errorDecoder() {
		return new ErrorDecoder.Default();
	}

	@org.springframework.context.annotation.Primary
	@org.springframework.context.annotation.Bean
	public feign.codec.ErrorDecoder transactionErrorDecoder(@Autowired feign.codec.ErrorDecoder decoder) {
		return new TransactionFeignErrorDecoder(decoder);
	}

	@org.springframework.context.annotation.Bean
	public TransactionHandlerInterceptor transactionHandlerInterceptor() {
		TransactionHandlerInterceptor interceptor = new TransactionHandlerInterceptor();
		interceptor.setEndpoint(this.identifier);
		return interceptor;
	}

	@org.springframework.context.annotation.Bean
	public TransactionRequestInterceptor transactionRequestInterceptor() {
		TransactionRequestInterceptor interceptor = new TransactionRequestInterceptor();
		interceptor.setEndpoint(this.identifier);
		return interceptor;
	}

	@org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean(ClientHttpRequestFactory.class)
	@org.springframework.context.annotation.Bean
	@SuppressWarnings("deprecation")
	public ClientHttpRequestFactory defaultRequestFactory() {
		return new org.springframework.http.client.Netty4ClientHttpRequestFactory();
	}

	@org.springframework.context.annotation.Bean("transactionRestTemplate")
	public RestTemplate transactionTemplate(@Autowired ClientHttpRequestFactory requestFactory) {
		RestTemplate restTemplate = new RestTemplate();
		restTemplate.setRequestFactory(requestFactory);
		return restTemplate;
	}

	@DependsOn("transactionRestTemplate")
	@org.springframework.context.annotation.Bean
	public SpringCloudBeanRegistry beanRegistry(@Qualifier("transactionRestTemplate") @Autowired RestTemplate restTemplate) {
		SpringCloudBeanRegistry registry = SpringCloudBeanRegistry.getInstance();
		registry.setRestTemplate(restTemplate);
		return registry;
	}

	@org.springframework.context.annotation.Primary
	@org.springframework.cloud.client.loadbalancer.LoadBalanced
	@org.springframework.context.annotation.Bean
	public RestTemplate defaultRestTemplate(@Autowired TransactionRequestInterceptor transactionRequestInterceptor) {
		RestTemplate restTemplate = new RestTemplate();
		restTemplate.getInterceptors().add(transactionRequestInterceptor);
		return restTemplate;
	}

	public void addInterceptors(InterceptorRegistry registry) {
		TransactionHandlerInterceptor transactionHandlerInterceptor = this.applicationContext
				.getBean(TransactionHandlerInterceptor.class);
		registry.addInterceptor(transactionHandlerInterceptor);
	}

	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

		String[] beanNameArray = beanFactory.getBeanDefinitionNames();
		for (int i = 0; i < beanNameArray.length; i++) {
			BeanDefinition beanDef = beanFactory.getBeanDefinition(beanNameArray[i]);
			String beanClassName = beanDef.getBeanClassName();

			if (FEIGN_FACTORY_CLASS.equals(beanClassName) == false) {
				continue;
			}

			MutablePropertyValues mpv = beanDef.getPropertyValues();
			PropertyValue pv = mpv.getPropertyValue("name");
			String client = String.valueOf(pv.getValue() == null ? "" : pv.getValue());
			if (StringUtils.isNotBlank(client)) {
				this.transientClientSet.add(client);
			}

		}

		this.fireAfterPropertiesSet();

	}

	public void fireAfterPropertiesSet() {
		TransactionClientRegistry registry = TransactionClientRegistry.getInstance();

		String inclusions = this.environment.getProperty(CONSTANT_INCLUSIONS);
		String exclusions = this.environment.getProperty(CONSTANT_EXCLUSIONS);

		if (StringUtils.isNotBlank(inclusions) && StringUtils.isNotBlank(exclusions)) {
			throw new IllegalStateException(String.format("Property '%s' and '%s' can not be configured together!",
					CONSTANT_INCLUSIONS, CONSTANT_EXCLUSIONS));
		} else if (StringUtils.isNotBlank(inclusions)) {
			String[] clients = inclusions.split("\\s*,\\s*");
			for (int i = 0; i < clients.length; i++) {
				String client = clients[i];
				registry.registerClient(client);
			} // end-for (int i = 0; i < clients.length; i++)

			this.transientClientSet.clear();
		} else if (StringUtils.isNotBlank(exclusions)) {
			String[] clients = exclusions.split("\\s*,\\s*");
			for (int i = 0; i < clients.length; i++) {
				String client = clients[i];
				this.transientClientSet.remove(client);
			} // end-for (int i = 0; i < clients.length; i++)

			Iterator<String> itr = this.transientClientSet.iterator();
			while (itr.hasNext()) {
				String client = itr.next();
				itr.remove();
				registry.registerClient(client);
			} // end-while (itr.hasNext())
		} else {
			Iterator<String> itr = this.transientClientSet.iterator();
			while (itr.hasNext()) {
				String client = itr.next();
				itr.remove();
				registry.registerClient(client);
			} // end-while (itr.hasNext())
		}

	}

	public String getEndpoint() {
		return this.identifier;
	}

	public void setEndpoint(String identifier) {
		this.identifier = identifier;
	}

	public void setEnvironment(Environment environment) {
		this.environment = environment;
	}

	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

}