package com.atlassian.stash.sal.api.lifecycle;

import com.atlassian.plugin.PluginAccessor;
import com.atlassian.plugin.event.PluginEventListener;
import com.atlassian.plugin.event.PluginEventManager;
import com.atlassian.plugin.event.events.PluginDisablingEvent;
import com.atlassian.plugin.event.events.PluginEnabledEvent;
import com.atlassian.plugin.event.events.PluginFrameworkShuttingDownEvent;
import com.atlassian.plugin.event.events.PluginFrameworkStartedEvent;
import com.atlassian.plugin.util.collect.Consumer;
import com.atlassian.plugin.util.collect.Predicate;
import com.atlassian.sal.api.lifecycle.LifecycleAware;
import com.atlassian.sal.api.lifecycle.LifecycleManager;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import org.osgi.framework.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;

import static org.osgi.framework.Constants.OBJECTCLASS;

/**
 * Patched version of SAL's {@link com.atlassian.sal.core.lifecycle.DefaultLifecycleManager} that's included in
 * Bitbucket Server until a new SAL version has been released with the fixes. The changed lines are clearly marked
 * with comments
 */
public abstract class PatchedLifecycleManager implements LifecycleManager, InitializingBean, DisposableBean {
    private static final Logger log = LoggerFactory.getLogger(PatchedLifecycleManager.class);

    private final PluginEventManager pluginEventManager;
    private final PluginAccessor pluginAccessor;
    private final BundleContext bundleContext;

    /**
     * The LifecycleAware services which we know about via OSGi notifications, but which we have not yet called onStart() on.
     * <p/>
     * The documentation for {@link org.osgi.framework.ServiceReference} is quite explicit that while a framework may return
     * multiple ServiceReference objects referring to the same ServiceRegistration, all such ServiceReference objects are equals()
     * and have the same hashCode(), so it is safe to use them in a set like this.
     * <p/>
     * Inspection of Felix code (as at 4.2.1 of this writing) shows that in fact Felix is careful to have only a single
     * ServiceReference for each ServiceRegistration. If we move to a framework where this is not so, or if this causes problems,
     * we'd need to replace this with a map from the ServiceReference.getProperty(SERVICE_ID) to ServiceReference so we can key
     * service tracking off SERVICE_ID.
     */
    private final Set<ServiceReference<LifecycleAware>> pendingOnStart;
    private final Set<ServiceReference<LifecycleAware>> pendingOnStop;
    private final Queue<String> pluginsPendingOnStart;
    private final ServiceListener serviceListener;

    private volatile boolean started;

