<?php

if(!defined('SOFTACULOUS')){
	die('Hacking Attempt');
}

class AIClient {
	private $provider;
	private $api_key;
	private $model;
	private $provider_type;
	private $base_url;
	private $provider_config;

	public function __construct($provider, $api_key, $model, $provider_config = array()){
		$this->provider = $provider;
		$this->api_key = $api_key;
		$this->model = $model;
		$this->provider_type = $provider->get_id();
		$this->base_url = $provider->get_api_endpoint();
		$this->provider_config = $provider_config;
	}

	public function chat($messages, $tools = array(), $options = array()){
		if($this->provider_type === 'anthropic'){
			return $this->anthropic_chat($messages, $tools, $options);
		}elseif($this->provider_type === 'google'){
			return $this->google_chat($messages, $tools, $options);
		}elseif($this->provider_type === 'azure'){
			return $this->azure_chat($messages, $tools, $options);
		}
		if($this->provider_type === 'openai' && $this->is_responses_only_model()){
			return $this->openai_responses_chat($messages, $tools, $options);
		}
		return $this->openai_chat($messages, $tools, $options);
	}

	public function chat_stream($messages, $tools, $on_event, $options = array()){
		if($this->provider_type === 'anthropic'){
			return $this->anthropic_stream($messages, $tools, $on_event, $options);
		}elseif($this->provider_type === 'google'){
			return $this->google_stream($messages, $tools, $on_event, $options);
		}elseif($this->provider_type === 'azure'){
			return $this->azure_stream($messages, $tools, $on_event, $options);
		}
		if($this->provider_type === 'openai' && $this->is_responses_only_model()){
			return $this->openai_responses_stream($messages, $tools, $on_event, $options);
		}
		return $this->openai_stream($messages, $tools, $on_event, $options);
	}

	private $abort_check_file = '';
	private $abort_check_time = 0;

	public function set_abort_check_file($filepath){
		$this->abort_check_file = $filepath;
	}

	private function should_abort(){
		if(empty($this->abort_check_file)) return false;
		$now = time();
		if($now - $this->abort_check_time < 1) return false;
		$this->abort_check_time = $now;
		if(file_exists($this->abort_check_file)) return true;
		if(function_exists('connection_aborted') && connection_aborted()) return true;
		return false;
	}

	public function test_connection(){
		$messages = array(array('role' => 'user', 'content' => 'Say "OK" in one word.'));
		try{
			$result = $this->chat($messages, array(), array('max_tokens' => 10));
			if(!empty($result['error'])){
				return array('success' => false, 'error' => $result['error']);
			}
			return array('success' => true, 'message' => __('Connection successful'));
		}catch(\Exception $e){
			return array('success' => false, 'error' => $e->getMessage());
		}
	}

	// ---- OpenAI Compatible ----

	private function openai_headers(){
		$h = array(
			'Content-Type: application/json'
		);
		if(!empty($this->api_key)){
			$h[] = 'Authorization: Bearer ' . $this->api_key;
		}
		if($this->provider_type === 'opencode_zen' || $this->provider_type === 'opencode_zen_premium' || $this->provider_type === 'openrouter'){
			$h[] = 'HTTP-Referer: https://opencode.ai/';
			$h[] = 'X-Title: Softaculous AI';
		}
		if(!empty($this->provider_config['extra_headers'])){
			foreach($this->provider_config['extra_headers'] as $eh){
				$h[] = $eh;
			}
		}
		return $h;
	}

	private function openai_chat($messages, $tools, $options){
		$url = rtrim($this->base_url, '/') . '/chat/completions';
		$headers = $this->openai_headers();
		$payload = $this->openai_build_payload($messages, $tools, $options);
		$payload['stream'] = false;

		$response = $this->curl_post($url, $headers, $payload, isset($options['timeout']) ? $options['timeout'] : 120);
		if(!empty($response['error'])){
			if($this->provider_type === 'openai' && $this->is_responses_endpoint_error($response['error'])){
				return $this->openai_responses_chat($messages, $tools, $options);
			}
			return $response;
		}
		return $this->openai_parse_response($response['body']);
	}

	private function is_responses_endpoint_error($msg){
		$msg = strtolower((string)$msg);
		if(strpos($msg, 'v1/responses') !== false){
			return true;
		}
		if(strpos($msg, 'not supported in the v1/chat/completions endpoint') !== false){
			return true;
		}
		return false;
	}

