<?php

declare(strict_types=1);

namespace App\Services\AiBiography;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

/**
 * Thin client for the Skinbase Vision LLM gateway.
 *
 * Uses the existing Vision gateway infrastructure (VISION_GATEWAY_URL / VISION_GATEWAY_API_KEY).
 * Prefers /ai/chat; falls back to /v1/chat/completions if configured.
 *
 * Error codes handled:
 *   401 – invalid API key
 *   413 – oversized request
 *   422 – invalid payload
 *   503 – LLM unavailable
 *   504 – timeout / upstream
 */
final class VisionLlmClient
{
    public function isConfigured(): bool
    {
        return match ($this->provider()) {
            'together' => $this->togetherApiKey() !== '' && $this->togetherModel() !== '',
            'gemini' => $this->geminiBaseUrl() !== '' && $this->geminiApiKey() !== '' && $this->geminiModel() !== '',
            'home' => $this->homeBaseUrl() !== '' && $this->homeModel() !== '',
            default => $this->baseUrl() !== '' && $this->apiKey() !== '',
        };
    }

    public function configuredModel(): string
    {
        return match ($this->provider()) {
            'together' => $this->togetherModel(),
            'gemini' => $this->geminiModel(),
            'home' => $this->homeModel(),
            default => trim((string) config('ai_biography.llm_model', 'vision-gateway')),
        };
    }

    /**
     * Send a chat completion payload to the Vision gateway.
     *
     * @param  array{messages: list<array{role: string, content: string}>, max_tokens: int, temperature: float, stream: bool}  $payload
     * @return string  The generated text content.
     *
     * @throws VisionLlmException  On structured gateway failure.
     */
    public function chat(array $payload): string
    {
        if (! $this->isConfigured()) {
            throw new VisionLlmException(
                match ($this->provider()) {                    'together' => 'Together.ai is not configured. Set TOGETHER_API_KEY and optionally AI_BIOGRAPHY_TOGETHER_MODEL.',                    'gemini' => 'Gemini API is not configured. Set GEMINI_API_KEY and optionally AI_BIOGRAPHY_GEMINI_MODEL.',
                    'home' => 'Home LM Studio is not configured. Set AI_BIOGRAPHY_HOME_BASE_URL and AI_BIOGRAPHY_HOME_MODEL.',
                    default => 'Vision LLM gateway is not configured. Set VISION_GATEWAY_URL and VISION_GATEWAY_API_KEY.',
                },
                0
            );
        }

        return match ($this->provider()) {
            'together' => $this->chatWithTogether($payload),
            'gemini' => $this->chatWithGemini($payload),
            'home' => $this->chatWithHome($payload),
            default => $this->chatWithVisionGateway($payload),
        };
    }

    /**
     * @param  array{messages: list<array{role: string, content: string}>, max_tokens: int, temperature: float, stream: bool}  $payload
     */
    private function chatWithTogether(array $payload): string
    {
        $response = $this->sendTogetherRequest($this->togetherEndpoint(), $this->toTogetherPayload($payload));

        $this->assertSuccessful($response, 'together');

        return $this->extractContent($response);
    }

    /**
     * @param  array{messages: list<array{role: string, content: string}>, max_tokens: int, temperature: float, stream: bool}  $payload
     */
    private function chatWithVisionGateway(array $payload): string
    {
        $endpoint = $this->primaryEndpoint();
        $response = $this->sendRequest($endpoint, $payload);

        // If primary endpoint returned a 404, fall back to the OpenAI-compatible path.
        if ($response->status() === 404) {
            $fallbackEndpoint = $this->fallbackEndpoint();
            if ($fallbackEndpoint !== $endpoint) {
                Log::debug('VisionLlmClient: primary endpoint returned 404, trying fallback', [
                    'primary'  => $endpoint,
                    'fallback' => $fallbackEndpoint,
                ]);
                $response = $this->sendRequest($fallbackEndpoint, $payload);
            }
        }

        $this->assertSuccessful($response, 'vision_gateway');

        return $this->extractContent($response);
    }

