package com.atlassian.stash.internal.repository;

import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.ServerException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.event.repository.*;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.permission.PermissionPredicateFactory;
import com.atlassian.bitbucket.project.NoSuchProjectException;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.project.ProjectType;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.DeleteCommandParameters;
import com.atlassian.bitbucket.scm.FeatureUnsupportedScmException;
import com.atlassian.bitbucket.scm.ForkCommandParameters;
import com.atlassian.bitbucket.scm.ScmFeature;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.server.FeatureManager;
import com.atlassian.bitbucket.server.StandardFeature;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.util.*;
import com.atlassian.bitbucket.validation.groups.Create;
import com.atlassian.bitbucket.validation.groups.Update;
import com.atlassian.cache.Cache;
import com.atlassian.cache.CacheFactory;
import com.atlassian.cache.CacheSettingsBuilder;
import com.atlassian.event.api.EventListener;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.fugue.retry.RetryFactory;
import com.atlassian.plugin.spring.AvailableToPlugins;
import com.atlassian.security.random.SecureTokenGenerator;
import com.atlassian.stash.internal.InternalConverter;
import com.atlassian.stash.internal.annotation.Secured;
import com.atlassian.stash.internal.annotation.Throttled;
import com.atlassian.stash.internal.annotation.Unsecured;
import com.atlassian.stash.internal.page.PageConstants;
import com.atlassian.stash.internal.project.InternalPersonalProject;
import com.atlassian.stash.internal.project.InternalProjectService;
import com.atlassian.stash.internal.scm.InternalScmService;
import com.atlassian.stash.internal.spring.SpringTransactionUtils;
import com.atlassian.stash.internal.user.InternalPermissionService;
import com.atlassian.utils.process.ProcessException;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.validation.Validator;
import java.io.File;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Predicate;

import static com.atlassian.bitbucket.util.PageUtils.asPageOf;
import static com.atlassian.bitbucket.util.PageUtils.createEmptyPage;
import static com.atlassian.bitbucket.util.ValidationUtils.validate;
import static com.atlassian.stash.internal.InternalConverter.convertToInternalProject;
import static com.google.common.base.MoreObjects.firstNonNull;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toSet;

@AvailableToPlugins(interfaces = {RepositoryService.class, RepositorySupplier.class})
@Service("repositoryService")
public class DefaultRepositoryService extends DefaultRepositorySupplier implements InternalRepositoryService {

    public static final String CACHE_NAME = "com.atlassian.bitbucket.repository.RepositoryService#isEmpty";

    /**
     * The maximum times a repository create/fork operation will be attempted.
     */
    private static final int MAX_REPOSITORY_CREATE_ATTEMPTS = 5;

    /**
     * The minimum maximum number of repositories on a single page. If a user configures a value less than this, it
     * will be ignored and this value will be used instead.
     * <p>
     * When the UI requests pages, it requests 25 repositories at a time. It does so to ensure reasonable behaviour
     * with infinite scroll, because if the minimum were, say, 2 repositories per page, the initial page would not
     * load enough repositories and the UI would immediately go fetch another page (and another, and another, until
     * the screen was filled enough to push the scroll threshold off the bottom). This minimum is high enough to
     * reinforce that minimum.
     */
    private static final int MINIMUM_MAX_REPOSITORIES = 50;

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

    private final AuthenticationContext authenticationContext;
    private final EventPublisher eventPublisher;
    private final ScheduledExecutorService executorService;
    private final FeatureManager featureManager;
    private final I18nService i18nService;
    private final Cache<Integer, Boolean> isEmptyCache;
    private final InternalPermissionService permissionService;
    private final PermissionPredicateFactory predicateFactory;
    private final InternalProjectService projectService;
    private final ApplicationPropertiesService propertiesService;
    private final InternalScmService scmService;
    private final SecureTokenGenerator tokenGenerator;
    private final TransactionTemplate transactionTemplate;
    private final Validator validator;

    private int maxRepositoriesPerPage;
    private int maxRepositoryCreateAttempts;

