package com.cku.aop;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Objects;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHeaders;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import com.cku.annotation.MultiSubmit;
import com.cku.annotation.MultiSubmitWithToken;
import com.cku.core.ZAErrorCode;
import com.cku.core.ZAException;
import com.cku.restful.v1.sys.utils.MD5Util;
import com.thinkgem.jeesite.common.mapper.JsonMapper;
import com.thinkgem.jeesite.common.utils.IdGen;
import com.thinkgem.jeesite.common.utils.JedisUtils;
import com.thinkgem.jeesite.modules.sys.utils.UserUtils;

import edu.emory.mathcs.backport.java.util.Arrays;
import lombok.extern.slf4j.Slf4j;

/**
 * 重复提交靠按钮解决不是特别靠谱，后台加mysql锁也是不优（今后分布式了）， 使用redis分布式锁(setnx)解决 hash值
 * 对象实例相同，默认的hash值相等(配合lombok的@Data对hashcode的重写)，通过hash值作为锁值校验入参(不要随便重写request
 * hash()方法) hash值超时时间（默认 5 s）
 */
@Aspect
@Component
@Slf4j
public class MultiSubmitHandler {

	/**
	 * 锁的默认时间为5秒
	 */
	public static final int REPEAT_LOCK_TIME = 5;

	@Pointcut("@annotation(com.cku.annotation.MultiSubmit) || @annotation(com.cku.annotation.MultiSubmitWithToken)")
	public void pointcut() {
	}

	/**
	 * 防止重复提交的请求,请求之前的逻辑 对 用户id + 请求参数的hashcode 加锁
	 *
	 * @param joinPoint 获取请求Request
	 */
	@Before("pointcut()")
	public void before(JoinPoint joinPoint) {
		long start = System.currentTimeMillis();
		Object[] objects = joinPoint.getArgs();
		Annotation multiSubmit = getMethodAnnotation(joinPoint, MultiSubmit.class);
		int timeout = REPEAT_LOCK_TIME;// 默认5秒
		if (multiSubmit instanceof MultiSubmit) {
			timeout = ((MultiSubmit) multiSubmit).timeout();
		} else {
			timeout = ((MultiSubmitWithToken) multiSubmit).timeout();
		}
		String rdsKey = getRedisKey(joinPoint, objects);
		if (JedisUtils.setNx(rdsKey, "1", timeout)) {
			log.info("submitting repeat check time : " + (System.currentTimeMillis() - start) + "ms, key:" + rdsKey);
		} else {
			log.error("submitting repeat: " + joinPoint.toLongString());
			throw new ZAException(ZAErrorCode.ZA_VALID_QUEST_FREQUENTLY, "重复提交");
		}
	}

	/**
	 * 防止重复提交的请求,请求之后的逻辑 释放锁
	 *
	 * @param joinPoint 获取请求Request
	 * @param res       暂无用
	 */
	@AfterReturning(pointcut = "pointcut()", returning = "res")
	public void afterReturning(JoinPoint joinPoint, Object res) {
		Object[] objects = joinPoint.getArgs();
		String key = getRedisKey(joinPoint, objects);
		JedisUtils.del(key);
		log.info("submitting repeat lock released, key:" + key);
	}

	/**
	 * 获取防止相同请求重复提交的redis锁的key
	 *
	 * @param objects 请求参数
	 * @return key redis锁的key
	 */
	private String getRedisKey(JoinPoint joinPoint, Object[] objects) {
		String key = getParamKey(objects);
		String uid;
		try {
			Annotation multiSubmit = getMethodAnnotation(joinPoint, MultiSubmit.class);
			if (multiSubmit instanceof MultiSubmit) {
				uid = UserUtils.getLoginUser().getId();
			} else {
				uid = UserUtils.getLoginUser().getTicket();
			}
		} catch (Exception e) {
			uid = null;
		}
		if (StringUtils.isNotBlank(uid)) {
			key = "R:" + uid + ":" + key;
		} else {
			key = "R:" + key;
		}
		return key;
	}

	@SuppressWarnings("unchecked")
	public String getParamKey(Object[] objects) {
		// 随机获取key（保证获取不到参数时，不阻断正常提交），如请求有参数，会根据参数改变 key
		String resultKey = IdGen.uuid();
		HttpServletRequest request = (HttpServletRequest) Arrays.asList(objects).stream()
				.filter(o -> o instanceof HttpServletRequest).findAny().orElse(null);
		// 非HttpServletRequest接收参数
		if (Objects.isNull(request)) {
			resultKey = String.valueOf(objects[0].hashCode());
		}
		// 非json类型
		else if (request.getHeader(HttpHeaders.CONTENT_TYPE) == null
				|| !request.getHeader(HttpHeaders.CONTENT_TYPE).equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
			// 普通参数
			if (Objects.nonNull(request.getParameterMap()) && !request.getParameterMap().isEmpty()) {
				String reqStr = JsonMapper.toJsonString(request.getParameterMap());
				resultKey = MD5Util.MD5Encode(StringUtils.isEmpty(reqStr) ? "" : reqStr, Charsets.UTF_8.toString());
			}
			// json类型
		} else {
			// 从输入流中取出body串, 如果为空，直接返回
			try {
				String reqBodyStr = IOUtils.toString(request.getInputStream(), Charsets.UTF_8);
				resultKey = MD5Util.MD5Encode(StringUtils.isEmpty(reqBodyStr) ? "" : reqBodyStr,
						Charsets.UTF_8.toString());
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return resultKey;
	}

	/**
	 * 获取方法注解
	 * 
	 * @param joinPoint
	 * @param clazz
	 * @param <T>
	 * @return
	 */
	private <T extends Annotation> T getMethodAnnotation(JoinPoint joinPoint, Class<T> clazz) {
		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
		Method method = methodSignature.getMethod();
		return method.getDeclaredAnnotation(clazz);
	}
}
