<?php

declare(strict_types=1);

namespace App\Config;

use InvalidArgumentException;

final class NdjsonHybridRetrieverConfig
{
    /**
     * @param array<string, mixed> $config
     */
    public function __construct(
        private array $config = [],
    ) {
    }

    public function hardMaxChunks(): int
    {
        return $this->requiredInt('hard_max_chunks', 1);
    }

    public function hardMaxVectorK(): int
    {
        return $this->requiredInt('hard_max_vectork', 1);
    }

    public function hardMaxKeywordK(): int
    {
        return $this->requiredInt('hard_max_keywordk', 1);
    }

    public function vectorScoreThreshold(): float
    {
        return $this->requiredFloat('vector_score_threshold', 0.0, 1.0);
    }

    public function thresholdFloor(): float
    {
        return $this->requiredFloat('threshold_floor', 0.0, 1.0);
    }

    public function thresholdCeil(): float
    {
        return $this->requiredFloat('threshold_ceil', 0.0, 1.0);
    }

    public function listBonus(): float
    {
        return $this->requiredFloat('list_bonus', 1.0);
    }

    public function rrfK(): int
    {
        return $this->requiredInt('rrf_k', 1);
    }

    public function keywordTopKMultiplier(): float
    {
        return $this->requiredFloat('keyword_topk_multiplier', 0.1);
    }

    public function keywordScoreThreshold(): float
    {
        return $this->requiredFloat('keyword_score_threshold', 0.0, 1.0);
    }

    public function keywordRrfWeight(): float
    {
        return $this->requiredFloat('keyword_rrf_weight', 0.0);
    }

    public function scopedVectorRrfWeight(): float
    {
        return $this->requiredFloat('scoped_vector_rrf_weight', 0.0);
    }

    public function scopedKeywordRrfWeight(): float
    {
        return $this->requiredFloat('scoped_keyword_rrf_weight', 0.0);
    }

    public function emptyRrfFallbackTopN(): int
    {
        return $this->requiredInt('empty_rrf_fallback_topn', 1);
    }

    public function maxChunksPerDoc(): int
    {
        return $this->requiredInt('max_chunks_per_doc', 1);
    }

    public function minChunkDistance(): int
    {
        return $this->requiredInt('min_chunk_distance', 0);
    }

    public function dominantDocWindow(): int
    {
        return $this->requiredInt('dominant_doc_window', 1);
    }

    public function dominantDocMinHits(): int
    {
        return $this->requiredInt('dominant_doc_min_hits', 1);
    }

    public function dominantDocMaxChunks(): int
    {
        return $this->requiredInt('dominant_doc_max_chunks', 1);
    }

    public function exactDocumentMaxChunks(): int
    {
        return $this->requiredInt('exact_document_max_chunks', 1);
    }

    public function focusedProductWindow(): int
    {
        return $this->requiredInt('focused_product_window', 1);
    }

    public function focusedProductMinScore(): float
    {
        return $this->requiredFloat('focused_product_min_score', 0.0);
    }

    public function focusedProductMinGap(): float
    {
        return $this->requiredFloat('focused_product_min_gap', 0.0);
    }

    public function focusedProductMaxChunks(): int
    {
        return $this->requiredInt('focused_product_max_chunks', 1);
    }

    /** @return string[] */
    public function catalogListShortcutPatterns(): array
    {
        return $this->requiredStringList('catalog_list_shortcut_patterns');
    }

    /** @return array<string, string[]> */
    public function exactSelectionTokenVariantPrefixes(): array
    {
        return $this->requiredStringListMap('exact_selection_token_variant_prefixes');
    }

    /** @return string[] */
    public function exactSelectionIndicatorQuestionTokens(): array
    {
        return $this->requiredStringList('exact_selection_indicator_question_tokens');
    }

    /** @return string[] */
    public function exactSelectionIndicatorQuestionPhrases(): array
    {
        return $this->requiredStringList('exact_selection_indicator_question_phrases');
    }

    /** @return string[] */
    public function exactSelectionIndicatorTableHeadingPatterns(): array
    {
        return $this->requiredStringList('exact_selection_indicator_table_heading_patterns');
    }

    /** @return string[] */
    public function exactSelectionIndicatorTableHeaderPatterns(): array
    {
        return $this->requiredStringList('exact_selection_indicator_table_header_patterns');
    }

    /** @return string[] */
    public function exactSelectionIndicatorTableRowPatterns(): array
    {
        return $this->requiredStringList('exact_selection_indicator_table_row_patterns');
    }

    /** @return string[] */
    public function exactSelectionIndicatorTableRequiredPrimaryTerms(): array
    {
        return $this->requiredStringList('exact_selection_indicator_table_required_primary_terms');
    }

