package com.zoctan.seedling.core.cache;

import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.cache.RedisCache;
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.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.lang.NonNull;
import org.springframework.util.ReflectionUtils;

import java.time.Duration;
import java.util.*;
import java.util.concurrent.Callable;

/**
 * Redis 容易出现缓存问题（超时、Redis 宕机等），当使用 spring cache 的注释 Cacheable、Cacheput 等处理缓存问题时， 我们无法使用 try catch
 * 处理出现的异常，所以最后导致结果是整个服务报错无法正常工作。 通过自定义 MyRedisCacheManager 并继承 RedisCacheManager 来处理异常可以解决这个问题
 *
 * <p>http://www.spring4all.com/article/937
 *
 * @author Zoctan
 * @date 2018/07/11
 */
@Slf4j
public class MyRedisCacheManager extends RedisCacheManager
    implements ApplicationContextAware, InitializingBean {
  /** key serializer */
  public static final StringRedisSerializer STRING_SERIALIZER = new StringRedisSerializer();
  /**
   * value serializer
   *
   * <p>使用 FastJsonRedisSerializer 会报错：java.lang.ClassCastException FastJsonRedisSerializer<Object>
   * fastSerializer = new FastJsonRedisSerializer<>(Object.class);
   */
  public static final GenericFastJsonRedisSerializer FASTJSON_SERIALIZER =
      new GenericFastJsonRedisSerializer();
  /** key serializer pair */
  public static final RedisSerializationContext.SerializationPair<String> STRING_PAIR =
      RedisSerializationContext.SerializationPair.fromSerializer(STRING_SERIALIZER);
  /** value serializer pair */
  public static final RedisSerializationContext.SerializationPair<Object> FASTJSON_PAIR =
      RedisSerializationContext.SerializationPair.fromSerializer(FASTJSON_SERIALIZER);

  private final Map<String, RedisCacheConfiguration> initialCacheConfiguration =
      new LinkedHashMap<>();
  private ApplicationContext applicationContext;

  public MyRedisCacheManager(
      final RedisCacheWriter cacheWriter, final RedisCacheConfiguration defaultCacheConfiguration) {
    super(cacheWriter, defaultCacheConfiguration);
  }

  public MyRedisCacheManager(
      final RedisCacheWriter cacheWriter,
      final RedisCacheConfiguration defaultCacheConfiguration,
      final Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
    super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
  }

  @Override
  public Cache getCache(@NonNull final String name) {
    final Cache cache = super.getCache(name);
    return new RedisCacheWrapper(cache);
  }

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

  @Override
  public void afterPropertiesSet() {
    final String[] beanNames = this.applicationContext.getBeanNamesForType(Object.class);
    for (final String beanName : beanNames) {
      final Class clazz = this.applicationContext.getType(beanName);
      this.add(clazz);
    }
    super.afterPropertiesSet();
  }

  @NonNull
  @Override
  protected Collection<RedisCache> loadCaches() {
    final List<RedisCache> caches = new LinkedList<>();
    for (final Map.Entry<String, RedisCacheConfiguration> entry :
        this.initialCacheConfiguration.entrySet()) {
      caches.add(super.createRedisCache(entry.getKey(), entry.getValue()));
    }
    return caches;
  }

  private void add(final Class clazz) {
    ReflectionUtils.doWithMethods(
        clazz,
        method -> {
          ReflectionUtils.makeAccessible(method);
          final CacheExpire cacheExpire = AnnotationUtils.findAnnotation(method, CacheExpire.class);
          if (!Optional.ofNullable(cacheExpire).isPresent()) {
            return;
          }
          final Cacheable cacheable = AnnotationUtils.findAnnotation(method, Cacheable.class);
          if (Optional.ofNullable(cacheable).isPresent()) {
            this.add(cacheable.cacheNames(), cacheExpire);
            return;
          }
          final Caching caching = AnnotationUtils.findAnnotation(method, Caching.class);
          if (Optional.ofNullable(caching).isPresent()) {
            final Cacheable[] cs = caching.cacheable();
            if (cs.length > 0) {
              for (final Cacheable c : cs) {
                if (Optional.ofNullable(c).isPresent()) {
                  this.add(c.cacheNames(), cacheExpire);
                }
              }
            }
          } else {
            final CacheConfig cacheConfig =
                AnnotationUtils.findAnnotation(clazz, CacheConfig.class);
            if (Optional.ofNullable(cacheConfig).isPresent()) {
              this.add(cacheConfig.cacheNames(), cacheExpire);
            }
          }
        },
        method -> null != AnnotationUtils.findAnnotation(method, CacheExpire.class));
  }

  private void add(final String[] cacheNames, final CacheExpire cacheExpire) {
    for (final String cacheName : cacheNames) {
      if (!Optional.ofNullable(cacheName).isPresent() || "".equals(cacheName.trim())) {
        continue;
      }
      final long expire = cacheExpire.expire();
      log.debug("cache name<{}> expire: {}", cacheName, expire);
      if (expire >= 0) {
        // 缓存配置
        final RedisCacheConfiguration config =
            RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(expire))
                .disableCachingNullValues()
                // .prefixKeysWith(cacheName)
                .serializeKeysWith(STRING_PAIR)
                .serializeValuesWith(FASTJSON_PAIR);
        this.initialCacheConfiguration.put(cacheName, config);
      } else {
        log.warn("{} use default expiration.", cacheName);
      }
    }
  }

  protected static class RedisCacheWrapper implements Cache {
    private final Cache cache;

    RedisCacheWrapper(final Cache cache) {
      this.cache = cache;
    }

    @Override
    public String getName() {
      MyRedisCacheManager.log.debug("get name: {}", this.cache.getName());
      try {
        return this.cache.getName();
      } catch (final Exception e) {
        MyRedisCacheManager.log.error("get name => {}", e.getMessage());
        return null;
      }
    }

    @Override
    public Object getNativeCache() {
      MyRedisCacheManager.log.debug("native cache: {}", this.cache.getNativeCache());
      try {
        return this.cache.getNativeCache();
      } catch (final Exception e) {
        MyRedisCacheManager.log.error("get native cache => {}", e.getMessage());
        return null;
      }
    }

    @Override
    public ValueWrapper get(@NonNull final Object o) {
      MyRedisCacheManager.log.debug("get => o: {}", o);
      try {
        return this.cache.get(o);
      } catch (final Exception e) {
        MyRedisCacheManager.log.error("get => o: {}, error: {}", o, e.getMessage());
        return null;
      }
    }

    @Override
    public <T> T get(@NonNull final Object o, final Class<T> aClass) {
      MyRedisCacheManager.log.debug("get => o: {}, clazz: {}", o, aClass);
      try {
        return this.cache.get(o, aClass);
      } catch (final Exception e) {
        MyRedisCacheManager.log.error("get => o: {}, clazz: {}, error: {}", o, aClass, e.getMessage());
        return null;
      }
    }

    @Override
    public <T> T get(@NonNull final Object o, @NonNull final Callable<T> callable) {
      MyRedisCacheManager.log.debug("get => o: {}", o);
      try {
        return this.cache.get(o, callable);
      } catch (final Exception e) {
        MyRedisCacheManager.log.error("get => o: {}, error: {}", o, e.getMessage());
        return null;
      }
    }

    @Override
    public void put(@NonNull final Object o, final Object o1) {
      MyRedisCacheManager.log.debug("put => o: {}, o1: {}", o, o1);
      try {
        this.cache.put(o, o1);
      } catch (final Exception e) {
        MyRedisCacheManager.log.error("put => o: {}, o1: {}, error: {}", o, o1, e.getMessage());
      }
    }

    @Override
    public ValueWrapper putIfAbsent(@NonNull final Object o, final Object o1) {
      MyRedisCacheManager.log.debug("put if absent => o: {}, o1: {}", o, o1);
      try {
        return this.cache.putIfAbsent(o, o1);
      } catch (final Exception e) {
        MyRedisCacheManager.log.error("put if absent => o: {}, o1: {}, error: {}", o, o1, e.getMessage());
        return null;
      }
    }

    @Override
    public void evict(@NonNull final Object o) {
      MyRedisCacheManager.log.debug("evict => o: {}", o);
      try {
        this.cache.evict(o);
      } catch (final Exception e) {
        MyRedisCacheManager.log.error("evict => o: {}, error: {}", o, e.getMessage());
      }
    }

    @Override
    public void clear() {
      MyRedisCacheManager.log.debug("clear");
      try {
        this.cache.clear();
      } catch (final Exception e) {
        MyRedisCacheManager.log.error("clear => error: {}", e.getMessage());
      }
    }
  }
}
