<?php

declare(strict_types=1);

namespace App\Commerce;

use App\Commerce\Dto\CommerceReferenceContext;
use App\Commerce\Dto\CommerceSearchQuery;
use App\Commerce\Dto\ShopProductResult;
use App\Config\ShopServiceConfig;
use App\Shopware\ShopwareCriteriaBuilder;
use App\Shopware\StoreApiClient;
use App\Shopware\StoreApiException;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

final class ShopSearchService
{
    private const FOCUS_NEUTRAL = 'neutral';
    private const FOCUS_DEVICE = 'device';
    private const FOCUS_ACCESSORY = 'accessory';

    private bool $lastSearchHadSystemFailure = false;
    private ?string $lastSearchFailureReason = null;

    public function __construct(
        private readonly CommerceQueryParser $queryParser,
        private readonly ShopwareCriteriaBuilder $criteriaBuilder,
        private readonly StoreApiClient $storeApiClient,
        private readonly ShopServiceConfig $shopConfig,
        private readonly LoggerInterface $logger,
        private readonly bool $enabled = true,
        private readonly int $maxResults = 25,
        private readonly string $baseUrl = ''
    ) {
    }

    public function hadLastSearchSystemFailure(): bool
    {
        return $this->lastSearchHadSystemFailure;
    }

    public function getLastSearchFailureReason(): ?string
    {
        return $this->lastSearchFailureReason;
    }

    public function buildSearchQueryPreview(
        string $originalPrompt,
        string $commerceIntent,
        string $commerceHistoryContext = ''
    ): CommerceSearchQuery {
        return $this->queryParser->parse(
            $originalPrompt,
            $commerceIntent,
            $commerceHistoryContext
        );
    }

    /**
     * @return ShopProductResult[]
     */
    public function search(
        string $originalPrompt,
        string $commerceIntent,
        string $commerceHistoryContext = '',
        ?CommerceReferenceContext $referenceContext = null
    ): array {
        $this->resetLastSearchFailure();

        if (!$this->enabled) {
            $this->logger->info('Shop search skipped because commerce search is disabled', [
                'commerceIntent' => $commerceIntent,
            ]);

            return [];
        }

        $primaryQuery = $this->queryParser->parse(
            $originalPrompt,
            $commerceIntent,
            $commerceHistoryContext
        );

        $focusMode = $this->determineFocusMode(
            originalPrompt: $originalPrompt,
            referenceContext: $referenceContext
        );

        $this->logger->info('Shop search started', [
            'commerceIntent' => $commerceIntent,
            'originalPrompt' => $originalPrompt,
            'normalizedPrompt' => $primaryQuery->normalizedPrompt,
            'searchText' => $primaryQuery->searchText,
            'brand' => $primaryQuery->brand,
            'sizes' => $primaryQuery->sizes,
            'priceMin' => $primaryQuery->priceMin,
            'priceMax' => $primaryQuery->priceMax,
            'focusMode' => $focusMode,
            'hasCommerceHistoryContext' => $commerceHistoryContext !== '',
            'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext),
            'hasReferenceContext' => $referenceContext !== null,
            'referenceProductName' => $referenceContext?->productName,
            'referenceFocusTerms' => $referenceContext?->focusTerms,
            'criteriaLimit' => $this->maxResults,
        ]);

        $referenceProbeResults = $this->probeReferenceContext(
            originalPrompt: $originalPrompt,
            commerceIntent: $commerceIntent,
            referenceContext: $referenceContext
        );

        if ($this->lastSearchHadSystemFailure) {
            $this->logger->warning('Shop search reference probe failed, continuing with primary search', [
                'commerceIntent' => $commerceIntent,
                'originalPrompt' => $originalPrompt,
                'failureReason' => $this->lastSearchFailureReason,
            ]);

            // A reference probe is only an enrichment step. It must not block the primary shop search.
            $this->resetLastSearchFailure();
        }

        $rankedProducts = $this->executeSearch(
            $primaryQuery,
            $commerceIntent,
            $originalPrompt,
            true
        );

        if ($rankedProducts === [] && $commerceHistoryContext !== '' && !$this->lastSearchHadSystemFailure) {
            $fallbackQuery = $this->queryParser->parse(
                $originalPrompt,
                $commerceIntent,
                ''
            );

            $this->logger->info('Shop search retry without commerce history context', [
                'commerceIntent' => $commerceIntent,
                'originalPrompt' => $originalPrompt,
                'normalizedPrompt' => $fallbackQuery->normalizedPrompt,
                'searchText' => $fallbackQuery->searchText,
                'brand' => $fallbackQuery->brand,
                'sizes' => $fallbackQuery->sizes,
                'priceMin' => $fallbackQuery->priceMin,
                'priceMax' => $fallbackQuery->priceMax,
                'focusMode' => $focusMode,
                'hasReferenceContext' => $referenceContext !== null,
                'referenceProductName' => $referenceContext?->productName,
                'referenceFocusTerms' => $referenceContext?->focusTerms,
            ]);

            $rankedProducts = $this->executeSearch(
                $fallbackQuery,
                $commerceIntent,
                $originalPrompt,
                false
            );
        }

        $finalProducts = $this->mergeRankedProductLists(
            $referenceProbeResults,
            $rankedProducts,
            $primaryQuery
        );

        $finalProducts = $this->applyFocusGuardrails(
            products: $finalProducts,
            focusMode: $focusMode,
            originalPrompt: $originalPrompt,
            referenceContext: $referenceContext
        );

        $finalProducts = $this->applyRoleGuardrails(
            products: $finalProducts,
            query: $primaryQuery,
            originalPrompt: $originalPrompt
        );

        $finalProducts = $this->applyPriceFilters(
            products: $finalProducts,
            query: $primaryQuery
        );

        $this->logger->info('Shop search finished', [
            'commerceIntent' => $commerceIntent,
            'originalPrompt' => $originalPrompt,
            'focusMode' => $focusMode,
            'referenceProbeResultsCount' => count($referenceProbeResults),
            'rankedProductsCount' => count($finalProducts),
            'topProducts' => array_map(
                static fn(ShopProductResult $product): array => [
                    'name' => $product->name,
                    'productNumber' => $product->productNumber,
                    'manufacturer' => $product->manufacturer,
                    'available' => $product->available,
                    'price' => $product->price,
                ],
                array_slice($finalProducts, 0, $this->shopConfig->getTopProductLogLimit())
            ),
        ]);

        return $finalProducts;
    }

