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

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 OpenAIClient
{
    /** @var string */
    protected $apiKey;

    /** @var string */
    protected $baseUrl = 'https://api.openai.com/v1';

    /** @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('openai');
        }

        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('OPENAI_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(
                'openai' => $options,
            ),
        )));

        $this->timeout = (int)$resolver->resolveProviderSetting('openai', 'timeout', 60, 'runtime');
        $this->rawHttpDebug = $resolver->resolveProviderSetting('openai', 'raw_http_debug', false, 'runtime') ? true : false;
        $this->streamReadSize = (int)$resolver->resolveProviderSetting('openai', '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 post($path, array $payload)
    {
        $request = $this->makeJsonRequest($path, $payload);
        $this->debugLine('OpenAI request: POST ' . $request->url());
        $response = $this->httpClient()->send($request);
        $this->debugLine('OpenAI response status: ' . $response->statusCode());
        if ($this->rawHttpDebug()) {
            $this->debugLine('OpenAI response body: ' . $response->body());
        }
        return $this->decodeJsonResponse($response);
    }

    public function embeddings(array $payload)
    {
        return $this->post('embeddings', $payload);
    }

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

        $payload['stream'] = true;
        $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('OpenAI SSE chunk: ' . str_replace(array("\r", "\n"), array('\\r', '\\n'), (string)$chunk));
            call_user_func($chunkCallback, $chunk, $response);
        });

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

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

        $filename = isset($options['filename']) && $options['filename'] ? $options['filename'] : basename($path);
        $multipart = array(
            array(
                'name' => 'purpose',
                'contents' => (string)$purpose,
            ),
            array(
                'name' => 'file',
                'contents' => fopen($path, 'r'),
                'filename' => $filename,
            ),
        );

        $request = $this->makeMultipartRequest('files', $multipart);
        $response = $this->httpClient()->send($request);
        return $this->decodeJsonResponse($response);
    }

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

        if (!$response->isSuccess()) {
            $message = 'OpenAI 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 OpenAI');
        }

        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 makeMultipartRequest($path, array $multipart)
    {
        $request = $this->makeBaseRequest($path);
        $request->multipart($multipart);
        return $request;
    }

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

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

        return $request;
    }

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

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