<?php
// Expects to be in /plugin_root/includes folder

class TWXChatOpenAIAssistant
{

    // Class properties (module variables)
    private $_apiKey;
    private $_assistantId;
    private $_assistant;
    private $_apiKeyFiles;
	
    // Constructor method
    public function __construct($apiKey, $assistantId)
    {
        $this->_apiKey = $apiKey;
        $this->_assistantId = $assistantId;
		$this->_assistant = $this->getAssistantPrivate($assistantId);
		$this->_apiKeyFiles = [];
    }

    // Destructor method
    public function __destruct()
    {
        // Log or perform actions needed at object destruction
    }

	public function queryAssistant($query, $threadId = null) {
		
		if(empty($threadId)) {
			$thread = $this->createThread();
			$threadId = $thread['id'];
		}

		$ret = [
			'query' => $query,
			'threadId' => $threadId
		];
		
		//TWXChatUtil::chatLog("$threadId query $query");

		$message = $this->sendMessage($query, $threadId);
		if(isset($message['error'])){
			$ret['answer'] = 'Error: '.$message['error']['message'];
			$ret['error'] = true;
			return $ret;
		}
		
		$response = $this->getMessageResponse($message['id'], $threadId);
		if(isset($response['error'])){
			$ret['answer'] = 'Error: '.$response['error']['message'];
			$ret['error'] = true;
			return $ret;
		}

		$ret['answer'] = TWXChatUtil::parseAnswer($response['answer']);
		$ret['usage'] = $this->getUseageCosts($response['usage']);
		$ret['model'] = $this->_assistant['model'];
		
		return $ret;
	}

	public function queryAssistantStreaming($query, $threadId, callable $callback) {

		if (empty($threadId)) {
			$thread = $this->createThread();
			$threadId = $thread['id'];
		}

		// Step 1: Push query message to thread
		$message = $this->sendMessage($query, $threadId);
		if (isset($message['error'])) {
			$ret['answer'] = 'Error: ' . $message['error']['message'];
			$ret['error']  = true;
			return $ret;
		}

		// Step 2: Create the run
		$runUrl = "https://api.openai.com/v1/threads/$threadId/runs";
		$runResponse = TWXChatUtil::getHttpOpenAI_WP(
			$this->_apiKey,
			$runUrl,
			[
				'assistant_id' => $this->_assistantId
			],
			'POST',
			[
				'OpenAI-Beta' => 'assistants=v2'
			]
		);

		if (isset($runResponse['error'])) {
			return $runResponse;
		}

		if (empty($runResponse['id'])) {
			return ['error' => ['message' => 'No run_id returned']];
		}

		$runId = $runResponse['id'];

		$fullStreamOutput = '';

		// Step 3: Attach to run stream
		add_action('http_api_curl', function($handle) use (&$fullStreamOutput, $callback) {
			curl_setopt($handle, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$fullStreamOutput, $callback) {
				if (!empty($data)) {
					$lines = explode("\n", $data);
					foreach ($lines as $line) {
						if (strpos($line, "data: ") === 0) {
							$payload = substr($line, 6);
							if (trim($payload) === "[DONE]") {
								$callback("[DONE]");
								continue;
							}
							$event = json_decode($payload, true);
							if ($event && isset($event['event'])) {
								$fullStreamOutput .= $payload . "\n";
								$callback($event);
							}
						}
					}
				}
				return strlen($data);
			});
			curl_setopt($handle, CURLOPT_TIMEOUT, 0);
		});

		$http = new WP_Http();
		$response = $http->request("https://api.openai.com/v1/threads/$threadId/runs/$runId/stream", [
			'method'  => 'GET', // ✅ stream endpoint is GET
			'headers' => [
				'Authorization' => 'Bearer ' . $this->_apiKey,
				'Content-Type'  => 'application/json',
				'OpenAI-Beta'   => 'assistants=v2',
			],
			'timeout' => 0,
		]);

		// remove curl hook
		remove_all_actions('http_api_curl');

		$ret = [];
		if (is_wp_error($response)) {
			$ret['error']['message'] = $response->get_error_message();
		} else {
			$ret['answer']          = 'streamed_to_callback';
			$ret['fullStreamOutput'] = $fullStreamOutput;
		}

		TWXChatUtil::log("queryAssistantStreaming:: ret: " . var_export($ret, true));

