<?php
declare(strict_types=1);

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

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

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

    private SnapshotCollection $snapshots;
    private SnapshotStore $store;

    private function __construct(
        private readonly string $basePath,
        private readonly SnapshotPolicy $policy = new SnapshotPolicy()
    ) {
        $this->snapshots = new SnapshotCollection();
        $this->store = new SnapshotStore($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, ?SnapshotPolicy $policy = null): self
    {
        $basePath ??= self::detectBasePath();
        self::$instance = new self($basePath, $policy ?? new SnapshotPolicy());

        return self::$instance;
    }

    public function create(AggregateState $state, array $metadata = []): AggregateSnapshot
    {
        $snapshot = AggregateSnapshot::create(
            $state->aggregateId(),
            $state->aggregateType(),
            $state->version(),
            $state->state(),
            $metadata
        );

        $this->snapshots->put($snapshot);
        $this->save();

        return $snapshot;
    }

    public function maybeCreate(AggregateState $state, array $metadata = []): ?AggregateSnapshot
    {
        if (!$this->policy->shouldSnapshot($state->version())) {
            return null;
        }

        return $this->create($state, $metadata);
    }

    public function latest(string $aggregateId): ?AggregateSnapshot
    {
        return $this->snapshots->latest($aggregateId);
    }

    public function rehydrateFromLatest(string $aggregateId, string $aggregateType = ''): AggregateState
    {
        $latest = $this->latest($aggregateId);

        if ($latest === null) {
            return AggregateRepositoryRuntime::instance($this->basePath)->load($aggregateId, $aggregateType);
        }

        $state = new AggregateState(
            $latest->aggregateId(),
            $latest->aggregateType(),
            $latest->version(),
            $latest->state(),
            [],
            date('c')
        );

        $history = AggregateRepositoryRuntime::instance($this->basePath)->history($aggregateId);

        foreach ($history as $event) {
            if ((int) ($event['aggregateVersion'] ?? 0) > $latest->version()) {
                $state = $state->apply($event);
            }
        }

        return $state;
    }

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

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

    public function snapshot(): SnapshotRuntimeSnapshot
    {
        $snapshots = $this->snapshots->toArray();

        return new SnapshotRuntimeSnapshot(
            $snapshots,
            SnapshotStatistics::from($snapshots),
            $this->policy->toArray(),
            date('c')
        );
    }

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

    public function clear(): void
    {
        $this->snapshots = new SnapshotCollection();
        $this->store->reset();
    }

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

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

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