package com.atlassian.stash.internal.scm.git.submodule;

import com.atlassian.bitbucket.content.NoSuchPathException;
import com.atlassian.bitbucket.content.SimplePath;
import com.atlassian.bitbucket.content.SimpleSubmodule;
import com.atlassian.bitbucket.content.Submodule;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryCloneLinksRequest;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.util.NamedLink;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.commons.lang3.StringUtils.removeEnd;

/**
 * Both <a href="http://www.kernel.org/pub/software/scm/git/docs/git-submodule.html">Git</a> and
 * <a href="http://mercurial.selenic.com/wiki/Subrepository">Mercurial</a> support this in an almost identical fashion.
 * <p>
 * Git stores a {@code commit} under the tree, and a map of paths to URLs under the root {@code .gitmodules} file.
 * <p>
 * Mercurial stores the commit details under {@code .hgsubstate} and the corresponding metadata under the {@code .hgsub}.
 * No information is stored in the manifest directly.
 *
 * @since 2.3
 */
public class DefaultGitSubmoduleHelper implements GitSubmoduleHelper {

    private static final Logger log = LoggerFactory.getLogger(DefaultGitSubmoduleHelper.class);
    private static final String HTTP = "http";

    private static final GitSubmoduleUriHandler GITHUB = new GitSubmoduleUriHandler() {
        @Nonnull
        @Override
        public SubmoduleUrls process(@Nonnull URI uri, @Nonnull String contentId) {
            return convertSimpleGitURI(uri, "https://github.com", "/tree/" + contentId);
        }
    };

    private static final GitSubmoduleUriHandler BITBUCKET = new GitSubmoduleUriHandler() {
        @Nonnull
        @Override
        public SubmoduleUrls process(@Nonnull URI uri, @Nonnull String contentId) {
            return convertSimpleGitURI(uri, "https://bitbucket.org", "/changeset/" + contentId + "/");
        }
    };

    private static final Map<String, GitSubmoduleUriHandler> URI_HANDLERS =
            ImmutableMap.of("github.com", GITHUB, "bitbucket.org", BITBUCKET);

    private final GitSubmoduleCommandFactory gitSubmoduleCommandFactory;
    private final GitSubmoduleUriHandler defaultURIHandler;
    private final RepositoryService repositoryService;

    public DefaultGitSubmoduleHelper(GitSubmoduleCommandFactory gitSubmoduleCommandFactory,
                                     GitSubmoduleUriHandler defaultURIHandler, RepositoryService repositoryService) {
        this.gitSubmoduleCommandFactory = gitSubmoduleCommandFactory;
        this.defaultURIHandler = defaultURIHandler;
        this.repositoryService = repositoryService;
    }

    @Nonnull
    public Submodules getSubmodules(@Nonnull final Repository repository, @Nonnull final String objectId, final String root) {
        checkNotNull(repository, "repository");
        checkNotNull(objectId, "objectId");
        // Lazily load (and then cache) the modules for this repository
        final Supplier<Map<String, String>> modules = Suppliers.memoize(new Supplier<Map<String, String>>() {
            @Override
            public Map<String, String> get() {
                try {
                    return gitSubmoduleCommandFactory.getSubmodules(repository, objectId).call();
                } catch (NoSuchPathException e) {
                    log.warn("Error looking up submodules for '{}' in '{}'", objectId, repository);
                    return Collections.emptyMap();
                }
            }
        });
        return new Submodules() {
            @Override
            public Submodule getSubmodule(@Nonnull String name, @Nonnull String contentId) {
                String url = modules.get().get(SimplePath.join(root, checkNotNull(name, "path")));
                if (url != null) {
                    SubmoduleUrls urls = convertUrl(resolveRelativePath(url, repository), contentId);
                    return new SimpleSubmodule.Builder()
                            .path(name)
                            .contentId(contentId)
                            .url(url)
                            .repositoryUrl(urls.getRepositoryUrl())
                            .browseUrl(urls.getBrowseUrl())
                            .build();
                } else {
                    return null;
                }
            }
        };
    }

    /**
     * Converts {@code ssh://host:port/content/path.git} to {@code http://$prefix/content/path/$postfix}.
     */
    private static SubmoduleUrls convertSimpleGitURI(URI uri, String prefix, String postfix) {
        String url = prefix + removeEnd(uri.getPath(), ".git");
        return new SubmoduleUrls(url, url + postfix);
    }

    private String findLinkWithProtocol(Set<NamedLink> cloneLinks, String protocol, String defaultUrl) {
        String resolvedUrl = defaultUrl;
        for (NamedLink link : cloneLinks) {
            if (protocol.equalsIgnoreCase(link.getName())) {
                resolvedUrl = link.getHref();
                break;
            }
        }
        return resolvedUrl;
    }

    /**
     * Converts invalid git URIs from {@code git@host:path/repo} to {@code ssh://git@host/path/repo}.
     */
    private static String fixUrlsWithNoProtocol(String url) {
        if (!url.contains("://") && url.contains("@")) {
            return "ssh://" + StringUtils.replaceOnce(url, ":", "/");
        } else {
            return url;
        }
    }


    @VisibleForTesting
    SubmoduleUrls convertUrl(String url, String contentId) {
        try {
            URI uri = new URI(fixUrlsWithNoProtocol(url));
            return MoreObjects.firstNonNull(URI_HANDLERS.get(uri.getHost()), defaultURIHandler).process(uri, contentId);
        } catch (URISyntaxException e) {
            return SubmoduleUrls.NONE;
        }
    }

    @VisibleForTesting
    String resolveRelativePath(String url, Repository repository) {
        if (url.startsWith("../") || url.startsWith("./")) {
            try {
                Set<NamedLink> cloneLinks = repositoryService.getCloneLinks(
                        new RepositoryCloneLinksRequest.Builder().repository(repository).build());
                // Note: The / at the end of the uri is required for the resolve to work correctly
                URI uri = new URI(findLinkWithProtocol(cloneLinks, HTTP, url) + "/");
                return uri.resolve(url).toASCIIString();
            } catch (URISyntaxException e) {
                // Never happen unless RepositoryService is sick
            }
        }
        return url;
    }
}