		return $ret;
	}

	
	public function uploadFileFromPath($newFileName, $sourceFilePath, $contentType, $andAttach = false){

		// create a new File from contents at $sourceFilePath and optionally attaches to Assistant
		// returns Assistant File object
		
		if(class_exists('TWXChatSpider')) TWXChatSpider::log("uploadFileFromPath $newFileName $sourceFilePath $contentType");
		
		$contents = @file_get_contents($sourceFilePath); // Suppress errors for failed fetches
		
		if(empty($contents)){
			$ret['error']['message'] = 'could not read contents from file: '.$sourceFilePath;
			return $ret;
		}

		$response = $this->uploadFile($contents, $newFileName, $contentType, 'assistants');
		if(isset($response['error'])){
			$ret['error']['message'] = $response['error']['message'];
			return $ret;
		}

		if(class_exists('TWXChatSpider')) TWXChatSpider::log("uploadFileFromPath upload successful");

		$file = $response;
		
		if($andAttach === true){
			$assistantFile = $this->attachFile($file['id']);
			if(isset($assistantFile['error'])){
				$ret['error']['message'] = $assistantFile['error']['message'];
				return $ret;
			}
		}

		return $file;  // Assistant file object
	}
	
	public function loadFiles($purpose = null){

		$this->_apiKeyFiles = [];
		
		$files = $this->getFiles($purpose);
		
		foreach($files as $file){
			$this->_apiKeyFiles[$file['id']] = $file;
		}
		
	}
	
	public function getAssistant(){
		
		return $this->_assistant;
	}
	
	public function getAssistants() {
		return TWXChatUtil::getAssistants($this->_apiKey);
	}
	
	public function getVectorStoreFiles($vectorStoreId){
		
		$url = "https://api.openai.com/v1/vector_stores/$vectorStoreId/files";
		
		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, null, 'GET');
		if(isset($response['error'])){
			$ret['error']['message'] = $response['error']['message'];
			return $ret;
		}

		return $response['data'];  // Vector Store Files
	}
	
	public function getAssistantFiles() {
		$ret = [];

		// Make sure the assistant is loaded
		if (empty($this->_assistant)) {
			$this->_assistant = $this->getAssistantPrivate($this->_assistantId);
			if (empty($this->_assistant)) {
				return [
					'error' => [
						'message' => 'Failed to load assistant details.'
					]
				];
			}
		}

		// Get vector store IDs from assistant tools
		$vectorStoreIds = [];
		if (isset($this->_assistant['tools'])) {
			foreach ($this->_assistant['tools'] as $tool) {
				if ($tool['type'] === 'file_search' && isset($tool['vector_store_ids'])) {
					$vectorStoreIds = array_merge($vectorStoreIds, $tool['vector_store_ids']);
				}
			}
		}

		$vectorStoreIds = array_unique($vectorStoreIds);

		// If no vector stores are found, return empty array
		if (empty($vectorStoreIds)) {
			return [];
		}

		// Load all files attached to the assistant via vector stores
		$attachedFileIds = [];

		foreach ($vectorStoreIds as $vsId) {
			$url = "https://api.openai.com/v1/vector_stores/{$vsId}/files";

			$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, null, 'GET');

			if (isset($response['error'])) {
				// Optionally log error or include it in results
				continue;
			}

			foreach ($response['data'] as $file) {
				$attachedFileIds[] = $file['id'];
			}
		}

		$attachedFileIds = array_unique($attachedFileIds);

		// Load all available files for the API key, if not already loaded
		if (empty($this->_apiKeyFiles)) {
			$this->loadFiles();
		}

		// Match and return detailed file info
		foreach ($attachedFileIds as $fileId) {
			if (isset($this->_apiKeyFiles[$fileId])) {
				$ret[] = $this->_apiKeyFiles[$fileId];
			}
		}

		return $ret;
	}

	public function getVectorStoreIds() {
		if(!isset($this->_assistant['tool_resources']['file_search']['vector_store_ids'])) return [];
		return $this->_assistant['tool_resources']['file_search']['vector_store_ids'];
	}

	public function getAssistantFiles_OLD(){

		$assistantId = $this->_assistantId;
		
		$url = "https://api.openai.com/v1/assistants/$assistantId/files";
		
		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, null, 'GET');

		if(isset($response['error'])){
			$ret['error']['message'] = $response['error']['message'];
			return $ret;
		}

		$assistFileIds = array_column($response['data'],'id'); 
		
		// For some reason, sometimes this result can be empty even if there are files attached to the Assistant
		// Lets also search  vector stores attached to the Assistant
		
		$vectorStoreFileIds = [];
		if(isset($this->_assistant['tool_resources']['file_search']['vector_store_ids'])){
			$vectorStoreIds = $this->_assistant['tool_resources']['file_search']['vector_store_ids'];
			foreach($vectorStoreIds as $vectorStoreId){
				$vectoreStoreFiles = $this->getVectorStoreFiles($vectorStoreId);
				if(!isset($vectoreStoreFiles['error'])){
					foreach($vectoreStoreFiles as $vectorStoreFile){
						$vectorStoreFileIds[] = $vectorStoreFile['id'];
					}
				}
			}
		}

		$assistFileIds = array_unique(array_merge($assistFileIds, $vectorStoreFileIds));

		$ret = [];

		if(empty($this->_apiKeyFiles)) $this->loadFiles();

		foreach($assistFileIds as $fileId){
			if(isset($this->_apiKeyFiles[$fileId])){
				$ret[] = $this->_apiKeyFiles[$fileId];
			}
		}
		
		return $ret;
	}
	
	public function detachFile($fileId){

		$assistantId = $this->_assistantId;
		$url = "https://api.openai.com/v2/assistants/$assistantId/files/$fileId";
		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, null, 'DELETE');

		return $response;  // Detach Status
	}
	
	
	public function replaceAssistantFile($fileId, $newFileName, $sourceFilePath, $contentType){
		
		// First detach fileId from Assistant
		$response = $this->detachFile($fileId);
		if(isset($response['error'])){
			$ret['error']['message'] = $response['error']['message'];
			return $ret;
		}

		// Now delete the file
		$response = $this->deleteFile($fileId);
		if(isset($response['error'])){
			$ret['error']['message'] = $response['error']['message'];
			return $ret;
		}
		
		// Now upload new
		$file = $this->uploadFileFromPath($newFileName, $sourceFilePath, $contentType, true);

		return $file;
	}
	
	public function submitFeedback($message, $feedback, $feedbackFilePath){
		
		// Creates/replaces the twxfeedback.json OpenAI file and attaches to the Assistant

		$ret = [];
		if(!file_exists($feedbackFilePath)){
			try {
				TWXChatUtil::touch($feedbackFilePath);
				if(!file_exists($feedbackFilePath)){
					$ret['error']['message'] = 'unable to create feedback file';
					return $ret;
				}
			}catch(Exception $ex){
				$ret['error']['message'] = 'error creating feedback file: '.$ex->getMessage();
				return $ret;
			}
		}

		$allFeedback = [];
		try {
			$contents = file_get_contents($feedbackFilePath);
			if(TWXChatUtil::isValidJson($contents)) $allFeedback = json_decode($contents,true);
		}catch(Exception $ex){
			// Do Nothing, will overwrite feedback
		}
		
		TWXChatUtil::log("previous feedback: ".json_encode($allFeedback));
		
		// Append new feedback
		$allFeedback[] = [
			'question' => $message,
			'answer' => $feedback
		];

		file_put_contents($feedbackFilePath, json_encode($allFeedback));

		$this->loadFiles(); // Loads all files for the API KEY
		TWXChatUtil::log("all apikey files: ".count($this->_apiKeyFiles));

		$assistantFiles = $this->getAssistantFiles(); // Get any files attached to the Assistant
		TWXChatUtil::log("assistant files: ".count($assistantFiles));

		$assistantFeedbackFileName = 'twxfeedback.json';

		// Detach any previous feedback files from Assistant
		foreach($assistantFiles as $af){

			$pos = strpos($af['filename'], $assistantFeedbackFileName);
			if($pos === 0){

				// Detach the file from the Assistant
				TWXChatUtil::log("detach file {$af['filename']} from Assistant");
				$ret = $this->detachFile($af['id']);
				if(isset($ret['error']['message'])){
					TWXChatUtil::log($ret['error']['message']);
				}else{
					//TWXChatUtil::deleteFile($this->_apiKey, $af['id']);
				}
			}
		}

		// Delete any detached feedback files
		foreach($this->_apiKeyFiles as $af){
			$pos = strpos($af['filename'], $assistantFeedbackFileName);
			if($pos === 0){
				TWXChatUtil::log("delete file {$af['filename']} from Storage");
				TWXChatUtil::deleteFile($this->_apiKey, $af['id']);
			}

		}
		
		TWXChatUtil::log("upload new feedback file with contents: ".json_encode($allFeedback));
		
		//$fileName = 'twxfeedback-'.date('YmdHis').'.json';
		
		// upload feedback contents as a file to OpenAI
		$response = $this->uploadFile(json_encode($allFeedback), $assistantFeedbackFileName, 'json', 'assistants');
		if(isset($response['error'])){
			$ret['error']['message'] = $response['error']['message'];
			return $ret;
		}

		if(!isset($response['id'])){
			$ret['error']['message'] = 'Error creating feedback file in OpenAI';
			return $ret;
		}

		// Attach the file to the Assistant in OpenAI
		$fileId = $response['id'];
		TWXChatUtil::log("attach new feedback file $fileId to Assistant.");

		$assistantFile = $this->attachFile($fileId);
		if(isset($assistantFile['error'])){
			$ret['error']['message'] = $assistantFile['error']['message'];
			return $ret;
		}
		
		$ret = [
			'file_id' => $fileId,
			'file_name' => $assistantFeedbackFileName
		];

		TWXChatUtil::log("return success");

		return $ret;
	}
	
	
	///////////////////////// PRIVATES ///////////////////////////

	private function getAssistantPrivate($assistantId){

		$url = "https://api.openai.com/v1/assistants/$assistantId";
		
		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, null, 'POST');
		if(isset($response['error'])){
			return null;
		}

		return $response;
	}
	
	private function createThread()
    {
		
		$url = 'https://api.openai.com/v1/threads';

		$data = [];
		
		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, $data, 'POST');

		if(isset($response['error'])){
			return $response['error']['message'];
		}

		return $response;	
    }
	
	private function sendMessage($query, $threadId, $role = 'user') {
		
		// asyncronously send message to Assistant. Returns Message object
		
		$url = "https://api.openai.com/v1/threads/$threadId/messages";

		$data = [
			'role' => $role,
			'content' => $query
		];
		
		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, $data, 'POST');

		return $response;	
	}
	
	private function getMessageResponse($messageId , $threadId){
		
		$assistantId = $this->_assistantId;
		
		// create a run
		$url = "https://api.openai.com/v1/threads/$threadId/runs";
		
		$data = [
			'assistant_id' => $assistantId,
			// "additional_instructions" => "Please address the user as Jane Doe. The user has a premium account."
		];

		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, $data, 'POST');
		if(isset($response['error'])) return $response;
		
		$runId = $response['id'];
		
		$usage = [];
		
		// Poll Run
		$i = 0; $max_i = 40;
		while($i<$max_i){
			
			$url = "https://api.openai.com/v1/threads/$threadId/runs/$runId";
			$data = [];
			
			$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, $data, 'GET');
			if(isset($response['error'])){
				return $response['error']['message'];
			}

			if($response['status'] == 'completed') {
				$usage = $response['usage'];
				break;
			}
			sleep(2); // 2 seconds
			$i++;
		}
		
		if($i >= $max_i){
			$ret = [];
			$ret['error']['message'] = 'Timeout getting response. Try again later.';
			return $ret;
		}

		// Get Messages
		$url = "https://api.openai.com/v1/threads/$threadId/messages";
		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, null, 'GET');
		if(isset($response['error'])){
			return $response['error']['message'];
		}

		foreach($response['data'] as $message){
			if($message['run_id'] == $runId) {
				$ret = [
					'answer' => $message['content'][0]['text']['value'],
					'usage' => $usage
				];
				return $ret; // $message['content'][0]['text']['value'];
			}
		}
		
		return null;
	}
	
	private function uploadFile($contents, $fileName, $contentType = 'application/json', $purpose = 'assistants') {

		// Uploads file contents to the Organisation belonging to the apiKey
		// Returns newly created file object
		
		try{
			$tmpFilePath = TWXChatUtil::getUploadsDir('tmp') . uniqid('feedback_', true).'.json';

			if(file_exists($tmpFilePath)) wp_delete_file($tmpFilePath);
			
			file_put_contents($tmpFilePath, $contents);
			
			$c_file = new CURLFile($tmpFilePath, $contentType, $fileName);

		}catch(Exception $ex){
			
			if(class_exists('TWXChatSpider')) TWXChatSpider::log('uploadFile error: '.$ex->getMessage(),'a',true);
		}
		
		
		$url = "https://api.openai.com/v1/files";

		$data = [
			'purpose' => $purpose,
			'file' => $c_file
		];

		$response = null;
		if(class_exists('TWXChatPlus')){
			$response = TWXChatPlus::goCURLFormData($this->_apiKey, $url, $data);
		}else{
			$response = $this->goWPFormData($url, $data);
		}
		
		wp_delete_file($tmpFilePath);
		
		return $response;
	}
	
	private function attachFile($fileId){


		$vectorStoreId = null;
		$vectorStoreIds = $this->getVectorStoreIds();
		if(empty($vectorStoreIds)){
			// create VS
			if(class_exists('TWXChatSpider')) TWXChatSpider::log("attachFile() no vector store found. create new");

			$response = $this->createVectorStore(true);
			if(isset($response['error'])){
				$ret['error']['message'] = $response['error']['message'];
				return $ret;
			}

			if(!isset($response['id'])){
				$ret['error']['message'] = 'Error creating Vector Store in OpenAI';
				return $ret;
			}

			$vectorStoreId = $response['id'];

		}else{
			$vectorStoreId = $vectorStoreIds[0];
		}
		
		// Attach the File to the VS
		$url = "https://api.openai.com/v1/vector_stores/{$vectorStoreId}/files";

		$data = [
			'file_id' => $fileId
		];

		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, $data, 'POST');
		
		return $response;  // File Object

	}
	
	public function createVectorStore($andAttachToAssistant = true) {

		$url = "https://api.openai.com/v1/vector_stores";

		$data = [
  			"name"=> "Vector Store for ".$this->_assistant['name'],
  			"description"=> "Vector Store for ".$this->_assistant['name'],
		];

		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, $data, 'POST');

		if(isset($response['error'])){
			return $response;
		}

		if(!isset($response['id'])){
			$ret['error']['message'] = 'Error creating Vector Store in OpenAI';
			return $ret;
		}

		if($andAttachToAssistant){
			$response2 = $this->attachVectorStoreToAssistant($response['id']);
			if(isset($response2['error'])){
				return $response2;
			}
		}

		return $response;
	}

	public function attachVectorStoreToAssistant($vectorStoreId) {

		$assistantId = $this->_assistant['id'] ?? '';
		if(empty($assistantId)) {
			$ret['error']['message'] = 'Missing Assistant Id';
			return $ret;
		}

		$vectorStoreIds = $this->getVectorStoreIds();
		if(in_array($vectorStoreId, $vectorStoreIds)) {
			$ret['error']['message'] = 'Vector Store Id already attached';
			return $ret;
		}

		$assistant = $this->_assistant;

		// Find existing file_search tool or create a new one
		$tools_resources = isset($assistant['tool_resources']) ? $assistant['tool_resources'] : [];

		$tools_resources['file_search']['vector_store_ids'][] = $vectorStoreId;

		$data['tool_resources'] = $tools_resources;

		$url = "https://api.openai.com/v1/assistants/{$assistantId}";

		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, $data, 'POST');

		return $response;
	}


	public function deleteFile($fileId){
		
		return TWXChatUtil::deleteFile($this->_apiKey, $fileId);
	}

	public function getFile($fileId){
		
		$url = "https://api.openai.com/v1/files/$fileId";

		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, null, 'GET');

		return $response;
	}
	
	public function getFiles($purpose = 'assistants') {

		return TWXChatUtil::getFiles($this->_apiKey, $purpose);
	}
	
	private function getFileContents($fileId){
		
		$url = "https://api.openai.com/v1/files/$fileId/content";
		
		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, null, 'GET');
		
		return $response;
	}
	
	private function getUseageCosts($tokenUsage){
		
		$model = $this->_assistant['model'];

		if(!isset($tokenUsage['prompt_tokens']) || !isset($tokenUsage['completion_tokens']) || empty(TWXChatUtil::getModelCosts($model))) return false;
		
		$ret = [];
		
		$prompt_tokens = $tokenUsage['prompt_tokens'];
		$completion_tokens = $tokenUsage['completion_tokens'];
		
		$modelCosts = TWXChatUtil::getModelCosts($model);
		$totalCost = $prompt_tokens * $modelCosts['in'];
		$totalCost += $completion_tokens * $modelCosts['out'];
		
		$ret['prompt_tokens'] = $prompt_tokens;
		$ret['completion_tokens'] = $completion_tokens;
		$ret['total_tokens'] = $prompt_tokens + $completion_tokens;
		$ret['cost_total'] = number_format($totalCost, 4, '.', '');
		$ret['currency'] = 'USD';
		$ret['is_total_usage'] = true; // is thread total
		
		return $ret;
	}
	
	private function goWPFormData($url, $data) {
		// Submits Form Data POST using wp_remote_post()

		$apiKey = $this->_apiKey;

		// Prepare headers
		$headers = [
			'Authorization' => 'Bearer ' . $apiKey,
			'OpenAI-Beta'   => 'assistants=v2',
			// 'Content-Type' => 'multipart/form-data', // Let WP handle this automatically
		];

		// Prepare args
		$args = [
			'headers' => $headers,
			'body'    => $data,
			'timeout' => 30,
		];

		$response = wp_remote_post($url, $args);

		if (is_wp_error($response)) {
			// Optional: log error
			TWXChatUtil::log('goWPFormData error: ' . $response->get_error_message());
			return false;
		}

		$body = wp_remote_retrieve_body($response);

		return json_decode($body, true);
	}

	
}

