package com.atlassian.stash.internal.rest.exception;

import com.atlassian.bitbucket.*;
import com.atlassian.bitbucket.auth.AuthenticationException;
import com.atlassian.bitbucket.auth.AuthenticationRequiredException;
import com.atlassian.bitbucket.auth.CaptchaAuthenticationException;
import com.atlassian.bitbucket.license.IncompatibleLicenseChangeException;
import com.atlassian.bitbucket.license.InvalidLicenseException;
import com.atlassian.bitbucket.license.LicenseLimitException;
import com.atlassian.bitbucket.license.LimitExceededException;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.pull.DuplicatePullRequestException;
import com.atlassian.bitbucket.pull.InvalidPullRequestReviewersException;
import com.atlassian.bitbucket.pull.PullRequestMergeVetoedException;
import com.atlassian.bitbucket.pull.UnmodifiablePullRequestRoleException;
import com.atlassian.bitbucket.repository.ConflictingRefNameException;
import com.atlassian.bitbucket.rest.RestEntityOutOfDateErrorMessage;
import com.atlassian.bitbucket.rest.RestErrorMessage;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.RestRequestCanceledMessage;
import com.atlassian.bitbucket.rest.annotation.JsonSurrogate;
import com.atlassian.bitbucket.rest.auth.RestAuthenticationRequiredErrorMessage;
import com.atlassian.bitbucket.rest.exception.UnhandledExceptionMapper;
import com.atlassian.bitbucket.rest.pull.*;
import com.atlassian.bitbucket.rest.repository.RestDuplicateRefMessage;
import com.atlassian.bitbucket.rest.repository.RestInvalidRefNameMessage;
import com.atlassian.bitbucket.rest.scm.RestMergeErrorMessage;
import com.atlassian.bitbucket.rest.util.ResponseFactory;
import com.atlassian.bitbucket.rest.util.RestUtils;
import com.atlassian.bitbucket.scm.CommandUsageException;
import com.atlassian.bitbucket.scm.MergeException;
import com.atlassian.bitbucket.throttle.ResourceBusyException;
import com.atlassian.bitbucket.util.PageStartOutOfBoundsException;
import com.atlassian.bitbucket.validation.ArgumentValidationException;
import com.atlassian.stash.internal.rest.renderer.AggregateBuilder;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import java.util.Map;

import static com.google.common.base.Preconditions.checkArgument;
import static javax.ws.rs.core.Response.Status.*;

/**
 * Maps exceptions from the {@link ServiceException} class hierarchy to JSON REST responses, choosing a status code for
 * the response based on the exception type.
 * <p>
 * Starting from 2.2, plugins with REST resources do not have to extend this class anymore. Plugins with REST resources
 * only need to extend the {@link UnhandledExceptionMapper} class.
 */
class ServiceExceptionMapper implements ExceptionMapper<ServiceException> {

    public static final Map<Class<? extends ServiceException>, Response.Status> STATUS_MAP;

    private static final Logger log = LoggerFactory.getLogger(ServiceExceptionMapper.class);

    static {
        STATUS_MAP = ImmutableMap.<Class<? extends ServiceException>, Response.Status>builder()
                .put(ArgumentValidationException.class, BAD_REQUEST)
                .put(AuthenticationException.class, UNAUTHORIZED)
                .put(AuthenticationRequiredException.class, OK)
                .put(AuthorisationException.class, UNAUTHORIZED)
                .put(CaptchaAuthenticationException.class, UNAUTHORIZED)
                .put(CommandUsageException.class, BAD_REQUEST)
                .put(ConflictingRefNameException.class, CONFLICT)
                .put(DuplicatePullRequestException.class, CONFLICT)
                .put(EntityOutOfDateException.class, CONFLICT)
                .put(FeatureDisabledException.class, CONFLICT)
                .put(ForbiddenException.class, FORBIDDEN)
                .put(IllegalEntityStateException.class, CONFLICT)
                .put(IntegrityException.class, CONFLICT)
                .put(InvalidPullRequestReviewersException.class, CONFLICT)
                .put(InvalidLicenseException.class, BAD_REQUEST)
                .put(LicenseLimitException.class, FORBIDDEN)
                .put(LimitExceededException.class, BAD_REQUEST)
                .put(IncompatibleLicenseChangeException.class, CONFLICT)
                .put(MergeException.class, CONFLICT)
                .put(NoSuchEntityException.class, NOT_FOUND)
                .put(PageStartOutOfBoundsException.class, NOT_FOUND)
                .put(PullRequestMergeVetoedException.class, CONFLICT)
                .put(RequestCanceledException.class, BAD_REQUEST)
                .put(ResourceBusyException.class, SERVICE_UNAVAILABLE)
                .put(UnmodifiablePullRequestRoleException.class, CONFLICT)
                .build();
    }