    @Autowired
    public DefaultRepositoryService(AuthenticationContext authenticationContext, CacheFactory cacheFactory,
                                    EventPublisher eventPublisher, ScheduledExecutorService executorService,
                                    FeatureManager featureManager, I18nService i18nService,
                                    PermissionPredicateFactory predicateFactory,
                                    InternalPermissionService permissionService, InternalProjectService projectService,
                                    ApplicationPropertiesService propertiesService, RepositoryDao repositoryDao,
                                    InternalScmService scmService, SecureTokenGenerator tokenGenerator,
                                    PlatformTransactionManager transactionManager, Validator validator) {
        super(repositoryDao);

        this.authenticationContext = authenticationContext;
        this.eventPublisher = eventPublisher;
        this.executorService = executorService;
        this.featureManager = featureManager;
        this.i18nService = i18nService;
        this.permissionService = permissionService;
        this.predicateFactory = predicateFactory;
        this.projectService = projectService;
        this.propertiesService = propertiesService;
        this.scmService = scmService;
        this.tokenGenerator = tokenGenerator;
        this.validator = validator;

        isEmptyCache = cacheFactory.getCache(CACHE_NAME,
                repositoryId -> {
                    throw new UnsupportedOperationException("Use a Supplier when accessing the isEmpty cache");
                },
                new CacheSettingsBuilder()
                        .remote()
                        .replicateAsynchronously()
                        .replicateViaInvalidation()
                        .build());
        maxRepositoriesPerPage = MINIMUM_MAX_REPOSITORIES;
        maxRepositoryCreateAttempts = MAX_REPOSITORY_CREATE_ATTEMPTS;
        transactionTemplate = new TransactionTemplate(transactionManager, SpringTransactionUtils.REQUIRES_NEW);
    }

    @Override
    @Secured("by the delegated method")
    @Transactional(readOnly = true)
    public int countByProject(@Nonnull Project project) {
        return permissionService.getCountOfAccessibleRepositories(project);
    }

    @Nonnull
    @Override
    @PreAuthorize("hasProjectPermission(#request.project, 'PROJECT_ADMIN')")
    @Throttled(Throttled.SCM_COMMAND)
    @Transactional(propagation = Propagation.SUPPORTS)
    public InternalRepository create(@Nonnull RepositoryCreateRequest request) {
        requireNonNull(request, "request");

        return createRepositoryWithRetry(request, request.getProject(), request.getScmId(), null);
    }

    @Override
    @PreAuthorize("hasRepositoryPermission(#repository, 'REPO_ADMIN')")
    @Transactional(propagation = Propagation.SUPPORTS) //Transaction-neutral, since it uses its own transaction
    public void delete(@Nonnull Repository repository) {
        if (requireNonNull(repository, "repository").getState() != Repository.State.AVAILABLE) {
            throw new IllegalRepositoryStateException(i18nService.createKeyedMessage("bitbucket.service.repository.delete.invalidstate",
                    repository.getProject().getKey(), repository.getSlug(),
                    Repository.State.AVAILABLE, repository.getState()));
        }

        //Update repositories in a separate transaction, and ensure it is committed prior to updating affected
        //repositories in the SCM
        InternalRepository internal = InternalConverter.convertToInternalRepository(repository);
        Iterable<Integer> forkIds = transactionTemplate.execute(transactionStatus -> {
            fireRepositoryDeletionRequested(internal);

            return deleteInDatabase(internal);
        });

        //Once the transaction is committed to update the repositories in the database, update them in the SCM. It is
        //left entirely to the SCM to determine what to do with the deleted repository. To help the SCM decide, a set
        //containing the IDs of all forks (which may be empty) is provided, in addition to a flag indicating whether
        //the deleted repository was the last one in its hierarchy.
        //
        //The fork IDs are collected during the deletion transaction, because reparenting in the database will make it
        //impossible to determine them after the transaction above commits.
        executorService.submit(() -> {
            DeleteCommandParameters.Builder builder = new DeleteCommandParameters.Builder().forkIds(forkIds);
            if (Iterables.isEmpty(forkIds)) {
                long count = transactionTemplate.execute(status -> {
                    status.setRollbackOnly(); //Shouldn't commit anything to the database in this transaction

                    return repositoryDao.countByHierarchy(internal.getHierarchyId());
                });

                if (count == 0L) {
                    builder.lastInHierarchy();
                }
            }

            scmService.delete(internal, builder.build());
        });
    }

