package org.ssssssss.magicapi.redis;


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import org.ssssssss.magicapi.utils.Assert;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * redis 多数据源插件模块
 * @since 3.0.0
 * @author 冰点
 */
@Slf4j
@Component
public class RedisInstanceFactory implements ApplicationContextAware {

    /**
     * RedisTemplate缓存 取值规则 redisName + “RedisTemplate”  和 redisName + “StringRedisTemplate”
     */
    public final Map<String, RedisTemplate<?, ?>> REDIS_HOLDER = new ConcurrentHashMap<>();

    private static ApplicationContext applicationContext;

    public RedisInstanceFactory() {
    }

    public RedisTemplate<?, ?> getRedisTemplate(String redisName, boolean isStringRedisTemplate) {
        return REDIS_HOLDER.get(getRedisTemplateName(redisName, isStringRedisTemplate));
    }
    /**
     * 生成RedisTemplate服务
     *
     * @param redisName       redis名称
     * @param redisProperties redis属性
     */
    public void buildRedisTemplate(String redisName, MagicRedisProperties redisProperties) {
        RedisConnectionFactory redisConnectionFactory = buildLettuceConnectionFactory(redisName, redisProperties);
        Assert.isNotNull(redisConnectionFactory, "Redis连接创建失败");
        RedisTemplate redisTemplate = buildRedisTemplate(redisName, redisConnectionFactory);
        Assert.isNotNull(redisTemplate, "RedisTemplate创建失败");
        StringRedisTemplate stringRedisTemplate = buildStringRedisTemplate(redisName, redisConnectionFactory);
        Assert.isNotNull(stringRedisTemplate, "StringRedisTemplate创建失败");
    }

    /**
     * 创建 ConnectionFactory
     */
    public RedisConnectionFactory buildLettuceConnectionFactory(String redisName, MagicRedisProperties redisProperties) {
        String redisClientName = redisName+"RedisConnectionFactory";
        if (applicationContext.containsBean(redisClientName)) {
            return getBean(redisClientName);
        }
        ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
        // 获取连接工厂
        RedisConnectionFactory lettuceConnectionFactory = getLettuceConnectionFactory(redisProperties);
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
                .genericBeanDefinition(RedisConnectionFactory.class, () -> lettuceConnectionFactory);
        BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
        // 设置primary bean
        if (redisName.startsWith(MagicDynamicRedisAutoConfig.primary)) {
            beanDefinition.setPrimary(true);
        }
        beanFactory.registerBeanDefinition(redisClientName, beanDefinition);
        return getBean(redisClientName);
    }

    /**
     * 获取Lettuce连接工厂
     *
     * @param redisProperties Redis配置
     */
    public static RedisConnectionFactory getLettuceConnectionFactory(MagicRedisProperties redisProperties) {
        MagicRedisProperties.Pool pool = redisProperties.getLettuce().getPool();
        GenericObjectPoolConfig poolConfig = (new PoolBuilderFactory()).getPoolConfig(pool);
        LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
                .commandTimeout(redisProperties.getTimeout())
                .poolConfig(poolConfig)
                .build();
        LettuceConnectionFactory lettuceConnectionFactory = null;
        RedisConnectionType connectionType = RedisConnectionType.match(redisProperties.getType());
        if (RedisConnectionType.SENTINEL.equals(connectionType)) {
            lettuceConnectionFactory = new LettuceConnectionFactory(buildSentinelConfig(redisProperties), clientConfig);
        } else if (RedisConnectionType.CLUSTER.equals(connectionType)) {
            lettuceConnectionFactory = new LettuceConnectionFactory(buildClusterConfig(redisProperties), clientConfig);
        } else {
            lettuceConnectionFactory = new LettuceConnectionFactory(buildStandaloneConfig(redisProperties), clientConfig);
        }
        return lettuceConnectionFactory;
    }

    /**
     * 生成 StringRedisTemplate
     *
     * @param redisName                redis名称
     * @param lettuceConnectionFactory 连接工厂
     * @return StringRedisTemplate
     */
    private StringRedisTemplate buildStringRedisTemplate(String redisName, RedisConnectionFactory lettuceConnectionFactory) {
        ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
        constructorArgumentValues.addIndexedArgumentValue(0, lettuceConnectionFactory);

        String beanName = getRedisTemplateName(redisName, true);
        registerBean(beanName, StringRedisTemplate.class,null, constructorArgumentValues);
        StringRedisTemplate redisTemplate = getBean(beanName);
        // 保存RedisTemplate
        saveRedisBean(beanName, redisTemplate);
        log.info("[动态Redis] - {}创建成功", beanName);
        return redisTemplate;
    }


