<?php

declare(strict_types=1);

namespace App\Services\Vision;

use App\Models\Artwork;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use RuntimeException;
use Throwable;

final class AiArtworkVectorSearchService
{
    private const MAX_SIMILAR_RESULTS = 120;

    public function __construct(
        private readonly VectorGatewayClient $client,
        private readonly ArtworkVisionImageUrl $imageUrl,
    ) {
    }

    public function isConfigured(): bool
    {
        return $this->client->isConfigured();
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function similarToArtwork(Artwork $artwork, int $limit = 12): array
    {
        $safeLimit = max(1, min(self::MAX_SIMILAR_RESULTS, $limit));
        $cacheKey = sprintf('rec:artwork:%d:similar-ai:%d', $artwork->id, $safeLimit);
        $ttl = max(60, (int) config('recommendations.ttl.similar_artworks', 30 * 60));

        return Cache::remember($cacheKey, $ttl, function () use ($artwork, $safeLimit): array {
            $matches = $this->searchMatchesForArtwork($artwork, $safeLimit + 1);

            return $this->resolveMatches($matches, $safeLimit, $artwork->id);
        });
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function searchByUploadedImage(UploadedFile $file, int $limit = 12): array
    {
        $safeLimit = max(1, min(self::MAX_SIMILAR_RESULTS, $limit));
        $matches = $this->client->searchByUploadedFile($file, $safeLimit);

        return $this->resolveMatches($matches, $safeLimit);
    }

    /**
     * @return array{contents: string, filename: string}|null
     */
    private function downloadArtworkImage(Artwork $artwork, string $url): ?array
    {
        $response = Http::accept('*/*')
            ->connectTimeout(5)
            ->timeout(20)
            ->retry(1, 200, throw: false)
            ->get($url);

        if (! $response->ok()) {
            return null;
        }

        $contents = $response->body();
        if ($contents === '') {
            return null;
        }

        $ext = strtolower(ltrim((string) ($artwork->thumb_ext ?: 'webp'), '.'));

        return [
            'contents' => $contents,
            'filename' => sprintf('artwork-%d.%s', (int) $artwork->id, $ext !== '' ? $ext : 'webp'),
        ];
    }

    /**
     * @return list<array{id: int|string, score: float, metadata: array<string, mixed>}>
     */
    private function searchMatchesForArtwork(Artwork $artwork, int $limit): array
    {
        $url = $this->imageUrl->fromArtwork($artwork);
        if ($url === null || $url === '') {
            return [];
        }

        $fileFailure = null;

        try {
            $payload = $this->downloadArtworkImage($artwork, $url);
            if ($payload !== null) {
                return $this->client->searchByFileContents($payload['contents'], $payload['filename'], $limit);
            }
        } catch (Throwable $e) {
            $fileFailure = $e;
        }

        try {
            return $this->client->searchByUrl($url, $limit);
        } catch (Throwable $e) {
            throw $this->normalizeSearchFailure($fileFailure, $e);
        }
    }

    private function normalizeSearchFailure(?Throwable $fileFailure, Throwable $fallbackFailure): RuntimeException
    {
        if ($fileFailure === null) {
            return $fallbackFailure instanceof RuntimeException
                ? $fallbackFailure
                : new RuntimeException($fallbackFailure->getMessage(), 0, $fallbackFailure);
        }

        return new RuntimeException(sprintf(
            'Vector search failed via file endpoint (%s) and URL fallback (%s).',
            $fileFailure->getMessage(),
            $fallbackFailure->getMessage(),
        ), 0, $fallbackFailure);
    }

    /**
     * @param list<array{id: int|string, score: float, metadata: array<string, mixed>}> $matches
     * @return list<array<string, mixed>>
     */
    private function resolveMatches(array $matches, int $limit, ?int $excludeArtworkId = null): array
    {
        $orderedIds = [];
        $scores = [];

        foreach ($matches as $match) {
            $artworkId = (int) ($match['id'] ?? 0);
            if ($artworkId <= 0) {
                continue;
            }

            if ($excludeArtworkId !== null && $artworkId === $excludeArtworkId) {
                continue;
            }

            if (isset($scores[$artworkId])) {
                continue;
            }

            $orderedIds[] = $artworkId;
            $scores[$artworkId] = (float) ($match['score'] ?? 0.0);
        }

        if ($orderedIds === []) {
            return [];
        }

        $artworks = Artwork::query()
            ->whereIn('id', $orderedIds)
            ->public()
            ->published()
            ->with(['user:id,name', 'user.profile:user_id,avatar_hash'])
            ->get()
            ->keyBy('id');

        $items = [];
        foreach ($orderedIds as $artworkId) {
            /** @var Artwork|null $artwork */
            $artwork = $artworks->get($artworkId);
            if ($artwork === null) {
                continue;
            }

            $items[] = [
                'id' => $artwork->id,
                'title' => $artwork->title,
                'slug' => $artwork->slug,
                'thumb' => $artwork->thumbUrl('sm'),
                'url' => '/art/' . $artwork->id . '/' . $artwork->slug,
                'author' => $artwork->user?->name ?? 'Artist',
                'author_avatar' => $artwork->user?->profile?->avatar_url,
                'author_id' => $artwork->user_id,
                'score' => round((float) ($scores[$artworkId] ?? 0.0), 5),
                'source' => 'vector_gateway',
            ];

            if (count($items) >= $limit) {
                break;
            }
        }

        return $items;
    }
}
