package com.atlassian.bitbucket.internal.key.ssh.rest;

import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.ssh.rest.RestSshKey;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.project.ProjectService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.rest.BadRequestException;
import com.atlassian.bitbucket.rest.RestErrorMessage;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.project.RestProject;
import com.atlassian.bitbucket.rest.repository.RestRepository;
import com.atlassian.bitbucket.rest.util.ResourcePatterns;
import com.atlassian.bitbucket.rest.util.ResponseFactory;
import com.atlassian.bitbucket.rest.util.RestPage;
import com.atlassian.bitbucket.rest.util.RestUtils;
import com.atlassian.bitbucket.ssh.SetSshAccessKeyRequest;
import com.atlassian.bitbucket.ssh.SshAccessKey;
import com.atlassian.bitbucket.ssh.SshAccessKeyService;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.sun.jersey.spi.resource.Singleton;

import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.Collection;
import java.util.function.Function;

import static java.util.stream.Collectors.toSet;

@AnonymousAllowed
@Consumes({MediaType.APPLICATION_JSON})
@Produces({RestUtils.APPLICATION_JSON_UTF8})
@Singleton
@Path("/")
public class SshAccessKeyResource {

    private static final String SUFFIX_SSH = "/ssh";

    private final SshAccessKeyService accessKeyService;
    private final I18nService i18nService;
    private final ProjectService projectService;
    private final RepositoryService repositoryService;

    private final Function<RestProject, Project> projectLookup = new Function<RestProject, Project>() {
        @Override
        public Project apply(RestProject restProj) {
            Project project = null;

            if (restProj.getId() > 0) {
                project = projectService.getById(restProj.getId());
            }
            if (project == null && restProj.getKey() != null) {
                project = projectService.getByKey(restProj.getKey());
            }
            if (project == null) {
                throw newNoResourceFoundException();
            }

            return project;
        }
    };

    private Function<RestRepository, Repository> repositoryLookup = new Function<RestRepository, Repository>() {
        @Override
        public Repository apply(RestRepository restRepo) {
            Repository repo = null;

            if (restRepo.getId() > 0) {
                repo = repositoryService.getById(restRepo.getId());
            }
            if (repo == null && restRepo.getSlug() != null &&
                    restRepo.getProject() != null && restRepo.getProject().getKey() != null) {
                repo = repositoryService.getBySlug(restRepo.getProject().getKey(), restRepo.getSlug());
            }
            if (repo == null) {
                throw newNoResourceFoundException();
            }

            return repo;
        }
    };

    public SshAccessKeyResource(SshAccessKeyService accessKeyService, I18nService i18nService,
                                ProjectService projectService, RepositoryService repositoryService) {
        this.accessKeyService = accessKeyService;
        this.i18nService = i18nService;
        this.projectService = projectService;
        this.repositoryService = repositoryService;
    }

    /**
     * Register a new SSH key and grants access to the project identified in the URL.
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestSshAccessKey#PROJECT_REQUEST_EXAMPLE}
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.example {@link RestSshAccessKey#PROJECT_RESPONSE_EXAMPLE}
     * @response.representation.200.doc The newly created access key
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The current request contains invalid or missing values.
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to add an access
     *                                  key to the project.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified project does not exist
     */
    @POST
    @Path(ResourcePatterns.PROJECT_URI + SUFFIX_SSH)
    public Response addForProject(@Context Project project, RestSshAccessKey accessKey,
                                  @Context UriInfo uriInfo) {
        if (accessKey == null || !accessKey.hasKey()) {
            return ResponseFactory.badRequest("Missing key").build();
        }

        try {
            RestSshKey key = accessKey.getKey();
            SshAccessKey result = accessKeyService.set(new SetSshAccessKeyRequest.Builder()
                            .key(accessKey.getKey().getText(), key.getLabel())
                            .projectAccess(project, accessKey.hasPermission() ? accessKey.getPermission() : Permission.PROJECT_READ)
                            .build()
            );
            URI uri = uriInfo.getRequestUriBuilder()
                    .replaceQuery(null) // remove query string
                    .path(Integer.toString(accessKey.getKey().getId()))
                    .build();
            return ResponseFactory
                    .created(uri)
                    .entity(new RestSshAccessKey(result))
                    .build();
        } catch (IllegalArgumentException e) {
            return ResponseFactory.badRequest(e.getMessage()).build();
        }
    }

