package com.njcn.gather.system.config.handler;

import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.system.log.pojo.dto.SysLogAuditRecord;
import com.njcn.gather.system.log.service.ISysLogAuditService;
import com.njcn.gather.system.pojo.enums.SystemResponseEnum;
import com.njcn.web.utils.HttpResultUtil;
import com.njcn.web.utils.ReflectCommonUtil;
import com.njcn.web.utils.RequestUtil;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONException;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.util.NestedServletException;

import javax.annotation.Resource;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 全局通用业务异常处理器
 *
 * @author hongawen
 * @version 1.0.0
 * @date 2021年04月20日 18:04
 */
@Slf4j
@RestControllerAdvice
public class GlobalBusinessExceptionHandler {

    @Resource
    private ISysLogAuditService sysLogAuditService;

    @Resource(name = "logAuditExecutor")
    private Executor logAuditExecutor;

    @ExceptionHandler(BusinessException.class)
    public HttpResult<String> handleBusinessException(BusinessException businessException) {
        String operate = resolveMethodDescribeByException(businessException);
        recodeBusinessExceptionLog(businessException, businessException.getMessage());
        return HttpResultUtil.assembleBusinessExceptionResult(businessException, null, operate);
    }

    @ExceptionHandler(NullPointerException.class)
    public HttpResult<String> handleNullPointerException(NullPointerException nullPointerException) {
        LogUtil.logExceptionStackInfo(CommonResponseEnum.NULL_POINTER_EXCEPTION.getMessage(), nullPointerException);
        recodeBusinessExceptionLog(nullPointerException, CommonResponseEnum.NULL_POINTER_EXCEPTION.getMessage());
        return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.NULL_POINTER_EXCEPTION, null, resolveMethodDescribeByException(nullPointerException));
    }

    @ExceptionHandler(ArithmeticException.class)
    public HttpResult<String> handleArithmeticException(ArithmeticException arithmeticException) {
        LogUtil.logExceptionStackInfo(CommonResponseEnum.ARITHMETIC_EXCEPTION.getMessage(), arithmeticException);
        recodeBusinessExceptionLog(arithmeticException, CommonResponseEnum.ARITHMETIC_EXCEPTION.getMessage());
        return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.ARITHMETIC_EXCEPTION, null, resolveMethodDescribeByException(arithmeticException));
    }

    @ExceptionHandler(ClassCastException.class)
    public HttpResult<String> handleClassCastException(ClassCastException classCastException) {
        LogUtil.logExceptionStackInfo(CommonResponseEnum.CLASS_CAST_EXCEPTION.getMessage(), classCastException);
        recodeBusinessExceptionLog(classCastException, CommonResponseEnum.CLASS_CAST_EXCEPTION.getMessage());
        return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.CLASS_CAST_EXCEPTION, null, resolveMethodDescribeByException(classCastException));
    }

    @ExceptionHandler(IndexOutOfBoundsException.class)
    public HttpResult<String> handleIndexOutOfBoundsException(IndexOutOfBoundsException indexOutOfBoundsException) {
        LogUtil.logExceptionStackInfo(CommonResponseEnum.INDEX_OUT_OF_BOUNDS_EXCEPTION.getMessage(), indexOutOfBoundsException);
        recodeBusinessExceptionLog(indexOutOfBoundsException, CommonResponseEnum.INDEX_OUT_OF_BOUNDS_EXCEPTION.getMessage());
        return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.INDEX_OUT_OF_BOUNDS_EXCEPTION, null, resolveMethodDescribeByException(indexOutOfBoundsException));
    }

    @ExceptionHandler(NoSuchFileException.class)
    public HttpResult<String> handleNoSuchFileException(NoSuchFileException noSuchFileException) {
        String filePath = noSuchFileException.getFile();
        log.warn("文件未找到异常 - 文件路径: {}", filePath, noSuchFileException);
        recodeBusinessExceptionLog(noSuchFileException, SystemResponseEnum.FILE_NOT_FOUND.getMessage());
        return HttpResultUtil.assembleResult(SystemResponseEnum.FILE_NOT_FOUND.getCode(), null,
                StrFormatter.format("{}{}{}", resolveMethodDescribeByException(noSuchFileException),
                        StrUtil.C_COMMA, SystemResponseEnum.FILE_NOT_FOUND.getMessage()));
    }

    @ExceptionHandler(IOException.class)
    public HttpResult<String> handleIOException(IOException ioException) {
        if (ioException instanceof NoSuchFileException) {
            return handleNoSuchFileException((NoSuchFileException) ioException);
        }
        LogUtil.logExceptionStackInfo(SystemResponseEnum.FILE_IO_ERROR.getMessage(), ioException);
        recodeBusinessExceptionLog(ioException, SystemResponseEnum.FILE_IO_ERROR.getMessage());
        return HttpResultUtil.assembleResult(SystemResponseEnum.FILE_IO_ERROR.getCode(), null,
                StrFormatter.format("{}{}{}", resolveMethodDescribeByException(ioException),
                        StrUtil.C_COMMA, SystemResponseEnum.FILE_IO_ERROR.getMessage()));
    }

    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public HttpResult<String> httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException httpMediaTypeNotSupportedException) {
        LogUtil.logExceptionStackInfo(CommonResponseEnum.HTTP_MEDIA_TYPE_NOT_SUPPORTED_EXCEPTION.getMessage(), httpMediaTypeNotSupportedException);
        recodeBusinessExceptionLog(httpMediaTypeNotSupportedException, CommonResponseEnum.HTTP_MEDIA_TYPE_NOT_SUPPORTED_EXCEPTION.getMessage());
        return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.HTTP_MEDIA_TYPE_NOT_SUPPORTED_EXCEPTION, null, resolveMethodDescribeByException(httpMediaTypeNotSupportedException));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public HttpResult<String> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException methodArgumentNotValidException) {
        String messages = methodArgumentNotValidException.getBindingResult().getAllErrors()
                .stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining("；"));
        LogUtil.njcnDebug(log, "参数校验异常，异常为：{}", messages);
        recodeBusinessExceptionLog(methodArgumentNotValidException, CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION.getMessage());
        return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION, messages, ControllerUtil.getMethodArgumentNotValidException(methodArgumentNotValidException));
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public HttpResult<String> constraintViolationExceptionExceptionHandler(ConstraintViolationException constraintViolationException) {
        String exceptionMessage = constraintViolationException.getMessage();
        StringBuilder messages = new StringBuilder();
        if (exceptionMessage.indexOf(StrUtil.COMMA) > 0) {
            String[] tempMessage = exceptionMessage.split(StrUtil.COMMA);
            Stream.of(tempMessage).forEach(message -> messages.append(message.substring(message.indexOf(StrUtil.COLON) + 2)).append(';'));
        } else {
            messages.append(exceptionMessage.substring(exceptionMessage.indexOf(StrUtil.COLON) + 2));
        }
        LogUtil.njcnDebug(log, "参数校验异常，异常为：{}", messages);
        recodeBusinessExceptionLog(constraintViolationException, CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION.getMessage());
        List<ConstraintViolation<?>> constraintViolationList = new ArrayList<>(constraintViolationException.getConstraintViolations());
        ConstraintViolation<?> constraintViolation = constraintViolationList.get(0);
        Class<?> rootBeanClass = constraintViolation.getRootBeanClass();
        if (rootBeanClass.getName().endsWith("Controller")) {
            String methodName = constraintViolation.getPropertyPath().toString().substring(0, constraintViolation.getPropertyPath().toString().indexOf(StrUtil.DOT));
            return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION, messages.toString(), resolveMethodDescribeByClassAndMethodName(rootBeanClass, methodName));
        } else {
            return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION, messages.toString(), resolveMethodDescribeByException(constraintViolationException));
        }
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public HttpResult<String> handleIndexOutOfBoundsException(IllegalArgumentException illegalArgumentException) {
        LogUtil.logExceptionStackInfo(CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION.getMessage(), illegalArgumentException);
        recodeBusinessExceptionLog(illegalArgumentException, CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION.getMessage());
        return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION, illegalArgumentException.getMessage(), resolveMethodDescribeByException(illegalArgumentException));
    }

    @ExceptionHandler(Exception.class)
    public HttpResult<String> handleException(Exception exception) {
        Exception tempException = exception;
        String exceptionCause = CommonResponseEnum.UN_DECLARE.getMessage();
        String code = CommonResponseEnum.UN_DECLARE.getCode();
        if (exception instanceof NestedServletException) {
            Throwable cause = exception.getCause();
            if (cause instanceof AssertionError) {
                if (cause.getCause() instanceof BusinessException) {
                    tempException = (BusinessException) cause.getCause();
                    BusinessException tempBusinessException = (BusinessException) cause.getCause();
                    exceptionCause = tempBusinessException.getMessage();
                    code = tempBusinessException.getCode();
                }
            }
        }
        LogUtil.logExceptionStackInfo(exceptionCause, tempException);
        recodeBusinessExceptionLog(exception, exceptionCause);
        return HttpResultUtil.assembleResult(code, null, StrFormatter.format("{}{}{}", resolveMethodDescribeByException(tempException), StrUtil.C_COMMA, exceptionCause));
    }

    @ExceptionHandler(JSONException.class)
    public HttpResult<String> handleIndexOutOfBoundsException(JSONException jsonException) {
        LogUtil.logExceptionStackInfo(CommonResponseEnum.JSON_CONVERT_EXCEPTION.getMessage(), jsonException);
        recodeBusinessExceptionLog(jsonException, CommonResponseEnum.JSON_CONVERT_EXCEPTION.getMessage());
        return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.JSON_CONVERT_EXCEPTION, jsonException.getMessage(), resolveMethodDescribeByException(jsonException));
    }

    private void recodeBusinessExceptionLog(Exception businessException, String message) {
        SysLogAuditRecord logRecord = buildExceptionLogRecord(businessException, message);
        submitLogTask(() -> sysLogAuditService.recodeBusinessExceptionLog(logRecord));
    }

    private SysLogAuditRecord buildExceptionLogRecord(Exception exception, String message) {
        Method method = resolveMethod(exception);
        Integer level = resolveOperateLevel(method);
        return SysLogAuditRecord.builder()
                .userId(resolveUserId())
                .loginName(resolveLoginName())
                .ip(resolveUserIp())
                .operate(resolveExceptionOperate(method, exception))
                .operateType(resolveOperateType(method))
                .result(CommonResponseEnum.FAIL.getMessage())
                .reason(message)
                .type(resolveEventType(method))
                .level(level)
                .warn(level == 1 ? 1 : 0)
                .build();
    }

    private Method resolveMethod(Exception exception) {
        if (exception instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) exception;
            return methodArgumentNotValidException.getParameter().getMethod();
        }
        try {
            return ReflectCommonUtil.getMethod(exception);
        } catch (Exception e) {
            return null;
        }
    }

    private String resolveExceptionOperate(Method method, Exception exception) {
        if (method != null) {
            try {
                String methodDescribe = ReflectCommonUtil.getMethodDescribeByMethod(method);
                if (StrUtil.isNotBlank(methodDescribe)) {
                    return methodDescribe;
                }
            } catch (Exception e) {
                log.warn("解析异常日志方法描述失败，method={}", method.getName(), e);
            }
        }
        try {
            return resolveMethodDescribeByException(exception);
        } catch (Exception e) {
            return "未知业务";
        }
    }

    private Integer resolveEventType(Method method) {
        try {
            if (method == null) {
                return 1;
            }
            String type = ReflectCommonUtil.getOperateInfoByMethod(method).getOperateType();
            return "业务事件".equalsIgnoreCase(type) ? 0 : 1;
        } catch (Exception e) {
            return 1;
        }
    }

    private Integer resolveOperateLevel(Method method) {
        try {
            if (method == null) {
                return 0;
            }
            String level = ReflectCommonUtil.getOperateInfoByMethod(method).getOperateLevel();
            if ("中等".equals(level)) {
                return 1;
            }
            if ("严重".equals(level)) {
                return 2;
            }
        } catch (Exception e) {
            return 0;
        }
        return 0;
    }

    private String resolveOperateType(Method method) {
        try {
            if (method == null) {
                return "";
            }
            return ReflectCommonUtil.getOperateTypeByMethod(method);
        } catch (Exception e) {
            return "";
        }
    }

    private String resolveMethodDescribeByException(Exception exception) {
        try {
            String methodDescribe = ReflectCommonUtil.getMethodDescribeByException(exception);
            return StrUtil.isBlank(methodDescribe) ? "未知业务" : methodDescribe;
        } catch (Exception e) {
            return "未知业务";
        }
    }

    private String resolveMethodDescribeByClassAndMethodName(Class<?> rootBeanClass, String methodName) {
        try {
            String methodDescribe = ReflectCommonUtil.getMethodDescribeByClassAndMethodName(rootBeanClass, methodName);
            return StrUtil.isBlank(methodDescribe) ? "未知业务" : methodDescribe;
        } catch (Exception e) {
            return "未知业务";
        }
    }

    private String resolveUserId() {
        try {
            String userId = RequestUtil.getUserId();
            return StrUtil.isBlank(userId) ? "" : userId;
        } catch (Exception e) {
            return "";
        }
    }

    private String resolveLoginName() {
        try {
            String loginName = RequestUtil.getLoginName();
            return StrUtil.isBlank(loginName) ? "" : loginName;
        } catch (Exception e) {
            return "";
        }
    }

    private String resolveUserIp() {
        try {
            String userIp = RequestUtil.getUserIp();
            return StrUtil.isBlank(userIp) ? "" : userIp;
        } catch (Exception e) {
            return "";
        }
    }

    private void submitLogTask(Runnable task) {
        try {
            logAuditExecutor.execute(() -> {
                try {
                    task.run();
                } catch (Exception e) {
                    log.error("异步记录异常审计日志失败", e);
                }
            });
        } catch (RuntimeException e) {
            log.error("提交异常审计日志任务失败", e);
        }
    }
}
