<?php
namespace Nibiru\Module\Ai\Plugins;

/**
 * Ollama HTTP transport. The lowest layer of the AI stack — every other
 * plugin (Chat, Embed, RAG, Agent) ultimately speaks through this.
 *
 * Works against any Ollama-compatible endpoint (localhost:11434, a
 * remote Ollama via reverse proxy, vLLM with the /api/chat shim,
 * LiteLLM proxies, etc.).
 */
class Ollama
{
    protected string $baseUrl;
    protected int $timeout;
    protected int $retries;

    public function __construct(\stdClass $cfg)
    {
        $this->baseUrl = rtrim((string) ($cfg->ollama_base_url ?? 'http://localhost:11434'), '/');
        $this->timeout = (int) ($cfg->ollama_timeout ?? 90);
        $this->retries = (int) ($cfg->ollama_retries ?? 1);
    }

    /**
     * POST /api/chat — synchronous, non-streaming.
     *
     * @param string $model   e.g. "nibiru-coder:1.0"
     * @param array  $messages [{role: 'user'|'assistant'|'system', content: '...'}]
     * @param array  $options {temperature, num_predict, top_p, ...}
     * @return array decoded JSON response
     */
    public function chat(string $model, array $messages, array $options = []): array
    {
        return $this->post('/api/chat', [
            'model'    => $model,
            'messages' => $messages,
            'stream'   => false,
            'options'  => $options,
        ]);
    }

    /**
     * POST /api/embeddings — single input.
     *
     * @return array{embedding: float[]}
     */
    public function embed(string $model, string $prompt): array
    {
        return $this->post('/api/embeddings', [
            'model'  => $model,
            'prompt' => $prompt,
        ]);
    }

    /**
     * POST /api/generate — single-prompt completion (no chat structure).
     */
    public function generate(string $model, string $prompt, array $options = []): array
    {
        return $this->post('/api/generate', [
            'model'   => $model,
            'prompt'  => $prompt,
            'stream'  => false,
            'options' => $options,
        ]);
    }

    /**
     * GET /api/tags — list available models.
     */
    public function listModels(): array
    {
        return $this->get('/api/tags');
    }

    /**
     * GET /api/ps — list currently-loaded models.
     */
    public function listLoaded(): array
    {
        return $this->get('/api/ps');
    }

    /**
     * POST /api/pull — pull a model onto the Ollama server.
     */
    public function pull(string $name): array
    {
        return $this->post('/api/pull', ['name' => $name, 'stream' => false]);
    }

    /**
     * POST /api/create — register a custom model from a Modelfile.
     */
    public function create(string $name, string $modelfile): array
    {
        return $this->post('/api/create', [
            'name'      => $name,
            'modelfile' => $modelfile,
            'stream'    => false,
        ]);
    }

    // -----------------------------------------------------------------------
    // HTTP helpers
    // -----------------------------------------------------------------------

    protected function post(string $path, array $body): array
    {
        $url = $this->baseUrl . $path;
        $payload = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return $this->withRetries(function () use ($url, $payload) {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_POST           => true,
                CURLOPT_POSTFIELDS     => $payload,
                CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT        => $this->timeout,
                CURLOPT_CONNECTTIMEOUT => 10,
            ]);
            $body = curl_exec($ch);
            $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $err  = curl_error($ch);
            curl_close($ch);
            if ($body === false) {
                throw new \RuntimeException("Ollama transport error: $err");
            }
            if ($code >= 400) {
                $snip = substr((string) $body, 0, 240);
                throw new \RuntimeException("Ollama HTTP $code: $snip");
            }
            $decoded = json_decode((string) $body, true);
            if (!is_array($decoded)) {
                throw new \RuntimeException('Ollama returned non-JSON body.');
            }
            return $decoded;
        });
    }

    protected function get(string $path): array
    {
        $ch = curl_init($this->baseUrl . $path);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 15,
            CURLOPT_CONNECTTIMEOUT => 5,
        ]);
        $body = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($body === false || $code >= 400) {
            throw new \RuntimeException("Ollama GET $path failed (HTTP $code)");
        }
        return json_decode((string) $body, true) ?? [];
    }

    protected function withRetries(callable $fn)
    {
        $last = null;
        for ($i = 0; $i <= $this->retries; $i++) {
            try {
                return $fn();
            } catch (\RuntimeException $e) {
                $last = $e;
                if ($i < $this->retries) usleep(250_000 * (1 << $i)); // 250ms, 500ms, ...
            }
        }
        throw $last ?? new \RuntimeException('Ollama call failed.');
    }
}
