<?php

declare(strict_types=1);

namespace App\Knowledge;

use Symfony\Component\Uid\Uuid;

final class ChunkManager
{
    private string $indexPath;

    public function __construct(
        string $projectDir,
        string $relativeIndexPath = '/var/knowledge/index.ndjson'
    ) {
        $this->indexPath = rtrim($projectDir, '/') . $relativeIndexPath;
    }

    // ============================================================
    // COUNT (Streaming, robust)
    // ============================================================

    public function countAllChunks(): int
    {
        if (!is_file($this->indexPath)) {
            return 0;
        }

        $handle = fopen($this->indexPath, 'rb');
        if (!$handle) {
            throw new \RuntimeException('Unable to open index.ndjson for counting');
        }

        $count = 0;

        try {
            while (($line = fgets($handle)) !== false) {
                $line = trim($line);
                if ($line === '') {
                    continue;
                }

                $data = json_decode($line, true);
                if (is_array($data)) {
                    $count++;
                }
            }
        } finally {
            fclose($handle);
        }

        return $count;
    }

    // ============================================================
    // APPEND (Streaming + Exception Safe)
    // ============================================================

    /**
     * @param iterable<array<string,mixed>> $records
     */
    public function appendChunks(iterable $records): void
    {
        $dir = \dirname($this->indexPath);

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

        $handle = fopen($this->indexPath, 'ab');
        if (!$handle) {
            throw new \RuntimeException('Unable to open index.ndjson for append');
        }

        try {
            foreach ($records as $record) {
                $json = json_encode(
                    $record,
                    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
                );

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

                if (fwrite($handle, $json . PHP_EOL) === false) {
                    throw new \RuntimeException('Unable to write chunk to index');
                }
            }
        } finally {
            fclose($handle);
        }
    }

    // ============================================================
    // COMPACTION (Streaming + Safe Handles)
    // ============================================================

    public function compactByDocument(Uuid $documentId): void
    {
        if (!is_file($this->indexPath)) {
            return;
        }

        $tmpPath = $this->indexPath . '.tmp';

        $in = fopen($this->indexPath, 'rb');
        $out = fopen($tmpPath, 'wb');

        if (!$in || !$out) {
            throw new \RuntimeException('Unable to open index for compaction');
        }

        $docIdString = $documentId->toRfc4122();

        try {
            while (($line = fgets($in)) !== false) {
                $line = trim($line);
                if ($line === '') {
                    continue;
                }

                $data = json_decode($line, true);
                if (!is_array($data)) {
                    continue;
                }

                if (($data['document_id'] ?? null) === $docIdString) {
                    continue;
                }

                if (fwrite($out, $line . PHP_EOL) === false) {
                    throw new \RuntimeException('Unable to write compacted chunk');
                }
            }
        } finally {
            fclose($in);
            fclose($out);
        }

        $this->atomicSwitch($tmpPath, $this->indexPath);
    }

    // ============================================================
    // FULL REWRITE (Streaming + Atomic)
    // ============================================================

    /**
     * @param iterable<array<string,mixed>> $records
     */
    public function rewriteAll(iterable $records): void
    {
        $dir = \dirname($this->indexPath);

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

        $tmpPath = $this->indexPath . '.tmp';

        $handle = fopen($tmpPath, 'wb');
        if (!$handle) {
            throw new \RuntimeException('Unable to open temp index file');
        }

        try {
            foreach ($records as $record) {
                $json = json_encode(
                    $record,
                    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
                );

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

                if (fwrite($handle, $json . PHP_EOL) === false) {
                    throw new \RuntimeException('Unable to write chunk during rewrite');
                }
            }
        } finally {
            fclose($handle);
        }

        $this->atomicSwitch($tmpPath, $this->indexPath);
    }

    // ============================================================
    // STREAM READ (FAISS rebuild safe)
    // ============================================================

    /**
     * @return \Generator<array<string,mixed>>
     */
    public function streamAll(): \Generator
    {
        if (!is_file($this->indexPath)) {
            return;
        }

        $handle = fopen($this->indexPath, 'rb');
        if (!$handle) {
            throw new \RuntimeException('Unable to open index.ndjson for read');
        }

        try {
            while (($line = fgets($handle)) !== false) {
                $line = trim($line);
                if ($line === '') {
                    continue;
                }

                $data = json_decode($line, true);
                if (is_array($data)) {
                    yield $data;
                }
            }
        } finally {
            fclose($handle);
        }
    }

    // ============================================================
    // INTERNAL ATOMIC SWITCH
    // ============================================================

    private function atomicSwitch(string $tmp, string $final): void
    {
        if (!rename($tmp, $final)) {
            @unlink($tmp);
            throw new \RuntimeException('Atomic switch failed for index.ndjson');
        }
    }
}