package com.atlassian.stash.internal.task;

import com.atlassian.bitbucket.task.*;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.plugin.spring.AvailableToPlugins;
import com.atlassian.bitbucket.comment.NoSuchCommentException;
import com.atlassian.bitbucket.event.task.TaskCreatedEvent;
import com.atlassian.bitbucket.event.task.TaskDeletedEvent;
import com.atlassian.bitbucket.event.task.TaskUpdatedEvent;
import com.atlassian.bitbucket.AuthorisationException;
import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.stash.internal.annotation.Secured;
import com.atlassian.stash.internal.annotation.Unsecured;
import com.atlassian.stash.internal.comment.CommentDao;
import com.atlassian.stash.internal.comment.InternalComment;
import com.atlassian.stash.internal.pull.InternalPullRequest;
import com.atlassian.stash.internal.pull.InternalPullRequestParticipantHelper;
import com.atlassian.stash.internal.pull.PullRequestDao;
import com.atlassian.stash.internal.task.enrich.TaskEnricher;
import com.atlassian.stash.internal.user.InternalApplicationUser;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.google.common.base.CharMatcher;
import com.google.common.collect.Iterables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

import static com.atlassian.stash.internal.InternalConverter.convertToInternalTask;
import static com.atlassian.stash.internal.InternalConverter.convertToInternalUser;
import static com.atlassian.bitbucket.permission.Permission.REPO_READ;
import static com.google.common.base.Preconditions.checkNotNull;

@AvailableToPlugins(TaskService.class)
@Service("taskService")
public class DefaultTaskService implements InternalTaskService {

    private static final Logger log = LoggerFactory.getLogger(DefaultTaskService.class);

    private final AuthenticationContext authenticationContext;
    private final CommentDao commentDao;
    private final I18nService i18nService;
    private final InternalPullRequestParticipantHelper participantHelper;
    private final PermissionService permissionService;
    private final PullRequestDao pullRequestDao;
    private final TaskDao taskDao;
    private final TaskEnricher taskEnricher;
    private final TaskPermissionsHelper taskPermissionsHelper;
    private final TaskPostProcessor taskPostProcessor;
    private final EventPublisher eventPublisher;

    private final int maxAnchorsPerRequest;
    private final int maxContextsPerRequest;
    private final int maxTasksPerRequest;

    @Autowired
    public DefaultTaskService(AuthenticationContext authenticationContext, CommentDao commentDao,
                              EventPublisher eventPublisher, I18nService i18nService,
                              @Value("${task.max.anchors.per.request}") int maxAnchorsPerRequest,
                              @Value("${task.max.contexts.per.request}") int maxContextsPerRequest,
                              @Value("${task.max.tasks.per.request}") int maxTasksPerRequest,
                              InternalPullRequestParticipantHelper participantHelper,
                              PermissionService permissionService, PullRequestDao pullRequestDao,
                              TaskDao taskDao, TaskEnricher taskEnricher,
                              TaskPermissionsHelper taskPermissionsHelper, TaskPostProcessor taskPostProcessor) {
        this.authenticationContext = authenticationContext;
        this.eventPublisher = eventPublisher;
        this.commentDao = commentDao;
        this.i18nService = i18nService;
        this.maxAnchorsPerRequest = maxAnchorsPerRequest;
        this.maxContextsPerRequest = maxContextsPerRequest;
        this.maxTasksPerRequest = maxTasksPerRequest;
        this.participantHelper = participantHelper;
        this.permissionService = permissionService;
        this.pullRequestDao = pullRequestDao;
        this.taskDao = taskDao;
        this.taskEnricher = taskEnricher;
        this.taskPermissionsHelper = taskPermissionsHelper;
        this.taskPostProcessor = taskPostProcessor;
    }

    @Nonnull
    @Override
    @Transactional
    @Unsecured("Internal interface method")
    public TaskCount count(@Nonnull InternalTaskSearchRequest request) {
        checkNotNull(request, "request");

        request = restrictAnchors(request);

        return taskDao.count(request);
    }

