package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.config.HostingConfig;
import com.atlassian.bitbucket.mesh.event.EventPublisher;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderSupport;
import com.atlassian.bitbucket.mesh.git.builder.httpbackend.GitHttpBackendBuilder;
import com.atlassian.bitbucket.mesh.git.cache.PackCacheService;
import com.atlassian.bitbucket.mesh.git.exception.InvalidArgumentException;
import com.atlassian.bitbucket.mesh.git.hook.GitHookService;
import com.atlassian.bitbucket.mesh.git.hosting.GitReceivePackListener;
import com.atlassian.bitbucket.mesh.management.SidebandManager;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.repository.RepositoryManager;
import com.atlassian.bitbucket.mesh.request.RequestManager;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.throttle.ThrottledResource;
import com.atlassian.bitbucket.mesh.transaction.UpdateRefsContextHolder;
import io.grpc.stub.StreamObserver;
import io.netty.handler.codec.http.QueryStringDecoder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;

import javax.annotation.Nonnull;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;

import static com.atlassian.bitbucket.mesh.git.BidirectionalFragmentRequestObserver.requireCase;
import static com.atlassian.bitbucket.mesh.git.GitConstants.CONFIG_UPLOAD_PACK_ALLOW_FILTER;
import static com.atlassian.bitbucket.mesh.rpc.v1.RpcHostingOperationType.*;
import static com.atlassian.bitbucket.mesh.throttle.ThrottledResource.PACK;
import static com.atlassian.bitbucket.mesh.throttle.ThrottledResource.REFS;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

class HttpBackendFragmentHandler extends AbstractHostingWriteFragmentHandler<RpcHttpBackendFragment> {

    private static final Logger log = LoggerFactory.getLogger(HttpBackendFragmentHandler.class);

    private final GitProcessBuilderFactory builderFactory;
    private final HostingConfig hostingConfig;
    private final PackCacheService packCacheService;
    private final RepositoryManager repositoryManager;

    private RpcHostingOperationType operationType;

    HttpBackendFragmentHandler(GitProcessBuilderFactory builderFactory, EventPublisher eventPublisher,
                               GitHookService hookService, Duration hookTimeout, HostingConfig hostingConfig,
                               long maxFragmentSize, StreamObserver<RpcHostingWriteStreamFragment> observer,
                               PackCacheService packCacheService, GrpcPushTracer pushTracer,
                               GitQuarantineRegistry quarantineRegistry,
                               List<GitReceivePackListener> receivePackListeners, RepositoryManager repositoryManager,
                               RequestManager requestManager, SidebandManager sidebandManager, UpdateRefsContextHolder updateRefsHelper) {
        super(HOSTING_OP_UNSPECIFIED, eventPublisher, hookService, hookTimeout, maxFragmentSize, observer,
                pushTracer, quarantineRegistry, receivePackListeners, requestManager, sidebandManager, updateRefsHelper);

        this.builderFactory = builderFactory;
        this.hostingConfig = hostingConfig;
        this.packCacheService = packCacheService;
        this.repositoryManager = repositoryManager;
    }

    @Nonnull
    @Override
    public GitProcessBuilderSupport<?> createBuilder(@Nonnull RpcHttpBackendFragment fragment) {
        requireCase(fragment.getRequestOneofCase(), RpcHttpBackendFragment.RequestOneofCase.REQUEST);

        //The repository's directory will be determined as part of creating the builder,
        //and passed to http-backend; we don't need to set it manually here
        RpcHttpBackendRequest request = fragment.getRequest();
        Repository repository = repositoryManager.getById(request.getRepository());

        GitHttpBackendBuilder builder = builderFactory.builder(request).httpBackend()
                //This is set for more than just the isUploadPack block because other requests,
                //like capability and ref advertisements, also need it
                .withConfiguration(CONFIG_UPLOAD_PACK_ALLOW_FILTER, hostingConfig.isFilterAllowed());
        maybeConfigureProtocolVersion(builder, request.getProtocolHeader());
        applyEnvironmentVars(builder, request, repository.getPath());

        RpcCgiEnvironment cgiEnv = request.getCgiEnvironment();

        //Get the operation type from the request (which may be overridden by a more specific type if a hook is called).
        operationType = getOperationTypeFromRequest(cgiEnv, protocolVersion);
        if (operationType == HOSTING_OP_PUSH) {
            invokeReceivePackListeners(repository);
            return registerHookRequest(repository)
                    .configure(builder);
        }
        if (isUploadPack(cgiEnv)) {
            if (packCacheService.isEnabledForHttp()) {
                return registerHookRequest(repository)
                        .configure(builder)
                        .throttled(REFS);
            }
            //When the cache is disabled, set the operation type to unspecified.
            operationType = HOSTING_OP_UNSPECIFIED;
        }

        return builder.throttled(getThrottledResource(cgiEnv));
    }

