<?php
/**
 * Retrieval Endpoint for BoostBot REST API.
 *
 * Handles evidence retrieval for direct browser-to-backend streaming.
 * Returns evidence, system prompt, and context without calling LLM.
 *
 * @package BoostBot
 * @since 1.15.0
 */

// Prevent direct access to the file.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * BoostBot_Retrieval_Endpoint class.
 *
 * Provides retrieval-only endpoint for the streaming architecture.
 *
 * @since 1.15.0
 */
class BoostBot_Retrieval_Endpoint extends BoostBot_Base_Endpoint {

    /**
     * The base path for this endpoint.
     *
     * @since 1.15.0
     * @access protected
     * @var string $rest_base
     */
    protected $rest_base = 'chat';

    /**
     * Knowledge index instance.
     *
     * @var BoostBot_Knowledge_Index|null
     */
    private $knowledge_index = null;

    /**
     * Last routing inputs from unified router.
     *
     * @var array
     */
    private $last_routing_inputs = array();

    /**
     * Last resolved bot object (used by helper methods).
     *
     * @var object|null
     */
    private $last_resolved_bot = null;

    /**
     * Register the routes for this endpoint.
     *
     * @since 1.15.0
     */
    public function register() {
        // POST /wp-json/boostbot/v1/chat/retrieve - Get evidence only (no LLM)
        $this->register_route(
            '/retrieve',
            'POST',
            array( $this, 'retrieve_evidence' ),
            array( $this, 'public_with_rate_limit' ),
            $this->get_retrieve_args()
        );

        // POST /wp-json/boostbot/v1/chat/retrieve-orchestrated - Two-tier chunks for Gemini orchestration
        $this->register_route(
            '/retrieve-orchestrated',
            'POST',
            array( $this, 'retrieve_evidence_orchestrated' ),
            array( $this, 'public_with_rate_limit' ),
            $this->get_retrieve_args()
        );
    }

    /**
     * Retrieve evidence for Gemini-orchestrated flow.
     * Returns two chunk sizes (small for quick response, large for detailed follow-up)
     * and full bot configuration including output limits.
     *
     * @since 1.16.0
     * @param WP_REST_Request $request The REST request object.
     * @return WP_REST_Response|WP_Error The response or error.
     */
    public function retrieve_evidence_orchestrated( $request ) {
        $t0 = microtime( true );
        
        $message = sanitize_textarea_field( $request->get_param( 'message' ) );
        $conversation_id = sanitize_text_field( $request->get_param( 'conversation_id' ) );
        $visitor_id = sanitize_text_field( $request->get_param( 'visitor_id' ) );
        $page_context = $request->get_param( 'context' ) ?: array();

        if ( empty( $message ) ) {
            return $this->error( __( 'Message is required', 'boostbot' ), 400 );
        }

        if ( empty( $visitor_id ) ) {
            $visitor_id = $this->get_visitor_identifier( $request );
        }

        if ( empty( $conversation_id ) ) {
            $conversation_id = wp_generate_uuid4();
        }

        BoostBot_Logger::info( sprintf( '[FLOW=ORCHESTRATED] retrieve-orchestrated conversation_id=%s visitor_id=%s', (string) $conversation_id, (string) $visitor_id ) );

        // ── Per-visitor message rate limiting (Week 33) ──────────────
        if ( class_exists( 'BoostBot_Message_Limiter' ) ) {
            $msg_identifier = $this->get_visitor_identifier( $request );
            $msg_check      = BoostBot_Message_Limiter::check( $msg_identifier );

            if ( ! $msg_check['allowed'] ) {
                if ( class_exists( 'BoostBot_Spam_Alert_Monitor' ) && in_array( $msg_check['limit_type'], [ 'hourly', 'daily' ], true ) ) {
                    BoostBot_Spam_Alert_Monitor::on_limit_hit( $msg_identifier, $msg_check['limit_type'] );
                }

                $ps = BoostBot_Prompt_Strings::get( isset( $bot ) ? ( $bot->language ?? 'he' ) : 'he' );
                $limit_reply = '';
                if ( 'hourly' === $msg_check['limit_type'] ) {
                    $minutes = max( 1, ceil( $msg_check['retry_after'] / 60 ) );
                    $limit_reply = sprintf( $ps['rate_hourly'], $minutes );
                } elseif ( 'daily' === $msg_check['limit_type'] ) {
                    $limit_reply = $ps['rate_daily'];
                } elseif ( 'cooldown' === $msg_check['limit_type'] ) {
                    $limit_reply = sprintf( $ps['rate_cooldown'], $msg_check['cooldown_seconds'] );
                }

                return $this->success( array(
                    'rate_limited'     => true,
                    'limit_type'       => $msg_check['limit_type'],
                    'retry_after'      => $msg_check['retry_after'],
                    'cooldown_seconds' => $msg_check['cooldown_seconds'],
                    'remaining_hour'   => $msg_check['remaining_hour'],
                    'remaining_day'    => $msg_check['remaining_day'],
                    'reply'            => $limit_reply,
                    'conversation_id'  => $conversation_id,
                ) );
            }

            // Increment counters — visitor is allowed.
            BoostBot_Message_Limiter::increment( $msg_identifier );
        }

        try {
            // Initialize knowledge index
            if ( class_exists( 'BoostBot_Knowledge_Index' ) ) {
                $this->knowledge_index = new BoostBot_Knowledge_Index();
            }

            // Get conversation history
            $history = $request->get_param( 'history' ) ?: array();
            if ( ! is_array( $history ) ) {
                $history = array();
            }
            
            if ( empty( $history ) && ! empty( $conversation_id ) ) {
                $conversation_model = new BoostBot_Conversation_Model();
                $conversation = $conversation_model->get_by_conversation_id( $conversation_id );
                if ( $conversation ) {
                    $session_data = json_decode( $conversation->session_data, true ) ?: array();
                    $history = $session_data['messages'] ?? array();
                }
            }

            // Resolve bot
            $bot = $this->resolve_bot( $page_context );
            $this->last_resolved_bot = $bot;

            // Call unified router
            $unified_result = null;
            $needs_retrieval = true;
            $use_unified_router = (bool) BoostBot_Settings::get( 'use_unified_router', true );
            
            if ( $use_unified_router && class_exists( 'BoostBot_Credits_Backend_Client' ) ) {
                $unified_result = $this->call_unified_router( $message, $history, $page_context, $bot );
                
                if ( $unified_result && ! empty( $unified_result['routing'] ) ) {
                    $needs_retrieval = $unified_result['routing']['needs_retrieval'] ?? true;
                    $this->last_routing_inputs = array(
                        'intents'           => $unified_result['intents'] ?? array(),
                        'entities'          => $unified_result['entities'] ?? array(),
                        'rewritten_query'   => $unified_result['rewritten_query'] ?? $message,
                        'original_query'    => $message,
                        'gemini_used'       => true,
                        
                        // New term classification (preferred)
                        'required_terms'    => $unified_result['required_terms'] ?? array(),    // AND logic
                        'optional_keywords' => $unified_result['optional_keywords'] ?? array(), // OR logic
                        'boost_terms'       => $unified_result['boost_terms'] ?? array(),       // Ranking boost
                        
                        // Backward compatibility
                        'must_include'      => $unified_result['must_include'] ?? $unified_result['boost_terms'] ?? array(),
                        'expanded_keywords' => $unified_result['expanded_keywords'] ?? array(),
                    );
                    
                    // Log Gemini's query building decision for debugging
                    BoostBot_Logger::info( sprintf(
                        '[Gemini QB] Original: "%s" | Rewritten: "%s" | Required: %s | Optional: %s | Boost: %s',
                        mb_substr( $message, 0, 50 ),
                        mb_substr( $unified_result['rewritten_query'] ?? '', 0, 100 ),
                        implode( ',', $unified_result['required_terms'] ?? array() ),
                        implode( ',', array_slice( $unified_result['optional_keywords'] ?? array(), 0, 5 ) ),
                        implode( ',', array_slice( $unified_result['boost_terms'] ?? array(), 0, 5 ) )
                    ) );
                }
            }

            // === TWO-TIER CHUNK RETRIEVAL ===
            $chunks_data = array( 'small_chunks' => '', 'large_chunks' => '' );
            if ( $needs_retrieval && $this->knowledge_index ) {
                $chunks_data = $this->search_knowledge_orchestrated( $message, $history );
            }

            // Get page content
            $page_content = '';
            $needs_page = $unified_result['routing']['needs_page_context'] ?? true;
            if ( $needs_page ) {
                $page_content = $this->get_page_content( $page_context );
            }

            // === BOT CONFIGURATION ===
            $output_limit_global = absint( BoostBot_Settings::get( 'bot_output_char_limit_global', 150 ) );
            $output_limit_bot = isset( $bot->bot_output_char_limit ) ? absint( $bot->bot_output_char_limit ) : 0;
            $effective_output_limit = $output_limit_bot > 0 ? $output_limit_bot : $output_limit_global;
            // Backward compat: old values were in chars (typically 500-2000). Convert to words.
            if ( $effective_output_limit > 400 ) {
                $effective_output_limit = (int) round( $effective_output_limit / 5 );
            }
            
            // Get bot's actual widget assignments (NEVER use global settings - each bot has its own widgets)
            $bot_id = $bot->id ?? 0;
            $bot_widget_types = array(); // Widget types this bot has assigned
            if ( $bot_id > 0 && class_exists( 'BoostBot_Widget_Model' ) ) {
                try {
                    $widget_model = new BoostBot_Widget_Model();
                    $assigned_widgets = $widget_model->get_for_chatbot( $bot_id );
                    if ( ! empty( $assigned_widgets ) ) {
                        foreach ( $assigned_widgets as $widget ) {
                            $widget_type = $widget->type ?? 'unknown';
                            if ( ! in_array( $widget_type, $bot_widget_types, true ) ) {
                                $bot_widget_types[] = $widget_type;
                            }
                        }
                    }
                } catch ( Exception $e ) {
                    BoostBot_Logger::error( 'Failed to get bot widget assignments: ' . $e->getMessage() );
                }
            }
            // Note: If bot has no widgets assigned, $bot_widget_types will be empty - this is correct behavior

            // === TRACK SHOWN WIDGETS IN CONVERSATION ===
            $shown_widgets = array();      // Types (legacy, for backward compat)
            $shown_widget_ids = array();   // IDs (for precise deduplication)
            foreach ( $history as $msg ) {
                if ( isset( $msg['widgets'] ) && is_array( $msg['widgets'] ) ) {
                    foreach ( $msg['widgets'] as $w ) {
                        $widget_type = $w['type'] ?? '';
                        $widget_id = isset( $w['id'] ) ? absint( $w['id'] ) : 0;
                        
                        // Track type (legacy)
                        if ( ! empty( $widget_type ) && ! in_array( $widget_type, $shown_widgets, true ) ) {
                            $shown_widgets[] = $widget_type;
                        }
                        
                        // Track ID (new - more precise)
                        if ( $widget_id > 0 && ! in_array( $widget_id, $shown_widget_ids, true ) ) {
                            $shown_widget_ids[] = $widget_id;
                        }
                    }
                }
            }

            // Build messages array
            $messages = $this->build_messages_array( $history, $message );

            $elapsed_ms = round( ( microtime( true ) - $t0 ) * 1000 );
            
            // === COMPREHENSIVE LOGGING ===
            BoostBot_Logger::info( '[Orchestrated] === REQUEST START ===' );
            BoostBot_Logger::info( sprintf( '[Orchestrated] Message: "%s"', mb_substr( $message, 0, 50 ) ) );
            BoostBot_Logger::info( sprintf( '[Orchestrated] ConversationID: %s', $conversation_id ) );
            BoostBot_Logger::info( sprintf( '[Orchestrated] History count: %d messages', count( $history ) ) );
            BoostBot_Logger::info( sprintf( '[Orchestrated] Bot: %s (limit=%d)', $bot->bot_name ?? 'BoostBot', $effective_output_limit ) );
            BoostBot_Logger::info( sprintf( '[Orchestrated] Chunks: small=%d chars, large=%d chars', 
                mb_strlen( $chunks_data['small_chunks'] ), 
                mb_strlen( $chunks_data['large_chunks'] ) 
            ) );
            BoostBot_Logger::info( sprintf( '[Orchestrated] Bot widgets: %s', ! empty( $bot_widget_types ) ? implode( ',', $bot_widget_types ) : '(none assigned)' ) );
            BoostBot_Logger::info( sprintf( '[Orchestrated] Widgets shown: %s', implode( ',', $shown_widgets ) ?: 'none' ) );
            BoostBot_Logger::info( sprintf( '[Orchestrated] TIMING: %dms total', $elapsed_ms ) );
            BoostBot_Logger::info( '[Orchestrated] === REQUEST END ===' );

            // === BUILD COMPREHENSIVE BOT CONFIG FOR GEMINI ===
            $bot_config = $this->build_orchestrator_bot_config( $bot, $effective_output_limit, $bot_widget_types, $message );

            // === OPENING SCREEN AI INSTRUCTIONS (per-question) ===
            // When admin configures per-question AI instructions for "שאלות + AI" mode,
            // the frontend passes them as ai_instructions. These get HIGH PRIORITY
            // by being prepended to the personality so Gemini follows them.
            $ai_instructions = sanitize_textarea_field( $request->get_param( 'ai_instructions' ) ?? '' );
            if ( ! empty( $ai_instructions ) ) {
                $instruction_block = "## CRITICAL: ANSWER INSTRUCTIONS FOR THIS SPECIFIC QUESTION\n\n"
                    . "The following instructions were set by the site administrator for this specific question. "
                    . "You MUST follow them precisely. Do NOT suggest contact forms, lead forms, or any widgets "
                    . "unless these instructions explicitly tell you to. Answer based on these instructions:\n\n"
                    . $ai_instructions
                    . "\n\n---\n\n";
                $bot_config['personality'] = $instruction_block . ( $bot_config['personality'] ?? '' );
                BoostBot_Logger::info( sprintf( '[Orchestrated] AI instructions injected: %s', mb_substr( $ai_instructions, 0, 100 ) ) );
            }

            return $this->success( array(
                'conversation_id'   => $conversation_id,
                'messages'          => $messages,
                'history'           => $history,
                
                // Two-tier chunks for Gemini orchestration
                'small_chunks'      => $chunks_data['small_chunks'],
                'large_chunks'      => $chunks_data['large_chunks'],
                'page_content'      => $page_content,
                
                // Comprehensive bot configuration for Gemini
                'bot_config'        => $bot_config,
                
                // Widget tracking for deduplication
                'shown_widgets_in_conversation' => $shown_widgets,
                'shown_widget_ids_in_conversation' => $shown_widget_ids,
                
                // Router info
                'routing'           => $unified_result['routing'] ?? null,
                
                'timing_ms'         => $elapsed_ms,
            ) );

        } catch ( Exception $e ) {
            BoostBot_Logger::error( 'Orchestrated retrieval error: ' . $e->getMessage() );
            return $this->error( __( 'Retrieval failed', 'boostbot' ), 500 );
        }
    }