	private function openai_stream($messages, $tools, $on_event, $options){
		$url = rtrim($this->base_url, '/') . '/chat/completions';
		$headers = $this->openai_headers();
		$payload = $this->openai_build_payload($messages, $tools, $options);
		$payload['stream'] = true;

		$accumulated = array('text' => '', 'reasoning' => '', 'tool_calls' => array(), 'usage' => array());
		$tool_call_buffers = array();
		$sse_buffer = '';

		try{
		$this->curl_post_stream($url, $headers, $payload, function($chunk) use ($on_event, &$accumulated, &$tool_call_buffers, &$sse_buffer){
			$sse_buffer .= $chunk;
			while(($pos = strpos($sse_buffer, "\n")) !== false){
				$line = substr($sse_buffer, 0, $pos);
				$sse_buffer = substr($sse_buffer, $pos + 1);
				$line = trim($line);
				if(strpos($line, 'data: ') !== 0) continue;
				$data_str = substr($line, 6);
				if(trim($data_str) === '[DONE]') return;

				$data = @json_decode($data_str, true);
				if(!$data) continue;

				if(!empty($data['choices'][0])){
					$choice = $data['choices'][0];
					$delta = (!empty($choice['delta']) ? $choice['delta'] : array());

					if(!empty($delta['content'])){
						$accumulated['text'] .= $delta['content'];
						$on_event(array('type' => 'text_delta', 'text' => $delta['content']));
					}

					if(!empty($delta['reasoning_content'])){
						$accumulated['reasoning'] .= $delta['reasoning_content'];
						$on_event(array('type' => 'reasoning_delta', 'text' => $delta['reasoning_content']));
					}

					if(!empty($delta['tool_calls'])){
						foreach($delta['tool_calls'] as $tc){
							$idx = isset($tc['index']) ? $tc['index'] : 0;
							if(!isset($tool_call_buffers[$idx])){
								$tool_call_buffers[$idx] = array(
									'id' => isset($tc['id']) ? $tc['id'] : '',
									'name' => isset($tc['function']['name']) ? $tc['function']['name'] : '',
									'arguments' => ''
								);
							}
							if(!empty($tc['id'])) $tool_call_buffers[$idx]['id'] = $tc['id'];
							if(!empty($tc['function']['name'])) $tool_call_buffers[$idx]['name'] = $tc['function']['name'];
							if(!empty($tc['function']['arguments'])) $tool_call_buffers[$idx]['arguments'] .= $tc['function']['arguments'];
						}
					}

					if(!empty($choice['finish_reason'])){
						if($choice['finish_reason'] === 'tool_calls' || $choice['finish_reason'] === 'function_call'){
						}
					}
				}

				if(!empty($data['usage'])){
					$accumulated['usage'] = $data['usage'];
					// Normalize OpenAI cached tokens from streaming
					if(!empty($data['usage']['prompt_tokens_details']['cached_tokens'])){
						$accumulated['usage']['cached_tokens'] = $data['usage']['prompt_tokens_details']['cached_tokens'];
					}
				}
			}
		}, isset($options['timeout']) ? $options['timeout'] : 120);
		}catch(\RuntimeException $e){
			if($this->provider_type === 'openai' && $this->is_responses_endpoint_error($e->getMessage())){
				return $this->openai_responses_stream($messages, $tools, $on_event, $options);
			}
			throw $e;
		}

		if(!empty($tool_call_buffers)){
			foreach($tool_call_buffers as $i => $tc){
				$args = @json_decode($tc['arguments'], true);
				if($args === null) $args = array();
				$tool_call = array(
					'id' => $tc['id'] ? $tc['id'] : 'call_'.$i,
					'name' => $tc['name'],
					'input' => $args
				);
				$accumulated['tool_calls'][] = $tool_call;
				$on_event(array('type' => 'tool_call', 'id' => $tool_call['id'], 'name' => $tool_call['name'], 'input' => $args));
			}
		}

		$on_event(array('type' => 'done', 'usage' => $accumulated['usage']));

		return array(
			'content' => $accumulated['text'],
			'tool_calls' => $accumulated['tool_calls'],
			'usage' => $accumulated['usage'],
			'parts' => $this->build_parts($accumulated)
		);
	}

	private function is_reasoning_model(){
		$model = strtolower((string)$this->model);
		if($model === '') return false;
		if(function_exists('ai_get_models_dev_cache')){
			$cache = ai_get_models_dev_cache();
			if(is_array($cache)){
				if(isset($cache['openai/' . $this->model]['reasoning']) && $cache['openai/' . $this->model]['reasoning']){
					return true;
				}
				if(isset($cache[$this->model]['reasoning']) && $cache[$this->model]['reasoning']){
					return true;
				}
			}
		}
		$patterns = array('gpt-5', 'gpt-6', 'o1', 'o2', 'o3', 'o4', 'o5', 'codex');
		foreach($patterns as $p){
			if(strpos($model, $p) === 0){
				return true;
			}
		}
		return false;
	}

	private function is_responses_only_model(){
		$model = strtolower((string)$this->model);
		if($model === '') return false;
		if(strpos($model, 'codex') !== false){
			return true;
		}
		return false;
	}

	private function openai_build_payload($messages, $tools, $options){
		$payload = array(
			'model' => $this->model,
			'messages' => $messages
		);

		if(!empty($tools)){
			$payload['tools'] = array_map(function($t){
				return array(
					'type' => 'function',
					'function' => array(
						'name' => $t['name'],
						'description' => $t['description'],
						'parameters' => $t['parameters']
					)
				);
			}, $tools);
		}

		$reasoning = $this->is_reasoning_model();

		if($reasoning){
			$effort = !empty($options['reasoning_effort']) ? $options['reasoning_effort'] : 'medium';
			$payload['reasoning_effort'] = $effort;
			if(!empty($options['max_tokens'])){
				$payload['max_completion_tokens'] = intval($options['max_tokens']);
			}
		}else{
			if(!empty($options['temperature'])){
				$payload['temperature'] = floatval($options['temperature']);
			}
			if(!empty($options['max_tokens'])){
				$payload['max_tokens'] = intval($options['max_tokens']);
			}
		}

		// OpenAI: automatic prefix caching (no explicit markers needed)
		// Just need to parse cached_tokens from usage response

		return $payload;
	}

	private function openai_parse_response($body){
		$data = @json_decode($body, true);
		if(!$data){
			return array('error' => __('Invalid JSON response from API'));
		}
		if(!empty($data['error'])){
			$msg = is_array($data['error']) ? (isset($data['error']['message']) ? $data['error']['message'] : json_encode($data['error'])) : $data['error'];
			return array('error' => $msg);
		}

		if(empty($data['choices'])) $choice = null; else $choice = $data['choices'][0];
		if(!$choice) return array('error' => __('No response choices'));

		if(empty($choice['message']['content'])) $text = ''; else $text = $choice['message']['content'];
		$reasoning = '';
		if(!empty($choice['message']['reasoning_content'])) $reasoning = $choice['message']['reasoning_content'];
		$tool_calls = array();

		if(!empty($choice['message']['tool_calls'])){
			foreach($choice['message']['tool_calls'] as $tc){
				$args = @json_decode(isset($tc['function']['arguments']) ? $tc['function']['arguments'] : '{}', true);
				if($args === null) $args = array();
				$tool_calls[] = array(
					'id' => isset($tc['id']) ? $tc['id'] : '',
					'name' => isset($tc['function']['name']) ? $tc['function']['name'] : '',
					'input' => $args
				);
			}
		}

		$accumulated = array('text' => $text, 'reasoning' => $reasoning, 'tool_calls' => $tool_calls, 'usage' => isset($data['usage']) ? $data['usage'] : array());
		// Normalize OpenAI cached tokens
		if(!empty($data['usage']['prompt_tokens_details']['cached_tokens'])){
			$accumulated['usage']['cached_tokens'] = $data['usage']['prompt_tokens_details']['cached_tokens'];
		}
		return array(
			'content' => $text,
			'tool_calls' => $tool_calls,
			'usage' => $accumulated['usage'],
			'parts' => $this->build_parts($accumulated)
		);
	}

