<?php

namespace AIPT\Api\Chatbot;

use AIPT\Api\StoreIndex\IndexConsumer;
use AIPT\Api\Services\AIClientFactoryInterface;
use AIPT\Api\Services\RestAPIClientFactory;
use AIPT\Api\Chatbot\Exceptions\ExceptionFactory;
use AIPT\Api\Chatbot\Exceptions\ChatbotException;
use AIPT\Api\Chatbot\Exceptions\RateLimitException;
use AIPT\Api\Chatbot\Exceptions\AuthenticationException;
use AIPT\Api\Chatbot\Exceptions\ServiceUnavailableException;
use AIPT\Api\Chatbot\Exceptions\InvalidRequestException;
use AIPT\Api\Chatbot\Exceptions\ContentFilterException;

/**
 * Core chatbot service.
 *
 * Orchestrates chat interactions between customers and AI,
 * using the product index for recommendations.
 *
 * @package AIPT\Api\Chatbot
 * @since 2.5.0
 */
class ChatbotService implements ChatbotServiceInterface {

    /**
     * Maximum message length (characters).
     */
    private const MAX_MESSAGE_LENGTH = 2000;

    /**
     * Default maximum products to load in bulk (memory protection).
     */
    private const DEFAULT_MAX_BULK_PRODUCTS = 50;

    /**
     * Default max products to return.
     */
    private const DEFAULT_MAX_RESULTS = 5;

    /**
     * Settings instance.
     *
     * @var ChatbotSettings
     */
    private $settings;

    /**
     * Index consumer instance.
     *
     * @var IndexConsumer
     */
    private $index_consumer;

    /**
     * AI client factory.
     *
     * @var AIClientFactoryInterface
     */
    private $ai_client_factory;

    /**
     * Prompt builder instance.
     *
     * @var PromptBuilder
     */
    private $prompt_builder;

    /**
     * Action registry instance.
     *
     * @var ActionRegistry
     */
    private $action_registry;

    /**
     * Message queue instance.
     *
     * @var MessageQueue
     */
    private $message_queue;

    /**
     * Maximum products to load in bulk.
     *
     * @var int
     */
    private $max_bulk_products;

    /**
     * Constructor.
     *
     * @param ChatbotSettings|null          $settings         Settings instance.
     * @param IndexConsumer|null            $index_consumer   Index consumer instance.
     * @param AIClientFactoryInterface|null $ai_client_factory AI client factory.
     * @param PromptBuilder|null            $prompt_builder   Prompt builder instance.
     * @param ActionRegistry|null           $action_registry  Action registry instance.
     * @param MessageQueue|null             $message_queue    Message queue instance.
     */
    public function __construct(
        ?ChatbotSettings $settings = null,
        ?IndexConsumer $index_consumer = null,
        ?AIClientFactoryInterface $ai_client_factory = null,
        ?PromptBuilder $prompt_builder = null,
        ?ActionRegistry $action_registry = null,
        ?MessageQueue $message_queue = null
    ) {
        $this->settings = $settings ?? new ChatbotSettings();

        if ($index_consumer === null) {
            $index_profile = $this->settings->get('behavior.index_profile');

            $profile_id = is_numeric($index_profile) ? (int) $index_profile : 0;
            $profile_id = ($profile_id > 0) ? $profile_id : null;

            $this->index_consumer = new IndexConsumer($profile_id);
        } else {
            $this->index_consumer = $index_consumer;
        }

        $this->ai_client_factory = $ai_client_factory ?? new RestAPIClientFactory();
        $this->prompt_builder = $prompt_builder ?? new PromptBuilder();
        $this->action_registry = $action_registry ?? new ActionRegistry();
        $this->message_queue = $message_queue ?? new MessageQueue();

        $max_bulk_products = (int) apply_filters(
            'aipt_chatbot_max_bulk_products',
            self::DEFAULT_MAX_BULK_PRODUCTS
        );

        if ($max_bulk_products < 1) {
            $max_bulk_products = self::DEFAULT_MAX_BULK_PRODUCTS;
        }

        if ($max_bulk_products > 100) {
            $max_bulk_products = 100;
        }

        $this->max_bulk_products = $max_bulk_products;
    }

