<?php

namespace App\Services\AiChat;

use App\Models\Admin;
use App\Models\AiCorrection;
use App\Models\AiProductIndex;
use App\Models\BusinessSetting;
use App\Models\CashBack;
use App\Models\ChatFaq;
use App\Models\ChatLog;
use App\Models\ChatMessage;
use App\Models\ChatOffer;
use App\Models\Conversation;
use App\Models\Coupon;
use App\Models\Item;
use App\Models\KnowledgeBaseEntry;
use App\Models\Message;
use App\Models\Order;
use App\Models\SocialMedia;
use App\Models\User;
use App\Models\UserInfo;
use App\Models\WalletBonus;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Http\UploadedFile;
use OpenAI\Laravel\Facades\OpenAI;

class AiChatService
{
    private const DEFAULT_PRIORITY_ORDER = [
        'correction',
        'knowledge',
        'faq',
        'offer',
        'product',
        'policy',
    ];

    // Cache TTLs (seconds)
    private const TTL_BRANDS      = 3600;  // 1 hour  — brand names rarely change
    private const TTL_CATEGORIES  = 3600;  // 1 hour  — category names rarely change
    private const TTL_PRODUCT     = 1800;  // 30 min  — per-product card cache
    private const TTL_SETTINGS    = 1800;  // 30 min  — about/contact/policy text
    private const MIN_PRODUCT_SCORE = 8;
    private const MIN_PRODUCT_MATCH_PERCENT = 0;

    private const STOP_WORDS = [
        // Bengali common/query words
        'দাম', 'কত', 'আছে', 'কিনতে', 'পাওয়া', 'যাবে', 'চাই', 'দিন', 'বলুন',
        'জানতে', 'এর', 'টা', 'টি', 'কি', 'আমি', 'আপনি', 'সেটা', 'এটা',
        'ভালো', 'দেখুন', 'বিস্তারিত', 'জানান', 'করুন', 'হবে', 'কোনো',
        'নাই', 'নেই', 'পণ্য', 'প্রোডাক্ট', 'জিনিস', 'মূল্য', 'খরচ',
        'পাঠান', 'দেখান', 'কোথায়', 'কখন', 'কিভাবে', 'পাবো', 'পাব',
        'প্রাইস', 'দরকার', 'লাগবে', 'করো', 'করেন', 'করে', 'অর্ডার',
        'দিতে', 'তোমার', 'এখানে', 'আগে', 'আমাকে', 'বলো', 'বল',
        'যায়', 'যায়', 'পাওয়া', 'পূর্ণ', 'পন্য', 'যে', 'তোমাকে',
        'করতে', 'পারো', 'খুঁজতে', 'খুঁজে', 'করা', 'দেখতে',
        // English common/query words
        'the', 'and', 'for', 'are', 'this', 'that', 'with', 'have',
        'what', 'how', 'much', 'want', 'need', 'get', 'can', 'please',
        'product', 'products', 'item', 'items', 'buy', 'purchase', 'price',
        'prices', 'pricing', 'cost', 'rate', 'available', 'stock', 'show',
        'give', 'tell', 'know', 'about', 'share', 'provide', 'send',
        'all', 'any', 'some', 'catalog', 'catalogue',
        // Banglish common/query words
        'ki', 'kiki', 'ache', 'ase', 'ace', 'asee', 'asey', 'acche', 'ase?', 'ache?',
        'dam', 'koto', 'taka',
        'dao', 'den', 'din', 'details', 'detail', 'info', 'information',
        'description', 'describe', 'spec', 'specs', 'feature', 'features',
        'ea', 'er', 'ar', 'ra', 'karo', 'koren', 'karon', 'cilck', 'click',
        'gula', 'gulo', 'guli', 'sob', 'shob', 'kon', 'konta', 'konti',
        'dekhai', 'dekhao', 'dekhan', 'dekhaben', 'dekte', 'dekhte',
    ];

    private const BANGLA_PRODUCT_TERMS = [
        'কিয়াম' => 'kiam',
        'কিয়াম' => 'kiam',
        'কিয়ামের' => 'kiam',
        'কিয়ামের' => 'kiam',
        'এসকেবি' => 'skb',
        'রাইস' => 'rice',
        'কুকার' => 'cooker',
        'কুকারের' => 'cooker',
        'প্রেসার' => 'pressure',
        'প্রেশার' => 'pressure',
        'ইনফ্রারেড' => 'infrared',
        'ইনডাকশন' => 'induction',
        'ডিশ' => 'dish',
        'ডিনার' => 'dinner',
        'সেট' => 'set',
        'গ্লাস' => 'glass',
        'পাইরেক্স' => 'pyrex',
        'প্লেট' => 'plate',
        'কেরামিক' => 'ceramic',
        'সিরামিক' => 'ceramic',
    ];

    public function settings(): array
    {
        return [
            'chat_enabled' => $this->getBooleanSetting('ai_chat_board_enabled', true),
            'auto_reply_enabled' => $this->getBooleanSetting('ai_chat_auto_reply_enabled', true),
            'offer_enabled' => $this->getBooleanSetting('ai_chat_offer_module_enabled', true),
            'faq_enabled' => $this->getBooleanSetting('ai_chat_faq_enabled', true),
            'knowledge_enabled' => $this->getBooleanSetting('ai_chat_knowledge_enabled', true),
            'correction_approval_enabled' => $this->getBooleanSetting('ai_chat_correction_approval_enabled', true),
            'sales_mode_enabled' => $this->getBooleanSetting('ai_chat_sales_mode_enabled', true),
            'product_recommendation_enabled' => $this->getBooleanSetting('ai_chat_product_recommendation_enabled', true),
            'image_search_enabled' => $this->getBooleanSetting('ai_chat_image_search_enabled', true),
            'verified_reply_only' => $this->getBooleanSetting('ai_chat_verified_reply_only', true),
            'reply_language' => $this->getStringSetting('ai_chat_reply_language', 'auto'),
            'custom_instruction' => $this->getStringSetting('ai_chat_custom_instruction', ''),
            'answer_priority_order' => $this->getPriorityOrder(),
            'fallback_message_bn' => $this->getStringSetting('ai_chat_fallback_message_bn', 'এই তথ্যটি আমি নিশ্চিতভাবে খুঁজে পাইনি, অনুগ্রহ করে product name/model দিন।'),
            'fallback_message_en'  => $this->getStringSetting('ai_chat_fallback_message_en', 'I could not verify that information. Please share the product name/model.'),
            // 0 = no filter; 1-100 = minimum match % to show product result
            'min_match_threshold'  => $this->getIntSetting('ai_chat_product_match_threshold', 0),
        ];
    }

    protected function understandUserMessage(string $question, array $settings, ?User $customer = null): array
    {
        $language = $this->detectLanguage($question);
        if ($language !== 'bn' && $this->looksLikeBanglish($question)) {
            $language = 'bn';
        }
        if (in_array(($settings['reply_language'] ?? 'auto'), ['bn', 'en'], true)) {
            $language = $settings['reply_language'];
        }

        $normalizedQuestion = $this->normalizeText($question);
        $queryType = $this->detectQueryType($question);
        $schemaContext = $this->detectSchemaIntent($question);
        if (!empty($schemaContext['query_type'])) {
            $queryType = $schemaContext['query_type'];
        }
        $productSearchMode = $this->detectProductSearchMode($question);
        $productAnswerIntent = $this->detectProductAnswerIntent($question, $queryType, $productSearchMode);

        if ($queryType === 'general'
            && ($this->looksLikeProductBrowseQuestion($question)
                || (in_array($productAnswerIntent, ['details', 'availability'], true) && count($this->extractProductTokens($question)) > 0))) {
            $queryType = 'product';
        }

        $productTokens = $this->extractProductTokens($question);
        $agentContext = $this->extractUserMessageContextWithAI($question, $language, [
            'query_type' => $queryType,
            'product_search_mode' => $productSearchMode,
            'product_answer_intent' => $productAnswerIntent,
            'data_domain' => $schemaContext['data_domain'] ?? null,
            'schema_table' => $schemaContext['schema_table'] ?? null,
        ]);

        if (in_array(($agentContext['language'] ?? null), ['bn', 'en'], true)) {
            $language = $agentContext['language'];
        }
        if (in_array(($settings['reply_language'] ?? 'auto'), ['bn', 'en'], true)) {
            $language = $settings['reply_language'];
        }

        $agentQueryType = $this->normalizeAgentQueryType($agentContext['query_type'] ?? null);
        $agentConfidence = (int) ($agentContext['confidence'] ?? 0);
        if ($agentQueryType
            && ($queryType === 'general'
                || $agentQueryType === 'human'
                || ($agentConfidence >= 55 && $agentQueryType !== 'general'))) {
            $queryType = ($queryType === 'price' && $agentQueryType === 'product')
                ? 'price'
                : $agentQueryType;
        }

        $dataDomain = $agentContext['data_domain'] ?? ($schemaContext['data_domain'] ?? null);
        $schemaTable = $agentContext['schema_table'] ?? ($schemaContext['schema_table'] ?? null);

        if (in_array(($agentContext['product_search_mode'] ?? null), ['exact', 'browse'], true)) {
            $productSearchMode = $agentContext['product_search_mode'];
        }

        if (in_array(($agentContext['product_answer_intent'] ?? null), ['summary', 'price', 'availability', 'details', 'browse'], true)) {
            $productAnswerIntent = $agentContext['product_answer_intent'];
        } else {
            $productAnswerIntent = $this->detectProductAnswerIntent($question, $queryType, $productSearchMode);
        }

        if ($queryType === 'price') {
            $productAnswerIntent = 'price';
        }

        $agentProductTerms = $this->sanitizeAgentProductTerms($agentContext['product_terms'] ?? []);
        $productSearchTerms = $this->prepareProductSearchTerms($agentProductTerms, $productTokens);
        $amounts = $this->extractMoneyAmounts($question, $agentContext);

        if ($queryType === 'general'
            && !empty($productSearchTerms)
            && in_array($productAnswerIntent, ['price', 'availability', 'details', 'browse'], true)) {
            $queryType = $productAnswerIntent === 'price' ? 'price' : 'product';
        }

        return [
            'question' => $question,
            'normalized_question' => $normalizedQuestion,
            'tokens' => $this->extractTokens($question),
            'language' => $language,
            'query_type' => $queryType,
            'product_search_mode' => $productSearchMode,
            'product_answer_intent' => $productAnswerIntent,
            'product_tokens' => $productTokens,
            'product_search_terms' => $productSearchTerms,
            'amounts' => $amounts,
            'data_domain' => $dataDomain,
            'schema_table' => $schemaTable,
            'detailed' => $this->wantsDetailedReply($question) || $productAnswerIntent === 'details',
            'preferred_source' => $this->preferredSourceForQueryType($queryType),
            'customer' => $customer,
            'settings' => $settings,
            'agent_context' => $agentContext,
        ];
    }

    protected function extractUserMessageContextWithAI(string $question, string $language, array $fallbackContext = []): array
    {
        $apiKey = config('openai.api_key');
        if (empty($apiKey)) {
            return [];
        }

        $schemaContext = $this->getAiChatSchemaContext();
        $cacheKey = 'ai_chat_context_v2_' . md5($language . '|' . $question . '|' . json_encode($fallbackContext) . '|' . json_encode($schemaContext));

        return Cache::remember($cacheKey, 300, function () use ($question, $language, $fallbackContext, $schemaContext) {
            try {
                $systemPrompt = implode("\n", [
                    'You are the context parser for Ammar e-commerce customer chat.',
                    'Understand the customer message before any database search. Do not answer the customer.',
                    'Use the provided available_schema to choose the best data domain/table.',
                    'Return JSON only with keys: language, query_type, data_domain, schema_table, product_search_mode, product_answer_intent, product_terms, amounts, confidence.',
                    'language must be bn for Bangla or Banglish, en for English.',
                    'query_type must be one of general, product, price, offer, discount, campaign, delivery, free_delivery, order_help, contact, business_info, social_media, policy, human, wallet_bonus, coupon, cashback, loyalty, wallet, keyword, knowledge, faq, correction.',
                    'data_domain must be one of product_catalog, wallet_bonus, coupon, cashback, loyalty, wallet, delivery, free_delivery, business_settings, social_media, ai_chat_board, policy, support, general.',
                    'schema_table must be the most relevant table name from available_schema, or null.',
                    'product_search_mode must be exact or browse.',
                    'product_answer_intent must be summary, price, availability, details, or browse.',
                    'product_terms must contain only searchable catalog words: product type, category, brand, model, size, color, material, or key attributes.',
                    'Remove filler/intent words such as price, dam, koto, ki, ache, ase, show, details, available, stock, please.',
                    'For wallet bonus, coupon, cashback, wallet, loyalty, contact, business info, social media, delivery, free delivery, keyword, knowledge, faq, correction, or policy questions, product_terms must be empty.',
                    'For messages like "100 tk bonus ki pabo", choose query_type wallet_bonus, data_domain wallet_bonus, schema_table wallet_bonuses, amounts [100].',
                    'For messages asking WhatsApp, phone, contact number, address, business location, or company info, choose contact or business_info with data_domain business_settings and schema_table business_settings.',
                    'For messages asking Facebook page, YouTube channel, Instagram, TikTok, Telegram, Messenger, or social links, choose social_media with data_domain social_media and schema_table social_media.',
                    'For free delivery questions, choose free_delivery with data_domain free_delivery and schema_table business_settings.',
                    'Translate common Bangla or Banglish product words to catalog English when useful, for example rice cooker, pressure cooker, dish set, glass, plate, ceramic.',
                    'If there is no product/category/brand/model, product_terms must be an empty array.',
                ]);

                $response = OpenAI::chat()->create([
                    'model' => 'gpt-4o-mini',
                    'max_tokens' => 180,
                    'temperature' => 0.1,
                    'messages' => [
                        ['role' => 'system', 'content' => $systemPrompt],
                        ['role' => 'user', 'content' => json_encode([
                            'message' => $question,
                            'fallback_context' => $fallbackContext,
                            'fallback_language' => $language,
                            'available_schema' => $schemaContext,
                        ], JSON_UNESCAPED_UNICODE)],
                    ],
                ]);

                $raw = trim((string) ($response->choices[0]->message->content ?? ''));
                if ($raw === '') {
                    return [];
                }

                $decoded = json_decode($raw, true);
                if (!is_array($decoded) && preg_match('/\{.*\}/su', $raw, $match)) {
                    $decoded = json_decode($match[0], true);
                }

                return is_array($decoded) ? $decoded : [];
            } catch (\Throwable $e) {
                Log::warning('OpenAI chat context parsing failed: ' . $e->getMessage());
                return [];
            }
        });
    }

    protected function normalizeAgentQueryType(mixed $queryType): ?string
    {
        $type = $this->normalizeText((string) $queryType);

        return match ($type) {
            'product', 'products', 'stock', 'availability' => 'product',
            'price', 'pricing', 'cost', 'rate' => 'price',
            'offer', 'offers', 'discount', 'campaign' => $type === 'discount' || $type === 'campaign' ? $type : 'offer',
            'wallet bonus', 'wallet_bonus', 'bonus', 'add fund bonus', 'fund bonus' => 'wallet_bonus',
            'coupon', 'coupons', 'coupon code', 'promo code', 'voucher' => 'coupon',
            'cashback', 'cash back', 'cash_back' => 'cashback',
            'loyalty', 'loyalty point', 'loyalty points', 'points' => 'loyalty',
            'wallet', 'wallet balance', 'wallet payment' => 'wallet',
            'free delivery', 'free_delivery', 'free shipping' => 'free_delivery',
            'order', 'order help', 'order_help', 'order status', 'track order', 'tracking' => 'order_help',
            'delivery', 'shipping', 'courier' => 'delivery',
            'contact', 'contact info', 'phone', 'phone number', 'whatsapp', 'what app', 'whats app', 'whatsapp number', 'support number' => 'contact',
            'business info', 'business_info', 'business address', 'address', 'company info', 'company address', 'location' => 'business_info',
            'social media', 'social_media', 'social link', 'social links', 'facebook', 'facebook page', 'youtube', 'youtube channel', 'instagram', 'tiktok', 'telegram', 'messenger' => 'social_media',
            'keyword', 'auto reply', 'auto_reply', 'keyword auto reply' => 'keyword',
            'knowledge', 'knowledge base', 'knowledge_base' => 'knowledge',
            'faq', 'faqs' => 'faq',
            'correction', 'override', 'correction override', 'correction_override' => 'correction',
            'policy', 'refund', 'return', 'privacy', 'terms' => 'policy',
            'human', 'support', 'agent', 'handoff', 'live chat' => 'human',
            'general' => 'general',
            default => null,
        };
    }

    protected function getAiChatSchemaContext(): array
    {
        return Cache::remember('ai_chat_schema_context_v2', 1800, function () {
            $domains = [
                'product_catalog' => [
                    'tables' => [
                        'items' => ['name', 'slug', 'description', 'price', 'discount', 'discount_type', 'stock', 'status', 'is_approved', 'category_id'],
                        'categories' => ['name', 'parent_id', 'status'],
                        'brands' => ['name', 'status'],
                        'ecommerce_item_details' => ['item_id', 'brand_id'],
                    ],
                    'use_for' => ['product search', 'price', 'stock', 'availability', 'product details', 'model lookup'],
                ],
                'wallet_bonus' => [
                    'tables' => [
                        'wallet_bonuses' => ['title', 'description', 'bonus_type', 'bonus_amount', 'minimum_add_amount', 'maximum_bonus_amount', 'start_date', 'end_date', 'status'],
                    ],
                    'use_for' => ['wallet bonus', 'add fund bonus', 'bonus amount eligibility', 'how to use wallet bonus'],
                ],
                'coupon' => [
                    'tables' => [
                        'coupons' => ['title', 'code', 'coupon_type', 'discount', 'discount_type', 'min_purchase', 'max_discount', 'start_date', 'expire_date', 'status'],
                    ],
                    'use_for' => ['coupon code', 'discount coupon', 'promo code', 'free delivery coupon'],
                ],
                'cashback' => [
                    'tables' => [
                        'cash_backs' => ['title', 'cashback_type', 'cashback_amount', 'min_purchase', 'max_discount', 'start_date', 'end_date', 'status'],
                    ],
                    'use_for' => ['cashback', 'cash back offer', 'cashback eligibility'],
                ],
                'loyalty' => [
                    'tables' => [
                        'loyalty_point_transactions' => ['user_id', 'transaction_type', 'credit', 'debit', 'balance'],
                    ],
                    'use_for' => ['loyalty point', 'reward point', 'points balance', 'points usage'],
                ],
                'wallet' => [
                    'tables' => [
                        'users' => ['wallet_balance', 'loyalty_point'],
                        'wallet_transactions' => ['user_id', 'transaction_type', 'credit', 'debit', 'admin_bonus', 'balance'],
                    ],
                    'use_for' => ['wallet balance', 'wallet payment', 'wallet transactions'],
                ],
                'delivery' => [
                    'tables' => [
                        'business_settings' => ['shipping_policy', 'home_delivery_status', 'parcel_per_km_shipping_charge', 'parcel_minimum_shipping_charge'],
                        'items' => ['item_delivery_charge', 'item_packaging_charge', 'free_delivery_badge_status'],
                    ],
                    'use_for' => ['delivery information', 'delivery time', 'delivery charge', 'shipping policy'],
                ],
                'free_delivery' => [
                    'tables' => [
                        'business_settings' => ['admin_free_delivery_status', 'admin_free_delivery_option', 'free_delivery_over'],
                        'items' => ['free_delivery_badge_status', 'item_delivery_charge'],
                    ],
                    'use_for' => ['free delivery information', 'free shipping eligibility', 'free delivery minimum order amount'],
                ],
                'business_settings' => [
                    'tables' => [
                        'business_settings' => ['business_name', 'phone', 'email_address', 'address', 'default_location', 'footer_text'],
                        'data_settings' => ['contact_us_title', 'contact_us_sub_title'],
                    ],
                    'use_for' => ['contact info', 'phone number', 'whatsapp number', 'business address', 'business information', 'feedback/contact form'],
                ],
                'social_media' => [
                    'tables' => [
                        'social_media' => ['name', 'link', 'status'],
                    ],
                    'use_for' => ['facebook page', 'youtube channel', 'instagram link', 'tiktok link', 'telegram link', 'messenger page', 'social media links'],
                ],
                'ai_chat_board' => [
                    'tables' => [
                        'knowledge_base' => ['title', 'source_category', 'question_patterns', 'tags', 'answer', 'answer_bn', 'answer_en', 'priority', 'is_active'],
                        'offers' => ['title', 'description', 'start_date', 'end_date', 'is_active', 'priority'],
                        'faq' => ['question', 'answer', 'answer_bn', 'answer_en', 'priority', 'is_active'],
                        'ai_corrections' => ['question', 'normalized_question', 'corrected_answer', 'is_active', 'is_approved', 'priority'],
                    ],
                    'use_for' => ['keyword auto-reply', 'knowledge base', 'offer management', 'faq management', 'correction override system'],
                ],
            ];

            return array_filter($domains, function (array $domain): bool {
                foreach (array_keys($domain['tables']) as $table) {
                    if (Schema::hasTable($table)) {
                        return true;
                    }
                }

                return false;
            });
        });
    }

