package com.atlassian.stash.internal.repository;

import com.atlassian.bitbucket.event.branch.BranchCreatedEvent;
import com.atlassian.bitbucket.event.branch.BranchCreationCanceledException;
import com.atlassian.bitbucket.event.branch.BranchCreationRequestedEvent;
import com.atlassian.bitbucket.event.repository.RepositoryDefaultBranchModifiedEvent;
import com.atlassian.bitbucket.event.tag.TagCreatedEvent;
import com.atlassian.bitbucket.event.tag.TagCreationCanceledException;
import com.atlassian.bitbucket.event.tag.TagCreationRequestedEvent;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.*;
import com.atlassian.bitbucket.scm.ref.CreateBranchCommandParameters;
import com.atlassian.bitbucket.scm.ref.CreateTagCommandParameters;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.bitbucket.util.SimpleCancelState;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.plugin.spring.AvailableToPlugins;
import com.atlassian.stash.internal.page.PageConstants;
import com.atlassian.stash.internal.scm.AbstractScmService;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import org.apache.commons.lang3.StringUtils;
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 javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

@AvailableToPlugins(RefService.class)
@Service("refService")
public class DefaultRefService extends AbstractScmService implements RefService {

    private static final String FQ_BRANCH_PATH = "refs/heads/";
    private static final String FQ_TAG_PATH = "refs/tags/";
    private static final String MAX_REF_METADATA_REQUEST_COUNT = "${ref.metadata.max.request.count}";
    private static final int MIN_BRANCHES_COUNT = 25;
    private static final int MIN_TAGS_COUNT = 25;
    private static final int MIN_REF_METADATA_REQUEST_COUNT = 25;

    private final EventPublisher eventPublisher;
    private final I18nService i18nService;
    private final RefMetadataMapProvider refMetadataMapProvider;

    private int maxBranches;
    private int maxTags;
    private int maxRefMetadataRequestCount;

    @Autowired
    public DefaultRefService(ScmService scmService, EventPublisher eventPublisher, I18nService i18nService,
                             RefMetadataMapProvider refMetadataMapProvider) {
        super(scmService);

        this.eventPublisher = eventPublisher;
        this.i18nService = i18nService;
        this.refMetadataMapProvider = refMetadataMapProvider;
    }

    @Nonnull
    @Override
    @PreAuthorize("hasRepositoryPermission(#request.repository, 'REPO_WRITE')")
    public Branch createBranch(@Nonnull CreateBranchRequest request) {
        checkNotNull(request, "request");

        Repository repository = request.getRepository();
        if (scmService.isEmpty(repository)) {
            throw new IllegalRepositoryStateException(i18nService.createKeyedMessage("bitbucket.service.branch.creationempty",
                    repository.getProject().getKey(), repository.getSlug()));
        }

        SimpleCancelState cancelState = new SimpleCancelState();
        eventPublisher.publish(new BranchCreationRequestedEvent(this, repository, new SimpleBranch.Builder()
                .id(qualify(request.getName(), FQ_BRANCH_PATH))
                .displayId(request.getName())
                .latestCommit(request.getStartPoint())
                .build(), cancelState));
        if (cancelState.isCanceled()) {
            throw new BranchCreationCanceledException(i18nService.createKeyedMessage("bitbucket.service.branch.creationcanceled"),
                    cancelState.getCancelMessages());
        }

        CreateBranchCommandParameters.Builder builder = new CreateBranchCommandParameters.Builder()
                .name(request.getName())
                .startPoint(request.getStartPoint());
        if (StringUtils.isNotBlank(request.getMessage())) {
            builder.message(request.getMessage());
        }
        Branch branch = scmService.getRefCommandFactory(repository)
                .createBranch(builder.build())
                .call();
        if (branch == null) {
            throw new NoSuchBranchException(i18nService.createKeyedMessage("bitbucket.service.branch.creationerror", request.getName()), request.getName());
        }

        eventPublisher.publish(new BranchCreatedEvent(this, repository, branch));

        return branch;
    }

