<?php
declare(strict_types=1);

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

use PpPhp\Core\Platform\Event\Sourcing\Projection\ProjectionRuntime;

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

    private ReadModelRegistry $registry;
    /** @var QueryResult[] */
    private array $results = [];
    private QueryStore $store;

    private function __construct(private readonly string $basePath)
    {
        $this->registry = new ReadModelRegistry();
        $this->store = new QueryStore($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(ReadModelDefinition $definition): ReadModelDefinition
    {
        $this->registry->register($definition);
        $this->save();

        return $definition;
    }

    public function query(QueryRequest $request): QueryResult
    {
        $started = microtime(true);

        $definition = $this->registry->get($request->readModelId());

        if ($definition === null || !$definition->active()) {
            $result = QueryResult::create($request, [], 0, (microtime(true) - $started) * 1000);
            $this->results[] = $result;
            $this->save();
            return $result;
        }

        $projection = ProjectionRuntime::instance($this->basePath)->readModel($definition->projectionId());
        $rows = $projection === null ? [] : array_values($projection->rows());

        $filtered = array_values(array_filter($rows, static fn (array $row): bool => $request->filter()->matches($row)));
        $sorted = $request->sorter()->sort($filtered);
        $paged = $request->paginator()->slice($sorted);

        $result = QueryResult::create($request, $paged, count($filtered), (microtime(true) - $started) * 1000);
        $this->results[] = $result;
        $this->save();

        return $result;
    }

    public function find(string $readModelId, string $aggregateId): ?array
    {
        $result = $this->query(QueryRequest::create($readModelId, ['aggregateId' => $aggregateId], '', 'asc', 1, 1));
        $rows = $result->rows();

        return $rows[0] ?? null;
    }

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

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

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

    public function snapshot(): QueryRuntimeSnapshot
    {
        $definitions = $this->registry->toArray();
        $results = array_map(static fn (QueryResult $result): array => $result->toArray(), $this->results);

        return new QueryRuntimeSnapshot(
            $definitions,
            $results,
            QueryStatistics::from($definitions, $results),
            date('c')
        );
    }

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

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

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

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

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