package com.atlassian.bitbucket.internal.ssh.servlet;

import com.atlassian.bitbucket.AuthorisationException;
import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.ssh.InternalSshKeyService;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.project.NoSuchProjectException;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.project.ProjectService;
import com.atlassian.bitbucket.repository.NoSuchRepositoryException;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.NoSuchUserException;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.bitbucket.user.UserType;
import com.atlassian.sal.api.auth.AuthenticationController;
import com.atlassian.sal.api.auth.LoginUriProvider;
import com.atlassian.sal.api.xsrf.XsrfTokenValidator;
import com.google.common.base.Strings;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A simple controller is used for SSH keys (as opposed to separate servlets) because the different SSH key pages have
 * overlapping URL schemes, which cannot be separated using {@code url-pattern} entries in atlassian-plugins.xml.
 * <p>
 * In addition to delegating the request to the correct {@link SshKeysRequestHandler handler}, this controller takes
 * care of the common logic. It
 *
 * <ul>
 *     <li>parses the request</li>
 *     <li>performs authorisation checks</li>
 *     <li>translates {@link NoSuchEntityException} and {@link AuthorisationException} into the correct HTTP error code</li>
 *     <li>delegates to the appropriate {@link SshKeysRequestHandler handler}</li>
 * </ul>
 */
public class SshKeysController extends HttpServlet {

    protected static final Pattern PROJECT_PATTERN = Pattern.compile("/projects/([^/]+)/keys(/add)?");
    protected static final Pattern REPO_PATTERN = Pattern.compile("/projects/([^/]+)/repos/([^/]+)/keys(/add)?");
    protected static final Pattern USER_ADMIN_PATTERN = Pattern.compile("/admin/users/([^/]+)/keys(/add)?");
    protected static final Pattern USER_PROFILE_PATTERN = Pattern.compile("/account/keys(/add)?");

    private final AuthenticationContext authenticationContext;
    private final AuthenticationController authenticationController;
    private final I18nService i18nService;
    private final InternalSshKeyService keyService;
    private final LoginUriProvider loginUriProvider;
    private final NavBuilder navBuilder;
    private final PermissionValidationService permissionValidationService;
    private final ProjectService projectService;
    private final RepositoryService repositoryService;
    private final UserService userService;
    private final XsrfTokenValidator xsrfTokenValidator;

    private final SshKeysRequestHandler addHandler;
    private final SshKeysRequestHandler notFoundHandler;
    private final SshKeysRequestHandler viewHandler;