    protected function detectSchemaIntent(string $question): array
    {
        $normalized = $this->normalizeText($question);
        $words = explode(' ', $normalized);

        $hasAny = function (array $needles) use ($normalized, $words): bool {
            foreach ($needles as $needle) {
                $needle = $this->normalizeText($needle);
                if ($needle !== '' && (str_contains($normalized, $needle) || in_array($needle, $words, true))) {
                    return true;
                }
            }

            return false;
        };

        if ($hasAny(['wallet bonus', 'add fund bonus', 'fund bonus', 'bonus', 'বোনাস'])) {
            return ['query_type' => 'wallet_bonus', 'data_domain' => 'wallet_bonus', 'schema_table' => 'wallet_bonuses'];
        }

        if ($hasAny(['free delivery', 'free shipping', 'free delivery info', 'free delivery information'])) {
            return ['query_type' => 'free_delivery', 'data_domain' => 'free_delivery', 'schema_table' => 'business_settings'];
        }

        if ($hasAny(['delivery info', 'delivery information', 'delivery charge', 'delivery time', 'shipping policy', 'shipping info', 'home delivery'])) {
            return ['query_type' => 'delivery', 'data_domain' => 'delivery', 'schema_table' => 'business_settings'];
        }

        if ($hasAny(['order status', 'track order', 'order tracking', 'where is my order', 'অর্ডার স্ট্যাটাস', 'অর্ডার কোথায়', 'অর্ডার ট্র্যাক'])) {
            return ['query_type' => 'order_help', 'data_domain' => 'support', 'schema_table' => 'orders'];
        }

        if ($hasAny(['whatsapp', 'whats app', 'what app', 'phone number', 'contact number', 'contact info', 'contact information', 'support number', 'phone', 'mobile number'])) {
            return ['query_type' => 'contact', 'data_domain' => 'business_settings', 'schema_table' => 'business_settings'];
        }

        if ($hasAny(['social media', 'social link', 'social links', 'facebook', 'facebook page', 'fb page', 'youtube', 'youtube channel', 'instagram', 'tiktok', 'telegram', 'messenger'])) {
            return ['query_type' => 'social_media', 'data_domain' => 'social_media', 'schema_table' => 'social_media'];
        }

        if ($hasAny(['business info', 'business information', 'business address', 'company address', 'address', 'location'])) {
            return ['query_type' => 'business_info', 'data_domain' => 'business_settings', 'schema_table' => 'business_settings'];
        }

        if ($hasAny(['keyword auto reply', 'auto reply', 'knowledge base', 'faq', 'correction override', 'override system'])) {
            return ['query_type' => 'knowledge', 'data_domain' => 'ai_chat_board', 'schema_table' => 'knowledge_base'];
        }

        if ($hasAny(['coupon', 'coupon code', 'promo code', 'voucher', 'কুপন'])) {
            return ['query_type' => 'coupon', 'data_domain' => 'coupon', 'schema_table' => 'coupons'];
        }

        if ($hasAny(['cashback', 'cash back', 'ক্যাশব্যাক'])) {
            return ['query_type' => 'cashback', 'data_domain' => 'cashback', 'schema_table' => 'cash_backs'];
        }

        if ($hasAny(['loyalty', 'loyalty point', 'reward point', 'points', 'পয়েন্ট', 'পয়েন্ট'])) {
            return ['query_type' => 'loyalty', 'data_domain' => 'loyalty', 'schema_table' => 'loyalty_point_transactions'];
        }

        if ($hasAny(['wallet balance', 'wallet payment', 'wallet', 'ওয়ালেট', 'ওয়ালেট'])) {
            return ['query_type' => 'wallet', 'data_domain' => 'wallet', 'schema_table' => 'wallet_transactions'];
        }

        return [];
    }

    protected function extractMoneyAmounts(string $question, array $agentContext = []): array
    {
        $amounts = [];

        foreach ((array) ($agentContext['amounts'] ?? []) as $amount) {
            if (is_numeric($amount)) {
                $amounts[] = (float) $amount;
            }
        }

        if (isset($agentContext['amount']) && is_numeric($agentContext['amount'])) {
            $amounts[] = (float) $agentContext['amount'];
        }

        if (preg_match_all('/(?<![a-z0-9])(\d+(?:\.\d+)?)(?:\s*(?:tk|taka|bdt|৳))?/iu', $question, $matches)) {
            foreach ($matches[1] as $match) {
                if (is_numeric($match)) {
                    $amounts[] = (float) $match;
                }
            }
        }

        return array_values(array_unique(array_filter($amounts, static fn ($amount) => $amount > 0)));
    }

    protected function sanitizeAgentProductTerms(mixed $terms): array
    {
        if (is_string($terms)) {
            $terms = preg_split('/[\s,]+/u', $terms) ?: [];
        }

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

        $clean = [];
        foreach ($terms as $term) {
            if (is_array($term)) {
                $term = implode(' ', array_filter(array_map('strval', $term)));
            }

            if (!is_scalar($term)) {
                continue;
            }

            $normalized = $this->mapProductSearchTerm($this->normalizeText((string) $term));
            if ($normalized !== '' && mb_strlen($normalized) >= 2 && !in_array($normalized, self::STOP_WORDS, true)) {
                $clean[] = $normalized;
            }
        }

        return array_slice(array_values(array_unique($clean)), 0, 12);
    }

    public function previewReply(string $question, ?User $customer = null): array
    {
        $settings = $this->settings();
        $context = $this->understandUserMessage($question, $settings, $customer);
        $language = $context['language'];
        $normalizedQuestion = $context['normalized_question'];
        $queryType = $context['query_type'];
        $productSearchMode = $context['product_search_mode'];

        if (!$settings['chat_enabled']) {
            return $this->fallbackReply($context, 'chat_disabled');
        }

        if ($queryType === 'human') {
            return $this->findHumanReply($context);
        }

        if ($queryType === 'order_help') {
            return $this->findOrderHelpReply($context);
        }

        $capabilityReply = $this->findCapabilityReply($context);
        if ($capabilityReply) {
            return $capabilityReply;
        }

        $schemaDataReply = $this->findSchemaDataReply($context);
        if ($schemaDataReply) {
            return $schemaDataReply + [
                'language' => $language,
                'normalized_question' => $normalizedQuestion,
                'query_type' => $queryType,
                'fallback_used' => false,
            ];
        }

        if (in_array($queryType, ['offer', 'discount', 'campaign'], true) && $settings['offer_enabled']) {
            $offerReply = $this->findOfferReply($context);
            if ($offerReply) {
                return $offerReply + [
                    'language' => $language,
                    'normalized_question' => $normalizedQuestion,
                    'query_type' => $queryType,
                    'fallback_used' => false,
                ];
            }
        }

        // 1. If query is price/product and has enough meaningful product-name tokens,
        //    run product search FIRST before keyword/KB entries intercept it.
        if (in_array($queryType, ['price', 'product'], true)) {
            $productTokens = !empty($context['product_search_terms'] ?? [])
                ? $context['product_search_terms']
                : ($context['product_tokens'] ?? $this->extractProductTokens($question));
            $meaningfulCount = count(array_filter(
                $productTokens,
                fn ($t) => !in_array($t, self::STOP_WORDS, true)
            ));
            $minMeaningful = ($productSearchMode === 'browse' || $this->hasCriticalProductToken($productTokens)) ? 1 : 2;
            if ($meaningfulCount >= $minMeaningful || ($queryType === 'product' && $productSearchMode === 'browse')) {
                $earlyProduct = $this->findProductReply($context);
                if ($earlyProduct) {
                    return $earlyProduct + [
                        'language'            => $language,
                        'normalized_question' => $normalizedQuestion,
                        'query_type'          => $queryType,
                        'fallback_used'        => false,
                    ];
                }
            }
        }

        // 2. Keyword exact/partial match (admin-defined keywords)
        $keywordReply = $this->findKeywordReply($context);
        if ($keywordReply) {
            return $keywordReply;
        }

        // 3. Greetings
        $greetingReply = $this->findGreetingReply($context);
        if ($greetingReply) {
            return $greetingReply;
        }

        foreach ($settings['answer_priority_order'] as $source) {
            $candidate = match ($source) {
                'correction' => $this->findCorrectionReply($context),
                'knowledge' => $settings['knowledge_enabled'] ? $this->findKnowledgeReply($context) : null,
                'faq' => $settings['faq_enabled'] ? $this->findFaqReply($context) : null,
                'offer' => $settings['offer_enabled'] ? $this->findOfferReply($context) : null,
                'product' => $this->findProductReply($context),
                'policy' => $this->findPolicyReply($context),
                default => null,
            };

            if ($candidate) {
                return $candidate + [
                    'language' => $language,
                    'normalized_question' => $normalizedQuestion,
                    'query_type' => $queryType,
                    'fallback_used' => false,
                ];
            }
        }

        $generalAiReply = $this->findGeneralAiReply($context);
        if ($generalAiReply) {
            return $generalAiReply;
        }

        return $this->fallbackReply($context, 'no_match');
    }

    public function handleApiMessage(string $message, ?User $customer = null, ?string $sessionId = null): array
    {
        $sessionId = $sessionId ?: (string) Str::uuid();
        $replyData = $this->previewReply($message, $customer);
        $replyText = $this->cleanReplyText($replyData['reply'] ?? '');

        $this->recordChatLog([
            'session_id' => $sessionId,
            'customer_id' => $customer?->id,
            'normalized_question' => $replyData['normalized_question'] ?? $this->normalizeText($message),
            'question' => $message,
            'reply' => $replyText,
            'language' => $replyData['language'] ?? $this->detectLanguage($message),
            'query_type' => $replyData['query_type'] ?? 'general',
            'source_type' => $replyData['source_type'] ?? 'fallback',
            'source_id' => $replyData['source_id'] ?? null,
            'matched_product_ids' => $replyData['matched_product_ids'] ?? [],
            'matched_keyword' => $replyData['matched_keyword'] ?? null,
            'confidence_score' => $replyData['confidence_score'] ?? 0,
            'fallback_used' => $replyData['fallback_used'] ?? false,
            'is_corrected' => ($replyData['source_type'] ?? null) === 'correction',
            'query_attempt' => $replyData['query_attempt'] ?? 0,
            'meta' => [
                'reply_short' => $replyData['reply_short'] ?? null,
                'reply_detailed' => $replyData['reply_detailed'] ?? null,
                'match_pct' => $replyData['match_pct'] ?? null,
                'product_answer_intent' => $replyData['product_answer_intent'] ?? null,
                'product_search_terms' => $replyData['product_search_terms'] ?? [],
                'agent_context' => $replyData['agent_context'] ?? null,
                'products' => $replyData['products'] ?? [],
            ],
        ]);

        return $replyData + ['session_id' => $sessionId];
    }

    public function handleApiImageSearch(UploadedFile|string $file, ?string $message = null, ?User $customer = null, ?string $sessionId = null): array
    {
        $sessionId = $sessionId ?: (string) Str::uuid();
        $replyData = $this->imageSearchReply($file, $message, $customer, $sessionId);
        $question = filled($message) ? (string) $message : '[image_product_search]';

        $this->recordChatLog([
            'session_id' => $sessionId,
            'customer_id' => $customer?->id,
            'normalized_question' => $this->normalizeText($question),
            'question' => $question,
            'reply' => $this->cleanReplyText($replyData['reply'] ?? ''),
            'language' => $replyData['language'] ?? $this->detectLanguage($question),
            'query_type' => 'image_product_search',
            'source_type' => $replyData['source_type'] ?? 'image_product_search',
            'source_id' => $replyData['source_id'] ?? null,
            'matched_product_ids' => $replyData['matched_product_ids'] ?? [],
            'matched_keyword' => $replyData['matched_keyword'] ?? null,
            'confidence_score' => $replyData['confidence_score'] ?? 0,
            'fallback_used' => $replyData['fallback_used'] ?? false,
            'is_corrected' => false,
            'query_attempt' => 0,
            'meta' => [
                'image_confidence' => $replyData['image_confidence'] ?? null,
                'products' => $replyData['products'] ?? [],
            ],
        ]);

        return $replyData + ['session_id' => $sessionId];
    }

    public function imageSearchReply(UploadedFile|string $file, ?string $message = null, ?User $customer = null, ?string $sessionId = null): array
    {
        $settings = $this->settings();
        $question = filled($message) ? (string) $message : 'image product search';
        $language = filled($message) ? $this->detectLanguage($question) : 'bn';
        if ($language !== 'bn' && $this->looksLikeBanglish($question)) {
            $language = 'bn';
        }
        if (in_array(($settings['reply_language'] ?? 'auto'), ['bn', 'en'], true)) {
            $language = $settings['reply_language'];
        }

        if (!$settings['chat_enabled'] || !($settings['image_search_enabled'] ?? true)) {
            $reply = $language === 'bn'
                ? 'Image search এখন বন্ধ আছে। অনুগ্রহ করে product name/model লিখুন, আমি ডাটাবেজ থেকে দাম ও স্টক দেখে জানাব।'
                : 'Image search is currently disabled. Please share the product name/model and I will check verified price and stock from the database.';

            return [
                'reply' => $reply,
                'reply_short' => $reply,
                'reply_detailed' => $reply,
                'language' => $language,
                'normalized_question' => $this->normalizeText($question),
                'query_type' => 'image_product_search',
                'source_type' => 'image_search_disabled',
                'source_id' => null,
                'matched_keyword' => null,
                'confidence_score' => 0,
                'image_confidence' => 0,
                'fallback_used' => true,
                'products' => [],
                'matched_product_ids' => [],
            ];
        }

        $matches = app(AiProductIndexService::class)->findSimilarByUpload($file, 3);
        if ($matches->isEmpty()) {
            $reply = $language === 'bn'
                ? 'এই ছবির সাথে নিশ্চিত product match খুঁজে পাইনি। অনুগ্রহ করে product name/model দিন, আমি ডাটাবেজ থেকে exact price/stock দেখে জানাব।'
                : 'I could not confidently match this image to a product. Please share the product name/model and I will verify exact price/stock from the database.';

            return [
                'reply' => $reply,
                'reply_short' => $reply,
                'reply_detailed' => $reply,
                'language' => $language,
                'normalized_question' => $this->normalizeText($question),
                'query_type' => 'image_product_search',
                'source_type' => 'image_product_not_found',
                'source_id' => null,
                'matched_keyword' => null,
                'confidence_score' => 0,
                'image_confidence' => 0,
                'fallback_used' => true,
                'products' => [],
                'matched_product_ids' => [],
            ];
        }

        $top = $matches->first();
        $topConfidence = (int) ($top->confidence_score ?? 0);
        $products = $matches->pluck('item')->filter()->values();
        $productCards = $products->map(function (Item $item) use ($matches) {
            $card = $this->buildProductCard($item);
            $card['image_confidence'] = (int) optional($matches->firstWhere('item_id', $item->id))->confidence_score;
            return $card;
        })->values()->all();

        if ($topConfidence >= 72 && $top?->item) {
            $reply = $this->formatProductTextReply(collect([$top->item]), $language, 'image_exact', 'exact', 'price');
            if ($matches->count() > 1) {
                $reply .= "\n" . ($language === 'bn'
                    ? 'ছবির ভিত্তিতে আরও কাছাকাছি option চাইলে আমি দেখাতে পারি।'
                    : 'I can also show nearby image matches if you want alternatives.');
            }

            return [
                'reply' => $reply,
                'reply_text' => $reply,
                'reply_short' => $reply,
                'reply_detailed' => $reply,
                'language' => $language,
                'normalized_question' => $this->normalizeText($question),
                'query_type' => 'image_product_search',
                'source_type' => 'image_product',
                'source_id' => $top->item->id,
                'matched_keyword' => $top->item->name,
                'confidence_score' => $topConfidence,
                'image_confidence' => $topConfidence,
                'fallback_used' => false,
                'products' => array_slice($productCards, 0, 3),
                'matched_product_ids' => $products->pluck('id')->values()->all(),
            ];
        }

        $lines = [$language === 'bn'
            ? 'ছবির সাথে নিশ্চিত exact product match পাইনি। আপনি কি এর মধ্যে কোনটি বুঝিয়েছেন?'
            : 'I could not confirm the exact product from the image. Did you mean one of these?'];

        foreach ($products->take(3) as $index => $item) {
            $confidence = (int) optional($matches->firstWhere('item_id', $item->id))->confidence_score;
            $lines[] = ($index + 1) . '. ' . $item->name . " ({$confidence}% match)";
        }

        $lines[] = $language === 'bn'
            ? 'একটি option select করলে আমি database থেকে exact price, stock, delivery ও link দেখাব।'
            : 'Select one option and I will show verified price, stock, delivery, and link from the database.';

        $reply = implode("\n", $lines);

        return [
            'reply' => $reply,
            'reply_text' => $reply,
            'reply_short' => $reply,
            'reply_detailed' => $reply,
            'language' => $language,
            'normalized_question' => $this->normalizeText($question),
            'query_type' => 'image_product_search',
            'source_type' => 'image_product_candidates',
            'source_id' => $top?->item?->id,
            'matched_keyword' => $top?->item?->name,
            'confidence_score' => $topConfidence,
            'image_confidence' => $topConfidence,
            'fallback_used' => false,
            'products' => array_slice($productCards, 0, 3),
            'matched_product_ids' => $products->pluck('id')->values()->all(),
        ];
    }

    public function autoReplyToConversation(Message $incomingMessage, Conversation $conversation, UserInfo $customerInfo): ?Message
    {
        $customer = $customerInfo->user_id ? User::find($customerInfo->user_id) : null;

        $this->recordChatMessage([
            'conversation_id' => $conversation->id,
            'message_id' => $incomingMessage->id,
            'customer_id' => $customer?->id,
            'user_info_id' => $customerInfo->id,
            'role' => 'customer',
            'message' => (string) $incomingMessage->message,
            'language' => $this->detectLanguage((string) $incomingMessage->message),
            'meta' => [
                'order_id' => $incomingMessage->order_id,
            ],
        ]);

        $settings = $this->settings();
        if (!$settings['chat_enabled'] || !$settings['auto_reply_enabled']) {
            $reply = $this->fallbackReply([
                'language' => $this->detectLanguage((string) $incomingMessage->message),
                'query_type' => 'disabled',
                'normalized_question' => $this->normalizeText((string) $incomingMessage->message),
                'settings' => $settings,
            ], 'auto_reply_disabled');

            $this->recordChatLog([
                'conversation_id' => $conversation->id,
                'incoming_message_id' => $incomingMessage->id,
                'customer_id' => $customer?->id,
                'normalized_question' => $reply['normalized_question'],
                'question' => $incomingMessage->message,
                'reply' => $reply['reply'],
                'language' => $reply['language'],
                'query_type' => $reply['query_type'],
                'source_type' => $reply['source_type'],
                'matched_keyword' => $reply['matched_keyword'] ?? null,
                'confidence_score' => $reply['confidence_score'] ?? 0,
                'fallback_used' => true,
                'is_corrected' => false,
                'meta' => [
                    'reason' => 'auto_reply_disabled',
                ],
            ]);

            return null;
        }

        $imagePath = ($settings['image_search_enabled'] ?? true)
            ? $this->firstConversationImagePath($incomingMessage)
            : null;

        $replyData = $imagePath
            ? $this->imageSearchReply($imagePath, (string) $incomingMessage->message, $customer, (string) $conversation->id)
            : $this->previewReply((string) $incomingMessage->message, $customer);
        $replyText = $this->cleanReplyText($replyData['reply'] ?? '');
        $storedReplyText = $this->composeStoredReplyMessage($replyData);
        $adminUserInfo = $this->resolveAdminUserInfo();
        if (!$adminUserInfo) {
            return null;
        }

        $replyMessage = new Message();
        $replyMessage->conversation_id = $conversation->id;
        $replyMessage->sender_id = $adminUserInfo->id;
        $replyMessage->message = $storedReplyText;
        $replyMessage->order_id = $incomingMessage->order_id;
        $replyMessage->save();

        $conversation->unread_message_count = ($conversation->unread_message_count ?? 0) + 1;
        $conversation->last_message_id = $replyMessage->id;
        $conversation->last_message_time = Carbon::now()->toDateTimeString();
        $conversation->save();

        $this->recordChatMessage([
            'conversation_id' => $conversation->id,
            'message_id' => $replyMessage->id,
            'customer_id' => $customer?->id,
            'user_info_id' => $adminUserInfo->id,
            'role' => 'assistant',
            'message' => $replyText,
            'language' => $replyData['language'],
            'meta' => [
                'source_type' => $replyData['source_type'],
                'source_id' => $replyData['source_id'],
                'confidence_score' => $replyData['confidence_score'],
                'product_answer_intent' => $replyData['product_answer_intent'] ?? null,
                'product_search_terms' => $replyData['product_search_terms'] ?? [],
                'data_domain' => $replyData['data_domain'] ?? null,
                'schema_table' => $replyData['schema_table'] ?? null,
                'amounts' => $replyData['amounts'] ?? [],
                'agent_context' => $replyData['agent_context'] ?? null,
                'products' => $replyData['products'] ?? [],
            ],
        ]);

        $this->recordChatLog([
            'conversation_id' => $conversation->id,
            'incoming_message_id' => $incomingMessage->id,
            'outgoing_message_id' => $replyMessage->id,
            'customer_id' => $customer?->id,
            'session_id' => (string) $conversation->id,
            'normalized_question' => $replyData['normalized_question'],
            'question' => $incomingMessage->message,
            'reply' => $replyText,
            'language' => $replyData['language'],
            'query_type' => $replyData['query_type'],
            'source_type' => $replyData['source_type'],
            'source_id' => $replyData['source_id'],
            'matched_product_ids' => $replyData['matched_product_ids'] ?? [],
            'matched_keyword' => $replyData['matched_keyword'] ?? null,
            'confidence_score' => $replyData['confidence_score'] ?? 0,
            'fallback_used' => $replyData['fallback_used'] ?? false,
            'is_corrected' => $replyData['source_type'] === 'correction',
            'query_attempt' => $replyData['query_attempt'] ?? 0,
            'meta' => [
                'reply_short'  => $replyData['reply_short'] ?? null,
                'reply_detailed' => $replyData['reply_detailed'] ?? null,
                'match_pct'    => $replyData['match_pct'] ?? null,
                'query_attempt' => $replyData['query_attempt'] ?? null,
                'product_answer_intent' => $replyData['product_answer_intent'] ?? null,
                'product_search_terms' => $replyData['product_search_terms'] ?? [],
                'data_domain' => $replyData['data_domain'] ?? null,
                'schema_table' => $replyData['schema_table'] ?? null,
                'amounts' => $replyData['amounts'] ?? [],
                'agent_context' => $replyData['agent_context'] ?? null,
                'products' => $replyData['products'] ?? [],
            ],
        ]);

        return $replyMessage;
    }

