package com.atlassian.bitbucket.internal.defaultreviewers;

import com.atlassian.bitbucket.event.repository.RepositoryDeletedEvent;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.defaultreviewers.dao.AoPullRequestCondition;
import com.atlassian.bitbucket.internal.defaultreviewers.dao.AoReviewer;
import com.atlassian.bitbucket.internal.defaultreviewers.dao.PullRequestConditionDao;
import com.atlassian.bitbucket.internal.defaultreviewers.event.PullRequestConditionCreatedEvent;
import com.atlassian.bitbucket.internal.defaultreviewers.event.PullRequestConditionDeletedEvent;
import com.atlassian.bitbucket.internal.defaultreviewers.event.PullRequestConditionUpdatedEvent;
import com.atlassian.bitbucket.internal.defaultreviewers.model.DefaultPullRequestCondition;
import com.atlassian.bitbucket.internal.defaultreviewers.model.PullRequestCondition;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.repository.NoSuchRepositoryException;
import com.atlassian.bitbucket.repository.Ref;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositorySupplier;
import com.atlassian.bitbucket.repository.ref.restriction.RefMatcher;
import com.atlassian.bitbucket.repository.ref.restriction.RefMatcherProviderRegistry;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.bitbucket.util.PagedIterable;
import com.atlassian.event.api.EventListener;
import com.atlassian.event.api.EventPublisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.*;

@Component
public class DefaultPullRequestConditionService implements PullRequestConditionService {

    private static final int MAX_PAGE_SIZE = 100;

    private final EventPublisher eventPublisher;
    private final I18nService i18nService;
    private final PermissionService permissionService;
    private final PermissionValidationService permissionValidationService;
    private final PullRequestConditionDao pullRequestConditionDao;
    private final RefMatcherProviderRegistry refMatcherProviderRegistry;
    private final RepositorySupplier repositorySupplier;
    private final UserService userService;

    @Autowired
    public DefaultPullRequestConditionService(EventPublisher eventPublisher,
                                              I18nService i18nService,
                                              PermissionService permissionService,
                                              PermissionValidationService permissionValidationService,
                                              PullRequestConditionDao pullRequestConditionDao,
                                              RefMatcherProviderRegistry refMatcherProviderRegistry,
                                              RepositorySupplier repositorySupplier,
                                              UserService userService) {
        this.eventPublisher = eventPublisher;
        this.i18nService = i18nService;
        this.permissionService = permissionService;
        this.permissionValidationService = permissionValidationService;
        this.pullRequestConditionDao = pullRequestConditionDao;
        this.refMatcherProviderRegistry = refMatcherProviderRegistry;
        this.repositorySupplier = repositorySupplier;
        this.userService = userService;
    }


    @Nonnull
    @Override
    public Page<PullRequestCondition> getPullRequestConditions(@Nonnull Repository repository, @Nonnull PageRequest pageRequest) {
        return pullRequestConditionDao
                .find(requireNonNull(repository, "repository").getId(), requireNonNull(pageRequest, "pageRequest"))
                .transform(this::toPullRequestCondition);
    }

    @Nonnull
    @Override
    public Set<ApplicationUser> getReviewers(@Nonnull Repository repository, @Nonnull Ref fromRef, @Nonnull Ref toRef) {
        PagedIterable<PullRequestCondition> pullRequestConditions = new PagedIterable<>(request ->
                PageUtils.asPageOf(PullRequestCondition.class, getPullRequestConditions(requireNonNull(repository, "repository"), request)), MAX_PAGE_SIZE);

        return StreamSupport.stream(pullRequestConditions.spliterator(), false)
                .filter(pullRequestCondition -> pullRequestCondition.getFromMatcher().matches(requireNonNull(fromRef, "fromRef")))
                .filter(pullRequestCondition -> pullRequestCondition.getToMatcher().matches(requireNonNull(toRef, "toRef")))
                .map(PullRequestCondition::getReviewers)
                .flatMap(Collection::stream)
                .filter(ApplicationUser::isActive)
                .filter(user -> permissionService.hasRepositoryPermission(user, repository, Permission.REPO_READ))
                .collect(collectingAndThen(toSet(), Collections::unmodifiableSet));
    }

    @Nonnull
    @Override
    public PullRequestCondition createPullRequestCondition(@Nonnull Repository repository,
                                                           @Nonnull RefMatcher sourceMatcher, @Nonnull RefMatcher targetMatcher,
                                                           @Nonnull Collection<ApplicationUser> reviewers, @Nonnull Integer requiredApprovals) {
        PullRequestCondition condition = toPullRequestCondition(
                pullRequestConditionDao.create(requireNonNull(repository).getId(),
                        requireNonNull(sourceMatcher),
                        requireNonNull(targetMatcher),
                        requireNonNull(reviewers)
                                .stream()
                                .map(ApplicationUser::getId)
                                .collect(toSet()),
                        requireNonNull(requiredApprovals)));

        eventPublisher.publish(new PullRequestConditionCreatedEvent(this, condition));
        return condition;
    }

