package cn.axzo.msg.center.inside.notices.aspect;

import cn.axzo.basics.common.manager.auth.UserContextHolder;
import cn.axzo.msg.center.common.exception.ServiceException;
import cn.axzo.msg.center.common.utils.MD5;
import cn.axzo.msg.center.common.utils.WrappedHttpServletRequest;
import cn.axzo.msg.center.inside.notices.annotation.RepeatSubCheck;
import cn.azxo.framework.common.model.CommonResponse;
import cn.hutool.core.util.StrUtil;
import io.micrometer.core.instrument.util.StringUtils;
import lombok.RequiredArgsConstructor;
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.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;

/**
 * 重复提交切面
 *
 * @author yangzhi
 * @Date 2021/5/20 15:00
 **/
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class RepeatedSubmitAspect {

  private final RedissonClient redissonClient;
  private static final String MOCK = "Mock";

  private static final String REDIS_LOCK_PREFIX="msg-center_";

  @Around("@annotation(repeatSubCheck)")
  public Object lock(ProceedingJoinPoint joinPoint, RepeatSubCheck repeatSubCheck)
      throws Exception {
    String redisKey;
    RLock lock = null;
    try {
      RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
      HttpServletRequest httpServletRequest = ((ServletRequestAttributes) attributes).getRequest();
      String isMock = httpServletRequest.getHeader(MOCK);
      if (StringUtils.isNotEmpty(isMock) && isMock.equals(MOCK)) {
        log.info("mock请求,不做重复提交校验");
        return joinPoint.proceed();
      }
      String uri = httpServletRequest.getRequestURI();
      String methodName = httpServletRequest.getMethod();
      StringJoiner stringJoiner = new StringJoiner(":");
      //stringJoiner.add(REDIS_LOCK_PREFIX);
      stringJoiner.add("URI_LIMIT");
      String spElKey = repeatSubCheck.spElKey();
      /*
       * 1.如果指定了key,将使用自定义的key
       * 2.否则使用uri当作redis的key
       */
      if (StrUtil.isNotBlank(spElKey)) {
        getKeyBySpElExpression(stringJoiner, joinPoint, repeatSubCheck);
      } else {
        stringJoiner.add(uri).add(methodName);
        /*
         * 1.unique为true时,redis不添加用户id
         * 2.unique为false时,只针对当前用户做过滤
         */
        if (!repeatSubCheck.unique()) {
          Long userId = UserContextHolder.get().getUserId();
          if (null == userId) {
            throw new ServiceException("获取用户信息失败");
          }
          stringJoiner.add(String.valueOf(userId));
        }
      }
      if (repeatSubCheck.containParams()) {
        try {
          String requestParams = ((WrappedHttpServletRequest) httpServletRequest).getRequestParams();
          stringJoiner.add(MD5.getMD5Code(requestParams));
        } catch (Exception e) {
          log.error("处理包含参数的 Redis 锁异常: {}", e.getMessage());
        }
      }
      redisKey = stringJoiner.toString();

      /*
       * redis加锁
       */
      log.info("redis-lock-key: {} start", redisKey);
      lock = redissonClient.getLock(redisKey);
      boolean flag = lock.tryLock(0, repeatSubCheck.value(), TimeUnit.MILLISECONDS);
      if (!flag) {
        /*防止释放别人的锁,所以将其置空*/
        lock = null;
        log.info("重复数据提交,请稍后重试!,redisKey={}" + redisKey);
        return CommonResponse.fail("重复数据提交,请稍后重试!");
      }
      return joinPoint.proceed();
    } catch (Throwable throwable) {
      throw (Exception) throwable;
    } finally {
      if (null != lock) {
        try {
          //释放锁时,有可能释放别人的锁,所以捕获异常,不抛异常
          lock.unlock();
          log.info("redis-lock-key: {} end", lock.getName());
        } catch (Exception e) {
        }
      }
    }
  }

  /**
   * 根据spEl获取key值
   *
   * @param stringJoiner
   * @param joinPoint
   * @param repeatSubCheck
   */
  private void getKeyBySpElExpression(StringJoiner stringJoiner, ProceedingJoinPoint joinPoint
      , RepeatSubCheck repeatSubCheck) {
    //读取spEl表达式中的值
    Object[] args = joinPoint.getArgs();
    Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
    String spElValue = getSpElValue(args, method, repeatSubCheck.spElKey());
    stringJoiner.add(spElValue);
  }

  /**
   * 获取spEl值
   *
   * @param arguments
   * @param signatureMethod
   * @param spelKey
   * @return 返回spel值
   */
  private String getSpElValue(Object[] arguments, Method signatureMethod, String spelKey) {
    EvaluationContext context = getContext(arguments, signatureMethod);
    if (null == context) {
      return StrUtil.EMPTY;
    }
    String value = getValue(context, spelKey, String.class);
    if (StrUtil.isBlank(value)) {
      return StrUtil.EMPTY;
    }
    return value;
  }

  /**
   * 获取spel 定义的参数值
   *
   * @param context 参数容器
   * @param key     key
   * @param clazz   需要返回的类型
   * @param <T>     返回泛型
   * @return 参数值
   */
  private <T> T getValue(EvaluationContext context, String key, Class<T> clazz) {
    SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
    Expression expression = spelExpressionParser.parseExpression(key);
    return expression.getValue(context, clazz);
  }


  /**
   * 获取参数容器
   *
   * @param arguments       方法的参数列表
   * @param signatureMethod 被执行的方法体
   * @return 装载参数的容器
   */
  private EvaluationContext getContext(Object[] arguments, Method signatureMethod) {
    String[] parameterNames = new LocalVariableTableParameterNameDiscoverer()
        .getParameterNames(signatureMethod);
    if (parameterNames == null) {
      return null;
    }
    EvaluationContext context = new StandardEvaluationContext();
    for (int i = 0; i < arguments.length; i++) {
      context.setVariable(parameterNames[i], arguments[i]);
    }
    return context;
  }
}
