package com.red.circle;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.red.circle.component.redis.annotation.aspect.TaskCacheLockAspect;
import com.red.circle.component.redis.props.RedisConfigProperties;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.tool.core.text.StringUtils;
import io.lettuce.core.internal.HostAndPort;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DnsResolvers;
import io.lettuce.core.resource.MappingSocketAddressResolver;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
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.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableConfigurationProperties({RedisConfigProperties.class, RedisProperties.class})
public class RedisServiceFallbackConfiguration {

  @Bean
  @Primary
  public RedisTemplate<String, Object> redCircleRedisTemplate(
      RedisConnectionFactory connectionFactory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setValueSerializer(new GenericToStringSerializer<>(Object.class));
    template.setKeySerializer(new StringRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setConnectionFactory(connectionFactory);
    return template;
  }

  @Bean
  @ConditionalOnMissingBean
  public KeyGenerator simpleKeyGenerator() {
    return (target, method, params) -> {
      StringBuilder builder = new StringBuilder();
      builder.append(target.getClass().getSimpleName());
      builder.append(".");
      builder.append(method.getName());
      builder.append("[");
      for (Object param : params) {
        builder.append(param);
      }
      builder.append("]");
      return builder.toString();
    };
  }

  @Bean
  @ConditionalOnMissingBean
  public CacheManager cacheManager(
      RedisConnectionFactory redisConnectionFactory,
      RedisConfigProperties redisConfigProperties) {
    return new RedisCacheManager(
        RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
        getRedisCacheConfigurationWithTtl(1800),
        getRedisCacheConfigurationMap(redisConfigProperties));
  }

  @Bean
  @Primary
  @ConditionalOnMissingBean
  public RedisService redisService(
      @Qualifier("redCircleRedisTemplate") RedisTemplate<String, Object> redisTemplate) {
    return new RedisService(redisTemplate);
  }

  @Bean
  @ConditionalOnMissingBean
  public TaskCacheLockAspect taskCacheLockAspect(
      @Qualifier("redCircleRedisTemplate") RedisTemplate<String, Object> redisTemplate) {
    return new TaskCacheLockAspect(redisTemplate.opsForValue());
  }

  @Bean
  @ConditionalOnMissingBean
  public LettuceConnectionFactory redisConnectionFactory(RedisProperties redisProperties) {
    MappingSocketAddressResolver resolver =
        MappingSocketAddressResolver.create(
            DnsResolvers.UNRESOLVED,
            port -> HostAndPort.of(redisProperties.getHost(), port.getPort()));
    ClientResources clientResources =
        ClientResources.builder().socketAddressResolver(resolver).build();

    LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder =
        LettucePoolingClientConfiguration.builder()
            .poolConfig(getPoolConfig(redisProperties.getLettuce().getPool()))
            .clientResources(clientResources);

    if (Objects.nonNull(redisProperties.getLettuce().getShutdownTimeout())) {
      builder.shutdownTimeout(redisProperties.getLettuce().getShutdownTimeout());
    }
    if (Objects.nonNull(redisProperties.getTimeout())) {
      builder.commandTimeout(redisProperties.getTimeout());
    }
    if (StringUtils.isNotBlank(redisProperties.getClientName())) {
      builder.clientName(redisProperties.getClientName());
    }

    RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
    configuration.setDatabase(redisProperties.getDatabase());
    configuration.setHostName(redisProperties.getHost());
    configuration.setPort(redisProperties.getPort());
    configuration.setPassword(redisProperties.getPassword());

    LettucePoolingClientConfiguration clientConfiguration =
        redisProperties.getSsl().isEnabled()
            ? builder.useSsl().disablePeerVerification().build()
            : builder.build();

    return new LettuceConnectionFactory(configuration, clientConfiguration);
  }

  private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap(
      RedisConfigProperties redisConfigProperties) {
    Map<String, RedisCacheConfiguration> configurationMap = new HashMap<>(16);
    redisConfigProperties
        .getInitExpiry()
        .forEach(
            (cacheName, ttl) ->
                configurationMap.put(cacheName, getRedisCacheConfigurationWithTtl(ttl)));
    return configurationMap;
  }

  private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
    Jackson2JsonRedisSerializer<Object> serializer =
        new Jackson2JsonRedisSerializer<>(Object.class);
    ObjectMapper mapper = new ObjectMapper();
    mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    mapper.activateDefaultTyping(
        LaissezFaireSubTypeValidator.instance,
        ObjectMapper.DefaultTyping.NON_FINAL,
        JsonTypeInfo.As.PROPERTY);

    RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
    return configuration
        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
        .entryTtl(Duration.ofSeconds(seconds.longValue()));
  }

  private GenericObjectPoolConfig<?> getPoolConfig(RedisProperties.Pool properties) {
    GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
    poolConfig.setMaxTotal(properties.getMaxActive());
    poolConfig.setMaxIdle(properties.getMaxIdle());
    poolConfig.setMinIdle(properties.getMinIdle());
    if (properties.getTimeBetweenEvictionRuns() != null) {
      poolConfig.setTimeBetweenEvictionRuns(properties.getTimeBetweenEvictionRuns());
    }
    if (properties.getMaxWait() != null) {
      poolConfig.setMaxWait(properties.getMaxWait());
    }
    return poolConfig;
  }
}
