package cn.datax.common.redis.aspectj;

import cn.datax.common.redis.annotation.RedisCacheAop;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Aspect
@Slf4j
public class RedisCacheAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 分隔符 生成key 格式为 类全类名|方法名|参数所属类全类名
     */
    private static final String DELIMITER = ":";

    /**
     * Service层切点 使用到了我们定义的 RedisCacheAspect 作为切点表达式。
     * 而且我们可以看出此表达式基于 annotation。
     * 并且用于内建属性为查询的方法之上
     */
    @Pointcut("@annotation(cn.datax.common.redis.annotation.RedisCacheAop)")
    public void redisCacheAspect() {
    }

    /**
     * Around 手动控制调用核心业务逻辑，以及调用前和调用后的处理,
     * <p>
     * 注意：当核心业务抛异常后，立即退出，转向AfterAdvice 执行完AfterAdvice，再转到ThrowingAdvice
     */
    @Around(value = "redisCacheAspect()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        // 得到类名、方法名和参数
        String clazzName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();

        // 根据类名、方法名和参数生成Key
        log.info("key参数: " + clazzName + "." + methodName);
        String key = getKey(clazzName, methodName, args);
        log.info("生成key: " + key);

        // 得到被代理的方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();

        String cacheName = method.getAnnotation(RedisCacheAop.class).cacheName();

        long expire = method.getAnnotation(RedisCacheAop.class).expire();

        Class objectType = method.getAnnotation(RedisCacheAop.class).clazz();

        boolean isArray = method.getAnnotation(RedisCacheAop.class).isArray();

        // 检查Redis中是否有缓存
        String value = (String) redisTemplate.opsForValue().get(key + DELIMITER + cacheName);

        // result是方法的最终返回结果
        Object result = null;
        try {
            if (null == value) {
                log.info("缓存未命中");
                // 调用数据库查询方法
                result = joinPoint.proceed(args);
                // 结果放入缓存
                if (expire > 0) {
                    redisTemplate.opsForValue().set(key + DELIMITER + cacheName, objectMapper.writeValueAsString(result), expire, TimeUnit.SECONDS);
                } else {
                    redisTemplate.opsForValue().set(key + DELIMITER + cacheName, objectMapper.writeValueAsString(result));
                }
            } else {
                /**
                 * 可以直接针对mapper进行缓存，如果mapper查询返回的List<Objec> 需要isArray 为true 否则转换异常
                 */
                if (isArray){
                    JavaType javaType = objectMapper.getTypeFactory().constructCollectionType(List.class, objectType);
                    return objectMapper.readValue(value, javaType);
                }else {
                    return objectMapper.readValue(value, objectType);
                }
            }
        } catch (Throwable e) {
            log.error("程序异常", e.getMessage());
            throw e;
        }
        return result;
    }

    /**
     * 根据类名、方法名和参数生成Key
     * @param clazzName
     * @param methodName
     * @param args
     * @return key格式：全类名:方法名:参数类型
     */
    private String getKey(String clazzName, String methodName, Object[] args) {
        StringBuilder key = new StringBuilder(clazzName);
        key.append(DELIMITER);
        key.append(methodName);
        key.append(DELIMITER);
        key.append(Arrays.stream(args).map(x -> x.toString()).collect(Collectors.joining(DELIMITER)));
        return key.toString();
    }
}