// Class is DEPRECATED now by TWXChatOpenAIResponses class
class TWXChatOpenAICompletions
{
    // Class properties (module variables)
    private $_apiKey;
    private $_model;
    private $_temperature;
	private $_tools;
	private $_toolChoice;

    // Constructor method
    public function __construct($apiKey, $model = null, $temperature = 0.5)
    {
        $this->_apiKey = $apiKey;
        $this->_model = ($model ? $model : 'gpt-4o-mini');
		$this->_temperature = $temperature;  // creativity 0-2
		$this->_tools = null;
		$this->_toolChoice = 'auto';
    }

    // Destructor method
    public function __destruct()
    {
        // Log or perform actions needed at object destruction
    }
	
	public function getModel(){
		return $this->_model;
	}

	public static function getMaxOutputTokenOptions(){

		$options = [
			['value' => '200', 'display' => '200: short / concise answers'],
			['value' => '800', 'display' => '800: normal chat responses (default)'],
			['value' => '1500', 'display' => '1500: detailed explanations'],
			['value' => '3000', 'display' => '3000: long, structured, multi-section replies'],
		];
		return $options;
	}	

	public function setUserFieldsTools($userFields){

		// Single function approach
		if(empty($userFields)) {
			$this->_tools = null;
			$this->_toolChoice = 'auto';
			return;
		}

		// Initialise with full response (answer) property
		$functionName = 'user_fields_main_function';
		$properties = [];
		$properties['full_response'] = [
			"type" => "string",
			"description" => "The full response to the user",
		];
		$required = ["full_response"];

		foreach($userFields as $field){
			$fieldName = $field['name'] ?? '';
			if(empty($fieldName)) continue;

			$properties[$fieldName] = [
				"type" => "string",
				"description" => $field['description'] ?? '',
			];
			//$required[] = $fieldName;
		}

		$this->_tools = [
			[
				"type" => "function",
				"function" => [
					"name" => $functionName,
					"description" => "Always call this function",
					"parameters" => [
						"type" => "object",
						"properties" => $properties,
					],
					"required" => $required,
				]
			]
		];

		$this->_toolChoice = [
			'type' => 'function',
			'function' => [
				'name' => $functionName
			]
		];

	}

