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

import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.rest.RestResource;
import com.atlassian.bitbucket.rest.BadRequestException;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.util.ResourcePatterns;
import com.atlassian.bitbucket.rest.util.ResponseFactory;
import com.atlassian.bitbucket.rest.util.RestUtils;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.bitbucket.project.NoSuchProjectException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.project.ProjectService;
import com.atlassian.bitbucket.rest.RestErrorMessage;
import com.atlassian.bitbucket.rest.util.RestPage;
import com.atlassian.bitbucket.rest.repository.RestRepository;
import com.atlassian.bitbucket.scm.AvailableScm;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.google.common.collect.Iterables;
import com.sun.jersey.spi.resource.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.atlassian.bitbucket.rest.util.ResourcePatterns.*;

@AnonymousAllowed
@Consumes(MediaType.APPLICATION_JSON)
@Path(ResourcePatterns.PROJECT_URI + "/repos")
@Produces(RestUtils.APPLICATION_JSON_UTF8)
@Singleton
public class RepositoryResource extends RestResource {

    private static final Pattern PATTERN_LOCATION = Pattern.compile("(.+projects/)([^/]+)(/repos/)(.+)$");

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

    private final ProjectService projectService;
    private final RepositoryService repositoryService;
    private final ScmService scmService;

    public RepositoryResource(I18nService i18nService, ProjectService projectService,
                              RepositoryService repositoryService, ScmService scmService) {
        super(i18nService);

        this.projectService = projectService;
        this.repositoryService = repositoryService;
        this.scmService = scmService;
    }

    /**
     * Create a new repository. Requires an existing project in which this repository will be created.
     * The only parameters which will be used are name and scmId.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the context project to call this
     * resource.
     *
     * @param project the project in which the new repository will be created
     *
     * @request.representation.example {@link RestRepository#REQUEST_EXAMPLE}
     *
     * @response.representation.201.mediaType application/json
     * @response.representation.201.qname repository
     * @response.representation.201.example {@link RestRepository#RESPONSE_EXAMPLE}
     * @response.representation.201.doc The newly created repository.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The repository 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
     * repository.
     *
     * @response.representation.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc A repository with same name already exists.
     */
    @POST
    public Response createRepository(@Context Project project,
                                     @Context UriInfo uriInfo,
                                     RestRepository restRepository) {
        String name = restRepository.getName();
        if (name == null) {
            throw new BadRequestException(i18nService.getMessage("bitbucket.rest.repository.noreponame"));
        }

        String scmId = restRepository.getScmId();
        if (StringUtils.isBlank(scmId)) {
            Set<AvailableScm> scms = scmService.getAvailable();
            if (scms.size() == 1) {
                //If only a single SCM is available, which will usually be the case, default to it. This was
                //the behavior for the REST endpoint before multiple SCM support was added
                scmId = Iterables.getOnlyElement(scms).getId();
                log.debug("{}: No SCM was supplied; defaulting to {}", name, scmId);
            } else {
                throw new BadRequestException(i18nService.getMessage("bitbucket.rest.repository.noscm",
                        Iterables.toString(Iterables.transform(scms, AvailableScm::getId))));
            }
        }

        RepositoryCreateRequest request = new RepositoryCreateRequest.Builder()
                .forkable(restRepository.isForkable())
                .name(name)
                .project(project)
                .scmId(scmId)
                .publiclyAccessible(restRepository.isPublic())
                .build();
        Repository repository = repositoryService.create(request);
        URI repoURI = uriInfo.getRequestUriBuilder().path(repository.getSlug()).build();

        return ResponseFactory.created(repoURI)
                .entity(transform(repository))
                .build();
    }