    /**
     * @return ShopProductResult[]
     */
    private function probeReferenceContext(
        string $originalPrompt,
        string $commerceIntent,
        ?CommerceReferenceContext $referenceContext
    ): array {
        if ($referenceContext === null) {
            return [];
        }

        $probeQueries = $this->buildReferenceProbeQueries($referenceContext);

        if ($probeQueries === []) {
            return [];
        }

        $allResults = [];

        foreach ($probeQueries as $referenceSearchText) {
            $probeQuery = new CommerceSearchQuery(
                originalPrompt: $originalPrompt,
                normalizedPrompt: mb_strtolower($referenceSearchText, 'UTF-8'),
                searchText: $referenceSearchText,
                brand: $referenceContext->manufacturer,
                sizes: [],
                properties: [],
                priceMin: null,
                priceMax: null,
                intent: $commerceIntent,
                needsLlmFallback: false,
            );

            $this->logger->info('Shop search reference probe', [
                'originalPrompt' => $originalPrompt,
                'commerceIntent' => $commerceIntent,
                'referenceSearchText' => $referenceSearchText,
                'referenceProductName' => $referenceContext->productName,
                'referenceProductNumber' => $referenceContext->productNumber,
                'referenceFocusTerms' => $referenceContext->focusTerms,
            ]);

            $results = $this->executeSearch(
                $probeQuery,
                $commerceIntent,
                $originalPrompt,
                false
            );

            if ($results !== []) {
                $allResults = array_merge($allResults, $results);
            }
        }

        if ($allResults === []) {
            return [];
        }

        $baseSearchText = $referenceContext->buildReferenceSearchText();

        $baseQuery = new CommerceSearchQuery(
            originalPrompt: $originalPrompt,
            normalizedPrompt: mb_strtolower($baseSearchText, 'UTF-8'),
            searchText: $baseSearchText,
            brand: $referenceContext->manufacturer,
            sizes: [],
            properties: [],
            priceMin: null,
            priceMax: null,
            intent: $commerceIntent,
            needsLlmFallback: false,
        );

        return $this->rerankProducts(
            $this->deduplicateProducts($allResults),
            $baseQuery
        );
    }

    /**
     * @return string[]
     */
    private function buildReferenceProbeQueries(CommerceReferenceContext $referenceContext): array
    {
        $queries = [];

        $baseProduct = trim($referenceContext->productName);
        $baseSearch = trim($referenceContext->buildReferenceSearchText());

        if ($baseSearch !== '') {
            $queries[] = $baseSearch;
        }

        if ($baseProduct !== '') {
            $queries[] = $baseProduct;
        }

        foreach ($referenceContext->focusTerms as $focusTerm) {
            if (!is_string($focusTerm)) {
                continue;
            }

            $focusTerm = trim($focusTerm);

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

            if ($baseProduct !== '') {
                $queries[] = trim($baseProduct . ' ' . $focusTerm);
            }

            foreach ($this->expandFocusTermVariants($focusTerm) as $variant) {
                if ($variant === '') {
                    continue;
                }

                if ($baseProduct !== '') {
                    $queries[] = trim($baseProduct . ' ' . $variant);
                }

                if ($referenceContext->productNumber !== null && $referenceContext->productNumber !== '') {
                    $queries[] = trim($baseProduct . ' ' . $referenceContext->productNumber . ' ' . $variant);
                }
            }
        }

        $queries = array_map(
            fn(string $value): string => $this->normalizeForMatching($value),
            $queries
        );

        $queries = array_values(array_unique(array_filter(
            $queries,
            static fn(string $value): bool => $value !== ''
        )));

        return $queries;
    }

    /**
     * @return string[]
     */
    private function expandFocusTermVariants(string $focusTerm): array
    {
        $normalized = $this->normalizeForMatching($focusTerm);
        $variants = [$normalized];
        $variantMap = $this->shopConfig->getAccessoryFocusVariantMap();

        if (isset($variantMap[$normalized]) && is_array($variantMap[$normalized])) {
            $variants = array_merge($variants, $variantMap[$normalized]);
        }

        return array_values(array_unique(array_filter(
            array_map(fn(string $value): string => $this->normalizeForMatching($value), $variants),
            static fn(string $value): bool => $value !== ''
        )));
    }

    /**
     * @return ShopProductResult[]
     */
    private function executeSearch(
        CommerceSearchQuery $query,
        string $commerceIntent,
        string $originalPrompt,
        bool $usesHistoryContext
    ): array {
        $criteria = $this->criteriaBuilder->build($query, $this->maxResults);

        try {
            $response = $this->storeApiClient->searchProducts($criteria);
            return $this->mapAndLogSearchResponse(
                response: $response,
                query: $query,
                commerceIntent: $commerceIntent,
                originalPrompt: $originalPrompt,
                usesHistoryContext: $usesHistoryContext,
                usedSafeCriteria: false
            );
        } catch (StoreApiException $e) {
            if ($e->isSafeCriteriaRetryRecommended()) {
                $safeResults = $this->retryWithSafeCriteria(
                    query: $query,
                    commerceIntent: $commerceIntent,
                    originalPrompt: $originalPrompt,
                    usesHistoryContext: $usesHistoryContext,
                    previousException: $e
                );

                if ($safeResults !== null) {
                    return $safeResults;
                }

                $minimalResults = $this->retryWithMinimalCriteria(
                    query: $query,
                    commerceIntent: $commerceIntent,
                    originalPrompt: $originalPrompt,
                    usesHistoryContext: $usesHistoryContext,
                    previousException: $e
                );

                if ($minimalResults !== null) {
                    $this->resetLastSearchFailure();
                    return $minimalResults;
                }

                $ultraSafeResults = $this->retryWithUltraSafeCriteria(
                    query: $query,
                    commerceIntent: $commerceIntent,
                    originalPrompt: $originalPrompt,
                    usesHistoryContext: $usesHistoryContext,
                    previousException: $e
                );

                if ($ultraSafeResults !== null) {
                    $this->resetLastSearchFailure();
                    return $ultraSafeResults;
                }
            }

            $this->recordFailedSearch($e);
            $this->logShopSearchFailure($query, $commerceIntent, $originalPrompt, $usesHistoryContext, $e);

            return [];
        } catch (
            ClientExceptionInterface |
            RedirectionExceptionInterface |
            ServerExceptionInterface |
            TransportExceptionInterface |
            \RuntimeException $e
        ) {
            $this->recordFailedSearch($e);
            $this->logShopSearchFailure($query, $commerceIntent, $originalPrompt, $usesHistoryContext, $e);

            return [];
        }
    }

