<?php
declare(strict_types=1);

namespace PpPhp\Core\Platform\Event\Dispatcher;

use PpPhp\Core\Platform\Event\Dispatcher\Contracts\DispatcherInterface;
use PpPhp\Core\Platform\Event\Dispatcher\Execution\DispatcherPipeline;
use PpPhp\Core\Platform\Event\Queue\DeferredEvent;
use PpPhp\Core\Platform\Event\Queue\DeferredQueueManager;
use PpPhp\Core\Platform\Event\Scheduler\EventScheduler;

final class DelayedDispatcher implements DispatcherInterface
{
    private DispatcherStatistics $statistics;

    public function __construct(
        private readonly DeferredQueueManager $queue = new DeferredQueueManager(),
        private readonly EventScheduler $scheduler = new EventScheduler(),
        private readonly DispatcherPipeline $pipeline = new DispatcherPipeline()
    ) {
        $this->statistics = new DispatcherStatistics();
    }

    public function queue(): DeferredQueueManager { return $this->queue; }
    public function scheduler(): EventScheduler { return $this->scheduler; }
    public function pipeline(): DispatcherPipeline { return $this->pipeline; }

    public function dispatch(DeferredEvent $event, ?callable $handler = null): DispatcherResult
    {
        $start = microtime(true);
        $result = $this->pipeline->process($event, $handler);
        $this->statistics = $this->statistics->record($result, (microtime(true) - $start) * 1000);
        return $result;
    }

    public function dispatchQueued(?callable $handler = null): int
    {
        $count = 0;

        while (($event = $this->queue->driver()->dequeue()) !== null) {
            $this->dispatch($event, $handler);
            $count++;
        }

        return $count;
    }

    public function dispatchScheduled(?callable $handler = null): int
    {
        /*
         * Synchronization rule:
         * Scheduled events are first moved into this dispatcher's DeferredQueue,
         * then the normal queued dispatch path processes them. This guarantees
         * that scheduler, queue, worker, retry and statistics all converge on
         * dispatch().
         */
        foreach ($this->scheduler->due() as $scheduled) {
            $this->queue->driver()->enqueue($scheduled->toDeferredEvent());
            $this->scheduler->cancel($scheduled->id());
        }

        return $this->dispatchQueued($handler);
    }

    public function run(?callable $handler = null): int
    {
        /*
         * First dispatch due scheduled work, then any remaining queued work.
         * Both paths route through dispatchQueued() -> dispatch().
         */
        $scheduled = $this->dispatchScheduled($handler);
        $queued = $this->dispatchQueued($handler);

        return $scheduled + $queued;
    }

    public function statistics(): DispatcherStatistics
    {
        return $this->statistics->withQueued($this->queue->statistics()->size());
    }

    public function snapshot(): DispatcherSnapshot
    {
        return new DispatcherSnapshot(
            $this->queue->snapshot()->toArray(),
            $this->scheduler->snapshot()->toArray(),
            $this->pipeline->worker()->retry()->summary(),
            $this->statistics()->toArray()
        );
    }

    public function clear(): void
    {
        $this->queue->clear();
        $this->scheduler->clear();
        $this->statistics = new DispatcherStatistics();
    }
}