    /**
     * Register a new SSH key and grants access to the repository identified in the URL.
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestSshAccessKey#REPOSITORY_REQUEST_EXAMPLE}
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.example {@link RestSshAccessKey#REPOSITORY_RESPONSE_EXAMPLE}
     * @response.representation.200.doc The newly created access key
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The current request contains invalid or missing values.
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to add an access
     *                                  key to the repository
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified repository does not exist
     */
    @POST
    @Path(ResourcePatterns.REPOSITORY_URI + SUFFIX_SSH)
    public Response addForRepository(@Context Repository repository, RestSshAccessKey accessKey,
                                     @Context UriInfo uriInfo) {
        if (accessKey == null || !accessKey.hasKey()) {
            return ResponseFactory.badRequest("Missing key").build();
        }

        try {
            Permission permission = accessKey.getPermission();
            RestSshKey key = accessKey.getKey();
            SshAccessKey result = accessKeyService.set(new SetSshAccessKeyRequest.Builder()
                            .key(accessKey.getKey().getText(), key.getLabel())
                            .repositoryAccess(repository, permission != null ? permission : Permission.REPO_READ)
                            .build()
            );
            URI uri = uriInfo.getRequestUriBuilder()
                    .replaceQuery(null) // remove query string
                    .path(Integer.toString(accessKey.getKey().getId()))
                    .build();
            return ResponseFactory
                    .created(uri)
                    .entity(new RestSshAccessKey(result))
                    .build();
        } catch (IllegalArgumentException e) {
            return ResponseFactory.badRequest(e.getMessage()).build();
        }
    }

    /**
     * Remove an existing access key for the projects and repositories in the submitted entity. If the same SSH key is
     * used as an access key for multiple projects or repositories not supplied, only the access to the projects
     * or repositories identified will be revoked.
     *
     * @param keyId the identifier of the SSH key
     *
     * @response.representation.204.mediaType application/json
     * @response.representation.204.doc The access keys were deleted (or none was found matching the given id and
     *                                  repositories or projects).
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to remove
     *                                  access keys for one or more of the specified projects or repositories
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc On or more of the specified repositories or projects does not exist or the key
     *                                  itself does not exist
     */
    @DELETE
    @Path(SUFFIX_SSH + "/{keyId}")
    public Response revokeMany(@PathParam("keyId") Integer keyId, RestSshAccessKeyLocations locations) {
        Collection<RestProject> projects = ensureAllLocationsNonNull("projects", locations.getProjects());
        Collection<RestRepository> repositories = ensureAllLocationsNonNull("repositories", locations.getRepositories());

        accessKeyService.revoke(keyId,
                repositories.stream().map(repositoryLookup).collect(toSet()),
                projects.stream().map(projectLookup).collect(toSet()));

        return Response.noContent().build();
    }

    /**
     * Retrieves the access keys for the project identified in the URL.
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestSshAccessKey#PAGE_PROJECT_EXAMPLE}
     * @response.representation.200.doc A single page of access keys associated with the project.
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to retrieve the
     *                                  access keys for this project
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified project does not exist
     */
    @GET
    @Path(ResourcePatterns.PROJECT_URI + SUFFIX_SSH)
    public Response getForProject(@Context Project project, @Context PageRequest pageRequest) {
        Page<SshAccessKey> page = accessKeyService.findByProject(project, pageRequest);
        return ResponseFactory.ok(new RestPage<>(page, RestSshAccessKey.REST_TRANSFORM)).build();
    }

    /**
     * Retrieves the access keys for the repository identified in the URL.
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestSshAccessKey#PAGE_REPOSITORY_EXAMPLE}
     * @response.representation.200.doc A single page of access keys for the repository.
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to retrieve the
     *                                  access keys for this repository
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified repository does not exist
     */
    @GET
    @Path(ResourcePatterns.REPOSITORY_URI + SUFFIX_SSH)
    public Response getForRepository(@Context Repository repository, @Context PageRequest pageRequest) {
        Page<SshAccessKey> page = accessKeyService.findByRepository(repository, pageRequest);
        return ResponseFactory.ok(new RestPage<>(page, RestSshAccessKey.REST_TRANSFORM)).build();
    }

