package com.atlassian.bitbucket.mesh.repair;

import com.atlassian.bitbucket.mesh.client.FetchObjectsParameters;
import com.atlassian.bitbucket.mesh.coordination.WriteToken;
import com.atlassian.bitbucket.mesh.git.Reflog;
import com.atlassian.bitbucket.mesh.git.ref.Ref;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.google.protobuf.ByteString;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Set;

/**
 * Abstraction that encapsulates the different phases of the repository repair process. This component is responsible
 * for performing parts of the repair process and as such provides building blocks to the
 * {@link RepairTarget repair coordinator}.
 *
 * The repair process consists of
 * <ol>
 *    <li>Repository state discovery - the coordinator {@link #setSourceState(String, String) provides} the source
 *    replica repository's hashes, after which it can check whether the {@link #isMetadataOutdated() metadata} and/or
 *    {@link #isContentOutdated() content} is outdated.</li>
 *    <li>If the repository does not exist locally, or its metadata is outdated, the coordinator retrieves the
 *    repository metadata from the source and calls {@link #updateMetadata(String, Map, boolean)} to create the
 *    local repository replica, or bring its metadata in sync.</li>
 *    <li>If the repository content is out of sync, the coordinator {@link #compareRefs() starts a ref comparison}
 *    between the source and target replicas, providing the source refs to {@link RefComparer#onSourceRef(Ref)}. While
 *    identifying out-of-sync refs, the {@link RefComparer} also identifies any missing commits.</li>
 *    <li>If the repository content is out of sync, and the refs have been compared, the coordinator
 *    {@link #compareReflogs starts a reflog comparison} between the source and target replicas, providing the source
 *    reflogs to {@link ReflogComparer#onSourceReflog(Reflog)}. Once the reflogs have been compared, the coordinator
 *    retrieves any missing or outdated reflogs from the source. While updating the repair replica's reflogs, any
 *    missing objects are also identified and fetched from the source.</li>
 *    <li>At this point, if the repair controller has detected any objects are missing, it fetches a pack of objects
 *    from the source replica and feeds it to {@link #newPackProcessor() a pack processor}. When the pack processor is
 *    done, the previously missing objects will be available in the target replica.</li>
 *    <li>With all required objects available, the controller calls {@link #maybeUpdateRefsAndReflogs()} to bring the
 *    target replica's refs and reflogs in sync with the source replica (as determined from the ref comparison in
 *    step 3 and any reflog repairs performed in step 4).</li>
 *    <li>Finally, the controller {@link #close() closes} the interactive repair to release any resources in use.</li>
 * </ol>
 *
 * @see RepairTarget
 */
public interface InteractiveRepair extends Closeable {

    /**
     * Completes repair, cleaning up any resources in use. If a pack of missing objects was downloaded as part of
     * repair, this will remove the {@code .keep} protection for the pack.
     */
    @Override
    void close();

    /**
     * Starts a comparison of the source and target replica reflogs. This comparison identifies the
     * {@link #getNextReflogsToRepairBatch reflogs to be fetched} to bring the target replica's reflogs in
     * sync with the source.
     *
     * @return the reflog comparer
     */
    @Nonnull
    ReflogComparer compareReflogs();

    /**
     * Starts a comparison of the source and target replica refs. This comparison identifies the
     * {@link #getFetchParameters() objects to be fetched} and the {@link #maybeUpdateRefsAndReflogs()} () ref updates}
     * needed to bring the target replica's refs in sync with the source.
     *
     * @return the ref comparer
     */
    @Nonnull
    RefComparer compareRefs();

    /**
     * @return the shared repair capabilities, as set through {@link #setCapabilities(Set)}
     */
    @Nonnull
    Set<RepairCapability> getCapabilities();

    /**
     * @return the fetch parameters required to retrieve the missing objects, as identified by
     *         {@link #compareRefs() comparing the local and source refs}; {@code null} if no
     *         objects need to be fetched.
     * @throws IllegalStateException if called before refs have been {@link #compareRefs() compared}
     */
    @Nullable
    FetchObjectsParameters getFetchParameters();

    /**
     * Returns a batch of up to {@code maxNumber} ref IDs whose reflogs need to be repaired. This method should
     * be called after {@link #compareReflogs} has been called with the same {@code shared} value. It may need
     * to be called multiple times, with a given {@code shared} value, to retrieve all batches.
     *
     * @param maxNumber the maximum number of ref IDs to return
     * @return a batch of ref IDs whose reflogs need repair; can be empty if no (more) reflogs need repair
     */
    @Nonnull
    Set<String> getNextReflogsToRepairBatch(int maxNumber);

    /**
     * @return the local repository under repair, which can be {@code null} if the repository does not yet exist
     */
    @Nullable
    Repository getRepository();

    /**
     * Increments the repository version by 1.
     * @return the updated repository version
     * @since 1.3
     */
    long incrementAndGetVersion();

    /**
     * Should be called after the {@link #setSourceState(String, String) source state has been set}.
     *
     * @return {@code true} if the source hashes have been set and the local hashes match the provided source hashes
     */
    boolean isConsistent();

    /**
     * @return {@code true} if the repository under repair does not exist locally, or has a different content hash
     *         than {@link #setSourceState the repair source}
     */
    boolean isContentOutdated();

