<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\Artwork;
use App\Services\TagNormalizer;
use App\Services\TagService;
use App\Services\Vision\VisionService;
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 AutoTagArtworkJob implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    /**
     * Keep retries low; tagging must never block publish.
     */
    public int $tries = 3;

    /**
     * Hard timeout safety for queue workers.
     */
    public int $timeout = 20;

    /**
     * @param int $artworkId
     * @param string $hash File hash used to build public derivative URLs.
     */
    public function __construct(
        private readonly int $artworkId,
        private readonly string $hash,
    ) {
        $queue = (string) config('vision.queue', 'default');
        if ($queue !== '') {
            $this->onQueue($queue);
        }
    }

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

    public function handle(TagService $tagService, TagNormalizer $normalizer, ?VisionService $vision = null): void
    {
        $vision ??= app(VisionService::class);

        if (! $vision->isEnabled()) {
            return;
        }

        // Dedup: skip if this artwork+hash was already successfully tagged (7-day window).
        // This prevents redundant processing when the job is enqueued multiple times.
        try {
            if (Redis::exists($this->processedKey($this->artworkId, $this->hash))) {
                return;
            }
        } catch (\Throwable) {
            // Redis unavailable — proceed without dedup guard.
        }

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

        $processingKey = $this->processingKey($this->artworkId, $this->hash);
        if (! $this->acquireProcessingLock($processingKey)) {
            return;
        }

        try {
            $analysis = $vision->analyzeArtwork($artwork, $this->hash);
            if ($analysis === []) {
                return;
            }

            $clipTags = $analysis['clip_tags'] ?? [];
            $yoloTags = $analysis['yolo_objects'] ?? [];

            $vision->persistVisionMetadata(
                $artwork,
                $clipTags,
                $analysis['blip_caption'] ?? null,
                $yoloTags,
            );

            $merged = $vision->mergeTags($clipTags, $yoloTags);
            if ($merged === []) {
                $this->markProcessed($this->processedKey($this->artworkId, $this->hash));
                return;
            }

            // Normalize explicitly (requirement), then attach via TagService (source=ai + confidence).
            $payload = [];
            foreach ($merged as $row) {
                $tag = $normalizer->normalize((string) ($row['tag'] ?? ''));
                if ($tag === '') {
                    continue;
                }
                $payload[] = [
                    'tag' => $tag,
                    'confidence' => isset($row['confidence']) && is_numeric($row['confidence']) ? (float) $row['confidence'] : null,
                ];
            }

            $tagService->attachAiTags($artwork, $payload);

            $this->markProcessed($this->processedKey($this->artworkId, $this->hash));
        } catch (\Throwable $e) {
            Log::error('AutoTagArtworkJob failed', [
                'artwork_id' => $this->artworkId,
                'hash' => $this->hash,
                'attempt' => $this->attempts(),
                'error' => $e->getMessage(),
            ]);

            // Retry-safe: allow queue retry on transient failures.
            throw $e;
        } finally {
            $this->releaseProcessingLock($processingKey);
        }
    }

    private function processingKey(int $artworkId, string $hash): string
    {
        return 'autotag:processing:' . $artworkId . ':' . $hash;
    }

    private function processedKey(int $artworkId, string $hash): string
    {
        return 'autotag:processed:' . $artworkId . ':' . $hash;
    }

    private function acquireProcessingLock(string $key): bool
    {
        try {
            $didSet = Redis::setnx($key, 1);
            if ($didSet) {
                Redis::expire($key, 1800);
            }
            return (bool) $didSet;
        } catch (\Throwable $e) {
            // If Redis is unavailable, proceed without dedupe.
            return true;
        }
    }

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

    private function markProcessed(string $key): void
    {
        try {
            Redis::setex($key, 604800, 1); // 7 days
        } catch (\Throwable $e) {
            // ignore
        }
    }

}
