<?php

declare(strict_types=1);

namespace App\Index;

final class IndexMetaManager
{
    private string $metaPath;
    private string $runtimePath;
    private IndexConfigurationProvider $provider;

    public function __construct(
        string $metaPath,
        string $runTimePath,
        IndexConfigurationProvider $provider
    ) {
        $this->metaPath    = $metaPath;
        $this->runtimePath = $runTimePath;
        $this->provider    = $provider;
    }

    // =====================================================
    // META (Governance – unverändert inhaltlich)
    // =====================================================

    public function ensureExists(): void
    {
        if (!is_file($this->metaPath)) {
            $this->writeMeta(1);
        }
    }

    public function readMeta(): ?array
    {
        if (!is_file($this->metaPath)) {
            return null;
        }

        $data = json_decode(
            (string) file_get_contents($this->metaPath),
            true
        );

        return is_array($data) ? $data : null;
    }

    public function validateAgainstCurrent(): void
    {
        $meta = $this->readMeta();
        $current = $this->provider
            ->getConfiguration()
            ->toStructureArray();

        if ($meta === null) {
            return;
        }

        foreach ($current as $key => $value) {
            if (($meta[$key] ?? null) !== $value) {
                throw new \RuntimeException(
                    'Index structure changed. Global Reindex required.'
                );
            }
        }
    }

    public function writeMetaForGlobalReindex(): void
    {
        $current = $this->readMeta();
        $nextVersion = ($current['index_version'] ?? 0) + 1;

        $this->writeMeta($nextVersion);
    }

    public function writeMeta(int $indexVersion): void
    {
        $config = $this->provider->getConfiguration();

        $payload = array_merge(
            [
                'index_version' => $indexVersion,
                'created_at'    => (new \DateTimeImmutable())->format(DATE_ATOM),
            ],
            $config->toStructureArray()
        );

        $this->atomicWriteJson($this->metaPath, $payload);
    }

    // =====================================================
    // RUNTIME (Chunk Counter etc.)
    // =====================================================

    private function ensureRuntimeFileExists(): void
    {
        if (is_file($this->runtimePath)) {
            return;
        }

        $payload = [
            'chunk_count'     => 0,
            'last_rebuild_at' => null,
        ];

        $this->atomicWriteJson($this->runtimePath, $payload);
    }

    /**
     * Für Chunk-Rebuilds (harte Zahl + Commit-Timestamp).
     */
    public function updateRuntimeStats(int $chunkCount): void
    {
        $this->ensureRuntimeFileExists();

        $payload = [
            'chunk_count'     => $chunkCount,
            'last_rebuild_at' => (new \DateTimeImmutable())->format(DATE_ATOM),
        ];

        $this->atomicWriteJson($this->runtimePath, $payload);
    }

    /**
     * Enterprise-Commit-Marker für alles, was einen Reload im Vector-Service erfordern soll,
     * ohne dass zwingend chunk_count aktualisiert werden kann (z.B. Tag-Rebuild).
     *
     * Beispiel:
     *   touchRuntime(['last_tags_rebuild_at' => DATE_ATOM])
     */
    public function touchRuntime(array $extra = []): void
    {
        $this->ensureRuntimeFileExists();

        $current = $this->readRuntime() ?? [];

        // Grundschema absichern
        if (!isset($current['chunk_count'])) {
            $current['chunk_count'] = 0;
        }

        $payload = array_merge(
            $current,
            $extra,
            [
                // Commit-Marker IMMER setzen/überschreiben
                'last_rebuild_at' => (new \DateTimeImmutable())->format(DATE_ATOM),
            ]
        );

        $this->atomicWriteJson($this->runtimePath, $payload);
    }

    /**
     * Runtime-Info lesen (z.B. für Tests, Debug, Polling).
     */
    public function readRuntime(): ?array
    {
        $this->ensureRuntimeFileExists();

        $data = json_decode(
            (string) file_get_contents($this->runtimePath),
            true
        );

        return is_array($data) ? $data : null;
    }

    public function getRuntimeChunkCount(): int
    {
        $data = $this->readRuntime();

        return (int)($data['chunk_count'] ?? 0);
    }

    public function getRuntimeLastRebuildAt(): ?string
    {
        $data = $this->readRuntime();

        $v = $data['last_rebuild_at'] ?? null;

        return is_string($v) && $v !== '' ? $v : null;
    }

    // =====================================================
    // INTERNAL ATOMIC JSON WRITE
    // =====================================================

    private function atomicWriteJson(string $path, array $payload): void
    {
        $dir = dirname($path);

        if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
            throw new \RuntimeException('Unable to create directory for meta/runtime');
        }

        $tmpPath = $path . '.tmp';

        $json = json_encode(
            $payload,
            JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
        );

        if ($json === false) {
            throw new \RuntimeException('Unable to encode JSON payload');
        }

        if (file_put_contents($tmpPath, $json) === false) {
            throw new \RuntimeException('Unable to write temporary JSON file');
        }

        if (!rename($tmpPath, $path)) {
            @unlink($tmpPath);
            throw new \RuntimeException('Atomic switch failed for JSON file');
        }
    }
}