/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.causeway.core.runtimeservices.transaction;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.LongAdder;

import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Provider;

import org.jspecify.annotations.NonNull;

import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;

import org.apache.causeway.applib.annotation.PriorityPrecedence;
import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker;
import org.apache.causeway.applib.services.xactn.TransactionId;
import org.apache.causeway.applib.services.xactn.TransactionService;
import org.apache.causeway.applib.services.xactn.TransactionState;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.functional.ThrowingRunnable;
import org.apache.causeway.commons.functional.Try;
import org.apache.causeway.commons.internal.base._NullSafe;
import org.apache.causeway.commons.internal.collections._Lists;
import org.apache.causeway.commons.internal.debug._Probe;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
import org.apache.causeway.core.interaction.session.CausewayInteraction;
import org.apache.causeway.core.runtime.flushmgmt.FlushMgmt;
import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
import org.apache.causeway.core.transaction.events.TransactionCompletionStatus;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

/**
 * Default implementation of {@link TransactionService}, which delegates to Spring's own transaction management
 * framework, such as {@link PlatformTransactionManager}.
 *
 * @implNote This implementation does not yet support more than one {@link PlatformTransactionManager}
 * on the same Spring context. If more than one are discovered, some methods will fail
 * with {@link IllegalStateException}s.
 *
 * @since 2.0 {@index}
 */
@Service
@Named(CausewayModuleCoreRuntimeServices.NAMESPACE + ".TransactionServiceSpring")
@Priority(PriorityPrecedence.MIDPOINT)
@Qualifier("Spring")
@Log4j2
public class TransactionServiceSpring
implements
    TransactionService {

    private final Can<PlatformTransactionManager> platformTransactionManagers;
    private final Provider<InteractionLayerTracker> interactionLayerTrackerProvider;
    private final Can<PersistenceExceptionTranslator> persistenceExceptionTranslators;
    private final ConfigurableListableBeanFactory configurableListableBeanFactory;

    @Inject
    public TransactionServiceSpring(
            final List<PlatformTransactionManager> platformTransactionManagers,
            final List<PersistenceExceptionTranslator> persistenceExceptionTranslators,
            final Provider<InteractionLayerTracker> interactionLayerTrackerProvider,
            final ConfigurableListableBeanFactory configurableListableBeanFactory
    ) {

        this.platformTransactionManagers = Can.ofCollection(platformTransactionManagers);
        log.info("PlatformTransactionManagers: {}", platformTransactionManagers);

        this.configurableListableBeanFactory = configurableListableBeanFactory;

        this.persistenceExceptionTranslators = Can.ofCollection(persistenceExceptionTranslators);
        log.info("PersistenceExceptionTranslators: {}", persistenceExceptionTranslators);

        this.interactionLayerTrackerProvider = interactionLayerTrackerProvider;
    }

    // -- API

    @Override
    public <T> Try<T> callTransactional(final TransactionDefinition def, final Callable<T> callable) {

        var platformTransactionManager = transactionManagerForElseFail(def); // always throws if configuration is wrong

        Try<T> result = null;

        try {
            TransactionStatus txStatus = platformTransactionManager.getTransaction(def);
            registerTransactionSynchronizations(txStatus);

            result = Try.call(() -> {
                        final T callResult = callable.call();

                        if(!FlushMgmt.isAutoFlushSuppressed()) {
                            // we flush here to ensure that the result captures any exception, eg from a declarative constraint violation
                            txStatus.flush();
                        }

                        return callResult;
                    })
                    .mapFailure(ex->translateExceptionIfPossible(ex, platformTransactionManager));

            if(result.isFailure()) {
                // if this is a nested transaction, then the javadoc says it will actually be just a call to
                // setRollbackOnly.
                platformTransactionManager.rollback(txStatus);
            } else {
                platformTransactionManager.commit(txStatus);
            }
        } catch (Exception ex) {

            // return the original failure cause (originating from calling the callable)
            // (so we don't shadow the original failure)
            // return the failure we just caught
            if (result != null && result.isFailure()) {
                return result;
            }

            // otherwise, we thought we had a success, but now we have an exception thrown by either ,
            // the call to rollback or commit above.  We don't need to do anything though; if either of
            // rollback or commit encountered an exception, they will have implicitly called setRollbackOnly (if nested)
            // or just rolled-back if top-level.

            return Try.failure(translateExceptionIfPossible(ex, platformTransactionManager));
        }

        return result;
    }

    private void registerTransactionSynchronizations(final TransactionStatus txStatus) {
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            configurableListableBeanFactory.getBeansOfType(TransactionSynchronization.class)
                    .values()
                    .stream().filter(AopUtils::isAopProxy)  // only the proxies
                    .forEach(TransactionSynchronizationManager::registerSynchronization);
        }
    }