	// ---- OpenAI Responses API (v1/responses) ----
	// Used for Codex models and any model OpenAI moves to the Responses-only endpoint.
	// Also used as a fallback when chat/completions returns the "use v1/responses" error.

	private function openai_responses_build_payload($messages, $tools, $options){
		$input = array();
		foreach($messages as $msg){
			$role = isset($msg['role']) ? $msg['role'] : 'user';
			if($role === 'system'){
				$input[] = array(
					'role' => 'system',
					'content' => isset($msg['content']) ? $msg['content'] : ''
				);
			}elseif($role === 'user'){
				$content = isset($msg['content']) ? $msg['content'] : '';
				if(is_array($content)) $content = json_encode($content);
				$input[] = array('role' => 'user', 'content' => $content);
			}elseif($role === 'assistant'){
				$assistant_content = array();
				if(!empty($msg['content'])){
					$content = $msg['content'];
					if(is_array($content)) $content = json_encode($content);
					if($content !== '' && $content !== null){
						$assistant_content[] = array('type' => 'output_text', 'text' => $content);
					}
				}
				if(!empty($msg['tool_calls'])){
					foreach($msg['tool_calls'] as $tc){
						$args_str = isset($tc['function']['arguments']) ? $tc['function']['arguments'] : '{}';
						$input[] = array(
							'type' => 'function_call',
							'call_id' => isset($tc['id']) ? $tc['id'] : '',
							'name' => isset($tc['function']['name']) ? $tc['function']['name'] : '',
							'arguments' => $args_str
						);
					}
				}
				if(!empty($assistant_content)){
					$input[] = array('role' => 'assistant', 'content' => $assistant_content);
				}
			}elseif($role === 'tool' || $role === 'tool_result'){
				$content = isset($msg['content']) ? $msg['content'] : '';
				if(is_array($content)) $content = json_encode($content);
				$input[] = array(
					'type' => 'function_call_output',
					'call_id' => isset($msg['tool_call_id']) ? $msg['tool_call_id'] : '',
					'output' => $content
				);
			}
		}

		$payload = array(
			'model' => $this->model,
			'input' => $input
		);

		if(!empty($tools)){
			$payload['tools'] = array_map(function($t){
				return array(
					'type' => 'function',
					'name' => $t['name'],
					'description' => $t['description'],
					'parameters' => $t['parameters']
				);
			}, $tools);
		}

		if($this->is_reasoning_model()){
			$effort = !empty($options['reasoning_effort']) ? $options['reasoning_effort'] : 'medium';
			$payload['reasoning'] = array('effort' => $effort);
			if(!empty($options['max_tokens'])){
				$payload['max_output_tokens'] = intval($options['max_tokens']);
			}
		}else{
			if(!empty($options['temperature'])){
				$payload['temperature'] = floatval($options['temperature']);
			}
			if(!empty($options['max_tokens'])){
				$payload['max_output_tokens'] = intval($options['max_tokens']);
			}
		}

		return $payload;
	}

	private function openai_responses_chat($messages, $tools, $options){
		$url = rtrim($this->base_url, '/') . '/responses';
		$headers = $this->openai_headers();
		$payload = $this->openai_responses_build_payload($messages, $tools, $options);
		$payload['stream'] = false;

		$response = $this->curl_post($url, $headers, $payload, isset($options['timeout']) ? $options['timeout'] : 120);
		if(!empty($response['error'])){
			return $response;
		}
		return $this->openai_responses_parse_response($response['body']);
	}

	private function openai_responses_parse_response($body){
		$data = @json_decode($body, true);
		if(!$data){
			return array('error' => __('Invalid JSON response from API'));
		}
		if(!empty($data['error'])){
			$msg = is_array($data['error']) ? (isset($data['error']['message']) ? $data['error']['message'] : json_encode($data['error'])) : $data['error'];
			return array('error' => $msg);
		}

		$text = '';
		$reasoning = '';
		$tool_calls = array();
		$output = isset($data['output']) && is_array($data['output']) ? $data['output'] : array();
		foreach($output as $item){
			$itype = isset($item['type']) ? $item['type'] : '';
			if($itype === 'message'){
				$content = isset($item['content']) && is_array($item['content']) ? $item['content'] : array();
				foreach($content as $c){
					$ctype = isset($c['type']) ? $c['type'] : '';
					if(($ctype === 'output_text' || $ctype === 'text') && !empty($c['text'])){
						$text .= $c['text'];
					}elseif($ctype === 'reasoning' && !empty($c['text'])){
						$reasoning .= $c['text'];
					}
				}
			}elseif($itype === 'function_call'){
				$args = @json_decode(isset($item['arguments']) ? $item['arguments'] : '{}', true);
				if($args === null) $args = array();
				$tool_calls[] = array(
					'id' => isset($item['call_id']) ? $item['call_id'] : (isset($item['id']) ? $item['id'] : ''),
					'name' => isset($item['name']) ? $item['name'] : '',
					'input' => $args
				);
			}elseif($itype === 'reasoning'){
				$summary = isset($item['summary']) && is_array($item['summary']) ? $item['summary'] : array();
				foreach($summary as $s){
					if(!empty($s['text'])) $reasoning .= $s['text'];
				}
			}
		}

		$accumulated = array('text' => $text, 'reasoning' => $reasoning, 'tool_calls' => $tool_calls, 'usage' => isset($data['usage']) ? $data['usage'] : array());
		if(!empty($data['usage']['input_tokens_details']['cached_tokens'])){
			$accumulated['usage']['cached_tokens'] = $data['usage']['input_tokens_details']['cached_tokens'];
		}
		return array(
			'content' => $text,
			'tool_calls' => $tool_calls,
			'usage' => $accumulated['usage'],
			'parts' => $this->build_parts($accumulated)
		);
	}

