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

use boru\ai\AI;
use boru\ai\Config\ConfigResolver;
use boru\ai\Embedding\EmbeddingProviderInterface;
use boru\ai\Embedding\EmbeddingRequest;
use boru\ai\Embedding\EmbeddingResult;
use boru\ai\Provider\Capabilities;
use boru\ai\Provider\ProviderInterface;
use boru\ai\Provider\ProviderSignal;
use boru\ai\Schema\Request;
use boru\ai\Schema\Response;
use boru\ai\Stream\StreamEvent;

class OpenAIProvider implements ProviderInterface, EmbeddingProviderInterface
{
    /** @var Capabilities */
    protected $capabilities;

    /** @var OpenAIClient */
    protected $client;

    /** @var OpenAIMapper */
    protected $mapper;

    /** @var array */
    protected $modelPrefixes = array('gpt-', 'o', 'chatgpt-');

    public function __construct(OpenAIClient $client = null, OpenAIMapper $mapper = null, Capabilities $capabilities = null)
    {
        $this->client = $client ? $client : new OpenAIClient();
        $this->mapper = $mapper ? $mapper : new OpenAIMapper();
        $this->capabilities = $capabilities ? $capabilities : new Capabilities(array(
            'streaming' => true,
            'tools' => true,
            'vision' => true,
            'files' => true,
            'reasoning' => true,
            'sessionState' => true,
            'embeddings' => true,
        ));
    }

    public function name()
    {
        return 'openai';
    }

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

    public function supportsModel($model)
    {
        $model = strtolower((string)$model);
        if ($model === '') {
            return false;
        }

        foreach ($this->modelPrefixes as $prefix) {
            if (strpos($model, strtolower($prefix)) === 0) {
                return true;
            }
        }

        return false;
    }

    public function execute(Request $request, $eventCallback = null)
    {
        $request = $this->applyReplayIfAvailable($request);

        if ($this->shouldStream($request, $eventCallback)) {
            return $this->streamResponse($request, $eventCallback);
        }

        $payload = $this->mapper->mapRequest($request);
        $result = $this->client->post('responses', $payload);
        return $this->mapper->mapResponse($result);
    }

    public function continueResponse(Request $request, Response $previousResponse, array $toolResults, $eventCallback = null)
    {
        $continuedRequest = $request->copy();
        foreach ($toolResults as $toolResult) {
            $continuedRequest->addToolResult($toolResult);
        }

        if ($previousResponse->id()) {
            $continuedRequest->option('previous_response_id', $previousResponse->id());
        }

        if ($this->shouldStream($continuedRequest, $eventCallback)) {
            return $this->streamResponse($continuedRequest, $eventCallback);
        }

        $payload = $this->mapper->mapRequest($continuedRequest);
        $result = $this->client->post('responses', $payload);
        return $this->mapper->mapResponse($result);
    }

    public function embed(EmbeddingRequest $request)
    {
        $payload = array(
            'model' => $request->model(),
            'input' => count($request->inputs()) === 1 ? $request->input() : $request->inputs(),
        );

        if ($request->option('dimensions') !== null) {
            $payload['dimensions'] = (int)$request->option('dimensions');
        }

        if ($request->option('encoding_format') !== null) {
            $payload['encoding_format'] = $request->option('encoding_format');
        }

        if ($request->option('user') !== null) {
            $payload['user'] = $request->option('user');
        }

        $response = $this->client->embeddings($payload);

        $result = new EmbeddingResult();
        $result->provider($this->name());
        $result->model(isset($response['model']) ? $response['model'] : $request->model());
        if (isset($response['usage']) && is_array($response['usage'])) {
            $result->usage($response['usage']);
        }

        $data = isset($response['data']) && is_array($response['data']) ? $response['data'] : array();
        foreach ($data as $index => $item) {
            if (!is_array($item)) {
                continue;
            }

            $inputIndex = isset($item['index']) ? (int)$item['index'] : $index;
            $inputs = $request->inputs();
            $input = isset($inputs[$inputIndex]) ? $inputs[$inputIndex] : null;
            $vector = isset($item['embedding']) && is_array($item['embedding']) ? $item['embedding'] : array();
            $result->addEmbedding($vector, $input, $inputIndex);
        }

        return $result;
    }

    protected function shouldStream(Request $request, $eventCallback = null)
    {
        $stream = $request->option('stream');
        if ($stream !== null) {
            return $stream ? true : false;
        }
        return is_callable($eventCallback);
    }

    protected function streamResponse(Request $request, $eventCallback = null)
    {
        $payload = $this->mapper->mapRequest($request);
        $parser = new OpenAIStreamParser($this->mapper);

        $httpResponse = $this->client->stream('responses', $payload, function ($chunk) use ($parser, $eventCallback) {
            $parser->push($chunk, function (ProviderSignal $signal) use ($eventCallback) {
                $event = $this->mapSignalToStreamEvent($signal);
                if ($event && is_callable($eventCallback)) {
                    call_user_func($eventCallback, $event);
                }
            });
        });

        $final = $parser->finalResponse();
        if ($final instanceof Response) {
            return $final;
        }

        $decoded = $httpResponse->json();
        if (is_array($decoded)) {
            return $this->mapper->mapResponse($decoded);
        }

        throw new \RuntimeException('OpenAI streaming response did not produce a valid final response', $httpResponse->statusCode());
    }

    protected function applyReplayIfAvailable(Request $request)
    {
        $conversation = $request->metadata('conversation');
        if (!$conversation instanceof \boru\ai\Conversation\Conversation) {
            return $request;
        }

        $resolver = new ConfigResolver(AI::config());
        $enabled = $resolver->resolveProviderSetting('openai', 'replay.enabled', true);
        if (!$enabled) {
            return $request;
        }

        $previousResponseId = $this->findLatestResponseId($conversation);
        if ($previousResponseId) {
            $request = $request->copy();
            $request->option('previous_response_id', $previousResponseId);
        }

        return $request;
    }

    protected function findLatestResponseId($conversation)
    {
        $turns = $conversation->turns();
        for ($i = count($turns) - 1; $i >= 0; $i--) {
            $turn = $turns[$i];
            if (!$turn instanceof \boru\ai\Conversation\Turn) {
                continue;
            }
            $response = $turn->response();
            if ($response instanceof Response && $response->id()) {
                return $response->id();
            }
        }

        return null;
    }

    protected function mapSignalToStreamEvent(ProviderSignal $signal)
    {
        $type = $signal->type();

        if ($type === ProviderSignal::TEXT_DELTA) {
            return new StreamEvent(StreamEvent::TEXT_DELTA, $signal->data(), $signal->response());
        }

        if ($type === ProviderSignal::TEXT_COMPLETED) {
            return new StreamEvent(StreamEvent::TEXT_COMPLETED, $signal->data(), $signal->response());
        }

        if ($type === ProviderSignal::RESPONSE_ERROR) {
            return new StreamEvent(StreamEvent::TURN_ERROR, $signal->data(), $signal->response());
        }

        return null;
    }
}