    @Nonnull
    @Override
    @Secured("Secured using a repository accessible predicate")
    public Page<Repository> findAll(@Nonnull PageRequest pageRequest) {
        pageRequest = requireNonNull(pageRequest, "pageRequest").buildRestrictedPageRequest(maxRepositoriesPerPage);

        return asPageOf(Repository.class, repositoryDao.findAll(pageRequest,
                predicateFactory.createRepositoryAccessiblePredicate()));
    }

    @Nonnull
    @Override
    @PreAuthorize("isRepositoryAccessible(#origin)")
    public Page<Repository> findByOrigin(@Nonnull Repository origin, @Nonnull PageRequest pageRequest) {
        requireNonNull(origin, "origin");

        pageRequest = requireNonNull(pageRequest, "pageRequest").buildRestrictedPageRequest(maxRepositoriesPerPage);

        //Doubly-secured. You can only get forks for repositories you can see, and that only returns the forks of that
        //repository that you can see.
        return asPageOf(Repository.class,
                repositoryDao.findByOrigin(InternalConverter.convertToInternalRepository(origin), pageRequest,
                        predicateFactory.createRepositoryAccessiblePredicate()));
    }

    @Nonnull
    @Override
    @Secured("Secured using a repository accessible predicate")
    public Page<Repository> findByOwner(@Nonnull ApplicationUser owner, @Nonnull PageRequest pageRequest) {
        requireNonNull(owner, "owner");

        pageRequest = requireNonNull(pageRequest, "pageRequest").buildRestrictedPageRequest(maxRepositoriesPerPage);

        return asPageOf(Repository.class, repositoryDao.findByOwner(owner.getId(), pageRequest,
                predicateFactory.createRepositoryAccessiblePredicate()));
    }

    @Nonnull
    @Override
    @Secured("Secured using a repository accessible predicate")
    public Page<Repository> findByProjectKey(@Nonnull String projectKey, @Nonnull PageRequest pageRequest)
            throws NoSuchProjectException {
        return findByProjectKey(null, projectKey, pageRequest);
    }

    @Nonnull
    @Override
    @Secured("Secured using a repository accessible predicate")
    public Page<Repository> findByProjectKey(@Nullable String projectNamespace, @Nonnull String projectKey, @Nonnull PageRequest pageRequest)
            throws NoSuchProjectException {
        requireNonNull(projectKey, "projectKey");

        pageRequest = requireNonNull(pageRequest, "pageRequest").buildRestrictedPageRequest(maxRepositoriesPerPage);

        if (projectService.getByKey(projectNamespace, projectKey) == null) {
            if (InternalPersonalProject.isPersonalProjectKey(projectKey) && StringUtils.isEmpty(projectNamespace)) {
                return createEmptyPage(pageRequest);
            } else {
                throw new NoSuchProjectException(i18nService.createKeyedMessage("bitbucket.service.project.notfound", projectKey));
            }
        }

        return asPageOf(Repository.class, repositoryDao.findByProjectKey(projectNamespace, projectKey, pageRequest,
                predicateFactory.createRepositoryAccessiblePredicate()));
    }

    @Override
    @PreAuthorize("isAuthenticated() and isRepositoryAccessible(#repository)")
    public InternalRepository findPersonalFork(@Nonnull Repository repository) {
        requireNonNull(repository, "repository");

        Project personalProject = projectService.getPersonalProject(false);
        if (personalProject == null) {
            //The user has no personal project, so they have no personal forks.
            return null;
        }

        return repositoryDao.findByOriginInProject(InternalConverter.convertToInternalRepository(repository),
                personalProject.getId());
    }