	private function openai_responses_stream($messages, $tools, $on_event, $options){
		$url = rtrim($this->base_url, '/') . '/responses';
		$headers = $this->openai_headers();
		$payload = $this->openai_responses_build_payload($messages, $tools, $options);
		$payload['stream'] = true;

		$accumulated = array('text' => '', 'reasoning' => '', 'tool_calls' => array(), 'usage' => array());
		$pending_tool_calls = array();
		$sse_buffer = '';

		$this->curl_post_stream($url, $headers, $payload, function($chunk) use ($on_event, &$accumulated, &$pending_tool_calls, &$sse_buffer){
			$sse_buffer .= $chunk;
			while(($pos = strpos($sse_buffer, "\n")) !== false){
				$line = substr($sse_buffer, 0, $pos);
				$sse_buffer = substr($sse_buffer, $pos + 1);
				$line = trim($line);
				if(strpos($line, 'data: ') !== 0) continue;
				$data_str = substr($line, 6);
				if(trim($data_str) === '[DONE]') return;
				$event = @json_decode($data_str, true);
				if(!$event) continue;

				$etype = isset($event['type']) ? $event['type'] : '';

				if($etype === 'response.output_text.delta'){
					if(!empty($event['delta'])){
						$accumulated['text'] .= $event['delta'];
						$on_event(array('type' => 'text_delta', 'text' => $event['delta']));
					}
				}elseif($etype === 'response.reasoning_summary_text.delta'){
					if(!empty($event['delta'])){
						$accumulated['reasoning'] .= $event['delta'];
						$on_event(array('type' => 'reasoning_delta', 'text' => $event['delta']));
					}
				}elseif($etype === 'response.output_item.added'){
					$item = isset($event['item']) ? $event['item'] : array();
					if((isset($item['type']) ? $item['type'] : '') === 'function_call'){
						$idx = count($pending_tool_calls);
						$pending_tool_calls[$idx] = array(
							'call_id' => isset($item['call_id']) ? $item['call_id'] : '',
							'id' => isset($item['id']) ? $item['id'] : '',
							'name' => isset($item['name']) ? $item['name'] : '',
							'arguments' => ''
						);
					}
				}elseif($etype === 'response.function_call_arguments.delta'){
					if(!empty($event['delta']) && !empty($pending_tool_calls)){
						$last_idx = count($pending_tool_calls) - 1;
						if($last_idx >= 0){
							$pending_tool_calls[$last_idx]['arguments'] .= $event['delta'];
						}
					}
				}elseif($etype === 'response.output_item.done'){
					$item = isset($event['item']) ? $event['item'] : array();
					if((isset($item['type']) ? $item['type'] : '') === 'function_call'){
						$args = @json_decode(isset($item['arguments']) ? $item['arguments'] : '{}', true);
						if($args === null) $args = array();
						$tc = array(
							'id' => isset($item['call_id']) ? $item['call_id'] : (isset($item['id']) ? $item['id'] : ''),
							'name' => isset($item['name']) ? $item['name'] : '',
							'input' => $args
						);
						$accumulated['tool_calls'][] = $tc;
						$on_event(array('type' => 'tool_call', 'id' => $tc['id'], 'name' => $tc['name'], 'input' => $args));
					}
				}elseif($etype === 'response.completed' || $etype === 'response.done'){
					if(!empty($event['response']['usage'])){
						$accumulated['usage'] = $event['response']['usage'];
						if(!empty($event['response']['usage']['input_tokens_details']['cached_tokens'])){
							$accumulated['usage']['cached_tokens'] = $event['response']['usage']['input_tokens_details']['cached_tokens'];
						}
					}elseif(!empty($event['usage'])){
						$accumulated['usage'] = $event['usage'];
					}
				}elseif($etype === 'error' || $etype === 'response.failed'){
					$err_msg = isset($event['error']['message']) ? $event['error']['message'] : (isset($event['message']) ? $event['message'] : __('Unknown error'));
					$on_event(array('type' => 'error', 'message' => $err_msg));
				}
			}
		}, isset($options['timeout']) ? $options['timeout'] : 120);

		$on_event(array('type' => 'done', 'usage' => $accumulated['usage']));

		return array(
			'content' => $accumulated['text'],
			'tool_calls' => $accumulated['tool_calls'],
			'usage' => $accumulated['usage'],
			'parts' => $this->build_parts($accumulated)
		);
	}

	private function anthropic_chat($messages, $tools, $options){
		$url = 'https://api.anthropic.com/v1/messages';
		$headers = array(
			'Content-Type: application/json',
			'x-api-key: ' . $this->api_key,
			'anthropic-version: 2023-06-01',
			'Accept: application/json'
		);
		$payload = $this->anthropic_build_payload($messages, $tools, $options);
		$payload['stream'] = false;

		$response = $this->curl_post($url, $headers, $payload, isset($options['timeout']) ? $options['timeout'] : 120);
		if(!empty($response['error'])) return $response;
		return $this->anthropic_parse_response($response['body']);
	}