    @Nonnull
    @Override
    @PreAuthorize("hasRepositoryPermission(#request.repository, 'REPO_WRITE')")
    public Tag createTag(@Nonnull CreateTagRequest request) {
        checkNotNull(request, "request");

        Repository repository = request.getRepository();
        if (scmService.isEmpty(repository)) {
            throw new IllegalRepositoryStateException(i18nService.createKeyedMessage("bitbucket.service.tag.creationempty",
                    repository.getProject().getKey(), repository.getSlug()));
        }

        SimpleCancelState cancelState = new SimpleCancelState();
        eventPublisher.publish(new TagCreationRequestedEvent(this, repository, new SimpleTag.Builder()
                .id(qualify(request.getName(), FQ_TAG_PATH))
                .displayId(request.getName())
                .latestCommit(request.getStartPoint())
                .build(), cancelState));
        if (cancelState.isCanceled()) {
            throw new TagCreationCanceledException(i18nService.createKeyedMessage("bitbucket.service.tag.creationcanceled"),
                    cancelState.getCancelMessages());
        }

        CreateTagCommandParameters.Builder builder = new CreateTagCommandParameters.Builder()
                .name(request.getName())
                .startPoint(request.getStartPoint());

        if (StringUtils.isNotBlank(request.getMessage())) {
            builder.message(request.getMessage());
        }

        Tag tag = scmService.getRefCommandFactory(repository)
                .createTag(builder.build())
                .call();
        if (tag == null) {
            throw new NoSuchTagException(i18nService.createKeyedMessage("bitbucket.service.tag.creationerror", request.getName()), request.getName());
        }

        eventPublisher.publish(new TagCreatedEvent(this, repository, tag));

        return tag;
    }

    @Nonnull
    @Override
    @PreAuthorize("isRepositoryAccessible(#request.repository)")
    public Page<Branch> getBranches(@Nonnull RepositoryBranchesRequest request, @Nonnull PageRequest pageRequest) {
        checkNotNull(request, "request");
        pageRequest = checkNotNull(pageRequest, "pageRequest").buildRestrictedPageRequest(maxBranches);

        Page<Branch> page = getCommandFactory(request.getRepository())
                .branches(new BranchesCommandParameters.Builder()
                        .filterText(request.getFilterText())
                        .order(request.getOrder())
                        .build(), pageRequest)
                .call();
        if (page == null) {
            page = PageUtils.createEmptyPage(pageRequest);
        }
        return page;
    }

    @Nonnull
    @Override
    @PreAuthorize("isRepositoryAccessible(#repository)")
    public Branch getDefaultBranch(@Nonnull Repository repository) {
        Branch defaultBranch = getCommandFactory(repository)
                .defaultBranch()
                .call();
        if (defaultBranch == null) {
            throw new NoDefaultBranchException(
                    i18nService.createKeyedMessage("bitbucket.service.repository.nodefaultbranch"),
                    repository.getName(), null);
        }
        return defaultBranch;
    }

    @Nonnull
    @Override
    @PreAuthorize("isRepositoryAccessible(#request.repository)")
    public Map<Ref, MetadataMap> getMetadataByRefs(@Nonnull RefMetadataRequest request) {
        checkNotNull(request, "request");

        Ref baseRef = request.getBaseRef();
        if (baseRef == null) {
            baseRef = getDefaultBranch(request.getRepository());
        }

        RefMetadataContext context = new SimpleRefMetadataContext(
                request.getRepository(),
                baseRef,
                Iterables.limit(request.getRefs(), maxRefMetadataRequestCount),
                request.getContext()
        );

        return refMetadataMapProvider.getMetadata(context);
    }

    @Nonnull
    @Override
    @PreAuthorize("isRepositoryAccessible(#request.repository)")
    public Page<Tag> getTags(@Nonnull RepositoryTagsRequest request, @Nonnull PageRequest pageRequest) {
        checkNotNull(request, "request");
        pageRequest = checkNotNull(pageRequest, "pageRequest").buildRestrictedPageRequest(maxTags);

        Page<Tag> page = getCommandFactory(request.getRepository())
                .tags(new TagsCommandParameters.Builder()
                        .filterText(request.getFilterText())
                        .order(request.getOrder())
                        .build(), pageRequest)
                .call();
        if (page == null) {
            page = PageUtils.createEmptyPage(pageRequest);
        }
        return page;
    }

    @Override
    @PreAuthorize("isRepositoryAccessible(#request.repository)")
    public Ref resolveRef(@Nonnull ResolveRefRequest request) {
        Optional<String> refId = checkNotNull(request, "request").getRefId();
        if (refId.isPresent()) {
            //For posterity: Be very careful of trying to do something like request.getRefId().map(...).orElseGet(...)
            //here. Using ScmCommandFactory.resolveRef inside map can lead to unexpected behavior when a requested ref
            //doesn't resolve. When the map Function returns null, map returns Optional.empty(), which means orElseGet
            //is invoked. If that's retrieving the default branch, it means any time a ref doesn't resolve the default
            //branch is returned!
            return getCommandFactory(request.getRepository())
                    .resolveRef(new ResolveRefCommandParameters.Builder(refId.get())
                            .type(request.getType().orElse(null))
                            .build())
                    .call();
        }

        return getDefaultBranch(request.getRepository());
    }

