<?php
$hubUrl = "http://omninode.ru/api/";
$nodeToken = "w5IPv92uesO2cP5aqDWtvebsTYa2Nxse";
$ollamaUrl = "172.17.0.1:11434";


/**
 * Класс для взаимодействия с API Ollama.
 */
class OllamaAPI {
    private string $baseUrl; // IP + порт
    private string $modelName;
    private array $chatHistory = []; // Хранилище истории диалога
    private array $registeredFunctions = []; // Хранилище зарегистрированных функций
    private string $ollamaApiUrl;
    private string $defaultSystemMessage = 'Ты полезный ИИ-ассистент. Используй доступные инструменты (функции), если они предоставлены, для получения нужной информации. Любишь отвечать кратко и поделу. Работаешь ты через сайт OmniNode.Ru';

	public $current_task_id = 0;

    /**
     * Конструктор класса.
     *
     * @param array $config Ассоциативный массив с конфигурацией:
     *                      'url' => '127.0.0.1:11434' (IP и порт Ollama)
     *                      'model' => 'qwen2.5-coder:7b' (Имя модели)
     */
    public function __construct(array $config = []){
        $this->baseUrl = $config["url"] ?? "127.0.0.1:11434";
        $this->modelName = $config["model"] ?? "qwen2.5-coder:7b"; // Значение по умолчанию

        $this->ollamaApiUrl = 'http://' . $this->baseUrl . '/api/chat';

        // Инициализируем историю с системным сообщением
        $this->chatHistory[] = ['role' => 'system', 'content' => $this->defaultSystemMessage];
    }
	
	public function SetTaskID($task){
		$this->current_task_id = $task;
	}

    /**
     * Заменяет всю текущую историю диалога на указанный массив.
     *
     * @param array $history Массив сообщений для установки в качестве истории.
     *                       Формат: [['role' => 'user', 'content' => '...'], ...]
     */
    public function addHistory($history){
        // Очищаем историю и добавляем системное сообщение, чтобы оно всегда было первым
        $this->chatHistory = [['role' => 'system', 'content' => $this->chatHistory[0]['content'] ?? $this->defaultSystemMessage]];
		if (!is_array($history)){
			return false;
		}
        foreach ($history as $message) {
            if (is_array($message) && isset($message['role']) && isset($message['content'])) {
                $this->chatHistory[] = $message;
            }
        }
    }

    /**
     * Регистрирует функцию, которую модель сможет вызывать, используя единую глобальную точку входа.
     *
     * @param string $funcName Название функции, которое модель будет использовать.
     * @param array $parameters Описание параметров функции (структура Ollama).
     * @param array $required Список обязательных параметров.
     */
    public function addFunction(string $funcName, $descFunction = "Функция у которой нет описания.", array $parameters = [], array $required = []){
        // Мы сохраняем только метаданные функции. Реальный вызов будет идти через ollama_app_func
        $id = uniqid('func_'); // Внутренний ID, не используется моделью
        $this->registeredFunctions[$id] = [
            'name' => $funcName,
            'description' => $descFunction,
            'parameters' => (object)[
                'type' => 'object',
                'properties' => (object)$this->formatFunctionParameters($parameters),
                'required' => $required,
            ],
        ];
    }

    /**
     * Форматирует параметры функций для соответствия структуре Ollama.
     *
     * @param array $params Массив описаний параметров.
     * @return array Отформатированный массив свойств.
     */
    private function formatFunctionParameters(array $params){
        $formatted = [];
        foreach ($params as $param) {
            $formatted[$param['name']] = [
                'type' => $param['type'],
                'description' => $param['description'] ?? '',
            ];
        }
        return $formatted;
    }

    /**
     * Ищет зарегистрированную функцию по имени и вызывает глобальную функцию-обработчик.
     *
     * @param array $toolCall Информация о вызове функции из ответа Ollama.
     * @return array Результат выполнения функции в формате для Ollama (tool_call_id, content).
     */
    private function executeFunction(array $toolCall){
        $functionName = $toolCall['function']['name'];
        $arguments = json_decode($toolCall['function']['arguments'], true);
        $id = $toolCall['id'];

        // Проверяем, зарегистрирована ли такая функция (ее имя)
        $isFunctionRegistered = false;
        foreach ($this->registeredFunctions as $regFunc){
            if ($regFunc['name'] === $functionName) {
                $isFunctionRegistered = true;
                break;
            }
        }

        if (!$isFunctionRegistered) {
            return ['tool_call_id' => $id, 'content' => "Error: Function '{$functionName}' is not registered."];
        }

        // Вызываем единую глобальную функцию-обработчик
        if (function_exists('ollama_app_func')){
            try {
                // Передаем имя функции и ее аргументы
                $result = ollama_app_func($functionName, $arguments ?? [], $this->current_task_id);
                return ['tool_call_id' => $id, 'content' => (string)$result];
            } catch (Throwable $e) {
                return ['tool_call_id' => $id, 'content' => "Error executing global handler for {$functionName}: " . $e->getMessage()];
            }
        } else {
            return ['tool_call_id' => $id, 'content' => "Error: Global handler 'ollama_app_func' does not exist."];
        }
    }

