<?php

declare(strict_types=1);

namespace App\Knowledge\Retrieval;

use App\Knowledge\ChunkManager;

final readonly class NdjsonChunkLookup
{
    public function __construct(
        private ChunkManager $chunkManager
    )
    {
    }

    /**
     * @param string[] $chunkIds RFC4122 UUID strings
     * @return array<string,array<string,mixed>> keyed by chunk_id
     */
    public function findByChunkIds(array $chunkIds): array
    {
        $wanted = array_fill_keys($chunkIds, true);
        $found = [];

        foreach ($this->chunkManager->streamAll() as $row) {
            $id = $row['chunk_id'] ?? null;

            if (!is_string($id) || !isset($wanted[$id])) {
                continue;
            }

            $found[$id] = $row;

            if (count($found) === count($wanted)) {
                break;
            }
        }

        return $found;
    }

    /**
     * Returns all chunks of one document keyed by chunk_id.
     *
     * @return array<string,array<string,mixed>>
     */
    public function findByDocumentId(string $documentId): array
    {
        $rows = [];

        foreach ($this->chunkManager->streamAll() as $row) {
            $rowDocumentId = $row['document_id'] ?? null;
            $chunkId = $row['chunk_id'] ?? null;

            if ($rowDocumentId !== $documentId || !is_string($chunkId) || $chunkId === '') {
                continue;
            }

            $rows[$chunkId] = $row;
        }

        return $rows;
    }

    /**
     * Resolves the best exact document title match from the user prompt.
     *
     * Matching rules:
     * - the normalized prompt must contain the full normalized document title
     * - titles containing digits are preferred, e.g. "Testomat 808"
     * - longer exact titles win over shorter generic titles
     *
     * @return array{
     *     document_id:string,
     *     document_title:string,
     *     rows:array<string,array<string,mixed>>
     * }|null
     */
    public function findBestExactDocumentByPrompt(string $prompt): ?array
    {
        $normalizedPrompt = $this->normalizeText($prompt);

        if ($normalizedPrompt === '') {
            return null;
        }

        $documents = [];

        foreach ($this->chunkManager->streamAll() as $row) {
            $documentId = $row['document_id'] ?? null;
            $chunkId = $row['chunk_id'] ?? null;

            if (!is_string($documentId) || $documentId === '' || !is_string($chunkId) || $chunkId === '') {
                continue;
            }

            if (!isset($documents[$documentId])) {
                $documentTitle = $this->extractDocumentTitle($row);

                if ($documentTitle === '') {
                    continue;
                }

                $documents[$documentId] = [
                    'document_id' => $documentId,
                    'document_title' => $documentTitle,
                    'normalized_title' => $this->normalizeText($documentTitle),
                    'rows' => [],
                ];
            }

            $documents[$documentId]['rows'][$chunkId] = $row;
        }

        $best = null;
        $bestScore = null;

        foreach ($documents as $document) {
            $normalizedTitle = $document['normalized_title'];

            if (!$this->isConfidentTitleMatch($normalizedPrompt, $normalizedTitle)) {
                continue;
            }

            $score = mb_strlen($normalizedTitle, 'UTF-8');

            if (preg_match('/\d/u', $normalizedTitle) === 1) {
                $score += 1000;
            }

            if ($best === null || $score > $bestScore) {
                $best = $document;
                $bestScore = $score;
            }
        }

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

        return [
            'document_id' => $best['document_id'],
            'document_title' => $best['document_title'],
            'rows' => $best['rows'],
        ];
    }

    /**
     * @param array<string,mixed> $row
     */
    private function extractDocumentTitle(array $row): string
    {
        $metadataTitle = $row['metadata']['document_title'] ?? null;

        if (is_string($metadataTitle) && trim($metadataTitle) !== '') {
            return trim($metadataTitle);
        }

        $text = (string) ($row['text'] ?? '');

        if (
            $text !== '' &&
            preg_match('/^#\s*Produkt\s+Titel:\s*`?([^`\n]+)`?/imu', $text, $matches) === 1
        ) {
            return trim((string) ($matches[1] ?? ''));
        }

        return '';
    }

    private function isConfidentTitleMatch(string $normalizedPrompt, string $normalizedTitle): bool
    {
        if ($normalizedPrompt === '' || $normalizedTitle === '') {
            return false;
        }

        $paddedPrompt = ' ' . $normalizedPrompt . ' ';
        $paddedTitle = ' ' . $normalizedTitle . ' ';

        if (!str_contains($paddedPrompt, $paddedTitle)) {
            return false;
        }

        $tokens = preg_split('/\s+/u', $normalizedTitle, -1, PREG_SPLIT_NO_EMPTY) ?: [];

        $significantTokens = array_values(array_filter(
            $tokens,
            static fn (string $token): bool => mb_strlen($token, 'UTF-8') >= 3 || preg_match('/\d/u', $token) === 1
        ));

        return count($significantTokens) >= 2 || preg_match('/\d/u', $normalizedTitle) === 1;
    }

    private function normalizeText(string $value): string
    {
        $value = mb_strtolower(trim($value), 'UTF-8');
        $value = str_replace(['-', '/', '_'], ' ', $value);
        $value = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $value) ?? $value;
        $value = preg_replace('/\s+/u', ' ', $value) ?? $value;

        return trim($value);
    }
}