package io.smallrye.reactive.messaging.extension;

import static io.smallrye.reactive.messaging.i18n.ProviderExceptions.ex;
import static io.smallrye.reactive.messaging.i18n.ProviderLogging.log;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Instance;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.DeploymentException;
import javax.inject.Inject;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.reactive.messaging.Incoming;
import org.eclipse.microprofile.reactive.messaging.Message;
import org.eclipse.microprofile.reactive.messaging.Outgoing;
import org.eclipse.microprofile.reactive.streams.operators.PublisherBuilder;
import org.eclipse.microprofile.reactive.streams.operators.ReactiveStreams;
import org.eclipse.microprofile.reactive.streams.operators.SubscriberBuilder;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;

import io.smallrye.mutiny.Multi;
import io.smallrye.reactive.messaging.AbstractMediator;
import io.smallrye.reactive.messaging.ChannelRegistar;
import io.smallrye.reactive.messaging.ChannelRegistry;
import io.smallrye.reactive.messaging.Invoker;
import io.smallrye.reactive.messaging.MediatorConfiguration;
import io.smallrye.reactive.messaging.MediatorFactory;
import io.smallrye.reactive.messaging.PublisherDecorator;
import io.smallrye.reactive.messaging.Shape;
import io.smallrye.reactive.messaging.WeavingException;
import io.smallrye.reactive.messaging.annotations.Incomings;
import io.smallrye.reactive.messaging.annotations.Merge;
import io.smallrye.reactive.messaging.connectors.WorkerPoolRegistry;

/**
 * Class responsible for managing mediators
 */
@ApplicationScoped
public class MediatorManager {

    private static final int DEFAULT_BUFFER_SIZE = 128;

    public static final String STRICT_MODE_PROPERTY = "smallrye-messaging-strict-binding";
    private final boolean strictMode = Boolean.parseBoolean(System.getProperty(STRICT_MODE_PROPERTY, "false"));

    private final CollectedMediatorMetadata collected = new CollectedMediatorMetadata();

    // TODO Populate this list
    private final List<Subscription> subscriptions = new CopyOnWriteArrayList<>();

    private final List<AbstractMediator> mediators = new ArrayList<>();

    @Inject
    @ConfigProperty(name = "mp.messaging.emitter.default-buffer-size", defaultValue = "128")
    int defaultBufferSize;

    @Inject
    @ConfigProperty(name = "smallrye.messaging.emitter.default-buffer-size", defaultValue = "128")
    @Deprecated // Use mp.messaging.emitter.default-buffer-size instead
    int defaultBufferSizeLegacy;

    @Inject
    @Any
    Instance<ChannelRegistar> streamRegistars;

    @Inject
    MediatorFactory mediatorFactory;

    @Inject
    ChannelRegistry channelRegistry;

    @Inject
    BeanManager beanManager;

    @Inject
    WorkerPoolRegistry workerPoolRegistry;

    @Inject
    Instance<PublisherDecorator> decorators;

    private volatile boolean initialized;

    public MediatorManager() {
        if (strictMode) {
            log.strictModeEnabled();
        }
    }

    public boolean isInitialized() {
        return initialized;
    }

    public <T> void analyze(AnnotatedType<T> annotatedType, Bean<T> bean) {
        log.scanningType(annotatedType.getJavaClass());
        Set<AnnotatedMethod<? super T>> methods = annotatedType.getMethods();

        methods.stream()
                .filter(this::hasMediatorAnnotations)
                .forEach(method -> collected.add(method.getJavaMember(), bean));
    }

    private <T> boolean hasMediatorAnnotations(AnnotatedMethod<? super T> method) {
        return method.isAnnotationPresent(Incomings.class) || method.isAnnotationPresent(Incoming.class)
                || method.isAnnotationPresent(Outgoing.class);
    }

    private boolean hasMediatorAnnotations(Method m) {
        return m.isAnnotationPresent(Incomings.class) || m.isAnnotationPresent(Incoming.class)
                || m.isAnnotationPresent(Outgoing.class);
    }

    @SuppressWarnings("unused")
    public <T> void analyze(Class<?> beanClass, Bean<T> bean) {
        Class<?> current = beanClass;
        while (current != Object.class) {
            Arrays.stream(current.getDeclaredMethods())
                    .filter(this::hasMediatorAnnotations)
                    .forEach(m -> collected.add(m, bean));

            current = current.getSuperclass();
        }
    }

    public void addAnalyzed(Collection<? extends MediatorConfiguration> mediators) {
        collected.addAll(mediators);
    }

    @PreDestroy
    void shutdown() {
        log.cancelSubscriptions();
        subscriptions.forEach(Subscription::cancel);
        subscriptions.clear();
    }

