<?php
// Handle streaming requests and responses for Chatbot queries

//if ( ! defined( 'ABSPATH' ) ) exit; // Cannot place this here as it breaks javascript client code. Nonce check is performed below to prevent unauth access

header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');

require_once './includes/twx_util.php';
require_once './includes/openai_include.php';
require_once './includes/knowledge_base_include.php';
require_once './includes/chatlog_include.php';
require_once './vendor/autoload.php';

try {
	TWXChatUtil::loadWordpress();

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

	// Nonce check. Check if called from valid Plugin Browser session
	$nonce = isset($_GET['nonce']) ? sanitize_text_field(wp_unslash($_GET['nonce'])) : '';
    if (!wp_verify_nonce($nonce, 'twx_chat_nonce')) {
		$ret = ['error' => 'Invalid nonce'];
		TWXChatUtil::log("stream_handler:: invalid nonce");
		twxchat_end_stream($ret);
		exit;
    }

	$options = get_option('twx_chat_options');

	// After Nonce check, get request params
	$threadId = isset($_GET['threadId']) ? sanitize_text_field(wp_unslash($_GET['threadId'])) : ''; // Nonce check prior to this line (line 22)
    if(empty($threadId)) twxchat_end_stream(['error'=>'missing/empty param: threadId']);
    $message = isset($_GET['message']) ? sanitize_text_field(wp_unslash(urldecode($_GET['message']))) : ''; // Nonce check prior to this line (line 22)
    if(empty($message)) twxchat_end_stream(['error'=>'missing/empty param: message']);
	$isCommonQuestion = (isset($_GET['isCommonQuestion']) && $_GET['isCommonQuestion'] == 'true'); // Nonce check prior to this line (line 22)
	$knowledgeBaseSource = isset($_GET['knowledgeBaseSource']) ? sanitize_text_field(wp_unslash($_GET['knowledgeBaseSource'])) : ''; // Nonce check prior to this line (line 22)
    $showSources = isset($_GET['showSources']) ? sanitize_text_field(wp_unslash($_GET['showSources'])) : ''; // Nonce check prior to this line (line 22)
	$minSimilarity = isset($_GET['minSimilarity']) ? sanitize_text_field(wp_unslash($_GET['minSimilarity'])) : ''; // Nonce check prior to this line (line 22)
	if(!empty($minSimilarity)) {
		$minSimilarity = intval($minSimilarity)/100;
	}else{
		$minSimilarity = intval($options['min_similarity'])/100;
	}
	$isVoiceInput = (isset($_GET['isVoiceInput']) && $_GET['isVoiceInput'] == 'true'); // Nonce check prior to this line (line 22)
	$llmModel = isset($_GET['llmModel']) ? sanitize_text_field(wp_unslash(urldecode($_GET['llmModel']))) : null; // Nonce check prior to this line (line 22)
	$chatbotId = isset($_GET['chatbotId']) ? sanitize_text_field(wp_unslash($_GET['chatbotId'])) : null; // Nonce check prior to this line (line 22)

	$chatbotAtts = TWXChatUtil::getChatbotAttributesById(['id'=>$chatbotId],true);
	$extraInstructions = $chatbotAtts['extra_instructions'] ?? '';
	$startingComments = $chatbotAtts['starting_comments'] ?? '';
    
    $apiKey = $options['openai_apikey'];
    $completionsObject = new TWXChatOpenAICompletions($apiKey, $llmModel);
    $model = $completionsObject->getModel();    
	$similarKnowledgeBaseEntries = [];
	$estimated = [];
	$sources = [];

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

	// Get params needed for LLM query
	list($newMessages, $similarKnowledgeBaseEntries, $estimated, $sources) = TWXChatUtil::getParamsForLLM($apiKey, $threadId, $extraInstructions, $startingComments, $message, $isCommonQuestion, $knowledgeBaseSource, $model, $minSimilarity, $showSources, $chatbotId, $userFieldsWithVals, $numUserFieldsToCollect);
	TWXChatUtil::log("pre-query messages: ".var_export($newMessages,true));

	// run OpenAI query and let response stream to twxchat_handle_chunk function
    $queryResponse = [];
	if(!empty($similarKnowledgeBaseEntries) || !empty($userFieldsWithVals)){
		// Begin streaming here
		$queryResponse = $completionsObject->queryStream($newMessages, 'twxchat_handle_chunk');
	}else{
		$answer = "I'm sorry. Please be a little more specific, or rephrase your question.";
        twxchat_stream_handler_insertChatLog($threadId, $message, $isVoiceInput, false, $answer, $knowledgeBaseSource, [], $extraInstructions);
		twxchat_end_stream(['answer' => $answer]);
	}

    if(isset($queryResponse['error']['message'])){
        TWXChatUtil::log("stream_handler:: error: ".$response['error']['message']);
        $ret = ['error' => esc_html($response['error']['message'])];
        twxchat_stream_handler_insertChatLog($threadId, $message, $isVoiceInput, true, $response['error']['message'], $knowledgeBaseSource, $sources, $extraInstructions);
        twxchat_end_stream($ret);
    }

	if(isset($queryResponse['fullStreamOutput'])){
		$parsedOutput = twxchat_extractConversationTextFromFullStreamOutput($queryResponse['fullStreamOutput']);
        TWXChatUtil::log("stream_handler:: fullStreamOutput: $parsedOutput");
	}

	// Chat Log insert for new user input, bot response will be updated later after stream
    twxchat_stream_handler_insertChatLog($threadId, $message, $isVoiceInput, false, '', $knowledgeBaseSource, $sources, $extraInstructions);

    TWXChatUtil::log("stream_handler:: end");
    die();  // End the script

    
} catch (Exception $ex) {
    $ret = ['error' => esc_html($ex->getMessage())];
    TWXChatUtil::log("stream_handler:: exception: ".$ex->getMessage());
    twxchat_end_stream($ret);
}