    /**
     * 生成RedisTemplate
     *
     * @param redisName         缓存名称
     * @param connectionFactory 连接工厂
     * @return RedisTemplate
     */
    private RedisTemplate buildRedisTemplate(String redisName, RedisConnectionFactory connectionFactory) {
        String redisClientName = redisName + RedisConstant.REDIS_TEMPLATE;
        if (applicationContext.containsBean(redisClientName)) {
            return getBean(redisClientName);
        }
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(mapper, Object.class);
        StringRedisSerializer keySerializer = new StringRedisSerializer();

        // 构造参数
        Map<String, Object> property = new HashMap<>(8);
        property.put("connectionFactory", connectionFactory);
        property.put("keySerializer", keySerializer);
        property.put("hashKeySerializer", keySerializer);
        property.put("valueSerializer", serializer);
        property.put("hashValueSerializer", serializer);
        registerBean(redisClientName, RedisTemplate.class, property,null);
//        setBean(redisClientName, RedisTemplate.class, property);
        RedisTemplate redisTemplate = getBean(redisClientName);
        saveRedisBean(redisClientName, redisTemplate);
        log.info("[动态Redis] - {}创建成功", redisClientName);
        return redisTemplate;
    }


    /**
     * 组装单机配置参数
     *
     * @param redisProperties 属性配置
     * @return 构造Bean参数
     */
    public static RedisStandaloneConfiguration buildStandaloneConfig(MagicRedisProperties redisProperties) {
        RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration();
        String[] addresses = redisProperties.getAddress().split(":");
        standaloneConfig.setHostName(addresses[0]);
        standaloneConfig.setPort(Integer.parseInt(addresses[1]));
        String username = redisProperties.getUsername();
        if (StringUtils.hasText(username)) {
            standaloneConfig.setUsername(username);
        }
        String password = redisProperties.getPassword();
        if (StringUtils.hasText(password)) {
            standaloneConfig.setPassword(RedisPassword.of(password));
        }
        standaloneConfig.setDatabase(redisProperties.getDatabase());
        return standaloneConfig;
    }


    /**
     * 组装哨兵配置参数
     *
     * @param redisProperties 属性配置
     * @return 构造Bean参数
     */
    private static RedisSentinelConfiguration buildSentinelConfig(MagicRedisProperties redisProperties) {
        String[] addresses = redisProperties.getAddress().split(",");
        Assert.isTrue(addresses.length > 1, "哨兵模式不能配置单节点");
        List<RedisNode> sentinelNode = new ArrayList<>();
        for (String address : addresses) {
            Assert.isTrue(address.contains(","), "地址格式不正确,格式必须是:[地址:端口]");
            String[] addr = address.split(",");
            sentinelNode.add(new RedisNode(addr[0].trim(), Integer.parseInt(addr[1])));
        }
        String masterName = redisProperties.getSentinel().getMaster();
        Assert.isNotBlank(masterName, "哨兵模式必须指定主节点名称");
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration();
        sentinelConfig.setMaster(masterName);
        sentinelConfig.setDatabase(redisProperties.getDatabase());
        String username = redisProperties.getUsername();
        if (StringUtils.hasText(username)) {
            sentinelConfig.setUsername(username);
        }
        String password = redisProperties.getPassword();
        if (StringUtils.hasText(password)) {
            sentinelConfig.setPassword(RedisPassword.of(password));
        }
        sentinelConfig.setSentinels(sentinelNode);
        return sentinelConfig;
    }

    /**
     * 组装集群配置参数
     *
     * @param redisProperties 属性配置
     * @return 构造Bean参数
     */
    private static RedisClusterConfiguration buildClusterConfig(MagicRedisProperties redisProperties) {
        String[] addresses = redisProperties.getAddress().split(",");
        Assert.isTrue(addresses.length > 1, "集群模式不能配置单节点");
        // 集群节点
        List<RedisNode> clusterNode = new ArrayList<>();
        for (String address : addresses) {
            Assert.isTrue(address.contains(","), "地址格式不正确,格式必须是:[地址:端口]");
            String[] addr = address.split(",");
            clusterNode.add(new RedisNode(addr[0].trim(), Integer.parseInt(addr[1])));
        }
        RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
        clusterConfig.setClusterNodes(clusterNode);
        // 密码，处理带用户名的场景
        String username = redisProperties.getUsername();
        if (StringUtils.hasText(username)) {
            clusterConfig.setUsername(username);
        }
        String password = redisProperties.getPassword();
        if (StringUtils.hasText(password)) {
            clusterConfig.setPassword(RedisPassword.of(password));
        }
        // 集群默认重试次数
        int maxRedirects = redisProperties.getMaxRedirects();
        clusterConfig.setMaxRedirects(maxRedirects <= 0 ? 5 : maxRedirects);
        return clusterConfig;
    }

