<?php

declare(strict_types=1);

namespace ContextPaging;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ResponseInterface;

/**
 * OpenAI-compatible chat completions client.
 *
 * Works with OpenAI, Vultr Inference, vLLM, and any OpenAI-compatible API.
 */
class OpenAICompatibleClient implements CompletionsClientInterface
{
    private Client $httpClient;
    private string $baseUrl;
    private ?string $apiKey;
    private int $timeout;
    private bool $verifySsl;

    /**
     * Patterns to sanitize from error messages (internal URLs, etc.)
     */
    private const SANITIZE_PATTERNS = [
        '#https?://[^\s]*vultrinference\.com[^\s]*#i',
        '#https?://prod\.[^\s]*#i',
        '#vllm[^\s]*#i',
    ];

    /**
     * Fields that should never be forwarded to the API.
     */
    private const INTERNAL_FIELDS = [
        'collection',
        '_original_md5',
        '_summarized',
    ];

    /**
     * @param string $baseUrl API base URL (e.g., "https://api.openai.com/v1")
     * @param string|null $apiKey API key (optional for local/dev endpoints)
     * @param int $timeout Request timeout in seconds
     * @param bool $verifySsl Whether to verify SSL certificates
     */
    public function __construct(
        string $baseUrl,
        ?string $apiKey = null,
        int $timeout = 300,
        bool $verifySsl = true
    ) {
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->apiKey = $apiKey;
        $this->timeout = $timeout;
        $this->verifySsl = $verifySsl;

        $this->httpClient = new Client([
            'timeout' => $timeout,
            'verify' => $verifySsl,
        ]);
    }

    /**
     * Create a chat completion (non-streaming).
     *
     * @param array $messages OpenAI-format messages
     * @param array $options Additional options (model, temperature, etc.)
     * @return ResponseInterface
     */
    public function chat(array $messages, array $options = []): ResponseInterface
    {
        $payload = $this->buildPayload($messages, $options);

        // Force non-streaming
        $payload['stream'] = false;

        return $this->sendRequest('/chat/completions', $payload);
    }

    /**
     * Create a streaming chat completion.
     *
     * Returns a generator that yields SSE data chunks.
     * Each chunk is the decoded JSON data (not the raw "data: " line).
     *
     * @param array $messages OpenAI-format messages
     * @param array $options Additional options (model, temperature, etc.)
     * @return \Generator Yields arrays: ['delta' => ..., 'finish_reason' => ..., 'usage' => ...]
     */
    public function chatStream(array $messages, array $options = []): \Generator
    {
        $payload = $this->buildPayload($messages, $options);

        // Force streaming
        $payload['stream'] = true;
        if (!isset($payload['stream_options'])) {
            $payload['stream_options'] = ['include_usage' => true];
        }

        $response = $this->sendStreamingRequest('/chat/completions', $payload);

        foreach ($response as $chunk) {
            yield $chunk;
        }
    }

    /**
     * List available models.
     */
    public function listModels(): ResponseInterface
    {
        return $this->sendRequest('/models', [], 'GET');
    }

    // -----------------------------------------------------------------
    // Internal
    // -----------------------------------------------------------------

    /**
     * Build the request payload.
     */
    private function buildPayload(array $messages, array $options): array
    {
        // Strip internal fields
        $payload = array_diff_key($options, array_flip(self::INTERNAL_FIELDS));

        // Set messages
        $payload['messages'] = $this->cleanMessages($messages);

        // Ensure model is set
        if (!isset($payload['model'])) {
            $payload['model'] = 'default';
        }

        return $payload;
    }

    /**
     * Clean messages for API consumption.
     */
    private function cleanMessages(array $messages): array
    {
        return array_map(function (array $msg): array {
            // Remove internal tracking fields
            unset($msg['_summarized'], $msg['_original_md5']);

            // Ensure content is present
            if (!isset($msg['content'])) {
                $msg['content'] = '';
            }

            return $msg;
        }, $messages);
    }