    @Nonnull
    @Override
    @PreAuthorize("isRepositoryAccessible(#repository)")
    public Page<Repository> findRelated(@Nonnull Repository repository,
                                        @Nonnull PageRequest pageRequest) {
        requireNonNull(repository, "repository");

        pageRequest = requireNonNull(pageRequest, "pageRequest").buildRestrictedPageRequest(maxRepositoriesPerPage);

        //Doubly-secured. You can only get hierarchies for repositories you can see, and that only returns hierarchy
        //members that you can see.
        return asPageOf(Repository.class,
                repositoryDao.findByHierarchy(InternalConverter.convertToInternalRepository(repository), pageRequest,
                        predicateFactory.createRepositoryAccessiblePredicate()));
    }

    @Nonnull
    @Override
    @PreAuthorize("hasProjectPermission(#request.project, 'PROJECT_ADMIN') and hasRepositoryPermission(#request.parent, 'REPO_READ')")
    @Throttled(Throttled.SCM_COMMAND)
    @Transactional(propagation = Propagation.SUPPORTS)
    public InternalRepository fork(@Nonnull RepositoryForkRequest request) {
        requireNonNull(request, "request");

        Repository parent = request.getParent();
        if (!isForkingEnabled()) {
            throw new ForkingDisabledException(
                    i18nService.createKeyedMessage("bitbucket.service.repository.forkingdisabled"), parent);
        }
        if (!scmService.isSupported(parent, ScmFeature.FORK)) {
            throw new FeatureUnsupportedScmException(
                    i18nService.createKeyedMessage("bitbucket.service.repository.scm.noforkfeature",
                            parent.getProject().getKey(), parent.getSlug(), scmService.getScmName(parent)),
                    parent.getScmId(), ScmFeature.FORK);
        }

        Project requestProject = request.getProject();
        checkCanForkRepository(parent, requestProject);

        //If a project was not selected, create the fork into the user's personal project.
        //There is no need to verify PROJECT_ADMIN for this, because the current user is implicitly always an
        //admin on their personal project.
        Project project = requestProject != null ? requestProject : projectService.getPersonalProject();

        return createRepositoryWithRetry(request, project, parent.getScmId(), parent);
    }

    @Nonnull
    @Override
    @PreAuthorize("isRepositoryAccessible(#request.repository)")
    @Transactional(propagation = Propagation.SUPPORTS)
    public Set<NamedLink> getCloneLinks(@Nonnull RepositoryCloneLinksRequest request) {
        requireNonNull(request, "request");

        Repository repository = request.getRepository();
        ApplicationUser user = request.isUseCurrentUser() ? authenticationContext.getCurrentUser() : request.getUser();

        return scmService.getProtocols(repository).stream()
                .filter(protocol -> request.getProtocolName() == null || request.getProtocolName().equalsIgnoreCase(protocol.getName()))
                .map(protocol -> {
                    String cloneUrl = protocol.getCloneUrl(repository, user);

                    return cloneUrl == null ? null : new SimpleNamedLink(cloneUrl, protocol.getName());
                })
                .filter(Objects::nonNull)
                .collect(toSet());
    }

    @Override
    @Unsecured("Internal service method, this method is also exposed via JMX")
    public long getCount() {
        return repositoryDao.countAll();
    }

    @Override
    @PreAuthorize("hasRepositoryPermission(#repository, 'REPO_READ')")
    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    public long getSize(@Nonnull Repository repository) {
        return scmService.getSize(repository);
    }

    @Override
    @PreAuthorize("isRepositoryAccessible(#repository)")
    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    public boolean isEmpty(@Nonnull Repository repository) {
        return isEmptyCache.get(requireNonNull(repository, "repository").getId(), () -> scmService.isEmpty(repository));
    }

    @Override
    @Unsecured("Allow anonymous users to check whether forking is enabled")
    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    public boolean isForkingEnabled() {
        return featureManager.isEnabled(StandardFeature.FORKS);
    }

