package com.atlassian.stash.internal.plugin;

import com.atlassian.bitbucket.server.ApplicationMode;
import com.atlassian.plugin.ModuleDescriptorFactory;
import com.atlassian.plugin.Plugin;
import com.atlassian.plugin.PluginParseException;
import com.atlassian.plugin.event.NotificationException;
import com.atlassian.plugin.event.PluginEventManager;
import com.atlassian.plugin.loaders.PluginLoader;
import com.atlassian.plugin.manager.DefaultPluginManager;
import com.atlassian.plugin.manager.PluginPersistentState;
import com.atlassian.plugin.manager.PluginPersistentStateStore;
import com.atlassian.stash.internal.plugin.event.PluginInstallRequestedEvent;
import com.google.common.collect.ImmutableSet;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * Extends the {@code DefaultPluginManager} to raise a {@link PluginInstallRequestedEvent} during {@code earlyStartup}
 * so that the {@link LifecyclePluginListener} can track the progress of starting all of the installed plugins.
 *
 * @since 3.2
 */
public class StashPluginManager extends DefaultPluginManager {

    // TODO: Fix our plugin bundling so that we can specify a different list of plugins to enable
    // depending on the application mode... thus removing these lists and the filtering code that uses it
    private static final Set<String> PLUGINS_DISABLED_FOR_DEFAULT = ImmutableSet.<String>builder()
            .add("com.atlassian.bitbucket.server.bitbucket-mirroring-mirror")
            .build();
    private static final Set<String> PLUGINS_DISABLED_FOR_MIRROR = ImmutableSet.<String>builder().add(
            "com.atlassian.bitbucket.search.bitbucket-search-common",
            "com.atlassian.bitbucket.search.bitbucket-search-index",
            "com.atlassian.bitbucket.search.bitbucket-search-query",
            "com.atlassian.bitbucket.search.embedded-elasticsearch-plugin",
            "com.atlassian.bitbucket.server.bitbucket-branch",
            "com.atlassian.bitbucket.server.bitbucket-cluster-info",
            "com.atlassian.bitbucket.server.bitbucket-comment-likes",
            "com.atlassian.bitbucket.server.bitbucket-comment-properties",
            "com.atlassian.bitbucket.server.bitbucket-compare",
            "com.atlassian.bitbucket.server.bitbucket-crowd-sso",
            "com.atlassian.bitbucket.server.bitbucket-default-reviewers",
            "com.atlassian.bitbucket.server.bitbucket-highlight",
            "com.atlassian.bitbucket.server.bitbucket-importer",
            "com.atlassian.bitbucket.server.bitbucket-jira",
            "com.atlassian.bitbucket.server.bitbucket-jira-development-integration",
            "com.atlassian.bitbucket.server.bitbucket-markup-renderers",
            "com.atlassian.bitbucket.server.bitbucket-mirroring-upstream",
            "com.atlassian.bitbucket.server.bitbucket-pull-request-branch-deletion",
            "com.atlassian.bitbucket.server.bitbucket-pull-request-properties",
            "com.atlassian.bitbucket.server.bitbucket-readme",
            "com.atlassian.bitbucket.server.bitbucket-ref-restriction",
            "com.atlassian.bitbucket.server.bitbucket-repository-ref-sync",
            "com.atlassian.bitbucket.server.bitbucket-sourcetree-plugin",
            "com.atlassian.bitbucket.server.bitbucket-stp",
            "com.atlassian.bitbucket.server.bitbucket-tag",
            "com.atlassian.integration.jira.jira-integration-plugin",
            "com.atlassian.plugins.atlassian-chaperone",
            "com.atlassian.plugins.atlassian-remote-event-common-plugin",
            "com.atlassian.plugins.atlassian-remote-event-consumer-plugin",
            "com.atlassian.plugins.atlassian-remote-event-producer-plugin",
            "com.atlassian.plugins.atlassian-nps-plugin",
            "com.atlassian.plugins.base-hipchat-integration-plugin",
            "com.atlassian.plugins.base-hipchat-integration-plugin-api",
            "com.atlassian.plugins.issue-status-plugin",
            "com.atlassian.stash.plugins.stash-hipchat-integration-plugin",
            "com.atlassian.stash.plugins.stash-remote-event-bitbucket-server-spi"
    ).build();

    private final PluginEventManager eventManager;
    private final ApplicationMode mode;
    private final PluginUpgradeHelper upgradeHelper;

    private volatile boolean starting;

    public StashPluginManager(PluginPersistentStateStore store, List<PluginLoader> pluginLoaders,
                              ModuleDescriptorFactory moduleDescriptorFactory,
                              PluginEventManager eventManager,
                              PluginUpgradeHelper upgradeHelper, ApplicationMode mode) {
        super(store, pluginLoaders, moduleDescriptorFactory, eventManager);

        this.eventManager = eventManager;
        this.mode = mode;
        this.upgradeHelper = upgradeHelper;
    }

    /**
     * Currently the system has no deferred plugins, so {@code earlyStartup} does the <i>full</i> processing to start
     * the plugin framework and all installed plugins. {@code lateStartup} does nothing, so it is not considered part
     * of the {@link #starting} phase.
     *
     * @throws PluginParseException  if thrown by the base class's implementation
     * @throws NotificationException if thrown by the base class's implementation
     */
    @Override
    public void earlyStartup() throws PluginParseException, NotificationException {
        starting = true; //Use to determine whether addPlugins is being called for the first time
        try {
            upgradeHelper.withUpgrade(super::earlyStartup);
        } finally {
            starting = false;
        }
    }

    /**
     * When called during {@link #starting startup}, raises a {@link PluginInstallRequestedEvent} with the number
     * of plugins to be added. All processing is delegated to the base class as normal.
     *
     * @param loader           the loader, generally {@code null}
     * @param pluginsToInstall one or more {@code Plugin}s to install
     * @throws PluginParseException if thrown by the base class's implementation
     */
    @Override
    protected void addPlugins(PluginLoader loader, Collection<Plugin> pluginsToInstall) throws PluginParseException {
        if (starting) { //Will not be true during lateStartup, since we do not defer any plugins

            Set<String> disabledPlugins = mode == ApplicationMode.MIRROR
                    ? PLUGINS_DISABLED_FOR_MIRROR : PLUGINS_DISABLED_FOR_DEFAULT;

            Iterator<Plugin> iter = pluginsToInstall.iterator();
            while (iter.hasNext()) {
                Plugin plugin = iter.next();
                if (disabledPlugins.contains(plugin.getKey())) {
                    // mutating the collection is the right thing to do here. The code in DefaultPluginManager
                    // iterates over it after the call to addPlugins for something else, and we want to remove
                    // the blacklisted plugins so that that subsequent loop doesn't see them.
                    iter.remove();
                }
            }

            eventManager.broadcast(new PluginInstallRequestedEvent(this, pluginsToInstall.size()));
        }

        super.addPlugins(loader, pluginsToInstall);
    }

    @Override
    protected PluginPersistentState getState() {
        return upgradeHelper.wrap(super.getState());
    }
}