    @Override
    public RpcStreamChunk onFragment(@Nonnull RpcHttpBackendFragment fragment) {
        switch (fragment.getRequestOneofCase()) {
            case STDIN:
                if (operationType == HOSTING_OP_PUSH) {
                    return onStdin(fragment.getStdin());
                }
                return fragment.getStdin();
            case POST_RECEIVE:
                onPostReceive(fragment.getPostReceive());
                break;
            case PRE_RECEIVE:
                onPreReceive(fragment.getPreReceive());
                break;
            default:
                throw new InvalidArgumentException("Received [" + fragment.getRequestOneofCase() +
                        "] where [STDIN, POST_RECEIVE or PRE_RECEIVE] was expected");
        }

        return null;
    }

    @Override
    protected Logger getLogger() {
        return log;
    }

    @Nonnull
    @Override
    protected RpcHostingOperationType getOperationType() {
        //A push that is not an update (i.e. it does not provide any ref changes) is treated as unspecified.
        if (operationType == HOSTING_OP_PUSH && !isUpdate()) {
            return HOSTING_OP_UNSPECIFIED;
        }
        return operationType;
    }

    private static RpcHostingOperationType getOperationTypeFromRequest(RpcCgiEnvironment cgi, Integer protocol) {
        String pathInfo = cgi.getPathInfo();
        if (pathInfo.endsWith("/git-receive-pack")) {
            return HOSTING_OP_PUSH;
        }
        // If the protocol was not specified default to 0
        int protocolVersion = protocol == null ? 0 : protocol;
        if (isUploadPack(cgi)) {
            return protocolVersion < 2 ? HOSTING_OP_NEGOTIATION : HOSTING_OP_REF_ADVERTISEMENT;
        }
        String service = parseQueryString(cgi.getQueryString(), "service");
        if ("git-receive-pack".equals(service)) {
            // URL = /.../pkey/slug.git?service=git-receive-pack. This is the refs advertisement for receive-pack.
            // The URL for the real receive-pack is /.../pkey/slug.git/git-receive-pack
            return HOSTING_OP_REF_ADVERTISEMENT;
        }
        if ("git-upload-pack".equals(service)) {
            // URL = /.../pkey/slug.git?service=git-upload-pack. This is the refs advertisement for upload-pack.
            // The URL for the real upload-pack is /.../pkey/slug.git/git-upload-pack
            return protocolVersion < 2 ? HOSTING_OP_REF_ADVERTISEMENT : HOSTING_OP_CAPABILITIES;
        }
        if (pathInfo.endsWith("/info/refs")) {
            return HOSTING_OP_REF_ADVERTISEMENT;
        }
        return HOSTING_OP_UNSPECIFIED;
    }

    private static ThrottledResource getThrottledResource(RpcCgiEnvironment cgi) {
        String pathInfo = cgi.getPathInfo();
        if (pathInfo.endsWith("/info/refs") || pathInfo.endsWith("/git-receive-pack")) {
            return REFS;
        }
        return PACK;
    }

    private static boolean isUploadPack(RpcCgiEnvironment cgi) {
        return "POST".equals(cgi.getRequestMethod()) && cgi.getPathInfo().endsWith("/git-upload-pack");
    }

