/*-
 * =========================LICENSE_START==================================
 * heimdall-api
 * ========================================================================
 * Copyright (C) 2018 Conductor Tecnologia SA
 * ========================================================================
 * Licensed under the Apache License, Version 2.0 (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ==========================LICENSE_END===================================
 */
package br.com.conductor.heimdall.api.configuration;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.http.NoHttpResponseException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
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 com.fasterxml.jackson.core.JsonParseException;

import br.com.conductor.heimdall.api.configuration.GlobalExceptionHandler.BindExceptionInfo.BindError;
import br.com.conductor.heimdall.core.exception.BadRequestException;
import br.com.conductor.heimdall.core.exception.ExceptionMessage;
import br.com.conductor.heimdall.core.exception.ForbiddenException;
import br.com.conductor.heimdall.core.exception.HeimdallException;
import br.com.conductor.heimdall.core.exception.NotFoundException;
import br.com.conductor.heimdall.core.exception.ServerErrorException;
import br.com.conductor.heimdall.core.exception.UnauthorizedException;
import br.com.conductor.heimdall.core.util.UrlUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartException;

/**
 * This class captures the exceptions generated by the system and redirects them to the Heimdall custom exceptions.
 *
 * @author Thiago Sampaio
 * @author Filipe Germano
 *
 */
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler{

     /**
      * Method that captures all the {@link NotFoundException} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     @ResponseStatus(HttpStatus.NOT_FOUND)
     @ExceptionHandler(NotFoundException.class)
     public @ResponseBody ErroInfo handleExceptionNotFound(HttpServletResponse response, HttpServletRequest request, Exception exception) {

          ErroInfo erroInfo = buildErrorInfo(request, exception);
          return erroInfo;

     }

     /**
      * Method that captures all the {@link ServerErrorException} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
     @ExceptionHandler(ServerErrorException.class)
     public @ResponseBody ErroInfo handleExceptionServerError(HttpServletResponse response, HttpServletRequest request, Exception exception) {

          ErroInfo erroInfo = buildErrorInfo(request, exception);
          return erroInfo;

     }

     /**
      * Method that captures all the {@link ServerErrorException} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
     @ExceptionHandler(Exception.class)
     public @ResponseBody ErroInfo handleException(HttpServletResponse response, HttpServletRequest request, Exception exception) {

          ErroInfo erroInfo = buildErrorInfoException(request, exception);
          log.error(exception.getMessage(), exception);
          return erroInfo;

     }

     /**
      * Method that captures all the {@link ServerErrorException} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
     @ExceptionHandler(NoHttpResponseException.class)
     public @ResponseBody ErroInfo noHttpResponseExceptionServerError(HttpServletResponse response, HttpServletRequest request, NoHttpResponseException exception) {

          ErroInfo erroInfo = buildErrorInfoException(request, exception);
          log.error(exception.getMessage(), exception);
          return erroInfo;

     }

     /**
      * Method that captures all the {@link BadRequestException} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(BadRequestException.class)
     public @ResponseBody ErroInfo handleExceptionBadRequest(HttpServletResponse response, HttpServletRequest request, Exception exception) {

          ErroInfo erroInfo = buildErrorInfo(request, exception);
          return erroInfo;

     }

     /**
      * Method that captures all the {@link UnauthorizedException} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     @ResponseStatus(HttpStatus.UNAUTHORIZED)
     @ExceptionHandler(UnauthorizedException.class)
     public @ResponseBody ErroInfo handleExceptionUnauthorized(HttpServletResponse response, HttpServletRequest request, Exception exception) {

          ErroInfo erroInfo = buildErrorInfo(request, exception);
          return erroInfo;

     }

     /**
      * Method that captures all the {@link ForbiddenException} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     @ResponseStatus(HttpStatus.FORBIDDEN)
     @ExceptionHandler(ForbiddenException.class)
     public @ResponseBody ErroInfo handleExceptionForbidden(HttpServletResponse response, HttpServletRequest request, Exception exception) {

          ErroInfo erroInfo = buildErrorInfo(request, exception);
          return erroInfo;

     }

     @Autowired
     private MessageSource messageSource;

     /**
      * Method that captures all the {@link BindExceptionInfo} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link BindExceptionInfo}
      * @return {@link BindExceptionInfo}.
      */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(BindException.class)
     public @ResponseBody BindExceptionInfo validationBindException(HttpServletResponse response, HttpServletRequest request, BindException exception) {

          BindExceptionInfo bindException = new BindExceptionInfo();
          List<BindError> errors = new ArrayList<>();
          List<ObjectError> objectsError = exception.getBindingResult().getAllErrors();

          objectsError.forEach(objectError -> {
               FieldError fieldError = (FieldError) objectError;

               String message = null;

               try {

                    String code = fieldError.getCodes()[0];
                    message = messageSource.getMessage(code, null, LocaleContextHolder.getLocale());
               } catch (Exception e) {

                    message = null;
               }

               bindException.timestamp = LocalDateTime.now();
               bindException.status = 400;
               bindException.exception = "BindExceptionPIER";

               BindError error = bindException.new BindError();
               error.defaultMessage = message != null ? message : fieldError.getDefaultMessage();
               error.objectName = fieldError.getObjectName();
               error.field = fieldError.getField();
               error.code = fieldError.getCode();

               errors.add(error);
          });

          bindException.erros = errors;

          return bindException;
     }

     /**
      * Method that captures all the {@link BindExceptionInfo} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link BindExceptionInfo}
      * @return {@link BindExceptionInfo}.
      */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(MethodArgumentNotValidException.class)
     public @ResponseBody BindExceptionInfo validationMethodArgumentNotValidException(HttpServletResponse response, HttpServletRequest request, MethodArgumentNotValidException exception) {

          BindExceptionInfo bindException = new BindExceptionInfo();
          List<BindError> errors = new ArrayList<>();
          List<ObjectError> objectsError = exception.getBindingResult().getAllErrors();

          objectsError.forEach(objectError -> {
               FieldError fieldError = (FieldError) objectError;

               bindException.timestamp = LocalDateTime.now();
               bindException.status = 400;
               bindException.exception = "MethodArgumentNotValidException";

               BindError error = bindException.new BindError();
               error.defaultMessage = fieldError.getDefaultMessage();
               error.objectName = fieldError.getObjectName();
               error.field = fieldError.getField();
               error.code = fieldError.getCode();

               errors.add(error);
          });

          bindException.erros = errors;

          return bindException;
     }

     /**
      * Method that captures all the {@link AccessDeniedException} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     @ResponseStatus(HttpStatus.UNAUTHORIZED)
     @ExceptionHandler(AccessDeniedException.class)
     public @ResponseBody ErroInfo handleExceptionAccessDenied(HttpServletResponse response, HttpServletRequest request, Exception exception) {

          ErroInfo erroInfo = buildErrorInfo(request, new HeimdallException(ExceptionMessage.ACCESS_DENIED));
          return erroInfo;

     }

     /**
      * Method that captures all the {@link HttpMessageNotReadableException} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(HttpMessageNotReadableException.class)
     public @ResponseBody ErroInfo handleExceptionHttpMessageNotReadable(HttpServletResponse response, HttpServletRequest request, HttpMessageNotReadableException exception) {

          ErroInfo erroInfo = buildErrorInfo(request, new HeimdallException(ExceptionMessage.GLOBAL_JSON_INVALID_FORMAT));
          return erroInfo;

     }

     /**
      * Method that captures all the {@link JsonParseException} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(JsonParseException.class)
     public @ResponseBody ErroInfo handleExceptionJsonParse(HttpServletResponse response, HttpServletRequest request, JsonParseException exception) {

          ErroInfo erroInfo = buildErrorInfo(request, new HeimdallException(ExceptionMessage.GLOBAL_JSON_INVALID_FORMAT));
          return erroInfo;

     }

     /**
      * Method that captures all the {@link DataIntegrityViolationException} exceptions.
      *
      * @param response
      * {@link HttpServletResponse}
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(DataIntegrityViolationException.class)
     public @ResponseBody ErroInfo handleSqlException(HttpServletResponse response, HttpServletRequest request, DataIntegrityViolationException exception) {

          if (exception.getMessage().contains("email")) {
               return buildErrorInfo(request, new HeimdallException(ExceptionMessage.EMAIL_ALREADY_EXIST));
          }

          if (exception.getMessage().contains("username")) {
               return buildErrorInfo(request, new HeimdallException(ExceptionMessage.USERNAME_ALREADY_EXIST));
          }

          return buildErrorInfo(request, new HeimdallException(ExceptionMessage.GLOBAL_RESOURCE_NOT_FOUND));

     }

    /**
     * Method that captures all the {@link MultipartException} exceptions.
     *
     * @param response  {@link HttpServletRequest}
     * @param request   {@link HttpServletResponse}
     * @param exception {@link Exception}
     * @return  {@link ErroInfo}
     */
     @ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
     @ExceptionHandler(MultipartException.class)
     public @ResponseBody ErroInfo handleExceptionMultipartException(HttpServletResponse response, HttpServletRequest request, MultipartException exception) {

         return buildErrorInfo(request, new HeimdallException(ExceptionMessage.MIDDLEWARE_PAYLOAD_TOO_LARGE));
     }

     /**
      * Method responsible to create the exception object.
      *
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     private ErroInfo buildErrorInfo(HttpServletRequest request, Exception exception) {

          HeimdallException exceptionPIER = (HeimdallException) exception;
          ErroInfo erroInfo = new ErroInfo(LocalDateTime.now(), exceptionPIER.getMsgEnum().getHttpCode(), exceptionPIER.getClass().getSimpleName(), exceptionPIER.getMessage(), UrlUtil.getCurrentUrl(request));
          return erroInfo;
     }

     /**
      * Method responsible to create the exception object.
      *
      * @param request
      * {@link HttpServletRequest}
      * @param exception
      * {@link Exception}
      * @return {@link ErroInfo}.
      */
     private ErroInfo buildErrorInfoException(HttpServletRequest request, Exception exception) {

          HeimdallException exceptionPIER = new HeimdallException(ExceptionMessage.GLOBAL_ERROR_ZUUL);
          ErroInfo erroInfo = new ErroInfo(LocalDateTime.now(), exceptionPIER.getMsgEnum().getHttpCode(), exceptionPIER.getClass().getSimpleName(), exceptionPIER.getMessage(), UrlUtil.getCurrentUrl(request));
          return erroInfo;
     }

     /**
      * Class that represents the return object used by all Heimdall Exceptions.
      *
      * @author Thiago Sampaio
      *
      */
     @AllArgsConstructor
     @Getter
     public class ErroInfo {

         /**
          * TImestamp from the moment that the exception was created.
          */
         private LocalDateTime timestamp;

         /**
          * Exception identifier.
          */
         private Integer code;

         /**
          * Exception class name.
          */
         private String exception;

         /**
          * Exception description.
          */
         private String message;

         /**
          * Path that generated the request that caused the exception.
          */
         private String path;

     }

     /**
      * Class that represents the exceptions created by the Heimdall validations.
      *
      * @author Filipe Germano
      *
      */
     public class BindExceptionInfo {

          @Getter
          private LocalDateTime timestamp;

          @Getter
          private Integer status;

          @Getter
          private String exception;

          @Getter
          private List<BindError> erros;

          @Data
          public class BindError {

        	  private String defaultMessage;

        	  private String objectName;

        	  private String field;

        	  private String code;
          }

     }

}