package link.at17.mid.tushare.web.exception;

import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.stream.Collectors;

import javax.security.auth.login.LoginException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import link.at17.mid.tushare.dto.LayPageResp;
import link.at17.mid.tushare.dto.R;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import com.fasterxml.jackson.databind.JsonNode;

/**
 * 异常处理
 */
@ControllerAdvice
@Slf4j
public class TushareWebPlatformExceptionHandler {

    @Autowired
    HttpServletResponse response;

    @Autowired
    HttpServletRequest request;

    @ExceptionHandler({
        BindException.class,
        MethodArgumentNotValidException.class })
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <Ex extends BindException> R<?> handleBindException(Ex ex) {
        StringBuilder messageSb = new StringBuilder();
        ex.getBindingResult().getAllErrors().forEach(error -> messageSb.append(error.getDefaultMessage()).append("\n"));
        log.warn("Resolved Exception {}", messageSb.substring(0, messageSb.length() - 1));
        log.warn(httpServletRequestToString(request));
        return bodyOrPage(HttpStatus.BAD_REQUEST, messageSb.substring(0, messageSb.length() - 1));
    }

    @ExceptionHandler(LoginException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
    public R<?> handleLoginException(LoginException ex) {
        return bodyOrPage(HttpStatus.NOT_ACCEPTABLE, ex);
    }

    @ExceptionHandler(RException.class)
    @ResponseBody
    public R<?> handleRException(RException ex) {
        response.setStatus(ex.getHttpStatus().value());
        if (ex.getLogRequest()) {
            log.warn(httpServletRequestToString(request));
        }
        return bodyOrPage(ex.getHttpStatus(), ex);
    }

    @ExceptionHandler(ServletException.class)
    @ResponseBody
    public R<?> handleServletException(ServletException ex) {
        HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
        if (ex instanceof HttpRequestMethodNotSupportedException)
            httpStatus = HttpStatus.METHOD_NOT_ALLOWED;
        response.setStatus(httpStatus.value());
        return bodyOrPage(httpStatus, ex);
    }

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public R<?> handleException(Exception ex) {
        if (ex instanceof PageNotFoundException) {
            throw (PageNotFoundException) ex;
        }
        String message = null;
        if (ex.getMessage() != null) {
            message = ex.getMessage();
        }
        else if (ex.getCause() != null) {
            message = ex.getCause().getMessage();
        }
        ex.printStackTrace();
        log.warn("Resolved exception {}", message);
        log.warn(httpServletRequestToString(request));
        return bodyOrPage(HttpStatus.INTERNAL_SERVER_ERROR, ex);
    }
    
    /**
     * 根据 @Autowired request 等自动判断当前请求是 text/html 还是 application/json
     * @return
     */
    private boolean isPage() {
        boolean isPage = true;
        String accept;
        // 匹配当前 request 在控制层的方法，根据是否是
        // @ResponseBody 注解、@RestController 注解、
        // 已知的返回类型来判断返回的是否是页面
        Object handler = request.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
        
        if (handler instanceof HandlerMethod handlerMethod) {
            Method method = handlerMethod.getMethod();
            Class<?> controllerClass = handlerMethod.getBeanType();
            
            boolean hasResponseBody =
                    method.isAnnotationPresent(ResponseBody.class) ||
                    controllerClass.isAnnotationPresent(ResponseBody.class) ||
                    controllerClass.isAnnotationPresent(RestController.class);
            
            Class<?> returnType = method.getReturnType();
            boolean isDtoReturnType = 
                    returnType.equals(R.class) ||
                    returnType.equals(LayPageResp.class) ||
                    JsonNode.class.isAssignableFrom(returnType);
            
            if (hasResponseBody || isDtoReturnType) {
                isPage = false;
            }
        }
        if (isPage && (accept = request.getHeader("Accept")) != null) {
            int indexOfHtml = accept.indexOf("text/html"), indexOfJson = accept.indexOf("application/json");
            if (indexOfHtml == -1 && indexOfJson != -1) {
                isPage = false;
            } else if (indexOfHtml != -1 && indexOfJson != -1) {
                isPage = indexOfHtml < indexOfJson;
            }
        }
        return isPage;
    }

    private R<?> bodyOrPage(HttpStatus httpStatus, String message) {
        boolean isPage = isPage();
        if (isPage) {
            throw message == null ? new RuntimeException("Page exception raised") : new RuntimeException(message);
        }
        R<?> r = message != null ?
                R.status(httpStatus).setMessage(message).setData(message)
                : R.status(httpStatus);
        return r;
    }
    
    private R<?> bodyOrPage(HttpStatus httpStatus, Exception ex) {
        boolean isPage = true;
        String message = null;
        if (ex instanceof RException ||
                ex instanceof LoginException) {
            isPage = false;
            message = ex.getMessage();
        }
        else {
            isPage = isPage();
        }
        if (isPage) {
            if (ex instanceof NoResourceFoundException nrfe) {
                if (StringUtils.isNotEmpty(nrfe.getMessage())
                        && nrfe.getMessage().endsWith(" .well-known/appspecific/com.chrome.devtools.json.")) {
                    // 傻逼 Chrome 开发工具默认调用该地址
                    return null;
                }
            }
            throw ex == null ? new RuntimeException("Page exception raised") : new RuntimeException(ex);
        }
        R<?> r = message != null ? 
                R.status(httpStatus).setMessage(message).setData(message) 
                : R.status(httpStatus);
        return r;
    }

    private static String httpServletRequestToString(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();

        sb.append("Request Method = \"" + request.getMethod() + "\", ");
        sb.append("Request URL Path = \"" + request.getRequestURL() + "\", ");

        String headers = Collections.list(request.getHeaderNames()).stream()
                .map(headerName -> headerName + " : " + Collections.list(request.getHeaders(headerName)))
                .collect(Collectors.joining(", "));

        if (headers.isEmpty()) {
            sb.append("Request headers: NONE,");
        } else {
            sb.append("Request headers: [" + headers + "],");
        }

        String parameters = Collections.list(request.getParameterNames()).stream().map(p -> {
            String[] values = request.getParameterValues(p);
            return p + ": [" + StringUtils.join(values, ", ") + "]";
        }).collect(Collectors.joining(", "));

        if (parameters.isEmpty()) {
            sb.append("Request parameters: NONE.");
        } else {
            sb.append("Request parameters: [" + parameters + "].");
        }

        return sb.toString();
    }
}
