<?php
declare(strict_types=1);

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

final class EventVersioningRuntime
{
    private static ?self $instance = null;
    private EventVersionRegistry $versions;
    private EventUpcasterRegistry $upcasters;
    private array $results = [];
    private EventVersioningStore $store;

    private function __construct(private readonly string $basePath)
    {
        $this->versions = new EventVersionRegistry();
        $this->upcasters = new EventUpcasterRegistry();
        $this->store = new EventVersioningStore($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 registerVersion(EventVersion $version): EventVersion { $this->versions->register($version); $this->save(); return $version; }
    public function registerUpcaster(EventUpcaster $upcaster): EventUpcaster { $this->upcasters->register($upcaster); $this->save(); return $upcaster; }

    public function upcast(array $event): EventVersioningResult
    {
        $eventType = (string) ($event['eventType'] ?? '');
        $from = (int) (($event['metadata']['eventVersion'] ?? $event['version'] ?? 1));
        $target = $this->versions->currentVersion($eventType);
        $current = $from;
        $changed = false;
        $upcasted = $event;

        while ($current < $target) {
            $upcaster = $this->upcasters->find($eventType, $current);
            if ($upcaster === null) { break; }
            $upcasted = $upcaster->upcast($upcasted);
            $current = $upcaster->toVersion();
            $changed = true;
        }

        $result = EventVersioningResult::create($upcasted, $from, $current, $changed);
        $this->results[] = $result;
        $this->save();
        return $result;
    }

    public function hydrate(): void
    {
        $snapshot = $this->store->load();
        if ($snapshot->versions() !== []) { $this->versions = EventVersionRegistry::fromArray($snapshot->versions()); }
        if ($snapshot->upcasters() !== []) { $this->upcasters = EventUpcasterRegistry::fromArray($snapshot->upcasters()); }
        $this->results = [];
        foreach ($snapshot->results() as $result) {
            if (is_array($result)) { $this->results[] = EventVersioningResult::fromArray($result); }
        }
    }

    public function snapshot(): EventVersioningSnapshot
    {
        $versions = $this->versions->toArray();
        $upcasters = $this->upcasters->toArray();
        $results = array_map(static fn (EventVersioningResult $result): array => $result->toArray(), $this->results);
        return new EventVersioningSnapshot($versions, $upcasters, $results, EventVersioningStatistics::from($versions, $upcasters, $results), date('c'));
    }

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

    public function clear(): void
    {
        $this->versions = new EventVersionRegistry();
        $this->upcasters = new EventUpcasterRegistry();
        $this->results = [];
        $this->store->reset();
    }

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

    public function summary(): array
    {
        $snapshot = $this->snapshot();
        return ['versions' => $snapshot->versions(), 'upcasters' => $snapshot->upcasters(), '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;
    }
}
