package com.atlassian.stash.internal.scm.git.protocol.ssh;

import com.atlassian.bitbucket.Product;
import com.atlassian.bitbucket.help.HelpPathService;
import com.atlassian.bitbucket.hook.HookService;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.repository.NoSuchRepositoryException;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.request.RequestManager;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.protocol.ssh.GitSshScmRequestHandler;
import com.atlassian.bitbucket.scm.http.RepositoryUrlFragment;
import com.atlassian.bitbucket.scm.ssh.ExitCodeCallback;
import com.atlassian.bitbucket.scm.ssh.SshScmRequest;
import com.atlassian.bitbucket.scm.ssh.SshScmRequestHandler;
import com.atlassian.bitbucket.scm.throttle.ThrottledScmRequestFactory;
import com.atlassian.bitbucket.throttle.ThrottleService;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.stash.internal.scm.git.GitWriteTracker;
import com.atlassian.stash.internal.scm.git.InternalGitScmConfig;
import com.atlassian.stash.internal.scm.git.protocol.RepositoryPath;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.Optional.empty;
import static java.util.Optional.of;

/**
 * {@link SshScmRequestHandler} for handling remote Git push/pull commands over SSH.
 */
public class DefaultGitSshScmRequestHandler implements GitSshScmRequestHandler {

    private static final Pattern PATTERN_GIT_COMMAND = Pattern.compile("git-([^\\s]+)(.*)");
    /**
     * @see RepositoryUrlFragment#NAMESPACED_HOSTING_URL for the equivalent regex for HTTP/HTTPS URLs
     */
    private static final Pattern PATTERN_REPOSITORY_PATH = Pattern.compile("\\s*'/?(?:([^/]+)/)?([^/]+)/([^/']+?)(?:\\.git)?'");

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

    private final GitCommandBuilderFactory builderFactory;
    private final InternalGitScmConfig config;
    private final EventPublisher eventPublisher;
    private final HelpPathService helpPathService;
    private final HookService hookService;
    private final I18nService i18nService;
    private final RepositoryService repositoryService;
    private final RequestManager requestManager;
    private final ThrottledScmRequestFactory throttledFactory;
    private final ThrottleService throttleService;
    private final GitWriteTracker writeTracker;

    public DefaultGitSshScmRequestHandler(GitCommandBuilderFactory builderFactory, InternalGitScmConfig config,
                                          EventPublisher eventPublisher, HelpPathService helpPathService,
                                          HookService hookService, I18nService i18nService,
                                          RepositoryService repositoryService, RequestManager requestManager,
                                          ThrottleService throttleService, ThrottledScmRequestFactory throttledFactory,
                                          GitWriteTracker writeTracker) {
        this.builderFactory = builderFactory;
        this.config = config;
        this.eventPublisher = eventPublisher;
        this.helpPathService = helpPathService;
        this.hookService = hookService;
        this.i18nService = i18nService;
        this.repositoryService = repositoryService;
        this.requestManager = requestManager;
        this.throttledFactory = throttledFactory;
        this.throttleService = throttleService;
        this.writeTracker = writeTracker;
    }

    @Override
    @Nonnull
    public Optional<SshScmRequest> create(@Nonnull String command, @Nonnull InputStream in, @Nonnull OutputStream out,
                                          @Nonnull OutputStream err, @Nonnull ExitCodeCallback exitCodeCallback) {
        return createUnthrottled(command, in, out, err, exitCodeCallback)
                .map(this::maybeThrottle);
    }

    @Override
    public boolean supports(@Nonnull String command) {
        return getGitCommand(command).isPresent();
    }

    @Nonnull
    private Optional<SshScmRequest> createUnthrottled(@Nonnull String command, @Nonnull InputStream in,
                                                      @Nonnull OutputStream out, @Nonnull OutputStream err,
                                                      @Nonnull ExitCodeCallback exitCodeCallback) {
        Optional<GitCommand> optionalGitCommand = getGitCommand(command);
        if (!optionalGitCommand.isPresent()) {
            log.debug("{} does not appear to be a git command", command);
            return empty();
        }

        GitCommand gitCommand = optionalGitCommand.get();

        log.trace("{} is a supported git command; extracting repository", command);

        RepositoryPath repositoryPath = parseArguments(gitCommand.getArgument());
        if (repositoryPath != null) {
            Repository repository = getRepositoryOrThrow(repositoryPath);
            switch (gitCommand.getCommand()) {
                case GitSshUtils.COMMAND_RECEIVE_PACK:
                    return of(new SshReceivePack(repository, builderFactory, config, eventPublisher, helpPathService,
                            hookService, i18nService, exitCodeCallback, requestManager, in, out, err, writeTracker));
                case GitSshUtils.COMMAND_UPLOAD_ARCHIVE:
                    return of(new SshUploadArchive(repository, builderFactory, config, eventPublisher, i18nService,
                            exitCodeCallback, requestManager, in, out, err, writeTracker));
                case GitSshUtils.COMMAND_UPLOAD_PACK:
                    // not throttled because SshUploadPack manages throttling internally
                    return of(new SshUploadPack(repository, builderFactory, config, eventPublisher, i18nService,
                            exitCodeCallback, requestManager, throttleService, in, out, err, writeTracker));
                default:
                    // should never happen because we've checked SUPPORTED_COMMANDS before. Purely defensive code
                    throw new IllegalStateException("Could not create an SSH request for supported operation "
                            + gitCommand.getCommand());
            }
        } else {
            log.debug("Repository information could not be extracted from {}", command);

            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.git.ssh.url.invalid.message",
                    Product.NAME, gitCommand.getArgument().trim());
            throw new NoSuchRepositoryException(message, null);
        }
    }

    private RepositoryPath parseArguments(String arguments) {
        Matcher matcher = PATTERN_REPOSITORY_PATH.matcher(arguments);
        if (matcher.matches()) {
            String namespace = matcher.group(1);
            String projectKey = matcher.group(2);
            String repositorySlug = matcher.group(3);

            log.trace("Extracted namespace {}, project key {} and repository slug {}; loading repository",
                    StringUtils.defaultString(namespace, "<none>"), projectKey, repositorySlug);

            return new RepositoryPath(namespace, projectKey, repositorySlug);
        }

        return null;
    }

    private Repository getRepositoryOrThrow(RepositoryPath reference) {
        Repository repository = repositoryService.getBySlug(
                reference.getNamespace(), reference.getProjectKey(), reference.getRepositorySlug());
        if (repository != null) {
            return repository;
        }
        KeyedMessage message = i18nService.createKeyedMessage("bitbucket.scm.no.such.repository.detail");
        throw new NoSuchRepositoryException(message, null);
    }

    private Optional<GitCommand> getGitCommand(String command) {
        Matcher matcher = PATTERN_GIT_COMMAND.matcher(command);
        if (matcher.matches()) {
            String cmd = matcher.group(1);
            if (GitSshUtils.SUPPORTED_COMMANDS.contains(cmd)) {
                String arguments = matcher.group(2);
                return of(new GitCommand(cmd, arguments));
            }
        }
        return empty();
    }

    private SshScmRequest maybeThrottle(SshScmRequest request) {
        if (!config.isRefAdvertisementThrottled() && request instanceof SshUploadPack) {
            // SshUploadPack manages throttling internally to allow throttle-free ref advertisement
            return request;
        }
        return throttledFactory.throttled(request);
    }

    private static class GitCommand {

        private final String command;
        private final String argument;

        public GitCommand(String command, String argument) {
            this.command = command;
            this.argument = argument;
        }

        public String getCommand() {
            return command;
        }

        public String getArgument() {
            return argument;
        }
    }
}