    /**
     * @param  array{messages: list<array{role: string, content: string}>, max_tokens: int, temperature: float, stream: bool}  $payload
     */
    private function chatWithGemini(array $payload): string
    {
        $response = $this->sendGeminiRequest($this->geminiEndpoint(), $this->toGeminiPayload($payload));

        $this->assertSuccessful($response, 'gemini');

        return $this->extractGeminiContent($response);
    }

    /**
     * @param  array{messages: list<array{role: string, content: string}>, max_tokens: int, temperature: float, stream: bool}  $payload
     */
    private function chatWithHome(array $payload): string
    {
        $response = $this->sendHomeRequest($this->homeEndpoint(), $this->toHomePayload($payload));

        $this->assertSuccessful($response, 'home');

        return $this->extractContent($response);
    }

    // -------------------------------------------------------------------------

    private function sendRequest(string $url, array $payload): Response
    {
        try {
            return $this->buildRequest()->post($url, $payload);
        } catch (\Illuminate\Http\Client\ConnectionException $e) {
            throw new VisionLlmException(
                'Vision LLM gateway connection failed: ' . $e->getMessage(),
                504,
                $e
            );
        }
    }

    private function sendTogetherRequest(string $url, array $payload): Response
    {
        try {
            return $this->buildTogetherRequest()->post($url, $payload);
        } catch (\Illuminate\Http\Client\ConnectionException $e) {
            throw new VisionLlmException(
                'Together.ai connection failed: ' . $e->getMessage(),
                504,
                $e
            );
        }
    }

    private function sendGeminiRequest(string $url, array $payload): Response
    {
        try {
            return $this->buildGeminiRequest()->post($url, $payload);
        } catch (\Illuminate\Http\Client\ConnectionException $e) {
            throw new VisionLlmException(
                'Gemini API connection failed: ' . $e->getMessage(),
                504,
                $e
            );
        }
    }

    private function sendHomeRequest(string $url, array $payload): Response
    {
        try {
            return $this->buildHomeRequest()->post($url, $payload);
        } catch (\Illuminate\Http\Client\ConnectionException $e) {
            throw new VisionLlmException(
                'Home LM Studio connection failed: ' . $e->getMessage(),
                504,
                $e
            );
        }
    }

    private function assertSuccessful(Response $response, string $provider): void
    {
        if ($response->successful()) {
            return;
        }

        $status = $response->status();
        $body   = mb_substr(trim($response->body()), 0, 300);

        $label = match ($provider) {
            'together' => 'Together.ai',
            'gemini' => 'Gemini API',
            'home' => 'Home LM Studio',
            default => 'Vision LLM gateway',
        };

        $message = match ($status) {
            401, 403 => "{$label}: invalid or unauthorized API key ({$status}).",
            413      => "{$label}: request payload too large (413).",
            422      => "{$label}: invalid payload (422). {$body}",
            429      => "{$label}: rate limit or quota exceeded (429).",
            503      => "{$label}: LLM service unavailable (503).",
            504      => "{$label}: upstream timeout (504).",
            default  => "{$label}: unexpected HTTP {$status}. {$body}",
        };

        Log::warning('VisionLlmClient: gateway error', [
            'provider' => $provider,
            'status'  => $status,
            'excerpt' => $body,
        ]);

        throw new VisionLlmException($message, $status);
    }

    private function extractContent(Response $response): string
    {
        $json = $response->json();

        // Standard OpenAI-compatible shape: choices[0].message.content
        if (isset($json['choices'][0]['message']['content'])) {
            return trim((string) $json['choices'][0]['message']['content']);
        }

        // Simple shape: { "content": "..." }
        if (isset($json['content']) && is_string($json['content'])) {
            return trim($json['content']);
        }

        // Shape: { "text": "..." }
        if (isset($json['text']) && is_string($json['text'])) {
            return trim($json['text']);
        }

        throw new VisionLlmException(
            'Vision LLM gateway: unrecognized response shape. Could not extract generated text.',
            0
        );
    }

