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.rpc.v1.RpcTransaction;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcTransactionSummary;
import com.atlassian.bitbucket.mesh.util.SatiableConsumer;

import javax.annotation.Nonnull;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CompletionStage;

/**
 * Encapsulates the interactions with another {@link MeshNode} when coordinating a write operation.
 * <ul>
 *     <li>Once a replica transaction is created, the first operation should be to {@link #prepare} the write operation
 *     that will be replicated.</li>
 *     <li>After the write operation is prepared, {@link #commit} or {@link #rollBack} should be called to specify the
 *     desired disposition for the prepared changes.
 *     <ul>
 *         <li>For example, if multiple replicas are being coordinated and a quorum is not reached, the changes should
 *         be rolled back on any replica that successfully prepared them.</li>
 *     </ul>
 *     </li>
 *     <li>After a disposition is specified, {@link #await} can be used to block until the replica has completed
 *     processing, with the result used to determine whether the replica has arrived at the expected state.</li>
 *     <li>If the transaction was {@link #commit committed}, a {@link #summarize summary} should be sent to inform
 *     the replica of any other replicas that failed to commit.</li>
 * </ul>
 *
 * @see com.atlassian.bitbucket.mesh.client.MeshClient#replicate
 */
public interface ReplicaTransaction {

    /**
     * Blocks until the replica has completed transaction processing.
     * <p>
     * This should be called for any replica which successfully {@link #prepare prepares} the transaction, and
     * it should only be called after {@link #commit} or {@link #rollBack} has been used to signal the desired
     * disposition for the prepared changes.
     *
     * @param timeout the maximum amount of time to wait for the replica to complete processing before giving
     *                up and throwing an exception
     * @return {@code true} if the transaction was committed; otherwise {@code false} if it was rolled back
     * @throws TransactionCommitFailedException   if the replica was instructed to {@link #commit} and failed, or the
     *                                            timeout expired after the commit instruction was sent
     * @throws TransactionRollbackFailedException if the replica was instructed to {@link #rollBack} and failed, or
     *                                            the timeout expired after the roll back instruction was sent
     */
    boolean await(@Nonnull Duration timeout);

    /**
     * Instructs the replica to commit the transaction. This should be called after a write operation is
     * {@link #prepare prepared}.
     *
     * @param logicalTimestamp the logical timestamp of the commit instruction
     */
    void commit(long logicalTimestamp);

    /**
     * Retrieves the node hosting this replica.
     *
     * @return the replica node
     */
    @Nonnull
    MeshNode getNode();

    /**
     * @return the result of {@link #prepare preparing} the transaction, or {@link Optional#empty()} if the replica
     *         hasn't prepared (yet)
     */
    @Nonnull
    Optional<PrepareResult> getPrepareResult();

    /**
     * @return the replica version post-commit; {@link Optional#empty()} if the transaction hasn't committed
     *         successfully (yet)
     */
    @Nonnull
    Optional<ReplicaVersion> getReplicaVersion();

    /**
     * Starts a new writer to send a pack of objects to the replica.
     * <p>
     * The caller should use the returned {@link SatiableConsumer} to send the pack contents to the replica.
     * The returned {@link SatiableConsumer} returns {@code true} when the replica has signalled that it's 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 the replica
     */
    @Nonnull
    SatiableConsumer<RpcStreamChunk> newPackWriter(@Nonnull Repository repository);

    /**
     * Instructs the replica to prepare the provided write {@code operation}.
     *
     * @param operationFragments the write operation that is being replicated, possibly split in multiple fragments
     * @param logicalTimestamp   the logical timestamp of the prepare instruction
     * @return a {@link CompletionStage} that completes when the replica responds with its {@link PrepareResult} for the
     *         {@code operation}; an exceptional result should be interpreted as a NO vote.
     */
    @Nonnull
    CompletionStage<PrepareResult> prepare(@Nonnull Iterable<RpcTransaction> operationFragments, long logicalTimestamp);

    /**
     * Instructs the replica to roll back the transaction. This should be called after a write operation is
     * {@link #prepare prepared}.
     *
     * @param logicalTimestamp the logical timestamp of the rollback instruction
     */
    void rollBack(long logicalTimestamp);

    /**
     * Sends a summary of the transaction to the replica. This should only be called if the transaction was
     * {@link #commit committed}, after {@link #await waiting for processing to complete}, and it should be
     * the final call on the replica.
     *
     * @param summary          the summary to send
     * @param logicalTimestamp the logical timestamp of the summary
     */
    void summarize(@Nonnull RpcTransactionSummary summary, long logicalTimestamp);
}
