<?php

declare(strict_types=1);

namespace App\Intent;

use App\Config\CommerceIntentConfig;

final class CommerceIntentLite
{
    public const NONE = 'none';
    public const PRODUCT_SEARCH = 'product_search';
    public const ADVISORY_PRODUCT_SEARCH = 'advisory_product_search';

    public function __construct(
        private readonly CommerceIntentConfig $config
    ) {
    }

    /**
     * @return array{intent:string, score:int, signals:string[]}
     */
    public function detect(string $originalPrompt): array
    {
        $prompt = mb_strtolower(trim($originalPrompt));

        if ($prompt === '') {
            return $this->buildDetectionResult(
                intent: self::NONE,
                score: 0,
                signals: []
            );
        }

        if ($this->isSupportOrDiagnosticQuery($prompt) && !$this->hasExplicitCommerceIntent($prompt)) {
            return $this->buildDetectionResult(
                intent: self::NONE,
                score: 0,
                signals: [$this->config->getSupportOrDiagnosticSignalLabel()]
            );
        }

        $hasAdvisoryProductSelectionIntent = $this->hasAdvisoryProductSelectionIntent($prompt);

        if (
            $this->isTechnicalFactualKnowledgeQuery($prompt)
            && !$this->hasExplicitCommerceIntent($prompt)
            && !$hasAdvisoryProductSelectionIntent
        ) {
            return $this->buildDetectionResult(
                intent: self::NONE,
                score: 0,
                signals: ['technical_factual_knowledge_query']
            );
        }

        $score = 0;
        $signals = [];

        [$score, $signals] = $this->applyStrongSignals($prompt, $score, $signals);
        [$score, $signals] = $this->applySkuSignal($prompt, $score, $signals);
        [$score, $signals] = $this->applyModelLikeProductSignal($prompt, $score, $signals);
        [$score, $signals] = $this->applyPriceSignal($prompt, $score, $signals);
        [$score, $signals] = $this->applySizeSignal($prompt, $score, $signals);
        [$score, $signals] = $this->applySizeTokenSignal($prompt, $score, $signals);
        [$score, $signals] = $this->applyColorSignal($prompt, $score, $signals);
        [$score, $signals] = $this->applyAdvisoryProductSelectionSignal($prompt, $score, $signals);
        [$score, $signals] = $this->applyAdvisorySignals($prompt, $score, $signals);

        $signals = array_values(array_unique($signals));

        if ($score >= $this->config->getProductSearchMinScore()) {
            return $this->buildDetectionResult(
                intent: self::PRODUCT_SEARCH,
                score: $score,
                signals: $signals
            );
        }

        if ($score >= $this->config->getAdvisoryProductSearchMinScore()) {
            return $this->buildDetectionResult(
                intent: self::ADVISORY_PRODUCT_SEARCH,
                score: $score,
                signals: $signals
            );
        }

        return $this->buildDetectionResult(
            intent: self::NONE,
            score: $score,
            signals: $signals
        );
    }

    private function isSupportOrDiagnosticQuery(string $prompt): bool
    {
        return $this->matchesAnyPattern($prompt, $this->config->getSupportDiagnosticPatterns());
    }

    private function hasExplicitCommerceIntent(string $prompt): bool
    {
        return $this->matchesAnyPattern($prompt, $this->config->getExplicitCommerceIntentPatterns());
    }

    /**
     * Product advisory prompts can contain technical words such as "Messung".
     *
     * They must not be swallowed by the factual RAG guard when the user is
     * clearly asking for a suitable shop product or device class.
     */
    private function hasAdvisoryProductSelectionIntent(string $prompt): bool
    {
        if ($this->matchesAnyPattern($prompt, $this->config->getAdvisoryProductSelectionPatterns())) {
            return true;
        }

        if (!$this->containsConfiguredAdvisorySignal($prompt)) {
            return false;
        }

        if (preg_match($this->config->getModelLikeProductPattern(), $prompt) === 1) {
            return true;
        }

        foreach ($this->config->getStrongSignalsList() as $signal) {
            $signal = mb_strtolower(trim($signal));

            if ($signal === '' || $this->isNonProductCommerceSignal($signal)) {
                continue;
            }

            if (str_contains($prompt, $signal)) {
                return true;
            }
        }

        return false;
    }

    private function containsConfiguredAdvisorySignal(string $prompt): bool
    {
        foreach ($this->config->getAdvisorySignals() as $signal) {
            if ($this->containsConfiguredPhraseOrToken($prompt, $signal)) {
                return true;
            }
        }

        return false;
    }

    private function containsConfiguredPhraseOrToken(string $prompt, string $signal): bool
    {
        $signal = mb_strtolower(trim($signal));

        if ($signal === '') {
            return false;
        }

        if (preg_match('/\s/u', $signal) === 1) {
            return str_contains($prompt, $signal);
        }

        $pattern = '/(?<![\p{L}\p{N}])' . preg_quote($signal, '/') . '(?![\p{L}\p{N}])/u';

        return preg_match($pattern, $prompt) === 1;
    }

    private function isNonProductCommerceSignal(string $signal): bool
    {
        return in_array($signal, [
            'shop',
            'alle',
            'kunde',
            'online',
            'kaufen',
            'kostet',
            'suche',
            'such',
            'finde',
            'finden',
        ], true);
    }

