<?php

namespace Modules\VetAI\Services;

use Illuminate\Support\Facades\Http;
use Modules\VetAI\Entities\VaiSetting;

/**
 * Thin multi-provider LLM client. Supports Anthropic (Claude), OpenAI and
 * Google Gemini. Every method returns a normalised result array:
 *   ['ok' => bool, 'text' => string, 'tokens_in' => int, 'tokens_out' => int, 'error' => ?string]
 */
class AiClient
{
    public function chat(VaiSetting $settings, string $systemPrompt, string $userPrompt): array
    {
        try {
            switch ($settings->provider) {
                case 'anthropic':
                    return $this->anthropic($settings, $systemPrompt, $userPrompt);
                case 'openai':
                    return $this->openaiStyle($settings, $systemPrompt, $userPrompt, 'https://api.openai.com/v1');
                case 'openai_compatible':
                    if (empty($settings->base_url)) {
                        return $this->fail('No base URL configured for the OpenAI-compatible provider.');
                    }
                    return $this->openaiStyle($settings, $systemPrompt, $userPrompt, $settings->base_url);
                case 'gemini':
                    return $this->gemini($settings, $systemPrompt, $userPrompt);
                default:
                    return $this->fail('Unknown provider: ' . $settings->provider);
            }
        } catch (\Throwable $e) {
            return $this->fail('Request failed: ' . $e->getMessage());
        }
    }

    private function anthropic(VaiSetting $s, string $system, string $user): array
    {
        $resp = Http::withHeaders([
            'x-api-key'         => $s->api_key,
            'anthropic-version' => '2023-06-01',
            'content-type'      => 'application/json',
        ])->timeout(90)->post('https://api.anthropic.com/v1/messages', [
            'model'       => $s->model,
            'max_tokens'  => (int) $s->max_tokens,
            'temperature' => (float) $s->temperature,
            'system'      => $system,
            'messages'    => [['role' => 'user', 'content' => $user]],
        ]);

        if (!$resp->successful()) {
            return $this->fail($this->errMsg($resp->json(), $resp->status()));
        }
        $j = $resp->json();
        $text = '';
        foreach (($j['content'] ?? []) as $block) {
            if (($block['type'] ?? '') === 'text') $text .= $block['text'];
        }
        return [
            'ok'         => $text !== '',
            'text'       => $text ?: '(empty response)',
            'tokens_in'  => (int) ($j['usage']['input_tokens'] ?? 0),
            'tokens_out' => (int) ($j['usage']['output_tokens'] ?? 0),
            'error'      => $text === '' ? 'Empty response from model' : null,
        ];
    }

    /**
     * OpenAI Chat Completions format — used by OpenAI itself and by every
     * OpenAI-compatible endpoint (DeepSeek, Groq, OpenRouter, local Ollama…).
     */
    private function openaiStyle(VaiSetting $s, string $system, string $user, string $baseUrl): array
    {
        $endpoint = rtrim($baseUrl, '/') . '/chat/completions';

        $req = Http::timeout(90);
        if (!empty($s->api_key)) {
            $req = $req->withToken($s->api_key); // optional — keyless for local Ollama
        }

        $resp = $req->post($endpoint, [
            'model'       => $s->model,
            'max_tokens'  => (int) $s->max_tokens,
            'temperature' => (float) $s->temperature,
            'messages'    => [
                ['role' => 'system', 'content' => $system],
                ['role' => 'user',   'content' => $user],
            ],
        ]);

        if (!$resp->successful()) {
            return $this->fail($this->errMsg($resp->json(), $resp->status()));
        }
        $j = $resp->json();
        $text = $j['choices'][0]['message']['content'] ?? '';
        return [
            'ok'         => $text !== '',
            'text'       => $text ?: '(empty response)',
            'tokens_in'  => (int) ($j['usage']['prompt_tokens'] ?? 0),
            'tokens_out' => (int) ($j['usage']['completion_tokens'] ?? 0),
            'error'      => $text === '' ? 'Empty response from model' : null,
        ];
    }

    private function gemini(VaiSetting $s, string $system, string $user): array
    {
        $url = 'https://generativelanguage.googleapis.com/v1beta/models/'
             . rawurlencode($s->model) . ':generateContent?key=' . urlencode($s->api_key);

        $generationConfig = [
            'temperature'     => (float) $s->temperature,
            'maxOutputTokens' => (int) $s->max_tokens,
        ];
        // Gemini 2.5 *flash* models "think" by default — reasoning tokens eat the
        // output budget and truncate the answer. Disable thinking on flash models.
        $model = strtolower((string) $s->model);
        if (str_contains($model, '2.5') && str_contains($model, 'flash')) {
            $generationConfig['thinkingConfig'] = ['thinkingBudget' => 0];
        }

        $resp = Http::timeout(90)->post($url, [
            'systemInstruction' => ['parts' => [['text' => $system]]],
            'contents'          => [['role' => 'user', 'parts' => [['text' => $user]]]],
            'generationConfig'  => $generationConfig,
        ]);

        if (!$resp->successful()) {
            return $this->fail($this->errMsg($resp->json(), $resp->status()));
        }
        $j = $resp->json();
        $text = '';
        foreach (($j['candidates'][0]['content']['parts'] ?? []) as $part) {
            $text .= $part['text'] ?? '';
        }

        // Surface a truncated answer instead of silently returning a cut-off one.
        $finish = $j['candidates'][0]['finishReason'] ?? '';
        if ($finish === 'MAX_TOKENS' && $text !== '') {
            $text .= "\n\n_[Response truncated — raise “Max Response Tokens” in AI Settings.]_";
        }

        $usage = $j['usageMetadata'] ?? [];
        return [
            'ok'         => $text !== '',
            'text'       => $text ?: '(empty response)',
            'tokens_in'  => (int) ($usage['promptTokenCount'] ?? 0),
            // include reasoning ("thoughts") tokens so the budget guard is accurate
            'tokens_out' => (int) ($usage['candidatesTokenCount'] ?? 0)
                          + (int) ($usage['thoughtsTokenCount'] ?? 0),
            'error'      => $text === '' ? 'Empty / blocked response from model' : null,
        ];
    }

    private function errMsg($json, int $status): string
    {
        $msg = $json['error']['message'] ?? ($json['error']['type'] ?? null);
        if (is_array($msg)) $msg = json_encode($msg);
        return 'API error ' . $status . ($msg ? ': ' . $msg : '');
    }

    private function fail(string $error): array
    {
        return ['ok' => false, 'text' => '', 'tokens_in' => 0, 'tokens_out' => 0, 'error' => $error];
    }
}
