package com.atlassian.stash.internal.comment;

import com.atlassian.bitbucket.AuthorisationException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.comment.*;
import com.atlassian.bitbucket.commit.CommitDiscussion;
import com.atlassian.bitbucket.commit.CommitDiscussionCommentAnchor;
import com.atlassian.bitbucket.content.DiffFileType;
import com.atlassian.bitbucket.content.DiffSegmentType;
import com.atlassian.bitbucket.event.commit.CommitDiscussionCommentAddedEvent;
import com.atlassian.bitbucket.event.commit.CommitDiscussionCommentDeletedEvent;
import com.atlassian.bitbucket.event.commit.CommitDiscussionCommentEditedEvent;
import com.atlassian.bitbucket.event.commit.CommitDiscussionCommentRepliedEvent;
import com.atlassian.bitbucket.event.pull.PullRequestCommentAddedEvent;
import com.atlassian.bitbucket.event.pull.PullRequestCommentDeletedEvent;
import com.atlassian.bitbucket.event.pull.PullRequestCommentEditedEvent;
import com.atlassian.bitbucket.event.pull.PullRequestCommentRepliedEvent;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.pull.PullRequest;
import com.atlassian.bitbucket.pull.PullRequestDiffCommentAnchor;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.util.MoreCollectors;
import com.atlassian.bitbucket.validation.ArgumentValidationException;
import com.atlassian.bitbucket.validation.groups.Create;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.stash.internal.InternalConverter;
import com.atlassian.stash.internal.content.InternalChangeLocation;
import com.atlassian.stash.internal.pull.comment.CommentPostProcessor;
import com.atlassian.stash.internal.task.InternalTaskService;
import com.atlassian.stash.internal.user.InternalApplicationUser;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;

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

import static com.atlassian.bitbucket.util.ValidationUtils.validate;

/**
 * @since 2.11
 */
