<?php
declare(strict_types=1);

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

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

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

    /** @var EventCompactionResult[] */
    private array $results = [];
    /** @var EventCompactionArchive[] */
    private array $archives = [];
    private EventCompactionStore $store;
    private EventStreamOptimizer $optimizer;

    private function __construct(
        private readonly string $basePath,
        private readonly EventCompactionPolicy $policy = new EventCompactionPolicy()
    ) {
        $this->store = new EventCompactionStore($basePath);
        $this->optimizer = new EventStreamOptimizer();
        $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, ?EventCompactionPolicy $policy = null): self
    {
        $basePath ??= self::detectBasePath();
        self::$instance = new self($basePath, $policy ?? new EventCompactionPolicy());

        return self::$instance;
    }

    public function compactAggregate(string $aggregateId): EventCompactionResult
    {
        $events = array_map(
            static fn ($event): array => $event->toArray(),
            EventStoreRuntime::instance($this->basePath)->eventsForAggregate($aggregateId)
        );

        $events = $this->optimizer->optimize($events);
        $before = count($events);

        if ($before <= $this->policy->retainLastEvents()) {
            $aggregateType = (string) ($events[0]['aggregateType'] ?? '');
            $result = EventCompactionResult::create($aggregateId, $aggregateType, $before, $before, false, 'stream below retention threshold');
            $this->results[] = $result;
            $this->save();
            return $result;
        }

        $retain = array_slice($events, -$this->policy->retainLastEvents());
        $compact = array_slice($events, 0, $before - $this->policy->retainLastEvents());
        $aggregateType = (string) ($events[0]['aggregateType'] ?? '');

        if ($this->policy->archiveCompacted()) {
            $this->archives[] = EventCompactionArchive::create($aggregateId, $aggregateType, $compact);
        }

        $result = EventCompactionResult::create($aggregateId, $aggregateType, $before, count($retain), $this->policy->archiveCompacted());
        $this->results[] = $result;
        $this->save();

        return $result;
    }

    public function optimize(array $events): array
    {
        return $this->optimizer->optimize($events);
    }

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

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

        $this->archives = [];
        foreach ($snapshot->archives() as $archive) {
            if (is_array($archive)) {
                $this->archives[] = EventCompactionArchive::fromArray($archive);
            }
        }
    }

    public function snapshot(): EventCompactionSnapshot
    {
        $results = array_map(static fn (EventCompactionResult $result): array => $result->toArray(), $this->results);
        $archives = array_map(static fn (EventCompactionArchive $archive): array => $archive->toArray(), $this->archives);

        return new EventCompactionSnapshot(
            $results,
            $archives,
            EventCompactionStatistics::from($results, $archives),
            $this->policy->toArray(),
            date('c')
        );
    }

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

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

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

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

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