	private function anthropic_stream($messages, $tools, $on_event, $options){
		$url = 'https://api.anthropic.com/v1/messages';
		$headers = array(
			'Content-Type: application/json',
			'x-api-key: ' . $this->api_key,
			'anthropic-version: 2023-06-01',
			'Accept: text/event-stream'
		);
		$payload = $this->anthropic_build_payload($messages, $tools, $options);
		$payload['stream'] = true;

		$accumulated = array('text' => '', 'reasoning' => '', 'tool_calls' => array(), 'usage' => array());
		$current_tool = null;
		$current_tool_args = '';
		$sse_buffer = '';

		$this->curl_post_stream($url, $headers, $payload, function($chunk) use ($on_event, &$accumulated, &$current_tool, &$current_tool_args, &$sse_buffer){
			$sse_buffer .= $chunk;
			while(($pos = strpos($sse_buffer, "\n")) !== false){
				$line = substr($sse_buffer, 0, $pos);
				$sse_buffer = substr($sse_buffer, $pos + 1);
				$line = trim($line);
				if(strpos($line, 'data: ') !== 0) continue;
				$data_str = substr($line, 6);
				$event = @json_decode($data_str, true);
				if(!$event) continue;

				$type = isset($event['type']) ? $event['type'] : '';

				if($type === 'content_block_delta'){
					$delta = isset($event['delta']) ? $event['delta'] : array();
					$delta_type = isset($delta['type']) ? $delta['type'] : '';
					if($delta_type === 'text_delta' && !empty($delta['text'])){
						$accumulated['text'] .= $delta['text'];
						$on_event(array('type' => 'text_delta', 'text' => $delta['text']));
					}elseif($delta_type === 'thinking_delta' && !empty($delta['thinking'])){
						$accumulated['reasoning'] .= $delta['thinking'];
						$on_event(array('type' => 'reasoning_delta', 'text' => $delta['thinking']));
					}elseif($delta_type === 'input_json_delta' && !empty($delta['partial_json'])){
						$current_tool_args .= $delta['partial_json'];
					}
				}elseif($type === 'content_block_start'){
					$block = isset($event['content_block']) ? $event['content_block'] : array();
					if((isset($block['type']) ? $block['type'] : '') === 'tool_use'){
						$current_tool = array(
							'id' => isset($block['id']) ? $block['id'] : '',
							'name' => isset($block['name']) ? $block['name'] : '',
							'input' => array()
						);
						$current_tool_args = '';
					}
				}elseif($type === 'content_block_stop'){
					if($current_tool){
						$args = @json_decode($current_tool_args, true);
						if($args === null) $args = array();
						$current_tool['input'] = $args;
						$accumulated['tool_calls'][] = $current_tool;
						$on_event(array('type' => 'tool_call', 'id' => $current_tool['id'], 'name' => $current_tool['name'], 'input' => $args));
						$current_tool = null;
						$current_tool_args = '';
					}
				}elseif($type === 'message_delta'){
					if(!empty($event['usage'])){
						$accumulated['usage']['output_tokens'] = isset($event['usage']['output_tokens']) ? $event['usage']['output_tokens'] : 0;
					}
				}elseif($type === 'message_start'){
					if(!empty($event['message']['usage'])){
						$accumulated['usage']['input_tokens'] = isset($event['message']['usage']['input_tokens']) ? $event['message']['usage']['input_tokens'] : 0;
						if(!empty($event['message']['usage']['cache_creation_input_tokens'])) $accumulated['usage']['cache_creation_input_tokens'] = $event['message']['usage']['cache_creation_input_tokens'];
						if(!empty($event['message']['usage']['cache_read_input_tokens'])) $accumulated['usage']['cache_read_input_tokens'] = $event['message']['usage']['cache_read_input_tokens'];
					}
				}elseif($type === 'message_stop'){
					// done
				}elseif($type === 'error'){
					$err_msg = isset($event['error']['message']) ? $event['error']['message'] : __('Unknown error');
					$on_event(array('type' => 'error', 'message' => $err_msg));
				}
			}
		}, isset($options['timeout']) ? $options['timeout'] : 120);

		$on_event(array('type' => 'done', 'usage' => $accumulated['usage']));

		return array(
			'content' => $accumulated['text'],
			'tool_calls' => $accumulated['tool_calls'],
			'usage' => $accumulated['usage'],
			'parts' => $this->build_parts($accumulated)
		);
	}

	private function anthropic_build_payload($messages, $tools, $options){
		$system_msg = '';
		$filtered = array();
		foreach($messages as $msg){
			if($msg['role'] === 'system'){
				$system_msg .= $msg['content'] . "\n";
			}else{
				$filtered[] = $msg;
			}
		}

		$payload = array(
			'model' => $this->model,
			'messages' => $filtered,
			'max_tokens' => intval(isset($options['max_tokens']) ? $options['max_tokens'] : 8192)
		);

		// Anthropic prompt caching: cache system prompt with 1h TTL
		if(!empty($system_msg)){
			$payload['system'] = array(
				array(
					'type' => 'text',
					'text' => trim($system_msg),
					'cache_control' => array('type' => 'ephemeral', 'ttl' => '1h')
				)
			);
		}

		// Cache tools (last tool gets cache breakpoint)
		if(!empty($tools)){
			$tool_arr = array();
			$tool_count = count($tools);
			foreach($tools as $i => $t){
				$tool = array(
					'name' => $t['name'],
					'description' => $t['description'],
					'input_schema' => $t['parameters']
				);
			// Cache the last tool definition
			if($i === $tool_count - 1){
				$tool['cache_control'] = array('type' => 'ephemeral', 'ttl' => '1h');
			}
				$tool_arr[] = $tool;
			}
			$payload['tools'] = $tool_arr;
		}

		if(!empty($options['temperature'])){
			$payload['temperature'] = floatval($options['temperature']);
		}

		// Cache last 2 messages for conversation continuity
		$msg_count = count($filtered);
		if($msg_count >= 2){
			for($i = max(0, $msg_count - 2); $i < $msg_count; $i++){
				if(!isset($filtered[$i]['content'])) continue;
				// Convert string content to array format with cache_control
				if(is_string($filtered[$i]['content'])){
					$filtered[$i]['content'] = array(
						array(
							'type' => 'text',
							'text' => $filtered[$i]['content'],
							'cache_control' => array('type' => 'ephemeral')
						)
					);
				}elseif(is_array($filtered[$i]['content'])){
					// Add cache_control to last content block
					$last_block = end($filtered[$i]['content']);
					if($last_block){
						$last_block['cache_control'] = array('type' => 'ephemeral');
						$filtered[$i]['content'][count($filtered[$i]['content']) - 1] = $last_block;
					}
				}
			}
			$payload['messages'] = $filtered;
		}

		return $payload;
	}

