<?php
namespace boru\ai\Provider\Gemini;

use boru\ai\Config\Config;
use boru\ai\Config\ConfigResolver;
use boru\ai\Http\GuzzleHttpClient;
use boru\ai\Http\HttpClientInterface;
use boru\ai\Http\HttpRequest;

class GeminiClient
{
    /** @var string */
    protected $apiKey;

    /** @var string */
    protected $baseUrl = 'https://generativelanguage.googleapis.com/v1beta';

    /** @var int */
    protected $timeout = 60;

    /** @var bool */
    protected $rawHttpDebug = false;

    /** @var int */
    protected $streamReadSize = 1024;

    /** @var HttpClientInterface */
    protected $httpClient;

    public function __construct($apiKey = null, array $options = array(), HttpClientInterface $httpClient = null)
    {
        if ($options instanceof Config) {
            $options = $options->provider('gemini');
        }

        if ($httpClient === null && isset($options['http_client']) && $options['http_client'] instanceof HttpClientInterface) {
            $httpClient = $options['http_client'];
        }

        if (isset($options['api_key']) && $options['api_key']) {
            $apiKey = $options['api_key'];
        }
        if (!$apiKey) {
            $apiKey = getenv('GEMINI_API_KEY');
        }
        $this->apiKey = (string)$apiKey;

        if (isset($options['base_url']) && $options['base_url']) {
            $this->baseUrl = rtrim($options['base_url'], '/');
        }

        $resolver = new ConfigResolver(new Config(array(
            'runtime' => array(
                'timeout' => isset($options['timeout']) ? $options['timeout'] : null,
                'raw_http_debug' => isset($options['raw_http_debug']) ? $options['raw_http_debug'] : null,
                'stream_read_size' => isset($options['stream_read_size']) ? $options['stream_read_size'] : null,
            ),
            'providers' => array(
                'gemini' => $options,
            ),
        )));

        $this->timeout = (int)$resolver->resolveProviderSetting('gemini', 'timeout', 60, 'runtime');
        $this->rawHttpDebug = $resolver->resolveProviderSetting('gemini', 'raw_http_debug', false, 'runtime') ? true : false;
        $this->streamReadSize = (int)$resolver->resolveProviderSetting('gemini', 'stream_read_size', 1024, 'runtime');
        if ($this->streamReadSize <= 0) {
            $this->streamReadSize = 1024;
        }

        $this->httpClient = $httpClient ? $httpClient : new GuzzleHttpClient();
    }

    public function apiKey($value = null)
    {
        if ($value !== null) {
            $this->apiKey = (string)$value;
            return $this;
        }
        return $this->apiKey;
    }

    public function httpClient(HttpClientInterface $httpClient = null)
    {
        if ($httpClient !== null) {
            $this->httpClient = $httpClient;
            return $this;
        }
        return $this->httpClient;
    }

    public function rawHttpDebug($value = null)
    {
        if ($value !== null) {
            $this->rawHttpDebug = $value ? true : false;
            return $this;
        }
        return $this->rawHttpDebug;
    }

    public function streamReadSize($value = null)
    {
        if ($value !== null) {
            $this->streamReadSize = (int)$value > 0 ? (int)$value : 1024;
            return $this;
        }
        return $this->streamReadSize;
    }

    public function generateContent($model, array $payload)
    {
        $path = 'models/' . rawurlencode((string)$model) . ':generateContent';
        $request = $this->makeJsonRequest($path, $payload);
        $this->debugLine('Gemini request: POST ' . $request->url());
        $response = $this->httpClient()->send($request);
        $this->debugLine('Gemini response status: ' . $response->statusCode());
        if ($this->rawHttpDebug()) {
            $this->debugLine('Gemini response body: ' . $response->body());
        }
        return $this->decodeJsonResponse($response);
    }

    public function embedContent($model, array $payload)
    {
        $path = 'models/' . rawurlencode((string)$model) . ':embedContent';
        $request = $this->makeJsonRequest($path, $payload);
        $response = $this->httpClient()->send($request);
        return $this->decodeJsonResponse($response);
    }

    public function batchEmbedContents($model, array $payload)
    {
        $path = 'models/' . rawurlencode((string)$model) . ':batchEmbedContents';
        $request = $this->makeJsonRequest($path, $payload);
        $response = $this->httpClient()->send($request);
        return $this->decodeJsonResponse($response);
    }

    public function streamGenerateContent($model, array $payload, $chunkCallback)
    {
        if (!is_callable($chunkCallback)) {
            throw new \InvalidArgumentException('Stream callback must be callable');
        }

        $path = 'models/' . rawurlencode((string)$model) . ':streamGenerateContent?alt=sse';
        $request = $this->makeJsonRequest($path, $payload);
        $request->header('Accept', 'text/event-stream');
        $request->option('stream_read_size', $this->streamReadSize());
        $request->streamCallback(function ($chunk, $response = null) use ($chunkCallback) {
            $this->debugLine('Gemini SSE chunk: ' . str_replace(array("\r", "\n"), array('\\r', '\\n'), (string)$chunk));
            call_user_func($chunkCallback, $chunk, $response);
        });

        $this->debugLine('Gemini stream request: POST ' . $request->url());
        $response = $this->httpClient()->send($request);
        $this->debugLine('Gemini stream response status: ' . $response->statusCode());
        if ($this->rawHttpDebug()) {
            $this->debugLine('Gemini stream response body: ' . $response->body());
        }
        return $response;
    }

