package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.config.HostingConfig;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderSupport;
import com.atlassian.bitbucket.mesh.git.builder.uploadpack.GitUploadPackBuilder;
import com.atlassian.bitbucket.mesh.git.cache.PackCacheService;
import com.atlassian.bitbucket.mesh.git.hook.GitHookService;
import com.atlassian.bitbucket.mesh.git.hook.HookRequestHandle;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.repository.RepositoryManager;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcStreamChunk;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcThrottledResource;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcUploadPackFragment;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcUploadPackRequest;
import com.atlassian.bitbucket.mesh.throttle.ThrottledResource;

import javax.annotation.Nonnull;
import java.time.Duration;

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.git.GitConstants.ENV_THROTTLED_RESOURCE;
import static com.atlassian.bitbucket.mesh.rpc.v1.RpcHostingOperationType.HOSTING_OP_REF_ADVERTISEMENT;
import static com.atlassian.bitbucket.mesh.rpc.v1.RpcHostingOperationType.HOSTING_OP_UNSPECIFIED;
import static com.atlassian.bitbucket.mesh.throttle.ThrottledResource.*;

class UploadPackFragmentHandler extends AbstractHostingReadFragmentHandler<RpcUploadPackFragment> {

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

    UploadPackFragmentHandler(GitProcessBuilderFactory builderFactory, GitHookService hookService,
                              HostingConfig hostingConfig, PackCacheService packCacheService,
                              RepositoryManager repositoryManager) {
        //If the pack cache is enabled set the default operation type to refs (it may be overridden by a more specific
        //type if a hook is called). Otherwise, when the cache is disabled, default it to unspecified.
        super(packCacheService.isEnabledForSsh() ? HOSTING_OP_REF_ADVERTISEMENT : HOSTING_OP_UNSPECIFIED, hookService);

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

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

        RpcUploadPackRequest request = fragment.getRequest();
        Repository repository = repositoryManager.getById(request.getRepository());

        //The repository's directory will be determined as part of creating the builder,
        //and passed to upload-pack; we don't need to set it manually here
        GitUploadPackBuilder builder = builderFactory.builder(request).uploadPack()
                .advertiseRefs(request.getAdvertiseRefs())
                .statelessRpc(request.getStatelessRpc())
                .strict(request.getStrict())
                .timeout(getIdleTimeout(request))
                .withConfiguration(CONFIG_UPLOAD_PACK_ALLOW_FILTER, hostingConfig.isFilterAllowed());
        maybeConfigureProtocolVersion(builder, request.getProtocolEnv());

        ThrottledResource resource = getThrottledResource(request);
        if (packCacheService.isEnabledForSsh()) {
            HookRequestHandle hookHandle = registerHookRequest(repository);

            return hookHandle.configure(builder)
                    //Acquire a REFS ticket for the uploadPack request; for cache misses the pack-object hook will
                    //upgrade the ticket to an PACK or MIRROR_PACK ticket based on the value of the ENV_THROTTLED_RESOURCE
                    //environment variable.
                    .throttled(REFS)
                    .withEnvironment(ENV_THROTTLED_RESOURCE, resource.getId());
        }

        return builder.throttled(resource);
    }

    @Nonnull
    @Override
    public RpcStreamChunk onFragment(@Nonnull RpcUploadPackFragment fragment) {
        requireCase(fragment.getRequestOneofCase(), RpcUploadPackFragment.RequestOneofCase.STDIN);

        return fragment.getStdin();
    }

    private static Duration getIdleTimeout(RpcUploadPackRequest request) {
        if (request.hasTimeouts()) {
            long idle = request.getTimeouts().getIdle();
            if (idle > 5) {
                //We apply a small offset to the idle timeout to try and increase the odds of git upload-pack
                //applying the timeout itself before the process-level timeout kicks in and kills it
                return Duration.ofSeconds(idle - 5);
            }
        }

        return null;
    }

    private static ThrottledResource getThrottledResource(RpcUploadPackRequest request) {
        if (request.getThrottledResource() == RpcThrottledResource.RESOURCE_MIRROR_PACK) {
            return MIRROR_PACK;
        }
        return PACK;
    }
}