	private function anthropic_parse_response($body){
		$data = @json_decode($body, true);
		if(!$data) return array('error' => __('Invalid JSON response'));
		if(!empty($data['error'])) return array('error' => isset($data['error']['message']) ? $data['error']['message'] : json_encode($data['error']));

		$text = '';
		$tool_calls = array();
		foreach(isset($data['content']) ? $data['content'] : array() as $block){
			if((isset($block['type']) ? $block['type'] : '') === 'text'){
				$text .= isset($block['text']) ? $block['text'] : '';
			}elseif((isset($block['type']) ? $block['type'] : '') === 'tool_use'){
				$tool_calls[] = array(
					'id' => isset($block['id']) ? $block['id'] : '',
					'name' => isset($block['name']) ? $block['name'] : '',
					'input' => isset($block['input']) ? $block['input'] : array()
				);
			}
		}

		$accumulated = array('text' => $text, 'tool_calls' => $tool_calls, 'usage' => isset($data['usage']) ? $data['usage'] : array());
		return array(
			'content' => $text,
			'tool_calls' => $tool_calls,
			'usage' => isset($data['usage']) ? $data['usage'] : array(),
			'parts' => $this->build_parts($accumulated)
		);
	}

	private function google_chat($messages, $tools, $options){
		$key = $this->api_key;
		$url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $this->model . ':generateContent?key=' . $key;
		$headers = array('Content-Type: application/json');
		$payload = $this->google_build_payload($messages, $tools, $options);

		$response = $this->curl_post($url, $headers, $payload, isset($options['timeout']) ? $options['timeout'] : 120);
		if(!empty($response['error'])) return $response;
		return $this->google_parse_response($response['body']);
	}

	private function google_stream($messages, $tools, $on_event, $options){
		$key = $this->api_key;
		$url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $this->model . ':streamGenerateContent?alt=sse&key=' . $key;
		$headers = array('Content-Type: application/json');
		$payload = $this->google_build_payload($messages, $tools, $options);

		$accumulated = array('text' => '', 'tool_calls' => array(), 'usage' => array());
		$sse_buffer = '';

		$this->curl_post_stream($url, $headers, $payload, function($chunk) use ($on_event, &$accumulated, &$sse_buffer){
			$sse_buffer .= $chunk;
			while(($pos = strpos($sse_buffer, "\n")) !== false){
				$line = substr($sse_buffer, 0, $pos);
				$sse_buffer = substr($sse_buffer, $pos + 1);
				$line = trim($line);
				if(strpos($line, 'data: ') !== 0) continue;
				$data_str = substr($line, 6);
				$data = @json_decode($data_str, true);
				if(!$data) continue;

				if(!empty($data['candidates'][0]['content']['parts'])){
					foreach($data['candidates'][0]['content']['parts'] as $part){
						if(!empty($part['text'])){
							$accumulated['text'] .= $part['text'];
							$on_event(array('type' => 'text_delta', 'text' => $part['text']));
						}
						if(!empty($part['functionCall'])){
							$tc = array(
								'id' => 'gcall_'.count($accumulated['tool_calls']),
								'name' => isset($part['functionCall']['name']) ? $part['functionCall']['name'] : '',
								'input' => isset($part['functionCall']['args']) ? $part['functionCall']['args'] : array()
							);
							$accumulated['tool_calls'][] = $tc;
							$on_event(array('type' => 'tool_call', 'id' => $tc['id'], 'name' => $tc['name'], 'input' => $tc['input']));
						}
					}
				}
				if(!empty($data['usageMetadata'])){
					$accumulated['usage'] = $data['usageMetadata'];
					// Normalize Gemini cached tokens
					if(!empty($data['usageMetadata']['cachedContentTokenCount'])){
						$accumulated['usage']['cached_tokens'] = $data['usageMetadata']['cachedContentTokenCount'];
					}
				}
			}
		}, isset($options['timeout']) ? $options['timeout'] : 120);

		$on_event(array('type' => 'done', 'usage' => $accumulated['usage']));
		return array(
			'content' => $accumulated['text'],
			'tool_calls' => $accumulated['tool_calls'],
			'usage' => $accumulated['usage'],
			'parts' => $this->build_parts($accumulated)
		);
	}

	private function google_build_payload($messages, $tools, $options){
		$contents = array();
		$system_instruction = '';

		// Build a tool_call_id => tool_name map from preceding assistant
		// tool_calls. Google removed the 'function' role; tool results must
		// now be sent as a user turn with a functionResponse part whose
		// name must match the prior functionCall name, so we resolve it
		// from the assistant tool_use that produced this result.
		$tool_name_by_id = array();
		foreach($messages as $m){
			if(!isset($m['role']) || $m['role'] !== 'assistant') continue;
			if(empty($m['tool_calls'])) continue;
			foreach($m['tool_calls'] as $tc){
				$tid = isset($tc['id']) ? $tc['id'] : '';
				$tname = isset($tc['function']['name']) ? $tc['function']['name'] : (isset($tc['name']) ? $tc['name'] : '');
				if($tid !== '' && $tname !== '') $tool_name_by_id[$tid] = $tname;
			}
		}

		foreach($messages as $msg){
			$role = isset($msg['role']) ? $msg['role'] : 'user';
			if($role === 'system'){
				$system_instruction .= $msg['content'] . "\n";
				continue;
			}
			if($role === 'tool_result' || $role === 'tool'){
				if(!empty($msg['content'])){
					$tcid = isset($msg['tool_call_id']) ? $msg['tool_call_id'] : '';
					$tool_name = isset($tool_name_by_id[$tcid]) ? $tool_name_by_id[$tcid] : '';
					if($tool_name === ''){
						// Google requires a non-empty functionResponse.name; use a
						// stable synthetic name when the id can't be matched.
						$tool_name = 'tool_' . abs(crc32((string)$tcid ?: 'unknown'));
					}
					$result_text = is_string($msg['content']) ? $msg['content'] : json_encode($msg['content']);
					$contents[] = array(
						'role' => 'user',
						'parts' => array(array(
							'functionResponse' => array(
								'name' => $tool_name,
								'response' => array(
									'name' => $tool_name,
									'content' => $result_text
								)
							)
						))
					);
				}
				continue;
			}
			if($role === 'assistant'){
				// Emit functionCall parts for any tool_calls so Gemini sees the
				// call that subsequent functionResponse parts answer.
				$g_parts = array();
				$content = isset($msg['content']) ? $msg['content'] : '';
				if(is_array($content)) $content = json_encode($content);
				if($content !== '' && $content !== null){
					$g_parts[] = array('text' => $content);
				}
				if(!empty($msg['tool_calls'])){
					foreach($msg['tool_calls'] as $tc){
						$tname = isset($tc['function']['name']) ? $tc['function']['name'] : (isset($tc['name']) ? $tc['name'] : '');
						$args_str = isset($tc['function']['arguments']) ? $tc['function']['arguments'] : '{}';
						$args_arr = json_decode($args_str, true);
						if(!is_array($args_arr)) $args_arr = array();
						$g_parts[] = array('functionCall' => array('name' => $tname, 'args' => $args_arr));
					}
				}
				if(!empty($g_parts)){
					$contents[] = array('role' => 'model', 'parts' => $g_parts);
				}
				continue;
			}
			// Plain user message
			$content = isset($msg['content']) ? $msg['content'] : '';
			if(is_array($content)) $content = json_encode($content);
			$contents[] = array(
				'role' => 'user',
				'parts' => array(array('text' => $content))
			);
		}

		$payload = array('contents' => $contents);

		if(!empty($system_instruction)){
			$payload['systemInstruction'] = array('parts' => array(array('text' => trim($system_instruction))));
		}

		$gen_config = array();
		if(!empty($options['temperature'])) $gen_config['temperature'] = floatval($options['temperature']);
		if(!empty($options['max_tokens'])) $gen_config['maxOutputTokens'] = intval($options['max_tokens']);
		if(!empty($gen_config)) $payload['generationConfig'] = $gen_config;

		if(!empty($tools)){
			$payload['tools'] = array(array('functionDeclarations' => array_map(function($t){
				return array(
					'name' => $t['name'],
					'description' => $t['description'],
					'parameters' => $t['parameters']
				);
			}, $tools)));
		}

		return $payload;
	}

