package com.atlassian.stash.internal.rest.project;

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.project.*;
import com.atlassian.bitbucket.rest.BadRequestException;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.RestResource;
import com.atlassian.bitbucket.rest.project.RestProject;
import com.atlassian.bitbucket.rest.util.CachePolicies;
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.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.plugins.rest.common.multipart.FilePart;
import com.atlassian.plugins.rest.common.multipart.MultipartConfigClass;
import com.atlassian.plugins.rest.common.multipart.MultipartFormParam;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.stash.internal.rest.avatar.AvatarMultipartConfig;
import com.atlassian.stash.internal.rest.avatar.FilePartAvatarSupplier;
import com.sun.jersey.spi.container.ContainerRequest;
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 static com.atlassian.bitbucket.rest.util.ResourcePatterns.PROJECT_KEY_PATH;
import static com.atlassian.stash.internal.rest.util.RestPermissionUtils.parsePermission;
import static com.google.common.base.MoreObjects.firstNonNull;

@AnonymousAllowed
@Consumes({MediaType.APPLICATION_JSON})
@Path("projects")
@Produces({RestUtils.APPLICATION_JSON_UTF8})
@Singleton
public class ProjectResource extends RestResource {

    private static final String AVATAR_PATH = PROJECT_KEY_PATH + "/avatar.png";

    private final ProjectService projectService;

    public ProjectResource(ProjectService projectService, I18nService i18nService) {
        super(i18nService);

        this.projectService = projectService;
    }

    /**
     * Create a new project.
     * <p>
     * To include a custom avatar for the project, the project definition should contain an additional attribute with
     * the key <code>avatar</code> and the value a data URI containing Base64-encoded image data. The URI should be in
     * the following format:
     * <pre>
     *     data:(content type, e.g. image/png);base64,(data)
     * </pre>
     * If the data is not Base64-encoded, or if a character set is defined in the URI, or the URI is otherwise invalid,
     * <em>project creation will fail</em>.
     * <p>
     * The authenticated user must have <strong>PROJECT_CREATE</strong> permission to call this resource.
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestProject#REQUEST_EXAMPLE}
     *
     * @response.representation.201.mediaType application/json
     * @response.representation.201.qname project
     * @response.representation.201.example {@link RestProject#RESPONSE_EXAMPLE}
     * @response.representation.201.doc The newly created project.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#VALIDATION_EXAMPLE}
     * @response.representation.400.doc The project was not created due to a validation error.
     *
     * @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 create a
     * project.
     *
     * @response.representation.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc The project key or name is already in use.
     */
    @POST
    public Response createProject(@Context UriInfo uriInfo, RestProject projectParam) {
        String projectKey = projectParam.getKey();
        if (projectKey == null) {
            String message = i18nService.getMessage("bitbucket.rest.project.noprojectkey");
            throw new BadRequestException(message);
        }

        String name = projectParam.getName();
        if (name == null) {
            name = projectKey;
        }

        Project project = projectService.create(new ProjectCreateRequest.Builder()
                .key(projectKey)
                .name(name)
                .description(projectParam.getDescription())
                .avatarUri(projectParam.getAvatar())
                .publiclyAccessible(projectParam.isPublic())
                .build());

        URI projectURI = uriInfo.getRequestUriBuilder().path(project.getKey()).build();
        return ResponseFactory.created(projectURI)
                .entity(RestProject.REST_TRANSFORM.apply(project))
                .build();
    }

    /**
     * Delete the project matching the supplied <strong>projectKey</strong>.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project to call this
     * resource.
     *
     * @response.representation.204.doc The project matching the supplied <strong>projectKey</strong> was
     * deleted.
     *
     * @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 delete 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.
     *
     * @response.representation.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc The project can not be deleted as it contains repositories.
     */
    @DELETE
    @Path(PROJECT_KEY_PATH)
    public Response deleteProject(@Context Project project) {
        //if deleting failed because the project was deleted from under us then return no such project
        if (!projectService.delete(project)) {
            throw noSuchProjectException(project.getKey());
        }
        return ResponseFactory.noContent().build();
    }

    /**
     * Retrieve the project matching the supplied <strong>projectKey</strong>.
     * <p>
     * The authenticated user must have <strong>PROJECT_VIEW</strong> permission for the specified project to call this
     * resource.
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname project
     * @response.representation.200.example {@link RestProject#RESPONSE_EXAMPLE}
     * @response.representation.200.doc The project matching the supplied <strong>projectKey</strong>.
     *
     * @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 view 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.
     */
    @GET
    @Path(PROJECT_KEY_PATH)
    public Response getProject(@Context Project project) {
        return ResponseFactory.ok(RestProject.REST_TRANSFORM.apply(project)).build();
    }

    /**
     * Retrieve the avatar for the project matching the supplied <strong>projectKey</strong>.
     * <p>
     * The authenticated user must have <strong>PROJECT_VIEW</strong> permission for the specified project to call this
     * resource.
     *
     * @param size The desired size of the image. The server will return an image as close as possible to the specified
     *             size.
     *
     * @response.representation.200.mediaType image/png
     * @response.representation.200.doc The avatar of the project matching the supplied <strong>projectKey</strong>.
     *
     * @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 view 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.
     *
     * @since 2.1
     */
    @GET
    @Path(AVATAR_PATH)
    @Produces("image/png")
    public Response getProjectAvatar(@Context ContainerRequest request, @Context Project project, @QueryParam("s") @DefaultValue("0") int size) {
        return RestUtils.processAvatarResponse(request, CachePolicies.noCache(), projectService.getAvatar(project.getId(), size));
    }

