package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderSupport;
import com.atlassian.bitbucket.mesh.git.hook.GitHookHandler;
import com.atlassian.bitbucket.mesh.git.hook.GitHookService;
import com.atlassian.bitbucket.mesh.git.hook.HookRequestHandle;
import com.atlassian.bitbucket.mesh.git.hosting.HostingCacheResult;
import com.atlassian.bitbucket.mesh.git.hosting.HostingOperationType;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcHookResult;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcHostingOperationType;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcHostingStreamResult;
import com.google.common.base.Splitter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;

import static com.atlassian.bitbucket.mesh.git.GitConstants.ENV_PROTOCOL;
import static com.atlassian.bitbucket.mesh.rpc.v1.RpcHookResult.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

abstract class AbstractHostingFragmentHandler<ReqT, RespT> implements RpcFragmentHandler<ReqT, RespT> {

    protected Integer protocolVersion;

    private final RpcHostingOperationType defaultOperationType;
    private final GitHookService hookService;

    private HookRequestHandle hookHandle;
    private Logger logger;

    protected AbstractHostingFragmentHandler(@Nonnull RpcHostingOperationType defaultOperationType,
                                             @Nonnull GitHookService hookService) {
        this.defaultOperationType = defaultOperationType;
        this.hookService = hookService;
    }

    protected AbstractHostingFragmentHandler(@Nonnull RpcHostingOperationType defaultOperationType) {
        this.defaultOperationType = defaultOperationType;

        hookService = null;
    }

    protected void maybeConfigureProtocolVersion(GitProcessBuilderSupport<?> builder, String protocol) {
        protocolVersion = parseProtocolVersion(protocol);
        if (isNotBlank(protocol)) {
            builder.withEnvironment(ENV_PROTOCOL, protocol);
        }
    }

    @Nonnull
    protected HostingCacheResult getCacheResult() {
        if (hookHandle == null) {
            return HostingCacheResult.NONE;
        }
        return hookHandle.getCacheResult().orElse(HostingCacheResult.NONE);
    }

    protected Logger getLogger() {
        if (logger == null) {
            logger = LoggerFactory.getLogger(getClass());
        }
        return logger;
    }

    @Nonnull
    protected RpcHostingOperationType getOperationType() {
        return defaultOperationType;
    }

    protected RpcHostingStreamResult getResult(int exitCode) {
        String filter = null;
        RpcHostingOperationType operationType = getOperationType();

        RpcHookResult hookResult;
        if (hookHandle == null) {
            hookResult = HOOK_RESULT_UNSPECIFIED;
        } else {
            filter = hookHandle.getFilter().orElse(null);
            operationType = hookHandle.getOperationType().map(HostingOperationType::toRpc).orElse(operationType);

            if (isUpdate() && !hookHandle.isCalled()) {
                if (exitCode == 0) {
                    getLogger().warn("{}: Git hooks have not been called; please verify that the hooks are configured correctly. ",
                            hookHandle.getRepository());
                }
                hookResult = HOOK_RESULT_NOT_CALLED;
            } else {
                hookResult = hookHandle.isAccepted() ? HOOK_RESULT_ACCEPTED : HOOK_RESULT_REJECTED;
            }
        }

        RpcHostingStreamResult.Builder result = RpcHostingStreamResult.newBuilder()
                .setCacheResult(getCacheResult().toRpc())
                .setExitCode(exitCode)
                .setHookResult(hookResult)
                .setOperationType(operationType);
        if (filter != null) {
            result.setFilter(filter);
        }
        if (protocolVersion != null) {
            result.setProtocolVersion(protocolVersion);
        }
        return result.build();
    }

    protected boolean isUpdate() {
        return defaultOperationType == RpcHostingOperationType.HOSTING_OP_PUSH;
    }

    @Nonnull
    protected HookRequestHandle registerHookRequest(Repository repository) {
        if (hookService == null) {
            throw new IllegalStateException("No GitHookService was set; a hook request cannot be registered");
        }

        hookHandle = hookService.registerRequest(repository);
        getLogger().trace("Acquired hook handle {}", hookHandle.getRequestId());

        return hookHandle;
    }

    @Nonnull
    protected HookRequestHandle registerHookRequest(Repository repository, GitHookHandler... handlers) {
        if (hookService == null) {
            throw new IllegalStateException("No GitHookService was set; a hook request cannot be registered");
        }

        hookHandle = hookService.registerRequest(repository, handlers);
        getLogger().trace("Acquired hook handle {}", hookHandle.getRequestId());

        return hookHandle;
    }

    private Integer parseProtocolVersion(String protocolVersion) {
        if (isBlank(protocolVersion)) {
            return 0;
        }
        // Multiple versions can be provided
        int highestVersion = 0;
        for (String item : Splitter.on(':').split(protocolVersion)) {
            if (item.startsWith("version=")) {
                try {
                    int version = Integer.parseInt(item.substring(8));
                    if (version > highestVersion) {
                        highestVersion = version;
                    }
                } catch (NumberFormatException e) {
                    getLogger().debug("Ignoring invalid git protocol version: {}", item);
                }
            }
        }
        return highestVersion;
    }
}
