<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\Category;
use App\Models\ContentType;
use App\Models\RankList;
use App\Services\RankingService;
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;
use Illuminate\Support\Facades\Log;

/**
 * RankBuildListsJob
 *
 * Runs hourly (after RankComputeArtworkScoresJob).
 *
 * Builds ordered artwork_id arrays for:
 *  • global        – trending, new_hot, best
 *  • each category – trending, new_hot, best
 *  • each content_type – trending, new_hot, best
 *
 * Applies author-diversity cap (max 3 per author in a list of 50).
 * Stores results in rank_lists and busts relevant Redis keys.
 */
class RankBuildListsJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

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

    private const LIST_TYPES = ['trending', 'new_hot', 'best'];

    public function handle(RankingService $ranking): void
    {
        $modelVersion  = config('ranking.model_version', 'rank_v1');
        $maxPerAuthor  = (int) config('ranking.diversity.max_per_author', 3);
        $listSize      = (int) config('ranking.diversity.list_size', 50);
        $candidatePool = (int) config('ranking.diversity.candidate_pool', 200);
        $listsBuilt    = 0;

        // ── 1. Global ──────────────────────────────────────────────────────
        foreach (self::LIST_TYPES as $listType) {
            $this->buildAndStore(
                $ranking, $listType, 'global', 0,
                $modelVersion, $maxPerAuthor, $listSize, $candidatePool
            );
            $listsBuilt++;
        }

        // ── 2. Per category ────────────────────────────────────────────────
        Category::query()
            ->select(['id'])
            ->where('is_active', true)
            ->orderBy('id')
            ->chunk(200, function ($categories) use (
                $ranking, $modelVersion, $maxPerAuthor, $listSize, $candidatePool, &$listsBuilt
            ): void {
                foreach ($categories as $cat) {
                    foreach (self::LIST_TYPES as $listType) {
                        $this->buildAndStore(
                            $ranking, $listType, 'category', $cat->id,
                            $modelVersion, $maxPerAuthor, $listSize, $candidatePool
                        );
                        $listsBuilt++;
                    }
                }
            });

        // ── 3. Per content type ────────────────────────────────────────────
        ContentType::query()
            ->select(['id'])
            ->orderBy('id')
            ->chunk(50, function ($ctypes) use (
                $ranking, $modelVersion, $maxPerAuthor, $listSize, $candidatePool, &$listsBuilt
            ): void {
                foreach ($ctypes as $ct) {
                    foreach (self::LIST_TYPES as $listType) {
                        $this->buildAndStore(
                            $ranking, $listType, 'content_type', $ct->id,
                            $modelVersion, $maxPerAuthor, $listSize, $candidatePool
                        );
                        $listsBuilt++;
                    }
                }
            });

        Log::info('RankBuildListsJob: finished', [
            'lists_built'   => $listsBuilt,
            'model_version' => $modelVersion,
        ]);
    }

    // ── Private helpers ────────────────────────────────────────────────────

    /**
     * Fetch candidates, apply diversity, and upsert the resulting list.
     */
    private function buildAndStore(
        RankingService $ranking,
        string $listType,
        string $scopeType,
        int $scopeId,
        string $modelVersion,
        int $maxPerAuthor,
        int $listSize,
        int $candidatePool
    ): void {
        $scoreCol   = $this->scoreColumn($listType);
        $candidates = $this->fetchCandidates(
            $scopeType, $scopeId, $scoreCol, $candidatePool, $modelVersion
        );

        $diverse = $ranking->applyDiversity(
            $candidates->all(), $maxPerAuthor, $listSize
        );

        $ids = array_map(
            fn ($item) => (int) ($item->artwork_id ?? $item['artwork_id']),
            $diverse
        );

        // Upsert the list (unique: scope_type + scope_id + list_type + model_version)
        DB::table('rank_lists')->upsert(
            [[
                'scope_type'    => $scopeType,
                'scope_id'      => $scopeId,
                'list_type'     => $listType,
                'model_version' => $modelVersion,
                'artwork_ids'   => json_encode($ids),
                'computed_at'   => now()->toDateTimeString(),
            ]],
            ['scope_type', 'scope_id', 'list_type', 'model_version'],
            ['artwork_ids', 'computed_at']
        );

        // Bust Redis cache so next request picks up the new list
        $ranking->bustCache($scopeType, $scopeId === 0 ? null : $scopeId, $listType);
    }

    /**
     * Fetch top N candidates (with user_id) for a given scope/score column.
     *
     * @return \Illuminate\Support\Collection<int, object>
     */
    private function fetchCandidates(
        string $scopeType,
        int $scopeId,
        string $scoreCol,
        int $limit,
        string $modelVersion
    ): \Illuminate\Support\Collection {
        $query = DB::table('rank_artwork_scores as ras')
            ->select(['ras.artwork_id', 'a.user_id', "ras.{$scoreCol}"])
            ->join('artworks as a', function ($join): void {
                $join->on('a.id', '=', 'ras.artwork_id')
                     ->where('a.is_public', 1)
                     ->where('a.is_approved', 1)
                     ->whereNull('a.deleted_at');
            })
            ->where('ras.model_version', $modelVersion)
            ->orderByDesc("ras.{$scoreCol}")
            ->limit($limit);

        if ($scopeType === 'category' && $scopeId > 0) {
            $query->join(
                'artwork_category as ac',
                fn ($j) => $j->on('ac.artwork_id', '=', 'a.id')
                              ->where('ac.category_id', $scopeId)
            );
        }

        if ($scopeType === 'content_type' && $scopeId > 0) {
            $query->join(
                'artwork_category as ac',
                'ac.artwork_id', '=', 'a.id'
            )->join(
                'categories as cat',
                fn ($j) => $j->on('cat.id', '=', 'ac.category_id')
                              ->where('cat.content_type_id', $scopeId)
                              ->whereNull('cat.deleted_at')
            );
        }

        return $query->get();
    }

    /**
     * Map list_type to the rank_artwork_scores column name.
     */
    private function scoreColumn(string $listType): string
    {
        return match ($listType) {
            'new_hot' => 'score_new_hot',
            'best'    => 'score_best',
            default   => 'score_trending',
        };
    }
}