    private function extractGeminiContent(Response $response): string
    {
        $json = $response->json();
        $parts = $json['candidates'][0]['content']['parts'] ?? null;

        if (! is_array($parts) || $parts === []) {
            throw new VisionLlmException(
                'Gemini API: unrecognized response shape. Could not extract generated text.',
                0
            );
        }

        $text = collect($parts)
            ->map(fn ($part) => is_array($part) ? trim((string) ($part['text'] ?? '')) : '')
            ->filter(fn (string $value): bool => $value !== '')
            ->implode("\n");

        if ($text === '') {
            throw new VisionLlmException(
                'Gemini API: response did not contain text content.',
                0
            );
        }

        return $text;
    }

    private function buildRequest(): PendingRequest
    {
        return Http::acceptJson()
            ->contentType('application/json')
            ->withHeaders(['X-API-Key' => $this->apiKey()])
            ->connectTimeout(max(1, (int) config('vision.gateway.connect_timeout_seconds', 3)))
            ->timeout(max(5, (int) config('ai_biography.llm_timeout_seconds', 30)));
    }

    private function buildTogetherRequest(): PendingRequest
    {
        return Http::acceptJson()
            ->contentType('application/json')
            ->withToken($this->togetherApiKey())
            ->connectTimeout(max(1, (int) config('ai_biography.together.connect_timeout_seconds', 5)))
            ->timeout(max(5, (int) config('ai_biography.together.timeout_seconds', config('ai_biography.llm_timeout_seconds', 90))));
    }

    private function buildGeminiRequest(): PendingRequest
    {
        return Http::acceptJson()
            ->contentType('application/json')
            ->withHeaders(['X-goog-api-key' => $this->geminiApiKey()])
            ->connectTimeout(max(1, (int) config('vision.gateway.connect_timeout_seconds', 3)))
            ->timeout(max(5, (int) config('ai_biography.llm_timeout_seconds', 30)));
    }

    private function buildHomeRequest(): PendingRequest
    {
        $request = Http::acceptJson()
            ->contentType('application/json')
            ->connectTimeout(max(1, (int) config('ai_biography.home.connect_timeout_seconds', 3)))
            ->timeout(max(5, (int) config('ai_biography.home.timeout_seconds', config('ai_biography.llm_timeout_seconds', 30))));

        if (! (bool) config('ai_biography.home.verify_ssl', true)) {
            $request = $request->withoutVerifying();
        }

        if ($this->homeApiKey() !== '') {
            $request = $request->withToken($this->homeApiKey());
        }

        return $request;
    }

    private function baseUrl(): string
    {
        return rtrim((string) config('vision.gateway.base_url', ''), '/');
    }

    private function apiKey(): string
    {
        return trim((string) config('vision.gateway.api_key', ''));
    }

    private function primaryEndpoint(): string
    {
        $path = ltrim((string) config('ai_biography.llm_endpoint', '/ai/chat'), '/');

        return $this->baseUrl() . '/' . $path;
    }

    private function fallbackEndpoint(): string
    {
        $path = ltrim((string) config('ai_biography.llm_fallback_endpoint', '/v1/chat/completions'), '/');

        return $this->baseUrl() . '/' . $path;
    }

    private function provider(): string
    {
        $override = trim(strtolower((string) config('ai_biography.provider_override', '')));

        if (in_array($override, ['together', 'vision_gateway', 'gemini', 'home'], true)) {
            return $override;
        }

        if ($this->togetherApiKey() !== '' && $this->togetherModel() !== '') {
            return 'together';
        }

        $provider = trim(strtolower((string) config('ai_biography.provider', 'together')));

        return in_array($provider, ['together', 'vision_gateway', 'gemini', 'home'], true) ? $provider : 'together';
    }

    private function geminiBaseUrl(): string
    {
        return rtrim((string) config('ai_biography.gemini.base_url', 'https://generativelanguage.googleapis.com'), '/');
    }

    private function geminiApiKey(): string
    {
        return trim((string) config('ai_biography.gemini.api_key', ''));
    }