    /**
     * Retrieve evidence and context for streaming.
     *
     * @since 1.15.0
     * @param WP_REST_Request $request The REST request object.
     * @return WP_REST_Response|WP_Error The response or error.
     */
    public function retrieve_evidence( $request ) {
        BoostBot_Logger::warning( '[Deprecated] FLOW=DIRECT_RETRIEVAL endpoint /chat/retrieve was called. Canonical flow is /chat/retrieve-orchestrated.' );
        $t0 = microtime( true );
        
        $message = sanitize_textarea_field( $request->get_param( 'message' ) );
        $conversation_id = sanitize_text_field( $request->get_param( 'conversation_id' ) );
        $visitor_id = sanitize_text_field( $request->get_param( 'visitor_id' ) );
        $page_context = $request->get_param( 'context' ) ?: array();

        if ( empty( $message ) ) {
            return $this->error( __( 'Message is required', 'boostbot' ), 400 );
        }

        if ( empty( $visitor_id ) ) {
            $visitor_id = $this->get_visitor_identifier( $request );
        }

        // Generate conversation ID if needed
        if ( empty( $conversation_id ) ) {
            $conversation_id = wp_generate_uuid4();
        }

        // ── Per-visitor message rate limiting (Week 33) ──────────────
        if ( class_exists( 'BoostBot_Message_Limiter' ) ) {
            $msg_identifier = $this->get_visitor_identifier( $request );
            $msg_check      = BoostBot_Message_Limiter::check( $msg_identifier );

            if ( ! $msg_check['allowed'] ) {
                if ( class_exists( 'BoostBot_Spam_Alert_Monitor' ) && in_array( $msg_check['limit_type'], [ 'hourly', 'daily' ], true ) ) {
                    BoostBot_Spam_Alert_Monitor::on_limit_hit( $msg_identifier, $msg_check['limit_type'] );
                }

                $ps = BoostBot_Prompt_Strings::get( isset( $bot ) ? ( $bot->language ?? 'he' ) : 'he' );
                $limit_reply = '';
                if ( 'hourly' === $msg_check['limit_type'] ) {
                    $minutes = max( 1, ceil( $msg_check['retry_after'] / 60 ) );
                    $limit_reply = sprintf( $ps['rate_hourly'], $minutes );
                } elseif ( 'daily' === $msg_check['limit_type'] ) {
                    $limit_reply = $ps['rate_daily'];
                } elseif ( 'cooldown' === $msg_check['limit_type'] ) {
                    $limit_reply = sprintf( $ps['rate_cooldown'], $msg_check['cooldown_seconds'] );
                }

                return $this->success( array(
                    'rate_limited'     => true,
                    'limit_type'       => $msg_check['limit_type'],
                    'retry_after'      => $msg_check['retry_after'],
                    'cooldown_seconds' => $msg_check['cooldown_seconds'],
                    'remaining_hour'   => $msg_check['remaining_hour'],
                    'remaining_day'    => $msg_check['remaining_day'],
                    'reply'            => $limit_reply,
                    'conversation_id'  => $conversation_id,
                ) );
            }

            // Increment counters — visitor is allowed.
            BoostBot_Message_Limiter::increment( $msg_identifier );
        }

        try {
            // Initialize knowledge index
            if ( class_exists( 'BoostBot_Knowledge_Index' ) ) {
                $this->knowledge_index = new BoostBot_Knowledge_Index();
            }

            // Get conversation history - prefer from request, fallback to database
            $history = $request->get_param( 'history' ) ?: array();
            
            // Ensure history is an array
            if ( ! is_array( $history ) ) {
                $history = array();
            }
            
            // If no history from request, try to load from database
            if ( empty( $history ) && ! empty( $conversation_id ) ) {
                $conversation_model = new BoostBot_Conversation_Model();
                $conversation = $conversation_model->get_by_conversation_id( $conversation_id );
                
                if ( $conversation ) {
                    $session_data = json_decode( $conversation->session_data, true ) ?: array();
                    $history = $session_data['messages'] ?? array();
                }
            }
            
            BoostBot_Logger::info( sprintf( '[DEBUG] History messages count: %d', count( $history ) ) );

            // Resolve bot
            $bot = $this->resolve_bot( $page_context );
            $this->last_resolved_bot = $bot;

            // Call unified router first (if enabled)
            $unified_result = null;
            $needs_retrieval = true;
            $use_unified_router = (bool) BoostBot_Settings::get( 'use_unified_router', true );
            
            if ( $use_unified_router && class_exists( 'BoostBot_Credits_Backend_Client' ) ) {
                $unified_result = $this->call_unified_router( $message, $history, $page_context, $bot );
                
                if ( $unified_result && ! empty( $unified_result['routing'] ) ) {
                    $needs_retrieval = $unified_result['routing']['needs_retrieval'] ?? true;
                    
                    // Store routing inputs (including Gemini's new query building fields)
                    $this->last_routing_inputs = array(
                        'intents'           => $unified_result['intents'] ?? array(),
                        'entities'          => $unified_result['entities'] ?? array(),
                        'rewritten_query'   => $unified_result['rewritten_query'] ?? $message,
                        'original_query'    => $message,
                        'gemini_used'       => true,
                        
                        // New term classification (preferred)
                        'required_terms'    => $unified_result['required_terms'] ?? array(),
                        'optional_keywords' => $unified_result['optional_keywords'] ?? array(),
                        'boost_terms'       => $unified_result['boost_terms'] ?? array(),
                        
                        // Backward compatibility
                        'must_include'      => $unified_result['must_include'] ?? $unified_result['boost_terms'] ?? array(),
                        'expanded_keywords' => $unified_result['expanded_keywords'] ?? array(),
                    );
                    
                    // Log Gemini's query building decision
                    BoostBot_Logger::info( sprintf(
                        '[Gemini QB Legacy] Original: "%s" | Rewritten: "%s" | Required: %s',
                        mb_substr( $message, 0, 50 ),
                        mb_substr( $unified_result['rewritten_query'] ?? '', 0, 100 ),
                        implode( ',', $unified_result['required_terms'] ?? array() )
                    ) );
                }
            }

            // Search knowledge base if needed
            $evidence = '';
            if ( $needs_retrieval && $this->knowledge_index ) {
                $evidence = $this->search_knowledge( $message, $history );
            }

            // Get page content if needed
            $page_content = '';
            $needs_page = $unified_result['routing']['needs_page_context'] ?? true;
            if ( $needs_page ) {
                $page_content = $this->get_page_content( $page_context );
            }

            // ===== WIDGET DETECTION (before system prompt so LLM knows about widgets) =====
            $selected_widgets = array();
            $widget_ids = $unified_result['selected_widget_ids'] ?? array();
            $routing = $unified_result['routing'] ?? array();
            
            // Get bot's assigned widget types (NEVER use global settings)
            $bot_widget_types = $this->get_bot_widget_types( $bot );
            
            // Log what the router returned
            BoostBot_Logger::info( sprintf( '[DEBUG] Unified router selected widget IDs: %s', 
                ! empty( $widget_ids ) ? implode( ', ', $widget_ids ) : 'none' ) );
            BoostBot_Logger::info( sprintf( '[DEBUG] Unified router routing: %s', 
                wp_json_encode( $routing ) ) );
            BoostBot_Logger::info( sprintf( '[DEBUG] Bot widget types: %s', 
                ! empty( $bot_widget_types ) ? implode( ', ', $bot_widget_types ) : '(none assigned)' ) );
            
            // === OPENING SCREEN AI INSTRUCTIONS (per-question) ===
            // When present, these instructions take priority — suppress auto widget detection
            $ai_instructions = sanitize_textarea_field( $request->get_param( 'ai_instructions' ) ?? '' );
            $has_ai_instructions = ! empty( $ai_instructions );

            if ( $has_ai_instructions ) {
                // Skip widget auto-detection — admin set specific instructions for this question
                BoostBot_Logger::info( sprintf( '[DEBUG] AI instructions present — skipping widget auto-detection. Instructions: %s', mb_substr( $ai_instructions, 0, 100 ) ) );
            } else {
                // SMART WIDGET DETECTION: Use AI-based signals from unified router
                // The router's AI already analyzed the query - use its signals
                $needs_contacts = $routing['needs_contacts'] ?? false;
                $needs_widgets = $routing['needs_widgets'] ?? false;
                
                // If router says we need contacts/widgets, show lead form (only if bot has form widget assigned)
                if ( $needs_contacts || $needs_widgets ) {
                    if ( in_array( 'form', $bot_widget_types, true ) || in_array( 'lead_form', $bot_widget_types, true ) ) {
                        BoostBot_Logger::info( '[DEBUG] Lead form triggered by AI router (needs_contacts=' . ( $needs_contacts ? 'true' : 'false' ) . ', needs_widgets=' . ( $needs_widgets ? 'true' : 'false' ) . ')' );
                        $selected_widgets[] = array(
                            'type' => 'lead_form',
                            'id'   => 'ai_detected_lead_form',
                            'data' => $this->get_default_lead_form_config(),
                        );
                    }
                }
                
                // Fallback: keyword-based detection if router didn't identify widget need
                if ( empty( $selected_widgets ) ) {
                    $widgets_to_show = $this->detect_widget_intent( $message, $bot_widget_types );
                    if ( ! empty( $widgets_to_show ) ) {
                        $selected_widgets[] = $widgets_to_show;
                        BoostBot_Logger::info( sprintf( '[DEBUG] Widget triggered by keyword fallback: %s', $widgets_to_show['type'] ?? 'unknown' ) );
                    }
                }
            }
            
            // Also add router-selected widgets (specific IDs)
            if ( ! empty( $widget_ids ) ) {
                $allowed_widget_ids = array_fill_keys( $this->get_allowed_widget_ids_for_bot( $bot ), true );
                foreach ( $widget_ids as $widget_id ) {
                    if ( empty( $allowed_widget_ids[ (int) $widget_id ] ) ) {
                        continue;
                    }
                    $widget_config = $this->get_widget_config_by_id( $widget_id );
                    if ( $widget_config ) {
                        $selected_widgets[] = $widget_config;
                    }
                }
            }

            // Build system prompt WITH widget context (so LLM knows what widgets will be shown)
            $system_prompt = $this->build_system_prompt( $bot, $evidence, $page_content, $unified_result, $selected_widgets );

            // Prepend per-question AI instructions with high priority
            if ( $has_ai_instructions ) {
                $instruction_block = "## CRITICAL: ANSWER INSTRUCTIONS FOR THIS SPECIFIC QUESTION\n\n"
                    . "The following instructions were set by the site administrator for this specific question. "
                    . "You MUST follow them precisely. Do NOT suggest contact forms, lead forms, or any widgets "
                    . "unless these instructions explicitly tell you to. Answer based on these instructions:\n\n"
                    . $ai_instructions
                    . "\n\n---\n\n";
                $system_prompt = $instruction_block . $system_prompt;
                BoostBot_Logger::info( sprintf( '[DEBUG] AI instructions injected into system prompt: %s', mb_substr( $ai_instructions, 0, 100 ) ) );
            }

            // Build messages array for streaming
            $messages = $this->build_messages_array( $history, $message );

            $elapsed_ms = round( ( microtime( true ) - $t0 ) * 1000 );
            BoostBot_Logger::info( sprintf( '[TIMING] retrieve_evidence.total: %dms', $elapsed_ms ) );
            BoostBot_Logger::info( sprintf( '[DEBUG] Evidence length: %d chars', strlen( $evidence ) ) );
            BoostBot_Logger::info( sprintf( '[DEBUG] System prompt length: %d chars', strlen( $system_prompt ) ) );
            BoostBot_Logger::info( sprintf( '[DEBUG] Widgets to return: %d', count( $selected_widgets ) ) );

            return $this->success( array(
                'conversation_id' => $conversation_id,
                'system_prompt'   => $system_prompt,
                'messages'        => $messages,
                'evidence'        => $evidence,
                'history'         => $history,
                'routing'         => $unified_result['routing'] ?? null,
                'selected_widgets'=> $selected_widgets,
                'bot'             => array(
                    'name'        => $bot->bot_name ?? 'BoostBot',
                    'personality' => $bot->personality ?? '',
                ),
                'timing_ms'       => $elapsed_ms,
            ) );

        } catch ( Exception $e ) {
            BoostBot_Logger::error( 'Retrieval error: ' . $e->getMessage() );
            return $this->error( __( 'Retrieval failed', 'boostbot' ), 500 );
        }
    }

    /**
     * Resolve the active bot for the context.
     *
     * @param array $page_context Page context data.
     * @return object The bot object.
     */
    private function resolve_bot( array $page_context ) {
        try {
            if ( class_exists( 'BoostBot_Bot_Resolver' ) ) {
                $resolver = new BoostBot_Bot_Resolver();
                $post_id  = (int) ( $page_context['id'] ?? $page_context['page_id'] ?? $page_context['post_id'] ?? 0 );
                $page_url = $page_context['url'] ?? '';

                $context = $resolver->build_context_from_request( $post_id, $page_url );
                $bot     = $resolver->resolve( $context );
                if ( $bot ) {
                    return $bot;
                }
            }
        } catch ( Exception $e ) {
            BoostBot_Logger::error( 'Bot resolution error: ' . $e->getMessage() );
        }

        return (object) array(
            'id'          => 0,
            'bot_name'    => BoostBot_Settings::get( 'bot_name', 'BoostBot' ),
            'personality' => BoostBot_Settings::get( 'personality', '' ),
        );
    }

