package com.sonic.common.exception;

import java.util.Map;
import java.util.function.Consumer;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;

import com.sonic.common.auth.AuthInterceptor;
import com.sonic.common.rpc.ApiResultCode;
import com.sonic.common.rpc.GlobalResultCode;
import com.sonic.common.rpc.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.TypeMismatchException;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
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.ResponseBody;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;

import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

/**
 * @author code
 */
@Slf4j
public abstract class AbstractExceptionHandler {
    private static final Pattern SYSTEM_PATTERN = Pattern.compile("^.*\\(\\d+\\)$");

    private String appId;

    private ExceptionHandlerHook exceptionHook;

    public AbstractExceptionHandler(ExceptionHandlerHook hook) {
        Preconditions.checkArgument(hook != null);
        Preconditions.checkArgument(!Strings.isNullOrEmpty(hook.getAppId()));

        this.appId = hook.getAppId();
        this.exceptionHook = hook;
    }

    @ExceptionHandler(SysException.class)
    @ResponseBody
    public Result sysExceptionHandler(HttpServletRequest request, HttpServletResponse response, SysException e) {
        return Result.error(e.getErrorCode(), buildErrorMessage(e.getErrorMsg()));
    }

    @ExceptionHandler(BizException.class)
    @ResponseBody
    public Result businessExceptionHandler(HttpServletRequest request, HttpServletResponse response, BizException e) {
        return Result.error(e.getErrorCode(), buildErrorMessage(e.getErrorMsg()));
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result defaultExceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception e) {
        // 未捕获异常不需要在该处打印日志, 由 RequestLogAspect 处理
        ExceptionContext exceptionContext = new ExceptionContext(request, response, e, GlobalResultCode.SYSTEM_EXCEPTION);
        exceptionHook.getExceptionConsumer().accept(exceptionContext);

        return Result.error(exceptionContext.getResultCode().getErrorCode(),
                buildErrorMessage(exceptionContext.getResultCode().getErrorMsg()));
    }

    /**
     * 支持内部抛出的 IllegalArgumentException, 如 com.google.common.base.Preconditions.checkArgument() 抛出的异常
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseBody
    public Result illegalArgumentExceptionHandler(HttpServletResponse response, IllegalArgumentException e) {
        return Result.error(GlobalResultCode.SYSTEM_EXCEPTION, buildErrorMessage(Strings.isNullOrEmpty(e.getMessage()) ? "parameter error" : e.getMessage()));
    }

    /**
     * 404 请求path无效
     * @param response
     * @return
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseBody
    public Result noHandlerExceptionHandler(HttpServletResponse response) {
        return Result.error(GlobalResultCode.SYSTEM_EXCEPTION, buildErrorMessage("request path error"));
    }

    /**
     * 请求 body 未设置或者 JSONObject.parse 参数类型失败
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseBody
    public Result httpMessageNotReadableExceptionHandler(HttpServletResponse response, HttpMessageNotReadableException e) {
        log.error("Invalid request, parameter error",e);
        return Result.error(GlobalResultCode.INVALID_PARAMS, buildErrorMessage("Invalid request, parameter error"));
    }



    /**
     * LocalDateTime 等类型错误
     * @param response
     * @return
     */
    @ExceptionHandler(TypeMismatchException.class)
    @ResponseBody
    public Result typeMismatchExceptionHandler(HttpServletResponse response) {
        return Result.error(GlobalResultCode.INVALID_PARAMS, buildErrorMessage("Invalid request, parameter type error"));
    }

    /**
     * 包装类型参数缺失
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    public Result constraintViolationExceptionHandler(HttpServletResponse response, ConstraintViolationException e) {
        return Result.error(GlobalResultCode.INVALID_PARAMS,
                buildErrorMessage(e.getConstraintViolations().stream()
                        .findFirst()
                        .map(ex -> buildPrettyMessage(shortPropertyPath(ex.getPropertyPath()) + " " + ex.getMessage()))
                        .orElse(e.getMessage())));
    }

    /**
     * 对象类型参数错误
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(BindException.class)
    @ResponseBody
    public Result bindExceptionExceptionHandler(HttpServletResponse response, BindException e) {
        FieldError fieldError = e.getBindingResult().getFieldError();
        if (fieldError.contains(TypeMismatchException.class)) {
            return Result.error(GlobalResultCode.INVALID_PARAMS.getErrorCode(),
                    buildErrorMessage(String.format("parameter type error, parameter name: %s", fieldError.getField())));
        }
        return Result.error(GlobalResultCode.INVALID_PARAMS.getErrorCode(),
                buildErrorMessage(fieldError.getField() + " " + fieldError.getDefaultMessage()));
    }

    /**
     * 包装类型参数绑定失败
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    @ResponseBody
    public Result methodArgumentTypeMismatchExceptionNotReadable(HttpServletResponse response, MethodArgumentTypeMismatchException e) {
        return Result.error(GlobalResultCode.INVALID_PARAMS,
                buildErrorMessage(String.format("parameter type error, parameter name: %s", e.getName())));
    }

    /**
     * 包装类型参数校验失败
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public Result methodArgumentNotValidExceptionExceptionNotReadable(HttpServletResponse response, MethodArgumentNotValidException e) {
        String errorMsg = e.getBindingResult().getFieldErrors().stream()
                .findFirst()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .orElse("parameter verification failed");
        return Result.error(GlobalResultCode.INVALID_PARAMS, buildErrorMessage(errorMsg));
    }

    /**
     * MissingServletRequestParameterException 处理 &#064; RequestParam注解参数缺失的异常
     * @param response
     * @param e
     * @return Result
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseBody
    public Result missingServletRequestParameterException(HttpServletResponse response, MissingServletRequestParameterException e) {
        return Result.error(GlobalResultCode.INVALID_PARAMS,
                buildErrorMessage(String.format("parameter lost[%s - %s]", e.getParameterName(), e.getParameterType())));
    }

    private String shortPropertyPath(Path path) {
        String s = StringUtils.substringAfterLast(path.toString(), ".");
        if (StringUtils.isEmpty(s)) {
            // 如果没有".", 返回原始路径
            return path.toString();
        }
        return s;
    }

    private String buildPrettyMessage(String message) {
        return StringUtils.replace(message, "null", "空");
    }

    private String buildErrorMessage(String originMessage) {
        // 原错误消息里已经有appId，那么不用再追加appId到后面
        boolean existAppId = SYSTEM_PATTERN.matcher(originMessage).find();

        if (existAppId) {
            return originMessage.substring(0, originMessage.length() - 6);
        }
        return originMessage;
    }

    private static String dumpRequest(HttpServletRequest request) {
        Map parameterMap = Maps.newHashMap(request.getParameterMap());
        return new JSONObject()
                .fluentPut("path", request.getRequestURI())
                .fluentPut("caller", request.getAttribute(AuthInterceptor.CALLER_PAYLOAD_KEY))
                .fluentPut("body", request.getAttribute("params"))
                .fluentPut("parameters", parameterMap)
                .toJSONString();
    }

    public interface ExceptionHandlerHook {
        String getAppId();

        default Consumer<ExceptionContext> getExceptionConsumer() {
            return exceptionContext -> {
            };
        }
    }

    @Data
    @AllArgsConstructor
    public static class ExceptionContext {
        HttpServletRequest request;
        HttpServletResponse response;
        Exception exception;
        ApiResultCode resultCode;

        public String dumpRequest() {
            return AbstractExceptionHandler.dumpRequest(request);
        }
    }
}
