package sgc.portal.base.exception.advice;

import sgc.portal.base.Profiles;
import sgc.portal.base.error.ResolvedMessageErrorAttributes;
import sgc.portal.base.exception.BusinessException;
import sgc.portal.base.exception.NestedRuntimeException;
import sgc.portal.base.exception.SessionTimeoutException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.sql.SQLException;
import java.util.Map;

/**
 * <pre>
 * Framework에서 정의한 NestedRuntimeException 그리고 정의되지 않은 예외인
 * Exception에 대한 예외 처리를 담당한다.
 * </pre>
 *
 * @author YongSang
 * @version 1.0.0
 * @since 2020. 7. 28.
 */
@Slf4j
@Controller
@RestControllerAdvice
public class DefaultErrorHandler implements ErrorController {

    private static final String DEFAULT_VIEW_NAME = "ErrorPage/Error400";

    private static final String ERROR_PATH = "/error";

    @Autowired
    private ResolvedMessageErrorAttributes errorAttributes;

    @Autowired
    private Environment environment;

    /**
     * Framework에서 정의한 NestedRuntimeException이 발생한 경우 처리
     *
     * @param e       the e
     * @param request the request
     * @return object object
     */
    @ExceptionHandler(value = NestedRuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Object handleNestedRuntimeException(NestedRuntimeException e, HttpServletRequest request) {
        ErrorMessage errorMessage = new ErrorMessage();

        errorMessage.setErrorCode(e.getErrorCode());
        errorMessage.setMessageCode(e.getCode());
        errorMessage.setArgs(e.getArguments());
        errorMessage.setCause(ExceptionUtils.getRootCause(e));
        errorMessage.setMessage(e.getMessage());
        errorMessage.setBusiMessage(e.getMessage());
        logging(e);

        if ((request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").toLowerCase().equals("xmlhttprequest")) || request.getHeader("ORIGIN") != null){
            return errorMessage;
        } else{
            return new ModelAndView(DEFAULT_VIEW_NAME);
        }
    }

    /**
     * Handle business exception object.
     *
     * @param e       the e
     * @param request the request
     * @return the object
     */
    @ExceptionHandler(value = BusinessException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Object handleBusinessException(BusinessException e, HttpServletRequest request) {
        ErrorMessage errorMessage = new ErrorMessage();

        /* Business Exception I18n Error message Setting*/
        getErrorAttributes(request);

        errorMessage.setErrorCode(e.getErrorCode());
        errorMessage.setCause(ExceptionUtils.getRootCause(e));
        errorMessage.setMessage(e.getMessage());
        errorMessage.setBusiMessage(e.getBuisMessage());
        errorMessage.setBusiErrorCode(e.getBuisErrorCode());
        log.error("*****************Business Exception start***********************");
        log.error(e.getBuisMessage());
        log.error("*****************Business Exception end***********************");
        logging(e);


        if (request.getHeader(MediaType.TEXT_HTML_VALUE) != null ){
            return new ModelAndView(DEFAULT_VIEW_NAME);
        } else{
            return errorMessage;
        }
    }


    /**
     * Handle session timeout exception object.
     *
     * @param e       the e
     * @param request the request
     * @return the object
     */
    @ExceptionHandler(SessionTimeoutException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Object handleSessionTimeoutException(SessionTimeoutException e, HttpServletRequest request) {
        ErrorMessage errorMessage = new ErrorMessage();

        errorMessage.setErrorCode(e.getErrorCode());
        errorMessage.setMessageCode(e.getCode());
        errorMessage.setArgs(e.getArguments());
        errorMessage.setCause(ExceptionUtils.getRootCause(e));

        logging(e);

        if (request.getHeader(MediaType.TEXT_HTML_VALUE) != null ){
            return new ModelAndView(DEFAULT_VIEW_NAME);
        } else{
            return errorMessage;
        }

    }

    /**
     * 정의되지 않은 모든 예외에 대한 처리
     *
     * @param e the e
     * @return error message
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorMessage handleException(Exception e) {
        Throwable cause = ExceptionUtils.getRootCause(e);
        ErrorMessage errorMessage = new ErrorMessage();
        errorMessage.setCause(ExceptionUtils.getRootCause(e));
        if (cause instanceof SQLException) {
            SQLException se = (SQLException) e.getCause();
            errorMessage = handleSQLException(se.getErrorCode(), se.getMessage());
        } else {
            errorMessage.setErrorCode(ErrorConst.UNKNOWN_ERROR);
            errorMessage.setMessage(e.getMessage());
        }
        logging(e);
        return errorMessage;
    }

    /**
     * SQLException의 경우 테이블의 이름 또는 컬럼명 등이 외부 사용자에게 노출될 경우
     * 보안적으로 취약할 수 있으므로 별도의 처리를 통해 SQLException의 메시지를 노출시키지 않는다.
     * @return ErrorMessage
     */
    @SuppressWarnings("unused")
    private ErrorMessage handleSQLException(int errorCode, String errorMsg) {
        ErrorMessage errorMessage;
        errorMessage = new ErrorMessage();
        errorMessage.setErrorCode(errorCode);
        if(errorCode == -271){
            errorMessage.setMessageCode("exception.sql.271");
        }else if(errorCode == -244){
            errorMessage.setMessageCode("exception.sql.244");
        }else if(errorCode == -268 || errorCode == -100 || errorCode == -10007){
            errorMessage.setMessageCode("exception.sql.268");
        }else if(errorCode == -691){
            errorMessage.setMessageCode("exception.sql.691");
        }else if(errorCode == -284){
            errorMessage.setMessageCode("exception.sql.284");
        }else if(errorCode == -20000){
            errorMessage.setMessageCode("exception.sql.20000");
            //에러 메시지 중 사용자 정의 문구만 발췌(JDBC-20000:[사용자 정의 문구].   \nTBR-15163~~~ 형태로 넘어 옴)
            String msg = errorMsg;
            msg = msg.substring(11, msg.indexOf("TBR-15163")-5);
            errorMessage.setMessage(msg);
            errorMessage.setBusiMessage(msg);
        }else{
            errorMessage.setMessageCode("exception.sql");
        }
        return errorMessage;
    }

    /**
     * 예외 로깅
     */
    private void logging(Throwable t) {
        log.error(t.getMessage(), t);
    }


    /**
     * html request 에 대한 에러 처리
     *
     * @param request the request
     * @return model and view
     */
    @RequestMapping(value = ERROR_PATH, produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request) {
        String viewName;
        viewName = DEFAULT_VIEW_NAME;
        Map<String, Object> model = getErrorAttributes(request);
        HttpStatus status = getStatus(request);

        if(status == HttpStatus.UNAUTHORIZED) {
            viewName = "redirect:/";
            model.clear();
        }
        return new ModelAndView(viewName, model, status);
    }

    /**
     * html 외 error에대한 처리
     *
     * @param request the request
     * @return response entity
     */
    @RequestMapping(value = ERROR_PATH)
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request);
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(body, status);
    }

    @Override
    public String getErrorPath() {
        return ERROR_PATH;
    }

    /**
     * Gets error attributes.
     *
     * @param request the request
     * @return the error attributes
     */
    private Map<String, Object> getErrorAttributes(HttpServletRequest request) {
        WebRequest webRequest = new ServletWebRequest(request);
        return this.errorAttributes.getErrorAttributes(webRequest, isIncludeStackTrace());
    }

    /**
     * Is include stack trace boolean.
     *
     * @return the boolean
     */
    private boolean isIncludeStackTrace() {
        String[] activeProfiles = environment.getActiveProfiles();
        for (String activeProfile : activeProfiles) {
            if (Profiles.valueOf(activeProfile.toUpperCase()) == Profiles.PROD || Profiles.valueOf(activeProfile.toUpperCase()) == Profiles.DEV) {
                return false;
            }
        }
        return true;
    }

    /**
     * Gets status.
     *
     * @param request the request
     * @return the status
     */
    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        try {
            if(isIncludeStackTrace()){
                return HttpStatus.valueOf(statusCode);
            }else{
                return HttpStatus.INTERNAL_SERVER_ERROR;
            }
        }
        catch (Exception ex) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
    }


}