package quant.rich.emoney.component;

import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import quant.rich.emoney.client.EmoneyClient;
import quant.rich.emoney.entity.sqlite.ProxySetting;
import quant.rich.emoney.entity.sqlite.RequestInfo;
import quant.rich.emoney.service.sqlite.ProxySettingService;
import quant.rich.emoney.service.sqlite.RequestInfoService;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;

/**
 * 注解在受 Spring 管理的类的成员方法上，使其在执行前校验代理状态、鉴权状态
 * <p>需要开启 AOP：在任意配置类上增加注解：<i><code>@EnableAspectJAutoProxy</code></i>
 */
@Component
@Aspect
public class RequireAuthAndProxyAspect {
    
    /**
     * 调用链深度，用以判断同线程同类型切点命中的次数
     */
    private static final ThreadLocal<Integer> DEPTH =
            ThreadLocal.withInitial(() -> 0);

    @Autowired
    RequestInfoService requestInfoService;
    
    @Autowired
    ProxySettingService proxySettingService;

    @Pointcut("@annotation(quant.rich.emoney.component.RequireAuthAndProxyAspect.RequireAuthAndProxy)")
    public void pointCut() {}
    
    @Before("pointCut()")
    public void before(JoinPoint jp) throws Throwable {
        int depth = DEPTH.get();
        if (depth++ == 0) {
            beforeRoot(jp);
        }
        DEPTH.set(depth);
    }
    
    @After("pointCut()")
    public void after() {
        int depth = DEPTH.get() - 1;
        if (depth == 0) {
            DEPTH.remove();
        } else {
            DEPTH.set(depth);
        }
    }
    
    public void beforeRoot(JoinPoint jp) throws Throwable {

        ProxySetting defualtProxySetting = proxySettingService.getDefaultProxySetting();
        
        if (defualtProxySetting == null) {
            throw new RuntimeException("需要配置默认代理设置");
        }
        
        RequestInfo defaultRequestInfo = requestInfoService.getDefaultRequestInfo();
        
        if (defaultRequestInfo == null) {
            throw new RuntimeException("需要配置默认请求信息");
        }
        
        MethodSignature signature = (MethodSignature) jp.getSignature();
        Method method = signature.getMethod();
        RequireAuthAndProxy annotation = method.getAnnotation(RequireAuthAndProxy.class);
        
        if (StringUtils.isBlank(defaultRequestInfo.getAuthorization())) {
            if (!annotation.autoLogin()) {
                throw new RuntimeException("需要手动为请求信息鉴权");
            }
            if (!EmoneyClient.loginWithManaged()) {
                throw new RuntimeException("鉴权登录失败");
            }
        }
        else if (!EmoneyClient.reloginCheck()) { 
            throw new RuntimeException("检查重鉴权失败");
        }
    }
    
    /**
     * 在方法上添加此注解，则进入该方法前先校验 defaultRequestInfo 已鉴权、代理已配置，否则不允许进入方法
     * <p>
     * <b><font color="red">由于 AOP 特性使然, 必须为 public 方法才能生效</font></b>
     * <p>
     * 为防止反复重登录验证, 在同一调用链中的校验, 仅在首次调用时进行检查, 如：
     * A、B 和 C 方法都添加了本注解, A 中调用 B, B 中调用 C, 则仅在 A 方法执行时才进行检查。
     * <p>
     * 为了安全起见, 无论 autoLogin 是否为 true, 只要添加了本注解, 在执行方法前, 最终都会进行
     * EmoneyClient.reloginCheck(), 如果检查重鉴权失败, 被注解的方法将不会执行, 
     * 并由切片方法抛出异常。如果某个方法并不需要严格控制鉴权的, 可以不用本注解, 
     * 而是由在方法内自行编写逻辑来替代。
     * 
     * @see RequireAuthAndProxyAspect
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public static @interface RequireAuthAndProxy {
        /**
         * 当存在默认请求配置但未鉴权时，是否自动鉴权。由于某些场景下，
         * 鉴权信息需要系统统一维护，故此区分
         * @return
         */
        boolean autoLogin() default false;
    }
    
}