
package org.hongxi.whatsmars.dubbo.actuate.endpoint.mvc;

import com.alibaba.dubbo.config.*;
import com.alibaba.dubbo.config.annotation.Reference;
import com.alibaba.dubbo.config.spring.ReferenceBean;
import com.alibaba.dubbo.config.spring.ServiceBean;
import com.alibaba.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor;
import org.hongxi.whatsmars.dubbo.actuate.endpoint.DubboEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.InjectionMetadata;
import org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentMap;

import static com.alibaba.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor.BEAN_NAME;
import static com.alibaba.dubbo.registry.support.AbstractRegistryFactory.getRegistries;
import static org.hongxi.whatsmars.dubbo.actuate.endpoint.DubboEndpoint.*;
import static org.hongxi.whatsmars.dubbo.autoconfigure.util.DubboUtils.filterDubboProperties;
import static org.springframework.beans.factory.BeanFactoryUtils.beansOfTypeIncludingAncestors;
import static org.springframework.util.ClassUtils.isPrimitiveOrWrapper;

/**
 * {@link MvcEndpoint} to expose Dubbo Metadata
 *
 * @see
 */
public class DubboMvcEndpoint extends EndpointMvcAdapter implements ApplicationContextAware, EnvironmentAware {

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

    private ApplicationContext applicationContext;

    private ConfigurableEnvironment environment;

    public DubboMvcEndpoint(DubboEndpoint dubboEndpoint) {
        super(dubboEndpoint);
    }


