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

import com.atlassian.bitbucket.AuthorisationException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.help.HelpPathService;
import com.atlassian.bitbucket.hook.HookService;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.NoSuchRepositoryException;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.request.RequestContext;
import com.atlassian.bitbucket.request.RequestManager;
import com.atlassian.bitbucket.scm.AuthenticationState;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.protocol.UploadPackRequestAnalyzer;
import com.atlassian.bitbucket.scm.git.protocol.http.GitHttpScmRequestHandler;
import com.atlassian.bitbucket.scm.http.HttpRequestDetails;
import com.atlassian.bitbucket.scm.http.HttpScmRequest;
import com.atlassian.bitbucket.scm.throttle.ThrottledScmRequestFactory;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.plugin.servlet.ServletContextFactory;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;

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

/**
 * A handler for remote Git push/pull commands over HTTP.
 */
public class DefaultGitHttpScmRequestHandler implements GitHttpScmRequestHandler {

    private static final String GIT_RECEIVE_PACK = "git-receive-pack"; // push request (server receives a pack file from the client)
    private static final String GIT_UPLOAD_PACK = "git-upload-pack";   // pull request (server uploads a pack file to the client)

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

    private final AuthenticationContext authenticationContext;
    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 ServletContextFactory servletContextFactory;
    private final ThrottledScmRequestFactory throttledFactory;
    private final GitWriteTracker writeTracker;

    public DefaultGitHttpScmRequestHandler(AuthenticationContext authenticationContext,
                                           GitCommandBuilderFactory builderFactory, InternalGitScmConfig config,
                                           EventPublisher eventPublisher, HelpPathService helpPathService,
                                           HookService hookService, I18nService i18nService,
                                           RepositoryService repositoryService, RequestManager requestManager,
                                           ServletContextFactory servletContextFactory,
                                           ThrottledScmRequestFactory throttledFactory,
                                           GitWriteTracker writeTracker) {
        this.authenticationContext = authenticationContext;
        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.servletContextFactory = servletContextFactory;
        this.throttledFactory = throttledFactory;
        this.writeTracker = writeTracker;
    }

    /**
     * This implementation returns a git 'smart' request, an invalid git request, or {@link Optional#empty()} if the
     * request cannot be interpreted as a git request.
     *
     * @return an HTTP SCM request if the request type can be determined, based on the value of the 'service'
     *         parameter or the path info; or {@link Optional#empty()} if the request type cannot be determined.
     */
    @SuppressWarnings("ConstantConditions")
    @Override
    @Nonnull
    public Optional<HttpScmRequest> create(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response) {
        return createUnthrottled(request, response)
                .map(this::maybeThrottle);
    }

    @Override
    public void sendAuthenticationError(@Nonnull AuthenticationState state, @Nonnull String summary, @Nonnull String detailMessage,
                                        @Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response) throws IOException {
        GitHttpUtils.sendAuthenticationError(request, response, state, summary, detailMessage);
    }

    @Override
    public void sendError(@Nonnull String summary, @Nonnull String detailMessage,
                          @Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response) throws IOException {
        GitHttpUtils.sendError(request, response, summary, detailMessage);
    }

    @Override
    public boolean supports(@Nonnull HttpRequestDetails requestDetails) {
        return getRequestType(requestDetails).isPresent();
    }

    @Nonnull
    private Optional<HttpScmRequest> createUnthrottled(@Nonnull HttpServletRequest request,
                                                       @Nonnull HttpServletResponse response) {
        ServletContext servletContext = servletContextFactory.getServletContext();
        RequestContext requestContext = requestManager.getRequestContext();

        Optional<RequestType> optionalRequestType = getRequestType(HttpRequestDetails.ofServletRequest(request));
        if (!optionalRequestType.isPresent()) {
            return empty();
        }
        RequestType requestType = optionalRequestType.get();

        Repository repository = getRepositoryOrThrow(request, requestType);

        switch (requestType) {
            case OTHER_WRITE:
                return of(new HttpOtherWrite(repository, builderFactory, config, eventPublisher, i18nService,
                        request, response, servletContext, writeTracker, requestContext));
            case OTHER_READ:
                return of(new HttpOtherRead(repository, builderFactory, config, eventPublisher, i18nService,
                        request, response, servletContext, requestContext));
            case RECEIVE_PACK:
                return of(new HttpReceivePack(repository, builderFactory, config, eventPublisher, helpPathService,
                        hookService, i18nService, request, response, servletContext, writeTracker, requestContext));
            case UPLOAD_PACK:
                return of(new HttpUploadPack(repository, builderFactory, new UploadPackRequestAnalyzer(), config,
                        eventPublisher, i18nService, request, response, servletContext, requestContext));
            default:
                throw new IllegalStateException("Unhandled request type " + requestType);
        }
    }

