package com.codvision.webcore.config.encrypt.resolver;

import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.symmetric.AES;
import com.codvision.commoncore.exception.BusinessException;
import com.codvision.webcore.config.encrypt.ApiSecretProperties;
import com.codvision.webcore.config.encrypt.ApiSecretSupports;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 请求数据解密处理
 * 仅对方法的所有参数均被{@link RequestParam}标记的场景生效，参数类型适用于基本类型（含包装类型）
 * 代码参考自{@link org.springframework.web.method.annotation.RequestParamMethodArgumentResolver}
 *
 * @author hxl
 */
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(value = ApiSecretProperties.PREFIX + ".enabled", havingValue = "true")
@ConditionalOnExpression("!'${security.api.encrypt.secret-key}'.isEmpty()")
public class ApiSecretRequestParamResolver implements HandlerMethodArgumentResolver {

    private final ObjectMapper objectMapper;
    private final ApiSecretProperties properties;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // 判断当前方法是否符合要求，不符合则直接过滤
        Method method = parameter.getMethod();
        if (precondition(method)) {
            return false;
        }

        return ApiSecretSupports.supports(parameter, properties);
    }

    @Override
    @SneakyThrows
    @SuppressWarnings("unchecked")
    public Object resolveArgument(MethodParameter methodParameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) {
        Parameter parameter = methodParameter.getParameter();
        String params = webRequest.getParameter(ApiSecretProperties.PARAM_NAME);
        if (StringUtils.isBlank(params)) {
            return convertIfNecessary(methodParameter, webRequest, binderFactory);
        }

        AES aes = new AES(Mode.CBC, Padding.ZeroPadding, new SecretKeySpec(properties.getSecretKey().getBytes(), "AES"),
                new IvParameterSpec(properties.getSecretKey().getBytes()));
        try {
            params = aes.decryptStr(params);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new BusinessException(30001, "请检查encryption参数值是否已加密传输");
        }

        Map<String, Object> map = objectMapper.readValue(params, Map.class);
        Object o = map.get(parameter.getName());
        if (o == null) {
            o = map.get(methodParameter.getParameterName());
            if (o == null) {
                return convertIfNecessary(methodParameter, webRequest, binderFactory);
            }
        }
        return o;
    }

    private Object convertIfNecessary(MethodParameter methodParameter, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        Object o = null;
        Parameter parameter = methodParameter.getParameter();

        if (parameter.getType().equals(HttpServletRequest.class)) {
            return webRequest.getNativeRequest(HttpServletRequest.class);
        }
        RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
        String defaultValue = (ValueConstants.DEFAULT_NONE.equals(requestParam.defaultValue()) ? null : requestParam.defaultValue());
        if (StringUtils.isNotBlank(defaultValue)) {
            o = defaultValue;
        } else if (requestParam.required()) {
            throw new MissingServletRequestParameterException(parameter.getName(),
                    parameter.getType().getSimpleName(), false);
        }

        o = handleNullValue(parameter.getName(), o, parameter.getType());
        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, null, parameter.getName());
            try {
                o = binder.convertIfNecessary(o, methodParameter.getParameterType(), methodParameter);
            } catch (ConversionNotSupportedException ex) {
                throw new MethodArgumentConversionNotSupportedException(o, ex.getRequiredType(),
                        parameter.getName(), methodParameter, ex.getCause());
            } catch (TypeMismatchException ex) {
                throw new MethodArgumentTypeMismatchException(o, ex.getRequiredType(),
                        parameter.getName(), methodParameter, ex.getCause());
            }

            if (o == null && defaultValue == null && requestParam.required()) {
                throw new MissingServletRequestParameterException(parameter.getName(),
                        parameter.getType().getSimpleName(), true);
            }
        }

        return o;
    }


    private Object handleNullValue(String name, @Nullable Object o, Class<?> paramType) {
        if (o == null) {
            if (Boolean.TYPE.equals(paramType)) {
                return Boolean.FALSE;
            } else if (paramType.isPrimitive()) {
                throw new IllegalStateException("Optional " + paramType.getSimpleName() + " parameter '" + name +
                        "' is present but cannot be translated into a null value due to being declared as a " +
                        "primitive type. Consider declaring it as object wrapper for the corresponding primitive type.");
            }
        }

        return o;
    }

    private boolean precondition(Method method) {
        List<Parameter> parameters = Arrays.stream(Objects.requireNonNull(method).getParameters())
                .filter(p -> !p.isAnnotationPresent(RequestParam.class) && !p.getType().equals(HttpServletRequest.class))
                .collect(Collectors.toList());
        return CollectionUtils.isNotEmpty(parameters);
    }

}
