<?php
declare(strict_types=1);

namespace PpPhp\Core\Platform\Event\Sourcing\Domain;

use PpPhp\Core\Platform\Event\Sourcing\EventStore\EventStoreRuntime;

final class DomainEventDispatcherRuntime
{
    private static ?self $instance = null;

    private DomainEventListenerRegistry $registry;
    /** @var DomainEventEnvelope[] */
    private array $envelopes = [];
    /** @var DomainEventDispatchResult[] */
    private array $results = [];
    private DomainEventCheckpoint $checkpoint;
    private DomainEventStore $store;

    private function __construct(private readonly string $basePath)
    {
        $this->registry = new DomainEventListenerRegistry();
        $this->checkpoint = new DomainEventCheckpoint();
        $this->store = new DomainEventStore($basePath);
        $this->hydrate();
    }

    public static function instance(?string $basePath = null): self
    {
        if (self::$instance === null) {
            $basePath ??= self::detectBasePath();
            self::$instance = new self($basePath);
        }

        return self::$instance;
    }

    public static function resetInstance(?string $basePath = null): self
    {
        $basePath ??= self::detectBasePath();
        self::$instance = new self($basePath);

        return self::$instance;
    }

    public function register(DomainEventListener $listener): DomainEventListener
    {
        $this->registry->register($listener);
        $this->save();

        return $listener;
    }

    public function dispatchEnvelope(DomainEventEnvelope $event): array
    {
        $listeners = $this->registry->matching($event);
        $results = [];

        if ($listeners === []) {
            $result = DomainEventDispatchResult::skipped($event, 'no listener matched');
            $this->results[] = $result;
            $results[] = $result;
            return $results;
        }

        foreach ($listeners as $listener) {
            $started = microtime(true);
            $result = DomainEventDispatchResult::dispatched($event, $listener, (microtime(true) - $started) * 1000);
            $this->results[] = $result;
            $results[] = $result;
        }

        return $results;
    }

    public function dispatchNew(): int
    {
        $processed = 0;

        foreach (EventStoreRuntime::instance($this->basePath)->summary()['events'] as $storedEvent) {
            $sequence = (int) ($storedEvent['sequence'] ?? 0);
            if ($sequence <= $this->checkpoint->lastSequence()) {
                continue;
            }

            $envelope = DomainEventEnvelope::fromStoredEvent($storedEvent);
            $this->envelopes[] = $envelope;
            $this->dispatchEnvelope($envelope);
            $this->checkpoint = $this->checkpoint->advance($sequence);
            $processed++;
        }

        $this->save();

        return $processed;
    }

    public function replay(): int
    {
        $this->envelopes = [];
        $this->results = [];
        $this->checkpoint = $this->checkpoint->reset();

        return $this->dispatchNew();
    }

    public function hydrate(): void
    {
        $snapshot = $this->store->load();

        if ($snapshot->listeners() !== []) {
            $this->registry = DomainEventListenerRegistry::fromArray($snapshot->listeners());
        }

        $this->envelopes = [];
        foreach ($snapshot->envelopes() as $envelope) {
            if (is_array($envelope)) {
                $this->envelopes[] = DomainEventEnvelope::fromArray($envelope);
            }
        }

        $this->results = [];
        foreach ($snapshot->results() as $result) {
            if (is_array($result)) {
                $this->results[] = DomainEventDispatchResult::fromArray($result);
            }
        }

        if ($snapshot->checkpoint() !== []) {
            $this->checkpoint = DomainEventCheckpoint::fromArray($snapshot->checkpoint());
        }
    }

    public function snapshot(): DomainEventRuntimeSnapshot
    {
        $listeners = $this->registry->toArray();
        $envelopes = array_map(static fn (DomainEventEnvelope $event): array => $event->toArray(), $this->envelopes);
        $results = array_map(static fn (DomainEventDispatchResult $result): array => $result->toArray(), $this->results);
        $checkpoint = $this->checkpoint->toArray();

        return new DomainEventRuntimeSnapshot(
            $listeners,
            $envelopes,
            $results,
            $checkpoint,
            DomainEventStatistics::from($listeners, $envelopes, $results, $checkpoint),
            date('c')
        );
    }

    public function save(): DomainEventRuntimeSnapshot
    {
        return $this->store->save($this->snapshot());
    }

    public function clear(): void
    {
        $this->registry = new DomainEventListenerRegistry();
        $this->envelopes = [];
        $this->results = [];
        $this->checkpoint = new DomainEventCheckpoint();
        $this->store->reset();
    }

    public function store(): DomainEventStore { return $this->store; }

    public function summary(): array
    {
        $snapshot = $this->snapshot();

        return [
            'listeners' => $snapshot->listeners(),
            'envelopes' => $snapshot->envelopes(),
            'results' => $snapshot->results(),
            'checkpoint' => $snapshot->checkpoint(),
            'statistics' => $snapshot->statistics(),
            'persisted' => $this->store->exists(),
            'stateFile' => $this->store->stateFile(),
        ];
    }

    private static function detectBasePath(): string
    {
        $candidate = dirname(__DIR__, 5);
        $real = realpath($candidate);
        return $real !== false ? $real : $candidate;
    }
}
