package de.accso.flexinale.common.infrastructure.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 org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.errors.WakeupException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.listener.CommonErrorHandler;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static de.accso.flexinale.common.infrastructure.eventbus.KafkaConfiguration.CONSUMER_GROUP_ID_CONFIG;

final class KafkaConsumerAdapter<E extends Event> implements Runnable {
    private static final Logger LOGGER = LoggerFactory.getLogger(KafkaConsumerAdapter.class);

    private final KafkaConsumer<String, String> internal_KafkaConsumer;
    private final static Lock lock4KafkaConsumer = new ReentrantLock();  // the internal_KafkaConsumer is not thread-safe

    private final String kafkaTopicName;

    private final AtomicBoolean consumerIsShutdownAndNoLongerConsumingFromKafka = new AtomicBoolean(false);
    private final CommonErrorHandler errorHandler = new KafkaConsumerErrorHandler();

    private final Class<E> eventType;
    private final EventContextHolder eventContextHolder;

    private final ConcurrentLinkedQueue<EventSubscriber<E>>            subscribers = new ConcurrentLinkedQueue<>();
    private final ConcurrentLinkedQueue<EventSubscriber<Event>> genericSubscribers = new ConcurrentLinkedQueue<>();

    private final EventSubscriberReceiveDispatcher<E>            eventSubscriberReceiveDispatcher = new EventSubscriberReceiveDispatcher<>();
    private final EventSubscriberReceiveDispatcher<Event> eventGenericSubscriberReceiveDispatcher = new EventSubscriberReceiveDispatcher<>();

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

    KafkaConsumerAdapter(final Class<E> eventType, final EventSubscriber<?> subscriber, EventContextHolder eventContextHolder,
                         final EventSubscriptionAtStart eventSubscriptionAtStart) {
        this.eventType = eventType;
        this.eventContextHolder = eventContextHolder; //TODO This is probably not working correctly.
                                                      //     As each KafkaConsumerAdapter is running in its own thread, they cannot share the same ThreadLocal.
                                                      //     The eventContextHolder must not be given from the bus but instantiated in each KafkaConsumerAdapter separately.

        this.kafkaTopicName = KafkaTopicHelper.createTopicNameFromEventClazz(eventType);

        final Map<String, Object> kafkaConfig = KafkaConfiguration.consumerConfig(eventType, subscriber, eventSubscriptionAtStart);
        final String kafkaConsumerGroupId = (String) kafkaConfig.get(CONSUMER_GROUP_ID_CONFIG);

        LOGGER.info("create new subscription from %s (%s), for Kafka topic %s, using Kafka consumer group id %s".
                    formatted(subscriber.getName(),
                              subscriber.getGroupName(),
                              kafkaTopicName,
                              kafkaConsumerGroupId));

        internal_KafkaConsumer = new KafkaConsumer<>(kafkaConfig);

        addShutDownHook();
    }

    @Override
    public void run() {
        // keep running forever or until shutdown() is called from another thread.
        try {
            internal_KafkaConsumer.subscribe(List.of(kafkaTopicName));
            Duration kafkaTimeoutInMs = Duration.ofMillis( KafkaConfiguration.kafkaTimeoutInMs );

            // does not necessarily read messages "from beginning", i.e. already existing records which had
            // been published before the consumer was connected to the topic!
            while (!consumerIsShutdownAndNoLongerConsumingFromKafka.get()) {
                ConsumerRecords<String, String> kafkaRecords = internal_KafkaConsumer.poll(kafkaTimeoutInMs);
                processAllKafkaRecords(kafkaTopicName, kafkaRecords);
            }
        }
        catch (WakeupException ex) {
            if (!consumerIsShutdownAndNoLongerConsumingFromKafka.get()) {
                throw ex; // ignore exception if closing
            }
        }
    }

    private void processAllKafkaRecords(final String kafkaTopicName, final ConsumerRecords<String, String> records) {
        LOGGER.debug("consuming from %s now %d records ...".formatted(kafkaTopicName, records.count()));
        records.forEach(record -> {
            try {
                LOGGER.debug("consuming from %s: %s".formatted(kafkaTopicName, record));
                E event = EventSerializationHelper.deserializeJsonString2Event(record.value(), eventType);
                processEventAndCallReceiveOnAllSubscribers(eventType, event);
            }
            catch (Exception ex) {
                this.errorHandler.handleOne(ex, record,
                        /* not used */ null,
                        /* not used */ null);
            }
        });
        LOGGER.debug("consuming from %s now %d records ... done".formatted(kafkaTopicName, records.count()));
    }

    private void processEventAndCallReceiveOnAllSubscribers(final Class<E> eventType, final E event) {
        eventContextHolder.set(event.eventContext());

        try {
            subscribers.forEach(subscriber ->
                    eventSubscriberReceiveDispatcher.callSubscriberReceiveMethod(eventType, event, subscriber));
            genericSubscribers.forEach(subscriber ->
                    eventGenericSubscriberReceiveDispatcher.callSubscriberReceiveMethod(Event.class, event, subscriber));
        }
        finally {
            eventContextHolder.remove();
        }
    }

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

    void addSubscriber(final EventSubscriber<E> subscriber) {
        subscribers.add(subscriber);
    }
    void addGenericSubscriber(final EventSubscriber<Event> genericSubscriber) {
        genericSubscribers.add(genericSubscriber);
    }

    void removeSubscriber(final EventSubscriber<E> subscriber) {
        subscribers.remove(subscriber);
    }
    void removeGenericSubscriber(final EventSubscriber<Event> genericSubscriber) {
        genericSubscribers.remove(genericSubscriber);
    }

    void removeAllSubscribers() {
        subscribers.forEach(subscribers::remove);
    }
    void removeAllGenericSubscribers() {
        genericSubscribers.forEach(genericSubscribers::remove);
    }

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

    private void addShutDownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
    }

    // note that currently there is no way of reactiving the KafkaConsumerAdapter once it has been shutdown
    void shutdown() {
        try {

/**
 * the lock does not seem to help, one still sees (with Java 21) these kind of errors:
 *
 * 20240327 14:42:20.284 [Thread-5] ERROR o.a.k.clients.consumer.KafkaConsumer - caught exception during shutdown
 * java.util.ConcurrentModificationException: KafkaConsumer is not safe for multi-threaded access. currentThread(name: Thread-5, id: 43) otherThread(id: 44)
 * 	at org.apache.kafka.clients.consumer.KafkaConsumer.acquire(KafkaConsumer.java:2484)
 * 	at org.apache.kafka.clients.consumer.KafkaConsumer.close(KafkaConsumer.java:2343)
 * 	at org.apache.kafka.clients.consumer.KafkaConsumer.close(KafkaConsumer.java:2321)
 * 	at de.accso.flexinale.common.infrastructure.eventbus.KafkaConsumerAdapter.shutdown(KafkaConsumerAdapter.java:147)
 * 	at java.base/java.lang.Thread.run(Thread.java:1583)
 */

            boolean isLockAcquired = lock4KafkaConsumer.tryLock(1, TimeUnit.SECONDS);
            if (isLockAcquired) {
                try {
                    consumerIsShutdownAndNoLongerConsumingFromKafka.set(true);
                    internal_KafkaConsumer.close();
                } finally {
                    lock4KafkaConsumer.unlock();
                }
            }
        }
        catch (Exception ex) {
            LOGGER.error("caught exception during shutdown", ex);
        }
    }

}
