<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\Artwork;
use App\Models\RecArtworkRec;
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\DB;

/**
 * Compute tag-based (+ category boost) similarity for artworks.
 *
 * Spec §7.2 — runs nightly + on-demand.
 * For each artwork: find candidates by shared tags/category, score with IDF-weighted
 * tag overlap, apply diversity, store top N.
 */
final class RecComputeSimilarByTagsJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries   = 2;
    public int $timeout = 600;

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

    public function handle(): void
    {
        $modelVersion = (string) config('recommendations.similarity.model_version', 'sim_v1');
        $candidatePool = (int) config('recommendations.similarity.candidate_pool', 100);
        $maxPerAuthor  = (int) config('recommendations.similarity.max_per_author', 2);
        $resultLimit   = (int) config('recommendations.similarity.result_limit', 30);

        // ── Tag IDF weights (global) ───────────────────────────────────────────
        $tagFreqs = DB::table('artwork_tag')
            ->select('tag_id', DB::raw('COUNT(*) as cnt'))
            ->groupBy('tag_id')
            ->pluck('cnt', 'tag_id')
            ->all();

        $query = Artwork::query()->public()->published()->select('id', 'user_id');

        if ($this->artworkId !== null) {
            $query->where('id', $this->artworkId);
        }

        $query->chunkById($this->batchSize, function ($artworks) use (
            $tagFreqs, $modelVersion, $candidatePool, $maxPerAuthor, $resultLimit
        ) {
            foreach ($artworks as $artwork) {
                $this->processArtwork($artwork, $tagFreqs, $modelVersion, $candidatePool, $maxPerAuthor, $resultLimit);
            }
        });
    }

    private function processArtwork(
        Artwork $artwork,
        array $tagFreqs,
        string $modelVersion,
        int $candidatePool,
        int $maxPerAuthor,
        int $resultLimit,
    ): void {
        // Get source artwork's tags and categories
        $srcTagIds = DB::table('artwork_tag')
            ->where('artwork_id', $artwork->id)
            ->pluck('tag_id')
            ->all();

        $srcCatIds = DB::table('artwork_category')
            ->where('artwork_id', $artwork->id)
            ->pluck('category_id')
            ->all();

        // Source content_type_ids (via categories)
        $srcContentTypeIds = $srcCatIds !== []
            ? DB::table('categories')
                ->whereIn('id', $srcCatIds)
                ->whereNotNull('content_type_id')
                ->pluck('content_type_id')
                ->unique()
                ->all()
            : [];

        if ($srcTagIds === [] && $srcCatIds === []) {
            return;
        }

        // ── Find candidates that share at least one tag ────────────────────────
        $candidateQuery = DB::table('artwork_tag')
            ->join('artworks', 'artworks.id', '=', 'artwork_tag.artwork_id')
            ->whereIn('artwork_tag.tag_id', $srcTagIds)
            ->where('artwork_tag.artwork_id', '!=', $artwork->id)
            ->where('artworks.is_public', true)
            ->where('artworks.is_approved', true)
            ->whereNotNull('artworks.published_at')
            ->where('artworks.published_at', '<=', now())
            ->whereNull('artworks.deleted_at')
            ->select('artwork_tag.artwork_id', 'artworks.user_id')
            ->groupBy('artwork_tag.artwork_id', 'artworks.user_id')
            ->orderByRaw('COUNT(*) DESC')
            ->limit($candidatePool * 3); // over-fetch before scoring

        $candidates = $candidateQuery->get();

        if ($candidates->isEmpty()) {
            return;
        }

        // Gather tags for all candidates in one query
        $candidateIds = $candidates->pluck('artwork_id')->all();
        $candidateTagMap = DB::table('artwork_tag')
            ->whereIn('artwork_id', $candidateIds)
            ->select('artwork_id', 'tag_id')
            ->get()
            ->groupBy('artwork_id');

        $candidateCatMap = DB::table('artwork_category')
            ->whereIn('artwork_id', $candidateIds)
            ->select('artwork_id', 'category_id')
            ->get()
            ->groupBy('artwork_id');

        // Build content_type_id lookup for candidates (via categories table)
        $allCandidateCatIds = $candidateCatMap->flatten(1)->pluck('category_id')->unique()->all();
        $catContentTypeMap = $allCandidateCatIds !== []
            ? DB::table('categories')
                ->whereIn('id', $allCandidateCatIds)
                ->whereNotNull('content_type_id')
                ->pluck('content_type_id', 'id')
                ->all()
            : [];
        $srcContentTypeSet = array_flip($srcContentTypeIds);

        $srcTagSet = array_flip($srcTagIds);
        $srcCatSet = array_flip($srcCatIds);

        // ── Score each candidate ───────────────────────────────────────────────
        $scored = [];
        foreach ($candidates as $cand) {
            $cTagIds = $candidateTagMap->get($cand->artwork_id, collect())->pluck('tag_id')->all();
            $cCatIds = $candidateCatMap->get($cand->artwork_id, collect())->pluck('category_id')->all();

            // IDF-weighted tag overlap (spec §5.1)
            $tagScore = 0.0;
            foreach ($cTagIds as $tagId) {
                if (isset($srcTagSet[$tagId])) {
                    $freq = $tagFreqs[$tagId] ?? 1;
                    $tagScore += 1.0 / log(2 + $freq);
                }
            }

            // Category match bonus
            $catScore = 0.0;
            foreach ($cCatIds as $catId) {
                if (isset($srcCatSet[$catId])) {
                    $catScore = 1.0;
                    break;
                }
            }

            // Content type match bonus (spec §5.1)
            $ctScore = 0.0;
            foreach ($cCatIds as $catId) {
                $ctId = $catContentTypeMap[$catId] ?? null;
                if ($ctId !== null && isset($srcContentTypeSet[$ctId])) {
                    $ctScore = 1.0;
                    break;
                }
            }

            $scored[] = [
                'artwork_id' => (int) $cand->artwork_id,
                'user_id'    => (int) $cand->user_id,
                'tag_score'  => $tagScore,
                'cat_score'  => $catScore,
                'score'      => $tagScore + $catScore * 0.1 + $ctScore * 0.05,
            ];
        }

        // Sort by score descending
        usort($scored, fn (array $a, array $b) => $b['score'] <=> $a['score']);

        // ── Apply diversity (max per author) ───────────────────────────────────
        $authorCounts = [];
        $final = [];
        foreach ($scored as $item) {
            $authorId = $item['user_id'];
            $authorCounts[$authorId] = ($authorCounts[$authorId] ?? 0) + 1;
            if ($authorCounts[$authorId] > $maxPerAuthor) {
                continue;
            }
            $final[] = $item['artwork_id'];
            if (count($final) >= $resultLimit) {
                break;
            }
        }

        // ── Persist ────────────────────────────────────────────────────────────
        RecArtworkRec::query()->updateOrCreate(
            [
                'artwork_id'    => $artwork->id,
                'rec_type'      => 'similar_tags',
                'model_version' => $modelVersion,
            ],
            [
                'recs'        => $final,
                'computed_at' => now(),
            ],
        );
    }
}