    /**
     * @return ShopProductResult[]|null
     */
    private function retryWithSafeCriteria(
        CommerceSearchQuery $query,
        string $commerceIntent,
        string $originalPrompt,
        bool $usesHistoryContext,
        StoreApiException $previousException
    ): ?array {
        $this->logger->warning('Shop search retrying with safe criteria', [
            'commerceIntent' => $commerceIntent,
            'originalPrompt' => $originalPrompt,
            'normalizedPrompt' => $query->normalizedPrompt,
            'searchText' => $query->searchText,
            'usesHistoryContext' => $usesHistoryContext,
            'previousStatusCode' => $previousException->getStatusCode(),
            'previousUtf8Failure' => $previousException->isUtf8Failure(),
            'previousExceptionMessage' => $previousException->getMessage(),
        ]);

        try {
            $safeCriteria = $this->criteriaBuilder->buildSafe($query, $this->maxResults);
            $response = $this->storeApiClient->searchProducts($safeCriteria);

            return $this->mapAndLogSearchResponse(
                response: $response,
                query: $query,
                commerceIntent: $commerceIntent,
                originalPrompt: $originalPrompt,
                usesHistoryContext: $usesHistoryContext,
                usedSafeCriteria: true
            );
        } catch (
            ClientExceptionInterface |
            RedirectionExceptionInterface |
            ServerExceptionInterface |
            TransportExceptionInterface |
            \RuntimeException $safeException
        ) {
            $this->logger->warning('Shop search safe criteria retry failed', [
                'commerceIntent' => $commerceIntent,
                'originalPrompt' => $originalPrompt,
                'normalizedPrompt' => $query->normalizedPrompt,
                'searchText' => $query->searchText,
                'usesHistoryContext' => $usesHistoryContext,
                'exceptionClass' => $safeException::class,
                'exceptionMessage' => $safeException->getMessage(),
            ]);

            return null;
        }
    }

    /**
     * @return ShopProductResult[]|null
     */
    private function retryWithMinimalCriteria(
        CommerceSearchQuery $query,
        string $commerceIntent,
        string $originalPrompt,
        bool $usesHistoryContext,
        StoreApiException $previousException
    ): ?array {
        $this->logger->warning('Shop search retrying with minimal UTF-8-safe criteria', [
            'commerceIntent' => $commerceIntent,
            'originalPrompt' => $originalPrompt,
            'normalizedPrompt' => $query->normalizedPrompt,
            'searchText' => $query->searchText,
            'usesHistoryContext' => $usesHistoryContext,
            'previousStatusCode' => $previousException->getStatusCode(),
            'previousUtf8Failure' => $previousException->isUtf8Failure(),
            'previousExceptionMessage' => $previousException->getMessage(),
        ]);

        try {
            $minimalCriteria = $this->criteriaBuilder->buildMinimal($query, $this->maxResults);
            $response = $this->storeApiClient->searchProducts($minimalCriteria);

            return $this->mapAndLogSearchResponse(
                response: $response,
                query: $query,
                commerceIntent: $commerceIntent,
                originalPrompt: $originalPrompt,
                usesHistoryContext: $usesHistoryContext,
                usedSafeCriteria: true
            );
        } catch (
            ClientExceptionInterface |
            RedirectionExceptionInterface |
            ServerExceptionInterface |
            TransportExceptionInterface |
            \RuntimeException $minimalException
        ) {
            $this->logger->warning('Shop search minimal UTF-8-safe criteria retry failed', [
                'commerceIntent' => $commerceIntent,
                'originalPrompt' => $originalPrompt,
                'normalizedPrompt' => $query->normalizedPrompt,
                'searchText' => $query->searchText,
                'usesHistoryContext' => $usesHistoryContext,
                'exceptionClass' => $minimalException::class,
                'exceptionMessage' => $minimalException->getMessage(),
            ]);

            return null;
        }
    }

    /**
     * @return ShopProductResult[]|null
     */
    private function retryWithUltraSafeCriteria(
        CommerceSearchQuery $query,
        string $commerceIntent,
        string $originalPrompt,
        bool $usesHistoryContext,
        StoreApiException $previousException
    ): ?array {
        $this->logger->warning('Shop search retrying with ultra-safe UTF-8 criteria', [
            'commerceIntent' => $commerceIntent,
            'originalPrompt' => $originalPrompt,
            'normalizedPrompt' => $query->normalizedPrompt,
            'searchText' => $query->searchText,
            'usesHistoryContext' => $usesHistoryContext,
            'previousStatusCode' => $previousException->getStatusCode(),
            'previousUtf8Failure' => $previousException->isUtf8Failure(),
            'previousExceptionMessage' => $previousException->getMessage(),
        ]);

        try {
            $ultraSafeCriteria = $this->criteriaBuilder->buildUltraSafe($query, $this->maxResults);
            $response = $this->storeApiClient->searchProducts($ultraSafeCriteria);

            return $this->mapAndLogSearchResponse(
                response: $response,
                query: $query,
                commerceIntent: $commerceIntent,
                originalPrompt: $originalPrompt,
                usesHistoryContext: $usesHistoryContext,
                usedSafeCriteria: true
            );
        } catch (
            ClientExceptionInterface |
            RedirectionExceptionInterface |
            ServerExceptionInterface |
            TransportExceptionInterface |
            \RuntimeException $ultraSafeException
        ) {
            $this->logger->warning('Shop search ultra-safe UTF-8 criteria retry failed', [
                'commerceIntent' => $commerceIntent,
                'originalPrompt' => $originalPrompt,
                'normalizedPrompt' => $query->normalizedPrompt,
                'searchText' => $query->searchText,
                'usesHistoryContext' => $usesHistoryContext,
                'exceptionClass' => $ultraSafeException::class,
                'exceptionMessage' => $ultraSafeException->getMessage(),
            ]);

            return null;
        }
    }
    /**
     * @param array<mixed> $response
     * @return ShopProductResult[]
     */
    private function mapAndLogSearchResponse(
        array $response,
        CommerceSearchQuery $query,
        string $commerceIntent,
        string $originalPrompt,
        bool $usesHistoryContext,
        bool $usedSafeCriteria
    ): array {
        $mappedProducts = $this->mapProducts($response);
        $rankedProducts = $this->rerankProducts($mappedProducts, $query);

        $this->logger->info('Shop search request finished', [
            'commerceIntent' => $commerceIntent,
            'originalPrompt' => $originalPrompt,
            'normalizedPrompt' => $query->normalizedPrompt,
            'searchText' => $query->searchText,
            'brand' => $query->brand,
            'sizes' => $query->sizes,
            'priceMin' => $query->priceMin,
            'priceMax' => $query->priceMax,
            'usesHistoryContext' => $usesHistoryContext,
            'usedSafeCriteria' => $usedSafeCriteria,
            'rawElementsCount' => is_array($response['elements'] ?? null) ? count($response['elements']) : 0,
            'mappedProductsCount' => count($mappedProducts),
            'rankedProductsCount' => count($rankedProducts),
        ]);

        return $rankedProducts;
    }