	public static function getUserFieldFunction($userField){
		// field must have name and description and optionally a value
		$function = [];
		$fieldName = $userField['name'];
		$fieldDesc = $userField['description'];
		$function['name'] = "{$fieldName}";
		$function['description'] = "Called when {$fieldName} ($fieldDesc) is in the user message";
		$function['parameters']['type'] = "object";
		//$function['parameters']['properties'] = [$fieldName => $userField];
		$function['parameters']['properties'][$fieldName] = ['type'=>'string','description'=>$fieldDesc];
		$function['parameters']['required'] = [$fieldName];
		return $function;
	}
	
	public function query($query, $parseResult = true) {
		
		$messages = [];
		if(is_array($query)){
			$messages = $query;
		}else{
			$messages = [['role'=>'user', 'content'=>$query]];
		}

		$ret = [];
		$url = "https://api.openai.com/v1/chat/completions";

		$data = [
			'model' => $this->_model,
			'messages' => $messages,
			'temperature' => $this->_temperature,
		];

		if(!empty($this->_tools)){
			$data['tools'] = $this->_tools;
			$data['tool_choice'] = $this->_toolChoice;
		}
		
		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, $data, 'POST');

		if (isset($response['error']['message'])) {
			$ret['error']['message'] = $response['error']['message'];
		} else {

			$answer = '';

			if(isset($response['choices'][0]['message']['content'])){
				$responseText = $response['choices'][0]['message']['content'];
				$source_ids = TWXChatUtil::extractSourceIdsFromText($responseText);
				if($parseResult){
					$parsedAnswer = TWXChatUtil::parseAnswer($responseText);
					//list($parsed, $source_ids) = TWXChatUtil::parseInlineSources($parsedAnswer);
					$answer = $parsedAnswer;
				}else{
					$answer = $responseText;
				}

				if(!empty($source_ids)) $ret['relevant_source_ids'] = $source_ids;
			}

			// Check if OpenAI has tool/function output
			if (isset($response['choices'][0]['message']['tool_calls'])) {
				$tool_calls = $response['choices'][0]['message']['tool_calls'];

				foreach($tool_calls as $tool_call){
					if($tool_call['type'] == 'function'){
						$function = $tool_call['function'];
						$funcName = $function['name'];
						if($funcName === 'user_fields_main_function'){
							$args = json_decode($function['arguments'],true);
							foreach($args as $arg_name => $arg_val){
								if(!empty($arg_val)) {
									if($arg_name === 'full_response') {
										$answer = $arg_val;
									}else{
										$ret['user_fields'][$arg_name] = $arg_val;
									}
								}
							}
						}
					}
				}
			}

			if(empty($answer)){
				$ret['error']['message'] = 'no response';
			}else{
				$ret['answer'] = $answer;
			}

			if(isset($response['usage'])) $ret['usage'] = $this->getUseageCosts($response['usage']);
			$ret['model'] = $this->_model;
		}
		
