package io.geekidea.springbootplus.framework.signature;

import lombok.extern.slf4j.Slf4j;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import javax.servlet.http.HttpServletRequest;

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.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.google.common.collect.Lists;

/**
 * 签名拦截器
 * @Author linux
 * @Date 2021年01月20日 04:21:30
 * @Version 1.0
 */
@Order(2)
@Aspect
@Component
@Slf4j
public class ControllerValidatorAspect implements InitializingBean {

    private static final String REQUEST_URL_OPEN = "request-url-open";

    /**
     * 同一个请求多长时间内有效
     */
    @Value("${signature.expireTime:600000}")
    private Long expireTime;

    /**
     * 同一个nonce 请求多长时间内不允许重复请求
     */
    @Value("${signature.resubmitDuration:60000}")
    private Long resubmitDuration;

    /**
     * appSecret
     */
    @Value("${signature.appSecret}")
    private String appSecret;

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    @Around("@annotation(io.geekidea.springbootplus.framework.signature.Signature) " +
            "&& (@annotation(org.springframework.web.bind.annotation.RequestMapping) " +
            "|| @annotation(org.springframework.web.bind.annotation.GetMapping) " +
            "|| @annotation(org.springframework.web.bind.annotation.PostMapping) " +
            "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping) " +
            "|| @annotation(org.springframework.web.bind.annotation.PatchMapping))"
    )

    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {//NOSONAR
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        //如果不是开放的URL, 进行签名校验
        if (Objects.isNull(request.getHeader(REQUEST_URL_OPEN))) {
            MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
            Method method = methodSignature.getMethod();
            Signature signature = AnnotationUtils.findAnnotation(method, Signature.class);

            //获取header中的相关参数
            SignatureHeaders signatureHeaders = generateSignatureHeaders(signature, request);

            //客户端签名
            String clientSignature = signatureHeaders.getSignature();

            //获取到header中的拼接结果
            String headersToSplice = SignatureUtils.toSplice(signatureHeaders);
            //服务端签名
            List<String> allSplice = generateAllSplice(method, pjp.getArgs(), headersToSplice);

            String serverSignature = SignatureUtils.signature(allSplice.toArray(new String[]{}), signatureHeaders.getAppsecret());

            if (!clientSignature.equals(serverSignature)) {
                if (log.isErrorEnabled()) {
                    log.error(String.format("签名不一致... clientSignature=%s, serverSignature=%s", clientSignature,
                            serverSignature));
                }
                throw new SignatureException("signature error");
            }
//            SignatureContext.setSignatureHeaders(signatureHeaders);
            log.info("签名验证通过, 相关信息: " + signatureHeaders);
        }
        try {
            return pjp.proceed();
        } catch (Throwable e) {
            //NOSONAR
            throw e;
        }
    }

