package com.malk.filter;

import com.alibaba.fastjson.JSONException;
import com.malk.server.common.McException;
import com.malk.server.common.McR;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import javax.validation.ConstraintDeclarationException;
import javax.validation.ConstraintViolationException;
import java.util.ArrayList;
import java.util.List;

/**
 * 统一错误拦截
 * -
 * 参数校验 @Validated【javax.validation】
 * 1. @Validated 针对实体类进行字段入参校验，可做分组，实现不同接口请求下字段的校验规则
 * 2. @RestControllerAdvice 搭配 @ExceptionHandler 注解进行错误统一捕获和拦截返回，无需 try…catch 即可格式化返回标准输出：：注意类名
 * 3. 若无分组验证需求，可将@Validated 置于类上，不需要在@requestBody前声明。分组验证，可以在实体类组合好，若是取并集，则可直接在@requestBody上设置多个即可
 * -
 * 拦截param自定义message: @RequestParam默认是required，若想使用该检验，请求方法无需添加@RequestParam，且需要在Controller上添加@Validated
 * 参数@RequestParam，可以配置默认值，指定name，指定name后可以设置是否必填。若是要获取所有(@RequestParam MultiValueMap<String, String> paramMap)，每一个字段的值是一个集合
 * -
 * 关于在Service上使用校验, 首先需要impl类上添加@Validated [和Controller一致]
 * 建议注解到方法声明上, 抛出到ConstraintViolationException. 若校验注解到方法实现上, 则错误抛出到 ConstraintDeclarationException, 没有友好的提示
 * -
 * -
 * 若入参是Body，需要使用@RequestBody解析到实体/Map内，校验错误抛出到MethodArgumentNotValidException。@RequestBody @Validated({YDParam.Retrieve_Condition.class}) YDParam param
 * 如入参是formData，不能使用@RequestBody，参数会自动解析到实体; 若不是实体则需要通过方法转Map, 错会抛出到BindException. @Validated({YDParam.Retrieve_Condition.class}) YDParam param
 * -
 * 使用@RestController后，请求方法无需再使用@RequestBody，@RequestParam，直接可对入参进行修饰，效果类似解构，除了正常获取HttpServletRequest外，相关参数自动处理到注解内
 * RESTFul 风格接口, 参数获取方式均相同, GET 没有 body, @PathVariable 获取 url 占位符, RequestParam 获取 url 参数, @RequestBody 获取 body 数据
 * -
 * 关于入参为JSONString类型: body内的json需要传入字符串, 若为对象会解析报错. 若是formData, 传入对象会自动转为字符串, 不会解析错误, 无需转为JSONString
 * 注解@JsonInclude(JsonInclude.Include.NON_NULL)：类注解过滤null字段，包含入参和返回值【参数类搭配 @Data，才可以实例化返回值以及ToString输出】
 */
@Slf4j
@RestControllerAdvice(annotations = RestController.class)
public class CatchException<T> {

//    @Autowired
//    private ExceptionNotice notice;

    /**
     * 通用错误类抛出
     */
    @ExceptionHandler(McException.class)
    public McR McException(McException e) {
        log.error(e.getMessage(), e);  // 记录错误日志
        return McR.R(e.isSuccess(), e.getCode(), e.getMessage(), null, e.getSource());  // IGNORE_EXECUTE 为成功
    }

    /**************** validated不合法 ****************/

    /**
     * @validated 验证入参对象 @Validated({YDParam.Retrieve_Condition.class}) YDParam param
     * -
     * 如入参是formData，不能使用@RequestBody，参数会自动解析到实体; 若不是实体通过方法转Map .报错会抛出到BindException。@Validated({YDParam.Retrieve_Condition.class}) YDParam param
     */
    @ExceptionHandler(value = BindException.class)
    public McR BindException(BindException e) {
        List errorParam = new ArrayList();
        e.getFieldErrors().forEach(fieldError -> {
            errorParam.add(fieldError.getField() + ": " + fieldError.getDefaultMessage());
        });
        log.error(e.getMessage(), e);  // 记录错误日志
        return McR.errorParam("formData参数 -> " + String.join(", ", errorParam));
    }

    /**
     * @Validated 关于在Service上使用校验,
     * -
     * 首先需要impl类上添加@Validated [和Controller一致]
     * 建议注解到方法声明上, 抛出到ConstraintViolationException. 若校验注解到方法实现上, 则错误抛出到 ConstraintDeclarationException, 没有友好的提示
     */
    @ExceptionHandler(ConstraintDeclarationException.class)
    public McR ConstraintDeclarationException(ConstraintDeclarationException e) {
        log.error(e.getMessage(), e);  // 记录错误日志
        return McR.errorParam("param参数 -> " + e.getMessage());
    }