    /**
     * Call unified router.
     *
     * @param string $message      User message.
     * @param array  $history      Conversation history.
     * @param array  $page_context Page context.
     * @param object $bot          Bot object.
     * @return array|null Router result or null.
     */
    private function call_unified_router( string $message, array $history, array $page_context, $bot ) {
        try {
            $client = new BoostBot_Credits_Backend_Client();
            $site_id = $client->ensure_site_id();
            
            if ( is_wp_error( $site_id ) ) {
                return null;
            }

            // Build context_messages with correct schema (matches chat handler)
            $context_messages = array();
            $history_slice = array_slice( $history, -8 );
            foreach ( $history_slice as $msg ) {
                if ( ! isset( $msg['role'] ) || ! isset( $msg['content'] ) ) {
                    continue;
                }
                $role = ( 'user' === $msg['role'] ) ? 'user' : 'assistant';
                $content = mb_substr( (string) $msg['content'], 0, 800 );
                if ( '' !== trim( $content ) ) {
                    $context_messages[] = array(
                        'role'    => $role,
                        'content' => $content,
                    );
                }
            }

            // Get available widgets for this bot
            $available_widgets = array();
            if ( $bot && isset( $bot->id ) && class_exists( 'BoostBot_Widget_Model' ) ) {
                try {
                    $widget_model = new BoostBot_Widget_Model();
                    $assigned_widgets = $widget_model->get_for_chatbot( (int) $bot->id );
                    foreach ( $assigned_widgets as $widget ) {
                        $available_widgets[] = array(
                            'id'          => (int) ( $widget->id ?? 0 ),
                            'type'        => (string) ( $widget->type ?? '' ),
                            'name'        => (string) ( $widget->name ?? '' ),
                            'description' => (string) ( $widget->description ?? '' ),
                        );
                    }
                } catch ( Exception $e ) {
                    // Silent fail - continue without widgets
                }
            }

            // Get available contacts
            $available_contacts = array();
            // TODO: Populate from contact list if needed

            // Get brand name from BrandCard (per-language)
            $bot_language = $bot->language ?? 'he';
            $brand_name = null;
            $brand_card_json = (string) BoostBot_Settings::get_for_language( 'brand_card_global_json', $bot_language, '' );
            if ( '' !== trim( $brand_card_json ) ) {
                $decoded = json_decode( $brand_card_json, true );
                if ( is_array( $decoded ) && isset( $decoded['company_name'] ) ) {
                    $brand_name = (string) $decoded['company_name'];
                }
            }

            $result = $client->rag_unified_router(
                (string) $site_id,
                array(
                    'query'              => $message,
                    'language_hint'      => $bot->language ?? BoostBot_Language_Manager::get_instance()->get_frontend_language(),
                    'context_messages'   => $context_messages,
                    'available_contacts' => $available_contacts,
                    'available_widgets'  => $available_widgets,
                    'brand_name'         => $brand_name,
                    'page_title'         => $page_context['title'] ?? '',
                    'page_url'           => $page_context['url'] ?? '',
                )
            );

            if ( is_wp_error( $result ) ) {
                return null;
            }

            return $result;
        } catch ( Exception $e ) {
            BoostBot_Logger::error( 'Unified router error: ' . $e->getMessage() );
            return null;
        }
    }

    /**
     * Search knowledge base and return two chunk sets for orchestrated flow.
     * 
     * Now uses Gemini-powered query building instead of local heuristics.
     * Gemini handles: query rewriting, vague follow-up expansion, keyword extraction.
     *
     * @param string $query   User query.
     * @param array  $history Conversation history.
     * @return array { 'small_chunks' => string, 'large_chunks' => string, 'raw_chunks' => array }
     */
    private function search_knowledge_orchestrated( string $query, array $history ): array {
        $result = array(
            'small_chunks' => '',
            'large_chunks' => '',
            'raw_chunks'   => array(),
        );

        if ( ! $this->knowledge_index ) {
            return $result;
        }

        try {
            $stats = $this->knowledge_index->get_stats();
            if ( empty( $stats['total'] ) || $stats['total'] < 1 ) {
                return $result;
            }

            // === GEMINI-POWERED QUERY BUILDING ===
            // Gemini now handles: query rewriting, vague follow-up expansion, keyword extraction
            // The rewritten_query from Gemini is already optimized for retrieval
            $retrieval_query = $this->last_routing_inputs['rewritten_query'] ?? $query;
            $gemini_used = $this->last_routing_inputs['gemini_used'] ?? false;
            
            // === FALLBACK: Local enhancement if Gemini wasn't used ===
            if ( ! $gemini_used ) {
                $ps_lang = ( $this->last_resolved_bot->language ?? 'he' );
                $ps = BoostBot_Prompt_Strings::get( $ps_lang ?: 'he' );
                $vague_indicators = $ps['vague_indicators'];
                $is_vague_followup = false;
                foreach ( $vague_indicators as $indicator ) {
                    if ( mb_strpos( mb_strtolower( $query ), $indicator ) !== false ) {
                        $is_vague_followup = true;
                        break;
                    }
                }
                
                if ( $is_vague_followup && ! empty( $history ) ) {
                    $history_context = $this->extract_history_topics( $history, $ps_lang );
                    if ( ! empty( $history_context ) ) {
                        $retrieval_query = $query . ' ' . $history_context;
                        BoostBot_Logger::info( sprintf( '[Fallback] Local vague query enhancement: %s', $history_context ) );
                    }
                }
            }

            // Get RAPTOR hints (hierarchical summaries - keep this as it's not query building)
            $force_chunk_ids = $this->knowledge_index->get_raptor_chunk_hints(
                $retrieval_query,
                array( 'limit_nodes' => 10, 'max_chunk_ids' => 80 )
            );

            // === BUILD TERM CONFIGURATION FROM GEMINI OUTPUT ===
            // New term classification: required_terms (AND), optional_keywords (OR), boost_terms (ranking)
            $term_config = array(
                'required_terms'    => array(),
                'optional_keywords' => array(),
                'boost_terms'       => array(),
            );
            
            // Build required_terms (AND logic - all must appear)
            foreach ( $this->last_routing_inputs['required_terms'] ?? array() as $term ) {
                if ( is_string( $term ) && '' !== trim( $term ) ) {
                    $term_config['required_terms'][] = trim( $term );
                }
            }
            
            // Build optional_keywords (OR logic - ranked by count)
            foreach ( $this->last_routing_inputs['optional_keywords'] ?? array() as $kw ) {
                if ( is_string( $kw ) && '' !== trim( $kw ) ) {
                    $term_config['optional_keywords'][] = trim( $kw );
                }
            }
            
            // Build boost_terms (ranking boost - synonyms)
            foreach ( $this->last_routing_inputs['boost_terms'] ?? array() as $kw ) {
                if ( is_string( $kw ) && '' !== trim( $kw ) ) {
                    $term_config['boost_terms'][] = trim( $kw );
                }
            }
            
            // Add entities to optional keywords
            foreach ( $this->last_routing_inputs['entities'] ?? array() as $entity ) {
                if ( is_string( $entity ) && '' !== trim( $entity ) && ! in_array( trim( $entity ), $term_config['optional_keywords'], true ) ) {
                    $term_config['optional_keywords'][] = trim( $entity );
                }
            }
            
            // Build extra_keywords for backward compatibility (combined list for prefilter)
            $extra_keywords = array_merge(
                $term_config['optional_keywords'],
                $term_config['boost_terms'],
                // Fallback to old fields if new ones are empty
                $this->last_routing_inputs['must_include'] ?? array(),
                $this->last_routing_inputs['expanded_keywords'] ?? array()
            );
            $extra_keywords = array_unique( array_filter( $extra_keywords ) );
            
            // Get required_terms for hard post-filtering (backward compatible)
            $hard_terms = $term_config['required_terms'];

            // Semantic search - get more chunks for two-tier system
            $bot_language = $bot->language ?? null;
            $chunks = $this->knowledge_index->semantic_search( $retrieval_query, array(
                'limit'           => 15,
                'prefilter_limit' => 50,
                'max_chunks'      => 400,
                'extra_keywords'  => $extra_keywords,
                'force_chunk_ids' => $force_chunk_ids,
                'term_config'     => $term_config,
                'language'        => $bot_language,
            ) );

            // === FALLBACK SEARCH ===
            // If semantic search returned no results OR if required_terms didn't match any chunks,
            // fall back to title/summary/keywords search on knowledge_index table.
            // This handles pages that exist but don't have embedding chunks.
            $used_fallback = false;
            
            if ( empty( $chunks ) ) {
                BoostBot_Logger::warning( '[Fallback] Semantic search returned 0 chunks, trying fallback title search' );
                $chunks = $this->knowledge_index->fallback_title_search( $retrieval_query, array(
                    'limit'            => 10,
                    'required_terms'   => $term_config['required_terms'],
                    'optional_keywords'=> $term_config['optional_keywords'],
                ) );
                $used_fallback = true;
            }
            
            // Also check if we should merge with fallback results for required_terms
            // This ensures pages without chunks are still found when their title matches required_terms
            if ( ! empty( $hard_terms ) && ! $used_fallback ) {
                // Check if any chunk contains the required terms
                $has_required_match = false;
                foreach ( $chunks as $chunk ) {
                    $chunk_text = mb_strtolower( $chunk['chunk_text'] ?? '' );
                    $chunk_title = mb_strtolower( $chunk['title'] ?? '' );
                    $combined = $chunk_text . ' ' . $chunk_title;
                    
                    $all_terms_found = true;
                    foreach ( $hard_terms as $term ) {
                        if ( mb_strpos( $combined, mb_strtolower( $term ) ) === false ) {
                            $all_terms_found = false;
                            break;
                        }
                    }
                    
                    if ( $all_terms_found ) {
                        $has_required_match = true;
                        break;
                    }
                }
                
                // If no chunk matched required terms, try fallback and merge results
                if ( ! $has_required_match ) {
                    BoostBot_Logger::warning( sprintf( '[Fallback] No chunks matched required_terms: %s, trying fallback', implode( ', ', $hard_terms ) ) );
                    
                    $fallback_chunks = $this->knowledge_index->fallback_title_search( $retrieval_query, array(
                        'limit'            => 5,
                        'required_terms'   => $term_config['required_terms'],
                        'optional_keywords'=> $term_config['optional_keywords'],
                    ) );
                    
                    if ( ! empty( $fallback_chunks ) ) {
                        // Prepend fallback results (they contain required terms) to ensure they're included
                        $chunks = array_merge( $fallback_chunks, $chunks );
                        $used_fallback = true;
                        BoostBot_Logger::info( sprintf( '[Fallback] Merged %d fallback results with %d semantic results',
                            count( $fallback_chunks ), count( $chunks ) - count( $fallback_chunks ) ) );
                    }
                }
            }

            if ( empty( $chunks ) ) {
                return $result;
            }
            
            // === HARD TERMS FILTERING (from Gemini's required_terms) ===
            // If Gemini specified required_terms, filter chunks to only include those containing the terms
            if ( ! empty( $hard_terms ) ) {
                $filtered_chunks = array();
                foreach ( $chunks as $chunk ) {
                    $chunk_text = mb_strtolower( $chunk['chunk_text'] ?? '' );
                    $chunk_title = mb_strtolower( $chunk['title'] ?? '' );
                    $combined_text = $chunk_text . ' ' . $chunk_title;
                    
                    // Check if ALL required terms appear in the chunk
                    $all_terms_found = true;
                    foreach ( $hard_terms as $term ) {
                        if ( mb_strpos( $combined_text, mb_strtolower( $term ) ) === false ) {
                            $all_terms_found = false;
                            break;
                        }
                    }
                    
                    if ( $all_terms_found ) {
                        $filtered_chunks[] = $chunk;
                    }
                }
                
                // Only use filtered results if we have enough; otherwise fall back to unfiltered
                if ( count( $filtered_chunks ) >= 2 ) {
                    BoostBot_Logger::info( sprintf( '[Gemini QB] Hard terms filter: %d → %d chunks (terms: %s)',
                        count( $chunks ), count( $filtered_chunks ), implode( ', ', $hard_terms ) ) );
                    $chunks = $filtered_chunks;
                } else {
                    BoostBot_Logger::info( sprintf( '[Gemini QB] Hard terms filter skipped (too few results): %d chunks for terms: %s',
                        count( $filtered_chunks ), implode( ', ', $hard_terms ) ) );
                }
            }

            $result['raw_chunks'] = $chunks;

            // === BUILD SMALL CHUNKS (top 3, ~1500 chars) ===
            $small_context = "Quick context:\n\n";
            $small_max = 1500;
            $small_count = 0;
            foreach ( $chunks as $item ) {
                if ( $small_count >= 3 ) break;
                
                $text = isset( $item['chunk_text'] ) ? (string) $item['chunk_text'] : '';
                $title = isset( $item['title'] ) ? (string) $item['title'] : '';
                $url   = isset( $item['url'] ) ? urldecode( (string) $item['url'] ) : '';
                
                if ( mb_strlen( $text ) > 400 ) {
                    $text = mb_substr( $text, 0, 400 ) . '...';
                }
                
                // Include URL so Gemini can pass it to GPT for linking
                $block = "• {$title}: {$text}" . ( $url ? " (URL: {$url})" : '' ) . "\n";
                if ( mb_strlen( $small_context ) + mb_strlen( $block ) > $small_max ) {
                    break;
                }
                $small_context .= $block;
                $small_count++;
            }
            $result['small_chunks'] = $small_context;

            // === BUILD LARGE CHUNKS (top 8, ~5000 chars) ===
            $large_context = "Detailed evidence from website:\n\n";
            $large_max = 5000;
            $large_count = 0;
            foreach ( $chunks as $item ) {
                if ( $large_count >= 8 ) break;
                
                $title = isset( $item['title'] ) ? (string) $item['title'] : '';
                $url   = isset( $item['url'] ) ? urldecode( (string) $item['url'] ) : '';
                $text  = isset( $item['chunk_text'] ) ? (string) $item['chunk_text'] : '';
                
                if ( mb_strlen( $text ) > 1000 ) {
                    $text = mb_substr( $text, 0, 1000 ) . '...';
                }
                
                $block = "- {$title}\n\"{$text}\"\nURL: {$url}\n\n";
                if ( mb_strlen( $large_context ) + mb_strlen( $block ) > $large_max ) {
                    break;
                }
                $large_context .= $block;
                $large_count++;
            }
            $result['large_chunks'] = $large_context;

            BoostBot_Logger::info( sprintf( '[DEBUG] Orchestrated search: %d chunks, small=%d chars, large=%d chars',
                count( $chunks ), mb_strlen( $result['small_chunks'] ), mb_strlen( $result['large_chunks'] ) ) );

            return $result;

        } catch ( Exception $e ) {
            BoostBot_Logger::error( 'Knowledge search error: ' . $e->getMessage() );
            return $result;
        }
    }

