package ru.yandex.qe.dispenser.ws.param;

import java.util.stream.Collectors;

import javax.annotation.Nonnull;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.NoContentException;
import javax.ws.rs.core.Response;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.google.common.collect.ImmutableMap;
import org.apache.cxf.logging.FaultListener;
import org.apache.cxf.message.Message;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.eclipse.jetty.io.EofException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.MessageSource;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.security.access.AccessDeniedException;

import ru.yandex.qe.bus.exception.ExceptionToResponseMapper;
import ru.yandex.qe.dispenser.api.util.SerializationUtils;
import ru.yandex.qe.dispenser.domain.exception.ExceptionType;
import ru.yandex.qe.dispenser.domain.exception.LocalizableException;
import ru.yandex.qe.dispenser.domain.exception.MultiMessageException;
import ru.yandex.qe.dispenser.domain.exception.SingleMessageException;
import ru.yandex.qe.dispenser.domain.hierarchy.Session;
import ru.yandex.qe.dispenser.domain.i18n.LocalizableString;
import ru.yandex.qe.dispenser.domain.util.LocalizationUtils;
import ru.yandex.qe.dispenser.ws.aspect.ForbiddenException;
import ru.yandex.qe.dispenser.ws.aspect.UnauthorizedException;
import ru.yandex.qe.dispenser.ws.common.domain.exceptions.ConflictException;
import ru.yandex.qe.dispenser.ws.common.domain.exceptions.TooManyRequestsException;

public final class DiExceptionMapper extends ExceptionToResponseMapper {
    private static final Logger LOG = LoggerFactory.getLogger(DiExceptionMapper.class);

    @Autowired
    @Qualifier("errorMessageSource")
    private MessageSource errorMessageSource;

    @NotNull
    @Override
    public Response toResponse(@NotNull final Throwable e) {
        Session.ERROR.set(e);
        // TODO: DISPENSER-157: Улучшить обработку ошибок
        if (e instanceof IncorrectResultSizeDataAccessException) {
            return handleIncorrectResultSizeDataAccessException((IncorrectResultSizeDataAccessException) e);
        }
        if (e instanceof JsonMappingException && e.getCause() != null && e.getCause().getMessage() != null
                && e.getCause() instanceof IllegalArgumentException) {
            return handleJsonMappingCauseException((JsonMappingException) e, (IllegalArgumentException) e.getCause());
        }
        if (e instanceof JsonProcessingException) {
            return handleJsonProcessingException((JsonProcessingException) e);
        }
        if (e instanceof IllegalArgumentException) {
            return handleIllegalArgumentException((IllegalArgumentException) e);
        }
        if (e instanceof NotFoundException) {
            return handleNotFoundException((NotFoundException) e);
        }
        if (e instanceof WebApplicationException) {
            return handleWebApplicationException((WebApplicationException) e);
        }
        if (e instanceof ForbiddenException) {
            return handleForbiddenException((ForbiddenException) e);
        }
        if (e instanceof UnauthorizedException) {
            return handleUnauthorizedException((UnauthorizedException) e);
        }
        if (e instanceof AccessDeniedException) {
            return handleAccessDeniedException((AccessDeniedException) e);
        }
        if (e instanceof NoContentException) {
            return handleNoContentException((NoContentException) e);
        }
        if (e instanceof EofException) {
            return handleEofException((EofException) e);
        }
        if (e instanceof TooManyRequestsException) {
            return handleTooManyRequestsException((TooManyRequestsException) e);
        }
        if (e instanceof ConflictException) {
            return handleConflictException(((ConflictException) e));
        }
        if (e instanceof SingleMessageException) {
            return handleLocalizableSingleMessageException(((SingleMessageException) e));
        }
        if (e instanceof MultiMessageException) {
            return handleLocalizableMultiMessageException(((MultiMessageException) e));
        }
        return handleUnknownException(e);
    }

    private Response handleLocalizableSingleMessageException(@NotNull final SingleMessageException e) {
        final LocalizableString localizedMessage = e.getLocalizableString();
        final String message = LocalizationUtils.resolveWithDefaultAsKey(errorMessageSource, localizedMessage, Session.USER_LOCALE.get());
        final ExceptionType type = e.getType();
        logLocalizableException(e, message);
        return toErrorResponse(type.getStatus(), type.getTitle(), message);
    }

