<?php
namespace EasyChat\Library\AiProvider;

use EasyChat\Library\Ai\GeminiStreamParser;
use EasyChat\Library\Http\ExternalHttpClient;
use EasyChat\Library\SafeRuntime;

 
class Gemini implements AiProvider {

    private $token = null;
    private $messages = [];  
    private $system_instruction = null;  
    private $host = 'https://generativelanguage.googleapis.com/v1beta/models/';
    private $model = 'gemini-3.5-flash';  
    private $default_role = 'user';
    private $connector = null;
    private $chunks = 1;
    private $n = 1;
    private $repetition_penalty = 0;  
    private $top_p = 1.0;
    private $temperature = 1.0;
    private $keySeeker = null;
    private $tools = [];  
    private $lastCurlError = '';
    private $lastHttpStatus = 0;
    private $proxy = [
        'ip' => null,
        'port' => null,
        'login' => null,
        'password' => null,
    ];

    public function __construct($token, $data = []) {

        $this->token = $token;
        $this->messages = [];
        $this->model = isset($data['model']) ? $data['model'] : 'gemini-3.5-flash';
        $this->chunks = isset($data['chunks']) ? $data['chunks'] : 1;

        if (isset($data['temperature']))
            $this->temperature = (double) $data['temperature'];

        if (isset($data['role']))
            $this->default_role = $data['role'];

        if (isset($data['host']))
            $this->host = $data['host'];

        if (isset($data['top_p']))
            $this->top_p = (double) $data['top_p'];

        if (isset($data['repetition_penalty']))
            $this->repetition_penalty = $data['repetition_penalty'];

        if (isset($data['proxy']))
            $this->proxy = $data['proxy'];

        return $this;
    }

    public function setChunks($chunks = 1) {
        $this->chunks = $chunks;
    }

    public function setModel($model) {
        $this->model = $model;
    }

    public function setContext($context) {
        if (empty($context)) {
            $this->system_instruction = null;
            return null;
        }

        $this->system_instruction = $context;
    }

    public function getHistory() {
        return $this->messages;
    }
    public function clearHistory() {
        $this->messages = [];
        $this->system_instruction = null;
    }
    public function setHistory($messages) {
        $this->messages = $messages;
    }

    public function getToken() {
        $api_key   = preg_split('/[\n\r]+/', $this->token);
        $nTokens   = count($api_key);
        $keySeeker = $this->keySeeker !== null ? $this->keySeeker : rand(0, $nTokens - 1);
        return trim((string) $api_key[$keySeeker]);
    }

    public function setKeySeeker($keySeeker) {
        $this->keySeeker = (int) $keySeeker;
    }

     
     
     

    






    public function setTools(array $toolsConfig) {
        $this->tools = [];
        foreach ($toolsConfig as $tool) {
            if (is_string($tool)) {
                $config = $this->toolConfig($tool);
                if (is_array($config)) {
                    $this->tools[] = $config;
                }
            } elseif (is_array($tool) && !empty($tool['name'])) {
                $this->tools[] = $tool;
            }
        }
        return $this;
    }

    public function getTools() {
        return $this->tools;
    }

    private function toolConfig($tool) {
        switch ((string)$tool) {
            case 'search_pages':
                return require __DIR__ . DIRECTORY_SEPARATOR . 'tools' . DIRECTORY_SEPARATOR . 'search_pages.php';
            case 'search_products':
                return require __DIR__ . DIRECTORY_SEPARATOR . 'tools' . DIRECTORY_SEPARATOR . 'search_products.php';
            default:
                return null;
        }
    }

    



    private function buildToolsPayload() {
        if (empty($this->tools)) {
            return [];
        }
        $declarations = [];
        foreach ($this->tools as $tool) {
            $declarations[] = [
                'name'        => $tool['name'],
                'description' => isset($tool['description']) ? $tool['description'] : '',
                'parameters'  => $this->buildJsonSchemaParameters(isset($tool['parameters']) ? $tool['parameters'] : []),
            ];
        }
        return [
            ['function_declarations' => $declarations],
        ];
    }

    



    private function buildJsonSchemaParameters(array $parameters) {
        $properties = [];
        $required   = [];

        foreach ($parameters as $name => $param) {
            $prop = ['type' => isset($param['type']) ? $param['type'] : 'string'];

            if (isset($param['description'])) $prop['description'] = $param['description'];
            if (isset($param['enum']))        $prop['enum']        = $param['enum'];
            if (isset($param['items']))       $prop['items']       = $param['items'];
            if (isset($param['properties']))  $prop['properties']  = $param['properties'];

            $properties[$name] = $prop;

            if (!empty($param['required'])) {
                $required[] = $name;
            }
        }

        $schema = [
            'type'       => 'object',
            'properties' => $properties,
        ];
        if (!empty($required)) {
            $schema['required'] = $required;
        }
        return $schema;
    }

     
     
     

    