    /**
     * 注册Bean
     *
     * @param beanName        bean名称
     * @param clazz           class
     * @param constructorArgs 构造函数
     */
    public synchronized void setCosBean(String beanName, Class<?> clazz, ConstructorArgumentValues constructorArgs) {
        checkApplicationContext();
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
        if (beanFactory.containsBean(beanName)) {
            return;
        }
        GenericBeanDefinition definition = new GenericBeanDefinition();
        //类class
        definition.setBeanClass(clazz);
        if (beanName.startsWith(MagicDynamicRedisAutoConfig.primary)) {
            definition.setPrimary(true);
        }
        //属性赋值
        definition.setConstructorArgumentValues(new ConstructorArgumentValues(constructorArgs));
        //注册到spring上下文
        beanFactory.registerBeanDefinition(beanName, definition);
    }

    /**
     * 同步方法注册bean到ApplicationContext中
     *
     * @param beanName bean name
     * @param clazz    bean class
     * @param original bean的属性值
     */
    public synchronized void setBean(String beanName, Class<?> clazz, Map<String, Object> original) {
        checkApplicationContext();
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
        if (beanFactory.containsBean(beanName)) {
            return;
        }
        GenericBeanDefinition definition = new GenericBeanDefinition();
        //类class
        definition.setBeanClass(clazz);
        if (beanName.startsWith(MagicDynamicRedisAutoConfig.primary)) {
            definition.setPrimary(true);
        }
        //属性赋值
        definition.setPropertyValues(new MutablePropertyValues(original));
        //注册到spring上下文
        beanFactory.registerBeanDefinition(beanName, definition);
    }

    private  String getRedisTemplateName(String redisName, boolean isStringRedisTemplate) {
        return redisName + (isStringRedisTemplate ? RedisConstant.STRING_REDIS_TEMPLATE : RedisConstant.REDIS_TEMPLATE);
    }

    private void registerBeanToContext(String beanName, Class<?> clazz, Map<String, Object> properties, ConstructorArgumentValues args) {
        checkApplicationContext();
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
        if (beanFactory.containsBean(beanName)) {
            return;
        }
        GenericBeanDefinition definition = new GenericBeanDefinition();
        definition.setBeanClass(clazz);
        definition.setPrimary(beanName.startsWith(MagicDynamicRedisAutoConfig.primary));
        if (args != null) {
            definition.setConstructorArgumentValues(args);
        } else {
            definition.setPropertyValues(new MutablePropertyValues(properties));
        }
        beanFactory.registerBeanDefinition(beanName, definition);
    }

    // 合并 setCosBean 和 setBean 方法
    public synchronized void registerBean(String beanName, Class<?> clazz, Map<String, Object> properties, ConstructorArgumentValues args) {
        registerBeanToContext(beanName, clazz, properties, args);
    }
    /**
     * 保存RedisBean
     *
     * @param beanName      redisName名称
     * @param redisTemplate redisTemplate
     */
    private void saveRedisBean(String beanName, RedisTemplate<?, ?> redisTemplate) {
        REDIS_HOLDER.put(beanName, redisTemplate);
    }

    /**
     * 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) {
        checkApplicationContext();
        if (applicationContext.containsBean(name)) {
            return (T) applicationContext.getBean(name);
        }
        return null;
    }

    private static void checkApplicationContext() {
        if (applicationContext == null) {
            throw new IllegalStateException("applicaitonContext未注入,请检查配置");
        }
    }

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

    /**
     * 线程池工程
     */
    private static class PoolBuilderFactory {
        public GenericObjectPoolConfig getPoolConfig(MagicRedisProperties.Pool properties) {
            GenericObjectPoolConfig config = new GenericObjectPoolConfig();
            config.setMaxTotal(properties.getMaxActive());
            config.setMaxIdle(properties.getMaxIdle());
            config.setMinIdle(properties.getMinIdle());
            if (properties.getMaxWait() != null) {
                config.setMaxWaitMillis(properties.getMaxWait().toMillis());
            }
            return config;
        }
    }
}