public abstract class AbstractCommentHelper<T extends InternalCommentable, A extends InternalDiffCommentAnchor>
        implements InternalCommentHelper<T, A> {

    protected final DiffCommentAnchorDao<T, A> anchorDao;
    protected final EventPublisher eventPublisher;
    protected final I18nService i18nService;

    private final AuthenticationContext authenticationContext;
    private final CommentDao commentDao;
    private final PermissionService permissionService;
    private final CommentPostProcessor postProcessor;
    private final InternalTaskService taskService;
    private final Validator validator;

    protected AbstractCommentHelper(DiffCommentAnchorDao<T, A> anchorDao, AuthenticationContext authenticationContext,
                                    CommentDao commentDao, EventPublisher eventPublisher,
                                    I18nService i18nService, PermissionService permissionService,
                                    CommentPostProcessor postProcessor, InternalTaskService taskService,
                                    Validator validator) {
        this.anchorDao = anchorDao;
        this.authenticationContext = authenticationContext;
        this.commentDao = commentDao;
        this.eventPublisher = eventPublisher;
        this.i18nService = i18nService;
        this.permissionService = permissionService;
        this.postProcessor = postProcessor;
        this.taskService = taskService;
        this.validator = validator;
    }

    @Nonnull
    @Override
    public Map<InternalChangeLocation, CommentCounts> countsByLocation(@Nonnull T entity) {
        return anchorDao.getCountsByLocation(entity);
    }

    @Nonnull
    public InternalComment create(@Nonnull T entity, @Nonnull String text) {
        return createComment(entity, null, text);
    }

    @Nonnull
    public InternalComment createReply(@Nonnull T entity, long parentId, String commentText) {
        return createComment(entity, getById(entity, parentId), commentText);
    }

    public boolean delete(@Nonnull T entity, long commentId, int version) {
        final InternalComment comment = commentDao.getById(commentId);
        if (comment == null) {
            return false;
        }
        validateVersion(version, comment.getVersion());

        ApplicationUser currentUser = authenticationContext.getCurrentUser();
        //ADMINs and above may delete other users' comments, but non-admins can only delete their own comments.
        if ((currentUser == null || ObjectUtils.notEqual(currentUser.getId(), comment.getAuthor().getId())) &&
                !permissionService.hasRepositoryPermission(entity.getScopeRepository(), Permission.REPO_ADMIN)) {
            throw new AuthorisationException(i18nService.createKeyedMessage("bitbucket.service.comment.delete.otheruser"));
        }
        if (!comment.getComments().isEmpty()) {
            throw new CommentDeletionException(i18nService.createKeyedMessage("bitbucket.service.comment.delete.hasreplies"));
        }
        if (taskService.hasTasks(comment)) {
            throw new CommentDeletionException(i18nService.createKeyedMessage("bitbucket.service.comment.delete.hastasks"));
        }

        deleteActivities(entity, comment);

        commentDao.delete(InternalConverter.convertToInternalComment(comment));
        eventPublisher.publish(entity.accept(new CommentableVisitor<Object>() {

            @Override
            public Object visit(@Nonnull CommitDiscussion discussion) {
                return new CommitDiscussionCommentDeletedEvent(AbstractCommentHelper.this, discussion,
                        comment, comment.getParent());
            }

            @Override
            public Object visit(@Nonnull PullRequest pullRequest) {
                return new PullRequestCommentDeletedEvent(AbstractCommentHelper.this, pullRequest,
                        comment, comment.getParent());
            }
        }));

        return true;
    }

    //TODO: This does not validate that the comment belongs to the specified entity
    @Nonnull
    @Override
    public InternalComment getById(@Nonnull T entity, long commentId) {
        return postProcessor.process(entity, getCommentOrFail(commentId));
    }

    //TODO: This does not validate that the comment belongs to the specified entity
    @Nonnull
    @Override
    public InternalComment update(@Nonnull T entity, long commentId, int version, @Nonnull String text) {
        if (StringUtils.isBlank(text)) {
            throw new ArgumentValidationException(i18nService.createKeyedMessage("bitbucket.service.comment.update.requirestext"));
        }

        InternalComment comment = getCommentOrFail(commentId, version);
        String previousText = comment.getText();

        InternalApplicationUser currentUser = InternalConverter.convertToInternalUser(authenticationContext.getCurrentUser());
        if (currentUser == null) {
            throw new AuthorisationException(i18nService.createKeyedMessage("bitbucket.service.comment.update.anonymous"));
        }
        if (ObjectUtils.notEqual(currentUser.getId(), comment.getAuthor().getId())) {
            throw new AuthorisationException(i18nService.createKeyedMessage("bitbucket.service.comment.update.otheruser"));
        }

        InternalComment updated = commentDao.update(new InternalComment.Builder(comment)
                .text(text)
                .updatedDate(new Date())
                .build());

        eventPublisher.publish(entity.accept(new CommentableVisitor<Object>() {

            @Override
            public Object visit(@Nonnull CommitDiscussion discussion) {
                return new CommitDiscussionCommentEditedEvent(AbstractCommentHelper.this, discussion,
                        updated, updated.getParent(), previousText);
            }

            @Override
            public Object visit(@Nonnull PullRequest pullRequest) {
                return new PullRequestCommentEditedEvent(AbstractCommentHelper.this, pullRequest,
                        updated, updated.getParent(), previousText);
            }
        }));
        addActivity(entity, updated, null, CommentAction.EDITED);

        return postProcessor.process(entity, updated);
    }

    protected abstract void addActivity(@Nonnull T entity, @Nonnull InternalComment comment, @Nullable A anchor,
                                        @Nonnull CommentAction action);

    protected abstract void deleteActivities(@Nonnull T entity, @Nonnull InternalComment comment);

    private InternalComment createComment(T entity, InternalComment parent, String text) {
        return doCreateComment(entity, parent, text, null);
    }

    protected InternalComment doCreateComment(T entity, InternalComment parent, String text,
                                              Function<InternalComment, A> anchorFunction) {
        if (StringUtils.isBlank(text)) {
            throw new ArgumentValidationException(i18nService.createKeyedMessage("bitbucket.service.comment.new.requirestext"));
        }

        Date modified = new Date();
        InternalApplicationUser user = InternalConverter.convertToInternalUser(authenticationContext.getCurrentUser());

        InternalComment comment = commentDao.create(new InternalComment.Builder()
                .author(user)
                .createdDate(modified)
                .parent(parent)
                .text(text)
                .updatedDate(modified)
                .build());

        A anchor;
        CommentAction commentAction;
        if (parent == null) {
            if (anchorFunction == null) {
                anchor = null;
            } else {
                anchor = anchorFunction.apply(comment);
                anchorDao.create(validate(validator, anchor, Create.class));
            }
            commentAction = CommentAction.ADDED;

            eventPublisher.publish(entity.accept(new CommentableVisitor<Object>() {

                @Override
                public Object visit(@Nonnull CommitDiscussion discussion) {
                    return new CommitDiscussionCommentAddedEvent(AbstractCommentHelper.this, discussion,
                            comment, null, (CommitDiscussionCommentAnchor) anchor);
                }

                @Override
                public Object visit(@Nonnull PullRequest pullRequest) {
                    return new PullRequestCommentAddedEvent(AbstractCommentHelper.this, pullRequest,
                            comment, null, (PullRequestDiffCommentAnchor) anchor);
                }
            }));
        } else {
            anchor = null;
            commentAction = CommentAction.REPLIED;

            eventPublisher.publish(entity.accept(new CommentableVisitor<Object>() {

                @Override
                public Object visit(@Nonnull CommitDiscussion discussion) {
                    return new CommitDiscussionCommentRepliedEvent(AbstractCommentHelper.this, discussion,
                            comment, comment.getParent());
                }

                @Override
                public Object visit(@Nonnull PullRequest pullRequest) {
                    return new PullRequestCommentRepliedEvent(AbstractCommentHelper.this, pullRequest,
                            comment, comment.getParent());
                }
            }));
        }
        addActivity(entity, comment, anchor, commentAction);

        return postProcessor.process(entity, comment);
    }

    protected void processAnchors(T entity, List<A> anchors) {
        postProcessor.processAll(entity, anchors.stream()
                .map(InternalDiffCommentAnchor::getComment)
                .collect(MoreCollectors.toImmutableSet()));
    }

    protected void validateFileType(AbstractAddLineCommentRequest request) {
        if (request.getLineType() == DiffSegmentType.REMOVED && request.getFileType() != DiffFileType.FROM) {
            throw new ArgumentValidationException(i18nService.createKeyedMessage("bitbucket.service.comment.error.filetype",
                    DiffSegmentType.REMOVED, DiffFileType.FROM, request.getFileType()));
        }
        if (request.getLineType() == DiffSegmentType.ADDED && request.getFileType() != DiffFileType.TO) {
            throw new ArgumentValidationException(i18nService.createKeyedMessage("bitbucket.service.comment.error.filetype",
                    DiffSegmentType.ADDED, DiffFileType.TO, request.getFileType()));
        }
    }

    private InternalComment getCommentOrFail(long commentId) {
        InternalComment comment = commentDao.getById(commentId);
        if (comment == null) {
            throw new NoSuchCommentException(i18nService.createKeyedMessage("bitbucket.service.comment.nosuchcomment"));
        }
        return comment;
    }

    private InternalComment getCommentOrFail(long commentId, int version) {
        InternalComment comment = getCommentOrFail(commentId);
        validateVersion(version, comment.getVersion());

        return comment;
    }

    private void validateVersion(int expectedVersion, int actualVersion) {
        if (actualVersion != expectedVersion) {
            throw new CommentOutOfDateException(i18nService.createKeyedMessage("bitbucket.service.comment.outofdate"),
                    expectedVersion, actualVersion);
        }
    }
}