    /**
     * Search knowledge base (legacy single-tier).
     * 
     * Now uses Gemini-powered query building instead of local heuristics.
     *
     * @param string $query   User query.
     * @param array  $history Conversation history.
     * @return string Evidence text.
     */
    private function search_knowledge( string $query, array $history ): string {
        if ( ! $this->knowledge_index ) {
            return '';
        }

        try {
            // Get stats
            $stats = $this->knowledge_index->get_stats();
            if ( empty( $stats['total'] ) || $stats['total'] < 1 ) {
                return '';
            }

            // === GEMINI-POWERED QUERY BUILDING ===
            $retrieval_query = $this->last_routing_inputs['rewritten_query'] ?? $query;
            $gemini_used = $this->last_routing_inputs['gemini_used'] ?? false;
            
            // === FALLBACK: Local enhancement if Gemini wasn't used ===
            if ( ! $gemini_used ) {
                $ps_lang = ( $this->last_resolved_bot->language ?? 'he' );
                $ps = BoostBot_Prompt_Strings::get( $ps_lang ?: 'he' );
                $vague_indicators = $ps['vague_indicators'];
                $is_vague_followup = false;
                foreach ( $vague_indicators as $indicator ) {
                    if ( mb_strpos( mb_strtolower( $query ), $indicator ) !== false ) {
                        $is_vague_followup = true;
                        break;
                    }
                }
                
                if ( $is_vague_followup && ! empty( $history ) ) {
                    $history_context = $this->extract_history_topics( $history, $ps_lang );
                    if ( ! empty( $history_context ) ) {
                        $retrieval_query = $query . ' ' . $history_context;
                        BoostBot_Logger::info( sprintf( '[Fallback] Local vague query enhancement: %s', $history_context ) );
                    }
                }
            }
            
            BoostBot_Logger::info( sprintf( '[DEBUG] Original query: %s', $query ) );
            BoostBot_Logger::info( sprintf( '[DEBUG] Final retrieval query: %s', $retrieval_query ) );
            BoostBot_Logger::info( sprintf( '[DEBUG] Gemini used: %s', $gemini_used ? 'yes' : 'no (fallback)' ) );

            // Get RAPTOR hints
            $force_chunk_ids = $this->knowledge_index->get_raptor_chunk_hints(
                $retrieval_query,
                array( 'limit_nodes' => 8, 'max_chunk_ids' => 60 )
            );

            // === BUILD TERM CONFIGURATION FROM GEMINI OUTPUT ===
            $term_config = array(
                'required_terms'    => array(),
                'optional_keywords' => array(),
                'boost_terms'       => array(),
            );
            
            // Build required_terms (AND logic)
            foreach ( $this->last_routing_inputs['required_terms'] ?? array() as $term ) {
                if ( is_string( $term ) && '' !== trim( $term ) ) {
                    $term_config['required_terms'][] = trim( $term );
                }
            }
            
            // Build optional_keywords (OR logic)
            foreach ( $this->last_routing_inputs['optional_keywords'] ?? array() as $kw ) {
                if ( is_string( $kw ) && '' !== trim( $kw ) ) {
                    $term_config['optional_keywords'][] = trim( $kw );
                }
            }
            
            // Build boost_terms (ranking boost)
            foreach ( $this->last_routing_inputs['boost_terms'] ?? array() as $kw ) {
                if ( is_string( $kw ) && '' !== trim( $kw ) ) {
                    $term_config['boost_terms'][] = trim( $kw );
                }
            }
            
            // Add entities to optional keywords
            foreach ( $this->last_routing_inputs['entities'] ?? array() as $entity ) {
                if ( is_string( $entity ) && '' !== trim( $entity ) && ! in_array( trim( $entity ), $term_config['optional_keywords'], true ) ) {
                    $term_config['optional_keywords'][] = trim( $entity );
                }
            }
            
            // Build extra_keywords for backward compatibility
            $extra_keywords = array_merge(
                $term_config['optional_keywords'],
                $term_config['boost_terms'],
                $this->last_routing_inputs['must_include'] ?? array(),
                $this->last_routing_inputs['expanded_keywords'] ?? array()
            );
            $extra_keywords = array_unique( array_filter( $extra_keywords ) );
            
            // Get required_terms for hard post-filtering
            $hard_terms = $term_config['required_terms'];

            // Semantic search
            $bot_language = $bot->language ?? null;
            $chunks = $this->knowledge_index->semantic_search( $retrieval_query, array(
                'limit'           => 10,
                'prefilter_limit' => 40,
                'max_chunks'      => 300,
                'extra_keywords'  => $extra_keywords,
                'force_chunk_ids' => $force_chunk_ids,
                'term_config'     => $term_config,
                'language'        => $bot_language,
            ) );

            // === FALLBACK SEARCH ===
            // If semantic search returned no results, use fallback title/summary search
            if ( empty( $chunks ) ) {
                BoostBot_Logger::warning( '[Legacy Fallback] Semantic search returned 0 chunks, trying fallback' );
                $chunks = $this->knowledge_index->fallback_title_search( $retrieval_query, array(
                    'limit'            => 8,
                    'required_terms'   => $term_config['required_terms'],
                    'optional_keywords'=> $term_config['optional_keywords'],
                ) );
            }

            if ( empty( $chunks ) ) {
                return '';
            }
            
            // === HARD TERMS FILTERING ===
            if ( ! empty( $hard_terms ) ) {
                $filtered_chunks = array();
                foreach ( $chunks as $chunk ) {
                    $chunk_text = mb_strtolower( $chunk['chunk_text'] ?? '' );
                    $chunk_title = mb_strtolower( $chunk['title'] ?? '' );
                    $combined_text = $chunk_text . ' ' . $chunk_title;
                    
                    $all_terms_found = true;
                    foreach ( $hard_terms as $term ) {
                        if ( mb_strpos( $combined_text, mb_strtolower( $term ) ) === false ) {
                            $all_terms_found = false;
                            break;
                        }
                    }
                    
                    if ( $all_terms_found ) {
                        $filtered_chunks[] = $chunk;
                    }
                }
                
                if ( count( $filtered_chunks ) >= 2 ) {
                    BoostBot_Logger::info( sprintf( '[Gemini QB] Hard terms filter: %d → %d chunks',
                        count( $chunks ), count( $filtered_chunks ) ) );
                    $chunks = $filtered_chunks;
                }
            }

            // Build evidence bundle
            // Note: Increased limits to ensure complete information (e.g., all pricing packages)
            $context = "Relevant evidence from the website:\n\n";
            $max_total = 5000;  // Increased from 2000 to capture more context
            $selected = array_slice( $chunks, 0, 8 );  // Increased from 4 to include more chunks

            // Debug: Log retrieved chunks count and sources
            BoostBot_Logger::info( sprintf( '[DEBUG] Semantic search returned %d chunks, using top %d', 
                count( $chunks ), count( $selected ) ) );

            $chunk_index = 0;
            foreach ( $selected as $item ) {
                $title = isset( $item['title'] ) ? (string) $item['title'] : '';
                $url   = isset( $item['url'] ) ? urldecode( (string) $item['url'] ) : '';
                $text  = isset( $item['chunk_text'] ) ? (string) $item['chunk_text'] : '';
                $score = isset( $item['score'] ) ? (float) $item['score'] : 0;

                // Debug: Log each chunk being included
                BoostBot_Logger::info( sprintf( '[DEBUG] Chunk %d: score=%.3f, title="%s", text_len=%d', 
                    $chunk_index++, $score, mb_substr( $title, 0, 50 ), mb_strlen( $text ) ) );

                // Increased from 500 to preserve complete information in each chunk
                if ( mb_strlen( $text ) > 1000 ) {
                    $text = mb_substr( $text, 0, 1000 ) . '...';
                }

                $block = "- {$title}\n\"{$text}\"\nURL: {$url}\n\n";
                if ( mb_strlen( $context ) + mb_strlen( $block ) > $max_total ) {
                    BoostBot_Logger::warning( sprintf( '[DEBUG] Evidence truncated at chunk %d (max_total=%d)', 
                        $chunk_index, $max_total ) );
                    break;
                }
                $context .= $block;
            }

            return $context;

        } catch ( Exception $e ) {
            BoostBot_Logger::error( 'Knowledge search error: ' . $e->getMessage() );
            return '';
        }
    }

    /**
     * Get page content from the knowledge index.
     *
     * If the current page has been deep-indexed, include the summary and
     * full_content (capped) so the LLM has rich context about the page
     * the visitor is currently viewing.
     *
     * @param array $page_context Page context (id, url, title).
     * @return string Page content for the system prompt.
     */
    private function get_page_content( array $page_context ): string {
        $title   = $page_context['title'] ?? '';
        $url     = $page_context['url'] ?? '';
        $post_id = (int) ( $page_context['id'] ?? $page_context['page_id'] ?? $page_context['post_id'] ?? 0 );

        if ( empty( $title ) && empty( $url ) && $post_id <= 0 ) {
            return '';
        }

        $parts = array();
        $parts[] = "Current page: {$title}";
        if ( ! empty( $url ) ) {
            $parts[] = "URL: {$url}";
        }

        // Try to enrich with indexed content for this page
        if ( $post_id > 0 && $this->knowledge_index ) {
            $post = get_post( $post_id );
            $content_type = $post ? $post->post_type : 'page';
            $entry = $this->knowledge_index->get_by_content_id( $post_id, $content_type );

            if ( $entry ) {
                if ( ! empty( $entry->summary ) ) {
                    $parts[] = "\nPage summary: " . $entry->summary;
                }
                if ( ! empty( $entry->full_content ) ) {
                    $content = $entry->full_content;
                    if ( mb_strlen( $content ) > 4000 ) {
                        $content = mb_substr( $content, 0, 4000 ) . '...';
                    }
                    $parts[] = "\nPage content:\n" . $content;
                }
            }
        }

        return implode( "\n", $parts ) . "\n";
    }

