<?php
/**
 * AJAX Handler class for managing plugin AJAX operations.
 *
 * This class handles all AJAX operations for the Ask Any plugin,
 * including chat interactions, content indexing, and Q&A management.
 *
 * @package Askany
 * @since 1.0.0
 */
namespace Askany;

use ask\askany\wpnts_nts_fs;

/**
 * AJAX Handler class for managing plugin AJAX operations
 *
 * @package Askany
 * @since 1.0.0
 */
class AjaxHandler {
	/**
	 * API Manager instance for handling AI interactions.
	 *
	 * @var ApiManager
	 */
	private $api_manager;
	/**
	 * Content Indexer instance for managing content indexing.
	 *
	 * @var ContentIndexer
	 */
	private $content_indexer;
	/**
	 * PDF Handler instance for managing PDF processing.
	 *
	 * @var PdfHandler
	 */
	private $pdf_handler;
	/**
	 * Plugin instance.
	 *
	 * @var Plugin
	 */
	private $plugin;
	/**
	 * Database instance for managing database operations.
	 *
	 * @var Database
	 */
	private $database;

	/**
	 * Slack Notifier instance for sending notifications.
	 *
	 * @var SlackNotifier
	 */
	private $slack_notifier;

	/**
	 * Constructor for AjaxHandler.
	 *
	 * Initializes the API Manager, Content Indexer, PDF Handler, Database, and SlackNotifier instances.
	 *
	 * @param Plugin         $plugin          Plugin instance.
	 * @param ApiManager     $api_manager     API Manager instance.
	 * @param ContentIndexer $content_indexer Content Indexer instance.
	 * @param PdfHandler     $pdf_handler     PDF Handler instance.
	 * @param Database       $database        Database instance.
	 * @param SlackNotifier  $slack_notifier  Slack Notifier instance.
	 */
	public function __construct( $plugin, $api_manager, $content_indexer, $pdf_handler, $database, $slack_notifier ) {
		$this->plugin = $plugin;
		$this->api_manager = $api_manager;
		$this->content_indexer = $content_indexer;
		$this->pdf_handler = $pdf_handler;
		$this->database = $database;
		$this->slack_notifier = $slack_notifier;
	}

	/**
	 * Initialize the AjaxHandler.
	 *
	 * This method hooks into WordPress to register AJAX actions and REST API routes.
	 *
	 * @return void
	 */
	public function init() {
		// Chat frontend AJAX handler.
		add_action('wp_ajax_askany_chat_response', array( $this, 'chat_response' ));
		add_action('wp_ajax_nopriv_askany_chat_response', array( $this, 'chat_response' ));

		// Fresh nonces for pages served from a full-page cache (cookie-aware, unlike REST)
		add_action('wp_ajax_askany_get_nonce', array( $this, 'get_fresh_nonce' ));
		add_action('wp_ajax_nopriv_askany_get_nonce', array( $this, 'get_fresh_nonce' ));

		// Visitor attachment download (session-secret protected)
		add_action('wp_ajax_askany_visitor_download_attachment', array( $this, 'visitor_download_attachment' ));
		add_action('wp_ajax_nopriv_askany_visitor_download_attachment', array( $this, 'visitor_download_attachment' ));

		// Streaming chat handler (P4.7)
		add_action('wp_ajax_askany_chat_stream', array( $this, 'chat_response_stream' ));
		add_action('wp_ajax_nopriv_askany_chat_stream', array( $this, 'chat_response_stream' ));

		// Chat with file attachments AJAX handler.
		add_action('wp_ajax_askany_chat_response_with_files', array( $this, 'chat_response_with_files' ));
		add_action('wp_ajax_nopriv_askany_chat_response_with_files', array( $this, 'chat_response_with_files' ));

		// P3.4 — Pinecone settings + test + migration
		add_action( 'wp_ajax_askany_save_pinecone_settings',    array( $this, 'save_pinecone_settings' ) );
		add_action( 'wp_ajax_askany_test_pinecone_connection',  array( $this, 'test_pinecone_connection' ) );
		add_action( 'wp_ajax_askany_pinecone_migrate_batch',    array( $this, 'pinecone_migrate_batch' ) );

		// P5.4 — Register WP-Cron cleanup jobs
		add_action( 'askany_cron_cleanup', array( $this, 'run_cron_cleanup' ) );
		if ( ! wp_next_scheduled( 'askany_cron_cleanup' ) ) {
			wp_schedule_event( time(), 'daily', 'askany_cron_cleanup' );
		}

		// Admin AJAX handlers.
		add_action('wp_ajax_askany_save_settings', array( $this, 'save_settings' ));
		add_action('wp_ajax_askany_test_api_connection', array( $this, 'test_api_connection' ));
		add_action('wp_ajax_askany_test_fcm_connection', array( $this, 'test_fcm_connection' ));
		add_action('wp_ajax_askany_send_test_push', array( $this, 'send_test_push' ));
		add_action('wp_ajax_askany_reindex_content', array( $this, 'reindex_content' ));
		add_action('wp_ajax_askany_start_indexing', array( $this, 'start_indexing' ));
		add_action('wp_ajax_askany_process_indexing_batch', array( $this, 'process_indexing_batch' ));
		add_action('wp_ajax_askany_process_pdf', array( $this, 'process_pdf' ));
		add_action('wp_ajax_askany_clear_chat_all_history', array( $this, 'clear_chat_all_history' ));

		 // Handler for provider settings.
		add_action('wp_ajax_askany_get_provider_settings', array( $this, 'get_provider_settings' ));
		add_action('wp_ajax_askany_get_provider_models', array( $this, 'get_provider_models' ));
		add_action('wp_ajax_askany_test_custom_provider', array( $this, 'test_custom_provider' ));

		// Setup REST API endpoint for chat.
		add_action('rest_api_init', array( $this, 'register_rest_routes' ));

		add_action('wp_ajax_askany_test_api_connection', array( $this, 'test_api_connection' ));

		add_action('wp_ajax_askany_add_qa_pair', array( $this, 'add_qa_pair' ));
		add_action('wp_ajax_askany_update_qa_pair', array( $this, 'update_qa_pair' ));
		add_action('wp_ajax_askany_delete_qa_pair', array( $this, 'delete_qa_pair' ));
		add_action('wp_ajax_askany_get_qa_pair', array( $this, 'get_qa_pair' ));
		add_action('wp_ajax_askany_export_qa_pairs', array( $this, 'export_qa_pairs' ));
		add_action('wp_ajax_askany_import_qa_pairs', array( $this, 'import_qa_pairs' ));

		// AI Feedback Memory AJAX handlers
		add_action('wp_ajax_askany_add_feedback_memory', array( $this, 'add_feedback_memory' ));
		add_action('wp_ajax_askany_submit_learning_feedback', array( $this, 'submit_learning_feedback' ));
		add_action('wp_ajax_askany_update_feedback_memory', array( $this, 'update_feedback_memory' ));
		add_action('wp_ajax_askany_delete_feedback_memory', array( $this, 'delete_feedback_memory' ));
		add_action('wp_ajax_askany_get_feedback_memories', array( $this, 'get_feedback_memories' ));

		// Chat management AJAX handlers
		add_action('wp_ajax_askany_add_agent', array( $this, 'add_agent' ));
		add_action('wp_ajax_askany_remove_agent', array( $this, 'remove_agent' ));
		add_action('wp_ajax_askany_get_chat_sessions', array( $this, 'get_chat_sessions' ));
		add_action('wp_ajax_askany_get_chat_messages', array( $this, 'get_chat_messages' ));
		add_action('wp_ajax_askany_assign_agent', array( $this, 'assign_agent' ));
		add_action('wp_ajax_askany_release_agent', array( $this, 'release_agent' ));
		add_action('wp_ajax_askany_send_agent_message', array( $this, 'send_agent_message' ));
		add_action('wp_ajax_askany_merge_session', array( $this, 'merge_session' ));
		add_action('wp_ajax_nopriv_askany_merge_session', array( $this, 'merge_session' ));

		// Real-time functionality AJAX handlers
		add_action('wp_ajax_askany_set_typing', array( $this, 'set_typing_indicator' ));
		add_action('wp_ajax_nopriv_askany_set_typing', array( $this, 'set_typing_indicator' ));
		add_action('wp_ajax_askany_get_realtime_updates', array( $this, 'get_realtime_updates' ));
		add_action('wp_ajax_nopriv_askany_get_realtime_updates', array( $this, 'get_realtime_updates' ));
		add_action('wp_ajax_askany_get_batch_updates', array( $this, 'get_batch_realtime_updates' ));
		add_action('wp_ajax_askany_poll_messages', array( $this, 'poll_messages' ));

		// Home page management AJAX handlers
		add_action('wp_ajax_askany_save_home_settings', array( $this, 'save_home_settings' ));
		add_action('wp_ajax_askany_save_home_faq', array( $this, 'save_home_faq' ));
		add_action('wp_ajax_askany_get_home_faq', array( $this, 'get_home_faq' ));
		add_action('wp_ajax_askany_delete_home_faq', array( $this, 'delete_home_faq' ));
		add_action('wp_ajax_askany_toggle_home_faq', array( $this, 'toggle_home_faq' ));
		add_action('wp_ajax_askany_save_home_link', array( $this, 'save_home_link' ));
		add_action('wp_ajax_askany_get_home_link', array( $this, 'get_home_link' ));
		add_action('wp_ajax_askany_delete_home_link', array( $this, 'delete_home_link' ));
		add_action('wp_ajax_askany_toggle_home_link', array( $this, 'toggle_home_link' ));
		
		// Get started page AJAX handler
		add_action('wp_ajax_askany_submit_get_started', array( $this, 'submit_get_started' ));
		add_action('wp_ajax_nopriv_askany_submit_get_started', array( $this, 'submit_get_started' ));

		// Chat management AJAX handlers
		add_action('wp_ajax_askany_clear_chat_history', array( $this, 'clear_chat_history' ));
		add_action('wp_ajax_askany_delete_user_session', array( $this, 'delete_user_session' ));
		
		// Internet search AJAX handler
		add_action('wp_ajax_askany_search_internet', array( $this, 'search_internet' ));
		add_action('wp_ajax_nopriv_askany_search_internet', array( $this, 'search_internet' ));

		// Session info AJAX handler
		add_action('wp_ajax_askany_get_session_info', array( $this, 'get_session_info' ));
		add_action('wp_ajax_nopriv_askany_get_session_info', array( $this, 'get_session_info' ));

		// Custom DB Cache handler
		add_action('wp_ajax_askany_clear_custom_db_cache', array( $this, 'clear_custom_db_cache' ));

		// RAG embeddings batch processing
		add_action('wp_ajax_askany_generate_embeddings_batch', array( $this, 'generate_embeddings_batch' ));
		add_action('wp_ajax_askany_get_embeddings_status', array( $this, 'get_embeddings_status' ));
		add_action('wp_ajax_askany_clear_all_embeddings', array( $this, 'clear_all_embeddings' ));
		add_action('wp_ajax_askany_save_embedding_model', array( $this, 'save_embedding_model' ));
		add_action('wp_ajax_askany_get_embedding_errors', array( $this, 'get_embedding_errors' ));
		add_action('wp_ajax_askany_reset_stuck_embeddings', array( $this, 'reset_stuck_embeddings' ));
		add_action('wp_ajax_askany_retry_permanently_failed_embeddings', array( $this, 'retry_permanently_failed_embeddings' )); // NEW
		add_action('wp_ajax_askany_dismiss_rag_notice', array( $this, 'dismiss_rag_notice' ));
		add_action('wp_ajax_askany_check_quota_error', array( $this, 'check_quota_error' ));
		add_action('wp_ajax_askany_save_quota_error', array( $this, 'save_quota_error' ));
		add_action('wp_ajax_askany_dismiss_quota_error', array( $this, 'dismiss_quota_error' ));
		add_action('wp_ajax_askany_clear_quota_error', array( $this, 'clear_quota_error' ));

		// News AJAX handlers
		add_action('wp_ajax_askany_get_full_post', array( $this, 'get_full_post' ));
		add_action('wp_ajax_nopriv_askany_get_full_post', array( $this, 'get_full_post' ));
		
		// Content viewing AJAX handlers
		add_action('wp_ajax_askany_get_indexed_content', array( $this, 'get_indexed_content' ));
		add_action('wp_ajax_askany_get_content_details', array( $this, 'get_content_details' ));

		// Get all messages for a session AJAX handler
		add_action('wp_ajax_askany_get_all_messages', array( $this, 'get_all_messages' ));
		
		// Unread message management AJAX handlers
		add_action('wp_ajax_askany_mark_session_read', array( $this, 'mark_session_read' ));
		add_action('wp_ajax_askany_get_unread_count', array( $this, 'get_unread_count' ));
		
		// Integration AJAX handlers
		add_action('wp_ajax_askany_save_integration_settings', array( $this, 'save_integration_settings' ));
		add_action('wp_ajax_askany_save_slack_settings', array( $this, 'save_slack_settings' ));
		add_action('wp_ajax_askany_test_slack_webhook', array( $this, 'test_slack_webhook' ));
		add_action('wp_ajax_askany_save_woocommerce_settings', array( $this, 'save_woocommerce_settings' ));
		add_action('wp_ajax_askany_save_rate_limit_settings', array( $this, 'save_rate_limit_settings' ));
		add_action('wp_ajax_nopriv_askany_get_all_messages', array( $this, 'get_all_messages' ));

		// Save user message only AJAX handler
		add_action('wp_ajax_askany_save_user_message', array( $this, 'save_user_message' ));
		add_action('wp_ajax_nopriv_askany_save_user_message', array( $this, 'save_user_message' ));

		// Product AJAX handlers
		add_action('wp_ajax_askany_get_product_details', array( $this, 'get_product_details' ));
		add_action('wp_ajax_nopriv_askany_get_product_details', array( $this, 'get_product_details' ));
		add_action('wp_ajax_askany_add_to_cart', array( $this, 'add_to_cart' ));
		add_action('wp_ajax_nopriv_askany_add_to_cart', array( $this, 'add_to_cart' ));
		
		// API statistics AJAX handlers
		add_action('wp_ajax_askany_get_api_stats', array( $this, 'get_api_stats' ));
		add_action('wp_ajax_askany_refresh_api_stats', array( $this, 'refresh_api_stats' ));
		add_action('wp_ajax_askany_get_paginated_errors', array( $this, 'get_paginated_errors' ));
		add_action('wp_ajax_askany_clear_api_errors', array( $this, 'clear_api_errors' ));
		add_action('wp_ajax_askany_get_paginated_chat_logs', array( $this, 'get_paginated_chat_logs' ));

		// Database management AJAX handlers
		add_action('wp_ajax_askany_cleanup_old_data', array( $this, 'cleanup_old_data' ));
		
		add_action('wp_ajax_askany_truncate_all_data', array( $this, 'truncate_all_data' ));
		add_action('wp_ajax_askany_optimize_tables', array( $this, 'optimize_tables' ));
		add_action('wp_ajax_askany_delete_date_range', array( $this, 'delete_date_range' ));
		add_action('wp_ajax_askany_export_data', array( $this, 'export_data' ));
		add_action('wp_ajax_askany_import_data', array( $this, 'import_data' ));
		add_action('wp_ajax_askany_filter_data', array( $this, 'filter_data' ));
		add_action('wp_ajax_askany_get_database_stats', array( $this, 'get_database_stats' ));
		add_action('wp_ajax_askany_clear_chat_logs', array( $this, 'clear_chat_logs' ));
		
		// Feature request and bug report AJAX handlers
		add_action('wp_ajax_askany_save_feature_request', array( $this, 'save_feature_request' ));
		add_action('wp_ajax_nopriv_askany_save_feature_request', array( $this, 'save_feature_request' ));
		add_action('wp_ajax_askany_save_bug_report', array( $this, 'save_bug_report' ));
		add_action('wp_ajax_nopriv_askany_save_bug_report', array( $this, 'save_bug_report' ));
		add_action('wp_ajax_askany_get_feature_requests', array( $this, 'get_feature_requests' ));
		add_action('wp_ajax_askany_get_bug_reports', array( $this, 'get_bug_reports' ));
		add_action('wp_ajax_askany_update_feature_request_status', array( $this, 'update_feature_request_status' ));
		add_action('wp_ajax_askany_update_bug_report_status', array( $this, 'update_bug_report_status' ));
		add_action('wp_ajax_askany_get_feature_request_details', array( $this, 'get_feature_request_details' ));
		add_action('wp_ajax_askany_get_bug_report_details', array( $this, 'get_bug_report_details' ));
		add_action('wp_ajax_askany_delete_feature_request', array( $this, 'delete_feature_request' ));
		add_action('wp_ajax_askany_delete_bug_report', array( $this, 'delete_bug_report' ));
		add_action('wp_ajax_askany_check_status', array( $this, 'check_status' ));
		add_action('wp_ajax_nopriv_askany_check_status', array( $this, 'check_status' ));
		
		// File attachment AJAX handlers
		add_action('wp_ajax_askany_download_attachment', array( $this, 'download_attachment' ));
		add_action('wp_ajax_askany_get_session_attachments', array( $this, 'get_session_attachments' ));
		add_action('wp_ajax_askany_delete_attachment', array( $this, 'delete_attachment' ));
		
		// Admin file upload AJAX handler
		add_action('wp_ajax_askany_admin_upload_file', array( $this, 'admin_upload_file' ));
		
		// AI review message AJAX handler
		add_action('wp_ajax_askany_ai_review_message', array( $this, 'ai_review_message' ));

		// Custom Database Table Test & Index AJAX handlers
		add_action('wp_ajax_askany_test_custom_tables', array( $this, 'test_custom_tables' ));
		add_action('wp_ajax_askany_index_custom_table', array( $this, 'index_custom_table' ));

		// Addon Manager AJAX handlers
		add_action('wp_ajax_askany_install_addon', array( $this, 'install_addon' ));
		add_action('wp_ajax_askany_activate_addon', array( $this, 'activate_addon' ));

		// Download transcript (free feature)
		add_action('wp_ajax_askany_download_transcript', array( $this, 'download_transcript' ));
		add_action('wp_ajax_nopriv_askany_download_transcript', array( $this, 'download_transcript' ));
	}

	/**
	 * Retrieve session secret from request data, POST, GET, or HTTP header.
	 *
	 * @param array $request_data Decrypted or processed request data.
	 * @return string
	 */
	private function get_request_session_secret( $request_data = array() ) {
		$secret = '';
		if ( ! empty( $request_data['session_secret'] ) && is_string( $request_data['session_secret'] ) ) {
			$secret = sanitize_text_field( wp_unslash( $request_data['session_secret'] ) );
		} elseif ( ! empty( $_POST['session_secret'] ) && is_string( $_POST['session_secret'] ) ) {
			$secret = sanitize_text_field( wp_unslash( $_POST['session_secret'] ) );
		} elseif ( ! empty( $_GET['session_secret'] ) && is_string( $_GET['session_secret'] ) ) {
			$secret = sanitize_text_field( wp_unslash( $_GET['session_secret'] ) );
		} elseif ( ! empty( $_SERVER['HTTP_X_ASKANY_SESSION_SECRET'] ) ) {
			$secret = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_ASKANY_SESSION_SECRET'] ) );
		}
		return substr( $secret, 0, 64 );
	}

	/**
	 * Maximum accepted chat message length (characters).
	 */
	const MAX_MESSAGE_LENGTH = 4000;

	/**
	 * Sanitize a visitor chat message: keep line breaks, strip tags, cap length.
	 *
	 * @param mixed $raw Raw (already unslashed or not) value.
	 * @return string
	 */
	private function sanitize_chat_message( $raw ) {
		if ( ! is_string( $raw ) ) {
			return '';
		}
		$message = sanitize_textarea_field( wp_unslash( $raw ) );
		if ( function_exists( 'mb_substr' ) ) {
			$message = mb_substr( $message, 0, self::MAX_MESSAGE_LENGTH );
		} else {
			$message = substr( $message, 0, self::MAX_MESSAGE_LENGTH );
		}
		return trim( $message );
	}

	/**
	 * Read + sanitize a session_id from request data. Returns '' when absent and
	 * false when present but malformed.
	 *
	 * @param array  $source Request data array.
	 * @param string $key    Field name.
	 * @return string|false
	 */
	private function read_session_id( $source, $key = 'session_id' ) {
		if ( ! isset( $source[ $key ] ) || '' === $source[ $key ] ) {
			return '';
		}
		if ( ! is_string( $source[ $key ] ) ) {
			return false;
		}
		$session_id = sanitize_text_field( wp_unslash( $source[ $key ] ) );
		return Database::is_valid_session_id( $session_id ) ? $session_id : false;
	}

	/**
	 * Send a JSON error for an invalid session id and stop.
	 *
	 * @return void
	 */
	private function send_invalid_session_error() {
		wp_send_json_error( array( 'message' => __( 'Invalid session ID.', 'askany' ), 'code' => 'invalid_session' ), 400 );
	}

	/**
	 * Remove internal diagnostics from a visitor-facing payload.
	 *
	 * @param mixed $response Response array.
	 * @return mixed
	 */
	private function strip_private_fields( $response ) {
		if ( is_array( $response ) && ! current_user_can( 'manage_options' ) ) {
			unset( $response['debug_info'], $response['debug'], $response['_debug'] );
		}
		return $response;
	}

	/**
	 * Save an AI reply unless an agent took the session over (or AI replies were
	 * globally disabled) while the reply was being generated.
	 *
	 * The session row is re-read right before saving.
	 *
	 * @param string $session_id Session ID.
	 * @param string $text       AI reply text.
	 * @return int Saved message id, or 0 when the reply was dropped.
	 */
	private function save_ai_reply_if_allowed( $session_id, $text ) {
		if ( get_option( 'askany_prevent_ai_response', false ) ) {
			return 0;
		}
		if ( $this->database->is_session_agent_handled( $session_id ) ) {
			return 0;
		}
		$id = $this->database->save_chat_message( $session_id, 'ai', null, $text );
		if ( $id ) {
			do_action( 'askany_after_message_saved', $session_id, 'ai', $text );
		}
		return (int) $id;
	}

	/**
	 * Standard payload for an AI reply that was dropped because of agent takeover.
	 *
	 * @param string $session_id      Session ID.
	 * @param string $session_secret  Session secret.
	 * @param int    $user_message_id Saved visitor message id.
	 * @return array
	 */
	private function ai_dropped_payload( $session_id, $session_secret, $user_message_id ) {
		return array(
			'message'         => '',
			'session_id'      => $session_id,
			'session_secret'  => $session_secret,
			'agent_handled'   => true,
			'ai_dropped'      => true,
			'user_message_id' => (int) $user_message_id,
			'ai_message_id'   => null,
		);
	}

	/**
	 * Mark the current admin as an online agent (throttled to once a minute).
	 *
	 * @return void
	 */
	private function touch_agent_presence() {
		$user_id = get_current_user_id();
		if ( ! $user_id || ! current_user_can( 'manage_options' ) ) {
			return;
		}
		$key = 'askany_agent_seen_' . $user_id;
		if ( get_transient( $key ) ) {
			return;
		}
		set_transient( $key, 1, MINUTE_IN_SECONDS );
		if ( $this->database->is_user_agent( $user_id ) ) {
			$this->database->update_agent_status( $user_id, true );
		}
	}

	/**
	 * Clear scheduled cron events registered by this class (call on deactivation).
	 *
	 * @return void
	 */
	public static function clear_cron() {
		wp_clear_scheduled_hook( 'askany_cron_cleanup' );
	}

	/**
	 * AJAX: return fresh chat + REST nonces for the current (cookie) user.
	 *
	 * Used by the widget to recover from a stale localized nonce on cached pages.
	 *
	 * @return void
	 */
	public function get_fresh_nonce() {
		nocache_headers();
		wp_send_json( array(
			'nonce'      => wp_create_nonce( 'askany_chat_nonce' ),
			'rest_nonce' => wp_create_nonce( 'wp_rest' ),
		) );
	}

	/**
	 * AJAX: visitor download of a chat attachment (owner of the session only).
	 *
	 * @return void
	 */
	public function visitor_download_attachment() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		$attachment_id = isset( $_REQUEST['attachment_id'] ) ? absint( $_REQUEST['attachment_id'] ) : 0;
		$session_id    = $this->read_session_id( $_REQUEST );
		if ( ! $attachment_id || ! $session_id ) {
			wp_send_json_error( array( 'message' => __( 'Invalid request.', 'askany' ) ), 400 );
		}

		$file_handler = $this->plugin->get_file_attachment_handler();
		$result       = $file_handler->handle_visitor_download( $attachment_id, $session_id, $this->get_request_session_secret() );

