<?php

declare(strict_types=1);

namespace App\Services\Recommendation;

use App\DTOs\UserRecoProfileDTO;
use App\Models\User;
use App\Models\UserRecoProfile;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * UserPreferenceBuilder
 *
 * Builds (and caches) a UserRecoProfileDTO for a given user by aggregating
 * signals from their interaction history:
 *
 *   Signal                    Weight (default)
 *   ───────────────────────── ────────────────
 *   Award given (gold/silver) +5
 *   Artwork favourited        +3
 *   Reaction given            +2
 *   Creator followed (tags)   +2 for their tags
 *   View recorded             +1
 *
 * The result is persisted to `user_reco_profiles` (6-hour TTL by default) so
 * subsequent requests in the same window skip all DB queries.
 *
 * Usage:
 *   $builder = app(UserPreferenceBuilder::class);
 *   $dto     = $builder->build($user);          // cached
 *   $dto     = $builder->buildFresh($user);     // force rebuild
 */
class UserPreferenceBuilder
{
    // Redis / file cache key (short-lived insurance layer on top of DB row)
    private const REDIS_TTL = 300; // 5 minutes — warm cache after DB write

    public function __construct() {}

    // ─── Public API ───────────────────────────────────────────────────────────

    /**
     * Return a cached profile DTO, rebuilding from DB if stale or absent.
     */
    public function build(User $user): UserRecoProfileDTO
    {
        $cacheKey = $this->cacheKey($user->id);

        // 1. Redis warm layer
        /** @var array<string,mixed>|null $cached */
        $cached = Cache::get($cacheKey);
        if ($cached !== null) {
            return UserRecoProfileDTO::fromArray($cached);
        }

        // 2. Persistent DB row
        $row = UserRecoProfile::find($user->id);
        if ($row !== null && $row->isFresh()) {
            $dto = $row->toDTO();
            Cache::put($cacheKey, $dto->toArray(), self::REDIS_TTL);
            return $dto;
        }

        // 3. Rebuild
        return $this->buildFresh($user);
    }

    /**
     * Force a full rebuild from source tables, persist and cache the result.
     */
    public function buildFresh(User $user): UserRecoProfileDTO
    {
        try {
            $dto = $this->compute($user);
            $this->persist($user->id, $dto);
            Cache::put($this->cacheKey($user->id), $dto->toArray(), self::REDIS_TTL);
            return $dto;
        } catch (\Throwable $e) {
            Log::warning('UserPreferenceBuilder: failed to build profile', [
                'user_id' => $user->id,
                'error'   => $e->getMessage(),
            ]);
            return new UserRecoProfileDTO();
        }
    }

    /**
     * Invalidate only the Redis warm layer (DB row stays intact until TTL).
     */
    public function invalidate(int $userId): void
    {
        Cache::forget($this->cacheKey($userId));
    }

    // ─── Computation ──────────────────────────────────────────────────────────

    private function compute(User $user): UserRecoProfileDTO
    {
        $sigWeights   = (array)  config('recommendations.signal_weights', []);
        $wAward       = (float) ($sigWeights['award']    ?? 5.0);
        $wFav         = (float) ($sigWeights['favorite'] ?? 3.0);
        $wFollow      = (float) ($sigWeights['follow']   ?? 2.0);

        $tagLimit      = (int) config('recommendations.profile.top_tags_limit',        20);
        $catLimit      = (int) config('recommendations.profile.top_categories_limit',   5);
        $creatorLimit  = (int) config('recommendations.profile.strong_creators_limit', 50);

        // ── 1. Tag scores from favourited artworks ────────────────────────────
        $tagRaw = $this->tagScoresFromFavourites($user->id, $wFav);

        // ── 2. Tag scores from awards given ──────────────────────────────────
        foreach ($this->tagScoresFromAwards($user->id, $wAward) as $slug => $score) {
            $tagRaw[$slug] = ($tagRaw[$slug] ?? 0.0) + $score;
        }

        // ── 3. Creator IDs from follows (top N) ───────────────────────────────
        $followedIds = $this->followedCreatorIds($user->id, $creatorLimit);

        // ── 4. Tag scores lifted from followed creators' recent works ─────────
        if ($followedIds !== []) {
            foreach ($this->tagScoresFromFollowedCreators($followedIds, $wFollow) as $slug => $score) {
                $tagRaw[$slug] = ($tagRaw[$slug] ?? 0.0) + $score;
            }
        }

        // ── 5. Category scores from favourited artworks ───────────────────────
        $catRaw = $this->categoryScoresFromFavourites($user->id, $wFav);

        // Sort descending and take top N
        arsort($tagRaw);
        arsort($catRaw);

        $topTagSlugs  = array_keys(array_slice($tagRaw, 0, $tagLimit));
        $topCatSlugs  = array_keys(array_slice($catRaw, 0, $catLimit));

        $tagWeights = $this->normalise($tagRaw);
        $catWeights = $this->normalise($catRaw);

        return new UserRecoProfileDTO(
            topTagSlugs:      $topTagSlugs,
            topCategorySlugs: $topCatSlugs,
            strongCreatorIds: $followedIds,
            tagWeights:       $tagWeights,
            categoryWeights:  $catWeights,
        );
    }