    public void initializeAndRun() {
        if (initialized) {
            throw ex.illegalStateForMediatorManagerAlreadyInitialized();
        }
        log.deploymentDoneStartProcessing();

        streamRegistars.stream().forEach(ChannelRegistar::initialize);
        Set<String> unmanagedSubscribers = channelRegistry.getOutgoingNames();
        log.initializingMediators();
        collected.mediators()
                .forEach(configuration -> {

                    AbstractMediator mediator = createMediator(configuration);

                    log.initializingMethod(mediator.getMethodAsString());

                    mediator.setDecorators(decorators);
                    mediator.setWorkerPoolRegistry(workerPoolRegistry);

                    try {
                        Object beanInstance = beanManager.getReference(configuration.getBean(), Object.class,
                                beanManager.createCreationalContext(configuration.getBean()));

                        if (configuration.getInvokerClass() != null) {
                            try {
                                Constructor<? extends Invoker> constructorUsingBeanInstance = configuration.getInvokerClass()
                                        .getConstructor(Object.class);
                                if (constructorUsingBeanInstance != null) {
                                    mediator.setInvoker(constructorUsingBeanInstance.newInstance(beanInstance));
                                } else {
                                    mediator.setInvoker(configuration.getInvokerClass().getDeclaredConstructor()
                                            .newInstance());
                                }

                            } catch (InstantiationException | IllegalAccessException e) {
                                log.unableToCreateInvoker(configuration.getInvokerClass(), e);
                                return;
                            }
                        }

                        mediator.initialize(beanInstance);
                    } catch (Throwable e) {
                        log.unableToInitializeMediator(mediator.getMethodAsString(), e);
                        return;
                    }

                    if (mediator.getConfiguration().shape() == Shape.PUBLISHER) {
                        log.registeringAsPublisher(mediator.getConfiguration().methodAsString(),
                                mediator.getConfiguration().getOutgoing());
                        channelRegistry.register(mediator.getConfiguration().getOutgoing(), mediator.getStream());
                    }
                    if (mediator.getConfiguration().shape() == Shape.SUBSCRIBER) {
                        List<String> list = mediator.getConfiguration().getIncoming();
                        log.registeringAsSubscriber(mediator.getConfiguration().methodAsString(), list);
                        for (String l : list) {
                            channelRegistry.register(l, mediator.getComputedSubscriber());
                        }
                    }
                });

        try {
            weaving(unmanagedSubscribers);
        } catch (WeavingException e) {
            throw new DeploymentException(e);
        }
    }

    @SuppressWarnings("unchecked")
    private void weaving(Set<String> unmanagedSubscribers) {
        // At that point all the publishers have been registered in the registry
        log.connectingMediators();
        List<AbstractMediator> unsatisfied = getAllNonSatisfiedMediators();
        // This list contains the names of the streams that have bean connected and
        List<LazySource> lazy = new ArrayList<>();
        while (!unsatisfied.isEmpty()) {
            int numberOfUnsatisfiedBeforeLoop = unsatisfied.size();

            unsatisfied.forEach(mediator -> {
                log.attemptToResolve(mediator.getMethodAsString());
                List<String> list = mediator.configuration().getIncoming();
                if (list.size() == 1) {
                    // Single source.
                    List<PublisherBuilder<? extends Message<?>>> sources = channelRegistry.getPublishers(list.get(0));
                    Optional<PublisherBuilder<? extends Message<?>>> maybeSource = getAggregatedSource(sources, list.get(0),
                            mediator, lazy);
                    maybeSource.ifPresent(publisher -> {
                        mediator.connectToUpstream(publisher);
                        log.connectingTo(mediator.getMethodAsString(), list, publisher);
                        if (mediator.configuration().getOutgoing() != null) {
                            channelRegistry.register(mediator.getConfiguration().getOutgoing(), mediator.getStream());
                        }
                    });
                } else {
                    List<PublisherBuilder<? extends Message<?>>> upstreams = new ArrayList<>();
                    for (String sn : list) {
                        List<PublisherBuilder<? extends Message<?>>> sources = channelRegistry.getPublishers(sn);
                        Optional<PublisherBuilder<? extends Message<?>>> maybeSource = getAggregatedSource(sources, sn,
                                mediator,
                                lazy);
                        maybeSource.ifPresent(upstreams::add);
                    }

                    if (upstreams.size() == list.size()) {
                        // We have all our upstreams
                        Multi<? extends Message<?>> merged = Multi.createBy().merging()
                                .streams(upstreams.stream().map(PublisherBuilder::buildRs).collect(Collectors.toList()));
                        mediator.connectToUpstream(ReactiveStreams.fromPublisher(merged));
                        log.connectingTo(mediator.getMethodAsString(), list);
                        if (mediator.configuration().getOutgoing() != null) {
                            channelRegistry.register(mediator.getConfiguration().getOutgoing(), mediator.getStream());
                        }
                    }
                }
            });

            unsatisfied = getAllNonSatisfiedMediators();
            int numberOfUnsatisfiedAfterLoop = unsatisfied.size();

            if (numberOfUnsatisfiedAfterLoop == numberOfUnsatisfiedBeforeLoop) {
                // Stale!
                if (strictMode) {
                    throw ex.weavingImposibleToBind(unsatisfied.stream()
                            .map(m -> m.configuration().methodAsString())
                            .collect(Collectors.toList()),
                            channelRegistry.getIncomingNames(),
                            channelRegistry.getEmitterNames());
                } else {
                    log.impossibleToBindMediators(
                            unsatisfied.stream().map(m -> m.configuration().methodAsString()).collect(Collectors.toList()),
                            channelRegistry.getIncomingNames(),
                            channelRegistry.getEmitterNames());
                }
                break;
            }
        }

        // Inject lazy sources
        lazy.forEach(l -> l.configure(channelRegistry));

        // Run
        mediators.stream()
                .filter(m -> m.configuration()
                        .shape() == Shape.SUBSCRIBER)
                .filter(AbstractMediator::isConnected)
                .forEach(AbstractMediator::run);

        // We also need to connect mediator and emitter to un-managed subscribers
        for (String name : unmanagedSubscribers) {
            List<AbstractMediator> list = lookupForMediatorsWithMatchingDownstream(name);
            EmitterImpl emitter = (EmitterImpl) channelRegistry.getEmitter(name);
            List<SubscriberBuilder<? extends Message<?>, Void>> subscribers = channelRegistry.getSubscribers(name);
            for (AbstractMediator mediator : list) {
                if (subscribers.size() == 1) {
                    log.connectingMethodToSink(mediator.getMethodAsString(), name);
                    mediator.getStream().to((SubscriberBuilder<Message<?>, Void>) subscribers.get(0)).run();
                } else if (subscribers.size() > 2) {
                    log.numberOfSubscribersConsumingStream(subscribers.size(), name);
                    subscribers.forEach(s -> {
                        log.connectingMethodToSink(mediator.getMethodAsString(), name);
                        mediator.getStream().to((SubscriberBuilder<Message<?>, Void>) s).run();
                    });
                }
            }

            if (list.isEmpty() && emitter != null) {
                if (subscribers.size() == 1) {
                    log.connectingEmitterToSink(name);
                    ReactiveStreams.fromPublisher(emitter.getPublisher()).to(subscribers.get(0)).run();
                } else if (subscribers.size() > 2) {
                    log.numberOfSubscribersConsumingStream(subscribers.size(), name);
                    subscribers.forEach(s -> {
                        log.connectingEmitterToSink(name);
                        ReactiveStreams.fromPublisher(emitter.getPublisher()).to(s).run();
                    });
                }
            }
        }

        initialized = true;
    }