    /**
     * Schedule the repository matching the supplied <strong>projectKey</strong> and <strong>repositorySlug</strong> to
     * be deleted. If the request repository is not present
     * <p>
     * The authenticated user must have <strong>REPO_ADMIN</strong> permission for the specified repository to call this
     * resource.
     *
     *  @param projectKey the parent project key
     *  @param repositorySlug the repository slug
     *
     * @response.representation.202.mediaType application/json
     * @response.representation.202.qname message
     * @response.representation.202.example {@link #DELETION_SCHEDULED_EXAMPLE}
     * @response.representation.202.doc The repository has been scheduled for deletion.
     *
     * @response.representation.204.doc No repository matching the supplied <strong>projectKey</strong> and
     * <strong>repositorySlug</strong> was found.
     *
     * @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
     * repository.
     */
    @DELETE
    @Path(REPOSITORY_SLUG_PATH)
    public Response deleteRepository(@PathParam(PROJECT_KEY) String projectKey,
                                     @PathParam(REPOSITORY_SLUG) String repositorySlug) {
        Repository repository = repositoryService.getBySlug(projectKey, repositorySlug);
        if (repository == null) {
            return ResponseFactory.noContent().build();
        }

        repositoryService.delete(repository);

        return ResponseFactory.status(Response.Status.ACCEPTED)
                .entity(new RestErrorMessage(i18nService.getMessage("bitbucket.rest.repository.deletionscheduled")))
                .build();
    }

    /**
     * Create a new repository forked from an existing repository.
     * <p>
     * The JSON body for this {@code POST} is not required to contain <i>any</i> properties. Even the name may be
     * omitted. The following properties will be used, if provided:
     * <ul>
     *     <li>{@code "name":"Fork name"} - Specifies the forked repository's name
     *     <ul>
     *         <li>Defaults to the name of the origin repository if not specified</li>
     *     </ul>
     *     </li>
     *     <li>{@code "project":{"key":"TARGET_KEY"}} - Specifies the forked repository's target project by key
     *     <ul>
     *         <li>Defaults to the current user's personal project if not specified</li>
     *     </ul>
     *     </li>
     * </ul>
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the specified repository and
     * <strong>PROJECT_ADMIN</strong> on the target project to call this resource. Note that users <i>always</i>
     * have <b>PROJECT_ADMIN</b> permission on their personal projects.
     *
     * @request.representation.example {@link RestRepository#REQUEST_EXAMPLE_WITH_PROJECT}
     *
     * @response.representation.201.mediaType application/json
     * @response.representation.201.qname repository
     * @response.representation.201.example {@link RestRepository#RESPONSE_EXAMPLE_WITH_ORIGIN}
     * @response.representation.201.doc The newly created fork.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc A validation error prevented the fork from being created. Possible validation
     *                                  errors include: The name or slug for the fork collides with another repository
     *                                  in the target project; an SCM type was specified in the JSON body; a project
     *                                  was specified in the JSON body without a "key" property.
     *
     * @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
     *                                  fork.
     *
     * @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, or, if a target project was specified,
     *                                  the target project does not exist.
     */
    @POST
    @Path(REPOSITORY_SLUG_PATH)
    public Response forkRepository(@Context Repository repository, @Context UriInfo uriInfo, RestRepository restFork) {
        //This check is performed explicitly to handle the case where a caller attempts to change the SCM type, since
        //such a change is not possible. If the SCM ID is not provided, or matches the current type, either is fine.
        String scmId = restFork.getScmId();
        if (!(scmId == null || scmId.equals(repository.getScmId()))) {
            String message = i18nService.getMessage("bitbucket.rest.repository.forkwithtype");
            throw new BadRequestException(message);
        }

        //If the caller specifies a target project, retrieve the real project to verify:
        //1. That it exists
        //2. That the user can see it
        //The real project then replaces the minimal project required on the REST request, and will be used as the
        //target when the fork is created.
        Project project = restFork.getProject();
        if (project != null) {
            String projectKey = project.getKey();
            if (StringUtils.isBlank(projectKey)) {
                throw new BadRequestException(i18nService.getMessage("bitbucket.rest.repository.fork.missingprojectkey"));
            }

            project = projectService.getByKey(projectKey);
            if (project == null) {
                throw new NoSuchProjectException(i18nService.createKeyedMessage("bitbucket.rest.repository.fork.nosuchproject",
                        projectKey));
            }
        }

        RepositoryForkRequest request = new RepositoryForkRequest.Builder(repository)
                .forkable(restFork.isForkable())
                .name(restFork.getName())
                .project(project)
                .build();
        return created(repositoryService.fork(request), uriInfo);
    }

