<?php

namespace App\LLM;

/**
 * Abstract base class for all LLM provider services.
 * Every svc*.php must extend this and implement all abstract methods.
 */
abstract class LLMProvider
{
    protected string $apiKey    = '';
    protected string $model     = '';
    protected string $systemPrompt = '';
    protected int    $maxTokens = 4096;
    protected float  $temperature = 0.7;
    protected int    $timeout   = 120;

    /**
     * Send a prompt and return the full response.
     *
     * @param  array  $messages  [['role' => 'user', 'content' => '...']]
     * @param  array  $options   Override defaults (model, max_tokens, temperature, etc.)
     * @return array  ['success' => bool, 'content' => string, 'usage' => array, 'error' => string]
     */
    abstract public function sendPrompt(array $messages, array $options = []): array;

    /**
     * Stream a prompt response chunk by chunk.
     *
     * @param  array    $messages
     * @param  array    $options
     * @param  callable $callback  Called with each chunk: function(string $chunk)
     * @return array   ['success' => bool, 'error' => string]
     */
    abstract public function streamPrompt(array $messages, array $options, callable $callback): array;

    /**
     * Return available models for this provider.
     *
     * @return array [['id' => '...', 'label' => '...']]
     */
    abstract public function getModels(): array;

    /**
     * Test the API key is valid and the provider is reachable.
     *
     * @return array ['success' => bool, 'latency_ms' => int, 'error' => string]
     */
    abstract public function testConnection(): array;

    // ─── Fluent Setters ──────────────────────────────────────────────────────

    public function getModel(): string
    {
        return $this->model;
    }

    public function setModel(string $model): static
    {
        $this->model = $model;
        return $this;
    }

    public function setMaxTokens(int $tokens): static
    {
        $this->maxTokens = $tokens;
        return $this;
    }

    public function setSystemPrompt(string $prompt): static
    {
        $this->systemPrompt = $prompt;
        return $this;
    }

    public function setTemperature(float $temp): static
    {
        $this->temperature = $temp;
        return $this;
    }

    public function setTimeout(int $seconds): static
    {
        $this->timeout = $seconds;
        return $this;
    }

    // ─── Shared Helpers ──────────────────────────────────────────────────────

    /**
     * Build a standard error response.
     */
    protected function errorResponse(string $message): array
    {
        return ['success' => false, 'content' => '', 'usage' => [], 'error' => $message];
    }

    /**
     * Build a standard success response.
     */
    protected function successResponse(string $content, array $usage = []): array
    {
        return ['success' => true, 'content' => $content, 'usage' => $usage, 'error' => ''];
    }

    /**
     * Execute a cURL request and return the decoded JSON response.
     */
    protected function curlPost(string $url, array $headers, array $payload): array
    {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($payload),
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_TIMEOUT        => $this->timeout,
        ]);

        $body  = curl_exec($ch);
        $error = curl_error($ch);
        $code  = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($error) {
            throw new \Exception("cURL error: $error");
        }

        $decoded = json_decode($body, true);

        if ($code >= 400) {
            $msg = $decoded['error']['message'] ?? $decoded['message'] ?? "HTTP $code error";
            throw new \Exception($msg);
        }

        return $decoded;
    }

    /**
     * Execute a streaming cURL request, calling $callback for each SSE data chunk.
     */
    protected function curlStream(string $url, array $headers, array $payload, callable $callback): void
    {
        $payload['stream'] = true;

        $errorBody = '';

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => false,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($payload),
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_TIMEOUT        => $this->timeout,
            CURLOPT_WRITEFUNCTION  => function($ch, $data) use ($callback, &$errorBody) {
                $lines      = explode("\n", $data);
                $hasSseData = false;
                foreach ($lines as $line) {
                    $line = trim($line);
                    if (str_starts_with($line, 'data: ')) {
                        $hasSseData = true;
                        $json = substr($line, 6);
                        if ($json === '[DONE]') break;
                        $chunk = json_decode($json, true);
                        if ($chunk) $callback($chunk);
                    }
                }
                // If no SSE data lines were found this chunk may be an error body
                if (!$hasSseData) {
                    $errorBody .= $data;
                }
                return strlen($data);
            },
        ]);

        curl_exec($ch);
        $curlError = curl_error($ch);
        $httpCode  = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($curlError) {
            throw new \Exception("cURL stream error: $curlError");
        }

        if ($httpCode >= 400) {
            $decoded = json_decode(trim($errorBody), true);
            $msg     = $decoded['error']['message'] ?? $decoded['message'] ?? "API error (HTTP $httpCode)";
            throw new \Exception($msg);
        }
    }
}