    /**
     * @param array<mixed> $row
     */
    private function extractProductName(array $row): string
    {
        $translatedName = trim((string) ($row['translated']['name'] ?? ''));

        if ($translatedName !== '') {
            return $translatedName;
        }

        $name = trim((string) ($row['name'] ?? ''));

        if ($name !== '') {
            return $name;
        }

        $productNumber = trim((string) ($row['productNumber'] ?? ''));

        if ($productNumber !== '') {
            return 'Produkt ' . $productNumber;
        }

        return '';
    }

    private function logShopSearchFailure(
        CommerceSearchQuery $query,
        string $commerceIntent,
        string $originalPrompt,
        bool $usesHistoryContext,
        \Throwable $e
    ): void {
        $this->logger->warning('Shop search request failed', [
            'commerceIntent' => $commerceIntent,
            'originalPrompt' => $originalPrompt,
            'normalizedPrompt' => $query->normalizedPrompt,
            'searchText' => $query->searchText,
            'brand' => $query->brand,
            'sizes' => $query->sizes,
            'priceMin' => $query->priceMin,
            'priceMax' => $query->priceMax,
            'usesHistoryContext' => $usesHistoryContext,
            'systemFailure' => $this->lastSearchHadSystemFailure,
            'failureReason' => $this->lastSearchFailureReason,
            'exceptionClass' => $e::class,
            'exceptionMessage' => $e->getMessage(),
        ]);
    }

    private function resetLastSearchFailure(): void
    {
        $this->lastSearchHadSystemFailure = false;
        $this->lastSearchFailureReason = null;
    }

    private function recordFailedSearch(\Throwable $e): void
    {
        $isSystemFailure = $e instanceof StoreApiException
            ? $e->isSystemFailure()
            : $e instanceof ServerExceptionInterface || $e instanceof TransportExceptionInterface;

        if (!$isSystemFailure) {
            return;
        }

        $this->lastSearchHadSystemFailure = true;
        $this->lastSearchFailureReason = $e->getMessage();
    }

    /**
     * @param ShopProductResult[] $referenceProbeResults
     * @param ShopProductResult[] $rankedProducts
     * @return ShopProductResult[]
     */
    private function mergeRankedProductLists(
        array $referenceProbeResults,
        array $rankedProducts,
        CommerceSearchQuery $primaryQuery
    ): array {
        if ($referenceProbeResults === [] && $rankedProducts === []) {
            return [];
        }

        $merged = $this->deduplicateProducts(array_merge($referenceProbeResults, $rankedProducts));

        return $this->rerankProducts($merged, $primaryQuery);
    }

    private function determineFocusMode(
        string $originalPrompt,
        ?CommerceReferenceContext $referenceContext
    ): string {
        $normalizedPrompt = $this->normalizeForMatching($originalPrompt);

        if ($this->containsAnyKeyword($normalizedPrompt, $this->shopConfig->getDeviceFocusKeywords())) {
            return self::FOCUS_DEVICE;
        }

        if ($this->containsAnyKeyword($normalizedPrompt, $this->shopConfig->getAccessoryFocusKeywords())) {
            return self::FOCUS_ACCESSORY;
        }

        foreach ($referenceContext?->focusTerms ?? [] as $focusTerm) {
            if (!is_string($focusTerm)) {
                continue;
            }

            $normalizedFocusTerm = $this->normalizeForMatching($focusTerm);

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

            if ($this->isAccessoryFocusToken($normalizedFocusTerm)) {
                return self::FOCUS_ACCESSORY;
            }
        }

        return self::FOCUS_NEUTRAL;
    }

    /**
     * @param ShopProductResult[] $products
     * @return ShopProductResult[]
     */
    private function applyFocusGuardrails(
        array $products,
        string $focusMode,
        string $originalPrompt,
        ?CommerceReferenceContext $referenceContext
    ): array {
        if ($products === []) {
            return [];
        }

        return match ($focusMode) {
            self::FOCUS_ACCESSORY => $this->filterForAccessoryFocus(
                products: $products,
                originalPrompt: $originalPrompt,
                referenceContext: $referenceContext
            ),
            self::FOCUS_DEVICE => $this->filterForDeviceFocus(
                products: $products,
                originalPrompt: $originalPrompt
            ),
            default => $products,
        };
    }

