package com.atlassian.bitbucket.internal.plugin.extender;

import com.atlassian.event.api.EventPublisher;
import com.atlassian.event.config.ListenerHandlersConfiguration;
import com.atlassian.event.spi.ListenerHandler;
import com.atlassian.sal.api.lifecycle.LifecycleAware;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

/**
 * Registers/unregisters beans that implement the {@code EventListener} interfaces, or has method(s) annotated with the
 * {@code @EventListener} annotation with the {@link EventPublisher} on bean creation and bean destruction. Registration
 * and unregistration happens according to the {@link LifecycleAware} contract to ensure that plugins do not receive
 * events from other plugins until they are properly initialized.
 * <p>
 * {@code EventListenerRegistrar} is implemented as a Spring {@link BeanPostProcessor}, which means that it gets called
 * whenever a bean is created or destroyed in the application context. Because we need to get callbacks for all beans
 * that get created, it is important to register an {@code EventListenerRegistrar} instance in the application context
 * as early as possible.
 *
 * @since 4.7
 */
class BeanEventListenerScanner implements BeanPostProcessor, DisposableBean, Ordered {

    private static final Logger log = LoggerFactory.getLogger(BeanEventListenerScanner.class);
    private static final Object IGNORE = new Object();

    private final ReentrantLock lock;
    private final Map<String, Object> beansRegistered;
    private final Map<String, Object> beansUnscanned;
    private final BundleContext bundleContext;
    private final String bundleName;
    private final ServiceTracker<ListenerHandlersConfiguration, ListenerHandlersConfiguration> configurationTracker;
    private final ServiceTracker<EventPublisher, EventPublisher> eventPublisherTracker;
    private final ServiceRegistration<?> lifecycleAwareRegistration;

    private volatile boolean started;

    BeanEventListenerScanner(BundleContext bundleContext) {
        this.bundleContext = bundleContext;

        beansRegistered = new ConcurrentHashMap<>();
        beansUnscanned = new ConcurrentHashMap<>();
        bundleName = bundleContext.getBundle().getSymbolicName();
        lock = new ReentrantLock();

        // register as LifecycleAware so the EventPublisher registration and deregistration happens on lifecycle events
        lifecycleAwareRegistration = bundleContext.registerService(
                LifecycleAware.class.getName(), new EventListenerLifecycle(), null);

        configurationTracker = new ServiceTracker<>(
                bundleContext, ListenerHandlersConfiguration.class, new TriggeringTrackerCustomizer<>());
        eventPublisherTracker = new ServiceTracker<>(
                bundleContext, EventPublisher.class, new TriggeringTrackerCustomizer<>());
        configurationTracker.open();
        eventPublisherTracker.open();
    }

    @Override
    public void destroy() throws Exception {
        unregisterEventListeners();

        configurationTracker.close();
        eventPublisherTracker.close();
        lifecycleAwareRegistration.unregister();
    }

