package com.atlassian.stash.internal.watcher;

import com.atlassian.event.api.EventPublisher;
import com.atlassian.bitbucket.event.watch.WatcherAddedEvent;
import com.atlassian.bitbucket.event.watch.WatcherRemovedEvent;
import com.atlassian.stash.internal.AbstractService;
import com.atlassian.stash.internal.InternalConverter;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.watcher.Watchable;
import com.atlassian.bitbucket.watcher.Watcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Nonnull;
import java.util.Set;


/**
 * A default implementation of the {@link InternalWatcherService}.
 * <p>
 * This service is currently only suitable for use as an implementation detail of other services.
 * <p>
 * Permissions: this class is in internal service and assumes, for the most part, that appropriate permissions
 * checking has already been done by the caller. There remains a liberal sprinkling of @PreAuthroise("isAuthenticated()")
 * because these act as pre-conditions for the methods they annotate.
 *
 * @since 1.3
 */
@Service("watcherService")
@SuppressWarnings("ConstantConditions")
public class DefaultWatcherService extends AbstractService implements InternalWatcherService {

    private final AuthenticationContext authenticationContext;
    private final WatcherDao watcherDao;
    private final EventPublisher eventPublisher;

    @Autowired
    public DefaultWatcherService(AuthenticationContext authenticationContext, WatcherDao watcherDao,
                                 EventPublisher eventPublisher) {
        this.authenticationContext = authenticationContext;
        this.watcherDao = watcherDao;
        this.eventPublisher = eventPublisher;
    }

    /**
     * Note: calling this method will result in an NPE inside StashUserAuthenticationToken if there is no context user,
     * but the thread is running with elevated permissions granted by the SecurityService. See STASHDEV-1801
     */
    @Nonnull
    @Override
    @PreAuthorize("isAuthenticated()")
    @Transactional
    public Watcher addCurrentUserAsWatcher(@Nonnull InternalWatchable watchable) {
        return addUserAsWatcher(watchable, authenticationContext.getCurrentUser());
    }

    @Nonnull
    @Override
    @Transactional
    public Watcher addUserAsWatcher(@Nonnull InternalWatchable watchable, @Nonnull ApplicationUser user) {
        InternalWatcher watcher = watcherDao.findByWatchableAndUser(watchable, user);
        if (watcher == null) {
            watcher = watcherDao.create(new InternalWatcher.Builder()
                    .user(InternalConverter.convertToInternalUser(user))
                    .watchable(watchable)
                    .build());
            eventPublisher.publish(new WatcherAddedEvent(this, watcher));
        }

        return watcher;
    }

    @Override
    public long countWatchers(@Nonnull Watchable watchable) {
        return watcherDao.countWatchers(watchable);
    }

    @Nonnull
    @Override
    public Set<? extends Watcher> findWatchers(@Nonnull Watchable watchable) {
        return watcherDao.findByWatchable(watchable);
    }

    @Override
    @PreAuthorize("isAuthenticated()")
    public boolean isCurrentUserWatching(@Nonnull Watchable watchable) {
        return watcherDao.findByWatchableAndUser(watchable, authenticationContext.getCurrentUser()) != null;
    }

    @Override
    @PreAuthorize("isAuthenticated()")
    @Transactional
    public boolean removeCurrentUserAsWatcher(@Nonnull InternalWatchable watchable) {
        return removeUserAsWatcher(watchable, authenticationContext.getCurrentUser());
    }

    @Override
    @Transactional
    public boolean removeUserAsWatcher(@Nonnull InternalWatchable watchable, @Nonnull ApplicationUser user) {
        InternalWatcher watcher = watcherDao.findByWatchableAndUser(watchable, user);
        if (watcher == null) {
            return false;
        }

        watcherDao.delete(watcher);
        eventPublisher.publish(new WatcherRemovedEvent(this, watcher));
        return true;
    }
}
