package com.atlassian.stash.internal.web.filters;

import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryCloneLinksRequest;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.scm.AvailableScm;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.util.NamedLink;
import com.atlassian.soy.renderer.SoyTemplateRenderer;
import com.atlassian.stash.internal.scm.protocol.http.PluginHttpScmProtocol;
import com.google.common.collect.ImmutableMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;

import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;

@Component("goGetFilter")
public class GoGetFilter implements Filter {

    private static final String GO_GET = "go-get";
    private static final String MODULE_KEY = "com.atlassian.bitbucket.server.bitbucket-web:server-soy-templates";
    private static final String TEMPLATE_KEY = "bitbucket.internal.page.goget";

    private final NavBuilder navBuilder;
    private final RepositoryService repositoryService;
    private final ScmService scmService;
    private final SecurityService securityService;
    private final SoyTemplateRenderer soyTemplateRenderer;

    @Autowired
    public GoGetFilter(NavBuilder navBuilder, RepositoryService repositoryService, ScmService scmService,
                       SecurityService securityService, SoyTemplateRenderer soyTemplateRenderer) {
        this.navBuilder = navBuilder;
        this.repositoryService = repositoryService;
        this.scmService = scmService;
        this.securityService = securityService;
        this.soyTemplateRenderer = soyTemplateRenderer;
    }

    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        if ("1".equals(httpRequest.getParameter(GO_GET))) {
            Map<String, Object> context = parse(httpRequest);

            httpResponse.setStatus(context.isEmpty() ? HttpServletResponse.SC_BAD_REQUEST : HttpServletResponse.SC_OK);
            soyTemplateRenderer.render(httpResponse.getWriter(), MODULE_KEY, TEMPLATE_KEY, context);
        } else {
            chain.doFilter(httpRequest, httpResponse);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    private static String extractBaseUrl(HttpServletRequest request) {
        String schemalessUrl = request.getRequestURL().toString().replaceFirst("^http[s]?://", "");

        return schemalessUrl.substring(0, schemalessUrl.length() - request.getRequestURI().length()) + request.getContextPath();
    }

    private Supplier<String> buildCloneUrl(String projectKey, String repositorySlug, String vcs) {
        return () -> {
            //We can't leak repository existence so, if the repository wasn't found or no clone URL was
            //generated, we need to approximate one.
            String scmUrl = navBuilder.buildAbsolute() + "/scm/" + projectKey + "/" + repositorySlug;
            if ("git".equalsIgnoreCase(vcs)) {
                //To make the URL look more like a canonical Git URL, add the ".git" suffix
                scmUrl += ".git";
            }

            return scmUrl;
        };
    }

    private Function<Repository, Optional<NamedLink>> getCloneLink() {
        return repository -> {
            Set<NamedLink> links = repositoryService.getCloneLinks(
                    new RepositoryCloneLinksRequest.Builder()
                            .protocol(PluginHttpScmProtocol.HTTP) //go get requires HTTP, not SSH
                            .repository(repository)
                            .user(null) //Don't include a username in the URL
                            .build());

            return links.stream().findFirst();
        };
    }

    private Map<String, Object> parse(HttpServletRequest request) {
        String requestUri = request.getRequestURI().substring(request.getContextPath().length() + 1);
        String[] requestUriComponents = requestUri.split("/");
        if (requestUriComponents.length < 2) {
            return Collections.emptyMap();
        }

        //The full parsing is done with escalated permissions, rather than just looking up the repository, because
        //getting the clone links also has a permission check
        return securityService.withPermission(Permission.REPO_READ, "go get").call(() -> {
            String projectKey = requestUriComponents[0];
            String repositorySlug = requestUriComponents[1];
            Optional<Repository> maybeRepository = ofNullable(repositoryService.getBySlug(projectKey, repositorySlug));

            String vcs = maybeRepository
                    .map(Repository::getScmId)
                    .orElseGet(randomScm(projectKey, repositorySlug));
            String repoRoot = maybeRepository
                    .flatMap(getCloneLink())
                    .map(NamedLink::getHref)
                    .orElseGet(buildCloneUrl(projectKey, repositorySlug, vcs));
            String importPrefix = extractBaseUrl(request) + "/" + projectKey + "/" + repositorySlug;

            return ImmutableMap.<String, Object>builder()
                    .put("importPrefix", importPrefix)
                    .put("repoRoot", repoRoot)
                    .put("vcs", vcs)
                    .build();
        });
    }

    private Supplier<String> randomScm(String projectKey, String repositorySlug) {
        return () -> {
            List<String> scms = scmService.getAvailable().stream()
                    .map(AvailableScm::getId)
                    .collect(toList());
            if (scms.size() == 1) {
                return scms.get(0);
            }

            //If more than one SCM is installed, we want to select one randomly. However, the selection must be
            //deterministic so that multiple requests to the same (invalid) URL must return the same SCM. Since
            //getBySlug is case-insensitive, the URL is lowercased before hashing.
            int hash = (projectKey + "/" + repositorySlug).toLowerCase(Locale.ROOT).hashCode();
            return scms.get(hash % scms.size());
        };
    }
}