		if ( is_wp_error( $result ) ) {
			$data = $result->get_error_data();
			wp_send_json_error( array( 'message' => $result->get_error_message() ), isset( $data['status'] ) ? (int) $data['status'] : 403 );
		}
		exit;
	}

	/**
	 * Register REST API routes.
	 *
	 * This method registers the REST API endpoint for chat interactions.
	 *
	 * @return void
	 */
	public function register_rest_routes() {
		register_rest_route('askany/v1', '/chat', array(
			'methods' => 'POST',
			'callback' => array( $this, 'rest_chat_response' ),
			'permission_callback' => '__return_true',
		));

		register_rest_route('askany/v1', '/nonce', array(
			'methods' => 'GET',
			'callback' => function() {
				nocache_headers();
				return array(
					'nonce'      => wp_create_nonce('askany_chat_nonce'),
					'rest_nonce' => wp_create_nonce('wp_rest'),
				);
			},
			'permission_callback' => '__return_true',
		));

		// ── Mobile App REST endpoints (Application Password auth) ────────────

		// GET /askany/v1/sessions — list all chat sessions
		register_rest_route('askany/v1', '/sessions', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'rest_get_sessions' ),
			'permission_callback' => array( $this, 'rest_admin_permission' ),
		));

		// GET /askany/v1/sessions/{session_id}/messages
		register_rest_route('askany/v1', '/sessions/(?P<session_id>[^/]+)/messages', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'rest_get_session_messages' ),
			'permission_callback' => array( $this, 'rest_admin_permission' ),
		));

		// POST /askany/v1/sessions/{session_id}/messages — send agent message
		register_rest_route('askany/v1', '/sessions/(?P<session_id>[^/]+)/messages', array(
			'methods'             => 'POST',
			'callback'            => array( $this, 'rest_send_agent_message' ),
			'permission_callback' => array( $this, 'rest_admin_permission' ),
		));

		// POST /askany/v1/sessions/{session_id}/assign — take over as agent
		register_rest_route('askany/v1', '/sessions/(?P<session_id>[^/]+)/assign', array(
			'methods'             => 'POST',
			'callback'            => array( $this, 'rest_assign_agent' ),
			'permission_callback' => array( $this, 'rest_admin_permission' ),
		));

		// POST /askany/v1/sessions/{session_id}/release — release back to AI
		register_rest_route('askany/v1', '/sessions/(?P<session_id>[^/]+)/release', array(
			'methods'             => 'POST',
			'callback'            => array( $this, 'rest_release_agent' ),
			'permission_callback' => array( $this, 'rest_admin_permission' ),
		));

		// POST /askany/v1/sessions/{session_id}/read — mark session read
		register_rest_route('askany/v1', '/sessions/(?P<session_id>[^/]+)/read', array(
			'methods'             => 'POST',
			'callback'            => array( $this, 'rest_mark_read' ),
			'permission_callback' => array( $this, 'rest_admin_permission' ),
		));

		// GET /askany/v1/chat/history — visitor chat history (no auth)
		register_rest_route('askany/v1', '/chat/history', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'rest_get_chat_history' ),
			'permission_callback' => '__return_true',
		));

		// GET /askany/v1/chat/poll?session_id=...&since=... — visitor realtime poll (no auth)
		register_rest_route('askany/v1', '/chat/poll', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'rest_visitor_poll' ),
			'permission_callback' => '__return_true',
		));

		// GET /askany/v1/sessions/{session_id}/poll?since=... — realtime poll (admin auth)
		register_rest_route('askany/v1', '/sessions/(?P<session_id>[^/]+)/poll', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'rest_poll_session' ),
			'permission_callback' => array( $this, 'rest_admin_permission' ),
		));
	}

	/**
	 * Permission callback: requires manage_options (admin/Application Password).
	 */
	public function rest_admin_permission() {
		return current_user_can('manage_options');
	}

	/**
	 * Free plan: only the N most recent active sessions can be opened (mirrors the
	 * live chat UI lock: sessions are ordered by last_activity DESC, index > 3 locked).
	 *
	 * @param string $session_id Session ID.
	 * @return bool True when the session is locked for the Free plan.
	 */
	private function is_session_locked_for_free( $session_id ) {
		$plugin = \Askany\Plugin::get_instance();
		if ( $plugin && $plugin->is_pro_active() ) {
			return false;
		}

		$free_limit = 3;
		global $wpdb;
		$sessions_table = $this->database->get_sessions_table();
		$allowed = $wpdb->get_col( $wpdb->prepare(
			"SELECT session_id FROM {$sessions_table} WHERE status = 'active' ORDER BY last_activity DESC LIMIT %d",
			$free_limit
		) );

		return ! in_array( $session_id, (array) $allowed, true );
	}

	/**
	 * Stop with a "Pro feature" error unless AskAny Pro is active.
	 *
	 * @return void
	 */
	private function require_pro_feature() {
		$plugin = \Askany\Plugin::get_instance();
		if ( ! $plugin || ! $plugin->is_pro_active() ) {
			wp_send_json_error( array( 'message' => __( 'This is a Pro feature. Please upgrade to AskAny Pro.', 'askany' ), 'code' => 'pro_required' ) );
		}
	}

	/**
	 * Whether AskAny Pro is active.
	 *
	 * @return bool
	 */
	private function is_pro() {
		$plugin = \Askany\Plugin::get_instance();
		return $plugin && $plugin->is_pro_active();
	}

	/**
	 * Read + validate a session_id route/query param for REST endpoints.
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return string|\WP_Error
	 */
	private function rest_session_id( $request ) {
		$session_id = sanitize_text_field( (string) $request->get_param('session_id') );
		if ( empty( $session_id ) ) {
			return new \WP_Error( 'missing_session', 'session_id is required.', array( 'status' => 400 ) );
		}
		if ( ! Database::is_valid_session_id( $session_id ) ) {
			return new \WP_Error( 'invalid_session', 'Invalid session ID.', array( 'status' => 400 ) );
		}
		return $session_id;
	}

	/**
	 * REST: GET /askany/v1/sessions
	 */
	public function rest_get_sessions( \WP_REST_Request $request ) {
		$sessions = $this->database->get_active_chat_sessions_with_preview();
		if ( ! $sessions ) {
			return rest_ensure_response( array() );
		}

		$plugin  = \Askany\Plugin::get_instance();
		$is_pro  = $plugin && $plugin->is_pro_active();
		$index   = 0;
		foreach ( $sessions as $session ) {
			$index++;
			$session->locked = ( ! $is_pro && $index > 3 );
			if ( $session->locked ) {
				$session->last_message = '';
			}
		}
		return rest_ensure_response( array_values( $sessions ) );
	}

	/**
	 * REST: GET /askany/v1/sessions/{session_id}/messages
	 */
	public function rest_get_session_messages( \WP_REST_Request $request ) {
		$session_id = $this->rest_session_id( $request );
		if ( is_wp_error( $session_id ) ) {
			return $session_id;
		}
		if ( $this->is_session_locked_for_free( $session_id ) ) {
			return new \WP_Error( 'pro_required', __( 'Upgrade to Pro to open more than 3 live chat sessions.', 'askany' ), array( 'status' => 403 ) );
		}

		$session = $this->database->get_chat_session( $session_id );
		if ( ! $session ) {
			return new \WP_Error( 'not_found', 'Session not found.', array( 'status' => 404 ) );
		}

		$messages = $this->database->get_chat_messages( $session_id );
		if ( $messages ) {
			foreach ( $messages as $msg ) {
				$msg->message = $this->decrypt_message_for_admin( $msg->message );
			}
		}

		// Mark as read
		$this->database->mark_session_messages_as_read( $session_id );

		unset( $session->session_secret );

		return rest_ensure_response( array(
			'messages' => $this->database->format_messages_for_client( $messages, true ),
			'session'  => $session,
		) );
	}

	/**
	 * Save an agent message, auto-assigning the sending agent when the session has
	 * no agent yet (so the AI stops replying to the visitor).
	 *
	 * @param string $session_id Session ID.
	 * @param string $message    Message text.
	 * @param int    $user_id    Agent user ID.
	 * @return int|false Message id.
	 */
	private function save_agent_message_with_assignment( $session_id, $message, $user_id ) {
		$session = $this->database->get_chat_session( $session_id );
		if ( ! $session ) {
			return false;
		}

		if ( empty( $session->agent_id ) ) {
			$status = $this->database->assign_agent_to_session_status( $session_id, $user_id );
			if ( 'assigned' === $status ) {
				$this->post_agent_joined_message( $session_id, $user_id );
			}
		}

		return $this->database->save_chat_message( $session_id, 'agent', $user_id, $message );
	}

	/**
	 * Post the "agent joined" system message.
	 *
	 * @param string $session_id Session ID.
	 * @param int    $agent_id   Agent user ID.
	 * @return void
	 */
	private function post_agent_joined_message( $session_id, $agent_id ) {
		$agent    = get_user_by( 'id', $agent_id );
		$name     = $agent ? $agent->display_name : __( 'An agent', 'askany' );
		$template = get_option( 'askany_label_agent_joined', __( '👋 {name} has joined the chat and will assist you now. Your messages will be handled by our live support team.', 'askany' ) );
		$join_msg = str_replace( array( '{name}', '%s' ), $name, $template );
		$this->database->save_chat_message( $session_id, 'system', null, $join_msg );
	}

	/**
	 * Post the "AI is back" system message.
	 *
	 * @param string $session_id Session ID.
	 * @return void
	 */
	private function post_ai_takeover_message( $session_id ) {
		$takeover_msg = get_option( 'askany_label_ai_takeover', __( '🤖 You are now chatting with our AI assistant. Your messages will be handled automatically.', 'askany' ) );
		$this->database->save_chat_message( $session_id, 'system', null, $takeover_msg );
	}

	/**
	 * REST: POST /askany/v1/sessions/{session_id}/messages
	 */
	public function rest_send_agent_message( \WP_REST_Request $request ) {
		$session_id = $this->rest_session_id( $request );
		if ( is_wp_error( $session_id ) ) {
			return $session_id;
		}
		$message    = sanitize_textarea_field( (string) $request->get_param('message') );

		if ( empty( $message ) ) {
			return new \WP_Error( 'missing_message', 'Message is required.', array( 'status' => 400 ) );
		}

		$user_id = get_current_user_id();
		$result  = $this->save_agent_message_with_assignment( $session_id, $message, $user_id );

		if ( ! $result ) {
			return new \WP_Error( 'save_failed', 'Failed to save message.', array( 'status' => 500 ) );
		}

		do_action( 'askany_after_message_saved', $session_id, 'agent', $message );

		return rest_ensure_response( array( 'success' => true, 'message_id' => (int) $result ) );
	}

	/**
	 * REST: POST /askany/v1/sessions/{session_id}/assign
	 */
	public function rest_assign_agent( \WP_REST_Request $request ) {
		$session_id = $this->rest_session_id( $request );
		if ( is_wp_error( $session_id ) ) {
			return $session_id;
		}
		$agent_id   = intval( $request->get_param('agent_id') ) ?: get_current_user_id();

		$status = $this->database->assign_agent_to_session_status( $session_id, $agent_id );

		if ( 'assigned' === $status ) {
			$this->post_agent_joined_message( $session_id, $agent_id );
		}

		return rest_ensure_response( array(
			'success' => in_array( $status, array( 'assigned', 'unchanged' ), true ),
			'changed' => 'assigned' === $status,
			'status'  => $status,
		) );
	}

	/**
	 * REST: POST /askany/v1/sessions/{session_id}/release
	 */
	public function rest_release_agent( \WP_REST_Request $request ) {
		if ( get_option( 'askany_prevent_ai_response', false ) ) {
			return new \WP_Error( 'ai_disabled', __( 'Cannot release to AI: AI auto response is currently disabled in Home Page Settings.', 'askany' ), array( 'status' => 400 ) );
		}

		$session_id = $this->rest_session_id( $request );
		if ( is_wp_error( $session_id ) ) {
			return $session_id;
		}

		$changed = $this->database->release_agent_from_session_changed( $session_id );

		if ( $changed ) {
			$this->post_ai_takeover_message( $session_id );
		}

		return rest_ensure_response( array( 'success' => true, 'changed' => $changed ) );
	}

	/**
	 * REST: POST /askany/v1/sessions/{session_id}/read
	 */
	public function rest_mark_read( \WP_REST_Request $request ) {
		$session_id = $this->rest_session_id( $request );
		if ( is_wp_error( $session_id ) ) {
			return $session_id;
		}
		$this->database->mark_session_messages_as_read( $session_id );
		return rest_ensure_response( array( 'success' => true ) );
	}

	/**
	 * Session secret from a REST request (param or X-Askany-Session-Secret header).
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return string
	 */
	private function rest_session_secret( $request ) {
		$secret = $request->get_param('session_secret');
		if ( empty( $secret ) || ! is_string( $secret ) ) {
			$secret = (string) $request->get_header('x_askany_session_secret');
		}
		return substr( sanitize_text_field( $secret ), 0, 64 );
	}

	/**
	 * Poll helper for the REST poll endpoints.
	 *
	 * Prefers `last_message_id` (id-based). For backward compatibility with app
	 * clients, a `since` timestamp (site-local time, as stored) is still honoured,
	 * and with neither parameter the last 5 minutes (site-local) are returned.
	 *
	 * @param \WP_REST_Request $request    Request.
	 * @param string           $session_id Session ID.
	 * @param bool             $for_admin  Admin payload.
	 * @return array
	 */
	private function rest_poll_updates( $request, $session_id, $for_admin ) {
		$last_message_id = absint( $request->get_param('last_message_id') );
		if ( $last_message_id > 0 ) {
			return $this->database->get_realtime_updates( $session_id, null, $last_message_id, null, $for_admin );
		}

		$since = sanitize_text_field( (string) $request->get_param('since') );
		if ( '' === $since || ! strtotime( $since ) ) {
			$since = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - 5 * MINUTE_IN_SECONDS );
		}

		$updates = $this->database->get_realtime_updates( $session_id, null, PHP_INT_MAX, null, $for_admin );
		$messages = $this->database->get_new_messages_since( $session_id, $since );
		$updates['messages'] = $this->database->format_messages_for_client( $messages, $for_admin );
		$updates['last_message_id'] = 0;
		foreach ( $updates['messages'] as $m ) {
			$updates['last_message_id'] = max( $updates['last_message_id'], (int) $m['id'] );
		}
		return $updates;
	}

	/**
	 * REST: GET /askany/v1/chat/history?session_id=xxx
	 */
	public function rest_get_chat_history( \WP_REST_Request $request ) {
		nocache_headers();
		$session_id = $this->rest_session_id( $request );
		if ( is_wp_error( $session_id ) ) {
			return $session_id;
		}
		if ( ! $this->database->verify_session_access( $session_id, $this->rest_session_secret( $request ) ) ) {
			return new \WP_Error( 'session_invalid', 'Unauthorized session access.', array( 'status' => 403 ) );
		}
		$messages = $this->database->get_chat_messages( $session_id );
		return rest_ensure_response( $this->database->format_messages_for_client( $messages, current_user_can( 'manage_options' ) ) );
	}

	/**
	 * REST: GET /askany/v1/sessions/{session_id}/poll?last_message_id=N
	 * Returns new messages after the given message id (id-based; `since` is ignored).
	 * Uses Application Password auth — no nonce needed.
	 */
	public function rest_poll_session( \WP_REST_Request $request ) {
		$session_id = $this->rest_session_id( $request );
		if ( is_wp_error( $session_id ) ) {
			return $session_id;
		}
		if ( $this->is_session_locked_for_free( $session_id ) ) {
			return new \WP_Error( 'pro_required', __( 'Upgrade to Pro to open more than 3 live chat sessions.', 'askany' ), array( 'status' => 403 ) );
		}

		$this->touch_agent_presence();

		$updates = $this->rest_poll_updates( $request, $session_id, true );
		foreach ( $updates['messages'] as &$msg ) {
			$msg['message'] = $this->decrypt_message_for_admin( $msg['message'] );
		}
		unset( $msg );

		return rest_ensure_response( array(
			'messages'          => $updates['messages'],
			'typing'            => ! empty( $updates['typing_indicators'] ),
			'typing_indicators' => $updates['typing_indicators'],
			'last_message_id'   => $updates['last_message_id'],
			'session_info'      => $updates['session_info'],
		) );
	}

	/**
	 * REST: GET /askany/v1/chat/poll?session_id=...&last_message_id=N
	 * Public visitor poll — requires the visitor's session secret.
	 */
	public function rest_visitor_poll( \WP_REST_Request $request ) {
		nocache_headers();
		$session_id = $this->rest_session_id( $request );
		if ( is_wp_error( $session_id ) ) {
			return $session_id;
		}

		if ( ! $this->database->verify_session_access( $session_id, $this->rest_session_secret( $request ) ) ) {
			return new \WP_Error( 'session_invalid', 'Unauthorized session access.', array( 'status' => 403 ) );
		}

		$updates = $this->rest_poll_updates( $request, $session_id, false );

		return rest_ensure_response( array(
			'messages'          => $updates['messages'],
			'typing'            => ! empty( $updates['typing_indicators'] ),
			'typing_indicators' => $updates['typing_indicators'],
			'last_message_id'   => $updates['last_message_id'],
			'session_info'      => $updates['session_info'],
		) );
	}

	/**
	 * Handle chat response via REST API.
	 *
	 * This method processes the chat request and returns a response.
	 *
	 * @param \WP_REST_Request $request The REST request object.
	 * @return \WP_Error|\WP_REST_Response The response object or error.
	 */
	public function rest_chat_response( $request ) {
		nocache_headers();
		$params = $request->get_json_params();
		if ( ! is_array( $params ) ) {
			$params = $request->get_body_params();
		}
		if ( ! is_array( $params ) ) {
			$params = array();
		}

		// Process encrypted request data
		$request_data = EncryptionHandler::process_encrypted_request($params);

		$message = isset($request_data['query']) ? $this->sanitize_chat_message( $request_data['query'] ) : '';
		$session_id = $this->read_session_id( $request_data );
		$is_encrypted = isset($params['encrypted']) && filter_var( $params['encrypted'], FILTER_VALIDATE_BOOLEAN );

		if ( false === $session_id ) {
			return new \WP_Error( 'invalid_session', __( 'Invalid session ID.', 'askany' ), array( 'status' => 400 ) );
		}

		// Log encryption status for debugging
		if ($is_encrypted) {
			EncryptionHandler::log_encryption_event('REST_REQUEST', 'Received encrypted chat request', array(
				'session_id' => substr($session_id, 0, 10) . '...',
				'message_length' => strlen($message)
			));
		}

		if ( empty($message) ) {
			return new \WP_Error('empty_message', __('Please enter a message.', 'askany'), array( 'status' => 400 ));
		}

		// Get current user info (recognised when the client sends X-WP-Nonce)
		$current_user_id = get_current_user_id();

		// Ensure we have a valid session ID
		if (empty($session_id)) {
			$session_id = 'user_' . $current_user_id . '_' . wp_generate_password( 16, false );
		}

		$session_secret = $this->get_request_session_secret( $request_data );
		if ( '' === $session_secret ) {
			$header_secret  = $request->get_header('x_askany_session_secret');
			$session_secret = $header_secret ? substr( sanitize_text_field( $header_secret ), 0, 64 ) : '';
		}

		// Get user data for session creation
		$user_data = array();
		if ( isset($params['user_name']) && is_string( $params['user_name'] ) ) {
			$user_data['name'] = sanitize_text_field($params['user_name']);
		}
		if ( isset($params['user_email']) && is_string( $params['user_email'] ) ) {
			$user_data['email'] = sanitize_email($params['user_email']);
		}

		// Check if session already exists before creating or updating
		$existing_session = $this->database->get_chat_session($session_id);

		if ( $existing_session && ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
			return new \WP_Error('session_invalid', 'Unauthorized session access.', array( 'status' => 403 ));
		}

		if ( ! RateLimiter::flood_guard( $session_id, 'chat' ) ) {
			return new \WP_Error( 'rate_limited', __( 'You are sending messages too quickly. Please wait a moment.', 'askany' ), array( 'status' => 429 ) );
		}

		if ( ! $existing_session ) {
			// New session: keep the secret this browser already holds (request or HttpOnly cookie).
			if ( strlen( (string) $session_secret ) < 16 ) {
				$session_secret = '' !== \Askany\Database::cookie_session_secret( $session_id )
					? \Askany\Database::cookie_session_secret( $session_id )
					: wp_generate_password( 32, false );
			}
		} elseif ( empty( $session_secret ) ) {
			$session_secret = ( isset( $session_id ) && is_string( $session_id ) && '' !== \Askany\Database::cookie_session_secret( $session_id ) )
				? \Askany\Database::cookie_session_secret( $session_id )
				: wp_generate_password( 32, false );
		}

		$msg_id = isset($request_data['msg_id']) && is_string( $request_data['msg_id'] ) ? sanitize_text_field( wp_unslash($request_data['msg_id']) ) : '';
		$transient_key = '';
		if ( ! empty( $msg_id ) ) {
			// Idempotency cache scoped to verified session
			$transient_key = 'askany_msg_' . md5( $session_id . '_' . $msg_id );
			$cached = get_transient( $transient_key );
			if ( false !== $cached && $existing_session ) {
				return rest_ensure_response( $cached );
			}
		}

		if (!$existing_session) {
			// Create brand new session
			$created_id = $this->database->create_chat_session($session_id, $user_data, $session_secret);
			if ( false === $created_id ) {
				return new \WP_Error('session_invalid', 'Unauthorized session access.', array( 'status' => 403 ));
			}
		} else {
			// Update last activity (re-activate archived sessions) without changing agent assignment
			global $wpdb;
			$wpdb->update(
				$wpdb->prefix . 'askany_chat_sessions',
				array( 'last_activity' => current_time('mysql'), 'status' => 'active' ),
				array('session_id' => $session_id),
				array( '%s', '%s' ),
				array('%s')
			);
		}

		// Save user message (should be decrypted at this point)
		$user_message_id = (int) $this->database->save_chat_message($session_id, 'user', $current_user_id, $message);

		// Notify admin devices that a user sent a message
		do_action( 'askany_after_message_saved', $session_id, 'user', $message );

		$base = array(
			'session_id'      => $session_id,
			'session_secret'  => $session_secret,
			'user_message_id' => $user_message_id,
			'ai_message_id'   => null,
		);

		// Check if AI auto response is globally disabled or if session is handled by an agent
		if ( get_option( 'askany_prevent_ai_response', false ) ) {
			return array_merge( $base, array(
				'message'             => '',
				'agent_handled'       => true,
				'prevent_ai_response' => true,
			) );
		}

		if ( $this->database->is_session_agent_handled( $session_id ) ) {
			return array_merge( $base, array(
				'message' => get_option( 'askany_label_agent_pending', __( 'Your message has been received by our support team. An agent will respond shortly.', 'askany' ) ),
				'agent_handled' => true,
			) );
		}

		// ── Rate limiting check (message kept, AI reply withheld) ──
		$rate_check = \Askany\RateLimiter::check( $current_user_id );
		if ( ! $rate_check['allowed'] ) {
			if ( ! empty( $rate_check['use_fallback'] ) && get_option( 'askany_fallback_enabled', false ) ) {
				$fallback         = $this->api_manager->get_fallback_response_public( $message );
				$fallback_message = is_array( $fallback ) ? $fallback['text'] : $fallback;
				$ai_message_id    = $this->save_ai_reply_if_allowed( $session_id, $fallback_message );
				return array_merge( $base, array(
					'message'       => $ai_message_id ? $fallback_message : '',
					'ai_message_id' => $ai_message_id ?: null,
				) );
			}
			return new \WP_Error( 'rate_limited', $rate_check['message'], array( 'status' => 429 ) );
		}

		// Get settings (client provider/model overrides are ignored)
		$settings = $this->api_manager->get_settings();

		// Check if API is enabled.
		if ( ! $settings['api_enabled'] ) {
			return new \WP_Error('api_disabled', get_option( 'askany_label_api_disabled', __( 'Chat API is currently disabled. Please contact the administrator.', 'askany' ) ), array( 'status' => 403 ));
		}

		// Get response from API.
		$response = $this->api_manager->get_response($message, $session_id);

		// If API fails or returns empty message, use fallback or error.
		$has_valid_message = is_array($response) && '' !== trim( (string) ( $response['message'] ?? '' ) );
		if ( ! $has_valid_message && $settings['fallback_enabled'] ) {
			$response = array(
				'message' => get_option( 'askany_label_ai_unavailable', __( 'I apologize, but I am currently unable to process your request. Please try again later.', 'askany' ) ),
				'related_products' => array(),
				'session_id' => $session_id,
			);
		} elseif ( ! $has_valid_message ) {
			return new \WP_Error('api_error', __('Sorry, I could not process your request.', 'askany'), array( 'status' => 500 ));
		}

		// Save AI response (re-checks for agent takeover first)
		$ai_message_id = $this->save_ai_reply_if_allowed( $session_id, $response['message'] );
		if ( ! $ai_message_id ) {
			return $this->ai_dropped_payload( $session_id, $session_secret, $user_message_id );
		}

		// Store chat history in old format for backward compatibility
		$this->log_chat_history($message, $response['message']);

		$response = $this->strip_private_fields( $response );
		$response['session_id']      = $session_id;
		$response['user_message_id'] = $user_message_id;
		$response['ai_message_id']   = $ai_message_id;

		// Process response for encryption if needed
		if ($is_encrypted) {
			$response = EncryptionHandler::process_encrypted_response($response, $params);
			EncryptionHandler::log_encryption_event('REST_RESPONSE', 'Sending encrypted chat response', array(
				'session_id' => substr($session_id, 0, 10) . '...',
				'response_length' => strlen($response['message'])
			));
		}

		$response['session_secret'] = $session_secret;

		if ( $transient_key ) {
			set_transient( $transient_key, $response, 60 );
		}

		return $response;
	}

	/**
	 * Handle report issue or feature request conversation.
	 *
	 * @param string $message The user's message.
	 * @param string $context The conversation context (report_issue or feature_request).
	 * @param array  $state The current conversation state.
	 * @param string $session_id The session ID.
	 *
	 * @return array Response data with message and submission status.
	 */
	private function handle_report_feature_conversation( $message, $context, $state, $session_id ) {
		// ('=== handle_report_feature_conversation ===');
		// ('Message: ' . $message);
		// ('Context: ' . $context);
		// ('State: ' . print_r($state, true));
		// ('Session ID: ' . $session_id);
		
		// Check if user is confirming submission
		$message_lower = strtolower(trim($message));
		// ('Message lower: ' . $message_lower);
		// ('State step: ' . (isset($state['step']) ? $state['step'] : 'NOT SET'));
		
		if ( in_array($message_lower, array('confirm', 'submit', 'yes')) && isset($state['step']) && $state['step'] === 'confirmed' ) {
			// ('User is confirming submission!');
			
			// User has confirmed, submit to database
			$user_name = isset($state['userName']) ? sanitize_text_field($state['userName']) : '';
			$user_email = isset($state['userEmail']) ? sanitize_email($state['userEmail']) : '';
			$details = isset($state['details']) ? sanitize_textarea_field($state['details']) : '';
			
			// ('Extracted data - Name: ' . $user_name . ', Email: ' . $user_email . ', Details: ' . $details);
			
			// Validate data
			if ( empty($user_name) || empty($user_email) || empty($details) ) {
				// ('Validation failed - missing fields');
				return array(
					'message' => __('Please fill out all required fields in the form before confirming.', 'askany'),
					'submitted' => false
				);
			}
			
			// Validate email
			if ( ! is_email($user_email) ) {
				// ('Validation failed - invalid email');
				return array(
					'message' => __('Please enter a valid email address.', 'askany'),
					'submitted' => false
				);
			}
			
			// Submit to database
			if ( $context === 'report_issue' ) {
				// ('Saving bug report to database...');
				$result = $this->database->save_bug_report(
					$session_id,
					$user_name,
					$user_email,
					'Bug Report', // Default title
					$details,
					'open',
					'medium'
				);
				
				// ('Bug report save result: ' . ($result ? 'SUCCESS' : 'FAILED'));
				
				if ( $result ) {
					return array(
						'message' => sprintf(
							__('✅ Thank you, %s! Your bug report has been submitted successfully. We\'ll review it and get back to you at %s.', 'askany'),
							$user_name,
							$user_email
						),
						'submitted' => true
					);
				} else {
					return array(
						'message' => __('❌ Sorry, there was an error submitting your report. Please try again later.', 'askany'),
						'submitted' => false
					);
				}
			} else if ( $context === 'feature_request' ) {
				// ('Saving feature request to database...');
				$result = $this->database->save_feature_request(
					$session_id,
					$user_name,
					$user_email,
					'Feature Request', // Default title
					$details,
					'open',
					'medium'
				);
				
				// ('Feature request save result: ' . ($result ? 'SUCCESS' : 'FAILED'));
				
				if ( $result ) {
					return array(
						'message' => sprintf(
							__('✅ Thank you, %s! Your feature request has been submitted successfully. We\'ll consider it for future updates and contact you at %s.', 'askany'),
							$user_name,
							$user_email
						),
						'submitted' => true
					);
				} else {
					return array(
						'message' => __('❌ Sorry, there was an error submitting your request. Please try again later.', 'askany'),
						'submitted' => false
					);
				}
			}
		}
		
		// ('Not confirming - returning instruction message');
		// If not confirming, just acknowledge the message
		return array(
			'message' => __('Please fill out the form above and type "confirm" or "submit" to send your report.', 'askany'),
			'submitted' => false
		);
	}

	/**
	 * Extract user information from message.
	 *
	 * @param string $message The user's message.
	 *
	 * @return array Extracted information.
	 */
	private function extract_user_info( $message ) {
		$info = array(
			'name' => null,
			'email' => null,
			'details' => $message,
		);

		// Extract email using regex
		if ( preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $email_match) ) {
			$info['email'] = $email_match[0];
			// Remove email from details
			$info['details'] = trim(str_replace($email_match[0], '', $message));
		}

		// Extract name (capitalized words, typically 2-4 words)
		if ( preg_match('/\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3})\b/', $message, $name_match) ) {
			// Verify it's not a common word
			$common_words = array('Please', 'Thanks', 'Hello', 'Issue', 'Feature', 'Request', 'Report', 'Bug');
			if ( ! in_array($name_match[1], $common_words) ) {
				$info['name'] = $name_match[1];
				// Remove name from details
				$info['details'] = trim(str_replace($name_match[1], '', $info['details']));
			}
		}

		// Clean up details
		$info['details'] = preg_replace('/\s+/', ' ', $info['details']);
		$info['details'] = trim($info['details'], ' ,-');

		return $info;
	}

	/**
	 * Generate response for collecting missing information.
	 *
	 * @param array  $missing Missing information fields.
	 * @param array  $state Current conversation state.
	 * @param string $context Conversation context.
	 *
	 * @return string Response message.
	 */
	private function generate_collection_response( $missing, $state, $context ) {
		$responses = array();

		// Acknowledge what we received
		if ( ! empty($state['userName']) ) {
			$responses[] = sprintf(__('Thanks %s! 👍', 'askany'), $state['userName']);
		} elseif ( ! empty($state['userEmail']) ) {
			$responses[] = __('Got your email! 📧', 'askany');
		} elseif ( ! empty($state['details']) ) {
			$responses[] = __('Thanks for sharing that! 📝', 'askany');
		}

		// Ask for missing information
		if ( count($missing) === 3 ) {
			// Nothing received yet, ask for everything
			if ( $context === 'report_issue' ) {
				$responses[] = __('To help us track and fix this issue, please share:', 'askany');
			} else {
				$responses[] = __('To consider your feature request, please share:', 'askany');
			}
			$responses[] = __('1️⃣ Your name', 'askany');
			$responses[] = __('2️⃣ Your email address', 'askany');
			if ( $context === 'report_issue' ) {
				$responses[] = __('3️⃣ Details about the bug/issue', 'askany');
			} else {
				$responses[] = __('3️⃣ Details about the feature you\'d like', 'askany');
			}
		} elseif ( in_array('name', $missing) && in_array('email', $missing) ) {
			// Missing name and email
			$responses[] = __('Could you please share your name and email address?', 'askany');
		} elseif ( in_array('name', $missing) ) {
			// Missing only name
			$responses[] = __('What\'s your name?', 'askany');
		} elseif ( in_array('email', $missing) ) {
			// Missing only email
			$responses[] = __('What\'s your email address?', 'askany');
		} elseif ( in_array('details', $missing) ) {
			// Missing only details
			if ( $context === 'report_issue' ) {
				$responses[] = __('Please describe the issue you\'re experiencing.', 'askany');
			} else {
				$responses[] = __('Please describe the feature you\'d like to see.', 'askany');
			}
		}

		return implode("\n\n", $responses);
	}

	/**
	 * Generate confirmation response.
	 *
	 * @param array  $state Current conversation state.
	 * @param string $context Conversation context.
	 *
	 * @return string Response message.
	 */
	private function generate_confirmation_response( $state, $context ) {
		$type = $context === 'report_issue' ? __('bug report', 'askany') : __('feature request', 'askany');

		$response = sprintf(
			__('Perfect! Let me confirm your %s:', 'askany'),
			$type
		);
		$response .= "\n\n";
		$response .= sprintf(__('👤 Name: %s', 'askany'), $state['userName']) . "\n";
		$response .= sprintf(__('📧 Email: %s', 'askany'), $state['userEmail']) . "\n";
		if ( $context === 'report_issue' ) {
			$response .= sprintf(__('🐛 Issue: %s', 'askany'), $state['details']) . "\n";
		} else {
			$response .= sprintf(__('✨ Feature: %s', 'askany'), $state['details']) . "\n";
		}
		$response .= "\n";
		$response .= __('Is this correct? Reply with "yes" or "confirm" to submit, or provide any corrections.', 'askany');

		return $response;
	}


	/**
	 * Handle chat response via AJAX.
	 *
	 * This method processes the chat request and returns a response.
	 *
	 * @return void
	 */
	public function chat_response() {
		nocache_headers();
		check_ajax_referer('askany_chat_nonce', 'nonce');

		// Process encrypted request data
		$request_data = EncryptionHandler::process_encrypted_request($_POST);

		$message = isset($request_data['message']) ? $this->sanitize_chat_message( $request_data['message'] ) : '';
		$session_id = $this->read_session_id( $request_data );
		$is_encrypted = isset($_POST['encrypted']) && filter_var( wp_unslash( $_POST['encrypted'] ), FILTER_VALIDATE_BOOLEAN );

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
			return;
		}

		// Log encryption status for debugging
		if ($is_encrypted) {
			EncryptionHandler::log_encryption_event('AJAX_REQUEST', 'Received encrypted chat request', array(
				'session_id' => substr($session_id, 0, 10) . '...',
				'message_length' => strlen($message)
			));
		}

		if ( empty($message) ) {
			$error_response = array('message' => __('Please enter a message.', 'askany'));

			if ($is_encrypted) {
				$error_response = EncryptionHandler::process_encrypted_response($error_response, $_POST);
			}

			wp_send_json_error($error_response);
		}

		// Get current user info
		$current_user_id = get_current_user_id();

		// Ensure we have a valid session ID
		if (empty($session_id)) {
			$session_id = 'user_' . $current_user_id . '_' . wp_generate_password( 16, false );
		}

		$session_secret = $this->get_request_session_secret($request_data);

		// Get user data for session creation
		$user_data = array();
		if ( isset($_POST['user_name']) && is_string( $_POST['user_name'] ) ) {
			$user_data['name'] = sanitize_text_field( wp_unslash( $_POST['user_name'] ) );
		}
		if ( isset($_POST['user_email']) && is_string( $_POST['user_email'] ) ) {
			$user_data['email'] = sanitize_email( wp_unslash( $_POST['user_email'] ) );
		}

		// Check if session already exists before creating or updating
		$existing_session = $this->database->get_chat_session($session_id);

		if ( $existing_session && ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
			return;
		}

		// Minimal flood guard for public endpoints
		if ( ! RateLimiter::flood_guard( $session_id, 'chat' ) ) {
			wp_send_json_error( array( 'message' => __( 'You are sending messages too quickly. Please wait a moment.', 'askany' ), 'rate_limit' => true ), 429 );
			return;
		}

		if ( ! $existing_session ) {
			// New session: keep the secret this browser already holds (request or HttpOnly cookie).
			if ( strlen( (string) $session_secret ) < 16 ) {
				$session_secret = '' !== \Askany\Database::cookie_session_secret( $session_id )
					? \Askany\Database::cookie_session_secret( $session_id )
					: wp_generate_password( 32, false );
			}
		} elseif ( empty( $session_secret ) ) {
			$session_secret = ( isset( $session_id ) && is_string( $session_id ) && '' !== \Askany\Database::cookie_session_secret( $session_id ) )
				? \Askany\Database::cookie_session_secret( $session_id )
				: wp_generate_password( 32, false );
		}

		// For logged in users always use their WP display name
		if ( $current_user_id > 0 ) {
			$wp_user = get_userdata( $current_user_id );
			if ( $wp_user ) {
				$user_data['name']  = $wp_user->display_name ?: $wp_user->user_login;
				$user_data['email'] = $wp_user->user_email;
			}
		}

		if (!$existing_session) {
			// Create brand new session
			$created_id = $this->database->create_chat_session($session_id, $user_data, $session_secret);
			if ( false === $created_id ) {
				wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
				return;
			}
		} else {
			// Update last activity (re-activate archived sessions); also update user_name/email when appropriate
			global $wpdb;
			$update_data = array(
				'last_activity' => current_time('mysql'),
				'status'        => 'active',
			);

			if ( ! empty( $user_data['name'] ) ) {
				$current_name = $existing_session->user_name ?? '';
				// Always update for logged-in users; for anon, only update if current name is a placeholder or empty
				$is_anon_placeholder = ( strpos( $current_name, 'Anonymous_' ) === 0 || strpos( $current_name, 'anon_' ) === 0 );
				if ( $current_user_id > 0 || empty( $current_name ) || $is_anon_placeholder ) {
					$update_data['user_name'] = $user_data['name'];
				}
			}
			if ( ! empty( $user_data['email'] ) && empty( $existing_session->user_email ) ) {
				$update_data['user_email'] = $user_data['email'];
			}
			$wpdb->update(
				$wpdb->prefix . 'askany_chat_sessions',
				$update_data,
				array('session_id' => $session_id),
				null,
				array('%s')
			);
		}

		// Save user message (always, so agents see it)
		$user_message_id = (int) $this->database->save_chat_message($session_id, 'user', $current_user_id, $message);

		// Fire hook for push notifications
		do_action('askany_after_message_saved', $session_id, 'user', $message);

		// Check if AI auto response is globally disabled or if session is handled by an agent
		$prevent_ai = (bool) get_option( 'askany_prevent_ai_response', false );

		if ( $prevent_ai ) {
			// Globally disabled: user message is saved, no AI response generated
			wp_send_json_success( array(
				'message'             => '',
				'session_id'          => $session_id,
				'session_secret'      => $session_secret,
				'agent_handled'       => true,
				'prevent_ai_response' => true,
				'user_message_id'     => $user_message_id,
				'ai_message_id'       => null,
			) );
			return;
		}

		if ( $this->database->is_session_agent_handled( $session_id ) ) {
			// Session is handled by an agent: no AI response, and no rate limiting
			wp_send_json_success(array(
				'message' => get_option( 'askany_label_agent_pending', __( 'Your message has been received by our support team. An agent will respond shortly.', 'askany' ) ),
				'session_id' => $session_id,
				'session_secret' => $session_secret,
				'agent_handled' => true,
				'user_message_id' => $user_message_id,
				'ai_message_id' => null,
			));
			return;
		}

		// ── Rate limiting check (message is already saved; only the AI reply is withheld) ──
		$rate_check = \Askany\RateLimiter::check( $current_user_id );
		if ( ! $rate_check['allowed'] ) {
			if ( ! empty( $rate_check['use_fallback'] ) && get_option( 'askany_fallback_enabled', false ) ) {
				// Use fallback response instead of showing error
				$fallback = $this->api_manager->get_fallback_response_public( $message );
				$fallback_message = is_array( $fallback ) ? $fallback['text'] : $fallback;
				$ai_message_id = $this->save_ai_reply_if_allowed( $session_id, $fallback_message );
				wp_send_json_success( array(
					'message'         => $ai_message_id ? $fallback_message : '',
					'session_id'      => $session_id,
					'session_secret'  => $session_secret,
					'user_message_id' => $user_message_id,
					'ai_message_id'   => $ai_message_id ?: null,
				) );
				return;
			}
			wp_send_json_error( array(
				'message'         => $rate_check['message'],
				'rate_limit'      => true,
				'user_message_id' => $user_message_id,
			) );
			return;
		}
		// ────────────────────────────────────────────────────────────────────

		// Check if user is in report_issue or feature_request mode
		$conversation_context = isset($_POST['conversation_context']) && is_string( $_POST['conversation_context'] ) ? sanitize_text_field( wp_unslash( $_POST['conversation_context'] ) ) : '';
		$conversation_state = isset($_POST['conversation_state']) && is_string( $_POST['conversation_state'] ) ? json_decode( wp_unslash( $_POST['conversation_state'] ), true ) : array();
		if ( ! is_array( $conversation_state ) ) {
			$conversation_state = array();
		}

		if ( in_array($conversation_context, array('report_issue', 'feature_request'), true) ) {
			// Handle report issue or feature request conversation
			$response_data = $this->handle_report_feature_conversation($message, $conversation_context, $conversation_state, $session_id);

			// Extract message and submitted status
			$response_message = is_array($response_data) ? $response_data['message'] : $response_data;
			$submitted = is_array($response_data) && isset($response_data['submitted']) ? $response_data['submitted'] : false;

			// Save AI response (unless an agent took over meanwhile)
			$ai_message_id = $this->save_ai_reply_if_allowed( $session_id, $response_message );
			if ( ! $ai_message_id ) {
				wp_send_json_success( $this->ai_dropped_payload( $session_id, $session_secret, $user_message_id ) );
				return;
			}

			wp_send_json_success(array(
				'message' => $response_message,
				'session_id' => $session_id,
				'session_secret' => $session_secret,
				'conversation_mode' => true,
				'submitted' => $submitted,
				'user_message_id' => $user_message_id,
				'ai_message_id' => $ai_message_id,
			));
			return;
		}

		// Get settings. Client-supplied provider/model overrides are ignored:
		// the configured provider and model are always used.
		$settings = $this->api_manager->get_settings();

		// Check if API is enabled.
		if ( ! $settings['api_enabled'] ) {
			wp_send_json_error(array(
				'message' => get_option( 'askany_label_api_disabled', __( 'Chat API is currently disabled. Please contact the administrator.', 'askany' ) ),
				'user_message_id' => $user_message_id,
			));
		}

		// Get response from API.
		$response = $this->api_manager->get_response($message, $session_id);

		// If API fails and fallback is enabled, use fallback response.
		$has_valid_message = is_array( $response ) && '' !== trim( (string) ( $response['message'] ?? '' ) );
		if ( ! $has_valid_message && $settings['fallback_enabled'] ) {
			$response = array(
				'message' => get_option( 'askany_label_ai_unavailable', __( 'I apologize, but I am currently unable to process your request. Please try again later.', 'askany' ) ),
				'related_products' => array(),
				'session_id' => $session_id,
			);
		} elseif ( ! $has_valid_message ) {
			wp_send_json_error(array(
				'message' => __('Sorry, I could not process your request at this time.', 'askany'),
				'user_message_id' => $user_message_id,
			));
		}

		// Save AI response — re-checks the session first and drops the reply if an agent took over.
		$ai_message_id = $this->save_ai_reply_if_allowed( $session_id, $response['message'] );
		if ( ! $ai_message_id ) {
			wp_send_json_success( $this->ai_dropped_payload( $session_id, $session_secret, $user_message_id ) );
			return;
		}

		// Store chat history in old format for backward compatibility
		$this->log_chat_history($message, $response['message']);

		$response = $this->strip_private_fields( $response );
		$response['session_id']      = $session_id;
		$response['user_message_id'] = $user_message_id;
		$response['ai_message_id']   = $ai_message_id;

		// Process response for encryption if needed
		if ($is_encrypted) {
			$response = EncryptionHandler::process_encrypted_response($response, $_POST);
			EncryptionHandler::log_encryption_event('AJAX_RESPONSE', 'Sending encrypted chat response', array(
				'session_id' => substr($session_id, 0, 10) . '...',
				'response_length' => strlen($response['message'])
			));
		}

		$response['session_secret'] = $session_secret;
		wp_send_json_success($response);
	}

	/**
	 * P4.7 — Streaming chat response via Server-Sent Events.
	 *
	 * Tokens arrive progressively — user sees first word in ~1s even if total
	 * generation takes 15s. Requires the active provider to implement
	 * send_chat_request_stream(). Falls back to the non-streaming path if
	 * streaming is disabled or cURL is unavailable.
	 *
	 * @return void
	 */
	public function chat_response_stream() {
		nocache_headers();
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		// Respect the streaming enabled toggle
		if ( ! get_option( 'askany_streaming_enabled', true ) ) {
			$this->chat_response();
			return;
		}

		// cURL required for streaming
		if ( ! function_exists( 'curl_init' ) ) {
			$this->chat_response();
			return;
		}

		// EventSource sends GET, fetch() sends POST: accept both.
		$request_data = \Askany\EncryptionHandler::process_encrypted_request( $_REQUEST );
		$message      = isset( $request_data['message'] ) ? $this->sanitize_chat_message( $request_data['message'] ) : '';
		$session_id   = $this->read_session_id( $request_data );

		if ( false === $session_id ) {
			$this->send_stream_event_and_exit( array( 'done' => true, 'error' => __( 'Invalid session ID.', 'askany' ), 'code' => 'invalid_session' ) );
		}

		if ( empty( $message ) ) {
			$this->send_stream_event_and_exit( array( 'done' => true, 'error' => __( 'Please enter a message.', 'askany' ) ) );
		}

		$session_secret = $this->get_request_session_secret( $request_data );

		$current_user_id = get_current_user_id();
		if ( empty( $session_id ) ) {
			$session_id = 'user_' . $current_user_id . '_' . wp_generate_password( 16, false );
		}

		// Session management
		$session = $this->database->get_chat_session( $session_id );
		if ( $session && ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
			$this->send_stream_event_and_exit( array( 'done' => true, 'error' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ) );
		}

		if ( ! RateLimiter::flood_guard( $session_id, 'chat' ) ) {
			$this->send_stream_event_and_exit( array( 'done' => true, 'error' => __( 'You are sending messages too quickly. Please wait a moment.', 'askany' ), 'rate_limit' => true ) );
		}

		if ( ! $session ) {
			// A new session keeps the secret this browser already holds, so the
			// widget's other requests (typing, polling) are authorised right away.
			if ( strlen( (string) $session_secret ) < 16 ) {
				$session_secret = '' !== \Askany\Database::cookie_session_secret( $session_id )
					? \Askany\Database::cookie_session_secret( $session_id )
					: wp_generate_password( 32, false );
			}
			$stream_user = array();
			if ( ! empty( $request_data['user_name'] ) && is_string( $request_data['user_name'] ) ) {
				$stream_user['name'] = sanitize_text_field( wp_unslash( $request_data['user_name'] ) );
			}
			if ( ! empty( $request_data['user_email'] ) && is_string( $request_data['user_email'] ) ) {
				$stream_user['email'] = sanitize_email( wp_unslash( $request_data['user_email'] ) );
			}
			$created_id = $this->database->create_chat_session( $session_id, $stream_user, $session_secret );
			if ( false === $created_id ) {
				$this->send_stream_event_and_exit( array( 'done' => true, 'error' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ) );
			}
		} elseif ( empty( $session_secret ) ) {
			$session_secret = ( isset( $session_id ) && is_string( $session_id ) && '' !== \Askany\Database::cookie_session_secret( $session_id ) )
				? \Askany\Database::cookie_session_secret( $session_id )
				: wp_generate_password( 32, false );
		}
		$user_message_id = (int) $this->database->save_chat_message( $session_id, 'user', $current_user_id, $message );
		do_action( 'askany_after_message_saved', $session_id, 'user', $message );

		$base_event = array(
			'session_id'      => $session_id,
			'session_secret'  => $session_secret,
			'user_message_id' => $user_message_id,
		);

		// Check if AI auto response is globally disabled or if session is handled by an agent
		$prevent_ai = (bool) get_option( 'askany_prevent_ai_response', false );
		if ( $prevent_ai || $this->database->is_session_agent_handled( $session_id ) ) {
			$this->send_stream_event_and_exit( array_merge( $base_event, array(
				'done'                => true,
				'agent_handled'       => true,
				'prevent_ai_response' => $prevent_ai,
				'message'             => $prevent_ai ? '' : get_option( 'askany_label_agent_pending', __( 'Your message has been received by our support team. An agent will respond shortly.', 'askany' ) ),
				'ai_message_id'       => null,
			) ) );
		}

		// Rate limiting (user message is kept; AI reply withheld)
		$rate_check = \Askany\RateLimiter::check( $current_user_id );
		if ( ! $rate_check['allowed'] ) {
			$this->send_stream_event_and_exit( array_merge( $base_event, array( 'done' => true, 'error' => $rate_check['message'], 'rate_limit' => true ) ) );
		}

		// Check API enabled
		$settings = $this->api_manager->get_settings();
		if ( ! $settings['api_enabled'] ) {
			$this->send_stream_event_and_exit( array_merge( $base_event, array( 'done' => true, 'error' => get_option( 'askany_label_api_disabled', __( 'Chat API is currently disabled.', 'askany' ) ) ) ) );
		}

		$this->start_event_stream();

		$full_response = '';
		$emit          = function( $token ) use ( &$full_response ) {
			if ( ! is_string( $token ) || '' === $token ) {
				return;
			}
			$full_response .= $token;
			echo 'data: ' . wp_json_encode( array( 'token' => $token ) ) . "\n\n";
			flush();
		};

		// Stream tokens from the provider; never let a provider error kill the stream.
		try {
			$this->api_manager->stream_response( $message, $session_id, $emit );
		} catch ( \Throwable $e ) {
			// Nothing (or only part) was streamed; the fallback below handles an empty reply.
			unset( $e );
		}

		// Provider without streaming support / stream failure: fall back to the
		// regular (non-streaming) pipeline and send the reply as one token.
		if ( '' === trim( $full_response ) ) {
			$response = null;
			try {
				$response = $this->api_manager->get_response( $message, $session_id );
			} catch ( \Throwable $e ) {
				$response = null;
			}
			if ( is_array( $response ) && '' !== trim( (string) ( $response['message'] ?? '' ) ) ) {
				$emit( (string) $response['message'] );
			} elseif ( ! empty( $settings['fallback_enabled'] ) ) {
				$emit( get_option( 'askany_label_ai_unavailable', __( 'I apologize, but I am currently unable to process your request. Please try again later.', 'askany' ) ) );
			}
		}

		$done = array_merge( $base_event, array( 'done' => true, 'ai_message_id' => null ) );

		if ( '' === trim( $full_response ) ) {
			$done['error'] = __( 'Sorry, I could not process your request at this time.', 'askany' );
		} else {
			// Re-check the session right before saving: drop the reply if an agent took over.
			$ai_message_id = $this->save_ai_reply_if_allowed( $session_id, $full_response );
			if ( $ai_message_id ) {
				$done['ai_message_id'] = $ai_message_id;
				$meta                  = method_exists( $this->api_manager, 'get_last_stream_meta' ) ? $this->api_manager->get_last_stream_meta() : array();
				if ( ! empty( $meta['related_products'] ) ) {
					$done['related_products'] = array_values( (array) $meta['related_products'] );
				}
				$this->log_chat_history( $message, $full_response );
			} else {
				$done['agent_handled'] = true;
				$done['ai_dropped']    = true;
			}
		}

		echo 'data: ' . wp_json_encode( $done ) . "\n\n";
		flush();
		exit;
	}

	/**
	 * Send SSE headers and disable buffering for the chat stream.
	 *
	 * @return void
	 */
	private function start_event_stream() {
		// A long answer (plus continuation) must not be cut by a short host time limit.
		if ( function_exists( 'set_time_limit' ) ) {
			@set_time_limit( 300 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		}
		// Finish and save the reply even if the visitor's connection drops.
		ignore_user_abort( true );
		if ( ! headers_sent() ) {
			header( 'Content-Type: text/event-stream; charset=utf-8' );
			header( 'Cache-Control: no-cache' );
			header( 'X-Accel-Buffering: no' );
		}
		while ( ob_get_level() ) {
			ob_end_clean();
		}
		ini_set( 'output_buffering', 'off' ); // phpcs:ignore
		ini_set( 'zlib.output_compression', '0' ); // phpcs:ignore
	}

	/**
	 * Emit one SSE event (as the whole stream) and exit.
	 *
	 * @param array $event Event payload.
	 * @return void
	 */
	private function send_stream_event_and_exit( $event ) {
		$this->start_event_stream();
		echo 'data: ' . wp_json_encode( $event ) . "\n\n";
		flush();
		exit;
	}

	/**
	 * Handle chat response with file attachments via AJAX.
	 *
	 * This method processes the chat request with file attachments and returns a response.
	 *
	 * @return void
	 */
	public function chat_response_with_files() {
		nocache_headers();
		check_ajax_referer('askany_chat_nonce', 'nonce');

		// Process encrypted request data
		$request_data = EncryptionHandler::process_encrypted_request($_POST);

		$message = isset($request_data['message']) ? $this->sanitize_chat_message( $request_data['message'] ) : '';
		$session_id = $this->read_session_id( $request_data );
		$is_encrypted = isset($_POST['encrypted']) && filter_var( wp_unslash( $_POST['encrypted'] ), FILTER_VALIDATE_BOOLEAN );

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
			return;
		}

		// Log encryption status for debugging
		if ($is_encrypted) {
			EncryptionHandler::log_encryption_event('AJAX_FILE_REQUEST', 'Received encrypted chat request with files', array(
				'session_id' => substr($session_id, 0, 10) . '...',
				'message_length' => strlen($message),
				'files_count' => isset($_FILES['attachments']['name']) && is_array($_FILES['attachments']['name']) ? count($_FILES['attachments']['name']) : 0
			));
		}

		// Validate message or files
		$has_files = isset($_FILES['attachments']['name']) && is_array($_FILES['attachments']['name']) && !empty($_FILES['attachments']['name'][0]);
		if (empty($message) && !$has_files) {
			$error_response = array('message' => __('Please enter a message or attach a file.', 'askany'));

			if ($is_encrypted) {
				$error_response = EncryptionHandler::process_encrypted_response($error_response, $_POST);
			}

			wp_send_json_error($error_response);
		}

		// Get current user info
		$current_user_id = get_current_user_id();

		// Ensure we have a valid session ID
		if (empty($session_id)) {
			$session_id = 'user_' . $current_user_id . '_' . wp_generate_password( 16, false );
		}

		$session_secret = $this->get_request_session_secret($request_data);

		// Get user data for session creation
		$user_data = array();
		if ( isset($_POST['user_name']) && is_string( $_POST['user_name'] ) ) {
			$user_data['name'] = sanitize_text_field( wp_unslash( $_POST['user_name'] ) );
		}
		if ( isset($_POST['user_email']) && is_string( $_POST['user_email'] ) ) {
			$user_data['email'] = sanitize_email( wp_unslash( $_POST['user_email'] ) );
		}

		// Check if session already exists before creating or updating
		$existing_session = $this->database->get_chat_session($session_id);

		if ( $existing_session && ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
			return;
		}

		// Minimal flood guard
		if ( ! RateLimiter::flood_guard( $session_id, 'chat' ) ) {
			wp_send_json_error( array( 'message' => __( 'You are sending messages too quickly. Please wait a moment.', 'askany' ), 'rate_limit' => true ), 429 );
			return;
		}

		if ( ! $existing_session ) {
			// New session: keep the secret this browser already holds (request or HttpOnly cookie).
			if ( strlen( (string) $session_secret ) < 16 ) {
				$session_secret = '' !== \Askany\Database::cookie_session_secret( $session_id )
					? \Askany\Database::cookie_session_secret( $session_id )
					: wp_generate_password( 32, false );
			}
		} elseif ( empty( $session_secret ) ) {
			$session_secret = ( isset( $session_id ) && is_string( $session_id ) && '' !== \Askany\Database::cookie_session_secret( $session_id ) )
				? \Askany\Database::cookie_session_secret( $session_id )
				: wp_generate_password( 32, false );
		}

		if (!$existing_session) {
			// Create brand new session
			$created_id = $this->database->create_chat_session($session_id, $user_data, $session_secret);
			if ( false === $created_id ) {
				wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
				return;
			}
		} else {
			// Update last activity for existing session without changing agent assignment
			global $wpdb;
			$wpdb->update(
				$wpdb->prefix . 'askany_chat_sessions',
				array( 'last_activity' => current_time('mysql'), 'status' => 'active' ),
				array('session_id' => $session_id),
				array( '%s', '%s' ),
				array('%s')
			);
		}

		// Process file attachments
		$processed_files = array( 'files' => array(), 'errors' => array() );
		$file_analysis = '';

		if ($has_files) {
			$processed_files = $this->process_file_attachments($_FILES['attachments'], $session_id);

			if (!empty($processed_files['errors'])) {
				wp_send_json_error(array(
					'message' => 'File processing errors: ' . implode(', ', $processed_files['errors'])
				));
			}

			// File names + types for AI context (vision input is not wired yet)
			$file_analysis = $this->generate_file_analysis($processed_files['files']);
		}

		// Combine message with file analysis
		$full_message = $message;
		if (!empty($file_analysis)) {
			$full_message .= "\n\n[ATTACHED FILES]\n" . $file_analysis;
		}

		// Save user message with file info
		$message_to_save = $message;
		if (!empty($processed_files['files'])) {
			$message_to_save .= "\n📎 " . count($processed_files['files']) . " file(s) attached: " .
				implode(', ', array_column($processed_files['files'], 'file_name'));
		}

		$user_message_id = (int) $this->database->save_chat_message($session_id, 'user', $current_user_id, trim( $message_to_save ));
		do_action( 'askany_after_message_saved', $session_id, 'user', $message_to_save );

		if ($user_message_id && !empty($processed_files['files'])) {
			global $wpdb;
			$attachments_table = $wpdb->prefix . 'askany_chat_attachments';
			foreach ($processed_files['files'] as $file) {
				if (!empty($file['id'])) {
					$wpdb->update(
						$attachments_table,
						array('message_id' => $user_message_id),
						array('id' => $file['id'], 'session_id' => $session_id),
						array('%d'),
						array('%d', '%s')
					);
				}
			}
		}

		// Safe file information for the response (never expose absolute server path)
		$safe_files = array();
		foreach ( $processed_files['files'] as $pf ) {
			$safe_files[] = array(
				'id'        => $pf['id'] ?? 0,
				'file_name' => $pf['file_name'] ?? '',
				'file_type' => $pf['file_type'] ?? '',
				'file_size' => $pf['file_size'] ?? 0,
			);
		}

		$base = array(
			'session_id'      => $session_id,
			'session_secret'  => $session_secret,
			'user_message_id' => $user_message_id,
			'ai_message_id'   => null,
			'attached_files'  => $safe_files,
		);

		// Check if AI auto response is globally disabled or if session is handled by an agent
		$prevent_ai = (bool) get_option( 'askany_prevent_ai_response', false );

		if ( $prevent_ai ) {
			// Globally disabled: user message and files saved, no AI response generated
			wp_send_json_success( array_merge( $base, array(
				'message'             => '',
				'agent_handled'       => true,
				'prevent_ai_response' => true,
			) ) );
			return;
		}

		if ( $this->database->is_session_agent_handled( $session_id ) ) {
			// Session is handled by an agent, don't send AI response
			wp_send_json_success( array_merge( $base, array(
				'message' => get_option( 'askany_label_agent_pending', __( 'Your message has been received by our support team. An agent will respond shortly.', 'askany' ) ),
				'agent_handled' => true,
			) ) );
			return;
		}

		// Rate limiting (message kept; AI reply withheld)
		$rate_check = \Askany\RateLimiter::check( $current_user_id );
		if ( ! $rate_check['allowed'] ) {
			wp_send_json_error( array(
				'message'         => $rate_check['message'],
				'rate_limit'      => true,
				'user_message_id' => $user_message_id,
			) );
			return;
		}

		// Get settings (client provider/model overrides are ignored)
		$settings = $this->api_manager->get_settings();

		// Check if API is enabled
		if ( ! $settings['api_enabled'] ) {
			wp_send_json_error(array(
				'message' => get_option( 'askany_label_api_disabled', __( 'Chat API is currently disabled. Please contact the administrator.', 'askany' ) ),
				'user_message_id' => $user_message_id,
			));
		}

		// Get response from API with file context
		// Images are sent to the AI itself when attachment analysis is on and the provider supports vision.
		$response = $this->api_manager->get_response( $full_message, $session_id, '', '', ! empty( $processed_files['files'] ) ? $processed_files['files'] : array() );

		// If API fails and fallback is enabled, use fallback response
		$has_valid_message = is_array( $response ) && '' !== trim( (string) ( $response['message'] ?? '' ) );
		if ( ! $has_valid_message && $settings['fallback_enabled'] ) {
			$fallback_message = get_option( 'askany_label_ai_unavailable', __( 'I apologize, but I am currently unable to process your request. Please try again later.', 'askany' ) );
			if (!empty($processed_files['files'])) {
				$fallback_message = __('I received your files but am currently unable to analyze them. Please try again later.', 'askany');
			}

			$response = array(
				'message' => $fallback_message,
				'related_products' => array(),
				'session_id' => $session_id,
			);
		} elseif ( ! $has_valid_message ) {
			wp_send_json_error(array(
				'message' => __('Sorry, I could not process your request at this time.', 'askany'),
				'user_message_id' => $user_message_id,
			));
		}

		// Save AI response (re-checks for agent takeover first)
		$ai_message_id = $this->save_ai_reply_if_allowed( $session_id, $response['message'] );
		if ( ! $ai_message_id ) {
			$dropped = $this->ai_dropped_payload( $session_id, $session_secret, $user_message_id );
			$dropped['attached_files'] = $safe_files;
			wp_send_json_success( $dropped );
			return;
		}

		// Store chat history in old format for backward compatibility
		$this->log_chat_history($message_to_save, $response['message']);

		$response = $this->strip_private_fields( $response );
		$response['session_id']      = $session_id;
		$response['user_message_id'] = $user_message_id;
		$response['ai_message_id']   = $ai_message_id;
		if ( ! empty( $safe_files ) ) {
			$response['attached_files'] = $safe_files;
		}

		// Process response for encryption if needed
		if ($is_encrypted) {
			$response = EncryptionHandler::process_encrypted_response($response, $_POST);
			EncryptionHandler::log_encryption_event('AJAX_FILE_RESPONSE', 'Sending encrypted chat response with files', array(
				'session_id' => substr($session_id, 0, 10) . '...',
				'response_length' => strlen($response['message']),
				'files_processed' => count($processed_files['files'])
			));
		}

		$response['session_secret'] = $session_secret;
		wp_send_json_success($response);
	}

	/**
	 * Process file attachments from upload.
	 *
	 * @param array  $files      Files array from $_FILES.
	 * @param string $session_id The decrypted session ID.
	 * @return array Processed files with errors.
	 */
	private function process_file_attachments( $files, $session_id = '' ) {
		$file_handler = $this->plugin->get_file_attachment_handler();
		$settings = $file_handler->get_settings();
		
		$processed = array(
			'files' => array(),
			'errors' => array(),
		);

		if ( empty( $files['name'][0] ) ) {
			return $processed;
		}

		$file_count = count( $files['name'] );

		// Check max files limit
		if ( $file_count > $settings['max_files'] ) {
			$processed['errors'][] = sprintf( __( 'Maximum %d files allowed per message.', 'askany' ), $settings['max_files'] );
			return $processed;
		}

		// Use provided session_id (already decrypted) instead of raw $_POST value
		if ( empty( $session_id ) ) {
			$session_id = isset( $_POST['session_id'] ) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : '';
		}
		$ai_analyze = ! empty( $settings['ai_analysis_enabled'] );

		for ( $i = 0; $i < $file_count; $i++ ) {
			$file = array(
				'name' => $files['name'][$i],
				'type' => $files['type'][$i],
				'tmp_name' => $files['tmp_name'][$i],
				'error' => $files['error'][$i],
				'size' => $files['size'][$i],
			);

			if ( $file['error'] !== UPLOAD_ERR_OK ) {
				$processed['errors'][] = sprintf( __( 'Error uploading %s', 'askany' ), $file['name'] );
				continue;
			}

			$result = $file_handler->handle_upload( $file, $session_id, $ai_analyze );

			if ( is_wp_error( $result ) ) {
				$processed['errors'][] = $result->get_error_message();
			} else {
				$processed['files'][] = $result;
			}
		}

		return $processed;
	}

	/**
	 * Generate file analysis text for AI context.
	 *
	 * @param array $files Processed files array.
	 * @return string Analysis text.
	 */
	private function generate_file_analysis( $files ) {
		if ( empty( $files ) ) {
			return '';
		}

		$analysis = array();

		foreach ( $files as $file ) {
			$type_label = 'file';
			
			if ( strpos( $file['file_type'], 'image/' ) === 0 ) {
				$type_label = 'image';
			} elseif ( $file['file_type'] === 'application/pdf' ) {
				$type_label = 'PDF document';
			} elseif ( strpos( $file['file_type'], 'video/' ) === 0 ) {
				$type_label = 'video';
			}

			$size_mb = round( $file['file_size'] / 1024 / 1024, 2 );
			$analysis[] = sprintf( '- %s (%s, %s MB)', $file['file_name'], $type_label, $size_mb );
		}

		return implode( "\n", $analysis );
	}

	/**
	 * Format file size in human readable format.
	 *
	 * @param int $bytes File size in bytes.
	 * @return string Formatted file size.
	 */
	private function format_file_size($bytes) {
		if ($bytes === 0) return '0 Bytes';
		$k = 1024;
		$sizes = array('Bytes', 'KB', 'MB', 'GB');
		$i = floor(log($bytes) / log($k));
		return round($bytes / pow($k, $i), 2) . ' ' . $sizes[$i];
	}

	/**
	 * Log chat history in the database.
	 *
	 * @param string $message The user message.
	 * @param string $response The AI response.
	 * @return void
	 */
	private function log_chat_history( $message, $response ) {
		// Every exchange is already stored in askany_chat_messages (chat logs,
		// transcripts, history). This legacy copy is read by nothing, so it is
		// off by default; the filter turns it back on.
		if ( ! apply_filters( 'askany_log_legacy_chat_history', false ) ) {
			return;
		}
		global $wpdb;
		$table_name = $wpdb->prefix . 'askany_chat_history';

		$wpdb->insert(
			$table_name,
			array(
				'message' => $message,
				'response' => $response,
				'created_at' => current_time('mysql'),
			),
			array( '%s', '%s', '%s' )
		);
	}

	/**
	 * Save plugin settings via AJAX.
	 *
	 * This method processes the settings and updates them in the database.
	 *
	 * @return void
	 */
	public function save_settings() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		if ( isset( $_POST['settings'] ) && is_array( $_POST['settings'] ) ) {
			$settings = wp_unslash( $_POST['settings'] );
		} else {
			$settings_json = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : '';
			$settings      = json_decode( $settings_json, true );
		}

		if ( ! is_array( $settings ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid settings format.', 'askany' ) ) );
			return;
		}

		$settings_to_update = array();

		// String / Text fields
		$string_keys = array(
			'api_key', 'api_provider', 'api_model', 'widget_position', 'chat_header_text',
			'assistant_avatar', 'contact_form_shortcode', 'response_language', 'push_project_id',
			'push_client_email', 'push_sender_id', 'push_private_key', 'web_search_mode',
			'response_tone', 'typing_speed', 'cta_text', 'cta_description', 'cta_design_mode',
			'cta_mode2_button_label', 'cta_mode2_icon', 'cta_show_again_after', 'contact_pill_label',
			'assistant_name', 'custom_prompt_instructions', 'custom_db_tables', 'custom_db_query_rules',
			'custom_db_query_mode', 'retrieval_mode',
			// Fallback mode strategy & static message
			'fallback_type', 'fallback_static_message',
			// Fallback greeting triggers and custom replies
			'fallback_triggers_hello', 'fallback_reply_hello',
			'fallback_triggers_who', 'fallback_reply_who',
			'fallback_triggers_help', 'fallback_reply_help',
			'fallback_triggers_thanks', 'fallback_reply_thanks',
			'fallback_triggers_how_are_you', 'fallback_reply_how_are_you',
			// Custom (OpenAI-compatible) provider fields
			'custom_provider_auth_scheme', 'custom_provider_api_version', 'custom_provider_model',
		);

		foreach ( $string_keys as $key ) {
			if ( array_key_exists( $key, $settings ) ) {
				$val = wp_unslash( $settings[ $key ] );
				if ( in_array( $key, array( 'assistant_avatar', 'cta_mode2_icon' ), true ) ) {
					$settings_to_update[ $key ] = esc_url_raw( $val );
				} elseif ( $key === 'push_client_email' ) {
					$settings_to_update[ $key ] = sanitize_email( $val );
				} elseif ( $key === 'push_private_key' ) {
					$settings_to_update[ $key ] = ApiManager::normalize_private_key( $val );
				} elseif ( in_array( $key, array( 'cta_description', 'custom_prompt_instructions', 'custom_db_query_rules', 'fallback_static_message', 'fallback_reply_hello', 'fallback_reply_who', 'fallback_reply_help', 'fallback_reply_thanks', 'fallback_reply_how_are_you' ), true ) ) {
					$settings_to_update[ $key ] = sanitize_textarea_field( $val );
				} else {
					$settings_to_update[ $key ] = sanitize_text_field( $val );
				}
			}
		}

		// Boolean fields
		$bool_keys = array(
			'api_enabled', 'fallback_enabled', 'fallback_enable_greetings', 'floating_widget_enabled', 'contact_form_enabled',
			'typing_animation_enabled', 'rag_enabled', 'hybrid_search_enabled', 'rag_optimization_enabled',
			'realtime_enabled', 'mobile_bridge_enabled', 'push_modify_credentials', 'web_search_enabled',
			'attachments_enabled', 'admin_attachments_enabled', 'attachments_ai_analysis', 'ai_review_enabled',
			'cta_enabled', 'cta_show_description', 'contact_pill_enabled', 'direct_faq_override',
			'prioritize_own_products', 'prevent_ai_response', 'streaming_enabled', // P4.9 — streaming responses toggle
		);

		foreach ( $bool_keys as $key ) {
			if ( array_key_exists( $key, $settings ) ) {
				$settings_to_update[ $key ] = (bool) $settings[ $key ];
			}
		}

		// Integer fields
		$int_keys = array( 'cta_display_delay', 'custom_db_cache_duration' );
		foreach ( $int_keys as $key ) {
			if ( array_key_exists( $key, $settings ) ) {
				$settings_to_update[ $key ] = absint( $settings[ $key ] );
			}
		}

		// Validation rules for enum options if present
		if ( isset( $settings_to_update['response_language'] ) ) {
			$free_languages = array( 'auto', 'en', 'bn' );
			$all_languages  = array(
				'auto', 'en', 'bn', 'es', 'fr', 'de', 'it', 'pt', 'ru',
				'zh', 'ja', 'ko', 'ar', 'hi', 'tr', 'nl', 'pl', 'sv', 'vi', 'th', 'id',
			);
			$plugin  = \Askany\Plugin::get_instance();
			$allowed = $plugin->is_pro_active() ? $all_languages : $free_languages;

			if ( ! in_array( $settings_to_update['response_language'], $allowed, true ) ) {
				$settings_to_update['response_language'] = 'auto';
			}
		}
		if ( isset( $settings_to_update['web_search_mode'] ) && ! in_array( $settings_to_update['web_search_mode'], array( 'fallback', 'always' ), true ) ) {
			$settings_to_update['web_search_mode'] = 'fallback';
		}
		if ( isset( $settings_to_update['response_tone'] ) && ! in_array( $settings_to_update['response_tone'], array( 'human', 'professional', 'friendly', 'robotic' ), true ) ) {
			$settings_to_update['response_tone'] = 'human';
		}
		if ( isset( $settings_to_update['typing_speed'] ) && ! in_array( $settings_to_update['typing_speed'], array( 'regular', 'fast', 'faster', 'instant' ), true ) ) {
			$settings_to_update['typing_speed'] = 'regular';
		}
		if ( isset( $settings_to_update['custom_db_query_mode'] ) && ! in_array( $settings_to_update['custom_db_query_mode'], array( 'indexed', 'cached', 'live' ), true ) ) {
			$settings_to_update['custom_db_query_mode'] = 'indexed';
		}
		if ( isset( $settings_to_update['custom_db_cache_duration'] ) && ! in_array( (int) $settings_to_update['custom_db_cache_duration'], array( 18000, 43200, 86400, 604800 ), true ) ) {
			$settings_to_update['custom_db_cache_duration'] = 86400;
		}
		// Enum validation for retrieval_mode (P2.4)
		if ( isset( $settings_to_update['retrieval_mode'] ) && ! in_array( $settings_to_update['retrieval_mode'], array( 'rag_only', 'hybrid', 'keyword_only' ), true ) ) {
			$settings_to_update['retrieval_mode'] = 'rag_only';
		}

		$updated = $this->api_manager->update_settings( $settings_to_update );

		if ( $updated ) {
			// Invalidate static system prompt cache whenever settings change
			$this->api_manager->invalidate_static_prompt_cache();
			wp_send_json_success( array( 'message' => __( 'Settings saved successfully.', 'askany' ) ) );
		} else {
			wp_send_json_error( array( 'message' => __( 'Failed to save settings.', 'askany' ) ) );
		}
	}


	/**
	 * Save plugin settings via AJAX.
	 *
	 * This method processes the settings and updates them in the database.
	 *
	 * @return void
	 */
	public function reindex_content() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$result = $this->content_indexer->index_content();

		if ( $result ) {
			wp_send_json_success(array(
				'message' => __('Content reindexed successfully.', 'askany'),
				'stats' => $this->content_indexer->get_content_stats(),
			));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to reindex content.', 'askany') ));
		}
	}

	public function start_indexing() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$tasks = $this->content_indexer->get_indexing_tasks();
		$total_items = array_sum(array_column($tasks, 'total'));

		// Refresh in place instead of emptying the table. TRUNCATE deleted the
		// crawled External Source pages (only a re-crawl brings those back), threw
		// away every embedding, and left the chat with no knowledge until the run
		// finished. Rows whose content no longer exists are removed by the final
		// "cleanup" task, using this start time.
		$this->content_indexer->begin_batch_reindex();

		wp_send_json_success(array(
			'message' => __('Indexing started.', 'askany'),
			'tasks' => $tasks,
			'total' => $total_items,
		));
	}

	public function process_indexing_batch() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$task_type = isset($_POST['task_type']) ? sanitize_text_field($_POST['task_type']) : '';
		$offset = isset($_POST['offset']) ? intval($_POST['offset']) : 0;
		$limit = isset($_POST['limit']) ? intval($_POST['limit']) : 50;

		if (empty($task_type)) {
			wp_send_json_error(array( 'message' => __('Invalid task type.', 'askany') ));
		}

		$result = $this->content_indexer->process_task_batch($task_type, $offset, $limit);

		wp_send_json_success(array(
			'message' => __('Batch processed.', 'askany'),
			'processed' => $result['processed'],
			'done' => $result['done'],
			'logs' => $result['logs'],
		));
	}


	/**
	 * Add, update, delete, and get Q&A pairs via AJAX.
	 *
	 * This method processes the Q&A requests and updates them in the database.
	 *
	 * @return void
	 */
	public function add_qa_pair() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array(
				'message' => __('You do not have permission to perform this action.', 'askany'),
			));
		}

		$question = isset( $_POST['question'] ) ? sanitize_text_field( wp_unslash( $_POST['question'] ) ) : '';
		$answer = isset( $_POST['answer'] ) ? wp_kses_post( wp_unslash( $_POST['answer'] ) ) : '';

		if ( empty($question) || empty($answer) ) {
			wp_send_json_error(array(
				'message' => __('Question and answer are required.', 'askany'),
			));
		}



		if ( ! $this->is_pro() && count( (array) $this->database->get_qa_pairs() ) >= 10 ) {
			wp_send_json_error( array(
				'message' => sprintf( __( 'Free plan allows up to %d Q&A pairs. Upgrade to Pro for unlimited Q&A pairs.', 'askany' ), 10 ),
				'code'    => 'pro_required',
			) );
		}

		$result = $this->database->add_qa_pair($question, $answer);

		if ( $result ) {
			do_action( 'askany_qa_pair_saved', $result );
			wp_send_json_success(array(
				'message' => __('Q&A pair added successfully.', 'askany'),
			));
		} else {
			wp_send_json_error(array(
				'message' => __('Failed to add Q&A pair.', 'askany'),
			));
		}
	}

	/**
	 * Add AI Feedback Memory entry via AJAX.
	 */
	
	/**
	 * AJAX handler for submitting learning feedback memory & auto-generating Q&A pairs.
	 */
	public function submit_learning_feedback() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error( array( 'message' => __('Unauthorized access.', 'askany') ) );
		}

		$query_text       = isset($_POST['query_text']) ? sanitize_textarea_field( wp_unslash($_POST['query_text']) ) : '';
		$failed_response  = isset($_POST['failed_response']) ? sanitize_textarea_field( wp_unslash($_POST['failed_response']) ) : '';
		$corrected_answer = isset($_POST['corrected_answer']) ? sanitize_textarea_field( wp_unslash($_POST['corrected_answer']) ) : '';
		$category         = isset($_POST['category']) ? sanitize_text_field( wp_unslash($_POST['category']) ) : 'general';

		if ( empty( $query_text ) || empty( $corrected_answer ) ) {
			wp_send_json_error( array( 'message' => __('Query and Corrected Answer are required.', 'askany') ) );
		}

		global $wpdb;
		$memory_table = $wpdb->prefix . 'askany_ai_feedback_memory';
		$qa_table     = $wpdb->prefix . 'askany_qa_pairs';

		// 1. Insert into ai_feedback_memory
		$wpdb->insert(
			$memory_table,
			array(
				'session_id'       => 'admin_learning',
				'query_text'       => $query_text,
				'failed_response'  => $failed_response,
				'user_feedback'    => 'Admin Correction',
				'corrected_answer' => $corrected_answer,
				'confidence_score' => 1.0,
				'status'           => 'active',
				'created_at'       => current_time('mysql'),
			),
			array( '%s', '%s', '%s', '%s', '%s', '%f', '%s', '%s' )
		);

		// 2. Automatically create/update QA Pair
		$existing_qa = $wpdb->get_var( $wpdb->prepare(
			"SELECT id FROM {$qa_table} WHERE question = %s",
			$query_text
		) );

		if ( $existing_qa ) {
			$wpdb->update(
				$qa_table,
				array(
					'answer'     => $corrected_answer,
					'updated_at' => current_time('mysql'),
				),
				array( 'id' => $existing_qa ),
				array( '%s', '%s' ),
				array( '%d' )
			);
			do_action( 'askany_qa_pair_saved', $existing_qa );
		} else {
			$wpdb->insert(
				$qa_table,
				array(
					'question'   => $query_text,
					'answer'     => $corrected_answer,
					'status'     => 'active',
					'created_at' => current_time('mysql'),
				),
				array( '%s', '%s', '%s', '%s' )
			);
			do_action( 'askany_qa_pair_saved', $wpdb->insert_id );
		}

		wp_send_json_success( array(
			'message' => __('AI Memory & Q&A Pair updated successfully! Future responses will prioritize this correction.', 'askany')
		) );
	}

	public function add_feedback_memory() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Permission denied.', 'askany') ));
		}

		$query_text       = isset( $_POST['query_text'] ) ? sanitize_textarea_field( wp_unslash( $_POST['query_text'] ) ) : '';
		$failed_response  = isset( $_POST['failed_response'] ) ? sanitize_textarea_field( wp_unslash( $_POST['failed_response'] ) ) : '';
		$user_feedback     = isset( $_POST['user_feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['user_feedback'] ) ) : '';
		$corrected_answer = isset( $_POST['corrected_answer'] ) ? sanitize_textarea_field( wp_unslash( $_POST['corrected_answer'] ) ) : '';

		if ( empty( $query_text ) || empty( $corrected_answer ) ) {
			wp_send_json_error(array( 'message' => __('Query text and corrected answer are required.', 'askany') ));
		}

		$res = $this->database->add_feedback_memory( $query_text, $failed_response, $user_feedback, $corrected_answer );
		if ( $res ) {
			wp_send_json_success(array( 'message' => __('Feedback memory added successfully.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to add feedback memory.', 'askany') ));
		}
	}

	/**
	 * Update AI Feedback Memory entry via AJAX.
	 */
	public function update_feedback_memory() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Permission denied.', 'askany') ));
		}

		$id               = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0;
		$query_text       = isset( $_POST['query_text'] ) ? sanitize_textarea_field( wp_unslash( $_POST['query_text'] ) ) : '';
		$failed_response  = isset( $_POST['failed_response'] ) ? sanitize_textarea_field( wp_unslash( $_POST['failed_response'] ) ) : '';
		$user_feedback     = isset( $_POST['user_feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['user_feedback'] ) ) : '';
		$corrected_answer = isset( $_POST['corrected_answer'] ) ? sanitize_textarea_field( wp_unslash( $_POST['corrected_answer'] ) ) : '';
		$status           = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : 'active';

		if ( ! $id || empty( $query_text ) || empty( $corrected_answer ) ) {
			wp_send_json_error(array( 'message' => __('Invalid data.', 'askany') ));
		}

		$res = $this->database->update_feedback_memory( $id, $query_text, $failed_response, $user_feedback, $corrected_answer, $status );
		if ( false !== $res ) {
			wp_send_json_success(array( 'message' => __('Feedback memory updated.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to update feedback memory.', 'askany') ));
		}
	}

	/**
	 * Delete AI Feedback Memory entry via AJAX.
	 */
	public function delete_feedback_memory() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Permission denied.', 'askany') ));
		}

		$id = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0;
		if ( ! $id ) {
			wp_send_json_error(array( 'message' => __('Invalid ID.', 'askany') ));
		}

		$res = $this->database->delete_feedback_memory( $id );
		if ( $res ) {
			wp_send_json_success(array( 'message' => __('Feedback memory deleted.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to delete memory.', 'askany') ));
		}
	}

	/**
	 * Fetch AI Feedback Memory entries via AJAX.
	 */
	public function get_feedback_memories() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Permission denied.', 'askany') ));
		}

		$memories = $this->database->get_all_feedback_memories(100);
		wp_send_json_success(array( 'memories' => $memories ));
	}

	/**
	 * Update Q&A pair via AJAX.
	 *
	 * This method processes the Q&A update request and updates it in the database.
	 *
	 * @return void
	 */
	public function update_qa_pair() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array(
				'message' => __('You do not have permission to perform this action.', 'askany'),
			));
		}

		$id = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : 0;
		$question = isset( $_POST['question'] ) ? sanitize_text_field( wp_unslash( $_POST['question'] ) ) : '';
		$answer = isset( $_POST['answer'] ) ? wp_kses_post( wp_unslash( $_POST['answer'] ) ) : '';

		if ( empty($id) || empty($question) || empty($answer) ) {
			wp_send_json_error(array(
				'message' => __('ID, question and answer are required.', 'askany'),
			));
		}

		$result = $this->database->update_qa_pair($id, $question, $answer);

		if ( $result !== false ) {
			do_action( 'askany_qa_pair_saved', $id );
			wp_send_json_success(array(
				'message' => __('Q&A pair updated successfully.', 'askany'),
			));
		} else {
			wp_send_json_error(array(
				'message' => __('Failed to update Q&A pair.', 'askany'),
			));
		}
	}

	/**
	 * Delete Q&A pair via AJAX.
	 *
	 * This method processes the Q&A delete request and deletes it from the database.
	 *
	 * @return void
	 */
	public function delete_qa_pair() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array(
				'message' => __('You do not have permission to perform this action.', 'askany'),
			));
		}

		$id = isset($_POST['id']) ? intval($_POST['id']) : 0;

		if ( empty($id) ) {
			wp_send_json_error(array(
				'message' => __('Q&A pair ID is required.', 'askany'),
			));
		}

		$result = $this->database->delete_qa_pair($id);

		if ( $result ) {
			do_action( 'askany_qa_pair_deleted', $id );
			wp_send_json_success(array(
				'message' => __('Q&A pair deleted successfully.', 'askany'),
			));
		} else {
			wp_send_json_error(array(
				'message' => __('Failed to delete Q&A pair.', 'askany'),
			));
		}
	}

	/**
	 * Get Q&A pair via AJAX.
	 *
	 * This method processes the Q&A get request and retrieves it from the database.
	 *
	 * @return void
	 */
	public function get_qa_pair() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array(
				'message' => __('You do not have permission to perform this action.', 'askany'),
			));
		}

		$id = isset($_POST['id']) ? intval($_POST['id']) : 0;

		if ( empty($id) ) {
			wp_send_json_error(array(
				'message' => __('Q&A pair ID is required.', 'askany'),
			));
		}

		$qa_pair = $this->database->get_qa_pair($id);

		if ( $qa_pair ) {
			wp_send_json_success(array(
				'qa_pair' => $qa_pair,
			));
		} else {
			wp_send_json_error(array(
				'message' => __('Q&A pair not found.', 'askany'),
			));
		}
	}

	/**
	 * Export Q&A pairs via AJAX.
	 *
	 * @return void
	 */
	public function export_qa_pairs() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array(
				'message' => __('You do not have permission to export Q&A pairs.', 'askany'),
			));
		}

		$format = isset($_POST['format']) ? sanitize_text_field($_POST['format']) : 'json';

		// Get all Q&A pairs
		$qa_pairs = $this->database->get_qa_pairs();

		if ( empty($qa_pairs) ) {
			wp_send_json_error(array(
				'message' => __('No Q&A pairs found to export.', 'askany'),
			));
		}

		// Prepare data for export
		$export_data = array();
		foreach ( $qa_pairs as $qa_pair ) {
			$export_data[] = array(
				'question' => $qa_pair->question,
				'answer' => $qa_pair->answer,
				'created_at' => $qa_pair->created_at,
			);
		}

		$filename = 'askany-qa-pairs-' . date('Y-m-d-H-i-s');
		$content = '';

		if ( $format === 'csv' ) {
			$filename .= '.csv';
			
			// Create CSV content
			$output = fopen('php://temp', 'r+');
			
			// Add CSV headers
			fputcsv($output, array('Question', 'Answer', 'Created At'));
			
			// Add data rows
			foreach ( $export_data as $row ) {
				fputcsv($output, array(
					$row['question'],
					$row['answer'],
					$row['created_at']
				));
			}
			
			rewind($output);
			$content = stream_get_contents($output);
			fclose($output);
		} else {
			$filename .= '.json';
			$content = wp_json_encode($export_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
		}

		wp_send_json_success(array(
			'content' => $content,
			'filename' => $filename,
			'count' => count($export_data),
		));
	}

	/**
	 * Import Q&A pairs via AJAX.
	 *
	 * @return void
	 */
	public function import_qa_pairs() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array(
				'message' => __('You do not have permission to import Q&A pairs.', 'askany'),
			));
		}

		if ( ! isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK ) {
			wp_send_json_error(array(
				'message' => __('No file uploaded or upload error occurred.', 'askany'),
			));
		}

		$file = $_FILES['file'];
		$file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

		if ( ! in_array($file_extension, array('json', 'csv')) ) {
			wp_send_json_error(array(
				'message' => __('Invalid file format. Only JSON and CSV files are supported.', 'askany'),
			));
		}

		$file_content = file_get_contents($file['tmp_name']);
		if ( $file_content === false ) {
			wp_send_json_error(array(
				'message' => __('Failed to read uploaded file.', 'askany'),
			));
		}

		$import_data = array();

		if ( $file_extension === 'json' ) {
			$decoded_data = json_decode($file_content, true);
			
			if ( json_last_error() !== JSON_ERROR_NONE ) {
				wp_send_json_error(array(
					'message' => __('Invalid JSON file format.', 'askany'),
				));
			}

			if ( ! is_array($decoded_data) ) {
				wp_send_json_error(array(
					'message' => __('JSON file must contain an array of Q&A pairs.', 'askany'),
				));
			}

			$import_data = $decoded_data;
		} else {
			// Parse CSV
			$lines = str_getcsv($file_content, "\n");
			$header = null;
			
			foreach ( $lines as $line ) {
				$row = str_getcsv($line);
				
				if ( $header === null ) {
					$header = $row;
					continue;
				}
				
				if ( count($row) >= 2 ) {
					$import_data[] = array(
						'question' => $row[0],
						'answer' => $row[1],
						'created_at' => isset($row[2]) ? $row[2] : current_time('mysql'),
					);
				}
			}
		}

		if ( empty($import_data) ) {
			wp_send_json_error(array(
				'message' => __('No valid Q&A pairs found in the file.', 'askany'),
			));
		}



		// Import Q&A pairs
		$imported_count = 0;
		$updated_count = 0;
		$errors = array();

		foreach ( $import_data as $index => $qa_data ) {
			if ( ! isset($qa_data['question']) || ! isset($qa_data['answer']) ) {
				$errors[] = sprintf(__('Row %d: Missing question or answer.', 'askany'), $index + 1);
				continue;
			}

			$question = sanitize_text_field($qa_data['question']);
			$answer = wp_kses_post($qa_data['answer']);

			if ( empty($question) || empty($answer) ) {
				$errors[] = sprintf(__('Row %d: Question and answer cannot be empty.', 'askany'), $index + 1);
				continue;
			}

			// Check if Q&A pair with same question already exists
			$existing_qa = $this->database->find_qa_match($question);
			
			if ( $existing_qa ) {
				// Update existing Q&A pair
				$result = $this->database->update_qa_pair($existing_qa->id, $question, $answer);
				if ( $result !== false ) {
					$updated_count++;
				} else {
					$errors[] = sprintf(__('Row %d: Failed to update existing Q&A pair.', 'askany'), $index + 1);
				}
			} else {
				// Add new Q&A pair
				$result = $this->database->add_qa_pair($question, $answer);
				if ( $result ) {
					$imported_count++;
				} else {
					$errors[] = sprintf(__('Row %d: Failed to add new Q&A pair.', 'askany'), $index + 1);
				}
			}
		}

		$message = '';
		if ( $imported_count > 0 ) {
			$message .= sprintf(__('%d Q&A pairs imported successfully. ', 'askany'), $imported_count);
		}
		if ( $updated_count > 0 ) {
			$message .= sprintf(__('%d existing Q&A pairs updated. ', 'askany'), $updated_count);
		}
		if ( ! empty($errors) ) {
			$message .= sprintf(__('%d errors occurred during import.', 'askany'), count($errors));
		}

		if ( $imported_count > 0 || $updated_count > 0 ) {
			wp_send_json_success(array(
				'message' => $message,
				'imported' => $imported_count,
				'updated' => $updated_count,
				'errors' => $errors,
			));
		} else {
			wp_send_json_error(array(
				'message' => $message ?: __('Import failed. No Q&A pairs were processed.', 'askany'),
				'errors' => $errors,
			));
		}
	}

	/**
	 * Process uploaded PDF file via AJAX.
	 *
	 * @return void
	 */
	public function process_pdf() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'askany' ) ) );
		}

		if ( ! isset( $_FILES['pdf_file'] ) ) {
			wp_send_json_error( array( 'message' => __( 'No PDF file uploaded.', 'askany' ) ) );
		}

		// Sanitize and validate file data.
		// Note: For file uploads, we can't directly sanitize the file content,
		// but we validate it through proper checks.
		$file = array(
			'name'     => isset( $_FILES['pdf_file']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['pdf_file']['name'] ) ) : '',
			'type'     => isset( $_FILES['pdf_file']['type'] ) ? sanitize_text_field( wp_unslash( $_FILES['pdf_file']['type'] ) ) : '',
			'tmp_name' => isset( $_FILES['pdf_file']['tmp_name'] ) ? $_FILES['pdf_file']['tmp_name'] : '', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- No appropriate direct sanitization function for file paths
			'error'    => isset( $_FILES['pdf_file']['error'] ) ? (int) $_FILES['pdf_file']['error'] : UPLOAD_ERR_NO_FILE,
			'size'     => isset( $_FILES['pdf_file']['size'] ) ? (int) $_FILES['pdf_file']['size'] : 0,
		);

		// Using Yoda condition
		if ( UPLOAD_ERR_OK !== $file['error'] ) {
			wp_send_json_error( array( 'message' => __( 'Error uploading file.', 'askany' ) ) );
		}

		// Check file type and use Yoda condition
		$file_type = wp_check_filetype( basename( $file['name'] ), array( 'pdf' => 'application/pdf' ) );
		if ( 'application/pdf' !== $file_type['type'] ) {
			wp_send_json_error( array( 'message' => __( 'Invalid file type. Please upload a PDF file.', 'askany' ) ) );
		}

		// Process PDF file.
		$result = $this->pdf_handler->process_pdf( $file );

		if ( $result ) {
			if ( is_array( $result ) && ! empty( $result['id'] ) ) {
				do_action( 'askany_pdf_saved', $result['id'] );
			}
			wp_send_json_success( array(
				'message'  => __( 'PDF processed successfully.', 'askany' ),
				'pdf_info' => $result,
				'stats'    => $this->pdf_handler->get_pdf_stats(),
			) );
		} else {
			wp_send_json_error( array( 'message' => __( 'Failed to process PDF file.', 'askany' ) ) );
		}
	}


	/**
	 * Clear chat history via AJAX.
	 *
	 * This method clears the chat history from the database.
	 *
	 * @return void
	 */
	public function clear_chat_all_history() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		global $wpdb;
		$table_name = $wpdb->prefix . 'askany_chat_history';

		$result = $wpdb->query("TRUNCATE TABLE {$table_name}");

		if ( $result !== false ) {
			wp_send_json_success(array( 'message' => __('Chat history cleared.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to clear chat history.', 'askany') ));
		}
	}

	/**
	 * Get available models for a provider via AJAX.
	 *
	 * @return void
	 */
	public function get_provider_models() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$provider = isset($_POST['provider']) ? sanitize_text_field( wp_unslash( $_POST['provider']) ) : 'deepseek';

		// Create provider instance to get models
		$provider_instance = \Askany\Providers\ProviderFactory::create( $provider, 'dummy_key', '' );

		if ( ! $provider_instance ) {
			wp_send_json_error(array( 'message' => __('Invalid provider.', 'askany') ));
		}

		$models = $provider_instance->get_available_models();
		$default_model = $provider_instance->get_default_model();

		wp_send_json_success(array(
			'models' => $models,
			'default_model' => $default_model,
		));
	}

	/**
	 * Get provider settings via AJAX.
	 *
	 * This method retrieves the API key and URL based on the selected provider.
	 *
	 * @return void
	 */
	public function get_provider_settings() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$provider = isset($_POST['provider']) ? sanitize_text_field( wp_unslash( $_POST['provider']) ) : 'deepseek';

		// Get the appropriate API key and URL based on provider.
		$api_key = '';
		$api_url = '';

		switch ( $provider ) {
			case 'openai':
				$api_key = get_option('askany_api_key_openai', '');
				$api_url = get_option('askany_api_url_openai', '');
				if ( empty( $api_url ) ) {
					$api_url = 'https://api.openai.com/v1/chat/completions';
				}
				break;
			case 'claude':
				$api_key = get_option('askany_api_key_claude', '');
				$api_url = get_option('askany_api_url_claude', '');
				if ( empty( $api_url ) ) {
					$api_url = 'https://api.anthropic.com/v1/messages';
				}
				break;
			case 'gemini':
				$api_key = get_option('askany_api_key_gemini', '');
				$api_url = get_option('askany_api_url_gemini', '');
				if ( empty( $api_url ) ) {
					$api_url = 'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent';
				}
				break;
			case 'grok':
				$api_key = get_option('askany_api_key_grok', '');
				$api_url = get_option('askany_api_url_grok', '');
				if ( empty( $api_url ) ) {
					$api_url = 'https://api.x.ai/v1/chat/completions';
				}
				break;
			case 'openrouter':
				$api_key = get_option('askany_api_key_openrouter', '');
				$api_url = get_option('askany_api_url_openrouter', '');
				if ( empty( $api_url ) ) {
					$api_url = 'https://openrouter.ai/api/v1/chat/completions';
				}
				break;
			case 'custom':
				$api_key = get_option('askany_api_key_custom', '');
				$api_url = get_option('askany_api_url_custom', '');
				if ( empty( $api_url ) ) {
					$api_url = 'http://localhost:11434/v1';
				}
				break;
			case 'deepseek':
			default:
				$api_key = get_option('askany_api_key_deepseek', '');
				$api_url = get_option('askany_api_url_deepseek', '');
				if ( empty( $api_url ) ) {
					$api_url = 'https://api.deepseek.com/v1/chat/completions';
				}
				break;
		}

		// Get selected model
		$api_model = get_option('askany_api_model', '');

		$response_data = array(
			'api_key' => $api_key,
			'api_url' => $api_url,
			'api_model' => $api_model,
		);

		// Return extra fields for custom provider
		if ( $provider === 'custom' ) {
			$response_data['custom_provider_auth_scheme'] = get_option( 'askany_custom_provider_auth_scheme', 'bearer' );
			$response_data['custom_provider_api_version'] = get_option( 'askany_custom_provider_api_version', '' );
			$response_data['custom_provider_model']       = get_option( 'askany_custom_provider_model', '' );
		}

		wp_send_json_success( $response_data );
	}

	/**
	 * Test a custom (OpenAI-compatible) provider connection.
	 * Hits the /models endpoint with stored credentials.
	 *
	 * @return void
	 */
	public function test_custom_provider() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized', 'askany' ) ) );
		}

		$base_url    = isset( $_POST['base_url'] )    ? esc_url_raw( wp_unslash( $_POST['base_url'] ) )    : '';
		$api_key     = isset( $_POST['api_key'] )     ? sanitize_text_field( wp_unslash( $_POST['api_key'] ) )     : '';
		$auth_scheme = isset( $_POST['auth_scheme'] ) ? sanitize_text_field( wp_unslash( $_POST['auth_scheme'] ) ) : 'bearer';
		$api_version = isset( $_POST['api_version'] ) ? sanitize_text_field( wp_unslash( $_POST['api_version'] ) ) : '';

		if ( empty( $base_url ) ) {
			wp_send_json_error( array( 'message' => __( 'Base URL is empty. Save your settings first.', 'askany' ) ) );
			return;
		}

		$models_url = rtrim( $base_url, '/' ) . '/models';
		if ( ! empty( $api_version ) ) {
			$models_url = add_query_arg( 'api-version', $api_version, $models_url );
		}

		$headers = array( 'Content-Type' => 'application/json' );
		if ( ! empty( $api_key ) ) {
			if ( $auth_scheme === 'api-key' ) {
				$headers['api-key'] = $api_key;
			} else {
				$headers['Authorization'] = 'Bearer ' . $api_key;
			}
		}

		$response = wp_remote_get( $models_url, array(
			'headers' => $headers,
			'timeout' => 10,
		) );

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => sprintf( __( 'Network error: %s', 'askany' ), $response->get_error_message() ) ) );
			return;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );

		if ( $code === 401 || $code === 403 ) {
			wp_send_json_error( array( 'message' => sprintf( __( 'Auth rejected (HTTP %d). Check API key and auth scheme.', 'askany' ), $code ) ) );
			return;
		}
		if ( $code === 404 ) {
			wp_send_json_error( array( 'message' => __( 'Endpoint not found (HTTP 404). Check the Base URL.', 'askany' ) ) );
			return;
		}
		if ( $code < 200 || $code >= 300 ) {
			wp_send_json_error( array( 'message' => sprintf( __( 'Server returned HTTP %d.', 'askany' ), $code ) ) );
			return;
		}

		$body  = json_decode( wp_remote_retrieve_body( $response ), true );
		$count = 0;
		if ( is_array( $body ) ) {
			if ( isset( $body['data'] ) && is_array( $body['data'] ) ) {
				$count = count( $body['data'] );
			} elseif ( isset( $body['models'] ) && is_array( $body['models'] ) ) {
				$count = count( $body['models'] );
			}
		}

		wp_send_json_success( array(
			'message' => sprintf( __( 'Connection OK — %d model(s) reported.', 'askany' ), $count ),
			'count'   => $count,
		) );
	}

	/**
	 * Get provider settings via AJAX.
	 *
	 * This method retrieves the API key and URL based on the selected provider.
	 *
	 * @return void
	 */
	public function test_api_connection() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$provider = isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : 'deepseek';
		$api_key = isset( $_POST['api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['api_key'] ) ) : '';
		$api_model = isset( $_POST['api_model'] ) ? sanitize_text_field( wp_unslash( $_POST['api_model'] ) ) : '';

		if ( empty( $api_key ) ) {
			wp_send_json_error( array( 'message' => __( 'API key is required.', 'askany' ) ) );
		}

		// Always use empty URL - let provider use its default
		$api_url = '';

		// Create provider instance for testing without modifying saved settings
		$provider_instance = \Askany\Providers\ProviderFactory::create( $provider, $api_key, $api_url );

		if ( ! $provider_instance ) {
			wp_send_json_error( array( 'message' => __( 'Invalid provider selected.', 'askany' ) ) );
		}

		// Pass model to test connection
		$test_options = array();
		if ( ! empty( $api_model ) ) {
			$test_options['model'] = $api_model;
		}

		$result = $provider_instance->test_connection( $test_options );

		if ( 'success' === $result['status'] ) {
			wp_send_json_success( $result );
		} else {
			wp_send_json_error( $result );
		}
	}

	/**
	 * Test Firebase FCM V1 connection by attempting to get an OAuth2 access token.
	 *
	 * @return void
	 */
	public function test_fcm_connection() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error( array( 'message' => __('You do not have permission to perform this action.', 'askany') ) );
		}

		$project_id   = isset( $_POST['project_id'] )   ? sanitize_text_field( wp_unslash( $_POST['project_id'] ) )   : '';
		$client_email = isset( $_POST['client_email'] ) ? sanitize_email( wp_unslash( $_POST['client_email'] ) )       : '';
		$sender_id    = isset( $_POST['sender_id'] )    ? sanitize_text_field( wp_unslash( $_POST['sender_id'] ) )     : '';
		$private_key  = isset( $_POST['private_key'] )  ? wp_unslash( $_POST['private_key'] )                         : '';

		// Normalize private key: handles literal \n strings, space-flattened keys, and PEM formatting
		$private_key = ApiManager::normalize_private_key( $private_key );

		// Validate all fields are present
		$missing = array();
		if ( empty( $project_id ) )   $missing[] = 'Project ID';
		if ( empty( $client_email ) ) $missing[] = 'Client Email';
		if ( empty( $sender_id ) )    $missing[] = 'Sender ID';
		if ( empty( $private_key ) )  $missing[] = 'Private Key';

		if ( ! empty( $missing ) ) {
			wp_send_json_error( array(
				'message' => sprintf( __('Missing required fields: %s', 'askany'), implode(', ', $missing) ),
			) );
		}

		// Attempt to get an OAuth2 access token using the service account credentials
		$now = time();
		$exp = $now + 3600;

		$header = rtrim( strtr( base64_encode( wp_json_encode( array( 'alg' => 'RS256', 'typ' => 'JWT' ) ) ), '+/', '-_' ), '=' );
		$claims = rtrim( strtr( base64_encode( wp_json_encode( array(
			'iss'   => $client_email,
			'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
			'aud'   => 'https://oauth2.googleapis.com/token',
			'iat'   => $now,
			'exp'   => $exp,
		) ) ), '+/', '-_' ), '=' );

		$signing_input        = $header . '.' . $claims;
		$private_key_resource = openssl_pkey_get_private( $private_key );

		if ( ! $private_key_resource ) {
			wp_send_json_error( array(
				'message' => __('Invalid Private Key. Make sure you pasted the full key including the -----BEGIN PRIVATE KEY----- (or BEGIN RSA PRIVATE KEY) header and footer.', 'askany'),
				'checks'  => array(
					'project_id'   => ! empty( $project_id ),
					'client_email' => ! empty( $client_email ),
					'sender_id'    => ! empty( $sender_id ),
					'private_key'  => false,
				),
			) );
		}

		$signature = '';
		if ( ! openssl_sign( $signing_input, $signature, $private_key_resource, OPENSSL_ALGO_SHA256 ) ) {
			wp_send_json_error( array( 'message' => __('Failed to sign JWT with the provided private key.', 'askany') ) );
		}

		$jwt = $signing_input . '.' . rtrim( strtr( base64_encode( $signature ), '+/', '-_' ), '=' );

		$response = wp_remote_post( 'https://oauth2.googleapis.com/token', array(
			'body'    => array(
				'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
				'assertion'  => $jwt,
			),
			'timeout' => 15,
		) );

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array(
				'message' => __('Network error connecting to Google OAuth2: ', 'askany') . $response->get_error_message(),
			) );
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		$code = wp_remote_retrieve_response_code( $response );

		if ( ! empty( $body['access_token'] ) ) {
			wp_send_json_success( array(
				'message'      => __('Firebase FCM connection successful! All credentials are valid.', 'askany'),
				'checks'       => array(
					'project_id'   => true,
					'client_email' => true,
					'sender_id'    => true,
					'private_key'  => true,
				),
				'project_id'   => $project_id,
				'client_email' => $client_email,
				'sender_id'    => $sender_id,
			) );
		} else {
			$error_desc = isset( $body['error_description'] ) ? $body['error_description'] : ( isset( $body['error'] ) ? $body['error'] : __('Unknown error', 'askany') );
			wp_send_json_error( array(
				'message' => sprintf( __('Google OAuth2 rejected the credentials (HTTP %d): %s', 'askany'), $code, $error_desc ),
				'checks'  => array(
					'project_id'   => ! empty( $project_id ),
					'client_email' => ! empty( $client_email ),
					'sender_id'    => ! empty( $sender_id ),
					'private_key'  => ! empty( $private_key ),
				),
			) );
		}
	}

	/**
	 * Send a test push notification to a specific FCM device token.
	 *
	 * @return void
	 */
	public function send_test_push() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'askany' ) ) );
		}

		$device_token = isset( $_POST['device_token'] ) ? sanitize_text_field( wp_unslash( $_POST['device_token'] ) ) : '';

		if ( empty( $device_token ) ) {
			wp_send_json_error( array( 'message' => __( 'Device token is required.', 'askany' ) ) );
		}

		// Load credentials from saved options
		$project_id   = get_option( 'askany_push_project_id', '' );
		$client_email = get_option( 'askany_push_client_email', '' );
		$private_key  = ApiManager::normalize_private_key( get_option( 'askany_push_private_key', '' ) );

		if ( empty( $project_id ) || empty( $client_email ) || empty( $private_key ) ) {
			wp_send_json_error( array( 'message' => __( 'Firebase credentials are not configured. Please save your credentials first.', 'askany' ) ) );
		}

		// Get OAuth2 access token
		$now    = time();
		$header = rtrim( strtr( base64_encode( wp_json_encode( array( 'alg' => 'RS256', 'typ' => 'JWT' ) ) ), '+/', '-_' ), '=' );
		$claims = rtrim( strtr( base64_encode( wp_json_encode( array(
			'iss'   => $client_email,
			'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
			'aud'   => 'https://oauth2.googleapis.com/token',
			'iat'   => $now,
			'exp'   => $now + 3600,
		) ) ), '+/', '-_' ), '=' );

		$signing_input        = $header . '.' . $claims;
		$private_key_resource = openssl_pkey_get_private( $private_key );

		if ( ! $private_key_resource ) {
			wp_send_json_error( array( 'message' => __( 'Invalid private key. Please re-save your credentials.', 'askany' ) ) );
		}

		$signature = '';
		openssl_sign( $signing_input, $signature, $private_key_resource, OPENSSL_ALGO_SHA256 );
		$jwt = $signing_input . '.' . rtrim( strtr( base64_encode( $signature ), '+/', '-_' ), '=' );

		$token_response = wp_remote_post( 'https://oauth2.googleapis.com/token', array(
			'body'    => array(
				'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
				'assertion'  => $jwt,
			),
			'timeout' => 15,
		) );

		if ( is_wp_error( $token_response ) ) {
			wp_send_json_error( array( 'message' => __( 'Failed to get access token: ', 'askany' ) . $token_response->get_error_message() ) );
		}

		$token_body   = json_decode( wp_remote_retrieve_body( $token_response ), true );
		$access_token = $token_body['access_token'] ?? '';

		if ( empty( $access_token ) ) {
			wp_send_json_error( array( 'message' => __( 'Could not obtain access token from Google.', 'askany' ) ) );
		}

		// Send the test notification via FCM V1
		$payload = array(
			'message' => array(
				'token'        => $device_token,
				'notification' => array(
					'title' => '🔔 AskAny Test Notification',
					'body'  => 'Push notifications are working correctly from ' . get_bloginfo( 'name' ) . '!',
				),
				'data' => array(
					'type'     => 'test',
					'site_url' => get_site_url(),
					'sent_at'  => (string) $now,
				),
				'android' => array( 'priority' => 'high' ),
				'apns'    => array( 'headers' => array( 'apns-priority' => '10' ) ),
			),
		);

		$fcm_url      = 'https://fcm.googleapis.com/v1/projects/' . rawurlencode( $project_id ) . '/messages:send';
		$fcm_response = wp_remote_post( $fcm_url, array(
			'headers' => array(
				'Authorization' => 'Bearer ' . $access_token,
				'Content-Type'  => 'application/json',
			),
			'body'    => wp_json_encode( $payload ),
			'timeout' => 15,
		) );

		if ( is_wp_error( $fcm_response ) ) {
			wp_send_json_error( array( 'message' => __( 'FCM request failed: ', 'askany' ) . $fcm_response->get_error_message() ) );
		}

		$fcm_body = json_decode( wp_remote_retrieve_body( $fcm_response ), true );
		$fcm_code = wp_remote_retrieve_response_code( $fcm_response );

		if ( $fcm_code === 200 && ! empty( $fcm_body['name'] ) ) {
			wp_send_json_success( array(
				'message'    => __( 'Test notification sent successfully!', 'askany' ),
				'message_id' => $fcm_body['name'],
			) );
		} else {
			$error = $fcm_body['error']['message'] ?? __( 'Unknown FCM error', 'askany' );
			$status = $fcm_body['error']['status'] ?? '';
			wp_send_json_error( array(
				'message' => sprintf( __( 'FCM rejected the request (HTTP %d): %s', 'askany' ), $fcm_code, $error ),
				'status'  => $status,
				'hint'    => $status === 'INVALID_ARGUMENT' ? __( 'The device token may be invalid or expired. Get a fresh token from the mobile app.', 'askany' ) : '',
			) );
		}
	}


	/**
	 * Add user as agent via AJAX.
	 *
	 * @return void
	 */
	public function add_agent() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$user_id = isset($_POST['user_id']) ? intval($_POST['user_id']) : 0;

		if ( ! $user_id ) {
			wp_send_json_error(array( 'message' => __('Invalid user ID.', 'askany') ));
		}



		if ( ! $this->is_pro() && ! $this->database->is_user_agent( $user_id ) && count( (array) $this->database->get_agents() ) >= 2 ) {
			wp_send_json_error( array( 'message' => __( 'The free plan allows 2 agents. Upgrade to Pro for unlimited agents.', 'askany' ), 'code' => 'pro_required' ) );
		}

		$result = $this->database->add_user_agent($user_id);

		if ( $result ) {
			wp_send_json_success(array( 'message' => __('User added as agent successfully.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to add user as agent.', 'askany') ));
		}
	}

	/**
	 * Remove user as agent via AJAX.
	 *
	 * @return void
	 */
	public function remove_agent() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$user_id = isset($_POST['user_id']) ? intval($_POST['user_id']) : 0;

		if ( ! $user_id ) {
			wp_send_json_error(array( 'message' => __('Invalid user ID.', 'askany') ));
		}

		$result = $this->database->remove_user_agent($user_id);

		if ( $result ) {
			wp_send_json_success(array( 'message' => __('Agent removed successfully.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to remove agent.', 'askany') ));
		}
	}

	/**
	 * Get chat sessions via AJAX.
	 *
	 * @return void
	 */
	public function get_chat_sessions() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$this->touch_agent_presence();

		// 100 most recent active sessions; unread counts come from the same query.
		$sessions = $this->database->get_active_chat_sessions( 100 );

		wp_send_json_success(array( 'sessions' => $sessions ? $sessions : array() ));
	}

	/**
	 * Get chat messages via AJAX.
	 *
	 * @return void
	 */
	public function get_chat_messages() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$session_id = $this->read_session_id( $_POST );

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) ) {
			wp_send_json_error(array( 'message' => __('Session ID is required.', 'askany') ));
		}

		if ( $this->is_session_locked_for_free( $session_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Upgrade to Pro to open more than 3 live chat sessions.', 'askany' ), 'code' => 'pro_required' ), 403 );
		}

		$this->touch_agent_presence();

		$messages = $this->database->get_chat_messages($session_id);
		$session = $this->database->get_chat_session($session_id);

		if ( $session ) {
			unset( $session->session_secret );
		}

		// Decrypt encrypted messages for admin display and add avatar URLs
		if ( $messages ) {
			$assistant_avatar = get_option( 'askany_assistant_avatar', '' );
			foreach ( $messages as $message ) {
				$message->message = $this->decrypt_message_for_admin($message->message);

				// Avatar URL based on sender type (sender_id holds the WP user for user/agent rows)
				if ( $message->sender_type === 'user' || $message->sender_type === 'agent' ) {
					$message->avatar_url = ! empty( $message->sender_id ) && (int) $message->sender_id > 0
						? get_avatar_url( (int) $message->sender_id, array( 'size' => 32 ) )
						: '';
				} else {
					// AI assistant / system - custom avatar or default
					$message->avatar_url = ! empty( $assistant_avatar ) ? esc_url( $assistant_avatar ) : '';
				}
			}
		}

		wp_send_json_success(array(
			'messages' => $messages,
			'session' => $session,
		));
	}

	/**
	 * Decrypt message for admin display
	 *
	 * Only messages carrying the legacy client obfuscation envelope
	 * (base64 of "<15 hex padding>|<data>|<reversed padding>") are treated as
	 * legacy-encrypted; any other text (including plain base64-looking text) is shown as-is.
	 *
	 * @param string $message The potentially encrypted message
	 * @return string The decrypted message or original if not encrypted
	 */
	private function decrypt_message_for_admin($message) {
		if ( ! is_string( $message ) || strlen( $message ) <= 50 || ! EncryptionHandler::is_base64_encoded( $message ) ) {
			return $message;
		}

		$decoded = base64_decode( $message, true );
		if ( false === $decoded ) {
			return $message;
		}

		$parts = explode( '|', $decoded );
		$is_legacy_envelope = count( $parts ) === 3
			&& preg_match( '/^[a-f0-9]{15}$/', $parts[0] )
			&& $parts[2] === strrev( $parts[0] );

		if ( ! $is_legacy_envelope ) {
			return $message;
		}

		try {
			$inner = $parts[1];
			if ( EncryptionHandler::is_base64_encoded( $inner ) ) {
				$plain = EncryptionHandler::decode_base64_message( $inner );
				if ( $plain !== $inner && ! EncryptionHandler::is_base64_encoded( $plain ) && wp_check_invalid_utf8( $plain ) === $plain ) {
					return $plain;
				}
			}
		} catch ( \Throwable $e ) {
			EncryptionHandler::log_encryption_event('ADMIN_LEGACY_DECRYPT', 'Found legacy encrypted message', array(
				'message_length' => strlen($message)
			));
		}

		return '🔒 [LEGACY ENCRYPTED MESSAGE - Content secured]';
	}

	/**
	 * Get session info via AJAX for frontend.
	 *
	 * @return void
	 */
	public function get_session_info() {
		nocache_headers();
		$nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';

		if ( ! wp_verify_nonce($nonce, 'askany_chat_nonce') ) {
			wp_send_json_error(array( 'message' => __('Security check failed.', 'askany') ), 403);
		}

		$session_id = $this->read_session_id( $_POST );

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) ) {
			wp_send_json_error(array( 'message' => __('Session ID is required.', 'askany') ));
		}

		$session = $this->database->get_chat_session($session_id);

		if ( ! $session ) {
			// Not created yet (before the first message): nothing to read, not an error.
			wp_send_json_success( array( 'session_exists' => false ) );
		}

		$session_secret = $this->get_request_session_secret();
		if ( ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
		}

		$is_admin = current_user_can( 'manage_options' );

		// Visitor-facing payload: whitelisted, no secret/IP/agent internals.
		$info = array(
			'session_id'        => $session->session_id,
			'user_name'         => $session->user_name,
			'user_display_name' => $session->user_display_name,
			'is_ai_handled'     => (bool) $session->is_ai_handled,
			'agent_id'          => ! empty( $session->agent_id ) ? (int) $session->agent_id : null,
			'has_agent'         => ! empty( $session->agent_id ),
			'status'            => $session->status,
			'created_at'        => $session->created_at,
			'last_activity'     => $session->last_activity,
		);

		if ( $is_admin ) {
			// Get user agent from first message in session
			global $wpdb;
			$messages_table = $wpdb->prefix . 'askany_chat_messages';
			$first_message = $wpdb->get_row( $wpdb->prepare(
				"SELECT user_agent FROM {$messages_table} WHERE session_id = %s AND user_agent IS NOT NULL ORDER BY id ASC LIMIT 1",
				$session_id
			) );

			// Country info from IP (cached per session)
			$country_code = null;
			$country_name = null;
			if ( ! empty( $session->user_ip ) ) {
				$geo_data = $this->get_ip_geolocation( $session->user_ip, $session_id );
				if ( $geo_data ) {
					$country_code = $geo_data['country_code'];
					$country_name = $geo_data['country_name'];
				}
			}

			$info['user_id']             = $session->user_id;
			$info['user_email']          = $session->user_email;
			$info['user_ip']             = $session->user_ip;
			$info['user_agent']          = $first_message ? $first_message->user_agent : null;
			$info['country_code']        = $country_code;
			$info['country_name']        = $country_name;
			$info['previous_session_id'] = ! empty( $session->previous_session_id ) ? $session->previous_session_id : null;
		}

		wp_send_json_success( $info );
	}

	/**
	 * Get IP geolocation data (cached per session / IP, 3s timeout).
	 *
	 * @param string $ip         IP address.
	 * @param string $session_id Session ID (cache key scope).
	 * @return array|null Geolocation data or null.
	 */
	private function get_ip_geolocation( $ip, $session_id = '' ) {
		// Skip private/local IPs
		if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false ) {
			return null;
		}

		$cache_key = 'askany_geo_' . md5( $session_id . '|' . $ip );
		$cached    = get_transient( $cache_key );
		if ( false !== $cached ) {
			return is_array( $cached ) ? $cached : null;
		}

		// Use free IP geolocation API (ip-api.com)
		$response = wp_remote_get( 'http://ip-api.com/json/' . rawurlencode( $ip ) . '?fields=status,country,countryCode', array(
			'timeout' => 3,
		) );

		$result = 'none';
		if ( ! is_wp_error( $response ) ) {
			$data = json_decode( wp_remote_retrieve_body( $response ), true );
			if ( ! empty( $data['status'] ) && $data['status'] === 'success' ) {
				$result = array(
					'country_code' => sanitize_text_field( $data['countryCode'] ?? '' ),
					'country_name' => sanitize_text_field( $data['country'] ?? '' ),
				);
			}
		}

		// Cache hits for a week, misses/failures for an hour.
		set_transient( $cache_key, $result, is_array( $result ) ? WEEK_IN_SECONDS : HOUR_IN_SECONDS );

		return is_array( $result ) ? $result : null;
	}

	/**
	 * Get all messages for a session via AJAX.
	 *
	 * @return void
	 */
	public function get_all_messages() {
		$nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';

		if ( ! wp_verify_nonce($nonce, 'askany_chat_nonce') ) {
			wp_send_json_error(array( 'message' => __('Security check failed.', 'askany') ), 403);
		}

		$session_id = $this->read_session_id( $_POST );

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) ) {
			wp_send_json_error(array( 'message' => __('Session ID is required.', 'askany') ));
		}

		// A brand-new visitor has no session row yet: nothing to read (fail closed, empty result).
		if ( ! $this->database->get_chat_session( $session_id ) ) {
			wp_send_json_success( array( 'messages' => array(), 'last_message_id' => 0, 'session_exists' => false ) );
		}

		$session_secret = $this->get_request_session_secret();
		if ( ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
		}

		// Get all messages for this session ordered by id
		$messages  = $this->database->get_chat_messages($session_id);
		$formatted = $this->database->format_messages_for_client( $messages, current_user_can( 'manage_options' ) );
		$last_id   = 0;
		foreach ( $formatted as $m ) {
			$last_id = max( $last_id, (int) $m['id'] );
		}

		wp_send_json_success(array( 'messages' => $formatted, 'last_message_id' => $last_id ));
	}

	/**
	 * Save user message only (without AI processing) via AJAX.
	 *
	 * @return void
	 */
	public function save_user_message() {
		$nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';

		if ( ! wp_verify_nonce($nonce, 'askany_chat_nonce') ) {
			wp_send_json_error(array( 'message' => __('Security check failed.', 'askany') ), 403);
		}

		$session_id     = $this->read_session_id( $_POST );
		$message        = isset($_POST['message']) ? $this->sanitize_chat_message( $_POST['message'] ) : '';
		$session_secret = $this->get_request_session_secret();

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) || empty($message) ) {
			wp_send_json_error(array( 'message' => __('Session ID and message are required.', 'askany') ));
		}

		$current_user_id = get_current_user_id();

		// Get user data for session creation
		$user_data = array();
		if ( isset($_POST['user_name']) && is_string( $_POST['user_name'] ) ) {
			$user_data['name'] = sanitize_text_field( wp_unslash( $_POST['user_name'] ) );
		}
		if ( isset($_POST['user_email']) && is_string( $_POST['user_email'] ) ) {
			$user_data['email'] = sanitize_email( wp_unslash( $_POST['user_email'] ) );
		}

		// Check if session already exists before creating or updating
		$existing_session = $this->database->get_chat_session($session_id);

		if ( $existing_session && ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
		}

		if ( ! RateLimiter::flood_guard( $session_id, 'chat' ) ) {
			wp_send_json_error( array( 'message' => __( 'You are sending messages too quickly. Please wait a moment.', 'askany' ), 'rate_limit' => true ), 429 );
		}

		if ( ! $existing_session ) {
			// New session: keep the secret this browser already holds (request or HttpOnly cookie).
			if ( strlen( (string) $session_secret ) < 16 ) {
				$session_secret = '' !== \Askany\Database::cookie_session_secret( $session_id )
					? \Askany\Database::cookie_session_secret( $session_id )
					: wp_generate_password( 32, false );
			}
		} elseif ( empty( $session_secret ) ) {
			$session_secret = ( isset( $session_id ) && is_string( $session_id ) && '' !== \Askany\Database::cookie_session_secret( $session_id ) )
				? \Askany\Database::cookie_session_secret( $session_id )
				: wp_generate_password( 32, false );
		}

		if (!$existing_session) {
			// Create brand new session
			$created_id = $this->database->create_chat_session($session_id, $user_data, $session_secret);
			if ( false === $created_id ) {
				wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
				return;
			}
		} else {
			// Update last activity for existing session without changing agent assignment
			global $wpdb;
			$wpdb->update(
				$wpdb->prefix . 'askany_chat_sessions',
				array( 'last_activity' => current_time('mysql'), 'status' => 'active' ),
				array('session_id' => $session_id),
				array( '%s', '%s' ),
				array('%s')
			);
		}

		// Save user message only
		$result = $this->database->save_chat_message($session_id, 'user', $current_user_id, $message);

		if ( $result ) {
			do_action( 'askany_after_message_saved', $session_id, 'user', $message );
			$response_payload = array(
				'message'         => __('Message saved successfully.', 'askany'),
				'user_message_id' => (int) $result,
			);
			if ( ! $existing_session ) {
				$response_payload['session_secret'] = $session_secret;
			}
			wp_send_json_success( $response_payload );
		} else {
			wp_send_json_error(array( 'message' => __('Failed to save message.', 'askany') ));
		}
	}

	/**
	 * Assign agent to chat session via AJAX.
	 *
	 * An empty agent_id releases the session back to the AI.
	 *
	 * @return void
	 */
	public function assign_agent() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$session_id = $this->read_session_id( $_POST );
		$agent_id = isset($_POST['agent_id']) ? intval($_POST['agent_id']) : 0;

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) ) {
			wp_send_json_error(array( 'message' => __('Session ID is required.', 'askany') ));
		}

		if ( ! $this->database->get_chat_session( $session_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Session not found.', 'askany' ) ), 404 );
		}

		if ( $agent_id ) {
			// Validate the agent: a registered agent, or an administrator.
			$agent_user = get_userdata( $agent_id );
			if ( ! $agent_user || ( ! $this->database->is_user_agent( $agent_id ) && ! user_can( $agent_user, 'manage_options' ) ) ) {
				wp_send_json_error( array( 'message' => __( 'Invalid agent.', 'askany' ) ) );
			}

			$status = $this->database->assign_agent_to_session_status( $session_id, $agent_id );

			if ( 'taken' === $status ) {
				wp_send_json_error( array( 'message' => __( 'This chat is already handled by another agent.', 'askany' ), 'code' => 'already_assigned' ) );
			}
			if ( 'invalid' === $status ) {
				wp_send_json_error( array( 'message' => __( 'Failed to update session.', 'askany' ) ) );
			}

			// Post the "joined" system message only when the state actually changed
			if ( 'assigned' === $status ) {
				$this->post_agent_joined_message( $session_id, $agent_id );
			}

			wp_send_json_success( array(
				'message' => __( 'Agent assigned successfully.', 'askany' ),
				'changed' => 'assigned' === $status,
			) );
		}

		if ( get_option( 'askany_prevent_ai_response', false ) ) {
			wp_send_json_error( array( 'message' => __( 'Cannot release to AI: AI auto response is currently disabled in Home Page Settings.', 'askany' ) ) );
		}

		$changed = $this->database->release_agent_from_session_changed( $session_id );
		if ( $changed ) {
			$this->post_ai_takeover_message( $session_id );
		}

		wp_send_json_success( array(
			'message' => __( 'Session released to AI successfully.', 'askany' ),
			'changed' => $changed,
		) );
	}

	/**
	 * Release agent from chat session via AJAX.
	 *
	 * @return void
	 */
	public function release_agent() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		if ( get_option( 'askany_prevent_ai_response', false ) ) {
			wp_send_json_error( array( 'message' => __( 'Cannot release to AI: AI auto response is currently disabled in Home Page Settings.', 'askany' ) ) );
		}

		$session_id = $this->read_session_id( $_POST );

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) ) {
			wp_send_json_error(array( 'message' => __('Session ID is required.', 'askany') ));
		}

		if ( ! $this->database->get_chat_session( $session_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Session not found.', 'askany' ) ), 404 );
		}

		$changed = $this->database->release_agent_from_session_changed( $session_id );

		if ( $changed ) {
			// Uses the configurable label (same as REST + assign_agent)
			$this->post_ai_takeover_message( $session_id );
		}

		wp_send_json_success(array(
			'message' => __('Session released to AI successfully.', 'askany'),
			'changed' => $changed,
		));
	}

	/**
	 * Send agent message via AJAX.
	 *
	 * @return void
	 */
	public function send_agent_message() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$session_id = $this->read_session_id( $_POST );
		$message = isset($_POST['message']) && is_string( $_POST['message'] ) ? wp_kses_post( wp_unslash( $_POST['message'] ) ) : '';

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) || empty($message) ) {
			wp_send_json_error(array( 'message' => __('Session ID and message are required.', 'askany') ));
		}

		$current_user_id = get_current_user_id();

		// Auto-assigns the sending agent when the session has no agent yet
		$result = $this->save_agent_message_with_assignment( $session_id, $message, $current_user_id );

		if ( $result ) {
			// Handle attachments if any
			if ( isset($_POST['attachment_ids']) && is_array($_POST['attachment_ids']) ) {
				global $wpdb;
				$attachments_table = $wpdb->prefix . 'askany_chat_attachments';
				$attachment_ids = array_filter( array_map('absint', $_POST['attachment_ids']) );

				foreach ($attachment_ids as $attachment_id) {
					$wpdb->update(
						$attachments_table,
						array('message_id' => $result),
						array('id' => $attachment_id, 'session_id' => $session_id),
						array('%d'),
						array('%d', '%s')
					);
				}
			}

			// Fire hook for push notifications
			do_action('askany_after_message_saved', $session_id, 'agent', $message);

			wp_send_json_success(array(
				'message'    => __('Message sent successfully.', 'askany'),
				'message_id' => (int) $result,
			));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to send message.', 'askany') ));
		}
	}

	/**
	 * Admin file upload via AJAX.
	 *
	 * Works when only askany_admin_attachments_enabled is on (visitor uploads may be off).
	 *
	 * @return void
	 */
	public function admin_upload_file() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$session_id = $this->read_session_id( $_POST );
		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) ) {
			wp_send_json_error(array( 'message' => __('Session ID is required.', 'askany') ));
		}

		if ( ! $this->database->get_chat_session( $session_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Session not found.', 'askany' ) ), 404 );
		}

		if ( ! isset($_FILES['file']) || ! is_array( $_FILES['file'] ) ) {
			wp_send_json_error(array( 'message' => __('No file uploaded.', 'askany') ));
		}

		// Use FileAttachmentHandler in the admin context (checks askany_admin_attachments_enabled)
		$file_handler = $this->plugin->get_file_attachment_handler();
		$result = $file_handler->handle_upload($_FILES['file'], $session_id, false, 'admin');
		if ( is_wp_error($result) ) {
			wp_send_json_error(array( 'message' => $result->get_error_message() ));
		}

		// Never expose the absolute server path
		unset( $result['file_path'], $result['base64'] );

		wp_send_json_success(array( 'attachment' => $result ));
	}

	/**
	 * AI review message via AJAX (Pro).
	 *
	 * Uses a plain completion (no RAG / order tracking / chatbot fallback) when the
	 * API manager provides complete(); otherwise falls back to get_response().
	 *
	 * @return void
	 */
	public function ai_review_message() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$plugin = \Askany\Plugin::get_instance();
		if ( ! $plugin || ! $plugin->is_pro_active() ) {
			wp_send_json_error( array( 'message' => __( 'AI review is a Pro feature.', 'askany' ), 'code' => 'pro_required' ) );
		}

		$message = isset($_POST['message']) && is_string( $_POST['message'] ) ? wp_kses_post( wp_unslash( $_POST['message'] ) ) : '';
		if ( empty($message) ) {
			wp_send_json_error(array( 'message' => __('Message is required.', 'askany') ));
		}

		$settings = $this->api_manager->get_settings();
		$ai_review_active = !empty($settings['ai_review_enabled']) && $settings['ai_review_enabled'] !== 'false' && $settings['ai_review_enabled'] !== '0';
		if ( !$ai_review_active ) {
			wp_send_json_error(array( 'message' => __('AI review is disabled.', 'askany') ));
		}

		$prompt = !empty($settings['ai_review_prompt']) ? $settings['ai_review_prompt'] : "You are a professional assistant reviewing a draft message from an agent to a user. Please correct any grammar and spelling mistakes. Improve clarity and tone, but keep it friendly and professional. Only output the revised message, nothing else.";

		if ( method_exists( $this->api_manager, 'complete' ) ) {
			$result = $this->api_manager->complete( (string) $prompt, (string) $message, array( 'purpose' => 'ai_review' ) );
			if ( ! is_array( $result ) || empty( $result['ok'] ) || '' === trim( (string) ( $result['text'] ?? '' ) ) ) {
				wp_send_json_error( array( 'message' => __( 'Failed to get AI review.', 'askany' ) ) );
			}
			wp_send_json_success( array( 'reviewed_message' => (string) $result['text'] ) );
		}

		// Legacy fallback: chatbot pipeline with a throwaway session id.
		$query = $prompt . "\n\nDraft Message:\n" . $message;
		$dummy_session_id = 'admin_review_' . get_current_user_id() . '_' . time();

		$response = $this->api_manager->get_response($query, $dummy_session_id);

		if ( ! $response || empty($response['message']) ) {
			wp_send_json_error(array( 'message' => __('Failed to get AI review.', 'askany') ));
		}

		wp_send_json_success(array( 'reviewed_message' => $response['message'] ));
	}

	/**
	 * Merge anonymous session with user account via AJAX.
	 *
	 * @return void
	 */
	public function merge_session() {

		$nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
		if (!wp_verify_nonce($nonce, 'askany_chat_nonce')) {
			wp_send_json_error(array('message' => __('Security check failed.', 'askany')), 403);
		}

		$current_user_id = get_current_user_id();
		if ( ! $current_user_id ) {
			wp_send_json_error(array( 'message' => __('User must be logged in.', 'askany') ));
		}

		$session_id     = $this->read_session_id( $_POST );
		$old_session_id = $this->read_session_id( $_POST, 'old_session_id' );

		if ( false === $session_id || false === $old_session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) ) {
			wp_send_json_error(array( 'message' => __('Session ID is required.', 'askany') ));
		}

		$session_secret = $this->get_request_session_secret();

		// Target session: must exist and belong to caller
		$target = $this->database->get_chat_session( $session_id );
		if ( ! $target ) {
			$created_id = $this->database->create_chat_session( $session_id, array(), $session_secret );
			if ( false === $created_id ) {
				wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
				return;
			}
			$target = $this->database->get_chat_session( $session_id );
		} elseif ( ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
			return;
		}

		// Old session must exist and be verified
		$old_secret = isset( $_POST['old_session_secret'] ) && is_string( $_POST['old_session_secret'] ) ? substr( sanitize_text_field( wp_unslash( $_POST['old_session_secret'] ) ), 0, 64 ) : $session_secret;
		if ( ! empty( $old_session_id ) && $old_session_id !== $session_id ) {
			$old = $this->database->get_chat_session( $old_session_id );
			if ( ! $old || ! $this->database->verify_session_access( $old_session_id, $old_secret ) ) {
				wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
				return;
			}
			if ( ! empty( $old->user_id ) && (int) $old->user_id !== (int) $current_user_id ) {
				wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
				return;
			}
		}

		// Get current user info for session name update
		$current_user = wp_get_current_user();
		$user_name = $current_user->display_name ?: $current_user->user_login;

		// Moves the old session's messages + attachments into $session_id and removes the old row
		$result = $this->database->merge_session_with_user( $session_id, $current_user_id, (string) $old_session_id );

		// Also update the session name from anonymous to logged-in user name
		if ( $result && $user_name ) {
			$this->database->update_session_user_name($session_id, $user_name);
		}

		if ( $result ) {
			wp_send_json_success(array(
				'message' => __('Session merged successfully.', 'askany'),
				'user_name' => $user_name
			));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to merge session.', 'askany') ));
		}
	}

	/**
	 * Set typing indicator via AJAX.
	 *
	 * @return void
	 */
	public function set_typing_indicator() {
		$nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';

		if ( ! wp_verify_nonce( $nonce, 'askany_chat_nonce' ) ) {
			wp_send_json_error(array('message' => __('Security check failed.', 'askany')), 403);
		}

		$session_id = $this->read_session_id( $_POST );
		$is_typing  = isset( $_POST['is_typing'] ) ? (bool) filter_var( wp_unslash( $_POST['is_typing'] ), FILTER_VALIDATE_BOOLEAN ) : false;

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) ) {
			wp_send_json_error(array( 'message' => __('Session ID is required.', 'askany') ));
		}

		// Session must exist (typing never creates sessions or renames visitors)
		if ( ! $this->database->get_chat_session( $session_id ) ) {
			// Normal before the visitor's first message: nothing to do, not an error.
			wp_send_json_success( array( 'session_exists' => false ) );
		}

		$session_secret = $this->get_request_session_secret();
		if ( ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
		}

		$current_user_id = get_current_user_id();

		// The server decides the role: 'agent' only for admins or registered agents.
		$requested_type = isset( $_POST['user_type'] ) && is_string( $_POST['user_type'] ) ? sanitize_key( wp_unslash( $_POST['user_type'] ) ) : 'user';
		$can_be_agent   = $current_user_id && ( current_user_can( 'manage_options' ) || $this->database->is_user_agent( $current_user_id ) );
		$user_type      = ( 'agent' === $requested_type && $can_be_agent ) ? 'agent' : 'user';

		$result = $this->database->set_typing_indicator($session_id, $current_user_id, $user_type, $is_typing);

		if ( $result ) {
			wp_send_json_success(array( 'message' => __('Typing indicator updated.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to update typing indicator.', 'askany') ));
		}
	}

	/**
	 * Get real-time updates for a session (polling).
	 *
	 * Id-based: the client sends last_message_id (0 → latest 50 messages).
	 *
	 * @return void
	 */
	public function get_realtime_updates() {
		nocache_headers();
		$nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
		if ( ! wp_verify_nonce( $nonce, 'askany_chat_nonce' ) ) {
			wp_send_json_error(array('message' => __('Security check failed.', 'askany')), 403);
			return;
		}

		$session_id      = $this->read_session_id( $_POST );
		$last_message_id = isset($_POST['last_message_id']) ? absint($_POST['last_message_id']) : 0;

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
			return;
		}
		if ( empty($session_id) ) {
			wp_send_json_error(array( 'message' => __('Session ID is required.', 'askany') ));
			return;
		}

		$is_admin = current_user_can( 'manage_options' );

		// Fetch the session once; no row = nothing to read (fail closed with an empty result).
		$session = $this->database->get_chat_session( $session_id );
		if ( ! $session ) {
			wp_send_json_success( array(
				'messages'          => array(),
				'typing_indicators' => array(),
				'session_info'      => null,
				'last_message_id'   => $last_message_id,
				'has_active_agent'  => false,
				'agent_id'          => null,
				'session_exists'    => false,
			) );
			return;
		}

		if ( ! $is_admin && ! $this->database->verify_session_access( $session_id, $this->get_request_session_secret() ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
			return;
		}

		if ( ! $is_admin && ! RateLimiter::flood_guard( $session_id, 'poll' ) ) {
			wp_send_json_error( array( 'message' => __( 'Too many requests.', 'askany' ), 'rate_limit' => true ), 429 );
			return;
		}

		if ( $is_admin ) {
			$this->touch_agent_presence();
		}

		// Purge stale typing rows at most once a minute (not on every poll).
		$this->database->cleanup_typing_indicators();

		$updates = $this->database->get_realtime_updates( $session_id, null, $last_message_id, $session, $is_admin );

		if ( $is_admin ) {
			foreach ( $updates['messages'] as &$msg ) {
				$msg['message'] = $this->decrypt_message_for_admin( $msg['message'] );
			}
			unset( $msg );
		}

		// Agent status for smart polling
		$updates['has_active_agent'] = ! empty( $session->agent_id );
		$updates['agent_id']         = ! empty( $session->agent_id ) ? (int) $session->agent_id : null;
		$updates['last_check']       = current_time('mysql');

		wp_send_json_success($updates);
	}

	/**
	 * Get batch real-time updates for multiple sessions (agent admin panel).
	 *
	 * Clients send last_id_<md5(session_id)> = last seen message id per session.
	 *
	 * @return void
	 */
	public function get_batch_realtime_updates() {
		// Verify admin nonce
		$nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
		if (!wp_verify_nonce($nonce, 'askany_chat_nonce')) {
			wp_send_json_error(array('message' => __('Security check failed.', 'askany')), 403);
			return;
		}

		// Only admins/agents can use batch polling
		if (!current_user_can('manage_options')) {
			wp_send_json_error(array('message' => __('Unauthorized.', 'askany')));
			return;
		}

		$this->touch_agent_presence();

		// Get current agent's ID
		$agent_id = get_current_user_id();

		// Get all sessions assigned to this agent
		global $wpdb;
		$sessions_table = $wpdb->prefix . 'askany_chat_sessions';
		$agent_sessions = $wpdb->get_results($wpdb->prepare(
			"SELECT session_id FROM {$sessions_table} WHERE agent_id = %d AND status = 'active' ORDER BY last_activity DESC LIMIT 50",
			$agent_id
		));

		if (empty($agent_sessions)) {
			wp_send_json_success(array(
				'updates' => array(),
				'session_count' => 0,
				'update_count' => 0
			));
			return;
		}

		$all_updates = array();

		foreach ($agent_sessions as $session) {
			$session_id = $session->session_id;

			$last_id_key     = 'last_id_' . md5($session_id);
			$last_message_id = isset($_POST[$last_id_key]) ? absint($_POST[$last_id_key]) : 0;

			$updates = $this->database->get_realtime_updates($session_id, null, $last_message_id, null, true);

			if (!empty($updates['messages']) || !empty($updates['typing_indicators'])) {
				$all_updates[$session_id] = $updates;
			}
		}

		wp_send_json_success(array(
			'updates' => $all_updates,
			'session_count' => count($agent_sessions),
			'update_count' => count($all_updates),
			'timestamp' => current_time('mysql')
		));
	}

	/**
	 * Poll for new messages via AJAX (for admin interface).
	 *
	 * @return void
	 */
	public function poll_messages() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$session_id      = $this->read_session_id( $_POST );
		$last_message_id = isset( $_POST['last_message_id'] ) ? absint( $_POST['last_message_id'] ) : 0;

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) ) {
			wp_send_json_error(array( 'message' => __('Session ID is required.', 'askany') ));
		}

		$this->touch_agent_presence();

		$updates = $this->database->get_realtime_updates( $session_id, null, $last_message_id, null, true );

		// Also get updated session list for admin
		$updates['sessions'] = $this->database->get_active_chat_sessions( 100 );

		wp_send_json_success($updates);
	}

	// ===== HOME PAGE MANAGEMENT AJAX HANDLERS =====

	/**
	 * Save home page settings via AJAX.
	 *
	 * @return void
	 */
	public function save_home_settings() {
		// Different nonce verification based on request type
		$type = isset($_POST['type']) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : '';

		if ( $type === 'toggle' ) {
			// For toggle operations, use the admin nonce
			check_ajax_referer('askany_chat_nonce', 'nonce');
		} else {
			// For form submissions, use the home settings nonce
			check_ajax_referer('askany_save_home_settings', 'askany_home_nonce');
		}

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$settings_to_update = array();

		switch ( $type ) {
			case 'general':
				$social_channels_data = array();
				if ( isset($_POST['social_channels']) && is_array($_POST['social_channels']) ) {
					$unslashed_channels = wp_unslash($_POST['social_channels']);
					foreach ( $unslashed_channels as $ch_id => $ch_config ) {
						if ( ! is_array( $ch_config ) ) {
							continue;
						}
						$social_channels_data[ sanitize_key( $ch_id ) ] = array(
							'enabled'          => ! empty( $ch_config['enabled'] ) ? 1 : 0,
							'open_in_new_tab'  => ! empty( $ch_config['open_in_new_tab'] ) ? 1 : 0,
							'url'              => ! empty( $ch_config['url'] ) ? esc_url_raw( $ch_config['url'] ) : '',
							'phone'            => ! empty( $ch_config['phone'] ) ? sanitize_text_field( $ch_config['phone'] ) : '',
							'message'          => ! empty( $ch_config['message'] ) ? sanitize_text_field( $ch_config['message'] ) : '',
							'include_site_url' => ! empty( $ch_config['include_site_url'] ) ? 1 : 0,
						);
					}
				}

				$settings_to_update = array(
					'home_page_enabled'    => isset($_POST['home_page_enabled']) ? 1 : 0,
					'home_logo'            => isset($_POST['home_logo']) ? esc_url_raw( wp_unslash( $_POST['home_logo'] ) ) : '',
					'home_welcome_message' => isset($_POST['home_welcome_message']) ? sanitize_text_field( wp_unslash( $_POST['home_welcome_message'] ) ) : '',
					'show_agents'          => isset($_POST['show_agents']) ? 1 : 0,
					'show_agent_name'      => isset($_POST['show_agent_name']) ? 1 : 0,
					'show_chat_tab'        => isset($_POST['show_chat_tab']) ? 1 : 0,
					'prevent_ai_response'  => isset($_POST['prevent_ai_response']) ? 1 : 0,
					'show_social_channels' => isset($_POST['show_social_channels']) ? 1 : 0,
					'show_transcript'      => isset($_POST['show_transcript']) ? 1 : 0,
					'social_channels'      => $social_channels_data,
				);
				break;

			case 'getstarted':
				$settings_to_update = array(
					'get_started_enabled' => isset($_POST['get_started_enabled']) ? 1 : 0,
					'get_started_in_chat_only' => isset($_POST['get_started_in_chat_only']) ? 1 : 0,
					'get_started_title' => isset($_POST['get_started_title']) ? sanitize_text_field( wp_unslash( $_POST['get_started_title'] ) ) : '',
					'get_started_description' => isset($_POST['get_started_description']) ? sanitize_textarea_field( wp_unslash( $_POST['get_started_description'] ) ) : '',
					'get_started_name_label' => isset($_POST['get_started_name_label']) ? sanitize_text_field( wp_unslash( $_POST['get_started_name_label'] ) ) : '',
					'get_started_email_label' => isset($_POST['get_started_email_label']) ? sanitize_text_field( wp_unslash( $_POST['get_started_email_label'] ) ) : '',
					'get_started_button_text' => isset($_POST['get_started_button_text']) ? sanitize_text_field( wp_unslash( $_POST['get_started_button_text'] ) ) : '',
					'get_started_name_required' => isset($_POST['get_started_name_required']) ? 1 : 0,
					'get_started_email_required' => isset($_POST['get_started_email_required']) ? 1 : 0,
				);
				break;

			case 'documentation':
				$settings_to_update = array(
					'show_documentation' => isset($_POST['show_documentation']) ? 1 : 0,
					'documentation_url' => isset($_POST['documentation_url']) ? esc_url_raw( wp_unslash( $_POST['documentation_url'] ) ) : '',
					'documentation_description' => isset($_POST['documentation_description']) ? sanitize_text_field( wp_unslash( $_POST['documentation_description'] ) ) : '',
				);
				break;

			case 'news':
				$selected_posts = isset($_POST['news_selected_posts']) && is_array($_POST['news_selected_posts']) 
					? array_map('intval', $_POST['news_selected_posts']) 
					: array();
				
				$settings_to_update = array(
					'news_enabled' => isset($_POST['news_enabled']) ? 1 : 0,
					'news_label' => isset($_POST['news_label']) ? sanitize_text_field( wp_unslash( $_POST['news_label'] ) ) : __('News', 'askany'),
					'news_mode' => isset($_POST['news_mode']) && in_array($_POST['news_mode'], array('latest', 'selected')) ? sanitize_text_field( wp_unslash( $_POST['news_mode'] ) ) : 'latest',
					'news_timeframe' => isset($_POST['news_timeframe']) ? intval($_POST['news_timeframe']) : 7,
					'news_count' => isset($_POST['news_count']) ? max(1, min(20, intval($_POST['news_count']))) : 5,
					'news_excerpt_length' => isset($_POST['news_excerpt_length']) ? max(50, min(500, intval($_POST['news_excerpt_length']))) : 150,
					'news_selected_posts' => $selected_posts,
				);
				break;

			case 'report_issue':
				$settings_to_update = array(
					'report_issue_enabled' => isset($_POST['report_issue_enabled']) ? 1 : 0,
					'report_issue_title' => isset($_POST['report_issue_title']) ? sanitize_text_field( wp_unslash( $_POST['report_issue_title'] ) ) : __('Report an issue?', 'askany'),
					'report_issue_description' => isset($_POST['report_issue_description']) ? sanitize_text_field( wp_unslash( $_POST['report_issue_description'] ) ) : __('Found a bug? Let us know.', 'askany'),
					'report_issue_message' => isset($_POST['report_issue_message']) ? sanitize_textarea_field( wp_unslash( $_POST['report_issue_message'] ) ) : __('Hey 👋 Found a bug? Please describe the issue below. Also, please share your name and email so we can follow up with you.', 'askany'),
				);
				break;

			case 'feature_request':
				$settings_to_update = array(
					'feature_request_enabled' => isset($_POST['feature_request_enabled']) ? 1 : 0,
					'feature_request_title' => isset($_POST['feature_request_title']) ? sanitize_text_field( wp_unslash( $_POST['feature_request_title'] ) ) : __('Request a feature', 'askany'),
					'feature_request_description' => isset($_POST['feature_request_description']) ? sanitize_text_field( wp_unslash( $_POST['feature_request_description'] ) ) : __('What would you like to see next?', 'askany'),
					'feature_request_message' => isset($_POST['feature_request_message']) ? sanitize_textarea_field( wp_unslash( $_POST['feature_request_message'] ) ) : __('What feature would you like to see next? Share your name, email and feature details so we can consider it for future updates.', 'askany'),
				);
				break;

			case 'attachments':
				$allowed_types = isset($_POST['attachments_allowed_types']) && is_array($_POST['attachments_allowed_types']) 
					? array_map('sanitize_text_field', $_POST['attachments_allowed_types']) 
					: array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf' );
				
				$settings_to_update = array(
					'attachments_enabled' => isset($_POST['attachments_enabled']) ? 1 : 0,
					'admin_attachments_enabled' => isset($_POST['admin_attachments_enabled']) ? 1 : 0,
					'attachments_ai_analysis' => isset($_POST['attachments_ai_analysis']) ? 1 : 0,
					'attachments_allowed_types' => $allowed_types,
					'attachments_max_size' => isset($_POST['attachments_max_size']) ? max(1, min(100, intval($_POST['attachments_max_size']))) : 10,
					'attachments_max_files' => isset($_POST['attachments_max_files']) ? max(1, min(20, intval($_POST['attachments_max_files']))) : 5,
				);
				break;
				
			case 'ai_assistant':
				$settings_to_update = array(
					'ai_review_enabled' => isset($_POST['ai_review_enabled']) ? 1 : 0,
					'ai_review_prompt' => isset($_POST['ai_review_prompt']) ? sanitize_textarea_field( wp_unslash( $_POST['ai_review_prompt'] ) ) : '',
				);
				break;

			case 'chat_widget':
				$raw_questions = isset( $_POST['suggested_questions'] ) && is_array( $_POST['suggested_questions'] )
					? $_POST['suggested_questions']
					: array();
				$suggested_questions = array_values(
					array_filter(
						array_map( 'sanitize_text_field', array_map( 'wp_unslash', $raw_questions ) )
					)
				);

				$settings_to_update = array(
					'chat_welcome_enabled'        => isset( $_POST['chat_welcome_enabled'] ) ? 1 : 0,
					'chat_welcome_message'        => isset( $_POST['chat_welcome_message'] ) ? sanitize_text_field( wp_unslash( $_POST['chat_welcome_message'] ) ) : __( "Hi! I'm your AI assistant. How can I help you today? 👋", 'askany' ),
					'suggested_questions_enabled' => isset( $_POST['suggested_questions_enabled'] ) ? 1 : 0,
					'suggested_questions'         => $suggested_questions,
				);
				break;

			case 'frontend_labels':
				$settings_to_update = array(
					// Frontend label customization
					'label_online'              => sanitize_text_field( wp_unslash( $_POST['askany_label_online'] ?? '' ) )              ?: __( 'Online', 'askany' ),
					'label_tab_home'            => sanitize_text_field( wp_unslash( $_POST['askany_label_tab_home'] ?? '' ) )            ?: __( 'Home', 'askany' ),
					'label_tab_messages'        => sanitize_text_field( wp_unslash( $_POST['askany_label_tab_messages'] ?? '' ) )        ?: __( 'Messages', 'askany' ),
					'label_tab_chat'            => sanitize_text_field( wp_unslash( $_POST['askany_label_tab_chat'] ?? '' ) )            ?: __( 'Chat', 'askany' ),
					'label_tab_contact'         => sanitize_text_field( wp_unslash( $_POST['askany_label_tab_contact'] ?? '' ) )         ?: __( 'Contact', 'askany' ),
					'label_faq_title'           => sanitize_text_field( wp_unslash( $_POST['askany_label_faq_title'] ?? '' ) )           ?: __( 'Frequently Asked Questions', 'askany' ),
					'label_suggested_questions' => sanitize_text_field( wp_unslash( $_POST['askany_label_suggested_questions'] ?? '' ) ) ?: __( 'Suggested Questions:', 'askany' ),
					'label_doc_title'           => sanitize_text_field( wp_unslash( $_POST['askany_label_doc_title'] ?? '' ) )           ?: __( 'Visit our Documentation', 'askany' ),
					'documentation_description' => sanitize_text_field( wp_unslash( $_POST['askany_documentation_description'] ?? '' ) ) ?: __( 'Find detailed guides and tutorials', 'askany' ),
					'report_issue_title'        => sanitize_text_field( wp_unslash( $_POST['askany_report_issue_title'] ?? '' ) )        ?: __( 'Report an issue?', 'askany' ),
					'report_issue_description'  => sanitize_text_field( wp_unslash( $_POST['askany_report_issue_description'] ?? '' ) )  ?: __( 'Found a bug? Let us know.', 'askany' ),
					'feature_request_title'     => sanitize_text_field( wp_unslash( $_POST['askany_feature_request_title'] ?? '' ) )     ?: __( 'Request a feature', 'askany' ),
					'feature_request_description'=> sanitize_text_field( wp_unslash( $_POST['askany_feature_request_description'] ?? '' ) ) ?: __( 'What would you like to see next?', 'askany' ),
					'label_download_transcript' => sanitize_text_field( wp_unslash( $_POST['askany_label_download_transcript'] ?? '' ) ) ?: __( 'Download Transcript', 'askany' ),
					'label_start_new_chat'      => sanitize_text_field( wp_unslash( $_POST['askany_label_start_new_chat'] ?? '' ) )      ?: __( 'Start New Chat', 'askany' ),
					'label_clear_chat_history'  => sanitize_text_field( wp_unslash( $_POST['askany_label_clear_chat_history'] ?? '' ) )  ?: __( 'Clear Chat History', 'askany' ),

					// Fallback & Offline
					'fallback_static_message'         => sanitize_textarea_field( wp_unslash( $_POST['askany_fallback_static_message'] ?? '' ) ) ?: __( 'Thanks for your queries, we have sent your queries to our support agent, and once they are online they will reach you.', 'askany' ),
					'label_fallback_greeting'         => sanitize_text_field( wp_unslash( $_POST['askany_label_fallback_greeting'] ?? '' ) ) ?: __( 'Hello! How can I help you today?', 'askany' ),
					'label_fallback_no_match'         => sanitize_textarea_field( wp_unslash( $_POST['askany_label_fallback_no_match'] ?? '' ) ) ?: __( 'I am sorry, I do not have enough information to answer that right now. Please reach out to our support team for assistance.', 'askany' ),
					'label_api_disabled'              => sanitize_text_field( wp_unslash( $_POST['askany_label_api_disabled'] ?? '' ) ) ?: __( 'AI assistant is currently offline. Please try again later or contact support.', 'askany' ),
					'label_ai_unavailable'            => sanitize_text_field( wp_unslash( $_POST['askany_label_ai_unavailable'] ?? '' ) ) ?: __( 'AI service is temporarily unavailable. Please try again in a few moments.', 'askany' ),

					// Live Agent & Chat Handoff
					'label_agent_joined'              => sanitize_text_field( wp_unslash( $_POST['askany_label_agent_joined'] ?? '' ) ) ?: __( 'Live agent {name} has joined the conversation.', 'askany' ),
					'label_ai_takeover'               => sanitize_text_field( wp_unslash( $_POST['askany_label_ai_takeover'] ?? '' ) ) ?: __( 'Live agent ended the session. AI assistant is back online.', 'askany' ),
					'label_agent_pending'             => sanitize_text_field( wp_unslash( $_POST['askany_label_agent_pending'] ?? '' ) ) ?: __( 'A live agent is currently assisting another visitor or waiting for connection. Please hold on a moment...', 'askany' ),

					// WooCommerce & Product Cards
					'label_add_to_cart'               => sanitize_text_field( wp_unslash( $_POST['askany_label_add_to_cart'] ?? '' ) ) ?: __( 'Add to Cart', 'askany' ),
					'label_view_details'              => sanitize_text_field( wp_unslash( $_POST['askany_label_view_details'] ?? '' ) ) ?: __( 'View Details', 'askany' ),
					'label_view_product_page'         => sanitize_text_field( wp_unslash( $_POST['askany_label_view_product_page'] ?? '' ) ) ?: __( 'View Full Product Page', 'askany' ),
					'label_qty'                       => sanitize_text_field( wp_unslash( $_POST['askany_label_qty'] ?? $_POST['askany_label_quantity'] ?? '' ) ) ?: __( 'Quantity:', 'askany' ),
					'label_quantity'                  => sanitize_text_field( wp_unslash( $_POST['askany_label_qty'] ?? $_POST['askany_label_quantity'] ?? '' ) ) ?: __( 'Quantity:', 'askany' ),
					'label_product_attrs'             => sanitize_text_field( wp_unslash( $_POST['askany_label_product_attrs'] ?? $_POST['askany_label_product_attributes'] ?? '' ) ) ?: __( 'Product Attributes:', 'askany' ),
					'label_product_attributes'        => sanitize_text_field( wp_unslash( $_POST['askany_label_product_attrs'] ?? $_POST['askany_label_product_attributes'] ?? '' ) ) ?: __( 'Product Attributes:', 'askany' ),
					'label_cart_added_single'         => sanitize_text_field( wp_unslash( $_POST['askany_label_cart_added_single'] ?? '' ) ) ?: __( 'Added "{product}" to your cart.', 'askany' ),
					'label_cart_added_multiple'       => sanitize_text_field( wp_unslash( $_POST['askany_label_cart_added_multiple'] ?? '' ) ) ?: __( 'Added {quantity}x "{product}" to your cart.', 'askany' ),
					'label_cart_error'                => sanitize_text_field( wp_unslash( $_POST['askany_label_cart_error'] ?? '' ) ) ?: __( 'Failed to add product to cart. Please try again.', 'askany' ),
					'label_products_found_intro'      => sanitize_text_field( wp_unslash( $_POST['askany_label_products_found_intro'] ?? '' ) ) ?: __( 'I found some great products for you! Check them out below.', 'askany' ),

					// Modals & Confirmation Dialogs
					'label_clear_confirm_title'       => sanitize_text_field( wp_unslash( $_POST['askany_label_clear_confirm_title'] ?? $_POST['askany_label_modal_clear_title'] ?? '' ) ) ?: __( 'Clear Chat History?', 'askany' ),
					'label_modal_clear_title'         => sanitize_text_field( wp_unslash( $_POST['askany_label_clear_confirm_title'] ?? $_POST['askany_label_modal_clear_title'] ?? '' ) ) ?: __( 'Clear Chat History?', 'askany' ),
					'label_clear_confirm_desc'        => sanitize_textarea_field( wp_unslash( $_POST['askany_label_clear_confirm_desc'] ?? $_POST['askany_label_modal_clear_body'] ?? '' ) ) ?: __( 'Are you sure you want to clear your chat history? This cannot be undone.', 'askany' ),
					'label_modal_clear_body'          => sanitize_text_field( wp_unslash( $_POST['askany_label_clear_confirm_desc'] ?? $_POST['askany_label_modal_clear_body'] ?? '' ) ) ?: __( 'Are you sure you want to clear your chat history?', 'askany' ),
					'label_modal_clear_note'          => sanitize_textarea_field( wp_unslash( $_POST['askany_label_modal_clear_note'] ?? '' ) ) ?: __( 'This will only clear messages from your browser. Admins will still be able to see the conversation history.', 'askany' ),
					'label_btn_confirm_clear'         => sanitize_text_field( wp_unslash( $_POST['askany_label_btn_confirm_clear'] ?? $_POST['askany_label_modal_clear_btn'] ?? '' ) ) ?: __( 'Clear History', 'askany' ),
					'label_modal_clear_btn'           => sanitize_text_field( wp_unslash( $_POST['askany_label_btn_confirm_clear'] ?? $_POST['askany_label_modal_clear_btn'] ?? '' ) ) ?: __( 'Clear History', 'askany' ),
					'label_btn_cancel'                => sanitize_text_field( wp_unslash( $_POST['askany_label_btn_cancel'] ?? $_POST['askany_label_modal_cancel_btn'] ?? '' ) ) ?: __( 'Cancel', 'askany' ),
					'label_modal_cancel_btn'          => sanitize_text_field( wp_unslash( $_POST['askany_label_btn_cancel'] ?? $_POST['askany_label_modal_cancel_btn'] ?? '' ) ) ?: __( 'Cancel', 'askany' ),
					'label_new_chat_confirm_title'    => sanitize_text_field( wp_unslash( $_POST['askany_label_new_chat_confirm_title'] ?? $_POST['askany_label_modal_new_chat_title'] ?? '' ) ) ?: __( 'Start New Chat?', 'askany' ),
					'label_modal_new_chat_title'      => sanitize_text_field( wp_unslash( $_POST['askany_label_new_chat_confirm_title'] ?? $_POST['askany_label_modal_new_chat_title'] ?? '' ) ) ?: __( 'Start New Chat?', 'askany' ),
					'label_new_chat_confirm_desc'     => sanitize_textarea_field( wp_unslash( $_POST['askany_label_new_chat_confirm_desc'] ?? $_POST['askany_label_modal_new_chat_body'] ?? '' ) ) ?: __( 'This will end the current session and start a fresh conversation.', 'askany' ),
					'label_modal_new_chat_body'       => sanitize_textarea_field( wp_unslash( $_POST['askany_label_new_chat_confirm_desc'] ?? $_POST['askany_label_modal_new_chat_body'] ?? '' ) ) ?: __( 'This will end the current session and start a fresh conversation.', 'askany' ),
					'label_btn_confirm_new_chat'      => sanitize_text_field( wp_unslash( $_POST['askany_label_btn_confirm_new_chat'] ?? $_POST['askany_label_modal_new_chat_btn'] ?? '' ) ) ?: __( 'Start New', 'askany' ),
					'label_modal_new_chat_btn'        => sanitize_text_field( wp_unslash( $_POST['askany_label_btn_confirm_new_chat'] ?? $_POST['askany_label_modal_new_chat_btn'] ?? '' ) ) ?: __( 'Start New', 'askany' ),
					'label_download_transcript_title' => sanitize_text_field( wp_unslash( $_POST['askany_label_download_transcript_title'] ?? '' ) ) ?: __( 'Download Chat Transcript', 'askany' ),
					'label_modal_no_session_title'    => sanitize_text_field( wp_unslash( $_POST['askany_label_modal_no_session_title'] ?? '' ) ) ?: __( 'No Active Chat Session', 'askany' ),
					'label_modal_no_session_body'     => sanitize_textarea_field( wp_unslash( $_POST['askany_label_modal_no_session_body'] ?? '' ) ) ?: __( 'Please send a message first before downloading the transcript.', 'askany' ),
					'label_modal_dl_failed_title'     => sanitize_text_field( wp_unslash( $_POST['askany_label_modal_dl_failed_title'] ?? '' ) ) ?: __( 'Download Failed', 'askany' ),
					'label_modal_dl_failed_body'      => sanitize_textarea_field( wp_unslash( $_POST['askany_label_modal_dl_failed_body'] ?? '' ) ) ?: __( 'Could not download transcript.', 'askany' ),
					'label_modal_net_error_title'     => sanitize_text_field( wp_unslash( $_POST['askany_label_modal_net_error_title'] ?? '' ) ) ?: __( 'Network Error', 'askany' ),
					'label_modal_net_error_body'      => sanitize_textarea_field( wp_unslash( $_POST['askany_label_modal_net_error_body'] ?? '' ) ) ?: __( 'Could not reach the server. Please check your connection and try again.', 'askany' ),
					'label_modal_ok_btn'              => sanitize_text_field( wp_unslash( $_POST['askany_label_modal_ok_btn'] ?? '' ) ) ?: __( 'OK', 'askany' ),

					// Chat Interface & System Helpers
					'label_typing'                    => sanitize_text_field( wp_unslash( $_POST['askany_label_typing'] ?? $_POST['askany_label_typing_indicator'] ?? '' ) ) ?: __( '{name} is typing...', 'askany' ),
					'label_typing_indicator'          => sanitize_text_field( wp_unslash( $_POST['askany_label_typing'] ?? $_POST['askany_label_typing_indicator'] ?? '' ) ) ?: __( '{name} is typing...', 'askany' ),
					'label_typing_default_name'       => sanitize_text_field( wp_unslash( $_POST['askany_label_typing_default_name'] ?? '' ) ) ?: __( 'Someone', 'askany' ),
					'label_user_typing'               => sanitize_text_field( wp_unslash( $_POST['askany_label_user_typing'] ?? '' ) ) ?: __( 'You are typing', 'askany' ),
					'label_sending'                   => sanitize_text_field( wp_unslash( $_POST['askany_label_sending'] ?? '' ) ) ?: __( 'Sending...', 'askany' ),
					'label_input_error'               => sanitize_text_field( wp_unslash( $_POST['askany_label_input_error'] ?? '' ) ) ?: __( 'Error occurred. Please try again.', 'askany' ),
					'label_empty_input_warning'       => sanitize_text_field( wp_unslash( $_POST['askany_label_empty_input_warning'] ?? $_POST['askany_label_enter_message'] ?? '' ) ) ?: __( 'Please enter a message.', 'askany' ),
					'label_enter_message'             => sanitize_text_field( wp_unslash( $_POST['askany_label_empty_input_warning'] ?? $_POST['askany_label_enter_message'] ?? '' ) ) ?: __( 'Please enter a message.', 'askany' ),
					'label_searching_web'             => sanitize_text_field( wp_unslash( $_POST['askany_label_searching_web'] ?? '' ) ) ?: __( 'Searching the web for latest information...', 'askany' ),
					'label_brand_footer'              => sanitize_text_field( wp_unslash( $_POST['askany_label_brand_footer'] ?? $_POST['askany_label_footer_branding'] ?? '' ) ) ?: __( 'Secure Chat by AskAny', 'askany' ),
					'label_footer_branding'           => sanitize_text_field( wp_unslash( $_POST['askany_label_brand_footer'] ?? $_POST['askany_label_footer_branding'] ?? '' ) ) ?: __( 'Secure Chat by AskAny', 'askany' ),
					'label_no_news'                   => sanitize_text_field( wp_unslash( $_POST['askany_label_no_news'] ?? $_POST['askany_label_news_empty'] ?? '' ) ) ?: __( 'No news available at the moment.', 'askany' ),
					'label_news_empty'                => sanitize_text_field( wp_unslash( $_POST['askany_label_no_news'] ?? $_POST['askany_label_news_empty'] ?? '' ) ) ?: __( 'No news available at the moment.', 'askany' ),
					'label_copied_url'                => sanitize_text_field( wp_unslash( $_POST['askany_label_copied_url'] ?? '' ) ) ?: __( 'Page URL copied to clipboard!', 'askany' ),
					'label_max_files_exceeded'        => sanitize_text_field( wp_unslash( $_POST['askany_label_max_files_exceeded'] ?? '' ) ) ?: __( 'You can upload a maximum of {max} files at once.', 'askany' ),
					'label_file_too_large'            => sanitize_text_field( wp_unslash( $_POST['askany_label_file_too_large'] ?? '' ) ) ?: __( 'File "{name}" exceeds the 10MB size limit.', 'askany' ),
					'label_file_already_attached'     => sanitize_text_field( wp_unslash( $_POST['askany_label_file_already_attached'] ?? '' ) ) ?: __( 'File "{name}" has already been attached.', 'askany' ),
					'label_attachment_error_prefix'   => sanitize_text_field( wp_unslash( $_POST['askany_label_attachment_error_prefix'] ?? '' ) ) ?: __( 'File attachment errors:', 'askany' ),
					'label_search_no_results'         => sanitize_text_field( wp_unslash( $_POST['askany_label_search_no_results'] ?? '' ) ) ?: __( "I couldn't find any relevant results for that search.", 'askany' ),
					'label_search_failed'             => sanitize_text_field( wp_unslash( $_POST['askany_label_search_failed'] ?? '' ) ) ?: __( 'Sorry, the internet search failed. Please try again later.', 'askany' ),

					// i18n JS strings
					'i18n_greeting_with_name'         => sanitize_text_field( wp_unslash( $_POST['askany_i18n_greeting_with_name'] ?? '' ) )          ?: 'Hi {name}! 👋 How can I help you today?',
					'i18n_welcome_title_with_name'    => sanitize_text_field( wp_unslash( $_POST['askany_i18n_welcome_title_with_name'] ?? '' ) )     ?: 'Hi {name}',
					'i18n_welcome_title_no_name'      => sanitize_text_field( wp_unslash( $_POST['askany_i18n_welcome_title_no_name'] ?? '' ) )       ?: 'Hi',
					'i18n_please_wait'                => sanitize_text_field( wp_unslash( $_POST['askany_i18n_please_wait'] ?? '' ) )                 ?: 'Please wait...',
					'i18n_form_error'                 => sanitize_text_field( wp_unslash( $_POST['askany_i18n_form_error'] ?? '' ) )                  ?: 'Error submitting form. Please try again.',
					'i18n_fill_required'              => sanitize_text_field( wp_unslash( $_POST['askany_i18n_fill_required'] ?? '' ) )               ?: '❌ Please fill out all required fields.',
					'i18n_invalid_email'              => sanitize_text_field( wp_unslash( $_POST['askany_i18n_invalid_email'] ?? '' ) )               ?: 'Please enter a valid email address.',
					'i18n_submitting'                 => sanitize_text_field( wp_unslash( $_POST['askany_i18n_submitting'] ?? '' ) )                  ?: 'Submitting...',
					'i18n_report_form_title'          => sanitize_text_field( wp_unslash( $_POST['askany_i18n_report_form_title'] ?? '' ) )           ?: 'Report an Issue',
					'i18n_feature_form_title'         => sanitize_text_field( wp_unslash( $_POST['askany_i18n_feature_form_title'] ?? '' ) )          ?: 'Request a Feature',
					'i18n_form_description'           => sanitize_text_field( wp_unslash( $_POST['askany_i18n_form_description'] ?? '' ) )            ?: 'Please fill out the form below with your details.',
					'i18n_label_name'                 => sanitize_text_field( wp_unslash( $_POST['askany_i18n_label_name'] ?? '' ) )                  ?: 'Your Name',
					'i18n_placeholder_name'           => sanitize_text_field( wp_unslash( $_POST['askany_i18n_placeholder_name'] ?? '' ) )            ?: 'Enter your name',
					'i18n_label_email'                => sanitize_text_field( wp_unslash( $_POST['askany_i18n_label_email'] ?? '' ) )                 ?: 'Your Email',
					'i18n_placeholder_email'          => sanitize_text_field( wp_unslash( $_POST['askany_i18n_placeholder_email'] ?? '' ) )           ?: 'Enter your email',
					'i18n_label_issue_details'        => sanitize_text_field( wp_unslash( $_POST['askany_i18n_label_issue_details'] ?? '' ) )         ?: 'Issue Details',
					'i18n_placeholder_issue_details'  => sanitize_text_field( wp_unslash( $_POST['askany_i18n_placeholder_issue_details'] ?? '' ) )   ?: 'Describe the bug or issue you encountered...',
					'i18n_label_feature_details'      => sanitize_text_field( wp_unslash( $_POST['askany_i18n_label_feature_details'] ?? '' ) )       ?: 'Feature Details',
					'i18n_placeholder_feature_details'=> sanitize_text_field( wp_unslash( $_POST['askany_i18n_placeholder_feature_details'] ?? '' ) ) ?: 'Describe the feature you would like to see...',
					'i18n_submit_bug_report'          => sanitize_text_field( wp_unslash( $_POST['askany_i18n_submit_bug_report'] ?? '' ) )           ?: 'Submit Bug Report',
					'i18n_submit_feature_request'     => sanitize_text_field( wp_unslash( $_POST['askany_i18n_submit_feature_request'] ?? '' ) )      ?: 'Submit Feature Request',
					'i18n_bug_success'                => sanitize_textarea_field( wp_unslash( $_POST['askany_i18n_bug_success'] ?? '' ) )             ?: "✅ Thank you! Your bug report has been submitted successfully.\n\n📋 Your tracking ID: {trackingId}",
					'i18n_feature_success'            => sanitize_textarea_field( wp_unslash( $_POST['askany_i18n_feature_success'] ?? '' ) )         ?: "✅ Thank you! Your feature request has been submitted successfully.\n\n📋 Your tracking ID: {trackingId}",
					'i18n_error_generic'              => sanitize_text_field( wp_unslash( $_POST['askany_i18n_error_generic'] ?? '' ) )               ?: 'Sorry, there was an error submitting your request. Please try again.',
					'i18n_error_network'              => sanitize_text_field( wp_unslash( $_POST['askany_i18n_error_network'] ?? '' ) )               ?: '❌ Network error. Please check your connection and try again.',
					'i18n_report_initial_message'     => sanitize_textarea_field( wp_unslash( $_POST['askany_i18n_report_initial_message'] ?? '' ) )  ?: 'Hey 👋 Found a bug? Please fill out the form above with your details, then type "confirm" or "submit" in the chat to send your report.',
					'i18n_feature_initial_message'    => sanitize_textarea_field( wp_unslash( $_POST['askany_i18n_feature_initial_message'] ?? '' ) ) ?: 'What feature would you like to see next? Please fill out the form above with your details, then type "confirm" or "submit" in the chat to send your request.',
				);
				break;

			case 'toggle':
				$setting = isset($_POST['setting']) ? sanitize_text_field( wp_unslash( $_POST['setting'] ) ) : '';
				$value = isset($_POST['value']) ? intval($_POST['value']) : 0;

				if ( in_array($setting, array( 'show_faq', 'show_custom_links' )) ) {
					$settings_to_update = array( $setting => $value );
				}
				break;
		}

		if ( ! empty($settings_to_update) ) {
			$updated = $this->api_manager->update_settings($settings_to_update);

			if ( $updated ) {
				wp_send_json_success(array( 'message' => __('Settings saved successfully.', 'askany') ));
			} else {
				wp_send_json_error(array( 'message' => __('Failed to save settings.', 'askany') ));
			}
		} else {
			wp_send_json_error(array( 'message' => __('No valid settings to update.', 'askany') ));
		}
	}

	/**
	 * Save home page FAQ via AJAX.
	 *
	 * @return void
	 */
	public function save_home_faq() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$faq_id = isset($_POST['faq_id']) ? intval($_POST['faq_id']) : 0;
		$question = isset($_POST['question']) ? sanitize_text_field( wp_unslash( $_POST['question'] ) ) : '';
		$answer = isset($_POST['answer']) ? wp_kses_post( wp_unslash( $_POST['answer'] ) ) : '';
		$display_order = isset($_POST['display_order']) ? intval($_POST['display_order']) : 0;

		if ( empty($question) || empty($answer) ) {
			wp_send_json_error(array( 'message' => __('Question and answer are required.', 'askany') ));
		}

		if ( $faq_id <= 0 && ! $this->is_pro() && count( (array) $this->database->get_all_home_faqs() ) >= 1 ) {
			wp_send_json_error( array( 'message' => __( 'The free plan allows 1 FAQ. Upgrade to Pro for unlimited FAQs.', 'askany' ), 'code' => 'pro_required' ) );
		}

		if ( $faq_id > 0 ) {
			// Update existing FAQ
			$result = $this->database->update_home_faq($faq_id, $question, $answer, $display_order);
			$target_id = $faq_id;
		} else {
			// Add new FAQ
			$result = $this->database->add_home_faq($question, $answer, $display_order);
			$target_id = $result;
		}

		if ( $result !== false ) {
			do_action( 'askany_faq_saved', $target_id );
			wp_send_json_success(array( 'message' => __('FAQ saved successfully.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to save FAQ.', 'askany') ));
		}
	}

	/**
	 * Get home page FAQ via AJAX.
	 *
	 * @return void
	 */
	public function get_home_faq() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$faq_id = isset($_POST['faq_id']) ? intval($_POST['faq_id']) : 0;

		if ( ! $faq_id ) {
			wp_send_json_error(array( 'message' => __('Invalid FAQ ID.', 'askany') ));
		}

		$faq = $this->database->get_home_faq($faq_id);

		if ( $faq ) {
			wp_send_json_success($faq);
		} else {
			wp_send_json_error(array( 'message' => __('FAQ not found.', 'askany') ));
		}
	}

	/**
	 * Delete home page FAQ via AJAX.
	 *
	 * @return void
	 */
	public function delete_home_faq() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$faq_id = isset($_POST['faq_id']) ? intval($_POST['faq_id']) : 0;

		if ( ! $faq_id ) {
			wp_send_json_error(array( 'message' => __('Invalid FAQ ID.', 'askany') ));
		}

		$result = $this->database->delete_home_faq($faq_id);

		if ( $result ) {
			do_action( 'askany_faq_deleted', $faq_id );
			wp_send_json_success(array( 'message' => __('FAQ deleted successfully.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to delete FAQ.', 'askany') ));
		}
	}

	/**
	 * Toggle home page FAQ status via AJAX.
	 *
	 * @return void
	 */
	public function toggle_home_faq() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$faq_id = isset($_POST['faq_id']) ? intval($_POST['faq_id']) : 0;
		$is_active = isset($_POST['is_active']) ? intval($_POST['is_active']) : 0;

		if ( ! $faq_id ) {
			wp_send_json_error(array( 'message' => __('Invalid FAQ ID.', 'askany') ));
		}

		$result = $this->database->toggle_home_faq_status($faq_id, $is_active);

		if ( $result !== false ) {
			do_action( 'askany_faq_saved', $faq_id );
			wp_send_json_success(array( 'message' => __('FAQ status updated successfully.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to update FAQ status.', 'askany') ));
		}
	}

	/**
	 * Save home page custom link via AJAX.
	 *
	 * @return void
	 */
	public function save_home_link() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$link_id = isset($_POST['link_id']) ? intval($_POST['link_id']) : 0;
		$title = isset($_POST['title']) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '';
		$description = isset($_POST['description']) ? sanitize_text_field( wp_unslash( $_POST['description'] ) ) : '';
		$url = isset($_POST['url']) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '';
		$display_order = isset($_POST['display_order']) ? intval($_POST['display_order']) : 0;

		if ( empty($title) || empty($url) ) {
			wp_send_json_error(array( 'message' => __('Title and URL are required.', 'askany') ));
		}

		if ( $link_id <= 0 && ! $this->is_pro() && count( (array) $this->database->get_all_home_links() ) >= 1 ) {
			wp_send_json_error( array( 'message' => __( 'The free plan allows 1 custom link. Upgrade to Pro for unlimited links.', 'askany' ), 'code' => 'pro_required' ) );
		}

		if ( $link_id > 0 ) {
			// Update existing link
			$result = $this->database->update_home_link($link_id, $title, $description, $url, '', $display_order);
		} else {
			// Add new link
			$result = $this->database->add_home_link($title, $description, $url, '', $display_order);
		}

		if ( $result !== false ) {
			wp_send_json_success(array( 'message' => __('Link saved successfully.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to save link.', 'askany') ));
		}
	}

	/**
	 * Get home page custom link via AJAX.
	 *
	 * @return void
	 */
	public function get_home_link() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$link_id = isset($_POST['link_id']) ? intval($_POST['link_id']) : 0;

		if ( ! $link_id ) {
			wp_send_json_error(array( 'message' => __('Invalid link ID.', 'askany') ));
		}

		$link = $this->database->get_home_link($link_id);

		if ( $link ) {
			wp_send_json_success($link);
		} else {
			wp_send_json_error(array( 'message' => __('Link not found.', 'askany') ));
		}
	}

	/**
	 * Delete home page custom link via AJAX.
	 *
	 * @return void
	 */
	public function delete_home_link() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$link_id = isset($_POST['link_id']) ? intval($_POST['link_id']) : 0;

		if ( ! $link_id ) {
			wp_send_json_error(array( 'message' => __('Invalid link ID.', 'askany') ));
		}

		$result = $this->database->delete_home_link($link_id);

		if ( $result ) {
			wp_send_json_success(array( 'message' => __('Link deleted successfully.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to delete link.', 'askany') ));
		}
	}

	/**
	 * Toggle home page custom link status via AJAX.
	 *
	 * @return void
	 */
	public function toggle_home_link() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$link_id = isset($_POST['link_id']) ? intval($_POST['link_id']) : 0;
		$is_active = isset($_POST['is_active']) ? intval($_POST['is_active']) : 0;

		if ( ! $link_id ) {
			wp_send_json_error(array( 'message' => __('Invalid link ID.', 'askany') ));
		}

		$result = $this->database->toggle_home_link_status($link_id, $is_active);

		if ( $result !== false ) {
			wp_send_json_success(array( 'message' => __('Link status updated successfully.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to update link status.', 'askany') ));
		}
	}

	/**
	 * Clear chat history for a specific session via AJAX.
	 *
	 * @return void
	 */
	public function clear_chat_history() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$session_id = $this->read_session_id( $_POST );

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) ) {
			wp_send_json_error(array( 'message' => __('Session ID is required.', 'askany') ));
		}

		$session_secret = $this->get_request_session_secret();
		if ( ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
		}

		$result = $this->database->clear_session_messages($session_id);

		if ( $result !== false ) {
			wp_send_json_success(array(
				'message'      => __('Chat history cleared successfully.', 'askany'),
				'unread_count' => $this->database->get_total_unread_message_count( 'user' ),
			));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to clear chat history.', 'askany') ));
		}
	}

	/**
	 * Delete user session and all messages via AJAX.
	 *
	 * @return void
	 */
	public function delete_user_session() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$session_id = $this->read_session_id( $_POST );

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) ) {
			wp_send_json_error(array( 'message' => __('Session ID is required.', 'askany') ));
		}

		$session_secret = $this->get_request_session_secret();
		if ( ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
		}

		$result = $this->database->delete_chat_session($session_id);

		if ( $result !== false ) {
			wp_send_json_success(array(
				'message'      => __('User session deleted successfully.', 'askany'),
				'unread_count' => $this->database->get_total_unread_message_count( 'user' ),
			));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to delete user session.', 'askany') ));
		}
	}

	/**
	 * Handle internet search via AJAX.
	 *
	 * This method performs internet search when AI needs additional information.
	 *
	 * @return void
	 */
	public function search_internet() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		// Feature must be enabled by the site owner
		$settings = $this->api_manager->get_settings();
		if ( empty( $settings['web_search_enabled'] ) || 'false' === $settings['web_search_enabled'] ) {
			wp_send_json_error( array( 'message' => __( 'Internet search is disabled.', 'askany' ) ), 403 );
		}

		$query = isset($_POST['query']) ? $this->sanitize_chat_message( $_POST['query'] ) : '';
		$session_id = $this->read_session_id( $_POST );

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}

		// Requires an existing session the caller owns (fail closed)
		if ( empty( $session_id ) || ! $this->database->get_chat_session( $session_id ) || ! $this->database->verify_session_access( $session_id, $this->get_request_session_secret() ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'unauthorized_session' ), 403 );
		}

		if ( ! RateLimiter::flood_guard( $session_id, 'search' ) ) {
			wp_send_json_error( array( 'message' => __( 'You are sending messages too quickly. Please wait a moment.', 'askany' ), 'rate_limit' => true ), 429 );
		}

		$rate_check = RateLimiter::check( get_current_user_id() );
		if ( ! $rate_check['allowed'] ) {
			wp_send_json_error( array( 'message' => $rate_check['message'], 'rate_limit' => true ) );
		}

		if ( empty($query) ) {
			wp_send_json_error(array(
				'message' => __('Search query is required.', 'askany'),
			));
		}

		// Get internet search response
		$search_response = $this->api_manager->get_internet_search_response($query, $session_id);

		if ( $search_response ) {
			wp_send_json_success(array(
				'message' => $search_response['text'],
				'search_results' => isset($search_response['search_results']) ? $search_response['search_results'] : array(),
				'session_id' => $session_id,
				'source' => 'internet_search'
			));
		} else {
			wp_send_json_error(array(
				'message' => __('Unable to search the internet at this time. Please try again later.', 'askany'),
			));
		}
	}

	/**
	 * Get API statistics via AJAX.
	 *
	 * This method retrieves API statistics for the dashboard.
	 *
	 * @return void
	 */
	public function get_api_stats() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$period = isset($_POST['period']) ? sanitize_text_field( wp_unslash( $_POST['period']) ) : 'week';
		$days = isset($_POST['days']) ? intval($_POST['days']) : 30;

		$stats = array(
			'overview' => $this->database->get_api_performance_metrics($period),
			'detailed' => $this->database->get_api_stats($days),
		);

		wp_send_json_success($stats);
	}

	/**
	 * Refresh API statistics via AJAX.
	 *
	 * This method refreshes and returns updated API statistics.
	 *
	 * @return void
	 */
	public function refresh_api_stats() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		// Get fresh statistics
		$stats = array(
			'performance_today' => $this->database->get_api_performance_metrics('today'),
			'performance_week' => $this->database->get_api_performance_metrics('week'),
			'performance_month' => $this->database->get_api_performance_metrics('month'),
			'stats_30_days' => $this->database->get_api_stats(30),
			'stats_7_days' => $this->database->get_api_stats(7),
		);

		wp_send_json_success(array(
			'message' => __('Statistics refreshed successfully.', 'askany'),
			'data' => $stats,
		));
	}

	/**
	 * Get paginated API errors via AJAX.
	 *
	 * This method retrieves paginated API error logs.
	 *
	 * @return void
	 */
	public function get_paginated_errors() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$page = isset($_POST['page']) ? intval($_POST['page']) : 1;
		$per_page = isset($_POST['per_page']) ? intval($_POST['per_page']) : 10;
		$days = isset($_POST['days']) ? intval($_POST['days']) : 30;

		$result = $this->database->get_paginated_api_errors($page, $per_page, $days);

		wp_send_json_success($result);
	}

	/**
	 * Clear API errors via AJAX.
	 *
	 * This method clears API error logs.
	 *
	 * @return void
	 */
	public function clear_api_errors() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$days = isset($_POST['days']) ? intval($_POST['days']) : 0;

		$result = $this->database->clear_api_errors($days);

		if ( $result !== false ) {
			$message = $days > 0 
				? sprintf(__('Cleared errors older than %d days. %d errors removed.', 'askany'), $days, $result)
				: sprintf(__('All API errors cleared. %d errors removed.', 'askany'), $result);
			
			wp_send_json_success(array( 
				'message' => $message,
				'cleared_count' => $result
			));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to clear API errors.', 'askany') ));
		}
	}

	/**
	 * Get paginated chat logs via AJAX.
	 *
	 * This method retrieves paginated chat logs.
	 *
	 * @return void
	 */
	public function get_paginated_chat_logs() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$page = isset($_POST['page']) ? intval($_POST['page']) : 1;
		$per_page = isset($_POST['per_page']) ? intval($_POST['per_page']) : 20;
		$search = isset($_POST['search']) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';

		$result = $this->database->get_paginated_chat_logs($page, $per_page, $search);

		wp_send_json_success($result);
	}

	/**
	 * Clear chat logs via AJAX.
	 *
	 * This method clears chat logs.
	 *
	 * @return void
	 */
	public function clear_chat_logs() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$days = isset($_POST['days']) ? intval($_POST['days']) : 0;

		$result = $this->database->clear_chat_logs($days);

		if ( $result !== false ) {
			$message = $days > 0 
				? sprintf(__('Cleared chat logs older than %d days. %d logs removed.', 'askany'), $days, $result)
				: sprintf(__('All chat logs cleared. %d logs removed.', 'askany'), $result);
			
			wp_send_json_success(array( 
				'message' => $message,
				'cleared_count' => $result
			));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to clear chat logs.', 'askany') ));
		}
	}


	/*
	*
	 * Get product details via AJAX.
	 *
	 * This method retrieves detailed product information for the modal popup.
	 *
	 * @return void
	 */
	public function get_product_details() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		$product_id = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;

		if (!$product_id) {
			wp_send_json_error(array(
				'message' => __('Invalid product ID.', 'askany')
			));
		}

		// Check if WooCommerce is active
		if (!class_exists('WooCommerce')) {
			wp_send_json_error(array(
				'message' => __('WooCommerce is not active.', 'askany')
			));
		}

		// Get product
		$product = wc_get_product($product_id);

		// Only published products that are visible in the catalog or search
		if (
			! $product || ! $product->exists()
			|| 'publish' !== $product->get_status()
			|| ! empty( $product->get_post_password() )
			|| ! in_array( $product->get_catalog_visibility(), array( 'visible', 'catalog', 'search' ), true )
			|| ( $product->get_parent_id() && 'publish' !== get_post_status( $product->get_parent_id() ) )
		) {
			wp_send_json_error(array(
				'message' => __('Product not found.', 'askany')
			));
		}

		// Prepare product data
		$product_data = array(
			'id' => $product->get_id(),
			'name' => $product->get_name(),
			'description' => $product->get_description(),
			'short_description' => $product->get_short_description(),
			'price' => $product->get_price(),
			'regular_price' => $product->get_regular_price(),
			'sale_price' => $product->get_sale_price(),
			'price_html' => $product->get_price_html(),
			'on_sale' => $product->is_on_sale(),
			'sku' => $product->get_sku(),
			'stock_status' => $product->get_stock_status(),
			'stock_quantity' => $product->get_stock_quantity(),
			'manage_stock' => $product->get_manage_stock(),
			'url' => get_permalink($product->get_id()),
			'image_url' => wp_get_attachment_image_url($product->get_image_id(), 'full'),
			'gallery_images' => array(),
			'categories' => array(),
			'tags' => array(),
			'attributes' => array(),
			'average_rating' => $product->get_average_rating(),
			'reviews_count' => $product->get_review_count(),
			'weight' => $product->get_weight(),
			'dimensions' => array(
				'length' => $product->get_length(),
				'width' => $product->get_width(),
				'height' => $product->get_height()
			)
		);

		// Get gallery images
		$gallery_ids = $product->get_gallery_image_ids();
		foreach ($gallery_ids as $gallery_id) {
			$product_data['gallery_images'][] = wp_get_attachment_image_url($gallery_id, 'full');
		}

		// Get categories
		$categories = get_the_terms($product->get_id(), 'product_cat');
		if ($categories && !is_wp_error($categories)) {
			foreach ($categories as $category) {
				$product_data['categories'][] = $category->name;
			}
		}

		// Get tags
		$tags = get_the_terms($product->get_id(), 'product_tag');
		if ($tags && !is_wp_error($tags)) {
			foreach ($tags as $tag) {
				$product_data['tags'][] = $tag->name;
			}
		}

		// Get attributes
		$attributes = $product->get_attributes();
		foreach ($attributes as $attribute) {
			if ($attribute->is_taxonomy()) {
				$terms = wp_get_post_terms($product->get_id(), $attribute->get_name());
				$values = array();
				foreach ($terms as $term) {
					$values[] = $term->name;
				}
				$product_data['attributes'][wc_attribute_label($attribute->get_name())] = implode(', ', $values);
			} else {
				$product_data['attributes'][$attribute->get_name()] = $attribute->get_options();
			}
		}

		// Get variations if it's a variable product
		if ($product->is_type('variable')) {
			$variations = $product->get_available_variations();
			$product_data['variations'] = $variations;
			$product_data['variation_attributes'] = $product->get_variation_attributes();
		}

		wp_send_json_success($product_data);
	}

	/**
	 * Add product to cart via AJAX.
	 *
	 * This method adds a product to the WooCommerce cart.
	 *
	 * @return void
	 */
	public function add_to_cart() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		$product_id = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;
		$quantity = isset($_POST['quantity']) ? intval($_POST['quantity']) : 1;
		$variation_id = isset($_POST['variation_id']) ? intval($_POST['variation_id']) : 0;
		$variation = isset($_POST['variation']) ? (array) $_POST['variation'] : array();

		if (!$product_id) {
			wp_send_json_error(array(
				'message' => __('Invalid product ID.', 'askany')
			));
		}

		// Check if WooCommerce is active
		if (!class_exists('WooCommerce')) {
			wp_send_json_error(array(
				'message' => __('WooCommerce is not active.', 'askany')
			));
		}

		// Sanitize quantity
		if ($quantity <= 0) {
			$quantity = 1;
		}

		// Get product
		$product = wc_get_product($product_id);

		if (!$product || !$product->exists()) {
			wp_send_json_error(array(
				'message' => __('Product not found.', 'askany')
			));
		}

		// Check if product is purchasable
		if (!$product->is_purchasable()) {
			wp_send_json_error(array(
				'message' => __('This product cannot be purchased.', 'askany')
			));
		}

		// Check stock
		if (!$product->is_in_stock()) {
			wp_send_json_error(array(
				'message' => __('This product is out of stock.', 'askany')
			));
		}

		// Check stock quantity
		if ($product->managing_stock() && $product->get_stock_quantity() < $quantity) {
			wp_send_json_error(array(
				'message' => sprintf(__('Only %d items available in stock.', 'askany'), $product->get_stock_quantity())
			));
		}

		// Handle variable products
		if ($product->is_type('variable') && !$variation_id) {
			wp_send_json_error(array(
				'message' => __('Please select product options before adding to cart.', 'askany')
			));
		}

		// Sanitize variation data
		if (!empty($variation)) {
			$variation = array_map('sanitize_text_field', $variation);
		}

		try {
			// Add to cart
			$cart_item_key = WC()->cart->add_to_cart(
				$product_id,
				$quantity,
				$variation_id,
				$variation
			);

			if ($cart_item_key) {
				// Get cart count
				$cart_count = WC()->cart->get_cart_contents_count();
				
				// Get product name for success message
				$product_name = $product->get_name();
				
				// Prepare success message
				$single_tpl = get_option( 'askany_label_cart_added_single', __( '{product} has been added to your cart.', 'askany' ) );
				$message = str_replace( array( '{product}', '%s' ), $product_name, $single_tpl );

				if ($quantity > 1) {
					$multi_tpl = get_option( 'askany_label_cart_added_multiple', __( '{quantity} × {product} have been added to your cart.', 'askany' ) );
					$message = str_replace( array( '{quantity}', '%d' ), $quantity, $multi_tpl );
					$message = str_replace( array( '{product}', '%s' ), $product_name, $message );
				}

				// Get cart fragments for updating cart UI
				$fragments = array();
				if (function_exists('wc_get_template')) {
					ob_start();
					wc_get_template('cart/mini-cart.php');
					$mini_cart = ob_get_clean();
					$fragments['div.widget_shopping_cart_content'] = '<div class="widget_shopping_cart_content">' . $mini_cart . '</div>';
				}

				wp_send_json_success(array(
					'message' => $message,
					'cart_count' => $cart_count,
					'cart_total' => WC()->cart->get_cart_total(),
					'cart_subtotal' => WC()->cart->get_cart_subtotal(),
					'cart_url' => wc_get_cart_url(),
					'checkout_url' => wc_get_checkout_url(),
					'fragments' => $fragments,
					'cart_hash' => WC()->cart->get_cart_hash()
				));
			} else {
				wp_send_json_error(array(
					'message' => __('Failed to add product to cart. Please try again.', 'askany')
				));
			}
		} catch ( \Throwable $e ) {
			wp_send_json_error(array(
				'message' => $e->getMessage()
			));
		}
	}

	/**
	 * Cleanup old data via AJAX.
	 *
	 * @return void
	 */
	public function cleanup_old_data() {
		// Verify nonce and capabilities
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_db_management' ) || ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed.', 'askany' ) ) );
		}

		// Server-side Pro gate (mirrors the locked UI in admin-database-management.php)
		$this->require_pro_feature();

		// Delegate to Pro Database Manager if available
		$plugin = \Askany\Plugin::get_instance();
		if ( $plugin->is_pro_active() && class_exists( '\AskanyPro\DatabaseManagerPro' ) ) {
			$pro_db = new \AskanyPro\DatabaseManagerPro( $this->database );
			$result = $pro_db->delete_old_data( intval( $_POST['days'] ?? 0 ) );
			if ( $result['success'] ) {
				wp_send_json_success( $result );
			} else {
				wp_send_json_error( $result );
			}
		}

		$days = intval( $_POST['days'] ?? 0 );
		
		if ( $days <= 0 ) {
			wp_send_json_error( array( 'message' => __( 'Invalid number of days.', 'askany' ) ) );
		}

		$database_manager = new \Askany\DatabaseManager( $this->database );
		$result = $database_manager->delete_old_data( $days );

		if ( $result['success'] ) {
			wp_send_json_success( $result );
		} else {
			wp_send_json_error( $result );
		}
	}

	/**
	 * P3.4 — Save Pinecone settings.
	 *
	 * @return void
	 */
	public function save_pinecone_settings() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'askany' ) ) );
		}

		$enabled   = ! empty( $_POST['pinecone_enabled'] );
		$host      = esc_url_raw( wp_unslash( $_POST['pinecone_host'] ?? '' ) );
		$namespace = sanitize_text_field( wp_unslash( $_POST['pinecone_namespace'] ?? '' ) );
		$api_key   = sanitize_text_field( wp_unslash( $_POST['pinecone_api_key'] ?? '' ) );

		update_option( 'askany_pinecone_enabled',   (int) $enabled );
		update_option( 'askany_pinecone_host',       $host );
		update_option( 'askany_pinecone_namespace',  $namespace );

		// Only overwrite the stored key if a new one was provided
		if ( ! empty( $api_key ) ) {
			update_option( 'askany_pinecone_api_key', $api_key );
		}

		wp_send_json_success( array( 'message' => __( 'Pinecone settings saved.', 'askany' ) ) );
	}

	/**
	 * P3.4 — Test Pinecone connection.
	 *
	 * @return void
	 */
	public function test_pinecone_connection() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'askany' ) ) );
		}

		$pinecone = new \Askany\Pinecone_Handler();
		$result   = $pinecone->test_connection();

		if ( $result['success'] ) {
			wp_send_json_success( $result );
		} else {
			wp_send_json_error( $result );
		}
	}

	/**
	 * P3.6 — Migrate one batch of local embeddings to Pinecone.
	 *
	 * Called repeatedly by the frontend until done=true.
	 * Each call processes $batch rows from wp_askany_embeddings.
	 *
	 * @return void
	 */
	public function pinecone_migrate_batch() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'askany' ) ) );
		}

		if ( ! \Askany\Pinecone_Handler::is_enabled() ) {
			wp_send_json_error( array( 'message' => __( 'Pinecone is not enabled or configured.', 'askany' ) ) );
		}

		global $wpdb;
		$offset = absint( $_POST['offset'] ?? 0 );
		$batch  = min( absint( $_POST['batch'] ?? 50 ), 100 ); // cap at 100 per request

		$emb_table = $wpdb->prefix . 'askany_embeddings';
		$ci_table  = $wpdb->prefix . 'askany_content_index';

		// Get total count once (offset=0)
		$total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$emb_table}" );

		// Fetch batch: join with content_index to get source_url and title
		$rows = $wpdb->get_results( $wpdb->prepare(
			"SELECT e.id, e.vector_id, e.content_id, e.content_type,
			        e.source_url, e.title, e.excerpt,
			        e.chunk_index, e.total_chunks, e.embedding
			 FROM {$emb_table} e
			 ORDER BY e.id ASC
			 LIMIT %d OFFSET %d",
			$batch,
			$offset
		), ARRAY_A );

		if ( empty( $rows ) ) {
			wp_send_json_success( array(
				'processed' => 0,
				'total'     => $total,
				'done'      => true,
			) );
		}

		$pinecone = new \Askany\Pinecone_Handler();
		$vectors  = array();

		foreach ( $rows as $row ) {
			$embedding = json_decode( $row['embedding'], true );
			if ( ! is_array( $embedding ) || empty( $embedding ) ) {
				continue;
			}

			$excerpt = $row['excerpt'] ?? '';
			if ( empty( $excerpt ) ) {
				// Pull excerpt from content_index if missing
				$content = $wpdb->get_var( $wpdb->prepare(
					"SELECT content FROM {$ci_table} WHERE id = %d",
					(int) $row['content_id']
				) );
				if ( $content ) {
					$excerpt = substr( wp_strip_all_tags( $content ), 0, 500 );
				}
			}

			$vectors[] = array(
				'id'        => $row['vector_id'] ?: ( $row['content_type'] . '_' . $row['content_id'] . '_' . $row['chunk_index'] ),
				'embedding' => $embedding,
				'metadata'  => array(
					'content_id'   => (int) $row['content_id'],
					'content_type' => $row['content_type'],
					'source_url'   => $row['source_url'] ?? '',
					'title'        => $row['title'] ?? '',
					'excerpt'      => $excerpt,
					'chunk_index'  => (int) $row['chunk_index'],
					'total_chunks' => (int) $row['total_chunks'],
				),
			);
		}

		$processed = count( $vectors );
		if ( ! empty( $vectors ) ) {
			$pinecone->upsert_vectors_batch( $vectors );
		}

		$done = ( $offset + count( $rows ) ) >= $total;

		wp_send_json_success( array(
			'processed' => $processed,
			'total'     => $total,
			'done'      => $done,
		) );
	}

	/**
	 * P5.4 — Daily WP-Cron cleanup.
	 *
	 * - Deletes stale query-embedding transients (those that expired but WordPress
	 *   hasn't auto-purged yet, common on object-cache-less sites).
	 * - Deletes stale response-cache transients.
	 * - Archives sessions inactive for more than 30 days.
	 * - Removes orphaned embeddings (vectors with no matching content_index row).
	 *
	 * @return void
	 */
	public function run_cron_cleanup() {
		global $wpdb;

		// 1. Clear expired embedding + response cache transients
		$wpdb->query(
			"DELETE FROM {$wpdb->options}
			 WHERE ( option_name LIKE '_transient_askany_qembed_%'
			         OR option_name LIKE '_transient_askany_resp_%'
			         OR option_name LIKE '_transient_timeout_askany_qembed_%'
			         OR option_name LIKE '_transient_timeout_askany_resp_%' )
			   AND option_name LIKE '_transient_timeout_%'
			   AND CAST(option_value AS UNSIGNED) < UNIX_TIMESTAMP()"
		);
		// Also delete the value side for expired transients
		$wpdb->query(
			"DELETE o FROM {$wpdb->options} o
			 LEFT JOIN {$wpdb->options} t
			   ON t.option_name = CONCAT('_transient_timeout_', SUBSTRING(o.option_name, 12))
			 WHERE ( o.option_name LIKE '_transient_askany_qembed_%'
			         OR o.option_name LIKE '_transient_askany_resp_%' )
			   AND o.option_name NOT LIKE '_transient_timeout_%'
			   AND ( t.option_value IS NULL OR CAST(t.option_value AS UNSIGNED) < UNIX_TIMESTAMP() )"
		);

		// 2. Archive sessions inactive for 30+ days
		// last_activity is stored in site-local time (current_time('mysql'))
		$cutoff = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - 30 * DAY_IN_SECONDS );
		$sessions_table = $wpdb->prefix . 'askany_chat_sessions';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $sessions_table ) ) === $sessions_table ) {
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE {$sessions_table} SET status = 'archived'
					 WHERE last_activity < %s AND status = 'active'",
					$cutoff
				)
			);
		}

		// 3. Remove orphaned embeddings (no matching content_index row)
		$embeddings_table   = $wpdb->prefix . 'askany_embeddings';
		$content_index_table = $wpdb->prefix . 'askany_content_index';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $embeddings_table ) ) === $embeddings_table ) {
			$wpdb->query(
				"DELETE e FROM {$embeddings_table} e
				 LEFT JOIN {$content_index_table} c ON e.content_id = c.id
				 WHERE c.id IS NULL"
			);
		}
	}

	/**
	 * Truncate all data via AJAX.
	 *
	 * @return void
	 */
	public function truncate_all_data() {
		// Verify nonce and capabilities
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_db_management' ) || ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed.', 'askany' ) ) );
		}

		// Server-side Pro gate (mirrors the locked UI in admin-database-management.php)
		$this->require_pro_feature();

		// Delegate to Pro Database Manager if available
		$plugin = \Askany\Plugin::get_instance();
		if ( $plugin->is_pro_active() && class_exists( '\AskanyPro\DatabaseManagerPro' ) ) {
			$pro_db = new \AskanyPro\DatabaseManagerPro( $this->database );
			$result = $pro_db->truncate_all_data();
			if ( $result['success'] ) {
				wp_send_json_success( $result );
			} else {
				wp_send_json_error( $result );
			}
		}

		$database_manager = new \Askany\DatabaseManager( $this->database );
		$result = $database_manager->truncate_all_data();

		if ( $result['success'] ) {
			wp_send_json_success( $result );
		} else {
			wp_send_json_error( $result );
		}
	}

	/**
	 * Optimize database tables via AJAX.
	 *
	 * @return void
	 */
	public function optimize_tables() {
		// Verify nonce and capabilities
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_db_management' ) || ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed.', 'askany' ) ) );
		}

		// Server-side Pro gate (mirrors the locked UI in admin-database-management.php)
		$this->require_pro_feature();

		// Delegate to Pro Database Manager if available
		$plugin = \Askany\Plugin::get_instance();
		if ( $plugin->is_pro_active() && class_exists( '\AskanyPro\DatabaseManagerPro' ) ) {
			$pro_db = new \AskanyPro\DatabaseManagerPro( $this->database );
			$result = $pro_db->optimize_tables();
			if ( $result['success'] ) {
				wp_send_json_success( $result );
			} else {
				wp_send_json_error( $result );
			}
		}

		$database_manager = new \Askany\DatabaseManager( $this->database );
		$result = $database_manager->optimize_tables();

		if ( $result['success'] ) {
			wp_send_json_success( $result );
		} else {
			wp_send_json_error( $result );
		}
	}

	/**
	 * Delete data by date range via AJAX.
	 *
	 * @return void
	 */
	public function delete_date_range() {
		// Verify nonce and capabilities
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_db_management' ) || ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed.', 'askany' ) ) );
		}

		// Server-side Pro gate (mirrors the locked UI in admin-database-management.php)
		$this->require_pro_feature();

		// Delegate to Pro Database Manager if available
		$plugin = \Askany\Plugin::get_instance();
		if ( $plugin->is_pro_active() && class_exists( '\AskanyPro\DatabaseManagerPro' ) ) {
			$start_date = sanitize_text_field( wp_unslash( $_POST['start_date'] ?? '' ) );
			$end_date   = sanitize_text_field( wp_unslash( $_POST['end_date'] ?? '' ) );
			$pro_db     = new \AskanyPro\DatabaseManagerPro( $this->database );
			$result     = $pro_db->delete_data_by_date_range( $start_date, $end_date );
			if ( $result['success'] ) {
				wp_send_json_success( $result );
			} else {
				wp_send_json_error( $result );
			}
		}

		$start_date = sanitize_text_field( wp_unslash( $_POST['start_date'] ?? '' ) );
		$end_date = sanitize_text_field( wp_unslash( $_POST['end_date'] ?? '' ) );

		if ( empty( $start_date ) || empty( $end_date ) ) {
			wp_send_json_error( array( 'message' => __( 'Start date and end date are required.', 'askany' ) ) );
		}

		$database_manager = new \Askany\DatabaseManager( $this->database );
		$result = $database_manager->delete_data_by_date_range( $start_date, $end_date );

		if ( $result['success'] ) {
			wp_send_json_success( $result );
		} else {
			wp_send_json_error( $result );
		}
	}

	/**
	 * Export data via AJAX.
	 *
	 * @return void
	 */
	public function export_data() {
		// Verify nonce and capabilities
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_db_management' ) || ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed.', 'askany' ) ) );
		}

		// Server-side Pro gate (mirrors the locked UI in admin-database-management.php)
		$this->require_pro_feature();



		// Sanitize and unslash input data
		$format = sanitize_text_field( wp_unslash( $_POST['format'] ?? 'csv' ) );
		$export_type = sanitize_text_field( wp_unslash( $_POST['export_type'] ?? 'all' ) );
		$start_date = sanitize_text_field( wp_unslash( $_POST['start_date'] ?? '' ) );
		$end_date = sanitize_text_field( wp_unslash( $_POST['end_date'] ?? '' ) );

		// Validate format
		if ( ! in_array( $format, array( 'csv', 'json' ), true ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid export format.', 'askany' ) ) );
		}

		// Validate export type
		if ( ! in_array( $export_type, array( 'all', 'filtered' ), true ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid export type.', 'askany' ) ) );
		}

		try {
			global $wpdb;
			
			// Get all table data
			$export_data = array();
			
			// Define all tables to export
			$tables_to_export = array(
				'chat_sessions' => $this->database->get_sessions_table(),
				'chat_messages' => $this->database->get_messages_table(),
				'qa_pairs' => $this->database->get_qa_pairs_table(),
				'external_sources' => $this->database->get_external_sources_table(),
				'content_index' => $this->database->get_content_index_table(),
				'pdf_index' => $this->database->get_pdf_index_table(),
				'chat_history' => $this->database->get_chat_history_table(),
				'user_agents' => $this->database->get_user_agents_table(),
				'typing_indicators' => $this->database->get_typing_indicators_table(),
				'home_faqs' => $this->database->get_home_faqs_table(),
				'home_links' => $this->database->get_home_links_table(),
				'api_stats' => $this->database->get_api_stats_table(),
				'feature_requests' => $this->database->get_feature_requests_table(),
				'bug_reports' => $this->database->get_bug_reports_table(),
				'embeddings' => $this->database->get_embeddings_table(),
			);
			
			// Export each table
			foreach ( $tables_to_export as $table_key => $table_name ) {
				$query = "SELECT * FROM {$table_name}";
				
				// Apply date filtering for tables with created_at field
				$date_tables = array( 'chat_sessions', 'chat_messages', 'qa_pairs', 'chat_history', 'user_agents', 'typing_indicators', 'home_faqs', 'api_stats', 'feature_requests', 'bug_reports' );
				
				if ( $export_type === 'filtered' && ! empty( $start_date ) && ! empty( $end_date ) && in_array( $table_key, $date_tables ) ) {
					$start_datetime = $start_date . ' 00:00:00';
					$end_datetime = $end_date . ' 23:59:59';
					$query .= $wpdb->prepare( " WHERE created_at BETWEEN %s AND %s", $start_datetime, $end_datetime );
				}
				
				$query .= " ORDER BY id DESC";
				$table_data = $wpdb->get_results( $query );
				
				if ( ! empty( $table_data ) ) {
					$export_data[$table_key] = $table_data;
				}
			}
			
			// Flatten data for CSV export or keep structured for JSON
			if ( $format === 'csv' ) {
				$results = array();
				foreach ( $export_data as $table_name => $table_data ) {
					foreach ( $table_data as $row ) {
						$row_array = (array) $row;
						$row_array['table_name'] = $table_name; // Add table identifier
						$results[] = (object) $row_array;
					}
				}
			} else {
				$results = $export_data;
			}

			if ( empty( $results ) ) {
				wp_send_json_error( array( 'message' => __( 'No data found to export.', 'askany' ) ) );
			}

			// Create export file
			$filename = 'askany_export_' . date( 'Y-m-d_H-i-s' ) . '.' . $format;
			$upload_dir = wp_upload_dir();
			$file_path = $upload_dir['path'] . '/' . $filename;

			if ( $format === 'csv' ) {
				$this->create_csv_export( $results, $file_path );
			} else {
				$this->create_json_export( $results, $file_path );
			}

			// Create download URL
			$download_url = $upload_dir['url'] . '/' . $filename;

			wp_send_json_success( array(
				'message' => sprintf( 
					__( 'Export completed! %d records exported.', 'askany' ),
					count( $results )
				),
				'download_url' => $download_url,
				'filename' => $filename
			) );

		} catch ( \Throwable $e ) {
			wp_send_json_error( array( 'message' => __( 'Export failed: ', 'askany' ) . $e->getMessage() ) );
		}
	}

	/**
	 * Create CSV export file.
	 *
	 * @param array $data Export data
	 * @param string $file_path File path
	 */
	private function create_csv_export( $data, $file_path ) {
		$file = fopen( $file_path, 'w' );
		
		if ( empty( $data ) ) {
			fclose( $file );
			return;
		}
		
		// Get all possible columns from the data
		$all_columns = array();
		foreach ( $data as $row ) {
			$row_array = (array) $row;
			$all_columns = array_merge( $all_columns, array_keys( $row_array ) );
		}
		$all_columns = array_unique( $all_columns );
		
		// Add CSV headers
		fputcsv( $file, $all_columns );

		// Add data rows
		foreach ( $data as $row ) {
			$row_array = (array) $row;
			$csv_row = array();
			
			foreach ( $all_columns as $column ) {
				$value = isset( $row_array[$column] ) ? $row_array[$column] : '';
				
				// Handle boolean values
				if ( is_bool( $value ) ) {
					$value = $value ? 'Yes' : 'No';
				} elseif ( $value === '1' || $value === '0' ) {
					// Handle string boolean values
					if ( in_array( $column, array( 'is_ai_handled', 'is_read', 'is_typing', 'is_active', 'is_online' ) ) ) {
						$value = $value === '1' ? 'Yes' : 'No';
					}
				}
				
				$csv_row[] = $value;
			}
			
			fputcsv( $file, $csv_row );
		}

		fclose( $file );
	}

	/**
	 * Create JSON export file.
	 *
	 * @param array $data Export data
	 * @param string $file_path File path
	 */
	private function create_json_export( $data, $file_path ) {
		// Calculate total records
		$total_records = 0;
		if ( is_array( $data ) ) {
			if ( isset( $data[0] ) && is_object( $data[0] ) ) {
				// Flattened data (CSV format)
				$total_records = count( $data );
			} else {
				// Structured data (table format)
				foreach ( $data as $table_data ) {
					if ( is_array( $table_data ) ) {
						$total_records += count( $table_data );
					}
				}
			}
		}
		
		$export_data = array(
			'export_date' => current_time( 'mysql' ),
			'plugin_version' => ASKANY_VERSION,
			'total_records' => $total_records,
			'data' => $data
		);

		file_put_contents( $file_path, wp_json_encode( $export_data, JSON_PRETTY_PRINT ) );
	}

	/**
	 * Import data via AJAX.
	 *
	 * @return void
	 */
	public function import_data() {
		// Verify nonce and capabilities
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_db_management' ) || ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed.', 'askany' ) ) );
		}

		// Server-side Pro gate (mirrors the locked UI in admin-database-management.php)
		$this->require_pro_feature();



		if ( empty( $_FILES['import_file'] ) ) {
			wp_send_json_error( array( 'message' => __( 'No file uploaded.', 'askany' ) ) );
		}

		$file = $_FILES['import_file'];
		
		// Validate file upload errors
		if ( $file['error'] !== UPLOAD_ERR_OK ) {
			wp_send_json_error( array( 'message' => __( 'File upload failed.', 'askany' ) ) );
		}
		
		// Sanitize filename
		$filename = sanitize_file_name( $file['name'] );
		$file_extension = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );

		// Validate file extension with strict comparison
		if ( ! in_array( $file_extension, array( 'csv', 'json', 'sql' ), true ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid file format. Only CSV, JSON, and SQL files are allowed.', 'askany' ) ) );
		}

		// Validate MIME type for additional security
		$allowed_mime_types = array(
			'csv' => array( 'text/csv', 'text/plain', 'application/csv' ),
			'json' => array( 'application/json', 'text/plain' ),
			'sql' => array( 'application/sql', 'text/plain' )
		);
		
		$finfo = finfo_open( FILEINFO_MIME_TYPE );
		$mime_type = finfo_file( $finfo, $file['tmp_name'] );
		finfo_close( $finfo );
		
		if ( ! in_array( $mime_type, $allowed_mime_types[ $file_extension ], true ) ) {
			wp_send_json_error( array( 'message' => __( 'File MIME type does not match extension.', 'askany' ) ) );
		}

		// TODO: Implement data import functionality
		// This would process the uploaded file and import data
		
		wp_send_json_success( array(
			'message' => sprintf( 
				__( 'Import functionality for %s files will be implemented in the next phase.', 'askany' ),
				strtoupper( $file_extension )
			)
		) );
	}

	/**
	 * Filter data via AJAX.
	 *
	 * @return void
	 */
	public function filter_data() {
		// Verify nonce and capabilities
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_db_management' ) || ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed.', 'askany' ) ) );
		}

		// Server-side Pro gate (mirrors the locked UI in admin-database-management.php)
		$this->require_pro_feature();



		// Sanitize and unslash input data
		$start_date = sanitize_text_field( wp_unslash( $_POST['start_date'] ?? '' ) );
		$end_date = sanitize_text_field( wp_unslash( $_POST['end_date'] ?? '' ) );
		$session_type = sanitize_text_field( wp_unslash( $_POST['session_type'] ?? 'all' ) );

		if ( empty( $start_date ) || empty( $end_date ) ) {
			wp_send_json_error( array( 'message' => __( 'Start date and end date are required.', 'askany' ) ) );
		}

		// Validate session type with whitelist
		$valid_session_types = array( 'all', 'anonymous', 'logged_in', 'agent_handled', 'ai_only' );
		if ( ! in_array( $session_type, $valid_session_types, true ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid session type.', 'askany' ) ) );
		}

		global $wpdb;
		$sessions_table = $this->database->get_sessions_table();
		$messages_table = $this->database->get_messages_table();

		// Convert to datetime for database query
		$start_datetime = $start_date . ' 00:00:00';
		$end_datetime = $end_date . ' 23:59:59';

		// Build queries based on session type
		$session_query = "SELECT COUNT(*) FROM {$sessions_table} WHERE created_at BETWEEN %s AND %s";
		$message_query = "SELECT COUNT(*) FROM {$messages_table} m INNER JOIN {$sessions_table} s ON m.session_id = s.session_id WHERE s.created_at BETWEEN %s AND %s";
		
		$query_params = array( $start_datetime, $end_datetime );

		// Add session type filter
		switch ( $session_type ) {
			case 'anonymous':
				$session_query .= " AND session_id LIKE %s";
				$message_query .= " AND s.session_id LIKE %s";
				$query_params[] = 'anon_%';
				break;
			case 'logged_in':
				$session_query .= " AND session_id LIKE %s";
				$message_query .= " AND s.session_id LIKE %s";
				$query_params[] = 'user_%';
				break;
			case 'agent_handled':
				$session_query .= " AND agent_id IS NOT NULL";
				$message_query .= " AND s.agent_id IS NOT NULL";
				break;
			case 'ai_only':
				$session_query .= " AND (agent_id IS NULL OR is_ai_handled = 1)";
				$message_query .= " AND (s.agent_id IS NULL OR s.is_ai_handled = 1)";
				break;
		}

		// Count sessions in date range
		if ( in_array( $session_type, array( 'anonymous', 'logged_in' ), true ) ) {
			$session_count = $wpdb->get_var( $wpdb->prepare( $session_query, $query_params ) );
			$message_count = $wpdb->get_var( $wpdb->prepare( $message_query, $query_params ) );
		} else {
			$session_count = $wpdb->get_var( $wpdb->prepare( $session_query, $start_datetime, $end_datetime ) );
			$message_count = $wpdb->get_var( $wpdb->prepare( $message_query, $start_datetime, $end_datetime ) );
		}

		wp_send_json_success( array(
			'sessions' => (int) $session_count,
			'messages' => (int) $message_count,
			'start_date' => $start_date,
			'end_date' => $end_date,
			'session_type' => $session_type
		) );
	}

	/**
	 * Get fresh database statistics via AJAX.
	 *
	 * @return void
	 */
	public function get_database_stats() {
		// Verify nonce and capabilities
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_db_management' ) || ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed.', 'askany' ) ) );
		}

		$database_manager = new \Askany\DatabaseManager( $this->database );
		
		// Get fresh statistics
		$table_sizes = $database_manager->get_table_sizes();
		$record_counts = $database_manager->get_record_counts();

		// Format the data for JavaScript
		$stats = array();
		
		// Add all available tables
		$table_keys = array(
			'chat_messages', 'chat_sessions', 'qa_pairs', 'external_sources',
			'content_index', 'pdf_index', 'chat_history', 'user_agents',
			'typing_indicators', 'home_faqs', 'home_links', 'api_stats'
		);
		
		foreach ( $table_keys as $key ) {
			if ( isset( $table_sizes[$key] ) && isset( $record_counts[$key] ) ) {
				$stats[$key] = array(
					'formatted_size' => $table_sizes[$key]['formatted_size'],
					'record_count' => number_format( $record_counts[$key] )
				);
			}
		}
		
		// Add total
		if ( isset( $table_sizes['total'] ) ) {
			$stats['total'] = array(
				'formatted_size' => $table_sizes['total']['formatted_size']
			);
		}

		wp_send_json_success( $stats );
	}
	
	/**
	 * Handle get started form submission
	 *
	 * @return void
	 */
	public function submit_get_started() {
		nocache_headers();
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );
		
		$name = isset( $_POST['name'] ) && is_string( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
		$email = isset( $_POST['email'] ) && is_string( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
		$session_id = $this->read_session_id( $_POST );

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		
		// Validate email if provided
		if ( ! empty( $email ) && ! is_email( $email ) ) {
			wp_send_json_error( array(
				'message' => __( 'Please provide a valid email address.', 'askany' )
			) );
		}
		
		// Check if email belongs to a registered user
		$avatar_url = '';
		if ( ! empty( $email ) ) {
			$user = get_user_by( 'email', $email );
			if ( $user ) {
				// Get user avatar
				$avatar_url = get_avatar_url( $email, array( 'size' => 96 ) );
			} else {
				// Get Gravatar for non-registered email
				$avatar_url = get_avatar_url( $email, array( 'size' => 96, 'default' => 'mp' ) );
			}
		}
		
		$session_secret = $this->get_request_session_secret();
		
		// Store user info in session metadata if session_id is provided
		if ( ! empty( $session_id ) ) {
			global $wpdb;
			$sessions_table = $this->database->get_sessions_table();

			$existing = $this->database->get_chat_session( $session_id );
			if ( $existing && ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
				wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
				return;
			}

			if ( ! $existing ) {
				// New session: keep the secret this browser already holds (request or HttpOnly cookie).
				if ( strlen( (string) $session_secret ) < 16 ) {
					$session_secret = '' !== \Askany\Database::cookie_session_secret( $session_id )
						? \Askany\Database::cookie_session_secret( $session_id )
						: wp_generate_password( 32, false );
				}
			} elseif ( empty( $session_secret ) ) {
				$session_secret = ( isset( $session_id ) && is_string( $session_id ) && '' !== \Askany\Database::cookie_session_secret( $session_id ) )
					? \Askany\Database::cookie_session_secret( $session_id )
					: wp_generate_password( 32, false );
			}

			if ( $existing ) {
				$wpdb->update(
					$sessions_table,
					array(
						'user_name'  => $name,
						'user_email' => $email,
					),
					array( 'session_id' => $session_id ),
					array( '%s', '%s' ),
					array( '%s' )
				);
			} else {
				$created_id = $this->database->create_chat_session( $session_id, array( 'name' => $name, 'email' => $email ), $session_secret );
				if ( false === $created_id ) {
					wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
					return;
				}
			}
		}
		
		$resp_data = array(
			'message'    => __( 'Welcome! Let\'s get started.', 'askany' ),
			'name'       => $name,
			'email'      => $email,
			'avatar_url' => $avatar_url,
		);
		if ( empty( $existing ) ) {
			$resp_data['session_secret'] = $session_secret;
		}

		wp_send_json_success( $resp_data );
	}

	/**
	 * Mark session messages as read.
	 *
	 * @return void
	 */
	public function mark_session_read() {
		// Verify nonce
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_chat_nonce' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed', 'askany' ) ) );
		}

		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'askany' ) ) );
		}

		$session_id = isset( $_POST['session_id'] ) && is_string( $_POST['session_id'] ) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : '';

		if ( empty( $session_id ) || ! Database::is_valid_session_id( $session_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Session ID is required', 'askany' ) ) );
		}

		// Mark all messages in the session as read
		$result = $this->database->mark_session_messages_as_read( $session_id );

		if ( $result ) {
			wp_send_json_success( array( 'message' => __( 'Messages marked as read', 'askany' ) ) );
		} else {
			wp_send_json_error( array( 'message' => __( 'Failed to mark messages as read', 'askany' ) ) );
		}
	}

	/**
	 * Get total unread message count.
	 *
	 * @return void
	 */
	public function get_unread_count() {
		// Verify nonce
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_chat_nonce' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed', 'askany' ) ) );
		}

		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'askany' ) ) );
		}

		// Get total unread count (only user messages)
		$unread_count = $this->database->get_total_unread_message_count( 'user' );

		wp_send_json_success( array( 
			'unread_count' => $unread_count 
		) );
	}

	/**
	 * Save integration settings.
	 *
	 * @return void
	 */
	public function save_integration_settings() {
		// Verify nonce
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'askany_chat_nonce' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed', 'askany' ) ) );
		}

		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'askany' ) ) );
		}

		$setting = isset( $_POST['setting'] ) ? sanitize_text_field( wp_unslash( $_POST['setting'] ) ) : '';
		$value = isset( $_POST['value'] ) ? sanitize_text_field( wp_unslash( $_POST['value'] ) ) : '';

		if ( empty( $setting ) ) {
			wp_send_json_error( array( 'message' => __( 'Setting name is required', 'askany' ) ) );
		}

		// Get current settings
		$settings = $this->api_manager->get_settings();
		
		// Ensure settings is an array
		if ( ! is_array( $settings ) ) {
			$settings = array();
		}
		
		// Update the specific setting
		$settings[$setting] = $value;
		
		// Save settings
		$result = $this->api_manager->update_settings( $settings );

		if ( $result ) {
			wp_send_json_success( array( 'message' => __( 'Settings saved successfully', 'askany' ) ) );
		} else {
			wp_send_json_error( array( 'message' => __( 'Failed to save settings', 'askany' ) ) );
		}
	}

	/**
	 * Save Slack settings.
	 *
	 * @return void
	 */
	public function save_slack_settings() {
		// Verify nonce
		if ( ! isset( $_POST['askany_slack_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['askany_slack_nonce'] ) ), 'askany_save_slack_settings' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed', 'askany' ) ) );
		}

		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'askany' ) ) );
		}

		$type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : '';

		// Get current settings
		$settings = $this->api_manager->get_settings();
		
		// Ensure settings is an array
		if ( ! is_array( $settings ) ) {
			$settings = array();
		}
		
		if ( ! isset( $settings['slack'] ) ) {
			$settings['slack'] = array();
		}

		if ( $type === 'webhook' ) {
			// Save webhook settings
			$settings['slack']['enabled'] = isset( $_POST['slack_enabled'] ) ? 1 : 0;
			$settings['slack']['webhook_url'] = isset( $_POST['slack_webhook_url'] ) ? esc_url_raw( wp_unslash( $_POST['slack_webhook_url'] ) ) : '';
			$settings['slack']['channel'] = isset( $_POST['slack_channel'] ) ? sanitize_text_field( wp_unslash( $_POST['slack_channel'] ) ) : '';
		} elseif ( $type === 'forwarding' ) {
			// Save forwarding settings
			$settings['slack']['notify_user_messages'] = isset( $_POST['notify_user_messages'] ) ? 1 : 0;
			$settings['slack']['forward_agent_messages'] = isset( $_POST['forward_agent_messages'] ) ? 1 : 0;
			$settings['slack']['forward_ai_messages'] = isset( $_POST['forward_ai_messages'] ) ? 1 : 0;
		}

		// Save settings
		$result = $this->api_manager->update_settings( $settings );

		if ( $result ) {
			wp_send_json_success( array( 'message' => __( 'Slack settings saved successfully', 'askany' ) ) );
		} else {
			wp_send_json_error( array( 'message' => __( 'Failed to save Slack settings', 'askany' ) ) );
		}
	}

	/**
	 * Test Slack webhook.
	 *
	 * @return void
	 */
	public function test_slack_webhook() {
		// Verify nonce
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_chat_nonce' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed', 'askany' ) ) );
		}

		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'askany' ) ) );
		}

		$webhook_url = esc_url_raw( $_POST['webhook_url'] );
		$channel = sanitize_text_field( $_POST['channel'] );

		if ( empty( $webhook_url ) ) {
			wp_send_json_error( array( 'message' => __( 'Webhook URL is required', 'askany' ) ) );
		}

		// Prepare test message
		$message = array(
			'text' => __( 'Test message from Askany! 🚀', 'askany' ),
			'username' => 'Askany',
			'icon_emoji' => ':robot_face:',
		);

		if ( ! empty( $channel ) ) {
			$message['channel'] = $channel;
		}

		// Send test message
		$response = wp_remote_post( $webhook_url, array(
			'body' => wp_json_encode( $message ),
			'headers' => array(
				'Content-Type' => 'application/json',
			),
			'timeout' => 15,
		) );

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => __( 'Failed to send test message: ', 'askany' ) . $response->get_error_message() ) );
		}

		$response_code = wp_remote_retrieve_response_code( $response );
		
		if ( $response_code === 200 ) {
			wp_send_json_success( array( 'message' => __( 'Test message sent successfully!', 'askany' ) ) );
		} else {
			$response_body = wp_remote_retrieve_body( $response );
			wp_send_json_error( array( 'message' => __( 'Failed to send test message. Response: ', 'askany' ) . $response_body ) );
		}
	}

	/**
	 * Save Telegram integration settings.
	 *
	 * @return void
	 */
	public function save_telegram_settings() {
		// Verify nonce
		if ( ! isset( $_POST['askany_telegram_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['askany_telegram_nonce'] ) ), 'askany_save_telegram_settings' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed', 'askany' ) ) );
		}

		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'askany' ) ) );
		}

		$type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : '';

		// Get current settings
		$settings = $this->api_manager->get_settings();
		
		// Ensure settings is an array
		if ( ! is_array( $settings ) ) {
			$settings = array();
		}
		
		if ( ! isset( $settings['messenger'] ) ) {
			$settings['messenger'] = array();
		}
		
		if ( ! isset( $settings['messenger']['telegram'] ) ) {
			$settings['messenger']['telegram'] = array();
		}

		if ( $type === 'connection' ) {
			// Save connection settings
			$settings['messenger']['telegram']['enabled'] = isset( $_POST['telegram_enabled'] ) ? 1 : 0;
			$settings['messenger']['telegram']['bot_token'] = isset( $_POST['telegram_bot_token'] ) ? sanitize_text_field( wp_unslash( $_POST['telegram_bot_token'] ) ) : '';
			$settings['messenger']['telegram']['chat_id'] = isset( $_POST['telegram_chat_id'] ) ? sanitize_text_field( wp_unslash( $_POST['telegram_chat_id'] ) ) : '';
		} elseif ( $type === 'forwarding' ) {
			// Save forwarding settings
			$settings['messenger']['telegram']['notify_user_messages'] = isset( $_POST['notify_user_messages'] ) ? 1 : 0;
			$settings['messenger']['telegram']['forward_agent_messages'] = isset( $_POST['forward_agent_messages'] ) ? 1 : 0;
			$settings['messenger']['telegram']['forward_ai_messages'] = isset( $_POST['forward_ai_messages'] ) ? 1 : 0;
		}

		// Save settings
		$result = $this->api_manager->update_settings( $settings );

		if ( $result ) {
			wp_send_json_success( array( 'message' => __( 'Telegram settings saved successfully', 'askany' ) ) );
		} else {
			wp_send_json_error( array( 'message' => __( 'Failed to save Telegram settings', 'askany' ) ) );
		}
	}

	/**
	 * Set Telegram webhook.
	 *
	 * @return void
	 */
	public function set_telegram_webhook() {
		// Verify nonce
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_chat_nonce' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed', 'askany' ) ) );
		}

		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'askany' ) ) );
		}

		$bot_token = sanitize_text_field( $_POST['bot_token'] );
		$webhook_url = esc_url_raw( $_POST['webhook_url'] );

		if ( empty( $bot_token ) || empty( $webhook_url ) ) {
			wp_send_json_error( array( 'message' => __( 'Bot token and webhook URL are required', 'askany' ) ) );
		}

		// Set webhook via Telegram API
		$api_url = "https://api.telegram.org/bot{$bot_token}/setWebhook";
		
		$response = wp_remote_post( $api_url, array(
			'body' => array(
				'url' => $webhook_url,
			),
			'timeout' => 15,
		) );

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => __( 'Failed to set webhook: ', 'askany' ) . $response->get_error_message() ) );
		}

		$response_body = wp_remote_retrieve_body( $response );
		$data = json_decode( $response_body, true );

		if ( isset( $data['ok'] ) && $data['ok'] ) {
			wp_send_json_success( array( 'message' => __( 'Webhook set successfully!', 'askany' ) ) );
		} else {
			$error_message = isset( $data['description'] ) ? $data['description'] : __( 'Unknown error', 'askany' );
			wp_send_json_error( array( 'message' => __( 'Failed to set webhook: ', 'askany' ) . $error_message ) );
		}
	}

	/**
	 * Get Telegram webhook info.
	 *
	 * @return void
	 */
	public function get_telegram_webhook_info() {
		// Verify nonce
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_chat_nonce' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed', 'askany' ) ) );
		}

		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'askany' ) ) );
		}

		$bot_token = sanitize_text_field( $_POST['bot_token'] );

		if ( empty( $bot_token ) ) {
			wp_send_json_error( array( 'message' => __( 'Bot token is required', 'askany' ) ) );
		}

		// Get webhook info via Telegram API
		$api_url = "https://api.telegram.org/bot{$bot_token}/getWebhookInfo";
		
		$response = wp_remote_get( $api_url, array(
			'timeout' => 15,
		) );

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => __( 'Failed to get webhook info: ', 'askany' ) . $response->get_error_message() ) );
		}

		$response_body = wp_remote_retrieve_body( $response );
		$data = json_decode( $response_body, true );

		if ( isset( $data['ok'] ) && $data['ok'] ) {
			wp_send_json_success( array( 
				'message' => __( 'Webhook info retrieved successfully', 'askany' ),
				'webhook_info' => $data['result']
			) );
		} else {
			$error_message = isset( $data['description'] ) ? $data['description'] : __( 'Unknown error', 'askany' );
			wp_send_json_error( array( 'message' => __( 'Failed to get webhook info: ', 'askany' ) . $error_message ) );
		}
	}

	/**
	 * Save WhatsApp integration settings.
	 *
	 * @return void
	 */
	public function save_whatsapp_settings() {
		// Verify nonce
		if ( ! isset( $_POST['askany_whatsapp_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['askany_whatsapp_nonce'] ) ), 'askany_save_whatsapp_settings' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed', 'askany' ) ) );
		}

		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'askany' ) ) );
		}

		$type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : '';

		// Get current settings
		$settings = $this->api_manager->get_settings();
		
		// Ensure settings is an array
		if ( ! is_array( $settings ) ) {
			$settings = array();
		}
		
		if ( ! isset( $settings['messenger'] ) ) {
			$settings['messenger'] = array();
		}
		
		if ( ! isset( $settings['messenger']['whatsapp'] ) ) {
			$settings['messenger']['whatsapp'] = array();
		}

		if ( $type === 'connection' ) {
			// Save connection settings
			$settings['messenger']['whatsapp']['enabled'] = isset( $_POST['whatsapp_enabled'] ) ? 1 : 0;
			$settings['messenger']['whatsapp']['phone_number_id'] = isset( $_POST['whatsapp_phone_number_id'] ) ? sanitize_text_field( wp_unslash( $_POST['whatsapp_phone_number_id'] ) ) : '';
			$settings['messenger']['whatsapp']['access_token'] = isset( $_POST['whatsapp_access_token'] ) ? sanitize_text_field( wp_unslash( $_POST['whatsapp_access_token'] ) ) : '';
			$settings['messenger']['whatsapp']['verify_token'] = isset( $_POST['whatsapp_verify_token'] ) ? sanitize_text_field( wp_unslash( $_POST['whatsapp_verify_token'] ) ) : '';
		} elseif ( $type === 'forwarding' ) {
			// Save forwarding settings
			$settings['messenger']['whatsapp']['notify_user_messages'] = isset( $_POST['notify_user_messages'] ) ? 1 : 0;
			$settings['messenger']['whatsapp']['forward_agent_messages'] = isset( $_POST['forward_agent_messages'] ) ? 1 : 0;
			$settings['messenger']['whatsapp']['forward_ai_messages'] = isset( $_POST['forward_ai_messages'] ) ? 1 : 0;
		}

		// Save settings
		$result = $this->api_manager->update_settings( $settings );

		if ( $result ) {
			wp_send_json_success( array( 'message' => __( 'WhatsApp settings saved successfully', 'askany' ) ) );
		} else {
			wp_send_json_error( array( 'message' => __( 'Failed to save WhatsApp settings', 'askany' ) ) );
		}
	}

	/**
	 * Test WhatsApp connection.
	 *
	 * @return void
	 */
	public function test_whatsapp_connection() {
		// Verify nonce
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_chat_nonce' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed', 'askany' ) ) );
		}

		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'askany' ) ) );
		}

		$phone_number_id = sanitize_text_field( $_POST['phone_number_id'] );
		$access_token = sanitize_text_field( $_POST['access_token'] );

		if ( empty( $phone_number_id ) || empty( $access_token ) ) {
			wp_send_json_error( array( 'message' => __( 'Phone number ID and access token are required', 'askany' ) ) );
		}

		// Test connection by fetching phone number info
		$api_url = "https://graph.facebook.com/v18.0/{$phone_number_id}";
		
		$response = wp_remote_get( $api_url, array(
			'headers' => array(
				'Authorization' => 'Bearer ' . $access_token,
			),
			'timeout' => 15,
		) );

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => __( 'Failed to connect: ', 'askany' ) . $response->get_error_message() ) );
		}

		$response_code = wp_remote_retrieve_response_code( $response );
		$response_body = wp_remote_retrieve_body( $response );
		$data = json_decode( $response_body, true );

		if ( $response_code === 200 && isset( $data['display_phone_number'] ) ) {
			wp_send_json_success( array( 
				'message' => __( 'Connection successful! Phone: ', 'askany' ) . $data['display_phone_number'],
				'phone_info' => $data
			) );
		} else {
			$error_message = isset( $data['error']['message'] ) ? $data['error']['message'] : __( 'Unknown error', 'askany' );
			wp_send_json_error( array( 'message' => __( 'Failed to connect: ', 'askany' ) . $error_message ) );
		}
	}

	/**
	 * Save WooCommerce integration settings via AJAX.
	 *
	 * @return void
	 */
	public function save_woocommerce_settings() {
		// Verify nonce
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'askany_chat_nonce' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed', 'askany' ) ) );
		}

		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'You do not have permission to save settings', 'askany' ) ) );
		}

		// Get settings
		$woocommerce_enabled = isset( $_POST['woocommerce_enabled'] ) ? (bool) $_POST['woocommerce_enabled'] : false;
		$order_tracking_enabled = isset( $_POST['woocommerce_order_tracking'] ) ? (bool) $_POST['woocommerce_order_tracking'] : false;

		// Save settings
		update_option( 'askany_woocommerce_enabled', $woocommerce_enabled );
		update_option( 'askany_woocommerce_order_tracking', $order_tracking_enabled );

		wp_send_json_success( array( 
			'message' => __( 'WooCommerce settings saved successfully!', 'askany' ),
			'woocommerce_enabled' => $woocommerce_enabled,
			'order_tracking_enabled' => $order_tracking_enabled,
		) );
	}
	
	/**
	 * Get indexed content by type for viewing.
	 *
	 * @return void
	 */
	public function get_indexed_content() {
		// Verify nonce
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_admin_nonce' ) ) {
			wp_send_json_error( 'Invalid nonce' );
		}
		
		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Insufficient permissions' );
		}
		
		$content_type = sanitize_text_field( $_POST['content_type'] );
		
		// Get content indexer instance
		$database = new Database();
		$content_indexer = new ContentIndexer( $database );
		
		// Get indexed content
		$content_items = $content_indexer->get_indexed_content_by_type( $content_type, 50, 0 );
		
		wp_send_json_success( $content_items );
	}
	
	/**
	 * Get full content details by ID.
	 *
	 * @return void
	 */
	public function get_content_details() {
		// Verify nonce
		if ( ! wp_verify_nonce( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'askany_admin_nonce' ) ) {
			wp_send_json_error( 'Invalid nonce' );
		}
		
		// Check user permissions
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Insufficient permissions' );
		}
		
		$content_id = intval( $_POST['content_id'] );
		
		// Get content indexer instance
		$database = new Database();
		$content_indexer = new ContentIndexer( $database );
		
		// Get content details
		$content_item = $content_indexer->get_indexed_content_by_id( $content_id );
		
		if ( ! $content_item ) {
			wp_send_json_error( 'Content not found' );
		}
		
		wp_send_json_success( $content_item );
	}

	/**
	 * Get full post content for news display.
	 *
	 * @return void
	 */
	public function get_full_post() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		$post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;

		if ( ! $post_id ) {
			wp_send_json_error( array( 'message' => __( 'Invalid post ID', 'askany' ) ) );
		}

		$news_handler = $this->plugin->get_news_handler();
		$post = $news_handler->get_full_post( $post_id );

		if ( ! $post ) {
			wp_send_json_error( array( 'message' => __( 'Post not found', 'askany' ) ) );
		}

		wp_send_json_success( $post );
	}


	/**
	 * Save feature request from chat conversation.
	 *
	 * @return void
	 */
	public function save_feature_request() {
		$this->save_visitor_report( 'feature' );
	}

	/**
	 * Shared handler for visitor feature requests / bug reports.
	 *
	 * Requires a valid session (created on first use, verified when it exists)
	 * and applies the flood guard + configured rate limit.
	 *
	 * @param string $kind 'feature' or 'bug'.
	 * @return void
	 */
	private function save_visitor_report( $kind ) {
		nocache_headers();
		check_ajax_referer('askany_chat_nonce', 'nonce');

		$session_id = $this->read_session_id( $_POST );
		$user_name = isset($_POST['user_name']) && is_string( $_POST['user_name'] ) ? sanitize_text_field( wp_unslash( $_POST['user_name'] ) ) : '';
		$user_email = isset($_POST['user_email']) && is_string( $_POST['user_email'] ) ? sanitize_email( wp_unslash( $_POST['user_email'] ) ) : '';
		$title = isset($_POST['title']) && is_string( $_POST['title'] ) ? mb_substr( sanitize_text_field( wp_unslash( $_POST['title'] ) ), 0, 255 ) : '';
		$description = isset($_POST['description']) && is_string( $_POST['description'] ) ? $this->sanitize_chat_message( $_POST['description'] ) : '';

		if ( false === $session_id ) {
			$this->send_invalid_session_error();
		}
		if ( empty($session_id) || empty($title) || empty($description) ) {
			wp_send_json_error(array( 'message' => __('Missing required fields.', 'askany') ));
		}

		$session_secret = $this->get_request_session_secret();
		if ( $this->database->get_chat_session( $session_id ) ) {
			if ( ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
				wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
				return;
			}
		} else {
			if ( empty( $session_secret ) ) {
				$session_secret = wp_generate_password( 32, false );
			}
			$created_id = $this->database->create_chat_session( $session_id, array( 'name' => $user_name, 'email' => $user_email ), $session_secret );
			if ( false === $created_id ) {
				wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
				return;
			}
		}

		if ( ! RateLimiter::flood_guard( $session_id, 'report' ) ) {
			wp_send_json_error( array( 'message' => __( 'You are sending requests too quickly. Please wait a moment.', 'askany' ), 'rate_limit' => true ), 429 );
		}
		$rate_check = RateLimiter::check( get_current_user_id() );
		if ( ! $rate_check['allowed'] ) {
			wp_send_json_error( array( 'message' => $rate_check['message'], 'rate_limit' => true ) );
		}

		if ( 'bug' === $kind ) {
			$result = $this->database->save_bug_report($session_id, $user_name, $user_email, $title, $description);
		} else {
			$result = $this->database->save_feature_request($session_id, $user_name, $user_email, $title, $description);
		}

		if ( $result ) {
			wp_send_json_success(array(
				'message' => 'bug' === $kind ? __('Bug report submitted successfully!', 'askany') : __('Feature request submitted successfully!', 'askany'),
				'id' => $result['id'],
				'unique_id' => $result['unique_id']
			));
		} else {
			wp_send_json_error(array( 'message' => 'bug' === $kind ? __('Failed to save bug report.', 'askany') : __('Failed to save feature request.', 'askany') ));
		}
	}

	/**
	 * Save bug report from chat conversation.
	 *
	 * @return void
	 */
	public function save_bug_report() {
		$this->save_visitor_report( 'bug' );
	}

	/**
	 * Get feature requests for admin page.
	 *
	 * @return void
	 */
	public function get_feature_requests() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$status = isset($_POST['status']) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : null;
		$limit = isset($_POST['limit']) ? intval($_POST['limit']) : 50;

		$requests = $this->database->get_feature_requests($status, $limit);

		if ( $requests !== false ) {
			wp_send_json_success(array( 'requests' => $requests ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to retrieve feature requests.', 'askany') ));
		}
	}

	/**
	 * Get bug reports for admin page.
	 *
	 * @return void
	 */
	public function get_bug_reports() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$status = isset($_POST['status']) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : null;
		$limit = isset($_POST['limit']) ? intval($_POST['limit']) : 50;

		$reports = $this->database->get_bug_reports($status, $limit);

		if ( $reports !== false ) {
			wp_send_json_success(array( 'reports' => $reports ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to retrieve bug reports.', 'askany') ));
		}
	}

	/**
	 * Update feature request status.
	 *
	 * @return void
	 */
	public function update_feature_request_status() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
		$status = isset($_POST['status']) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : '';
		$notes = isset($_POST['notes']) ? sanitize_textarea_field( wp_unslash( $_POST['notes'] ) ) : '';

		if ( empty($id) || empty($status) ) {
			wp_send_json_error(array( 'message' => __('Missing required fields.', 'askany') ));
		}

		$result = $this->database->update_feature_request_status($id, $status, $notes);

		if ( $result ) {
			wp_send_json_success(array( 'message' => __('Feature request status updated successfully.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to update feature request status.', 'askany') ));
		}
	}

	/**
	 * Update bug report status.
	 *
	 * @return void
	 */
	public function update_bug_report_status() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
		$status = isset($_POST['status']) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : '';
		$notes = isset($_POST['notes']) ? sanitize_textarea_field( wp_unslash( $_POST['notes'] ) ) : '';

		if ( empty($id) || empty($status) ) {
			wp_send_json_error(array( 'message' => __('Missing required fields.', 'askany') ));
		}

		$result = $this->database->update_bug_report_status($id, $status, $notes);

		if ( $result ) {
			wp_send_json_success(array( 'message' => __('Bug report status updated successfully.', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to update bug report status.', 'askany') ));
		}
	}

	/**
	 * Get feature request details.
	 *
	 * @return void
	 */
	public function get_feature_request_details() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$id = isset($_POST['id']) ? intval($_POST['id']) : 0;

		if ( empty($id) ) {
			wp_send_json_error(array( 'message' => __('Missing required fields.', 'askany') ));
		}

		global $wpdb;
		$table = $wpdb->prefix . 'askany_feature_requests';
		$request = $wpdb->get_row(
			$wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id)
		);

		if ( $request ) {
			wp_send_json_success(array(
				'description' => $request->feature_description,
				'notes' => $request->notes
			));
		} else {
			wp_send_json_error(array( 'message' => __('Feature request not found.', 'askany') ));
		}
	}

	/**
	 * Get bug report details.
	 *
	 * @return void
	 */
	public function get_bug_report_details() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$id = isset($_POST['id']) ? intval($_POST['id']) : 0;

		if ( empty($id) ) {
			wp_send_json_error(array( 'message' => __('Missing required fields.', 'askany') ));
		}

		global $wpdb;
		$table = $wpdb->prefix . 'askany_bug_reports';
		$report = $wpdb->get_row(
			$wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id)
		);

		if ( $report ) {
			wp_send_json_success(array(
				'description' => $report->bug_description,
				'notes' => $report->notes
			));
		} else {
			wp_send_json_error(array( 'message' => __('Bug report not found.', 'askany') ));
		}
	}

	/**
	 * Delete feature request
	 */
	public function delete_feature_request() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$id = isset($_POST['id']) ? intval($_POST['id']) : 0;

		if ( empty($id) ) {
			wp_send_json_error(array( 'message' => __('Invalid feature request ID.', 'askany') ));
		}

		$result = $this->database->delete_feature_request($id);

		if ( $result ) {
			wp_send_json_success(array( 'message' => __('Feature request deleted successfully!', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to delete feature request.', 'askany') ));
		}
	}

	/**
	 * Delete bug report
	 */
	public function delete_bug_report() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('You do not have permission to perform this action.', 'askany') ));
		}

		$id = isset($_POST['id']) ? intval($_POST['id']) : 0;

		if ( empty($id) ) {
			wp_send_json_error(array( 'message' => __('Invalid bug report ID.', 'askany') ));
		}

		$result = $this->database->delete_bug_report($id);

		if ( $result ) {
			wp_send_json_success(array( 'message' => __('Bug report deleted successfully!', 'askany') ));
		} else {
			wp_send_json_error(array( 'message' => __('Failed to delete bug report.', 'askany') ));
		}
	}

	/**
	 * Check status of bug report or feature request by unique ID
	 */
	public function check_status() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		$unique_id = isset($_POST['unique_id']) && is_string( $_POST['unique_id'] ) ? strtoupper( trim( sanitize_text_field( wp_unslash( $_POST['unique_id'] ) ) ) ) : '';
		$email     = isset($_POST['email']) && is_string( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';

		if ( empty($unique_id) || strlen( $unique_id ) > 120 ) {
			wp_send_json_error(array( 'message' => __('Invalid ID provided.', 'askany') ));
		}

		// Throttle lookups per IP to prevent enumeration
		if ( ! RateLimiter::flood_guard( '', 'status' ) ) {
			wp_send_json_error( array( 'message' => __( 'Too many requests. Please wait a moment.', 'askany' ), 'rate_limit' => true ), 429 );
		}

		// Check if it's a bug report or feature request
		if ( strpos($unique_id, 'BUG-') === 0 ) {
			$item = $this->database->get_bug_report_by_unique_id($unique_id);
			$type = 'bug';
		} elseif ( strpos($unique_id, 'FEATURE-') === 0 ) {
			$item = $this->database->get_feature_request_by_unique_id($unique_id);
			$type = 'feature';
		} else {
			wp_send_json_error(array( 'message' => __('Invalid ID format.', 'askany') ));
			return;
		}

		$not_found = __( 'No record found with this ID.', 'askany' );

		if ( ! $item ) {
			wp_send_json_error( array( 'message' => $not_found ) );
		}

		// New IDs are random (PREFIX-<12 alnum>). Legacy IDs (PREFIX-NAME-TIMESTAMP) are
		// guessable, so they additionally require the submitter's email address.
		$is_random_id = (bool) preg_match( '/^(BUG|FEATURE)-[A-Z0-9]{12}$/', $unique_id );
		if ( ! $is_random_id ) {
			$stored_email = strtolower( (string) $item->user_email );
			if ( '' === $email || '' === $stored_email || ! hash_equals( $stored_email, strtolower( $email ) ) ) {
				wp_send_json_error( array(
					'message'        => __( 'Please enter the email address you used when submitting this request.', 'askany' ),
					'email_required' => true,
				) );
			}
		}

		wp_send_json_success( array(
			'type'      => $type,
			'unique_id' => $item->unique_id,
			'status'    => $item->status,
			'title'     => 'bug' === $type ? $item->bug_title : $item->feature_title,
		) );
	}

	/**
	 * Generate embeddings in batch for RAG with time budget control
	 */
	public function generate_embeddings_batch() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Unauthorized access.', 'askany') ));
		}

		// Check if RAG is enabled
		$rag_enabled = get_option( 'askany_rag_enabled', false );
		if ( ! $rag_enabled ) {
			wp_send_json_error(array( 'message' => __('RAG is not enabled.', 'askany') ));
		}

		// Get batch size (default: 10)
		$batch_size = isset($_POST['batch_size']) ? intval($_POST['batch_size']) : 10;
		// Use offset as last_id for stable cursor-based pagination
		$last_id = isset($_POST['offset']) ? intval($_POST['offset']) : 0;

		// Get content without embeddings (including pending, processing, partial, and failed)
		// NOTE: 'permanently_failed' is deliberately excluded here — that's the circuit breaker.
		// Those rows need an explicit "Retry failed items" click.
		// For 'processing' items, only include those that haven't been updated in 5+ minutes (stuck items)
		global $wpdb;
		$content_table = $wpdb->prefix . 'askany_content_index';
		
		$content_items = $wpdb->get_results( $wpdb->prepare(
			"SELECT * FROM {$content_table} 
			WHERE (
				(has_embedding = 0 OR has_embedding IS NULL)
				OR embedding_status = 'pending'
				OR (embedding_status = 'processing' AND (embedding_last_attempt_at IS NULL OR embedding_last_attempt_at < DATE_SUB(NOW(), INTERVAL 5 MINUTE)))
				OR embedding_status = 'partial'
				OR embedding_status = 'failed'
			)
			AND id > %d
			ORDER BY id ASC
			LIMIT %d",
			$last_id,
			$batch_size
		), ARRAY_A );

		if ( empty($content_items) ) {
			wp_send_json_success(array(
				'completed' => true,
				'processed' => 0,
				'message' => __('All embeddings have been generated!', 'askany')
			));
		}

		// Get API manager instance
		$api_manager = new ApiManager();
		
		// Initialize embedding manager
		$embedding_manager = new Embedding_Manager( $this->database, $api_manager );
		
		// Check if optimization is enabled
		$optimization_enabled = get_option( 'askany_rag_optimization_enabled', false );

		// --- TIME BUDGET ---
		// This is the core fix: return a valid, partial response comfortably
		// before any host's PHP max_execution_time or reverse-proxy gateway
		// timeout can kill the request outright. That's what was actually
		// causing "stuck at 89%" — not a JS timeout, a server-side one, with
		// no graceful way to recover from it.
		$time_budget_seconds = (int) apply_filters( 'askany_rag_batch_time_budget', 20 );
		$batch_deadline = time() + $time_budget_seconds;
		if ( function_exists( 'set_time_limit' ) ) {
			@set_time_limit( $time_budget_seconds + 15 ); // best-effort safety margin
		}

		$processed = 0;
		$partial   = 0;
		$failed = 0;
		$errors = array();
		$max_id = $last_id;
		$time_budget_hit = false;

		foreach ( $content_items as $item ) {
			// Check time budget before each item
			if ( time() >= $batch_deadline ) {
				$time_budget_hit = true;
				break; // untouched items simply stay queued for the next call
			}

			if ($item['id'] > $max_id) {
				$max_id = $item['id'];
			}

			// Update status to processing
			$wpdb->update(
				$content_table,
				array( 'embedding_status' => 'processing' ),
				array( 'id' => $item['id'] )
			);

			// Get resume point
			$resume_from_index = isset( $item['embedding_last_chunk_index'] ) ? (int) $item['embedding_last_chunk_index'] : -1;
			
			// Prepare metadata
			$item_metadata = array(
				'source_url' => $item['url'],
				'title' => $item['title'],
				'excerpt' => substr( wp_strip_all_tags( $item['content'] ), 0, 500 ),
			);

			// Generate embeddings (use optimized method if enabled)
			if ( $optimization_enabled ) {
				$result = $embedding_manager->generate_content_embeddings_optimized(
					$item['id'],
					$item['object_type'],
					$item['content'],
					$item_metadata,
					$batch_deadline,
					$resume_from_index
				);
			} else {
				$result = $embedding_manager->generate_content_embeddings(
					$item['id'],
					$item['object_type'],
					$item['content'],
					$item_metadata,
					$batch_deadline,
					$resume_from_index
				);
			}

			if ( is_wp_error( $result ) ) {
				if ( $result->get_error_code() === 'partial_timeout' ) {
					$partial++; // paused, not failed — will resume next call
				} else {
					$failed++;
					$errors[] = array(
						'id' => $item['id'],
						'type' => $item['object_type'],
						'error' => $result->get_error_message()
					);
				}
			} else {
				$processed++;
			}
		}

		// Get total count
		$total = $wpdb->get_var( "SELECT COUNT(*) FROM {$content_table}" );
		$remaining = $wpdb->get_var( "SELECT COUNT(*) FROM {$content_table} 
			WHERE (
				(has_embedding = 0 OR has_embedding IS NULL)
				OR embedding_status = 'pending'
				OR (embedding_status = 'processing' AND (embedding_last_attempt_at IS NULL OR embedding_last_attempt_at < DATE_SUB(NOW(), INTERVAL 5 MINUTE)))
				OR embedding_status = 'partial'
				OR embedding_status = 'failed'
			)" );
		$permanently_failed = $wpdb->get_var( "SELECT COUNT(*) FROM {$content_table} WHERE embedding_status = 'permanently_failed'" );

		// Clear dismissed notice when processing completes
		if ( $remaining == 0 ) {
			delete_option( 'askany_rag_notice_dismissed' );
		}

		wp_send_json_success(array(
			'completed' => $remaining == 0,
			'processed' => $processed,
			'partial' => $partial,
			'failed' => $failed,
			'permanently_failed' => (int) $permanently_failed,
			'total' => $total,
			'remaining' => $remaining,
			'errors' => $errors,
			'time_budget_hit' => $time_budget_hit,
			'next_offset' => $max_id
		));
	}

	/**
	 * Get embeddings generation status
	 */
	public function get_embeddings_status() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Unauthorized access.', 'askany') ));
		}

		global $wpdb;
		$content_table = $wpdb->prefix . 'askany_content_index';
		$embeddings_table = $wpdb->prefix . 'askany_embeddings';

		$stats = array(
			'total_content' => $wpdb->get_var( "SELECT COUNT(*) FROM {$content_table}" ),
			'with_embeddings' => $wpdb->get_var( "SELECT COUNT(*) FROM {$content_table} WHERE has_embedding = 1" ),
			'pending' => $wpdb->get_var( "SELECT COUNT(*) FROM {$content_table} WHERE embedding_status = 'pending' OR embedding_status IS NULL" ),
			'processing' => $wpdb->get_var( "SELECT COUNT(*) FROM {$content_table} WHERE embedding_status = 'processing'" ),
			'failed' => $wpdb->get_var( "SELECT COUNT(*) FROM {$content_table} WHERE embedding_status = 'failed'" ),
			'total_embeddings' => $wpdb->get_var( "SELECT COUNT(*) FROM {$embeddings_table}" ),
			'chunked_content' => $wpdb->get_var( "SELECT COUNT(DISTINCT content_id) FROM {$embeddings_table} WHERE total_chunks > 1" ),
		);

		// Get embeddings by content type
		$by_type = $wpdb->get_results( "
			SELECT content_type, COUNT(*) as count 
			FROM {$embeddings_table} 
			GROUP BY content_type
		", ARRAY_A );

		$stats['by_type'] = $by_type;

		wp_send_json_success($stats);
	}

	/**
	 * Clear all embeddings
	 */
	public function clear_all_embeddings() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Unauthorized access.', 'askany') ));
		}

		global $wpdb;
		$content_table = $wpdb->prefix . 'askany_content_index';
		$embeddings_table = $wpdb->prefix . 'askany_embeddings';

		// Clear embeddings table
		$wpdb->query( "TRUNCATE TABLE {$embeddings_table}" );

		// Reset content index embedding status
		$wpdb->query( "
			UPDATE {$content_table} 
			SET has_embedding = 0, 
				embedding_status = 'pending', 
				embedding_error = NULL
		" );

		wp_send_json_success(array(
			'message' => __('All embeddings cleared successfully', 'askany')
		));
	}

	/**
	 * Save embedding model selection
	 */
	public function save_embedding_model() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Unauthorized access.', 'askany') ));
		}

		$model = isset( $_POST['model'] ) ? sanitize_text_field( $_POST['model'] ) : '';

		if ( empty( $model ) ) {
			wp_send_json_error(array( 'message' => __('Invalid model', 'askany') ));
		}

		// Validate model
		$valid_models = array(
			'text-embedding-3-small',
			'text-embedding-3-large',
			'text-embedding-ada-002',
			'gemini-embedding-001',
		);

		if ( ! in_array( $model, $valid_models ) ) {
			wp_send_json_error(array( 'message' => __('Invalid model', 'askany') ));
		}

		// Save the model
		update_option( 'askany_embedding_model', $model );

		wp_send_json_success(array(
			'message' => __('Embedding model saved successfully', 'askany'),
			'model' => $model
		));
	}

	/**
	 * Get embedding errors from database
	 */
	public function get_embedding_errors() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Unauthorized access.', 'askany') ));
		}

		global $wpdb;
		$content_table = $wpdb->prefix . 'askany_content_index';

		// Get all failed items with details (using object_type instead of content_type)
		$errors = $wpdb->get_results( "
			SELECT 
				id as content_id,
				object_type as content_type,
				title,
				embedding_error as error_message,
				updated_at as last_attempt
			FROM {$content_table}
			WHERE embedding_status = 'failed'
			ORDER BY updated_at DESC
			LIMIT 100
		", ARRAY_A );

		if ( empty( $errors ) ) {
			wp_send_json_success(array(
				'errors' => array(),
				'message' => __('No errors found', 'askany')
			));
		}

		// Format the errors for better display
		foreach ( $errors as &$error ) {
			// Format the timestamp
			if ( $error['last_attempt'] ) {
				$error['last_attempt'] = human_time_diff( strtotime( $error['last_attempt'] ), current_time('timestamp') ) . ' ' . __('ago', 'askany');
			}
			
			// Decode title if needed
			if ( $error['title'] ) {
				$error['title'] = html_entity_decode( $error['title'], ENT_QUOTES, 'UTF-8' );
			}
			
			// Clean up error message
			if ( $error['error_message'] ) {
				$error['error_message'] = strip_tags( $error['error_message'] );
			}
		}

		wp_send_json_success(array(
			'errors' => $errors,
			'total' => count( $errors )
		));
	}

	/**
	 * Reset stuck embeddings (items stuck in 'processing' status)
	 */
	public function reset_stuck_embeddings() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Unauthorized access.', 'askany') ));
		}

		global $wpdb;
		$content_table = $wpdb->prefix . 'askany_content_index';

		// Reset items stuck in 'processing' status back to 'pending'
		$updated = $wpdb->query( "
			UPDATE {$content_table}
			SET embedding_status = 'pending'
			WHERE embedding_status = 'processing'
		" );

		wp_send_json_success(array(
			'message' => sprintf( __('Reset %d stuck items', 'askany'), $updated ),
			'updated' => $updated
		));
	}

	/**
	 * Reset permanently-failed items back to pending so they re-enter the
	 * automatic batch queue. Manual, deliberate action — not automatic,
	 * so a genuinely broken item doesn't loop forever without anyone noticing.
	 */
	public function retry_permanently_failed_embeddings() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Unauthorized access.', 'askany') ));
		}

		global $wpdb;
		$content_table = $wpdb->prefix . 'askany_content_index';

		$updated = $wpdb->query( "
			UPDATE {$content_table}
			SET embedding_status = 'pending', embedding_attempts = 0, embedding_error = NULL
			WHERE embedding_status = 'permanently_failed'
		" );

		wp_send_json_success(array(
			'message' => sprintf( __('%d permanently-failed item(s) re-queued for retry', 'askany'), $updated ),
			'updated' => $updated
		));
	}

	/**
	 * Dismiss RAG notice
	 */
	public function dismiss_rag_notice() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Unauthorized access.', 'askany') ));
		}

		$notice_type = isset( $_POST['notice_type'] ) ? sanitize_text_field( wp_unslash( $_POST['notice_type'] ) ) : '';

		if ( empty( $notice_type ) ) {
			wp_send_json_error(array( 'message' => __('Invalid notice type.', 'askany') ));
		}

		// Save dismissed notice type to database
		update_option( 'askany_rag_notice_dismissed', $notice_type );

		wp_send_json_success(array( 'message' => __('Notice dismissed.', 'askany') ));
	}

	/**
	 * Check if quota error exists
	 */
	public function check_quota_error() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Unauthorized access.', 'askany') ));
		}

		$quota_error = get_option( 'askany_quota_error_state', array() );
		
		if ( ! empty( $quota_error ) && isset( $quota_error['has_error'] ) && $quota_error['has_error'] ) {
			wp_send_json_success(array(
				'has_quota_error' => true,
				'error_message' => isset( $quota_error['message'] ) ? $quota_error['message'] : '',
				'is_dismissed' => isset( $quota_error['is_dismissed'] ) ? $quota_error['is_dismissed'] : false
			));
		} else {
			wp_send_json_success(array( 'has_quota_error' => false ));
		}
	}

	/**
	 * Save quota error state
	 */
	public function save_quota_error() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Unauthorized access.', 'askany') ));
		}

		$error_message = isset( $_POST['error_message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['error_message'] ) ) : '';

		// Get existing quota error state
		$existing_error = get_option( 'askany_quota_error_state', array() );
		$existing_message = isset( $existing_error['message'] ) ? $existing_error['message'] : '';

		// If this is a NEW error (different message), clear the dismissed state
		// This allows the notice to show again for a new quota error
		$is_dismissed = false;
		if ( $existing_message === $error_message && isset( $existing_error['is_dismissed'] ) ) {
			// Same error, keep dismissed state
			$is_dismissed = $existing_error['is_dismissed'];
		}

		// Save quota error state to database
		update_option( 'askany_quota_error_state', array(
			'has_error' => true,
			'message' => $error_message,
			'is_dismissed' => $is_dismissed,
			'timestamp' => current_time( 'mysql' )
		));

		wp_send_json_success(array( 'message' => __('Quota error state saved.', 'askany') ));
	}

	/**
	 * Dismiss quota error notice (but keep blocking processing)
	 */
	public function dismiss_quota_error() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Unauthorized access.', 'askany') ));
		}

		// Get existing quota error state
		$quota_error = get_option( 'askany_quota_error_state', array() );

		if ( ! empty( $quota_error ) ) {
			// Mark as dismissed but keep the error state (still blocks processing)
			$quota_error['is_dismissed'] = true;
			update_option( 'askany_quota_error_state', $quota_error );
		}

		wp_send_json_success(array( 'message' => __('Quota error notice dismissed.', 'askany') ));
	}

	/**
	 * Clear quota error state (allow retry)
	 */
	public function clear_quota_error() {
		check_ajax_referer('askany_chat_nonce', 'nonce');

		if ( ! current_user_can('manage_options') ) {
			wp_send_json_error(array( 'message' => __('Unauthorized access.', 'askany') ));
		}

		// Clear quota error state from database completely
		delete_option( 'askany_quota_error_state' );

		wp_send_json_success(array( 'message' => __('Quota error state cleared.', 'askany') ));
	}

	/**
	 * Save rate limit settings via AJAX.
	 *
	 * @return void
	 */
	public function save_rate_limit_settings() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'askany' ) ) );
		}

		$enabled = isset( $_POST['rate_limit_enabled'] ) ? (bool) $_POST['rate_limit_enabled'] : false;
		$roles_raw = isset( $_POST['rate_limit_roles'] ) ? wp_unslash( $_POST['rate_limit_roles'] ) : '{}';
		$roles = json_decode( $roles_raw, true );

		if ( ! is_array( $roles ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid data format.', 'askany' ) ) );
		}

		\Askany\RateLimiter::save_settings( $enabled, $roles );

		wp_send_json_success( array( 'message' => __( 'Rate limit settings saved.', 'askany' ) ) );
	}

	/**
	 * Handle file attachment download.
	 */
	public function download_attachment() {
		$attachment_id = isset( $_GET['attachment_id'] ) ? intval( $_GET['attachment_id'] ) : 0;
		
		if ( ! $attachment_id ) {
			wp_die( __( 'Invalid attachment ID.', 'askany' ) );
		}

		$file_handler = $this->plugin->get_file_attachment_handler();
		$file_handler->handle_download( $attachment_id );
	}

	/**
	 * Get attachments for a session.
	 */
	public function get_session_attachments() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'askany' ) ) );
		}

		$session_id = isset( $_POST['session_id'] ) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : '';

		if ( empty( $session_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Session ID is required.', 'askany' ) ) );
		}

		$file_handler = $this->plugin->get_file_attachment_handler();
		$attachments = $file_handler->get_session_attachments( $session_id );

		// Add download URLs and remove server filesystem paths
		foreach ( $attachments as &$attachment ) {
			$attachment->download_url = $file_handler->get_download_url( $attachment->id );
			unset( $attachment->file_path );
		}

		wp_send_json_success( array( 'attachments' => $attachments ) );
	}

	/**
	 * Delete an attachment.
	 */
	public function delete_attachment() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'askany' ) ) );
		}

		$attachment_id = isset( $_POST['attachment_id'] ) ? intval( $_POST['attachment_id'] ) : 0;

		if ( ! $attachment_id ) {
			wp_send_json_error( array( 'message' => __( 'Invalid attachment ID.', 'askany' ) ) );
		}

		$file_handler = $this->plugin->get_file_attachment_handler();
		$result = $file_handler->delete_attachment( $attachment_id );

		if ( $result ) {
			wp_send_json_success( array( 'message' => __( 'Attachment deleted successfully.', 'askany' ) ) );
		} else {
			wp_send_json_error( array( 'message' => __( 'Failed to delete attachment.', 'askany' ) ) );
		}
	}

	/**
	 * Test custom database tables connection and structure.
	 */
	public function test_custom_tables() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized access.', 'askany' ) ) );
		}

		$table_names = isset( $_POST['table_names'] ) ? sanitize_text_field( wp_unslash( $_POST['table_names'] ) ) : '';

		if ( empty( trim( $table_names ) ) ) {
			wp_send_json_error( array( 'message' => __( 'No table names provided.', 'askany' ) ) );
		}

		$tables  = array_map( 'trim', explode( ',', $table_names ) );
		$results = array();

		foreach ( $tables as $table_name ) {
			if ( empty( $table_name ) ) {
				continue;
			}

			$table_info = $this->database->get_custom_table_info( $table_name );
			$results[]  = $table_info;
		}

		wp_send_json_success( array(
			'tables' => $results,
			'total'  => count( $results ),
		) );
	}

	/**
	 * Index custom database table data into RAG content index.
	 */
	public function index_custom_table() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized access.', 'askany' ) ) );
		}

		$table_name    = isset( $_POST['table_name'] ) ? sanitize_text_field( wp_unslash( $_POST['table_name'] ) ) : '';
		$create_chunks = isset( $_POST['create_chunks'] ) && ( $_POST['create_chunks'] === 'true' || $_POST['create_chunks'] === '1' );
		$chunk_size    = isset( $_POST['chunk_size'] ) ? absint( $_POST['chunk_size'] ) : 10;

		if ( empty( trim( $table_name ) ) ) {
			wp_send_json_error( array( 'message' => __( 'No table name provided.', 'askany' ) ) );
		}

		$options = array(
			'create_chunks' => $create_chunks,
			'chunk_size'    => $chunk_size,
			'limit'         => 1000,
		);

		$result = $this->database->index_custom_table_data( $table_name, $options );

		if ( isset( $result['error'] ) ) {
			wp_send_json_error( array( 'message' => $result['error'] ) );
		}

		// Automatically persist table name into askany_custom_db_tables option
		$custom_tables = get_option( 'askany_custom_db_tables', array() );
		if ( ! is_array( $custom_tables ) ) {
			$custom_tables = array();
		}
		if ( ! in_array( $table_name, $custom_tables, true ) ) {
			$custom_tables[] = $table_name;
			update_option( 'askany_custom_db_tables', array_values( array_unique( array_filter( $custom_tables ) ) ) );
		}

		wp_send_json_success( $result );
	}

	/**
	 * Clear custom database transient query cache.
	 */
	public function clear_custom_db_cache() {
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized access.', 'askany' ) ) );
		}

		global $wpdb;
		$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_askany_cdb_%' OR option_name LIKE '_transient_timeout_askany_cdb_%'" );

		wp_send_json_success( array( 'message' => __( 'Custom DB query cache cleared successfully.', 'askany' ) ) );
	}

	/**
	 * AJAX handler to install an addon from WordPress.org.
	 *
	 * @return void
	 */
	public function install_addon() {
		check_ajax_referer( 'askany_addons_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) || ! current_user_can( 'install_plugins' ) ) {
			wp_send_json_error( array( 'message' => __( 'You do not have permission to install plugins.', 'askany' ) ) );
		}

		$addon_slug = isset( $_POST['addon_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['addon_slug'] ) ) : 'postloom-ai-post-generator-and-publisher';
		if ( empty( $addon_slug ) || 'ai-post-generator-and-publisher' === $addon_slug ) {
			$addon_slug = 'postloom-ai-post-generator-and-publisher';
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

		// Query WordPress.org plugins API for download URL.
		$api = plugins_api( 'plugin_information', array(
			'slug'   => $addon_slug,
			'fields' => array(
				'sections' => false,
			),
		) );

		$download_url = '';
		if ( ! is_wp_error( $api ) && ! empty( $api->download_link ) ) {
			$download_url = $api->download_link;
		} else {
			$download_url = sprintf( 'https://downloads.wordpress.org/plugin/%s.zip', $addon_slug );
		}

		$skin     = new \WP_Ajax_Upgrader_Skin();
		$upgrader = new \Plugin_Upgrader( $skin );
		$result   = $upgrader->install( $download_url );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array(
				'message' => sprintf( __( 'Failed to install addon: %s', 'askany' ), $result->get_error_message() ),
			) );
		}

		if ( is_wp_error( $skin->result ) ) {
			wp_send_json_error( array(
				'message' => sprintf( __( 'Failed to install addon: %s', 'askany' ), $skin->result->get_error_message() ),
			) );
		}

		$addon_file = 'postloom-ai-post-generator-and-publisher/postloom-ai-post-generator-and-publisher.php';
		if ( ! file_exists( WP_PLUGIN_DIR . '/' . $addon_file ) && file_exists( WP_PLUGIN_DIR . '/ai-post-generator-and-publisher/ai-post-generator-and-publisher.php' ) ) {
			$addon_file = 'ai-post-generator-and-publisher/ai-post-generator-and-publisher.php';
		}

		if ( ! file_exists( WP_PLUGIN_DIR . '/' . $addon_file ) ) {
			wp_send_json_error( array(
				'message' => __( 'Addon installed but main plugin file was not found.', 'askany' ),
			) );
		}

		wp_send_json_success( array(
			'message'      => __( 'Addon installed successfully from WordPress.org!', 'askany' ),
			'addon_file'   => $addon_file,
			'redirect_url' => admin_url( 'admin.php?page=aipgp-dashboard' ),
		) );
	}

	/**
	 * AJAX handler to activate an installed addon.
	 *
	 * @return void
	 */
	public function activate_addon() {
		check_ajax_referer( 'askany_addons_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) || ! current_user_can( 'activate_plugins' ) ) {
			wp_send_json_error( array( 'message' => __( 'You do not have permission to activate plugins.', 'askany' ) ) );
		}

		$addon_file  = isset( $_POST['addon_file'] ) ? sanitize_text_field( wp_unslash( $_POST['addon_file'] ) ) : 'postloom-ai-post-generator-and-publisher/postloom-ai-post-generator-and-publisher.php';
		if ( ! file_exists( WP_PLUGIN_DIR . '/' . $addon_file ) && file_exists( WP_PLUGIN_DIR . '/ai-post-generator-and-publisher/ai-post-generator-and-publisher.php' ) ) {
			$addon_file = 'ai-post-generator-and-publisher/ai-post-generator-and-publisher.php';
		}
		$plugin_path = WP_PLUGIN_DIR . '/' . $addon_file;

		if ( ! file_exists( $plugin_path ) ) {
			wp_send_json_error( array( 'message' => __( 'Addon plugin file does not exist.', 'askany' ) ) );
		}

		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		$result = activate_plugin( $addon_file );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array(
				'message' => sprintf( __( 'Failed to activate addon: %s', 'askany' ), $result->get_error_message() ),
			) );
		}

		wp_send_json_success( array(
			'message'      => __( 'Addon activated successfully!', 'askany' ),
			'redirect_url' => admin_url( 'admin.php?page=aipgp-dashboard' ),
		) );
	}

	/**
	 * Download chat transcript for a session (free feature).
	 *
	 * Returns JSON with transcript text. JS creates and downloads the file client-side.
	 * Security: session_id is the unique user token — only the session owner knows it.
	 *
	 * @return void
	 */
	public function download_transcript() {
		nocache_headers();
		check_ajax_referer( 'askany_chat_nonce', 'nonce' );

		$session_id     = isset( $_POST['session_id'] ) && is_string( $_POST['session_id'] ) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : '';
		$session_secret = $this->get_request_session_secret();

		if ( empty( $session_id ) || ! Database::is_valid_session_id( $session_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Session ID is required.', 'askany' ) ) );
		}

		if ( ! $this->database->verify_session_access( $session_id, $session_secret ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized session access.', 'askany' ), 'code' => 'session_invalid' ), 403 );
		}

		global $wpdb;

		// Use the Database class property names — correct table names
		$table_sessions = $wpdb->prefix . 'askany_chat_sessions';
		$table_messages = $wpdb->prefix . 'askany_chat_messages';

		// Verify the tables exist before querying (prevents DB errors on fresh installs)
		$sessions_exists = $wpdb->get_var( "SHOW TABLES LIKE '{$table_sessions}'" );
		$messages_exists = $wpdb->get_var( "SHOW TABLES LIKE '{$table_messages}'" );

		if ( ! $sessions_exists || ! $messages_exists ) {
			wp_send_json_error( array( 'message' => __( 'Chat tables not found. Please ensure the plugin is fully activated.', 'askany' ) ) );
		}

		// Verify session exists
		$session = $wpdb->get_row( $wpdb->prepare(
			"SELECT id, session_id, user_name FROM {$table_sessions} WHERE session_id = %s LIMIT 1",
			$session_id
		) );

		if ( ! $session ) {
			wp_send_json_error( array( 'message' => __( 'Session not found.', 'askany' ) ) );
		}

		// Fetch messages — exclude system type, order ascending
		// Column names: sender_type (user/assistant/agent), message, message_type, created_at
		$messages = $wpdb->get_results( $wpdb->prepare(
			"SELECT sender_type, message, created_at
			 FROM {$table_messages}
			 WHERE session_id = %s
			   AND message_type != 'system'
			 ORDER BY id ASC",
			$session_id
		) );

		if ( empty( $messages ) ) {
			wp_send_json_error( array( 'message' => __( 'No messages found for this session.', 'askany' ) ) );
		}

		// Build plain-text transcript
		$site_name  = get_bloginfo( 'name' );
		$date_str   = current_time( 'Y-m-d H:i' );
		$user_label = ! empty( $session->user_name ) ? sanitize_text_field( $session->user_name ) : __( 'You', 'askany' );

		$lines   = array();
		$lines[] = $site_name . ' — ' . __( 'Chat Transcript', 'askany' );
		$lines[] = __( 'Date:', 'askany' ) . ' ' . $date_str;
		$lines[] = str_repeat( '-', 50 );
		$lines[] = '';

		foreach ( $messages as $msg ) {
			$time = date_i18n( 'H:i', strtotime( $msg->created_at ) );

			switch ( $msg->sender_type ) {
				case 'user':
					$sender = $user_label;
					break;
				case 'agent':
					$sender = __( 'Agent', 'askany' );
					break;
				default:
					$sender = __( 'Assistant', 'askany' );
					break;
			}

			$text    = wp_strip_all_tags( $msg->message );
			$lines[] = '[' . $time . '] ' . $sender . ': ' . $text;
		}

		$lines[] = '';
		$lines[] = str_repeat( '-', 50 );
		/* translators: %s: site name */
		$lines[] = sprintf( __( 'Powered by %s', 'askany' ), $site_name );

		wp_send_json_success( array(
			'transcript' => implode( "\n", $lines ),
			'filename'   => 'chat-transcript-' . date_i18n( 'Y-m-d-Hi' ) . '.txt',
		) );
	}
}
