package me.alidg.errors.conf;

import me.alidg.errors.*;
import me.alidg.errors.adapter.DefaultHttpErrorAttributesAdapter;
import me.alidg.errors.adapter.HttpErrorAttributesAdapter;
import me.alidg.errors.fingerprint.UuidFingerprintProvider;
import me.alidg.errors.handlers.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.web.context.WebApplicationContext;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET;

/**
 * Auto-configuration responsible for registering a {@link WebErrorHandlers} filled with
 * builtin, custom and default fallback {@link WebErrorHandler}s.
 *
 * <h3>Builtin Web Error Handlers</h3>
 * Built in {@link WebErrorHandler}s are those we provided out of the box. It's highly recommended
 * to use these implementations with most possible priority, as we did in this auto-configuration.
 *
 * <h3>Custom Web Error Handlers</h3>
 * You can also provide your own custom {@link WebErrorHandler} implementations. Just implement the
 * {@link WebErrorHandler} interface and register it as Spring Bean. If you're willing to prioritize
 * your implementations, use the Spring {@link org.springframework.core.annotation.Order} annotation
 * to specify the priority requirements of the bean.
 * <p><b>Please Note that</b> your custom handlers would be registered after the built-in ones. If you're
 * not OK with that, you can always discard this auto-configuration by registering your own
 * {@link WebErrorHandlers} factory bean.</p>
 *
 * <h3>Default Fallback Web Error Handler</h3>
 * While handling a particular exception, each registered {@link WebErrorHandler} in {@link WebErrorHandlers}
 * would be consulted one after another (Depending on their priority). If all of the registered handlers
 * refuse to handle the exception, then a default fallback {@link WebErrorHandler} should handle the exception.
 * By default, {@link WebErrorHandlers} use the {@link me.alidg.errors.handlers.LastResortWebErrorHandler} as the
 * default handler. You can replace this handler by providing a {@link WebErrorHandler} and register it with
 * a bean named {@code defaultWebErrorHandler}.
 *
 * @author Ali Dehghani
 * @see WebErrorHandler
 * @see WebErrorHandlers
 * @see me.alidg.errors.handlers.LastResortWebErrorHandler
 */
@ConditionalOnWebApplication
@EnableConfigurationProperties(ErrorsProperties.class)
public class ErrorsAutoConfiguration {

    /**
     * Built-in {@link WebErrorHandler}s which would be on top of all other {@link WebErrorHandler}s
     * and will be consulted before any other implementations for error handling.
     */
    private static final List<WebErrorHandler> BUILT_IN_HANDLERS = Arrays.asList(
        new SpringValidationWebErrorHandler(),
        new ConstraintViolationWebErrorHandler(),
        new AnnotatedWebErrorHandler(),
        new TypeMismatchWebErrorHandler(),
        new MultipartWebErrorHandler()
    );

    /**
     * Registers a bean of type {@link WebErrorHandlers} (If not provided by the user) filled with a set of
     * built-in {@link WebErrorHandler}s, a set of custom {@link WebErrorHandler}s and a default fallback
     * {@link WebErrorHandler}.
     *
     * @param messageSource                 Will be used for error code to error message translation.
     * @param customHandlers                Optional custom {@link WebErrorHandler}s.
     * @param defaultWebErrorHandler        A default {@link WebErrorHandler} to be used as the fallback error handler.
     * @param exceptionRefiner              To refine exceptions before handling them.
     * @param exceptionLogger               To log exceptions.
     * @param webErrorHandlerPostProcessors Post processors to execute after we handled the exception.
     * @param fingerprintProvider           To generate unique fingerprints for handled exceptions.
     * @param errorsProperties              Configuration properties bean.
     * @param context                       To tell Servlet or Reactive stacks apart.
     * @return The expected {@link WebErrorHandlers}.
     */
    @Bean
    @ConditionalOnMissingBean
    public WebErrorHandlers webErrorHandlers(MessageSource messageSource,
                                             @Autowired(required = false) List<WebErrorHandler> customHandlers,
                                             @Qualifier("defaultWebErrorHandler") @Autowired(required = false) WebErrorHandler defaultWebErrorHandler,
                                             ExceptionRefiner exceptionRefiner,
                                             ExceptionLogger exceptionLogger,
                                             @Autowired(required = false) List<WebErrorHandlerPostProcessor> webErrorHandlerPostProcessors,
                                             FingerprintProvider fingerprintProvider,
                                             ErrorsProperties errorsProperties,
                                             ApplicationContext context) {

        List<WebErrorHandler> handlers = new ArrayList<>(BUILT_IN_HANDLERS);
        if (isServletApplication(context)) handlers.add(new ServletWebErrorHandler());

        if (customHandlers != null && !customHandlers.isEmpty()) {
            customHandlers.remove(defaultWebErrorHandler);
            customHandlers.removeIf(Objects::isNull);
            customHandlers.sort(AnnotationAwareOrderComparator.INSTANCE);

            handlers.addAll(customHandlers);
        }

        WebErrorHandlersBuilder builder = WebErrorHandlers
            .builder(messageSource)
            .withErrorsProperties(errorsProperties)
            .withErrorHandlers(handlers)
            .withExceptionRefiner(exceptionRefiner)
            .withExceptionLogger(exceptionLogger)
            .withFingerprintProvider(fingerprintProvider);

        if (defaultWebErrorHandler != null) builder.withDefaultWebErrorHandler(defaultWebErrorHandler);
        if (webErrorHandlerPostProcessors != null) builder.withPostProcessors(webErrorHandlerPostProcessors);

        return builder.build();
    }