    /**
     * Retrieves the access key for the SSH key with id <code>keyId</code> on the repository identified in the URL.
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestSshAccessKey#PAGE_REPOSITORY_EXAMPLE}
     * @response.representation.200.doc the access key for the repository and SSH key with ID {@code keyId}.
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to retrieve the
     *                                  access keys for this repository
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified repository or key does not exist or the key does not have access
     *                                  on the repository
     */
    @GET
    @Path(ResourcePatterns.PROJECT_URI + SUFFIX_SSH + "/{keyId}")
    public Response getForProject(@Context Project project, @PathParam("keyId") Integer keyId) {
        return accessKeyService.getByKeyAndProject(keyId, project)
                .map(accessKey -> ResponseFactory.ok(new RestSshAccessKey(accessKey)).build())
                .orElse(notFound(project, keyId));
    }

    /**
     * Retrieves the access key for the SSH key with id {@code keyId} on the repository identified in the URL.
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestSshAccessKey#PAGE_REPOSITORY_EXAMPLE}
     * @response.representation.200.doc the access key for the repository and SSH key with ID {@code keyId}.
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to retrieve the
     *                                  access keys for this repository
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified repository or key does not exist or the key does not have access
     *                                  on the repository
     */
    @GET
    @Path(ResourcePatterns.REPOSITORY_URI + SUFFIX_SSH + "/{keyId}")
    public Response getForRepository(@Context Repository repository, @PathParam("keyId") Integer keyId) {
        return accessKeyService.getByKeyAndRepository(keyId, repository)
                .map(accessKey -> ResponseFactory.ok(new RestSshAccessKey(accessKey)).build())
                .orElse(notFound(repository, keyId));
    }

    /**
     * Retrieves all project-related access keys for the SSH key with id {@code keyId}. If the current user is not an
     * admin any of the projects the key provides access to, none are returned.
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.doc the SSH key with ID {@code keyId}.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified key does not exist
     *
     * @since 2.12
     */
    @GET
    @Path(SUFFIX_SSH + "/{keyId}/projects")
    public Response getForProjects(@PathParam("keyId") Integer keyId, @Context PageRequest pageRequest) {
        return ResponseFactory.ok(new RestPage<>(
                accessKeyService.findByKeyForProjects(keyId, pageRequest), RestSshAccessKey.REST_TRANSFORM)).build();
    }

    /**
     * Retrieves all repository-related access keys for the SSH key with id {@code keyId}. If the current user is not
     * an admin of any of the projects the key provides access to, none are returned.
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.doc the SSH key with ID {@code keyId}.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified key does not exist
     *
     * @since 2.12
     */
    @GET
    @Path(SUFFIX_SSH + "/{keyId}/repos")
    public Response getForRepositories(@PathParam("keyId") Integer keyId, @Context PageRequest pageRequest) {
        return ResponseFactory.ok(new RestPage<>(
                accessKeyService.findByKeyForRepositories(keyId, pageRequest), RestSshAccessKey.REST_TRANSFORM)).build();
    }

    /**
     * Remove an existing access key for the project identified in the URL. If the same SSH key is used as an access
     * key for multiple projects or repositories, only the access to the project identified in the URL will be
     * revoked.
     *
     * @param keyId the identifier of the SSH key
     *
     * @response.representation.204.mediaType application/json
     * @response.representation.204.doc The access key was deleted (or none was found matching the given id).

     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to remove
     *                                  access keys for this project
     */
    @DELETE
    @Path(ResourcePatterns.PROJECT_URI + SUFFIX_SSH + "/{keyId}")
    public Response revokeForProject(@Context Project project, @PathParam("keyId") Integer keyId) {
        accessKeyService.revoke(keyId, project);

        return ResponseFactory.noContent().build();
    }