    public function knowledgeFeed(array $filters = []): Collection
    {
        $query = KnowledgeBaseEntry::query()->orderByDesc('priority')->latest('updated_at');

        if (!empty($filters['active_only'])) {
            $query->where('is_active', true);
        }

        if (!empty($filters['search'])) {
            $search = trim((string) $filters['search']);
            $query->where(function ($builder) use ($search) {
                $builder->where('title', 'like', "%{$search}%")
                    ->orWhere('tags', 'like', "%{$search}%")
                    ->orWhere('question_patterns', 'like', "%{$search}%")
                    ->orWhere('answer', 'like', "%{$search}%")
                    ->orWhere('answer_bn', 'like', "%{$search}%")
                    ->orWhere('answer_en', 'like', "%{$search}%");
            });
        }

        return $query->get();
    }

    public function offersFeed(array $filters = []): Collection
    {
        $query = ChatOffer::query()->orderByDesc('priority')->latest('updated_at');

        if (!empty($filters['active_only'])) {
            $query->activeWindow();
        }

        if (!empty($filters['search'])) {
            $search = trim((string) $filters['search']);
            $query->where(function ($builder) use ($search) {
                $builder->where('title', 'like', "%{$search}%")
                    ->orWhere('description', 'like', "%{$search}%")
                    ->orWhere('title_bn', 'like', "%{$search}%")
                    ->orWhere('title_en', 'like', "%{$search}%")
                    ->orWhere('description_bn', 'like', "%{$search}%")
                    ->orWhere('description_en', 'like', "%{$search}%");
            });
        }

        return $query->get();
    }

    protected function findKeywordReply(array $context): ?array
    {
        if (!Schema::hasTable('knowledge_base') || !Schema::hasColumn('knowledge_base', 'source_category')) {
            return null;
        }

        $entries = KnowledgeBaseEntry::query()
            ->where('is_active', true)
            ->where('source_category', 'keyword')
            ->orderByDesc('priority')
            ->latest('updated_at')
            ->get();

        foreach ($entries as $entry) {
            if (!filled($entry->question_patterns)) {
                continue;
            }

            $keywords = array_filter(array_map('trim', explode(',', (string) $entry->question_patterns)));

            foreach ($keywords as $keyword) {
                $normalizedKeyword = $this->normalizeText($keyword);
                if ($normalizedKeyword === '') {
                    continue;
                }

                if ($context['normalized_question'] === $normalizedKeyword
                    || str_contains($context['normalized_question'], $normalizedKeyword)) {
                    $reply = $this->selectLocalizedText(
                        $entry->answer_bn,
                        $entry->answer_en,
                        $entry->answer,
                        $context['language']
                    );

                    if (!filled($reply)) {
                        continue;
                    }

                    return [
                        'reply' => $reply,
                        'reply_short' => $this->shortenText($reply, 200),
                        'reply_detailed' => $reply,
                        'source_type' => 'keyword',
                        'source_id' => $entry->id,
                        'matched_keyword' => $keyword,
                        'confidence_score' => 100,
                        'language' => $context['language'],
                        'normalized_question' => $context['normalized_question'],
                        'query_type' => $context['query_type'],
                        'fallback_used' => false,
                    ];
                }
            }
        }

        return null;
    }

    protected function findGreetingReply(array $context): ?array
    {
        $greetings = [
            'en' => ['hi', 'hello', 'hey', 'howdy', 'greetings', 'good morning', 'good afternoon', 'good evening', 'good day', 'whats up', 'sup'],
            'bn' => ['হ্যালো', 'হেলো', 'হাই', 'নমস্কার', 'আস্সালামু', 'আসসালামু', 'সালাম', 'ওহে'],
        ];

        $normalized = $context['normalized_question'];

        $allGreetings = array_merge($greetings['en'], $greetings['bn']);
        $isGreeting = false;
        foreach ($allGreetings as $g) {
            $normalizedG = $this->normalizeText($g);
            if ($normalized === $normalizedG || $normalized === $normalizedG . '!' || $normalized === $normalizedG . '.') {
                $isGreeting = true;
                break;
            }
        }

        if (!$isGreeting) {
            return null;
        }

        $businessName = $context['settings']['fallback_message_en'] ?? 'us';
        $configName = BusinessSetting::query()->where('key', 'business_name')->value('value') ?? 'Ammar';

        $reply = $context['language'] === 'bn'
            ? "হ্যালো! {$configName}-এ স্বাগতম 😊 আমরা কীভাবে আপনাকে সাহায্য করতে পারি?"
            : "Hello! Welcome to {$configName} 😊 How can we help you today?";

        return [
            'reply' => $reply,
            'reply_short' => $reply,
            'reply_detailed' => $reply,
            'source_type' => 'greeting',
            'source_id' => null,
            'matched_keyword' => $normalized,
            'confidence_score' => 100,
            'language' => $context['language'],
            'normalized_question' => $context['normalized_question'],
            'query_type' => 'greeting',
            'fallback_used' => false,
        ];
    }

    protected function findHumanReply(array $context): array
    {
        $reply = $context['language'] === 'bn'
            ? 'অবশ্যই, আমি আপনাকে মানব সাপোর্ট টিমের সাথে যুক্ত করছি। দ্রুত সহায়তার জন্য WhatsApp: 01930222254 নম্বরে যোগাযোগ করতে পারেন।'
            : 'Sure, I can connect you with our human support team. For faster help, please contact us on WhatsApp: 01930222254.';

        return [
            'reply' => $reply,
            'reply_short' => $reply,
            'reply_detailed' => $reply,
            'source_type' => 'human_handoff',
            'source_id' => null,
            'matched_keyword' => 'human_support',
            'confidence_score' => 100,
            'language' => $context['language'],
            'normalized_question' => $context['normalized_question'],
            'query_type' => 'human',
            'fallback_used' => false,
        ];
    }

    protected function findOrderHelpReply(array $context): array
    {
        $verifiedReply = $this->findVerifiedOrderStatusReply($context);
        if ($verifiedReply) {
            return $verifiedReply;
        }

        $reply = ($context['language'] ?? 'en') === 'bn'
            ? 'অর্ডার স্ট্যাটাস জানতে হলে অনুগ্রহ করে order ID এবং যে phone number দিয়ে order করেছেন সেটি দিন। যাচাই ছাড়া আমি কোনো customer/order personal data দেখাব না।'
            : 'To check order status, please share the order ID and the phone number used for the order. I will not show any customer/order personal data without verification.';

        return [
            'reply' => $reply,
            'reply_short' => $reply,
            'reply_detailed' => $reply,
            'source_type' => 'order_help_verification_required',
            'source_id' => null,
            'matched_keyword' => 'order_status',
            'confidence_score' => 95,
            'language' => $context['language'],
            'normalized_question' => $context['normalized_question'],
            'query_type' => 'order_help',
            'fallback_used' => false,
        ];
    }

    protected function findVerifiedOrderStatusReply(array $context): ?array
    {
        if (!Schema::hasTable('orders')) {
            return null;
        }

        $question = (string) ($context['question'] ?? '');
        [$orderId, $phone] = $this->extractOrderVerificationPair($question);
        if (!$orderId || !$phone) {
            return null;
        }

        try {
            $order = Order::withoutGlobalScopes()
                ->with(['customer:id,phone,f_name,l_name', 'store:id,name'])
                ->find($orderId);
        } catch (\Throwable $e) {
            Log::warning('AiChat verified order lookup failed: ' . $e->getMessage());
            return null;
        }

        $language = $context['language'] ?? 'en';
        if (!$order || !$this->orderPhoneMatches($order, $phone)) {
            $reply = $language === 'bn'
                ? 'দুঃখিত, এই order ID এবং phone number দিয়ে কোনো verified order খুঁজে পাইনি। অনুগ্রহ করে order ID ও phone number আবার মিলিয়ে দিন।'
                : 'Sorry, I could not verify an order with that order ID and phone number. Please check both and send them again.';

            return [
                'reply' => $reply,
                'reply_short' => $reply,
                'reply_detailed' => $reply,
                'source_type' => 'order_verification_failed',
                'source_id' => null,
                'matched_keyword' => 'order_status',
                'confidence_score' => 60,
                'language' => $language,
                'normalized_question' => $context['normalized_question'],
                'query_type' => 'order_help',
                'fallback_used' => false,
            ];
        }

        $status = (string) ($order->order_status ?? 'unknown');
        $paymentStatus = (string) ($order->payment_status ?? 'unknown');
        $paymentMethod = (string) ($order->payment_method ?? 'not specified');
        $amount = rtrim(rtrim(number_format((float) ($order->order_amount ?? 0), 2), '0'), '.');
        $deliveryCharge = rtrim(rtrim(number_format((float) ($order->delivery_charge ?? 0), 2), '0'), '.');
        $createdAt = $order->created_at?->format('d M Y h:i A');

        $reply = $language === 'bn'
            ? implode("\n", array_filter([
                "জি, আপনার order #{$order->id} verified হয়েছে।",
                "বর্তমান অর্ডার স্ট্যাটাস: {$status}",
                "Payment status: {$paymentStatus}",
                "Payment method: {$paymentMethod}",
                "Order amount: ৳{$amount}",
                "Delivery charge: ৳{$deliveryCharge}",
                $createdAt ? "Order date: {$createdAt}" : null,
                'আরও সহায়তা দরকার হলে এখানে লিখুন, আমি verified order data দেখেই উত্তর দেব।',
            ]))
            : implode("\n", array_filter([
                "Yes, order #{$order->id} is verified.",
                "Current order status: {$status}",
                "Payment status: {$paymentStatus}",
                "Payment method: {$paymentMethod}",
                "Order amount: BDT {$amount}",
                "Delivery charge: BDT {$deliveryCharge}",
                $createdAt ? "Order date: {$createdAt}" : null,
                'If you need more help, ask here and I will answer from verified order data.',
            ]));

        return [
            'reply' => $reply,
            'reply_short' => $reply,
            'reply_detailed' => $reply,
            'source_type' => 'order',
            'source_id' => $order->id,
            'matched_keyword' => 'order_status',
            'confidence_score' => 100,
            'language' => $language,
            'normalized_question' => $context['normalized_question'],
            'query_type' => 'order_help',
            'fallback_used' => false,
        ];
    }

    protected function extractOrderVerificationPair(string $question): array
    {
        $phone = null;
        if (preg_match('/(?:\+?88)?01[3-9][0-9][\s\-.]?[0-9]{3}[\s\-.]?[0-9]{4}/', $question, $phoneMatch)) {
            $phone = $phoneMatch[0];
        }

        $orderId = null;
        if (preg_match('/(?:order\s*(?:id|no|number)?|অর্ডার\s*(?:আইডি|নম্বর)?|#)\s*[:#-]?\s*([0-9]{2,12})/iu', $question, $orderMatch)) {
            $orderId = (int) $orderMatch[1];
        } elseif (preg_match_all('/(?<![0-9])([0-9]{2,12})(?![0-9])/', $question, $matches)) {
            $phoneDigits = $this->normalizePhoneForAiChat($phone);
            foreach ($matches[1] as $number) {
                if ($phoneDigits && str_contains($phoneDigits, $number)) {
                    continue;
                }
                $orderId = (int) $number;
                break;
            }
        }

        return [$orderId ?: null, $phone];
    }

    protected function orderPhoneMatches(Order $order, string $inputPhone): bool
    {
        $input = $this->normalizePhoneForAiChat($inputPhone);
        if ($input === '') {
            return false;
        }

        $phones = [
            $order->customer?->phone,
            data_get($this->decodeJsonArray($order->getRawOriginal('delivery_address')), 'contact_person_number'),
            data_get($this->decodeJsonArray($order->getRawOriginal('shipping_address_data')), 'contact_person_number'),
            data_get($this->decodeJsonArray($order->getRawOriginal('receiver_details')), 'contact_person_number'),
            data_get($this->decodeJsonArray($order->receiver_details), 'contact_person_number'),
        ];

        foreach ($phones as $phone) {
            $normalized = $this->normalizePhoneForAiChat($phone);
            if ($normalized !== '' && ($normalized === $input || substr($normalized, -10) === substr($input, -10))) {
                return true;
            }
        }

        return false;
    }

    protected function normalizePhoneForAiChat(mixed $phone): string
    {
        $digits = preg_replace('/\D+/', '', (string) $phone) ?: '';
        if ($digits === '') {
            return '';
        }
        if (str_starts_with($digits, '8801') && strlen($digits) === 13) {
            return $digits;
        }
        if (str_starts_with($digits, '01') && strlen($digits) === 11) {
            return '88' . $digits;
        }
        if (str_starts_with($digits, '1') && strlen($digits) === 10) {
            return '880' . $digits;
        }

        return $digits;
    }

    protected function findGeneralAiReply(array $context): ?array
    {
        if (($context['query_type'] ?? 'general') !== 'general') {
            return null;
        }

        $question = trim((string) ($context['question'] ?? ''));
        $apiKey = config('openai.api_key');
        if ($question === '' || empty($apiKey)) {
            return null;
        }

        $language = $context['language'] ?? 'en';
        $cacheKey = 'ai_general_reply_v1_' . md5($language . '|' . $question);

        $reply = Cache::remember($cacheKey, 120, function () use ($question, $language, $context) {
            try {
                $systemPrompt = implode("\n", [
                    'You are a helpful e-commerce sales assistant for Ammar.',
                    'This is General AI Answer Mode: answer general product-use, comparison, gift, kitchen, and shopping-advice questions.',
                    'Never invent or guess Ammar product price, stock, delivery charge, warranty, offer, policy, order status, payment info, or customer data.',
                    'If exact business/product/order/policy data is needed, ask for product name/model/order ID/phone so the backend can verify it from database.',
                    'Do not mention exact prices or availability unless they were provided as verified context. In this mode there is no verified product context.',
                    'Keep the answer concise, polite, and sales-focused without fake urgency.',
                    filled($context['settings']['custom_instruction'] ?? '') ? 'Admin instruction: ' . $context['settings']['custom_instruction'] : '',
                    $language === 'bn' ? 'Reply in Bangla.' : 'Reply in English.',
                ]);

                $response = OpenAI::chat()->create([
                    'model' => 'gpt-4o-mini',
                    'max_tokens' => 280,
                    'temperature' => 0.4,
                    'messages' => [
                        ['role' => 'system', 'content' => $systemPrompt],
                        ['role' => 'user', 'content' => $question],
                    ],
                ]);

                return $this->cleanReplyText((string) ($response->choices[0]->message->content ?? ''));
            } catch (\Throwable $e) {
                Log::warning('OpenAI general reply failed: ' . $e->getMessage());
                return '';
            }
        });

        if ($reply === '' || $this->looksLikeBusinessDataGuess($reply)) {
            return null;
        }

        return [
            'reply' => $reply,
            'reply_short' => $reply,
            'reply_detailed' => $reply,
            'source_type' => 'general_ai',
            'source_id' => null,
            'matched_keyword' => 'general_ai',
            'confidence_score' => 70,
            'language' => $language,
            'normalized_question' => $context['normalized_question'] ?? $this->normalizeText($question),
            'query_type' => 'general',
            'fallback_used' => false,
            'agent_context' => $context['agent_context'] ?? null,
        ];
    }

    protected function looksLikeBusinessDataGuess(string $reply): bool
    {
        return (bool) preg_match('/(৳|BDT|tk|taka|delivery charge|shipping charge|warranty|stock|in stock|out of stock|available now|offer price|discount price|order status|payment status|দাম|মূল্য|ডেলিভারি চার্জ|ওয়ারেন্টি|ওয়ারেন্টি|স্টক|অর্ডার স্ট্যাটাস|পেমেন্ট স্ট্যাটাস)/iu', $reply);
    }

    protected function findCapabilityReply(array $context): ?array
    {
        $normalized = $context['normalized_question'];
        $phrases = [
            'what can you do',
            'how can you help',
            'help me',
            'তুমি কি কি সহযোগিতা',
            'তুমি কী কী সহযোগিতা',
            'কি কি সহযোগিতা',
            'কী কী সহযোগিতা',
            'কি করতে পারো',
            'কী করতে পারো',
            'সাহায্য করতে পারো',
        ];

        $matched = false;
        foreach ($phrases as $phrase) {
            if (str_contains($normalized, $this->normalizeText($phrase))) {
                $matched = true;
                break;
            }
        }

        if (!$matched) {
            return null;
        }

        $reply = $context['language'] === 'bn'
            ? 'আমি পণ্য খুঁজে দিতে, দাম/অফার/স্টক জানাতে, ডেলিভারি তথ্য দিতে, অর্ডার বিষয়ে সাহায্য করতে এবং দরকার হলে মানব সাপোর্টে যুক্ত করতে পারি। আপনি পণ্যের নাম, ব্র্যান্ড বা মডেল লিখলেই আমি খুঁজে দেখব।'
            : 'I can help you find products, check prices/offers/stock, explain delivery information, help with order questions, and connect you to human support when needed. Send a product name, brand, or model and I will search it.';

        return [
            'reply' => $reply,
            'reply_short' => $reply,
            'reply_detailed' => $reply,
            'source_type' => 'capability',
            'source_id' => null,
            'matched_keyword' => 'capability',
            'confidence_score' => 100,
            'language' => $context['language'],
            'normalized_question' => $context['normalized_question'],
            'query_type' => 'capability',
            'fallback_used' => false,
        ];
    }

    protected function findSchemaDataReply(array $context): ?array
    {
        return match ($context['query_type'] ?? 'general') {
            'wallet_bonus' => $this->findWalletBonusReply($context),
            'coupon' => $this->findCouponReply($context),
            'cashback' => $this->findCashBackReply($context),
            'wallet' => $this->findWalletReply($context),
            'loyalty' => $this->findLoyaltyReply($context),
            'delivery' => $this->findDeliveryReply($context),
            'free_delivery' => $this->findFreeDeliveryReply($context),
            'contact' => $this->findContactReply($context),
            'business_info' => $this->findBusinessInfoReply($context),
            'social_media' => $this->findSocialMediaReply($context),
            default => null,
        };
    }

    protected function findWalletBonusReply(array $context): ?array
    {
        if (!Schema::hasTable('wallet_bonuses')) {
            return null;
        }

        $bonuses = WalletBonus::query()
            ->active()
            ->running()
            ->latest('updated_at')
            ->get();

        $language = $context['language'] ?? 'en';
        $amount = $context['amounts'][0] ?? null;

        if ($bonuses->isEmpty()) {
            $reply = $language === 'bn'
                ? 'Ekhon kono active wallet bonus nei. Active bonus thakle wallet/add fund section e dekhabe.'
                : 'There is no active wallet bonus right now. Active bonuses will appear in the wallet/add fund section.';

            return $this->schemaReply($reply, 'wallet_bonus', null, 'wallet_bonuses', null, 75, $context);
        }

        if ($amount) {
            $eligible = $bonuses
                ->map(function (WalletBonus $bonus) use ($amount) {
                    $bonus->_calculated_bonus = $this->calculateWalletBonusForAmount($bonus, (float) $amount);
                    return $bonus;
                })
                ->filter(fn (WalletBonus $bonus) => (float) ($bonus->_calculated_bonus ?? 0) > 0)
                ->sortByDesc('_calculated_bonus')
                ->values();

            if ($eligible->isNotEmpty()) {
                $best = $eligible->first();
                $bonusAmount = (float) $best->_calculated_bonus;
                $reply = $language === 'bn'
                    ? 'Ji, wallet e ' . $this->formatMoney($amount) . ' add korle ' . $this->formatMoney($bonusAmount) . ' bonus paben. Bonus wallet balance e add hobe; checkout e Wallet Payment select kore use korte parben.'
                    : 'Yes, if you add ' . $this->formatMoney($amount) . ' to your wallet, you can get ' . $this->formatMoney($bonusAmount) . ' bonus. The bonus is added to wallet balance and can be used at checkout with Wallet Payment.';

                return $this->schemaReply($reply, 'wallet_bonus', $best->id, 'wallet_bonuses', $best->title, 95, $context);
            }

            $minimum = $bonuses->min('minimum_add_amount');
            $reply = $language === 'bn'
                ? $this->formatMoney($amount) . ' add korle current wallet bonus applicable hocche na. Bonus pete minimum ' . $this->formatMoney((float) $minimum) . ' add korte hote pare.'
                : 'A wallet top-up of ' . $this->formatMoney($amount) . ' does not qualify for the current bonus. You may need to add at least ' . $this->formatMoney((float) $minimum) . '.';

            return $this->schemaReply($reply, 'wallet_bonus', $bonuses->first()?->id, 'wallet_bonuses', $bonuses->first()?->title, 82, $context);
        }

        $lines = $bonuses->take(3)->map(function (WalletBonus $bonus) {
            $bonusText = $bonus->bonus_type === 'percentage'
                ? rtrim(rtrim(number_format((float) $bonus->bonus_amount, 2), '0'), '.') . '%'
                : $this->formatMoney((float) $bonus->bonus_amount);

            return $bonus->title . ' - bonus ' . $bonusText . ', minimum add ' . $this->formatMoney((float) $bonus->minimum_add_amount);
        })->all();

        $reply = $language === 'bn'
            ? 'Current wallet bonus: ' . implode('; ', $lines) . '. Wallet e fund add korle eligible bonus automatically wallet balance e add hobe.'
            : 'Current wallet bonuses: ' . implode('; ', $lines) . '. Add funds to your wallet and the eligible bonus will be added automatically.';

        return $this->schemaReply($reply, 'wallet_bonus', $bonuses->first()?->id, 'wallet_bonuses', $bonuses->first()?->title, 90, $context);
    }

