<?php
/**
 * Event Bus Service
 *
 * Central event dispatcher for the plugin's event system.
 *
 * @package     WPUltimateSecurity\UltimateSecurity\Services
 * @since       2.0.0
 */

declare(strict_types=1);

namespace WPUltimateSecurity\UltimateSecurity\Services;

use WPUltimateSecurity\UltimateSecurity\Contracts\EventInterface;
use WPUltimateSecurity\UltimateSecurity\Contracts\EventSubscriberInterface;

/**
 * Central event dispatcher implementing pub/sub pattern.
 *
 * Provides:
 * - Priority-based listener execution
 * - One-time listener subscriptions
 * - Automatic subscriber registration
 * - Propagation stopping support
 *
 * @since 2.0.0
 */
class EventBus
{
    /**
     * Singleton instance.
     *
     * @var self|null
     */
    private static ?self $instance = null;

    /**
     * Registered event listeners.
     *
     * Array structure: [event_name => [priority => [callable, ...]]]
     *
     * @var array<string, array<int, array<int, callable>>>
     */
    private array $listeners = [];

    /**
     * One-time listeners to be removed after execution.
     *
     * @var array<string, array<int, callable>>
     */
    private array $onceListeners = [];

    /**
     * Sorted listener cache per event.
     *
     * @var array<string, array<int, callable>>
     */
    private array $sortedListeners = [];

    /**
     * Private constructor for singleton pattern.
     *
     * @since 2.0.0
     */
    private function __construct()
    {
        // Private constructor to enforce singleton.
    }

    /**
     * Prevent cloning of singleton instance.
     *
     * @since 2.0.0
     *
     * @return void
     */
    private function __clone(): void
    {
        // Prevent cloning.
    }

    /**
     * Get the singleton instance.
     *
     * @since 2.0.0
     *
     * @return self The EventBus instance.
     */
    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    /**
     * Reset the singleton instance.
     *
     * Primarily used for testing purposes.
     *
     * @since 2.0.0
     *
     * @return void
     */
    public static function resetInstance(): void
    {
        self::$instance = null;
    }

    /**
     * Dispatch an event to all registered listeners.
     *
     * Listeners are called in priority order (higher priority first).
     * Propagation stops if the event's isPropagationStopped() returns true.
     *
     * @since 2.0.0
     *
     * @param EventInterface $event The event to dispatch.
     *
     * @return EventInterface The dispatched event (may be modified by listeners).
     */
    public function dispatch(EventInterface $event): EventInterface
    {
        $eventName = $event->getName();
        $listeners = $this->getListeners($eventName);

        foreach ($listeners as $listener) {
            if ($event->isPropagationStopped()) {
                break;
            }

            $listener($event);

            // Remove one-time listeners after execution.
            $this->removeOnceListener($eventName, $listener);
        }

        return $event;
    }

    /**
     * Subscribe a listener to an event.
     *
     * @since 2.0.0
     *
     * @param string   $event    The event name to subscribe to.
     * @param callable $listener The listener callback.
     * @param int      $priority Listener priority (higher = earlier execution). Default 10.
     *
     * @return void
     */
    public function subscribe(string $event, callable $listener, int $priority = 10): void
    {
        if (!isset($this->listeners[$event])) {
            $this->listeners[$event] = [];
        }

        if (!isset($this->listeners[$event][$priority])) {
            $this->listeners[$event][$priority] = [];
        }

        $this->listeners[$event][$priority][] = $listener;

        // Invalidate sorted cache for this event.
        unset($this->sortedListeners[$event]);
    }

    /**
     * Subscribe a listener that will only be called once.
     *
     * The listener is automatically removed after the first event dispatch.
     *
     * @since 2.0.0
     *
     * @param callable $listener The listener callback.
     * @param string   $event    The event name to subscribe to.
     *
     * @return void
     */
    public function subscribeOnce(string $event, callable $listener): void
    {
        // Register as normal listener with default priority.
        $this->subscribe($event, $listener, 10);

        // Track as one-time listener.
        if (!isset($this->onceListeners[$event])) {
            $this->onceListeners[$event] = [];
        }

        $this->onceListeners[$event][] = $listener;
    }

    /**
     * Unsubscribe a listener from an event.
     *
     * @since 2.0.0
     *
     * @param string   $event    The event name.
     * @param callable $listener The listener callback to remove.
     *
     * @return void
     */
    public function unsubscribe(string $event, callable $listener): void
    {
        if (!isset($this->listeners[$event])) {
            return;
        }

        foreach ($this->listeners[$event] as $priority => $listeners) {
            foreach ($listeners as $index => $registered) {
                if ($this->listenersMatch($registered, $listener)) {
                    unset($this->listeners[$event][$priority][$index]);

                    // Clean up empty priority levels.
                    if (empty($this->listeners[$event][$priority])) {
                        unset($this->listeners[$event][$priority]);
                    }

                    // Invalidate sorted cache.
                    unset($this->sortedListeners[$event]);

                    return;
                }
            }
        }
    }

