<?php

declare(strict_types=1);

namespace App\Jobs;

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;

class RankBuildScopeListsJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

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

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

    public function __construct(
        public readonly string $scopeType,
        public readonly int $scopeId,
    ) {}

    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);

        foreach (self::LIST_TYPES as $listType) {
            $this->buildAndStore(
                $ranking,
                $listType,
                $this->scopeType,
                $this->scopeId,
                $modelVersion,
                $maxPerAuthor,
                $listSize,
                $candidatePool
            );
        }

        Log::info('RankBuildScopeListsJob: finished', [
            'scope_type'    => $this->scopeType,
            'scope_id'      => $this->scopeId,
            'model_version' => $modelVersion,
        ]);
    }

    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
        );

        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']
        );

        $ranking->bustCache($scopeType, $scopeId === 0 ? null : $scopeId, $listType);
    }

    /**
     * @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();
    }

    private function scoreColumn(string $listType): string
    {
        return match ($listType) {
            'new_hot' => 'score_new_hot',
            'best' => 'score_best',
            default => 'score_trending',
        };
    }
}