    // ─── Signal collectors ────────────────────────────────────────────────────

    /**
     * @return array<string, float>  slug → raw score
     */
    private function tagScoresFromFavourites(int $userId, float $weight): array
    {
        $rows = DB::table('artwork_favourites as af')
            ->join('artwork_tag as at', 'at.artwork_id', '=', 'af.artwork_id')
            ->join('tags as t', 't.id', '=', 'at.tag_id')
            ->where('af.user_id', $userId)
            ->where('t.is_active', true)
            ->selectRaw('t.slug, COUNT(*) as cnt')
            ->groupBy('t.id', 't.slug')
            ->get();

        $scores = [];
        foreach ($rows as $row) {
            $scores[(string) $row->slug] = (float) $row->cnt * $weight;
        }

        return $scores;
    }

    /**
     * @return array<string, float>
     */
    private function tagScoresFromAwards(int $userId, float $weight): array
    {
        $rows = DB::table('artwork_medals as aa')
            ->join('artwork_tag as at', 'at.artwork_id', '=', 'aa.artwork_id')
            ->join('tags as t', 't.id', '=', 'at.tag_id')
            ->where('aa.user_id', $userId)
            ->where('t.is_active', true)
            ->selectRaw('t.slug, SUM(aa.weight) as total_weight')
            ->groupBy('t.id', 't.slug')
            ->get();

        $scores = [];
        foreach ($rows as $row) {
            $scores[(string) $row->slug] = (float) $row->total_weight * $weight;
        }

        return $scores;
    }

    /**
     * @param  array<int, int> $creatorIds
     * @return array<string, float>
     */
    private function tagScoresFromFollowedCreators(array $creatorIds, float $weight): array
    {
        if ($creatorIds === []) {
            return [];
        }

        // Sample the 500 most-recent artworks first (subquery), then count tags.
        // ORDER BY must not appear on a non-aggregated column inside GROUP BY
        // (MySQL only_full_group_by mode rejects it).
        $recentIds = DB::table('artworks')
            ->whereIn('user_id', $creatorIds)
            ->where('is_public', true)
            ->where('is_approved', true)
            ->whereNull('deleted_at')
            ->orderByDesc('published_at')
            ->limit(500)
            ->select('id');

        $rows = DB::table('artwork_tag as at')
            ->joinSub($recentIds, 'a', 'a.id', '=', 'at.artwork_id')
            ->join('tags as t', 't.id', '=', 'at.tag_id')
            ->where('t.is_active', true)
            ->selectRaw('t.slug, COUNT(*) as cnt')
            ->groupBy('t.id', 't.slug')
            ->get();

        $scores = [];
        foreach ($rows as $row) {
            $scores[(string) $row->slug] = (float) $row->cnt * $weight;
        }

        return $scores;
    }

    /**
     * @return array<string, float>
     */
    private function categoryScoresFromFavourites(int $userId, float $weight): array
    {
        $rows = DB::table('artwork_favourites as af')
            ->join('artwork_category as ac', 'ac.artwork_id', '=', 'af.artwork_id')
            ->join('categories as c', 'c.id', '=', 'ac.category_id')
            ->where('af.user_id', $userId)
            ->whereNull('c.deleted_at')
            ->selectRaw('c.slug, COUNT(*) as cnt')
            ->groupBy('c.id', 'c.slug')
            ->get();

        $scores = [];
        foreach ($rows as $row) {
            $scores[(string) $row->slug] = (float) $row->cnt * $weight;
        }

        return $scores;
    }

    /**
     * @return array<int, int>
     */
    private function followedCreatorIds(int $userId, int $limit): array
    {
        return DB::table('user_followers')
            ->where('follower_id', $userId)
            ->orderByDesc('created_at')
            ->limit($limit)
            ->pluck('user_id')
            ->map(fn ($id) => (int) $id)
            ->all();
    }

    // ─── Helpers ──────────────────────────────────────────────────────────────

    /**
     * Normalise a raw score map to sum 1.0.
     *
     * @param array<string, float> $raw
     * @return array<string, float>
     */
    private function normalise(array $raw): array
    {
        $sum = array_sum($raw);
        if ($sum <= 0.0) {
            return $raw;
        }

        return array_map(fn (float $v): float => round($v / $sum, 6), $raw);
    }

    private function cacheKey(int $userId): string
    {
        return "user_reco_profile:{$userId}";
    }

    // ─── Persistence ──────────────────────────────────────────────────────────

    private function persist(int $userId, UserRecoProfileDTO $dto): void
    {
        $data = $dto->toArray();

        UserRecoProfile::query()->updateOrCreate(
            ['user_id' => $userId],
            [
                'top_tags_json'             => $data['top_tags'],
                'top_categories_json'       => $data['top_categories'],
                'followed_creator_ids_json' => $data['strong_creators'],
                'tag_weights_json'          => $data['tag_weights'],
                'category_weights_json'     => $data['category_weights'],
                'disliked_tag_ids_json'     => $data['disliked_tags'],
            ]
        );
    }
}
