<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\Artwork;
use App\Models\ArtworkEmbedding;
use App\Services\Vision\ArtworkEmbeddingClient;
use App\Services\Vision\ArtworkVisionImageUrl;
use App\Services\Vision\ArtworkVectorIndexService;
use App\Services\Vision\VectorService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;

final class GenerateArtworkEmbeddingJob implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public int $tries = 3;

    public int $timeout = 20;

    public function __construct(
        private readonly int $artworkId,
        private readonly ?string $sourceHash = null,
        private readonly bool $force = false,
    ) {
        $queue = (string) config('recommendations.queue', config('vision.queue', 'default'));
        if ($queue !== '') {
            $this->onQueue($queue);
        }
    }

    public function backoff(): array
    {
        return [2, 10, 30];
    }

    public function handle(
        ArtworkEmbeddingClient $client,
        ArtworkVisionImageUrl $imageUrlBuilder,
        ArtworkVectorIndexService $vectors,
    ): void
    {
        if (! (bool) config('recommendations.embedding.enabled', true)) {
            return;
        }

        $artwork = Artwork::query()
            ->with(['categories' => fn ($categories) => $categories->with('contentType')->orderBy('sort_order')->orderBy('name')])
            ->find($this->artworkId);
        if (! $artwork) {
            return;
        }

        $sourceHash = strtolower((string) preg_replace('/[^a-z0-9]/', '', (string) ($this->sourceHash ?? $artwork->hash ?? '')));
        if ($sourceHash === '') {
            return;
        }

        $model = (string) config('recommendations.embedding.model', 'clip');
        $modelVersion = (string) config('recommendations.embedding.model_version', 'v1');
        $algoVersion = (string) config('recommendations.embedding.algo_version', 'clip-cosine-v1');

        if (! $this->force) {
            $existing = ArtworkEmbedding::query()
                ->where('artwork_id', $artwork->id)
                ->where('model', $model)
                ->where('model_version', $modelVersion)
                ->first();

            if ($existing && (string) ($existing->source_hash ?? '') === $sourceHash) {
                return;
            }
        }

        $lockKey = $this->lockKey($artwork->id, $model, $modelVersion);
        if (! $this->acquireLock($lockKey)) {
            return;
        }

        try {
            $imageUrl = $imageUrlBuilder->fromArtwork($artwork);
            if ($imageUrl === null) {
                return;
            }

            $vector = $client->embed($imageUrl, (int) $artwork->id, $sourceHash);
            if ($vector === []) {
                return;
            }

            $normalized = $this->normalize($vector);

            ArtworkEmbedding::query()->updateOrCreate(
                [
                    'artwork_id' => (int) $artwork->id,
                    'model' => $model,
                    'model_version' => $modelVersion,
                ],
                [
                    'algo_version' => $algoVersion,
                    'dim' => count($normalized),
                    'embedding_json' => json_encode($normalized, JSON_THROW_ON_ERROR),
                    'source_hash' => $sourceHash,
                    'is_normalized' => true,
                    'generated_at' => now(),
                    'meta' => [
                        'source' => 'clip',
                        'image_variant' => (string) config('vision.image_variant', 'md'),
                    ],
                ]
            );

            $this->upsertVectorIndex($vectors, $artwork);
        } finally {
            $this->releaseLock($lockKey);
        }
    }

    private function upsertVectorIndex(
        ArtworkVectorIndexService $vectors,
        Artwork $artwork
    ): void
    {
        if (! $vectors->isConfigured()) {
            return;
        }

        try {
            $vectors->upsertArtwork($artwork);
        } catch (\Throwable $e) {
            Log::warning('GenerateArtworkEmbeddingJob vector upsert failed', [
                'artwork_id' => (int) $artwork->id,
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * @param array<int, float> $vector
     * @return array<int, float>
     */
    private function normalize(array $vector): array
    {
        $sumSquares = 0.0;
        foreach ($vector as $value) {
            $sumSquares += ($value * $value);
        }

        if ($sumSquares <= 0.0) {
            return $vector;
        }

        $norm = sqrt($sumSquares);
        return array_map(static fn (float $value): float => $value / $norm, $vector);
    }

    private function lockKey(int $artworkId, string $model, string $version): string
    {
        return 'artwork-embedding:lock:' . $artworkId . ':' . $model . ':' . $version;
    }

    private function acquireLock(string $key): bool
    {
        try {
            $didSet = Redis::setnx($key, 1);
            if ($didSet) {
                Redis::expire($key, 1800);
            }
            return (bool) $didSet;
        } catch (\Throwable) {
            return true;
        }
    }

    private function releaseLock(string $key): void
    {
        try {
            Redis::del($key);
        } catch (\Throwable) {
            // ignore
        }
    }
}
