<?php

declare(strict_types=1);

namespace App\Services\Recommendations;

use App\Models\Artwork;
use App\Models\RecArtworkRec;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

/**
 * Runtime service for the Similar Artworks hybrid recommender (spec §8).
 *
 * Flow:
 *   1. Try precomputed similar_hybrid list
 *   2. Else similar_visual (if enabled)
 *   3. Else similar_tags
 *   4. Else similar_behavior
 *   5. Else trending fallback in the same category/content_type
 *
 * Lists are cached in Redis/cache with a configurable TTL.
 * Hydration fetches artworks in one query, preserving stored order.
 * An author-cap diversity filter is applied at runtime as a final check.
 */
final class HybridSimilarArtworksService
{
    private const FALLBACK_ORDER = [
        'similar_hybrid',
        'similar_visual',
        'similar_tags',
        'similar_behavior',
    ];

    /**
     * Get similar artworks for the given artwork.
     *
     * @param  string|null  $type  null|'similar'='hybrid fallback', 'visual', 'tags', 'behavior'
     * @return Collection<int, Artwork>
     */
    public function forArtwork(int $artworkId, int $limit = 12, ?string $type = null): Collection
    {
        $modelVersion = (string) config('recommendations.similarity.model_version', 'sim_v1');
        $cacheTtl     = (int) config('recommendations.similarity.cache_ttl', 6 * 3600);
        $maxPerAuthor = (int) config('recommendations.similarity.max_per_author', 2);
        $vectorEnabled = (bool) config('recommendations.similarity.vector_enabled', false);

        $typeSuffix = $type && $type !== 'similar' ? ":{$type}" : '';
        $cacheKey = "rec:artwork:{$artworkId}:similar:{$modelVersion}{$typeSuffix}";

        $ids = Cache::remember($cacheKey, $cacheTtl, function () use (
            $artworkId, $modelVersion, $vectorEnabled, $type
        ): array {
            return $this->resolveIds($artworkId, $modelVersion, $vectorEnabled, $type);
        });

        if ($ids === []) {
            return collect();
        }

        // Take requested limit + buffer for author-diversity filtering
        $idSlice = array_slice($ids, 0, $limit * 3);

        $artworks = Artwork::query()
            ->whereIn('id', $idSlice)
            ->public()
            ->published()
            ->get()
            ->keyBy('id');

        // Preserve precomputed order + apply author cap
        $authorCounts = [];
        $result = [];

        foreach ($idSlice as $id) {
            /** @var Artwork|null $artwork */
            $artwork = $artworks->get($id);
            if (! $artwork) {
                continue;
            }

            $authorId = $artwork->user_id;
            $authorCounts[$authorId] = ($authorCounts[$authorId] ?? 0) + 1;
            if ($authorCounts[$authorId] > $maxPerAuthor) {
                continue;
            }

            $result[] = $artwork;
            if (count($result) >= $limit) {
                break;
            }
        }

        return collect($result);
    }

    /**
     * Resolve the precomputed ID list, falling through rec types.
     *
     * @return list<int>
     */
    private function resolveIds(int $artworkId, string $modelVersion, bool $vectorEnabled, ?string $type = null): array
    {
        // If a specific type was requested, try only that type + trending fallback
        if ($type && $type !== 'similar') {
            $recType = match ($type) {
                'visual'   => 'similar_visual',
                'tags'     => 'similar_tags',
                'behavior' => 'similar_behavior',
                default    => null,
            };

            if ($recType) {
                $rec = RecArtworkRec::query()
                    ->where('artwork_id', $artworkId)
                    ->where('rec_type', $recType)
                    ->where('model_version', $modelVersion)
                    ->first();

                if ($rec && is_array($rec->recs) && $rec->recs !== []) {
                    return array_map('intval', $rec->recs);
                }
            }

            return $this->trendingFallback($artworkId);
        }

        // Default: hybrid fallback chain
        $tryTypes = $vectorEnabled
            ? self::FALLBACK_ORDER
            : array_filter(self::FALLBACK_ORDER, fn (string $t) => $t !== 'similar_visual');

        foreach ($tryTypes as $recType) {
            $rec = RecArtworkRec::query()
                ->where('artwork_id', $artworkId)
                ->where('rec_type', $recType)
                ->where('model_version', $modelVersion)
                ->first();

            if ($rec && is_array($rec->recs) && $rec->recs !== []) {
                return array_map('intval', $rec->recs);
            }
        }

        // ── Trending fallback (category-scoped) ────────────────────────────────
        return $this->trendingFallback($artworkId);
    }

    /**
     * Trending fallback: fetch recent popular artworks in the same category.
     *
     * @return list<int>
     */
    private function trendingFallback(int $artworkId): array
    {
        $catIds = DB::table('artwork_category')
            ->where('artwork_id', $artworkId)
            ->pluck('category_id')
            ->all();

        $query = Artwork::query()
            ->public()
            ->published()
            ->where('id', '!=', $artworkId);

        if ($catIds !== []) {
            $query->whereHas('categories', function ($q) use ($catIds) {
                $q->whereIn('categories.id', $catIds);
            });
        }

        return $query
            ->orderByDesc('published_at')
            ->limit(30)
            ->pluck('id')
            ->map(fn ($id) => (int) $id)
            ->all();
    }
}
