<?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 document-title match from the user prompt.
     *
     * Matching rules:
     * - exact contiguous title matches still win
     * - if no exact match exists, all significant title tokens may match in any order
     * - token fallback is intentionally conservative and handles light German suffixes
     *
     * @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) {
            foreach ($documents as $document) {
                $normalizedTitle = $document['normalized_title'];

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

                $score = 500 + 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;
    }

    /**
     * Fallback for factual prompts that mention a document title by its terms,
     * but not as one contiguous phrase.
     */
    private function isConfidentTitleTokenMatch(string $normalizedPrompt, string $normalizedTitle): bool
    {
        if ($normalizedPrompt === '' || $normalizedTitle === '') {
            return false;
        }

        $titleTokens = $this->significantTitleTokens($normalizedTitle);

        if (count($titleTokens) < 3 && preg_match('/\d/u', $normalizedTitle) !== 1) {
            return false;
        }

        $promptTokenVariants = $this->tokenVariantLookup($normalizedPrompt);

        foreach ($titleTokens as $titleToken) {
            $matched = false;

            foreach ($this->tokenVariants($titleToken) as $variant) {
                if (isset($promptTokenVariants[$variant])) {
                    $matched = true;
                    break;
                }
            }

            if (!$matched) {
                return false;
            }
        }

        return true;
    }

    /**
     * @return string[]
     */
    private function significantTitleTokens(string $normalizedTitle): array
    {
        $tokens = preg_split('/\s+/u', $normalizedTitle, -1, PREG_SPLIT_NO_EMPTY) ?: [];
        $out = [];

        foreach ($tokens as $token) {
            $token = trim($token);

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

            if (mb_strlen($token, 'UTF-8') < 3 && preg_match('/\d/u', $token) !== 1) {
                continue;
            }

            $out[] = $token;
        }

        return array_values(array_unique($out));
    }

    /**
     * @return array<string,bool>
     */
    private function tokenVariantLookup(string $normalizedText): array
    {
        $tokens = preg_split('/\s+/u', $normalizedText, -1, PREG_SPLIT_NO_EMPTY) ?: [];
        $lookup = [];

        foreach ($tokens as $token) {
            foreach ($this->tokenVariants($token) as $variant) {
                $lookup[$variant] = true;
            }
        }

        return $lookup;
    }

    /**
     * @return string[]
     */
    private function tokenVariants(string $token): array
    {
        $token = trim($token);

        if ($token === '') {
            return [];
        }

        $variants = [$token];
        $length = mb_strlen($token, 'UTF-8');

        if ($length >= 5) {
            foreach (['innen', 'enen', 'ern', 'en', 'er', 'es', 'e', 's', 'n'] as $suffix) {
                if (!str_ends_with($token, $suffix)) {
                    continue;
                }

                $stem = mb_substr($token, 0, $length - mb_strlen($suffix, 'UTF-8'), 'UTF-8');

                if (mb_strlen($stem, 'UTF-8') >= 3) {
                    $variants[] = $stem;
                }
            }
        }

        return array_values(array_unique($variants));
    }

    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);
    }
}