    /**
     * Отправляет запрос к API Ollama.
     *
     * @param array $messages Сообщения для отправки.
     * @param array $tools Инструменты для отправки.
     * @return array|false Ответ от API или false в случае ошибки cURL.
     */
    private function makeApiRequest(array $messages, array $tools = []){
        $headers = ['Content-Type: application/json'];

        $requestData = [
            'model' => $this->modelName,
            'messages' => $messages,
            'stream' => false,
        ];

        if (!empty($tools)) {
            $requestData['tools'] = $tools;
        }

        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $this->ollamaApiUrl);
        curl_setopt($curl, CURLOPT_POST, true);
        curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($requestData));
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);

        $response = curl_exec($curl);

        if (curl_errno($curl)){
            error_log("cURL Error: " . curl_error($curl));
            curl_close($curl);
            return false;
        }

        curl_close($curl);
        return json_decode($response, true);
    }

    /**
     * Обрабатывает ответ от модели, включая вызовы функций.
     *
     * @param array $responseData Ответ от API Ollama.
     * @return string|false Обработанный ответ или false при ошибке.
     */
    private function processResponse(array $responseData){
        if ($responseData === false) {
            return "Error: ResponseData"; // Ошибка cURL
        }

        if (isset($responseData['error'])) {
            return "Error from Ollama: " . $responseData['error'];
        }

        if (isset($responseData['message'])) {
            $message = $responseData['message'];

            // 1. Модель вернула 'tool_calls' (идеальный сценарий)
            if (isset($message['tool_calls']) && is_array($message['tool_calls']) && !empty($message['tool_calls'])) {
                $this->chatHistory[] = $message; // Сообщение ассистента с tool_calls

                $toolResults = [];
                foreach ($message['tool_calls'] as $toolCall) {
                    $result = $this->executeFunction($toolCall);
                    $toolResults[] = $result;
                    $this->chatHistory[] = ['role' => 'tool', 'tool_call_id' => $result['tool_call_id'], 'content' => $result['content']];
                }

                $finalResponseData = $this->makeApiRequest($this->chatHistory, array_values($this->registeredFunctions));
                if ($finalResponseData === false) return false;
                if (isset($finalResponseData['error'])) return "Error after function execution: " . $finalResponseData['error'];
                if (isset($finalResponseData['message']['content'])) {
                    $this->chatHistory[] = $finalResponseData['message'];
                    return $finalResponseData['message']['content'];
                } else {
                    return "Error: No final content after function execution.";
                }
            }
            // 2. Модель вернула JSON в 'content' (ваш случай)
            elseif (isset($message['content']) && substr($message['content'], 0, 1) === '{') {
                $decodedContent = json_decode($message['content'], true);

                if (is_array($decodedContent) && isset($decodedContent['name']) && isset($decodedContent['arguments'])) {
                    $this->chatHistory[] = $message; // Сообщение ассистента с JSON-контентом

                    $fakeToolCallId = uniqid('fake_tool_call_');
                    $fakeToolCall = [
                        'id' => $fakeToolCallId,
                        'type' => 'function',
                        'function' => [
                            'name' => $decodedContent['name'],
                            'arguments' => json_encode($decodedContent['arguments'] ?? []),
                        ],
                    ];

                    $result = $this->executeFunction($fakeToolCall);
                    $this->chatHistory[] = ['role' => 'tool', 'tool_call_id' => $result['tool_call_id'], 'content' => $result['content']];

                    $finalResponseData = $this->makeApiRequest($this->chatHistory, array_values($this->registeredFunctions));
                    if ($finalResponseData === false) return false;
                    if (isset($finalResponseData['error'])) return "Error after handling JSON content: " . $finalResponseData['error'];
                    if (isset($finalResponseData['message']['content'])) {
                        $this->chatHistory[] = $finalResponseData['message'];
                        return $finalResponseData['message']['content'];
                    } else {
                        return "Error: No final content after handling JSON content.";
                    }
                } else {
                    // Это JSON, но не вызов функции. Обрабатываем как обычный текст.
                    $this->chatHistory[] = $message;
                    return $message['content'];
                }
            }
            // 3. Модель вернула обычный текстовый ответ (не JSON)
            elseif (isset($message['content'])) {
                $this->chatHistory[] = $message;
                return $message['content'];
            }
            // 4. Неожиданный формат ответа
            else {
                return "Unexpected response format: No tool_calls and no identifiable content.";
            }
        }

        return "Unexpected response format: No message field in response.";
    }

    /**
     * Задает вопрос модели, сохраняя контекст диалога.
     *
     * @param array $history Массив сообщений для установки в качестве истории перед запросом.
     * @param string $message Текст вопроса.
     * @return string|false Ответ модели или false при ошибке.
     */
    public function askWithHistoryAndFunctions($history, string $message){
        if (empty(trim($message))) {
            return false; // Пустой запрос
        }
        
        // Устанавливаем историю
        $this->addHistory($history);
        
        // Добавляем новый пользовательский запрос в установленную историю
        $this->chatHistory[] = ['role' => 'user', 'content' => trim($message)];
        
        // Отправляем текущую историю и все зарегистрированные инструменты
        $response = $this->makeApiRequest($this->chatHistory, array_values($this->registeredFunctions));
        
        return $this->processResponse($response);
    }

    /**
     * Задает вопрос модели без сохранения контекста и без использования функций.
     *
     * @param string $message Текст вопроса.
     * @return string|false Ответ модели или false при ошибке.
     */
    public function ask(string $message){
        if (empty(trim($message))) {
            return false; // Пустой запрос
        }
        // Для одиночного запроса отправляем только сообщение пользователя и никаких инструментов
        $messagesToSend = [['role' => 'user', 'content' => trim($message)]];
        $response = $this->makeApiRequest($messagesToSend, []);

        if ($response === false) {
            return false;
        }
        if (isset($response['message']['content'])) {
            return $response['message']['content'];
        }
        if (isset($response['error'])) {
            return "Error: " . $response['error'];
        }
        return "Unexpected response format for single ask.";
    }

    /**
     * Получает текущую историю чата.
     *
     * @return array Текущая история чата.
     */
    public function getChatHistory(){
        return $this->chatHistory;
    }

    /**
     * Устанавливает новое системное сообщение.
     *
     * @param string $message Новое системное сообщение.
     */
    public function setSystemMessage(string $message){
        if (!empty($this->chatHistory)) {
            $this->chatHistory[0]['content'] = $message;
        } else {
            $this->chatHistory[] = ['role' => 'system', 'content' => $message];
        }
        $this->defaultSystemMessage = $message; // Обновляем и дефолтное
    }
}