//    @Override
//    public void nextTransaction() {
//
//        var txManager = singletonTransactionManagerElseFail();
//
//        try {
//
//            var txTemplate = new TransactionTemplate(txManager);
//            txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
//
//            // either reuse existing or create new
//            var txStatus = txManager.getTransaction(txTemplate);
//            if(txStatus.isNewTransaction()) {
//                // we have created a new transaction, so we are done
//                return;
//            }
//            // we are reusing an exiting transaction, so end it and create a new one afterwards
//            if(txStatus.isRollbackOnly()) {
//                txManager.rollback(txStatus);
//            } else {
//                //XXX we removed the entire method, because of following subtlety
                  // If the transaction wasn't a new one, omit the commit for proper participation in
//                // the surrounding transaction. If a previous transaction has been suspended to be
//                // able to create a new one, resume the previous transaction after committing the new one.
//                txManager.commit(txStatus);
//           }
//
//            // begin a new transaction
//            txManager.getTransaction(txTemplate);
//
//        } catch (RuntimeException ex) {
//
//            var translatedEx = translateExceptionIfPossible(ex, txManager);
//
//            if(translatedEx instanceof RuntimeException) {
//                throw ex;
//            }
//
//            throw new RuntimeException(ex);
//
//        }
//
//    }

    @Override
    public void flushTransaction() {

        try {

            log.debug("about to flush tx");

            currentTransactionStatus()
                .ifPresent(TransactionStatus::flush);

        } catch (RuntimeException ex) {

            var txManager = singletonTransactionManagerElseFail();

            var translatedEx = translateExceptionIfPossible(ex, txManager);

            if(translatedEx instanceof RuntimeException) {
                throw ex;
            }

            throw new RuntimeException(ex);

        }
    }

    @Override
    public Optional<TransactionId> currentTransactionId() {
        return interactionLayerTrackerProvider.get().getInteractionId()
                .map(uuid->{
                    //XXX get current transaction's persistence context (once we support multiple contexts)
                    var persistenceContext = "";
                    return new TransactionId(uuid, txCounter.get().intValue(), persistenceContext);
                });
    }

    @Override
    public TransactionState currentTransactionState() {

        return currentTransactionStatus()
        .map(txStatus->{

            if(txStatus.isCompleted()) {
                return txStatus.isRollbackOnly()
                        ? TransactionState.ABORTED
                        : TransactionState.COMMITTED;
            }

            return txStatus.isRollbackOnly()
                    ? TransactionState.MUST_ABORT
                    : TransactionState.IN_PROGRESS;

        })
        .orElse(TransactionState.NONE);
    }

    // -- TRANSACTION SEQUENCE TRACKING

    // TODO: this ThreadLocal (as with all thread-locals) should perhaps somehow be managed using
    //  TransactionSynchronizationManager; see its javadoc for more details and look at implementations of
    //  TransactionSynchronization
    private ThreadLocal<LongAdder> txCounter = ThreadLocal.withInitial(LongAdder::new);

    // -- SPRING INTEGRATION

    private PlatformTransactionManager transactionManagerForElseFail(final TransactionDefinition def) {
        if(def instanceof TransactionTemplate) {
            var txManager = ((TransactionTemplate)def).getTransactionManager();
            if(txManager!=null) {
                return txManager;
            }
        }
        return platformTransactionManagers.getSingleton()
                .orElseThrow(()->
                    platformTransactionManagers.getCardinality().isMultiple()
                        ? _Exceptions.illegalState(
                                "Multiple PlatformTransactionManagers are configured, cannot determine which one to use. "
                                + "Instead make sure a PlatformTransactionManager is provided explicitly by passing in a TransactionTemplate (implementation of TransactionDefinition).")
                        : _Exceptions.illegalState("Needs a PlatformTransactionManager."));
    }

    private PlatformTransactionManager singletonTransactionManagerElseFail() {
        return platformTransactionManagers.getSingleton()
                .orElseThrow(()->
                    platformTransactionManagers.getCardinality().isMultiple()
                        ? _Exceptions.illegalState("Multiple PlatformTransactionManagers are configured, "
                                + "cannot reason about which one to use.")
                        : _Exceptions.illegalState("Needs a PlatformTransactionManager."));
    }

    private Optional<TransactionStatus> currentTransactionStatus() {

        var txManager = singletonTransactionManagerElseFail();
        var txTemplate = new TransactionTemplate(txManager);
        txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY);

        // not strictly required, but to prevent stack-trace creation later on
        if(!TransactionSynchronizationManager.isActualTransactionActive()) {
            return Optional.empty();
        }

        // get current transaction else throw an exception
        return Try.call(()->
                //XXX creating stack-traces is expensive
                txManager.getTransaction(txTemplate))
                .getValue();

    }

    private Throwable translateExceptionIfPossible(final Throwable ex, final PlatformTransactionManager txManager) {

        if(ex instanceof DataAccessException) {
            return ex; // nothing to do, already translated
        }

        if(ex instanceof RuntimeException) {

            var translatedEx = persistenceExceptionTranslators.stream()
            //debug .peek(translator->System.out.printf("%s%n", translator.getClass().getName()))
            .map(translator->translator.translateExceptionIfPossible((RuntimeException)ex))
            .filter(_NullSafe::isPresent)
            .findFirst()
            .orElse(null);

            if(translatedEx!=null) {
                return translatedEx;
            }

        }

        return ex;
    }

    /**
     * For use only by {@link org.apache.causeway.core.runtimeservices.session.InteractionServiceDefault}, sets up
     * the initial transaction automatically against all available {@link PlatformTransactionManager}s.
     *
     * @param interaction The {@link CausewayInteraction} object representing the current interaction.
     */
    public void onOpen(final @NonNull CausewayInteraction interaction) {

        txCounter.get().reset();

        if (log.isDebugEnabled()) {
            log.debug("opening on {}", _Probe.currentThreadId());
        }

        if (!platformTransactionManagers.isEmpty()) {
            var onCloseTasks = _Lists.<CloseTask>newArrayList(platformTransactionManagers.size());

            interaction.putAttribute(OnCloseHandle.class, new OnCloseHandle(onCloseTasks));

            platformTransactionManagers.forEach(txManager -> {

                var txDefn = new TransactionTemplate(txManager); // specify the txManager in question
                txDefn.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

                // either participate in existing or create new transaction
                TransactionStatus txStatus = txManager.getTransaction(txDefn);

                if(!txStatus.isNewTransaction()) {
                    // we are participating in an exiting transaction (or testing), nothing to do
                    return;
                }
                registerTransactionSynchronizations(txStatus);

                // we have created a new transaction, so need to provide a CloseTask
                onCloseTasks.add(
                    new CloseTask(
                        txStatus,
                        txManager.getClass().getName(), // info to be used for display in case of errors
                        () -> {
                            _Xray.txBeforeCompletion(interactionLayerTrackerProvider.get(), "tx: beforeCompletion");
                            final TransactionCompletionStatus event;
                            if (txStatus.isRollbackOnly()) {
                                txManager.rollback(txStatus);
                                event = TransactionCompletionStatus.ROLLED_BACK;
                            } else {
                                txManager.commit(txStatus);
                                event = TransactionCompletionStatus.COMMITTED;
                            }
                            _Xray.txAfterCompletion(interactionLayerTrackerProvider.get(), String.format("tx: afterCompletion (%s)", event.name()));

                            txCounter.get().increment();
                        }
                    )
                );
            });
        }
    }

    /**
     * For use only by {@link org.apache.causeway.core.runtimeservices.session.InteractionServiceDefault}, if
     * {@link org.apache.causeway.applib.services.iactnlayer.InteractionService#run(InteractionContext, ThrowingRunnable)}
     * or {@link org.apache.causeway.applib.services.iactnlayer.InteractionService#call(InteractionContext, Callable)}
     * (or their various overloads) result in an exception.
     *
     * @param interaction The {@link CausewayInteraction} object representing the current interaction.
     */
    public void requestRollback(final @NonNull CausewayInteraction interaction) {
        Optional.ofNullable(interaction.getAttribute(OnCloseHandle.class))
                .ifPresent(OnCloseHandle::requestRollback);
    }

    /**
     * For use only by {@link org.apache.causeway.core.runtimeservices.session.InteractionServiceDefault}, to close the
     * transaction initially set up in {@link #onOpen(CausewayInteraction)} against all configured
     * {@link PlatformTransactionManager}s.
     *
     * @param interaction The {@link CausewayInteraction} object representing the current interaction.
     */
    public void onClose(final @NonNull CausewayInteraction interaction) {
        if (log.isDebugEnabled()) {
            log.debug("closing on {}", _Probe.currentThreadId());
        }

        if (!platformTransactionManagers.isEmpty()) {
            Optional.ofNullable(interaction.getAttribute(OnCloseHandle.class))
                    .ifPresent(OnCloseHandle::runOnCloseTasks);
        }

        txCounter.remove(); //XXX not tested yet: can we be certain that no txCounter.get() is called afterwards?
    }

    private record CloseTask(
            @NonNull TransactionStatus txStatus,
            @NonNull String onErrorInfo,
            @NonNull ThrowingRunnable runnable) {
    }

    @RequiredArgsConstructor
    private static class OnCloseHandle {
        private final @NonNull List<CloseTask> onCloseTasks;
        void requestRollback() {
            onCloseTasks.forEach(onCloseTask->{
                onCloseTask.txStatus.setRollbackOnly();
            });
        }
        void runOnCloseTasks() {
            onCloseTasks.forEach(onCloseTask->{

                try {
                    onCloseTask.runnable().run();
                } catch(final Throwable ex) {
                    // ignore
                    log.error(
                            "failed to close transactional boundary using transaction-manager {}; "
                                    + "continuing to avoid memory leakage",
                            onCloseTask.onErrorInfo(),
                            ex);
                }
            });
        }
    }
}
