<?php

namespace App\Services\Ai;

use App\Services\Settings;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Http;
use RuntimeException;

/**
 * Single seam for all OpenAI calls. Returns a normalized result so AiController
 * never touches raw request/response shapes. Swap the impl here when adding a
 * second provider — the controller stays the same.
 *
 * @return array{json: mixed, prompt_tokens: int, completion_tokens: int, total_tokens: int}
 */
class OpenAiClient
{
    private const BASE_URL = 'https://api.openai.com/v1';

    /**
     * Send a chat-completions request and return a normalized response.
     *
     * @param  array<array{role:string,content:mixed}>  $messages
     * @param  array<string,mixed>  $options  Extra fields merged into the request body.
     */
    public function complete(array $messages, array $options = []): array
    {
        $key     = $this->decryptedKey();
        $model   = Settings::get('ai.model', 'gpt-4o-mini');
        $timeout = (int) (Settings::get('ai.timeout') ?? 45);

        $body = array_merge([
            'model'           => $model,
            'max_tokens'      => 4000,
            'response_format' => ['type' => 'json_object'],
            'messages'        => $messages,
        ], $options);

        $response = Http::withHeaders(['Authorization' => 'Bearer '.$key])
            ->timeout($timeout)
            ->post(self::BASE_URL.'/chat/completions', $body);

        if (! $response->successful()) {
            $error = $response->json('error.message') ?? $response->body();
            throw new RuntimeException('OpenAI error: '.$error);
        }

        $data  = $response->json();
        $usage = $data['usage'] ?? [];

        $content = $data['choices'][0]['message']['content'] ?? null;
        if ($content === null) {
            throw new RuntimeException('OpenAI returned an empty response.');
        }

        $json = json_decode($content, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new RuntimeException('OpenAI response was not valid JSON: '.json_last_error_msg());
        }

        return [
            'json'              => $json,
            'prompt_tokens'     => (int) ($usage['prompt_tokens'] ?? 0),
            'completion_tokens' => (int) ($usage['completion_tokens'] ?? 0),
            'total_tokens'      => (int) ($usage['total_tokens'] ?? 0),
        ];
    }

    /**
     * Vision-capable call — images are passed as base64 data URIs.
     *
     * @param  array<string>  $imageDataUris  base64 data URIs (data:image/jpeg;base64,...)
     * @param  array<string,mixed>  $options
     */
    public function vision(string $prompt, array $imageDataUris, array $options = []): array
    {
        $content = [['type' => 'text', 'text' => $prompt]];

        foreach ($imageDataUris as $dataUri) {
            $content[] = [
                'type'      => 'image_url',
                'image_url' => ['url' => $dataUri, 'detail' => 'low'],
            ];
        }

        $messages = [
            ['role' => 'user', 'content' => $content],
        ];

        return $this->complete($messages, $options);
    }

    /**
     * Lightweight ping — used by `testAiConnection`. Returns latency in ms.
     */
    public function ping(): int
    {
        $start = microtime(true);

        $this->complete([
            ['role' => 'user', 'content' => 'Respond with the single word OK.'],
        ], ['max_tokens' => 5]);

        return (int) round((microtime(true) - $start) * 1000);
    }

    private function decryptedKey(): string
    {
        $raw = Settings::get('ai.api_key');
        if (! filled($raw)) {
            throw new RuntimeException('No OpenAI API key is configured.');
        }

        try {
            return Crypt::decrypt($raw);
        } catch (\Throwable $e) {
            throw new RuntimeException('Failed to decrypt the OpenAI API key: '.$e->getMessage());
        }
    }
}
