package com.cool.store.aspect;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.cool.store.annotation.Debounce;
import com.cool.store.context.CurrentUserHolder;
import com.cool.store.context.PartnerUserHolder;
import com.cool.store.enums.ErrorCodeEnum;
import com.cool.store.exception.ServiceException;
import com.cool.store.utils.RedisUtilPool;
import lombok.extern.slf4j.Slf4j;
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.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Objects;

/**
 * <p>
 * 防抖切面
 * </p>
 *
 * @author wangff
 * @since 2026/2/7
 */
@Aspect
@Component
@Slf4j
public class DebounceAspect {
    @Resource
    private RedisUtilPool redisUtilPool;

    private static final String KEY_PREFIX = "debounce:";
    private static final String FIELD_NEXT_ALLOWED_AT = "nextAllowedAt";
    private static final String FIELD_LAST_RESULT = "lastResult";

    private final ExpressionParser spelParser = new SpelExpressionParser();

    @Around("execution(public * *(..)) && (@within(com.cool.store.annotation.Debounce) || @annotation(com.cool.store.annotation.Debounce))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();

        Debounce debounce = AnnotatedElementUtils.findMergedAnnotation(method, Debounce.class);
        if (debounce == null) {
            debounce = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Debounce.class);
        }
        if (debounce == null || debounce.timeMs() <= 0) {
            return pjp.proceed();
        }

        String key = KEY_PREFIX + buildKey(debounce, pjp, method);
        long now = System.currentTimeMillis();
        long ttlMs = debounce.timeMs();

        Long nextAllowedAt = readNextAllowedAt(key);
        if (nextAllowedAt != null && now < nextAllowedAt) {
            if (debounce.throwOnDebounce()) {
                throw new ServiceException(ErrorCodeEnum.OPERATION_OVER_TIME);
            }
            String val = redisUtilPool.getString(key + ":" + FIELD_LAST_RESULT);
            return parseToReturnType(val, method);
        }

        boolean allowed = trySetNextAllowedAt(key, now + ttlMs, ttlMs);
        if (!allowed) {
            if (debounce.throwOnDebounce()) {
                throw new ServiceException(ErrorCodeEnum.OPERATION_OVER_TIME);
            }
            String val = redisUtilPool.getString(key + ":" + FIELD_LAST_RESULT);
            return parseToReturnType(val, method);
        }

        Object result = null;
        try {
            result = pjp.proceed();
            return result;
        } finally {
            if (!isVoidReturn(method) && Objects.nonNull(result)) {
                redisUtilPool.setNxExpire(key + ":" + FIELD_LAST_RESULT, JSONObject.toJSONString(result), (int) ttlMs * 2);
            }
        }
    }

    private Long readNextAllowedAt(String key) {
        String val = redisUtilPool.getString(key + ":" + FIELD_NEXT_ALLOWED_AT);
        if (StringUtils.isBlank(val)) {
            return null;
        }
        Object obj = JSONObject.parse(val);
        if (obj instanceof Number) {
            return ((Number) obj).longValue();
        }
        try {
            return Long.parseLong(val);
        } catch (Exception e) {
            return null;
        }
    }

    private boolean trySetNextAllowedAt(String key, long nextAllowedAt, long ttlMs) {
        return redisUtilPool.setNxExpire(key + ":" + FIELD_NEXT_ALLOWED_AT, String.valueOf(nextAllowedAt), (int) ttlMs);
    }

    private boolean isVoidReturn(Method method) {
        return Void.TYPE.equals(method.getReturnType()) || Void.class.equals(method.getReturnType());
    }

    private Object parseToReturnType(String json, Method method) {
        if (StringUtils.isBlank(json)) {
            return null;
        }
        Class<?> returnType = method.getReturnType();
        if (returnType == null || Object.class.equals(returnType)) {
            return JSONObject.parse(json);
        }
        if (String.class.equals(returnType)) {
            return json;
        }
        return JSON.parseObject(json, returnType);
    }

    private String buildKey(Debounce debounce, ProceedingJoinPoint pjp, Method method) {
        String spel = debounce.key();
        String suffix = debounce.diffUser() ? ":" + (debounce.pc() ? CurrentUserHolder.getUserId() : PartnerUserHolder.getUser().getPartnerId()) : "";
        if (spel != null && !spel.isEmpty()) {
            EvaluationContext ctx = new StandardEvaluationContext();
            Object[] args = pjp.getArgs();
            String[] paramNames = ((MethodSignature) pjp.getSignature()).getParameterNames();
            if (paramNames != null) {
                for (int i = 0; i < paramNames.length; i++) {
                    ctx.setVariable(paramNames[i], args[i]);
                }
            }
            Object val = spelParser.parseExpression(spel).getValue(ctx);
            return "spel:" + method.getDeclaringClass().getName() + "#" + method.getName() + ":" + val + suffix;
        }
        return "default:" + method.getDeclaringClass().getName() + "#" + method.getName() + ":" + Arrays.deepHashCode(pjp.getArgs()) + suffix;
    }
}