    protected function findCouponReply(array $context): ?array
    {
        if (!Schema::hasTable('coupons')) {
            return null;
        }

        $amount = $context['amounts'][0] ?? null;
        $coupons = Coupon::query()
            ->active()
            ->where(function ($query) {
                $query->whereDate('start_date', '<=', date('Y-m-d'))->orWhereNull('start_date');
            })
            ->where(function ($query) {
                $query->whereDate('expire_date', '>=', date('Y-m-d'))->orWhereNull('expire_date');
            })
            ->when($amount, fn ($query) => $query->where('min_purchase', '<=', (float) $amount))
            ->orderByDesc('discount')
            ->latest('updated_at')
            ->limit(5)
            ->get();

        $language = $context['language'] ?? 'en';
        if ($coupons->isEmpty()) {
            $reply = $language === 'bn'
                ? 'Ekhon ei amount er jonno active coupon paoa jacche na. Checkout er coupon box e valid code apply korte parben.'
                : 'No active coupon was found for this amount right now. You can apply a valid code in the coupon box at checkout.';

            return $this->schemaReply($reply, 'coupon', null, 'coupons', null, 75, $context);
        }

        $lines = $coupons->map(function (Coupon $coupon) {
            $discount = $coupon->discount_type === 'percent'
                ? rtrim(rtrim(number_format((float) $coupon->discount, 2), '0'), '.') . '%'
                : $this->formatMoney((float) $coupon->discount);

            return $coupon->code . ' - ' . $discount . ' off, min order ' . $this->formatMoney((float) $coupon->min_purchase);
        })->all();

        $reply = $language === 'bn'
            ? 'Active coupon: ' . implode('; ', $lines) . '. Checkout e coupon code apply kore discount use korte parben.'
            : 'Active coupons: ' . implode('; ', $lines) . '. Apply the code at checkout to use the discount.';

        return $this->schemaReply($reply, 'coupon', $coupons->first()?->id, 'coupons', $coupons->first()?->code, 88, $context);
    }

    protected function findCashBackReply(array $context): ?array
    {
        if (!Schema::hasTable('cash_backs')) {
            return null;
        }

        $amount = $context['amounts'][0] ?? null;
        $cashBacks = CashBack::query()
            ->active()
            ->running()
            ->when($amount, fn ($query) => $query->where('min_purchase', '<=', (float) $amount))
            ->orderByDesc('cashback_amount')
            ->latest('updated_at')
            ->limit(5)
            ->get();

        $language = $context['language'] ?? 'en';
        if ($cashBacks->isEmpty()) {
            $reply = $language === 'bn'
                ? 'Ekhon ei amount er jonno active cashback paoa jacche na.'
                : 'No active cashback was found for this amount right now.';

            return $this->schemaReply($reply, 'cashback', null, 'cash_backs', null, 75, $context);
        }

        if ($amount) {
            $best = $cashBacks
                ->map(function (CashBack $cashBack) use ($amount) {
                    $cashBack->_calculated_cashback = $this->calculateCashBackForAmount($cashBack, (float) $amount);
                    return $cashBack;
                })
                ->sortByDesc('_calculated_cashback')
                ->first();

            $cashbackAmount = (float) ($best->_calculated_cashback ?? 0);
            $reply = $language === 'bn'
                ? $this->formatMoney($amount) . ' order e approx ' . $this->formatMoney($cashbackAmount) . ' cashback applicable hote pare. Final cashback checkout/order rule onujayi confirm hobe.'
                : 'For an order of ' . $this->formatMoney($amount) . ', approximately ' . $this->formatMoney($cashbackAmount) . ' cashback may apply. Final cashback is confirmed by checkout/order rules.';

            return $this->schemaReply($reply, 'cashback', $best?->id, 'cash_backs', $best?->title, 86, $context);
        }

        $lines = $cashBacks->take(3)->map(function (CashBack $cashBack) {
            $value = $cashBack->cashback_type === 'percentage'
                ? rtrim(rtrim(number_format((float) $cashBack->cashback_amount, 2), '0'), '.') . '%'
                : $this->formatMoney((float) $cashBack->cashback_amount);

            return $cashBack->title . ' - ' . $value . ', min order ' . $this->formatMoney((float) $cashBack->min_purchase);
        })->all();

        $reply = $language === 'bn'
            ? 'Current cashback: ' . implode('; ', $lines) . '. Eligible order korle cashback wallet e add hobe.'
            : 'Current cashback offers: ' . implode('; ', $lines) . '. Eligible cashback is added to your wallet after order rules are met.';

        return $this->schemaReply($reply, 'cashback', $cashBacks->first()?->id, 'cash_backs', $cashBacks->first()?->title, 86, $context);
    }

    protected function findWalletReply(array $context): array
    {
        $customer = $context['customer'] ?? null;
        $language = $context['language'] ?? 'en';

        if ($customer) {
            $balance = (float) ($customer->wallet_balance ?? 0);
            $reply = $language === 'bn'
                ? 'Apnar current wallet balance ' . $this->formatMoney($balance) . '. Checkout e Wallet Payment select kore use korte parben.'
                : 'Your current wallet balance is ' . $this->formatMoney($balance) . '. You can use it at checkout by selecting Wallet Payment.';
        } else {
            $reply = $language === 'bn'
                ? 'Wallet balance/use dekhte login korte hobe. Wallet e fund add kore checkout e Wallet Payment diye pay korte parben.'
                : 'Please log in to view or use wallet balance. You can add funds to wallet and pay at checkout with Wallet Payment.';
        }

        return $this->schemaReply($reply, 'wallet', null, 'wallet_transactions', 'wallet', 80, $context);
    }

    protected function findLoyaltyReply(array $context): array
    {
        $customer = $context['customer'] ?? null;
        $language = $context['language'] ?? 'en';

        if ($customer) {
            $points = (float) ($customer->loyalty_point ?? 0);
            $reply = $language === 'bn'
                ? 'Apnar current loyalty point ' . rtrim(rtrim(number_format($points, 2), '0'), '.') . '. App/profile er loyalty section theke point details dekhte parben.'
                : 'Your current loyalty points are ' . rtrim(rtrim(number_format($points, 2), '0'), '.') . '. You can view details in the loyalty section of your profile.';
        } else {
            $reply = $language === 'bn'
                ? 'Loyalty point dekhte login korte hobe. Profile er loyalty section e point balance and transaction paben.'
                : 'Please log in to view loyalty points. Your profile loyalty section shows point balance and transactions.';
        }

        return $this->schemaReply($reply, 'loyalty', null, 'loyalty_point_transactions', 'loyalty', 80, $context);
    }

    protected function findDeliveryReply(array $context): array
    {
        $language = $context['language'] ?? 'en';
        $settings = $this->getBusinessSettingsMap([
            'shipping_policy',
            'home_delivery_status',
            'parcel_minimum_shipping_charge',
            'parcel_per_km_shipping_charge',
            'additional_charge_status',
            'additional_charge',
            'additional_charge_name',
        ]);

        $shippingPolicy = trim(strip_tags((string) ($settings['shipping_policy'] ?? '')));
        $homeDelivery = (int) ($settings['home_delivery_status'] ?? 1) === 1;
        $parts = [];

        if (!$homeDelivery) {
            $parts[] = $language === 'bn'
                ? 'Home delivery currently off ache.'
                : 'Home delivery is currently unavailable.';
        }

        if ($shippingPolicy !== '') {
            $parts[] = $this->shortenText($shippingPolicy, 320);
        }

        if (isset($settings['parcel_minimum_shipping_charge']) || isset($settings['parcel_per_km_shipping_charge'])) {
            $minimum = (float) ($settings['parcel_minimum_shipping_charge'] ?? 0);
            $perKm = (float) ($settings['parcel_per_km_shipping_charge'] ?? 0);
            $chargeLine = $language === 'bn'
                ? 'Delivery charge location/order onujayi calculate hoy'
                : 'Delivery charge is calculated based on location/order';
            if ($minimum > 0) {
                $chargeLine .= ', minimum ' . $this->formatMoney($minimum);
            }
            if ($perKm > 0) {
                $chargeLine .= ', per km ' . $this->formatMoney($perKm);
            }
            $parts[] = $chargeLine . '.';
        }

        if ((int) ($settings['additional_charge_status'] ?? 0) === 1 && (float) ($settings['additional_charge'] ?? 0) > 0) {
            $name = trim((string) ($settings['additional_charge_name'] ?? 'Additional charge'));
            $parts[] = $name . ': ' . $this->formatMoney((float) $settings['additional_charge']) . '.';
        }

        if (empty($parts)) {
            $parts[] = $language === 'bn'
                ? 'Delivery information checkout e address select korar por dekhabe. Product/order/location onujayi charge change hote pare.'
                : 'Delivery information appears at checkout after selecting your address. Charges may vary by product, order, and location.';
        }

        return $this->schemaReply(implode("\n", $parts), 'delivery', null, 'business_settings', 'delivery_info', 86, $context);
    }

    protected function findFreeDeliveryReply(array $context): array
    {
        $language = $context['language'] ?? 'en';
        $settings = $this->getBusinessSettingsMap([
            'admin_free_delivery_status',
            'admin_free_delivery_option',
            'free_delivery_over',
        ]);

        $enabled = (int) ($settings['admin_free_delivery_status'] ?? 0) === 1;
        $type = (string) ($settings['admin_free_delivery_option'] ?? '');
        $over = (float) ($settings['free_delivery_over'] ?? 0);

        if ($enabled) {
            if ($over > 0) {
                $reply = $language === 'bn'
                    ? 'Free delivery active ache. Eligible order amount ' . $this->formatMoney($over) . ' ba tar beshi hole free delivery pawa jete pare. Final eligibility checkout e confirm hobe.'
                    : 'Free delivery is active. Orders of ' . $this->formatMoney($over) . ' or more may qualify. Final eligibility is confirmed at checkout.';
            } else {
                $reply = $language === 'bn'
                    ? 'Free delivery active ache. Product/order/location rule onujayi checkout e final free delivery dekhabe.'
                    : 'Free delivery is active. Final availability depends on product/order/location rules and is shown at checkout.';
            }

            if ($type !== '') {
                $reply .= ' Type: ' . $type . '.';
            }
        } else {
            $reply = $language === 'bn'
                ? 'Currently admin free delivery off ache. Kichu product e free delivery badge thakte pare; product card/checkout e seta dekhabe.'
                : 'Admin free delivery is currently off. Some products may still have a free delivery badge; it will appear on the product card or at checkout.';
        }

        return $this->schemaReply($reply, 'free_delivery', null, 'business_settings', 'free_delivery', 88, $context);
    }

    protected function findContactReply(array $context): array
    {
        $language = $context['language'] ?? 'en';
        $settings = $this->getBusinessSettingsMap([
            'business_name',
            'phone',
            'email_address',
            'address',
            'whatsapp',
            'whatsapp_number',
            'whatsapp_phone',
            'business_whatsapp',
        ]);

        $businessName = trim((string) ($settings['business_name'] ?? 'Ammar'));
        $phone = trim((string) ($settings['phone'] ?? ''));
        $whatsapp = trim((string) (
            $settings['whatsapp_number']
            ?? $settings['whatsapp_phone']
            ?? $settings['business_whatsapp']
            ?? $settings['whatsapp']
            ?? ''
        ));
        $email = trim((string) ($settings['email_address'] ?? ''));
        $address = trim((string) ($settings['address'] ?? ''));

        if ($whatsapp === '' && $phone !== '') {
            $whatsapp = $phone;
        }

        $lines = [$language === 'bn' ? "{$businessName} contact info:" : "{$businessName} contact information:"];
        if ($phone !== '') {
            $lines[] = 'Phone: ' . $phone;
        }
        if ($whatsapp !== '') {
            $lines[] = 'WhatsApp: ' . $whatsapp;
        }
        if ($email !== '') {
            $lines[] = 'Email: ' . $email;
        }
        if ($address !== '') {
            $lines[] = 'Address: ' . $address;
        }
        $lines = array_merge($lines, $this->formatSocialMediaLines($this->getActiveSocialMediaLinks(), $language));
        $lines[] = $language === 'bn'
            ? 'Feedback dite contact form use korte paren: ' . url('/contact-us')
            : 'For feedback, you can use the contact form: ' . url('/contact-us');

        return $this->schemaReply(implode("\n", $lines), 'contact', null, 'business_settings', 'contact_info', 92, $context);
    }

    protected function findBusinessInfoReply(array $context): array
    {
        $language = $context['language'] ?? 'en';
        $settings = $this->getBusinessSettingsMap([
            'business_name',
            'address',
            'phone',
            'email_address',
            'footer_text',
            'default_location',
        ]);

        $businessName = trim((string) ($settings['business_name'] ?? 'Ammar'));
        $address = trim((string) ($settings['address'] ?? ''));
        $phone = trim((string) ($settings['phone'] ?? ''));
        $email = trim((string) ($settings['email_address'] ?? ''));
        $footer = trim(strip_tags((string) ($settings['footer_text'] ?? '')));

        $lines = [$language === 'bn' ? "Business: {$businessName}" : "Business: {$businessName}"];
        if ($address !== '') {
            $lines[] = 'Address: ' . $address;
        }
        if ($phone !== '') {
            $lines[] = 'Phone: ' . $phone;
        }
        if ($email !== '') {
            $lines[] = 'Email: ' . $email;
        }
        if ($footer !== '') {
            $lines[] = $this->shortenText($footer, 220);
        }
        $lines = array_merge($lines, $this->formatSocialMediaLines($this->getActiveSocialMediaLinks(), $language));

        return $this->schemaReply(implode("\n", $lines), 'business_info', null, 'business_settings', 'business_info', 88, $context);
    }

    protected function findSocialMediaReply(array $context): array
    {
        $language = $context['language'] ?? 'en';
        $socialLinks = $this->getActiveSocialMediaLinks();

        if (empty($socialLinks)) {
            $reply = $language === 'bn'
                ? 'Facebook page, YouTube channel ba onno social media link ekhono set kora nei.'
                : 'Facebook page, YouTube channel, or other social media links are not set yet.';

            return $this->schemaReply($reply, 'social_media', null, 'social_media', 'social_media', 72, $context);
        }

        $lines = $this->formatSocialMediaLines($socialLinks, $language);

        return $this->schemaReply(
            implode("\n", $lines),
            'social_media',
            $socialLinks[0]['id'] ?? null,
            'social_media',
            $socialLinks[0]['name'] ?? 'social_media',
            92,
            $context
        );
    }

    protected function schemaReply(string $reply, string $sourceType, mixed $sourceId, ?string $schemaTable, ?string $matchedKeyword, int $confidence, array $context): array
    {
        return [
            'reply' => $reply,
            'reply_short' => $reply,
            'reply_detailed' => $reply,
            'source_type' => $sourceType,
            'source_id' => $sourceId,
            'matched_keyword' => $matchedKeyword,
            'confidence_score' => $confidence,
            'data_domain' => $context['data_domain'] ?? $sourceType,
            'schema_table' => $schemaTable,
            'amounts' => $context['amounts'] ?? [],
            'agent_context' => $context['agent_context'] ?? null,
            'products' => [],
        ];
    }

    protected function calculateWalletBonusForAmount(WalletBonus $bonus, float $amount): float
    {
        if ($amount < (float) $bonus->minimum_add_amount) {
            return 0.0;
        }

        if ($bonus->bonus_type === 'percentage') {
            $calculated = ($amount * (float) $bonus->bonus_amount) / 100;
            $maximum = (float) $bonus->maximum_bonus_amount;

            return $calculated > $maximum ? $maximum : $calculated;
        }

        return (float) $bonus->bonus_amount;
    }

    protected function calculateCashBackForAmount(CashBack $cashBack, float $amount): float
    {
        if ($amount < (float) $cashBack->min_purchase) {
            return 0.0;
        }

        if ($cashBack->cashback_type === 'percentage') {
            $calculated = ($amount * (float) $cashBack->cashback_amount) / 100;
            $maximum = (float) $cashBack->max_discount;

            return $maximum > 0 && $calculated > $maximum ? $maximum : $calculated;
        }

        return (float) $cashBack->cashback_amount;
    }

    protected function formatMoney(float $amount): string
    {
        return 'BDT ' . rtrim(rtrim(number_format($amount, 2), '0'), '.');
    }

    protected function getBusinessSettingsMap(array $keys): array
    {
        if (! Schema::hasTable('business_settings')) {
            return [];
        }

        $keys = array_values(array_unique(array_filter($keys)));

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

        return BusinessSetting::query()
            ->whereIn('key', $keys)
            ->pluck('value', 'key')
            ->toArray();
    }

    protected function getActiveSocialMediaLinks(): array
    {
        if (! Schema::hasTable('social_media')) {
            return [];
        }

        return SocialMedia::query()
            ->active()
            ->orderBy('id')
            ->get(['id', 'name', 'link'])
            ->map(function (SocialMedia $social): array {
                return [
                    'id' => $social->id,
                    'name' => $this->humanizeSocialMediaName((string) $social->name),
                    'link' => trim((string) $social->link),
                ];
            })
            ->filter(fn (array $social): bool => $social['name'] !== '' && $social['link'] !== '')
            ->values()
            ->all();
    }

    protected function formatSocialMediaLines(array $socialLinks, string $language): array
    {
        if (empty($socialLinks)) {
            return [];
        }

        $lines = [$language === 'bn' ? 'Social media links:' : 'Social media links:'];

        foreach ($socialLinks as $social) {
            $lines[] = "{$social['name']}: {$social['link']}";
        }

        return $lines;
    }

    protected function humanizeSocialMediaName(string $name): string
    {
        $key = Str::of($name)
            ->lower()
            ->replace(['_', '-'], ' ')
            ->squish()
            ->value();

        return match ($key) {
            'fb', 'facebook', 'facebook page' => 'Facebook',
            'youtube', 'you tube', 'youtube channel' => 'YouTube',
            'instagram' => 'Instagram',
            'twitter', 'x', 'x twitter' => 'X/Twitter',
            'linkedin', 'linked in' => 'LinkedIn',
            'pinterest' => 'Pinterest',
            'tiktok', 'tik tok' => 'TikTok',
            'telegram' => 'Telegram',
            'messenger', 'facebook messenger' => 'Messenger',
            'whatsapp', 'whats app' => 'WhatsApp',
            default => Str::title($key),
        };
    }

    protected function findCorrectionReply(array $context): ?array
    {
        $query = AiCorrection::query()
            ->where('is_active', true)
            ->orderByDesc('priority')
            ->latest('updated_at');

        if ($context['settings']['correction_approval_enabled']) {
            $query->where('is_approved', true);
        }

        return $this->pickBestTextCandidate(
            $query->get(),
            [
                'question',
                'question_bn',
                'question_en',
                'normalized_question',
            ],
            function (AiCorrection $candidate) use ($context) {
                return [
                    'reply' => $this->selectLocalizedText($candidate->corrected_answer_bn, $candidate->corrected_answer_en, $candidate->corrected_answer, $context['language']),
                    'reply_short' => $this->shortenText(
                        $this->selectLocalizedText($candidate->corrected_answer_bn, $candidate->corrected_answer_en, $candidate->corrected_answer, $context['language']),
                        160
                    ),
                    'reply_detailed' => $this->selectLocalizedText($candidate->corrected_answer_bn, $candidate->corrected_answer_en, $candidate->corrected_answer, $context['language']),
                    'source_type' => 'correction',
                    'source_id' => $candidate->id,
                    'matched_keyword' => $candidate->question,
                    'confidence_score' => 0,
                ];
            },
            $context
        );
    }

    protected function findKnowledgeReply(array $context): ?array
    {
        return $this->pickBestTextCandidate(
            KnowledgeBaseEntry::query()
                ->where('is_active', true)
                ->orderByDesc('priority')
                ->latest('updated_at')
                ->get(),
            [
                'title',
                'tags',
                'question_patterns',
                'answer',
                'answer_bn',
                'answer_en',
            ],
            function (KnowledgeBaseEntry $candidate) use ($context) {
                $detailedAnswer = $this->selectLocalizedText($candidate->answer_bn, $candidate->answer_en, $candidate->answer, $context['language']);

                return [
                    'reply' => $context['detailed'] ? $detailedAnswer : $this->shortenText($detailedAnswer, 180),
                    'reply_short' => $this->shortenText($detailedAnswer, 180),
                    'reply_detailed' => $detailedAnswer,
                    'source_type' => 'knowledge',
                    'source_id' => $candidate->id,
                    'matched_keyword' => $candidate->title,
                    'confidence_score' => 0,
                ];
            },
            $context
        );
    }

