package com.atlassian.stash.internal.content.comment;

import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.comment.CommentAction;
import com.atlassian.bitbucket.comment.DiffCommentAnchor;
import com.atlassian.bitbucket.comment.commit.AddCommitFileCommentRequest;
import com.atlassian.bitbucket.comment.commit.AddCommitLineCommentRequest;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.pull.PullRequest;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.validation.ArgumentValidationException;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.stash.internal.HibernateUtils;
import com.atlassian.stash.internal.InternalConverter;
import com.atlassian.stash.internal.comment.AbstractCommentHelper;
import com.atlassian.stash.internal.comment.CommentCounts;
import com.atlassian.stash.internal.comment.CommentDao;
import com.atlassian.stash.internal.comment.InternalComment;
import com.atlassian.stash.internal.commit.*;
import com.atlassian.stash.internal.content.InternalChangeLocation;
import com.atlassian.stash.internal.pull.InternalPullRequest;
import com.atlassian.stash.internal.pull.InternalPullRequestService;
import com.atlassian.stash.internal.pull.comment.CommentPostProcessor;
import com.atlassian.stash.internal.repository.RepositoryActivityDao;
import com.atlassian.stash.internal.task.InternalTaskService;
import com.google.common.collect.ImmutableList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.validation.Validator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;

import static com.atlassian.stash.internal.InternalConverter.convertToInternalPullRequest;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Optional.ofNullable;

/**
 * @since 2.11
 */