    private function buildCurlOptions($url, $body, array $headers, array $extraOptions = []) {
        $options = [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $body,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT        => 35,
            CURLOPT_NOSIGNAL       => 1,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_SSL_VERIFYPEER => true,
        ];

        if ($this->proxy['ip'] && $this->proxy['port']) {
            $proxy_ip = strpos($this->proxy['ip'], ':') !== false
                ? '[' . $this->proxy['ip'] . ']'
                : $this->proxy['ip'];
            $options[CURLOPT_PROXY]     = $proxy_ip;
            $options[CURLOPT_PROXYPORT] = $this->proxy['port'];
            if ($this->proxy['login'] && $this->proxy['password']) {
                $options[CURLOPT_PROXYUSERPWD] = $this->proxy['login'] . ':' . $this->proxy['password'];
            }
        }

        foreach ($extraOptions as $key => $value) {
            $options[$key] = $value;
        }

        return $options;
    }

    


    private function execCurl(array $options) {
        $response = (new ExternalHttpClient())->requestCurlOptions($options, 'ai', false);
        $this->lastCurlError = (string)($response['curl_error'] ?? '');
        $this->lastHttpStatus = (int)($response['http_code'] ?? 0);
        $this->connector = null;
        return $response['raw'] ?? false;
    }

    public function getLastTransportError() {
        return $this->lastCurlError;
    }

    public function getLastHttpStatus() {
        return $this->lastHttpStatus;
    }

     
     
     

    public function get($url, $headers, $data) {
        $options = $this->buildCurlOptions($url, $data, $headers);
        if (empty($data)) {
            unset($options[CURLOPT_POSTFIELDS]);
        }
        $result = $this->execCurl($options);
        return json_decode($result, true);
    }

     
     
     

    public function getEmbedding($text, $embeddingModel = 'gemini-embedding-001', $dimensions = 768, array $options = []) {

        if (empty($text)) {
            return null;
        }

        $apiAddress = $this->host . $embeddingModel . ':embedContent?key=' . $this->getToken();

        $data = [
            "model"   => "models/" . $embeddingModel,
            "content" => [
                "parts" => [
                    ["text" => $text]
                ]
            ],
            "outputDimensionality" => (int) $dimensions,
        ];

        $postData = json_encode($data);
        $headers  = ["Content-Type: application/json"];

        $extra_options = [];
        if (isset($options['timeout']) && (int)$options['timeout'] > 0) {
            $extra_options[CURLOPT_TIMEOUT] = max(1, min(120, (int)$options['timeout']));
        }
        if (isset($options['connect_timeout']) && (int)$options['connect_timeout'] > 0) {
            $extra_options[CURLOPT_CONNECTTIMEOUT] = max(1, min(20, (int)$options['connect_timeout']));
        }
        if (isset($options['first_byte_timeout']) && (int)$options['first_byte_timeout'] > 0) {
            $extra_options[CURLOPT_LOW_SPEED_LIMIT] = 1;
            $extra_options[CURLOPT_LOW_SPEED_TIME] = max(5, min(60, (int)$options['first_byte_timeout']));
        }

        $result = $this->execCurl(
            $this->buildCurlOptions($apiAddress, $postData, $headers, $extra_options)
        );

        $response = json_decode($result, true);

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

        throw new \Exception('gpt answer:' . $result);

        return null;
    }

    public function getEmbeddings(array $texts, $embeddingModel = 'gemini-embedding-001', $dimensions = 768, array $options = []) {
        $embeddings = [];
        foreach ($texts as $text) {
            $embeddings[] = $this->getEmbedding($text, $embeddingModel, $dimensions, $options);
        }
        return $embeddings;
    }

    public function supportsKnowledgeGrounding() {
        return false;
    }

    public function supportsGroundingSources() {
        return false;
    }

    public function getLastGrounding() {
        return null;
    }

    public function getLastProviderError() {
        return null;
    }

     
     
     