    protected function findFaqReply(array $context): ?array
    {
        return $this->pickBestTextCandidate(
            ChatFaq::query()
                ->where('is_active', true)
                ->orderByDesc('priority')
                ->latest('updated_at')
                ->get(),
            [
                'question',
                'question_bn',
                'question_en',
                'answer',
                'answer_bn',
                'answer_en',
            ],
            function (ChatFaq $candidate) use ($context) {
                $detailedAnswer = $this->selectLocalizedText($candidate->answer_bn, $candidate->answer_en, $candidate->answer, $context['language']);

                return [
                    'reply' => $context['detailed'] ? $detailedAnswer : $this->shortenText($detailedAnswer, 170),
                    'reply_short' => $this->shortenText($detailedAnswer, 170),
                    'reply_detailed' => $detailedAnswer,
                    'source_type' => 'faq',
                    'source_id' => $candidate->id,
                    'matched_keyword' => $this->selectLocalizedText($candidate->question_bn, $candidate->question_en, $candidate->question, $context['language']),
                    'confidence_score' => 0,
                ];
            },
            $context
        );
    }

    protected function findOfferReply(array $context): ?array
    {
        $offers = ChatOffer::query()
            ->activeWindow()
            ->orderByDesc('priority')
            ->latest('updated_at')
            ->get();

        if ($offers->isEmpty()) {
            return null;
        }

        if (in_array($context['query_type'], ['offer', 'discount', 'campaign'], true)) {
            return $this->formatOfferCollectionReply($offers, $context, 98);
        }

        return $this->pickBestTextCandidate(
            $offers,
            [
                'title',
                'title_bn',
                'title_en',
                'description',
                'description_bn',
                'description_en',
            ],
            fn (ChatOffer $candidate) => $this->formatSingleOfferReply($candidate, $context),
            $context
        );
    }

    protected function findProductReply(array $context): ?array
    {
        $queryType = $context['query_type'] ?? 'general';
        if (!in_array($queryType, ['product', 'price', 'general'], true)) {
            return null;
        }

        $tokens   = $context['product_tokens'] ?? $this->extractProductTokens($context['question']);
        $language = $context['language'];
        $searchMode = $context['product_search_mode'] ?? $this->detectProductSearchMode($context['question']);
        $answerIntent = $context['product_answer_intent'] ?? $this->detectProductAnswerIntent($context['question'], $queryType, $searchMode);

        $meaningfulTokens = array_values(array_filter(
            $tokens,
            fn ($t) => !in_array($t, self::STOP_WORDS, true)
        ));

        if (empty($meaningfulTokens)
            && ($searchMode === 'browse' || $answerIntent === 'browse')
            && ($context['query_type'] ?? 'general') === 'product') {
            return $this->productCatalogBrowseReply($context);
        }

        if (empty($meaningfulTokens) && in_array($context['query_type'] ?? 'general', ['price', 'product'], true)) {
            return $this->productClarificationReply($context, $answerIntent);
        }

        if (empty($meaningfulTokens) && $context['query_type'] !== 'product') {
            return null;
        }

        $aiTerms     = $context['product_search_terms'] ?? $this->extractProductSearchTermsWithAI($context['question'], $language);
        $searchTerms = $this->prepareProductSearchTerms($aiTerms, $meaningfulTokens);

        if (empty($searchTerms)) {
            return in_array($context['query_type'] ?? 'general', ['price', 'product'], true)
                ? $this->productClarificationReply($context, $answerIntent)
                : null;
        }

        $productSearchResult = $this->searchProductsInDb($searchTerms, $meaningfulTokens);

        [
            'products'   => $products,
            'match_type' => $matchType,
            'match_pct'  => $matchPct,
            'attempt'    => $queryAttempt,
        ] = $productSearchResult;

        // Admin-configured minimum match threshold
        $minThreshold = (int) $this->getIntSetting('ai_chat_product_match_threshold', 0);

        // Nothing found in DB, or match quality below admin threshold → honest "not found" reply
        $isAlternativeMatch = $matchType === 'category_alternative';
        $notFound = $products->isEmpty() || (!$isAlternativeMatch && $minThreshold > 0 && $matchPct < $minThreshold);
        if ($notFound) {
            if ($context['query_type'] === 'product' || count($meaningfulTokens) >= 2) {
                $searched = implode(' ', !empty($searchTerms) ? $searchTerms : $meaningfulTokens);
                $reply    = $language === 'bn'
                    ? "এই তথ্যটি আমি নিশ্চিতভাবে খুঁজে পাইনি। আপনি কি \"{$searched}\"-এর কোনো নির্দিষ্ট মডেল/ব্র্যান্ড বুঝিয়েছেন? অনুগ্রহ করে product name/model দিন।"
                    : "I could not verify this product. Did you mean a specific model or brand for \"{$searched}\"? Please share the product name/model.";
                if ($searchMode === 'browse') {
                    $reply = $language === 'bn'
                        ? "এই তথ্যটি আমি নিশ্চিতভাবে খুঁজে পাইনি। \"{$searched}\"-এর আরেকটু নির্দিষ্ট product name/model/brand দিন।"
                        : "I could not verify a matching product for \"{$searched}\". Please share a more specific product name/model/brand.";
                }

                return [
                    'reply'            => $reply,
                    'reply_short'      => $reply,
                    'reply_detailed'   => $reply,
                    'source_type'      => 'product_not_found',
                    'source_id'        => null,
                    'matched_keyword'  => $searched,
                    'confidence_score' => $matchPct,
                    'match_pct'        => $matchPct,
                    'query_attempt'    => $queryAttempt,
                    'product_search_terms' => $searchTerms,
                    'agent_context'    => $context['agent_context'] ?? null,
                    'products'         => [],
                ];
            }
            return null;
        }

        try {
            $primaryProduct = $products->first();
            $alternatives = collect();
            if ($primaryProduct && (int) ($primaryProduct->stock ?? 0) <= 0 && ($context['settings']['product_recommendation_enabled'] ?? true)) {
                $alternatives = $this->findAlternativeProducts($primaryProduct, 3);
            }

            $textReply = $isAlternativeMatch
                ? $this->formatAlternativeProductTextReply($products, $language, $answerIntent, $productSearchResult)
                : ($alternatives->isNotEmpty() && $products->count() === 1
                    ? $this->formatOutOfStockProductReply($primaryProduct, $alternatives, $language, $answerIntent, $context['settings']['sales_mode_enabled'] ?? true)
                    : $this->formatProductTextReply($products, $language, $matchType, $searchMode, $answerIntent, $context['settings']['sales_mode_enabled'] ?? true));

            $productCards = $products->map(function (Item $item) {
                try {
                    return $this->buildProductCard($item);
                } catch (\Throwable $e) {
                    Log::warning('AiChat buildProductCard failed for item #' . $item->id . ': ' . $e->getMessage());
                    return [
                        'id'    => $item->id,
                        'name'  => $item->name,
                        'price' => (float) ($item->price ?? 0),
                    ];
                }
            })->merge($alternatives->map(function (Item $item) {
                try {
                    return $this->buildProductCard($item) + ['is_alternative' => true];
                } catch (\Throwable $e) {
                    return ['id' => $item->id, 'name' => $item->name, 'is_alternative' => true];
                }
            }))->values()->all();

            if (!$isAlternativeMatch && !($context['settings']['verified_reply_only'] ?? true)) {
                $textReply = $this->composeProductReplyWithAI(
                    $context['question'],
                    $language,
                    $answerIntent,
                    $productCards,
                    $textReply
                );
            }

            $fullReply = $textReply;
        } catch (\Throwable $e) {
            Log::warning('AiChat product reply build failed: ' . $e->getMessage());
            $textReply    = $products->first()->name . ($language === 'bn' ? ' — দাম জানতে আমাদের সাথে যোগাযোগ করুন।' : ' — Please contact us for pricing.');
            $fullReply    = $textReply;
            $productCards = [];
        }

        return [
            'reply'            => $fullReply,
            'reply_text'       => $textReply,
            'reply_short'      => $textReply,
            'reply_detailed'   => $textReply,
            'source_type'      => 'product',
            'source_id'        => $products->first()->id,
            'matched_keyword'  => $products->first()->name,
            'confidence_score' => $matchPct,
            'match_pct'        => $matchPct,
            'query_attempt'    => $queryAttempt,
            'matched_product_ids' => $products->pluck('id')->merge($alternatives->pluck('id'))->unique()->values()->all(),
            'product_answer_intent' => $answerIntent,
            'product_search_terms' => $searchTerms,
            'alternative_for' => $productSearchResult['alternative_for'] ?? null,
            'alternative_brands' => $productSearchResult['alternative_brands'] ?? [],
            'agent_context'    => $context['agent_context'] ?? null,
            'products'         => $productCards,
        ];
    }

    protected function productClarificationReply(array $context, string $answerIntent = 'summary'): array
    {
        $language = $context['language'] ?? 'en';

        if ($answerIntent === 'price') {
            $reply = $language === 'bn'
                ? 'কোন পণ্যের দাম জানতে চান? অনুগ্রহ করে পণ্যের নাম, ব্র্যান্ড বা মডেল লিখুন।'
                : 'Which product price would you like to know? Please share the product name, brand, or model.';
        } elseif ($answerIntent === 'availability') {
            $reply = $language === 'bn'
                ? 'কোন পণ্যের স্টক জানতে চান? অনুগ্রহ করে পণ্যের নাম, ব্র্যান্ড বা মডেল লিখুন।'
                : 'Which product availability would you like to check? Please share the product name, brand, or model.';
        } else {
            $reply = $language === 'bn'
                ? 'কোন পণ্যের তথ্য জানতে চান? অনুগ্রহ করে পণ্যের নাম, ব্র্যান্ড বা মডেল লিখুন।'
                : 'Which product would you like information about? Please share the product name, brand, or model.';
        }

        return [
            'reply'            => $reply,
            'reply_short'      => $reply,
            'reply_detailed'   => $reply,
            'source_type'      => 'product_clarification',
            'source_id'        => null,
            'matched_keyword'  => null,
            'confidence_score' => 20,
            'match_pct'        => 0,
            'query_attempt'    => 0,
            'product_answer_intent' => $answerIntent,
            'product_search_terms' => $context['product_search_terms'] ?? [],
            'agent_context'    => $context['agent_context'] ?? null,
            'products'         => [],
        ];
    }

    protected function productCatalogBrowseReply(array $context): array
    {
        $language = $context['language'] ?? 'en';
        $categories = collect();

        try {
            if (Schema::hasTable('categories') && Schema::hasTable('items')) {
                $categories = \DB::table('categories')
                    ->join('items', 'items.category_id', '=', 'categories.id')
                    ->where('categories.status', 1)
                    ->where('items.status', 1)
                    ->where('items.is_approved', 1)
                    ->where('items.stock', '>', 0)
                    ->select('categories.name', \DB::raw('COUNT(items.id) as products_count'))
                    ->groupBy('categories.id', 'categories.name')
                    ->orderByDesc('products_count')
                    ->limit(8)
                    ->pluck('categories.name')
                    ->filter()
                    ->values();
            }
        } catch (\Throwable $e) {
            Log::warning('AiChat category browse reply failed: ' . $e->getMessage());
        }

        if ($categories->isNotEmpty()) {
            $reply = $language === 'bn'
                ? 'আমাদের কাছে এই ধরনের পণ্য আছে: ' . $categories->implode(', ') . '। কোন পণ্য/ব্র্যান্ড/মডেল খুঁজছেন লিখুন, আমি দাম ও স্টক দেখিয়ে দেব।'
                : 'We have products in categories like: ' . $categories->implode(', ') . '. Tell me the product, brand, or model you need and I will check price and stock.';
        } else {
            $reply = $language === 'bn'
                ? 'আপনি কোন ধরনের পণ্য খুঁজছেন? পণ্যের নাম, ব্র্যান্ড বা মডেল লিখুন, আমি দাম ও স্টক দেখিয়ে দেব।'
                : 'What type of product are you looking for? Share the product name, brand, or model and I will check price and stock.';
        }

        return [
            'reply' => $reply,
            'reply_short' => $reply,
            'reply_detailed' => $reply,
            'source_type' => 'product_catalog_browse',
            'source_id' => null,
            'matched_keyword' => null,
            'confidence_score' => 80,
            'match_pct' => 0,
            'query_attempt' => 0,
            'product_answer_intent' => 'browse',
            'product_search_terms' => $context['product_search_terms'] ?? [],
            'agent_context' => $context['agent_context'] ?? null,
            'products' => [],
        ];
    }

    protected function composeProductReplyWithAI(string $question, string $language, string $answerIntent, array $productCards, string $fallbackReply): string
    {
        $apiKey = config('openai.api_key');
        if (empty($apiKey) || empty($productCards)) {
            return $fallbackReply;
        }

        $compactProducts = array_map(static function (array $product): array {
            return [
                'name' => $product['name'] ?? null,
                'price' => $product['price'] ?? null,
                'offer_price' => $product['offer_price'] ?? null,
                'in_stock' => $product['in_stock'] ?? null,
                'stock' => $product['stock'] ?? null,
                'category' => $product['category'] ?? null,
                'free_delivery' => $product['free_delivery'] ?? null,
                'delivery_charge' => $product['delivery_charge'] ?? null,
                'packaging_charge' => $product['packaging_charge'] ?? null,
                'description' => $product['description'] ?? null,
                'link' => $product['link'] ?? null,
            ];
        }, array_slice($productCards, 0, 5));

        $cacheKey = 'ai_product_reply_v1_' . md5($language . '|' . $answerIntent . '|' . $question . '|' . json_encode($compactProducts));

        return Cache::remember($cacheKey, 120, function () use ($question, $language, $answerIntent, $compactProducts, $fallbackReply) {
            try {
                $systemPrompt = implode("\n", [
                    'You are a careful e-commerce customer service assistant for Ammar.',
                    'Answer only from the provided product JSON. Do not invent prices, stock, delivery, models, or policies.',
                    'If the user asks price, focus on price and offer price.',
                    'If the user asks availability, focus on stock status.',
                    'If the user asks details, include useful product details and link.',
                    'If multiple products are provided, list the best matches briefly.',
                    'Never include raw JSON, [PRODUCT_CARDS], markdown tables, or hidden metadata.',
                    $language === 'bn' ? 'Reply in Bangla.' : 'Reply in concise customer-friendly English.',
                ]);

                $response = OpenAI::chat()->create([
                    'model'       => 'gpt-4o-mini',
                    'max_tokens'  => 260,
                    'temperature' => 0.2,
                    'messages'    => [
                        ['role' => 'system', 'content' => $systemPrompt],
                        ['role' => 'user', 'content' => json_encode([
                            'customer_question' => $question,
                            'answer_intent' => $answerIntent,
                            'products' => $compactProducts,
                        ], JSON_UNESCAPED_UNICODE)],
                    ],
                ]);

                $reply = trim((string) ($response->choices[0]->message->content ?? ''));
                if ($reply === '' || str_contains($reply, '[PRODUCT_CARDS]') || str_contains($reply, '{')) {
                    return $fallbackReply;
                }

                return $this->cleanReplyText($reply);
            } catch (\Throwable $e) {
                Log::warning('OpenAI product reply composition failed: ' . $e->getMessage());
                return $fallbackReply;
            }
        });
    }

    protected function extractProductSearchTermsWithAI(string $question, string $language): array
    {
        $apiKey = config('openai.api_key');
        if (empty($apiKey)) {
            return [];
        }

        $cacheVersion = (string) Cache::get('ai_product_cache_version', 'v1');
        $cacheKey = 'ai_product_terms_v2_' . $cacheVersion . '_' . md5($language . '|' . $question);
        return Cache::remember($cacheKey, 300, function () use ($question, $language) {
            try {
                $systemPrompt = "You are an e-commerce product-search parser. Extract only catalog terms: product type, category, brand, model, size, color, material, or key attributes. Understand Bangla, English, and Banglish. Convert common Bangla product words to English catalog words when useful, e.g. কিয়াম=kiam, রাইস=rice, কুকার=cooker, প্রেসার=pressure, ডিশ=dish, সেট=set. Remove intent/filler words like ki, ki ki, ache, ase, available, stock, price, dekhte chai, show me, দাম, কত, প্রাইস. Return only space-separated keywords. Examples: 'dish set ki ki ache' => 'dish set'; 'কিয়াম রাইস কুকারের প্রাইস কত' => 'kiam rice cooker'; 'Please share product price information' => ''. If there is no specific product/category/brand/model, return empty text.";

                $response = OpenAI::chat()->create([
                    'model'       => 'gpt-4o-mini',
                    'max_tokens'  => 40,
                    'temperature' => 0.1,
                    'messages'    => [
                        ['role' => 'system', 'content' => $systemPrompt],
                        ['role' => 'user',   'content' => $question],
                    ],
                ]);

                $raw = trim($response->choices[0]->message->content ?? '');
                if (empty($raw)) return [];

                return array_values(array_filter(
                    explode(' ', $this->normalizeText($raw)),
                    static fn ($t) => mb_strlen($t) >= 2
                ));
            } catch (\Throwable $e) {
                Log::warning('OpenAI product term extraction failed: ' . $e->getMessage());
                return [];
            }
        });
    }

    /**
     * Load all active brand names from DB as lowercase → id map.
     * Cached 1 hour. Used for token classification.
     * e.g. ['kiam' => 5, 'skb' => 12, 'akij' => 3]
     */
    protected function getBrandsMap(): array
    {
        return Cache::remember('ai_catalog_brands', self::TTL_BRANDS, function () {
            if (!Schema::hasTable('brands')) {
                return [];
            }

            $map = [];
            \DB::table('brands')->where('status', 1)->orderBy('id')->each(function ($row) use (&$map) {
                $lower = mb_strtolower(trim($row->name));
                if ($lower !== '') {
                    $map[$lower] = (int) $row->id;
                }
            });
            return $map;
        });
    }

    /**
     * Load all active category names (parent + sub) as lowercase → id map.
     * Cached 1 hour. Used for token classification.
     * e.g. ['rice cooker' => 22, 'pressure cooker' => 45, 'cookware' => 10]
     */
    protected function getCategoriesMap(): array
    {
        return Cache::remember('ai_catalog_categories', self::TTL_CATEGORIES, function () {
            if (!Schema::hasTable('categories')) {
                return [];
            }

            $map = [];
            \DB::table('categories')->where('status', 1)->orderBy('id')->each(function ($row) use (&$map) {
                $lower = mb_strtolower(trim($row->name));
                if ($lower !== '') {
                    $map[$lower] = (int) $row->id;
                }
            });
            return $map;
        });
    }

    protected function extractProductTokens(string $text): array
    {
        return $this->prepareProductSearchTerms([], explode(' ', $this->normalizeText($text)));
    }

    protected function prepareProductSearchTerms(array $aiTerms, array $fallbackTokens): array
    {
        $terms = array_merge($aiTerms, $fallbackTokens);

        $terms = array_map(fn ($term) => $this->mapProductSearchTerm($this->normalizeText((string) $term)), $terms);
        $terms = array_filter($terms, function (string $term) {
            return $term !== ''
                && mb_strlen($term) >= 2
                && !in_array($term, self::STOP_WORDS, true);
        });

        usort($terms, static fn ($a, $b) => mb_strlen($b) <=> mb_strlen($a));

        return array_values(array_unique($terms));
    }

    protected function mapProductSearchTerm(string $term): string
    {
        if ($term === '') {
            return '';
        }

        $term = str_replace(['’', "'"], '', $term);
        $term = preg_replace('/^(0p)([0-9a-z]+)/u', 'op$2', $term) ?? $term;

        if (isset(self::BANGLA_PRODUCT_TERMS[$term])) {
            return self::BANGLA_PRODUCT_TERMS[$term];
        }

        foreach (self::BANGLA_PRODUCT_TERMS as $bangla => $english) {
            if (str_contains($term, $bangla)) {
                return $english;
            }
        }

        return $term;
    }