    @Override
    public int getOrder() {
        // process EventListenerRegistrar as early as possible to guarantee that we don't get passed any AOP-ed proxies.
        return 1;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        EventPublisher eventPublisher = null;
        ListenerHandlersConfiguration configuration = null;
        if (started) {
            eventPublisher = eventPublisherTracker.getService();
            configuration = configurationTracker.getService();
        }

        if (eventPublisher != null && configuration != null) {
            registerIfEventListener(eventPublisher, configuration, beanName, bean);
        } else {
            beansUnscanned.put(beanName, bean);
            maybeRegisterUnscannedBeans();
        }

        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    private static boolean isEventListener(ListenerHandlersConfiguration configuration, Object bean) {
        for (ListenerHandler handler : configuration.getListenerHandlers()) {
            if (!handler.getInvokers(bean).isEmpty()) {
                return true;
            }
        }
        return false;
    }

    private void maybeRegisterUnscannedBeans() {
        // only register event listeners if the plugin is in the started state (LifecycleAware) _and_ eventPublisher
        // and configuration are available over OSGI.
        EventPublisher eventPublisher = eventPublisherTracker.getService();
        ListenerHandlersConfiguration configuration = configurationTracker.getService();
        if (started && !beansUnscanned.isEmpty() && eventPublisher != null && configuration != null) {
            // ensure that only one thread attempts to register the beansUnscanned entries at a time. While
            // iteration over the map is thread safe, there is still the potential of two concurrent threads
            // registering the same bean
            lock.lock();
            try {
                Iterator<Map.Entry<String, Object>> it = beansUnscanned.entrySet().iterator();
                Set<String> newlyRegistered = new HashSet<>();
                while (it.hasNext()) {
                    Map.Entry<String, Object> entry = it.next();
                    Object bean = entry.getValue();
                    String name = entry.getKey();
                    if (registerIfEventListener(eventPublisher, configuration, name, bean)) {
                        newlyRegistered.add(name);
                    }
                    it.remove();
                }
                if (log.isDebugEnabled() && !newlyRegistered.isEmpty()) {
                    log.debug("{}: registered [{}] as event listener", bundleName,
                            newlyRegistered.stream().collect(Collectors.joining(", ")));
                }
            } finally {
                lock.unlock();
            }
        }
    }

    private boolean registerIfEventListener(EventPublisher eventPublisher, ListenerHandlersConfiguration configuration,
                                         String name, Object bean) {
        try {
            Object registeredBean = beansRegistered.get(name);
            if (registeredBean != null) {
                // a bean was already created and registered with the give name. This means that the
                // currently registered bean is either a prototype bean or a non-cached plugin module.
                // Either way, these beans should _not_ be registered because we could end up registering
                // an unbounded number of listeners with the event publisher. Having too many listeners
                // will impact event dispatching and overall performance (e.g. deplete the event queue
                // more quickly). Therefore, we should not dispatch events to this type of bean at all.
                if (registeredBean != IGNORE && registeredBean != bean) {
                    eventPublisher.unregister(registeredBean);
                    log.debug("{}: unregistering {} as an event listener because it is either a " +
                                    "prototype bean or an uncached plugin module. If the module should be " +
                                    "automatically registered with the EventPublisher, please define the bean " +
                                    "in the Spring context and use a bean: prefix in the atlassian-plugin.xml",
                            bundleName, name);
                    beansRegistered.put(name, IGNORE);
                }
            } else if (isEventListener(configuration, bean)) {
                eventPublisher.register(bean);
                beansRegistered.put(name, bean);
                return true;
            }
        } catch (RuntimeException e) {
            log.warn("{}: could not register {} as an event listener (reason: {})", bundleName, name,
                    e.getMessage(), log.isDebugEnabled() ? e : null);
        }
        return false;
    }

    private void unregisterEventListeners() {
        EventPublisher eventPublisher = eventPublisherTracker.getService();
        Iterator<Object> it = beansRegistered.values().iterator();
        if (log.isDebugEnabled() && !beansRegistered.isEmpty()) {
            String beanNames = beansRegistered.keySet().stream().collect(Collectors.joining(", "));
            if (eventPublisher != null) {
                log.debug("{}: unregistering [{}] as event listener", bundleName, beanNames);
            } else {
                log.debug("{}: could not unregister [{}] as event listener; the event publisher is no longer available",
                        bundleName, beanNames);
            }
        }
        while (it.hasNext()) {
            Object bean = it.next();
            it.remove();

            if (eventPublisher != null && IGNORE != bean) {
                try {
                    eventPublisher.unregister(bean);
                } catch (RuntimeException e) {
                    // can happen if the EventPublisher service gets unpublished in OSGI concurrently
                    log.debug("{}: Could not unregister event listener of type {}",
                            bundleName, bean.getClass().getName(), e);
                }
            }
        }
    }

    /**
     * Ensures that beans are registered and unregistered with the EventPublisher according to the SAL Lifecycle
     */
    private class EventListenerLifecycle implements LifecycleAware {

        @Override
        public void onStart() {
            started = true;
            maybeRegisterUnscannedBeans();
        }

        @Override
        public void onStop() {
            started = false;
            unregisterEventListeners();
        }
    }

    /**
     * Triggers event listener registration when an OSGI dependency (event publisher or listener configuration)
     * becomes available.
     */
    private class TriggeringTrackerCustomizer<T> implements ServiceTrackerCustomizer<T, T> {

        @Override
        public T addingService(ServiceReference<T> serviceReference) {
            T service = bundleContext.getService(serviceReference);
            maybeRegisterUnscannedBeans();
            return service;
        }

        @Override
        public void modifiedService(ServiceReference<T> serviceReference, T service) {
            // ignored
        }

        @Override
        public void removedService(ServiceReference<T> serviceReference, T service) {
            bundleContext.ungetService(serviceReference);
        }
    }
}
