package com.atlassian.stash.internal.johnson;

import com.atlassian.johnson.event.Event;
import com.atlassian.johnson.support.EventExceptionTranslator;
import com.atlassian.stash.internal.cluster.NodePassivationException;
import com.atlassian.stash.internal.db.UnsupportedDatabaseException;
import com.atlassian.stash.internal.home.HomeLockDeniedException;
import com.atlassian.stash.internal.home.HomeLockFailedException;
import com.atlassian.stash.internal.home.UnsupportedDirectoryOverrideException;
import com.atlassian.stash.internal.jdbc.UnsupportedJdbcUrlException;
import com.atlassian.stash.internal.johnson.handler.EventDetails;
import com.atlassian.stash.internal.johnson.handler.bean.*;
import com.atlassian.stash.internal.johnson.handler.exception.*;
import com.google.common.collect.ImmutableMap;
import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanCreationException;

import javax.annotation.Nonnull;
import java.util.Map;

/**
 * Implements Johnson's {@code EventExceptionTranslator} to provide application-specific help for errors encountered
 * trying to start SpringMVC.
 *
 * @since 3.2
 */
public class DefaultEventExceptionTranslator implements EventExceptionTranslator {

    private static final Map<String, Class<? extends BeanFailureHandler>> BEAN_TO_HANDLER_MAP
            = new ImmutableMap.Builder<String, Class<? extends BeanFailureHandler>>()
                .put("configurationMigration", ConfigurationMigrationHandler.class)
                .put("dataSourcePrototype", DataSourcePrototypeFailureHandler.class)
                .put("liquibase", LiquibaseFailureHandler.class)
                .put("homeDirectoryMigration", HomeDirectoryMigrationHandler.class)
                .put("homeDirectoryDetails", HomeDirectoryDetailsFailureHandler.class)
                .put("sessionFactory", SessionFactoryFailureHandler.class)
                .build();
    /**
     * Maps specific exception types to a handler for producing {@link EventDetails}.
     */
    private static final Map<Class<? extends Throwable>, Class<? extends ExceptionFailureHandler>> EXCEPTION_TO_HANDLER_MAP
            = ImmutableMap.<Class<? extends Throwable>, Class<? extends ExceptionFailureHandler>>builder()
                .put(PoolInitializationException.class, ConnectionFailureHandler.class)
                .put(HomeLockDeniedException.class, HomeLockedHandler.class)
                .put(HomeLockFailedException.class, HomeLockFailedHandler.class)
                .put(NodePassivationException.class, NodePassivationHandler.class)
                .put(UnsupportedDirectoryOverrideException.class, UnsupportedDirectoryOverrideHandler.class)
                .put(UnsupportedDatabaseException.class, UnsupportedDatabaseHandler.class)
                .put(UnsupportedJdbcUrlException.class, UnsupportedJdbcUrlHandler.class)
                .build();

    public DefaultEventExceptionTranslator() {
    }

    @Override
    public Event translate(@Nonnull Throwable t) {
        EventDetails details = loadDetails(t);

        return details == null ? null : details.toEvent();
    }

    private static EventDetails invokeHandlers(Throwable root, BeanCreationException failedBean) {
        EventDetails details = null;

        //If the selected root exception has specific handling, that gets the first pass at it
        Class<? extends ExceptionFailureHandler> exceptionHandlerClass = EXCEPTION_TO_HANDLER_MAP.get(root.getClass());
        if (exceptionHandlerClass != null) {
            ExceptionFailureHandler handler = BeanUtils.instantiate(exceptionHandlerClass);

            details = handler.handle(root);
        }

        //If there was no specific exception handler, or if the handler was not able to process the exception, check
        //if the failed bean has specific handling
        if (details == null && failedBean != null) {
            Class<? extends BeanFailureHandler> beanHandlerClass = BEAN_TO_HANDLER_MAP.get(failedBean.getBeanName());
            if (beanHandlerClass != null) {
                details = BeanUtils.instantiate(beanHandlerClass).handle(failedBean);
            }
        }

        return details;
    }

    private static EventDetails loadDetails(Throwable root) {
        BeanCreationException failedBean = null;
        while (root.getCause() != null) {
            if (root instanceof BeanCreationException) {
                //If beans failed creation, find the first bean to fail (since this is the bean that caused the entire
                //context to fail, but it may have caused any number of other beans to fail along the way)
                failedBean = (BeanCreationException) root;
            }
            if (EXCEPTION_TO_HANDLER_MAP.containsKey(root.getClass())) {
                break;
            }
            root = root.getCause();
        }

        return invokeHandlers(root, failedBean);
    }
}