    /**
     * Check if chatbot is available for use.
     *
     * Index availability is no longer a hard requirement - chatbot can operate
     * in "no recommendations" mode when the index is unavailable.
     *
     * @return bool True if chatbot is enabled.
     */
    public function isAvailable(): bool {
        return $this->settings->isEnabled();
    }

    /**
     * Check if product recommendations are available.
     *
     * @return bool True if index is available for product searches.
     */
    public function isProductRecommendationAvailable(): bool {
        return $this->index_consumer->isAvailable();
    }

    /**
     * Process a customer message and generate AI response.
     *
     * @param string   $message           Customer's message.
     * @param string   $session_id        UUID session identifier.
     * @param array    $history           Conversation history for context.
     * @param string   $user_name         Optional user name from pre-chat form.
     * @param string   $user_email        Optional user email from pre-chat form.
     * @param bool     $consent_given     Whether user gave consent for PII storage.
     * @param int|null $consent_timestamp Unix timestamp when consent was given.
     * @return array Response array.
     * @throws \InvalidArgumentException If message or session_id invalid.
     */
    public function processMessage(string $message, string $session_id, array $history = [], string $user_name = '', string $user_email = '', bool $consent_given = false, ?int $consent_timestamp = null): array {

        $message = trim($message);
        if (empty($message)) {

            throw new \InvalidArgumentException(__('Message cannot be empty.', 'ai-product-tools'));
        }

        if (strlen($message) > self::MAX_MESSAGE_LENGTH) {
            throw new \InvalidArgumentException(
                /* translators: %d is the maximum allowed message length in characters */
                sprintf( __('Message is too long (max %d characters).', 'ai-product-tools'), self::MAX_MESSAGE_LENGTH ) 
            );
        }

        if (!$this->isValidUuid($session_id)) {

            throw new \InvalidArgumentException(__('Invalid session ID format.', 'ai-product-tools'));
        }

        if (!$this->isAvailable()) {
            return [
                'success' => false,
                'error' => 'chatbot_unavailable',
                'message' => __('The chatbot is currently unavailable.', 'ai-product-tools'),
            ];
        }

        /**
         * Action fired before AI response is generated.
         *
         * @since 2.5.0
         * @param string $message    Customer's message.
         * @param string $session_id Session identifier.
         */
        do_action('aipt_chatbot_before_response', $message, $session_id);

        if ( $consent_given && ! empty( $user_email ) ) {
            $this->message_queue->trackFirstContact(
                $message,
                $session_id,
                $user_name,
                $user_email,
                $consent_given,
                $consent_timestamp
            );
        }

        try {

            $index_available = $this->isProductRecommendationAvailable();

            $search_query = $this->buildSearchQueryWithContext($message, $history);

            $products = $index_available ? $this->getProductRecommendations($search_query) : [];

            $products = $this->enrichWithLiveData($products);

            $all_settings = $this->settings->getAll();
            $system_prompt = $this->prompt_builder->buildSystemPrompt($all_settings);

            if (!$index_available) {
                $system_prompt .= "\n\nIMPORTANT: The product catalog is currently unavailable. Provide helpful general guidance without recommending specific products. If asked about products, politely explain that product information is temporarily unavailable.";
            }

            $sanitized_user_name = ! empty( $user_name ) ? sanitize_text_field( $user_name ) : '';
            $user_prompt = $this->prompt_builder->buildUserPrompt($message, $products, $all_settings, $history, $sanitized_user_name);

            $provider = $this->settings->get('behavior.provider');
            $model = $this->settings->get('behavior.model');
            $ai_client = $this->ai_client_factory->create($provider);
            $ai_response = $ai_client->generate_description($system_prompt, $user_prompt, $model);

            if (empty($ai_response)) {
                throw new \Exception(esc_html__('AI returned empty response.', 'ai-product-tools'));
            }

            $recommendation_mode = $this->settings->get('behavior.product_recommendation_mode', 'auto');
            $parsed_response = $this->parseAIResponse($ai_response, $recommendation_mode);
            $show_products = $parsed_response['show_products'];
            $clean_response = $parsed_response['response'];

            $mentioned_product_ids = $this->prompt_builder->extractMentionedProducts($clean_response, $products);

            $products_to_show = [];
            if (!empty($mentioned_product_ids)) {
                foreach ($products as $product) {
                    if (in_array($product['product_id'], $mentioned_product_ids, true)) {
                        $products_to_show[] = $product;
                    }
                }
            }

            $product_cards = [];
            if ($show_products && !empty($products_to_show)) {
                $max_products = $this->settings->get('behavior.max_products', 3);
                $product_cards = $this->formatProductCards(array_slice($products_to_show, 0, $max_products));
            }

            $this->recordSuccessfulExchange(
                $message,
                $session_id,
                $clean_response
            );

            $response = [
                'success' => true,
                'data' => [
                    'response' => $clean_response,
                    'products' => $product_cards,
                    'session_id' => $session_id,
                ],
            ];

            /**
             * Filter the chatbot response before sending to client.
             *
             * @since 2.5.0
             * @param array  $response   The response array.
             * @param string $message    Customer's message.
             * @param string $session_id Session identifier.
             */
            $response = apply_filters('aipt_chatbot_response', $response, $message, $session_id);

            return $response;

		} catch (RateLimitException $e) {

			$provider = (string) ( $this->settings->get( 'behavior.provider' ) ?: '' );
			return $this->handleErrorWithQueue($message, $session_id, $e->getMessage(), 'rate_limit', $user_name, $user_email, $consent_given, $consent_timestamp, $e, $provider);

		} catch (AuthenticationException $e) {
			$provider = $this->settings->get( 'behavior.provider' );
			return $this->handleProviderAuthError( $session_id, $e, is_string( $provider ) ? $provider : '', $e->getHttpStatus() );

        } catch (ServiceUnavailableException $e) {

            $provider = (string) ( $this->settings->get( 'behavior.provider' ) ?: '' );
            return $this->handleErrorWithQueue($message, $session_id, $e->getMessage(), 'service_unavailable', $user_name, $user_email, $consent_given, $consent_timestamp, $e, $provider);

        } catch (ContentFilterException $e) {

            $provider = (string) ( $this->settings->get( 'behavior.provider' ) ?: '' );
            return $this->handleErrorWithQueue($message, $session_id, $e->getMessage(), 'content_filter', $user_name, $user_email, $consent_given, $consent_timestamp, $e, $provider);

        } catch (InvalidRequestException $e) {

            $provider = (string) ( $this->settings->get( 'behavior.provider' ) ?: '' );
            return $this->handleErrorWithQueue($message, $session_id, $e->getMessage(), 'invalid_request', $user_name, $user_email, $consent_given, $consent_timestamp, $e, $provider);

		} catch (ChatbotException $e) {
			if ( 'provider_auth_error' === $e->getErrorCode() ) {
				return $this->handleProviderAuthError( $session_id, $e );
			}

            $provider = (string) ( $this->settings->get( 'behavior.provider' ) ?: '' );
            return $this->handleErrorWithQueue($message, $session_id, $e->getMessage(), $e->getErrorCode(), $user_name, $user_email, $consent_given, $consent_timestamp, $e, $provider);

		} catch (\Exception $e) {

			$chatbot_provider = $this->settings->get('behavior.provider');
			$chatbot_exception = ExceptionFactory::fromException($e, $chatbot_provider);
			$error_message = $e->getMessage();

			if ( 'provider_auth_error' === $chatbot_exception->getErrorCode() ) {
				return $this->handleProviderAuthError( $session_id, $chatbot_exception );
			}

            $provider_str = is_string( $chatbot_provider ) ? $chatbot_provider : '';
            return $this->handleErrorWithQueue($message, $session_id, $error_message, $chatbot_exception->getErrorCode(), $user_name, $user_email, $consent_given, $consent_timestamp, $chatbot_exception, $provider_str);
        }
    }