    /** @return string[] */
    public function exactSelectionIndicatorTableRequiredContextTerms(): array
    {
        return $this->requiredStringList('exact_selection_indicator_table_required_context_terms');
    }

    /** @return string[] */
    public function exactDetailTokens(): array
    {
        return $this->requiredStringList('exact_detail_tokens');
    }

    /** @return string[] */
    public function genericExactSelectionTokens(): array
    {
        return $this->requiredStringList('generic_exact_selection_tokens');
    }

    /** @return string[] */
    public function genericProductTokens(): array
    {
        return $this->requiredStringList('generic_product_tokens');
    }

    /** @return string[] */
    public function importantShortModelTokens(): array
    {
        return $this->requiredStringList('important_short_model_tokens');
    }

    /** @return string[] */
    public function familyDescriptorTokens(): array
    {
        return $this->requiredStringList('family_descriptor_tokens');
    }

    /** @return string[] */
    public function looksLikeReagentTokens(): array
    {
        return $this->requiredStringList('looks_like_reagent_tokens');
    }

    /** @return string[] */
    public function looksLikeSafetyDocs(): array
    {
        return $this->requiredStringList('looks_like_safety_docs');
    }

    /** @return string[] */
    public function looksLikeReagentWords(): array
    {
        return $this->requiredStringList('looks_like_reagent_words');
    }

    /** @return string[] */
    public function looksLikeDocumentWords(): array
    {
        return $this->requiredStringList('looks_like_document_words');
    }

    /** @return string[] */
    public function looksLikeSafetyWords(): array
    {
        return $this->requiredStringList('looks_like_safety_words');
    }

    /** @return string[] */
    public function looksLikeDeviceWords(): array
    {
        return $this->requiredStringList('looks_like_device_words');
    }