    /**
     * Build system prompt.
     *
     * @param object      $bot            Bot object.
     * @param string      $evidence       Evidence text.
     * @param string      $page_content   Page content.
     * @param array|null  $unified_result Unified router result.
     * @return string System prompt.
     */
    private function build_system_prompt( $bot, string $evidence, string $page_content, $unified_result, array $selected_widgets = array() ): string {
        $ps = BoostBot_Prompt_Strings::get( $bot->language ?? 'he' );
        $bot_name = $bot->bot_name ?? 'BoostBot';
        $personality = $bot->personality ?? '';

        $prompt = "You are {$bot_name}, a helpful AI assistant.\n\n";

        if ( ! empty( $personality ) ) {
            $prompt .= "Your personality: {$personality}\n\n";
        }

        // Add brand card if available (per-language)
        $brand_card = (string) BoostBot_Settings::get_for_language( 'brand_card_global_json', $bot->language ?? '', '' );
        if ( ! empty( $brand_card ) ) {
            $decoded = json_decode( $brand_card, true );
            if ( is_array( $decoded ) ) {
                $prompt .= "About the business:\n";
                if ( ! empty( $decoded['business_name'] ) ) {
                    $prompt .= "- Name: {$decoded['business_name']}\n";
                }
                if ( ! empty( $decoded['tagline'] ) ) {
                    $prompt .= "- Tagline: {$decoded['tagline']}\n";
                }
                if ( ! empty( $decoded['description'] ) ) {
                    $prompt .= "- Description: {$decoded['description']}\n";
                }
                $prompt .= "\n";
            }
        }

        if ( ! empty( $page_content ) ) {
            $prompt .= $page_content . "\n";
        }

        if ( ! empty( $evidence ) ) {
            $prompt .= $evidence . "\n";
        }

        // Add widget context so LLM knows what interactive elements will be shown
        if ( ! empty( $selected_widgets ) ) {
            $prompt .= "\n=== WIDGETS BEING DISPLAYED ===\n";
            $prompt .= "The following interactive widgets will be shown to the user along with your response:\n";
            foreach ( $selected_widgets as $widget ) {
                $widget_type = $widget['type'] ?? 'unknown';
                $widget_title = $widget['data']['title'] ?? '';
                
                if ( $widget_type === 'lead_form' || $widget_type === 'form' ) {
                    $prompt .= "- LEAD FORM: A contact form titled \"{$widget_title}\" will appear so the user can leave their details.\n";
                    $prompt .= "  You should acknowledge this in your response (e.g., \"{$ps['prompt_form_example']}\").\n";
                } elseif ( $widget_type === 'calendar_meeting' || $widget_type === 'calendar_call' ) {
                    $prompt .= "- CALENDAR: A scheduling widget will appear so the user can book a meeting/call.\n";
                    $prompt .= "  You should mention this in your response (e.g., \"{$ps['prompt_calendar_example']}\").\n";
                } elseif ( $widget_type === 'youtube' ) {
                    $prompt .= "- YOUTUBE: A YouTube video will appear.\n";
                    $prompt .= "  You should mention this in your response (e.g., \"{$ps['prompt_youtube_example']}\" or \"{$ps['prompt_youtube_alt']}\").\n";
                } elseif ( $widget_type === 'gallery' ) {
                    $prompt .= "- GALLERY: An image gallery will appear.\n";
                    $prompt .= "  You should mention this in your response (e.g., \"{$ps['prompt_gallery_example']}\").\n";
                } elseif ( $widget_type === 'whatsapp' ) {
                    $prompt .= "- WHATSAPP: A WhatsApp contact button will appear.\n";
                    $prompt .= "  You should mention this in your response (e.g., \"{$ps['prompt_whatsapp_example']}\").\n";
                } elseif ( $widget_type === 'contact_card' ) {
                    $prompt .= "- CONTACT CARD: A contact card with staff details will appear.\n";
                    $prompt .= "  You should mention this in your response.\n";
                } else {
                    $prompt .= "- {$widget_type}: Interactive element will be displayed.\n";
                }
            }
            $prompt .= "=== END WIDGETS ===\n\n";
        }

        // Add info about ALL available widgets for this bot (so LLM can mention them naturally)
        $bot_id = isset( $bot->id ) ? absint( $bot->id ) : 0;
        if ( $bot_id > 0 && class_exists( 'BoostBot_Widget_Model' ) ) {
            try {
                $widget_model = new BoostBot_Widget_Model();
                $bot_widgets = $widget_model->get_for_chatbot( $bot_id );
                if ( ! empty( $bot_widgets ) ) {
                    $widget_names = array(
                        'form'             => $ps['widget_form'],
                        'lead_form'        => $ps['widget_form_alt'],
                        'calendar_meeting' => $ps['widget_calendar_meet'],
                        'calendar_call'    => $ps['widget_calendar_call'],
                        'youtube'          => $ps['widget_youtube'],
                        'gallery'          => $ps['widget_gallery'],
                        'whatsapp'         => $ps['widget_whatsapp'],
                        'contact_card'     => $ps['widget_contact_card'],
                        'survey'           => $ps['widget_survey'],
                    );
                    $prompt .= "\n=== YOUR AVAILABLE CAPABILITIES ===\n";
                    $prompt .= "You have the following interactive widgets available. When the user asks for something related, let them know it's available:\n";
                    foreach ( $bot_widgets as $w ) {
                        $wtype = $w->type ?? '';
                        $wname = $w->name ?? ( $widget_names[ $wtype ] ?? $wtype );
                        $desc  = $w->description ?? '';
                        if ( empty( $desc ) ) {
                            $desc = BoostBot_Widget_Model::get_default_bot_description( $wtype );
                        }
                        $prompt .= "- {$wname} ({$wtype})";
                        if ( ! empty( $desc ) ) {
                            $prompt .= ": {$desc}";
                        }
                        $prompt .= "\n";
                    }
                    $prompt .= "When the user asks about one of these capabilities, mention that you can provide it and it will appear automatically.\n";
                    $prompt .= "=== END CAPABILITIES ===\n\n";
                }
            } catch ( Exception $e ) {
                // Silently ignore
            }
        }

        $prompt .= "Instructions:\n";
        $lang_name = ( 'he' === ( $bot->language ?? 'he' ) ) ? 'Hebrew (עברית)' : ucfirst( $bot->language ?? 'English' );
        $prompt .= "- ALWAYS respond in {$lang_name}.\n";
        $prompt .= "- Be concise and helpful.\n";
        $prompt .= "- Use the provided evidence to answer questions accurately.\n";
        $prompt .= "- Use the conversation history context - if you already provided information in a previous response, reference it rather than saying you don't have it.\n";
        $prompt .= "- If you've already shared all available information on a topic, say so (e.g., \"{$ps['prompt_exhausted_example']}\").\n";
        $prompt .= "- Only say you don't have information if it truly wasn't mentioned before AND isn't in the evidence.\n";
        if ( ! empty( $selected_widgets ) ) {
            $prompt .= "- Reference the widgets being displayed when appropriate.\n";
        }

        return $prompt;
    }

    /**
     * Build messages array for LLM.
     *
     * @param array  $history Conversation history.
     * @param string $message Current user message.
     * @return array Messages array.
     */
    private function build_messages_array( array $history, string $message ): array {
        $messages = array();

        // Add full conversation history for complete context
        foreach ( $history as $msg ) {
            if ( ! is_array( $msg ) ) {
                continue;
            }
            $incoming_role = (string) ( $msg['role'] ?? '' );
            $role = ( $incoming_role === 'bot' || $incoming_role === 'assistant' ) ? 'assistant' : 'user';
            $content = $msg['content'] ?? '';
            if ( ! empty( $content ) ) {
                $messages[] = array(
                    'role'    => $role,
                    'content' => $content,
                );
            }
        }

        // Add current message
        $messages[] = array(
            'role'    => 'user',
            'content' => $message,
        );

        // Debug: log constructed messages so we can verify role mapping in multi-turn chats.
        // Keep logs compact to avoid dumping full conversations into debug.log.
        try {
            $debug_messages = array();
            foreach ( $messages as $m ) {
                $preview = isset( $m['content'] ) ? (string) $m['content'] : '';
                $preview = preg_replace( '/\s+/', ' ', $preview );
                $debug_messages[] = array(
                    'role'    => $m['role'] ?? '',
                    'preview' => mb_substr( $preview, 0, 120 ),
                    'len'     => isset( $m['content'] ) ? mb_strlen( (string) $m['content'] ) : 0,
                );
            }
            BoostBot_Logger::info( sprintf( '[DEBUG] build_messages_array(): %s', wp_json_encode( $debug_messages ) ) );
        } catch ( Exception $e ) {
            // Ignore logging errors (never break response).
        }

        return $messages;
    }

    /**
     * Extract key topics from conversation history for query enhancement.
     *
     * @param array $history Conversation history.
     * @return string Space-separated key topics.
     */
    private function extract_history_topics( array $history, string $language = 'he' ): string {
        $topics = array();

        $keywords_by_lang = array(
            'he' => array(
                'עיצוב גרפי', 'עיצוב לוגו', 'עיצוב', 'מיתוג', 'לוגו',
                'בניית אתר', 'אתר', 'אתרים', 'וורדפרס',
                'שיווק דיגיטלי', 'שיווק', 'פרסום', 'קידום',
                'צ\'אטבוט', 'בוט',
                'מחיר', 'עלות', 'חבילה', 'חבילות',
                'פרמיום', 'מתקדם', 'בסיסי',
                'משלוח', 'הנחה', 'שירות', 'אחריות', 'מוצר', 'מבצע',
            ),
            'en' => array(
                'graphic design', 'logo design', 'design', 'branding', 'logo',
                'website', 'web development', 'wordpress',
                'digital marketing', 'marketing', 'advertising', 'seo',
                'chatbot', 'bot',
                'price', 'cost', 'package', 'plan', 'pricing',
                'premium', 'advanced', 'basic',
                'shipping', 'discount', 'service', 'warranty', 'product', 'sale',
            ),
        );

        $topic_patterns = $keywords_by_lang[ $language ] ?? $keywords_by_lang['he'];

        $recent = array_slice( $history, -4 );

        foreach ( $recent as $msg ) {
            $content = $msg['content'] ?? '';
            if ( empty( $content ) ) {
                continue;
            }

            $content_lower = mb_strtolower( $content );
            foreach ( $topic_patterns as $pattern ) {
                if ( mb_strpos( $content_lower, mb_strtolower( $pattern ) ) !== false ) {
                    $topics[ $pattern ] = true;
                }
            }
        }

        return implode( ' ', array_keys( $topics ) );
    }
    
    /**
     * Get widget types assigned to a bot.
     *
     * @param object|null $bot The bot object.
     * @return array Array of widget type strings (e.g., ['form', 'gallery', 'calendar_meeting']).
     */
    private function get_bot_widget_types( $bot ): array {
        if ( ! $bot || empty( $bot->id ) || ! class_exists( 'BoostBot_Widget_Model' ) ) {
            return array();
        }
        
        $widget_types = array();
        try {
            $widget_model = new BoostBot_Widget_Model();
            $assigned_widgets = $widget_model->get_for_chatbot( absint( $bot->id ) );
            if ( ! empty( $assigned_widgets ) ) {
                foreach ( $assigned_widgets as $widget ) {
                    $widget_type = $widget->type ?? 'unknown';
                    if ( ! in_array( $widget_type, $widget_types, true ) ) {
                        $widget_types[] = $widget_type;
                    }
                }
            }
        } catch ( Exception $e ) {
            BoostBot_Logger::error( 'Failed to get bot widget types: ' . $e->getMessage() );
        }

        return $this->filter_widget_types_for_bot( $bot, $widget_types );
    }

    /**
     * Remove WooCommerce widget types when the bot has no valid Woo preset.
     *
     * @param object|null $bot          Bot object.
     * @param array       $widget_types Raw widget type list.
     * @return array
     */
    private function filter_widget_types_for_bot( $bot, array $widget_types ): array {
        if ( $this->bot_has_valid_woocommerce_preset( $bot ) ) {
            return $widget_types;
        }

        return array_values(
            array_filter(
                $widget_types,
                static function ( $widget_type ) {
                    return ! class_exists( 'BoostBot_Widget_Model' )
                        || ! BoostBot_Widget_Model::is_woocommerce_widget_type( (string) $widget_type );
                }
            )
        );
    }

    /**
     * Check whether the current bot has a valid WooCommerce preset.
     *
     * @param object|null $bot Bot object.
     * @return bool
     */
    private function bot_has_valid_woocommerce_preset( $bot ): bool {
        if ( ! $bot || empty( $bot->woocommerce_preset_id ) || ! class_exists( 'BoostBot_WooCommerce_Preset_Model' ) ) {
            return false;
        }

        try {
            return ! empty( ( new BoostBot_WooCommerce_Preset_Model() )->get( (int) $bot->woocommerce_preset_id ) );
        } catch ( \Throwable $e ) {
            BoostBot_Logger::error( 'Failed to validate Woo preset: ' . $e->getMessage() );
        }

        return false;
    }

    /**
     * Check whether a widget should be exposed for the current bot.
     *
     * @param object|null $bot       Bot object.
     * @param string      $type      Widget type.
     * @param object|null $wc_preset Loaded Woo preset.
     * @return bool
     */
    private function should_include_widget_for_bot( $bot, string $type, $wc_preset = null ): bool {
        if ( ! class_exists( 'BoostBot_Widget_Model' ) || ! BoostBot_Widget_Model::is_woocommerce_widget_type( $type ) ) {
            return true;
        }

        return ! empty( $wc_preset ) || $this->bot_has_valid_woocommerce_preset( $bot );
    }

    /**
     * Get the allowed widget IDs for the current bot after Woo preset filtering.
     *
     * @param object|null $bot Bot object.
     * @return array
     */
    private function get_allowed_widget_ids_for_bot( $bot ): array {
        if ( ! $bot || empty( $bot->id ) || ! class_exists( 'BoostBot_Widget_Model' ) ) {
            return array();
        }

        $widget_model = new BoostBot_Widget_Model();
        $assigned     = $widget_model->get_for_chatbot( absint( $bot->id ) );
        $allowed_ids  = array();
        $has_wc_preset = $this->bot_has_valid_woocommerce_preset( $bot );

        foreach ( $assigned as $widget ) {
            $widget_type = (string) ( $widget->type ?? '' );
            if ( ! $has_wc_preset && BoostBot_Widget_Model::is_woocommerce_widget_type( $widget_type ) ) {
                continue;
            }
            $allowed_ids[] = (int) ( $widget->id ?? 0 );
        }

        return array_values( array_filter( $allowed_ids ) );
    }
    
    /**
     * Detect widget intent from user message.
     *
     * @param string $message          User message.
     * @param array  $bot_widget_types Widget types assigned to the bot.
     * @return array|null Widget config or null.
     */
    private function detect_widget_intent( string $message, array $bot_widget_types = array() ) {
        $message_lower = mb_strtolower( $message );
        
        // If bot has no widgets assigned, return null
        if ( empty( $bot_widget_types ) ) {
            return null;
        }

        // ── Use the centralized AI trigger keywords from BoostBot_Widget_Model ──
        // This ensures detect_widget_intent stays in sync with all widget types
        // and their trigger keywords automatically.
        if ( class_exists( 'BoostBot_Widget_Model' ) ) {
            foreach ( $bot_widget_types as $widget_type ) {
                $triggers = BoostBot_Widget_Model::get_widget_ai_triggers( $widget_type, $this->last_resolved_bot->language ?? 'he' );
                if ( empty( $triggers['keywords'] ) ) {
                    continue;
                }
                foreach ( $triggers['keywords'] as $keyword ) {
                    if ( mb_strpos( $message_lower, mb_strtolower( $keyword ) ) !== false ) {
                        BoostBot_Logger::info( sprintf( '[DEBUG] Widget intent detected via keyword: "%s" → %s', $keyword, $widget_type ) );

                        // Resolve the actual widget ID from the bot's assigned widgets
                        $widget_config = $this->resolve_widget_for_bot( $widget_type );
                        if ( $widget_config ) {
                            return $widget_config;
                        }

                        // Fallback: return generic config for the widget type
                        return array(
                            'type' => $widget_type,
                            'id'   => 'auto_' . $widget_type,
                            'data' => array(),
                        );
                    }
                }
            }
        }

        // ── Extended lead-form keywords not covered by the model's triggers ──
        $ps_lead = BoostBot_Prompt_Strings::get( $bot->language ?? 'he' );
        $lead_extra = $ps_lead['extended_lead_keywords'];
        if ( in_array( 'form', $bot_widget_types, true ) || in_array( 'lead_form', $bot_widget_types, true ) ) {
            foreach ( $lead_extra as $keyword ) {
                if ( mb_strpos( $message_lower, $keyword ) !== false ) {
                    BoostBot_Logger::info( sprintf( '[DEBUG] Lead form intent (extra keyword): %s', $keyword ) );
                    $widget_config = $this->resolve_widget_for_bot( 'form' );
                    return $widget_config ?: array(
                        'type' => 'lead_form',
                        'id'   => 'auto_lead_form',
                        'data' => $this->get_default_lead_form_config(),
                    );
                }
            }
        }
        
        return null;
    }

