<?php
declare(strict_types=1);

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

use PpPhp\Core\Platform\Event\Sourcing\Aggregate\AggregateRepositoryRuntime;

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

    private CommandHandlerRegistry $registry;
    /** @var CommandMessage[] */
    private array $commands = [];
    /** @var CommandResult[] */
    private array $results = [];
    private CommandStore $store;

    private function __construct(private readonly string $basePath)
    {
        $this->registry = new CommandHandlerRegistry();
        $this->store = new CommandStore($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(CommandHandlerDefinition $handler): CommandHandlerDefinition
    {
        $this->registry->register($handler);
        $this->save();

        return $handler;
    }

    public function dispatch(CommandMessage $command): CommandResult
    {
        $started = microtime(true);
        $this->commands[] = $command;

        $handler = $this->registry->findFor($command);
        if ($handler === null) {
            $result = CommandResult::failed($command, 'no command handler registered', (microtime(true) - $started) * 1000);
            $this->results[] = $result;
            $this->save();

            return $result;
        }

        $state = AggregateRepositoryRuntime::instance($this->basePath)->save(
            $command->aggregateId(),
            $command->aggregateType(),
            $handler->eventType(),
            $command->payload(),
            $command->metadata() + ['commandId' => $command->commandId(), 'handlerId' => $handler->handlerId()]
        );

        $events = $state->events();
        $lastEvent = end($events);
        $eventId = is_array($lastEvent) ? (string) ($lastEvent['eventId'] ?? '') : '';

        $result = CommandResult::handled($command, $eventId, $handler->eventType(), (microtime(true) - $started) * 1000);
        $this->results[] = $result;
        $this->save();

        return $result;
    }

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

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

        $this->commands = [];
        foreach ($snapshot->commands() as $command) {
            if (is_array($command)) {
                $this->commands[] = CommandMessage::fromArray($command);
            }
        }

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

    public function snapshot(): CommandRuntimeSnapshot
    {
        $handlers = $this->registry->toArray();
        $commands = array_map(static fn (CommandMessage $command): array => $command->toArray(), $this->commands);
        $results = array_map(static fn (CommandResult $result): array => $result->toArray(), $this->results);

        return new CommandRuntimeSnapshot(
            $handlers,
            $commands,
            $results,
            CommandStatistics::from($handlers, $commands, $results),
            date('c')
        );
    }

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

    public function clear(): void
    {
        $this->registry = new CommandHandlerRegistry();
        $this->commands = [];
        $this->results = [];
        $this->store->reset();
    }

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

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

        return [
            'handlers' => $snapshot->handlers(),
            'commands' => $snapshot->commands(),
            'results' => $snapshot->results(),
            '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;
    }
}