	private function google_parse_response($body){
		$data = @json_decode($body, true);
		if(!$data) return array('error' => __('Invalid JSON response'));
		if(!empty($data['error'])) return array('error' => isset($data['error']['message']) ? $data['error']['message'] : json_encode($data['error']));

		$text = '';
		$tool_calls = array();
		$parts = isset($data['candidates'][0]['content']['parts']) ? $data['candidates'][0]['content']['parts'] : array();
		foreach($parts as $part){
			if(!empty($part['text'])) $text .= $part['text'];
			if(!empty($part['functionCall'])){
				$tool_calls[] = array(
					'id' => 'gcall_'.count($tool_calls),
					'name' => isset($part['functionCall']['name']) ? $part['functionCall']['name'] : '',
					'input' => isset($part['functionCall']['args']) ? $part['functionCall']['args'] : array()
				);
			}
		}

		$usage = isset($data['usageMetadata']) ? $data['usageMetadata'] : array();
		// Normalize Gemini cached tokens
		if(!empty($data['usageMetadata']['cachedContentTokenCount'])){
			$usage['cached_tokens'] = $data['usageMetadata']['cachedContentTokenCount'];
		}
		$accumulated = array('text' => $text, 'tool_calls' => $tool_calls, 'usage' => $usage);
		return array(
			'content' => $text,
			'tool_calls' => $tool_calls,
			'usage' => $usage,
			'parts' => $this->build_parts($accumulated)
		);
	}

	private function azure_chat($messages, $tools, $options){
		$config = isset($this->provider_config) ? $this->provider_config : array();
		$resource = isset($config['resource']) ? $config['resource'] : '{resource}';
		$deployment = isset($config['deployment']) ? $config['deployment'] : $this->model;
		$url = "https://{$resource}.openai.azure.com/openai/deployments/{$deployment}/chat/completions?api-version=2024-02-01";
		$headers = array(
			'Content-Type: application/json',
			'api-key: ' . $this->api_key
		);
		$payload = $this->openai_build_payload($messages, $tools, $options);
		$payload['stream'] = false;

		$response = $this->curl_post($url, $headers, $payload, isset($options['timeout']) ? $options['timeout'] : 120);
		if(!empty($response['error'])) return $response;
		return $this->openai_parse_response($response['body']);
	}

	private function azure_stream($messages, $tools, $on_event, $options){
		$config = isset($this->provider_config) ? $this->provider_config : array();
		$resource = isset($config['resource']) ? $config['resource'] : '{resource}';
		$deployment = isset($config['deployment']) ? $config['deployment'] : $this->model;
		$url = "https://{$resource}.openai.azure.com/openai/deployments/{$deployment}/chat/completions?api-version=2024-02-01";
		$headers = array(
			'Content-Type: application/json',
			'api-key: ' . $this->api_key
		);
		$payload = $this->openai_build_payload($messages, $tools, $options);
		$payload['stream'] = true;

		return $this->openai_stream_impl($url, $headers, $payload, $on_event, $options);
	}