    /**
     * Resolve a widget's full config from the bot's assigned widgets.
     *
     * @param string $widget_type Widget type to look for.
     * @return array|null Widget config array or null.
     */
    private function resolve_widget_for_bot( string $widget_type ): ?array {
        if ( ! class_exists( 'BoostBot_Widget_Model' ) ) {
            return null;
        }

        try {
            $model = new BoostBot_Widget_Model();

            // We need the bot. Use the last resolved bot stored in the instance.
            $bot = $this->last_resolved_bot ?? null;
            if ( ! $bot || empty( $bot->id ) ) {
                return null;
            }

            $assigned = $model->get_for_chatbot( absint( $bot->id ) );
            if ( empty( $assigned ) || ! is_array( $assigned ) ) {
                return null;
            }

            foreach ( $assigned as $w ) {
                $wt = $w->type ?? '';
                // Match the requested type (handle form / lead_form aliasing)
                if ( $wt === $widget_type || ( $widget_type === 'lead_form' && $wt === 'form' ) || ( $widget_type === 'form' && $wt === 'lead_form' ) ) {
                    $config = $w->config ?? array();
                    if ( is_string( $config ) ) {
                        $config = json_decode( $config, true ) ?: array();
                    }
                    return array(
                        'type' => $wt,
                        'id'   => (int) ( $w->id ?? 0 ),
                        'data' => $config,
                    );
                }
            }
        } catch ( Exception $e ) {
            BoostBot_Logger::error( 'resolve_widget_for_bot error: ' . $e->getMessage() );
        }

        return null;
    }
    
    /**
     * Get default lead form configuration.
     *
     * @return array Lead form config.
     */
    private function get_default_lead_form_config(): array {
        $bot_language = ( $this->last_resolved_bot->language ?? 'he' );
        $ps = BoostBot_Prompt_Strings::get( $bot_language ?: 'he' );
        return array(
            'title'       => $ps['default_form_title'],
            'description' => $ps['default_form_success'],
            'fields'      => array(
                array( 'name' => 'name', 'label' => $ps['default_form_name_label'], 'type' => 'text', 'required' => true ),
                array( 'name' => 'email', 'label' => $ps['default_form_email_label'], 'type' => 'email', 'required' => true ),
                array( 'name' => 'phone', 'label' => $ps['default_form_phone_label'], 'type' => 'tel', 'required' => false ),
                array( 'name' => 'message', 'label' => $ps['default_form_message_label'], 'type' => 'textarea', 'required' => false ),
            ),
            'submit_text' => $ps['default_form_submit'],
        );
    }
    
    /**
     * Get widget configuration by ID.
     *
     * @param int|string $widget_id Widget ID.
     * @return array|null Widget config or null.
     */
    private function get_widget_config_by_id( $widget_id ): ?array {
        if ( empty( $widget_id ) || ! is_numeric( $widget_id ) ) {
            return null;
        }
        
        try {
            if ( ! class_exists( 'BoostBot_Widget_Model' ) ) {
                return null;
            }
            
            $model = new BoostBot_Widget_Model();
            $widget = $model->get( (int) $widget_id );
            
            if ( ! $widget ) {
                return null;
            }
            
            // Parse config JSON if string
            $config = $widget->config ?? array();
            if ( is_string( $config ) ) {
                $config = json_decode( $config, true ) ?: array();
            }
            
            return array(
                'type' => $widget->type ?? 'unknown',
                'id'   => $widget->id,
                'data' => array_merge( $config, array(
                    'name' => $widget->name ?? '',
                ) ),
            );
        } catch ( Exception $e ) {
            BoostBot_Logger::error( 'Error getting widget config: ' . $e->getMessage() );
            return null;
        }
    }
    
    /**
     * Get argument schema for retrieve endpoint.
     *
     * @since 1.15.0
     * @return array Argument schema.
     */
    private function get_retrieve_args() {
        return array(
            'message' => array(
                'required'          => true,
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_textarea_field',
            ),
            'conversation_id' => array(
                'required'          => false,
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_text_field',
            ),
            'visitor_id' => array(
                'required'          => false,
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_text_field',
            ),
            'context' => array(
                'required' => false,
                'type'     => 'object',
            ),
            'history' => array(
                'required' => false,
                'type'     => 'array',
            ),
            'ai_instructions' => array(
                'required'          => false,
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_textarea_field',
            ),
        );
    }

