<?php

declare(strict_types=1);

namespace App\Services\Vision;

use App\Models\Artwork;
use App\Services\TagNormalizer;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;

final class ArtworkLlmTagSuggestionService
{
    private const SYSTEM_PROMPT = <<<'PROMPT'
You are a precise visual-art tagging engine for Skinbase.

Analyze the provided artwork thumbnail and generate search-friendly tags that help users discover the work in a gallery.

Rules:
- Use only what is clearly visible or strongly implied by the image.
- Prefer concrete visual concepts over vague opinions.
- Do not include artist names, brands, platform names, or watermarks.
- Do not write sentences, explanations, numbering, or markdown.
- Return concise gallery-style tags only.
- Favor subject, setting, style, mood, palette, medium, and composition when visible.
- Avoid filler tags like "art", "image", "beautiful", "cool", or "design".
- Avoid duplicates and near-duplicates.
PROMPT;

    private const USER_PROMPT = <<<'PROMPT'
Analyze this artwork thumbnail and return ONLY a valid JSON array of lowercase strings.

Requirements:
- Return between 10 and 15 tags.
- Each tag must be 1 to 3 words.
- Use only letters, numbers, spaces, and hyphens.
- No markdown, no explanations, no extra text.
- Order tags from most useful to least useful.

Focus on:
1. main subject or scene
2. style or genre
3. mood or atmosphere
4. dominant colours
5. medium or technique
6. notable composition or visual elements

Good example:
["fantasy portrait","digital painting","female warrior","blue tones","dramatic lighting","glowing eyes","detailed armor","cinematic mood","character art","moody background"]

Bad example:
["art","beautiful image","masterpiece","cool fantasy woman"]
PROMPT;

    public function __construct(
        private readonly TagNormalizer $normalizer,
        private readonly ArtworkVisionImageUrl $imageUrl,
    ) {
    }

    /**
     * @return array{provider: string, tags: list<string>, model: ?string, endpoint: ?string, image_url: ?string, variant: string, raw_content?: string, reason?: string, error?: string}
     */
    public function suggestForArtwork(Artwork $artwork, int $minTags = 10, int $maxTags = 15, ?string $providerOverride = null): array
    {
        $provider = $this->resolveProvider($providerOverride);
        $variant = 'md';
        $imageUrl = $this->imageUrl->fromHash((string) ($artwork->hash ?? ''), (string) ($artwork->thumb_ext ?: 'webp'), $variant);
        if ($imageUrl === null) {
            return [
                'provider' => $provider,
                'tags' => [],
                'model' => null,
                'endpoint' => null,
                'image_url' => null,
                'variant' => $variant,
                'reason' => 'image_url_unavailable',
            ];
        }

        $configuration = $this->providerConfiguration($provider);
        $baseUrl = rtrim((string) ($configuration['base_url'] ?? ''), '/');
        $endpointPath = (string) ($configuration['endpoint'] ?? '/v1/chat/completions');
        $model = trim((string) ($configuration['model'] ?? ''));
        if ($baseUrl === '' || $model === '') {
            return [
                'provider' => $provider,
                'tags' => [],
                'model' => $model !== '' ? $model : null,
                'endpoint' => $baseUrl !== '' ? $baseUrl . '/' . ltrim($endpointPath, '/') : null,
                'image_url' => $imageUrl,
                'variant' => $variant,
                'reason' => $provider . '_not_configured',
            ];
        }

        $endpoint = $baseUrl . '/' . ltrim($endpointPath, '/');
        $safeMin = min(15, max(1, $minTags));
        $safeMax = min(15, max($safeMin, $maxTags));

        $payload = [
            'model' => $model,
            'temperature' => (float) config('vision.lm_studio.temperature', 0.3),
            'max_tokens' => (int) config('vision.lm_studio.max_tokens', 300),
            'messages' => [
                [
                    'role' => 'system',
                    'content' => self::SYSTEM_PROMPT,
                ],
                [
                    'role' => 'user',
                    'content' => [
                        ['type' => 'image_url', 'image_url' => ['url' => $imageUrl]],
                        ['type' => 'text', 'text' => str_replace(['10 and 15', '10 to 15'], ["{$safeMin} and {$safeMax}", "{$safeMin} to {$safeMax}"], self::USER_PROMPT)],
                    ],
                ],
            ],
        ];

        try {
            $response = $this->buildRequest($provider, $configuration)
                ->post($endpoint, $payload);

            if (! $response->ok()) {
                return [
                    'provider' => $provider,
                    'tags' => [],
                    'model' => $model,
                    'endpoint' => $endpoint,
                    'image_url' => $imageUrl,
                    'variant' => $variant,
                    'reason' => 'http_' . $response->status(),
                    'error' => substr($response->body(), 0, 500),
                ];
            }

            $body = $response->json();
            $content = is_array($body)
                ? (string) (($body['choices'][0]['message']['content'] ?? ''))
                : '';
            $tags = $this->parseTags($content, $safeMax);

            return [
                'provider' => $provider,
                'tags' => $tags,
                'model' => $model,
                'endpoint' => $endpoint,
                'image_url' => $imageUrl,
                'variant' => $variant,
                'raw_content' => $content,
            ];
        } catch (\Throwable $exception) {
            return [
                'provider' => $provider,
                'tags' => [],
                'model' => $model,
                'endpoint' => $endpoint,
                'image_url' => $imageUrl,
                'variant' => $variant,
                'reason' => 'request_failed',
                'error' => $exception->getMessage(),
            ];
        }
    }