    /**
     * @param ShopProductResult[] $products
     * @return ShopProductResult[]
     */
    private function filterForAccessoryFocus(
        array $products,
        string $originalPrompt,
        ?CommerceReferenceContext $referenceContext
    ): array {
        $normalizedPrompt = $this->normalizeForMatching($originalPrompt);
        $focusTerms = $this->extractAccessoryFocusTerms($normalizedPrompt, $referenceContext);

        if ($focusTerms === []) {
            return $products;
        }

        $accessoryMatches = [];
        $deviceMatches = [];
        $neutralMatches = [];

        foreach ($products as $product) {
            $isAccessoryLike = $this->isAccessoryLikeProduct($product);
            $isDeviceLike = $this->isDeviceLikeProduct($product);
            $matchesFocus = $this->productMatchesAnyFocusTerm($product, $focusTerms);

            if ($matchesFocus && $isAccessoryLike) {
                $accessoryMatches[] = $product;
                continue;
            }

            if ($matchesFocus) {
                $neutralMatches[] = $product;
                continue;
            }

            if ($isDeviceLike && !$isAccessoryLike) {
                $deviceMatches[] = $product;
                continue;
            }

            $neutralMatches[] = $product;
        }

        if ($accessoryMatches !== []) {
            $filtered = array_merge($accessoryMatches, $neutralMatches);

            $this->logger->info('Accessory focus guardrail kept focused accessory-like results', [
                'originalPrompt' => $originalPrompt,
                'focusTerms' => $focusTerms,
                'beforeCount' => count($products),
                'afterCount' => count($filtered),
            ]);

            return $filtered;
        }

        if ($deviceMatches !== [] && $neutralMatches === []) {
            $this->logger->info('Accessory focus guardrail suppressed device-only results', [
                'originalPrompt' => $originalPrompt,
                'focusTerms' => $focusTerms,
                'suppressedDeviceCount' => count($deviceMatches),
            ]);

            return [];
        }

        return $neutralMatches !== [] ? $neutralMatches : $products;
    }

    /**
     * @param ShopProductResult[] $products
     * @return ShopProductResult[]
     */
    private function filterForDeviceFocus(array $products, string $originalPrompt): array
    {
        $deviceMatches = [];
        $neutralMatches = [];
        $accessoryMatches = [];

        foreach ($products as $product) {
            $isAccessoryLike = $this->isAccessoryLikeProduct($product);
            $isDeviceLike = $this->isDeviceLikeProduct($product);

            if ($isDeviceLike && !$isAccessoryLike) {
                $deviceMatches[] = $product;
                continue;
            }

            if ($isAccessoryLike && !$isDeviceLike) {
                $accessoryMatches[] = $product;
                continue;
            }

            $neutralMatches[] = $product;
        }

        if ($deviceMatches !== []) {
            $filtered = array_merge($deviceMatches, $neutralMatches);

            $this->logger->info('Device focus guardrail kept device-like results', [
                'originalPrompt' => $originalPrompt,
                'beforeCount' => count($products),
                'afterCount' => count($filtered),
            ]);

            return $filtered;
        }

        if ($accessoryMatches !== [] && $neutralMatches === []) {
            $this->logger->info('Device focus guardrail suppressed accessory-only results', [
                'originalPrompt' => $originalPrompt,
                'suppressedAccessoryCount' => count($accessoryMatches),
            ]);

            return [];
        }

        return $neutralMatches !== [] ? $neutralMatches : $products;
    }

    /**
     * @param ShopProductResult[] $products
     * @return ShopProductResult[]
     */
    private function applyPriceFilters(array $products, CommerceSearchQuery $query): array
    {
        if ($products === []) {
            return [];
        }

        if ($query->priceMin === null && $query->priceMax === null) {
            return $products;
        }

        $filtered = [];

        foreach ($products as $product) {
            $price = $this->extractNumericPrice($product->price);

            if ($price === null) {
                continue;
            }

            if ($query->priceMin !== null && $price < $query->priceMin) {
                continue;
            }

            if ($query->priceMax !== null && $price > $query->priceMax) {
                continue;
            }

            $filtered[] = $product;
        }

        return $filtered;
    }

    /**
     * @return string[]
     */
    private function extractAccessoryFocusTerms(string $normalizedPrompt, ?CommerceReferenceContext $referenceContext): array
    {
        $terms = [];

        foreach ($referenceContext?->focusTerms ?? [] as $focusTerm) {
            if (!is_string($focusTerm)) {
                continue;
            }

            $normalized = $this->normalizeForMatching($focusTerm);

            if ($normalized !== '' && $this->isAccessoryFocusToken($normalized)) {
                $terms[$normalized] = $normalized;
            }
        }

        foreach ($this->shopConfig->getAccessoryFocusKeywords() as $candidate) {
            $normalizedCandidate = $this->normalizeForMatching($candidate);

            if ($normalizedCandidate !== '' && str_contains($normalizedPrompt, $normalizedCandidate)) {
                $terms[$normalizedCandidate] = $normalizedCandidate;
            }
        }

        return array_values($terms);
    }