    /**
     * Effective retrieval vocabulary as a dedicated structure for diagnostics and config dumps.
     *
     * @return array<string, array<int, string>>
     */
    public function vocabularyToArray(): array
    {
        return [
            'generic_product_tokens' => $this->genericProductTokens(),
            'important_short_model_tokens' => $this->importantShortModelTokens(),
            'family_descriptor_tokens' => $this->familyDescriptorTokens(),
            'looks_like_reagent_tokens' => $this->looksLikeReagentTokens(),
            'looks_like_safety_docs' => $this->looksLikeSafetyDocs(),
            'looks_like_reagent_words' => $this->looksLikeReagentWords(),
            'looks_like_document_words' => $this->looksLikeDocumentWords(),
            'looks_like_safety_words' => $this->looksLikeSafetyWords(),
            'looks_like_device_words' => $this->looksLikeDeviceWords(),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        return [
            'hard_max_chunks' => $this->hardMaxChunks(),
            'hard_max_vectork' => $this->hardMaxVectorK(),
            'hard_max_keywordk' => $this->hardMaxKeywordK(),
            'vector_score_threshold' => $this->vectorScoreThreshold(),
            'threshold_floor' => $this->thresholdFloor(),
            'threshold_ceil' => $this->thresholdCeil(),
            'list_bonus' => $this->listBonus(),
            'rrf_k' => $this->rrfK(),
            'keyword_topk_multiplier' => $this->keywordTopKMultiplier(),
            'keyword_score_threshold' => $this->keywordScoreThreshold(),
            'keyword_rrf_weight' => $this->keywordRrfWeight(),
            'scoped_vector_rrf_weight' => $this->scopedVectorRrfWeight(),
            'scoped_keyword_rrf_weight' => $this->scopedKeywordRrfWeight(),
            'empty_rrf_fallback_topn' => $this->emptyRrfFallbackTopN(),
            'max_chunks_per_doc' => $this->maxChunksPerDoc(),
            'min_chunk_distance' => $this->minChunkDistance(),
            'dominant_doc_window' => $this->dominantDocWindow(),
            'dominant_doc_min_hits' => $this->dominantDocMinHits(),
            'dominant_doc_max_chunks' => $this->dominantDocMaxChunks(),
            'exact_document_max_chunks' => $this->exactDocumentMaxChunks(),
            'focused_product_window' => $this->focusedProductWindow(),
            'focused_product_min_score' => $this->focusedProductMinScore(),
            'focused_product_min_gap' => $this->focusedProductMinGap(),
            'focused_product_max_chunks' => $this->focusedProductMaxChunks(),
            'catalog_list_shortcut_patterns' => $this->catalogListShortcutPatterns(),
            'exact_selection_token_variant_prefixes' => $this->exactSelectionTokenVariantPrefixes(),
            'exact_selection_indicator_question_tokens' => $this->exactSelectionIndicatorQuestionTokens(),
            'exact_selection_indicator_question_phrases' => $this->exactSelectionIndicatorQuestionPhrases(),
            'exact_selection_indicator_table_heading_patterns' => $this->exactSelectionIndicatorTableHeadingPatterns(),
            'exact_selection_indicator_table_header_patterns' => $this->exactSelectionIndicatorTableHeaderPatterns(),
            'exact_selection_indicator_table_row_patterns' => $this->exactSelectionIndicatorTableRowPatterns(),
            'exact_selection_indicator_table_required_primary_terms' => $this->exactSelectionIndicatorTableRequiredPrimaryTerms(),
            'exact_selection_indicator_table_required_context_terms' => $this->exactSelectionIndicatorTableRequiredContextTerms(),
            'exact_detail_tokens' => $this->exactDetailTokens(),
            'generic_exact_selection_tokens' => $this->genericExactSelectionTokens(),
            'generic_product_tokens' => $this->genericProductTokens(),
            'important_short_model_tokens' => $this->importantShortModelTokens(),
            'family_descriptor_tokens' => $this->familyDescriptorTokens(),
            'looks_like_reagent_tokens' => $this->looksLikeReagentTokens(),
            'looks_like_safety_docs' => $this->looksLikeSafetyDocs(),
            'looks_like_reagent_words' => $this->looksLikeReagentWords(),
            'looks_like_document_words' => $this->looksLikeDocumentWords(),
            'looks_like_safety_words' => $this->looksLikeSafetyWords(),
            'looks_like_device_words' => $this->looksLikeDeviceWords(),
        ];
    }

    private function requiredInt(string $key, int $min = PHP_INT_MIN, ?int $max = null): int
    {
        $value = $this->requiredValue($key);

        if (!is_numeric($value)) {
            throw $this->invalid($key, 'must be numeric');
        }

        $value = (int) $value;
        if ($value < $min) {
            throw $this->invalid($key, sprintf('must be greater than or equal to %d', $min));
        }

        if ($max !== null && $value > $max) {
            throw $this->invalid($key, sprintf('must be less than or equal to %d', $max));
        }

        return $value;
    }

    private function requiredFloat(string $key, float $min = -INF, ?float $max = null): float
    {
        $value = $this->requiredValue($key);

        if (!is_numeric($value)) {
            throw $this->invalid($key, 'must be numeric');
        }

        $value = (float) $value;
        if ($value < $min) {
            throw $this->invalid($key, sprintf('must be greater than or equal to %s', (string) $min));
        }

        if ($max !== null && $value > $max) {
            throw $this->invalid($key, sprintf('must be less than or equal to %s', (string) $max));
        }

        return $value;
    }

    /** @return string[] */
    private function requiredStringList(string $key): array
    {
        $value = $this->requiredValue($key);

        if (!is_array($value)) {
            throw $this->invalid($key, 'must be a list of non-empty strings');
        }

        $out = [];
        foreach ($value as $item) {
            if (!is_scalar($item)) {
                continue;
            }

            $item = trim((string) $item);
            if ($item === '') {
                continue;
            }

            if (!in_array($item, $out, true)) {
                $out[] = $item;
            }
        }

        if ($out === []) {
            throw $this->invalid($key, 'must contain at least one non-empty string');
        }

        return $out;
    }

    /**
     * @return array<string, string[]>
     */
    private function requiredStringListMap(string $key): array
    {
        $value = $this->requiredValue($key);

        if (!is_array($value)) {
            throw $this->invalid($key, 'must be a map of string lists');
        }

        $out = [];
        foreach ($value as $mapKey => $items) {
            if (!is_string($mapKey) || trim($mapKey) === '' || !is_array($items)) {
                continue;
            }

            $cleanItems = [];
            foreach ($items as $item) {
                if (!is_scalar($item)) {
                    continue;
                }

                $item = trim((string) $item);
                if ($item !== '' && !in_array($item, $cleanItems, true)) {
                    $cleanItems[] = $item;
                }
            }

            if ($cleanItems !== []) {
                $out[trim($mapKey)] = $cleanItems;
            }
        }

        if ($out === []) {
            throw $this->invalid($key, 'must contain at least one non-empty map entry');
        }

        return $out;
    }

    private function requiredValue(string $key): mixed
    {
        if (!array_key_exists($key, $this->config)) {
            throw $this->missing($key);
        }

        return $this->config[$key];
    }

    private function missing(string $key): InvalidArgumentException
    {
        return new InvalidArgumentException(sprintf('RetrieX retrieval config "%s" is missing.', $key));
    }

    private function invalid(string $key, string $reason): InvalidArgumentException
    {
        return new InvalidArgumentException(sprintf('RetrieX retrieval config "%s" %s.', $key, $reason));
    }
}
