package com.atlassian.stash.internal.commit;

import com.atlassian.event.api.EventPublisher;
import com.atlassian.bitbucket.commit.Commit;
import com.atlassian.bitbucket.commit.NoSuchCommitException;
import com.atlassian.bitbucket.event.commit.CommitDiscussionParticipantsUpdatedEvent;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.stash.internal.InternalConverter;
import com.atlassian.stash.internal.spring.SpringTransactionUtils;
import com.atlassian.stash.internal.watcher.InternalWatcherService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.CommitCommandParameters;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.user.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collections;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.instanceOf;
import static com.google.common.base.Throwables.getCausalChain;
import static com.google.common.collect.Iterables.any;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

/**
 * @since 2.11
 */
@Component("commitDiscussionSource")
public class DefaultCommitDiscussionSource implements CommitDiscussionSource {

    private final AuthenticationContext authenticationContext;
    private final CommitDiscussionDao discussionDao;
    private final EventPublisher eventPublisher;
    private final I18nService i18nService;
    private final CommitDiscussionParticipantDao participantDao;
    private final ScmService scmService;
    private final TransactionTemplate transactionTemplate;
    private final UserService userService;
    private final InternalWatcherService watcherService;

    @Autowired
    public DefaultCommitDiscussionSource(AuthenticationContext authenticationContext,
                                         CommitDiscussionDao discussionDao, EventPublisher eventPublisher,
                                         I18nService i18nService, CommitDiscussionParticipantDao participantDao,
                                         ScmService scmService, PlatformTransactionManager transactionManager,
                                         UserService userService, InternalWatcherService watcherService) {
        this.authenticationContext = authenticationContext;
        this.discussionDao = discussionDao;
        this.eventPublisher = eventPublisher;
        this.i18nService = i18nService;
        this.participantDao = participantDao;
        this.scmService = scmService;
        this.userService = userService;
        this.watcherService = watcherService;

        transactionTemplate = new TransactionTemplate(transactionManager, SpringTransactionUtils.REQUIRES_NEW);
    }

    @Override
    public InternalCommitDiscussion get(@Nonnull CommitDiscussionRequest request) {
        checkNotNull(request, "request");

        InternalCommitDiscussion discussion = doGet(request);
        if (discussion != null && request.isParticipate()) {
            maybeAddParticipant(discussion);
        }
        return discussion;
    }

    private void addParticipant(InternalCommitDiscussion discussion, ApplicationUser user) {
        participantDao.create(new InternalCommitDiscussionParticipant.Builder(discussion)
                .user(InternalConverter.convertToInternalUser(user))
                .build());
        watcherService.addUserAsWatcher(discussion, user);
        eventPublisher.publish(new CommitDiscussionParticipantsUpdatedEvent(this, discussion,
                Collections.singleton(user), Collections.<ApplicationUser>emptySet()));
    }

    private void maybeAddParticipant(InternalCommitDiscussion discussion) {
        maybeAddParticipant(discussion, authenticationContext.getCurrentUser());
    }

    private void maybeAddParticipant(InternalCommitDiscussion discussion, ApplicationUser user) {
        InternalCommitDiscussionParticipant participant = participantDao.findByDiscussionAndUser(discussion.getId(), user.getId());
        if (participant == null) {
            addParticipant(discussion, user);
        }
    }

    private InternalCommitDiscussion doGet(CommitDiscussionRequest request) {
        String commitId = request.getCommitId();
        Repository repository = request.getRepository();

        InternalCommitDiscussion discussion = discussionDao.findByRepositoryAndCommit(repository.getId(), commitId);
        if (request.isCreate() && discussion == null) {
            //This commit hasn't been discussed before, so the discussion must be created. First, we need to verify
            //that the commit they're trying to annotate actually exists
            Commit commit = scmService.getCommandFactory(repository)
                    .commit(new CommitCommandParameters.Builder().commitId(commitId).build())
                    .call();
            if (commit == null) {
                throw new NoSuchCommitException(
                        i18nService.createKeyedMessage("bitbucket.service.repository.commitnotfound",
                                repository.getName(), commitId),
                        commitId);
            }

            //If we make it here, it means the commit exists in the repository. Before we attempt to create a new
            //discussion, make sure we're using a real hash.
            discussion = discussionDao.findByRepositoryAndCommit(repository.getId(), commit.getId());
            if (discussion == null) {
                try {
                    //When a commit is first discussed, attempt to match its author to a known user and, if one
                    //is found, add that user as a participant and as a watcher
                    ApplicationUser author = findAuthor(commit);
                    discussion = createDiscussion(repository, commit, author);
                } catch (RuntimeException e) {
                    if (isDataIntegrityViolation(e)) {
                        discussion = discussionDao.findByRepositoryAndCommit(repository.getId(), commit.getId());
                        if (discussion != null) {
                            //The exception was due to a duplicate discussion being created so we just return that
                            return discussion;
                        }
                    }

                    //If we're here it's either not really because of a duplicate discussion or
                    //it was because of some other exception when creating the discussion
                    throw e;
                }
            }
        }

        return discussion;
    }

    private InternalCommitDiscussion createDiscussion(@Nonnull final Repository repository,
                                                      @Nonnull final Commit commit,
                                                      @Nullable final ApplicationUser author) {
        //Now that we're sure we're using a full hash, create the CommitDiscussion on a separate transaction
        return transactionTemplate.execute(new TransactionCallback<InternalCommitDiscussion>() {

            @Override
            public InternalCommitDiscussion doInTransaction(TransactionStatus transactionStatus) {
                //Use Commit.getId() here when creating the discussion instead of the provided commitId,
                //which might be a ref name or a short hash
                InternalCommitDiscussion discussion = discussionDao.create(
                        new InternalCommitDiscussion.Builder(
                                InternalConverter.convertToInternalRepository(repository), commit.getId())
                                .parents(commit.getParents())
                                .build()
                );

                if (author != null) {
                    addParticipant(discussion, author);
                }

                return discussion;
            }
        });
    }

    private ApplicationUser findAuthor(Commit commit) {
        String emailAddress = commit.getAuthor().getEmailAddress();
        return isNotBlank(emailAddress) ? userService.findUserByNameOrEmail(emailAddress) : null;
    }

    private boolean isDataIntegrityViolation(RuntimeException e) {
        return any(getCausalChain(e), instanceOf(DataIntegrityViolationException.class));
    }
}