@Component("commitDiscussionCommentHelper")
public class DefaultCommitDiscussionCommentHelper
        extends AbstractCommentHelper<InternalCommitDiscussion, InternalCommitDiscussionCommentAnchor>
        implements InternalCommitDiscussionCommentHelper {

    private final RepositoryActivityDao activityDao;
    private final CommitDiscussionCommentAnchorDao anchorDao;
    private final InternalPullRequestService pullRequestService;

    /**
     * The InternalPullRequestService is lazy to avoid a circular dependency:
     * DefaultPullRequestService -> MergePullRequestActivityEnricher -> DefaultCommitService -> DefaultCommitDiscussionCommentHelper
     */
    @Autowired
    public DefaultCommitDiscussionCommentHelper(RepositoryActivityDao activityDao, CommitDiscussionCommentAnchorDao anchorDao,
                                                AuthenticationContext authenticationContext, CommentDao commentDao,
                                                EventPublisher eventPublisher, I18nService i18nService,
                                                PermissionService permissionService,
                                                @Lazy InternalPullRequestService pullRequestService,
                                                CommentPostProcessor postProcessor,
                                                InternalTaskService taskService, Validator validator) {
        super(anchorDao, authenticationContext, commentDao, eventPublisher, i18nService, permissionService,
                postProcessor, taskService, validator);

        this.activityDao = activityDao;
        this.anchorDao = anchorDao;
        this.pullRequestService = pullRequestService;
    }

    @Nonnull
    @Override
    public Map<InternalChangeLocation, CommentCounts> countsByLocation(@Nonnull InternalCommitDiscussion discussion,
                                                                       @Nullable Long pullRequestId) {
        return anchorDao.getCountsByLocation(discussion, pullRequestId);
    }

    @Nonnull
    @Override
    public InternalComment createFileComment(@Nonnull InternalCommitDiscussion discussion,
                                             @Nonnull AddCommitFileCommentRequest request) {
        checkNotNull(request, "request");
        validateToHash(discussion.getCommitId(), request.getToHash());

        InternalComment comment = doCreateComment(discussion, null, request.getText(), fileAnchor(discussion, request));
        request.getPullRequestId().ifPresent(pullRequestId ->
                pullRequestService.addFileAnchor(request.getRepository().getId(), pullRequestId, comment, request));

        return comment;
    }

    @Nonnull
    @Override
    public InternalComment createLineComment(@Nonnull InternalCommitDiscussion discussion,
                                             @Nonnull AddCommitLineCommentRequest request) {
        checkNotNull(request, "request");
        validateFileType(request);
        validateToHash(discussion.getCommitId(), request.getToHash());

        InternalComment comment = doCreateComment(discussion, null, request.getText(), diffAnchor(discussion, request));
        request.getPullRequestId().ifPresent(pullRequestId ->
                pullRequestService.addLineAnchor(request.getRepository().getId(), pullRequestId, comment, request));

        return comment;
    }

    @Nonnull
    @Override
    public InternalComment createReply(@Nonnull InternalCommitDiscussion entity, long parentId, String commentText) {
        InternalComment comment = super.createReply(entity, parentId, commentText);
        InternalCommitDiscussionCommentAnchor a = anchorDao.getAnchorByComment(entity, parentId);

        ofNullable(a).ifPresent(anchor -> anchor.getPullRequest().ifPresent(pullRequest ->
                pullRequestService.addCommentActivity(convertToInternalPullRequest(pullRequest), comment,
                        CommentAction.REPLIED)));
        return comment;
    }

    @Nonnull
    @Override
    public List<DiffCommentAnchor> findDiffAnchors(@Nonnull CommitDiscussionCommentAnchorSearchRequest request,
                                                   boolean enrich) {
        InternalCommitDiscussion discussion = request.getDiscussion();
        List<InternalCommitDiscussionCommentAnchor> anchors = HibernateUtils.initializeList(anchorDao.search(
                new CommitDiscussionAnchorSearchCriteria.Builder(discussion)
                        .fromHash(request.getParentId().orElse(discussion.getParentId()))
                        .path(request.getPath().orElse(null))
                        .pullRequestScopedId(request.getPullRequestScopeId().orElse(null))
                        .build()));

        if (enrich) {
            processAnchors(discussion, anchors);
        }

        return ImmutableList.copyOf(anchors);
    }

    @Nullable
    @Override
    public InternalCommitDiscussionCommentAnchor getAnchorByComment(@Nonnull InternalCommitDiscussion discussion,
                                                                    long commentId) {

        return anchorDao.getAnchorByComment(discussion, commentId);
    }

    @SuppressWarnings("ConstantConditions")
    @Nonnull
    @Override
    public InternalComment update(@Nonnull InternalCommitDiscussion entity, long commentId, int version,
                                  @Nonnull String text) {
        InternalComment comment = super.update(entity, commentId, version, text);
        InternalCommitDiscussionCommentAnchor anchor = anchorDao.getAnchorByComment(entity, commentId);

        if (anchor != null) {
            anchor.getPullRequest()
                    .map(InternalConverter::convertToInternalPullRequest)
                    .ifPresent(pullRequest ->
                            pullRequestService.addCommentActivity(pullRequest, comment, CommentAction.EDITED));
        }

        return comment;
    }

    @Override
    protected void addActivity(@Nonnull InternalCommitDiscussion discussion, @Nonnull InternalComment comment,
                               InternalCommitDiscussionCommentAnchor anchor, @Nonnull CommentAction action) {
        activityDao.create(new InternalCommitDiscussionCommentActivity.Builder(discussion)
                .comment(comment)
                .commentAction(action)
                .commentAnchor(anchor)
                .createdDate(comment.getUpdatedDate())
                .user(comment.getAuthor())
                .build());
    }

    @Override
    protected void deleteActivities(@Nonnull InternalCommitDiscussion discussion, @Nonnull InternalComment comment) {
        activityDao.deleteByComment(comment.getId());
    }

    private Function<InternalComment, InternalCommitDiscussionCommentAnchor> diffAnchor(@Nonnull InternalCommitDiscussion discussion,
                                                                                        @Nonnull AddCommitLineCommentRequest request) {
        InternalPullRequest pullRequest = request.getPullRequestId()
                .map(pullRequestId -> getPullRequest(pullRequestId, request.getRepository()))
                .orElse(null);

        return comment -> new InternalCommitDiscussionCommentAnchor.Builder(discussion)
                .apply(request)
                .comment(comment)
                //Since 4.7 the CommitService attempts to set a fromHash in the request
                //We still need to handle the null case for root commits which do not have a parent
                .fromHash(request.getFromHash().orElse(null))
                .pullRequest(pullRequest)
                //Always ignore the request's hash, which may be null, and use the discussion's hash (which is never
                //null and, if a request hash was provided, should always match it anyway)
                .toHash(discussion.getCommitId())
                .build();
    }

    private Function<InternalComment, InternalCommitDiscussionCommentAnchor> fileAnchor(@Nonnull InternalCommitDiscussion discussion,
                                                                                        @Nonnull AddCommitFileCommentRequest request) {
        InternalPullRequest pullRequest = request.getPullRequestId()
                .map(pullRequestId -> getPullRequest(pullRequestId, request.getRepository()))
                .orElse(null);

        return comment -> new InternalCommitDiscussionCommentAnchor.Builder(discussion)
                .apply(request)
                .comment(comment)
                //Since 4.7 the CommitService attempts to set a fromHash in the request
                //We still need to handle the null case for root commits which do not have a parent
                .fromHash(request.getFromHash().orElse(null))
                .pullRequest(pullRequest)
                //Always ignore the request's hash, which may be null, and use the discussion's hash (which is never
                //null and, if a request hash was provided, should always match it anyway)
                .toHash(discussion.getCommitId())
                .build();
    }

    private InternalPullRequest getPullRequest(Long pullRequestId, Repository repository) {
        PullRequest pullRequest = pullRequestService.getById(repository.getId(), pullRequestId);
        if (pullRequest == null) {
            throw new ArgumentValidationException(
                    i18nService.createKeyedMessage("bitbucket.discussion.comment.pullrequest.notfound",
                            pullRequestId, repository));
        }
        return convertToInternalPullRequest(pullRequest);
    }

    private void validateToHash(String expected, String actual) {
        if (!Objects.equals(expected, actual)) {
            throw new ArgumentValidationException(
                    i18nService.createKeyedMessage("bitbucket.discussion.comment.invalidtohash", actual, expected));
        }
    }
}
