package com.crm.rely.backend.aspect;

import com.crm.rely.backend.core.constant.Constants;
import com.crm.rely.backend.core.constant.LimitType;
import com.crm.rely.backend.core.constant.RequestNumLimiter;
import com.crm.rely.backend.core.constant.RequestSinglePointLimiter;
import com.crm.rely.backend.core.entity.login.InfoEntity;
import com.crm.rely.backend.exception.LoginException;
import com.crm.rely.backend.exception.ServiceException;
import com.crm.rely.backend.service.RedisService;
import com.crm.rely.backend.util.DateUtil;
import com.crm.rely.backend.util.GetIpAndMac;
import com.crm.rely.backend.util.HttpServletRequestUtil;
import com.crm.rely.backend.util.UUIDUtil;
import com.google.common.base.Strings;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
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.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
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.Collections;
import java.util.List;
@Aspect
@Component
@Slf4j
public class LimiterAspect {

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;
    @Autowired
    private RedisScript<Long> limitScript;
    @Autowired
    private RedisService redisService;

    private ThreadLocal<SinglePointLimiterEntity> singlePointLimiterThreadLocal = ThreadLocal.withInitial(() -> null);

    @Pointcut("execution(* com.crm..controller..*.save*(..)) || " +
            "execution(* com.crm..controller..*.add*(..)) || " +
            "execution(* com.crm..controller..*.apply*(..))")
    public void allControllerMethods() {
    }


    @Before("@annotation(requestNumLimiter)")
    public void handleRequestNumLimiter(JoinPoint point, RequestNumLimiter requestNumLimiter) throws Throwable {
        String combineKey = generateCombineKey(requestNumLimiter.key(), requestNumLimiter.limitType(), point);
        List<Object> keys = Collections.singletonList(combineKey);

        try {
            Long currentRequestCount = redisTemplate.execute(limitScript, keys, requestNumLimiter.count(),
                    requestNumLimiter.time());
            if (currentRequestCount == null || currentRequestCount > requestNumLimiter.count()) {
                throw new ServiceException("访问过于频繁，请稍候再试");
            }
            log.info("限流成功: [限制次数: {}, 当前请求次数: {}, 缓存Key: {}]", requestNumLimiter.count(), currentRequestCount,
                    combineKey);
        } catch (Exception e) {
            log.error("限流脚本执行失败, Key: {}, 错误: {}", combineKey, e.getMessage(), e);
            throw new RuntimeException("服务器限流异常，请稍候再试");
        }
    }

    @Before("@annotation(requestSinglePointLimiter)")
//    @Before("@annotation(requestSinglePointLimiter)||allController()||allIbController()")
    public void handleRequestSinglePointLimiter(JoinPoint point, RequestSinglePointLimiter requestSinglePointLimiter) throws Throwable {
        String combineKey = generateCombineKey(requestSinglePointLimiter.key(), requestSinglePointLimiter.limitType()
                , point);
        String requestId = UUIDUtil.getUUID();

        if (!redisService.tryLock(combineKey, requestId)) {
            log.info("单点限流触发: [时间: {}, 缓存Key: {}]", DateUtil.formatDateTime(), combineKey);
            throw ServiceException.restrictRequest();
        }

        singlePointLimiterThreadLocal.set(new SinglePointLimiterEntity(combineKey, requestId));
    }

    @After("@annotation(requestSinglePointLimiter)")
//    @After("@annotation(requestSinglePointLimiter)||allController()||allIbController()")
    public void releaseRequestSinglePointLimiter(RequestSinglePointLimiter requestSinglePointLimiter) {
        SinglePointLimiterEntity limiterEntity = singlePointLimiterThreadLocal.get();
        if (limiterEntity != null) {
            try {
                redisService.unlock(limiterEntity.getKey(), limiterEntity.getRequest());
            } catch (Exception e) {
                log.error("释放锁失败, Key: {}, 错误: {}", limiterEntity.getKey(), e.getMessage(), e);
            } finally {
                singlePointLimiterThreadLocal.remove();
            }
        }
    }

    private String generateCombineKey(String baseKey, LimitType limitType, JoinPoint point) throws ServiceException {
        StringBuilder keyBuilder = new StringBuilder(baseKey);
        HttpServletRequest request =
                ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

        keyBuilder.append(resolveLimitTypeKey(limitType, request));

        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        keyBuilder.append(method.getDeclaringClass().getName()).append("-").append(method.getName());

        return keyBuilder.toString();
    }

    private String resolveLimitTypeKey(LimitType limitType, HttpServletRequest request) throws ServiceException {
        switch (limitType) {
            case IP:
                return GetIpAndMac.getIp(request) + "-";
            case CLIENT_IP:
                return GetIpAndMac.getClientIp(request) + "-";
            case TOKEN:
                String accessToken = request.getHeader(Constants.ACCESS_TOKEN);
                if (Strings.isNullOrEmpty(accessToken)) {
                    throw LoginException.exception(Constants.INVALID);
                }
                return accessToken + "-";
            case USER:
                InfoEntity userInfo = HttpServletRequestUtil.getLoginInfo(request, redisService);
                return userInfo.getPrefix().getCode() + "-" + userInfo.getId() + "-";
            case NOT:
            default:
                return "";
        }
    }

    @Data
    @AllArgsConstructor
    private static class SinglePointLimiterEntity {
        private String key;
        private String request;
    }
}