    /**
     * Record a successful exchange without interrupting the customer response.
     *
     * @param string $message     Customer's message.
     * @param string $session_id  Session identifier.
     * @param string $ai_response AI-generated response.
     */
    private function recordSuccessfulExchange(string $message, string $session_id, string $ai_response): void {
        try {
            $this->message_queue->recordSuccessfulExchange(
                $message,
                $session_id,
                $ai_response
            );
        } catch (\Throwable $e) {

        }
    }

    /**
     * Get product recommendations for a query.
     *
     * Uses IndexConsumer::searchByKeywords() for memory-efficient searching
     * on large product catalogs (scales to 50,000+ products).
     *
     * @param string $query      Natural language query.
     * @param int    $max_results Maximum products to return.
     * @return array Array of product data with product_id.
     */
    public function getProductRecommendations(string $query, int $max_results = self::DEFAULT_MAX_RESULTS): array {

        $max_results = min($max_results, $this->max_bulk_products);
        $result      = $this->index_consumer->searchByKeywords($query, $max_results);

        if (isset($result['no_match']) && true === $result['no_match']) {
            return [];
        }

        return $result;
    }

    /**
     * Enrich index search results with live WooCommerce data.
     *
     * Validates product existence (filters deleted/draft products) and updates
     * volatile fields (price, stock) with real-time data from WooCommerce.
     * Stable fields (description, categories, attributes) are kept from the index.
     *
     * WooCommerce's internal object cache ensures that products loaded here
     * won't cause duplicate DB queries when formatProductCards() loads them again.
     *
     * @param array $products Products from index search results.
     * @return array Enriched products with live data, dead products filtered out.
     */
    private function enrichWithLiveData(array $products): array {
        if (empty($products)) {
            return [];
        }

        $product_ids = array_filter(array_map(function ($product) {
            return $product['product_id'] ?? 0;
        }, $products));

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

        $product_ids = array_slice($product_ids, 0, $this->max_bulk_products);

        $wc_products = wc_get_products([
            'include' => $product_ids,
            'limit'   => count($product_ids),
            'status'  => 'publish',
        ]);

        $wc_map = [];
        foreach ($wc_products as $wc_product) {
            $wc_map[$wc_product->get_id()] = $wc_product;
        }

        $enriched = [];
        foreach ($products as $product) {
            $pid = $product['product_id'] ?? 0;

            if (!$pid || !isset($wc_map[$pid])) {
                continue;
            }

            $wc_product = $wc_map[$pid];

            $product['title'] = $wc_product->get_name();
            $product['price'] = $wc_product->get_price();
            $product['stock_status'] = $wc_product->get_stock_status();

            $regular_price = $wc_product->get_regular_price();
            if ($regular_price !== '') {
                $product['regular_price'] = $regular_price;
            }

            $sale_price = $wc_product->get_sale_price();
            if ($sale_price !== '') {
                $product['sale_price'] = $sale_price;
            } else {
                unset($product['sale_price']);
            }

            $stock_quantity = $wc_product->get_stock_quantity();
            if ($stock_quantity !== null) {
                $product['stock_quantity'] = (int) $stock_quantity;
            }

            $enriched[] = $product;
        }

        return $enriched;
    }