    @Nonnull
    @Override
    @Transactional
    @Unsecured("Instances of TaskContext imply that the permissions to obtain them was already performed")
    public <T extends InternalTaskContext> Map<T, TaskCount> countForContexts(@Nonnull Iterable<T> contexts) {
        checkNotNull(contexts, "contexts");

        contexts = restrictContexts(contexts);

        return taskDao.countForContexts(contexts);
    }

    @Nonnull
    @Override
    @Secured("Permission check performed internally")
    @Transactional
    public Task create(@Nonnull TaskCreateRequest request) {
        checkNotNull(request, "request");

        TaskAnchor anchor;
        InternalTaskContext context;

        switch (request.getAnchorType()) {
            case COMMENT:
                anchor = getCommentOrFail(request.getAnchorId());
                InternalPullRequest pullRequest = getContextOrFail(request.getAnchorId());
                validateContextIsVisible(pullRequest);
                participantHelper.makeCurrentUserParticipantAndWatcher(pullRequest);
                context = pullRequest;
                break;
            default:
                throw new IllegalArgumentException("Unknown anchor type: " + request.getAnchorType());
        }

        InternalApplicationUser author = convertToInternalUser(authenticationContext.getCurrentUser());

        InternalTask taskToCreate = new InternalTask.Builder()
                .anchor(anchor)
                .author(author)
                .context(context)
                .createdDate(new Date())
                .text(normalizeText(request.getText()))
                .build();

        InternalTask task = taskPostProcessor.process(taskDao.create(taskToCreate));
        eventPublisher.publish(new TaskCreatedEvent(this, task));
        return task;
    }

    @Override
    @Secured("Permission check performed internally")
    @Transactional
    public void delete(long taskId) {
        InternalTask taskToDelete = getById(taskId);

        if (taskToDelete == null) {
            throw new NoSuchTaskException(i18nService.createKeyedMessage("bitbucket.service.task.nosuchtask", taskId));
        }

        validateTaskCanBeDeleted(taskToDelete);

        taskDao.delete(taskToDelete);
        eventPublisher.publish(new TaskDeletedEvent(this, taskToDelete));
    }

    @Nullable
    @Override
    @Secured("Permission check performed internally")
    @Transactional
    public InternalTask getById(long taskId) {
        InternalTask task = taskDao.getById(taskId);
        if (task == null || !permissionService.hasRepositoryPermission(task.getContext().getScopeRepository(), REPO_READ)) {
            return null;
        }

        return taskPostProcessor.process(task);
    }

    @Override
    @Transactional
    @Unsecured("An instance of TaskAnchor implies that the permission to obtain it was already verified")
    public boolean hasTasks(@Nonnull TaskAnchor anchor) {
        checkNotNull(anchor, "anchor");

        return taskDao.hasTasks(anchor);
    }

    @Nonnull
    @Override
    @Transactional
    @Unsecured("Internal interface method")
    public Page<Task> search(@Nonnull InternalTaskSearchRequest searchRequest, @Nonnull PageRequest pageRequest, boolean withProperties) {
        checkNotNull(searchRequest, "searchRequest");
        checkNotNull(pageRequest, "pageRequest");

        // apply limits to the number of anchors and tasks
        searchRequest = restrictAnchors(searchRequest);
        pageRequest = pageRequest.buildRestrictedPageRequest(maxTasksPerRequest);

        Page<InternalTask> page = taskDao.search(searchRequest, pageRequest);

        taskPostProcessor.processAll(searchRequest.getContext(), page.getValues());

        if (withProperties) {
            taskEnricher.enrich(searchRequest.getContext(), page.getValues());
        }

        return PageUtils.asPageOf(Task.class, page);
    }

    @Nonnull
    @Override
    @Secured("Permission check performed internally")
    @Transactional
    public Task update(@Nonnull TaskUpdateRequest request) {
        checkNotNull(request, "request");

        InternalTask taskToUpdate = convertToInternalTask(request.getTask());

        InternalTaskContext context = taskToUpdate.getContext();
        validateContextIsVisible(context);

        InternalTask.Builder update = new InternalTask.Builder(taskToUpdate);
        if (request.getState() != null) {
            update.state(request.getState());
        }
        if (request.getText() != null && !request.getText().equals(taskToUpdate.getText())) {
            validateTaskCanBeUpdated(taskToUpdate);
            update.text(normalizeText(request.getText()));
        }
        InternalTask task = taskPostProcessor.process(taskDao.update(update.build()));

        updateParticipant(task.getContext());
        eventPublisher.publish(new TaskUpdatedEvent(this, task, taskToUpdate.getState(), taskToUpdate.getText()));
        return task;
    }