    /**
     * @validated 验证入参字段 @NotNull(message = "cur不能为空") String cur
     * -
     * 若入参是Body，需要使用@RequestBody解析到实体内，校验错误抛出到MethodArgumentNotValidException。@RequestBody @Validated({YDParam.Retrieve_Condition.class}) YDParam param
     * 如入参是formData，不能使用@RequestBody，参数会自动解析到实体; 若不是实体通过方法转Map .报错会抛出到BindException。@Validated({YDParam.Retrieve_Condition.class}) YDParam param
     * 关于入参为JSONString类型: body内的json需要传入字符串, 若为对象会解析报错. 若是formData, 传入对象会自动转为字符串, 不会解析错误, 无需转为JSONString
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public McR ConstraintViolationException(ConstraintViolationException e) {
        List errorParam = new ArrayList();
        e.getConstraintViolations().forEach(fieldError -> {
            errorParam.add(fieldError.getPropertyPath() + ": " + fieldError.getMessage());
        });
        log.error(e.getMessage(), e);  // 记录错误日志
        return McR.errorParam("param参数 -> " + String.join(", ", errorParam));
    }

    /**
     * @validated 验证入参对象 @RequestBody @Validated({YDParam.Retrieve_Condition.class}) YDParam param
     * -
     * 若入参是Body，需要使用@RequestBody解析到实体内，校验错误抛出到MethodArgumentNotValidException。@RequestBody @Validated({YDParam.Retrieve_Condition.class}) YDParam param
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public McR MethodArgumentNotValidException(MethodArgumentNotValidException e) {
        List errorParam = new ArrayList();
        e.getBindingResult().getAllErrors().forEach(err -> {
            FieldError fieldError = (FieldError) err;
            errorParam.add(fieldError.getField() + ": " + fieldError.getDefaultMessage());
        });
        log.error(e.getMessage(), e);  // 记录错误日志
        return McR.errorParam("body参数 -> " + String.join(", ", errorParam));
    }

    /**************** 请求参数异常 ****************/

    /**
     * @RequestParam默认是required 验证入参字段 @RequestParam String cur
     * -
     * 参数@RequestParam，可以配置默认值，指定name，指定name后可以设置是否必填。若是要获取所有(@RequestParam MultiValueMap<String, String> param
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public McR MissingServletRequestParameterException(MissingServletRequestParameterException e) {
        log.error(e.getMessage(), e);  // 记录错误日志
        return McR.errorParam("param参数 -> " + e.getMessage());
    }

    /**
     * @RequestParam 参数类型解析异常
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public McR MethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        log.error(e.getMessage(), e);  // 记录错误日志
        return McR.errorParam("param参数 ->  " + e.getName() + "不合法: " + e.getMessage());
    }

    /**
     * @RequestBody 序列化字段为非Sting类型
     * -
     * 关于入参为JSONString类型: body内的json需要传入字符串, 若为对象会解析报错. 若是formData, 传入对象会自动转为字符串, 不会解析错误, 无需转为JSONString
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public McR HttpMessageNotReadableException(HttpMessageNotReadableException e) {
        log.error(e.getMessage(), e);  // 记录错误日志
        if (e.getMessage().contains("Required request body is missing")) {
            return McR.errorParam("请求体参数不能为空");
        }
        return McR.errorParam("请求体参数格式不合法");
    }

    /**
     * Content-Type不合法
     */
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public McR HttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
        log.error(e.getMessage(), e);  // 记录错误日志
        return McR.errorParam("Content-Type不合法: " + e.getMessage());
    }

    /**************** 网络请求异常 ****************/

    /**
     * http请求返回json解析异常
     */
    @ExceptionHandler(JSONException.class)
    public McR JSONException(JSONException e) {
        log.error(e.getMessage(), e);  // 记录错误日志
        return McR.errorParam("返回JSON解析异常: " + e.getMessage());
    }

    /**************** 数据类型异常 ****************/

    /**
     * BigDecimal 非数值型字符串, 空字符串取值异常
     */
    @ExceptionHandler(NumberFormatException.class)
    public McR NumberFormatException(NumberFormatException e) {
        log.error(e.getMessage(), e);  // 记录错误日志
        return McR.errorParam(e.getMessage() + ": 非数值型字符串, 空字符串取值异常");
    }

    /**
     * 反射未匹配到路径
     */
    @ExceptionHandler(ClassNotFoundException.class)
    public McR ClassNotFoundException(ClassNotFoundException e) {
        log.error(e.getMessage(), e);  // 记录错误日志
        return McR.errorParam("反射未匹配到路径: " + e.getMessage());
    }

    /**
     * 系统错误抛出
     * -
     * NOTE: 空指针判定, 日志目前会记录在warn内, 不输出到error内, 做一层包装返回 (空指针message为null)
     */
    @ExceptionHandler(Exception.class)
    public McR Exception(Exception e) {
        log.error(e.getMessage(), e);       // 记录错误日志
//        notice.noticeErrorByDingtalk(e);    // 上报错误日志
        if (e instanceof NullPointerException) {
            return McR.errorNullPointer();
        }
        return McR.errorUnknown(e.getMessage());
    }
}