    @RequestMapping(value = DUBBO_SHUTDOWN_ENDPOINT_URI, method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public DeferredResult shutdown() throws Exception {

        DeferredResult result = new DeferredResult();

        Map<String, Object> shutdownCountData = new LinkedHashMap<>();

        // registries
        int registriesCount = getRegistries().size();

        // protocols
        int protocolsCount = getProtocolConfigsBeanMap().size();

        ProtocolConfig.destroyAll();
        shutdownCountData.put("registries", registriesCount);
        shutdownCountData.put("protocols", protocolsCount);

        // Service Beans
        Map<String, ServiceBean> serviceBeansMap = getServiceBeansMap();
        if (!serviceBeansMap.isEmpty()) {
            for (ServiceBean serviceBean : serviceBeansMap.values()) {
                serviceBean.destroy();
            }
        }
        shutdownCountData.put("services", serviceBeansMap.size());

        // Reference Beans
        ReferenceAnnotationBeanPostProcessor beanPostProcessor = getReferenceAnnotationBeanPostProcessor();

        int referencesCount = beanPostProcessor.getReferenceBeans().size();

        beanPostProcessor.destroy();

        shutdownCountData.put("references", referencesCount);

        // Set Result to complete
        Map<String, Object> shutdownData = new TreeMap<>();
        shutdownData.put("shutdown.count", shutdownCountData);
        result.setResult(shutdownData);

        return result;

    }

    @RequestMapping(value = DUBBO_CONFIGS_ENDPOINT_URI, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Map<String, Map<String, Map<String, Object>>> configs() {

        Map<String, Map<String, Map<String, Object>>> configsMap = new LinkedHashMap<>();

        addDubboConfigBeans(ApplicationConfig.class, configsMap);
        addDubboConfigBeans(ConsumerConfig.class, configsMap);
        addDubboConfigBeans(MethodConfig.class, configsMap);
        addDubboConfigBeans(ModuleConfig.class, configsMap);
        addDubboConfigBeans(MonitorConfig.class, configsMap);
        addDubboConfigBeans(ProtocolConfig.class, configsMap);
        addDubboConfigBeans(ProviderConfig.class, configsMap);
        addDubboConfigBeans(ReferenceConfig.class, configsMap);
        addDubboConfigBeans(RegistryConfig.class, configsMap);
        addDubboConfigBeans(ServiceConfig.class, configsMap);

        return configsMap;
    }


    @RequestMapping(value = DUBBO_SERVICES_ENDPOINT_URI, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Map<String, Map<String, Object>> services() {

        Map<String, ServiceBean> serviceBeansMap = getServiceBeansMap();

        Map<String, Map<String, Object>> servicesMetadata = new LinkedHashMap<>(serviceBeansMap.size());

        for (Map.Entry<String, ServiceBean> entry : serviceBeansMap.entrySet()) {

            String serviceBeanName = entry.getKey();

            ServiceBean serviceBean = entry.getValue();

            Map<String, Object> serviceBeanMetadata = resolveBeanMetadata(serviceBean);

            Object service = resolveServiceBean(serviceBeanName, serviceBean);

            if (service != null) {
                // Add Service implementation class
                serviceBeanMetadata.put("serviceClass", service.getClass().getName());
            }

            servicesMetadata.put(serviceBeanName, serviceBeanMetadata);

        }

        return servicesMetadata;

    }

    @RequestMapping(value = DUBBO_REFERENCES_ENDPOINT_URI, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Map<String, Map<String, Object>> references() {

        Map<String, Map<String, Object>> referencesMetadata = new LinkedHashMap<>();

        Map<InjectionMetadata.InjectedElement, ReferenceBean<?>> injectedElementReferenceBeanMap
                = resolveInjectedElementReferenceBeanMap();

        for (Map.Entry<InjectionMetadata.InjectedElement, ReferenceBean<?>> entry :
                injectedElementReferenceBeanMap.entrySet()) {

            InjectionMetadata.InjectedElement injectedElement = entry.getKey();

            ReferenceBean<?> referenceBean = entry.getValue();

            Map<String, Object> beanMetadata = resolveBeanMetadata(referenceBean);
            beanMetadata.put("invoker", resolveBeanMetadata(referenceBean.get()));

            referencesMetadata.put(String.valueOf(injectedElement.getMember()), beanMetadata);

        }

        return referencesMetadata;

    }

    @RequestMapping(value = DUBBO_PROPERTIES_ENDPOINT_URI, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public SortedMap<String, Object> properties() {

        return filterDubboProperties(environment);

    }

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

    @Override
    public void setEnvironment(Environment environment) {
        if (environment instanceof ConfigurableEnvironment) {
            this.environment = (ConfigurableEnvironment) environment;
        }
    }

    private Map<String, ServiceBean> getServiceBeansMap() {
        return beansOfTypeIncludingAncestors(applicationContext, ServiceBean.class);
    }


    private void addDubboConfigBeans(Class<? extends AbstractConfig> dubboConfigClass,
                                     Map<String, Map<String, Map<String, Object>>> configsMap) {

        Map<String, ? extends AbstractConfig> dubboConfigBeans = beansOfTypeIncludingAncestors(applicationContext, dubboConfigClass);

        String name = dubboConfigClass.getSimpleName();

        Map<String, Map<String, Object>> beansMetadata = new TreeMap<>();

        for (Map.Entry<String, ? extends AbstractConfig> entry : dubboConfigBeans.entrySet()) {

            String beanName = entry.getKey();
            AbstractConfig configBean = entry.getValue();
            Map<String, Object> configBeanMeta = resolveBeanMetadata(configBean);
            beansMetadata.put(beanName, configBeanMeta);

        }

        configsMap.put(name, beansMetadata);

    }

    private ReferenceAnnotationBeanPostProcessor getReferenceAnnotationBeanPostProcessor() {
        return applicationContext.getBean(BEAN_NAME, ReferenceAnnotationBeanPostProcessor.class);
    }

    /**
     * Resolves the {@link Collection} of {@link InjectionMetadata.InjectedElement} that were annotated by {@link Reference}
     * from all Spring Beans.
     *
     * @return non-null {@link Collection}
     * TODO Reactors ReferenceAnnotationBeanPostProcessor to expose those info
     */
    private Map<InjectionMetadata.InjectedElement, ReferenceBean<?>> resolveInjectedElementReferenceBeanMap() {

        Map<InjectionMetadata.InjectedElement, ReferenceBean<?>> injectedElementReferenceBeanMap = new LinkedHashMap<>();

        final ReferenceAnnotationBeanPostProcessor processor = getReferenceAnnotationBeanPostProcessor();

        ConcurrentMap<String, InjectionMetadata> injectionMetadataCache =
                getFieldValue(processor, "injectionMetadataCache", ConcurrentMap.class);

        ConcurrentMap<String, ReferenceBean<?>> referenceBeansCache =
                getFieldValue(processor, "referenceBeansCache", ConcurrentMap.class);

        for (InjectionMetadata metadata : injectionMetadataCache.values()) {

            Set<InjectionMetadata.InjectedElement> checkedElements =
                    getFieldValue(metadata, "checkedElements", Set.class);

            Collection<InjectionMetadata.InjectedElement> injectedElements =
                    getFieldValue(metadata, "injectedElements", Collection.class);

            Collection<InjectionMetadata.InjectedElement> actualInjectedElements =
                    checkedElements != null ? checkedElements : injectedElements;

            for (InjectionMetadata.InjectedElement injectedElement : actualInjectedElements) {

                ReferenceBean<?> referenceBean = resolveReferenceBean(injectedElement, referenceBeansCache);

                injectedElementReferenceBeanMap.put(injectedElement, referenceBean);

            }
        }

        return injectedElementReferenceBeanMap;

    }

    private ReferenceBean<?> resolveReferenceBean(InjectionMetadata.InjectedElement injectedElement,
                                                  ConcurrentMap<String, ReferenceBean<?>> referenceBeansCache) {

        // Member is Field or Method annotated @Reference
        Member member = injectedElement.getMember();

        Class<?> beanClass = null;

        Reference reference = getFieldValue(injectedElement, "reference", Reference.class);

        if (member instanceof Field) {

            Field field = (Field) member;

            beanClass = field.getType();

        } else if (member instanceof Method) {

            Method method = (Method) member;

            beanClass = ((Method) member).getReturnType();

        } else {

            if (logger.isWarnEnabled()) {
                logger.warn("What's wrong with Member? Member should not be Field or Method");
            }

            throw new IllegalStateException("What's wrong with Member? Member should not be Field or Method");

        }

        String referenceBeanCacheKey = generateReferenceBeanCacheKey(reference, beanClass);

        return referenceBeansCache.get(referenceBeanCacheKey);

    }


    /**
     * Original implementation :
     *
     * @see ReferenceAnnotationBeanPostProcessor#generateReferenceBeanCacheKey(Reference, Class)
     */
    private static String generateReferenceBeanCacheKey(Reference reference, Class<?> beanClass) {

        String interfaceName = resolveInterfaceName(reference, beanClass);

        String key = reference.group() + "/" + interfaceName + ":" + reference.version();

        return key;

    }

    /**
     * Original implementation:
     *
     * @see ReferenceAnnotationBeanPostProcessor#resolveInterfaceName(Reference, Class)
     */
    private static String resolveInterfaceName(Reference reference, Class<?> beanClass)
            throws IllegalStateException {

        String interfaceName;
        if (!"".equals(reference.interfaceName())) {
            interfaceName = reference.interfaceName();
        } else if (!void.class.equals(reference.interfaceClass())) {
            interfaceName = reference.interfaceClass().getName();
        } else if (beanClass.isInterface()) {
            interfaceName = beanClass.getName();
        } else {
            throw new IllegalStateException(
                    "The @Reference undefined interfaceClass or interfaceName, and the property type "
                            + beanClass.getName() + " is not a interface.");
        }

        return interfaceName;

    }


    private <T> T getFieldValue(Object object, String fieldName, Class<T> fieldType) {

        Field field = ReflectionUtils.findField(object.getClass(), fieldName, fieldType);

        ReflectionUtils.makeAccessible(field);

        return (T) ReflectionUtils.getField(field, object);

    }

    private Map<String, Object> resolveBeanMetadata(final Object bean) {

        final Map<String, Object> beanMetadata = new LinkedHashMap<>();

        try {

            BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());
            PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();

            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {

                Method readMethod = propertyDescriptor.getReadMethod();

                if (readMethod != null && isSimpleType(propertyDescriptor.getPropertyType())) {

                    String name = Introspector.decapitalize(propertyDescriptor.getName());
                    Object value = readMethod.invoke(bean);

                    beanMetadata.put(name, value);
                }

            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return beanMetadata;

    }

    private Object resolveServiceBean(String serviceBeanName, ServiceBean serviceBean) {

        int index = serviceBeanName.indexOf("#");

        if (index > -1) {

            Class<?> interfaceClass = serviceBean.getInterfaceClass();

            String serviceName = serviceBeanName.substring(index + 1);

            if (applicationContext.containsBean(serviceName)) {
                return applicationContext.getBean(serviceName, interfaceClass);
            }

        }

        return null;

    }


    private static boolean isSimpleType(Class<?> type) {
        return isPrimitiveOrWrapper(type)
                || type == String.class
                || type == BigDecimal.class
                || type == BigInteger.class
                || type == Date.class
                || type == URL.class
                || type == Class.class
                ;
    }


    private Map<String, ProtocolConfig> getProtocolConfigsBeanMap() {
        return beansOfTypeIncludingAncestors(applicationContext, ProtocolConfig.class);
    }


}