    private Response handleLocalizableMultiMessageException(@NotNull final MultiMessageException e) {
        final String message = e.getErrors().getErrors()
                .stream()
                .map(ls -> LocalizationUtils.resolveWithDefaultAsKey(errorMessageSource, ls, Session.USER_LOCALE.get()))
                .collect(Collectors.joining("\n"));
        final ExceptionType type = e.getType();
        logLocalizableException(e, message);
        return toErrorResponse(type.getStatus(), type.getTitle(), message);
    }

    private void logLocalizableException(@NotNull final LocalizableException e, @NotNull final String message) {
        if (e.getType() == ExceptionType.UNKNOWN) {
            LOG.error("Unexpected exception", e);
        } else {
            if (e.getCause() != null) {
                LOG.warn("{}: '{}', cause: {}", e.getClass().getSimpleName(), message, e.getMessage());
            } else {
                LOG.warn("{}: '{}'", e.getClass().getSimpleName(), message);
            }
        }
    }

    private Response handleConflictException(@NotNull final ConflictException e) {
        LOG.warn("Conflict: {}", exceptionToString(e));
        final String message = e.getMessage() != null ? e.getMessage() : "Conflict has occurred";
        return toErrorResponse(Response.Status.CONFLICT, "Conflict", message);
    }

    private Response handleTooManyRequestsException(@NotNull final TooManyRequestsException e) {
        LOG.warn("Too many requests: {}", exceptionToString(e));
        return toErrorResponse(Response.Status.TOO_MANY_REQUESTS, "Too many requests",
                "Too many requests, try again later");
    }

    private Response handleJsonProcessingException(@NotNull final JsonProcessingException e) {
        LOG.warn("Invalid json: {}", exceptionToString(e));
        return toErrorResponse(Response.Status.BAD_REQUEST, "Invalid json", "Invalid json was received");
    }

    private Response handleJsonMappingCauseException(@NotNull final JsonMappingException e, @NotNull final IllegalArgumentException cause) {
        LOG.warn("Invalid json: {}", exceptionToString(e));
        final String message = cause.getMessage() != null ? cause.getMessage() : "Invalid json was received";
        return toErrorResponse(Response.Status.BAD_REQUEST, "Invalid json", message);
    }

    private Response handleIllegalArgumentException(@NotNull final IllegalArgumentException e) {
        LOG.warn("Illegal argument: {}", exceptionToString(e));
        final String message = e.getMessage() != null ? e.getMessage() : "Validation failure, invalid argument";
        return toErrorResponse(Response.Status.BAD_REQUEST, "Invalid argument", message);
    }

    @NotNull
    private Response handleIncorrectResultSizeDataAccessException(@NotNull final IncorrectResultSizeDataAccessException e) {
        LOG.warn("Invalid object key: {}", exceptionToString(e));
        final String message = e.getMessage() != null ? e.getMessage() : "A required object was not found";
        return toErrorResponse(Response.Status.BAD_REQUEST, "Invalid object key", message);
    }

    @NotNull
    private Response toErrorResponse(@NotNull final Response.Status status, @NotNull final String title,
                                     final @NotNull String description) {
        final String entity = SerializationUtils.writeValueAsString(ImmutableMap.<String, Object>builder()
                .put("status", status.getStatusCode())
                .put("title", status.toString() + " - " + title)
                .put("description", description)
                .build());

        return Response.status(status)
                .type(MediaType.APPLICATION_JSON_TYPE)
                .entity(entity)
                .build();
    }