		return $ret;
	}

	public function queryStream($query, callable $callback) {

		// Executes OpenAI query and returns text stream back via $callback function - uses Wordpress functions/classes

		$messages = is_array($query) ? $query : [['role' => 'user', 'content' => $query]];

		$data = [
			'model'       => $this->_model,
			'messages'    => $messages,
			'temperature' => $this->_temperature,
			'stream'      => true,
		];

		$fullStreamOutput = '';

		// Add a temporary curl handler to stream data
		add_action('http_api_curl', function($handle) use (&$fullStreamOutput, $callback) {
			curl_setopt($handle, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$fullStreamOutput, $callback) {
				if (!empty($data)) {
					$fullStreamOutput .= $data;
					$callback($data);
				}
				return strlen($data);
			});
			curl_setopt($handle, CURLOPT_TIMEOUT, 0); // keep stream alive
		});

		$http = new WP_Http(); // Tried using wp_remote_post() but kept closing mid-stream
		$response = $http->request('https://api.openai.com/v1/chat/completions', [
			'method'  => 'POST',
			'headers' => [
				'Authorization' => 'Bearer ' . $this->_apiKey,
				'Content-Type'  => 'application/json',
			],
			'body'    => wp_json_encode($data),
			'timeout' => 0, // streaming can take long
		]);

		// remove curl hook after request
		remove_all_actions('http_api_curl');

		$ret = [];
		if (is_wp_error($response)) {
			$ret['error']['message'] = $response->get_error_message();
		} else {
			$ret['answer'] = 'streamed_to_callback';
			$ret['fullStreamOutput'] = $fullStreamOutput;
		}

		return $ret;
	}

	//// PRIVATES ////

	private function getUseageCosts($tokenUsage){
		
		$model = $this->_model;

		if(!isset($tokenUsage['prompt_tokens']) || !isset($tokenUsage['completion_tokens']) || empty(TWXChatUtil::getModelCosts($model))) return false;
		
		$ret = [];
		
		$prompt_tokens = $tokenUsage['prompt_tokens'];
		$completion_tokens = $tokenUsage['completion_tokens'];
		
		$modelCosts = TWXChatUtil::getModelCosts($model);
		$totalCost = $prompt_tokens * $modelCosts['in'];
		$totalCost += $completion_tokens * $modelCosts['out'];
		
		$ret['prompt_tokens'] = $prompt_tokens;
		$ret['completion_tokens'] = $completion_tokens;
		$ret['total_tokens'] = $prompt_tokens + $completion_tokens;
		$ret['cost_total'] = number_format($totalCost, 4, '.', '');
		$ret['currency'] = 'USD';
		$ret['is_total_usage'] = false;
		
		return $ret;
	}
	
}

