<?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 behavior-based (co-like) similarity from precomputed item pairs.
 *
 * Spec §7.3 — runs nightly.
 * For each artwork: read top pairs from rec_item_pairs, store top N.
 */
final class RecComputeSimilarByBehaviorJob 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');
        $resultLimit   = (int) config('recommendations.similarity.result_limit', 30);
        $maxPerAuthor  = (int) config('recommendations.similarity.max_per_author', 2);

        $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 ($modelVersion, $resultLimit, $maxPerAuthor) {
            foreach ($artworks as $artwork) {
                $this->processArtwork($artwork, $modelVersion, $resultLimit, $maxPerAuthor);
            }
        });
    }

    private function processArtwork(
        Artwork $artwork,
        string $modelVersion,
        int $resultLimit,
        int $maxPerAuthor,
    ): void {
        // Fetch top co-occurring artworks (bi-directional)
        $candidates = DB::table('rec_item_pairs')
            ->where('a_artwork_id', $artwork->id)
            ->select(DB::raw('b_artwork_id AS related_id'), 'weight')
            ->union(
                DB::table('rec_item_pairs')
                    ->where('b_artwork_id', $artwork->id)
                    ->select(DB::raw('a_artwork_id AS related_id'), 'weight')
            )
            ->orderByDesc('weight')
            ->limit($resultLimit * 3)
            ->get();

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

        $relatedIds = $candidates->pluck('related_id')->map(fn ($id) => (int) $id)->all();

        // Fetch author info for diversity filtering
        $authorMap = DB::table('artworks')
            ->whereIn('id', $relatedIds)
            ->where('is_public', true)
            ->where('is_approved', true)
            ->whereNotNull('published_at')
            ->where('published_at', '<=', now())
            ->whereNull('deleted_at')
            ->pluck('user_id', 'id')
            ->all();

        // Apply diversity cap
        $authorCounts = [];
        $final = [];
        foreach ($candidates as $cand) {
            $relatedId = (int) $cand->related_id;
            if (! isset($authorMap[$relatedId])) {
                continue; // not public/published
            }
            $authorId = (int) $authorMap[$relatedId];
            $authorCounts[$authorId] = ($authorCounts[$authorId] ?? 0) + 1;
            if ($authorCounts[$authorId] > $maxPerAuthor) {
                continue;
            }
            $final[] = $relatedId;
            if (count($final) >= $resultLimit) {
                break;
            }
        }

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

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