    /**
     * @return array{base_url: string, endpoint: string, model: string, timeout: int, connect_timeout: int, api_key?: string}
     */
    private function providerConfiguration(string $provider): array
    {
        return match ($provider) {
            'together' => [
                'base_url' => (string) config('vision.together.base_url', 'https://api.together.xyz'),
                'endpoint' => (string) config('vision.together.endpoint', '/v1/chat/completions'),
                'model' => (string) config('vision.together.model', 'meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo'),
                'timeout' => (int) config('vision.together.timeout', 90),
                'connect_timeout' => (int) config('vision.together.connect_timeout', 5),
                'api_key' => (string) config('vision.together.api_key', ''),
            ],
            default => [
                'base_url' => (string) config('vision.lm_studio.base_url', ''),
                'endpoint' => '/v1/chat/completions',
                'model' => (string) config('vision.lm_studio.model', ''),
                'timeout' => (int) config('vision.lm_studio.timeout', 60),
                'connect_timeout' => (int) config('vision.lm_studio.connect_timeout', 5),
            ],
        };
    }

    /**
     * @param array{base_url: string, endpoint: string, model: string, timeout: int, connect_timeout: int, api_key?: string} $configuration
     */
    private function buildRequest(string $provider, array $configuration): PendingRequest
    {
        $request = Http::acceptJson()
            ->asJson()
            ->timeout(max(1, (int) ($configuration['timeout'] ?? 60)))
            ->connectTimeout(max(1, (int) ($configuration['connect_timeout'] ?? 5)));

        if ($provider === 'together') {
            $apiKey = trim((string) ($configuration['api_key'] ?? ''));
            if ($apiKey !== '') {
                $request = $request->withToken($apiKey);
            }
        }

        return $request;
    }

    private function resolveProvider(?string $providerOverride = null): string
    {
        $candidate = trim(strtolower((string) ($providerOverride ?? config('vision.tag_suggestions.provider', 'lm_studio'))));

        return match ($candidate) {
            'together', 'together_ai' => 'together',
            'lm-studio', 'local', 'home' => 'lm_studio',
            default => 'lm_studio',
        };
    }

    /**
     * @return list<string>
     */
    private function parseTags(string $content, int $maxTags): array
    {
        $trimmed = trim($content);
        $trimmed = preg_replace('/^```(?:json)?\s*/i', '', $trimmed) ?? $trimmed;
        $trimmed = preg_replace('/\s*```$/', '', $trimmed) ?? $trimmed;

        if (! preg_match('/(\[.*?\])/s', $trimmed, $matches)) {
            return [];
        }

        $decoded = json_decode($matches[1], true);
        if (! is_array($decoded)) {
            return [];
        }

        $tags = [];
        foreach ($decoded as $item) {
            if (! is_string($item)) {
                continue;
            }

            $normalized = $this->normalizer->normalize($item);
            if ($normalized === '') {
                continue;
            }

            $tags[] = $normalized;
        }

        return array_slice(array_values(array_unique($tags)), 0, $maxTags);
    }
}