    private Optional<RequestType> getRequestType(@Nonnull HttpRequestDetails request) {
        String service = request.getParameter("service");

        if (GIT_RECEIVE_PACK.equals(service)) {
            return of(RequestType.OTHER_WRITE);
        }
        if (GIT_UPLOAD_PACK.equals(service)) {
            return of(RequestType.OTHER_READ);
        }

        PathInfo info = PathInfo.of(request.getPathInfo());
        if (isPost(request) && info.isReceive()) {
            return of(RequestType.RECEIVE_PACK);
        }

        if (isPost(request) && info.isUpload()) {
            return of(RequestType.UPLOAD_PACK);
        }

        if (isGet(request) && info.isDumb()) {
            log.debug("It looks like we've received a 'dumb' protocol git request. service={}, pathInfo={}",
                    service, request.getPathInfo());
        }

        return empty();
    }

    /**
     * Parses the project key & repository slug from the url and returns the relevant repository.
     *
     * @param request the currently executing HttpServletRequest
     * @param requestType the type of request
     * @return the repository that the request is targeting
     * @throws NoSuchRepositoryException if the request doesn't specify a repository or the repository doesn't exist
     * @throws AuthorisationException if there are insufficient permissions
     */
    private Repository getRepositoryOrThrow(HttpServletRequest request, RequestType requestType) {
        // In case of a write request without authentication, we want to send the client a 401 so that they
        // authenticate. If they're authenticated, RepositoryAuthorisationCheck later checks whether they
        // actually have REPO_WRITE permission and sends a nice "insufficient permissions" if they don't.
        if (requestType.isWrite() && !authenticationContext.isAuthenticated()) {
            throw new AuthorisationException(i18nService.createKeyedMessage("bitbucket.scm.not.authenticated"));
        }
        if (request.getPathInfo() != null) {
            PathInfo pathInfo = PathInfo.of(request.getPathInfo());
            Optional<RepositoryPath> repoPath = pathInfo.getRepositoryPath();
            if (repoPath.isPresent()) {
                Repository repository = repositoryService.getBySlug(
                        repoPath.get().getNamespace(), repoPath.get().getProjectKey(), repoPath.get().getRepositorySlug());
                if (repository != null) {
                    return repository;
                }
            }
        }
        throw new NoSuchRepositoryException(i18nService.createKeyedMessage("bitbucket.scm.no.such.repository.detail"), null);
    }

    private boolean isGet(HttpRequestDetails request) {
        return request.getMethod().equals("GET");
    }

    private boolean isPost(HttpRequestDetails request) {
        return request.getMethod().equals("POST");
    }

    private HttpScmRequest maybeThrottle(HttpScmRequest request) {
        if (config.isRefAdvertisementThrottled() ||
                request instanceof HttpUploadPack || request instanceof HttpReceivePack) {
            return throttledFactory.throttled(request);
        }
        // ref advertisement does not have to be throttled and the request is ref advertisement for either a push
        // or a fetch/clone. No need to throttle.
        return request;
    }

    private enum RequestType {

        OTHER_WRITE(true),
        OTHER_READ(false),
        RECEIVE_PACK(true),
        UPLOAD_PACK(false);

        private final boolean write;

        RequestType(boolean write) {
            this.write = write;
        }

        public boolean isWrite() {
            return write;
        }
    }
}
