<?php

namespace App\Services\Bot;

use App\Models\Organization;
use Illuminate\Support\Facades\DB;

class MatchIncomingMessage
{
    private array $stopwords = [
        'a', 'an', 'the', 'is', 'am', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'shall', 'should', 'can', 'could', 'may', 'might', 'must', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'up', 'about', 'into', 'over', 'after', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them', 'my', 'your', 'his', 'our', 'their', 'this', 'that', 'these', 'those', 'then', 'than',
        'ami', 'amar', 'amake', 'apni', 'apnar', 'apnake', 'ki', 'je', 'er', 'theke', 'jonno', 'to', 'ta', 'ti', 'gulo', 'gula', 'ra', 'der', 'ke', 're', 'hoite', 'theka', 'e', 'te', 'y', 'tei', 'o', 'ba', 'ebong', 'ar', 'hoy', 'holo', 'tai', 'naki', 'korte', 'kore', 'kori', 'koro', 'achhe', 'ase', 'asei', 'nai',
        'আমি', 'আমার', 'আমাকে', 'আপনি', 'আপনার', 'আপনাকে', 'কি', 'যে', 'এর', 'থেকে', 'জন্য', 'তো', 'তা', 'টি', 'গুলো', 'গুলা', 'রা', 'দের', 'কে', 'রে', 'হইতে', 'তে', 'ও', 'বা', 'এবং', 'আর', 'হয়', 'হয়', 'হল', 'হলো', 'তাই', 'নাকি', 'করতে', 'করে', 'করি', 'করো', 'আছে', 'নাই',
        'bhai', 'vai', 'vaiya', 'bhaiya', 'sir', 'madam', 'apu', 'hello', 'hi', 'hey', 'salam', 'assalamualaikum', 'nomoskar', 'adab', 'plz', 'please', 'pls', 'doya'
    ];

    private array $synonyms = [
        'dam' => 'price',
        'daam' => 'price',
        'mullo' => 'price',
        'mulla' => 'price',
        'tk' => 'price',
        'taka' => 'price',
        'cost' => 'price',
        'rate' => 'price',
        
        'deliveri' => 'delivery',
        'delivari' => 'delivery',
        'dalivery' => 'delivery',
        
        'charz' => 'charge',
        'crs' => 'charge',
        
        'ordar' => 'order',
        'buy' => 'purchase',
        'kena' => 'purchase',
        'kinbo' => 'purchase',
        'kine' => 'purchase',
        'nib' => 'purchase',
        'nibo' => 'purchase',
        
        'thikana' => 'address',
        'location' => 'address',
        'shop' => 'store',
        'dokan' => 'store',
        'showroom' => 'store',
        
        'somoy' => 'time',
        'kobe' => 'time',
        'kakhon' => 'time',
        'kokhon' => 'time',
    ];

    public function handle(Organization $organization, string $message, string $locale = 'bn'): ?BotMatch
    {
        $candidate = $this->bestCandidate($organization, $message, $locale);

        if ($candidate === null || $candidate->confidence < (float) $candidate->intent->confidence_threshold) {
            return null;
        }

        return $candidate;
    }

    public function bestCandidate(Organization $organization, string $message, string $locale = 'bn'): ?BotMatch
    {
        $typos = $organization->typoCorrections()->pluck('corrected_word', 'input_word')->toArray();

        $normalizedMessage = $this->normalize($message, $typos);

        if ($normalizedMessage === '') {
            return null;
        }
        
        $messageTokens = $this->tokens($normalizedMessage);

        // Fetch intents with response templates eager loaded
        $intents = $organization->intents()
            ->where('status', 'active')
            ->with([
                'responseTemplates' => fn ($query) => $query
                    ->where('status', 'approved')
                    ->whereIn('locale', [$locale, 'en'])
                    ->orderBy('priority'),
            ])
            ->get()
            ->keyBy('id');

        // Fetch phrases as raw objects to bypass Eloquent hydration overhead for 60k records
        $phrases = DB::table('intent_phrases')
            ->where('organization_id', $organization->id)
            ->where('status', 'approved')
            ->get(['intent_id', 'phrase']);

        $intentScores = [];
        $intentPhrases = [];

        foreach ($phrases as $phraseRow) {
            if (!isset($intents[$phraseRow->intent_id])) {
                continue;
            }

            $normalizedPhrase = $this->normalize($phraseRow->phrase, $typos);
            $confidence = $this->score($normalizedMessage, $messageTokens, $normalizedPhrase);

            if ($confidence <= 0.0) {
                continue;
            }

            $existingScore = $intentScores[$phraseRow->intent_id] ?? 0.0;
            if ($confidence > $existingScore) {
                $intentScores[$phraseRow->intent_id] = $confidence;
                $intentPhrases[$phraseRow->intent_id] = $phraseRow->phrase;
            }
        }

        if (empty($intentScores)) {
            return null;
        }

        arsort($intentScores);
        $topIntentId = array_key_first($intentScores);
        $bestConfidence = $intentScores[$topIntentId];
        
        $bestIntent = $intents[$topIntentId];
        $bestPhrase = $intentPhrases[$topIntentId];

        if ($bestIntent === null || $bestIntent->responseTemplates->isEmpty()) {
            return null;
        }

        $localeTemplates = $bestIntent->responseTemplates->where('locale', $locale);
        $responseTemplate = $localeTemplates->isNotEmpty() 
            ? $localeTemplates->random() 
            : $bestIntent->responseTemplates->random();

        return new BotMatch($bestIntent, $responseTemplate, $bestConfidence, $bestPhrase);
    }

