<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Artwork;
use App\Models\ArtworkMetricSnapshotHourly;
use App\Models\Group;
use App\Models\Leaderboard;
use App\Models\Story;
use App\Models\StoryLike;
use App\Models\StoryView;
use App\Models\User;
use App\Models\World;
use App\Models\WorldSubmission;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class LeaderboardService
{
    private const CACHE_TTL_SECONDS = 3600;
    private const CREATOR_STORE_LIMIT = 10000;
    private const ENTITY_STORE_LIMIT = 500;

    public function __construct(
        private readonly GroupReputationService $groupReputation,
    ) {
    }

    public function calculateCreatorLeaderboard(string $period): int
    {
        $normalizedPeriod = $this->normalizePeriod($period);
        $rows = $normalizedPeriod === Leaderboard::PERIOD_ALL_TIME
            ? $this->allTimeCreatorRows()
            : $this->windowedCreatorRows($this->periodStart($normalizedPeriod));

        return $this->persistRows(Leaderboard::TYPE_CREATOR, $normalizedPeriod, $rows, self::CREATOR_STORE_LIMIT);
    }

    public function calculateArtworkLeaderboard(string $period): int
    {
        $normalizedPeriod = $this->normalizePeriod($period);
        $rows = $normalizedPeriod === Leaderboard::PERIOD_ALL_TIME
            ? $this->allTimeArtworkRows()
            : $this->windowedArtworkRows($this->periodStart($normalizedPeriod));

        return $this->persistRows(Leaderboard::TYPE_ARTWORK, $normalizedPeriod, $rows, self::ENTITY_STORE_LIMIT);
    }

    public function calculateStoryLeaderboard(string $period): int
    {
        $normalizedPeriod = $this->normalizePeriod($period);
        $rows = $normalizedPeriod === Leaderboard::PERIOD_ALL_TIME
            ? $this->allTimeStoryRows()
            : $this->windowedStoryRows($this->periodStart($normalizedPeriod));

        return $this->persistRows(Leaderboard::TYPE_STORY, $normalizedPeriod, $rows, self::ENTITY_STORE_LIMIT);
    }

    public function calculateGroupLeaderboard(string $period): int
    {
        $normalizedPeriod = $this->normalizePeriod($period);
        $rows = $normalizedPeriod === Leaderboard::PERIOD_ALL_TIME
            ? $this->allTimeGroupRows()
            : $this->windowedGroupRows($this->periodStart($normalizedPeriod));

        return $this->persistRows(Leaderboard::TYPE_GROUP, $normalizedPeriod, $rows, self::ENTITY_STORE_LIMIT);
    }

    public function calculateWorldLeaderboard(string $period): int
    {
        $normalizedPeriod = $this->normalizePeriod($period);
        $rows = $normalizedPeriod === Leaderboard::PERIOD_ALL_TIME
            ? $this->allTimeWorldRows()
            : $this->windowedWorldRows($this->periodStart($normalizedPeriod));

        return $this->persistRows(Leaderboard::TYPE_WORLD, $normalizedPeriod, $rows, self::ENTITY_STORE_LIMIT);
    }

    public function refreshAll(): array
    {
        $results = [];

        foreach ([
            Leaderboard::TYPE_CREATOR,
            Leaderboard::TYPE_ARTWORK,
            Leaderboard::TYPE_GROUP,
            Leaderboard::TYPE_STORY,
            Leaderboard::TYPE_WORLD,
        ] as $type) {
            foreach ($this->periods() as $period) {
                $results[$type][$period] = match ($type) {
                    Leaderboard::TYPE_CREATOR => $this->calculateCreatorLeaderboard($period),
                    Leaderboard::TYPE_ARTWORK => $this->calculateArtworkLeaderboard($period),
                    Leaderboard::TYPE_GROUP => $this->calculateGroupLeaderboard($period),
                    Leaderboard::TYPE_STORY => $this->calculateStoryLeaderboard($period),
                    Leaderboard::TYPE_WORLD => $this->calculateWorldLeaderboard($period),
                };
            }
        }

        return $results;
    }

    public function getLeaderboard(string $type, string $period, int $limit = 50): array
    {
        $normalizedType = $this->normalizeType($type);
        $normalizedPeriod = $this->normalizePeriod($period);
        $limit = max(1, min($limit, 100));

        return Cache::remember(
            $this->cacheKey($normalizedType, $normalizedPeriod, $limit),
            self::CACHE_TTL_SECONDS,
            function () use ($normalizedType, $normalizedPeriod, $limit): array {
                $items = $this->leaderboardRows($normalizedType, $normalizedPeriod, $limit);

                if ($items->isEmpty()) {
                    $this->generateLeaderboard($normalizedType, $normalizedPeriod);
                    $items = $this->leaderboardRows($normalizedType, $normalizedPeriod, $limit);
                }

                if ($items->isEmpty()) {
                    return [
                        'type' => $normalizedType,
                        'period' => $normalizedPeriod,
                        'items' => [],
                    ];
                }

                $entities = match ($normalizedType) {
                    Leaderboard::TYPE_CREATOR => $this->creatorEntities($items->pluck('entity_id')->all()),
                    Leaderboard::TYPE_ARTWORK => $this->artworkEntities($items->pluck('entity_id')->all()),
                    Leaderboard::TYPE_GROUP => $this->groupEntities($items->pluck('entity_id')->all()),
                    Leaderboard::TYPE_STORY => $this->storyEntities($items->pluck('entity_id')->all()),
                    Leaderboard::TYPE_WORLD => $this->worldEntities($items->pluck('entity_id')->all()),
                };

                return [
                    'type' => $normalizedType,
                    'period' => $normalizedPeriod,
                    'items' => $items->values()->map(function (Leaderboard $row, int $index) use ($entities): array {
                        return [
                            'rank' => $index + 1,
                            'score' => round((float) $row->score, 1),
                            'entity' => $entities[(int) $row->entity_id] ?? null,
                        ];
                    })->filter(fn (array $item): bool => $item['entity'] !== null)->values()->all(),
                ];
            }
        );
    }

    public function creatorRankSummary(int $userId, string $period = Leaderboard::PERIOD_WEEKLY): ?array
    {
        $normalizedPeriod = $this->normalizePeriod($period);

        return Cache::remember(
            sprintf('leaderboard:creator-rank:%d:%s', $userId, $normalizedPeriod),
            self::CACHE_TTL_SECONDS,
            function () use ($userId, $normalizedPeriod): ?array {
                $row = Leaderboard::query()
                    ->where('type', Leaderboard::TYPE_CREATOR)
                    ->where('period', $normalizedPeriod)
                    ->where('entity_id', $userId)
                    ->first(['entity_id', 'score']);

                if (! $row) {
                    return null;
                }

                $higherScores = Leaderboard::query()
                    ->where('type', Leaderboard::TYPE_CREATOR)
                    ->where('period', $normalizedPeriod)
                    ->where(function ($query) use ($row): void {
                        $query->where('score', '>', $row->score)
                            ->orWhere(function ($ties) use ($row): void {
                                $ties->where('score', '=', $row->score)
                                    ->where('entity_id', '<', $row->entity_id);
                            });
                    })
                    ->count();

                return [
                    'period' => $normalizedPeriod,
                    'rank' => $higherScores + 1,
                    'score' => round((float) $row->score, 1),
                ];
            }
        );
    }

    public function periods(): array
    {
        return [
            Leaderboard::PERIOD_DAILY,
            Leaderboard::PERIOD_WEEKLY,
            Leaderboard::PERIOD_MONTHLY,
            Leaderboard::PERIOD_ALL_TIME,
        ];
    }

    public function normalizePeriod(string $period): string
    {
        return match (strtolower(trim($period))) {
            'daily' => Leaderboard::PERIOD_DAILY,
            'weekly' => Leaderboard::PERIOD_WEEKLY,
            'monthly' => Leaderboard::PERIOD_MONTHLY,
            'all', 'all_time', 'all-time' => Leaderboard::PERIOD_ALL_TIME,
            default => Leaderboard::PERIOD_WEEKLY,
        };
    }

    private function normalizeType(string $type): string
    {
        return match (strtolower(trim($type))) {
            'creator', 'creators' => Leaderboard::TYPE_CREATOR,
            'artwork', 'artworks' => Leaderboard::TYPE_ARTWORK,
            'group', 'groups' => Leaderboard::TYPE_GROUP,
            'story', 'stories' => Leaderboard::TYPE_STORY,
            'world', 'worlds' => Leaderboard::TYPE_WORLD,
            default => Leaderboard::TYPE_CREATOR,
        };
    }

    private function leaderboardRows(string $type, string $period, int $limit): Collection
    {
        return Leaderboard::query()
            ->where('type', $type)
            ->where('period', $period)
            ->orderByDesc('score')
            ->orderBy('entity_id')
            ->limit($limit)
            ->get(['entity_id', 'score'])
            ->values();
    }

    private function generateLeaderboard(string $type, string $period): void
    {
        match ($type) {
            Leaderboard::TYPE_CREATOR => $this->calculateCreatorLeaderboard($period),
            Leaderboard::TYPE_ARTWORK => $this->calculateArtworkLeaderboard($period),
            Leaderboard::TYPE_GROUP => $this->calculateGroupLeaderboard($period),
            Leaderboard::TYPE_STORY => $this->calculateStoryLeaderboard($period),
            Leaderboard::TYPE_WORLD => $this->calculateWorldLeaderboard($period),
        };
    }

    private function periodStart(string $period): CarbonImmutable
    {
        $now = CarbonImmutable::now();

        return match ($period) {
            Leaderboard::PERIOD_DAILY => $now->subDay(),
            Leaderboard::PERIOD_WEEKLY => $now->subWeek(),
            Leaderboard::PERIOD_MONTHLY => $now->subMonth(),
            default => $now->subWeek(),
        };
    }

    private function persistRows(string $type, string $period, Collection $rows, int $limit): int
    {
        $trimmed = $rows
            ->sortByDesc('score')
            ->take($limit)
            ->values();

        DB::transaction(function () use ($type, $period, $trimmed): void {
            Leaderboard::query()
                ->where('type', $type)
                ->where('period', $period)
                ->delete();

            if ($trimmed->isNotEmpty()) {
                $timestamp = now();
                Leaderboard::query()->insert(
                    $trimmed->map(fn (array $row): array => [
                        'type' => $type,
                        'period' => $period,
                        'entity_id' => (int) $row['entity_id'],
                        'score' => round((float) $row['score'], 2),
                        'created_at' => $timestamp,
                        'updated_at' => $timestamp,
                    ])->all()
                );
            }
        });

        $this->flushCache($type, $period);

        return $trimmed->count();
    }

    private function flushCache(string $type, string $period): void
    {
        foreach ([10, 25, 50, 100] as $limit) {
            Cache::forget($this->cacheKey($type, $period, $limit));
        }

        if ($type === Leaderboard::TYPE_CREATOR) {
            Cache::forget('leaderboard:top-creators-widget:' . $period);
        }
    }

    private function cacheKey(string $type, string $period, int $limit): string
    {
        return sprintf('leaderboard:%s:%s:%d', $type, $period, $limit);
    }

    private function allTimeCreatorRows(): Collection
    {
        return User::query()
            ->from('users')
            ->leftJoin('user_statistics as us', 'us.user_id', '=', 'users.id')
            ->whereNull('users.deleted_at')
            ->where('users.is_active', true)
            ->select([
                'users.id',
                DB::raw('COALESCE(users.xp, 0) as xp'),
                DB::raw('COALESCE(us.followers_count, 0) as followers_count'),
                DB::raw('COALESCE(us.favorites_received_count, 0) as likes_received'),
                DB::raw('COALESCE(us.artwork_views_received_count, 0) as artwork_views'),
            ])
            ->get()
            ->map(function ($row): array {
                $score = ((int) $row->xp * 1)
                    + ((int) $row->followers_count * 10)
                    + ((int) $row->likes_received * 2)
                    + ((int) $row->artwork_views * 0.1);

                return [
                    'entity_id' => (int) $row->id,
                    'score' => $score,
                ];
            })
            ->filter(fn (array $row): bool => $row['score'] > 0)
            ->values();
    }

    private function windowedCreatorRows(CarbonImmutable $start): Collection
    {
        $xp = DB::table('user_xp_logs')
            ->select('user_id', DB::raw('SUM(xp) as xp'))
            ->where('created_at', '>=', $start)
            ->groupBy('user_id');

        $followers = DB::table('user_followers')
            ->select('user_id', DB::raw('COUNT(*) as followers_count'))
            ->where('created_at', '>=', $start)
            ->groupBy('user_id');

        $likes = DB::table('artwork_likes as likes')
            ->join('artworks as artworks', 'artworks.id', '=', 'likes.artwork_id')
            ->select('artworks.user_id', DB::raw('COUNT(*) as likes_received'))
            ->where('likes.created_at', '>=', $start)
            ->groupBy('artworks.user_id');

        $views = DB::query()
            ->fromSub($this->artworkSnapshotDeltas($start), 'deltas')
            ->join('artworks as artworks', 'artworks.id', '=', 'deltas.artwork_id')
            ->select('artworks.user_id', DB::raw('SUM(deltas.views_delta) as artwork_views'))
            ->groupBy('artworks.user_id');

        return User::query()
            ->from('users')
            ->leftJoinSub($xp, 'xp', 'xp.user_id', '=', 'users.id')
            ->leftJoinSub($followers, 'followers', 'followers.user_id', '=', 'users.id')
            ->leftJoinSub($likes, 'likes', 'likes.user_id', '=', 'users.id')
            ->leftJoinSub($views, 'views', 'views.user_id', '=', 'users.id')
            ->whereNull('users.deleted_at')
            ->where('users.is_active', true)
            ->select([
                'users.id',
                DB::raw('COALESCE(xp.xp, 0) as xp'),
                DB::raw('COALESCE(followers.followers_count, 0) as followers_count'),
                DB::raw('COALESCE(likes.likes_received, 0) as likes_received'),
                DB::raw('COALESCE(views.artwork_views, 0) as artwork_views'),
            ])
            ->get()
            ->map(function ($row): array {
                $score = ((int) $row->xp * 1)
                    + ((int) $row->followers_count * 10)
                    + ((int) $row->likes_received * 2)
                    + ((float) $row->artwork_views * 0.1);

                return [
                    'entity_id' => (int) $row->id,
                    'score' => $score,
                ];
            })
            ->filter(fn (array $row): bool => $row['score'] > 0)
            ->values();
    }

    private function allTimeArtworkRows(): Collection
    {
        return Artwork::query()
            ->from('artworks')
            ->join('artwork_stats as stats', 'stats.artwork_id', '=', 'artworks.id')
            ->public()
            ->select([
                'artworks.id',
                DB::raw('COALESCE(stats.favorites, 0) as likes_count'),
                DB::raw('COALESCE(stats.views, 0) as views_count'),
                DB::raw('COALESCE(stats.downloads, 0) as downloads_count'),
                DB::raw('COALESCE(stats.comments_count, 0) as comments_count'),
            ])
            ->get()
            ->map(fn ($row): array => [
                'entity_id' => (int) $row->id,
                'score' => ((int) $row->likes_count * 3)
                    + ((int) $row->views_count * 1)
                    + ((int) $row->downloads_count * 5)
                    + ((int) $row->comments_count * 4),
            ])
            ->filter(fn (array $row): bool => $row['score'] > 0)
            ->values();
    }

    private function windowedArtworkRows(CarbonImmutable $start): Collection
    {
        $views = $this->artworkSnapshotDeltas($start);

        $likes = DB::table('artwork_likes')
            ->select('artwork_id', DB::raw('COUNT(*) as favourites_delta'))
            ->where('created_at', '>=', $start)
            ->groupBy('artwork_id');

        $downloads = DB::table('artwork_downloads')
            ->select('artwork_id', DB::raw('COUNT(*) as downloads_delta'))
            ->where('created_at', '>=', $start)
            ->groupBy('artwork_id');

        $comments = DB::table('artwork_comments')
            ->select('artwork_id', DB::raw('COUNT(*) as comments_delta'))
            ->where('created_at', '>=', $start)
            ->where('is_approved', true)
            ->whereNull('deleted_at')
            ->groupBy('artwork_id');

        return Artwork::query()
            ->from('artworks')
            ->leftJoinSub($views, 'views', 'views.artwork_id', '=', 'artworks.id')
            ->leftJoinSub($likes, 'likes', 'likes.artwork_id', '=', 'artworks.id')
            ->leftJoinSub($downloads, 'downloads', 'downloads.artwork_id', '=', 'artworks.id')
            ->leftJoinSub($comments, 'comments', 'comments.artwork_id', '=', 'artworks.id')
            ->where('artworks.is_public', true)
            ->where('artworks.is_approved', true)
            ->whereNull('artworks.deleted_at')
            ->whereNotNull('artworks.published_at')
            ->select([
                'artworks.id',
                DB::raw('COALESCE(likes.favourites_delta, 0) as favourites_delta'),
                DB::raw('COALESCE(views.views_delta, 0) as views_delta'),
                DB::raw('COALESCE(downloads.downloads_delta, 0) as downloads_delta'),
                DB::raw('COALESCE(comments.comments_delta, 0) as comments_delta'),
            ])
            ->get()
            ->map(fn ($row): array => [
                'entity_id' => (int) $row->id,
                'score' => ((int) $row->favourites_delta * 3)
                    + ((int) $row->views_delta * 1)
                    + ((int) $row->downloads_delta * 5)
                    + ((int) $row->comments_delta * 4),
            ])
            ->filter(fn (array $row): bool => $row['score'] > 0)
            ->values();
    }

    private function allTimeStoryRows(): Collection
    {
        return Story::query()
            ->published()
            ->select(['id', 'views', 'likes_count', 'comments_count', 'reading_time'])
            ->get()
            ->map(fn (Story $story): array => [
                'entity_id' => (int) $story->id,
                'score' => ((int) $story->views * 1)
                    + ((int) $story->likes_count * 3)
                    + ((int) $story->comments_count * 4)
                    + ((int) $story->reading_time * 0.5),
            ])
            ->filter(fn (array $row): bool => $row['score'] > 0)
            ->values();
    }

    private function windowedStoryRows(CarbonImmutable $start): Collection
    {
        $views = StoryView::query()
            ->select('story_id', DB::raw('COUNT(*) as views_count'))
            ->where('created_at', '>=', $start)
            ->groupBy('story_id');

        $likes = StoryLike::query()
            ->select('story_id', DB::raw('COUNT(*) as likes_count'))
            ->where('created_at', '>=', $start)
            ->groupBy('story_id');

        return Story::query()
            ->from('stories')
            ->leftJoinSub($views, 'views', 'views.story_id', '=', 'stories.id')
            ->leftJoinSub($likes, 'likes', 'likes.story_id', '=', 'stories.id')
            ->published()
            ->select([
                'stories.id',
                'stories.comments_count',
                'stories.reading_time',
                DB::raw('COALESCE(views.views_count, 0) as views_count'),
                DB::raw('COALESCE(likes.likes_count, 0) as likes_count'),
            ])
            ->get()
            ->map(fn ($row): array => [
                'entity_id' => (int) $row->id,
                'score' => ((int) $row->views_count * 1)
                    + ((int) $row->likes_count * 3)
                    + ((int) $row->comments_count * 4)
                    + ((int) $row->reading_time * 0.5),
            ])
            ->filter(fn (array $row): bool => $row['score'] > 0)
            ->values();
    }

    private function allTimeGroupRows(): Collection
    {
        $members = DB::table('group_members')
            ->select('group_id', DB::raw('COUNT(*) as members_count'))
            ->where('status', Group::STATUS_ACTIVE)
            ->groupBy('group_id');

        $releases = DB::table('group_releases')
            ->select('group_id', DB::raw('COUNT(*) as releases_count'))
            ->where('visibility', 'public')
            ->where('status', 'released')
            ->groupBy('group_id');

        $projects = DB::table('group_projects')
            ->select('group_id', DB::raw('COUNT(*) as projects_count'))
            ->where('visibility', 'public')
            ->whereIn('status', ['active', 'review', 'released'])
            ->groupBy('group_id');

        $challenges = DB::table('group_challenges')
            ->select('group_id', DB::raw('COUNT(*) as challenges_count'))
            ->where('visibility', 'public')
            ->whereIn('status', ['published', 'active'])
            ->groupBy('group_id');

        $events = DB::table('group_events')
            ->select('group_id', DB::raw('COUNT(*) as events_count'))
            ->where('visibility', 'public')
            ->where('status', 'published')
            ->groupBy('group_id');

        $activity = DB::table('group_activity_items')
            ->select('group_id', DB::raw('COUNT(*) as activity_count'))
            ->where('visibility', 'public')
            ->groupBy('group_id');

        return Group::query()
            ->from('groups')
            ->leftJoinSub($members, 'members', 'members.group_id', '=', 'groups.id')
            ->leftJoinSub($releases, 'releases', 'releases.group_id', '=', 'groups.id')
            ->leftJoinSub($projects, 'projects', 'projects.group_id', '=', 'groups.id')
            ->leftJoinSub($challenges, 'challenges', 'challenges.group_id', '=', 'groups.id')
            ->leftJoinSub($events, 'events', 'events.group_id', '=', 'groups.id')
            ->leftJoinSub($activity, 'activity', 'activity.group_id', '=', 'groups.id')
            ->public()
            ->select([
                'groups.id',
                'groups.followers_count',
                'groups.artworks_count',
                'groups.collections_count',
                'groups.is_verified',
                DB::raw('COALESCE(members.members_count, 0) as members_count'),
                DB::raw('COALESCE(releases.releases_count, 0) as releases_count'),
                DB::raw('COALESCE(projects.projects_count, 0) as projects_count'),
                DB::raw('COALESCE(challenges.challenges_count, 0) as challenges_count'),
                DB::raw('COALESCE(events.events_count, 0) as events_count'),
                DB::raw('COALESCE(activity.activity_count, 0) as activity_count'),
            ])
            ->get()
            ->map(function ($row): array {
                $score = ((int) $row->followers_count * 8)
                    + ((int) $row->artworks_count * 10)
                    + ((int) $row->collections_count * 6)
                    + ((int) $row->members_count * 20)
                    + ((int) $row->releases_count * 30)
                    + ((int) $row->projects_count * 24)
                    + ((int) $row->challenges_count * 18)
                    + ((int) $row->events_count * 14)
                    + ((int) $row->activity_count * 4)
                    + ((bool) $row->is_verified ? 120 : 0);

                return [
                    'entity_id' => (int) $row->id,
                    'score' => $score,
                ];
            })
            ->filter(fn (array $row): bool => $row['score'] > 0)
            ->values();
    }

    private function allTimeWorldRows(): Collection
    {
        if (! $this->worldTablesExist()) {
            return collect();
        }

        return World::query()
            ->from('worlds')
            ->withCount([
                'worldRelations as relations_count',
                'worldRelations as featured_relations_count' => fn ($query) => $query->where('is_featured', true),
                'worldSubmissions as approved_submissions_count' => fn ($query) => $query->where('status', WorldSubmission::STATUS_LIVE),
                'worldSubmissions as featured_submissions_count' => fn ($query) => $query->where('status', WorldSubmission::STATUS_LIVE)->where('is_featured', true),
            ])
            ->publiclyVisible()
            ->get()
            ->map(fn (World $world): array => [
                'entity_id' => (int) $world->id,
                'score' => $this->scoreWorld(
                    (int) ($world->relations_count ?? 0),
                    (int) ($world->featured_relations_count ?? 0),
                    (int) ($world->approved_submissions_count ?? 0),
                    (int) ($world->featured_submissions_count ?? 0),
                    $world->isCurrent(),
                    (bool) $world->is_featured,
                    false,
                ),
            ])
            ->filter(fn (array $row): bool => $row['score'] > 0)
            ->values();
    }

    private function windowedWorldRows(CarbonImmutable $start): Collection
    {
        if (! $this->worldTablesExist()) {
            return collect();
        }

        $relations = DB::table('world_relations')
            ->select('world_id', DB::raw('COUNT(*) as relations_count'))
            ->where('created_at', '>=', $start)
            ->groupBy('world_id');

        $featuredRelations = DB::table('world_relations')
            ->select('world_id', DB::raw('COUNT(*) as featured_relations_count'))
            ->where('created_at', '>=', $start)
            ->where('is_featured', true)
            ->groupBy('world_id');

        $approvedSubmissions = DB::table('world_submissions')
            ->select('world_id', DB::raw('COUNT(*) as approved_submissions_count'))
            ->where('status', WorldSubmission::STATUS_LIVE)
            ->where(function ($query) use ($start): void {
                $query->where('reviewed_at', '>=', $start)
                    ->orWhere(function ($fallback) use ($start): void {
                        $fallback->whereNull('reviewed_at')
                            ->where('created_at', '>=', $start);
                    });
            })
            ->groupBy('world_id');

        $featuredSubmissions = DB::table('world_submissions')
            ->select('world_id', DB::raw('COUNT(*) as featured_submissions_count'))
            ->where('status', WorldSubmission::STATUS_LIVE)
            ->where('is_featured', true)
            ->where(function ($query) use ($start): void {
                $query->where('reviewed_at', '>=', $start)
                    ->orWhere(function ($fallback) use ($start): void {
                        $fallback->whereNull('reviewed_at')
                            ->where('created_at', '>=', $start);
                    });
            })
            ->groupBy('world_id');

        return World::query()
            ->from('worlds')
            ->leftJoinSub($relations, 'relations', 'relations.world_id', '=', 'worlds.id')
            ->leftJoinSub($featuredRelations, 'featured_relations', 'featured_relations.world_id', '=', 'worlds.id')
            ->leftJoinSub($approvedSubmissions, 'approved_submissions', 'approved_submissions.world_id', '=', 'worlds.id')
            ->leftJoinSub($featuredSubmissions, 'featured_submissions', 'featured_submissions.world_id', '=', 'worlds.id')
            ->publiclyVisible()
            ->select([
                'worlds.id',
                'worlds.status',
                'worlds.starts_at',
                'worlds.ends_at',
                'worlds.published_at',
                'worlds.is_featured',
                DB::raw('COALESCE(relations.relations_count, 0) as relations_count'),
                DB::raw('COALESCE(featured_relations.featured_relations_count, 0) as featured_relations_count'),
                DB::raw('COALESCE(approved_submissions.approved_submissions_count, 0) as approved_submissions_count'),
                DB::raw('COALESCE(featured_submissions.featured_submissions_count, 0) as featured_submissions_count'),
                DB::raw("CASE WHEN worlds.published_at >= '" . $start->toDateTimeString() . "' OR worlds.starts_at >= '" . $start->toDateTimeString() . "' THEN 1 ELSE 0 END as recent_launch_bonus"),
            ])
            ->get()
            ->map(function ($row): array {
                $world = new World();
                $world->forceFill([
                    'status' => (string) $row->status,
                    'starts_at' => $row->starts_at,
                    'ends_at' => $row->ends_at,
                    'published_at' => $row->published_at,
                    'is_featured' => (bool) $row->is_featured,
                ]);

                return [
                    'entity_id' => (int) $row->id,
                    'score' => $this->scoreWorld(
                        (int) $row->relations_count,
                        (int) $row->featured_relations_count,
                        (int) $row->approved_submissions_count,
                        (int) $row->featured_submissions_count,
                        $world->isCurrent(),
                        (bool) $row->is_featured,
                        (bool) $row->recent_launch_bonus,
                    ),
                ];
            })
            ->filter(fn (array $row): bool => $row['score'] > 0)
            ->values();
    }

    private function windowedGroupRows(CarbonImmutable $start): Collection
    {
        $follows = DB::table('group_follows')
            ->select('group_id', DB::raw('COUNT(*) as follows_count'))
            ->where('created_at', '>=', $start)
            ->groupBy('group_id');

        $artworks = DB::table('artworks')
            ->select('group_id', DB::raw('COUNT(*) as artworks_count'))
            ->whereNotNull('group_id')
            ->where('is_public', true)
            ->where('is_approved', true)
            ->whereNull('deleted_at')
            ->whereNotNull('published_at')
            ->where('published_at', '>=', $start)
            ->groupBy('group_id');

        $releases = DB::table('group_releases')
            ->select('group_id', DB::raw('COUNT(*) as releases_count'))
            ->where('visibility', 'public')
            ->where('status', 'released')
            ->where('released_at', '>=', $start)
            ->groupBy('group_id');

        $projects = DB::table('group_projects')
            ->select('group_id', DB::raw('COUNT(*) as projects_count'))
            ->where('visibility', 'public')
            ->whereIn('status', ['active', 'review', 'released'])
            ->where('updated_at', '>=', $start)
            ->groupBy('group_id');

        $challenges = DB::table('group_challenges')
            ->select('group_id', DB::raw('COUNT(*) as challenges_count'))
            ->where('visibility', 'public')
            ->whereIn('status', ['published', 'active'])
            ->where(function ($query) use ($start): void {
                $query->where('updated_at', '>=', $start)
                    ->orWhere('start_at', '>=', $start)
                    ->orWhere('created_at', '>=', $start);
            })
            ->groupBy('group_id');

        $events = DB::table('group_events')
            ->select('group_id', DB::raw('COUNT(*) as events_count'))
            ->where('visibility', 'public')
            ->where('status', 'published')
            ->where(function ($query) use ($start): void {
                $query->where('published_at', '>=', $start)
                    ->orWhere('start_at', '>=', $start)
                    ->orWhere('updated_at', '>=', $start);
            })
            ->groupBy('group_id');

        $activity = DB::table('group_activity_items')
            ->select('group_id', DB::raw('COUNT(*) as activity_count'))
            ->where('visibility', 'public')
            ->where('occurred_at', '>=', $start)
            ->groupBy('group_id');

        $members = DB::table('group_members')
            ->select('group_id', DB::raw('COUNT(*) as members_count'))
            ->where('status', Group::STATUS_ACTIVE)
            ->groupBy('group_id');

        return Group::query()
            ->from('groups')
            ->leftJoinSub($follows, 'follows', 'follows.group_id', '=', 'groups.id')
            ->leftJoinSub($artworks, 'artworks', 'artworks.group_id', '=', 'groups.id')
            ->leftJoinSub($releases, 'releases', 'releases.group_id', '=', 'groups.id')
            ->leftJoinSub($projects, 'projects', 'projects.group_id', '=', 'groups.id')
            ->leftJoinSub($challenges, 'challenges', 'challenges.group_id', '=', 'groups.id')
            ->leftJoinSub($events, 'events', 'events.group_id', '=', 'groups.id')
            ->leftJoinSub($activity, 'activity', 'activity.group_id', '=', 'groups.id')
            ->leftJoinSub($members, 'members', 'members.group_id', '=', 'groups.id')
            ->public()
            ->select([
                'groups.id',
                'groups.is_verified',
                DB::raw('COALESCE(follows.follows_count, 0) as follows_count'),
                DB::raw('COALESCE(artworks.artworks_count, 0) as artworks_count'),
                DB::raw('COALESCE(releases.releases_count, 0) as releases_count'),
                DB::raw('COALESCE(projects.projects_count, 0) as projects_count'),
                DB::raw('COALESCE(challenges.challenges_count, 0) as challenges_count'),
                DB::raw('COALESCE(events.events_count, 0) as events_count'),
                DB::raw('COALESCE(activity.activity_count, 0) as activity_count'),
                DB::raw('COALESCE(members.members_count, 0) as members_count'),
            ])
            ->get()
            ->map(function ($row): array {
                $score = ((int) $row->follows_count * 18)
                    + ((int) $row->artworks_count * 16)
                    + ((int) $row->releases_count * 34)
                    + ((int) $row->projects_count * 22)
                    + ((int) $row->challenges_count * 20)
                    + ((int) $row->events_count * 16)
                    + ((int) $row->activity_count * 6)
                    + ((int) $row->members_count * 8)
                    + ((bool) $row->is_verified ? 45 : 0);

                return [
                    'entity_id' => (int) $row->id,
                    'score' => $score,
                ];
            })
            ->filter(fn (array $row): bool => $row['score'] > 0)
            ->values();
    }

    private function artworkSnapshotDeltas(CarbonImmutable $start): \Illuminate\Database\Query\Builder
    {
        return ArtworkMetricSnapshotHourly::query()
            ->from('artwork_metric_snapshots_hourly as snapshots')
            ->where('snapshots.bucket_hour', '>=', $start)
            ->select([
                'snapshots.artwork_id',
                DB::raw($this->nonNegativeSnapshotDelta('views_count', 'views_delta')),
                DB::raw($this->nonNegativeSnapshotDelta('downloads_count', 'downloads_delta')),
                DB::raw($this->nonNegativeSnapshotDelta('favourites_count', 'favourites_delta')),
                DB::raw($this->nonNegativeSnapshotDelta('comments_count', 'comments_delta')),
            ])
            ->groupBy('snapshots.artwork_id')
            ->toBase();
    }

    private function nonNegativeSnapshotDelta(string $column, string $alias): string
    {
        $delta = sprintf('MAX(snapshots.%1$s) - MIN(snapshots.%1$s)', $column);

        if (DB::connection()->getDriverName() === 'sqlite') {
            return sprintf('CASE WHEN %1$s > 0 THEN %1$s ELSE 0 END as %2$s', $delta, $alias);
        }

        return sprintf('GREATEST(%s, 0) as %s', $delta, $alias);
    }

    private function creatorEntities(array $ids): array
    {
        return User::query()
            ->from('users')
            ->leftJoin('user_profiles as profiles', 'profiles.user_id', '=', 'users.id')
            ->whereIn('users.id', $ids)
            ->select([
                'users.id',
                'users.username',
                'users.name',
                'users.level',
                'users.rank',
                'profiles.avatar_hash',
            ])
            ->get()
            ->mapWithKeys(fn ($row): array => [
                (int) $row->id => [
                    'id' => (int) $row->id,
                    'type' => Leaderboard::TYPE_CREATOR,
                    'name' => (string) ($row->username ?: $row->name ?: 'Creator'),
                    'username' => $row->username,
                    'url' => $row->username ? '/@' . $row->username : null,
                    'avatar' => \App\Support\AvatarUrl::forUser((int) $row->id, $row->avatar_hash, 128),
                    'level' => (int) ($row->level ?? 1),
                    'rank' => (string) ($row->rank ?? 'Newbie'),
                ],
            ])
            ->all();
    }

    private function artworkEntities(array $ids): array
    {
        return Artwork::query()
            ->with(['user.profile'])
            ->whereIn('id', $ids)
            ->get()
            ->mapWithKeys(fn (Artwork $artwork): array => [
                (int) $artwork->id => [
                    'id' => (int) $artwork->id,
                    'type' => Leaderboard::TYPE_ARTWORK,
                    'name' => $artwork->title,
                    'url' => '/art/' . $artwork->id . '/' . $artwork->slug,
                    'image' => $artwork->thumbUrl('md') ?? $artwork->thumbnail_url,
                    'creator_name' => (string) ($artwork->user?->username ?: $artwork->user?->name ?: 'Creator'),
                    'creator_url' => $artwork->user?->username ? '/@' . $artwork->user->username : null,
                ],
            ])
            ->all();
    }

    private function storyEntities(array $ids): array
    {
        return Story::query()
            ->with('creator.profile')
            ->whereIn('id', $ids)
            ->get()
            ->mapWithKeys(fn (Story $story): array => [
                (int) $story->id => [
                    'id' => (int) $story->id,
                    'type' => Leaderboard::TYPE_STORY,
                    'name' => $story->title,
                    'url' => '/stories/' . $story->slug,
                    'image' => $story->cover_url,
                    'creator_name' => (string) ($story->creator?->username ?: $story->creator?->name ?: 'Creator'),
                    'creator_url' => $story->creator?->username ? '/@' . $story->creator->username : null,
                ],
            ])
            ->all();
    }

    private function groupEntities(array $ids): array
    {
        return Group::query()
            ->with(['owner.profile', 'recruitmentProfile', 'badges', 'members'])
            ->whereIn('id', $ids)
            ->public()
            ->get()
            ->mapWithKeys(function (Group $group): array {
                return [
                    (int) $group->id => [
                        'id' => (int) $group->id,
                        'type' => Leaderboard::TYPE_GROUP,
                        'name' => (string) $group->name,
                        'headline' => (string) ($group->headline ?? ''),
                        'url' => $group->publicUrl(),
                        'avatar' => $group->avatarUrl(),
                        'image' => $group->bannerUrl() ?: $group->avatarUrl(),
                        'followers_count' => (int) ($group->followers_count ?? 0),
                        'artworks_count' => (int) ($group->artworks_count ?? 0),
                        'collections_count' => (int) ($group->collections_count ?? 0),
                        'members_count' => (int) $group->members->where('status', Group::STATUS_ACTIVE)->count(),
                        'is_recruiting' => (bool) ($group->recruitmentProfile?->is_recruiting ?? false),
                        'trust_signals' => $this->groupReputation->trustSignals($group),
                        'badges' => $this->groupReputation->groupBadges($group, 3),
                    ],
                ];
            })
            ->all();
    }

    private function worldEntities(array $ids): array
    {
        if (! $this->worldTablesExist()) {
            return [];
        }

        return World::query()
            ->withCount([
                'worldRelations as relations_count',
                'worldSubmissions as approved_submissions_count' => fn ($query) => $query->where('status', WorldSubmission::STATUS_LIVE),
                'worldSubmissions as featured_submissions_count' => fn ($query) => $query->where('status', WorldSubmission::STATUS_LIVE)->where('is_featured', true),
            ])
            ->whereIn('id', $ids)
            ->publiclyVisible()
            ->get()
            ->mapWithKeys(function (World $world): array {
                return [
                    (int) $world->id => [
                        'id' => (int) $world->id,
                        'type' => Leaderboard::TYPE_WORLD,
                        'name' => (string) $world->title,
                        'summary' => (string) ($world->summary ?: $world->tagline ?: ''),
                        'url' => $world->publicUrl(),
                        'image' => $world->coverUrl(),
                        'timeframe_label' => $this->worldTimeframeLabel($world),
                        'badge_label' => (string) ($world->badge_label ?? ''),
                        'phase' => $world->isCurrent() ? 'active' : ((string) $world->status === World::STATUS_ARCHIVED ? 'archive' : 'published'),
                        'icon_name' => (string) ($world->icon_name ?: 'fa-solid fa-globe'),
                        'theme_label' => $this->worldThemeLabel($world),
                        'relations_count' => (int) ($world->relations_count ?? 0),
                        'approved_submissions_count' => (int) ($world->approved_submissions_count ?? 0),
                        'featured_submissions_count' => (int) ($world->featured_submissions_count ?? 0),
                        'is_featured' => (bool) $world->is_featured,
                    ],
                ];
            })
            ->all();
    }

    private function scoreWorld(
        int $relationsCount,
        int $featuredRelationsCount,
        int $approvedSubmissionsCount,
        int $featuredSubmissionsCount,
        bool $isCurrent,
        bool $isFeatured,
        bool $isRecentLaunch,
    ): int {
        return ($relationsCount * 22)
            + ($featuredRelationsCount * 10)
            + ($approvedSubmissionsCount * 16)
            + ($featuredSubmissionsCount * 26)
            + ($isCurrent ? 48 : 0)
            + ($isFeatured ? 70 : 0)
            + ($isRecentLaunch ? 24 : 0);
    }

    private function worldTablesExist(): bool
    {
        return Schema::hasTable('worlds')
            && Schema::hasTable('world_relations')
            && Schema::hasTable('world_submissions');
    }

    private function worldThemeLabel(World $world): string
    {
        return match ((string) $world->type) {
            World::TYPE_EVENT => 'Event',
            World::TYPE_CAMPAIGN => 'Campaign',
            World::TYPE_TRIBUTE => 'Tribute',
            default => 'Seasonal',
        };
    }

    private function worldTimeframeLabel(World $world): ?string
    {
        if ($world->starts_at && $world->ends_at) {
            return $world->starts_at->format('M j, Y') . ' - ' . $world->ends_at->format('M j, Y');
        }

        if ($world->starts_at) {
            return 'Starts ' . $world->starts_at->format('M j, Y');
        }

        if ($world->ends_at) {
            return 'Ends ' . $world->ends_at->format('M j, Y');
        }

        return null;
    }
}