    /**
     * Classify search tokens into brand / category / product-name buckets.
     *
     * Algorithm:
     *  1. Try bigrams (2-token combinations) against category map first.
     *     e.g. "rice cooker" matches Category("Rice Cooker")
     *  2. Try each remaining single token against brand map (partial/exact).
     *     e.g. "kiam" matches Brand("Kiam")
     *  3. Try each remaining single token against category map (partial/exact).
     *     e.g. "cooker" partial-matches Category("Rice Cooker")
     *  4. Remaining tokens go into name_terms for LIKE search.
     *
     * Returns:
     *   [
     *     'brand_ids'  => [5, 12],        // matched brand IDs
     *     'cat_ids'    => [22, 45],        // matched category IDs
     *     'name_terms' => ['5704', 'xl'],  // leftover for name LIKE search
     *     'brand_names' => ['kiam'],       // original brand token strings
     *   ]
     */
    protected function classifyTokens(array $tokens): array
    {
        $tokens = $this->prepareProductSearchTerms([], $tokens);
        $brandMap = $this->getBrandsMap();
        $catMap   = $this->getCategoriesMap();

        $brandIds   = [];
        $catIds     = [];
        $brandNames = [];
        $nameTerms  = [];
        $used       = array_fill(0, count($tokens), false);

        // Pass 1: bigrams → category match
        for ($size = min(4, count($tokens)); $size >= 2; $size--) {
            for ($i = 0; $i <= count($tokens) - $size; $i++) {
                if (count(array_filter(array_slice($used, $i, $size))) > 0) {
                    continue;
                }

                $phrase = implode(' ', array_slice($tokens, $i, $size));
                if (isset($catMap[$phrase])) {
                    $catIds[] = $catMap[$phrase];
                    for ($j = $i; $j < $i + $size; $j++) {
                        $used[$j] = true;
                    }
                }
            }
        }

        // Pass 2: single tokens → brand or category
        foreach ($tokens as $i => $token) {
            if ($used[$i]) {
                continue;
            }

            // Exact brand match
            if (isset($brandMap[$token])) {
                $brandIds[]   = $brandMap[$token];
                $brandNames[] = $token;
                $used[$i]     = true;
                continue;
            }

            // Partial brand match: token is substring of a brand name or vice-versa
            $foundBrand = false;
            if (mb_strlen($token) >= 3) {
                foreach ($brandMap as $brandName => $brandId) {
                    if (str_contains($brandName, $token) || str_contains($token, $brandName) || $this->fuzzyTokenMatchesField($token, $brandName)) {
                        $brandIds[]   = $brandId;
                        $brandNames[] = $brandName;
                        $used[$i]     = true;
                        $foundBrand   = true;
                        break;
                    }
                }
            }
            if ($foundBrand) {
                continue;
            }

            // Exact category match
            if (isset($catMap[$token])) {
                $catIds[] = $catMap[$token];
                $used[$i] = true;
                continue;
            }

            // Partial category match: token is substring of a category name
            if (mb_strlen($token) >= 3) {
                foreach ($catMap as $catName => $catId) {
                    if (str_contains($catName, $token) || $this->fuzzyTokenMatchesField($token, $catName)) {
                        $catIds[] = $catId;
                        $used[$i] = true;
                        break;
                    }
                }
            }
        }

        // Remaining tokens → name LIKE terms
        foreach ($tokens as $i => $token) {
            if (!$used[$i]) {
                $nameTerms[] = $token;
            }
        }

        return [
            'brand_ids'   => array_values(array_unique($brandIds)),
            'cat_ids'     => array_values(array_unique($catIds)),
            'name_terms'  => $nameTerms,
            'brand_names' => $brandNames,
        ];
    }

    /**
     * Weighted relevance score — Elasticsearch BM25-inspired, pure PHP.
     *
     * Scoring hierarchy:
     *   1. Brand match (via DB join)              → +30
     *   2. Category match (via DB filter)         → +15
     *   3. Each query token matched in name       → +10 + charLen*2  (character-weighted)
     *      Position bonus (token starts name)     → +8
     *   4. Each token matched in sub-category     → +6 + charLen
     *   5. Each token matched in parent category  → +4 + charLen/2
     *   6. Coverage bonus (% of tokens matched)   → quadratic up to +40
     *      Forces products matching MORE tokens to the top.
     *
     * Result range: 0 → ~200+. Higher = better match.
     */
    protected function computeRelevanceScore(Item $item, array $tokens, array $classified): int
    {
        $tokens    = $this->prepareProductSearchTerms([], $tokens);
        $name      = $this->normalizeText((string) ($item->name ?? ''));
        // _cat_name and _parent_cat_name come from LEFT JOIN in the base query
        $catName   = $this->normalizeText((string) ($item->_cat_name ?? $item->category?->name ?? ''));
        $parentCat = $this->normalizeText((string) ($item->_parent_cat_name ?? $item->category?->parent?->name ?? ''));
        $brandName = $this->normalizeText((string) ($item->_brand_name ?? $item->ecommerce_item_details?->brand?->name ?? ''));
        $slug      = $this->normalizeText((string) ($item->slug ?? ''));
        $desc      = $this->normalizeText(strip_tags((string) ($item->description ?? '')));
        $skuText   = $this->normalizeText((string) $this->firstFilled([
            $item->getAttribute('item_code'),
            $item->getAttribute('code'),
            $item->getAttribute('sku'),
            $this->firstVariationValue($item, 'sku'),
        ]));
        $variantText = $this->normalizeText(implode(' ', $this->variantSummary($item)));
        $score     = 0;

        // Structural signals (from DB joins — not guessed)
        $brandId = (int) ($item->_brand_id ?? $item->ecommerce_item_details?->brand_id ?? 0);
        if ($brandId > 0 && in_array($brandId, $classified['brand_ids'] ?? [], true)) {
            $score += 45;
        }

        $catId = (int) ($item->category_id ?? 0);
        $parentCatId = (int) ($item->_parent_cat_id ?? 0);
        if ($catId > 0 && in_array($catId, $classified['cat_ids'] ?? [], true)) {
            $score += 32;
        } elseif ($parentCatId > 0 && in_array($parentCatId, $classified['cat_ids'] ?? [], true)) {
            $score += 22;
        }

        $matchedTokens = 0;
        $totalTokens   = count($tokens);
        $queryPhrase   = implode(' ', $tokens);

        if ($queryPhrase !== '') {
            if ($name === $queryPhrase) {
                $score += 120;
            } elseif (str_contains($name, $queryPhrase)) {
                $score += 70;
            } elseif ($this->fuzzyPhraseScore($queryPhrase, $name) >= 82) {
                $score += 42;
            }
        }

        foreach ($tokens as $token) {
            $tLen     = mb_strlen($token);
            $inName   = $this->fieldContainsToken($name, $token);
            $inCat    = $this->fieldContainsToken($catName, $token);
            $inParent = $this->fieldContainsToken($parentCat, $token);
            $inBrand  = $this->fieldContainsToken($brandName, $token);
            $inSlug   = $this->fieldContainsToken($slug, $token);
            $inDesc   = $this->fieldContainsToken($desc, $token);
            $inSku    = $this->fieldContainsToken($skuText, $token) || $this->fieldContainsToken($this->normalizeModelToken($skuText), $this->normalizeModelToken($token));
            $inVariant = $this->fieldContainsToken($variantText, $token);

            if ($inSku) {
                $matchedTokens++;
                $score += 75 + $tLen * 4;
            }

            if ($inName) {
                $matchedTokens++;
                $score += 24 + $tLen * 3;                  // longer match = more specific
                if (str_starts_with($name, $token)) {
                    $score += 14;                           // word at start of name = strong signal
                }
            } elseif ($this->fuzzyTokenMatchesField($token, $name)) {
                $matchedTokens++;
                $score += 16 + $tLen * 2;
            }
            if ($inBrand && !$inName) {
                $matchedTokens++;
                $score += 22 + $tLen * 2;
            } elseif (!$inName && $this->fuzzyTokenMatchesField($token, $brandName)) {
                $matchedTokens++;
                $score += 16 + $tLen;
            }
            if ($inCat && !$inName && !$inBrand) {
                $matchedTokens++;
                $score += 16 + $tLen * 2;
            } elseif (!$inName && !$inBrand && $this->fuzzyTokenMatchesField($token, $catName)) {
                $matchedTokens++;
                $score += 12 + $tLen;
            }
            if ($inParent && !$inName && !$inBrand && !$inCat) {
                $matchedTokens++;
                $score += 12 + $tLen;
            } elseif (!$inName && !$inBrand && !$inCat && $this->fuzzyTokenMatchesField($token, $parentCat)) {
                $matchedTokens++;
                $score += 8 + $tLen;
            }
            if ($inSlug && !$inName && !$inBrand && !$inCat && !$inParent) {
                $matchedTokens++;
                $score += 10 + $tLen;
            }
            if ($inDesc && !$inName && !$inBrand && !$inCat && !$inParent && !$inSlug) {
                $matchedTokens++;
                $score += 4 + $tLen;
            }
            if ($inVariant && !$inSku && !$inName && !$inBrand && !$inCat) {
                $matchedTokens++;
                $score += 12 + $tLen;
            }
        }

        $matchedTokens = $this->countCoveredProductTokens($tokens, [$name, $catName, $parentCat, $brandName, $slug, $skuText, $variantText], $desc);
        $coverage = 0.0;
        // Coverage bonus: quadratic so 4/4 tokens >> 2/4 tokens >> 1/4 tokens
        if ($totalTokens > 0) {
            $coverage = min(1, $matchedTokens / $totalTokens);
            $score   += (int) ($coverage * $coverage * 60);
        }

        $item->_matched_token_count = $matchedTokens;
        $item->_match_coverage = $coverage;

        $score += min(12, (int) (($item->recommended ?? 0) * 4));
        $score += min(10, (int) floor(((int) ($item->order_count ?? 0)) / 20));

        return $score;
    }

    protected function passesProductCoverageGate(Item $item, array $terms): bool
    {
        $terms = $this->prepareProductSearchTerms([], $terms);
        $termCount = count($terms);
        if ($termCount === 0) {
            return false;
        }

        $criticalTokens = $this->criticalProductTokens($terms);
        if (!empty($criticalTokens) && !$this->itemContainsCriticalProductTokens($item, $criticalTokens)) {
            return false;
        }

        $matched = (int) ($item->_matched_token_count ?? 0);
        if ($termCount === 1) {
            return $matched >= 1;
        }

        $required = $termCount === 2 ? 2 : (int) ceil($termCount * 0.75);
        return $matched >= $required;
    }

    protected function hasCriticalProductToken(array $terms): bool
    {
        return !empty($this->criticalProductTokens($terms));
    }

    protected function criticalProductTokens(array $terms): array
    {
        $terms = $this->prepareProductSearchTerms([], $terms);

        return array_values(array_filter($terms, static function (string $term) {
            return (bool) preg_match('/\d/u', $term)
                && !in_array($term, ['18', '28', '55', '105'], true);
        }));
    }

    protected function itemContainsCriticalProductTokens(Item $item, array $criticalTokens): bool
    {
        $fields = [
            $this->normalizeText((string) ($item->name ?? '')),
            $this->normalizeText((string) ($item->slug ?? '')),
            $this->normalizeText((string) ($item->_cat_name ?? $item->category?->name ?? '')),
            $this->normalizeText((string) ($item->_parent_cat_name ?? $item->category?->parent?->name ?? '')),
            $this->normalizeText((string) ($item->_brand_name ?? $item->ecommerce_item_details?->brand?->name ?? '')),
            $this->normalizeText((string) $this->firstFilled([
                $item->getAttribute('item_code'),
                $item->getAttribute('code'),
                $item->getAttribute('sku'),
                $this->firstVariationValue($item, 'sku'),
            ])),
            $this->normalizeText(implode(' ', $this->variantSummary($item))),
            $this->normalizeText(strip_tags((string) ($item->description ?? ''))),
        ];

        foreach ($criticalTokens as $token) {
            $needle = $this->normalizeModelToken($token);
            $found = false;
            foreach ($fields as $field) {
                $haystack = $this->normalizeModelToken($field);
                if ($needle !== '' && str_contains($haystack, $needle)) {
                    $found = true;
                    break;
                }
            }

            if (!$found) {
                return false;
            }
        }

        return true;
    }

    protected function normalizeModelToken(string $value): string
    {
        $value = $this->normalizeText($value);
        $value = preg_replace('/\s+/u', '', $value) ?? $value;

        if (preg_match('/[a-z]/i', $value)) {
            $value = str_replace('0', 'o', $value);
        }

        return $value;
    }

    protected function countCoveredProductTokens(array $tokens, array $fields, string $description = ''): int
    {
        $matched = 0;

        foreach ($tokens as $token) {
            $tokenMatched = false;

            foreach ($fields as $field) {
                if ($this->fieldContainsToken($field, $token) || $this->fuzzyTokenMatchesField($token, $field)) {
                    $tokenMatched = true;
                    break;
                }
            }

            if (!$tokenMatched && $this->fieldContainsToken($description, $token)) {
                $tokenMatched = true;
            }

            if ($tokenMatched) {
                $matched++;
            }
        }

        return $matched;
    }

    protected function fieldContainsToken(string $field, string $token): bool
    {
        return $field !== '' && $token !== '' && str_contains($field, $token);
    }

    protected function fuzzyTokenMatchesField(string $token, string $field): bool
    {
        if ($token === '' || $field === '' || mb_strlen($token) < 3) {
            return false;
        }

        foreach (explode(' ', $field) as $word) {
            $word = trim($word);
            if ($word === '') {
                continue;
            }

            if ($word === $token || str_contains($word, $token) || str_contains($token, $word)) {
                return true;
            }

            $tokenLength = mb_strlen($token);
            $wordLength = mb_strlen($word);
            if (abs($tokenLength - $wordLength) > max(2, (int) floor($tokenLength * 0.35))) {
                continue;
            }

            $distance = levenshtein($token, $word);
            $allowedDistance = $tokenLength <= 4 ? 1 : max(1, (int) floor($tokenLength * 0.28));
            if ($distance <= $allowedDistance) {
                return true;
            }

            similar_text($token, $word, $percent);
            if ($percent >= 82) {
                return true;
            }
        }

        return false;
    }

    protected function fuzzyPhraseScore(string $queryPhrase, string $field): int
    {
        if ($queryPhrase === '' || $field === '') {
            return 0;
        }

        similar_text($queryPhrase, $field, $percent);
        return (int) round($percent);
    }

    /**
     * Search products with up to 3 progressive attempts.
     * Each attempt fetches candidates via SQL then ranks them with computeRelevanceScore().
     *
     * The base query LEFT JOINs sub-category and parent-category so the PHP scorer
     * can weight matches across product name, category, and parent category together.
     *
     * Attempt 1 — Brand JOIN + Category filter + text OR (name/cat/parent_cat)
     * Attempt 2 — Brand OR Category (relax other constraint) + text OR
     * Attempt 3 — Pure text OR across name + category + parent category
     *
     * Returns: ['products', 'match_type', 'match_pct' (0-100), 'attempt' (1|2|3)]
     */
    protected function searchProductsInDb(array $searchTerms, array $fallbackTokens): array
    {
        $terms = !empty($searchTerms) ? $searchTerms : $fallbackTokens;
        if (empty($terms)) {
            return ['products' => collect(), 'match_type' => 'none', 'match_pct' => 0, 'attempt' => 0];
        }
        if (!Schema::hasTable('items')) {
            return ['products' => collect(), 'match_type' => 'none', 'match_pct' => 0, 'attempt' => 0];
        }

        try {
            $classified = $this->classifyTokens($terms);
            $brandIds   = $classified['brand_ids'];
            $catIds     = $classified['cat_ids'];
            $nameTerms  = $classified['name_terms'];
            $hasBrand   = !empty($brandIds);
            $hasCat     = !empty($catIds);
            $skuColumns = array_values(array_filter(
                ['item_code', 'code', 'sku'],
                static fn (string $column) => Schema::hasColumn('items', $column)
            ));
            $jsonSearchColumns = array_values(array_filter(
                ['variations', 'food_variations', 'choice_options'],
                static fn (string $column) => Schema::hasColumn('items', $column)
            ));

            // Base query: LEFT JOIN sub-category + parent-category tables.
            // _cat_name and _parent_cat_name are used by computeRelevanceScore()
            // so it can weight matches across all three fields (name, cat, parent cat).
            $base = fn () => Item::withoutGlobalScopes()
                ->with(['category:id,name,parent_id', 'unit:id,unit', 'colors:id,name', 'storage', 'ecommerce_item_details.brand:id,name'])
                ->leftJoin('categories as _cat', 'items.category_id', '=', '_cat.id')
                ->leftJoin('categories as _pcat', '_cat.parent_id', '=', '_pcat.id')
                ->leftJoin('ecommerce_item_details as _eid', 'items.id', '=', '_eid.item_id')
                ->leftJoin('brands as _brand', '_eid.brand_id', '=', '_brand.id')
                ->where('items.status', 1)
                ->where('items.is_approved', 1)
                ->select(
                    'items.*',
                    '_cat.name as _cat_name',
                    '_pcat.id as _parent_cat_id',
                    '_pcat.name as _parent_cat_name',
                    '_brand.id as _brand_id',
                    '_brand.name as _brand_name'
                );

            // OR filter: any term must appear in name OR sub-cat OR parent-cat
            $textFilter = function ($q, array $t) use ($skuColumns, $jsonSearchColumns): void {
                $q->where(function ($sq) use ($t, $skuColumns, $jsonSearchColumns) {
                    foreach ($t as $term) {
                        $like = '%' . $term . '%';
                        $sq->orWhere('items.name', 'like', $like)
                           ->orWhere('items.slug', 'like', $like)
                           ->orWhere('items.description', 'like', $like)
                           ->orWhere('_cat.name', 'like', $like)
                           ->orWhere('_pcat.name', 'like', $like)
                           ->orWhere('_brand.name', 'like', $like);
                        foreach ($skuColumns as $column) {
                            $sq->orWhere("items.{$column}", 'like', $like);
                        }
                        foreach ($jsonSearchColumns as $column) {
                            $sq->orWhere("items.{$column}", 'like', $like);
                        }
                    }
                });
            };

            // Theoretical max score for normalising match_pct
            $avgLen   = count($terms) > 0
                ? (int) (array_sum(array_map('mb_strlen', $terms)) / count($terms))
                : 5;
            $maxScore = 30 * (int) $hasBrand
                      + 15 * (int) $hasCat
                      + count($terms) * (28 + $avgLen * 3)
                      + 180;

            // Attempt 0: exact SKU/code/name match. This is deliberately checked
            // before fuzzy/product-name matching so a product code never resolves
            // to a different item's price.
            {
                $exactTerms = array_values(array_unique(array_filter(array_merge($terms, [implode(' ', $terms)]))));
                foreach ($exactTerms as $exactTerm) {
                    $exactTerm = trim((string) $exactTerm);
                    if ($exactTerm === '' || mb_strlen($exactTerm) < 2) {
                        continue;
                    }

                    $q = $base();
                    $q->where(function ($sq) use ($exactTerm, $skuColumns) {
                        $sq->orWhereRaw('LOWER(items.name) = ?', [mb_strtolower($exactTerm)])
                            ->orWhereRaw('LOWER(items.slug) = ?', [mb_strtolower($exactTerm)]);

                        foreach ($skuColumns as $column) {
                            $sq->orWhereRaw("LOWER(items.{$column}) = ?", [mb_strtolower($exactTerm)]);
                        }
                    });

                    $candidates = $q->orderByDesc('items.recommended')->limit(5)->get();
                    if ($candidates->isNotEmpty()) {
                        return [
                            'products' => $candidates,
                            'match_type' => 'exact',
                            'match_pct' => 100,
                            'attempt' => 0,
                        ];
                    }
                }
            }

            // Score candidates and keep only products that match enough query terms.
            $wrap = function (Collection $candidates, int $attempt) use ($terms, $classified, $maxScore): array {
                $scored = $candidates
                    ->map(function (Item $item) use ($terms, $classified) {
                        $item->_match_score = $this->computeRelevanceScore($item, $terms, $classified);
                        return $item;
                    })
                    ->filter(fn (Item $item) => (int) ($item->_match_score ?? 0) >= self::MIN_PRODUCT_SCORE
                        && $this->passesProductCoverageGate($item, $terms))
                    ->sortByDesc('_match_score')
                    ->values();

                $top = $scored->first()?->_match_score ?? 0;
                if ($top === 0) {
                    return ['products' => collect(), 'match_type' => 'none', 'match_pct' => 0, 'attempt' => $attempt];
                }

                $pct  = min(100, (int) round($top / $maxScore * 100));
                if ($pct < self::MIN_PRODUCT_MATCH_PERCENT) {
                    return ['products' => collect(), 'match_type' => 'none', 'match_pct' => $pct, 'attempt' => $attempt];
                }

                $type = $pct >= 85 ? 'exact'
                      : ($pct >= 55 ? 'brand_model'
                      : ($pct >= 25 ? 'brand' : 'partial'));

                return [
                    'products'   => $scored->take(5),
                    'match_type' => $type,
                    'match_pct'  => $pct,
                    'attempt'    => $attempt,
                ];
            };

            // ── Attempt 1: Brand + Category + text (name/cat/parent_cat) ───────
            {
                $q = $base();
                if ($hasBrand) {
                    $q->whereHas('ecommerce_item_details', fn ($sq) => $sq->whereIn('brand_id', $brandIds));
                }
                if ($hasCat) {
                    $q->whereIn('items.category_id', $catIds);
                }
                $textFilter($q, !empty($nameTerms) ? $nameTerms : $terms);
                $candidates = $q->orderByDesc('items.recommended')->limit(50)->get();
                if ($candidates->isNotEmpty()) {
                    $result = $wrap($candidates, 1);
                    if ($result['products']->isNotEmpty()) {
                        return $result;
                    }
                }
            }

            // Brand + category is a specific request. If that exact combination
            // is not available, do not relax to brand-only/category-only results.
            if ($hasBrand && $hasCat) {
                return ['products' => collect(), 'match_type' => 'none', 'match_pct' => 0, 'attempt' => 1];
            }

            // ── Attempt 2: Relax — Brand alone OR Category alone + full text ───
            {
                $q = $base();
                if ($hasBrand) {
                    $q->whereHas('ecommerce_item_details', fn ($sq) => $sq->whereIn('brand_id', $brandIds));
                } elseif ($hasCat) {
                    $q->whereIn('items.category_id', $catIds);
                }
                $textFilter($q, $terms);
                $candidates = $q->orderByDesc('items.recommended')->limit(50)->get();
                if ($candidates->isNotEmpty()) {
                    $result = $wrap($candidates, 2);
                    if ($result['products']->isNotEmpty()) {
                        return $result;
                    }
                }
            }

            // ── Attempt 3: Pure text OR — no brand/category restriction ─────────
            {
                $q = $base();
                $textFilter($q, $terms);
                $candidates = $q->orderByDesc('items.recommended')->limit(50)->get();
                if ($candidates->isNotEmpty()) {
                    return $wrap($candidates, 3);
                }
            }

            // Attempt 4: broad fuzzy fallback. This keeps typo searches useful
            // even when SQL LIKE cannot match the misspelled word.
            {
                $q = $base();
                if ($hasBrand) {
                    $q->whereIn('_brand.id', $brandIds);
                }
                if ($hasCat) {
                    $q->where(function ($sq) use ($catIds) {
                        $sq->whereIn('items.category_id', $catIds)
                            ->orWhereIn('_pcat.id', $catIds);
                    });
                }

                $candidates = $q->orderByDesc('items.recommended')
                    ->orderByDesc('items.order_count')
                    ->limit($hasBrand || $hasCat ? 300 : 500)
                    ->get();

                if ($candidates->isNotEmpty()) {
                    return $wrap($candidates, 4);
                }
            }

            // Attempt 5: use the synced AI product text index as a final semantic
            // fallback. This index is refreshed by admin sync and item save events.
            if (Schema::hasTable('ai_product_indices')) {
                $indexService = app(AiProductIndexService::class);
                $queryEmbedding = $indexService->textEmbeddingForText(implode(' ', $terms));
                $indexedMatches = AiProductIndex::query()
                    ->with(['item' => function ($query) {
                        $query->withoutGlobalScopes()
                            ->with(['category:id,name,parent_id', 'unit:id,unit', 'colors:id,name', 'storage', 'ecommerce_item_details.brand:id,name'])
                            ->where('status', 1)
                            ->where('is_approved', 1);
                    }])
                    ->where(function ($query) use ($terms) {
                        foreach ($terms as $term) {
                            $like = '%' . $term . '%';
                            $query->orWhere('sku', 'like', $like)
                                ->orWhere('brand', 'like', $like)
                                ->orWhere('category', 'like', $like)
                                ->orWhere('normalized_text', 'like', $like);
                        }
                    })
                    ->limit(100)
                    ->get()
                    ->map(function (AiProductIndex $index) use ($indexService, $queryEmbedding) {
                        $index->_vector_score = $indexService->textVectorSimilarity($queryEmbedding, $index->embedding ?? []);
                        return $index;
                    })
                    ->sortByDesc('_vector_score')
                    ->filter(fn (AiProductIndex $index) => $index->item !== null && (float) ($index->_vector_score ?? 0) >= 35)
                    ->values();

                if ($indexedMatches->isNotEmpty()) {
                    return [
                        'products' => $indexedMatches->pluck('item')->filter()->take(5)->values(),
                        'match_type' => 'semantic',
                        'match_pct' => min(100, (int) round((float) ($indexedMatches->first()->_vector_score ?? 0))),
                        'attempt' => 5,
                    ];
                }
            }

            return ['products' => collect(), 'match_type' => 'none', 'match_pct' => 0, 'attempt' => 5];

        } catch (\Throwable $e) {
            Log::warning('AiChat product search failed: ' . $e->getMessage());
            return ['products' => collect(), 'match_type' => 'none', 'match_pct' => 0, 'attempt' => 5];
        }
    }