function post($url, $data, $isJson = false) { // $isJson здесь управляет Content-Type
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    
    if ($isJson) {
        // Если отправляем JSON (например, запрос к Ollama)
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
    } else {
        // Если отправляем как form-urlencoded (для API Хаба)
        // PHP автоматически сериализует массив $data в строку 'key1=value1&key2=value2'
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        // Content-Type будет application/x-www-form-urlencoded по умолчанию
    }
    
    $res = curl_exec($ch);
    if ($res === false) {
        echo "cURL Error: " . curl_error($ch) . PHP_EOL;
    }
    curl_close($ch);
    return $res;
}

function ollama_app_func($funcName, $params = [], $currentTaskId = 0) {
	global $hubUrl, $nodeToken;
    $hubToolResponse = post($hubUrl . "call_tool/", [
        'node_token' => $nodeToken,
        'act' => 'call_tool',
        'task_id' => $currentTaskId,
        'func_name' => $funcName,
        'params' => json_encode($params) // Передаем параметры как JSON строку
    ]);
            
    $toolResult = json_decode($hubToolResponse, true);
    return isset($toolResult["tool_result"])?$toolResult["tool_result"]:"Error: Unknown function '{$funcName}'";
}

echo "Worker started...\n";
$warn_code = 0;
while (true) {
    // 1. Спрашиваем у Хаба задачу
    $taskData = post($hubUrl . "get_task/", ['node_token' => $nodeToken, 'act' => 'get_task']);
    $task = json_decode($taskData, true);

    if (isset($task['task_id'])) {
		$task['history'] = json_decode(base64_decode($task['history']), true);
		$warn_code = 0;
        echo "New task found: #{$task['task_id']} [{$task['model']}]\n";

        $ollamaClient = new OllamaAPI([
			'url' => $ollamaUrl,       // IP и порт вашего Ollama
			'model' => $task['model'],  // Укажите вашу модель
		]);
		$ollamaClient->setSystemMessage($task['system']);
		$ollamaClient->SetTaskID($task['task_id']);
		if (is_array($task["tools"])){
			foreach($task["tools"] as $_tool){
				$ollamaClient->addFunction($_tool[0],$_tool[1],$_tool[2],$_tool[3]);
			}
		}
		if (!is_array($task['history'])){
			$task['history'] = [];
		}
		$answer = $ollamaClient->askWithHistoryAndFunctions($task['history'],$task['prompt']);
		
		
	
        // 3. Отправляем финальный результат на Хаб
        post($hubUrl . "report_result/", [
            'node_token' => $nodeToken,
            'act' => 'report_result',
            'task_id' => $task['task_id'],
            'response' => ($answer !== false ? $answer : "Ошибка запроса.")
        ]);
		$currentHistory = $ollamaClient->getChatHistory();
		print_r($currentHistory);
		echo "\n";
        echo "Task #{$task['task_id']} completed.\n\n\n";

	} elseif ($task == NULL){
		if ($warn_code != 1){
			echo "\n\n\t!!!WARNING!!!\nERROR GET TASK TO HUB!!!\n";
			$warn_code = 1;
		}
    } else {
        if ($warn_code != 2){
			echo "No new task... Sleep...\n";
			$warn_code = 2;
		}
        sleep(2);
    }
}
?>