    /**
     * Get all listeners for an event.
     *
     * Returns listeners sorted by priority (higher priority first).
     *
     * @since 2.0.0
     *
     * @param string $event The event name.
     *
     * @return array<int, callable> Sorted array of listeners.
     */
    public function getListeners(string $event): array
    {
        if (isset($this->sortedListeners[$event])) {
            return $this->sortedListeners[$event];
        }

        if (!isset($this->listeners[$event])) {
            return [];
        }

        // Sort by priority (higher priority = earlier execution).
        $priorities = $this->listeners[$event];
        krsort($priorities, SORT_NUMERIC);

        $sorted = [];
        foreach ($priorities as $listeners) {
            foreach ($listeners as $listener) {
                $sorted[] = $listener;
            }
        }

        $this->sortedListeners[$event] = $sorted;

        return $sorted;
    }

    /**
     * Register an event subscriber.
     *
     * Automatically subscribes all events declared by the subscriber.
     *
     * @since 2.0.0
     *
     * @param EventSubscriberInterface $subscriber The subscriber to register.
     *
     * @return void
     */
    public function registerSubscriber(EventSubscriberInterface $subscriber): void
    {
        $events = $subscriber->getSubscribedEvents();

        foreach ($events as $eventName => $params) {
            if (is_string($params)) {
                // Simple handler: ['event.name' => 'methodName']
                $this->subscribe($eventName, [$subscriber, $params]);
            } elseif (is_array($params) && count($params) === 2) {
                // Handler with priority: ['event.name' => ['methodName', 10]]
                /** @var array{0: string, 1: int} $params */
                $this->subscribe($eventName, [$subscriber, $params[0]], $params[1]);
            }
        }
    }

    /**
     * Unregister an event subscriber.
     *
     * Removes all event subscriptions for the given subscriber.
     *
     * @since 2.0.0
     *
     * @param EventSubscriberInterface $subscriber The subscriber to unregister.
     *
     * @return void
     */
    public function unregisterSubscriber(EventSubscriberInterface $subscriber): void
    {
        $events = $subscriber->getSubscribedEvents();

        foreach ($events as $eventName => $params) {
            $method = is_string($params) ? $params : $params[0];
            $this->unsubscribe($eventName, [$subscriber, $method]);
        }
    }

    /**
     * Check if an event has any listeners.
     *
     * @since 2.0.0
     *
     * @param string $event The event name.
     *
     * @return bool True if the event has listeners.
     */
    public function hasListeners(string $event): bool
    {
        return !empty($this->getListeners($event));
    }

    /**
     * Clear all listeners for an event or all events.
     *
     * @since 2.0.0
     *
     * @param string|null $event Optional event name. If null, clears all listeners.
     *
     * @return void
     */
    public function clearListeners(?string $event = null): void
    {
        if ($event === null) {
            $this->listeners = [];
            $this->onceListeners = [];
            $this->sortedListeners = [];
        } else {
            unset(
                $this->listeners[$event],
                $this->onceListeners[$event],
                $this->sortedListeners[$event]
            );
        }
    }

    /**
     * Remove a one-time listener after execution.
     *
     * @since 2.0.0
     *
     * @param string   $event    The event name.
     * @param callable $listener The listener to check and remove.
     *
     * @return void
     */
    private function removeOnceListener(string $event, callable $listener): void
    {
        if (!isset($this->onceListeners[$event])) {
            return;
        }

        foreach ($this->onceListeners[$event] as $index => $onceListener) {
            if ($this->listenersMatch($onceListener, $listener)) {
                unset($this->onceListeners[$event][$index]);
                $this->unsubscribe($event, $listener);
                return;
            }
        }
    }

    /**
     * Check if two listeners are the same.
     *
     * Handles comparison of closures, arrays, and string callbacks.
     *
     * @since 2.0.0
     *
     * @param callable $a First listener.
     * @param callable $b Second listener.
     *
     * @return bool True if listeners match.
     */
    private function listenersMatch(callable $a, callable $b): bool
    {
        // Direct equality check.
        if ($a === $b) {
            return true;
        }

        // Array callback comparison (class/object + method).
        if (is_array($a) && is_array($b)) {
            // Compare objects/classes.
            $classMatch = false;
            if (is_object($a[0]) && is_object($b[0])) {
                $classMatch = $a[0] === $b[0];
            } elseif (is_string($a[0]) && is_string($b[0])) {
                $classMatch = $a[0] === $b[0];
            }

            return $classMatch && $a[1] === $b[1];
        }

        return false;
    }
}
