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

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.function.Consumer;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.support.TransactionTemplate;

import ru.yandex.qe.dispenser.domain.hierarchy.Session;
import ru.yandex.qe.dispenser.domain.util.ApplicationContextProvider;
import ru.yandex.qe.dispenser.domain.util.ExRunnable;

public enum TransactionWrapper {
    INSTANCE;

    private static final Logger LOG = LoggerFactory.getLogger(TransactionWrapper.class);

    @NotNull
    private final List<Consumer<TransactionTemplate>> transactionListeners = new ArrayList<>();

    @NotNull
    public TransactionTemplate createDefaultTemplate() {
        final PlatformTransactionManager manager = ApplicationContextProvider.getBean("txManager", PlatformTransactionManager.class);
        final TransactionTemplate template = manager != null ? new TransactionTemplate(manager) : new TransactionTemplate();
        template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        return template;
    }

    public void execute(@NotNull final ExRunnable action) throws TransactionException {
        execute(() -> {
            action.run();
            return null;
        });
    }

    @Nullable
    public <T> T execute(@NotNull final Callable<T> action) throws TransactionException {
        return execute(createDefaultTemplate(), action);
    }

    @Nullable
    public <T> T execute(@NotNull final TransactionTemplate template, @NotNull final Callable<T> action) throws TransactionException {
        if (template.getTransactionManager() == null) {
            try {
                return action.call();
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return template.execute(status -> {
            try {
                transactionListeners.forEach(listener -> listener.accept(template));
            } catch (@SuppressWarnings("ProhibitedExceptionCaught") Throwable t) {
                LOG.error("Transaction listener failed with error!", t);
            }
            try {
                return action.call();
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new RuntimeException(e);
            } catch (@SuppressWarnings("ProhibitedExceptionCaught") Throwable t) {
                status.setRollbackOnly();
                throw t;
            } finally {
                //noinspection ThrowableResultOfMethodCallIgnored
                if (Session.ERROR.get() != null) {
                    status.setRollbackOnly();
                }
            }
        });
    }

    public boolean addTransactionListener(@NotNull final Consumer<TransactionTemplate> listener) {
        return transactionListeners.add(listener);
    }

    public boolean removeTransactionListener(@NotNull final Consumer<TransactionTemplate> listener) {
        return transactionListeners.remove(listener);
    }
}