    /**
     * Remove an existing access key for the repository identified in the URL. If the same SSH key is used as an access
     * key for multiple projects or repositories, only the access to the repository identified in the URL will be
     * revoked.
     *
     * @param keyId the identifier of the SSH key
     *
     * @response.representation.204.mediaType application/json
     * @response.representation.204.doc The access key was deleted (or none was found matching the given id).

     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to remove
     *                                  access keys for this repository
     */
    @DELETE
    @Path(ResourcePatterns.REPOSITORY_URI + SUFFIX_SSH + "/{keyId}")
    public Response revokeForRepository(@Context Repository repository, @PathParam("keyId") Integer keyId) {
        accessKeyService.revoke(keyId, repository);

        return ResponseFactory.noContent().build();
    }

    /**
     * Updates the permission granted to the specified SSH key to the project identified in the URL.
     *
     * @param keyId the SSH key ID
     * @param permission the new permission to be granted to the SSH key
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.example {@link RestSshAccessKey#PROJECT_RESPONSE_EXAMPLE}
     * @response.representation.200.doc The newly created access key
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions on the project
     *                                  to edit its access keys
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified project does not exist
     */
    @PUT
    @Path(ResourcePatterns.PROJECT_URI + SUFFIX_SSH + "/{keyId}/permission/{permission}")
    public Response updatePermission(@Context Project project, @PathParam("keyId") Integer keyId,
                                     @PathParam("permission") Permission permission) {
        try {
            SshAccessKey accessKey = accessKeyService.set(new SetSshAccessKeyRequest.Builder()
                    .key(keyId)
                    .projectAccess(project, permission)
                    .build());

            return ResponseFactory.ok(new RestSshAccessKey(accessKey)).build();
        } catch (IllegalArgumentException e) {
            return ResponseFactory.badRequest(e.getMessage()).build();
        }
    }

    /**
     * Updates the permission granted to the specified SSH key to the repository identified in the URL.
     *
     * @param keyId the SSH key ID
     * @param permission the new permission to be granted to the SSH key
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.example {@link RestSshAccessKey#REPOSITORY_RESPONSE_EXAMPLE}
     * @response.representation.200.doc The newly created access key
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions on the repository
     *                                  to edit its access keys
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified repository does not exist
     */
    @PUT
    @Path(ResourcePatterns.REPOSITORY_URI + SUFFIX_SSH + "/{keyId}/permission/{permission}")
    public Response updatePermission(@Context Repository repository, @PathParam("keyId") Integer keyId,
                                     @PathParam("permission") Permission permission) {

        try {
            SshAccessKey accessKey = accessKeyService.set(new SetSshAccessKeyRequest.Builder()
                    .key(keyId)
                    .repositoryAccess(repository, permission)
                    .build());

            return ResponseFactory.ok(new RestSshAccessKey(accessKey)).build();
        } catch (IllegalArgumentException e) {
            return ResponseFactory.badRequest(e.getMessage()).build();
        }
    }

    private <T> Collection<T> ensureAllLocationsNonNull(String locationType, Collection<T> resources) {
        for (T resource : resources) {
            if (resource == null) {
                throw new BadRequestException(locationType,
                        i18nService.getMessage("bitbucket.service.ssh.key.access.resource.null", locationType));
            }
        }
        return resources;
    }

    private Response notFound(String message) {
        return ResponseFactory.status(Response.Status.NOT_FOUND)
                .entity(new RestErrorMessage(message))
                .type(RestUtils.APPLICATION_JSON_UTF8)
                .build();
    }

    private Response notFound(Project project, Integer keyId) {
        return notFound(i18nService.getMessage("bitbucket.ssh.project.nosuchkeyaccess", keyId, project.getKey()));
    }

    private Response notFound(Repository repository, Integer keyId) {
        return notFound(i18nService.getMessage("bitbucket.ssh.repo.nosuchkeyaccess", keyId,
                repository.getProject().getKey(), repository.getSlug()));
    }

    private NoSuchEntityException newNoResourceFoundException() {
        throw new NoSuchEntityException(i18nService.createKeyedMessage(
                "bitbucket.service.ssh.key.access.resource.notfound"));
    }
}