    /**
     * Retrieve a page of projects.
     * <p>
     * Only projects for which the authenticated user has the <strong>PROJECT_VIEW</strong> permission will be returned.
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestProject#PAGE_EXAMPLE}
     * @response.representation.200.doc A page of projects.
     *
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The permission level is unknown or not related to projects.
     */
    @GET
    public Response getProjects(@QueryParam("name") String name, @QueryParam("permission") String permission,
                                @Context PageRequest pageRequest) {
        ProjectSearchRequest.Builder builder = new ProjectSearchRequest.Builder()
                .name(name)
                .permission(parsePermission(i18nService, permission, Project.class));
        Page<? extends Project> page = projectService.search(builder.build(), pageRequest);

        return ResponseFactory.ok(new RestPage<>(page, RestProject.REST_TRANSFORM)).build();
    }

    /**
     * Update the project matching the <strong>projectKey</strong> supplied in the resource path.
     * <p>
     * To include a custom avatar for the updated project, the project definition should contain an additional attribute
     * with the key <code>avatar</code> and the value a data URI containing Base64-encoded image data. The URI should be
     * in the following format:
     * <code>
     *     data:(content type, e.g. image/png);base64,(data)
     * </code>
     * If the data is not Base64-encoded, or if a character set is defined in the URI, or the URI is otherwise invalid,
     * <em>project creation will fail</em>.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project to call this
     * resource.
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestProject#REQUEST_EXAMPLE}
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname project
     * @response.representation.200.example {@link RestProject#RESPONSE_EXAMPLE}
     * @response.representation.200.doc The updated project. The project's key <strong>was not</strong> updated.
     *
     * @response.representation.201.mediaType application/json
     * @response.representation.201.qname project
     * @response.representation.201.example {@link RestProject#RESPONSE_EXAMPLE}
     * @response.representation.201.doc The updated project. The project's key <strong>was</strong> updated.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#VALIDATION_EXAMPLE}
     * @response.representation.400.doc The project was not updated due to a validation error.
     *
     * @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 update 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.
     */
    @PUT
    @Path(PROJECT_KEY_PATH)
    public Response updateProject(@Context Project project, RestProject projectParam, @Context UriInfo uriInfo) {

        ProjectUpdateRequest.Builder builder = new ProjectUpdateRequest.Builder(project)
                .key(firstNonNull(projectParam.getKey(), project.getKey()))
                .name(firstNonNull(projectParam.getName(), project.getName()))
                .description(projectParam.getDescription() != null ? projectParam.getDescription() : project.getDescription());
        if (projectParam.hasPublic()) {
            builder.publiclyAccessible(projectParam.isPublic());
        }
        Project updatedProject = projectService.update(builder.build());
        if (projectParam.hasAvatar()) {
            projectService.updateAvatar(project.getId(), projectParam.getAvatar());
        }

        if (updatedProject.getKey().equals(project.getKey())) {
            return ResponseFactory.ok(RestProject.REST_TRANSFORM.apply(updatedProject))
                    .build();
        }

        // key was changed
        String requestPath = uriInfo.getRequestUri().getPath();
        int index = requestPath.toLowerCase().lastIndexOf(project.getKey().toLowerCase());
        String newPath = requestPath.substring(0, index) + updatedProject.getKey();
        return ResponseFactory.created(uriInfo.getRequestUriBuilder().replacePath(newPath).build())
                .entity(RestProject.REST_TRANSFORM.apply(updatedProject))
                .build();
    }

    /**
     * Update the avatar for the project matching the supplied <strong>projectKey</strong>.
     * <p>
     * This resource accepts POST multipart form data, containing a single image in a form-field named 'avatar'.
     * <p>
     * There are configurable server limits on both the dimensions (1024x1024 pixels by default) and uploaded file size
     * (1MB by default). Several different image formats are supported, but <strong>PNG</strong> and
     * <strong>JPEG</strong> are preferred due to the file size limit.
     * <p>
     * This resource has Cross-Site Request Forgery (XSRF) protection. To allow the request to
     * pass the XSRF check the caller needs to send an <code>X-Atlassian-Token</code> HTTP header with the
     * value <code>no-check</code>.
     * <p>
     * An example <a href="http://curl.haxx.se/">curl</a> request to upload an image name 'avatar.png' would be:
     * <pre>
     * curl -X POST -u username:password -H "X-Atlassian-Token: no-check" http://example.com/rest/api/1.0/projects/STASH/avatar.png -F avatar=@avatar.png
     * </pre>
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project to call this
     * resource.
     *
     * @request.representation.mediaType multipart/form-data
     *
     * @response.representation.201.doc The avatar was uploaded successfully.
     *
     * @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 update 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.
     *
     * @since 2.1
     */
    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @MultipartConfigClass(AvatarMultipartConfig.class)
    @Path(AVATAR_PATH)
    public Response uploadAvatar(@Context Project project, @Context UriInfo uriInfo,
                                 @MultipartFormParam("avatar") final FilePart file) {
        projectService.updateAvatar(project.getId(), new FilePartAvatarSupplier(file));

        return Response.created(uriInfo.getRequestUri()).build();
    }
}