    /**
     * Build comprehensive bot configuration for Gemini orchestrator.
     * 
     * Includes behavior settings, widget assignments, and brand info that
     * should affect how Gemini orchestrates the conversation.
     *
     * @param object $bot              The bot object.
     * @param int    $output_limit     Effective output limit.
     * @param array  $widgets_enabled  List of enabled widget types.
     * @return array Bot config for orchestrator.
     */
    private function build_orchestrator_bot_config( $bot, int $output_limit, array $widgets_enabled, string $query = '' ): array {
        $widgets_enabled = $this->filter_widget_types_for_bot( $bot, $widgets_enabled );

        $config = array(
            // Basic info
            'name'              => $bot->bot_name ?? 'BoostBot',
            'output_limit'      => $output_limit,
            'language'          => $bot->language ?? BoostBot_Language::get_primary(),
            
            // Enabled widgets (what Gemini can recommend)
            'enabled_widgets'   => $widgets_enabled,
        );

        $bot_language = $bot->language ?? BoostBot_Language::get_primary();

        // Load WooCommerce preset once (used by both product catalog and coupon info).
        $wc_preset = null;
        if ( class_exists( 'WooCommerce' ) && ! empty( $bot->woocommerce_preset_id ) && class_exists( 'BoostBot_WooCommerce_Preset_Model' ) ) {
            try {
                $wc_preset_model = new BoostBot_WooCommerce_Preset_Model();
                $wc_preset       = $wc_preset_model->get( $bot->woocommerce_preset_id );
            } catch ( \Throwable $e ) {
                BoostBot_Logger::error( 'Failed to load WooCommerce preset: ' . $e->getMessage() );
            }
        }

        // === PRODUCT CATALOG (WooCommerce) ===
        if ( class_exists( 'WooCommerce' ) && $wc_preset && in_array( 'product_card', $widgets_enabled, true ) ) {
            $config['product_catalog'] = array();
            try {
                $product_args = array(
                    'limit'   => 20,
                    'status'  => 'publish',
                    'orderby' => 'popularity',
                    'order'   => 'DESC',
                );

                $has_language_filter = false;
                if ( $bot_language && 'all' !== $bot_language ) {
                    $product_args['lang'] = $bot_language;
                    $has_language_filter  = true;
                }

                if ( $wc_preset ) {
                    $product_mode = $wc_preset->product_mode ?? 'all';

                    if ( 'categories' === $product_mode ) {
                        $allowed = is_string( $wc_preset->allowed_categories ?? null )
                            ? json_decode( $wc_preset->allowed_categories, true )
                            : ( $wc_preset->allowed_categories ?? array() );
                        if ( ! empty( $allowed ) && is_array( $allowed ) ) {
                            if ( ! isset( $product_args['tax_query'] ) ) {
                                $product_args['tax_query'] = array();
                            }
                            $product_args['tax_query'][] = array(
                                'taxonomy' => 'product_cat',
                                'field'    => 'term_id',
                                'terms'    => array_map( 'absint', $allowed ),
                                'operator' => 'IN',
                            );
                        }
                    } elseif ( 'specific' === $product_mode ) {
                        $specific = is_string( $wc_preset->specific_product_ids ?? null )
                            ? json_decode( $wc_preset->specific_product_ids, true )
                            : ( $wc_preset->specific_product_ids ?? array() );
                        if ( ! empty( $specific ) && is_array( $specific ) ) {
                            $product_args['include'] = array_map( 'absint', $specific );
                        }
                    }

                    $excluded_ids = is_string( $wc_preset->excluded_product_ids ?? null )
                        ? json_decode( $wc_preset->excluded_product_ids, true )
                        : ( $wc_preset->excluded_product_ids ?? array() );
                    if ( ! empty( $excluded_ids ) && is_array( $excluded_ids ) ) {
                        $product_args['exclude'] = array_map( 'absint', $excluded_ids );
                    }

                    $excluded_cats = is_string( $wc_preset->excluded_categories ?? null )
                        ? json_decode( $wc_preset->excluded_categories, true )
                        : ( $wc_preset->excluded_categories ?? array() );
                    if ( ! empty( $excluded_cats ) && is_array( $excluded_cats ) ) {
                        if ( ! isset( $product_args['tax_query'] ) ) {
                            $product_args['tax_query'] = array();
                        }
                        $product_args['tax_query'][] = array(
                            'taxonomy' => 'product_cat',
                            'field'    => is_numeric( $excluded_cats[0] ?? '' ) ? 'term_id' : 'slug',
                            'terms'    => $excluded_cats,
                            'operator' => 'NOT IN',
                        );
                        if ( count( $product_args['tax_query'] ) > 1 ) {
                            $product_args['tax_query']['relation'] = 'AND';
                        }
                    }

                    $min_stock = (int) ( $wc_preset->min_stock ?? 0 );
                    if ( $min_stock > 0 ) {
                        $product_args['stock_status'] = 'instock';
                    }

                    $min_rating = (float) ( $wc_preset->min_rating ?? 0 );
                    if ( $min_rating > 0 ) {
                        $product_args['meta_query'] = array(
                            array(
                                'key'     => '_wc_average_rating',
                                'value'   => $min_rating,
                                'compare' => '>=',
                                'type'    => 'DECIMAL',
                            ),
                        );
                    }
                }

                if ( ! empty( $query ) ) {
                    $published_products = wp_count_posts( 'product' );
                    $published_count    = isset( $published_products->publish ) ? (int) $published_products->publish : 0;
                    $product_args['limit'] = ( $published_count > 0 && $published_count <= 250 ) ? -1 : 100;
                }

                $products = wc_get_products( $product_args );
                $initial_product_count = count( $products );

                if ( empty( $products ) && $has_language_filter ) {
                    // Some WooCommerce + Polylang combinations do not honor `lang`
                    // on wc_get_products() reliably. Retry without the lang filter
                    // and resolve translations explicitly below.
                    unset( $product_args['lang'] );
                    $products = wc_get_products( $product_args );
                }

                if ( empty( $products ) && $bot_language && class_exists( 'BoostBot_Polylang_Context' ) ) {
                    // Last-resort compatibility fallback: some tenants only return
                    // language-scoped products when Polylang's current language is
                    // switched globally. Keep this scoped to the product catalog
                    // query only, so coupons/currency stay on the safer explicit path.
                    $product_context = new BoostBot_Polylang_Context();
                    $did_switch      = $product_context->switch_to( $bot_language );
                    try {
                        $products = wc_get_products( $product_args );
                    } finally {
                        if ( $did_switch ) {
                            $product_context->restore();
                        }
                    }
                }

                // Keep a translation fallback for installs where WooCommerce queries
                // still return mixed-language products despite the explicit `lang` arg.
                $catalog_lang = $bot_language;
                if ( ! empty( $products ) && function_exists( 'pll_get_post_language' ) && function_exists( 'pll_get_post' ) ) {
                    $resolved  = array();
                    $seen_ids  = array();
                    foreach ( $products as $product ) {
                        $pid  = $product->get_id();
                        $lang = pll_get_post_language( $pid );

                        if ( $lang && $lang !== $catalog_lang ) {
                            $translated_id = pll_get_post( $pid, $catalog_lang );
                            if ( $translated_id && $translated_id !== $pid ) {
                                $translated = wc_get_product( $translated_id );
                                if ( $translated ) {
                                    $product = $translated;
                                    $pid     = $translated_id;
                                }
                            }
                        }

                        if ( in_array( $pid, $seen_ids, true ) ) {
                            continue;
                        }
                        $seen_ids[]  = $pid;
                        $resolved[]  = $product;
                    }
                    $products = $resolved;
                }

                $resolved_product_count = count( $products );

                if ( ! empty( $query ) && ! empty( $products ) ) {
                    usort(
                        $products,
                        function( $a, $b ) use ( $query ) {
                            $score_a = $this->score_product_catalog_match( $a, $query );
                            $score_b = $this->score_product_catalog_match( $b, $query );

                            if ( $score_a === $score_b ) {
                                return 0;
                            }

                            return ( $score_a > $score_b ) ? -1 : 1;
                        }
                    );
                    $products = array_slice( $products, 0, 20 );
                }

                BoostBot_Logger::debug(
                    '[Retrieval] Product catalog build',
                    array(
                        'bot_id'                 => (int) ( $bot->id ?? 0 ),
                        'language'               => $bot_language,
                        'query'                  => $query,
                        'preset_id'              => $wc_preset ? (int) $wc_preset->id : 0,
                        'has_language_filter'    => $has_language_filter,
                        'initial_product_count'  => $initial_product_count,
                        'resolved_product_count' => $resolved_product_count,
                        'final_product_count'    => count( $products ),
                    )
                );

                if ( ! empty( $products ) ) {
                    $config['product_catalog'] = array_map( function( $p ) {
                        $max_quantity = (int) $p->get_max_purchase_quantity();
                        return array(
                            'id'         => $p->get_id(),
                            'name'       => $p->get_name(),
                            'price'      => $p->get_price(),
                            'type'       => $p->get_type(),
                            'short_desc' => wp_strip_all_tags( mb_substr( (string) $p->get_short_description(), 0, 100 ) ),
                            'stock_status' => $p->get_stock_status(),
                            'stock_quantity' => $p->get_stock_quantity(),
                            'in_stock'   => $p->is_in_stock(),
                            'on_sale'    => $p->is_on_sale(),
                            'is_purchasable' => $p->is_purchasable(),
                            'max_quantity' => $max_quantity > 0 ? $max_quantity : null,
                        );
                    }, $products );
                    BoostBot_Logger::debug( sprintf(
                        '[Orchestrated] Included %d products in catalog for bot %d (preset: %s)',
                        count( $config['product_catalog'] ),
                        $bot->id ?? 0,
                        $wc_preset ? $wc_preset->id : 'none'
                    ) );
                } else {
                    BoostBot_Logger::warning(
                        '[Retrieval] Product catalog empty',
                        array(
                            'bot_id'      => (int) ( $bot->id ?? 0 ),
                            'language'    => $bot_language,
                            'query'       => $query,
                            'preset_id'   => $wc_preset ? (int) $wc_preset->id : 0,
                            'product_args' => $product_args,
                        )
                    );
                }
            } catch ( \Throwable $e ) {
                $config['product_catalog'] = array();
                BoostBot_Logger::error(
                    '[Retrieval] Product catalog failed: ' . $e->getMessage(),
                    array(
                        'bot_id'    => (int) ( $bot->id ?? 0 ),
                        'language'  => $bot_language,
                        'query'     => $query,
                        'preset_id' => $wc_preset ? (int) $wc_preset->id : 0,
                    )
                );
            }
        }

        // === COUPON INFO (WooCommerce) ===
        if ( class_exists( 'WooCommerce' ) && $wc_preset && in_array( 'coupon', $widgets_enabled, true ) ) {
            try {
                $wc_service        = new BoostBot_WooCommerce_Service();
                $preset_coupon_map = array();
                $coupon_query_args = array(
                    'post_type'      => 'shop_coupon',
                    'posts_per_page' => 20,
                    'post_status'    => 'publish',
                    'lang'           => $bot_language,
                );

                // If the preset has linked coupons, restrict to those WC coupon IDs.
                if ( $wc_preset && class_exists( 'BoostBot_WooCommerce_Coupon_Model' ) ) {
                    $coupon_model   = new BoostBot_WooCommerce_Coupon_Model();
                    $preset_coupons = $coupon_model->get_by_preset_and_purpose(
                        (int) $wc_preset->id,
                        BoostBot_WooCommerce_Coupon_Model::PURPOSE_WIDGET,
                        $bot_language
                    );

                    if ( ! empty( $preset_coupons ) ) {
                        foreach ( $preset_coupons as $preset_coupon ) {
                            if ( ! empty( $preset_coupon->wc_coupon_id ) ) {
                                $preset_coupon_map[ (int) $preset_coupon->wc_coupon_id ] = $preset_coupon;
                            }
                        }

                        $wc_coupon_ids = array_filter( array_map( function( $c ) {
                            return ! empty( $c->wc_coupon_id ) ? (int) $c->wc_coupon_id : 0;
                        }, $preset_coupons ) );

                        if ( ! empty( $wc_coupon_ids ) ) {
                            $coupon_query_args['post__in'] = $wc_coupon_ids;
                        }
                    }
                }

                $coupon_posts = get_posts( $coupon_query_args );
                if ( empty( $coupon_posts ) && isset( $coupon_query_args['lang'] ) ) {
                    // Some Woo setups don't attach Polylang language to coupons.
                    // Retry without lang filter so preset-linked coupons are still discoverable.
                    BoostBot_Logger::info(
                        '[Retrieval] Coupon fallback: no coupons for language-filtered query, retrying without lang filter',
                        array(
                            'bot_id'    => (int) ( $bot->id ?? 0 ),
                            'language'  => $bot_language,
                            'preset_id' => $wc_preset ? (int) $wc_preset->id : 0,
                        )
                    );
                    unset( $coupon_query_args['lang'] );
                    $coupon_posts = get_posts( $coupon_query_args );
                }

                $active_count = 0;
                $discount_types = array();
                $coupon_summaries = array();
                foreach ( $coupon_posts as $cp ) {
                    $wc_coupon = new \WC_Coupon( $cp->ID );
                    $expiry    = $wc_coupon->get_date_expires();
                    if ( $expiry && $expiry->getTimestamp() < time() ) {
                        continue;
                    }

                    $preset_coupon = $preset_coupon_map[ $cp->ID ] ?? null;
                    $coupon_data   = $wc_service->get_coupon_applicability_data( $wc_coupon, $bot_language );

                    $active_count++;
                    $dtype = $wc_coupon->get_discount_type();
                    if ( ! in_array( $dtype, $discount_types, true ) ) {
                        $discount_types[] = $dtype;
                    }

                    $coupon_summaries[] = array(
                        'code'              => $wc_coupon->get_code(),
                        'display_name'      => ! empty( $preset_coupon->display_name ) ? $preset_coupon->display_name : $wc_coupon->get_code(),
                        'discount_type'     => $dtype,
                        'amount'            => (float) $wc_coupon->get_amount(),
                        'applicable'        => (bool) ( $coupon_data['applicable'] ?? false ),
                        'reason'            => $coupon_data['reason'] ?? null,
                        'reason_code'       => $coupon_data['reason_code'] ?? null,
                        'spend_gap'         => $coupon_data['spend_gap'] ?? null,
                        'spend_gap_display' => $coupon_data['spend_gap_display'] ?? null,
                        'min_spend'         => $coupon_data['min_spend'] ?? null,
                        'min_spend_display' => $coupon_data['min_spend_display'] ?? null,
                        'is_applied'        => (bool) ( $coupon_data['is_applied'] ?? false ),
                        'free_shipping'     => (bool) ( $coupon_data['free_shipping'] ?? false ),
                        'expires_soon'      => (bool) ( $coupon_data['expires_soon'] ?? false ),
                    );
                }

                if ( $active_count > 0 ) {
                    $applicable_coupons = array_values(
                        array_filter(
                            $coupon_summaries,
                            static function ( $coupon ) {
                                return ! empty( $coupon['applicable'] ) && empty( $coupon['is_applied'] );
                            }
                        )
                    );
                    $applied_coupon_codes = array_values(
                        array_map(
                            static function ( $coupon ) {
                                return (string) $coupon['code'];
                            },
                            array_filter(
                                $coupon_summaries,
                                static function ( $coupon ) {
                                    return ! empty( $coupon['is_applied'] );
                                }
                            )
                        )
                    );

                    $config['coupon_info'] = array(
                        'has_active_coupons' => true,
                        'coupon_count'       => $active_count,
                        'discount_types'     => $discount_types,
                        'applicable_coupon_count' => count( $applicable_coupons ),
                        'applied_coupon_codes'    => $applied_coupon_codes,
                        'coupons'                 => array_slice( $coupon_summaries, 0, 5 ),
                        'prompt_summary'          => $this->build_coupon_prompt_summary(
                            $coupon_summaries,
                            $active_count,
                            $discount_types,
                            $bot_language
                        ),
                    );
                    BoostBot_Logger::debug( sprintf(
                        '[Orchestrated] Included coupon info: %d active coupons for bot %d (preset: %s)',
                        $active_count,
                        $bot->id ?? 0,
                        $wc_preset ? $wc_preset->id : 'none'
                    ) );
                }
            } catch ( \Throwable $e ) {
                BoostBot_Logger::error( 'Failed to fetch coupon info: ' . $e->getMessage() );
            }
        }

        // === BUNDLE BUILDER CONTEXT (WooCommerce) ===
        if ( class_exists( 'WooCommerce' ) && $wc_preset && in_array( 'bundle_builder', $widgets_enabled, true ) ) {
            try {
                $budget_ranges = is_string( $wc_preset->budget_ranges ?? null )
                    ? json_decode( $wc_preset->budget_ranges, true )
                    : ( $wc_preset->budget_ranges ?? array() );

                if ( ! is_array( $budget_ranges ) ) {
                    $budget_ranges = array();
                }

                $budget_ranges = array_values(
                    array_filter(
                        array_map(
                            static function ( $range ) {
                                if ( ! is_array( $range ) ) {
                                    return null;
                                }

                                $label = '';
                                if ( ! empty( $range['label'] ) ) {
                                    $label = (string) $range['label'];
                                } elseif ( ! empty( $range['label_he'] ) || ! empty( $range['label_en'] ) ) {
                                    $label = (string) ( $range['label_he'] ?? $range['label_en'] ?? '' );
                                }

                                return array(
                                    'label' => $label,
                                    'min'   => isset( $range['min'] ) ? (float) $range['min'] : 0,
                                    'max'   => isset( $range['max'] ) && null !== $range['max'] ? (float) $range['max'] : null,
                                );
                            },
                            $budget_ranges
                        ),
                        static function ( $range ) {
                            return is_array( $range ) && '' !== ( $range['label'] ?? '' );
                        }
                    )
                );

                $bundle_categories = array();
                if ( class_exists( 'BoostBot_Bundle_Builder_Service' ) && class_exists( 'BoostBot_Bundle_Product_Selector' ) ) {
                    $bundle_service  = new BoostBot_Bundle_Builder_Service();
                    $preset_settings = $bundle_service->get_settings_from_preset_id( (int) $wc_preset->id );

                    if ( is_array( $preset_settings ) && ! empty( $preset_settings ) ) {
                        $product_selector   = new BoostBot_Bundle_Product_Selector( $preset_settings );
                        $available_categories = $product_selector->get_available_categories( $bot_language );

                        if ( is_array( $available_categories ) ) {
                            $bundle_categories = array_values(
                                array_map(
                                    static function ( $category ) {
                                        return array(
                                            'name'  => (string) ( $category['name'] ?? '' ),
                                            'slug'  => (string) ( $category['slug'] ?? '' ),
                                            'count' => isset( $category['count'] ) ? (int) $category['count'] : 0,
                                        );
                                    },
                                    $available_categories
                                )
                            );
                        }

                        $config['bundle_builder_context'] = array(
                            'budget_ranges' => $budget_ranges,
                            'categories'    => $bundle_categories,
                            'min_products'  => isset( $preset_settings['min_products_in_bundle'] ) ? (int) $preset_settings['min_products_in_bundle'] : null,
                            'max_products'  => isset( $preset_settings['max_products_in_bundle'] ) ? (int) $preset_settings['max_products_in_bundle'] : null,
                        );
                    }
                }
            } catch ( \Throwable $e ) {
                BoostBot_Logger::error( 'Failed to build bundle builder context: ' . $e->getMessage() );
            }
        }

        // === CART CONTEXT (enriched for AI) ===
        if ( class_exists( 'WooCommerce' ) && WC()->cart ) {
            try {
                $wc_service     = new BoostBot_WooCommerce_Service();
                $cart_snapshot  = $wc_service->get_cart_info( $bot->language ?? 'he' );

                if ( ! empty( $cart_snapshot ) && ! $cart_snapshot['is_empty'] ) {
                    $cart_items_for_ai = array();
                    foreach ( $cart_snapshot['items'] as $item ) {
                        $cart_items_for_ai[] = array(
                            'product_id'    => $item['id'],
                            'name'          => $item['name'],
                            'quantity'      => $item['quantity'],
                            'price_display' => $item['price_display'],
                            'categories'    => $item['categories'] ?? array(),
                        );
                    }

                    $config['cart'] = array(
                        'item_count'      => $cart_snapshot['count'],
                        'total_display'   => $cart_snapshot['total'],
                        'currency_symbol' => $cart_snapshot['currency_symbol'],
                        'applied_coupons' => array_map( function( $c ) {
                            return $c['code'];
                        }, $cart_snapshot['applied_coupons'] ),
                        'items'           => $cart_items_for_ai,
                    );

                    BoostBot_Logger::debug( '[Retrieval] Cart context added to bot_config', array(
                        'item_count' => $cart_snapshot['count'],
                        'items'      => count( $cart_items_for_ai ),
                    ) );
                }
            } catch ( \Throwable $e ) {
                BoostBot_Logger::warning( '[Retrieval] Cart context failed: ' . $e->getMessage() );
            }
        }

        // === CURRENCY INFO (language-aware) ===
        // Direct Polylang term-meta lookup — works on all Polylang versions
        // and in REST API contexts where pll_set_current_language() is unreliable.
        if ( class_exists( 'WooCommerce' ) ) {
            $currency = BoostBot_Polylang_Context::get_currency_for_language( $bot_language );
            $config['currency']        = $currency['code'];
            $config['currency_symbol'] = $currency['symbol'];
        }

        // === PERSONALITY INHERITANCE ===
        // When a bot is linked to a personality preset, the admin UI copies the
        // personality fields into the bot record at save time. However, the user
        // may later edit the personality — the bot record then holds stale values.
        //
        // The `personality_overrides` JSON tracks which fields the user has
        // explicitly detached ("overridden") from the preset. For any field that
        // is NOT overridden we MUST use the live personality value, regardless of
        // what the bot record contains.
        $personality_id = $bot->personality_id ?? 0;
        if ( $personality_id > 0 && class_exists( 'BoostBot_Personality_Model' ) ) {
            $personality_model = new BoostBot_Personality_Model();
            $personality = $personality_model->get( $personality_id );
            if ( $personality ) {
                $overrides = array();
                if ( ! empty( $bot->personality_overrides ) ) {
                    $overrides = is_string( $bot->personality_overrides )
                        ? json_decode( $bot->personality_overrides, true )
                        : (array) $bot->personality_overrides;
                    if ( ! is_array( $overrides ) ) {
                        $overrides = array();
                    }
                }

                if ( empty( $overrides['system_prompt'] ) && ! empty( $personality->system_prompt ) ) {
                    $bot->system_prompt = $personality->system_prompt;
                }
                if ( empty( $overrides['tone'] ) && ! empty( $personality->tone ) ) {
                    $bot->tone = $personality->tone;
                }
                if ( empty( $overrides['proactive_engagement'] ) ) {
                    $bot->proactive_engagement = $personality->proactive_engagement;
                }
                if ( empty( $overrides['proactive_delay_seconds'] ) && isset( $personality->proactive_delay_seconds ) ) {
                    $bot->proactive_delay_seconds = $personality->proactive_delay_seconds;
                }
                if ( empty( $overrides['custom_instructions'] ) && ! empty( $personality->custom_instructions ) ) {
                    $bot->custom_instructions = $personality->custom_instructions;
                }
            }
        }

        // === BEHAVIOR SETTINGS (התנהגות tab) ===
        
        // Personality/System prompt - core instructions
        $personality_parts = array();
        
        if ( ! empty( $bot->system_prompt ) ) {
            $personality_parts[] = $bot->system_prompt;
        }
        
        if ( ! empty( $bot->custom_instructions ) ) {
            $personality_parts[] = "Additional instructions: " . $bot->custom_instructions;
        }
        
        // Global rules — per-language so each language gets its own set of rules.
        $bot_language = $bot->language ?? '';
        if ( class_exists( 'BoostBot_Settings' ) && ! empty( $bot_language ) ) {
            $global_rules = BoostBot_Settings::get_for_language( 'global_rules', $bot_language, '' );
        } else {
            $global_rules = class_exists( 'BoostBot_Settings' ) ? BoostBot_Settings::get( 'global_rules', '' ) : '';
        }
        if ( ! empty( $global_rules ) ) {
            $personality_parts[] = "Global rules (MUST FOLLOW for all bots):\n" . $global_rules;
        }
        if ( ! empty( $bot->rules ) ) {
            $personality_parts[] = "Bot-specific rules:\n" . $bot->rules;
        }

        $config['personality'] = implode( "\n\n", $personality_parts );
        
        // Forbidden topics — per-language, merge with per-bot (both must be respected).
        $forbidden_parts = array();
        if ( class_exists( 'BoostBot_Settings' ) && ! empty( $bot_language ) ) {
            $global_forbidden = BoostBot_Settings::get_for_language( 'global_forbidden_topics', $bot_language, '' );
        } else {
            $global_forbidden = class_exists( 'BoostBot_Settings' ) ? BoostBot_Settings::get( 'global_forbidden_topics', '' ) : '';
        }
        if ( ! empty( $global_forbidden ) ) {
            $forbidden_parts[] = $global_forbidden;
        }
        if ( ! empty( $bot->forbidden_topics ) ) {
            $forbidden_parts[] = $bot->forbidden_topics;
        }
        if ( ! empty( $forbidden_parts ) ) {
            $config['forbidden_topics'] = implode( "\n", $forbidden_parts );
        }
        
        // Tone setting
        if ( ! empty( $bot->tone ) ) {
            $config['tone'] = $bot->tone;
        }
        
        // Behavior flags
        $config['behavior'] = array(
            'proactive_engagement'   => (bool) ( $bot->proactive_engagement ?? false ),
            'proactive_delay_seconds'=> (int) ( $bot->proactive_delay_seconds ?? 10 ), // Default 10 seconds
        );

        // === WIDGET ASSIGNMENTS (וידג'טים tab) ===
        // Get actual widget assignments from junction table using BoostBot_Widget_Model
        $bot_id = $bot->id ?? 0;
        
        if ( $bot_id > 0 && class_exists( 'BoostBot_Widget_Model' ) ) {
            try {
                $widget_model = new BoostBot_Widget_Model();
                $assigned_widgets = $widget_model->get_for_chatbot( $bot_id );
                
                if ( ! empty( $assigned_widgets ) ) {
                    $widget_details = array();
                    $enabled_types = array();
                    $bot_language = $bot->language ?? BoostBot_Language::get_primary();
                    
                    foreach ( $assigned_widgets as $widget ) {
                        $widget_type = $widget->type ?? 'unknown';
                        if ( ! $this->should_include_widget_for_bot( $bot, $widget_type, $wc_preset ) ) {
                            continue;
                        }
                        $widget_name = $widget->name ?? '';
                        $widget_description = $widget->description ?? ''; // "תיאור לבוט (מתי להשתמש)"
                        
                        // Fall back to default description when custom one is empty
                        if ( empty( $widget_description ) ) {
                            $widget_description = BoostBot_Widget_Model::get_default_bot_description( $widget_type );
                        }
                        
                        // Get AI trigger keywords for this widget type
                        $ai_triggers = BoostBot_Widget_Model::get_widget_ai_triggers( $widget_type, $bot_language );
                        
                        $widget_detail = array(
                            'type'        => $widget_type,
                            'name'        => $widget_name,
                            'id'          => (int) ( $widget->id ?? 0 ),
                            'description' => $widget_description, // Custom or default description
                            'core_description' => BoostBot_Widget_Model::get_widget_core_description( $widget_type, $bot_language ),
                            'keywords'    => $ai_triggers['keywords'], // Trigger keywords
                            'instruction' => $ai_triggers['instruction'], // When to show
                        );

                        // For form widgets, check if the form has a phone field
                        // so Gemini can decide to attach it when a phone number is detected.
                        if ( 'form' === $widget_type && class_exists( 'BoostBot_Form_Field_Model' ) ) {
                            $field_model = new BoostBot_Form_Field_Model();
                            $fields = $field_model->get_by_widget( (int) ( $widget->id ?? 0 ) );
                            $has_phone = false;
                            if ( ! empty( $fields ) && is_array( $fields ) ) {
                                foreach ( $fields as $field ) {
                                    if ( 'phone' === ( $field->field_type ?? '' ) || 'tel' === ( $field->field_type ?? '' ) ) {
                                        $has_phone = true;
                                        break;
                                    }
                                }
                            }
                            $widget_detail['has_phone_field'] = $has_phone;
                        }

                        $widget_details[] = $widget_detail;
                        $enabled_types[]  = $widget_type;
                    }
                    
                    // Deduplicate: keep one assignment per widget type (prefer managed/first seen)
                    $seen_types = array();
                    $deduped_details = array();
                    $deduped_types   = array();
                    foreach ( $widget_details as $detail ) {
                        $wtype = $detail['type'] ?? '';
                        if ( ! isset( $seen_types[ $wtype ] ) ) {
                            $seen_types[ $wtype ] = true;
                            $deduped_details[]    = $detail;
                            $deduped_types[]      = $wtype;
                        }
                    }
                    $widget_details = $deduped_details;
                    $enabled_types  = $deduped_types;

                    $config['widget_assignments'] = $widget_details;
                    $config['enabled_widgets']    = $enabled_types;
                    
                    BoostBot_Logger::info( sprintf( 
                        'Bot %d has %d fully-configured assigned widgets: %s', 
                        $bot_id, 
                        count( $widget_details ),
                        implode( ', ', array_map( function( $w ) { return $w['type'] . '(' . $w['id'] . ')'; }, $widget_details ) )
                    ) );
                }
            } catch ( Exception $e ) {
                BoostBot_Logger::error( 'Failed to get widget assignments: ' . $e->getMessage() );
            }
        }

        // === BRAND CARD (מתקדם tab) ===
        $brand_card = array();
        
        // Check if bot has brand card override
        $brand_mode = $bot->brand_card_mode ?? 'global';
        
        if ( 'override' === $brand_mode && ! empty( $bot->brand_card_override ) ) {
            $override = is_string( $bot->brand_card_override ) 
                ? json_decode( $bot->brand_card_override, true ) 
                : $bot->brand_card_override;
            
            if ( is_array( $override ) ) {
                $brand_card = $override;
            }
        } else {
            // Use global brand card (per-language)
            $bot_lang    = $bot->language ?? '';
            $global_json = (string) BoostBot_Settings::get_for_language( 'brand_card_global_json', $bot_lang, '' );
            if ( '' !== trim( $global_json ) ) {
                $decoded = json_decode( $global_json, true );
                if ( is_array( $decoded ) ) {
                    $brand_card = $decoded;
                }
            }
        }
        
        // Include the full brand card for Gemini (company context including custom tables).
        // No caps — Gemini input tokens are our cheapest resource.
        if ( ! empty( $brand_card ) ) {
            $config['brand'] = $brand_card;
        }

        // contact_list_id removed — contacts now sourced from Contact Card widgets.

        return $config;
    }