    public SshKeysController(
            AuthenticationContext authenticationContext,
            AuthenticationController authenticationController,
            I18nService i18nService,
            InternalSshKeyService keyService,
            LoginUriProvider loginUriProvider,
            NavBuilder navBuilder,
            PermissionValidationService permissionValidationService,
            ProjectService projectService,
            RepositoryService repositoryService,
            UserService userService,
            XsrfTokenValidator xsrfTokenValidator,
            SshKeysRequestHandler addHandler,
            SshKeysRequestHandler notFoundHandler,
            SshKeysRequestHandler viewHandler) {

        this.authenticationContext = authenticationContext;
        this.addHandler = addHandler;
        this.notFoundHandler = notFoundHandler;
        this.viewHandler = viewHandler;

        this.authenticationController = authenticationController;
        this.i18nService = i18nService;
        this.keyService = keyService;
        this.loginUriProvider = loginUriProvider;
        this.navBuilder = navBuilder;
        this.permissionValidationService = permissionValidationService;
        this.projectService = projectService;
        this.repositoryService = repositoryService;
        this.userService = userService;
        this.xsrfTokenValidator = xsrfTokenValidator;
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        SshKeysRequest keysRequest = parseRequest(req);
        getHandler(keysRequest, req).get(keysRequest, req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if (!xsrfTokenValidator.validateFormEncodedToken(req)) {
            req.getRequestDispatcher(navBuilder.xsrfNotification().buildRelNoContext()).forward(req, resp);
            return;
        }

        SshKeysRequest keysRequest = parseRequest(req);
        getHandler(keysRequest, req).post(keysRequest, req, resp);
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if (authenticationController.shouldAttemptAuthentication(req)) {
            sendRedirectToLogin(req, resp);
            return;
        }

        try {
            super.service(req, resp);
        } catch (NoSuchEntityException e) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getLocalizedMessage());
        } catch (AuthorisationException e) {
            resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getLocalizedMessage());
        }
    }

    private Project getProjectIfAdminOrThrow(String projectKey) {
        Project project = projectService.getByKey(projectKey);
        if (project == null) {
            // Don't leak which repositories do/don't exist to anonymous users - throw authentication error either way
            permissionValidationService.validateAuthenticated();
            throw new NoSuchProjectException(i18nService.createKeyedMessage("bitbucket.ssh.nosuchproject", projectKey));
        }
        permissionValidationService.validateForProject(project, Permission.PROJECT_ADMIN);

        return project;
    }

    private Repository getRepositoryIfAdminOrThrow(String projectKey, String repoSlug) {
        Repository repository = repositoryService.getBySlug(projectKey, repoSlug);
        if (repository == null) {
            // Don't leak which repositories do/don't exist to anonymous users - throw authentication error either way
            permissionValidationService.validateAuthenticated();
            throw new NoSuchRepositoryException(i18nService.createKeyedMessage("bitbucket.ssh.nosuchrepo", projectKey, repoSlug),
                    projectService.getByKey(projectKey));
        }
        permissionValidationService.validateForRepository(repository, Permission.REPO_ADMIN);

        return repository;
    }

    private ApplicationUser getUserIfCanEditKeysOrThrow(String userSlug) {
        ApplicationUser user = userService.getUserBySlug(userSlug);
        if (user == null || user.getType() == UserType.SERVICE) {
            throw new NoSuchUserException(i18nService.createKeyedMessage("bitbucket.ssh.nosuchuser", userSlug), userSlug);
        }

        if (!keyService.canEditSshKeyForUser(user)) {
            throw new AuthorisationException(i18nService.createKeyedMessage("bitbucket.ssh.keys.not.authorized", user.getName()));
        }
        return user;
    }

    private SshKeysRequest parseRequest(HttpServletRequest request) {
        String path = request.getPathInfo();
        if (StringUtils.isBlank(path)) {
            return null;
        }

        SshKeyForm model = extractModel(request);
        String nextUrl = request.getParameter("next");

        Matcher matcher = USER_PROFILE_PATTERN.matcher(path);

        if (matcher.matches() && authenticationContext.isAuthenticated()) {
            return new SshKeysRequest(false, null, null, authenticationContext.getCurrentUser(), model, nextUrl);
        }

        matcher = USER_ADMIN_PATTERN.matcher(path);
        if (matcher.matches()) {
            ApplicationUser user = getUserIfCanEditKeysOrThrow(matcher.group(1));
            return new SshKeysRequest(true, null, null, user, model, nextUrl);
        }

        matcher = REPO_PATTERN.matcher(path);
        if (matcher.matches()) {
            Repository repository = getRepositoryIfAdminOrThrow(matcher.group(1), matcher.group(2));
            return new SshKeysRequest(false, null, repository, null, model, nextUrl);
        }

        matcher = PROJECT_PATTERN.matcher(path);
        if (matcher.matches()) {
            Project project = getProjectIfAdminOrThrow(matcher.group(1));
            return new SshKeysRequest(false, project, null, null, model, nextUrl);
        }

        return null;
    }

    private void sendRedirectToLogin(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String requestUri = req.getRequestURI();
        String contextPath = req.getContextPath();
        if (!Strings.isNullOrEmpty(contextPath)) {
            requestUri = requestUri.substring(contextPath.length());
        }
        resp.sendRedirect(loginUriProvider.getLoginUri(URI.create(requestUri)).toString());
    }

    private SshKeyForm extractModel(HttpServletRequest request) {
        String text = request.getParameter("text");
        String readOnly = request.getParameter("readOnly");
        return new SshKeyForm(Strings.emptyToNull(text), readOnly == null ? true : Boolean.valueOf(readOnly));
    }

    private SshKeysRequestHandler getHandler(SshKeysRequest keysRequest, HttpServletRequest request) {
        if (keysRequest == null) {
            return notFoundHandler;
        }
        return request.getPathInfo().endsWith("/add") ? addHandler : viewHandler;
    }
}