    /**
     * Format products for chat response.
     *
     * Uses bulk loading to avoid N+1 query problem.
     * Instead of calling wc_get_product() N times, loads all products in single query.
     *
     * @param array $products Raw product data from index.
     * @return array Formatted product cards.
     */
    public function formatProductCards(array $products): array {
        if (empty($products)) {
            return [];
        }

        $language = $this->settings->get('general.language', 'en');
        $behavior = $this->settings->getSection('behavior');

        $product_ids = array_filter(array_map(function ($product) {
            return $product['product_id'] ?? 0;
        }, $products));

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

        $product_ids = array_slice($product_ids, 0, $this->max_bulk_products);

        $wc_products = wc_get_products([
            'include' => $product_ids,
            'limit' => count($product_ids),
            'status' => 'publish',
        ]);

        $wc_product_map = [];
        foreach ($wc_products as $wc_product) {
            $wc_product_map[$wc_product->get_id()] = $wc_product;
        }

        $cards = [];
        foreach ($products as $product) {
            $product_id = $product['product_id'] ?? 0;
            if (!$product_id || !isset($wc_product_map[$product_id])) {
                continue;
            }

            $wc_product = $wc_product_map[$product_id];

            if ($wc_product->get_stock_status() === 'outofstock') {
                continue;
            }

            $is_variable = $wc_product->is_type('variable');
            $price_html = $is_variable
                ? $wc_product->get_price_html()
                : wc_price($wc_product->get_price());

            $price_html = preg_replace('/<span[^>]*class=["\'][^"\']*screen-reader-text[^"\']*["\'][^>]*>.*?<\/span>/is', '', $price_html);

            $price = wp_strip_all_tags($price_html);
            $price = html_entity_decode($price, ENT_QUOTES, 'UTF-8');

            $price = str_replace(['<', '>'], '', $price);

            $price = trim(preg_replace('/\s+/', ' ', $price));

            $cards[] = [
                'product_id' => $product_id,
                'title' => $product['title'] ?? $wc_product->get_name(),
                'price' => $price,
                'image_url' => $this->getProductImageUrl($wc_product),
                'permalink' => $wc_product->get_permalink(),
                'is_variable' => $is_variable,
                'stock_status' => $wc_product->get_stock_status(),
                'actions' => $this->action_registry->getActionButtonsForProduct($product_id, $language, $behavior),
            ];
        }

        return $cards;
    }