    /**
     * @return {@code true} if the repository under repair does not exist locally, or has a different metadata hash
     *         than {@link #setSourceState the repair source}
     */
    boolean isMetadataOutdated();

    /**
     * @return {@code true} if {@link #compareRefs()} has detected any out of sync refs
     */
    boolean isOutOfSyncRefDetected();

    /**
     * Acquires a lock to ensure GC isn't run against the repository while it's being repaired.
     */
    void lockGc();

    /**
     * Applies any ref updates that have been detected while {@link #compareRefs() comparing refs} and publishes
     * any reflogs that have been staged for repair by {@link #startReflogRepair(String)}.
     *
     * @return {@code true} if any ref was updated
     * @throws IllegalStateException if {@link #compareRefs() has not been called yet}
     */
    boolean maybeUpdateRefsAndReflogs();

    /**
     * Creates a processor for {@link #getFetchParameters() fetched missing objects}.
     *
     * @return a {@link PackProcessor}
     */
    @Nonnull
    PackProcessor newPackProcessor();

    /**
     * Resets the content and metadata hash for the repository under repair.
     */
    void resetRepositoryHashes();

    /**
     * @param capabilities the repair capabilities shared with the upstream
     */
    void setCapabilities(@Nonnull Set<RepairCapability> capabilities);

    /**
     * @param metadataHash the repository metadata hash on the source replica
     * @param contentHash  the repository content hash on the source replica
     */
    void setSourceState(@Nonnull String metadataHash, @Nonnull String contentHash);

    /**
     * Starts the repair of the reflog for the provided {@code refId}. The returned {@link ReflogRepair} instance
     * should be used to {@link ReflogRepair#write(ByteBuffer) write the reflog content to (in chunks)} that's
     * provided by the repair source.
     * <p>
     * The returned {@link ReflogRepair} analyses the reflog contents from the source and determines which of the
     * referenced objects need to be {@link #getFetchParameters() fetched} to complete repair.
     *
     * @param refId the ref for which the reflog repair should be started
     * @return the {@link ReflogRepair} instance for the reflog
     * @throws IOException if the reflog could not be opened for writing
     */
    @Nonnull
    ReflogRepair startReflogRepair(@Nonnull String refId) throws IOException;

    /**
     * Brings the local repository's metadata in sync with the source.
     *
     * @param defaultBranch    the default branch (on the source replica)
     * @param metadata         the repository's metadata (on the source replica)
     * @param transcodeEnabled whether transcoding is enabled on the source replica
     */
    void updateMetadata(@Nullable String defaultBranch, @Nonnull Map<String, String> metadata,
                        boolean transcodeEnabled);

    /**
     * Updates the local repository version to the provided version.
     *
     * @param version the version to update to
     */
    void updateVersion(long version);

    /**
     * Acquires the local {@link WriteToken write token}, invokes the action and releases the token.
     *
     * @param action the action to run while holding the local write token
     */
    void withLock(@Nonnull Runnable action);

    /**
     * A processor that can be used to validate and {@link #write(ByteString, boolean) write} a git pack file to
     * the replica repository.
     */
    interface PackProcessor extends Closeable {

        /**
         * Called after the last pack fragment has been processed
         */
        @Override
        void close();

        /**
         * @param bytes  bytes from the incoming pack
         * @param closed {@code true} if this is the last fragment of the incoming pack
         */
        void write(@Nonnull ByteString bytes, boolean closed);
    }

    /**
     * Helper that compares the target replica's refs with the {@link #onSourceRef source refs} and identifies which
     * refs needs to be added, deleted or updated. While comparing refs, it also identifies which of the commits
     * referenced by the source refs are missing from the target and need to be {@link #getFetchParameters() fetched}
     * from the source.
     */
    interface RefComparer extends Closeable {

        /**
         * Called when the last of the source refs has been provided to {@link #onSourceRef(Ref)}.
         */
        @Override
        void close();

        /**
         * Called for each ref that's been advertised by the repair source
         *
         * @param sourceRef the source ref
         */
        void onSourceRef(@Nonnull Ref sourceRef);
    }

    /**
     * Helper that compares the target replica's reflogs with the {@link #onSourceReflog source refslog} and identifies
     * which reflogs needs to be added, deleted or updated.
     */
    interface ReflogComparer extends Closeable {

        /**
         * Called when the last of the source reflogs has been provided to {@link #onSourceReflog(Reflog)}.
         */
        @Override
        void close();

        /**
         * Called for each reflog file that's been advertised by the repair source
         *
         * @param sourceReflog the source reflog
         */
        void onSourceReflog(@Nonnull Reflog sourceReflog);
    }

    /**
     * Helper that is used to repair a reflog. The caller should call {@link #write(ByteBuffer)} with chunks
     * of data provided by repair source.
     * <p>
     * In addition to writing the reflog bytes to disk, the incoming reflog data is also analysed. Any referenced
     * objects are evaluated and if not present locally, added to {@link #getFetchParameters()}.
     */
    interface ReflogRepair extends Closeable {

        /**
         * Discards the (partially) repaired reflog if the repair has not yet been {@link #close() closed},
         * leaving the original intact.
         */
        void abort();

        /**
         * @return the ref ID of the reflog to repair
         */
        @Nonnull
        String getRefId();

        /**
         * Writes data to the reflog file.
         *
         * @param data the data to write
         */
        void write(@Nonnull ByteBuffer data) throws IOException;
    }
}
