<?php declare(strict_types=1);

namespace App\Commerce;

use App\Commerce\Dto\CommerceSearchQuery;
use App\Commerce\Dto\ShopProductResult;
use App\Config\ShopServiceConfig;
use App\Shopware\ShopwareCriteriaBuilder;
use App\Shopware\StoreApiClient;
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 readonly class ShopSearchService
{
    public function __construct(
        private CommerceQueryParser     $queryParser,
        private ShopwareCriteriaBuilder $criteriaBuilder,
        private StoreApiClient          $storeApiClient,
        private LoggerInterface         $logger,
        private bool                    $enabled = true,
        private int                     $maxResults = 25,
        private string                  $baseUrl
    )
    {
    }

    /** * @return ShopProductResult[] */
    public function search(string $originalPrompt, string $commerceIntent, string $commerceHistoryContext = ''): array
    {
        if (!$this->enabled) {
            $this->logger->info('Shop search skipped because commerce search is disabled', ['commerceIntent' => $commerceIntent,]);
            return [];
        }
        $primaryQuery = $this->queryParser->parse($originalPrompt, $commerceIntent, $commerceHistoryContext);
        $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, 'hasCommerceHistoryContext' => $commerceHistoryContext !== '', 'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext), 'criteriaLimit' => $this->maxResults,]);
        $rankedProducts = $this->executeSearch($primaryQuery, $commerceIntent, $originalPrompt, true);
        if ($rankedProducts === [] && $commerceHistoryContext !== '') {
            $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,]);
            $rankedProducts = $this->executeSearch($fallbackQuery, $commerceIntent, $originalPrompt, false);
        }
        $this->logger->info('Shop search finished', ['commerceIntent' => $commerceIntent, 'originalPrompt' => $originalPrompt, 'rankedProductsCount' => count($rankedProducts), 'topProducts' => array_map(static fn(ShopProductResult $product): array => ['name' => $product->name, 'productNumber' => $product->productNumber, 'manufacturer' => $product->manufacturer, 'available' => $product->available,], array_slice($rankedProducts, 0, 3)),]);
        return $rankedProducts;
    }

    /** * @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);
        } catch (ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface $e) {
            $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, 'criteria' => $criteria, 'exceptionClass' => $e::class, 'exceptionMessage' => $e->getMessage(),]);
            return [];
        }
        $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, 'rawElementsCount' => is_array($response['elements'] ?? null) ? count($response['elements']) : 0, 'mappedProductsCount' => count($mappedProducts), 'rankedProductsCount' => count($rankedProducts), 'topProducts' => array_map(static fn(ShopProductResult $product): array => ['name' => $product->name, 'productNumber' => $product->productNumber, 'manufacturer' => $product->manufacturer, 'available' => $product->available,], array_slice($rankedProducts, 0, 3)),]);
        return $rankedProducts;
    }

    /** * @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: trim((string)($row['translated']['name'] ?? '')), 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'] ?? 'no-image', 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 += 140;
        }
        if ($normalizedProductName !== '' && $this->containsWholePhrase($normalizedQuery, $normalizedProductName)) {
            $score += 80;
        }
        if ($normalizedBrand !== '') {
            if ($normalizedManufacturer !== '' && $normalizedManufacturer === $normalizedBrand) {
                $score += 40;
            } elseif ($this->containsWholePhrase($normalizedProductName, $normalizedBrand)) {
                $score += 20;
            }
        }
        $score += $this->countOverlap($queryTokens, $productNameTokens) * 6;
        $score += $this->countOverlap($queryTokens, $productNumberTokens) * 10;
        $score += $this->countOverlap($queryTokens, $productCorpusTokens) * 2;
        $score += $this->countOverlap($queryNumberTokens, $productNameNumberTokens) * 18;
        $score += $this->countOverlap($queryNumberTokens, $productNumberNumberTokens) * 28;
        $score += $this->countOverlap($queryNumberTokens, $productCorpusNumberTokens) * 8;
        foreach ($normalizedSizes as $normalizedSize) {
            if ($normalizedSize === '') {
                continue;
            }
            if ($this->containsWholePhrase($normalizedProductName, $normalizedSize) || $this->containsWholePhrase($normalizedProductNumber, $normalizedSize) || $this->containsWholePhrase($normalizedProductCorpus, $normalizedSize)) {
                $score += 12;
            }
        }
        $score += $this->scoreProductTypeMatch($product, $normalizedQuery);
        if ($product->available === true) {
            $score += 1;
        }
        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 += 60;
            }
            if ($isAccessoryLikeProduct) {
                $score -= 120;
            }
        }
        if ($isAccessoryQuery) {
            if ($isAccessoryLikeProduct) {
                $score += 30;
            }
            if ($isDeviceLikeProduct) {
                $score += 10;
            }
        }
        return $score;
    }

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

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

    private function isAccessoryLikeProduct(ShopProductResult $product): bool
    {
        $corpus = $this->buildNormalizedProductCorpus($product);
        foreach (ShopServiceConfig::ACCESSORY_PRODUCT_KEYWORDS as $keyword) {
            if (str_contains($corpus, $this->normalizeForMatching($keyword))) {
                return true;
            }
        }
        return false;
    }

    private function isDeviceLikeProduct(ShopProductResult $product): bool
    {
        $corpus = $this->buildNormalizedProductCorpus($product);
        foreach (ShopServiceConfig::DEVICE_PRODUCT_KEYWORDS as $keyword) {
            if (str_contains($corpus, $this->normalizeForMatching($keyword))) {
                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, static fn(string $token): bool => preg_match('/\d/u', $token) === 1));
    }

    private function normalizeForMatching(string $value): string
    {
        $value = mb_strtolower(trim($value));
        $value = preg_replace('/[^\p{L}\p{N}]+/u', ' ', $value) ?? $value;
        $value = preg_replace('/\s+/u', ' ', $value) ?? $value;
        return trim($value);
    }

    /** * @return string[] */
    private function tokenize(string $value): array
    {
        if ($value === '') {
            return [];
        }
        return preg_split('/[^\p{L}\p{N}]+/u', $value, -1, PREG_SPLIT_NO_EMPTY) ?: [];
    }

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

    private function getRelevantCustomFields(array $customField): string
    {
        $result = ($customField['migration_Backup_product_attr1'] ?? '') . ': ' . ($customField['migration_Backup_product_attr2'] ?? '');
        $result .= ' | Einsatzgebiete: ' . ($customField['migration_Backup_product_attr4'] ?? '');
        $result .= ' | Sprachen: ' . ($customField['migration_Backup_product_attr5'] ?? '');
        return trim($result);
    }

    private function cleanUpDescription(array $description): string
    {
        if (isset($description['translated']['description'])) {
            $newDesc = strip_tags((string)($description['translated']['description']));
            $newDesc = html_entity_decode($newDesc);
            $newDesc = preg_replace('/^[ \t]*\R/m', '', $newDesc);
            $newDesc = preg_replace('/[ \t]{2,}/', ' ', $newDesc);
            $result = trim((string)$newDesc);
            return mb_substr($result, 0, 1500);
        }
        return '';
    }

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

    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, 2, ',', '.') . ' €';
            }
        }
        return null;
    }

    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 '/' . ltrim($path, '/');
            }
        }
        return null;
    }

    private function buildAbsoluteUrl(?string $relativeUrl): ?string
    {
        if ($relativeUrl === null || trim($relativeUrl) === '') {
            return null;
        }
        return rtrim($this->baseUrl, '/') . '/' . ltrim($relativeUrl, '/');
    }

    /** * @return string[] */
    private function extractHighlights(array $row): array
    {
        $highlights = [];
        if (isset($row['available'])) {
            $highlights[] = (bool)$row['available'] ? 'Verfügbar' : 'Nicht verfügbar';
        }
        if (isset($row['productNumber']) && is_string($row['productNumber']) && trim($row['productNumber']) !== '') {
            $highlights[] = 'Produktnummer: ' . 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('|', [$product->id, $product->productNumber ?? '', $product->name, $product->url ?? '',])));
            if (isset($seen[$key])) {
                continue;
            }
            $seen[$key] = true;
            $unique[] = $product;
        }
        return $unique;
    }
}