    /**
     * Get product image URL.
     *
     * @param \WC_Product $product WC Product instance.
     * @return string Image URL.
     */
    private function getProductImageUrl(\WC_Product $product): string {
        $image_id = $product->get_image_id();
        if ($image_id) {
            $image_url = wp_get_attachment_image_url($image_id, 'woocommerce_thumbnail');
            if ($image_url) {
                return $image_url;
            }
        }

        return wc_placeholder_img_src('woocommerce_thumbnail');
    }

    /**
     * Check if error is a rate limit error.
     *
     * @param string $error_message Error message.
     * @return bool True if rate limit error.
     */
    private function isRateLimitError(string $error_message): bool {
        $rate_limit_indicators = [
            'rate limit',
            'too many requests',
            '429',
            'quota exceeded',
            'rate_limit',
        ];

        $error_lower = strtolower($error_message);
        foreach ($rate_limit_indicators as $indicator) {
            if (strpos($error_lower, $indicator) !== false) {
                return true;
            }
        }

        return false;
    }

    private function handleProviderAuthError( string $session_id, ChatbotException $exception, string $fallback_provider = '', int $fallback_status = 0 ): array {
        $provider = method_exists( $exception, 'getProvider' ) ? (string) $exception->getProvider() : $fallback_provider;
        if ( '' === $provider ) {
            $configured_provider = $this->settings->get( 'behavior.provider' );
            $provider = is_string( $configured_provider ) ? $configured_provider : '';
        }

        $provider_status = method_exists( $exception, 'getProviderStatus' ) ? (int) $exception->getProviderStatus() : $fallback_status;

        $status = [
            'provider'    => sanitize_key( $provider ),
            'timestamp'   => current_time( 'timestamp', true ),
            'status_code' => $provider_status,
        ];

        set_transient( 'aipt_chatbot_provider_auth_status', $status, defined( 'HOUR_IN_SECONDS' ) ? HOUR_IN_SECONDS : 3600 );

        ChatbotLogger::error(
            'provider_auth_error',
            [
                'session_id'   => $session_id,
                'provider'     => $status['provider'],
                'status_code'  => $status['status_code'],
                'error_detail' => $exception->getMessage(),
            ]
        );

        return [
            'success' => false,
            'error'   => 'provider_auth_error',
            'message' => $exception->getUserMessage(),
            'data'    => [
                'recoverable'     => false,
                'provider'        => $status['provider'],
                'provider_status' => $status['status_code'],
            ],
        ];
    }