	private function openai_stream_impl($url, $headers, $payload, $on_event, $options){
		$accumulated = array('text' => '', 'reasoning' => '', 'tool_calls' => array(), 'usage' => array());
		$tool_call_buffers = array();
		$sse_buffer = '';

		$this->curl_post_stream($url, $headers, $payload, function($chunk) use ($on_event, &$accumulated, &$tool_call_buffers, &$sse_buffer){
			$sse_buffer .= $chunk;
			while(($pos = strpos($sse_buffer, "\n")) !== false){
				$line = substr($sse_buffer, 0, $pos);
				$sse_buffer = substr($sse_buffer, $pos + 1);
				$line = trim($line);
				if(strpos($line, 'data: ') !== 0) continue;
				$data_str = substr($line, 6);
				if(trim($data_str) === '[DONE]') return;
				$data = @json_decode($data_str, true);
				if(!$data) continue;
				if(!empty($data['choices'][0])){
					$choice = $data['choices'][0];
					$delta = isset($choice['delta']) ? $choice['delta'] : array();
					if(!empty($delta['content'])){
						$accumulated['text'] .= $delta['content'];
						$on_event(array('type' => 'text_delta', 'text' => $delta['content']));
					}
					if(!empty($delta['reasoning_content'])){
						$accumulated['reasoning'] .= $delta['reasoning_content'];
						$on_event(array('type' => 'reasoning_delta', 'text' => $delta['reasoning_content']));
					}
					if(!empty($delta['tool_calls'])){
						foreach($delta['tool_calls'] as $tc){
							$idx = isset($tc['index']) ? $tc['index'] : 0;
							if(!isset($tool_call_buffers[$idx])){
								$tool_call_buffers[$idx] = array('id' => '', 'name' => '', 'arguments' => '');
							}
							if(!empty($tc['id'])) $tool_call_buffers[$idx]['id'] = $tc['id'];
							if(!empty($tc['function']['name'])) $tool_call_buffers[$idx]['name'] = $tc['function']['name'];
							if(!empty($tc['function']['arguments'])) $tool_call_buffers[$idx]['arguments'] .= $tc['function']['arguments'];
						}
					}
				}
				if(!empty($data['usage'])) $accumulated['usage'] = $data['usage'];
			}
		}, isset($options['timeout']) ? $options['timeout'] : 120);

		if(!empty($tool_call_buffers)){
			foreach($tool_call_buffers as $i => $tc){
				$args = @json_decode($tc['arguments'], true);
				if($args === null) $args = array();
				$tool_call = array('id' => $tc['id'] ? $tc['id'] : 'call_'.$i, 'name' => $tc['name'], 'input' => $args);
				$accumulated['tool_calls'][] = $tool_call;
				$on_event(array('type' => 'tool_call', 'id' => $tool_call['id'], 'name' => $tool_call['name'], 'input' => $args));
			}
		}

		$on_event(array('type' => 'done', 'usage' => $accumulated['usage']));
		return array(
			'content' => $accumulated['text'],
			'tool_calls' => $accumulated['tool_calls'],
			'usage' => $accumulated['usage'],
			'parts' => $this->build_parts($accumulated)
		);
	}

	private function build_parts($accumulated){
		$parts = array();
		if(!empty($accumulated['reasoning'])){
			$parts[] = array('type' => 'reasoning', 'text' => $accumulated['reasoning']);
		}
		if(!empty($accumulated['text'])){
			$parts[] = array('type' => 'text', 'text' => $accumulated['text']);
		}
		foreach($accumulated['tool_calls'] as $tc){
			$parts[] = array('type' => 'tool_use', 'id' => $tc['id'], 'name' => $tc['name'], 'input' => $tc['input']);
		}
		return $parts;
	}

	private function curl_post($url, $headers, $payload, $timeout = 120){
		$ch = curl_init($url);
		curl_setopt_array($ch, array(
			CURLOPT_HTTPHEADER => $headers,
			CURLOPT_POST => true,
			CURLOPT_POSTFIELDS => json_encode($payload),
			CURLOPT_RETURNTRANSFER => true,
			CURLOPT_TIMEOUT => $timeout,
			CURLOPT_SSL_VERIFYPEER => false,
			CURLOPT_CONNECTTIMEOUT => 10
		));

		$body = curl_exec($ch);
		$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
		$curl_error = curl_error($ch);
		curl_close($ch);

		if($body === false){
			return array('error' => __('cURL error: $0', array($curl_error)));
		}

		if($http_code >= 400){
			$err_data = @json_decode($body, true);
			if(!empty($err_data['error']['message'])){
				return array('error' => $err_data['error']['message']);
			}
			if(!empty($err_data['error'])){
				$e = $err_data['error'];
				$msg = is_string($e) ? $e : (isset($e['message']) ? $e['message'] : json_encode($e));
				return array('error' => $msg);
			}
			return array('error' => __('HTTP $0: ', array($http_code)) . mb_substr($body, 0, 500));
		}

		return array('body' => $body);
	}

	private function curl_post_stream($url, $headers, $payload, $on_chunk, $timeout = 120){
		$ch = curl_init($url);
		$aborted = false;
		$http_code = 0;
		$error_body = '';
		$in_error = false;
		curl_setopt_array($ch, array(
			CURLOPT_HTTPHEADER => $headers,
			CURLOPT_POST => true,
			CURLOPT_POSTFIELDS => json_encode($payload),
			CURLOPT_RETURNTRANSFER => false,
			CURLOPT_TIMEOUT => $timeout,
			CURLOPT_CONNECTTIMEOUT => 10,
			CURLOPT_LOW_SPEED_TIME => 60,
			CURLOPT_LOW_SPEED_LIMIT => 10,
			CURLOPT_SSL_VERIFYPEER => false,
			CURLOPT_WRITEFUNCTION => function($ch, $data) use ($on_chunk, &$aborted, &$http_code, &$error_body, &$in_error){
				if($this->should_abort()){
					$aborted = true;
					return -1;
				}
				$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
				if($code > 0){
					$http_code = $code;
					if($code >= 400){
						$in_error = true;
					}
				}
				if($in_error){
					$error_body .= $data;
					return strlen($data);
				}
				$on_chunk($data);
				return strlen($data);
			}
		));
		curl_exec($ch);
		$curl_error = curl_error($ch);
		if($http_code === 0 && $curl_error !== ''){
			curl_close($ch);
			throw new \RuntimeException(__('cURL error: $0', array($curl_error)));
		}
		curl_close($ch);
		if($aborted){
			throw new \RuntimeException(__('Generation aborted by user'));
		}
		if($http_code >= 400){
			$err_data = @json_decode($error_body, true);
			$msg = '';
			if(!empty($err_data['error']['message'])){
				$msg = is_array($err_data['error']['message']) ? json_encode($err_data['error']['message']) : $err_data['error']['message'];
			}elseif(!empty($err_data['error'])){
				$e = $err_data['error'];
				$msg = is_string($e) ? $e : (isset($e['message']) ? $e['message'] : json_encode($e));
			}elseif(!empty($err_data['message'])){
				$msg = $err_data['message'];
			}
			if($msg === ''){
				$msg = 'HTTP ' . $http_code . ': ' . mb_substr($error_body, 0, 500);
			}
			throw new \RuntimeException($msg);
		}
	}
}
