package com.sqx.common.aspect;

import com.sqx.common.annotation.Debounce;
import com.sqx.common.utils.SpelUtil;
import org.apache.commons.lang3.StringUtils;
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.stereotype.Component;

import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;


/**
 * 防抖切面
 * @author ww
 */
@Aspect
@Component
public class DebounceAspect {

	@Pointcut("@annotation(com.sqx.common.annotation.Debounce)")
	public void logPointCut() { 
		
	}

	// 用于存储基于方法和入参情况的上次执行时间，结构为：方法签名 -> (入参值 -> 上次执行时间)
	private static final ConcurrentHashMap<String, ConcurrentHashMap<Object, Long>> executionTimeMap = new ConcurrentHashMap<>();
	private static final ReentrantLock lock = new ReentrantLock();

	@Around("logPointCut()")
	public Object aroundDebounce(ProceedingJoinPoint joinPoint) throws Throwable {
		cleanExpiredRecords();
		String methodSignature = joinPoint.getSignature().toLongString();
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		Debounce annotation =  signature.getMethod().getAnnotation(Debounce.class);
		long interval = annotation.interval();
		TimeUnit timeUnit = annotation.timeUnit();
		String value = annotation.value();
		if (StringUtils.isBlank(value)) {
			// 没有指定入参表达式，按照整个方法进行防抖
			return debounceForWholeMethod(joinPoint, methodSignature, interval, timeUnit);
		}
		String[] split = value.split(",");
		StringBuilder values = new StringBuilder();
		for (String str : split) {
			values.append(SpelUtil.generateKeyBySpEL(str, joinPoint));
		}
		// 解析入参表达式，获取对应入参的值

		if (StringUtils.isBlank(values.toString())) {
			// 如果解析失败或值为null，按照整个方法进行防抖
			return debounceForWholeMethod(joinPoint, methodSignature, interval, timeUnit);
		}

		// 根据方法签名和入参值进行防抖判断
		return debounceForSpecificValue(joinPoint, methodSignature, interval, timeUnit, values.toString());
	}

	private Object debounceForWholeMethod(ProceedingJoinPoint joinPoint, String methodSignature, long interval, TimeUnit timeUnit) throws Throwable {
		ConcurrentHashMap<Object, Long> methodExecutionTimeMap = executionTimeMap.computeIfAbsent(methodSignature, k -> new ConcurrentHashMap<>());
		long currentTime = System.currentTimeMillis();
		Long lastTime = methodExecutionTimeMap.get(methodSignature);
		if (lastTime == null) {
			// 如果不存在对应键值对，设置初始值，这里设置为一个表示很久之前的时间戳，比如0
			lastTime = 0L;
		}
		if (lastTime == null || currentTime - timeUnit.toMillis(interval) >= lastTime) {
			// 满足防抖间隔，更新上次执行时间，并执行目标方法
			methodExecutionTimeMap.put(methodSignature, currentTime);
			return joinPoint.proceed();
		}
		// 在防抖间隔内，不执行目标方法，直接返回
		return null;
	}

	private Object debounceForSpecificValue(ProceedingJoinPoint joinPoint, String methodSignature, long interval, TimeUnit timeUnit, Object targetValue) throws Throwable {
		ConcurrentHashMap<Object, Long> methodExecutionTimeMap = executionTimeMap.computeIfAbsent(methodSignature, k -> new ConcurrentHashMap<>());
		long currentTime = System.currentTimeMillis();
		Long lastTime = methodExecutionTimeMap.get(targetValue);
		if (lastTime == null || currentTime - timeUnit.toMillis(interval) >= lastTime) {
			// 满足防抖间隔，更新上次执行时间，并执行目标方法
			methodExecutionTimeMap.put(targetValue, currentTime);
			return joinPoint.proceed();
		}
		// 在防抖间隔内，不执行目标方法，直接返回
		return null;
	}

	public void cleanExpiredRecords() {
		long expirationTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(10);
		lock.lock();
		try {
			for (Entry<String, ConcurrentHashMap<Object, Long>> outerEntry : executionTimeMap.entrySet()) {
				String methodSignature = outerEntry.getKey();
				ConcurrentHashMap<Object, Long> innerMap = outerEntry.getValue();
				ConcurrentHashMap<Object, Long> keysToRemove = new ConcurrentHashMap<>();
				for (Entry<Object, Long> innerEntry : innerMap.entrySet()) {
					if (innerEntry.getValue() < expirationTime) {
						keysToRemove.put(innerEntry.getKey(), innerEntry.getValue());
					}
				}
				innerMap.keySet().removeAll(keysToRemove.keySet());
				if (innerMap.isEmpty()) {
					executionTimeMap.remove(methodSignature);
				}
			}
		} finally {
			lock.unlock();
		}
	}
}
