package com.atlassian.bitbucket.mesh.transaction;

import com.atlassian.bitbucket.mesh.network.MeshNode;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcStreamChunk;
import com.atlassian.bitbucket.mesh.util.SatiableConsumer;

import javax.annotation.Nonnull;
import java.io.Closeable;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Supplier;

import static java.util.Objects.requireNonNull;
import static java.util.Optional.empty;

public interface Transaction extends Closeable {

    /**
     * Constructs a transaction ID of the form {@code n/ae127123/tx/9236187ce}:
     *
     *<ul>
     *     <li>{@code n/ae127123} {@link MeshNode#getId() identifies} the transaction leader</li>
     *     <li>{@code tx/9236187ce} identifies the transaction uniquely to that node</li>
     *</ul>
     *
     * @param leaderId the ID of the transaction leader
     * @param localId the local transaction ID
     * @return the fully qualified transaction ID
     */
    @Nonnull
    static String formatId(@Nonnull String leaderId, @Nonnull String localId) {
        return "n/" + requireNonNull(leaderId, "leaderId") + "/tx/" + requireNonNull(localId, "localId");
    }

    /**
     * @param listener to be invoked when the transaction {@link TransactionState#isFinished() completes}
     */
    void addListener(@Nonnull TransactionListener listener);

    /**
     * Commits the transaction unless {@link #setRollbackOnly()} has been called.
     */
    @Override
    void close();

    /**
     * @return the fully qualified transaction ID
     */
    @Nonnull
    String getId();

    /**
     * @return the directory where the transaction's staged objects are stored, if any
     */
    @Nonnull
    Optional<Path> getIncomingDir();

    /**
     * @return the transaction leader, which may be the local node
     */
    @Nonnull
    MeshNode getLeader();

    /**
     * @return the current state of the transaction
     */
    @Nonnull
    TransactionState getState();

    /**
     * @return {@code true} if the transaction has been {@link #setRollbackOnly() marked} for rollback
     */
    boolean isRollbackOnly();

    /**
     * Starts a new writer to send a pack of objects to follower transactions. Only defined for transactions
     * with a local transaction leader.
     * <p>
     * The caller should use the returned {@link SatiableConsumer} to send the pack contents to the followers. The
     * returned {@link SatiableConsumer} returns {@code true} when all follower transactions have signalled that
     * they're not interested in more data.
     *
     * @param repository the repository for which a pack is to be sent
     * @return a consumer that replicates the stream chunks to all follower transactions
     */
    @Nonnull
    default Optional<SatiableConsumer<RpcStreamChunk>> newPackWriter(@Nonnull Repository repository) {
        return empty();
    }

    /**
     * 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>
     * The {@link TransactionLeader}, which is either local or remote, will then 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 action the transactional action ready to update
     * @return the transaction coordination result
     */
    @Nonnull
    PrepareResult prepared(@Nonnull PreparedAction action);

    /**
     * Executes the provided {@code action}, temporarily intercepting any exception that is thrown. If an exception is
     * thrown, the transaction is rolled back if that is still allowed. If the leader has already instructed this
     * transaction to commit, the exception is recorded and {@link TransactionLeader#onCommitted(Throwable) provided
     * to the leader} when the transaction {@link #close() closes}.
     * <p>
     * If the transaction is already ROLLING_BACK, COMMITTED or ROLLED_BACK when the exception is caught, nothing
     * happens; the transaction is already complete or is already rolling back.
     * <p>
     * Any exception that is thrown is rethrown when the method completes
     *
     * @param action the transaction action to execute
     */
    void rollBackIfThrows(@Nonnull Runnable action);

    /**
     * Executes the provided {@code action}, temporarily intercepting any exception that is thrown. If an exception is
     * thrown, the transaction is rolled back if that is still allowed. If the leader has already instructed this
     * transaction to commit, the exception is recorded and {@link TransactionLeader#onCommitted(Throwable) provided
     * to the leader} when the transaction {@link #close() closes}.
     * <p>
     * If the transaction is already ROLLING_BACK, COMMITTED or ROLLED_BACK when the exception is caught, nothing
     * happens; the transaction is already complete or is already rolling back.
     * <p>
     * Any exception that is thrown is rethrown when the method completes
     *
     * @param action the transaction action to execute
     * @return the action result
     */
    <T> T rollBackIfThrows(@Nonnull Supplier<T> action);

    /**
     * Marks the transaction for rollback
     */
    void setRollbackOnly();
}