    /**
     * Retrieve repositories which have been forked from this one. Unlike {@link #getRelatedRepositories(Repository,
     * PageRequest) related repositories}, this only looks at a given repository's direct forks. If those forks have
     * themselves been the origin of more forks, such "grandchildren" repositories will not be retrieved.
     * <p>
     * Only repositories to which the authenticated user has <b>REPO_READ</b> permission will be included, even
     * if other repositories have been forked from this one.
     *
     * @param repository  the repository to retrieve related repositories for
     * @param pageRequest defines the starting offset of the first repository to return, and the quantity
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestRepository#PAGE_EXAMPLE_FOR_FORKS}
     * @response.representation.200.doc A page of repositories related to the request 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 see the
     *                                  request repository.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The request repository does not exist.
     */
    @GET
    @Path(REPOSITORY_SLUG_PATH + "/forks")
    public Response getForkedRepositories(@Context Repository repository, @Context PageRequest pageRequest) {
        Page<? extends Repository> page = repositoryService.findByOrigin(repository, pageRequest);

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

    /**
     * Retrieve repositories which are related to this one. Related repositories are from the same
     * {@link Repository#getHierarchyId() hierarchy} as this repository.
     * <p>
     * Only repositories to which the authenticated user has <b>REPO_READ</b> permission will be included, even
     * if more repositories are part of this repository's hierarchy.
     *
     * @param repository  the repository to retrieve related repositories for
     * @param pageRequest defines the starting offset of the first repository to return, and the quantity
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestRepository#PAGE_EXAMPLE_FOR_FORKS}
     * @response.representation.200.doc A page of repositories related to the request 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 see the
     *                                  request repository.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The request repository does not exist.
     */
    @GET
    @Path(REPOSITORY_SLUG_PATH + "/related")
    public Response getRelatedRepositories(@Context Repository repository, @Context PageRequest pageRequest) {
        Page<? extends Repository> page = repositoryService.findRelated(repository, pageRequest);

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

    /**
     * Retrieve repositories from the project corresponding to the supplied <strong>projectKey</strong>.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the specified project to call this
     * resource.
     *
     * @param projectKey the parent project key
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestRepository#PAGE_EXAMPLE}
     * @response.representation.200.doc The repositories 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 see the
     *                                  specified 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
    public Response getRepositories(@PathParam(PROJECT_KEY) String projectKey, @Context PageRequest pageRequest) {
        Page<? extends Repository> page = repositoryService.findByProjectKey(projectKey, pageRequest);

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

    /**
     * Retrieve the repository matching the supplied <strong>projectKey</strong> and <strong>repositorySlug</strong>.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the specified repository to call this
     * resource.
     *
     * @param repository created by the ResourceContextInterceptor from the URL
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname repository
     * @response.representation.200.example {@link RestRepository#RESPONSE_EXAMPLE}
     * @response.representation.200.doc The repository which matches the supplied <strong>projectKey</strong> and
     *                                  <strong>repositorySlug</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 see the
     *                                  specified 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(REPOSITORY_SLUG_PATH)
    public Response getRepository(@Context Repository repository) {
        return ResponseFactory.ok(transform(repository)).build();
    }

    /**
     * If a create or fork operation fails, calling this method will clean up the broken repository and try again. The
     * repository must be in an INITIALISATION_FAILED state.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project to call this
     * resource.
     *
     * @param repository created by the ResourceContextInterceptor from the URL
     *
     * @response.representation.201.mediaType application/json
     * @response.representation.201.qname repository
     * @response.representation.201.example {@link RestRepository#RESPONSE_EXAMPLE}
     * @response.representation.201.doc The newly created repository.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The repository 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
     * 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(REPOSITORY_SLUG_PATH + "/recreate")
    public Response retryCreateRepository(@Context Repository repository) {
        return ResponseFactory.ok(transform(repositoryService.retryCreate(repository))).build();
    }

    /**
     * Update the repository matching the <strong>repositorySlug</strong> supplied in the resource path.
     * <p>
     * The repository's slug is derived from its name. If the name changes the slug may also change.
     * <p>
     * This API can be used to move the repository to a different project by setting the new project in the request,
     * example: {@code {"project":{"key":"NEW_KEY"}}} .
     * <p>
     * The authenticated user must have <strong>REPO_ADMIN</strong> permission for the specified repository to call this
     * resource.
     *
     * @request.representation.example {@link RestRepository#REQUEST_EXAMPLE_FOR_UPDATE}
     *
     * @response.representation.201.mediaType application/json
     * @response.representation.201.qname repository
     * @response.representation.201.example {@link RestRepository#RESPONSE_EXAMPLE}
     * @response.representation.201.doc The updated repository.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The repository 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 a
     *                                  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.
     *
     * @response.representation.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc A repository with same name as the target already exists
     */
    @PUT
    @Path(REPOSITORY_SLUG_PATH)
    public Response updateRepository(@Context Repository repository,
                                     @Context UriInfo uriInfo,
                                     RestRepository restRepository) {
        //This check is performed explicitly to handle the case where a caller attempts to change the SCM type, since
        //such a change is not possible. If the SCM ID is not provided, or matches the current type, either is fine.
        if (restRepository.getScmId() != null && !restRepository.getScmId().equals(repository.getScmId())) {
            String message = i18nService.getMessage("bitbucket.rest.repository.nochangetype");
            throw new BadRequestException("scmId", message);
        }

        //Updating the repository's name _may_ change the slug. Capture the repository's existing slug before calling
        //through to update so we can see if it changed after the update completes.
        //You can also move the repository to a different project so we want to handle this the same way
        String slug = repository.getSlug();
        String projectKey = repository.getProject().getKey();

        RepositoryUpdateRequest.Builder builder = new RepositoryUpdateRequest.Builder(repository);
        // RepositoryUpdateRequest.Builder.name doesn't accept blank values
        if (!StringUtils.isBlank(restRepository.getName())) {
            builder = builder.name(restRepository.getName());
        }
        if (restRepository.hasForkable()) {
            builder = builder.forkable(restRepository.isForkable());
        }
        if (restRepository.hasPublic()) {
            builder = builder.publiclyAccessible(restRepository.isPublic());
        }
        if (restRepository.hasProject()) {
            builder = builder.project(projectService.getByKey(restRepository.getProject().getKey()));
        }

        repository = repositoryService.update(builder.build());

        if (slug.equals(repository.getSlug()) && projectKey.equals(repository.getProject().getKey())) {
            return ResponseFactory.ok(transform(repository)).build();
        }
        //The slug was changed, meaning the UI needs to refresh the repository from a new URL.
        return created(repository, uriInfo);
    }

    private Response created(Repository repository, UriInfo uriInfo) {
        String requestPath = uriInfo.getRequestUri().getPath();

        Matcher matcher = PATTERN_LOCATION.matcher(requestPath);
        if (!matcher.matches()) {
            throw new IllegalArgumentException("The request path, " + requestPath + ", is not understood");
        }
        String location = matcher.group(1) + repository.getProject().getKey() + matcher.group(3) + repository.getSlug();

        return ResponseFactory.created(uriInfo.getRequestUriBuilder().replacePath(location).build())
                .entity(transform(repository))
                .build();
    }

    private RestRepository transform(Repository repository) {
        return new RestRepository(repository, true);
    }

    @SuppressWarnings("unused") //Used in REST documentation
    public static final RestErrorMessage DELETION_SCHEDULED_EXAMPLE =
            new RestErrorMessage("Repository scheduled for deletion.");
}