    private List<AbstractMediator> lookupForMediatorsWithMatchingDownstream(String name) {
        return mediators.stream()
                .filter(m -> m.configuration()
                        .getOutgoing() != null)
                .filter(m -> m.configuration()
                        .getOutgoing()
                        .equalsIgnoreCase(name))
                .collect(Collectors.toList());
    }

    private List<AbstractMediator> getAllNonSatisfiedMediators() {
        return mediators.stream()
                .filter(mediator -> !mediator.isConnected())
                .collect(Collectors.toList());
    }

    private AbstractMediator createMediator(MediatorConfiguration configuration) {
        AbstractMediator mediator = mediatorFactory.create(configuration);
        log.mediatorCreated(configuration.methodAsString());
        mediators.add(mediator);
        return mediator;
    }

    private Optional<PublisherBuilder<? extends Message<?>>> getAggregatedSource(
            List<PublisherBuilder<? extends Message<?>>> sources,
            String sourceName,
            AbstractMediator mediator,
            List<LazySource> lazy) {
        if (sources.isEmpty()) {
            return Optional.empty();
        }

        Merge.Mode merge = mediator.getConfiguration()
                .getMerge();
        if (merge != null) {
            LazySource lazySource = new LazySource(sourceName, merge);
            lazy.add(lazySource);
            return Optional.of(ReactiveStreams.fromPublisher(lazySource));
        }

        if (sources.size() > 1) {
            throw new WeavingException(sourceName, mediator.getMethodAsString(), sources.size());
        }
        return Optional.of(sources.get(0));

    }

    public void initializeEmitters(List<EmitterConfiguration> emitters) {
        for (EmitterConfiguration config : emitters) {
            int bufferSize = getDefaultBufferSize();
            initializeEmitter(config, bufferSize);
        }
    }

    private int getDefaultBufferSize() {
        if (defaultBufferSize == DEFAULT_BUFFER_SIZE && defaultBufferSizeLegacy != DEFAULT_BUFFER_SIZE) {
            return defaultBufferSizeLegacy;
        } else {
            return defaultBufferSize;
        }
    }

    public void initializeEmitter(EmitterConfiguration emitterConfiguration, long defaultBufferSize) {
        EmitterImpl<?> emitter = new EmitterImpl<>(emitterConfiguration, defaultBufferSize);
        Publisher<? extends Message<?>> publisher = emitter.getPublisher();
        channelRegistry.register(emitterConfiguration.name, ReactiveStreams.fromPublisher(publisher));
        channelRegistry.register(emitterConfiguration.name, emitter);
    }
}