    public function uploadFile($path, array $options = array())
    {
        if (!is_string($path) || $path === '' || !file_exists($path)) {
            throw new \InvalidArgumentException('Gemini uploadFile requires an existing file path');
        }

        $mimeType = isset($options['mime_type']) && $options['mime_type'] ? $options['mime_type'] : 'application/octet-stream';
        $displayName = isset($options['display_name']) && $options['display_name'] ? $options['display_name'] : basename($path);
        $numBytes = filesize($path);
        if ($numBytes === false) {
            throw new \RuntimeException('Unable to determine Gemini upload file size: ' . $path);
        }

        $startRequest = new HttpRequest();
        $startRequest->method('POST')
            ->url($this->uploadBaseUrl() . '/files')
            ->headers(array(
                'x-goog-api-key' => $this->apiKey(),
                'X-Goog-Upload-Protocol' => 'resumable',
                'X-Goog-Upload-Command' => 'start',
                'X-Goog-Upload-Header-Content-Length' => (string)$numBytes,
                'X-Goog-Upload-Header-Content-Type' => $mimeType,
                'Content-Type' => 'application/json',
            ))
            ->json(array(
                'file' => array(
                    'display_name' => $displayName,
                ),
            ))
            ->options(array(
                'timeout' => $this->timeout,
            ));

        $this->debugLine('Gemini upload start request: POST ' . $startRequest->url());
        $startResponse = $this->httpClient()->send($startRequest);
        $uploadUrl = $this->headerValue($startResponse->headers(), 'x-goog-upload-url');
        if (!$startResponse->isSuccess() || !$uploadUrl) {
            $decoded = $startResponse->json();
            $message = 'Gemini file upload start failed';
            if (is_array($decoded) && isset($decoded['error']['message'])) {
                $message = $decoded['error']['message'];
            }
            throw new \RuntimeException($message, $startResponse->statusCode());
        }

        $contents = file_get_contents($path);
        if ($contents === false) {
            throw new \RuntimeException('Unable to read Gemini upload file: ' . $path);
        }

        $uploadRequest = new HttpRequest();
        $uploadRequest->method('POST')
            ->url($uploadUrl)
            ->headers(array(
                'Content-Length' => (string)$numBytes,
                'X-Goog-Upload-Offset' => '0',
                'X-Goog-Upload-Command' => 'upload, finalize',
            ))
            ->body($contents)
            ->options(array(
                'timeout' => $this->timeout,
            ));

        $this->debugLine('Gemini upload finalize request: POST ' . $uploadRequest->url());
        $uploadResponse = $this->httpClient()->send($uploadRequest);
        $this->debugLine('Gemini upload finalize response status: ' . $uploadResponse->statusCode());
        if ($this->rawHttpDebug()) {
            $this->debugLine('Gemini upload finalize response body: ' . $uploadResponse->body());
        }

        return $this->decodeJsonResponse($uploadResponse);
    }

    protected function decodeJsonResponse($response)
    {
        $decoded = $response->json();

        if (!$response->isSuccess()) {
            $message = 'Gemini request failed';
            if (is_array($decoded) && isset($decoded['error']['message'])) {
                $message = $decoded['error']['message'];
            }
            throw new \RuntimeException($message, $response->statusCode());
        }

        if (!is_array($decoded)) {
            throw new \RuntimeException('Invalid JSON returned from Gemini');
        }

        return $decoded;
    }

    protected function makeJsonRequest($path, array $payload)
    {
        $request = $this->makeBaseRequest($path);
        $request->header('Content-Type', 'application/json');
        $request->json($payload);
        return $request;
    }

    protected function makeBaseRequest($path)
    {
        if ($this->apiKey() === '') {
            throw new \RuntimeException('Gemini API key not configured');
        }

        $separator = strpos($path, '?') === false ? '?' : '&';

        $request = new HttpRequest();
        $request->method('POST')
            ->url($this->baseUrl . '/' . ltrim($path, '/') . $separator . 'key=' . rawurlencode($this->apiKey()))
            ->options(array(
                'timeout' => $this->timeout,
            ));

        return $request;
    }

    protected function uploadBaseUrl()
    {
        return preg_replace('#/v1beta$#', '/upload/v1beta', $this->baseUrl);
    }

    protected function headerValue(array $headers, $name)
    {
        foreach ($headers as $headerName => $values) {
            if (strtolower((string)$headerName) !== strtolower((string)$name)) {
                continue;
            }
            if (is_array($values) && isset($values[0])) {
                return $values[0];
            }
            if (is_string($values)) {
                return $values;
            }
        }
        return null;
    }

    protected function debugLine($message)
    {
        if (!$this->rawHttpDebug()) {
            return;
        }

        echo '[gemini debug] ' . $message . PHP_EOL;
    }
}