    @Nonnull
    @Override
    public PullRequestCondition updatePullRequestCondition(int pullRequestConditionId,
                                                           @Nonnull Repository newRepository,
                                                           @Nonnull RefMatcher newSourceMatcher,
                                                           @Nonnull RefMatcher newTargetMatcher,
                                                           @Nonnull Collection<ApplicationUser> newReviewers,
                                                           @Nonnull Integer newRequiredApprovals) {
        PullRequestCondition updatedCondition = toPullRequestCondition(
                pullRequestConditionDao.update(
                        pullRequestConditionDao.get(pullRequestConditionId),
                        requireNonNull(newRepository).getId(),
                        newSourceMatcher,
                        newTargetMatcher,
                        Optional.of(newReviewers)
                                .map(reviewers -> reviewers
                                        .stream()
                                        .map(ApplicationUser::getId)
                                        .collect(toList()))
                                .orElse(null),
                        newRequiredApprovals));

        eventPublisher.publish(new PullRequestConditionUpdatedEvent(this, updatedCondition));
        return updatedCondition;
    }

    @Override
    public void removePullRequestCondition(@Nonnull Repository repository, int pullRequestConditionId) {
        Objects.requireNonNull(repository, "repository");

        permissionValidationService.validateForRepository(repository, Permission.REPO_ADMIN);

        AoPullRequestCondition pullRequestConditionToDelete = pullRequestConditionDao.get(pullRequestConditionId);

        // We've checked they're the REPO admin, but we have to ensure that
        // they have supplied an ID that is from the known pullRequestConditions for
        // this repository and not from a different REPO/Project
        boolean idFromRepository = pullRequestConditionToDelete.getRepositoryId() == repository.getId();

        if (!idFromRepository) {
            throw new IllegalArgumentException(i18nService.getMessage("bitbucket.defaultreviewers.error.rest.invalid.id"));
        }

        PullRequestCondition pullRequestCondition = toPullRequestCondition(pullRequestConditionToDelete);

        pullRequestConditionDao.delete(pullRequestConditionToDelete);
        eventPublisher.publish(new PullRequestConditionDeletedEvent(this, pullRequestCondition));
    }

    @EventListener
    public void onRepositoryDeleted(final RepositoryDeletedEvent event) {
        pullRequestConditionDao.deleteForRepository(event.getRepository().getId());
    }

    private PullRequestCondition toPullRequestCondition(AoPullRequestCondition aoPullRequestCondition) {
        Repository repository = getRepository(aoPullRequestCondition.getRepositoryId());
        return DefaultPullRequestCondition.builder()
                .id(aoPullRequestCondition.getId())
                .repository(repository)
                .sourceMatcher(
                        getRefMatcher(repository,
                                aoPullRequestCondition.getSourceMatcherId(),
                                aoPullRequestCondition.getSourceMatcherType()))
                .targetMatcher(
                        getRefMatcher(repository,
                                aoPullRequestCondition.getTargetMatcherId(),
                                aoPullRequestCondition.getTargetMatcherType()))
                .reviewers(Arrays.asList(aoPullRequestCondition.getReviewers())
                        .stream()
                        .map(this::getUserById)
                        .collect(Collectors.toList()))
                .requiredApprovals(aoPullRequestCondition.getRequiredApprovals())
                .build();
    }

    private ApplicationUser getUserById(AoReviewer aoReviewer) {
        return Optional.ofNullable(aoReviewer.getUserId()).flatMap(userId ->
                Optional.ofNullable(userService.getUserById(userId, true)))
                .orElseThrow(() -> new RuntimeException("Error getting user"));
    }

    private RefMatcher getRefMatcher(Repository repository, String sourceMatcherId, String sourceMatcherType) {
        return Optional.ofNullable(refMatcherProviderRegistry.getProvider(sourceMatcherType))
                .flatMap(refMatcherProvider -> Optional.ofNullable(
                        refMatcherProvider.create(repository, sourceMatcherId)))
                .orElseThrow(() -> new RuntimeException("Error getting ref"));
    }

    private Repository getRepository(final int repositoryId) {
        Repository repository = repositorySupplier.getById(repositoryId);
        if (repository == null) {
            throw new NoSuchRepositoryException(i18nService.createKeyedMessage("bitbucket.defaultreviewers.error.no.such.repository", repositoryId), null);
        }
        return repository;
    }
}