package io.kaif.web.support;

import static java.util.stream.Collectors.*;

import java.util.Arrays;
import java.util.Iterator;
import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.servlet.support.RequestContextUtils;

import io.kaif.config.SpringProfile;

/**
 * common exception handler for restful controllers, allow convert common spring data access
 * exception and part of spring mvc exceptions(*) to json, include english error message
 * <p>
 * subclass should implements {@link #createErrorResponse(HttpStatus, String)} to create error json
 * <p>
 * (*) note that not all spring mvc internal exception could be catched and translate to json, this
 * seems due to spring 4.0 bug. when @ControllerAdvice specify selectors (in our case,
 * anontations=RestController), it could not catch all spring mvc exceptions, such as
 * {@link org.springframework.web.HttpRequestMethodNotSupportedException}
 *
 * @author ingram
 */
public abstract class AbstractRestExceptionHandler<E extends ErrorResponse>
    extends ResponseEntityExceptionHandler {

  protected final Logger logger = LoggerFactory.getLogger(this.getClass());

  @Autowired
  private MessageSource messageSource;

  @Autowired
  private Environment environment;

  @ExceptionHandler(AccessDeniedException.class)
  @ResponseBody
  public ResponseEntity<E> handleAccessDeniedException(final AccessDeniedException ex,
      final WebRequest request) {
    final HttpStatus status = HttpStatus.UNAUTHORIZED;
    final E errorResponse = createErrorResponse(status,
        i18n(request, "rest-error.RestAccessDeniedException"));
    if (environment.acceptsProfiles(SpringProfile.DEV)) {
      //only dev server log detail access denied
      logException(ex, errorResponse, request);
    } else {
      logger.warn("{} {}", guessUri(request), ex.getClass().getSimpleName());
    }
    return new ResponseEntity<>(errorResponse, status);
  }

  protected final String guessUri(WebRequest request) {
    String uri = "non uri";
    if (request instanceof ServletWebRequest) {
      uri = ((ServletWebRequest) request).getRequest().getRequestURI();
    }
    return uri;
  }

  @ExceptionHandler(DataAccessException.class)
  @ResponseBody
  public ResponseEntity<E> handleDataAccessException(final DataAccessException ex,
      final WebRequest request) {
    final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
    final E errorResponse = createErrorResponse(status,
        i18n(request, "rest-error.DataAccessException", ex.getClass().getSimpleName()));
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  protected abstract E createErrorResponse(HttpStatus status, String reason);

  @ExceptionHandler(EmptyResultDataAccessException.class)
  @ResponseBody
  public ResponseEntity<E> handleEmptyResultDataAccessException(final EmptyResultDataAccessException ex,
      final WebRequest request) {
    final HttpStatus status = HttpStatus.NOT_FOUND;
    final E errorResponse = createErrorResponse(status,
        i18n(request, "rest-error.EmptyResultDataAccessException", ex.getClass().getSimpleName()));
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  protected final String i18n(WebRequest request, String key, Object... args) {
    return messageSource.getMessage(key, args, "!" + key + "!", resolveLocale(request));
  }

  private Locale resolveLocale(WebRequest request) {
    if (!(request instanceof ServletWebRequest)) {
      return request.getLocale();
    }
    ServletWebRequest servletWebRequest = (ServletWebRequest) request;
    return RequestContextUtils.getLocale(servletWebRequest.getRequest());
  }

  @ExceptionHandler(DataIntegrityViolationException.class)
  @ResponseBody
  public ResponseEntity<E> handleDataIntegrityViolationException(final DataIntegrityViolationException ex,
      final WebRequest request) {
    final HttpStatus status = HttpStatus.CONFLICT;
    final E errorResponse = createErrorResponse(status,
        i18n(request, "rest-error.DataIntegrityViolationException"));
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  @ExceptionHandler(DuplicateKeyException.class)
  @ResponseBody
  public ResponseEntity<E> handleDuplicateKeyException(final DuplicateKeyException ex,
      final WebRequest request) {
    final HttpStatus status = HttpStatus.CONFLICT;
    final E errorResponse = createErrorResponse(status,
        i18n(request, "rest-error.DuplicateKeyException"));
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  @Override
  protected ResponseEntity<Object> handleExceptionInternal(final Exception ex,
      final Object body,
      final HttpHeaders headers,
      final HttpStatus status,
      final WebRequest request) {
    final E errorResponse = createErrorResponse(status, status.getReasonPhrase());
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  @Override
  protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
      HttpHeaders headers,
      HttpStatus status,
      WebRequest request) {

    //TODO detail i18n and missing parameter name
    final String detail = ex.getBindingResult()
        .getAllErrors()
        .stream()
        .map(DefaultMessageSourceResolvable::getDefaultMessage)
        .collect(joining(", "));
    final E errorResponse = createErrorResponse(status,
        i18n(request, "rest-error.MethodArgumentNotValidException", detail));
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  /**
   * Customize the response for MissingServletRequestParameterException.
   * This method delegates to
   * {@link #handleExceptionInternal(Exception, Object, org.springframework.http.HttpHeaders,
   * org.springframework.http.HttpStatus, org.springframework.web.context.request.WebRequest)}.
   *
   * @param ex
   *     the exception
   * @param headers
   *     the headers to be written to the response
   * @param status
   *     the selected response status
   * @param request
   *     the current request
   * @return a {@code ResponseEntity} instance
   */
  @Override
  protected ResponseEntity<Object> handleMissingServletRequestParameter(
      MissingServletRequestParameterException ex,
      HttpHeaders headers,
      HttpStatus status,
      WebRequest request) {
    final E errorResponse = createErrorResponse(status, ex.getMessage());
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  @ExceptionHandler({ OptimisticLockingFailureException.class })
  @ResponseBody
  public ResponseEntity<E> handleOptimisticLockingFailureException(final OptimisticLockingFailureException ex,
      final WebRequest request) {
    final HttpStatus status = HttpStatus.LOCKED;
    final E errorResponse = createErrorResponse(status,
        i18n(request, "rest-error.OptimisticLockingFailureException"));
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  @ExceptionHandler(Exception.class)
  @ResponseBody
  public ResponseEntity<E> handleOtherException(final Exception ex, final WebRequest request) {
    // IOException ...etc
    final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
    final E errorResponse = createErrorResponse(status,
        i18n(request, "rest-error.Exception", ex.getClass().getSimpleName()));
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  @ExceptionHandler({ PermissionDeniedDataAccessException.class })
  @ResponseBody
  public ResponseEntity<E> handlePermissionDeniedDataAccessException(final PermissionDeniedDataAccessException ex,
      final WebRequest request) {
    final HttpStatus status = HttpStatus.UNAUTHORIZED;
    final E errorResponse = createErrorResponse(status,
        i18n(request, "rest-error.PermissionDeniedDataAccessException"));
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  @ExceptionHandler({ PessimisticLockingFailureException.class })
  @ResponseBody
  public ResponseEntity<E> handlePessimisticLockingFailureException(final PessimisticLockingFailureException ex,
      final WebRequest request) {
    final HttpStatus status = HttpStatus.LOCKED;
    final E errorResponse = createErrorResponse(status,
        i18n(request, "rest-error.PessimisticLockingFailureException"));
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  @ExceptionHandler({ QueryTimeoutException.class })
  @ResponseBody
  public ResponseEntity<E> handleQueryTimeoutException(final QueryTimeoutException ex,
      final WebRequest request) {
    final HttpStatus status = HttpStatus.REQUEST_TIMEOUT;
    final E errorResponse = createErrorResponse(status,
        i18n(request, "rest-error.QueryTimeoutException"));
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  @ExceptionHandler(RuntimeException.class)
  @ResponseBody
  public ResponseEntity<E> handleRuntimeException(final RuntimeException ex,
      final WebRequest request) {
    // Runtime Exception always hidden, we should not leak internal Exception stacktrace
    final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
    final E errorResponse = createErrorResponse(status,
        i18n(request, "rest-error.RuntimeException", ex.getClass().getSimpleName()));
    logException(ex, errorResponse, request);
    return new ResponseEntity<>(errorResponse, status);
  }

  protected final void logException(final Exception ex,
      final E errorResponse,
      final WebRequest request) {
    final StringBuilder sb = new StringBuilder();
    sb.append(errorResponse);
    sb.append("\n");
    sb.append(request.getDescription(true));
    sb.append("\nparameters -- ");
    for (final Iterator<String> iter = request.getParameterNames(); iter.hasNext(); ) {
      final String name = iter.next();
      sb.append(name);
      sb.append(":");
      final String[] values = request.getParameterValues(name);
      if (values == null) {
        sb.append("null");
      } else if (values.length == 0) {
        sb.append("");
      } else if (values.length == 1) {
        sb.append(values[0]);
      } else {
        sb.append(Arrays.toString(values));
      }
      sb.append(" ");
    }
    logger.error(sb.toString(), ex);
  }

}