<?php
/**
 * Chat Service.
 *
 * Handles chat conversations, integrating RAG and AI providers.
 *
 * @package Inqyra
 */

// If this file is called directly, abort.
if (!defined('WPINC')) {
    die;
}

/**
 * Chat Service class.
 */
class Inqyra_Chat_Service {

    /**
     * RAG engine instance.
     *
     * @var Inqyra_RAG_Engine
     */
    private $rag_engine;

    /**
     * Maximum conversation history to include.
     */
    const MAX_HISTORY_MESSAGES = 10;

    /**
     * Constructor.
     */
    public function __construct() {
        try {
            $this->rag_engine = new Inqyra_RAG_Engine();
        } catch (\Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated behind WP_DEBUG.
                error_log(sprintf(
                    'Inqyra Chat Service: Failed to initialize RAG engine: %s in %s:%d',
                    $e->getMessage(),
                    $e->getFile(),
                    $e->getLine()
                ));
            }
            throw new \Exception(
                esc_html(__('Chat service initialization failed. Please check the error log.', 'inqyra')),
                0,
                $e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- $e is the previous exception, not output.
            );
        }
    }

    /**
     * Process a chat message.
     *
     * @param int    $agent_id   The agent ID.
     * @param string $session_id The session ID.
     * @param string $message    The user message.
     * @param array  $options    Additional options.
     * @return array Response data.
     * @throws Exception On error.
     */
    public function process_message(int $agent_id, string $session_id, string $message, array $options = []): array {
        global $wpdb;

        // Initialize debug logger for this request.
        $dbg = Inqyra_Debug_Logger::instance();
        $dbg->reset();

        $dbg->log('1_incoming_request', [
            'agent_id' => $agent_id,
            'session_id' => $session_id,
            'message' => $message,
            'message_length' => mb_strlen($message),
            'has_query_embedding' => !empty($options['query_embedding']),
        ]);

        // 1. Get agent configuration (with caching)
        $agent = $this->get_agent_cached($agent_id);

        if (!$agent) {
            throw new Exception(esc_html(__('Agent not found or inactive.', 'inqyra')));
        }

        $dbg->log('2_agent_config', [
            'agent_name' => $agent->name ?? '',
            'provider' => $agent->provider ?? '',
            'model' => $agent->model ?? '',
            'language' => $agent->language ?? '',
            'query_rewrite_enabled' => !empty($agent->query_rewrite_enabled),
            'document_search_mode' => $agent->document_search_mode ?? 'linked',
        ]);

        // 2. Check rate limits
        $this->check_rate_limits($agent, $session_id);

        // 3. Get or create conversation
        $conversation = $this->get_or_create_conversation($agent_id, $session_id);

        // 4. Get conversation history BEFORE saving current message
        //    to avoid including the current message in history (it's added separately in build_messages)
        $history = $this->get_conversation_history($conversation->id);

        $dbg->log('3_conversation', [
            'conversation_id' => $conversation->id,
            'history_messages' => count($history),
        ]);

        // 5. Save user message
        $this->save_message($conversation->id, 'user', $message);

        // 6. Expand synonyms for both search AND AI message
        $ai_message = $message;
        if (class_exists('Inqyra_Synonym_Service')) {
            $synonym_service = new Inqyra_Synonym_Service();
            $expansion = $synonym_service->expand_query(
                $message,
                $agent_id,
                $agent->synonym_expansion_strategy ?? 'append'
            );
            // Use expanded query for AI so it understands the actual intent
            if (!empty($expansion['matched_terms'])) {
                $ai_message = $expansion['expanded_query'];
            }

            $dbg->log('4_synonym_expansion', [
                'original' => $message,
                'expanded' => $ai_message,
                'matched_terms' => $expansion['matched_terms'] ?? [],
                'strategy' => $agent->synonym_expansion_strategy ?? 'append',
                'changed' => $message !== $ai_message,
            ]);
        } else {
            $dbg->log('4_synonym_expansion', ['status' => 'not_available']);
        }

        // 7. Search for relevant context using RAG
        $search_options = [
            'top_k' => 5,
            'min_score' => 0.3,
        ];
        // Pass through query_embedding if provided (from browser-based Transformers.js)
        if (!empty($options['query_embedding'])) {
            $search_options['query_embedding'] = $options['query_embedding'];
        }
        // Pass conversation history for query rewriting (resolves references in follow-up questions)
        $search_options['conversation_history'] = $history;
        $context_results = $this->rag_engine->search($agent_id, $message, $search_options);

        // Log RAG results summary.
        $dbg->log('8_rag_results_summary', [
            'total_results' => count($context_results),
            'results' => array_map(function ($r) {
                return [
                    'id' => $r['id'] ?? null,
                    'score' => round((float) ($r['score'] ?? 0), 4),
                    'match_type' => $r['match_type'] ?? '',
                    'title' => $r['title'] ?? '',
                    'source_url' => $r['source_url'] ?? '',
                    'content_type' => $r['content_type'] ?? '',
                    'content_preview' => mb_substr($r['content'] ?? '', 0, 200),
                ];
            }, $context_results),
        ]);

        // 8. Build context string
        $context = $this->rag_engine->build_context($context_results, 6000);

        $dbg->log('9_context_string', [
            'context_length' => mb_strlen($context),
            'chunks_included' => count($context_results),
            'context' => $context,
        ]);

        // 9. Build messages array for AI (using expanded message so AI understands synonyms)
        $messages = $this->build_messages($agent, $history, $ai_message, $context);

        $dbg->log('10_ai_request', [
            'provider' => $agent->provider ?? '',
            'model' => $agent->model ?: '(default)',
            'max_tokens' => (int) ($agent->max_tokens ?: 4096),
            'message_count' => count($messages),
            'system_prompt_length' => mb_strlen($messages[0]['content'] ?? ''),
            'system_prompt' => $messages[0]['content'] ?? '',
            'user_message' => $ai_message,
            'history' => array_slice($messages, 1, -1), // history messages (between system and user)
        ]);

        // 10. Get AI provider and send request
        $provider = Inqyra_AI_Provider_Factory::create_for_agent($agent);

        $ai_start = microtime(true);
        $response = $provider->chat($messages, [
            'model' => $agent->model ?: $provider->get_default_model(),
            'max_tokens' => (int) ($agent->max_tokens ?: 4096),
        ]);
        $ai_duration = round((microtime(true) - $ai_start) * 1000, 1);

        // 10b. Parse and strip intent metadata from AI response
        $parsed = $this->parse_intent_from_response($response['content'] ?? '');
        $response['content'] = $parsed['content'];
        $intent = $parsed['intent'];

        $dbg->log('11_ai_response', [
            'duration_ms' => $ai_duration,
            'model' => $response['model'] ?? '',
            'tokens_input' => $response['tokens']['input'] ?? 0,
            'tokens_output' => $response['tokens']['output'] ?? 0,
            'cost_usd' => $response['cost'] ?? 0,
            'response_length' => mb_strlen($response['content'] ?? ''),
            'response' => $response['content'] ?? '',
            'intent' => $intent,
        ]);

        // 11. Save assistant message
        $chunk_ids = array_column($context_results, 'id');
        $this->save_message(
            $conversation->id,
            'assistant',
            $response['content'],
            $response['tokens']['input'] + $response['tokens']['output'],
            $response['cost'],
            $chunk_ids
        );

        // 12. Update conversation stats
        $this->update_conversation_stats($conversation->id, $response);

        // 13. Trigger webhooks if configured
        $this->trigger_webhooks($agent_id, 'message.received', [
            'conversation_id' => $conversation->id,
            'session_id' => $session_id,
            'user_message' => $message,
            'assistant_message' => $response['content'],
            'message_number' => (int) $conversation->message_count + 1,
            'tokens_used' => $response['tokens']['input'] + $response['tokens']['output'],
        ]);

        // 14. Lead extraction and event detection
        $this->process_lead_extraction($agent_id, $conversation, $message, $history, $intent);

        // Flush debug log to file.
        $dbg->flush();

        return [
            'response' => $response['content'],
            'conversation_id' => $conversation->id,
            'tokens' => $response['tokens'],
            'model' => $response['model'],
            'context_used' => count($context_results),
            'search_debug' => $this->rag_engine->get_search_logs(),
        ];
    }

    /**
     * Check rate limits for the agent.
     *
     * @param object $agent      The agent configuration.
     * @param string $session_id The session ID.
     * @throws Exception If rate limit exceeded.
     */
    private function check_rate_limits($agent, string $session_id): void {
        global $wpdb;

        // Check hourly request limit
        if ($agent->hourly_request_limit > 0) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Real-time rate limit check must not be cached.
            $hourly_requests = $wpdb->get_var($wpdb->prepare(
                'SELECT COUNT(*) FROM %i m
                 JOIN %i c ON c.id = m.conversation_id
                 WHERE c.agent_id = %d AND m.role = \'user\' AND m.created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)',
                $wpdb->prefix . 'inqyra_messages',
                $wpdb->prefix . 'inqyra_conversations',
                $agent->id
            ));

            if ($hourly_requests >= $agent->hourly_request_limit) {
                throw new Exception(esc_html(__('Hourly request limit exceeded. Please try again later.', 'inqyra')));
            }
        }

        // Check daily token limit
        if ($agent->daily_token_limit > 0) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Real-time token limit check must not be cached.
            $daily_tokens = $wpdb->get_var($wpdb->prepare(
                'SELECT COALESCE(SUM(m.tokens_used), 0) FROM %i m
                 JOIN %i c ON c.id = m.conversation_id
                 WHERE c.agent_id = %d AND m.created_at > DATE_SUB(NOW(), INTERVAL 1 DAY)',
                $wpdb->prefix . 'inqyra_messages',
                $wpdb->prefix . 'inqyra_conversations',
                $agent->id
            ));

            if ($daily_tokens >= $agent->daily_token_limit) {
                throw new Exception(esc_html(__('Daily token limit exceeded. Please try again tomorrow.', 'inqyra')));
            }
        }

        // Check daily cost limit
        if ($agent->cost_limit_usd > 0) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Real-time cost limit check must not be cached.
            $daily_cost = $wpdb->get_var($wpdb->prepare(
                'SELECT COALESCE(SUM(m.cost_usd), 0) FROM %i m
                 JOIN %i c ON c.id = m.conversation_id
                 WHERE c.agent_id = %d AND m.created_at > DATE_SUB(NOW(), INTERVAL 1 DAY)',
                $wpdb->prefix . 'inqyra_messages',
                $wpdb->prefix . 'inqyra_conversations',
                $agent->id
            ));

            if ($daily_cost >= $agent->cost_limit_usd) {
                throw new Exception(esc_html(__('Daily cost limit exceeded. Please try again tomorrow.', 'inqyra')));
            }
        }
    }

    /**
     * Get or create a conversation.
     *
     * @param int    $agent_id   The agent ID.
     * @param string $session_id The session ID.
     * @return object Conversation object.
     */
    private function get_or_create_conversation(int $agent_id, string $session_id) {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Conversation lookup by session, must be real-time.
        $conversation = $wpdb->get_row($wpdb->prepare(
            'SELECT * FROM %i WHERE session_id = %s',
            $wpdb->prefix . 'inqyra_conversations',
            $session_id
        ));

        if (!$conversation) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Inserting new conversation record.
            $inserted = $wpdb->insert(
                $wpdb->prefix . 'inqyra_conversations',
                [
                    'agent_id' => $agent_id,
                    'session_id' => $session_id,
                    'visitor_ip' => $this->get_visitor_ip_hash(),
                    'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? substr(sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])), 0, 500) : '',
                    'referrer_url' => isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : '',
                    'started_at' => current_time('mysql'),
                ]
            );

            if ($inserted === false) {
                throw new \Exception(
                    esc_html(sprintf('Failed to create conversation: %s', $wpdb->last_error))
                );
            }

            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Fetch just-inserted conversation.
            $conversation = $wpdb->get_row($wpdb->prepare(
                'SELECT * FROM %i WHERE id = %d',
                $wpdb->prefix . 'inqyra_conversations',
                $wpdb->insert_id
            ));

            if (!$conversation) {
                throw new \Exception(esc_html('Failed to retrieve newly created conversation.'));
            }

            // Trigger conversation started webhook
            $this->trigger_webhooks($agent_id, 'conversation.started', [
                'conversation_id' => $conversation->id,
                'session_id' => $session_id,
            ]);
        }

        return $conversation;
    }

    /**
     * Get conversation history.
     *
     * @param int $conversation_id The conversation ID.
     * @return array Array of messages.
     */
    private function get_conversation_history(int $conversation_id): array {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Conversation history must be real-time.
        $messages = $wpdb->get_results($wpdb->prepare(
            'SELECT role, content FROM %i
             WHERE conversation_id = %d
             ORDER BY created_at DESC
             LIMIT %d',
            $wpdb->prefix . 'inqyra_messages',
            $conversation_id,
            self::MAX_HISTORY_MESSAGES
        ), ARRAY_A);

        // Reverse to get chronological order (oldest first)
        return array_reverse($messages);
    }

    /**
     * Build messages array for AI provider.
     *
     * @param object $agent   The agent configuration.
     * @param array  $history Conversation history.
     * @param string $message Current user message.
     * @param string $context RAG context.
     * @return array Messages array.
     */
    private function build_messages($agent, array $history, string $message, string $context): array {
        $messages = [];

        // System prompt with context
        $system_prompt = $agent->system_prompt ?: $this->get_default_system_prompt($agent->language);

        if ($context) {
            $system_prompt .= "\n\n## Relevante informatie uit de kennisbank:\n\n" . $context;
            $system_prompt .= "\n\n---\nGebruik bovenstaande informatie om de vraag te beantwoorden. Als de informatie niet relevant is voor de vraag, negeer deze dan en geef een algemeen antwoord.";
        }

        // Intent detection instruction (language-agnostic, works with all AI providers)
        $system_prompt .= "\n\n## Internal Metadata (never show this to the user)\n"
            . "At the very end of every response, on its own line, add exactly one intent tag:\n"
            . "[INTENT:escalation:0.9] — if the user wants to speak to a human, be transferred, or get live support\n"
            . "[INTENT:appointment:0.9] — if the user wants to schedule a call, meeting, or callback\n"
            . "[INTENT:none:1.0] — for all other messages\n"
            . "Replace 0.9 with your confidence (0.0–1.0). Always include exactly one tag.";

        $messages[] = [
            'role' => 'system',
            'content' => $system_prompt,
        ];

        // Add conversation history (excluding current message)
        foreach ($history as $msg) {
            $messages[] = [
                'role' => $msg['role'],
                'content' => $msg['content'],
            ];
        }

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

        return $messages;
    }

    /**
     * Get default system prompt.
     *
     * @param string $language Language code.
     * @return string Default system prompt.
     */
    private function get_default_system_prompt(string $language = 'nl'): string {
        return Inqyra_Default_Prompts::get( $language );
    }

    /**
     * Save a message to the database.
     *
     * @param int    $conversation_id The conversation ID.
     * @param string $role           Message role (user/assistant).
     * @param string $content        Message content.
     * @param int    $tokens_used    Tokens used (optional).
     * @param float  $cost_usd       Cost in USD (optional).
     * @param array  $chunk_ids      Context chunk IDs (optional).
     */
    private function save_message(int $conversation_id, string $role, string $content, int $tokens_used = 0, float $cost_usd = 0, array $chunk_ids = []): void {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Inserting chat message record.
        $wpdb->insert(
            $wpdb->prefix . 'inqyra_messages',
            [
                'conversation_id' => $conversation_id,
                'role' => $role,
                'content' => $content,
                'tokens_used' => $tokens_used,
                'cost_usd' => $cost_usd,
                'context_chunks' => !empty($chunk_ids) ? wp_json_encode($chunk_ids) : null,
                'created_at' => current_time('mysql'),
            ]
        );
    }

    /**
     * Update conversation statistics.
     *
     * @param int   $conversation_id The conversation ID.
     * @param array $response        AI response data.
     */
    private function update_conversation_stats(int $conversation_id, array $response): void {
        global $wpdb;

        $total_tokens = ($response['tokens']['input'] ?? 0) + ($response['tokens']['output'] ?? 0);
        $cost = $response['cost'] ?? 0;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic counter update for conversation stats.
        $wpdb->query($wpdb->prepare(
            'UPDATE %i
             SET message_count = message_count + 2,
                 total_tokens = total_tokens + %d,
                 total_cost_usd = total_cost_usd + %f,
                 last_message_at = %s
             WHERE id = %d',
            $wpdb->prefix . 'inqyra_conversations',
            $total_tokens,
            $cost,
            current_time('mysql'),
            $conversation_id
        ));
    }

    /**
     * Process lead extraction and trigger related webhook events.
     *
     * @param int        $agent_id     The agent ID.
     * @param object     $conversation The conversation object.
     * @param string     $message      The current user message.
     * @param array      $history      Conversation history.
     * @param array|null $intent       AI-detected intent ['type' => string, 'confidence' => float] or null.
     */
    private function process_lead_extraction(int $agent_id, object $conversation, string $message, array $history, ?array $intent = null): void {
        global $wpdb;

        if (!class_exists('Inqyra_Lead_Extractor')) {
            return;
        }

        try {
            $extractor = new Inqyra_Lead_Extractor();

            // Extract lead data from full conversation including current message
            $messages_for_extraction = array_merge($history, [
                ['role' => 'user', 'content' => $message],
            ]);
            $lead = $extractor->extract_from_messages($messages_for_extraction);

            // Fire lead.captured once per conversation
            if ($extractor->is_valid_lead($lead) && empty($conversation->lead_captured)) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Flag lead as captured once per conversation.
                $wpdb->update(
                    $wpdb->prefix . 'inqyra_conversations',
                    ['lead_captured' => 1],
                    ['id' => $conversation->id],
                    ['%d'],
                    ['%d']
                );

                $this->trigger_webhooks($agent_id, 'lead.captured', [
                    'conversation_id' => (int) $conversation->id,
                    'session_id' => $conversation->session_id,
                    'lead_email' => $lead['email'] ?? '',
                    'lead_phone' => $lead['phone'] ?? '',
                    'lead_name' => $lead['name'] ?? '',
                    'lead_company' => $lead['company'] ?? '',
                    'extraction_confidence' => $lead['score'] ?? 0,
                    'source_url' => $conversation->referrer_url ?: '',
                ]);

                do_action('inqyra_lead_captured', $lead, (int) $conversation->id, $agent_id);
            }

            // Fire escalation.requested — AI intent first, regex fallback
            $is_escalation = ($intent && $intent['type'] === 'escalation' && $intent['confidence'] >= 0.6)
                || $extractor->detect_escalation($message);

            if ($is_escalation) {
                $this->trigger_webhooks($agent_id, 'escalation.requested', [
                    'conversation_id' => (int) $conversation->id,
                    'session_id' => $conversation->session_id,
                    'reason' => 'User requested human assistance',
                    'trigger_message' => $message,
                    'lead_data' => $lead,
                    'conversation_history' => array_slice($history, -10),
                    'source_url' => $conversation->referrer_url ?: '',
                ]);

                do_action('inqyra_escalation_requested', $message, (int) $conversation->id, $agent_id);
            }

            // Fire appointment.requested — AI intent first, regex fallback
            $is_appointment = ($intent && $intent['type'] === 'appointment' && $intent['confidence'] >= 0.6)
                || $extractor->detect_appointment($message);

            if ($is_appointment) {
                $this->trigger_webhooks($agent_id, 'appointment.requested', [
                    'conversation_id' => (int) $conversation->id,
                    'session_id' => $conversation->session_id,
                    'trigger_message' => $message,
                    'lead_data' => $lead,
                    'source_url' => $conversation->referrer_url ?: '',
                ]);
            }
        } catch (Exception $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated behind WP_DEBUG.
                error_log('Inqyra lead extraction error: ' . $e->getMessage());
            }
        }
    }

    /**
     * Parse intent metadata tag from AI response and strip it.
     *
     * The AI is instructed to append [INTENT:type:confidence] to each response.
     * This method extracts the tag and returns the clean response plus parsed intent.
     *
     * @param string $content Raw AI response content.
     * @return array ['content' => clean string, 'intent' => ['type' => string, 'confidence' => float]|null]
     */
    private function parse_intent_from_response(string $content): array {
        $intent = null;

        if (preg_match('/\[INTENT:(escalation|appointment|none):([0-9.]+)\]\s*$/', $content, $match)) {
            $intent = [
                'type' => $match[1],
                'confidence' => (float) $match[2],
            ];
            $content = trim(preg_replace('/\[INTENT:[^\]]+\]\s*$/', '', $content));
        }

        return [
            'content' => $content,
            'intent' => $intent,
        ];
    }

    /**
     * Trigger webhooks for an event.
     *
     * @param int    $agent_id The agent ID.
     * @param string $event    Event type.
     * @param array  $data     Event data.
     */
    private function trigger_webhooks(int $agent_id, string $event, array $data): void {
        if (!class_exists('Inqyra_Webhook_Service')) {
            return;
        }

        try {
            $webhook_service = new Inqyra_Webhook_Service();
            $webhook_service->trigger($agent_id, $event, $data);
        } catch (Exception $e) {
            // Log but don't fail the request.
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated behind WP_DEBUG.
                error_log('Inqyra webhook error: ' . $e->getMessage());
            }
        }
    }

    /**
     * Get a hashed version of the visitor IP address for GDPR-compliant storage.
     *
     * Uses HMAC-SHA256 with a WordPress salt so hashes are consistent per site
     * (allowing grouping by visitor) but not reversible without the salt.
     *
     * @return string Hashed IP address, or empty string if unavailable.
     */
    private function get_visitor_ip_hash(): string {
        $ip = $this->get_visitor_ip();

        if (empty($ip)) {
            return '';
        }

        $salt = defined('AUTH_SALT') && AUTH_SALT ? AUTH_SALT : 'inqyra_ip_salt';
        return substr(hash_hmac('sha256', $ip, $salt), 0, 45);
    }

    /**
     * Get visitor IP address (raw, for internal use only - never store directly).
     *
     * @return string IP address.
     */
    private function get_visitor_ip(): string {
        $ip_keys = [
            'HTTP_CF_CONNECTING_IP', // Cloudflare
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP',
            'REMOTE_ADDR',
        ];

        foreach ($ip_keys as $key) {
            if (!empty($_SERVER[$key])) {
                $ip = sanitize_text_field(wp_unslash($_SERVER[$key]));
                // Handle comma-separated list (X-Forwarded-For)
                if (strpos($ip, ',') !== false) {
                    $ip = trim(explode(',', $ip)[0]);
                }
                if (filter_var($ip, FILTER_VALIDATE_IP)) {
                    return $ip;
                }
            }
        }

        return '';
    }

    /**
     * Get conversation by ID.
     *
     * @param int $conversation_id The conversation ID.
     * @return object|null Conversation object.
     */
    public function get_conversation(int $conversation_id) {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Single conversation lookup by primary key.
        return $wpdb->get_row($wpdb->prepare(
            'SELECT * FROM %i WHERE id = %d',
            $wpdb->prefix . 'inqyra_conversations',
            $conversation_id
        ));
    }

    /**
     * Get messages for a conversation.
     *
     * @param int $conversation_id The conversation ID.
     * @param int $limit           Maximum messages to return.
     * @return array Array of messages.
     */
    public function get_messages(int $conversation_id, int $limit = 100): array {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Message retrieval must be real-time.
        return $wpdb->get_results($wpdb->prepare(
            'SELECT * FROM %i
             WHERE conversation_id = %d
             ORDER BY created_at ASC
             LIMIT %d',
            $wpdb->prefix . 'inqyra_messages',
            $conversation_id,
            $limit
        ), ARRAY_A);
    }

    /**
     * Get agent configuration with caching.
     *
     * Uses WordPress transients to cache agent config for 60 seconds.
     *
     * @param int $agent_id The agent ID.
     * @return object|null The agent object or null if not found.
     */
    private function get_agent_cached(int $agent_id) {
        $cache_key = 'inqyra_agent_' . $agent_id;
        $cached = get_transient($cache_key);

        if ($cached !== false) {
            return $cached;
        }

        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Agent config cached via transient above.
        $agent = $wpdb->get_row($wpdb->prepare(
            'SELECT * FROM %i WHERE id = %d AND active = 1',
            $wpdb->prefix . 'inqyra_agents',
            $agent_id
        ));

        if ($agent) {
            set_transient($cache_key, $agent, 60); // Cache for 60 seconds
        }

        return $agent;
    }

    /**
     * Clear agent config cache.
     *
     * Call this when agent configuration is updated.
     *
     * @param int $agent_id The agent ID.
     */
    public static function clear_agent_cache(int $agent_id) {
        delete_transient('inqyra_agent_' . $agent_id);
    }
}
