<?php
declare(strict_types=1);

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

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

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

    private ProjectionRegistry $registry;
    /** @var array<string, ProjectionReadModel> */
    private array $readModels = [];
    /** @var array<string, ProjectionCheckpoint> */
    private array $checkpoints = [];
    private ProjectionStore $store;

    private function __construct(private readonly string $basePath)
    {
        $this->registry = new ProjectionRegistry();
        $this->store = new ProjectionStore($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(ProjectionDefinition $definition): ProjectionDefinition
    {
        $this->registry->register($definition);
        $this->readModels[$definition->projectionId()] ??= new ProjectionReadModel($definition->projectionId());
        $this->checkpoints[$definition->projectionId()] ??= new ProjectionCheckpoint($definition->projectionId());
        $this->save();

        return $definition;
    }

    public function run(string $projectionId): int
    {
        $definition = $this->registry->get($projectionId);
        if ($definition === null) {
            return 0;
        }

        $model = $this->readModels[$projectionId] ?? new ProjectionReadModel($projectionId);
        $checkpoint = $this->checkpoints[$projectionId] ?? new ProjectionCheckpoint($projectionId);

        $processed = 0;
        foreach (EventStoreRuntime::instance($this->basePath)->summary()['events'] as $event) {
            $sequence = (int) ($event['sequence'] ?? 0);
            if ($sequence <= $checkpoint->lastSequence()) {
                continue;
            }

            if (!$definition->accepts($event)) {
                continue;
            }

            $model = $model->apply($event);
            $checkpoint = $checkpoint->advance($sequence);
            $processed++;
        }

        $this->readModels[$projectionId] = $model;
        $this->checkpoints[$projectionId] = $checkpoint;
        $this->save();

        return $processed;
    }

    public function rebuild(string $projectionId): int
    {
        $definition = $this->registry->get($projectionId);
        if ($definition === null) {
            return 0;
        }

        $this->readModels[$projectionId] = (new ProjectionReadModel($projectionId))->clear();
        $this->checkpoints[$projectionId] = (new ProjectionCheckpoint($projectionId))->reset();

        return $this->run($projectionId);
    }

    public function readModel(string $projectionId): ?ProjectionReadModel
    {
        return $this->readModels[$projectionId] ?? null;
    }

    public function checkpoint(string $projectionId): ?ProjectionCheckpoint
    {
        return $this->checkpoints[$projectionId] ?? null;
    }

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

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

        $this->readModels = [];
        foreach ($snapshot->readModels() as $model) {
            if (is_array($model)) {
                $readModel = ProjectionReadModel::fromArray($model);
                $this->readModels[$readModel->projectionId()] = $readModel;
            }
        }

        $this->checkpoints = [];
        foreach ($snapshot->checkpoints() as $checkpoint) {
            if (is_array($checkpoint)) {
                $cp = ProjectionCheckpoint::fromArray($checkpoint);
                $this->checkpoints[$cp->projectionId()] = $cp;
            }
        }
    }

    public function snapshot(): ProjectionRuntimeSnapshot
    {
        $definitions = $this->registry->toArray();
        $readModels = array_map(static fn (ProjectionReadModel $model): array => $model->toArray(), array_values($this->readModels));
        $checkpoints = array_map(static fn (ProjectionCheckpoint $checkpoint): array => $checkpoint->toArray(), array_values($this->checkpoints));

        return new ProjectionRuntimeSnapshot(
            $definitions,
            $readModels,
            $checkpoints,
            ProjectionStatistics::from($definitions, $readModels, $checkpoints),
            date('c')
        );
    }

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

    public function clear(): void
    {
        $this->registry = new ProjectionRegistry();
        $this->readModels = [];
        $this->checkpoints = [];
        $this->store->reset();
    }

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

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

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