class TWXChatOpenAIResponses
{
	// Class properties
	private $_apiKey;
	private $_model;
	private $_temperature;
	private $_tools;        // Responses API format tools only
	private $_toolChoice;   // 'auto' | 'none' | object per Responses API

	// Constructor
	public function __construct($apiKey, $model = null, $temperature = 0.5)
	{
		$this->_apiKey = $apiKey;
		$this->_model = ($model ? $model : 'gpt-4o-mini');
		$this->_temperature = $temperature;
		$this->_tools = null;
		$this->_toolChoice = 'auto';
	}

	public function __destruct() {}

	public function getModel()
	{
		return $this->_model;
	}

	public function setTemperature($temperature)
	{
		$this->_temperature = $temperature;
		return $this->_temperature;
	}

	/**
	 * Tools must be in Responses API format ONLY, e.g.
	 * [
	 *   [
	 *     'type' => 'function',
	 *     'name' => 'user_fields_main_function',
	 *     'description' => '...',
	 *     'parameters' => [ ...json schema... ],
	 *   ]
	 * ]
	 */
	public function setTools($tools)
	{
		$this->_tools = $tools;
		return $this->_tools;
	}

	/**
	 * Responses API tool_choice:
	 * - 'auto'
	 * - 'none'
	 * - or an object (advanced)
	 */
	public function setToolChoice($toolChoice)
	{
		$this->_toolChoice = $toolChoice;
		return $this->_toolChoice;
	}