function twxchat_handle_chunk($data_raw){

	$data_escaped = twxchat_escape_chunk($data_raw);
	echo str_replace('\\n',"<br/>",$data_escaped); // escaped above
	try { ob_flush(); }catch(Exception $ex){}
	flush();
}

function twxchat_escape_chunk($chunk) {
	// only ecape the content that will be rendered otherwise will break the stream
	return preg_replace_callback(
		'/("delta"\s*:\s*\{\s*"content"\s*:\s*")([^"]*)(")/',
		function ($matches) {
			$escaped = wp_kses($matches[2], []);
			return $matches[1] . $escaped . $matches[3];
		},
		$chunk
	);
}

function twxchat_end_stream($ret) {

	if(is_array($ret)) $ret['end_of_stream'] = true;
    header('Connection: close');
    ob_implicit_flush(true);
    try { ob_end_flush(); }catch(Exception $error){}
	echo "data: " . wp_json_encode($ret) . "\n\n"; // $ret has been constructed within code (no user or LLM input) and is already escaped
    flush();  // Send the message immediately
    exit;
}

function twxchat_stream_handler_insertChatLog($threadId, $userInput, $isVoiceInput, $isError, $botResponse = '', $knowledgeBaseSource = '', $sources = '', $extraInstructions = '' ) {

    $logData = [
		'userInput' => $userInput,
		'isVoiceInput' => ($isVoiceInput ? 'true' : 'false'),
	];
	//if(!empty($botCorrections)) $logData['botCorrections'] = $botCorrections;
	if(!empty($botResponse)) $logData['botResponse'] = $botResponse;
	if(!empty($knowledgeBaseSource)) $logData['knowledgeBaseSource'] = $knowledgeBaseSource;
	if(!empty($sources)) $logData['sources'] = $sources;
	if(!empty($extraInstructions)) $logData['extraInstructions'] = $extraInstructions;
	//if(!empty($assistantId)) $logData['assistantId'] = $assistantId;
	TWXChatLog::insertChatLog($threadId, $logData);

}

function twxchat_getPrevPrompts($messages){

	if(empty($messages)) return [];
	if(count($messages)==1) return [];
	
	$ret = [];

	for($i=count($messages)-2; $i>=0; $i--){
		$msg = $messages[$i];
		if($msg['role'] !== 'system'){
			$ret[] = $msg['content'];
		}
	}

	return $ret;
}

function twxchat_extractConversationTextFromFullStreamOutput($fullStreamOutput) {
    $lines = explode("\n", $fullStreamOutput);
    
	$fullText = '';

    foreach ($lines as $line) {
        $line = trim($line);
        if (strpos($line, 'data: ') !== 0) continue; // ignore non-data lines

        $payload = trim(substr($line, 6)); // remove "data: "

        if ($payload === '[DONE]') break;

        $json = json_decode($payload, true);
        if (!is_array($json)) continue;

        $content = $json['choices'][0]['delta']['content'] ?? '';
        $fullText .= $content;
    }

    return $fullText;
}

?>