<?php
declare(strict_types=1);

namespace PpPhp\Core\Platform\Event\Streaming;

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

    private MessageBusTopicRegistry $topics;
    /** @var array<string, MessageBusEnvelope> */
    private array $messages = [];
    private MessageBusStore $store;

    private function __construct(private readonly string $basePath)
    {
        $this->topics = new MessageBusTopicRegistry();
        $this->store = new MessageBusStore($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 registerTopic(string $topic, int $priority = 0): MessageBusTopic
    {
        $record = $this->topics->register($topic, $priority);
        $this->save();

        return $record;
    }

    public function publish(string $topic, string $type, array $payload = [], array $headers = []): MessageBusEnvelope
    {
        if (!$this->topics->has($topic)) {
            $this->topics->register($topic);
        }

        $message = MessageBusMessage::create($topic, $type, $payload, $headers);
        $envelope = MessageBusEnvelope::publish($message);
        $this->messages[$message->messageId()] = $envelope;
        $this->save();

        return $envelope;
    }

    public function deliver(string $messageId): bool
    {
        if (!isset($this->messages[$messageId])) {
            return false;
        }

        $this->messages[$messageId] = $this->messages[$messageId]->markDelivered();
        $this->save();

        return true;
    }

    public function messagesForTopic(string $topic): array
    {
        return array_values(array_filter(
            $this->messages,
            static fn (MessageBusEnvelope $envelope): bool => $envelope->message()->topic() === $topic
        ));
    }

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

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

        $this->messages = [];
        foreach ($snapshot->messages() as $message) {
            if (is_array($message)) {
                $envelope = MessageBusEnvelope::fromArray($message);
                $this->messages[$envelope->message()->messageId()] = $envelope;
            }
        }
    }

    public function snapshot(): MessageBusSnapshot
    {
        $messages = array_map(static fn (MessageBusEnvelope $envelope): array => $envelope->toArray(), array_values($this->messages));

        return new MessageBusSnapshot(
            $this->topics->toArray(),
            $messages,
            $this->statistics($messages),
            date('c')
        );
    }

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

    public function clear(): void
    {
        $this->topics = new MessageBusTopicRegistry();
        $this->messages = [];
        $this->store->reset();
    }

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

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

        return [
            'topics' => $snapshot->topics(),
            'messages' => $snapshot->messages(),
            'statistics' => $snapshot->statistics(),
            'persisted' => $this->store->exists(),
            'stateFile' => $this->store->stateFile(),
        ];
    }

    private function statistics(array $messages): array
    {
        $stats = [
            'topics' => $this->topics->count(),
            'messages' => count($messages),
            'published' => 0,
            'delivered' => 0,
            'pending' => 0,
        ];

        foreach ($messages as $message) {
            $status = (string) ($message['status'] ?? 'pending');
            if (array_key_exists($status, $stats)) {
                $stats[$status]++;
            }
        }

        return $stats;
    }

    private static function detectBasePath(): string
    {
        $candidate = dirname(__DIR__, 4);
        $real = realpath($candidate);

        return $real !== false ? $real : $candidate;
    }
}