    private void updateParticipant(InternalTaskContext context) {
        //Handling only InternalPullRequest
        if (context instanceof InternalPullRequest) {
            participantHelper.makeCurrentUserParticipantAndWatcher((InternalPullRequest) context);
        }
    }

    @Nonnull
    private InternalComment getCommentOrFail(long commentId) {
        // Using the DAO because using the pull request service introduces a circular dependency.
        // As a result, the returned comment has not been post-processed (which is fine for our purposes).
        InternalComment comment = commentDao.getById(commentId);
        if (comment == null) {
            throw new NoSuchCommentException(i18nService.createKeyedMessage("bitbucket.service.comment.nosuchcomment"));
        }
        return comment;
    }

    @Nonnull
    private InternalPullRequest getContextOrFail(long commentId) {
        InternalPullRequest pullRequest = pullRequestDao.findByComment(commentId);

        if (pullRequest == null) {
            throw new NoSuchCommentException(i18nService.createKeyedMessage("bitbucket.service.comment.nosuchcomment"));
        }

        return pullRequest;
    }

    private InternalTaskSearchRequest restrictAnchors(InternalTaskSearchRequest request) {
        Set<Long> anchorIds = request.getAnchorIds();
        if (anchorIds.size() > maxAnchorsPerRequest) {
            log.warn("Restricting the search to the first {} anchors (out of {} total)", maxAnchorsPerRequest, anchorIds.size());
            Collection<Long> restricted = new ArrayList<>(maxAnchorsPerRequest);
            Iterables.addAll(restricted, Iterables.limit(anchorIds, maxAnchorsPerRequest));

            return new InternalTaskSearchRequest.Builder(request.getContext())
                    .anchors(restricted, request.getAnchorType())
                    .build();
        }
        return request;
    }

    private <T extends InternalTaskContext> Iterable<T> restrictContexts(Iterable<T> contexts) {
        return Iterables.limit(contexts, maxContextsPerRequest);
    }

    private void validateContextIsVisible(InternalTaskContext context) {
        if (!permissionService.hasRepositoryPermission(context.getScopeRepository(), REPO_READ)) {
            throw new NoSuchEntityException(i18nService.createKeyedMessage("bitbucket.service.task.nosuchcontext", context.getId()));
        }
    }

    private void validateTaskCanBeDeleted(InternalTask task) {
        if (!(isAuthorizedToUpdateOrDelete(task))) {
            throw new AuthorisationException(i18nService.createKeyedMessage("bitbucket.service.task.delete.unauthorized"));
        }

        if (task.getState().equals(TaskState.RESOLVED)) {
            throw new IllegalTaskStateException(i18nService.createKeyedMessage("bitbucket.service.task.delete.illegal"), TaskState.RESOLVED);
        }
    }

    private void validateTaskCanBeUpdated(InternalTask internalTask) {
        if (!isAuthorizedToUpdateOrDelete(internalTask)) {
            throw new AuthorisationException(i18nService.createKeyedMessage("bitbucket.service.task.update.unauthorized"));
        }

        if (internalTask.getState().equals(TaskState.RESOLVED)) {
            throw new IllegalTaskStateException(i18nService.createKeyedMessage("bitbucket.service.task.update.illegal"), TaskState.RESOLVED);
        }
    }

    private boolean isAuthorizedToUpdateOrDelete(InternalTask task) {
        InternalTaskContext context = task.getContext();
        boolean isRepoAdmin = taskPermissionsHelper.isRepoAdmin(context.getScopeRepository());

        return isRepoAdmin || taskPermissionsHelper.hasFullPermission(context, task);
    }

    private String normalizeText(String originalText) {
        return CharMatcher.WHITESPACE.trimAndCollapseFrom(originalText, ' ');
    }

}