    /**
     * In the absence of a bean of type {@link HttpErrorAttributesAdapter}, registers the default
     * implementation of {@link HttpErrorAttributesAdapter} as a bean, to adapt our
     * {@link me.alidg.errors.HttpError} to Spring's {@link ErrorAttributes} abstraction.
     *
     * @param errorsProperties Configuration properties bean.
     * @return The to-be-registered {@link HttpErrorAttributesAdapter}.
     */
    @Bean
    @ConditionalOnBean(WebErrorHandlers.class)
    @ConditionalOnMissingBean(HttpErrorAttributesAdapter.class)
    public HttpErrorAttributesAdapter httpErrorAttributesAdapter(ErrorsProperties errorsProperties) {
        return new DefaultHttpErrorAttributesAdapter(errorsProperties);
    }

    /**
     * Registers a {@link WebErrorHandler} bean to handle Spring Security specific exceptions when
     * Spring Security's jar file is present on the classpath.
     *
     * @return A web error handler for Spring Security exceptions.
     */
    @Bean
    @ConditionalOnBean(WebErrorHandlers.class)
    @ConditionalOnClass(name = "org.springframework.security.access.AccessDeniedException")
    public SpringSecurityWebErrorHandler springSecurityWebErrorHandler() {
        return new SpringSecurityWebErrorHandler();
    }

    /**
     * Registers a {@link WebErrorHandler} to handle new Servlet exceptions defined in Spring Framework 5.1.
     * This handler would be registered iff we're using Spring Boot 2.1.0+.
     *
     * @return A web error handler to handle a few new servlet exceptions.
     */
    @Bean
    @ConditionalOnBean(WebErrorHandlers.class)
    @ConditionalOnWebApplication(type = SERVLET)
    @ConditionalOnClass(name = "org.springframework.web.bind.MissingRequestHeaderException")
    public MissingRequestParametersWebErrorHandler missingRequestParametersWebErrorHandler() {
        return new MissingRequestParametersWebErrorHandler();
    }

    /**
     * Registers a handler expert at handling all possible
     * {@link org.springframework.web.server.ResponseStatusException}s.
     *
     * @return A web error handler to handle a set of brand new exceptions defined in Spring 5.x.
     */
    @Bean
    @ConditionalOnBean(WebErrorHandlers.class)
    @ConditionalOnClass(name = "org.springframework.web.server.ResponseStatusException")
    public ResponseStatusWebErrorHandler responseStatusWebErrorHandler() {
        return new ResponseStatusWebErrorHandler();
    }

    /**
     * Registers an empty {@link ExceptionRefiner} in the absence of a custom refiner.
     *
     * @return A no-op exception refiner.
     */
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnBean(WebErrorHandlers.class)
    public ExceptionRefiner exceptionRefiner() {
        return ExceptionRefiner.NoOp.INSTANCE;
    }

    /**
     * Registers an empty {@link ExceptionLogger} in the absence of a custom logger.
     *
     * @return A no-op exception logger.
     */
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnBean(WebErrorHandlers.class)
    public ExceptionLogger exceptionLogger() {
        return ExceptionLogger.NoOp.INSTANCE;
    }

    /**
     * Registers a very simple UUID based {@link FingerprintProvider} in the absence of a custom provider.
     *
     * @return The UUID based fingerprint provider.
     */
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnBean(WebErrorHandlers.class)
    public FingerprintProvider fingerprintProvider() {
        return new UuidFingerprintProvider();
    }

    /**
     * @param context The application context.
     * @return {@code true} if this a traditional web application, not a reactive one.
     */
    private boolean isServletApplication(ApplicationContext context) {
        return context instanceof WebApplicationContext;
    }
}