    private static String parseQueryString(String queryString, String argument) {
        List<String> values = new QueryStringDecoder("?" + queryString).parameters().get(argument);
        if (CollectionUtils.isEmpty(values)) {
            return null;
        }
        return values.get(0);
    }

    private static void withEnvIfNotBlank(GitHttpBackendBuilder builder, String name, String value) {
        if (isNotBlank(value)) {
            builder.withEnvironment(name, value);
        }
    }

    private static void withEnvIfNotBlank(GitHttpBackendBuilder builder, String name, List<String> values) {
        if (values.isEmpty()) {
            return;
        }

        builder.withEnvironment(name, StringUtils.join(values, ","));
    }

    private void applyEnvironmentVars(GitHttpBackendBuilder builder, RpcHttpBackendRequest request,
                                      Path directory) {
        builder.withEnvironment("GATEWAY_INTERFACE", "CGI/1.1")
                .withEnvironment("GIT_HTTP_EXPORT_ALL", "true");

        //From http-backend.c, we have two options:
        //1. Set GIT_PROJECT_ROOT and PATH_INFO, which git will join together with a / inbetween
        //2. Set PATH_TRANSLATED which is used as-is (and, therefore, must be the same as ROOT + PATH_INFO)
        builder.withEnvironment("GIT_PROJECT_ROOT", directory.toString());

        RpcCgiEnvironment cgiEnvironment = request.getCgiEnvironment();
        // The client may not send a Content-Length header. The CONTENT_LENGTH variable should only be set when it
        // is known. Omitting it causes Git to read until EOF (see http-backend.c#run_service), which is the desired
        // behavior in this case.
        if (cgiEnvironment.hasContentLength() && mayPropagateContentLength(cgiEnvironment)) {
            long value = cgiEnvironment.getContentLength();
            if (value > -1L) {
                builder.withEnvironment("CONTENT_LENGTH", Long.toString(value));
            }
        }
        withEnvIfNotBlank(builder, "CONTENT_TYPE", cgiEnvironment.getContentType());
        withEnvIfNotBlank(builder, "PATH_INFO", cgiEnvironment.getPathInfo());
        withEnvIfNotBlank(builder, "QUERY_STRING", cgiEnvironment.getQueryString());
        withEnvIfNotBlank(builder, "REMOTE_ADDR", cgiEnvironment.getRemoteAddr());
        withEnvIfNotBlank(builder, "REMOTE_USER", cgiEnvironment.getRemoteUser());
        withEnvIfNotBlank(builder, "REQUEST_METHOD", cgiEnvironment.getRequestMethod());
        withEnvIfNotBlank(builder, "SERVER_PROTOCOL", cgiEnvironment.getServerProtocol());
        withEnvIfNotBlank(builder, "SERVER_SOFTWARE", cgiEnvironment.getServerSoftware());

        withEnvIfNotBlank(builder, "HTTP_ACCEPT", cgiEnvironment.getHttpAcceptList());
        withEnvIfNotBlank(builder, "HTTP_ACCEPT_ENCODING", cgiEnvironment.getHttpAcceptEncodingList());
        withEnvIfNotBlank(builder, "HTTP_CONTENT_ENCODING", cgiEnvironment.getHttpContentEncoding());
        withEnvIfNotBlank(builder, "EXPECT", cgiEnvironment.getExpectList());
    }

    private boolean mayPropagateContentLength(RpcCgiEnvironment cgiEnvironment) {
        //Mesh _may_ intercept and modify the request payload for receive-pack calls to inject the 'atomic' capability
        //on non-atomic initial pushes. If we set the content length on those requests, things will break because
        //the manipulated stream has a different content length (+7 bytes).
        return hostingConfig.isAtomicInitialPush() && !("POST".equalsIgnoreCase(cgiEnvironment.getRequestMethod()) &&
                "/git-receive-pack".equals(cgiEnvironment.getPathInfo()));
    }
}