    /**
     * Send a non-streaming request.
     */
    private function sendRequest(string $endpoint, array $payload, string $method = 'POST'): ResponseInterface
    {
        $options = $this->buildRequestOptions($payload);

        try {
            $response = $this->httpClient->request($method, $this->baseUrl . $endpoint, $options);

            // Clean up the response body
            $body = json_decode($response->getBody()->getContents(), true);

            // Remove internal fields we don't expose
            unset($body['prompt_logprobs']);
            foreach ($body['choices'] ?? [] as $i => $choice) {
                unset($body['choices'][$i]['stop_reason']);
            }

            return new Response(
                $response->getStatusCode(),
                ['Content-Type' => 'application/json'],
                json_encode($body)
            );

        } catch (ConnectException $e) {
            return $this->errorResponse(503, 'The AI service is currently unavailable. Please try again later.');

        } catch (RequestException $e) {
            $response = $e->getResponse();
            if ($response) {
                $body = json_decode($response->getBody()->getContents(), true);
                $body = $this->sanitizeErrorBody($body);
                return new Response(
                    $response->getStatusCode(),
                    ['Content-Type' => 'application/json'],
                    json_encode($body)
                );
            }
            return $this->errorResponse(500, $this->sanitizeMessage($e->getMessage()));

        } catch (\Throwable $e) {
            return $this->errorResponse(500, $this->sanitizeMessage($e->getMessage()));
        }
    }

    /**
     * Send a streaming request.
     *
     * @return \Generator Yields decoded JSON chunks
     */
    private function sendStreamingRequest(string $endpoint, array $payload): \Generator
    {
        $options = $this->buildRequestOptions($payload);
        $options['stream'] = true;

        try {
            $response = $this->httpClient->request('POST', $this->baseUrl . $endpoint, $options);
            $body = $response->getBody();

            $buffer = '';

            while (!$body->eof()) {
                $chunk = $body->read(8192);
                if (empty($chunk)) {
                    continue;
                }

                $buffer .= $chunk;

                // Process complete lines
                while (($newlinePos = strpos($buffer, "\n")) !== false) {
                    $line = substr($buffer, 0, $newlinePos);
                    $buffer = substr($buffer, $newlinePos + 1);

                    $line = trim($line);
                    if (empty($line)) {
                        continue;
                    }

                    if (!str_starts_with($line, 'data: ')) {
                        continue;
                    }

                    $data = substr($line, 6);

                    if ($data === '[DONE]') {
                        return; // Generator complete
                    }

                    $decoded = json_decode($data, true);
                    if (!$decoded) {
                        continue;
                    }

                    // Clean up internal fields
                    foreach ($decoded['choices'] ?? [] as $i => $choice) {
                        unset($decoded['choices'][$i]['stop_reason']);
                    }

                    yield $decoded;
                }
            }

        } catch (ConnectException $e) {
            yield [
                'error' => [
                    'message' => 'The AI service is currently unavailable.',
                    'type' => 'connection_error',
                ],
            ];

        } catch (\Throwable $e) {
            yield [
                'error' => [
                    'message' => $this->sanitizeMessage($e->getMessage()),
                    'type' => 'server_error',
                ],
            ];
        }
    }

    /**
     * Build Guzzle request options.
     */
    private function buildRequestOptions(array $payload): array
    {
        $options = [
            'headers' => [
                'Content-Type' => 'application/json',
                'Accept' => 'application/json',
            ],
        ];

        if ($this->apiKey) {
            $options['headers']['Authorization'] = 'Bearer ' . $this->apiKey;
        }

        if (!empty($payload)) {
            $options['json'] = $payload;
        }

        return $options;
    }

    /**
     * Create an error response.
     */
    private function errorResponse(int $status, string $message): ResponseInterface
    {
        return new Response(
            $status,
            ['Content-Type' => 'application/json'],
            json_encode([
                'error' => [
                    'message' => $message,
                    'type' => 'server_error',
                ],
            ])
        );
    }

    /**
     * Sanitize error response body.
     */
    private function sanitizeErrorBody(?array $body): array
    {
        if ($body === null) {
            return ['error' => ['message' => 'Unknown error', 'type' => 'server_error']];
        }

        if (isset($body['error']['message'])) {
            $body['error']['message'] = $this->sanitizeMessage($body['error']['message']);
        }
        if (isset($body['message'])) {
            $body['message'] = $this->sanitizeMessage($body['message']);
        }

        return $body;
    }

    /**
     * Sanitize error messages to remove internal URLs/patterns.
     */
    private function sanitizeMessage(string $message): string
    {
        $cleaned = preg_replace(self::SANITIZE_PATTERNS, '', $message);
        return trim(preg_replace('/\s+/', ' ', $cleaned)) ?: 'An error occurred';
    }
}
