<?php

declare(strict_types=1);

namespace App\Commerce;

use App\Commerce\Dto\ShopProductResult;
use Psr\Log\LoggerInterface;

final readonly class SearchRepairService
{
    public function __construct(
        private ShopSearchService $shopSearchService,
        private LoggerInterface $logger,
        private bool $enabled = true,
        private int $maxRepairQueries = 3,
        private int $minPrimaryResultsWithoutRepair = 2,
    ) {
    }

    /**
     * @param ShopProductResult[] $primaryShopResults
     * @param array<mixed> $knowledgeChunks
     *
     * @return array{
     *     results: ShopProductResult[],
     *     attemptedRepair: bool,
     *     usedRepair: bool,
     *     repairQueries: string[]
     * }
     */
    public function repair(
        string $prompt,
        string $commerceIntent,
        string $commerceHistoryContext,
        string $primaryQuery,
        array $primaryShopResults,
        array $knowledgeChunks
    ): array {
        if (!$this->enabled) {
            return [
                'results' => $primaryShopResults,
                'attemptedRepair' => false,
                'usedRepair' => false,
                'repairQueries' => [],
            ];
        }

        if (!$this->shouldAttemptRepair($prompt, $primaryQuery, $primaryShopResults)) {
            return [
                'results' => $primaryShopResults,
                'attemptedRepair' => false,
                'usedRepair' => false,
                'repairQueries' => [],
            ];
        }

        $repairQueries = $this->buildRepairQueries(
            prompt: $prompt,
            primaryQuery: $primaryQuery,
            primaryShopResults: $primaryShopResults,
            knowledgeChunks: $knowledgeChunks
        );

        if ($repairQueries === []) {
            return [
                'results' => $primaryShopResults,
                'attemptedRepair' => false,
                'usedRepair' => false,
                'repairQueries' => [],
            ];
        }

        $this->logger->info('Shop repair started', [
            'commerceIntent' => $commerceIntent,
            'primaryQuery' => $primaryQuery,
            'primaryResultsCount' => count($primaryShopResults),
            'repairQueries' => $repairQueries,
            'hasCommerceHistoryContext' => $commerceHistoryContext !== '',
            'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext),
        ]);

        $repairResults = [];

        foreach ($repairQueries as $repairQuery) {
            $results = $this->shopSearchService->search($repairQuery, $commerceIntent, '');

            if ($results === []) {
                continue;
            }

            $repairResults = $this->mergeUniqueProducts($repairResults, $results);
        }

        if ($repairResults === []) {
            $this->logger->info('Shop repair finished without additional products', [
                'commerceIntent' => $commerceIntent,
                'primaryQuery' => $primaryQuery,
                'repairQueries' => $repairQueries,
            ]);

            return [
                'results' => $primaryShopResults,
                'attemptedRepair' => true,
                'usedRepair' => false,
                'repairQueries' => $repairQueries,
            ];
        }

        $mergedResults = $this->rankMergedResults(
            primaryResults: $primaryShopResults,
            repairResults: $repairResults,
            prompt: $prompt,
            primaryQuery: $primaryQuery,
            repairQueries: $repairQueries
        );

        $this->logger->info('Shop repair finished', [
            'commerceIntent' => $commerceIntent,
            'primaryQuery' => $primaryQuery,
            'primaryResultsCount' => count($primaryShopResults),
            'repairResultsCount' => count($repairResults),
            'mergedResultsCount' => count($mergedResults),
            'repairQueries' => $repairQueries,
            'topProducts' => array_map(
                static fn(ShopProductResult $product): array => [
                    'name' => $product->name,
                    'productNumber' => $product->productNumber,
                    'manufacturer' => $product->manufacturer,
                    'available' => $product->available,
                ],
                array_slice($mergedResults, 0, 3)
            ),
        ]);

        return [
            'results' => $mergedResults,
            'attemptedRepair' => true,
            'usedRepair' => true,
            'repairQueries' => $repairQueries,
        ];
    }

    /**
     * @param ShopProductResult[] $primaryShopResults
     */
    private function shouldAttemptRepair(
        string $prompt,
        string $primaryQuery,
        array $primaryShopResults
    ): bool {
        $asksForBundle = $this->asksForBundleOrAccessory($prompt);
        $hasModelLikePrimaryQuery = $this->containsModelLikePhrase($primaryQuery);
        $primaryResultsCount = count($primaryShopResults);

        if ($primaryResultsCount === 0) {
            return true;
        }

        // Always try repair for bundle/accessory prompts.
        // These prompts often need a second pass even when the first search
        // already returned some results, because the user is asking for a
        // combination of main device + matching accessory.
        if ($asksForBundle) {
            return true;
        }

        if ($primaryResultsCount >= $this->minPrimaryResultsWithoutRepair) {
            return false;
        }

        if ($hasModelLikePrimaryQuery && $primaryResultsCount > 0) {
            return false;
        }

        return $primaryResultsCount < $this->minPrimaryResultsWithoutRepair;
    }

    /**
     * @param ShopProductResult[] $primaryShopResults
     * @param array<mixed> $knowledgeChunks
     * @return string[]
     */
    private function buildRepairQueries(
        string $prompt,
        string $primaryQuery,
        array $primaryShopResults,
        array $knowledgeChunks
    ): array {
        $knowledgeText = $this->flattenToText($knowledgeChunks);
        $primaryResultText = $this->flattenPrimaryResults($primaryShopResults);
        $combinedText = trim($prompt . "\n" . $knowledgeText . "\n" . $primaryResultText);

        $modelCandidates = $this->extractModelCandidates($combinedText);
        $accessoryCandidates = $this->extractAccessoryCandidates($combinedText);

        $topPrimaryName = $primaryShopResults[0]->name ?? '';
        $topPrimaryProductNumber = $primaryShopResults[0]->productNumber ?? null;
        $topPrimaryPhrase = trim($topPrimaryName . ' ' . ($topPrimaryProductNumber ?? ''));

        $queries = [];

        if ($topPrimaryPhrase !== '' && $this->containsModelLikePhrase($topPrimaryPhrase)) {
            $queries[] = $topPrimaryPhrase;
        } elseif ($topPrimaryName !== '' && $this->containsModelLikePhrase($topPrimaryName)) {
            $queries[] = $topPrimaryName;
        }

        foreach ($modelCandidates as $candidate) {
            $queries[] = $candidate;
        }

        if ($this->asksForBundleOrAccessory($prompt)) {
            foreach ($accessoryCandidates as $accessoryCandidate) {
                if ($topPrimaryName !== '') {
                    $queries[] = trim($topPrimaryName . ' ' . $accessoryCandidate);
                }

                $queries[] = $accessoryCandidate;
            }
        }

        $queries = array_map(
            fn(string $query): string => $this->sanitizeQuery($query),
            $queries
        );

        $queries = array_values(array_filter(
            array_unique($queries),
            fn(string $query): bool => $query !== '' && !$this->isTooCloseToPrimaryQuery($query, $primaryQuery)
        ));

        return array_slice($queries, 0, max(1, $this->maxRepairQueries));
    }

    /**
     * @param array<mixed> $value
     */
    private function flattenToText(array $value): string
    {
        $parts = [];

        $walker = function (mixed $item) use (&$parts, &$walker): void {
            if (is_array($item)) {
                foreach ($item as $subItem) {
                    $walker($subItem);
                }

                return;
            }

            if (is_scalar($item)) {
                $text = trim((string) $item);
                if ($text !== '') {
                    $parts[] = $text;
                }
            }
        };

        $walker($value);

        return trim(implode("\n", $parts));
    }

    /**
     * @param ShopProductResult[] $products
     */
    private function flattenPrimaryResults(array $products): string
    {
        $parts = [];

        foreach ($products as $product) {
            $parts[] = trim(implode(' ', array_filter([
                $product->name,
                $product->productNumber,
                $product->manufacturer,
                $product->description,
                implode(' ', $product->highlights),
                $product->customFields,
            ])));
        }

        return trim(implode("\n", array_filter($parts)));
    }

    /**
     * @return string[]
     */
    private function extractModelCandidates(string $text): array
    {
        $candidates = [];

        preg_match_all(
            '/\b([A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*(?:\s+[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*){0,2}\s+\d{2,5}[A-Za-z0-9\-]*)\b/u',
            $text,
            $matches
        );

        foreach ($matches[1] ?? [] as $candidate) {
            $candidate = $this->sanitizeQuery($candidate);

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

            if ($this->looksTooGeneric($candidate)) {
                continue;
            }

            $candidates[] = $candidate;
        }

        return $this->sortCandidatesBySpecificity($candidates);
    }

    /**
     * @return string[]
     */
    private function extractAccessoryCandidates(string $text): array
    {
        $candidates = [];

        preg_match_all(
            '/\b((?:indikator|indicator|reagenz|reagent|kit|set)\s+\d{1,5}[A-Za-z0-9\-]*)\b/iu',
            $text,
            $matches
        );

        foreach ($matches[1] ?? [] as $candidate) {
            $candidate = $this->sanitizeQuery($candidate);

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

            $candidates[] = $candidate;
        }

        return $this->sortCandidatesBySpecificity($candidates);
    }

    /**
     * @param string[] $candidates
     * @return string[]
     */
    private function sortCandidatesBySpecificity(array $candidates): array
    {
        $decorated = [];

        foreach (array_values(array_unique($candidates)) as $candidate) {
            $decorated[] = [
                'candidate' => $candidate,
                'score' => $this->scoreCandidate($candidate),
            ];
        }

        usort($decorated, static function (array $a, array $b): int {
            return $b['score'] <=> $a['score'];
        });

        return array_values(array_map(
            static fn(array $row): string => $row['candidate'],
            $decorated
        ));
    }

    private function scoreCandidate(string $candidate): int
    {
        $score = 0;

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

        $wordCount = count($this->tokenize($candidate));
        $score += min($wordCount, 4);

        if (preg_match('/\b(?:indikator|indicator|testomat|tritromat|titromat|reagenz|reagent)\b/iu', $candidate) === 1) {
            $score += 3;
        }

        return $score;
    }

    private function asksForBundleOrAccessory(string $prompt): bool
    {
        return preg_match(
                '/\b(passend|passende|zubehor|zubehör|dazu|zusatz|erganzung|ergänzung|indikator|reagenz|kit|set|auch\s+das|mit\s+preis\s+und\s+allen\s+infos)\b/iu',
                $prompt
            ) === 1;
    }

    private function containsModelLikePhrase(string $text): bool
    {
        return preg_match(
                '/\b[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*(?:\s+[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*){0,2}\s+\d{2,5}[A-Za-z0-9\-]*\b/u',
                $text
            ) === 1;
    }

    private function looksTooGeneric(string $candidate): bool
    {
        $normalized = mb_strtolower($candidate);

        foreach ([
                     'wasser',
                     'messgerät',
                     'messgeraet',
                     'produkt',
                     'geräte',
                     'geraete',
                     'gerät',
                     'geraet',
                     'resthärte',
                     'resthaerte',
                     'preis',
                     'infos',
                     'wissen',
                 ] as $genericToken) {
            if ($normalized === $genericToken) {
                return true;
            }
        }

        return false;
    }

    private function sanitizeQuery(string $query): string
    {
        $query = trim($query);
        $query = preg_replace('/\s+/u', ' ', $query) ?? $query;
        $query = trim($query, " \t\n\r\0\x0B\"'`.,;:-");

        return trim($query);
    }

    private function isTooCloseToPrimaryQuery(string $candidateQuery, string $primaryQuery): bool
    {
        $candidateTokens = $this->tokenize($candidateQuery);
        $primaryTokens = $this->tokenize($primaryQuery);

        if ($candidateTokens === [] || $primaryTokens === []) {
            return false;
        }

        $intersection = array_intersect($candidateTokens, $primaryTokens);
        $overlapRatio = count($intersection) / max(count($candidateTokens), count($primaryTokens));

        return $overlapRatio >= 0.9;
    }

    /**
     * @param ShopProductResult[] $existing
     * @param ShopProductResult[] $incoming
     * @return ShopProductResult[]
     */
    private function mergeUniqueProducts(array $existing, array $incoming): array
    {
        $merged = $existing;
        $seen = [];

        foreach ($existing as $product) {
            $seen[$this->buildProductKey($product)] = true;
        }

        foreach ($incoming as $product) {
            $key = $this->buildProductKey($product);

            if (isset($seen[$key])) {
                continue;
            }

            $seen[$key] = true;
            $merged[] = $product;
        }

        return $merged;
    }

    /**
     * @param ShopProductResult[] $primaryResults
     * @param ShopProductResult[] $repairResults
     * @param string[] $repairQueries
     * @return ShopProductResult[]
     */
    private function rankMergedResults(
        array $primaryResults,
        array $repairResults,
        string $prompt,
        string $primaryQuery,
        array $repairQueries
    ): array {
        $allResults = $this->mergeUniqueProducts($primaryResults, $repairResults);
        $repairSignal = trim(implode(' ', $repairQueries));

        $decorated = [];

        foreach ($allResults as $index => $product) {
            $score = 0;
            $score += $this->scoreProductAgainstText($product, $prompt) * 3;
            $score += $this->scoreProductAgainstText($product, $primaryQuery) * 2;
            $score += $this->scoreProductAgainstText($product, $repairSignal) * 4;

            if ($index < count($primaryResults)) {
                $score += 1;
            }

            $decorated[] = [
                'index' => $index,
                'score' => $score,
                'product' => $product,
            ];
        }

        usort($decorated, static function (array $a, array $b): int {
            if ($a['score'] === $b['score']) {
                return $a['index'] <=> $b['index'];
            }

            return $b['score'] <=> $a['score'];
        });

        return array_values(array_map(
            static fn(array $row): ShopProductResult => $row['product'],
            $decorated
        ));
    }

    private function scoreProductAgainstText(ShopProductResult $product, string $text): int
    {
        $queryTokens = $this->tokenize($text);
        if ($queryTokens === []) {
            return 0;
        }

        $productText = trim(implode(' ', array_filter([
            $product->name,
            $product->productNumber,
            $product->manufacturer,
            implode(' ', $product->highlights),
            $product->description,
            $product->customFields,
        ])));

        $productTokens = $this->tokenize($productText);
        if ($productTokens === []) {
            return 0;
        }

        $score = 0;
        $intersection = array_intersect($queryTokens, $productTokens);
        $score += count($intersection) * 2;

        foreach ($this->extractNumberTokens($queryTokens) as $numberToken) {
            if (in_array($numberToken, $productTokens, true)) {
                $score += 4;
            }
        }

        return $score;
    }

    private function buildProductKey(ShopProductResult $product): string
    {
        return mb_strtolower(trim(implode('|', [
            $product->id,
            $product->productNumber ?? '',
            $product->name,
            $product->url ?? '',
        ])));
    }

    /**
     * @return string[]
     */
    private function tokenize(string $text): array
    {
        $text = mb_strtolower($text);
        $text = preg_replace('/[^\p{L}\p{N}\s\-]+/u', ' ', $text) ?? $text;
        $text = preg_replace('/\s+/u', ' ', $text) ?? $text;
        $text = trim($text);

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

        return array_values(array_filter(explode(' ', $text)));
    }

    /**
     * @param string[] $tokens
     * @return string[]
     */
    private function extractNumberTokens(array $tokens): array
    {
        return array_values(array_filter(
            $tokens,
            static fn(string $token): bool => preg_match('/\d/u', $token) === 1
        ));
    }
}