    /**
     * Handle error by queuing message for admin response.
     *
     * All non-authentication errors are queued so admin can respond manually.
     * Customer always sees the configured rate_limit_message for consistency.
     *
     * @param string          $message           Customer's message.
     * @param string          $session_id        Session identifier.
     * @param string          $error_message     Original error message for logging.
     * @param string          $error_code        Error code for categorization.
     * @param string          $user_name         Optional user name from pre-chat form.
     * @param string          $user_email        Optional user email from pre-chat form.
     * @param bool            $consent_given     Whether user gave consent for PII storage.
     * @param int|null        $consent_timestamp Unix timestamp when consent was given.
     * @param \Throwable|null $exception         Optional underlying exception for actionable
     *                                           provider-specific message resolution (@since 2.6.2).
     * @param string          $provider          Provider name ('openai', 'gemini', etc.) for the
     *                                           same actionable message resolution (@since 2.6.2).
     * @return array Response array.
     */
    private function handleErrorWithQueue(string $message, string $session_id, string $error_message, string $error_code = 'unknown', string $user_name = '', string $user_email = '', bool $consent_given = false, ?int $consent_timestamp = null, ?\Throwable $exception = null, string $provider = ''): array {

        $sanitized_detail = \AIPT\Core\ErrorHandler::sanitize_error_for_logging($error_message);
        error_log(sprintf(
            '[AIPT Chatbot] queued error: code=%s session=%s detail=%s',
            $error_code,
            $session_id,
            $sanitized_detail
        ));

        try {
            $queue_id = $this->message_queue->enqueue($message, $session_id, $error_message, $user_name, $user_email, $consent_given, $consent_timestamp);

            $user_message = $this->resolveUserFacingErrorMessage( $exception, $provider );

            /**
             * Action fired when a message is queued due to an error.
             *
             * @since 2.5.0
             * @param int    $queue_id     The queue record ID.
             * @param string $error_code   The error code that triggered queueing.
             * @param string $session_id   The session identifier.
             */
            do_action('aipt_chatbot_message_queued_on_error', $queue_id, $error_code, $session_id);

            return [
                'success' => true,
                'data' => [
                    'queued' => true,
                    'message' => $user_message,
                    'queue_id' => $queue_id,
                    'session_id' => $session_id,

                    'error_code' => $error_code,
                ],
            ];
        } catch (\Exception $e) {

            $user_message = $this->resolveUserFacingErrorMessage( $exception, $provider );
            return [
                'success' => false,
                'error' => 'queue_error',
                'message' => $user_message ?: __('Unable to process your request. Please try again later.', 'ai-product-tools'),
            ];
        }
    }

