<?php

declare(strict_types=1);

namespace App\Jobs;

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;

/**
 * Build item-item co-occurrence pairs from user favourites.
 *
 * Spec §7.1 — runs hourly or every few hours.
 * For each user: take last N favourites, create pairs, increment weights.
 *
 * Safety: limits per-user pairs to avoid O(n²) explosion.
 */
final class RecBuildItemPairsFromFavouritesJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

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

    public function __construct(
        private readonly int $userBatchSize = 500,
    ) {
        $queue = (string) config('recommendations.queue', 'default');
        if ($queue !== '') {
            $this->onQueue($queue);
        }
    }

    public function handle(): void
    {
        $favCap = (int) config('recommendations.similarity.user_favourites_cap', 50);

        // ── Pre-compute per-artwork total favourite counts for cosine normalization ──
        $this->artworkLikeCounts = DB::table('artwork_favourites')
            ->select('artwork_id', DB::raw('COUNT(*) as cnt'))
            ->groupBy('artwork_id')
            ->pluck('cnt', 'artwork_id')
            ->all();

        // ── Accumulate co-occurrence counts across all users ──
        $coOccurrenceCounts = [];

        DB::table('artwork_favourites')
            ->select('user_id')
            ->groupBy('user_id')
            ->orderBy('user_id')
            ->chunk($this->userBatchSize, function ($userRows) use ($favCap, &$coOccurrenceCounts) {
                foreach ($userRows as $row) {
                    $pairs = $this->pairsForUser((int) $row->user_id, $favCap);
                    foreach ($pairs as $pair) {
                        $key = $pair[0] . ':' . $pair[1];
                        $coOccurrenceCounts[$key] = ($coOccurrenceCounts[$key] ?? 0) + 1;
                    }
                }
            });

        // ── Normalize to cosine-like scores and flush ──
        $normalized = [];
        foreach ($coOccurrenceCounts as $key => $count) {
            [$a, $b] = explode(':', $key);
            $likesA = $this->artworkLikeCounts[(int) $a] ?? 1;
            $likesB = $this->artworkLikeCounts[(int) $b] ?? 1;
            $normalized[$key] = $count / sqrt($likesA * $likesB);
        }

        $this->flushPairs($normalized);
    }

    /** @var array<int, int> artwork_id => total favourite count */
    private array $artworkLikeCounts = [];

    /**
     * Collect pairs from a single user's last N favourites.
     *
     * @return list<array{0: int, 1: int}>
     */
    public function pairsForUser(int $userId, int $cap): array
    {
        $artworkIds = DB::table('artwork_favourites')
            ->where('user_id', $userId)
            ->orderByDesc('created_at')
            ->limit($cap)
            ->pluck('artwork_id')
            ->map(fn ($id) => (int) $id)
            ->all();

        $count = count($artworkIds);
        if ($count < 2) {
            return [];
        }

        $pairs = [];
        // Cap max pairs per user to avoid explosion: C(50,2) = 1225 worst case = acceptable
        for ($i = 0; $i < $count - 1; $i++) {
            for ($j = $i + 1; $j < $count; $j++) {
                $a = min($artworkIds[$i], $artworkIds[$j]);
                $b = max($artworkIds[$i], $artworkIds[$j]);
                $pairs[] = [$a, $b];
            }
        }

        return $pairs;
    }

    /**
     * Upsert normalized pair weights into rec_item_pairs.
     *
     * Uses Laravel's DB-agnostic upsert (works on MySQL, Postgres, SQLite).
     *
     * @param array<string, float> $upserts key = "a:b", value = cosine-normalized weight
     */
    private function flushPairs(array $upserts): void
    {
        if ($upserts === []) {
            return;
        }

        $now = now();

        foreach (array_chunk($upserts, 500, preserve_keys: true) as $chunk) {
            $rows = [];
            foreach ($chunk as $key => $weight) {
                [$a, $b] = explode(':', $key);
                $rows[] = [
                    'a_artwork_id' => (int) $a,
                    'b_artwork_id' => (int) $b,
                    'weight'       => $weight,
                    'updated_at'   => $now,
                ];
            }

            DB::table('rec_item_pairs')->upsert(
                $rows,
                ['a_artwork_id', 'b_artwork_id'],
                ['weight', 'updated_at'],
            );
        }
    }
}