    protected function buildProductCard(Item $item): array
    {
        // Cache per-product card. Key includes updated_at so cache auto-busts on product edit.
        $ts       = $item->updated_at?->timestamp ?? $item->id;
        $cacheKey = 'ai_product_card_' . $item->id . '_' . $ts;

        return Cache::remember($cacheKey, self::TTL_PRODUCT, fn () => $this->buildProductCardRaw($item));
    }

    protected function buildProductCardRaw(Item $item): array
    {
        $price        = (float) $item->price;
        $discount     = (float) ($item->discount ?? 0);
        $discountType = $item->discount_type ?? 'percent';

        $offerPrice = null;
        if ($discount > 0) {
            $offerPrice = $discountType === 'percent'
                ? round($price - ($price * $discount / 100), 2)
                : round($price - $discount, 2);
        }

        $totalStock = (int) ($item->stock ?? 0);
        $slug       = $item->slug ?? Str::slug($item->name);

        $deliveryCharge  = (float) ($item->item_delivery_charge ?? 0);
        $packagingCharge = (float) ($item->item_packaging_charge ?? 0);
        $freeDelivery    = (bool) ($item->free_delivery_badge_status ?? false);

        $colors = $item->relationLoaded('colors')
            ? $item->colors->pluck('name')->filter()->values()->all()
            : [];

        $frontendUrl = rtrim((string) config('app.front_end_url'), '/');
        $fullDescription = trim(strip_tags((string) ($item->description ?? '')));
        $sku = $this->firstFilled([
            $item->getAttribute('item_code'),
            $item->getAttribute('code'),
            $item->getAttribute('sku'),
            $this->firstVariationValue($item, 'sku'),
        ]);
        $brand = $item->ecommerce_item_details?->brand?->name ?? $item->getAttribute('_brand_name');
        $variants = $this->variantSummary($item);
        $warranty = $this->extractWarrantyText($fullDescription);

        return [
            'id'               => $item->id,
            'name'             => $item->name,
            'sku'              => $sku,
            'brand'            => $brand,
            'slug'             => $slug,
            'link'             => "{$frontendUrl}/product/{$item->id}",
            'image'            => $item->image_full_url ?? null,
            'regular_price'    => $price,
            'discount_price'   => $offerPrice,
            'price'            => $price,
            'offer_price'      => $offerPrice,
            'discount'         => $discount > 0 ? $discount : null,
            'discount_type'    => $discount > 0 ? $discountType : null,
            'in_stock'         => $totalStock > 0,
            'stock'            => $totalStock,
            'category'         => $item->category?->name ?? null,
            'variants'         => $variants,
            'unit'             => $item->unit?->unit ?? null,
            'warranty'         => $warranty,
            'free_delivery'    => $freeDelivery,
            'delivery_charge'  => $deliveryCharge > 0 ? $deliveryCharge : null,
            'packaging_charge' => $packagingCharge > 0 ? $packagingCharge : null,
            'colors'           => $colors,
            'short_description' => $fullDescription ? Str::limit($fullDescription, 120) : null,
            'full_description' => $fullDescription ?: null,
            'description'      => $fullDescription
                ? Str::limit($fullDescription, 120)
                : null,
        ];
    }

    protected function formatProductTextReply(Collection $products, string $language, string $matchType = 'exact', string $searchMode = 'exact', string $answerIntent = 'summary', bool $salesMode = true): string
    {
        $count = $products->count();
        $isBn  = $language === 'bn';

        if ($count === 1) {
            $item  = $products->first();
            $card  = $this->buildProductCard($item);
            $price = $this->formatProductPriceLine($card, $isBn);
            $stock = $card['in_stock']
                ? ($isBn ? 'Available' : 'Available')
                : ($isBn ? 'Out of stock' : 'Out of stock');
            $warranty = $card['warranty'] ?: ($isBn ? 'product data-তে উল্লেখ নেই' : 'not specified in product data');
            $delivery = $this->formatProductDeliveryLine($card, $isBn);
            $packaging = $card['packaging_charge'] ? ($isBn ? "প্যাকেজিং চার্জ: ৳" . number_format($card['packaging_charge'], 2) : "Packaging charge: BDT " . number_format($card['packaging_charge'], 2)) : '';
            $colors = !empty($card['colors']) ? ($isBn ? "রঙ: " . implode(', ', $card['colors']) : "Colors: " . implode(', ', $card['colors'])) : '';
            $variants = !empty($card['variants']) ? ($isBn ? "ভ্যারিয়েন্ট: " . implode(', ', array_slice($card['variants'], 0, 3)) : "Variants: " . implode(', ', array_slice($card['variants'], 0, 3))) : '';
            $benefit = $card['short_description'] ? ($isBn ? "হাইলাইট: {$card['short_description']}" : "Highlight: {$card['short_description']}") : '';
            $link = $card['link'] ? ($isBn ? "অর্ডার লিংক: " . $card['link'] : "Order link: " . $card['link']) : '';

            if ($answerIntent === 'price') {
                return implode("\n", array_filter([
                    $isBn ? "জি, {$card['name']} আমাদের ডাটাবেজে পাওয়া গেছে।" : "Yes, {$card['name']} is in our catalog.",
                    $isBn ? "বর্তমান দাম: {$price}" : "Current price: {$price}",
                    $isBn ? "স্টক: {$stock}" : "Stock: {$stock}",
                    $isBn ? "ওয়ারেন্টি: {$warranty}" : "Warranty: {$warranty}",
                    $delivery,
                    $link,
                    $salesMode ? ($isBn ? "আপনি চাইলে এখনই অর্ডার করতে পারেন, অথবা কাছাকাছি আরও ২-৩টি option দেখতে পারেন।" : "You can order now, or I can show 2-3 similar options.") : '',
                ]));
            }

            if ($answerIntent === 'availability') {
                return implode("\n", array_filter([
                    $isBn ? "জি, {$card['name']} এর stock status:" : "{$card['name']} availability:",
                    $isBn ? "স্টক: {$stock}" : "Stock: {$stock}",
                    $isBn ? "বর্তমান দাম: {$price}" : "Current price: {$price}",
                    $delivery,
                    $link,
                    $salesMode ? ($isBn ? "এই product cart-এ add করতে চান?" : "Would you like to add this product to cart?") : '',
                ]));
            }

            if ($answerIntent === 'details') {
                return implode("\n", array_filter([
                    $isBn ? "জি, {$card['name']} এর verified product information:" : "Verified product information for {$card['name']}:",
                    $card['sku'] ? ($isBn ? "SKU/Code: {$card['sku']}" : "SKU/Code: {$card['sku']}") : '',
                    $card['brand'] ? ($isBn ? "ব্র্যান্ড: {$card['brand']}" : "Brand: {$card['brand']}") : '',
                    $card['category'] ? ($isBn ? "ক্যাটাগরি: {$card['category']}" : "Category: {$card['category']}") : '',
                    $isBn ? "দাম: {$price}" : "Price: {$price}",
                    $isBn ? "স্টক: {$stock}" : "Stock: {$stock}",
                    $isBn ? "ওয়ারেন্টি: {$warranty}" : "Warranty: {$warranty}",
                    $delivery,
                    $packaging,
                    $colors,
                    $variants,
                    $benefit,
                    $link,
                    $salesMode ? ($isBn ? "আপনার বাজেট বললে কাছাকাছি best option দেখাতে পারি।" : "Share your budget and I can suggest the closest best options.") : '',
                ]));
            }

            return implode("\n", array_filter([
                $isBn ? "জি, {$card['name']} আমাদের কাছে আছে।" : "Yes, {$card['name']} is available in our catalog.",
                $benefit,
                $isBn ? "দাম: {$price}" : "Price: {$price}",
                $isBn ? "স্টক: {$stock}" : "Stock: {$stock}",
                $isBn ? "ওয়ারেন্টি: {$warranty}" : "Warranty: {$warranty}",
                $delivery,
                $packaging,
                $colors,
                $link,
                $salesMode ? ($isBn ? "আপনি চাইলে এখনই অর্ডার করতে পারেন।" : "You can order this now.") : '',
            ]));
        }

        // Header varies based on how well the products matched the query
        if ($answerIntent === 'price') {
            $header = $isBn
                ? "মিলে থাকা {$count}টি পণ্যের দাম:"
                : "I found {$count} matching products with prices:";
        } elseif ($answerIntent === 'availability') {
            $header = $isBn
                ? "মিলে থাকা {$count}টি পণ্যের stock status:"
                : "Stock status for {$count} matching products:";
        } elseif ($answerIntent === 'details') {
            $header = $isBn
                ? "{$count}টি মিলে থাকা পণ্য পাওয়া গেছে; পূর্ণ তথ্যের জন্য একটি নির্বাচন করুন:"
                : "I found {$count} matching products. Choose one for full details:";
        } elseif ($matchType === 'brand' && $searchMode !== 'browse') {
            // Brand found but specific model/type not in DB → show options
            $header = $isBn
                ? "মিলে থাকা কিছু পণ্য পাওয়া গেছে। আপনি কোন পণ্যটির বিষয়ে জানতে চাচ্ছেন?"
                : "I found a few similar products. Which one are you looking for?";
        } elseif ($matchType === 'brand_model') {
            $header = $isBn
                ? "{$count}টি পণ্য পাওয়া গেছে:"
                : "Found {$count} products:";
        } else {
            $header = $isBn
                ? "{$count}টি পণ্য পাওয়া গেছে:"
                : "Found {$count} products:";
        }

        $lines = [$header];
        foreach ($products as $i => $item) {
            $card  = $this->buildProductCard($item);
            $price = $this->formatProductPriceLine($card, $isBn);
            $stock = $card['in_stock'] ? ($isBn ? 'Available' : 'Available') : ($isBn ? 'Out of stock' : 'Out of stock');
            $lines[] = ($i + 1) . ". {$card['name']} - {$price}; {$stock}";
        }

        return implode("\n", $lines);
    }

    protected function formatAlternativeProductTextReply(Collection $products, string $language, string $answerIntent, array $searchResult = []): string
    {
        $isBn = $language === 'bn';
        $lines = [$isBn
            ? 'আপনার চাওয়া exact product নিশ্চিতভাবে পাইনি, তবে কাছাকাছি option পেয়েছি:'
            : 'I could not verify the exact product, but found these nearby options:'];

        foreach ($products->take(3) as $index => $item) {
            $card = $this->buildProductCard($item);
            $lines[] = ($index + 1) . '. ' . $card['name'] . ' - ' . $this->formatProductPriceLine($card, $isBn) . '; ' . ($card['in_stock'] ? 'Available' : 'Out of stock');
        }

        $lines[] = $isBn
            ? 'আপনি কোনটি বুঝিয়েছেন বললে আমি exact price/stock/link নিশ্চিত করে দেব।'
            : 'Tell me which one you meant and I will confirm the exact price/stock/link.';

        return implode("\n", $lines);
    }

    protected function findAlternativeProducts(Item $item, int $limit = 3): Collection
    {
        try {
            $query = Item::withoutGlobalScopes()
                ->with(['category:id,name,parent_id', 'unit:id,unit', 'colors:id,name', 'storage', 'ecommerce_item_details.brand:id,name'])
                ->leftJoin('ecommerce_item_details as _eid', 'items.id', '=', '_eid.item_id')
                ->leftJoin('brands as _brand', '_eid.brand_id', '=', '_brand.id')
                ->where('items.status', 1)
                ->where('items.is_approved', 1)
                ->where('items.stock', '>', 0)
                ->where('items.id', '!=', $item->id)
                ->select('items.*', '_brand.id as _brand_id', '_brand.name as _brand_name')
                ->orderByDesc('items.recommended')
                ->orderByDesc('items.order_count');

            if ($item->category_id) {
                $query->where('items.category_id', $item->category_id);
            }

            $sameCategory = $query->limit($limit)->get();
            if ($sameCategory->count() >= $limit || !$item->category_id) {
                return $sameCategory->take($limit)->values();
            }

            $fallback = Item::withoutGlobalScopes()
                ->with(['category:id,name,parent_id', 'unit:id,unit', 'colors:id,name', 'storage', 'ecommerce_item_details.brand:id,name'])
                ->where('status', 1)
                ->where('is_approved', 1)
                ->where('stock', '>', 0)
                ->where('id', '!=', $item->id)
                ->whereNotIn('id', $sameCategory->pluck('id')->all())
                ->orderByDesc('recommended')
                ->orderByDesc('order_count')
                ->limit($limit - $sameCategory->count())
                ->get();

            return $sameCategory->merge($fallback)->take($limit)->values();
        } catch (\Throwable $e) {
            Log::warning('AiChat alternative product lookup failed: ' . $e->getMessage());
            return collect();
        }
    }

    protected function formatOutOfStockProductReply(Item $item, Collection $alternatives, string $language, string $answerIntent = 'summary', bool $salesMode = true): string
    {
        $isBn = $language === 'bn';
        $card = $this->buildProductCard($item);
        $lines = [
            $isBn ? "জি, {$card['name']} আমাদের ডাটাবেজে পাওয়া গেছে।" : "Yes, {$card['name']} is in our catalog.",
            $isBn ? "বর্তমান দাম: " . $this->formatProductPriceLine($card, $isBn) : "Current price: " . $this->formatProductPriceLine($card, $isBn),
            $isBn ? 'স্টক: Out of stock' : 'Stock: Out of stock',
            $isBn ? 'ওয়ারেন্টি: ' . ($card['warranty'] ?: 'product data-তে উল্লেখ নেই') : 'Warranty: ' . ($card['warranty'] ?: 'not specified in product data'),
            $this->formatProductDeliveryLine($card, $isBn),
        ];

        if ($alternatives->isNotEmpty()) {
            $lines[] = $isBn ? 'কাছাকাছি available option:' : 'Similar available options:';
            foreach ($alternatives->take(3) as $index => $alternative) {
                $altCard = $this->buildProductCard($alternative);
                $lines[] = ($index + 1) . '. ' . $altCard['name'] . ' - ' . $this->formatProductPriceLine($altCard, $isBn) . '; ' . ($isBn ? 'স্টক: Available' : 'Stock: Available');
            }
            if ($salesMode) {
                $lines[] = $isBn ? 'আপনি চাইলে এই available optionগুলোর মধ্যে একটি cart-এ add করতে পারেন।' : 'You can add one of these available options to cart.';
            }
        } else {
            $lines[] = $isBn ? 'এই মুহূর্তে কাছাকাছি available alternative খুঁজে পাইনি। আপনার বাজেট/ব্র্যান্ড বললে আবার খুঁজে দেখব।' : 'I could not find a close in-stock alternative right now. Share your budget/brand and I will search again.';
        }

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

    protected function formatProductPriceLine(array $card, bool $isBn): string
    {
        $regular = (float) ($card['regular_price'] ?? $card['price'] ?? 0);
        $discount = isset($card['discount_price'])
            ? (float) $card['discount_price']
            : (isset($card['offer_price']) ? (float) $card['offer_price'] : 0);

        $money = fn (float $amount): string => $isBn
            ? '৳' . rtrim(rtrim(number_format($amount, 2), '0'), '.')
            : 'BDT ' . rtrim(rtrim(number_format($amount, 2), '0'), '.');

        if ($discount > 0 && $discount < $regular) {
            return $money($discount) . ($isBn ? ' (নিয়মিত: ' . $money($regular) . ')' : ' (regular: ' . $money($regular) . ')');
        }

        return $money($regular);
    }

    protected function formatProductDeliveryLine(array $card, bool $isBn): string
    {
        if (!empty($card['free_delivery'])) {
            return $isBn ? 'ডেলিভারি চার্জ: Free delivery' : 'Delivery charge: Free delivery';
        }

        if (!empty($card['delivery_charge'])) {
            $amount = rtrim(rtrim(number_format((float) $card['delivery_charge'], 2), '0'), '.');
            return $isBn ? "ডেলিভারি চার্জ: ৳{$amount}" : "Delivery charge: BDT {$amount}";
        }

        return $isBn
            ? 'ডেলিভারি চার্জ: product data-তে উল্লেখ নেই; final charge checkout/address অনুযায়ী দেখাবে।'
            : 'Delivery charge: not specified in product data; final charge appears at checkout/address.';
    }

    protected function firstConversationImagePath(Message $message): ?string
    {
        $files = $this->decodeJsonArray($message->file ?? null);
        foreach ($files as $file) {
            $name = is_array($file) ? ($file['img'] ?? $file['name'] ?? null) : (string) $file;
            if (!filled($name)) {
                continue;
            }

            $storage = is_array($file) ? ($file['storage'] ?? 'public') : 'public';
            $candidates = [
                storage_path('app/public/conversation/' . ltrim((string) $name, '/')),
                storage_path('app/conversation/' . ltrim((string) $name, '/')),
            ];

            if ($storage && $storage !== 'public') {
                try {
                    $storagePath = Storage::disk((string) $storage)->path('conversation/' . ltrim((string) $name, '/'));
                    array_unshift($candidates, $storagePath);
                } catch (\Throwable) {
                    // Ignore unsupported disks and keep checking local public paths.
                }
            }

            foreach ($candidates as $path) {
                if ($path && is_file($path)) {
                    return $path;
                }
            }
        }

        return null;
    }

    protected function firstFilled(array $values): ?string
    {
        foreach ($values as $value) {
            if (filled($value)) {
                return trim((string) $value);
            }
        }

        return null;
    }

    protected function firstVariationValue(Item $item, string $key): ?string
    {
        foreach (['variations', 'food_variations', 'choice_options'] as $field) {
            foreach ($this->decodeJsonArray($item->getAttribute($field)) as $entry) {
                $value = data_get($entry, $key);
                if (filled($value)) {
                    return (string) $value;
                }
            }
        }

        return null;
    }

    protected function variantSummary(Item $item): array
    {
        $summary = [];
        foreach (['choice_options', 'variations', 'food_variations'] as $field) {
            foreach ($this->decodeJsonArray($item->getAttribute($field)) as $entry) {
                if (!is_array($entry)) {
                    continue;
                }

                $label = $this->firstFilled([
                    data_get($entry, 'title'),
                    data_get($entry, 'name'),
                    data_get($entry, 'type'),
                    data_get($entry, 'variation'),
                ]);
                $options = data_get($entry, 'options');
                if (is_array($options)) {
                    $optionText = implode('/', array_filter(array_map('strval', $options)));
                    $summary[] = trim(($label ? "{$label}: " : '') . $optionText);
                    continue;
                }

                $parts = array_filter([
                    $label,
                    filled(data_get($entry, 'sku')) ? 'SKU ' . data_get($entry, 'sku') : null,
                    filled(data_get($entry, 'price')) ? '৳' . data_get($entry, 'price') : null,
                    filled(data_get($entry, 'stock')) ? 'stock ' . data_get($entry, 'stock') : null,
                ]);

                if (!empty($parts)) {
                    $summary[] = implode(' ', $parts);
                }
            }
        }

        return collect($summary)->filter()->unique()->values()->take(8)->all();
    }

    protected function decodeJsonArray(mixed $value): array
    {
        if (is_array($value)) {
            return $value;
        }

        if (is_object($value)) {
            return json_decode(json_encode($value), true) ?: [];
        }

        if (is_string($value) && $value !== '') {
            $decoded = json_decode($value, true);
            return is_array($decoded) ? $decoded : [];
        }

        return [];
    }

    protected function extractWarrantyText(?string $text): ?string
    {
        $text = trim(strip_tags((string) $text));
        if ($text === '') {
            return null;
        }

        $patterns = [
            '/(?:warranty|guarantee)[:\s-]*([^\.\n\r]{1,80})/iu',
            '/([0-9০-৯]+(?:\s|-)*(?:year|years|month|months|দিন|মাস|বছর|বৎসর)[^\.\n\r]{0,50}(?:warranty|guarantee|ওয়ারেন্টি|ওয়ারেন্টি|গ্যারান্টি))/iu',
            '/(?:ওয়ারেন্টি|ওয়ারেন্টি|গ্যারান্টি)[:\s-]*([^\.\n\r]{1,80})/iu',
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $text, $match)) {
                return trim($match[1] ?? $match[0]);
            }
        }