    /**
     * @return BotMatch[]
     */
    public function topCandidates(Organization $organization, string $message, string $locale = 'bn', int $limit = 3): array
    {
        $typos = $organization->typoCorrections()->pluck('corrected_word', 'input_word')->toArray();

        $normalizedMessage = $this->normalize($message, $typos);

        if ($normalizedMessage === '') {
            return [];
        }
        
        $messageTokens = $this->tokens($normalizedMessage);

        $intents = $organization->intents()
            ->where('status', 'active')
            ->with([
                'responseTemplates' => fn ($query) => $query
                    ->where('status', 'approved')
                    ->whereIn('locale', [$locale, 'en'])
                    ->orderBy('priority'),
            ])
            ->get()
            ->keyBy('id');

        $phrases = DB::table('intent_phrases')
            ->where('organization_id', $organization->id)
            ->where('status', 'approved')
            ->get(['intent_id', 'phrase']);

        $intentScores = [];
        $intentPhrases = [];

        foreach ($phrases as $phraseRow) {
            if (!isset($intents[$phraseRow->intent_id])) {
                continue;
            }

            $normalizedPhrase = $this->normalize($phraseRow->phrase, $typos);
            $confidence = $this->score($normalizedMessage, $messageTokens, $normalizedPhrase);

            if ($confidence <= 0.0) {
                continue;
            }

            $existingScore = $intentScores[$phraseRow->intent_id] ?? 0.0;
            if ($confidence > $existingScore) {
                $intentScores[$phraseRow->intent_id] = $confidence;
                $intentPhrases[$phraseRow->intent_id] = $phraseRow->phrase;
            }
        }

        if (empty($intentScores)) {
            return [];
        }

        arsort($intentScores);
        
        $topMatches = [];
        foreach (array_slice($intentScores, 0, $limit, true) as $intentId => $confidence) {
            $bestIntent = $intents[$intentId];
            $bestPhrase = $intentPhrases[$intentId];

            if ($bestIntent === null || $bestIntent->responseTemplates->isEmpty()) {
                continue;
            }

            $localeTemplates = $bestIntent->responseTemplates->where('locale', $locale);
            $responseTemplate = $localeTemplates->isNotEmpty() 
                ? $localeTemplates->random() 
                : $bestIntent->responseTemplates->random();

            $topMatches[] = new BotMatch($bestIntent, $responseTemplate, $confidence, $bestPhrase);
        }

        return $topMatches;
    }

    private function normalize(string $value, array $typos = []): string
    {
        $value = mb_strtolower($value);
        $value = preg_replace('/[^\pL\pN\s]+/u', ' ', $value);
        $value = trim(preg_replace('/\s+/', ' ', $value));

        if (empty($typos) && empty($this->synonyms)) {
            return $value;
        }

        $tokens = explode(' ', $value);
        foreach ($tokens as $i => $token) {
            if (isset($typos[$token])) {
                $token = $typos[$token];
            }
            if (isset($this->synonyms[$token])) {
                $token = $this->synonyms[$token];
            }
            $tokens[$i] = $token;
        }

        return implode(' ', $tokens);
    }

    private function score(string $message, array $messageTokens, string $phrase): float
    {
        if ($message === '' || $phrase === '') {
            return 0.0;
        }

        if ($message === $phrase) {
            return 1.0;
        }

        $phraseTokens = $this->tokens($phrase);

        if (empty($messageTokens) || empty($phraseTokens)) {
            return 0.0;
        }

        $matchingTokens = count(array_intersect($phraseTokens, $messageTokens));
        $phraseTokensCount = count($phraseTokens);
        $messageTokensCount = count($messageTokens);
        
        // Sørensen–Dice coefficient
        $tokenScore = (2 * $matchingTokens) / ($phraseTokensCount + $messageTokensCount);

        // Subset Match Boost: If ALL the tokens in the database phrase are found in the user's message
        if ($matchingTokens === $phraseTokensCount && $phraseTokensCount > 0) {
            $extraTokens = $messageTokensCount - $phraseTokensCount;
            if ($phraseTokensCount === 1) {
                // For a single word phrase, don't over-boost if the message is too long
                if ($extraTokens <= 2) {
                    $tokenScore = max($tokenScore, 0.8);
                }
            } else {
                // For multi-word phrase, we can be more confident
                if ($extraTokens <= 4) {
                    $tokenScore = max($tokenScore, 0.9);
                } else {
                    $tokenScore = max($tokenScore, 0.75);
                }
            }
        }

        if ($tokenScore >= 0.5) {
            return $tokenScore;
        }

        $fuzzyMatches = 0;
        foreach ($phraseTokens as $pToken) {
            $bestDistance = 999;
            foreach ($messageTokens as $mToken) {
                $dist = levenshtein($pToken, $mToken);
                if ($dist < $bestDistance) {
                    $bestDistance = $dist;
                }
            }

            $maxDistance = max(1, floor(strlen($pToken) / 4));
            if ($bestDistance <= $maxDistance) {
                $fuzzyMatches++;
            }
        }

        // Use Dice for fuzzy as well
        $fuzzyScore = (2 * $fuzzyMatches) / ($phraseTokensCount + $messageTokensCount);

        return min(0.7, $fuzzyScore);
    }

    /**
     * @return array<int, string>
     */
    private function tokens(string $value): array
    {
        $words = array_filter(explode(' ', $value), fn (string $token) => mb_strlen($token) > 1);
        
        $filteredWords = array_diff($words, $this->stopwords);
        
        if (empty($filteredWords) && !empty($words)) {
            $filteredWords = $words;
        }

        return array_unique(array_values($filteredWords));
    }

}
