package com.livingworld.events;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.livingworld.debug.DiagnosticCategory;
import com.livingworld.debug.LivingWorldLogger;

/**
 * Central event bus for publishing and subscribing to Living World simulation events.
 *
 * <p>This class routes events by type to registered listeners, provides metrics
 * on published count and listener counts per event type, and includes a simple
 * recursion guard to prevent event storms within the same tick.</p>
 */
public final class LivingWorldEventBus {

    private static final int MAX_EVENTS_PER_DISPATCH = 10_000;

    private final Map<String, List<LivingWorldEventListener>> listeners;
    private final Deque<LivingWorldEvent> pendingEvents;
    private int publishedCount;
    private boolean dispatching;

    /**
     * Creates a new empty event bus.
     */
    public LivingWorldEventBus() {
        this.listeners = new java.util.LinkedHashMap<>();
        this.pendingEvents = new ArrayDeque<>();
    }

    // ------------------------------------------------------------------
    // Registration
    // ------------------------------------------------------------------

    /**
     * Registers a listener for events of the given type.
     *
     * <p>If listeners are already registered for the event type, this adds the
     * new listener to the end of the list. Listeners are invoked in registration
     * order.</p>
     *
     * @param eventType the event type to listen for (must not be blank)
     * @param listener  the listener to register (must not be null)
     * @throws IllegalArgumentException if eventType is null or blank, or listener is null
     */
    public void register(String eventType, LivingWorldEventListener listener) {
        if (eventType == null || eventType.isBlank()) {
            throw new IllegalArgumentException("eventType must not be null or blank");
        }
        if (listener == null) {
            throw new IllegalArgumentException("listener must not be null");
        }

        listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener);
    }

    // ------------------------------------------------------------------
    // Publishing
    // ------------------------------------------------------------------

    /**
     * Publishes an event to all registered listeners for its type.
     *
     * <p>The event is dispatched in a loop to each listener. If the number of
     * listeners grows very large, consider using {@code CopyOnWriteArrayList}
     * instead of {@code ArrayList} for the listeners map (currently TODO).</p>
     *
     * @param event the event to publish (must not be null)
     * @throws IllegalArgumentException if event is null
     */
    public void publish(LivingWorldEvent event) {
        if (event == null) {
            throw new IllegalArgumentException("event must not be null");
        }

        pendingEvents.addLast(event);
        if (dispatching) {
            return;
        }

        dispatching = true;
        int dispatched = 0;
        try {
            while (!pendingEvents.isEmpty()) {
                if (dispatched >= MAX_EVENTS_PER_DISPATCH) {
                    int dropped = pendingEvents.size();
                    pendingEvents.clear();
                    LivingWorldLogger.warn(DiagnosticCategory.EVENTS,
                            "Stopped recursive event dispatch after " + MAX_EVENTS_PER_DISPATCH
                                    + " events; dropped " + dropped + " queued events");
                    break;
                }

                LivingWorldEvent nextEvent = pendingEvents.removeFirst();
                publishedCount++;
                dispatched++;

                List<LivingWorldEventListener> listenersForType = listeners.get(nextEvent.eventType());
                if (listenersForType == null) {
                    continue;
                }

                for (LivingWorldEventListener listener : List.copyOf(listenersForType)) {
                    try {
                        listener.onEvent(nextEvent);
                    } catch (Exception e) {
                        LivingWorldLogger.error(DiagnosticCategory.EVENTS,
                                "Event listener failed for type " + nextEvent.eventType(), e);
                    }
                }
            }
        } finally {
            dispatching = false;
        }
    }

    // ------------------------------------------------------------------
    // Metrics / Inspection
    // ------------------------------------------------------------------

    /**
     * Returns the total number of events published so far.
     *
     * @return the published event count (never negative)
     */
    public int getPublishedEventCount() {
        return publishedCount;
    }

    /**
     * Returns the number of listeners registered for the given event type.
     *
     * @param eventType the event type to query (must not be null or blank)
     * @return the listener count, or 0 if no listeners are registered for this type
     * @throws IllegalArgumentException if eventType is null or blank
     */
    public int getListenerCount(String eventType) {
        if (eventType == null || eventType.isBlank()) {
            throw new IllegalArgumentException("eventType must not be null or blank");
        }

        List<LivingWorldEventListener> typeListeners = this.listeners.get(eventType);
        return typeListeners != null ? typeListeners.size() : 0;
    }

    /**
     * Returns a snapshot of all registered event types and their listener counts.
     *
     * @return an unmodifiable map from eventType to listener count (never null)
     */
    public Map<String, Integer> getAllListenerCounts() {
        return listeners.entrySet().stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        e -> e.getValue().size()
                ));
    }

    /**
     * Retained for source compatibility. Dispatch state is now cleared
     * automatically after each publication batch.
     */
    public void clearSuppressionForCurrentTick() {
        // No-op.
    }
}