    private function isAccessoryFocusToken(string $token): bool
    {
        foreach ($this->shopConfig->getAccessoryFocusKeywords() as $candidate) {
            if ($token === $this->normalizeForMatching($candidate)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param ShopProductResult[] $products
     * @return ShopProductResult[]
     */
    private function applyRoleGuardrails(array $products, CommerceSearchQuery $query, string $originalPrompt): array
    {
        if ($products === [] || !$this->shopConfig->shouldFilterAccessoryProductsForDeviceQueries()) {
            return $products;
        }

        $normalizedQuery = $this->normalizeForMatching(trim(implode(' ', array_filter([
            $query->normalizedPrompt !== '' ? $query->normalizedPrompt : $query->originalPrompt,
            $query->searchText,
            $originalPrompt,
        ]))));

        if (!$this->isDeviceQuery($normalizedQuery) || $this->isAccessoryQuery($normalizedQuery)) {
            return $products;
        }

        $filtered = [];
        $excluded = [];

        foreach ($products as $product) {
            if (!$product instanceof ShopProductResult) {
                continue;
            }

            $isAccessoryLike = $this->isAccessoryLikeProduct($product);
            $isDeviceLike = $this->isDeviceLikeProduct($product);

            if ($isAccessoryLike && !$isDeviceLike) {
                $excluded[] = $product;
                continue;
            }

            if (!$isDeviceLike && !$this->shopConfig->shouldKeepAmbiguousProductsForDeviceQueries()) {
                $excluded[] = $product;
                continue;
            }

            $filtered[] = $product;
        }

        if ($excluded !== []) {
            $this->logger->info('Shop role guard excluded accessory-like products for device query', [
                'originalPrompt' => $originalPrompt,
                'searchText' => $query->searchText,
                'excludedCount' => count($excluded),
                'keptCount' => count($filtered),
                'excludedProducts' => array_map(
                    static fn(ShopProductResult $product): array => [
                        'name' => $product->name,
                        'productNumber' => $product->productNumber,
                    ],
                    array_slice($excluded, 0, $this->shopConfig->getTopProductLogLimit())
                ),
            ]);
        }

        return array_values($filtered);
    }

    /**
     * @param string[] $focusTerms
     */
    private function productMatchesAnyFocusTerm(ShopProductResult $product, array $focusTerms): bool
    {
        if ($focusTerms === []) {
            return false;
        }

        $corpus = $this->buildNormalizedProductCorpus($product);

        foreach ($focusTerms as $focusTerm) {
            if ($focusTerm === '') {
                continue;
            }

            if (str_contains($corpus, $focusTerm)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param string[] $keywords
     */
    private function containsAnyKeyword(string $text, array $keywords): bool
    {
        foreach ($keywords as $keyword) {
            $normalizedKeyword = $this->normalizeForMatching($keyword);

            if ($normalizedKeyword !== '' && str_contains($text, $normalizedKeyword)) {
                return true;
            }
        }

        return false;
    }

    private function extractNumericPrice(?string $price): ?float
    {
        if ($price === null) {
            return null;
        }

        $normalized = str_replace(
            $this->shopConfig->getPriceNormalizationSearch(),
            $this->shopConfig->getPriceNormalizationReplace(),
            $price
        );

        return is_numeric($normalized) ? (float) $normalized : null;
    }

    /**
     * @param array<string, mixed> $response
     * @return ShopProductResult[]
     */
    private function mapProducts(array $response): array
    {
        $elements = $response['elements'] ?? [];

        if (!is_array($elements)) {
            return [];
        }

        $results = [];

        foreach ($elements as $row) {
            if (!is_array($row)) {
                continue;
            }

            $relativeUrl = $this->extractUrl($row);

            $results[] = new ShopProductResult(
                id: (string) ($row['id'] ?? ''),
                name: $this->extractProductName($row),
                productNumber: isset($row['productNumber']) ? (string) $row['productNumber'] : null,
                manufacturer: $this->extractManufacturer($row),
                price: $this->extractPrice($row),
                available: isset($row['available']) ? (bool) $row['available'] : null,
                url: $this->buildAbsoluteUrl($relativeUrl),
                highlights: $this->extractHighlights($row),
                description: $this->cleanUpDescription($row),
                productImage: $row['cover']['media']['thumbnails'][0]['url'] ?? $this->shopConfig->getMissingProductImagePlaceholder(),
                customFields: $this->getRelevantCustomFields($row['customFields'] ?? [])
            );
        }

        $results = array_values(array_filter(
            $results,
            static fn(ShopProductResult $product): bool => $product->name !== ''
        ));

        return $this->deduplicateProducts($results);
    }

    /**
     * @param ShopProductResult[] $products
     * @return ShopProductResult[]
     */
    private function rerankProducts(array $products, CommerceSearchQuery $query): array
    {
        if (count($products) <= 1) {
            return $products;
        }

        $decorated = [];

        foreach ($products as $index => $product) {
            $decorated[] = [
                'index' => $index,
                'score' => $this->scoreProduct($product, $query),
                '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 $entry): ShopProductResult => $entry['product'],
            $decorated
        ));
    }

    private function scoreProduct(ShopProductResult $product, CommerceSearchQuery $query): int
    {
        $score = 0;
        $normalizedPrompt = $this->normalizeForMatching(
            $query->normalizedPrompt !== '' ? $query->normalizedPrompt : $query->originalPrompt
        );
        $normalizedSearchText = $this->normalizeForMatching($query->searchText);
        $normalizedBrand = $this->normalizeForMatching((string) ($query->brand ?? ''));
        $normalizedSizes = array_values(array_filter(array_map(
            fn(mixed $size): string => $this->normalizeForMatching((string) $size),
            $query->sizes
        )));

        $normalizedQuery = trim(implode(' ', array_filter([
            $normalizedPrompt,
            $normalizedSearchText,
            $normalizedBrand,
            implode(' ', $normalizedSizes),
        ])));

        $queryTokens = $this->tokenize($normalizedQuery);
        $queryNumberTokens = $this->extractNumberTokens($queryTokens);

        $normalizedProductName = $this->normalizeForMatching($product->name);
        $normalizedProductNumber = $this->normalizeForMatching((string) ($product->productNumber ?? ''));
        $normalizedManufacturer = $this->normalizeForMatching((string) ($product->manufacturer ?? ''));
        $normalizedProductCorpus = $this->buildNormalizedProductCorpus($product);

        $productNameTokens = $this->tokenize($normalizedProductName);
        $productNumberTokens = $this->tokenize($normalizedProductNumber);
        $productCorpusTokens = $this->tokenize($normalizedProductCorpus);

        $productNameNumberTokens = $this->extractNumberTokens($productNameTokens);
        $productNumberNumberTokens = $this->extractNumberTokens($productNumberTokens);
        $productCorpusNumberTokens = $this->extractNumberTokens($productCorpusTokens);

        if ($normalizedProductNumber !== '' && $this->containsWholePhrase($normalizedQuery, $normalizedProductNumber)) {
            $score += $this->shopConfig->getExactProductNumberPhraseScore();
        }

        if ($normalizedProductName !== '' && $this->containsWholePhrase($normalizedQuery, $normalizedProductName)) {
            $score += $this->shopConfig->getExactProductNamePhraseScore();
        }

        if ($normalizedBrand !== '') {
            if ($normalizedManufacturer !== '' && $normalizedManufacturer === $normalizedBrand) {
                $score += $this->shopConfig->getExactManufacturerMatchScore();
            } elseif ($this->containsWholePhrase($normalizedProductName, $normalizedBrand)) {
                $score += $this->shopConfig->getBrandContainedInNameScore();
            }
        }

        $score += $this->countOverlap($queryTokens, $productNameTokens) * $this->shopConfig->getNameTokenOverlapWeight();
        $score += $this->countOverlap($queryTokens, $productNumberTokens) * $this->shopConfig->getProductNumberTokenOverlapWeight();
        $score += $this->countOverlap($queryTokens, $productCorpusTokens) * $this->shopConfig->getCorpusTokenOverlapWeight();

        $score += $this->countOverlap($queryNumberTokens, $productNameNumberTokens) * $this->shopConfig->getNameNumberOverlapWeight();
        $score += $this->countOverlap($queryNumberTokens, $productNumberNumberTokens) * $this->shopConfig->getProductNumberNumberOverlapWeight();
        $score += $this->countOverlap($queryNumberTokens, $productCorpusNumberTokens) * $this->shopConfig->getCorpusNumberOverlapWeight();

        foreach ($normalizedSizes as $normalizedSize) {
            if ($normalizedSize === '') {
                continue;
            }

            if (
                $this->containsWholePhrase($normalizedProductName, $normalizedSize)
                || $this->containsWholePhrase($normalizedProductNumber, $normalizedSize)
                || $this->containsWholePhrase($normalizedProductCorpus, $normalizedSize)
            ) {
                $score += $this->shopConfig->getSizeMatchScore();
            }
        }

        $score += $this->scoreProductTypeMatch($product, $normalizedQuery);

        if ($product->available === true) {
            $score += $this->shopConfig->getAvailabilityBonusScore();
        }

        return $score;
    }

    private function scoreProductTypeMatch(ShopProductResult $product, string $normalizedQuery): int
    {
        $score = 0;

        $isDeviceQuery = $this->isDeviceQuery($normalizedQuery);
        $isAccessoryQuery = $this->isAccessoryQuery($normalizedQuery);

        if (!$isDeviceQuery && !$isAccessoryQuery) {
            return 0;
        }

        $isAccessoryLikeProduct = $this->isAccessoryLikeProduct($product);
        $isDeviceLikeProduct = $this->isDeviceLikeProduct($product);

        if ($isDeviceQuery && !$isAccessoryQuery) {
            if ($isDeviceLikeProduct) {
                $score += $this->shopConfig->getDeviceQueryDeviceProductBonus();
            }

            if ($isAccessoryLikeProduct) {
                $score -= $this->shopConfig->getDeviceQueryAccessoryPenalty();
            }
        }

        if ($isAccessoryQuery) {
            if ($isAccessoryLikeProduct) {
                $score += $this->shopConfig->getAccessoryQueryAccessoryProductBonus();
            }

            if ($isDeviceLikeProduct) {
                $score += $this->shopConfig->getAccessoryQueryDeviceProductBonus();
            }
        }

        return $score;
    }

    private function isDeviceQuery(string $normalizedQuery): bool
    {
        foreach ($this->shopConfig->getDeviceQueryKeywords() as $keyword) {
            if (str_contains($normalizedQuery, $this->normalizeForMatching($keyword))) {
                return true;
            }
        }

        return false;
    }

    private function isAccessoryQuery(string $normalizedQuery): bool
    {
        foreach ($this->shopConfig->getAccessoryQueryKeywords() as $keyword) {
            if (str_contains($normalizedQuery, $this->normalizeForMatching($keyword))) {
                return true;
            }
        }

        return false;
    }

    private function isAccessoryLikeProduct(ShopProductResult $product): bool
    {
        $primaryRole = $this->resolvePrimaryShopProductRole($product);

        if ($primaryRole === 'accessory_or_consumable') {
            return true;
        }

        if ($primaryRole === 'main_device') {
            return false;
        }

        return $this->containsAnyShopKeyword(
            $this->buildNormalizedProductCorpus($product),
            $this->shopConfig->getAccessoryProductKeywords()
        );
    }

    private function isDeviceLikeProduct(ShopProductResult $product): bool
    {
        $primaryRole = $this->resolvePrimaryShopProductRole($product);

        if ($primaryRole === 'main_device') {
            return true;
        }

        if ($primaryRole === 'accessory_or_consumable') {
            return false;
        }

        return $this->containsAnyShopKeyword(
            $this->buildNormalizedProductCorpus($product),
            $this->shopConfig->getDeviceProductKeywords()
        );
    }

    private function resolvePrimaryShopProductRole(ShopProductResult $product): string
    {
        $primaryText = $this->buildNormalizedPrimaryProductIdentity($product);

        if ($primaryText === '') {
            return 'unknown';
        }

        $isAccessoryLike = $this->containsAnyShopKeyword(
            $primaryText,
            $this->shopConfig->getAccessoryProductKeywords()
        );
        $isDeviceLike = $this->containsAnyShopKeyword(
            $primaryText,
            $this->shopConfig->getDeviceProductKeywords()
        );

        if ($isAccessoryLike && !$isDeviceLike) {
            return 'accessory_or_consumable';
        }

        if ($isDeviceLike && !$isAccessoryLike) {
            return 'main_device';
        }

        if ($isAccessoryLike && $isDeviceLike) {
            return 'ambiguous_mixed_role';
        }

        return 'unknown';
    }

    private function buildNormalizedPrimaryProductIdentity(ShopProductResult $product): string
    {
        return $this->normalizeForMatching(implode(' ', array_filter([
            $product->name,
            $product->url,
        ])));
    }

    /**
     * @param string[] $keywords
     */
    private function containsAnyShopKeyword(string $normalizedText, array $keywords): bool
    {
        foreach ($keywords as $keyword) {
            $normalizedKeyword = $this->normalizeForMatching((string) $keyword);

            if ($normalizedKeyword !== '' && str_contains($normalizedText, $normalizedKeyword)) {
                return true;
            }
        }

        return false;
    }

    private function buildNormalizedProductCorpus(ShopProductResult $product): string
    {
        return $this->normalizeForMatching(implode(' ', array_filter([
            $product->name,
            $product->productNumber,
            $product->manufacturer,
            implode(' ', $product->highlights),
            $product->description,
            $product->customFields,
            $product->url,
        ])));
    }

    /**
     * @param string[] $left
     * @param string[] $right
     */
    private function countOverlap(array $left, array $right): int
    {
        if ($left === [] || $right === []) {
            return 0;
        }

        $leftSet = array_fill_keys($left, true);
        $rightSet = array_fill_keys($right, true);

        return count(array_intersect_key($leftSet, $rightSet));
    }

    /**
     * @param string[] $tokens
     * @return string[]
     */
    private function extractNumberTokens(array $tokens): array
    {
        return array_values(array_filter(
            $tokens,
            fn(string $token): bool => preg_match($this->shopConfig->getContainsDigitPattern(), $token) === 1
        ));
    }

    private function normalizeForMatching(string $value): string
    {
        $value = mb_strtolower(trim($value), 'UTF-8');
        $value = preg_replace($this->shopConfig->getMatchingCleanupPattern(), ' ', $value) ?? $value;
        $value = preg_replace($this->shopConfig->getWhitespaceCollapsePattern(), ' ', $value) ?? $value;

        return trim($value);
    }

    /**
     * @return string[]
     */
    private function tokenize(string $value): array
    {
        if ($value === '') {
            return [];
        }

        return preg_split(
            $this->shopConfig->getTokenSplitPattern(),
            $value,
            -1,
            PREG_SPLIT_NO_EMPTY
        ) ?: [];
    }

    private function containsWholePhrase(string $normalizedText, string $normalizedPhrase): bool
    {
        if ($normalizedText === '' || $normalizedPhrase === '') {
            return false;
        }

        return str_contains(
            $this->shopConfig->wrapWithPaddingSpaces($normalizedText),
            $this->shopConfig->wrapWithPaddingSpaces($normalizedPhrase)
        );
    }

    /**
     * @param array<string, mixed> $customField
     */
    private function getRelevantCustomFields(array $customField): string
    {
        $primary = (string) ($customField[$this->shopConfig->getPrimaryCustomFieldKey()] ?? '');
        $secondary = (string) ($customField[$this->shopConfig->getSecondaryCustomFieldKey()] ?? '');
        $useCases = (string) ($customField[$this->shopConfig->getUseCasesCustomFieldKey()] ?? '');
        $languages = (string) ($customField[$this->shopConfig->getLanguagesCustomFieldKey()] ?? '');

        $parts = [];

        if ($primary !== '' || $secondary !== '') {
            $parts[] = trim($primary . $this->shopConfig->getPrimarySecondarySeparator() . $secondary);
        }

        if ($useCases !== '') {
            $parts[] = $this->shopConfig->getUseCasesLabel() . $useCases;
        }

        if ($languages !== '') {
            $parts[] = $this->shopConfig->getLanguagesLabel() . $languages;
        }

        return trim(implode($this->shopConfig->getCustomFieldJoinSeparator(), array_filter($parts)));
    }

    /**
     * @param array<string, mixed> $description
     */
    private function cleanUpDescription(array $description): string
    {
        if (!isset($description['translated']['description'])) {
            return '';
        }

        $newDesc = strip_tags((string) ($description['translated']['description']));
        $newDesc = html_entity_decode($newDesc);
        $newDesc = preg_replace($this->shopConfig->getDescriptionEmptyLinePattern(), '', $newDesc) ?? $newDesc;
        $newDesc = preg_replace($this->shopConfig->getDescriptionWhitespaceCleanupPattern(), ' ', $newDesc) ?? $newDesc;
        $result = trim((string) $newDesc);

        return mb_substr($result, 0, $this->shopConfig->getDescriptionMaxLength());
    }

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

        if (is_array($manufacturer) && isset($manufacturer['name']) && is_string($manufacturer['name'])) {
            $name = trim($manufacturer['name']);

            return $name !== '' ? $name : null;
        }

        return null;
    }

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

        if (!is_array($calculatedPrice)) {
            return null;
        }

        $candidates = [
            $calculatedPrice['unitPrice'] ?? null,
            $calculatedPrice['totalPrice'] ?? null,
            $calculatedPrice['referencePrice'] ?? null,
            $calculatedPrice['listPrice'] ?? null,
            $calculatedPrice['regulationPrice'] ?? null,
        ];

        foreach ($candidates as $candidate) {
            if (!is_numeric($candidate)) {
                continue;
            }

            $value = (float) $candidate;

            if ($value > 0.0) {
                return number_format(
                        $value,
                        $this->shopConfig->getPriceDecimals(),
                        $this->shopConfig->getPriceDecimalSeparator(),
                        $this->shopConfig->getPriceThousandsSeparator()
                    ) . $this->shopConfig->getPriceSuffix();
            }
        }

        return null;
    }

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

        if (!is_array($seoUrls) || $seoUrls === []) {
            return null;
        }

        foreach ($seoUrls as $seoUrl) {
            if (!is_array($seoUrl)) {
                continue;
            }

            $path = $seoUrl['seoPathInfo'] ?? null;

            if (is_string($path) && trim($path) !== '') {
                return $this->shopConfig->buildRelativeSeoUrl($path);
            }
        }

        return null;
    }

    private function buildAbsoluteUrl(?string $relativeUrl): ?string
    {
        if ($relativeUrl === null || trim($relativeUrl) === '') {
            return null;
        }

        return rtrim($this->baseUrl, '/') . '/' . ltrim($relativeUrl, '/');
    }

    /**
     * @param array<string, mixed> $row
     * @return string[]
     */
    private function extractHighlights(array $row): array
    {
        $highlights = [];

        if (isset($row['available'])) {
            $highlights[] = (bool) $row['available']
                ? $this->shopConfig->getAvailableHighlightLabel()
                : $this->shopConfig->getUnavailableHighlightLabel();
        }

        if (isset($row['productNumber']) && is_string($row['productNumber']) && trim($row['productNumber']) !== '') {
            $highlights[] = $this->shopConfig->getProductNumberHighlightPrefix() . trim($row['productNumber']);
        }

        return array_values(array_unique($highlights));
    }

    /**
     * @param ShopProductResult[] $products
     * @return ShopProductResult[]
     */
    private function deduplicateProducts(array $products): array
    {
        $unique = [];
        $seen = [];

        foreach ($products as $product) {
            $key = mb_strtolower(trim(implode($this->shopConfig->getDeduplicationSeparator(), [
                $product->id,
                $product->productNumber ?? '',
                $product->name,
                $product->url ?? '',
            ])), 'UTF-8');

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

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

        return $unique;
    }
}