/*
 * Copyright 2013-present 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.circuitbreaker.sentinel.feign;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import com.alibaba.cloud.circuitbreaker.sentinel.SentinelConfigBuilder;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.cloud.client.circuitbreaker.AbstractCircuitBreakerFactory;
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.core.annotation.AnnotationUtils;

/**
 * Sentinel circuit breaker config change listener.
 *
 * @author freeman
 * @since 2021.0.1.0
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
public class CircuitBreakerRuleChangeListener implements ApplicationContextAware,
		ApplicationListener<RefreshScopeRefreshedEvent>, SmartInitializingSingleton {
	private static final Logger LOGGER = LoggerFactory
			.getLogger(CircuitBreakerRuleChangeListener.class);

	private @Nullable SentinelFeignClientProperties properties;
	/**
	 * properties backup, prevent rules from being updated every time the container is
	 * refreshed.
	 */
	private @Nullable SentinelFeignClientProperties propertiesBackup;
	private @Nullable AbstractCircuitBreakerFactory circuitBreakerFactory;
	private @Nullable ApplicationContext applicationContext;

	@Override
	public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
		ensureReady();

		// No need to update the rules
		if (Objects.equals(properties, propertiesBackup)) {
			return;
		}

		if (properties == null || circuitBreakerFactory == null) {
			return;
		}

		clearRules();

		// rebind
		configureDefault();
		configureCustom();

		updateBackup();

		LOGGER.info("Sentinel circuit beaker rules refreshed: \n"
				+ prettyPrint(properties.getRules()));
	}

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

	@Override
	public void afterSingletonsInstantiated() {
		if (applicationContext != null) {
			this.propertiesBackup = applicationContext
					.getBean(SentinelFeignClientProperties.class).copy();
		}
	}

	private void ensureReady() {
		// Do not inject these beans directly,
		// as it will cause the bean to be initialized prematurely,
		// and we don't want to change the initialization order of the beans
		if (circuitBreakerFactory == null && applicationContext != null) {
			String[] names = applicationContext
					.getBeanNamesForType(AbstractCircuitBreakerFactory.class);
			if (names.length >= 1) {
				this.circuitBreakerFactory = applicationContext.getBean(names[0],
						AbstractCircuitBreakerFactory.class);
			}
		}
		if (properties == null && applicationContext != null) {
			this.properties = applicationContext
					.getBean(SentinelFeignClientProperties.class);
		}
	}

	private void clearRules() {
		clearCircuitBreakerFactory();
		clearFeignClientRulesInDegradeManager();
	}

	private void configureDefault() {
		if (properties != null && circuitBreakerFactory != null) {
			configureDefault(properties, circuitBreakerFactory);
		}
	}

	private void configureCustom() {
		if (properties != null && circuitBreakerFactory != null) {
			configureCustom(properties, circuitBreakerFactory);
		}
	}

	private void clearCircuitBreakerFactory() {
		if (circuitBreakerFactory != null) {
			Optional.ofNullable(getConfigurations(circuitBreakerFactory))
					.ifPresent(Map::clear);
		}
	}

	private void clearFeignClientRulesInDegradeManager() {
		if (propertiesBackup == null || applicationContext == null) {
			return;
		}
		// first, clear all manually configured feign clients and methods.
		propertiesBackup.getRules().keySet().stream()
				.filter(key -> !Objects.equals(key, propertiesBackup.getDefaultRule()))
				.forEach(resource -> Optional
						.ofNullable(DegradeRuleManager.getRulesOfResource(resource))
						.ifPresent(Set::clear));

		// Find all feign clients, clear the corresponding rules
		// NOTE: feign client name cannot be the same as the general resource name !!!
		Arrays.stream(applicationContext.getBeanNamesForAnnotation(FeignClient.class))
				// A little trick, FeignClient bean name is full class name.
				// Simple exclusions, such as its subclass.
				.filter(beanName -> beanName.contains(".")).map(beanName -> {
					try {
						return Class.forName(beanName);
					}
					catch (ClassNotFoundException ignore) {
						// definitely not a feign client, just ignore
						return null;
					}
				}).filter(Objects::nonNull).forEach(clazz -> {
					FeignClient anno = clazz.getAnnotation(FeignClient.class);
					Object value = AnnotationUtils.getValue(anno);
					if (anno == null || value == null) {
						return;
					}
					String feignClientName = value.toString();
					Optional.ofNullable(
							DegradeRuleManager.getRulesOfResource(feignClientName))
							.ifPresent(Set::clear);
				});
	}

	private void updateBackup() {
		if (this.properties != null) {
			this.propertiesBackup = this.properties.copy();
		}
	}

	private String prettyPrint(Object o) {
		try {
			return JsonMapper.builder().configure(SerializationFeature.INDENT_OUTPUT, true).build()
					.writeValueAsString(o);
		}
		catch (Exception e) {
			LOGGER.error("JSON serialization err.", e);
			return "__JSON format err__";
		}
	}

	// static method

	public static void configureCustom(SentinelFeignClientProperties properties,
			AbstractCircuitBreakerFactory factory) {
		properties.getRules().forEach((resourceName, degradeRules) -> {
			if (!Objects.equals(properties.getDefaultRule(), resourceName)) {
				factory.configure(builder -> ((SentinelConfigBuilder) builder)
						.rules(properties.getRules().getOrDefault(resourceName,
								new ArrayList<>())),
						resourceName);
			}
		});
	}

	public static void configureDefault(SentinelFeignClientProperties properties,
			AbstractCircuitBreakerFactory factory) {
		List<DegradeRule> defaultConfigurations = properties.getRules()
				.getOrDefault(properties.getDefaultRule(), new ArrayList<>());
		factory.configureDefault(
				resourceName -> new SentinelConfigBuilder(resourceName.toString())
						.entryType(EntryType.OUT).rules(defaultConfigurations).build());
	}

	public static Map getConfigurations(
			AbstractCircuitBreakerFactory circuitBreakerFactory) {
		try {
			Method method = AbstractCircuitBreakerFactory.class
					.getDeclaredMethod("getConfigurations");
			method.setAccessible(true);
			return (Map) method.invoke(circuitBreakerFactory);
		}
		catch (Exception ignored) {
		}
		return Collections.emptyMap();
	}

}