    private function geminiModel(): string
    {
        return trim((string) config('ai_biography.gemini.model', 'gemini-flash-latest'));
    }

    private function geminiEndpoint(): string
    {
        return $this->geminiBaseUrl() . '/v1beta/models/' . rawurlencode($this->geminiModel()) . ':generateContent';
    }

    private function togetherApiKey(): string
    {
        return trim((string) config('ai_biography.together.api_key', ''));
    }

    private function togetherModel(): string
    {
        return trim((string) config('ai_biography.together.model', 'google/gemma-3n-E4B-it'));
    }

    private function togetherEndpoint(): string
    {
        $base = rtrim((string) config('ai_biography.together.base_url', 'https://api.together.xyz'), '/');
        $path = ltrim((string) config('ai_biography.together.endpoint', '/v1/chat/completions'), '/');

        return $base . '/' . $path;
    }

    /**
     * @param  array{messages: list<array{role: string, content: string}>, max_tokens: int, temperature: float, stream: bool}  $payload
     * @return array<string, mixed>
     */
    private function toTogetherPayload(array $payload): array
    {
        return [
            'model'       => $this->togetherModel(),
            'messages'    => array_values((array) ($payload['messages'] ?? [])),
            'max_tokens'  => max(1, (int) ($payload['max_tokens'] ?? 256)),
            'temperature' => (float) ($payload['temperature'] ?? 0.3),
            'stream'      => false,
        ];
    }

    private function homeBaseUrl(): string
    {
        return rtrim((string) config('ai_biography.home.base_url', 'http://home.klevze.si:8200'), '/');
    }

    private function homeApiKey(): string
    {
        return trim((string) config('ai_biography.home.api_key', ''));
    }

    private function homeModel(): string
    {
        return trim((string) config('ai_biography.home.model', 'qwen/qwen3.5-9b'));
    }

    private function homeEndpoint(): string
    {
        $path = ltrim((string) config('ai_biography.home.endpoint', '/v1/chat/completions'), '/');

        return $this->homeBaseUrl() . '/' . $path;
    }

    /**
     * @param  array{messages: list<array{role: string, content: string}>, max_tokens: int, temperature: float, stream: bool}  $payload
     * @return array<string, mixed>
     */
    private function toGeminiPayload(array $payload): array
    {
        $systemParts = [];
        $contents = [];

        foreach ((array) ($payload['messages'] ?? []) as $message) {
            $role = strtolower((string) ($message['role'] ?? 'user'));
            $content = trim((string) ($message['content'] ?? ''));

            if ($content === '') {
                continue;
            }

            if ($role === 'system') {
                $systemParts[] = ['text' => $content];
                continue;
            }

            $contents[] = [
                'role' => $role === 'assistant' ? 'model' : 'user',
                'parts' => [
                    ['text' => $content],
                ],
            ];
        }

        if ($contents === [] && $systemParts !== []) {
            $contents[] = [
                'role' => 'user',
                'parts' => $systemParts,
            ];
            $systemParts = [];
        }

        $geminiPayload = [
            'contents' => $contents,
            'generationConfig' => [
                'temperature' => (float) ($payload['temperature'] ?? 0.3),
                'maxOutputTokens' => max(1, (int) ($payload['max_tokens'] ?? 256)),
            ],
        ];

        if ($systemParts !== []) {
            $geminiPayload['systemInstruction'] = [
                'parts' => $systemParts,
            ];
        }

        return $geminiPayload;
    }

    /**
     * @param  array{messages: list<array{role: string, content: string}>, max_tokens: int, temperature: float, stream: bool}  $payload
     * @return array<string, mixed>
     */
    private function toHomePayload(array $payload): array
    {
        return [
            'model' => $this->homeModel(),
            'messages' => array_values((array) ($payload['messages'] ?? [])),
            'max_tokens' => max(1, (int) ($payload['max_tokens'] ?? 256)),
            'temperature' => (float) ($payload['temperature'] ?? 0.3),
            'stream' => false,
        ];
    }
}
