package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.git.builder.GitQuarantineEnvironment;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.MoreExecutors;
import io.grpc.Context;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.QueryStringEncoder;

import javax.annotation.Nonnull;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;

import static com.atlassian.bitbucket.mesh.repository.Repository.PARAM_QUARANTINE;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;

/**
 * A helper for working with the push quarantine feature added in Git 2.11.0.
 * <p>
 * Quarantined pushes write the incoming objects to a temporary directory, rather than writing them directly into
 * the target repository, to allow them to be immediately cleaned up if a {@code pre-receive} hook declines, rather
 * than leaving them in the repository to eventually be removed by {@code git gc}.
 * <p>
 * When {@code pre-receive} hooks are run as part of processing a quarantined push, three specific environment
 * variables are set:
 * <ul>
 *     <li>{@code GIT_OBJECT_DIR} is set so that any {@code pre-receive} hook which creates new objects (creating
 *     notes, for example) writes its new objects into the <i>quarantine directory</i>
 *     <ul>
 *         <li>This is <i>absolutely critical</i> to ensure hooks don't create objects which depend on quarantine
 *         objects directly in the repository, as they would be corrupted if the push was ultimately rejected</li>
 *     </ul>
 *     </li>
 *     <li>{@code GIT_ALTERNATE_OBJECT_DIRS} is set to point back to the repository's existing {@code objects/}
 *     directory, allowing hooks to access existing objects as well as the new ones</li>
 *     <li>{@code GIT_QUARANTINE_PATH} is set with the location where quarantined objects are being written
 *     <ul>
 *         <li>This value is generally the same as {@code GIT_OBJECT_DIR}</li>
 *     </ul>
 *     </li>
 * </ul>
 *
 * {@code GitQuarantineRegistry} can be used to {@link #register(GitQuarantineEnvironment)}, which returns a
 * {@link Repository#getId() repository identifier} that uniquely identifies the {@link Repository} and
 * {@link GitQuarantineEnvironment}. It is expected that clients will provide their own mechanism for ensuring
 * subsequent requests for a given quarantine environment are routed to the same Mesh node.
 * <p>
 * The resulting repository identifier can be safely returned to RPC clients to be used as the repository identifier
 * for if subsequent RPC calls need to access the quarantine environment - typically when repository hook
 * invocation is delegated to the RPC client.
 */
public class GitQuarantineRegistry {

    private final AtomicInteger counter;
    private final ConcurrentMap<String, GitQuarantineEnvironment> environmentsById;

    public GitQuarantineRegistry() {
        counter = new AtomicInteger();
        environmentsById = new ConcurrentHashMap<>();
    }

    @Nonnull
    public Optional<GitQuarantineEnvironment> get(@Nonnull Repository repository) {
        String environmentId = requireNonNull(repository, "repository").getParameter(PARAM_QUARANTINE);
        return getEnvironment(environmentId);
    }

    @Nonnull
    public String register(@Nonnull GitQuarantineEnvironment environment) {
        Repository repository = requireNonNull(environment, "environment").getRepository();

        String environmentId = nextEnvironmentId();
        environmentsById.put(environmentId, environment);
        Context.current().addListener(ctx -> environmentsById.remove(environmentId), MoreExecutors.directExecutor());

        return formatQuarantineId(repository, environmentId);
    }

    @Nonnull
    @VisibleForTesting
    Optional<GitQuarantineEnvironment> get(@Nonnull String repositoryId) {
        String environmentId = getEnvironmentId(repositoryId);
        return getEnvironment(environmentId);
    }

    @VisibleForTesting
    void unregister(@Nonnull String repositoryId) {
        String environmentId = getEnvironmentId(repositoryId);
        if (environmentId != null) {
            environmentsById.remove(environmentId);
        }
    }

    private static String formatQuarantineId(Repository repository, String environmentId) {
        QueryStringEncoder encoder = new QueryStringEncoder(repository.getId());
        encoder.addParam(PARAM_QUARANTINE, environmentId);

        return encoder.toString();
    }

    private static String getEnvironmentId(String repositoryId) {
        List<String> values = new QueryStringDecoder(requireNonNull(repositoryId, "repositoryId"))
                .parameters()
                .get(PARAM_QUARANTINE);

        return values == null || values.isEmpty() ? null : values.get(values.size() - 1);
    }

    private Optional<GitQuarantineEnvironment> getEnvironment(String environmentId) {
        return environmentId == null ? empty() : ofNullable(environmentsById.get(environmentId));
    }

    private String nextEnvironmentId() {
        return Integer.toString(counter.incrementAndGet());
    }
}