    /**
     * 根据request 中 header值生成SignatureHeaders实体
     * @Author linux
     * @Date 2021年02月07日 10:50:14
     * @param signature
     * @param request
     * @Return
     */
    private SignatureHeaders generateSignatureHeaders(Signature signature, HttpServletRequest request) throws Exception {//NOSONAR
        // 筛选头信息
        Map<String, Object> headerMap = Collections.list(request.getHeaderNames())
                .stream()
                .filter(SignatureHeaders.SIGNATURE_HEADER_SET::contains)
                .collect(Collectors.toMap(headerName -> headerName.replaceAll("-", "."), request::getHeader));

        // 自定义ConfigurationProperty源信息
        ConfigurationPropertySource sources = new MapConfigurationPropertySource(headerMap);
        // 创建Binder绑定类
        Binder binder = new Binder(sources);
        // 绑定属性
        SignatureHeaders signatureHeaders = binder.bind("can.signature", Bindable.of(SignatureHeaders.class)).get();
        // 检验参数合法性
        Optional<String> result = ValidatorUtils.validateAll(signatureHeaders);
        if (result.isPresent() && StringUtils.isNotBlank(result.get())) {
            throw new SignatureException("WMH5000", result.get());
        }

        if (StringUtils.isBlank(appSecret)) {
            log.error("未设置app secret");
            throw new SignatureException("WMH5002");
        }

        //其他合法性校验
        Long now = System.currentTimeMillis();
        Long requestTimestamp = Long.parseLong(signatureHeaders.getTimestamp());
        if ((now - requestTimestamp) > expireTime) {
            String errMsg = "请求时间超过规定范围时间, signature=" + signatureHeaders.getSignature();
            if (log.isErrorEnabled()) {
                log.error(errMsg);
            }
            throw new SignatureException("WMH5000", errMsg);
        }
        String nonce = signatureHeaders.getNonce();
        if (nonce.length() < 10) {
            String errMsg = "6, nonce=" + nonce;
            log.error(errMsg);
            throw new SignatureException("WMH5000", errMsg);
        }
        if (!signature.resubmit()) {
            String existNonce = redisTemplate.opsForValue().get(nonce).toString();
            if (StringUtils.isBlank(existNonce)) {
                redisTemplate.opsForValue().set(nonce, nonce);
                redisTemplate.expire(nonce, (int) TimeUnit.MILLISECONDS.toSeconds(resubmitDuration), TimeUnit.SECONDS);
            } else {
                String errMsg = "不允许重复请求, nonce=" + nonce;
                log.error(errMsg);
                throw new SignatureException("WMH5000", errMsg);
            }
        }

        signatureHeaders.setAppsecret(appSecret);
        return signatureHeaders;
    }

    /**
     * 生成header中的参数，mehtod中的参数的拼接
     * @Author linux
     * @Date 2021年02月07日 10:53:18
     * @param method
     * @param args
     * @param headersToSplice
     * @Return
     */
    private List<String> generateAllSplice(Method method, Object[] args, String headersToSplice) {

        List<String> pathVariables = Lists.newArrayList();
        List<String> requestParams = Lists.newArrayList();

        String beanParams = StringUtils.EMPTY;

        for (int i = 0; i < method.getParameterCount(); ++i) {
            MethodParameter mp = new MethodParameter(method, i);
            boolean findSignature = false;
            for (Annotation anno : mp.getParameterAnnotations()) {
                if (anno instanceof PathVariable) {
                    if (!Objects.isNull(args[i])) {
                        pathVariables.add(args[i].toString());
                    }
                    findSignature = true;
                } else if (anno instanceof RequestParam) {
                    RequestParam rp = (RequestParam) anno;
                    String name = mp.getParameterName();
                    if (StringUtils.isNotBlank(rp.name())) {
                        name = rp.name();
                    }
                    if (!Objects.isNull(args[i])) {
                        List<String> values = Lists.newArrayList();
                        if (args[i].getClass().isArray()) {
                            //数组
                            for (int j = 0; j < Array.getLength(args[i]); ++j) {
                                values.add(Array.get(args[i], j).toString());
                            }
                        } else if (ClassUtils.isAssignable(Collection.class, args[i].getClass())) {
                            //集合
                            for (Object o : (Collection<?>) args[i]) {
                                values.add(o.toString());
                            }
                        } else {
                            //单个值
                            values.add(args[i].toString());
                        }
                        values.sort(Comparator.naturalOrder());
                        requestParams.add(name + "=" + StringUtils.join(values));
                    }
                    findSignature = true;
                } else if (anno instanceof RequestBody || anno instanceof ModelAttribute) {
                    beanParams = SignatureUtils.toSplice(args[i]);
                    findSignature = true;
                }

                if (findSignature) {
                    break;
                }
            }
            if (!findSignature) {
                log.info(String.format("签名未识别的注解, method=%s, parameter=%s, annotations=%s", method.getName(),
                        mp.getParameterName(), StringUtils.join(mp.getMethodAnnotations())));
            }
        }
        List<String> toSplices = Lists.newArrayList();
        toSplices.add(headersToSplice);
        toSplices.addAll(pathVariables);
        requestParams.sort(Comparator.naturalOrder());
        toSplices.addAll(requestParams);
        toSplices.add(beanParams);
        return toSplices;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (StringUtils.isBlank(appSecret)) {
            log.error("app secret is blank");
        }
    }
}