    @Deprecated
    @Override
    @PreAuthorize("isRepositoryAccessible(#repository)")
    public Ref resolveRef(@Nonnull Repository repository, @Nullable String objectId) {
        return resolveRef(new ResolveRefRequest.Builder(repository)
                .refId(objectId)
                .build());
    }

    @Nonnull
    @Override
    @PreAuthorize("isRepositoryAccessible(#request.repository)")
    public Map<String, Ref> resolveRefs(@Nonnull ResolveRefsRequest request) {
        Map<String, Ref> refsById = getCommandFactory(request.getRepository())
                .resolveRefs(new ResolveRefsCommandParameters.Builder(request).build())
                .call();

        return refsById == null ? Collections.emptyMap() : refsById;
    }

    @Override
    @PreAuthorize("hasRepositoryPermission(#repository, 'REPO_ADMIN')")
    public void setDefaultBranch(@Nonnull Repository repository, @Nonnull String branchName) {
        checkNotNull(repository, "repository");
        checkNotNull(branchName, "branchName");

        if (!scmService.isSupported(repository, ScmFeature.UPDATE_DEFAULT_BRANCH)) {
            throw new FeatureUnsupportedScmException(
                    i18nService.createKeyedMessage("bitbucket.service.repository.scm.noupdatedefaultbranchfeature",
                            repository.getProject().getKey(), repository.getSlug(), scmService.getScmName(repository)),
                    repository.getScmId(), ScmFeature.UPDATE_DEFAULT_BRANCH);
        }

        scmService.getExtendedCommandFactory(repository)
                .updateDefaultBranch(new UpdateDefaultBranchCommandParameters.Builder(branchName).build())
                .call();

        eventPublisher.publish(new RepositoryDefaultBranchModifiedEvent(this, repository, branchName));
    }

    @Value(PageConstants.MAX_BRANCHES)
    public void setMaxBranches(int maxBranches) {
        this.maxBranches = Math.max(maxBranches, MIN_BRANCHES_COUNT);
    }

    @Value(MAX_REF_METADATA_REQUEST_COUNT)
    public void setMaxRefMetadataRequestCount(int maxRefMetadataRequestCount) {
        this.maxRefMetadataRequestCount = Math.max(maxRefMetadataRequestCount, MIN_REF_METADATA_REQUEST_COUNT);
    }

    @Value(PageConstants.MAX_TAGS)
    public void setMaxTags(int maxTags) {
        this.maxTags = Math.max(maxTags, MIN_TAGS_COUNT);
    }

    @Override
    @PreAuthorize("isRepositoryAccessible(#request.repository)")
    public void streamTags(@Nonnull RepositoryTagsRequest request, @Nonnull TagCallback callback) {
        checkNotNull(request, "request");
        checkNotNull(callback, "callback");

        getCommandFactory(request.getRepository())
                .tags(new TagsCommandParameters.Builder()
                        .filterText(request.getFilterText())
                        .order(request.getOrder())
                        .build(), callback)
                .call();
    }

    /*
     * TODO: Copied almost verbatim from GitRefPattern. We are in the process of refactoring our API and this is here
     * to allow us to move the Ref creation events into the host app and out of the plugins. This MUST be removed
     * once we complete two pieces of work:
     *      * Introduce a type to MinimalRef
     *      * Change the branch permissions to rely on MinimalRef instead of Ref Id
     */
    private String qualify(@Nonnull String ref, @Nonnull String path) {
        checkArgument(StringUtils.isNotBlank(checkNotNull(ref, "ref")), "No ref was provided to qualify");

        if (ref.indexOf('/') > 0) {
            int index = 0;
            do {
                if (ref.startsWith(path.substring(index))) {
                    return path.substring(0, index) + ref;
                }

                index = path.indexOf('/', index) + 1;
            } while (index < path.length());
        }

        return path + ref;
    }

    private static class SimpleRefMetadataContext implements RefMetadataContext {

        private final Map<String, Object> context;
        private final Repository repository;
        private final Ref baseRef;
        private final Set<Ref> refs;

        private SimpleRefMetadataContext(Repository repository, Ref baseRef, Iterable<? extends Ref> refs,
                                         Map<String, Object> context) {
            this.repository = repository;
            this.baseRef = baseRef;
            this.refs = ImmutableSet.copyOf(refs);
            this.context = context;
        }

        @Nonnull
        @Override
        public Ref getBaseRef() {
            return baseRef;
        }

        @Nonnull
        @Override
        public Map<String, Object> getContext() {
            return context;
        }

        @Nonnull
        @Override
        public Set<Ref> getRefs() {
            return refs;
        }

        @Nonnull
        @Override
        public Repository getRepository() {
            return repository;
        }
    }
}
