package com.atlassian.bitbucket.mesh.transaction;

import com.atlassian.bitbucket.mesh.execution.ExecutionContext;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;

public interface TransactionManager {

    /**
     * @param listener to be invoked when the transaction {@link TransactionState#isFinished() completes}
     * @return {@code true} if a transaction was active and the listener was added to it; otherwise {@code false}
     */
    boolean addListener(@Nonnull TransactionListener listener);

    /**
     * Ensures a transaction is active and then calls the provided {@code callback}. If a transaction was already
     * active, this is equivalent to directly calling the {@code callback}.
     * <p>
     * If no transaction was active, a new transaction is started, the {@code callback} is invoked and finally,
     * the transaction is closed. If the {@code callback} throws an exception, the transaction is rolled back,
     * but only if a new transaction was started.
     * <p>
     * If a transaction was already active, the transaction is <em>NOT</em> closed at the end of this {@code execute}
     * invocation. Likewise, if the provided callback throws when no new transaction was started, the active transaction
     * is <em>NOT</em> rolled back. If the exception bubbles up far enough, the transaction will be rolled back when
     * it reaches the transaction boundary where the transaction was started.
     *
     * @param callback the callback to invoke from a transactional context
     * @param <T> the return type of the callback
     * @return the value returned by the callback
     */
    <T> T execute(@Nonnull Function<? super Transaction, T> callback);

    /**
     * Starts a new transaction that <em>DOES NOT</em> coordinate the transactional operations with other nodes. This
     * should only be used when initializing or repairing a repository replica.
     * <p>
     * If a transaction is already active, that transaction is temporarily suspended for the duration of this method
     * call. With the local-only transaction active, the {@code callback} is invoked and finally, the transaction is
     * closed. If the {@code callback} throws an exception, the transaction is rolled back.
     * <p>
     * If a transaction was already active, that transaction is restored when the method completes, irrespective of
     * the outcome of the local-only transaction.
     *
     * @param callback the callback to invoke from a transactional context
     * @param <T> the return type of the callback
     * @return the value returned by the callback
     */
    <T> T executeLocalOnly(@Nonnull Function<? super Transaction, T> callback);

    /**
     * Starts a new transaction that <em>DOES NOT</em> coordinate the transactional operations with other nodes. This
     * should only be used when initializing or repairing a repository replica.
     * <p>
     * If a transaction is already active, that transaction is temporarily suspended for the duration of this method
     * call. With the local-only transaction active, the {@code callback} is invoked and finally, the transaction is
     * closed. If the {@code callback} throws an exception, the transaction is rolled back.
     * <p>
     * If a transaction was already active, that transaction is restored when the method completes, irrespective of
     * the outcome of the local-only transaction.
     *
     * @param callback the callback to invoke from a transactional context
     */
    void executeLocalOnlyWithoutResult(@Nonnull Consumer<? super Transaction> callback);

    /**
     * Ensures a transaction is active and then calls the provided {@code callback}. If a transaction was already
     * active, this is equivalent to directly calling the {@code callback}.
     * <p>
     * If no transaction was active, a new transaction is started, the {@code callback} is invoked and finally,
     * the transaction is closed. If the {@code callback} throws an exception, the transaction is rolled back,
     * but only if a new transaction was started.
     * <p>
     * If a transaction was already active, the transaction is <em>NOT</em> closed at the end of this {@code execute}
     * invocation. Likewise, if the provided callback throws when no new transaction was started, the active transaction
     * is <em>NOT</em> rolled back. If the exception bubbles up far enough, the transaction will be rolled back when
     * it reaches the transaction boundary where the transaction was started.
     *
     * @param callback the callback to invoke from a transactional context
     */
    void executeWithoutResult(@Nonnull Consumer<? super Transaction> callback);

    /**
     * @return the transaction active in the current {@link ExecutionContext}, or {@link Optional#empty()} if there
     * is no active transaction or if no execution context is active.
     */
    @Nonnull
    Optional<Transaction> getCurrentTransaction();

    /**
     * Local transactional operations should call this method when they are fully prepared and ready to commit;
     * <em>after</em> the system has validated the operation parameters and the system state, has acquired or locked
     * any required resources and is ready to persist the the effects of the operation and make them visible.
     * <p>
     * If there is no {@link #getCurrentTransaction() active transaction} when this method is called, but there is
     * an active execution context, this method will start a new transaction and prepare said transaction.
     * <p>
     * If there is no active execution context, a new transaction is started, prepared and immediately closed.
     * <p>
     * If there is a {@link #getCurrentTransaction() active transaction}, calling this method is equivalent to calling
     * {@link Transaction#prepared(PreparedAction)} on that active transaction.
     * <p>
     * Assuming an active transaction exists or one is created, the {@link TransactionLeader}, which is either
     * local or remote, will coordinate with the replicas and return the outcome of the coordination. If the result is
     * {@link PrepareResult#isSuccess() successful}, the caller MUST commit the transaction. If the result is not
     * successful, the caller MUST roll back the transaction.
     *
     * @param operation  the transactional operation ready to update
     * @return the transaction coordination result
     */
    @Nonnull
    PrepareResult prepared(@Nonnull PreparedAction operation);

    /**
     * Starts and returns a transaction. This will be a coordinating transaction if this node is the transaction
     * {@link StartTransactionParameters#getLeader() leader}; otherwise a replicating transaction.
     *
     * @param parameters parameters that describe the transaction to create
     * @return the created transaction
     * @throws IllegalStateException if a transaction is already active
     */
    @Nonnull
    Transaction start(@Nonnull StartTransactionParameters parameters);

    /**
     * Starts and returns a coordinating transaction (where this node is the leader).
     *
     * @return the created transaction
     * @throws IllegalStateException if a transaction is already active
     */
    @Nonnull
    Transaction start();

    /**
     * Performs transaction-like coordination that acquires a write lock for the specified repository, compares
     * the version and hashes of each of the replicas and determines which replica(s) are up to date and which
     * should be marked {@link com.atlassian.bitbucket.mesh.repository.ReplicaState#INCONSISTENT} and scheduled
     * for repair.
     * <p>
     * If any replicas are found to be missing or inconsistent, they will be reported as inconsistent to the
     * control plane.
     *
     * @param repositoryId the ID of the repository to verify consistency of
     * @return a summary describing the state of all replicas, or {@code null} if the provided {@code repositoryId}
     *         is not a managed repository
     * @throws com.atlassian.bitbucket.mesh.git.exception.FailedPreconditionException if this node cannot determine
     *         the peer nodes for the provided {@code repositoryId} - e.g. because the partition is not included in
     *         this node's topology
     * @since 1.2
     */
    @Nullable
    RepositoryConsistencySummary verifyConsistency(@Nonnull String repositoryId);
}