	public static function getMaxOutputTokenOptions()
	{
		return [
			['value' => '200',  'display' => '200: short / concise answers'],
			['value' => '800',  'display' => '800: normal chat responses (default)'],
			['value' => '1500', 'display' => '1500: detailed explanations'],
			['value' => '3000', 'display' => '3000: long, structured, multi-section replies'],
		];
	}

	/**
	 * Non-streaming query.
	 *
	 * $query can be:
	 * - string (treated as a single user message)
	 * - array of Responses input messages: [['role'=>'user'|'system'|'assistant', 'content'=> '...'], ...]
	 */
	public function query($query, $maxOutputTokens = TWXCHAT_DEFAULT_MAX_OUTPUT_TOKENS, $parseResult = true)
	{
		$input = is_array($query) ? $query : [['role' => 'user', 'content' => $query]];

		$ret = [];
		$url = "https://api.openai.com/v1/responses";

		$maxOutputTokens = (int)$maxOutputTokens;
		if ($maxOutputTokens <= 0) $maxOutputTokens = (int)TWXCHAT_DEFAULT_MAX_OUTPUT_TOKENS;

		$data = [
			'model'             => $this->_model,
			'input'             => $input,
			'temperature'       => $this->_temperature,
			'store'             => false,
			'max_output_tokens' => $maxOutputTokens,
		];

		if (!empty($this->_tools)) {
			$data['tools'] = $this->_tools;
			if (!empty($this->_toolChoice)) {
				$data['tool_choice'] = $this->_toolChoice;
			}
		}

		$response = TWXChatUtil::getHttpOpenAI_WP($this->_apiKey, $url, $data, 'POST');

		if (isset($response['error']['message'])) {
			$ret['error']['message'] = $response['error']['message'];
			return $ret;
		}

		$answer = '';
		$responseText = '';

		// Extract assistant text + function_call outputs from Responses "output"
		if (!empty($response['output']) && is_array($response['output'])) {
			foreach ($response['output'] as $item) {

				// Assistant message text
				if (($item['type'] ?? '') === 'message' && ($item['role'] ?? '') === 'assistant') {
					if (!empty($item['content']) && is_array($item['content'])) {
						foreach ($item['content'] as $part) {
							if (($part['type'] ?? '') === 'output_text' && isset($part['text'])) {
								$responseText .= $part['text'];
							}
						}
					}
				}

				// Function/tool calls (returned as output items)
				if (($item['type'] ?? '') === 'function_call') {
					$funcName = $item['name'] ?? '';
					$args = [];

					if (!empty($item['arguments']) && is_string($item['arguments'])) {
						$decoded = json_decode($item['arguments'], true);
						if (is_array($decoded)) $args = $decoded;
					}

					// Keep your existing special handling
					if ($funcName === 'user_fields_main_function' && !empty($args)) {
						foreach ($args as $arg_name => $arg_val) {
							if (!empty($arg_val)) {
								if ($arg_name === 'full_response') {
									$responseText = (string)$arg_val;
								} else {
									$ret['user_fields'][$arg_name] = $arg_val;
								}
							}
						}
					} else if (!empty($args)) {
						// Optional: expose other function calls if you want them later
						$ret['tool_calls'][] = [
							'name' => $funcName,
							'arguments' => $args,
						];
					}
				}
			}
		}

		// Sources parsing (same behaviour you had)
		if (!empty($responseText)) {
			$source_ids = TWXChatUtil::extractSourceIdsFromText($responseText);

			$answer = $parseResult ? TWXChatUtil::parseAnswer($responseText) : $responseText;

			if (!empty($source_ids)) $ret['relevant_source_ids'] = $source_ids;
		}

		if (empty($answer)) {
			$ret['error']['message'] = 'no response';
		} else {
			$ret['answer'] = $answer;
		}

		// Map Responses usage -> your cost calculator (expects prompt_tokens/completion_tokens)
		if (isset($response['usage']) && is_array($response['usage'])) {
			$u = $response['usage'];
			$mappedUsage = [
				'prompt_tokens'     => (int)($u['input_tokens'] ?? 0),
				'completion_tokens' => (int)($u['output_tokens'] ?? 0),
				'total_tokens'      => (int)($u['total_tokens'] ?? (($u['input_tokens'] ?? 0) + ($u['output_tokens'] ?? 0))),
			];
			$ret['usage'] = $this->getUseageCosts($mappedUsage);
		}

		$ret['model'] = $this->_model;

		return $ret;
	}