    /**
     * Resolve the customer-facing error message under the @since 2.6.2 contract.
     *
     * Three branches by descending privacy:
     *   - show_errors_to_user=false             — neutral acknowledgement
     *   - use_custom_error_message=true         — configured rate_limit_message verbatim
     *   - (default)                             — ErrorHandler::format_api_response_error()
     *
     * Extracted into its own method so both the success and queue-failure paths
     * of handleErrorWithQueue() honour the same contract — a queue outage must
     * never accidentally leak provider details that privacy mode forbids.
     *
     * @since 2.6.2
     *
     * @param \Throwable|null $exception Underlying provider exception, if any.
     * @param string          $provider  Provider name for ErrorHandler routing.
     * @return string                    Customer-facing message.
     */
    private function resolveUserFacingErrorMessage( ?\Throwable $exception, string $provider ): string {
        $show_errors_to_user      = (bool) $this->settings->get( 'general.show_errors_to_user', true );
        $use_custom_error_message = (bool) $this->settings->get( 'general.use_custom_error_message', false );

        if ( ! $show_errors_to_user ) {
            return __( 'Your message has been received. Our support team will respond shortly.', 'ai-product-tools' );
        }

        if ( $use_custom_error_message ) {
            $rate_limit_message = (string) $this->settings->get( 'general.rate_limit_message', '' );
            return $rate_limit_message !== ''
                ? $rate_limit_message
                : __( 'Your message has been received. Our support team will respond shortly.', 'ai-product-tools' );
        }

        if ( $exception !== null && $provider !== '' ) {
            $http_code = method_exists( $exception, 'getHttpStatus' ) ? (int) $exception->getHttpStatus() : 0;
            $formatted = \AIPT\Core\ErrorHandler::format_api_response_error(
                $exception->getMessage(),
                $provider,
                $http_code
            );

            $sensitive_patterns = array(
                '/\/[a-zA-Z0-9_\-\/]+\.php/i',  
                '/line \d+/i',                   
                '/API key/i',                    
                '/password/i',                   
                '/SELECT|INSERT|UPDATE|DELETE/i', 
                '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', 
                '/sk-[a-zA-Z0-9]+/',             
            );
            foreach ( $sensitive_patterns as $pattern ) {
                if ( preg_match( $pattern, $formatted ) ) {
                    return __( 'An error occurred. Please try again.', 'ai-product-tools' );
                }
            }

            return $formatted;
        }

        $rate_limit_message = (string) $this->settings->get( 'general.rate_limit_message', '' );
        return $rate_limit_message !== ''
            ? $rate_limit_message
            : __( 'Your message has been received. Our support team will respond shortly.', 'ai-product-tools' );
    }

    /**
     * Validate UUID format.
     *
     * @param string $uuid UUID to validate.
     * @return bool True if valid UUID v4.
     */
    private function isValidUuid(string $uuid): bool {
        $pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i';
        return (bool) preg_match($pattern, $uuid);
    }

    /**
     * Get action registry instance.
     *
     * @return ActionRegistry
     */
    public function getActionRegistry(): ActionRegistry {
        return $this->action_registry;
    }

    /**
     * Get message queue instance.
     *
     * @return MessageQueue
     */
    public function getMessageQueue(): MessageQueue {
        return $this->message_queue;
    }

    /**
     * Get settings instance.
     *
     * @return ChatbotSettings
     */
    public function getSettings(): ChatbotSettings {
        return $this->settings;
    }

    /**
     * Build search query by combining current message with context from conversation history.
     *
     * Extracts product-related keywords from previous messages to maintain context
     * when users ask follow-up questions like "how much?" or "is it available?".
     *
     * @param string $message Current user message.
     * @param array  $history Conversation history.
     * @return string Enhanced search query.
     */
    private function buildSearchQueryWithContext(string $message, array $history): string {
        if (empty($history)) {
            return $message;
        }

        $is_followup = $this->isFollowUpQuestion($message);

        if (!$is_followup) {

            return $message;
        }

        $context_keywords = $this->extractContextKeywords($history);

        if (empty($context_keywords)) {
            return $message;
        }

        return $context_keywords . ' ' . $message;
    }

    /**
     * Check if message appears to be a follow-up question.
     *
     * Uses language-agnostic detection based on message structure rather than
     * multilingual keyword patterns. This approach works for any language.
     *
     * @param string $message User message.
     * @return bool True if likely a follow-up question.
     */
    private function isFollowUpQuestion(string $message): bool {
        $message = trim($message);

        $word_count = preg_match_all('/[\p{L}\p{N}]+/u', $message, $matches);

        if ($word_count <= 6) {
            return true;
        }

        if (preg_match('/\b[A-Z][a-zA-Z0-9]*(?:[-][A-Za-z0-9]+)+\b/', $message)) {
            return false;
        }

        if (strpos($message, '?') !== false && $word_count <= 12) {
            return true;
        }

        if ($word_count <= 10 && !preg_match('/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b/', $message)) {
            return true;
        }

        return false;
    }

