package de.accso.flexinale.common.infrastructure.eventbus;

import de.accso.flexinale.common.api.eventbus.EventBus;
import de.accso.flexinale.common.api.eventbus.EventContextHolder;
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart;
import de.accso.flexinale.common.api.event.Event;
import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException;
import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@SuppressWarnings("unused")
public final class KafkaAsyncEventBus<E extends Event> implements EventBus<E> {
    private static final Logger LOGGER = LoggerFactory.getLogger(KafkaAsyncEventBus.class);

    private static final EventContextHolder eventContextHolder = new ThreadLocalEventContextHolder();

    private KafkaProducerAdapter<E> kafkaProducerAdapter;
    private KafkaConsumerAdapter<E> kafkaConsumerAdapter;

    private final KafkaAvailabilityChecker kafkaAvailabilityChecker;

    private final Class<E> eventType; // TODO might want to use this in order to simplify the interface and get rid of the method's first parameter

    KafkaAsyncEventBus(final Class<E> eventType, final KafkaAvailabilityChecker kafkaAvailabilityChecker) {
        this.eventType = eventType;
        this.kafkaAvailabilityChecker = kafkaAvailabilityChecker;
    }

    @Override
    public void subscribe(final Class<E> eventType, final EventSubscriber<E> subscriber,
                          final EventSubscriptionAtStart eventSubscriptionAtStart) {
        LOGGER.info("KafkaAsyncEventBus<%s>: subscriber %s registered".formatted(eventType, subscriber));

        if (kafkaConsumerAdapter == null) {
            kafkaConsumerAdapter = createKafkaConsumerAdapter(eventType, subscriber, eventSubscriptionAtStart);
            startKafkaConsumerAdapterInSeparateThread(kafkaConsumerAdapter);
        }
        kafkaConsumerAdapter.addSubscriber(subscriber);
    }

    @Override
    public void subscribe(EventSubscriber<Event> genericSubscriber,
                          final EventSubscriptionAtStart eventSubscriptionAtStart) {
        LOGGER.info("KafkaAsyncEventBus<%s>: generic subscriber %s registered".formatted(eventType, genericSubscriber));

        if (kafkaConsumerAdapter == null) {
            kafkaConsumerAdapter = createKafkaConsumerAdapter(eventType, genericSubscriber, eventSubscriptionAtStart);
            startKafkaConsumerAdapterInSeparateThread(kafkaConsumerAdapter);
        }
        kafkaConsumerAdapter.addGenericSubscriber(genericSubscriber);
    }

    @Override
    public void unsubscribe(final Class<E> eventType, final EventSubscriber<E> subscriber) {
        LOGGER.info("KafkaAsyncEventBus<%s>: subscriber %s unregistered".formatted(eventType, subscriber));

        if (kafkaConsumerAdapter == null) {
            throw new DeveloperMistakeException("unsubscribe a subscriber from non-existing kafkaConsumerAdapter");
        }
        kafkaConsumerAdapter.removeSubscriber(subscriber);
    }

    @Override
    public void unsubscribe(EventSubscriber<Event> genericSubscriber) {
        LOGGER.info("KafkaAsyncEventBus<%s>: generic subscriber %s unregistered".formatted(eventType, genericSubscriber));

        if (kafkaConsumerAdapter == null) {
            throw new DeveloperMistakeException("unsubscribe a generic subscriber from non-existing kafkaConsumerAdapter");
        }
        kafkaConsumerAdapter.removeGenericSubscriber(genericSubscriber);
    }

    @Override
    public void unsubscribeAll() {
        LOGGER.info("KafkaAsyncEventBus<%s>: all subscribers unregistered".formatted(eventType));

        if (kafkaConsumerAdapter == null) {
            throw new DeveloperMistakeException("unsubscribe all subscribers from non-existing kafkaConsumerAdapter");
        }
        kafkaConsumerAdapter.removeAllSubscribers();
        kafkaConsumerAdapter.removeAllGenericSubscribers();
        kafkaConsumerAdapter.shutdown(); // shutdown adapter with last subscriber
        kafkaConsumerAdapter = null;

        // might want to add a common shutdown method?
    }

    @Override
    public void publish(final Class<E> eventType, final E event) {
        LOGGER.info("KafkaAsyncEventBus<%s>: publish new event %s".formatted(eventType, event));

        if (kafkaProducerAdapter == null) {
            kafkaProducerAdapter = createKafkaProducerAdapter(eventType);
        }

        kafkaProducerAdapter.publish(eventType, event);
    }

    @Override
    public EventContextHolder getEventContextHolder() {
        return eventContextHolder;
    }

    // --------------------------------------------------------------------------------------------------------------

    private KafkaConsumerAdapter<E> createKafkaConsumerAdapter(final Class<E> eventType, final EventSubscriber<?> subscriber,
                                                               final EventSubscriptionAtStart eventSubscriptionAtStart) {
        // check this once for every new KafkaConsumerAdapter
        checkKafkaAvailability();
        return new KafkaConsumerAdapter<>(eventType, subscriber, eventContextHolder, eventSubscriptionAtStart);
    }

    private void startKafkaConsumerAdapterInSeparateThread(final KafkaConsumerAdapter<E> kafkaConsumerAdapter) {
        String threadName = "thread4%s<%s>".formatted(kafkaConsumerAdapter.getClass().getName(), eventType);
        new Thread(this.kafkaConsumerAdapter, threadName).start();
    }

    // --------------------------------------------------------------------------------------------------------------

    private KafkaProducerAdapter<E> createKafkaProducerAdapter(final Class<E> eventType) {
        // check this once for every new KafkaProducerAdapter
        checkKafkaAvailability();
        return new KafkaProducerAdapter<>(eventType);
    }

    // --------------------------------------------------------------------------------------------------------------

    private void checkKafkaAvailability() {
        try {
            if (! kafkaAvailabilityChecker.isKafkaAvailable()) {
                throw new FlexinaleIllegalStateException("Kafka is not available.");
            }
        }
        catch (Exception ex) {
            throw new FlexinaleIllegalStateException("Kafka is not available.", ex);
        }
    }
}
