<?php

declare(strict_types=1);

namespace App\Config;

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

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

    /** @return string[] */
    public function getDeviceFocusKeywords(): array
    {
        return $this->configuredStringListOrVocabularyView(
            'device_focus_keywords',
            'vocabulary_views.device_focus_keywords'
        );
    }

    /** @return string[] */
    public function getAccessoryFocusKeywords(): array
    {
        return $this->configuredStringListOrVocabularyView(
            'accessory_focus_keywords',
            'vocabulary_views.accessory_focus_keywords'
        );
    }

    /** @return array<string, string[]> */
    public function getAccessoryFocusVariantMap(): array
    {
        return $this->configuredStringListMapOrVocabularyMap(
            'accessory_focus_variant_map',
            'vocabulary_maps.accessory_focus_variant_map'
        );
    }

    /** @return string[] */
    public function getDeviceQueryKeywords(): array
    {
        return $this->configuredStringListOrVocabularyView(
            'device_query_keywords',
            'vocabulary_views.device_query_keywords'
        );
    }

    /** @return string[] */
    public function getAccessoryQueryKeywords(): array
    {
        return $this->configuredStringListOrVocabularyView(
            'accessory_query_keywords',
            'vocabulary_views.accessory_query_keywords'
        );
    }

    /** @return string[] */
    public function getAccessoryProductKeywords(): array
    {
        return $this->configuredStringListOrVocabularyView(
            'accessory_product_keywords',
            'vocabulary_views.accessory_product_keywords'
        );
    }

    /** @return string[] */
    public function getDeviceProductKeywords(): array
    {
        return $this->configuredStringListOrVocabularyView(
            'device_product_keywords',
            'vocabulary_views.device_product_keywords'
        );
    }

    public function getExactProductNumberPhraseScore(): int
    {
        return $this->requiredInt('scores.exact_product_number_phrase');
    }

    public function getExactProductNamePhraseScore(): int
    {
        return $this->requiredInt('scores.exact_product_name_phrase');
    }

    public function getExactManufacturerMatchScore(): int
    {
        return $this->requiredInt('scores.exact_manufacturer_match');
    }

    public function getBrandContainedInNameScore(): int
    {
        return $this->requiredInt('scores.brand_contained_in_name');
    }

    public function getNameTokenOverlapWeight(): int
    {
        return $this->requiredInt('scores.name_token_overlap_weight');
    }

    public function getProductNumberTokenOverlapWeight(): int
    {
        return $this->requiredInt('scores.product_number_token_overlap_weight');
    }

    public function getCorpusTokenOverlapWeight(): int
    {
        return $this->requiredInt('scores.corpus_token_overlap_weight');
    }

    public function getNameNumberOverlapWeight(): int
    {
        return $this->requiredInt('scores.name_number_overlap_weight');
    }

    public function getProductNumberNumberOverlapWeight(): int
    {
        return $this->requiredInt('scores.product_number_number_overlap_weight');
    }

    public function getCorpusNumberOverlapWeight(): int
    {
        return $this->requiredInt('scores.corpus_number_overlap_weight');
    }

    public function getSizeMatchScore(): int
    {
        return $this->requiredInt('scores.size_match');
    }

    public function getAvailabilityBonusScore(): int
    {
        return $this->requiredInt('scores.availability_bonus');
    }

    public function getDeviceQueryDeviceProductBonus(): int
    {
        return $this->requiredInt('scores.device_query_device_product_bonus');
    }

    public function getDeviceQueryAccessoryPenalty(): int
    {
        return $this->requiredInt('scores.device_query_accessory_penalty');
    }

    public function getAccessoryQueryAccessoryProductBonus(): int
    {
        return $this->requiredInt('scores.accessory_query_accessory_product_bonus');
    }

    public function getAccessoryQueryDeviceProductBonus(): int
    {
        return $this->requiredInt('scores.accessory_query_device_product_bonus');
    }

    public function shouldFilterAccessoryProductsForDeviceQueries(): bool
    {
        return $this->requiredBool('role_guard.filter_accessory_products_for_device_queries');
    }

    public function shouldKeepAmbiguousProductsForDeviceQueries(): bool
    {
        return $this->requiredBool('role_guard.keep_ambiguous_products_for_device_queries');
    }

    public function getContainsDigitPattern(): string
    {
        return $this->requiredString('patterns.contains_digit');
    }

    public function getMatchingCleanupPattern(): string
    {
        return $this->requiredString('patterns.matching_cleanup');
    }

    public function getWhitespaceCollapsePattern(): string
    {
        return $this->requiredString('patterns.whitespace_collapse');
    }

    public function getTokenSplitPattern(): string
    {
        return $this->requiredString('patterns.token_split');
    }

    public function wrapWithPaddingSpaces(string $value): string
    {
        return $this->requiredString('padding.prefix', true) . trim($value) . $this->requiredString('padding.suffix', true);
    }

    /** @return string[] */
    public function getPriceNormalizationSearch(): array
    {
        return $this->requiredStringList('price.normalization_search');
    }

    /** @return string[] */
    public function getPriceNormalizationReplace(): array
    {
        return $this->requiredStringList('price.normalization_replace', true);
    }

    public function getPrimaryCustomFieldKey(): string
    {
        return $this->requiredString('custom_fields.primary');
    }

    public function getSecondaryCustomFieldKey(): string
    {
        return $this->requiredString('custom_fields.secondary');
    }

    public function getUseCasesCustomFieldKey(): string
    {
        return $this->requiredString('custom_fields.use_cases');
    }

    public function getLanguagesCustomFieldKey(): string
    {
        return $this->requiredString('custom_fields.languages');
    }

    public function getPrimarySecondarySeparator(): string
    {
        return $this->requiredString('text.primary_secondary_separator');
    }

    public function getUseCasesLabel(): string
    {
        return $this->requiredString('text.use_cases_label');
    }

    public function getLanguagesLabel(): string
    {
        return $this->requiredString('text.languages_label');
    }

    public function getCustomFieldJoinSeparator(): string
    {
        return $this->requiredString('text.custom_field_join_separator');
    }

    public function getDescriptionEmptyLinePattern(): string
    {
        return $this->requiredString('description.empty_line_pattern');
    }

    public function getDescriptionWhitespaceCleanupPattern(): string
    {
        return $this->requiredString('description.whitespace_cleanup_pattern');
    }

    public function getDescriptionMaxLength(): int
    {
        return $this->requiredInt('description.max_length', 0);
    }

    public function getPriceDecimals(): int
    {
        return $this->requiredInt('price.decimals', 0);
    }

    public function getPriceDecimalSeparator(): string
    {
        return $this->requiredString('price.decimal_separator');
    }

    public function getPriceThousandsSeparator(): string
    {
        return $this->requiredString('price.thousands_separator');
    }

    public function getPriceSuffix(): string
    {
        return $this->requiredString('price.suffix');
    }

    public function buildRelativeSeoUrl(string $path): string
    {
        return $this->requiredString('seo.relative_prefix') . ltrim($path, '/');
    }

    public function getAvailableHighlightLabel(): string
    {
        return $this->requiredString('highlight.available_label');
    }

    public function getUnavailableHighlightLabel(): string
    {
        return $this->requiredString('highlight.unavailable_label');
    }

    public function getProductNumberHighlightPrefix(): string
    {
        return $this->requiredString('highlight.product_number_prefix');
    }

    public function getMissingProductImagePlaceholder(): string
    {
        return $this->requiredString('image.missing_placeholder');
    }

    public function getDeduplicationSeparator(): string
    {
        return $this->requiredString('deduplication.separator');
    }

    /**
     * @return string[]
     */
    private function configuredStringListOrVocabularyView(string $configPath, string $viewPathConfigPath): array
    {
        if ($this->has($configPath)) {
            return $this->requiredStringList($configPath);
        }

        $viewPath = $this->requiredString($viewPathConfigPath);
        $terms = $this->vocabulary->view($viewPath, []);

        if ($terms === []) {
            throw new \InvalidArgumentException(sprintf(
                'RetrieX shop matching vocabulary view "%s" resolved to an empty list.',
                $viewPath
            ));
        }

        return $terms;
    }

    /**
     * @return array<string, string[]>
     */
    private function configuredStringListMapOrVocabularyMap(string $configPath, string $mapPathConfigPath): array
    {
        if ($this->has($configPath)) {
            return $this->requiredStringListMap($configPath);
        }

        $mapPath = $this->requiredString($mapPathConfigPath);
        $map = $this->vocabulary->map($mapPath, []);

        if ($map === []) {
            throw new \InvalidArgumentException(sprintf(
                'RetrieX shop matching vocabulary map "%s" resolved to an empty map.',
                $mapPath
            ));
        }

        return $map;
    }

    private function requiredBool(string $path): bool
    {
        $value = $this->requiredValue($path);

        if (is_bool($value)) {
            return $value;
        }

        if (is_scalar($value)) {
            $normalized = strtolower(trim((string) $value));

            if (in_array($normalized, ['1', 'true', 'yes', 'on'], true)) {
                return true;
            }

            if (in_array($normalized, ['0', 'false', 'no', 'off'], true)) {
                return false;
            }
        }

        throw new \InvalidArgumentException(sprintf(
            'RetrieX shop matching config path "%s" must be a boolean value.',
            $path
        ));
    }

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

        if (!is_numeric($value)) {
            throw new \InvalidArgumentException(sprintf(
                'RetrieX shop matching config path "%s" must be an integer value.',
                $path
            ));
        }

        $value = (int) $value;
        if ($value < $min) {
            throw new \InvalidArgumentException(sprintf(
                'RetrieX shop matching config path "%s" must be greater than or equal to %d.',
                $path,
                $min
            ));
        }

        return $value;
    }

    private function requiredString(string $path, bool $allowBlank = false): string
    {
        $value = $this->requiredValue($path);

        if (!is_scalar($value)) {
            throw new \InvalidArgumentException(sprintf(
                'RetrieX shop matching config path "%s" must be a scalar string value.',
                $path
            ));
        }

        $value = (string) $value;
        if (!$allowBlank && trim($value) === '') {
            throw new \InvalidArgumentException(sprintf(
                'RetrieX shop matching config path "%s" must not be empty.',
                $path
            ));
        }

        return $value;
    }

    /**
     * @return string[]
     */
    private function requiredStringList(string $path, bool $allowEmptyStrings = false): array
    {
        $value = $this->requiredValue($path);

        if (!is_array($value)) {
            throw new \InvalidArgumentException(sprintf(
                'RetrieX shop matching config path "%s" must be a string list.',
                $path
            ));
        }

        $out = [];
        foreach ($value as $index => $item) {
            if (!is_scalar($item)) {
                throw new \InvalidArgumentException(sprintf(
                    'RetrieX shop matching config path "%s.%s" must be a scalar string value.',
                    $path,
                    (string) $index
                ));
            }

            $item = (string) $item;
            if (!$allowEmptyStrings) {
                $item = trim($item);
            }

            if (!$allowEmptyStrings && $item === '') {
                throw new \InvalidArgumentException(sprintf(
                    'RetrieX shop matching config path "%s.%s" must not be empty.',
                    $path,
                    (string) $index
                ));
            }

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

        if ($out === []) {
            throw new \InvalidArgumentException(sprintf(
                'RetrieX shop matching config path "%s" must not be empty.',
                $path
            ));
        }

        return $out;
    }

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

        if (!is_array($value)) {
            throw new \InvalidArgumentException(sprintf(
                'RetrieX shop matching config path "%s" must be a map of string lists.',
                $path
            ));
        }

        $out = [];
        foreach ($value as $key => $items) {
            if (!is_scalar($key) || !is_array($items)) {
                throw new \InvalidArgumentException(sprintf(
                    'RetrieX shop matching config path "%s" must be a map of string lists.',
                    $path
                ));
            }

            $cleanKey = trim((string) $key);
            if ($cleanKey === '') {
                throw new \InvalidArgumentException(sprintf(
                    'RetrieX shop matching config path "%s" contains an empty map key.',
                    $path
                ));
            }

            $cleanItems = [];
            foreach ($items as $index => $item) {
                if (!is_scalar($item)) {
                    throw new \InvalidArgumentException(sprintf(
                        'RetrieX shop matching config path "%s.%s.%s" must be a scalar string value.',
                        $path,
                        $cleanKey,
                        (string) $index
                    ));
                }

                $item = trim((string) $item);
                if ($item === '') {
                    throw new \InvalidArgumentException(sprintf(
                        'RetrieX shop matching config path "%s.%s.%s" must not be empty.',
                        $path,
                        $cleanKey,
                        (string) $index
                    ));
                }

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

            if ($cleanItems === []) {
                throw new \InvalidArgumentException(sprintf(
                    'RetrieX shop matching config path "%s.%s" must not be empty.',
                    $path,
                    $cleanKey
                ));
            }

            $out[$cleanKey] = $cleanItems;
        }

        if ($out === []) {
            throw new \InvalidArgumentException(sprintf(
                'RetrieX shop matching config path "%s" must not be empty.',
                $path
            ));
        }

        return $out;
    }

    private function has(string $path): bool
    {
        $current = $this->config;

        foreach (explode('.', $path) as $segment) {
            if (!is_array($current) || !array_key_exists($segment, $current)) {
                return false;
            }

            $current = $current[$segment];
        }

        return true;
    }

    private function requiredValue(string $path): mixed
    {
        $current = $this->config;

        foreach (explode('.', $path) as $segment) {
            if (!is_array($current) || !array_key_exists($segment, $current)) {
                throw new \InvalidArgumentException(sprintf(
                    'RetrieX shop matching config path "%s" is missing. YAML must be the source of truth.',
                    $path
                ));
            }

            $current = $current[$segment];
        }

        return $current;
    }
}
