<?php
// Gets included in main plugin php, no need to get Wordpress Context or vendor include

if (!defined('ABSPATH')) exit;

require_once __DIR__ . '/twx_util.php';
require_once __DIR__ . '/openai_include.php';
require_once __DIR__ . '/knowledge_base_include.php';
require_once __DIR__ . '/chatlog_include.php';

if(file_exists(__DIR__ . '/twx_chat_plus.php')){
	require_once __DIR__ . '/twx_chat_plus.php';
}
if(file_exists(__DIR__ . '/twx_chat_pro.php')){
	require_once __DIR__ . '/twx_chat_pro.php';
}

function twxchat_query() {

	// AJAX Handler
	check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	TWXChatUtil::log('twx_chat_query:: start', 'w');

	$apiKey = get_option('twx_chat_options')['openai_apikey'] ?? '';
	
    // Run the User Query
    if (!isset($_POST['message'])) {
        wp_send_json_error(__('Invalid request','twx-chat'));
    }
    $message = sanitize_text_field($_POST['message']);
	$isCommonQuestion = (isset($_POST['isCommonQuestion']) && $_POST['isCommonQuestion'] == 'true'); // was the message from a Common Question button click
	$messageNo = sanitize_text_field($_POST['messageNo'] ?? '');
	$assistantId = sanitize_text_field($_POST['assistantId'] ?? '');
	$threadId = sanitize_text_field($_POST['threadId'] ?? null);
	$knowledgeBaseSource = sanitize_text_field($_POST['knowledgeBaseSource'] ?? '');
	$minSimilarity = sanitize_text_field($_POST['minSimilarity'] ?? '');
	if(!empty($minSimilarity)) {
		$minSimilarity = intval($minSimilarity)/100;
	}else{
		$minSimilarity = intval(get_option('twx_chat_options')['min_similarity'])/100;
	}
	$isVoiceInput = (isset($_POST['isVoiceInput']) && $_POST['isVoiceInput'] == 'true');
	$ttsVoice = sanitize_text_field($_POST['ttsVoice'] ?? '');
	$llmModel = sanitize_text_field($_POST['llmModel'] ?? '');
	$chatbotId = sanitize_text_field($_POST['chatbotId'] ?? null);

	if (empty($chatbotId)) {
		$response = [
			'status' => 'error',
			'message' => __('Missing chatbot ID','twx-chat'),
			'data' => [],
		];
		wp_send_json_error(json_encode($response));
		wp_die();
	}

	$chatbotAtts = TWXChatUtil::getChatbotAttributesById(['id'=>$chatbotId],true);
	$extraInstructions = $chatbotAtts['extra_instructions'] ?? '';
	$startingComments = $chatbotAtts['starting_comments'] ?? '';
	$noKBMatchText = $chatbotAtts['kb_no_match_text'] ?? '';
	$maxOutputTokens = $chatbotAtts['max_output_tokens'] ?? TWXCHAT_DEFAULT_MAX_OUTPUT_TOKENS;

	// Validate inputs
	if (empty($message) || empty($apiKey)) {
		$response = [
			'status' => 'error',
			'message' => __('Missing apiKey or message','twx-chat'),
			'data' => [],
		];
		wp_send_json_error(json_encode($response));
		wp_die();
	}
	
	// Perform Query to OpenAI LLM
	$queryResponse = [];
	if(!empty($assistantId)){
		$queryResponse = twxchat_query_with_assistant($apiKey,$threadId,$assistantId,$message,$extraInstructions,$minSimilarity,$knowledgeBaseSource);
	}else{
		$queryResponse = twxchat_query_no_assistant($apiKey, $threadId, $knowledgeBaseSource, $minSimilarity, $maxOutputTokens, $message, $isCommonQuestion, $extraInstructions, $startingComments, $noKBMatchText, $llmModel, $chatbotId);
	}

	// Prepare Response to Client
	$similarKnowledgeBaseEntries = null;
	$dataOut = [];
	$botCorrections = [];

	if(empty($threadId) && isset($queryResponse['threadId'])) $threadId = $queryResponse['threadId'];
	$dataOut['threadId'] = $threadId;

	if(isset($queryResponse['similarKnowledgeBaseEntries'])) $similarKnowledgeBaseEntries = $queryResponse['similarKnowledgeBaseEntries'];

	if(isset($queryResponse['error'])) {
		$dataOut['error'] = true;
		$dataOut['answer'] = $queryResponse['answer'];
	}else{
		$parseResults = twxchat_parseAnswer($queryResponse['answer'], $similarKnowledgeBaseEntries);
		$dataOut['answer'] = $parseResults['answer'];
		if(isset($parseResults['corrections'])) $botCorrections = $parseResults['corrections'];
	}
	
	if(isset($queryResponse['usage'])) $dataOut['usage'] = json_encode($queryResponse['usage']);
	if(isset($queryResponse['model'])) {
		$dataOut['model'] = $queryResponse['model'];
		$dataOut['model_max_tokens'] = TWXChatUtil::getModelCosts($queryResponse['model'])['max_tokens'] ?? 'unk';
	}
	
	// Source IDs
	$dataOut['sourceIds'] = [];
	if(isset($queryResponse['relevant_source_ids']) && !empty($queryResponse['relevant_source_ids'])) {
		$dataOut['sourceIds'] = $queryResponse['relevant_source_ids'];
	}

	// Sources
	$dataOut['sources'] = [];
	if(isset($queryResponse['relevant_sources']) && !empty($queryResponse['relevant_sources'])) {
		$dataOut['sources'] = $queryResponse['relevant_sources'];
	}

	if(isset($queryResponse['estimated'])) $dataOut['estimated'] = $queryResponse['estimated'];
	
	// Split out any JSON data from response
	list($chatLogBotResponse, $json_data_string) = TWXChatUtil::splitDataFromBotResponse($dataOut['answer']);

	// User Fields (Plus Plan)
	if(TWXChatUtil::customerPlanAccess('plus')){
		if(isset($queryResponse['user_fields'])){
			$dataOut['user_fields'] = $queryResponse['user_fields'];
		} else {
			if(!empty($json_data_string) && TWXChatUtil::isValidJson($json_data_string)) {
				$jsonData = json_decode($json_data_string, true);
				if(isset($jsonData['user_fields']) && !empty($jsonData['user_fields'])){
					$dataOut['user_fields'] = $jsonData['user_fields'];
				}
			}
		}

		// Invoke User Fields Webhook
		if(isset($dataOut['user_fields']) && !empty($dataOut['user_fields']) && isset($queryResponse['userFieldsUpdateWebhookUrl']) && !empty($queryResponse['userFieldsUpdateWebhookUrl'])){
			// This returns immediately and doesn't wait for a response
			$response = TWXChatPlus::invokeUpdateUserFieldsWebhook($queryResponse['userFieldsUpdateWebhookUrl'], $chatbotId, $threadId, $dataOut['user_fields']);
			// Ignore errors
		}
	}

	// Custom Text To Speech (TTS)
	if(!empty($ttsVoice) && $isVoiceInput === true) {
		// if user using microphone to make query, assume we want TTS in response
		$ttsProvider = 'openai'; 
		$providerVoiceCode = $ttsVoice;
		$split = explode(':',$ttsVoice);
		if(count($split)==2){
			$ttsProvider = $split[0];
			$providerVoiceCode = $split[1];
		}
		$text = twxchat_removeHyperlinkURLsFromMarkdown($chatLogBotResponse);

		$createTTS = twxchat_createTTSFile_internal($apiKey, $threadId, $messageNo, $text, $ttsProvider, $providerVoiceCode);
		if(!isset($createTTS['error']) && isset($createTTS['outputFileName'])){
			$dataOut['voiceOutputFileName'] = $createTTS['outputFileName'];
		}
	}

	// Chat Log entry
	$logSources = [];
	if(!empty($dataOut['sources'])){
		$logSources = $dataOut['sources'];
	}else{
		if(!empty($json_data_string) && TWXChatUtil::isValidJson($json_data_string)) {
			$jsonData = json_decode($json_data_string, true);
			if(isset($jsonData['source_ids']) && !empty($jsonData['source_ids'])){
				$relevant_source_ids = $jsonData['source_ids'];
				$logSources = TWXChatKnowledgeBaseUtil::getSourcesBySourceIds($relevant_source_ids);
			}
		}
	}
	$logData = [
		'userInput' => $message,
		'isVoiceInput' => ($isVoiceInput ? 'true' : 'false'),
		'botResponse' => $chatLogBotResponse, // $dataOut['answer'],
		'isError' => (isset($dataOut['error']) ? $dataOut['error'] : false),
		'sources' => $logSources,
		'wpUser' => TWXChatUtil::getWpUserLogin(),
	];
	if(!empty($botCorrections)) $logData['botCorrections'] = $botCorrections;
	if(!empty($knowledgeBaseSource)) $logData['knowledgeBaseSource'] = $knowledgeBaseSource;
	if(!empty($extraInstructions)) $logData['extraInstructions'] = $extraInstructions;
	if(!empty($assistantId)) $logData['assistantId'] = $assistantId;
	if(isset($dataOut['user_fields'])) $logData['user_fields'] = $dataOut['user_fields'];
	if(!empty($chatbotId)) $logData['chatbotId'] = $chatbotId;
	TWXChatLog::insertChatLog($threadId, $logData);
	
	// Finish up
	wp_send_json_success($dataOut);

    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_query', 'twxchat_query');
add_action('wp_ajax_nopriv_twx_chat_query', 'twxchat_query');

function twxchat_getKnowledgeBaseSources(){

	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	// return Knowledge Base sources by their IDs (unique sources)
	$sources = [];

	//$source_ids_raw = $_POST['source_ids'] ?? []; // sanitization on next line with absint
	$source_ids = array_map('absint', isset($_POST['source_ids']) && is_array($_POST['source_ids']) ? $_POST['source_ids'] : []);
	$include_feedback_sources = (isset($_POST['include_feedback_sources']) && $_POST['include_feedback_sources'] == 'true');

	$entries = TWXChatKnowledgeBaseUtil::getEntriesBySourceIds($source_ids);

	$source_urls = [];

	foreach($entries as $id => $entry){
		if(!$include_feedback_sources && TWXChatKnowledgeBaseUtil::isFeedbackSource($entry['source'])) continue;
		if(isset($source_urls[$entry['source']])) continue; // disallow dup urls
		$sources[] = [
			'source_id' => $id, 
			'source' => $entry['source'], 
			'title' => $entry['title'], 
			'similarity_score' => $entry['similarity_score'] ?? null, 
			'content_length_chars' => strlen($entry['orig_text']),
			'last_modified' => $entry['created_at'],
			'images' => TWXChatUtil::extractMarkdownImages($entry['orig_text'] ?? ''),
			'blurb'=> TWXChatUtil::extractBlurbFromMarkdown($entry['orig_text'] ?? ''),
		];
		$source_urls[$entry['source']] = true;
	}

	$ret = ['sources' => $sources];
	wp_send_json_success($ret);
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_getKnowledgeBaseSources', 'twxchat_getKnowledgeBaseSources');
add_action('wp_ajax_nopriv_twx_chat_getKnowledgeBaseSources', 'twxchat_getKnowledgeBaseSources');

function twxchat_updateChatLog() {

	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	// updates the chatlog with eg: user hyperlink clicks within bot response etc

	$updateType = sanitize_text_field($_POST['updateType'] ?? 'upsert'); // can be 'upsert', 'append', 'remove'
	
	$threadId = sanitize_text_field($_POST['threadId'] ?? '');
	if(empty($threadId)){
		TWXChatUtil::log("missing threadId. aborting");
		wp_send_json_error(__('Missing threadId. aborting','twx-chat'));
		wp_die();
	}
	$messageNo = intval($_POST['messageNo'] ?? '1');
	$properties = sanitize_text_field($_POST['properties'] ?? '');
	if(empty($properties)){
		TWXChatUtil::log("missing properties. aborting");
		wp_send_json_error(__('Missing properties. aborting','twx-chat'));
		wp_die();
	}
	
	try{
		$properties = stripslashes($properties);
		$properties = json_decode($properties,true);
	}catch(Exception $ex){
		TWXChatUtil::log("ajax_handler.php:: error parsing properties: ".$ex->getMessage(),'a',true);
		wp_send_json_error(__('Invalid json. Aborting','twx-chat'));
		wp_die();
	}
	
	$entries = TWXChatLog::getChatLogEntriesByThreadId($threadId);
	if(empty($entries)){
		TWXChatUtil::log("no entries with thread $threadId. aborting");
		wp_send_json_error(__('No entries with thread','twx-chat')." $threadId. ".__('Aborting','twx-chat'));
		wp_die();
	}

	if($messageNo > count($entries)){
		TWXChatUtil::log("invalid message no $messageNo");
		wp_send_json_error(__("Invalid message no",'twx-chat')." $messageNo");
		wp_die();
	}

	if(!isset($entries[$messageNo-1])){
		TWXChatUtil::log("error getting chatlog entry");
		wp_send_json_error(__("Error getting chatlog entry",'twx-chat'));
		wp_die();
	}
	$entry = $entries[$messageNo-1];
	$newProps = [];
	if(!empty($entry['properties'])) $newProps = json_decode($entry['properties'],true);

	foreach($properties as $propertyField => $propertyValue){

		switch ($updateType) {
			case 'upsert':
				$newProps[$propertyField] = $propertyValue;  // add if new or replace if existing
				break;
			case 'append':
				$newProps[$propertyField][] = $propertyValue;
				break;
			case 'remove':
				if(isset($newProps[$propertyField])) unset($newProps[$propertyField]);
				break;
		}

	}

	$ret = TWXChatLog::updateChatLogProperties($entry['id'], json_encode($newProps));
	
	wp_send_json_success($ret);

    // Always die in AJAX handlers
    wp_die();
	
}
add_action('wp_ajax_twx_chat_updateChatLog', 'twxchat_updateChatLog');
add_action('wp_ajax_nopriv_twx_chat_updateChatLog', 'twxchat_updateChatLog');

function twxchat_getChatLogEntries() {

	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	// gets chat log entries for given thread id and chatbot id

	$threadId = sanitize_text_field($_POST['threadId'] ?? '');
	if(empty($threadId)){
		TWXChatUtil::log("missing threadId. aborting");
		wp_send_json_error(__('Missing threadId. aborting','twx-chat'));
		wp_die();
	}

	$chatbotId = sanitize_text_field($_POST['chatbotId'] ?? '');
	if(empty($chatbotId)){
		TWXChatUtil::log("missing chatbotId. aborting");
		wp_send_json_error(__('Missing chatbotId. aborting','twx-chat'));
		wp_die();
	}

	$entries = TWXChatLog::getChatLogEntriesByThreadId($threadId, null, $chatbotId);

	$ret = [];
	$ret['thread_id'] = $threadId;
	$ret['chatbot_id'] = $chatbotId;
	$ret['chatlog_entries'] = $entries;

	wp_send_json_success($ret);

    // Always die in AJAX handlers
    wp_die();
	
}
add_action('wp_ajax_twx_chat_getChatLogEntries', 'twxchat_getChatLogEntries');
add_action('wp_ajax_nopriv_twx_chat_getChatLogEntries', 'twxchat_getChatLogEntries');

function twxchat_invokeUpdateUserFieldsWebhook() {

	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	// invokes update user fields webhook
	if(!TWXChatUtil::customerPlanAccess('plus')){
		wp_send_json_error(__('Permission denied','twx-chat'));
		wp_die();
	}

	$chatbotId = sanitize_text_field($_POST['chatbotId'] ?? '');
	if(empty($chatbotId)){
		TWXChatUtil::log("missing chatbotId. aborting");
		wp_send_json_error(__('Missing chatbotId. aborting','twx-chat'));
		wp_die();
	}

	$atts = TWXChatPlus::getUserFieldsAttributes($chatbotId);
	if(!isset($atts['update_webhook_url']) || empty($atts['update_webhook_url'])) {
		wp_send_json_success(__('No webhook to invoke','twx-chat'));
		wp_die();
	}
	$webhookUrl = esc_url_raw($atts['update_webhook_url']);

	$threadId = sanitize_text_field($_POST['threadId'] ?? '');
	if(empty($threadId)){
		TWXChatUtil::log("missing threadId. aborting");
		wp_send_json_error(__('Missing threadId. aborting','twx-chat'));
		wp_die();
	}

	$userFieldVals = sanitize_text_field($_POST['userFieldVals'] ?? '');
	if(empty($userFieldVals)){
		TWXChatUtil::log("missing userFieldVals. aborting");
		wp_send_json_error(__('Missing userFieldVals. aborting','twx-chat'));
		wp_die();
	}
	
	try{
		$userFieldVals = stripslashes($userFieldVals);
		$userFieldVals = json_decode($userFieldVals,true);
	}catch(Exception $ex){
		TWXChatUtil::log("ajax_handler.php:: error parsing userFieldVals: ".$ex->getMessage(),'a',true);
		wp_send_json_error(__('invalid json. aborting','twx-chat'));
		wp_die();
	}

	// This returns immediately and doesn't wait for a response
	$response = TWXChatPlus::invokeUpdateUserFieldsWebhook($webhookUrl, $chatbotId, $threadId, $userFieldVals);

    if (is_wp_error($response)) {
		TWXChatUtil::log("twx_chat_invokeUpdateUserFieldsWebhook() remote post error: ".$response->get_error_message(),'a',true);
        wp_send_json_error($response->get_error_message());
		die();
    }

	wp_send_json_success('success');

    // Always die in AJAX handlers
    wp_die();
	
}
add_action('wp_ajax_twx_chat_invokeUpdateUserFieldsWebhook', 'twxchat_invokeUpdateUserFieldsWebhook');
add_action('wp_ajax_nopriv_twx_chat_invokeUpdateUserFieldsWebhook', 'twxchat_invokeUpdateUserFieldsWebhook');

function twxchat_createTTSFile(){
	
	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	TWXChatUtil::log('twx_chat_createTTSFile start', 'w');

	$apiKey = get_option('twx_chat_options')['openai_apikey'] ?? '';
	
	$text = sanitize_text_field($_POST['text'] ?? '');
	$messageNo = sanitize_text_field($_POST['messageNo'] ?? '');
	$threadId = sanitize_text_field($_POST['threadId'] ?? '');
	$ttsVoice = sanitize_text_field($_POST['ttsVoice'] ?? '');
	
	$dataOut = [];
	
	$ttsProvider = 'openai'; 
	$providerVoiceCode = $ttsVoice;
	$split = explode(':',$ttsVoice);
	if(count($split)==2){
		$ttsProvider = $split[0];
		$providerVoiceCode = $split[1];
	}
	$createTTS = twxchat_createTTSFile_internal($apiKey, $threadId, $messageNo, $text, $ttsProvider, $providerVoiceCode);
	if(!isset($createTTS['error']) && isset($createTTS['outputFileName'])){
		$dataOut['voiceOutputFileName'] = $createTTS['outputFileName'];
		wp_send_json_success($dataOut);
	}else{
		wp_send_json_error($createTTS['error']);
	}
	
    // Always die in AJAX handlers
    wp_die();

}
add_action('wp_ajax_twx_chat_createTTSFile', 'twxchat_createTTSFile');
add_action('wp_ajax_nopriv_twx_chat_createTTSFile', 'twxchat_createTTSFile');

function twxchat_submitFeedback() {
	
	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	//TODO Move to Pro plan at some stage

	TWXChatUtil::log('twx_chat_submitFeedback start');

	$apiKey = get_option('twx_chat_options')['openai_apikey'] ?? '';
	
	$message = sanitize_text_field($_POST['message'] ?? '');
	$assistantId = sanitize_text_field($_POST['assistantId'] ?? '');
	$feedback = sanitize_text_field($_POST['feedback'] ?? '');
	$knowledgeBaseSource = sanitize_text_field($_POST['knowledgeBaseSource'] ?? '');
	
	if (empty($apiKey) || empty($message) || empty($feedback)) {
		$response = [
			'status' => 'error',
			'message' => __('Missing apiKey, message or feedback','twx-chat'),
			'data' => [],
		];
		echo wp_json_encode($response);
		return;
	}

	// Add feedback to local Knowledge Base
	$source = '_feedback';
	if(!empty($knowledgeBaseSource)) $source = $knowledgeBaseSource . '_feedback';
	$title = $source;
	$text = __("Question",'twx-chat').": $message. ".__("Answer",'twx-chat').": $feedback";

	$dataOut = [];
	try {
		TWXChatKnowledgeBaseUtil::storeEntry($apiKey, $source, $title, $text, false);
		$dataOut['status'] = 'success';
		$dataOut['message'] = __('Feedback submitted successfully','twx-chat');
	} catch (Exception $ex) {
		$dataOut['status'] = 'error';
		$dataOut['message'] = $ex->getMessage();
	}

	/* Attaching files to Assistants and having their contents make any difference to the query results is not really working. Stick with local Knowledge Base for the moment
	// Add feedback to Assistant files
	$asstObject = new TWXChatOpenAIAssistant($apiKey, $assistantId);
	$feedbackFile = __DIR__.'/../output/twx_feedback.json';
	$submitResponse = $asstObject->submitFeedback($message, $feedback, $feedbackFile); // await query response

	$dataOut = [];
	if(isset($submitResponse['error']['message'])) {
		wp_send_json_error('Error submitting feedback: '.$submitResponse['error']['message']);
		wp_die();
		exit;
	}else{
		$dataOut['status'] = 'success';
		$dataOut['message'] = 'Feedback submitted successfully';
	}
	
	if(isset($submitResponse['file_id'])) $ret['data']['fileId'] = $submitResponse['file_id'];
	if(isset($submitResponse['file_name'])) $ret['data']['fileName'] = $submitResponse['file_name'];
	*/
	
	// Return the JSON response
	wp_send_json_success($dataOut);

    // Always die in AJAX handlers
    wp_die();
	
}
add_action('wp_ajax_twx_chat_submitFeedback', 'twxchat_submitFeedback');
add_action('wp_ajax_nopriv_twx_chat_submitFeedback', 'twxchat_submitFeedback');

function twxchat_get_avatar_config() {

	// AJAX Handler to get Avatar configs
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	$options = get_option('twx_chat_options'); 

	$awsCognitoIdentityPoolId = isset($options['aws_cognito_identity_pool_id']) ? $options['aws_cognito_identity_pool_id'] : '';

	$dataOut = [
		'awsCognitoIdentityPoolId' => $awsCognitoIdentityPoolId
	];

	// Finish up
	wp_send_json_success($dataOut);

    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_get_avatar_config', 'twxchat_get_avatar_config');
add_action('wp_ajax_nopriv_twx_chat_get_avatar_config', 'twxchat_get_avatar_config');

function twxchat_fetch_remote_thumbnail_OLD() {

    // AJAX Handler - get first image within a URL (usually a source on the website)
    check_ajax_referer('twx_chat_nonce', 'nonce');

    $url = esc_url_raw($_POST['target_url'] ?? '');

    $parts = wp_parse_url($url);
    if (!isset($parts['scheme']) || !isset($parts['host'])) {
        TWXChatUtil::log("twx_chat_fetch_remote_thumbnail:: error with baseUrl");
        wp_send_json_success(['thumbnail' => '']);
        wp_die();
    }

    $baseUrl = $parts['scheme'] . '://' . $parts['host'];

    // Use WordPress HTTP API instead of cURL
    $response = TWXChatUtil::getHttpResponse($url, [
        'timeout' => 5,
        'redirection' => 5,
    ]);

    if (is_wp_error($response)) {
        TWXChatUtil::log("twx_chat_fetch_remote_thumbnail:: HTTP error: " . $response->get_error_message());
        wp_send_json_success(['thumbnail' => '']);
        wp_die();
    }

    $html = wp_remote_retrieve_body($response);
    $thumbnail = '';

    if ($html) {
        // 1. Try og:image
        if (preg_match('/<meta property="og:image" content="([^"]+)"/i', $html, $matches)) {
            $thumbnail = $matches[1];
        }

        // 2. If og:image not found, try to find first <img>
        if (!$thumbnail) {
            if (preg_match('/<img[^>]+src=["\']([^"\']+)["\']/i', $html, $imgMatches)) {
                $thumbnail = $imgMatches[1];

                // Handle relative URLs
                if (wp_parse_url($thumbnail, PHP_URL_SCHEME) === null && $baseUrl) {
                    // Build full URL
                    $thumbnail = rtrim($baseUrl, '/') . '/' . ltrim($thumbnail, '/');
                }
            }
        }
    }

    wp_send_json_success([
        'thumbnail' => $thumbnail
    ]);
    wp_die();
}

function twxchat_fetch_thumbnail() {
	check_ajax_referer('twx_chat_nonce', 'nonce');

	$url = esc_url_raw($_POST['target_url'] ?? '');

	$parts = wp_parse_url($url);
	if (!isset($parts['scheme']) || !isset($parts['host'])) {
		TWXChatUtil::log("twx_chat_fetch_remote_thumbnail:: error with baseUrl");
		wp_send_json_success(['thumbnail' => '']);
		wp_die();
	}

	$baseUrl = $parts['scheme'] . '://' . $parts['host'];

	$response = TWXChatUtil::getHttpResponse($url, [
		'timeout' => 5,
		'redirection' => 5,
	]);

	if (is_wp_error($response)) {
		TWXChatUtil::log("twx_chat_fetch_remote_thumbnail:: HTTP error: " . $response->get_error_message());
		wp_send_json_success(['thumbnail' => '']);
		wp_die();
	}

	$html = wp_remote_retrieve_body($response);
	$thumbnail = '';

	if ($html) {
		libxml_use_internal_errors(true);
		$doc = new DOMDocument();
		$doc->loadHTML($html);
		libxml_clear_errors();

		$xpath = new DOMXPath($doc);

		// Try og:image first
		$ogImage = $xpath->query('//meta[@property="og:image"]')->item(0);
		if ($ogImage && $ogImage->getAttribute('content')) {
			$thumbnail = $ogImage->getAttribute('content');
		}

		// Otherwise, try first image in <main>, .entry-content, or #content
		if (!$thumbnail) {
			$imgNodes = $xpath->query('//main//img | //*[@class="entry-content"]//img | //*[@id="content"]//img');

			foreach ($imgNodes as $img) {
				$src = $img->getAttribute('src');
				if ($src) {
					$thumbnail = $src;
					break;
				}
			}

			// Fallback to first <img> in the page
			/*
			if (!$thumbnail) {
				$img = $doc->getElementsByTagName('img')->item(0);
				if ($img) {
					$thumbnail = $img->getAttribute('src');
				}
			}
			*/
		}

		// Make relative URL absolute
		if ($thumbnail && wp_parse_url($thumbnail, PHP_URL_SCHEME) === null) {
			$thumbnail = rtrim($baseUrl, '/') . '/' . ltrim($thumbnail, '/');
		}
	}

	wp_send_json_success([
		'thumbnail' => $thumbnail
	]);
	wp_die();
}
add_action('wp_ajax_twx_chat_fetchThumbnail', 'twxchat_fetch_thumbnail');
add_action('wp_ajax_nopriv_twx_chat_fetchThumbnail', 'twxchat_fetch_thumbnail');

function twxchat_uploadConsoleLog(){

	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	$logEntries = array_map('sanitize_text_field', ($_POST['logEntries'] ?? []));

	if(is_array($logEntries) && !empty($logEntries)){
		foreach($logEntries as $entry){
			TWXChatUtil::consoleLog($entry);
		}
	}

	$ret = ['foo' => 'bar'];

	wp_send_json_success(json_encode($ret));
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_uploadConsoleLog', 'twxchat_uploadConsoleLog');
add_action('wp_ajax_nopriv_twx_chat_uploadConsoleLog', 'twxchat_uploadConsoleLog');


//// END AJAX HANDLERS ////

function twxchat_query_with_assistant($apiKey, $threadId, $assistantId, $message, $extraInstructions, $minSimilarity, $knowledgeBaseSource = ''){

	TWXChatUtil::log("twx_chat_query_with_assistant:: start");

	// Get local feedback context for RAG
	$source = 'feedback';
	if(!empty($knowledgeBaseSource)) $source = $knowledgeBaseSource . '_feedback';
	$entries = TWXChatKnowledgeBaseUtil::getEntriesBySource($source, true);

	//TWXChatKnowledgeBaseUtil::addLevenshteinSimilarityToEntries($message, $entries);

	// Query Knowledge Base Store
	$options = get_option('twx_chat_options');	
	$embeddingModel = $options['openai_embedding_model'] ?? TWXCHAT_DEFAULT_OPENAI_EMBEDDING_MODEL;
	$tiktokenEncoder = TWXChatUtil::getLocalTiktokenEncoder($embeddingModel);

	$similarKnowledgeBaseEntries = TWXChatKnowledgeBaseUtil::searchSimilarKnowledgeBaseEntries($tiktokenEncoder, $apiKey, TWXChatUtil::appendQuestionMark($message), $entries, [], null, -1, $minSimilarity);
	TWXChatUtil::log("similarKnowledgeBaseEntries = ".count($similarKnowledgeBaseEntries));

	$localContext = '';
	foreach($similarKnowledgeBaseEntries as $id => $entry){
		$localContext .= $entry['orig_text'] . "\n";
	}

	// Query OpenAI Assistant
	$asstObject = new TWXChatOpenAIAssistant($apiKey, $assistantId);
	TWXChatUtil::log("query assistant $assistantId");

	$prompt = $message;
	if(!empty($localContext)){
		$prompt = "Respond to the following message using assistant files and the additional context below: $message";
		$prompt .= "\n\nAdditional context below:\n$localContext";
	}
	if(!empty($extraInstructions)){
		$prompt .= "\n\nAdditional instructions are: $extraInstructions";
	}
	TWXChatUtil::log("prompt = $prompt");

	$queryResponse = $asstObject->queryAssistant($prompt, $threadId); // await query response

	return $queryResponse;
}

function twxchat_query_no_assistant($apiKey, $threadId, $knowledgeBaseSource, $minSimilarity, $maxOutputTokens, $message, $isCommonQuestion, $extraInstructions, $chatbotGreeting, $noKBMatchText, $llmModel = '', $chatbotId = null){

	// Query Completions (No Assistant, Not Streaming)
	TWXChatUtil::log("twx_chat_query_no_assistant:: start");

	//$completionsObject = new TWXChatOpenAICompletions($apiKey, $llmModel);
	$openai = new TWXChatOpenAIResponses($apiKey, $llmModel);
	$model = $openai->getModel(); // in case $llmModel is ''
	$showSources = '';

	// User Fields (Plus Plan)
	$userFieldsWithVals = [];
	$numUserFieldsToCollect = 0;
	$userFieldsUpdateWebhookUrl = null;
	if(TWXChatUtil::customerPlanAccess('plus')){
		list($userFieldsWithVals, $numUserFieldsToCollect, $userFieldsUpdateWebhookUrl) = TWXChatPlus::getUserFieldsWithValues($chatbotId, $threadId);
	}

	list($newMessages, $similarKnowledgeBaseEntries, $estimated, $sources) = TWXChatUtil::getParamsForLLM($apiKey, $threadId, $extraInstructions, $chatbotGreeting, $message, $isCommonQuestion, $knowledgeBaseSource, $model, $minSimilarity, $showSources, $chatbotId, $userFieldsWithVals, $numUserFieldsToCollect);

	// run OpenAI query and await response
	$queryResponse = [];
	if(!empty($similarKnowledgeBaseEntries) || !empty($userFieldsWithVals)){
		TWXChatUtil::log("twx_chat_query_no_assistant:: begin LLM query");
		$queryResponse = $openai->query($newMessages, $maxOutputTokens);
	}else{
		if(empty($noKBMatchText)) $noKBMatchText = __("I'm sorry. Please be a little more specific, or rephrase your question.",'twx-chat');
		$queryResponse = ['answer' => $noKBMatchText];
	}
	TWXChatUtil::log("twx_chat_query_no_assistant:: queryResponse: ".var_export($queryResponse,true));
	
	if(!empty($estimated)) $queryResponse['estimated'] = $estimated;
	$queryResponse['similarKnowledgeBaseEntries'] = $similarKnowledgeBaseEntries;
	if(isset($queryResponse['relevant_source_ids']) && !empty($queryResponse['relevant_source_ids'])){
		$queryResponse['relevant_sources'] = TWXChatKnowledgeBaseUtil::getSourcesBySourceIds($queryResponse['relevant_source_ids']);
		
	}

	if(!empty($userFieldsUpdateWebhookUrl)) $queryResponse['userFieldsUpdateWebhookUrl'] = $userFieldsUpdateWebhookUrl;

	return $queryResponse;
}

function twxchat_createTTSFile_internal($apiKey, $threadId, $seq, $text, $ttsProvider, $providerVoiceCode){
	
	// generate TTS audio file for playback on client
	$ret = [];
	
	$outputFileName = "$threadId-$seq.mp3";
	$outputFilePath = TWXChatUtil::getUploadsDir('mp3');
	if (!file_exists($outputFilePath)) TWXChatUtil::makeDir($outputFilePath);
	$outputFilePath .= $outputFileName;

	twxchat_deleteOldOutputFiles();
	
	if(file_exists($outputFilePath)){
		$ret['outputFileName'] = $outputFileName;
		TWXChatUtil::log("twx_chat_createTTSFile_internal $outputFileName already exists");
	}else{
		try{
			switch($ttsProvider){
				case 'openai':
					$synth = TWXChatUtil::synthesizeSpeech($apiKey, $text, $outputFilePath, $providerVoiceCode);
					if($synth === true){
						$ret['outputFileName'] = $outputFileName;
					}else{
						$ret['error'] = $synth;
					}
					break;
				//TODO more Providers
			}
		
		}catch(Exception $ex){
			$ret['error'] = $ex->getMessage();
		}
	}
	
	return $ret;
}

function twxchat_deleteOldOutputFiles() {
	
	$directory = TWXChatUtil::getUploadsDir('mp3');
	
    // Check if the directory exists
    if (!is_dir($directory)) {
		return;
    }

    // Get the current time (Unix epoch (January 1, 1970)
    $currentTime = time();
    // Time threshold: 30 minutes ago
    $thresholdTime = $currentTime - (30 * 60);

    // Get all files in the directory
    $files = glob($directory . '/*');

	$i=0;
    foreach ($files as $file) {
        // Check if it is a file
        if (is_file($file)) {
            // Get the last modified time of the file
            $fileModificationTime = filemtime($file);

            // If the file was modified more than 30 minutes ago, delete it
            if ($fileModificationTime < $thresholdTime) {
				$i++;
                wp_delete_file($file);
            }
        }
    }
	
	if($i>0) TWXChatUtil::log("deleted $i old output files");
}

function twxchat_parseAnswer($text, $knowledgeBaseEntries = null) {
    
	// Further Parse the Bot Response
	
	$parsedText = $text;
	
	$parsedText = str_replace("\n","<br/>",$parsedText);
	
	$converted = twxchat_convertMarkdownLinksToHtml($parsedText, $knowledgeBaseEntries);
	
	$ret = [];
	$ret['answer'] = $converted['html'];
	if(isset($converted['corrections'])) $ret['corrections'] = $converted['corrections'];
	
    return $ret;
}

function twxchat_convertMarkdownLinksToHtml($markdown, $knowledgeBaseEntries = null) {
	
	//TODO Note image markdown is very similar eg:  ![alt text](image-url). Ensure we are not parsing those
	$ret = [];
	$corrections = [];

    // Regex pattern to match Markdown hyperlinks
    $pattern = '/\[(.*?)\]\((.*?)\)/';
    
    // Callback function to replace Markdown hyperlinks with HTML hyperlinks
    $callback = function ($matches) use ($knowledgeBaseEntries, &$corrections) {

		$linkText = $matches[1];
		$url = $matches[2];
		
		if($url == '#' || empty($url)){
			return $linkText;
		}
		else if(!empty($knowledgeBaseEntries)){
			$source = '';
			$newUrl = twxchat_getUrlFromKnowledgeBaseEntries($linkText, $knowledgeBaseEntries, $source);
			if(!empty($newUrl)){
				if($url != $newUrl){
					$corrections['hyperlinks'][] = ['linkText' => $linkText, 'botUrl' => $url, 'newUrl' => $newUrl, 'source' => $source];
					$url = $newUrl;
				}
			}else{
				return $linkText; // return text only (remove link)
			}
		}
		return '<a onclick="onBotResponseHyperlinkClick(this)" href="' . $url . '">' . $linkText . '</a>';
    };
    
    // Perform the replacement
    $html = preg_replace_callback($pattern, $callback, $markdown);
	
	$ret['html'] = $html;
	if(!empty($corrections)) $ret['corrections'] = $corrections;
    
    return $ret;
}

function twxchat_getUrlFromKnowledgeBaseEntries($text, $entries, &$sourceRet){
	
	foreach($entries as $kbId => $entry){
		$markdown = $entry['orig_text'];
		$source = $entry['source'];
		$pos = stripos($markdown,$text);
		
		if($pos !== false){
			
			$newHref = TWXChatUtil::findMarkdownLinksWithText($markdown, $text);
			if(!empty($newHref)) {
				$sourceRet = $source;
				return $newHref;
			}
		}
	}
	
	return null;
}

function twxchat_removeHyperlinkURLsFromMarkdown($markdownText) {
	
    // Use regex to remove Markdown links but keep the text
    $result = preg_replace('/\[(.*?)\]\(.*?\)/', '$1', $markdownText);

    return $result;
}

function twxchat_removeNestedColumn($array, $keyToRemove) {
	// remove a column from within an assoc array, wherever it is (eg: really long values like embeddings)
    foreach ($array as $key => &$value) {
        // If the current value is an array, call the function recursively
        if (is_array($value)) {
            $value = twxchat_removeNestedColumn($value, $keyToRemove);
        }

        // Check and remove the key at the current level
        if ($key === $keyToRemove) {
            unset($array[$key]);
        }
    }
    unset($value); // Unset the reference to avoid unintended behavior

    return $array;
}