	/**
	 * Streaming query (Responses SSE).
	 * Emits native Responses events to $callback($evtArray).
	 */
	public function queryStream($query, $maxOutputTokens, callable $callback)
	{
		$input = is_array($query) ? $query : [['role' => 'user', 'content' => $query]];
		$maxOutputTokens = (int)$maxOutputTokens;
		if ($maxOutputTokens <= 0) $maxOutputTokens = (int)TWXCHAT_DEFAULT_MAX_OUTPUT_TOKENS;

		$data = [
			'model'             => $this->_model,
			'input'             => $input,
			'temperature'       => $this->_temperature,
			'stream'            => true,
			'store'             => false,
			'max_output_tokens' => $maxOutputTokens,
		];

		if (!empty($this->_tools)) {
			$data['tools'] = $this->_tools;
			if (!empty($this->_toolChoice)) {
				$data['tool_choice'] = $this->_toolChoice;
			}
		}

		// Add temporary curl handler to stream SSE
		add_action('http_api_curl', function ($handle) use ($callback) {

			$buffer = '';

			curl_setopt($handle, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$buffer, $callback) {

				if ($chunk === '' || $chunk === false) {
					return 0;
				}

				$buffer .= $chunk;

				// SSE events are separated by a blank line
				while (($pos = strpos($buffer, "\n\n")) !== false) {

					$rawEvent = substr($buffer, 0, $pos);
					$buffer   = substr($buffer, $pos + 2);

					$rawEvent = trim($rawEvent);
					if ($rawEvent === '') continue;

					$lines = preg_split("/\r\n|\n|\r/", $rawEvent);

					foreach ($lines as $line) {

						$line = trim($line);
						if (stripos($line, 'data:') !== 0) continue;

						$dataStr = trim(substr($line, 5));

						if ($dataStr === '[DONE]') {
							$callback(['type' => 'response.completed']);
							continue;
						}

						$evt = json_decode($dataStr, true);
						if (!is_array($evt)) continue;

						$callback($evt);
					}
				}

				return strlen($chunk);
			});

			curl_setopt($handle, CURLOPT_TIMEOUT, 0);
		});

		$http = new WP_Http();
		$response = $http->request('https://api.openai.com/v1/responses', [
			'method'  => 'POST',
			'headers' => [
				'Authorization' => 'Bearer ' . $this->_apiKey,
				'Content-Type'  => 'application/json',
				'Accept'        => 'text/event-stream',
			],
			'body'    => wp_json_encode($data),
			'timeout' => 0,
		]);

		remove_all_actions('http_api_curl');

		$ret = [];
		if (is_wp_error($response)) {
			$ret['error']['message'] = $response->get_error_message();
		} else {
			$ret['answer'] = 'streamed_to_callback';
		}

		return $ret;
	}

	//// PRIVATES ////

	private function getUseageCosts($tokenUsage)
	{
		$model = $this->_model;

		if (!isset($tokenUsage['prompt_tokens']) || !isset($tokenUsage['completion_tokens']) || empty(TWXChatUtil::getModelCosts($model))) {
			return false;
		}

		$prompt_tokens = (int)$tokenUsage['prompt_tokens'];
		$completion_tokens = (int)$tokenUsage['completion_tokens'];

		$modelCosts = TWXChatUtil::getModelCosts($model);

		$totalCost = $prompt_tokens * $modelCosts['in'];
		$totalCost += $completion_tokens * $modelCosts['out'];

		return [
			'prompt_tokens'     => $prompt_tokens,
			'completion_tokens' => $completion_tokens,
			'total_tokens'      => $prompt_tokens + $completion_tokens,
			'cost_total'        => number_format($totalCost, 4, '.', ''),
			'currency'          => 'USD',
			'is_total_usage'    => false,
		];
	}
}