        return null;
    }

    public function searchProducts(string $query, int $limit = 5): Collection
    {
        $context = $this->understandUserMessage($query, $this->settings());
        $tokens  = $context['product_tokens'] ?? $this->extractProductTokens($query);
        $terms   = $context['product_search_terms'] ?? [];

        if (empty($terms)) return collect();

        $result = $this->searchProductsInDb($terms, $tokens);

        return $result['products']
            ->take($limit)
            ->map(fn (Item $item) => $this->buildProductCard($item))
            ->values();
    }


    protected function findPolicyReply(array $context): ?array
    {
        $policies = collect([
            [
                'type' => 'delivery',
                'title' => 'Delivery Policy',
                'content' => strip_tags($this->getBusinessTextSetting('shipping_policy')),
            ],
            [
                'type' => 'refund',
                'title' => 'Refund Policy',
                'content' => strip_tags($this->getBusinessTextSetting('refund')),
            ],
            [
                'type' => 'privacy',
                'title' => 'Privacy Policy',
                'content' => strip_tags($this->getBusinessTextSetting('privacy_policy')),
            ],
            [
                'type' => 'terms',
                'title' => 'Terms & Conditions',
                'content' => strip_tags($this->getBusinessTextSetting('terms_and_conditions')),
            ],
        ])->filter(fn (array $policy) => filled($policy['content']))->values();

        if ($policies->isEmpty()) {
            return null;
        }

        foreach ($policies as $policy) {
            $score = $this->scoreCandidate(
                $context['normalized_question'],
                $context['tokens'],
                [
                    $policy['title'],
                    $policy['type'],
                    $policy['content'],
                ],
                $context['preferred_source'] === 'policy' ? 25 : 0
            );

            if ($score > 45) {
                $reply = $context['detailed']
                    ? $this->shortenText($policy['content'], 360)
                    : $this->shortenText($policy['content'], 180);

                return [
                    'reply' => $reply,
                    'reply_short' => $this->shortenText($policy['content'], 180),
                    'reply_detailed' => $this->shortenText($policy['content'], 360),
                    'source_type' => 'policy',
                    'source_id' => null,
                    'matched_keyword' => $policy['title'],
                    'confidence_score' => $score,
                ];
            }
        }

        return null;
    }

    protected function pickBestTextCandidate(Collection $candidates, array $fields, callable $formatter, array $context): ?array
    {
        $best = null;
        $bestScore = 0;

        foreach ($candidates as $candidate) {
            $texts = [];
            foreach ($fields as $field) {
                $value = $candidate->{$field} ?? null;
                if (filled($value)) {
                    $texts[] = (string) $value;
                }
            }

            if (empty($texts)) {
                continue;
            }

            $score = $this->scoreCandidate(
                $context['normalized_question'],
                $context['tokens'],
                $texts,
                ((int) ($candidate->priority ?? 0)) + ($context['preferred_source'] === ($candidate->source_category ?? $candidate->type ?? '') ? 20 : 0)
            );

            if ($score > $bestScore) {
                $bestScore = $score;
                $formatted = $formatter($candidate);
                if (filled($formatted['reply'] ?? null)) {
                    $best = $formatted;
                    $best['confidence_score'] = $score;
                }
            }
        }

        return $bestScore >= 55 ? $best : null;
    }

    protected function formatSingleOfferReply(ChatOffer $offer, array $context): array
    {
        $title = $this->selectLocalizedText($offer->title_bn, $offer->title_en, $offer->title, $context['language']);
        $description = $this->selectLocalizedText($offer->description_bn, $offer->description_en, $offer->description, $context['language']);
        $dateRange = $this->formatOfferDateRange($offer, $context['language']);
        $reply = trim($title . '. ' . $this->shortenText($description, $context['detailed'] ? 320 : 150) . ($dateRange ? ' ' . $dateRange : ''));

        return [
            'reply' => $reply,
            'reply_short' => $this->shortenText($reply, 180),
            'reply_detailed' => $this->shortenText(trim($title . '. ' . $description . ($dateRange ? ' ' . $dateRange : '')), 380),
            'source_type' => 'offer',
            'source_id' => $offer->id,
            'matched_keyword' => $title,
            'confidence_score' => 0,
        ];
    }

    protected function formatOfferCollectionReply(Collection $offers, array $context, int $confidence): array
    {
        $topOffers = $offers->take(3);
        $lines = $topOffers->map(function (ChatOffer $offer) use ($context) {
            $title = $this->selectLocalizedText($offer->title_bn, $offer->title_en, $offer->title, $context['language']);
            $dateRange = $this->formatOfferDateRange($offer, $context['language']);
            return trim($title . ($dateRange ? " ({$dateRange})" : ''));
        })->all();

        $reply = $context['language'] === 'bn'
            ? 'বর্তমানে এই অফারগুলো চালু আছে: ' . implode(', ', $lines)
            : 'These offers are currently active: ' . implode(', ', $lines);

        return [
            'reply' => $reply,
            'reply_short' => $reply,
            'reply_detailed' => $reply,
            'source_type' => 'offer',
            'source_id' => $topOffers->first()?->id,
            'matched_keyword' => $topOffers->first()?->title,
            'confidence_score' => $confidence,
        ];
    }

    protected function formatOfferDateRange(ChatOffer $offer, string $language): string
    {
        if (!$offer->start_date && !$offer->end_date) {
            return '';
        }

        $from = $offer->start_date?->format('d M Y');
        $to = $offer->end_date?->format('d M Y');

        if ($language === 'bn') {
            return trim('চালু সময়: ' . trim(($from ? "{$from}" : '') . ($to ? " - {$to}" : '')));
        }

        return trim('Valid: ' . trim(($from ? "{$from}" : '') . ($to ? " - {$to}" : '')));
    }

    protected function fallbackReply(array $context, string $reason): array
    {
        $language = $context['language'] ?? 'en';
        $settings = $context['settings'] ?? $this->settings();
        $reply = $language === 'bn'
            ? $settings['fallback_message_bn']
            : $settings['fallback_message_en'];

        return [
            'reply' => $reply,
            'reply_short' => $reply,
            'reply_detailed' => $reply,
            'source_type' => 'fallback',
            'source_id' => null,
            'matched_keyword' => null,
            'confidence_score' => 0,
            'language' => $language,
            'normalized_question' => $context['normalized_question'] ?? null,
            'query_type' => $context['query_type'] ?? 'unknown',
            'fallback_used' => true,
            'reason' => $reason,
            'agent_context' => $context['agent_context'] ?? null,
            'product_search_terms' => $context['product_search_terms'] ?? [],
            'data_domain' => $context['data_domain'] ?? null,
            'schema_table' => $context['schema_table'] ?? null,
            'amounts' => $context['amounts'] ?? [],
        ];
    }

    protected function composeStoredReplyMessage(array $replyData): string
    {
        $replyText = $this->cleanReplyText($replyData['reply'] ?? '');
        $products = $this->normalizeProductCardsForMessage($replyData['products'] ?? []);

        if (empty($products)) {
            return $replyText;
        }

        $encodedProducts = json_encode($products, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (!is_string($encodedProducts) || $encodedProducts === '') {
            return $replyText;
        }

        return trim($replyText . "\n\n[PRODUCT_CARDS]" . $encodedProducts . "[/PRODUCT_CARDS]");
    }

    protected function normalizeProductCardsForMessage(mixed $products): array
    {
        if ($products instanceof Collection) {
            $products = $products->all();
        }

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

        $cards = [];
        foreach ($products as $product) {
            if ($product instanceof Item) {
                $product = $this->buildProductCard($product);
            }

            if (!is_array($product)) {
                continue;
            }

            $cards[] = [
                'id' => $product['id'] ?? null,
                'name' => $product['name'] ?? null,
                'slug' => $product['slug'] ?? null,
                'link' => $product['link'] ?? null,
                'image' => $product['image'] ?? null,
                'price' => isset($product['price']) ? (float) $product['price'] : null,
                'offer_price' => isset($product['offer_price']) ? (float) $product['offer_price'] : null,
                'discount' => isset($product['discount']) ? (float) $product['discount'] : null,
                'discount_type' => $product['discount_type'] ?? null,
                'in_stock' => (bool) ($product['in_stock'] ?? false),
                'stock' => isset($product['stock']) ? (int) $product['stock'] : null,
                'category' => $product['category'] ?? null,
                'unit' => $product['unit'] ?? null,
                'free_delivery' => (bool) ($product['free_delivery'] ?? false),
                'delivery_charge' => isset($product['delivery_charge']) ? (float) $product['delivery_charge'] : null,
                'packaging_charge' => isset($product['packaging_charge']) ? (float) $product['packaging_charge'] : null,
                'colors' => array_values(array_filter((array) ($product['colors'] ?? []))),
                'description' => $product['description'] ?? null,
            ];
        }

        return array_slice($cards, 0, 5);
    }

    public function cleanReplyText(?string $reply): string
    {
        $reply = (string) $reply;
        $reply = preg_replace('/\s*\[PRODUCT_CARDS\].*?\[\/PRODUCT_CARDS\]\s*/su', '', $reply) ?? $reply;

        return trim($reply);
    }

    protected function scoreCandidate(string $normalizedQuestion, array $tokens, array $texts, int $priorityBonus = 0): int
    {
        $score = max(0, $priorityBonus);

        foreach ($texts as $text) {
            $normalizedText = $this->normalizeText($text);
            if ($normalizedText === '') {
                continue;
            }

            if ($normalizedQuestion !== '' && $normalizedText === $normalizedQuestion) {
                $score += 100;
            }

            if ($normalizedQuestion !== '' && str_contains($normalizedText, $normalizedQuestion)) {
                $score += 40;
            }

            foreach ($tokens as $token) {
                if ($token !== '' && str_contains($normalizedText, $token)) {
                    $score += 14;
                }
            }
        }

        return $score;
    }

    protected function detectLanguage(string $text): string
    {
        return preg_match('/[\x{0980}-\x{09FF}]/u', $text) ? 'bn' : 'en';
    }

    protected function looksLikeBanglish(string $text): bool
    {
        $words = explode(' ', $this->normalizeText($text));
        $banglishSignals = [
            'ki', 'kiki', 'konta', 'konti', 'kon', 'ache', 'ase', 'ace', 'acche',
            'dam', 'koto', 'taka', 'chai', 'lagbe', 'pabo', 'pawa', 'paoya',
            'dekhao', 'dekhan', 'dekhte', 'kinte', 'bhai',
        ];

        return count(array_intersect($words, $banglishSignals)) > 0;
    }

    protected function detectQueryType(string $question): string
    {
        $normalized = $this->normalizeText($question);

        $map = [
            'wallet_bonus' => ['wallet bonus', 'add fund bonus', 'fund bonus', 'bonus', 'bonus pabo', 'bonus ki pabo'],
            'coupon' => ['coupon', 'coupon code', 'promo code', 'voucher'],
            'cashback' => ['cashback', 'cash back'],
            'loyalty' => ['loyalty', 'loyalty point', 'reward point', 'points'],
            'wallet' => ['wallet balance', 'wallet payment', 'wallet'],
            'social_media' => ['social media', 'social link', 'social links', 'facebook', 'facebook page', 'fb page', 'youtube', 'youtube channel', 'instagram', 'tiktok', 'telegram', 'messenger'],
            'offer' => ['offer', 'offers', 'discount', 'campaign', 'coupon', 'promotion', 'deal', 'অফার', 'ডিসকাউন্ট', 'ক্যাম্পেইন', 'কুপন'],
            'delivery' => ['delivery', 'shipping', 'courier', 'pickup', 'picked up', 'ডেলিভারি', 'শিপিং', 'কুরিয়ার', 'পিকআপ'],
            'order_help' => ['order status', 'track order', 'order tracking', 'where is my order', 'অর্ডার স্ট্যাটাস', 'অর্ডার কোথায়', 'অর্ডার ট্র্যাক'],
            'price' => ['price', 'cost', 'amount', 'rate', 'tk', 'bdt', 'dam', 'koto', 'taka', 'দাম', 'মূল্য', 'কত'],
            'policy' => ['policy', 'refund', 'privacy', 'terms', 'return', 'refund policy', 'পলিসি', 'রিফান্ড', 'প্রাইভেসি', 'শর্ত'],
            'product' => ['product', 'item', 'stock', 'available', 'items', 'ache', 'ase', 'ace', 'ki ki', 'পণ্য', 'পন্য', 'পূর্ণ', 'প্রোডাক্ট', 'আইটেম', 'স্টক'],
            'human' => ['human', 'agent', 'support', 'representative', 'live chat', 'talk to a human', 'talk to human', 'মানুষ', 'মানব', 'সাপোর্ট', 'এজেন্ট', 'কাস্টমার কেয়ার'],
        ];

        foreach ($map as $type => $keywords) {
            foreach ($keywords as $keyword) {
                if (str_contains($normalized, $this->normalizeText($keyword))) {
                    return $type;
                }
            }
        }

        return 'general';
    }

    protected function preferredSourceForQueryType(string $queryType): ?string
    {
        return match ($queryType) {
            'offer', 'discount', 'campaign' => 'offer',
            'delivery', 'policy' => 'policy',
            'price', 'product' => 'product',
            'order_help' => 'support',
            'wallet_bonus', 'coupon', 'cashback', 'wallet', 'loyalty', 'social_media' => $queryType,
            default => null,
        };
    }

    protected function wantsDetailedReply(string $question): bool
    {
        $normalized = $this->normalizeText($question);
        return str_contains($normalized, 'details')
            || str_contains($normalized, 'detail')
            || str_contains($normalized, 'বিস্তারিত')
            || str_contains($normalized, 'how')
            || str_contains($normalized, 'কিভাবে')
            || mb_strlen(trim($question)) > 80;
    }

    protected function getPriorityOrder(): array
    {
        $value = $this->getStringSetting('ai_chat_answer_priority_order', implode(',', self::DEFAULT_PRIORITY_ORDER));
        $order = array_values(array_filter(array_map(static fn ($item) => trim((string) $item), explode(',', $value))));

        if (empty($order)) {
            return self::DEFAULT_PRIORITY_ORDER;
        }

        foreach (self::DEFAULT_PRIORITY_ORDER as $source) {
            if (!in_array($source, $order, true)) {
                $order[] = $source;
            }
        }

        return $order;
    }

    protected function getBooleanSetting(string $key, bool $default = false): bool
    {
        $value = BusinessSetting::query()->where('key', $key)->value('value');
        if ($value === null) {
            return $default;
        }

        return (bool) ((int) $value);
    }

    protected function getStringSetting(string $key, string $default = ''): string
    {
        $value = BusinessSetting::query()->where('key', $key)->value('value');
        return $value !== null ? (string) $value : $default;
    }

    protected function getIntSetting(string $key, int $default = 0): int
    {
        $value = BusinessSetting::query()->where('key', $key)->value('value');
        return $value !== null ? (int) $value : $default;
    }

    protected function getBusinessTextSetting(string $key): string
    {
        $value = BusinessSetting::query()->where('key', $key)->value('value');
        if ($value === null) {
            return '';
        }

        $decoded = json_decode((string) $value, true);
        if (json_last_error() === JSON_ERROR_NONE) {
            if (is_string($decoded)) {
                return $decoded;
            }

            return is_array($decoded) ? json_encode($decoded) : (string) $value;
        }

        return (string) $value;
    }

    protected function normalizeText(?string $text): string
    {
        $text = Str::of((string) $text)
            ->lower()
            ->replaceMatches('/\bp\s*\.?\s*c\b/u', ' pressure cooker ')
            ->replaceMatches('/[^\p{L}\p{M}\p{N}\s]+/u', ' ')
            ->replaceMatches('/\s+/u', ' ')
            ->trim()
            ->value();

        return $text;
    }

    protected function extractTokens(string $text): array
    {
        return array_values(array_filter(
            explode(' ', $this->normalizeText($text)),
            static fn ($token) => mb_strlen($token) >= 3
        ));
    }

    protected function detectProductSearchMode(string $question): string
    {
        $normalized = $this->normalizeText($question);
        if ($normalized === '') {
            return 'exact';
        }

        $browsePhrases = [
            'ki ki ache', 'ki ki ase', 'ki ache', 'ki ase', 'kiki ache', 'kiki ase',
            'ki ki available', 'available ache', 'available ase', 'stock ache', 'stock ase',
            'show me', 'list', 'what do you have', 'which products', 'available products',
            'dekhte chai', 'dekhan', 'dekhao',
            'কি কি আছে', 'কী কী আছে', 'কি কি পণ্য', 'কী কী পণ্য', 'কি কি প্রোডাক্ট',
            'পণ্য পাওয়া যায়', 'পণ্য পাওয়া যায়', 'পন্য পাওয়া যায়', 'পূর্ণ পাওয়া যায়',
            'পণ্য খুঁজতে চাই', 'প্রোডাক্ট খুঁজতে চাই',
        ];

        foreach ($browsePhrases as $phrase) {
            if (str_contains($normalized, $phrase)) {
                return 'browse';
            }
        }

        $words = explode(' ', $normalized);
        if (!empty(array_intersect($words, ['ache', 'ase', 'acche', 'available', 'stock']))) {
            return 'browse';
        }

        return 'exact';
    }

    protected function looksLikeProductBrowseQuestion(string $question): bool
    {
        if ($this->detectProductSearchMode($question) !== 'browse') {
            return false;
        }

        $tokens = $this->extractProductTokens($question);
        return count($tokens) > 0;
    }

    protected function detectProductAnswerIntent(string $question, string $queryType = 'general', string $searchMode = 'exact'): string
    {
        $normalized = $this->normalizeText($question);
        if ($normalized === '') {
            return 'summary';
        }

        $hasAny = function (array $needles) use ($normalized): bool {
            foreach ($needles as $needle) {
                if (str_contains($normalized, $this->normalizeText($needle))) {
                    return true;
                }
            }
            return false;
        };

        if ($queryType === 'price' || $hasAny(['price', 'cost', 'rate', 'tk', 'bdt', 'dam', 'koto', 'koto taka', 'দাম', 'মূল্য', 'কত'])) {
            return 'price';
        }

        if ($hasAny(['details', 'detail', 'description', 'describe', 'spec', 'specs', 'specification', 'feature', 'features', 'info', 'information', 'material', 'size', 'color', 'colour', 'warranty', 'বিস্তারিত', 'বর্ণনা', 'তথ্য'])) {
            return 'details';
        }

        if ($searchMode === 'browse' && $hasAny(['ki ki', 'kiki', 'list', 'show me', 'which products', 'what do you have', 'available products', 'dekhan', 'dekhao', 'কি কি', 'কী কী', 'পণ্য খুঁজতে', 'প্রোডাক্ট খুঁজতে'])) {
            return 'browse';
        }

        if ($hasAny(['available', 'availability', 'stock', 'in stock', 'ache', 'ase', 'ace', 'acche', 'আছে', 'স্টক'])) {
            return 'availability';
        }

        return $searchMode === 'browse' ? 'browse' : 'summary';
    }

    protected function selectLocalizedText(?string $bn, ?string $en, ?string $default, string $language): string
    {
        if ($language === 'bn' && filled($bn)) {
            return trim((string) $bn);
        }

        if ($language === 'en' && filled($en)) {
            return trim((string) $en);
        }

        if (filled($default)) {
            return trim((string) $default);
        }

        return trim((string) ($language === 'bn' ? ($bn ?: $en) : ($en ?: $bn)));
    }

    protected function shortenText(?string $text, int $limit): string
    {
        return Str::limit(trim((string) $text), $limit, '...');
    }

    protected function resolveAdminUserInfo(): ?UserInfo
    {
        $admin = Admin::query()->where('role_id', 1)->first();
        if (!$admin) {
            return null;
        }

        return UserInfo::query()->firstOrCreate(
            ['admin_id' => $admin->id],
            [
                'f_name' => $admin->f_name,
                'l_name' => $admin->l_name,
                'phone' => $admin->phone,
                'email' => $admin->email,
                'image' => $admin->image,
            ]
        );
    }

    protected function recordChatMessage(array $payload): void
    {
        ChatMessage::query()->create($payload);
    }

    protected function recordChatLog(array $payload): void
    {
        if (!Schema::hasTable('chat_logs')) {
            return;
        }

        static $columns = null;
        $columns ??= array_flip(Schema::getColumnListing('chat_logs'));
        $payload = array_intersect_key($payload, $columns);

        ChatLog::query()->create($payload);
    }
}