    private final AggregateBuilder exceptionMessageBuilder;

    public ServiceExceptionMapper(NavBuilder navBuilder) {
        exceptionMessageBuilder = new AggregateBuilder(navBuilder);

        registerSurrogate(RestAuthenticationRequiredErrorMessage.class);
        registerSurrogate(RestCaptchaAuthenticationException.class);
        registerSurrogate(RestDuplicatePullRequestErrorMessage.class);
        registerSurrogate(RestEntityOutOfDateErrorMessage.class);
        registerSurrogate(RestErrorMessage.class);
        registerSurrogate(RestInvalidPullRequestReviewersErrorMessage.class);
        registerSurrogate(RestMergeErrorMessage.class);
        registerSurrogate(RestPullRequestMergeVetoedErrorMessage.class);
        registerSurrogate(RestPullRequestOutOfDateErrorMessage.class);
        registerSurrogate(RestRequestCanceledMessage.class);
        registerSurrogate(RestDuplicateRefMessage.class);
        registerSurrogate(RestInvalidRefNameMessage.class);
    }

    @Override
    public Response toResponse(ServiceException e) {
        //We can guarantee the result of buildFor(...) is both non-null and an instance of RestMessage
        //because ServiceException is mapped by RestMessage as a fallback and the other surrogates are
        //sub-classes of RestMessage, as enforced by registerSurrogate above
        RestErrors errors = new RestErrors((RestErrorMessage) exceptionMessageBuilder.buildFor(e));
        Response response = createResponseBuilder(e)
                .entity(errors)
                .type(RestUtils.APPLICATION_JSON_UTF8)
                .build();

        log.debug("Mapping ServiceException to REST response " + response.getStatus(), e);
        return response;
    }

    private void registerSurrogate(Class<? extends RestErrorMessage> surrogateClass) {
        checkArgument(surrogateClass.isAnnotationPresent(JsonSurrogate.class),
                surrogateClass.getName() + " is not annotated as a surrogate");
        checkArgument(RestErrorMessage.class.isAssignableFrom(surrogateClass),
                surrogateClass.getName() + " must derive from RestErrorMessage");
        checkArgument(ServiceException.class.isAssignableFrom(surrogateClass.getAnnotation(JsonSurrogate.class).value()),
                surrogateClass.getAnnotation(JsonSurrogate.class).value().getName() + " must derive from ServiceException");

        exceptionMessageBuilder.registerSurrogate(surrogateClass);
    }

    /**
     * Creates a {@code ResponseBuilder} from the provided {@code ServiceException}.
     * <p>
     * The status code for the returned builder is looked up in {@link #STATUS_MAP}. Lookups begin with the exact class
     * of the provided exception and then work their way up its inheritance hierarchy to {@code ServiceException}. If
     * a match is found at any level, it is used. If no match is found from the exception hierarchy, a default server
     * error status (500) is used.
     *
     * @param e the exception to create a builder for
     * @return a builder with a status code appropriate for the provided exception
     */
    @SuppressWarnings("SuspiciousMethodCalls")
    private Response.ResponseBuilder createResponseBuilder(ServiceException e) {
        //Walk up the exception's class hierarchy until it we traverse past ServiceException (at which point the class
        //could no longer possibly be in the map). The most specific mapping is preferred, but a more generic match at
        //any point in the exception's hierarchy will be accepted
        Class<?> clazz = e.getClass();
        while (ServiceException.class.isAssignableFrom(clazz)) {
            Response.Status status = STATUS_MAP.get(clazz);
            if (status != null) {
                return ResponseFactory.status(status);
            }

            clazz = clazz.getSuperclass();
        }

        //If we make it here, there is no mapping for the provided exception, or any of its ServiceException-derived
        //superclasses, so return a default server error status.
        return ResponseFactory.serverError();
    }
}