    @EventListener
    public void onRepositoryDeleted(RepositoryDeletedEvent event) {
        isEmptyCache.remove(event.getRepository().getId());
    }

    @EventListener
    public void onRepositoryRefsChanged(RepositoryRefsChangedEvent event) {
        isEmptyCache.remove(event.getRepository().getId());
    }

    @Nonnull
    @Override
    @PreAuthorize("hasProjectPermission(#repository.project, 'PROJECT_ADMIN')")
    @Throttled(Throttled.SCM_COMMAND)
    @Transactional
    public InternalRepository retryCreate(@Nonnull Repository repository) {
        Project project = requireNonNull(repository, "repository").getProject();
        String projectName = requireNonNull(project, "project").getName();

        String name = repository.getName();
        InternalRepository existing = repositoryDao.getBySlug(project.getNamespace(), project.getKey(), repository.getSlug());
        if (existing == null) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.repository.nosuchreponame", name, projectName);
            throw new NoSuchEntityException(message);
        }

        if (existing.getState() == Repository.State.INITIALISATION_FAILED) {
            //Remove the previous database entry
            repositoryDao.delete(existing);

            //Create the repository in the database again. This time its creation cannot be canceled, because it has
            //already been approved previously
            InternalRepository newRepository = createRepositoryInDatabase(
                    new RepositoryCreateRequest.Builder(existing).build(), project,
                    existing.getScmId(), existing.getOrigin(), false);

            //Retry creating the repository in the SCM. This relies implicitly on the fact that the "recreated"
            //repository will have a different primary key than the failed one had
            createRepositoryInScm(newRepository);

            existing = newRepository;
        }
        return existing;
    }

    @Override
    @Unsecured("Permission checks are applied while searching for repositories matching the provided criteria")
    public Page<Repository> search(@Nonnull RepositorySearchRequest request, @Nonnull PageRequest pageRequest) {
        requireNonNull(request, "request");
        pageRequest = requireNonNull(pageRequest, "pageRequest").buildRestrictedPageRequest(maxRepositoriesPerPage);

        if (request.isEmpty()) {
            // optimisation: we don't need to construct a complicated search query if there is no filters set
            return asPageOf(Repository.class, findAll(pageRequest));
        }
        RepositorySearchCriteria criteria = createSearchCriteria(request);
        if (criteria == null) {
            return createEmptyPage(pageRequest);
        }

        Predicate<Repository> permissionPredicate = request.hasPermission() ?
                predicateFactory.createRepositoryPermissionPredicate(request.getPermission()) :
                // Small optimisation. If we are only searching public repositories then
                // we don't need a permission predicate as they will all be visible
                request.getVisibility() == RepositoryVisibility.PUBLIC ?
                        p -> true :
                        predicateFactory.createRepositoryAccessiblePredicate();

        return asPageOf(Repository.class, repositoryDao.search(criteria, pageRequest, permissionPredicate));
    }

    @Value(PageConstants.MAX_REPOSITORIES)
    public void setMaxRepositoriesPerPage(int maxRepositoriesPerPage) {
        this.maxRepositoriesPerPage = Math.max(maxRepositoriesPerPage, MINIMUM_MAX_REPOSITORIES);
    }

    /**
     * Sets the maximum number of times a repository create or fork operation will be attempted before aborting and raising
     * an error.
     *
     * @param maxRepositoryCreateAttempts the maximum create / fork attempts
     */
    @Value("${repository.create.max.attempts}")
    public void setMaxRepositoryCreateAttempts(int maxRepositoryCreateAttempts) {
        if (maxRepositoryCreateAttempts > 0 && maxRepositoryCreateAttempts <= 1000) {
            this.maxRepositoryCreateAttempts = maxRepositoryCreateAttempts;
        } else {
            this.maxRepositoryCreateAttempts = MAX_REPOSITORY_CREATE_ATTEMPTS;
            log.warn("Configuration property 'repository.create.max.attempts' is invalid, expected an integer value " +
                    "greater than 0 and less or equal to 1000, but got {}. Using default value of {}",
                    maxRepositoryCreateAttempts, MAX_REPOSITORY_CREATE_ATTEMPTS);
        }
    }

    @Nonnull
    @Override
    @PreAuthorize("hasRepositoryPermission(#request.id, 'REPO_ADMIN') and (#request.project == null or hasProjectPermission(#request.project, 'PROJECT_ADMIN'))")
    @Transactional
    public InternalRepository update(@Nonnull RepositoryUpdateRequest request) {
        requireNonNull(request, "request");

        InternalRepository repository = repositoryDao.getById(request.getId());
        if (repository == null) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.repository.nosuchrepo", request.getId());
            throw new NoSuchEntityException(message);
        }

        //STASH-2637: RepositoryDao.update merges changes into 'repository' instance, therefore we create a copy of
        //it to pass to events
        InternalRepository current = repository.copy().build();
        InternalRepository updated = current.copy()
                .forkable(request.isForkable())
                .publiclyAccessible(request.isPublic())
                .name(request.getName())
                .project(firstNonNull(convertToInternalProject(request.getProject()), repository.getProject()))
                .build();
        validate(validator, updated, Update.class);

        fireRepositoryModificationRequested(current, updated);
        updated = repositoryDao.update(updated);

        eventPublisher.publish(new RepositoryModifiedEvent(this, current, updated));
        return updated;
    }

    @Nonnull
    private InternalRepository createRepositoryInDatabase(AbstractRepositoryRequest request, Project project,
                                                          String scmId, Repository parent, boolean cancelable) {
        String hierarchyId = parent == null ? generateToken() : parent.getHierarchyId();

        checkCanCreateRepository(project);

        InternalRepository repository = new InternalRepository.Builder()
                .forkable(request.isForkable())
                .hierarchyId(hierarchyId)
                .name(request.getName())
                .origin(InternalConverter.convertToInternalRepository(parent))
                .project(convertToInternalProject(project))
                .publiclyAccessible(request.isPublic())
                .scmId(scmId)
                .build();
        validate(validator, repository, Create.class);

        if (cancelable) {
            if (repository.isFork()) {
                fireRepositoryForkRequested(repository);
            } else {
                fireRepositoryCreationRequested(repository);
            }
        }
        return repositoryDao.create(repository);
    }

    private void checkCanCreateRepository(Project project) {
        if ((project.getType() == ProjectType.PERSONAL) && !featureManager.isEnabled(StandardFeature.PERSONAL_REPOS)) {
            throw new PersonalRepositoryDisabledException(i18nService.createKeyedMessage("bitbucket.service.repository.create.personalrepositorydisabled"));
        }
    }

    private void checkCanForkRepository(Repository parent, Project project) {
        if (!parent.isForkable()) {
            throw new ForkingDisabledException(
                    i18nService.createKeyedMessage("bitbucket.service.repository.notforkable",
                            parent.getProject().getKey(), parent.getSlug()), parent);
        }
        if ((project == null || project.getType() == ProjectType.PERSONAL) &&
                !featureManager.isEnabled(StandardFeature.PERSONAL_REPOS)) {
            throw new PersonalRepositoryDisabledException(
                    i18nService.createKeyedMessage("bitbucket.service.repository.fork.personalrepositorydisabled"));
        }
    }

    private InternalRepository createRepositoryInScm(InternalRepository repository) {
        Repository.State state = Repository.State.INITIALISATION_FAILED;
        try {
            validateCanCreateRepositoryOnDisk(repository);

            if (repository.isFork()) {
                scmService.fork(repository.getOrigin(), new ForkCommandParameters.Builder(repository).build());
            } else {
                scmService.create(repository);
            }

            state = Repository.State.AVAILABLE;
        } catch (ServerException e) {
            throw stripLocation(e, repository.getName());
        } finally {
            repository = setRepositoryState(repository, state);

            if (state == Repository.State.AVAILABLE) {
                eventPublisher.publish(repository.isFork() ?
                        new RepositoryForkedEvent(this, repository) :
                        new RepositoryCreatedEvent(this, repository));
            } else {
                eventPublisher.publish(repository.isFork() ?
                        new RepositoryForkFailedEvent(this, repository) :
                        new RepositoryCreationFailedEvent(this, repository));
            }
        }
        return repository;
    }

    /**
     * Create or fork a repository, retrying up to {@link #maxRepositoryCreateAttempts} times if
     * {@link RepositoryAlreadyExistsException} is encountered.
     * <p>
     * Additionally sanitizes any {@link ServerException} raised.
     *
     * @param request a {@link AbstractRepositoryRequest request} defining the repository
     * @param project the project for the repository
     * @param scmId   the scm type id
     * @param parent  a parent repository {@link Repository} if a fork is being created else {@code null}
     * @return an {@link InternalRepository} created in both database and on disk
     * @throws RuntimeException of the last failed creation attempt
     */
    private InternalRepository createRepositoryWithRetry(AbstractRepositoryRequest request, Project project,
                                                         String scmId, @Nullable Repository parent) {
        return RetryFactory.create(
                () -> transactionTemplate.execute(state -> createRepositoryInScm(createRepositoryInDatabase(request,
                        project, scmId, parent, true))),
                maxRepositoryCreateAttempts,
                e -> {
                    if (e instanceof RepositoryAlreadyExistsException) {
                        log.error("The directory for repository {}/{} already exists on the filesystem at [{}]. " +
                                "Administrator intervention is required to recover the data contained within or " +
                                "to remove the folder to recover disk space.", project.getKey(),
                                InternalRepository.slugify(request.getName()),
                                ((RepositoryAlreadyExistsException) e).getRepositoryDir());
                    } else {
                        log.debug("Could not create repository {}/{}",
                                project.getKey(), InternalRepository.slugify(request.getName()), e);
                        throw e; // Don't retry if the exception is not a RepositoryAlreadyExistsException
                    }
                })
                .get();
    }

    private RepositorySearchCriteria createSearchCriteria(RepositorySearchRequest request) {
        RepositorySearchCriteria.Builder criteria = new RepositorySearchCriteria.Builder()
                .name(request.getName())
                .projectName(request.getProjectName());
        if (request.hasVisibility()) {
            criteria.visibility(request.getVisibility());
            criteria.order(RepositoryOrder.PROJECT_NAME);
            if (!featureManager.isEnabled(StandardFeature.PUBLIC_ACCESS)) {
                if (request.getVisibility() == RepositoryVisibility.PUBLIC) {
                    // if public access is disabled there are never any public repositories,
                    // so the result will always be empty - return null here
                    return null;
                } else if (request.getVisibility() == RepositoryVisibility.PRIVATE) {
                    // If public access is disabled all repositories are private regardless
                    // of what is stored in the database. As a result we null out the visibility
                    // so it isn't taken into account.
                    criteria.visibility(null);
                }
            }
        } else {
            criteria.order(RepositoryOrder.REPO_NAME);
        }
        return criteria.build();
    }

    /**
     * Deletes the {@link Repository} from the database.
     * <p>
     * If the repository being deleted has been forked, the {@link Repository#getOrigin() origins} for those forks are
     * updated to use the deleted repository's origin.
     * <p>
     * If the repository has no origin, meaning it is not itself a fork, there is currently no reasonable replacement
     * for it in the hierarchy. If it has any forks, <i>their origins are removed</i> and each becomes its own top-
     * level repository, but they remain in the same hierarchy. This allows pull requests to still be created between
     * forks.
     *
     * @param repository the repository being deleted
     * @return a list of repository ids that are forks of the specified {@code repository}
     */
    private Iterable<Integer> deleteInDatabase(InternalRepository repository) {
        Iterable<Integer> forkIds = repositoryDao.getForkIds(repository);
        if (!Iterables.isEmpty(forkIds)) {
            repositoryDao.updateOrigin(repository, repository.getOrigin());
        }
        repositoryDao.deleteById(repository.getId());

        fireRepositoryDeleted(repository);
        return forkIds;
    }

    private void fireRepositoryCreationRequested(Repository repository) {
        SimpleCancelState cancelState = new SimpleCancelState();
        eventPublisher.publish(new RepositoryCreationRequestedEvent(this, repository, cancelState));
        if (cancelState.isCanceled()) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.repository.creationcanceled");

            throw new RepositoryCreationCanceledException(message, cancelState.getCancelMessages());
        }
    }

    private void fireRepositoryDeleted(InternalRepository repository) {
        eventPublisher.publish(new RepositoryDeletedEvent(this, repository));
    }

    private void fireRepositoryDeletionRequested(Repository repository) {
        SimpleCancelState cancelState = new SimpleCancelState();
        eventPublisher.publish(new RepositoryDeletionRequestedEvent(this, repository, cancelState));
        if (cancelState.isCanceled()) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.repository.deletioncanceled");

            throw new RepositoryDeletionCanceledException(message, cancelState.getCancelMessages());
        }
    }

    private void fireRepositoryForkRequested(Repository repository) {
        SimpleCancelState cancelState = new SimpleCancelState();
        eventPublisher.publish(new RepositoryForkRequestedEvent(this, repository, cancelState));
        if (cancelState.isCanceled()) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.repository.forkcanceled");

            throw new RepositoryForkCanceledException(message, cancelState.getCancelMessages());
        }
    }

    private void fireRepositoryModificationRequested(Repository current, Repository updated) {
        SimpleCancelState cancelState = new SimpleCancelState();
        eventPublisher.publish(new RepositoryModificationRequestedEvent(this, current, updated, cancelState));
        if (cancelState.isCanceled()) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.repository.updatecanceled");

            throw new RepositoryModificationCanceledException(message, cancelState.getCancelMessages());
        }
    }

    @Nonnull
    private String generateToken() {
        String token = tokenGenerator.generateToken();
        if (token.length() > InternalRepository.TOKEN_LENGTH) {
            token = token.substring(0, InternalRepository.TOKEN_LENGTH);
        }
        return token;
    }

    private InternalRepository setRepositoryState(InternalRepository repository, Repository.State state) {
        return repositoryDao.update(repository.copy().state(state).build());
    }

    private void validateCanCreateRepositoryOnDisk(Repository repository) {
        File repositoryRoot = propertiesService.getRepositoryDir(repository);
        if (repositoryRoot.isFile()) {
            throw new RepositoryAlreadyExistsException(i18nService.createKeyedMessage("bitbucket.service.repository.createfailed.filepresent",
                    repository.getName(), repositoryRoot), repositoryRoot.toPath());
        } else if (repositoryRoot.isDirectory()) {
            throw new RepositoryAlreadyExistsException(i18nService.createKeyedMessage("bitbucket.service.repository.createfailed.directorypresent",
                    repository.getName(), repositoryRoot), repositoryRoot.toPath());
        }
    }

    /**
     * Strips locations to executables, files or directories from a {@link ServerException} so that it can safely be rethrown.
     *
     * @param exceptionToStrip the {@link ServerException exception} to clean
     * @param repositoryName   the repository that was accessed when the exception was raised
     * @return the cleaned {@link ServerException exceptionToStrip}
     */
    private ServerException stripLocation(ServerException exceptionToStrip, String repositoryName) {
        Throwable rootCause = Throwables.getRootCause(exceptionToStrip);
        //Apply special handling to ProcessExceptions so that the location on disk is not displayed to the caller.
        //Note that the location may still end up in the log file, but anyone with access to that has access to the
        //STASH_HOME directory and could find the location on disk anyway.
        if (rootCause instanceof ProcessException) {
            return new ServerException(i18nService.createKeyedMessage("bitbucket.web.repository.create.processerror",
                    repositoryName, rootCause.getClass().getSimpleName()));
        }
        return exceptionToStrip;
    }
}