    public PatchedLifecycleManager(
            final PluginEventManager pluginEventManager,
            final PluginAccessor pluginAccessor,
            final BundleContext bundleContext) {
        this.pluginEventManager = pluginEventManager;
        this.pluginAccessor = pluginAccessor;
        this.bundleContext = bundleContext;
        this.pendingOnStart = Collections.synchronizedSet(new HashSet<ServiceReference<LifecycleAware>>());
        this.pendingOnStop = Collections.synchronizedSet(new HashSet<ServiceReference<LifecycleAware>>());
        this.pluginsPendingOnStart = new ConcurrentLinkedQueue<>();
        this.serviceListener = new LifecycleAwareServiceListener();
        this.started = false;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        pluginEventManager.register(this);
        // Gather all the current LifecycleAware instances, and watch for new ones
        final String filter = "(" + OBJECTCLASS + "=" + LifecycleAware.class.getName() + ")";
        // Since OSGi doesn't (afaics) provide an atomic way to add a ServiceListener and get brought up to date, we need to be a
        // bit careful here. If we miss an add, we won't call an onStart() we should, and if we miss a remove we'll possibly leak.
        // So we starting listening, add everything we can see, and discard anything invalid.

        // An an aside, add everything, add listener, add everything doesn't work - a service that is removed after the snapshot
        // for the second add everything is readded when this snapshot is added, and so you end up missing a remove.

        // Services that come and go here before this point are irrelevant
        bundleContext.addServiceListener(serviceListener, filter);
        // Anything added from now on is captured, so we don't lose services
        final Collection<ServiceReference<LifecycleAware>> services =
                bundleContext.getServiceReferences(LifecycleAware.class, null);
        // Our snapshot may contain services that have already made it to pending. This means we might get double adds (which the
        // set takes care of), or we might re-add something in our snapshot that gets removed in this window. Such a service might
        // have turned up before we started listening or after, so the listener might think the remove is spurious or normal, but
        // the service is in our snapshot.
        pendingOnStart.addAll(services);
        // Any remove from now on is correctly processed - we've gathered everything and put it it pendingOnStart. However,
        // we might have re-added some service that was removed post snapshot but before we updated pendingOnStart. To deal
        // with these stale services, we do a sweep and chuck them out.  We can sweep the services from the getServiceReferences
        // snapshot, because those that arrived via the listener will be present when removed. A service added before the listener
        // is added but removed afterwards, but before the snapshot, is just ignored as a spurious remove.
        for (final ServiceReference<LifecycleAware> service : services) {
            // The documentation for getBundle() says this is a sanctioned way to detect deregistration
            if (null == service.getBundle()) {
                pendingOnStart.remove(service);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void clearPendingOnStop() {
        if (pendingOnStop.isEmpty()) {
            // all onStop() delivered
            return;
        }

        // it's safe to cast here, we know what pendingOnStop is
        log.warn("Failed to notify with LifecycleAware.onStop(): {}",
                listPluginKeys(Arrays.asList((ServiceReference<Object>[]) pendingOnStop.toArray())));

        pendingOnStop.clear();
    }

    @Override
    public void destroy() throws Exception {
        bundleContext.removeServiceListener(serviceListener);
        // We  clear out our pendingOnStart, to assist garbage collection. This is just a heuristic - if this code moves
        // this is *not* sufficient to prevent leaks, since a thread could be in serviceListener now, having arrived before we
        // removed the listener but not yet finished. Since this is a @PreDestroy and things are going away, the GC will save us.
        // If we do move this code, we should probably move pendingOnStart to the serviceListener and have their lifetimes
        // match, but I'm not going to pre-emptively solve this synchronization problem without knowledge of the lifetimes and where
        // things are moving to.
        pendingOnStart.clear();
        pluginsPendingOnStart.clear();
        clearPendingOnStop();
        pluginEventManager.unregister(this);
    }

    @PluginEventListener
    public void onPluginFrameworkStarted(final PluginFrameworkStartedEvent event) {
        startIfApplicationSetup();
    }

    @PluginEventListener
    public void onPluginEnabled(final PluginEnabledEvent event) {
        String pluginKey = event.getPlugin().getKey();
        if (started) {
            notifyStartableLifecycleAwaresForPlugin(pluginKey);
        } else {
            pluginsPendingOnStart.add(pluginKey);
        }
    }

    @PluginEventListener
    public void onPluginFrameworkShuttingDown(final PluginFrameworkShuttingDownEvent event) {
        // We notifying all waiting onStop() here, as the next thing after this event is
        // all plugins unregistered

        final Collection<ServiceReference<LifecycleAware>> completed = notifyLifecycleAwares(pendingOnStop,
                new Predicate<ServiceReference<LifecycleAware>>() {
                    @Override
                    public boolean evaluate(final ServiceReference<LifecycleAware> lifecycleAwareServiceReference) {
                        return notifyOnStopIfEnabled(lifecycleAwareServiceReference);
                    }
                });

        // Now remove those we handled.
        pendingOnStop.removeAll(completed);
    }

    private Predicate<ServiceReference<LifecycleAware>> notifyIfMyEvent(final PluginDisablingEvent event) {
        return new Predicate<ServiceReference<LifecycleAware>>() {
            @Override
            public boolean evaluate(final ServiceReference<LifecycleAware> lifecycleAwareServiceReference) {
                final Bundle bundle = lifecycleAwareServiceReference.getBundle();
                if (bundle == null) {
                    log.warn("Discarding onStop() for stale LifecycleAware");
                    // This reference is stale, so we report true as it can be discarded
                    return true;
                }

                // Notify if the event is for a plugin that waits for onStop()
                return getPluginKey(bundle).equals(event.getPlugin().getKey())
                        && notifyOnStopIfEnabled(lifecycleAwareServiceReference);
            }
        };
    }

    @PluginEventListener
    public void onPluginDisabling(final PluginDisablingEvent event) {
        final Collection<ServiceReference<LifecycleAware>> completed =
                notifyLifecycleAwares(pendingOnStop, notifyIfMyEvent(event));

        // Now remove those we handled.
        pendingOnStop.removeAll(completed);
    }

    public void start() {
        startIfApplicationSetup();
    }

    private void startIfApplicationSetup() {
        final boolean doSetup = !started && isApplicationSetUp();
        if (doSetup) {
            started = true;
            // We could call the internal notify irrespective of doSetup, since it guards internally, but since we need to guard
            // for subclasses, we may as well hoist this in here and save a bit of effort in the no-op case.
            notifyStartableLifecycleAwares();
            notifyOnStart();
        }
    }

    /**
     * An override point for subclasses that require callback notification after the initial start of the system.
     */
    protected void notifyOnStart() {
    }

    private void notifyStartableLifecycleAwares() {
        // Notify the LifecycleAwares in the order in which plugins have been enabled. This ensures that if
        // LifecycleAware component X from plugin A depends on LifecycleAware component Y from plugin B, Y is notified
        // before X is.
        String pluginKey;
        while ((pluginKey = pluginsPendingOnStart.poll()) != null) {
            notifyStartableLifecycleAwaresForPlugin(pluginKey);
        }
        int numPending = pendingOnStart.size();
        if (numPending > 0) {
            // all pending LifecycleAware services should have been started, but it _is_ possible that another plugin
            // has been enabled while the pending plugins were being processed after startup. We'll kick off processing
            // for them here just in case and log a non-alarming message so we can determine from the logs that this
            // unlikely event occurred.
            log.info("{} LifecycleAware services found that are pending initialization", numPending);
            notifyStartableLifecycleAwares(pendingOnStart);
        }
    }

    private void notifyStartableLifecycleAwaresForPlugin(final String pluginKey) {
        // copy to prevent concurrent modification problems
        final Set<ServiceReference<LifecycleAware>> pendingForPlugin = new HashSet<>();
        synchronized (pendingOnStart) {
            pendingForPlugin.addAll(pendingOnStart);
        }

        // only retain the LifeCycleAware services from the plugin.
        final Iterator<ServiceReference<LifecycleAware>> it = pendingForPlugin.iterator();
        while (it.hasNext()) {
            ServiceReference<LifecycleAware> ref = it.next();
            if (!pluginKey.equals(getPluginKeyFromBundle(ref.getBundle()))) {
                // the LifecycleAware service is from a different plugin
                it.remove();
            }
        }

        final int startedCount = notifyStartableLifecycleAwares(pendingForPlugin);
        if (startedCount > 0 && log.isTraceEnabled()) {
            log.trace("Started {} LifeCycleAware services for plugin {}; {} still pending for other plugins",
                    startedCount, pluginKey, pendingOnStart.size());
        }
    }

    private int notifyStartableLifecycleAwares(final Set<ServiceReference<LifecycleAware>> serviceReferences) {
        final Collection<ServiceReference<LifecycleAware>> completed = notifyLifecycleAwares(serviceReferences,
                new Predicate<ServiceReference<LifecycleAware>>() {
                    @Override
                    public boolean evaluate(final ServiceReference<LifecycleAware> lifecycleAwareServiceReference) {
                        return notifyOnStartIfStartedAndEnabled(lifecycleAwareServiceReference);
                    }
                });

        // Now remove those we handled.
        pendingOnStart.removeAll(completed);
        // and mark them waiting onStop
        pendingOnStop.addAll(completed);

        return completed.size();
    }

    private Collection<ServiceReference<LifecycleAware>> notifyLifecycleAwares(final Set<ServiceReference<LifecycleAware>> lifeCycleAwares,
                                                                               final Predicate<ServiceReference<LifecycleAware>> event) {
        // Grab a copy to operate on, so we don't call onStart()/onStop() holding a lock. We're using toArray here to keep the
        // synchronization clear at the cost of the unchecked cast below. If we went via ImmutableList.copyOf, we'd need to either
        // synchronize externally or depend on the implementation of copyOf - see Collections.synchronizedSet re iteration.
        final Object[] pending = lifeCycleAwares.toArray();

        // Notify those we can, remembering what we handled.
        final List<ServiceReference<LifecycleAware>> completed = new ArrayList<>(pending.length);
        for (final Object serviceRaw : pending) {
            @SuppressWarnings("unchecked")
            // serviceRaw comes from Object[] pending which is a snapshot of lifeCycleAwares so the cast is safe
            final ServiceReference<LifecycleAware> service = (ServiceReference<LifecycleAware>) serviceRaw;
            if (event.evaluate(service)) {
                completed.add(service);
            }
        }
        return completed;
    }

    /**
     * Notify a LifecycleAware with onStart() if the system is started and the plugin publishing the service is enabled.
     *
     * @param service the service to attempt to start
     * @return true iff the service was started or is stale, false if it should be retained for later processing.
     */
    private boolean notifyOnStartIfStartedAndEnabled(final ServiceReference<LifecycleAware> service) {
        if (started) {
            return notifyLifecyleAware(service,
                    new Consumer<LifecycleAware>() {
                        @Override
                        public void consume(final LifecycleAware lifecycleAware) {
                            lifecycleAware.onStart();
                        }

                        @Override
                        public String toString() {
                            return "onStart()";
                        }
                    });
        } else {
            // We have not called onStart and need to later (when the system is started)
            return false;
        }
    }

    private boolean notifyLifecyleAware(final ServiceReference<LifecycleAware> service,
                                        final Consumer<LifecycleAware> event) {
        final Bundle bundle = service.getBundle();
        final LifecycleAware lifecycleAware = bundleContext.getService(service);
        try {
            // Either of bundle or lifecycleAware can be null if the service isn't registered.
            if ((null != bundle) && (null != lifecycleAware)) {
                final String pluginKey = getPluginKey(bundle);
                if (pluginAccessor.isPluginEnabled(pluginKey)) {
                    try {
                        log.debug("Calling LifecycleAware.{} '{}' from plugin '{}'",
                                event, lifecycleAware, pluginKey);
                        event.consume(lifecycleAware);
                    } catch (final Throwable ex) {
                        log.error("LifecycleAware.{} failed for component with class '{}' from plugin '{}'",
                                event, lifecycleAware.getClass().getName(), pluginKey, ex);
                    }
                    // We have called event
                    return true;
                } else {
                    // We have not called event and need to do it later
                    //  - for onStart(), when the plugin enables
                    //  - for onStop(), as the last resort, when service is unregistered
                    return false;
                }
            } else {
                // This can happen if we take our snapshot of services to notify, and then one is unregistered. In this case,
                // likely both bundle and lifecycleAware are null, so there's not much we can get in the message here, and it means
                // niceties like trying to safely dig data out of them is probably not worth a whole lot.
                log.warn("Discarding {} for stale LifecycleAware", event);
                // This reference is stale, so we report true as it can be discarded
                return true;
            }
        } finally {
            if (null != lifecycleAware) {
                bundleContext.ungetService(service);
            }
        }
    }

    private boolean notifyOnStopIfEnabled(final ServiceReference<LifecycleAware> service) {
        return notifyLifecyleAware(service,
                new Consumer<LifecycleAware>() {
                    @Override
                    public void consume(final LifecycleAware lifecycleAware) {
                        try {
                            // onStop() is only @since 3.0, try to call it but if it's not there then it's OK
                            lifecycleAware.onStop();
                        } catch (AbstractMethodError e) {
                            final String errorMessage = "Failed to notify with LifecycleAware.onStop()";
                            try {
                                // the method will be there because at least the LifecycleAware interface has it
                                Method onStop = lifecycleAware.getClass().getMethod("onStop");
                                if (onStop.getDeclaringClass() == LifecycleAware.class) {
                                    // expected, the message is for debug if we need it
                                    log.debug(errorMessage, e);
                                } else {
                                    // onStop() is implemented, so we need to rethrow the exception
                                    throw e;
                                }
                            } catch (NoSuchMethodException e1) {
                                // not expected
                                log.warn(errorMessage, e1);
                            }
                        }
                    }

                    @Override
                    public String toString() {
                        return "onStop()";
                    }
                });
    }

    private class LifecycleAwareServiceListener implements ServiceListener {
        /**
         * Handle a service changed by updating the list of pendingOnStart/pendingOnStop or dispatching onStart/onStop as necessary.
         *
         * @param serviceEvent details of the changed service
         */
        @Override
        public void serviceChanged(final ServiceEvent serviceEvent) {
            @SuppressWarnings("unchecked")
            // The cast here is safe since we filter by this object class when adding the service listener
            final ServiceReference<LifecycleAware> service = (ServiceReference<LifecycleAware>) serviceEvent.getServiceReference();
            switch (serviceEvent.getType()) {
                case ServiceEvent.REGISTERED: {
                    if (!notifyOnStartIfStartedAndEnabled(service)) {
                        // We didn't satisfy notification conditions, so hang on to the service for later.
                        pendingOnStart.add(service);
                    }
                    // else we tried to onStart it (we're started and the plugin is enabled), or it's stale. Either way, we're done.
                    break;
                }
                case ServiceEvent.UNREGISTERING: {
                    // It's possible we still have a reference to the service, for example if the plugin got as far as
                    // registering the service before failing, and hence never became enabled. See the comment on the
                    // pendingOnStart field for an explanation of why removing the ServiceReference directly is safe.
                    pendingOnStart.remove(service);

                    // If we failed to notify with onStop earlier, it's our last chance
                    if (pendingOnStop.remove(service)) {
                        log.warn("Notifying with LifecycleAware.onStop() on service unregister");
                        if (!notifyOnStopIfEnabled(service)) {
                            // We failed to notify onStop(), most likely due to plugin has been disabled by now
                            final Bundle bundle = service.getBundle();
                            log.warn("Failed to notify {} with LifecycleAware.onStop()", getPluginKeyFromBundle(bundle));
                        }
                    }
                    break;
                }
                default: {
                    // We filter on objectClass, which org.osgi.framework.ServiceRegistration.setProperties() says cannot be
                    // changed dynamically, so we don't need to handle MODIFIED or MODIFIED_ENDMATCH.
                    break;
                }
            }
        }
    }

    static String getPluginKey(Bundle bundle) {
        String pluginKey = bundle.getHeaders().get("Atlassian-Plugin-Key");
        if (pluginKey != null) {
            return pluginKey;
        }
        return bundle.getSymbolicName() + " - " + bundle.getHeaders().get("Bundle-Version");
    }

    @Nonnull
    static String getPluginKeyFromBundle(@Nullable final Bundle bundle) {
        return bundle == null ? "<stale service reference>" : getPluginKey(bundle);
    }

    @Nonnull
    static <T> String listPluginKeys(@Nonnull Collection<ServiceReference<T>> services) {
        @SuppressWarnings("unchecked")
        Iterable<String> pluginKeys = Iterables.transform(services, new Function<ServiceReference<T>, String>() {
            @Nullable
            @Override
            public String apply(@Nonnull final ServiceReference<T> service) {

                return getPluginKeyFromBundle(service.getBundle());
            }
        });

        return "[" + Joiner.on(", ").join(pluginKeys) + "]";
    }
}