    /**
     * Extract product-related keywords from conversation history.
     *
     * Focuses on assistant responses where product names were mentioned,
     * as these contain the canonical product information.
     *
     * @param array $history Conversation history.
     * @return string Extracted keywords for search context.
     */
    private function extractContextKeywords(array $history): string {
        $keywords = [];

        $recent_history = array_slice(array_reverse($history), 0, 4);

        foreach ($recent_history as $msg) {
            $role = $msg['role'] ?? '';
            $content = $msg['content'] ?? '';

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

            $content = preg_replace('/\s*\[(SHOW|HIDE)_PRODUCTS\]\s*/i', '', $content);

            preg_match_all('/\b[A-Z][a-zA-Z]*(?:[-][A-Z]?[a-zA-Z]+)+\b/', $content, $hyphenated);
            if (!empty($hyphenated[0])) {
                $keywords = array_merge($keywords, $hyphenated[0]);
            }

            preg_match_all('/"([^"]+)"/', $content, $quoted);
            if (!empty($quoted[1])) {
                $keywords = array_merge($keywords, $quoted[1]);
            }

            if ($role === 'assistant') {

                preg_match_all('/\b(?:the|our|a)\s+([A-Z][a-zA-Z-]+(?:\s+[A-Z][a-zA-Z-]+)*)/i', $content, $products);
                if (!empty($products[1])) {
                    $keywords = array_merge($keywords, $products[1]);
                }
            }

            if ($role === 'user') {

                $words = preg_split('/\s+/', $content);
                $stop_words = ['what', 'have', 'your', 'about', 'with', 'this', 'that', 'from', 'they', 'been', 'have', 'were', 'said', 'each', 'which', 'their', 'will', 'would', 'there', 'could', 'other', 'into', 'more', 'some', 'such', 'only', 'over', 'these', 'also', 'back', 'after', 'most', 'made', 'being', 'where', 'much', 'when', 'very', 'just', 'know', 'take', 'come', 'than', 'them', 'then', 'been', 'many', 'well', 'does', 'tell', 'want', 'give', 'like', 'sell', 'price', 'cost', 'kadar', 'fiyat', 'kaç'];
                foreach ($words as $word) {
                    $clean_word = preg_replace('/[^a-zA-ZğüşöçıİĞÜŞÖÇ-]/u', '', $word);
                    if (strlen($clean_word) > 3 && !in_array(strtolower($clean_word), $stop_words, true)) {
                        $keywords[] = $clean_word;
                    }
                }
            }
        }

        $keywords = array_unique($keywords);
        $keywords = array_slice($keywords, 0, 5);

        return implode(' ', $keywords);
    }

    /**
     * Parse AI response for product recommendation tags.
     *
     * In 'auto' mode, the AI adds [SHOW_PRODUCTS] or [HIDE_PRODUCTS] at the end
     * of its response. This method extracts the tag and returns a clean response.
     *
     * @param string $response AI response text.
     * @param string $mode     Recommendation mode (auto|always|never).
     * @return array{response: string, show_products: bool}
     */
    private function parseAIResponse(string $response, string $mode): array {

        if ($mode === 'always') {
            return [
                'response' => $response,
                'show_products' => true,
            ];
        }

        if ($mode === 'never') {
            return [
                'response' => $response,
                'show_products' => false,
            ];
        }

        $show_products = null;
        $clean_response = $response;

        if (preg_match('/\[SHOW_PRODUCTS\]/i', $response)) {
            $show_products = true;

            $clean_response = preg_replace('/\s*\[SHOW_PRODUCTS\]\s*/i', '', $response);
        }

        elseif (preg_match('/\[HIDE_PRODUCTS\]/i', $response)) {
            $show_products = false;

            $clean_response = preg_replace('/\s*\[HIDE_PRODUCTS\]\s*/i', '', $response);
        }

        if ($show_products === null) {
            $show_products = true;
        }

        return [
            'response' => trim($clean_response),
            'show_products' => $show_products,
        ];
    }
}