    @NotNull
    private Response handleWebApplicationException(final WebApplicationException exception) {
        Response response = exception.getResponse();
        String errorMessage = null;
        if (response == null) {
            // Default error response is provided unless custom error response is present
            errorMessage = buildErrorMessage(exception, "Unexpected error");
            response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).type(MediaType.TEXT_PLAIN).entity(errorMessage).build();
        }
        final Message message = PhaseInterceptorChain.getCurrentMessage();
        final FaultListener faultListener = getFaultListener(message);
        if (faultListener == null || !faultListener.faultOccurred(exception,
                errorMessage == null ? buildErrorMessage(exception, "Unexpected error") : errorMessage, message)) {
            // Log exception unless default logging is enabled
            LOG.error(exception.getClass().getSimpleName() + " has been caught, status: " + response.getStatus(), exception);
        }
        return response;
    }

    @Nullable
    private FaultListener getFaultListener(final Message message) {
        return (message != null) ? (FaultListener) PhaseInterceptorChain.getCurrentMessage()
                .getContextualProperty(FaultListener.class.getName()) : null;
    }

    @NotNull
    private String buildErrorMessage(final WebApplicationException exception, final String prefix) {
        // Cause message is preferred
        final Throwable cause = exception.getCause();
        final String message = exception.getMessage();
        final String causeMessage = cause != null ? cause.getMessage() : null;
        if (cause == null || causeMessage == null) {
            return message != null ? prefix + ": " + message : prefix;
        }
        return prefix + ": " + causeMessage;
    }

    @Nonnull
    private Response handleNoContentException(@Nonnull final NoContentException e) {
        LOG.warn("No content exception {}", exceptionToString(e));
        final String message = e.getMessage() != null ? e.getMessage() : "Empty entity received";
        return Response.status(Response.Status.BAD_REQUEST).entity(message).type(MediaType.TEXT_PLAIN_TYPE).build();
    }

    @Nonnull
    private Response handleEofException(@Nonnull final EofException e) {
        // Connection reset by client, log as warning
        LOG.warn("EofException, connection was reset by client: {}", exceptionToString(e));
        final String message = e.getMessage() != null ? e.getMessage() : "Connection reset by client";
        return Response.status(ExtraStatus.CLIENT_CLOSED_REQUEST).entity(message).type(MediaType.TEXT_PLAIN_TYPE).build();
    }

    @Nonnull
    private Response handleUnknownException(@Nonnull final Throwable e) {
        final String message = e.getMessage() != null ? e.getMessage() : "Unexpected error occurred";
        LOG.error("Unexpected exception", e);
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(message).type(MediaType.TEXT_PLAIN_TYPE).build();
    }

    @Nonnull
    private Response handleAccessDeniedException(@Nonnull final AccessDeniedException e) {
        final String message = e.getMessage() != null ? e.getMessage() : "Access denied";
        LOG.warn("Access denied: {}", exceptionToString(e));
        return Response.status(Response.Status.FORBIDDEN).entity(message).type(MediaType.TEXT_PLAIN_TYPE).build();
    }

    @Nonnull
    private Response handleForbiddenException(@Nonnull final AccessDeniedException e) {
        final String message = e.getMessage() != null ? e.getMessage() : "An action is forbidden";
        LOG.warn("Forbidden: {}", exceptionToString(e));
        return toErrorResponse(Response.Status.FORBIDDEN, "Not enough permissions", message);
    }

    @Nonnull
    private Response handleUnauthorizedException(@Nonnull final AccessDeniedException e) {
        final String message = e.getMessage() != null ? e.getMessage() : "User is unauthorized";
        LOG.warn("Unauthorized: {}", exceptionToString(e));
        return toErrorResponse(Response.Status.UNAUTHORIZED, "Authorization was unsuccessful", message);
    }

    @Nonnull
    private Response handleNotFoundException(@Nonnull final NotFoundException e) {
        final String message = e.getMessage() != null ? e.getMessage() : "Not found";
        LOG.warn("Not found: {}", exceptionToString(e));
        return toErrorResponse(Response.Status.NOT_FOUND, "Not Found", message);
    }

    @Nonnull
    private String exceptionToString(@Nonnull final Throwable e) {
        return "" + e.getClass().getSimpleName() + (e.getMessage() != null ? ": " + e.getMessage() : "");
    }

    public enum ExtraStatus implements Response.StatusType {

        CLIENT_CLOSED_REQUEST(499, "Client Closed Request"),
        UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"),
        ;

        private final int code;
        private final String reason;
        private final Response.Status.Family family;

        ExtraStatus(final int statusCode, final String reasonPhrase) {
            this.code = statusCode;
            this.reason = reasonPhrase;
            this.family = Response.Status.Family.familyOf(statusCode);
        }

        @Override
        public Response.Status.Family getFamily() {
            return family;
        }

        @Override
        public int getStatusCode() {
            return code;
        }

        @Override
        public String getReasonPhrase() {
            return toString();
        }

        @Override
        public String toString() {
            return reason;
        }

    }

}