    /**
     * Score how well a product matches the current shopper query.
     *
     * @param WC_Product $product Product to score.
     * @param string     $query   Current user message.
     * @return int
     */
    private function score_product_catalog_match( $product, string $query ): int {
        $query = strtolower( trim( $query ) );
        if ( '' === $query ) {
            return 0;
        }

        $tokens = $this->normalize_catalog_tokens( $query );
        if ( empty( $tokens ) ) {
            return 0;
        }

        $name_tokens = $this->normalize_catalog_tokens( $product->get_name() );
        $text_tokens = $this->normalize_catalog_tokens(
            implode(
                ' ',
                array_filter(
                    array(
                        $product->get_name(),
                        wp_strip_all_tags( (string) $product->get_short_description() ),
                        wp_strip_all_tags( (string) $product->get_description() ),
                    )
                )
            )
        );

        $name_lookup = array_fill_keys( $name_tokens, true );
        $text_lookup = array_fill_keys( $text_tokens, true );

        $score = 0;
        foreach ( $tokens as $index => $token ) {
            $weight = $index + 1;

            if ( isset( $text_lookup[ $token ] ) ) {
                $score += 2 + $weight;
            }

            if ( isset( $name_lookup[ $token ] ) ) {
                $score += 3 + $weight;
            }
        }

        return $score;
    }

    /**
     * Normalize free text into comparable catalog tokens.
     *
     * @param string $text Source text.
     * @return array
     */
    private function normalize_catalog_tokens( string $text ): array {
        $tokens = preg_split( '/[^a-z0-9]+/', strtolower( $text ) );
        if ( ! is_array( $tokens ) ) {
            return array();
        }

        $tokens = array_map(
            static function( $token ) {
                $token = trim( (string) $token );
                if ( strlen( $token ) > 3 && substr( $token, -1 ) === 's' ) {
                    $token = substr( $token, 0, -1 );
                }

                return strlen( $token ) >= 3 ? $token : '';
            },
            $tokens
        );

        return array_values( array_unique( array_filter( $tokens ) ) );
    }

    /**
     * Build a concise cart-aware coupon summary for the orchestrator prompt.
     *
     * @param array  $coupon_summaries Coupon summaries.
     * @param int    $coupon_count     Active coupon count.
     * @param array  $discount_types   Active discount types.
     * @param string $language         Bot language.
     * @return string
     */
    private function build_coupon_prompt_summary( array $coupon_summaries, int $coupon_count, array $discount_types, string $language ): string {
        $is_english   = 'en' === $this->normalize_prompt_language( $language );
        $type_summary = ! empty( $discount_types ) ? implode( ', ', $discount_types ) : ( $is_english ? 'various' : 'מגוונים' );
        $lines        = array(
            $is_english
                ? sprintf( '### COUPON INFO' . "\n" . 'This store has %d active coupon(s) (types: %s).', $coupon_count, $type_summary )
                : sprintf( "### מידע על קופונים\nלחנות הזו יש %d קופונים פעילים (סוגים: %s).", $coupon_count, $type_summary ),
        );

        $applied_codes = array_values(
            array_map(
                static function ( $coupon ) {
                    return (string) $coupon['code'];
                },
                array_filter(
                    $coupon_summaries,
                    static function ( $coupon ) {
                        return ! empty( $coupon['is_applied'] );
                    }
                )
            )
        );

        if ( ! empty( $applied_codes ) ) {
            $lines[] = $is_english
                ? 'Applied in cart: ' . implode( ', ', $applied_codes ) . '.'
                : 'כבר מופעלים בעגלה: ' . implode( ', ', $applied_codes ) . '.';
        }

        $priority_coupons = $coupon_summaries;
        usort(
            $priority_coupons,
            static function ( $left, $right ) {
                $left_rank  = ! empty( $left['is_applied'] ) ? 0 : ( ! empty( $left['applicable'] ) ? 1 : 2 );
                $right_rank = ! empty( $right['is_applied'] ) ? 0 : ( ! empty( $right['applicable'] ) ? 1 : 2 );

                if ( $left_rank !== $right_rank ) {
                    return $left_rank - $right_rank;
                }

                $left_gap  = isset( $left['spend_gap'] ) ? (float) $left['spend_gap'] : 0;
                $right_gap = isset( $right['spend_gap'] ) ? (float) $right['spend_gap'] : 0;

                return $left_gap <=> $right_gap;
            }
        );

        $priority_lines = array();
        foreach ( array_slice( $priority_coupons, 0, 3 ) as $coupon ) {
            $priority_lines[] = '- ' . $coupon['code'] . ': ' . $this->describe_coupon_prompt_status( $coupon, $is_english );
        }

        if ( ! empty( $priority_lines ) ) {
            $lines[] = $is_english ? 'Cart-fit coupon signals:' : 'אותות קופון לפי העגלה:';
            $lines[] = implode( "\n", $priority_lines );
        }

        $lines[] = $is_english
            ? 'When the user asks about discounts or ways to save, prefer the coupon widget and focus on cart-fit coupons instead of giving generic promo advice.'
            : 'כשהמשתמש שואל על הנחות או איך לחסוך, העדף את וידג׳ט הקופונים והתמקד בקופונים שמתאימים לעגלה במקום תשובה כללית.';

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

    /**
     * Describe a coupon's cart-fit status for the orchestrator prompt.
     *
     * @param array $coupon     Coupon summary.
     * @param bool  $is_english Whether the prompt language is English.
     * @return string
     */
    private function describe_coupon_prompt_status( array $coupon, bool $is_english ): string {
        if ( ! empty( $coupon['is_applied'] ) ) {
            return $is_english ? 'already applied' : 'כבר מופעל';
        }

        if ( ! empty( $coupon['applicable'] ) ) {
            return $is_english ? 'ready to apply now' : 'מוכן להפעלה עכשיו';
        }

        if ( ! empty( $coupon['spend_gap_display'] ) ) {
            return $is_english
                ? 'needs ' . $coupon['spend_gap_display'] . ' more spend'
                : 'דורש עוד ' . $coupon['spend_gap_display'] . ' כדי להיות זמין';
        }

        if ( ! empty( $coupon['reason'] ) ) {
            return (string) $coupon['reason'];
        }

        return $is_english ? 'not currently eligible' : 'לא זמין כרגע';
    }

    /**
     * Normalize prompt language to a short slug.
     *
     * @param string $language Raw language code.
     * @return string
     */
    private function normalize_prompt_language( string $language ): string {
        $parts = preg_split( '/[-_]/', strtolower( trim( $language ) ) );
        return is_array( $parts ) && ! empty( $parts[0] ) ? $parts[0] : 'he';
    }
}