    public function query($prompt, $options = []) {

        $temperature = (double) isset($options['temperature']) && (double) $options['temperature'] ? $options['temperature'] : $this->temperature;

        list($apiAddres, $data) = $this->buildChatPayload($prompt, $temperature, $options);

        $postData = json_encode($data);
        $headers  = ["Content-Type: application/json"];

        $result = $this->execCurl(
            $this->buildCurlOptions($apiAddres, $postData, $headers)
        );

        $response = json_decode($result, true);

        if (isset($response["candidates"][0]["content"]["parts"][0])) {
            $text = $response["candidates"][0]["content"]["parts"][0]["text"];
            $response["choices"][0]["text"] = preg_replace("/[\*#]+/", "", $text);
            $result = json_encode($response);
        }

        return $result;
    }

    public function stream($messages, $options = []) {
        if (is_array($messages)) {
            $this->setHistory(array_unique(array_merge($this->messages, $messages), SORT_REGULAR));
            $prompt = '';
        } else {
            $prompt = (string)$messages;
        }

        $raw = $this->streamChat($prompt, $options, function ($delta) {
            echo 'data: ' . json_encode([
                'choices' => [[
                    'text'  => $delta,
                    'delta' => ['content' => $delta],
                ]],
            ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n\n";
            SafeRuntime::call(function () {
                ob_flush();
                return true;
            }, false);
            flush();
        });

        echo "data: [DONE]\n\n";
        return [
            'response_text' => $this->extractTextFromResponse($raw),
            'response_log'  => $raw,
        ];
    }

    public function streamChat($prompt, array $options = [], ?callable $on_delta = null) {
        $temperature = isset($options['temperature']) && (double) $options['temperature'] ? $options['temperature'] : $this->temperature;
        list($apiAddres, $data) = $this->buildChatPayload($prompt, $temperature, $options);
        $apiAddres = str_replace(':generateContent?', ':streamGenerateContent?', $apiAddres);

        $postData = json_encode($data);
        $parser = new GeminiStreamParser();

        $this->execCurl($this->buildCurlOptions(
            $apiAddres,
            $postData,
            ["Content-Type: application/json"],
            [
                CURLOPT_TIMEOUT        => isset($options['stream_timeout']) ? max(10, min(180, (int)$options['stream_timeout'])) : 120,
                CURLOPT_CONNECTTIMEOUT => isset($options['connect_timeout']) ? max(1, min(20, (int)$options['connect_timeout'])) : 10,
                CURLOPT_LOW_SPEED_LIMIT => 1,
                CURLOPT_LOW_SPEED_TIME  => isset($options['first_byte_timeout']) ? max(10, min(60, (int)$options['first_byte_timeout'])) : 35,
                CURLOPT_WRITEFUNCTION  => function ($ch, $chunk) use ($parser, $on_delta) {
                    $parser->feed($chunk, $on_delta);
                     
                     
                     
                    return strlen($chunk);
                },
            ]
        ));

        $parser->finish($on_delta);
        return $parser->toGeminiResponse();
    }

    public function getCurlOptions($request, $options = []) {

        if (isset($options['keySeeker']))
            $this->setKeySeeker($options['keySeeker']);

        $temperature = floatval(isset($options['temperature']) && (double) $options['temperature'] ? $options['temperature'] : $this->temperature);

        list($apiAddres, $data) = $this->buildChatPayload($request, $temperature, $options);

        $postData = json_encode($data);
        $headers  = ["Content-Type: application/json"];

        return $this->buildCurlOptions($apiAddres, $postData, $headers);
    }

    







    private function buildChatPayload($prompt, $temperature, array $options) {
        $apiAddres = $this->host . $this->model . ':generateContent?key=' . $this->getToken();
        $messages  = $this->messages;

        if ($prompt !== '' && $prompt !== null) {
            $messages[] = [
                'role'    => $this->default_role,
                'content' => $prompt,
            ];
        }

        $messages = $this->mapMessagesToGemini($messages);

        $data = [
            "contents" => $messages,
            "generationConfig" => [
                "temperature" => (double) min(1.99, $temperature),
                "topP"        => $this->top_p,
            ],
        ];

        if ($this->system_instruction) {
            $data['systemInstruction'] = [
                'parts' => [
                    ['text' => $this->system_instruction]
                ]
            ];
        }

        if (isset($options['max_tokens']) && (int) $options['max_tokens']) {
            $data["generationConfig"]["maxOutputTokens"] = (int) $options['max_tokens'];
        }

        $tools_payload = $this->buildToolsPayload();
        if (!empty($tools_payload)) {
            $data['tools'] = $tools_payload;
        }

        return [$apiAddres, $data];
    }

    






    private function mapMessagesToGemini(array $messages) {
        $result = [];
        foreach ($messages as $message) {
            if (isset($message['parts'])) {
                 
                $result[] = [
                    'role'  => isset($message['role']) ? $message['role'] : 'user',
                    'parts' => $message['parts'],
                ];
                continue;
            }
            $result[] = [
                'role'  => in_array($message['role'], ['user', 'system']) ? 'user' : 'model',
                'parts' => [['text' => isset($message['content']) ? (string)$message['content'] : '']],
            ];
        }
        return $result;
    }

     
     
     

    





    public function parseToolCalls($response) {
        $resp = is_array($response) ? $response : json_decode((string)$response, true);
        $parts = isset($resp['candidates'][0]['content']['parts'])
            ? $resp['candidates'][0]['content']['parts']
            : [];

        $result = [];
        foreach ($parts as $part) {
            if (!isset($part['functionCall'])) {
                continue;
            }
            $fc   = $part['functionCall'];
            $args = isset($fc['args']) && is_array($fc['args']) ? $fc['args'] : [];
            $result[] = [
                'id'        => '',
                'name'      => isset($fc['name']) ? $fc['name'] : '',
                'arguments' => $args,
            ];
        }
        return $result;
    }

    



    public function appendAssistantToolCalls($response) {
        $resp  = is_array($response) ? $response : json_decode((string)$response, true);
        $parts = isset($resp['candidates'][0]['content']['parts'])
            ? $resp['candidates'][0]['content']['parts']
            : [];

        $function_call_parts = [];
        foreach ($parts as $part) {
            if (isset($part['functionCall'])) {
                $function_call_parts[] = $part;
            }
        }
        if (empty($function_call_parts)) {
            return;
        }

        $this->messages[] = [
            'role'  => 'model',
            'parts' => $function_call_parts,
        ];
    }

    



    public function appendToolResult($call_id, $name, $result) {
         
         
        if (!is_array($result)) {
            $response_value = ['result' => $result];
        } else {
            $response_value = $result;
        }

        $this->messages[] = [
            'role'  => 'user',
            'parts' => [
                [
                    'functionResponse' => [
                        'name'     => (string)$name,
                        'response' => $response_value,
                    ],
                ],
            ],
        ];
    }

    


    public function runWithTools($prompt, $executor, array $options = [], $max_iterations = 5) {
        $response = $this->query($prompt, $options);
        $iter     = 0;

        while ($iter++ < $max_iterations) {
            $calls = $this->parseToolCalls($response);
            if (empty($calls)) {
                return $response;  
            }

            $this->appendAssistantToolCalls($response);

            foreach ($calls as $call) {
                if ($executor && method_exists($executor, 'execute')) {
                    $result = $executor->has($call['name'])
                        ? $executor->execute($call['name'], $call['arguments'])
                        : ['error' => 'Unknown tool: ' . $call['name']];
                } else {
                    $result = ['error' => 'No tool executor configured'];
                }
                $this->appendToolResult($call['id'], $call['name'], $result);
            }

             
            $response = $this->query('', $options);
        }

        return $this->toolCallLoopFallbackResponse();
    }

    public function runWithToolsStream($prompt, $executor, array $options = [], ?callable $on_delta = null, $max_iterations = 5) {
        $response = $this->streamChat($prompt, $options, null);
        $iter = 0;

        while ($iter++ < $max_iterations) {
            $calls = $this->parseToolCalls($response);
            if (empty($calls)) {
                $text = $this->extractTextFromResponse($response);
                if ($text !== '' && $on_delta) {
                    $on_delta($text);
                }
                return $response;
            }

            $this->appendAssistantToolCalls($response);

            foreach ($calls as $call) {
                if ($executor && method_exists($executor, 'execute')) {
                    $result = $executor->has($call['name'])
                        ? $executor->execute($call['name'], $call['arguments'])
                        : ['error' => 'Unknown tool: ' . $call['name']];
                } else {
                    $result = ['error' => 'No tool executor configured'];
                }
                $this->appendToolResult($call['id'], $call['name'], $result);
            }

            $response = $this->streamChat('', $options, null);
        }

        return $this->toolCallLoopFallbackResponse();
    }

    private function extractTextFromResponse($response) {
        $json = is_array($response) ? $response : json_decode((string)$response, true);
        if (!is_array($json)) {
            return '';
        }
        if (isset($json['choices'][0]['text'])) {
            return trim((string)$json['choices'][0]['text']);
        }
        if (isset($json['candidates'][0]['content']['parts'][0]['text'])) {
            return trim((string)$json['candidates'][0]['content']['parts'][0]['text']);
        }
        return '';
    }

    private function toolCallLoopFallbackResponse() {
        return json_encode([
            'choices' => [[
                'text' => '',
                'finish_reason' => 'tool_calls_exhausted',
            ]],
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    public function parallel() {
    }
}