    /**
     * Detects factual technical knowledge questions that must stay in RAG retrieval.
     *
     * Product names such as Testomat can look like commerce queries, but questions
     * about limits, measuring ranges, thresholds, resolution or monitoring values
     * must not trigger shop search. Shop search may still run when the user uses
     * explicit commerce wording such as price, buy, order, shop, article or SKU.
     */
    private function isTechnicalFactualKnowledgeQuery(string $prompt): bool
    {
        $hasQuestionMarker = $this->matchesAnyPattern($prompt, [
            '/\bwas\s+ist\b/u',
            '/\bwelche?r?s?\b/u',
            '/\bwie\s+(hoch|niedrig|klein|gross|groß)\b/u',
            '/\bniedrigste[rsn]?\b/u',
            '/\bkleinste[rsn]?\b/u',
            '/\bhöchste[rsn]?\b/u',
            '/\bhoechste[rsn]?\b/u',
        ]);

        if (!$hasQuestionMarker) {
            return false;
        }

        return $this->matchesAnyPattern($prompt, [
            '/\bgrenzwert(?:e|en|es)?\b/u',
            '/\bmessbereich(?:e|en|s)?\b/u',
            '/\bwasserhärte\b/u',
            '/\bwasserhaerte\b/u',
            '/\bresthärte\b/u',
            '/\bresthaerte\b/u',
            '/\bgesamthärte\b/u',
            '/\bgesamthaerte\b/u',
            '/\bauflösung\b/u',
            '/\baufloesung\b/u',
            '/\bindikator(?:en|s)?\b/u',
            '/\btestomat(?:en|s)?\b/u',
            '/\büberwach(?:t|en|ung)\b/u',
            '/\bueberwach(?:t|en|ung)\b/u',
            '/\bmess(?:en|ung|bar|wert)\b/u',
        ]);
    }

    /**
     * @param string[] $patterns
     */
    private function matchesAnyPattern(string $prompt, array $patterns): bool
    {
        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $prompt) === 1) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param string[] $signals
     * @return array{0:int,1:string[]}
     */
    private function applyStrongSignals(string $prompt, int $score, array $signals): array
    {
        foreach ($this->config->getStrongSignalsList() as $signal) {
            if (str_contains($prompt, mb_strtolower($signal))) {
                $score += $this->config->getStrongSignalScore();
                $signals[] = $signal;
            }
        }

        return [$score, $signals];
    }

    /**
     * @param string[] $signals
     * @return array{0:int,1:string[]}
     */
    private function applySkuSignal(string $prompt, int $score, array $signals): array
    {
        if (preg_match($this->config->getSkuLikePattern(), $prompt) === 1) {
            $score += $this->config->getSkuSignalScore();
            $signals[] = $this->config->getSkuSignalLabel();
        }

        return [$score, $signals];
    }

    /**
     * @param string[] $signals
     * @return array{0:int,1:string[]}
     */
    private function applyPriceSignal(string $prompt, int $score, array $signals): array
    {
        if (preg_match($this->config->getPriceValuePattern(), $prompt) === 1) {
            $score += $this->config->getPriceSignalScore();
            $signals[] = $this->config->getPriceSignalLabel();
        }

        return [$score, $signals];
    }

    private function applyModelLikeProductSignal(string $prompt, int $score, array $signals): array
    {
        if (preg_match($this->config->getModelLikeProductPattern(), $prompt) === 1) {
            $score += $this->config->getModelLikeProductSignalScore();
            $signals[] = $this->config->getModelLikeProductSignalLabel();
        }

        return [$score, $signals];
    }

    /**
     * @param string[] $signals
     * @return array{0:int,1:string[]}
     */
    private function applySizeSignal(string $prompt, int $score, array $signals): array
    {
        if (preg_match($this->config->getSizeValuePattern(), $prompt) === 1) {
            $score += $this->config->getSizeSignalScore();
            $signals[] = $this->config->getSizeSignalLabel();
        }

        return [$score, $signals];
    }

    /**
     * @param string[] $signals
     * @return array{0:int,1:string[]}
     */
    private function applySizeTokenSignal(string $prompt, int $score, array $signals): array
    {
        if (preg_match($this->config->getSizeTokenValuePattern(), $prompt) === 1) {
            $score += $this->config->getSizeTokenSignalScore();
            $signals[] = $this->config->getSizeTokenSignalLabel();
        }

        return [$score, $signals];
    }

    /**
     * @param string[] $signals
     * @return array{0:int,1:string[]}
     */
    private function applyColorSignal(string $prompt, int $score, array $signals): array
    {
        if (preg_match($this->config->getColorValuePattern(), $prompt) === 1) {
            $score += $this->config->getColorSignalScore();
            $signals[] = $this->config->getColorSignalLabel();
        }

        return [$score, $signals];
    }

    /**
     * @param string[] $signals
     * @return array{0:int,1:string[]}
     */
    private function applyAdvisoryProductSelectionSignal(string $prompt, int $score, array $signals): array
    {
        if (!$this->matchesAnyPattern($prompt, $this->config->getAdvisoryProductSelectionPatterns())) {
            return [$score, $signals];
        }

        $score += $this->config->getAdvisoryProductSelectionSignalScore();
        $signals[] = $this->config->getAdvisoryProductSelectionSignalLabel();

        return [$score, $signals];
    }

    /**
     * @param string[] $signals
     * @return array{0:int,1:string[]}
     */
    private function applyAdvisorySignals(string $prompt, int $score, array $signals): array
    {
        foreach ($this->config->getAdvisorySignals() as $signal) {
            if ($this->containsConfiguredPhraseOrToken($prompt, $signal)) {
                $score += $this->config->getAdvisorySignalScore();
                $signals[] = $this->config->getAdvisorySignalPrefix() . $signal;
            }
        }

        return [$score, $signals];
    }

    /**
     * @param string[] $signals
     * @return array{intent:string, score:int, signals:string[]}
     */
    private function buildDetectionResult(string $intent, int $score, array $signals): array
    {
        return [
            'intent' => $intent,
            'score' => $score,
            'signals' => $signals,
        ];
    }
}