<?php

declare(strict_types=1);

namespace App\Services\Worlds;

use App\Http\Resources\ArtworkListResource;
use App\Models\Artwork;
use App\Models\Collection;
use App\Models\Group;
use App\Models\GroupChallenge;
use App\Models\GroupChallengeOutcome;
use App\Models\GroupEvent;
use App\Models\GroupRelease;
use App\Models\NovaCard;
use App\Models\User;
use App\Models\World;
use App\Models\WorldRelation;
use App\Models\WorldSubmission;
use App\Models\WorldWebStory;
use App\Services\CollectionService;
use App\Services\GroupCardService;
use App\Services\Maturity\ArtworkMaturityService;
use App\Services\Worlds\WorldAnalyticsService;
use App\Support\AvatarUrl;
use App\Support\CoverUrl;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use cPad\Plugins\News\Models\NewsArticle;

final class WorldService
{
    public const COPY_MODE_STRUCTURE_ONLY = 'structure_only';

    public const COPY_MODE_WITH_RELATIONS = 'with_relations';

    private array $recurrenceEditionCache = [];

    private array $recurrenceCanonicalCache = [];

    public function __construct(
        private readonly CollectionService $collections,
        private readonly GroupCardService $groups,
        private readonly WorldSubmissionService $submissions,
        private readonly WorldRewardService $rewards,
        private readonly WorldAnalyticsService $analytics,
        private readonly ArtworkMaturityService $maturity,
    ) {
    }

    public function relationTypeOptions(): array
    {
        return collect((array) config('worlds.relation_types', []))
            ->map(fn (string $label, string $value): array => ['value' => $value, 'label' => $label])
            ->values()
            ->all();
    }

    public function sectionOptions(): array
    {
        return collect((array) config('worlds.sections', []))
            ->map(fn (array $config, string $key): array => [
                'value' => $key,
                'label' => (string) ($config['label'] ?? Str::headline($key)),
                'description' => (string) ($config['description'] ?? ''),
                'relation_types' => array_values((array) ($config['relation_types'] ?? [])),
            ])
            ->values()
            ->all();
    }

    public function themeOptions(): array
    {
        return collect((array) config('worlds.themes', []))
            ->map(fn (array $theme, string $key): array => [
                'value' => $key,
                'label' => (string) ($theme['label'] ?? Str::headline($key)),
                'accent_color' => $theme['accent_color'] ?? null,
                'accent_color_secondary' => $theme['accent_color_secondary'] ?? null,
                'background_motif' => $theme['background_motif'] ?? null,
                'icon_name' => $theme['icon_name'] ?? null,
                'related_tags_json' => array_values(array_map('strval', (array) ($theme['related_tags_json'] ?? []))),
                'suggested_badge_label' => (string) ($theme['suggested_badge_label'] ?? ''),
                'suggested_cta_label' => (string) ($theme['suggested_cta_label'] ?? ''),
            ])
            ->values()
            ->all();
    }

    public function studioListing(array $filters = []): array
    {
        $query = World::query()
            ->withCount([
                'worldRelations',
                'worldSubmissions as live_submission_count' => fn (Builder $builder): Builder => $builder->where('status', WorldSubmission::STATUS_LIVE),
            ])
            ->orderByDesc('is_active_campaign')
            ->orderByDesc('is_homepage_featured')
            ->orderByRaw('COALESCE(campaign_priority, 0) DESC')
            ->orderByDesc('is_featured')
            ->orderByRaw("CASE WHEN status = 'published' THEN 0 WHEN status = 'draft' THEN 1 ELSE 2 END")
            ->orderByRaw('COALESCE(promotion_starts_at, starts_at) ASC')
            ->orderByDesc('published_at');

        $search = trim((string) ($filters['q'] ?? ''));
        $status = trim((string) ($filters['status'] ?? ''));
        $type = trim((string) ($filters['type'] ?? ''));
        $perPage = max(10, min(50, (int) ($filters['per_page'] ?? 15)));

        if ($search !== '') {
            $query->where(function (Builder $builder) use ($search): void {
                $builder->where('title', 'like', '%' . $search . '%')
                    ->orWhere('slug', 'like', '%' . $search . '%')
                    ->orWhere('summary', 'like', '%' . $search . '%')
                    ->orWhere('description', 'like', '%' . $search . '%');
            });
        }

        if ($status !== '') {
            $query->where('status', $status);
        }

        if ($type !== '') {
            $query->where('type', $type);
        }

        $paginator = $query->paginate($perPage)->withQueryString();

        return [
            'items' => $paginator->getCollection()->map(fn (World $world): array => $this->mapStudioListItem($world))->all(),
            'meta' => $this->paginationMeta($paginator),
            'filters' => [
                'q' => $search,
                'status' => $status,
                'type' => $type,
                'per_page' => $perPage,
            ],
        ];
    }

    public function mapStudioWorld(World $world, ?User $viewer = null): array
    {
        $world->loadMissing(['createdBy.profile', 'parentWorld', 'worldRelations', 'linkedChallenge.group', 'linkedChallenge.outcomes', 'recapArticle.author.profile', 'recapArticle.category']);

        $canonicalEdition = $this->canonicalEditionForWorld($world);
        $previousEdition = $this->adjacentEditionForWorld($world, 'previous');
        $nextEdition = $this->adjacentEditionForWorld($world, 'next');
        $familyEditions = $this->familyEditionsForWorld($world);

        return [
            'id' => (int) $world->id,
            'title' => (string) $world->title,
            'slug' => (string) $world->slug,
            'tagline' => (string) ($world->tagline ?? ''),
            'summary' => (string) ($world->summary ?? ''),
            'teaser_title' => (string) ($world->teaser_title ?? ''),
            'teaser_summary' => (string) ($world->teaser_summary ?? ''),
            'description' => (string) ($world->description ?? ''),
            'cover_path' => (string) ($world->cover_path ?? ''),
            'cover_url' => $world->coverUrl(),
            'teaser_image_path' => (string) ($world->teaser_image_path ?? ''),
            'teaser_image_url' => $world->teaserImageUrl(),
            'theme_key' => (string) ($world->theme_key ?? ''),
            'theme' => $this->themePayload($world),
            'accent_color' => (string) ($world->accent_color ?? ''),
            'accent_color_secondary' => (string) ($world->accent_color_secondary ?? ''),
            'background_motif' => (string) ($world->background_motif ?? ''),
            'icon_name' => $this->resolvedIconName($world),
            'status' => (string) $world->status,
            'type' => (string) $world->type,
            'starts_at' => optional($world->starts_at)?->toIso8601String(),
            'ends_at' => optional($world->ends_at)?->toIso8601String(),
            'promotion_starts_at' => optional($world->promotion_starts_at)?->toIso8601String(),
            'promotion_ends_at' => optional($world->promotion_ends_at)?->toIso8601String(),
            'accepts_submissions' => (bool) $world->accepts_submissions,
            'participation_mode' => (string) ($world->participation_mode ?: World::PARTICIPATION_MODE_CLOSED),
            'submission_starts_at' => optional($world->submission_starts_at)?->toIso8601String(),
            'submission_ends_at' => optional($world->submission_ends_at)?->toIso8601String(),
            'submission_note_enabled' => (bool) $world->submission_note_enabled,
            'community_section_enabled' => (bool) $world->community_section_enabled,
            'allow_readd_after_removal' => (bool) $world->allow_readd_after_removal,
            'is_featured' => (bool) $world->is_featured,
            'is_active_campaign' => (bool) $world->is_active_campaign,
            'is_homepage_featured' => (bool) $world->is_homepage_featured,
            'campaign_priority' => $world->campaign_priority,
            'is_recurring' => (bool) $world->is_recurring,
            'recurrence_key' => (string) ($world->recurrence_key ?? ''),
            'recurrence_rule' => (string) ($world->recurrence_rule ?? ''),
            'edition_year' => $world->edition_year,
            'family_title' => $this->recurrenceFamilyLabel($world),
            'family_url' => $this->familyUrlForWorld($world),
            'edition_url' => $this->editionUrlForWorld($world),
            'is_canonical_edition' => $this->isCanonicalSurfaceWorld($world),
            'family_edition_count' => $familyEditions->count(),
            'archive_edition_count' => max(0, $familyEditions->count() - 1),
            'current_edition' => $canonicalEdition ? $this->mapWorldCard($canonicalEdition, $this->phaseForWorld($canonicalEdition)) : null,
            'previous_edition' => $previousEdition ? $this->mapWorldCard($previousEdition, $this->phaseForWorld($previousEdition)) : null,
            'next_edition' => $nextEdition ? $this->mapWorldCard($nextEdition, $this->phaseForWorld($nextEdition)) : null,
            'cta_label' => (string) ($world->cta_label ?? ''),
            'cta_url' => (string) ($world->cta_url ?? ''),
            'badge_label' => (string) ($world->badge_label ?? ''),
            'campaign_label' => (string) ($world->campaign_label ?? ''),
            'badge_description' => (string) ($world->badge_description ?? ''),
            'submission_guidelines' => (string) ($world->submission_guidelines ?? ''),
            'badge_url' => (string) ($world->badge_url ?? ''),
            'seo_title' => (string) ($world->seo_title ?? ''),
            'seo_description' => (string) ($world->seo_description ?? ''),
            'og_image_path' => (string) ($world->og_image_path ?? ''),
            'og_image_url' => $world->ogImageUrl(),
            'published_at' => optional($world->published_at)?->toIso8601String(),
            'recap_status' => (string) ($world->recap_status ?: World::RECAP_STATUS_DRAFT),
            'recap_status_label' => $this->recapStatusLabel($world),
            'recap_title' => (string) ($world->recap_title ?? ''),
            'recap_summary' => (string) ($world->recap_summary ?? ''),
            'recap_intro' => (string) ($world->recap_intro ?? ''),
            'recap_editor_note' => (string) ($world->recap_editor_note ?? ''),
            'recap_cover_path' => (string) ($world->recap_cover_path ?? ''),
            'recap_cover_url' => $world->recapCoverUrl(),
            'recap_article_id' => $world->recap_article_id ? (int) $world->recap_article_id : null,
            'recap_article' => $world->recap_article_id ? $this->resolveNewsPreview((int) $world->recap_article_id, 'Recap article') : null,
            'recap_published_at' => optional($world->recap_published_at)?->toIso8601String(),
            'recap_stats_snapshot' => $world->recap_stats_snapshot_json,
            'parent_world_id' => $world->parent_world_id ? (int) $world->parent_world_id : null,
            'parent_world' => $world->parentWorld ? [
                'id' => (int) $world->parentWorld->id,
                'title' => (string) $world->parentWorld->title,
                'slug' => (string) $world->parentWorld->slug,
            ] : null,
            'linked_challenge_id' => $world->linked_challenge_id ? (int) $world->linked_challenge_id : null,
            'linked_challenge' => $world->linked_challenge_id ? $this->resolveChallengePreview((int) $world->linked_challenge_id, $viewer, 'Primary challenge') : null,
            'show_linked_challenge_section' => (bool) ($world->show_linked_challenge_section ?? true),
            'show_linked_challenge_entries' => (bool) ($world->show_linked_challenge_entries ?? true),
            'show_linked_challenge_winners' => (bool) ($world->show_linked_challenge_winners ?? true),
            'show_linked_challenge_finalists' => (bool) ($world->show_linked_challenge_finalists ?? true),
            'auto_grant_challenge_world_rewards' => (bool) ($world->auto_grant_challenge_world_rewards ?? true),
            'challenge_teaser_override' => (string) ($world->challenge_teaser_override ?? ''),
            'hidden_linked_challenge_artwork_ids_json' => $this->hiddenLinkedChallengeArtworkIds($world),
            'related_tags_json' => array_values(array_map('strval', $world->related_tags_json ?? [])),
            'section_order_json' => $world->sectionOrder(),
            'section_visibility_json' => $world->sectionVisibility(),
            'campaign_state' => $this->campaignStateKey($world),
            'campaign_state_label' => $this->campaignStateLabel($world),
            'promotion_window_label' => $this->promotionWindowLabel($world),
            'status_badges' => $this->statusBadges($world, $this->preferredLinkedChallenge($world, $viewer)),
            'relations' => $world->worldRelations
                ->values()
                ->map(fn (WorldRelation $relation): array => [
                    'id' => (int) $relation->id,
                    'section_key' => (string) $relation->section_key,
                    'related_type' => (string) $relation->related_type,
                    'related_id' => (int) $relation->related_id,
                    'context_label' => (string) ($relation->context_label ?? ''),
                    'sort_order' => (int) $relation->sort_order,
                    'is_featured' => (bool) $relation->is_featured,
                    'preview' => $this->resolveEntityPreview((string) $relation->related_type, (int) $relation->related_id, $viewer, (string) ($relation->context_label ?? '')),
                ])
                ->all(),
            'created_by' => $world->createdBy ? [
                'id' => (int) $world->createdBy->id,
                'name' => (string) $world->createdBy->name,
                'username' => (string) ($world->createdBy->username ?? ''),
            ] : null,
            'submission_review_queue' => $this->submissions->studioReviewQueue($world),
            'rewarded_contributors' => $this->rewards->rewardedContributorsForWorld($world),
            'analytics' => $this->analytics->studioReport($world),
            'urls' => [
                'public' => $world->publicUrl(),
                'edit' => route('studio.worlds.edit', ['world' => $world]),
                'preview' => route('studio.worlds.preview', ['world' => $world]),
                'publish' => route('studio.worlds.publish', ['world' => $world]),
                'publish_recap' => route('studio.worlds.recap.publish', ['world' => $world]),
                'archive' => route('studio.worlds.archive', ['world' => $world]),
                'duplicate' => route('studio.worlds.duplicate', ['world' => $world]),
                'new_edition' => route('studio.worlds.new-edition', ['world' => $world]),
            ],
        ];
    }

    public function store(User $editor, array $data): World
    {
        $world = new World();

        return $this->persist($world, $editor, $data);
    }

    public function update(World $world, User $editor, array $data): World
    {
        return $this->persist($world, $editor, $data);
    }

    public function duplicate(World $source, User $editor, bool $asNewEdition = false): World
    {
        return $this->duplicateWithMode($source, $editor, $asNewEdition, self::COPY_MODE_WITH_RELATIONS);
    }

    public function duplicateWithMode(World $source, User $editor, bool $asNewEdition, string $copyMode): World
    {
        if ($asNewEdition && ! $this->canCreateNewEdition($source)) {
            throw ValidationException::withMessages([
                'recurrence_key' => 'Add recurrence data before creating a new edition.',
            ]);
        }

        $source->loadMissing('worldRelations');

        $derivedRecurrenceKey = $this->inferredRecurrenceKey($source);
        $copyRelations = $copyMode !== self::COPY_MODE_STRUCTURE_ONLY;
        $preserveRecurrence = $asNewEdition;

        $data = [
            'title' => $asNewEdition ? $this->nextEditionTitle($source) : $this->duplicateTitle($source),
            'slug' => $asNewEdition ? $this->nextEditionSlug($source) : $source->slug . '-copy',
            'tagline' => $source->tagline,
            'summary' => $source->summary,
            'teaser_title' => $source->teaser_title,
            'teaser_summary' => $source->teaser_summary,
            'description' => $source->description,
            'cover_path' => $source->cover_path,
            'teaser_image_path' => $source->teaser_image_path,
            'theme_key' => $source->theme_key,
            'accent_color' => $source->accent_color,
            'accent_color_secondary' => $source->accent_color_secondary,
            'background_motif' => $source->background_motif,
            'icon_name' => $source->icon_name,
            'status' => World::STATUS_DRAFT,
            'type' => $source->type,
            'starts_at' => null,
            'ends_at' => null,
            'promotion_starts_at' => null,
            'promotion_ends_at' => null,
            'accepts_submissions' => (bool) $source->accepts_submissions,
            'participation_mode' => (string) ($source->participation_mode ?: World::PARTICIPATION_MODE_CLOSED),
            'submission_starts_at' => null,
            'submission_ends_at' => null,
            'submission_note_enabled' => (bool) $source->submission_note_enabled,
            'community_section_enabled' => (bool) $source->community_section_enabled,
            'allow_readd_after_removal' => (bool) $source->allow_readd_after_removal,
            'published_at' => null,
            'is_featured' => false,
            'is_active_campaign' => false,
            'is_homepage_featured' => false,
            'campaign_priority' => null,
            'is_recurring' => $preserveRecurrence,
            'recurrence_key' => $preserveRecurrence ? $derivedRecurrenceKey : null,
            'recurrence_rule' => $preserveRecurrence ? $source->recurrence_rule : null,
            'edition_year' => $preserveRecurrence ? $this->nextEditionYear($source) : null,
            'cta_label' => $source->cta_label,
            'cta_url' => $asNewEdition ? null : $source->cta_url,
            'badge_label' => $source->badge_label,
            'campaign_label' => $source->campaign_label,
            'badge_description' => $source->badge_description,
            'submission_guidelines' => $source->submission_guidelines,
            'badge_url' => $asNewEdition ? null : $source->badge_url,
            'seo_title' => $source->seo_title,
            'seo_description' => $source->seo_description,
            'og_image_path' => $source->og_image_path,
            'recap_status' => World::RECAP_STATUS_DRAFT,
            'recap_title' => null,
            'recap_summary' => null,
            'recap_intro' => null,
            'recap_editor_note' => null,
            'recap_cover_path' => null,
            'recap_article_id' => null,
            'recap_stats_snapshot_json' => null,
            'recap_published_at' => null,
            'linked_challenge_id' => $asNewEdition ? null : $source->linked_challenge_id,
            'show_linked_challenge_section' => (bool) ($source->show_linked_challenge_section ?? true),
            'show_linked_challenge_entries' => (bool) ($source->show_linked_challenge_entries ?? true),
            'show_linked_challenge_winners' => (bool) ($source->show_linked_challenge_winners ?? true),
            'show_linked_challenge_finalists' => (bool) ($source->show_linked_challenge_finalists ?? true),
            'auto_grant_challenge_world_rewards' => (bool) ($source->auto_grant_challenge_world_rewards ?? true),
            'challenge_teaser_override' => $asNewEdition ? null : $source->challenge_teaser_override,
            'hidden_linked_challenge_artwork_ids_json' => $asNewEdition ? [] : $this->hiddenLinkedChallengeArtworkIds($source),
            'related_tags_json' => array_values(array_map('strval', $source->related_tags_json ?? [])),
            'section_order_json' => $source->sectionOrder(),
            'section_visibility_json' => $source->sectionVisibility(),
            'parent_world_id' => $asNewEdition ? (int) ($source->parent_world_id ?: $source->id) : $source->parent_world_id,
            'relations' => $copyRelations
                ? $source->worldRelations
                    ->map(fn (WorldRelation $relation): array => [
                        'section_key' => (string) $relation->section_key,
                        'related_type' => (string) $relation->related_type,
                        'related_id' => (int) $relation->related_id,
                        'context_label' => (string) ($relation->context_label ?? ''),
                        'sort_order' => (int) $relation->sort_order,
                        'is_featured' => (bool) $relation->is_featured,
                    ])
                    ->all()
                : [],
        ];

        return $this->store($editor, $data);
    }

    public function duplicateModeOptions(bool $asNewEdition = false): array
    {
        return [
            [
                'value' => self::COPY_MODE_STRUCTURE_ONLY,
                'label' => $asNewEdition ? 'Structure only' : 'Shell only',
                'description' => $asNewEdition
                    ? 'Carry the theme, layout, and recurrence family forward without copying curated relations.'
                    : 'Create a fresh draft shell with theme, layout, and metadata but no curated attachments.',
            ],
            [
                'value' => self::COPY_MODE_WITH_RELATIONS,
                'label' => 'Include curated relations',
                'description' => 'Copy the current curated relations as a starting point so the editorial structure is immediately reusable.',
            ],
        ];
    }

    public function canCreateNewEdition(World $world): bool
    {
        return (bool) ($world->is_recurring || $world->recurrence_key || $world->edition_year);
    }

    public function publish(World $world): World
    {
        $world->forceFill([
            'status' => World::STATUS_PUBLISHED,
            'published_at' => $world->published_at ?? now(),
        ])->save();

        return $world->fresh(['createdBy.profile', 'parentWorld', 'worldRelations', 'linkedChallenge.group']);
    }

    public function publishRecap(World $world): World
    {
        if (! $world->isEndedEdition()) {
            throw ValidationException::withMessages([
                'recap_status' => 'Publish recap after the world has ended or been archived.',
            ]);
        }

        $world->forceFill([
            'recap_status' => World::RECAP_STATUS_PUBLISHED,
            'recap_published_at' => $world->recap_published_at ?? now(),
            'recap_stats_snapshot_json' => $this->buildRecapStatsSnapshot($world),
            'is_active_campaign' => false,
            'is_homepage_featured' => false,
        ])->save();

        return $world->fresh(['createdBy.profile', 'parentWorld', 'worldRelations', 'linkedChallenge.group', 'recapArticle.author.profile', 'recapArticle.category']);
    }

    public function archive(World $world): World
    {
        $world->forceFill([
            'status' => World::STATUS_ARCHIVED,
            'ends_at' => $world->ends_at ?? now(),
            'is_active_campaign' => false,
            'is_homepage_featured' => false,
        ])->save();

        return $world->fresh(['createdBy.profile', 'parentWorld', 'worldRelations', 'linkedChallenge.group']);
    }

    public function searchEntities(string $type, string $query, ?User $viewer = null): array
    {
        $type = trim(Str::lower($type));
        $query = trim($query);

        return match ($type) {
            WorldRelation::TYPE_ARTWORK => $this->searchArtworks($query),
            WorldRelation::TYPE_COLLECTION => $this->searchCollections($query, $viewer),
            WorldRelation::TYPE_USER => $this->searchUsers($query),
            WorldRelation::TYPE_GROUP => $this->searchGroups($query, $viewer),
            WorldRelation::TYPE_NEWS => $this->searchNews($query),
            WorldRelation::TYPE_CHALLENGE => $this->searchChallenges($query, $viewer),
            WorldRelation::TYPE_EVENT => $this->searchEvents($query, $viewer),
            WorldRelation::TYPE_RELEASE => $this->searchReleases($query, $viewer),
            WorldRelation::TYPE_CARD => $this->searchCards($query, $viewer),
            default => [],
        };
    }

    public function publicIndexPayload(?User $viewer = null): array
    {
        $spotlight = $this->primarySpotlightWorld();

        $current = $this->filterCanonicalSurfaceWorlds(
            $this->currentSurfaceQuery()
                ->limit(24)
                ->get()
        )
            ->reject(fn (World $world): bool => $spotlight !== null && (int) $world->id === (int) $spotlight->id)
            ->take(12)
            ->values();

        $upcoming = $this->filterCanonicalSurfaceWorlds(
            $this->upcomingSurfaceQuery()
                ->limit(24)
                ->get()
        )
            ->take(12)
            ->values();

        $excludedIds = collect([$spotlight?->id])
            ->filter()
            ->merge($current->pluck('id'))
            ->merge($upcoming->pluck('id'))
            ->map(fn ($id): int => (int) $id)
            ->unique()
            ->values();

        $featured = $this->filterCanonicalSurfaceWorlds(
            $this->featuredSurfaceQuery($excludedIds->all())
                ->limit(24)
                ->get()
        )
            ->take(9)
            ->values();
        $archive = $this->archiveSurfaceQuery()->limit(18)->get();
        $recurringFamilies = $this->recurringFamilyIndexPayload(8);

        $spotlightPayload = $spotlight ? $this->mapWorldCard($spotlight, 'spotlight') : null;

        return [
            'title' => 'Worlds',
            'description' => 'Seasonal, cultural, and editorial campaign spaces that bring together artworks, collections, creators, groups, news, challenges, and releases.',
            'spotlightWorld' => $spotlightPayload,
            'featuredWorld' => $spotlightPayload,
            'activeWorlds' => $current->map(fn (World $world): array => $this->mapWorldCard($world, 'active'))->all(),
            'upcomingWorlds' => $upcoming->map(fn (World $world): array => $this->mapWorldCard($world, 'upcoming'))->all(),
            'featuredWorlds' => $featured->map(fn (World $world): array => $this->mapWorldCard($world, 'featured'))->all(),
            'recurringWorldFamilies' => $recurringFamilies,
            'archivedWorlds' => $archive->map(fn (World $world): array => $this->mapWorldCard($world, 'archive'))->all(),
            'themeOptions' => $this->themeOptions(),
        ];
    }

    public function resolvePublicWorld(string $slug): array
    {
        $world = World::query()
            ->publiclyVisible()
            ->where('slug', $slug)
            ->first();

        if ($world) {
            $canonicalUrl = $this->canonicalPublicUrl($world);

            return [
                'world' => $world,
                'redirect' => $canonicalUrl !== route('worlds.show', ['world' => $slug]) ? $canonicalUrl : null,
            ];
        }

        $world = $this->canonicalEditionForRecurrenceKey($slug);

        return [
            'world' => $world,
            'redirect' => null,
        ];
    }

    public function resolvePublicEdition(string $slug, int $year): array
    {
        $world = $this->familyEditionsForRecurrenceKey($slug)
            ->first(fn (World $edition): bool => (int) ($edition->edition_year ?? 0) === $year)
            ?? World::query()
                ->publiclyVisible()
                ->where('slug', $slug)
                ->where('edition_year', $year)
                ->first();

        if (! $world) {
            return [
                'world' => null,
                'redirect' => null,
            ];
        }

        $canonicalUrl = $this->canonicalPublicUrl($world);

        return [
            'world' => $world,
            'redirect' => $canonicalUrl !== route('worlds.editions.show', ['world' => $slug, 'year' => $year]) ? $canonicalUrl : null,
        ];
    }

    public function canonicalPublicUrl(World $world): string
    {
        return $this->publicUrlForWorld($world);
    }

    public function publicShowPayload(World $world, ?User $viewer = null, bool $includeDraftRecap = false): array
    {
        $world->loadMissing(['createdBy.profile', 'parentWorld', 'worldRelations', 'linkedChallenge.group', 'recapArticle.author.profile', 'recapArticle.category', 'publishedWebStory']);

        $sections = $this->resolveSections($world, $viewer);
        $familyEditions = $this->familyEditionsForWorld($world);
        $canonicalEdition = $this->canonicalEditionForWorld($world);
        $previousEdition = $this->adjacentEditionForWorld($world, 'previous');
        $nextEdition = $this->adjacentEditionForWorld($world, 'next');
        $linkedChallenge = $this->preferredLinkedChallenge($world, $viewer);
        $linkedChallengePanel = $this->linkedChallengePanelPayload($world, $viewer, $linkedChallenge);
        $linkedChallengeEntries = $linkedChallenge ? $this->linkedChallengeEntriesPayload($world, $linkedChallenge, $viewer) : null;
        $linkedChallengeWinners = $linkedChallenge ? $this->linkedChallengeWinnersPayload($world, $linkedChallenge, $viewer) : null;
        $linkedChallengeFinalists = $linkedChallenge ? $this->linkedChallengeFinalistsPayload($world, $linkedChallenge, $viewer) : null;
        $communitySubmissions = $this->submissions->publicSectionPayload($world, $viewer);
        $rewardedContributors = $this->rewards->rewardedContributorsForWorld($world);
        $recap = $this->recapPayload(
            $world,
            $sections,
            $communitySubmissions,
            $rewardedContributors,
            $linkedChallengePanel,
            $linkedChallengeWinners,
            $linkedChallengeFinalists,
            $includeDraftRecap,
        );

        if ($recap) {
            $sections = $this->sectionsAfterRecapExtraction($sections);
        }

        $otherEditions = $familyEditions
            ->reject(fn (World $edition): bool => (int) $edition->id === (int) $world->id)
            ->values();

        $archiveEditions = $otherEditions
            ->sortBy([
                fn (World $edition): int => -1 * (int) ($edition->edition_year ?? 0),
                fn (World $edition): int => -1 * ($edition->starts_at?->getTimestamp() ?? $edition->published_at?->getTimestamp() ?? 0),
                fn (World $edition): int => -1 * (int) $edition->id,
            ])
            ->values()
            ->map(fn (World $edition): array => $this->mapWorldCard($edition, 'archive'))
            ->all();

        $relatedWorlds = World::query()
            ->publiclyVisible()
            ->where('id', '!=', $world->id)
            ->when($world->recurrence_key, fn (Builder $builder) => $builder->where(function (Builder $nested) use ($world): void {
                $nested->where('theme_key', $world->theme_key)
                    ->orWhere('type', $world->type);
            })->where(function (Builder $nested) use ($world): void {
                $nested->whereNull('recurrence_key')
                    ->orWhere('recurrence_key', '!=', $world->recurrence_key);
            }))
            ->orderByDesc('starts_at')
            ->limit(3)
            ->get()
            ->map(fn (World $item): array => $this->mapWorldCard($item, $item->isCurrent() ? 'active' : 'archive'))
            ->all();

        return [
            'world' => $this->mapWorldDetail($world),
            'recap' => $recap,
            'sections' => $sections,
            'linkedChallenge' => $linkedChallengePanel,
            'linkedChallengeEntries' => $linkedChallengeEntries,
            'linkedChallengeWinners' => $linkedChallengeWinners,
            'linkedChallengeFinalists' => $linkedChallengeFinalists,
            'communitySubmissions' => $communitySubmissions,
            'rewardedContributors' => $rewardedContributors,
            'archiveNotice' => $this->archiveNoticePayload($world, $canonicalEdition),
            'currentEdition' => $canonicalEdition && (int) $canonicalEdition->id !== (int) $world->id
                ? $this->mapWorldCard($canonicalEdition, $this->phaseForWorld($canonicalEdition))
                : null,
            'previousEdition' => $previousEdition
                ? $this->mapWorldCard($previousEdition, $this->phaseForWorld($previousEdition))
                : null,
            'nextEdition' => $nextEdition
                ? $this->mapWorldCard($nextEdition, $this->phaseForWorld($nextEdition))
                : null,
            'archiveEditions' => $archiveEditions,
            'familySummary' => $this->mapRecurringFamilySummary($world),
            'relatedWorlds' => $relatedWorlds,
            'webStory' => $this->publishedWebStoryPayload($world),
        ];
    }

    public function homepageSpotlight(?User $viewer = null): ?array
    {
        $world = $this->primarySpotlightWorld();

        if (! $world) {
            return null;
        }

        $world->loadMissing('worldRelations');

        $secondary = $this->filterCanonicalSurfaceWorlds(
            $this->homepageSecondaryQuery((int) $world->id)
                ->limit(12)
                ->get()
        )
            ->take(3)
            ->map(fn (World $item): array => $this->mapWorldCard($item, $item->isActiveCampaign() ? 'active' : 'upcoming'))
            ->all();

        $payload = $this->mapWorldDetail($world);

        return [
            'primary' => [
                'id' => $payload['id'],
                'title' => $payload['title'],
                'headline' => $payload['teaser_title'] ?: $payload['title'],
                'tagline' => $payload['tagline'],
                'summary' => $payload['teaser_summary'] ?: $payload['summary'],
                'cover_url' => $payload['teaser_image_url'] ?: $payload['cover_url'],
                'icon_name' => $payload['icon_name'],
                'badge_label' => $payload['badge_label'],
                'campaign_label' => $payload['campaign_label'],
                'timeframe_label' => $payload['timeframe_label'],
                'promotion_window_label' => $payload['promotion_window_label'],
                'theme' => $payload['theme'],
                'cta_label' => $payload['cta_label'] ?: ($payload['challenge_cta_label'] ?: 'Explore world'),
                'cta_url' => $payload['public_url'],
                'public_url' => $payload['public_url'],
                'status_badges' => $payload['status_badges'],
                'live_submission_count' => $payload['live_submission_count'],
                'featured_submission_count' => $payload['featured_submission_count'],
                'relation_count' => $payload['relation_count'],
                'supporting_item' => $this->resolveHomepageSupportingItem($world, $viewer),
            ],
            'secondary' => $secondary,
            'index_url' => route('worlds.index'),
        ];
    }

    private function recapPayload(
        World $world,
        array $sections,
        ?array $communitySubmissions,
        array $rewardedContributors,
        ?array $linkedChallenge,
        ?array $linkedChallengeWinners,
        ?array $linkedChallengeFinalists,
        bool $includeDraftRecap = false,
    ): ?array {
        if (! $world->isEndedEdition()) {
            return null;
        }

        $isPreviewDraft = $includeDraftRecap
            && ! $world->hasPublishedRecap()
            && $world->hasRecapDraftContent();

        if (! $world->hasPublishedRecap() && ! $isPreviewDraft) {
            return null;
        }

        $featuredArtworkSection = collect($sections)->firstWhere('key', 'featured_artworks');
        $featuredCreatorSection = collect($sections)->firstWhere('key', 'featured_creators');
        $featuredGroupSection = collect($sections)->firstWhere('key', 'featured_groups');
        $newsSection = collect($sections)->firstWhere('key', 'news');

        $featuredArtworkItems = $this->collectRecapArtworkItems(
            $featuredArtworkSection,
            $linkedChallengeWinners,
            $linkedChallengeFinalists,
            $communitySubmissions,
        );
        $communityHighlightItems = $this->collectRecapCommunityHighlights($communitySubmissions);
        $article = $this->recapArticlePayload($world, $linkedChallenge['story'] ?? null, $newsSection);
        $statsSnapshot = $this->normalizeRecapStatsSnapshot($world->recap_stats_snapshot_json ?: $this->buildRecapStatsSnapshot($world));
        $summary = $statsSnapshot['summary'] ?? [];
        $winnerCount = (int) ($summary['winner_count'] ?? 0);
        $finalistCount = (int) ($summary['finalist_count'] ?? 0);
        $rewardGrantCount = (int) ($summary['reward_grants'] ?? 0);
        $submissionCount = (int) ($summary['submissions'] ?? $summary['live_participations'] ?? 0);

        return [
            'status' => $isPreviewDraft ? 'draft_preview' : World::RECAP_STATUS_PUBLISHED,
            'eyebrow' => $isPreviewDraft ? 'Recap draft preview' : 'Edition recap',
            'title' => trim((string) ($world->recap_title ?: ($world->title . ' recap'))),
            'summary' => trim((string) ($world->recap_summary ?: ($article['description'] ?? $world->summary ?? ''))),
            'intro' => trim((string) ($world->recap_intro ?: $this->defaultRecapIntro($world, $submissionCount, $rewardGrantCount, $winnerCount, $finalistCount))),
            'cover_url' => $world->recapCoverUrl(),
            'published_at' => optional($world->recap_published_at)?->toIso8601String(),
            'article' => $article,
            'stats' => [
                'captured_at' => $statsSnapshot['captured_at'] ?? null,
                'source' => $world->recap_stats_snapshot_json ? 'snapshot' : 'live',
                'items' => $this->recapStatItems($statsSnapshot),
            ],
            'featured_artworks' => [
                'title' => 'Edition highlights',
                'description' => 'Curated standouts, synced challenge outcomes, and featured community work that defined this edition.',
                'items' => $featuredArtworkItems,
            ],
            'community_highlights' => [
                'title' => 'Community highlights',
                'description' => 'Featured community submissions remain visible here so the edition archive keeps its strongest participation in view.',
                'items' => $communityHighlightItems,
            ],
            'creators' => [
                'title' => 'Creators and groups',
                'description' => 'Featured creators, groups, and rewarded contributors who shaped the atmosphere of this edition.',
                'items' => array_slice(array_values(array_merge(
                    array_values((array) ($featuredCreatorSection['items'] ?? [])),
                    array_values((array) ($featuredGroupSection['items'] ?? [])),
                )), 0, 8),
                'rewarded' => array_slice(array_values((array) ($rewardedContributors['items'] ?? [])), 0, 6),
            ],
            'winner_count' => $winnerCount,
            'finalist_count' => $finalistCount,
        ];
    }

    private function sectionsAfterRecapExtraction(array $sections): array
    {
        return array_values(array_filter($sections, fn (array $section): bool => ! in_array((string) ($section['key'] ?? ''), [
            'featured_artworks',
            'featured_creators',
            'featured_groups',
            'news',
        ], true)));
    }

    private function recapStatusLabel(World $world): string
    {
        return $world->hasPublishedRecap() ? 'Published recap' : 'Draft recap';
    }

    private function recapArticlePayload(World $world, ?array $fallbackStory = null, ?array $newsSection = null): ?array
    {
        if ((int) ($world->recap_article_id ?? 0) > 0) {
            $preview = $this->resolveNewsPreview((int) $world->recap_article_id, 'Recap article');

            if ($preview) {
                return array_merge($preview, [
                    'eyebrow' => 'Recap article',
                    'cta_label' => 'Read full recap',
                ]);
            }
        }

        if (($fallbackStory['intent'] ?? null) === 'recap') {
            return array_merge($fallbackStory, [
                'eyebrow' => $fallbackStory['eyebrow'] ?? 'Challenge recap',
                'cta_label' => $fallbackStory['cta_label'] ?? 'Read recap',
            ]);
        }

        $newsItem = collect((array) ($newsSection['items'] ?? []))
            ->first(fn (array $item): bool => is_string($item['url'] ?? null) && $item['url'] !== '');

        if (! $newsItem) {
            return null;
        }

        return array_merge($newsItem, [
            'eyebrow' => 'Related story',
            'cta_label' => 'Read story',
        ]);
    }

    private function recapPrimaryCta(World $world, ?array $article): ?array
    {
        if (! $world->hasPublishedRecap() || ! $world->isEndedEdition()) {
            return null;
        }

        if ($article && is_string($article['url'] ?? null) && $article['url'] !== '') {
            return [
                'label' => (string) ($article['cta_label'] ?? 'Read full recap'),
                'url' => (string) $article['url'],
            ];
        }

        return [
            'label' => 'Browse edition highlights',
            'url' => $this->publicUrlForWorld($world) . '#world-recap',
        ];
    }

    private function collectRecapArtworkItems(?array $featuredArtworkSection, ?array $linkedChallengeWinners, ?array $linkedChallengeFinalists, ?array $communitySubmissions): array
    {
        return collect(array_merge(
            array_values((array) ($featuredArtworkSection['items'] ?? [])),
            array_values((array) ($linkedChallengeWinners['items'] ?? [])),
            array_values((array) ($linkedChallengeFinalists['items'] ?? [])),
            array_values(array_filter((array) ($communitySubmissions['items'] ?? []), fn (array $item): bool => (string) ($item['status_label'] ?? '') === 'Featured')),
        ))
            ->filter(fn (array $item): bool => (int) ($item['id'] ?? 0) > 0)
            ->unique('id')
            ->take(8)
            ->values()
            ->all();
    }

    private function collectRecapCommunityHighlights(?array $communitySubmissions): array
    {
        $items = collect((array) ($communitySubmissions['items'] ?? []));

        if ($items->isEmpty()) {
            return [];
        }

        $featured = $items->filter(fn (array $item): bool => (string) ($item['status_label'] ?? '') === 'Featured');

        return ($featured->isNotEmpty() ? $featured : $items)
            ->take(6)
            ->values()
            ->all();
    }

    private function defaultRecapIntro(World $world, int $submissionCount, int $rewardGrantCount, int $winnerCount, int $finalistCount): string
    {
        $fragments = array_filter([
            $submissionCount > 0 ? number_format($submissionCount) . ' creator submissions' : null,
            $rewardGrantCount > 0 ? number_format($rewardGrantCount) . ' visible recognitions' : null,
            $winnerCount > 0 ? number_format($winnerCount) . ' winner' . ($winnerCount === 1 ? '' : 's') : null,
            $finalistCount > 0 ? number_format($finalistCount) . ' finalist' . ($finalistCount === 1 ? '' : 's') : null,
        ]);

        if ($fragments === []) {
            return 'This edition has moved into the archive, but its strongest moments, contributors, and editorial highlights remain preserved here as a recap.';
        }

        return sprintf(
            '%s has moved into the archive, and this recap preserves the edition through %s.',
            (string) $world->title,
            implode(', ', $fragments),
        );
    }

    private function buildRecapStatsSnapshot(World $world): array
    {
        $analytics = $this->analytics->studioReport($world);
        $summary = (array) ($analytics['ranges']['all']['summary'] ?? []);
        $liveSubmissions = (int) $world->worldSubmissions()->where('status', WorldSubmission::STATUS_LIVE)->count();
        $featuredParticipations = (int) $world->worldSubmissions()->where('status', WorldSubmission::STATUS_LIVE)->where('is_featured', true)->count();
        $rewardGrants = (int) $world->worldRewardGrants()->count();
        $winnerCount = (int) $world->worldRewardGrants()->where('reward_type', 'winner')->count();
        $finalistCount = (int) $world->worldRewardGrants()->where('reward_type', 'finalist')->count();
        $featuredArtworkCount = (int) $world->worldRelations()->where('section_key', 'featured_artworks')->count() + $featuredParticipations;

        return [
            'captured_at' => now()->toIso8601String(),
            'summary' => [
                'views' => (int) ($summary['views'] ?? 0),
                'unique_visitors' => (int) ($summary['unique_visitors'] ?? 0),
                'submissions' => (int) ($summary['submissions'] ?? $liveSubmissions),
                'live_participations' => max($liveSubmissions, (int) ($summary['approved_live_participations'] ?? 0)),
                'featured_participations' => max($featuredParticipations, (int) ($summary['featured_participations'] ?? 0)),
                'reward_grants' => max($rewardGrants, (int) ($summary['reward_grants'] ?? 0)),
                'challenge_clicks' => (int) ($summary['challenge_clicks'] ?? 0),
                'winner_count' => $winnerCount,
                'finalist_count' => $finalistCount,
                'featured_artwork_count' => $featuredArtworkCount,
            ],
        ];
    }

    private function recapStatItems(array $snapshot): array
    {
        $summary = (array) ($snapshot['summary'] ?? []);

        return array_values(array_filter([
            $this->recapStatItem('views', 'Tracked views', (int) ($summary['views'] ?? 0), 'Public visits recorded across the edition.'),
            $this->recapStatItem('unique_visitors', 'Unique visitors', (int) ($summary['unique_visitors'] ?? 0), 'Distinct visitors who reached the world page.'),
            $this->recapStatItem('submissions', 'Submitted works', (int) ($summary['submissions'] ?? 0), 'Creator submissions captured during the campaign.'),
            $this->recapStatItem('reward_grants', 'Recognitions', (int) ($summary['reward_grants'] ?? 0), 'World rewards and recognitions granted in this edition.'),
            $this->recapStatItem('featured_artwork_count', 'Highlights surfaced', (int) ($summary['featured_artwork_count'] ?? 0), 'Curated and featured works pulled into the recap.'),
            $this->recapStatItem('challenge_clicks', 'Challenge follow-through', (int) ($summary['challenge_clicks'] ?? 0), 'Tracked challenge CTA and section interactions.'),
        ]));
    }

    private function recapStatItem(string $key, string $label, int $value, string $description): ?array
    {
        if ($value < 1) {
            return null;
        }

        return [
            'key' => $key,
            'label' => $label,
            'value' => $value,
            'description' => $description,
        ];
    }

    public function navigationCampaign(): ?array
    {
        return Cache::remember('worlds.navigation_campaign', 60, function (): ?array {
            $world = World::query()
                ->select([
                    'id',
                    'slug',
                    'title',
                    'status',
                    'is_recurring',
                    'recurrence_key',
                    'edition_year',
                    'starts_at',
                    'ends_at',
                    'promotion_starts_at',
                    'promotion_ends_at',
                    'published_at',
                    'is_active_campaign',
                    'is_featured',
                    'is_homepage_featured',
                    'campaign_priority',
                    'campaign_label',
                ])
                ->campaignActive()
                ->orderByDesc('is_homepage_featured')
                ->orderByRaw('COALESCE(campaign_priority, 0) DESC')
                ->orderByDesc('is_featured')
                ->orderBy('title')
                ->limit(12)
                ->get()
                ->first(fn (World $candidate): bool => $this->isCanonicalSurfaceWorld($candidate));

            if (! $world || ! $world->isActiveCampaign()) {
                return null;
            }

            return [
                'title' => (string) $world->title,
                'campaign_label' => (string) ($world->campaign_label ?: 'Live now'),
                'status_label' => $this->campaignStateLabel($world),
                'url' => $this->publicPathForWorld($world),
            ];
        });
    }

    private function persist(World $world, User $editor, array $data): World
    {
        $originalCoverPath = (string) ($world->cover_path ?? '');
        $originalTeaserImagePath = (string) ($world->teaser_image_path ?? '');
        $originalOgImagePath = (string) ($world->og_image_path ?? '');
        $originalRecapCoverPath = (string) ($world->recap_cover_path ?? '');

        $title = trim((string) ($data['title'] ?? $world->title ?? 'Untitled World'));
        $status = (string) ($data['status'] ?? $world->status ?? World::STATUS_DRAFT);
        $publishedAt = $this->normalizePublishedAt($status, $data['published_at'] ?? $world->published_at);

        $world->fill([
            'title' => $title,
            'slug' => $this->resolveSlug($title, $world, $data),
            'tagline' => $this->nullableText($data['tagline'] ?? null),
            'summary' => $this->nullableText($data['summary'] ?? null),
            'teaser_title' => $this->nullableText($data['teaser_title'] ?? null),
            'teaser_summary' => $this->nullableText($data['teaser_summary'] ?? null),
            'description' => $this->nullableText($data['description'] ?? null),
            'cover_path' => $this->nullableText($data['cover_path'] ?? null),
            'teaser_image_path' => $this->nullableText($data['teaser_image_path'] ?? null),
            'theme_key' => $this->nullableText($data['theme_key'] ?? null),
            'accent_color' => $this->nullableText($data['accent_color'] ?? null),
            'accent_color_secondary' => $this->nullableText($data['accent_color_secondary'] ?? null),
            'background_motif' => $this->nullableText($data['background_motif'] ?? null),
            'icon_name' => $this->nullableText($data['icon_name'] ?? null),
            'status' => $status,
            'type' => (string) ($data['type'] ?? World::TYPE_SEASONAL),
            'starts_at' => ! empty($data['starts_at']) ? Carbon::parse((string) $data['starts_at']) : null,
            'ends_at' => ! empty($data['ends_at']) ? Carbon::parse((string) $data['ends_at']) : null,
            'promotion_starts_at' => ! empty($data['promotion_starts_at']) ? Carbon::parse((string) $data['promotion_starts_at']) : null,
            'promotion_ends_at' => ! empty($data['promotion_ends_at']) ? Carbon::parse((string) $data['promotion_ends_at']) : null,
            'accepts_submissions' => (bool) ($data['accepts_submissions'] ?? false),
            'participation_mode' => (string) ($data['participation_mode'] ?? ((bool) ($data['accepts_submissions'] ?? false) ? World::PARTICIPATION_MODE_MANUAL_APPROVAL : World::PARTICIPATION_MODE_CLOSED)),
            'submission_starts_at' => ! empty($data['submission_starts_at']) ? Carbon::parse((string) $data['submission_starts_at']) : null,
            'submission_ends_at' => ! empty($data['submission_ends_at']) ? Carbon::parse((string) $data['submission_ends_at']) : null,
            'submission_note_enabled' => (bool) ($data['submission_note_enabled'] ?? true),
            'community_section_enabled' => (bool) ($data['community_section_enabled'] ?? true),
            'allow_readd_after_removal' => (bool) ($data['allow_readd_after_removal'] ?? true),
            'is_featured' => (bool) ($data['is_featured'] ?? false),
            'is_active_campaign' => (bool) ($data['is_active_campaign'] ?? false),
            'is_homepage_featured' => (bool) ($data['is_homepage_featured'] ?? false),
            'campaign_priority' => isset($data['campaign_priority']) && $data['campaign_priority'] !== '' ? (int) $data['campaign_priority'] : null,
            'is_recurring' => (bool) ($data['is_recurring'] ?? false),
            'recurrence_key' => $this->nullableText($data['recurrence_key'] ?? null),
            'recurrence_rule' => $this->nullableText($data['recurrence_rule'] ?? null),
            'edition_year' => ! empty($data['edition_year']) ? (int) $data['edition_year'] : null,
            'cta_label' => $this->nullableText($data['cta_label'] ?? null),
            'cta_url' => $this->nullableText($data['cta_url'] ?? null),
            'badge_label' => $this->nullableText($data['badge_label'] ?? null),
            'campaign_label' => $this->nullableText($data['campaign_label'] ?? null),
            'badge_description' => $this->nullableText($data['badge_description'] ?? null),
            'submission_guidelines' => $this->nullableText($data['submission_guidelines'] ?? null),
            'badge_url' => $this->nullableText($data['badge_url'] ?? null),
            'seo_title' => $this->nullableText($data['seo_title'] ?? null),
            'seo_description' => $this->nullableText($data['seo_description'] ?? null),
            'og_image_path' => $this->nullableText($data['og_image_path'] ?? null),
            'recap_status' => (string) ($data['recap_status'] ?? $world->recap_status ?? World::RECAP_STATUS_DRAFT),
            'recap_title' => $this->nullableText($data['recap_title'] ?? null),
            'recap_summary' => $this->nullableText($data['recap_summary'] ?? null),
            'recap_intro' => $this->nullableText($data['recap_intro'] ?? null),
            'recap_editor_note' => $this->nullableText($data['recap_editor_note'] ?? null),
            'recap_cover_path' => $this->nullableText($data['recap_cover_path'] ?? null),
            'recap_article_id' => $this->normalizeRecapArticleId($data['recap_article_id'] ?? null),
            'recap_stats_snapshot_json' => array_key_exists('recap_stats_snapshot_json', $data)
                ? $this->normalizeRecapStatsSnapshot($data['recap_stats_snapshot_json'])
                : $world->recap_stats_snapshot_json,
            'recap_published_at' => ! empty($data['recap_published_at'])
                ? Carbon::parse((string) $data['recap_published_at'])
                : $world->recap_published_at,
            'related_tags_json' => collect((array) ($data['related_tags_json'] ?? []))->map(fn ($tag) => trim((string) $tag))->filter()->values()->all(),
            'section_order_json' => $this->normalizeSectionOrder($data['section_order_json'] ?? []),
            'section_visibility_json' => $this->normalizeSectionVisibility($data['section_visibility_json'] ?? []),
            'parent_world_id' => ! empty($data['parent_world_id']) ? (int) $data['parent_world_id'] : null,
            'linked_challenge_id' => $this->normalizeLinkedChallengeId($data['linked_challenge_id'] ?? null),
            'show_linked_challenge_section' => (bool) ($data['show_linked_challenge_section'] ?? true),
            'show_linked_challenge_entries' => (bool) ($data['show_linked_challenge_entries'] ?? true),
            'show_linked_challenge_winners' => (bool) ($data['show_linked_challenge_winners'] ?? true),
            'show_linked_challenge_finalists' => (bool) ($data['show_linked_challenge_finalists'] ?? true),
            'auto_grant_challenge_world_rewards' => (bool) ($data['auto_grant_challenge_world_rewards'] ?? true),
            'challenge_teaser_override' => $this->nullableText($data['challenge_teaser_override'] ?? null),
            'hidden_linked_challenge_artwork_ids_json' => $this->normalizeArtworkIdList($data['hidden_linked_challenge_artwork_ids_json'] ?? []),
            'published_at' => $publishedAt,
        ]);

        if (! $world->linked_challenge_id) {
            $world->challenge_teaser_override = null;
            $world->hidden_linked_challenge_artwork_ids_json = [];
        }

        if ((string) $world->recap_status !== World::RECAP_STATUS_PUBLISHED) {
            $world->recap_published_at = null;
        }

        if ((string) $world->participation_mode === World::PARTICIPATION_MODE_CLOSED) {
            $world->accepts_submissions = false;
        }

        if (! $world->exists) {
            $world->created_by_user_id = (int) $editor->id;
        }

        $world->save();

        $this->deleteWorldMediaIfReplaced($originalCoverPath, (string) ($world->cover_path ?? ''));
        $this->deleteWorldMediaIfReplaced($originalTeaserImagePath, (string) ($world->teaser_image_path ?? ''));
        $this->deleteWorldMediaIfReplaced($originalOgImagePath, (string) ($world->og_image_path ?? ''));
        $this->deleteWorldMediaIfReplaced($originalRecapCoverPath, (string) ($world->recap_cover_path ?? ''));

        $this->syncRelations($world, (array) ($data['relations'] ?? []));
        $this->rewards->syncLinkedChallengeRewardsForWorld($world);

        return $world->fresh(['createdBy.profile', 'parentWorld', 'worldRelations', 'linkedChallenge.group', 'recapArticle.author.profile', 'recapArticle.category']);
    }

    private function syncRelations(World $world, array $relations): void
    {
        $normalized = collect($relations)
            ->map(function (array $relation): ?array {
                $sectionKey = trim((string) ($relation['section_key'] ?? ''));
                $relatedType = trim((string) ($relation['related_type'] ?? ''));
                $relatedId = (int) ($relation['related_id'] ?? 0);

                if ($sectionKey === '' || $relatedType === '' || $relatedId < 1) {
                    return null;
                }

                return [
                    'section_key' => $sectionKey,
                    'related_type' => $relatedType,
                    'related_id' => $relatedId,
                    'context_label' => Str::limit(trim((string) ($relation['context_label'] ?? '')), 120, ''),
                    'sort_order' => max(0, (int) ($relation['sort_order'] ?? 0)),
                    'is_featured' => (bool) ($relation['is_featured'] ?? false),
                ];
            })
            ->filter()
            ->sortBy(fn (array $relation): string => sprintf('%s:%09d:%s:%09d', $relation['section_key'], (int) $relation['sort_order'], $relation['related_type'], (int) $relation['related_id']))
            ->values();

        $world->worldRelations()->delete();

        foreach ($normalized as $index => $relation) {
            $world->worldRelations()->create([
                'section_key' => $relation['section_key'],
                'related_type' => $relation['related_type'],
                'related_id' => $relation['related_id'],
                'context_label' => $relation['context_label'] !== '' ? $relation['context_label'] : null,
                'sort_order' => $relation['sort_order'] ?: $index,
                'is_featured' => $relation['is_featured'],
            ]);
        }
    }

    private function resolveSections(World $world, ?User $viewer = null): array
    {
        $relations = $world->worldRelations->values();

        if ($relations->isEmpty()) {
            return [];
        }

        $entities = $this->resolveEntitiesForRelations($relations, $viewer);
        $sectionConfigs = (array) config('worlds.sections', []);
        $sectionVisibility = $world->sectionVisibility();
        $sections = [];

        foreach ($world->sectionOrder() as $sectionKey) {
            if (($sectionVisibility[$sectionKey] ?? true) !== true) {
                continue;
            }

            $items = $relations
                ->where('section_key', $sectionKey)
                ->map(function (WorldRelation $relation) use ($entities): ?array {
                    $item = $entities->get($this->entityMapKey((string) $relation->related_type, (int) $relation->related_id));

                    if (! $item) {
                        return null;
                    }

                    return array_merge($item, [
                        'context_label' => trim((string) ($relation->context_label ?? '')) !== ''
                            ? (string) $relation->context_label
                            : ($item['context_label'] ?? null),
                        'is_featured' => (bool) $relation->is_featured,
                    ]);
                })
                ->filter()
                ->values()
                ->all();

            if ($items === []) {
                continue;
            }

            $sections[] = [
                'key' => $sectionKey,
                'title' => (string) ($sectionConfigs[$sectionKey]['label'] ?? Str::headline($sectionKey)),
                'description' => (string) ($sectionConfigs[$sectionKey]['description'] ?? ''),
                'items' => $items,
            ];
        }

        return $sections;
    }

    private function resolveEntitiesForRelations(SupportCollection $relations, ?User $viewer = null): SupportCollection
    {
        $map = collect();
        $grouped = $relations->groupBy('related_type');

        if ($grouped->has(WorldRelation::TYPE_ARTWORK)) {
            $ids = $grouped[WorldRelation::TYPE_ARTWORK]->pluck('related_id')->map(fn ($id) => (int) $id)->all();
            $artworks = Artwork::query()
                ->with(['user.profile', 'categories.contentType'])
                ->whereIn('id', $ids)
                ->where('artwork_status', 'published')
                ->where('visibility', Artwork::VISIBILITY_PUBLIC)
                ->get();

            foreach ($artworks as $artwork) {
                $preview = $this->resolveArtworkPreview((int) $artwork->id, '');
                if ($preview) {
                    $map->put($this->entityMapKey(WorldRelation::TYPE_ARTWORK, (int) $artwork->id), $preview);
                }
            }
        }

        if ($grouped->has(WorldRelation::TYPE_COLLECTION)) {
            $ids = $grouped[WorldRelation::TYPE_COLLECTION]->pluck('related_id')->map(fn ($id) => (int) $id)->all();
            $collections = Collection::query()->with(['user.profile', 'group', 'coverArtwork'])->whereIn('id', $ids)->get();
            $payloads = collect($this->collections->mapCollectionCardPayloads($collections, false, $viewer))->keyBy(fn (array $item): int => (int) $item['id']);

            foreach ($ids as $id) {
                $payload = $payloads->get($id);
                if ($payload) {
                    $map->put($this->entityMapKey(WorldRelation::TYPE_COLLECTION, $id), array_merge($payload, [
                        'entity_type' => WorldRelation::TYPE_COLLECTION,
                        'entity_label' => (string) (config('worlds.relation_types.collection') ?? 'Collection'),
                    ]));
                }
            }
        }

        if ($grouped->has(WorldRelation::TYPE_USER)) {
            $ids = $grouped[WorldRelation::TYPE_USER]->pluck('related_id')->map(fn ($id) => (int) $id)->all();
            $users = User::query()->with('profile')->whereIn('id', $ids)->get();

            foreach ($users as $user) {
                $preview = $this->resolveUserPreview((int) $user->id, '');
                if ($preview) {
                    $map->put($this->entityMapKey(WorldRelation::TYPE_USER, (int) $user->id), $preview);
                }
            }
        }

        if ($grouped->has(WorldRelation::TYPE_GROUP)) {
            $ids = $grouped[WorldRelation::TYPE_GROUP]->pluck('related_id')->map(fn ($id) => (int) $id)->all();
            $groups = Group::query()->with(['owner.profile', 'recruitmentProfile', 'discoveryMetric', 'members', 'badges'])->whereIn('id', $ids)->get();

            foreach ($groups as $group) {
                if ($group->canBeViewedBy($viewer)) {
                    $map->put($this->entityMapKey(WorldRelation::TYPE_GROUP, (int) $group->id), array_merge($this->groups->mapGroupCard($group, $viewer), [
                        'entity_type' => WorldRelation::TYPE_GROUP,
                        'entity_label' => (string) (config('worlds.relation_types.group') ?? 'Group'),
                    ]));
                }
            }
        }

        if ($grouped->has(WorldRelation::TYPE_NEWS)) {
            $ids = $grouped[WorldRelation::TYPE_NEWS]->pluck('related_id')->map(fn ($id) => (int) $id)->all();
            $articles = NewsArticle::query()->with(['author.profile', 'category'])->published()->whereIn('id', $ids)->get();

            foreach ($articles as $article) {
                $preview = $this->resolveNewsPreview((int) $article->id, '');
                if ($preview) {
                    $map->put($this->entityMapKey(WorldRelation::TYPE_NEWS, (int) $article->id), $preview);
                }
            }
        }

        if ($grouped->has(WorldRelation::TYPE_CHALLENGE)) {
            $ids = $grouped[WorldRelation::TYPE_CHALLENGE]->pluck('related_id')->map(fn ($id) => (int) $id)->all();
            $items = GroupChallenge::query()->with('group')->whereIn('id', $ids)->get();

            foreach ($items as $item) {
                $preview = $this->resolveChallengePreview((int) $item->id, $viewer, '');
                if ($preview) {
                    $map->put($this->entityMapKey(WorldRelation::TYPE_CHALLENGE, (int) $item->id), $preview);
                }
            }
        }

        if ($grouped->has(WorldRelation::TYPE_EVENT)) {
            $ids = $grouped[WorldRelation::TYPE_EVENT]->pluck('related_id')->map(fn ($id) => (int) $id)->all();
            $items = GroupEvent::query()->with('group')->whereIn('id', $ids)->get();

            foreach ($items as $item) {
                $preview = $this->resolveEventPreview((int) $item->id, $viewer, '');
                if ($preview) {
                    $map->put($this->entityMapKey(WorldRelation::TYPE_EVENT, (int) $item->id), $preview);
                }
            }
        }

        if ($grouped->has(WorldRelation::TYPE_RELEASE)) {
            $ids = $grouped[WorldRelation::TYPE_RELEASE]->pluck('related_id')->map(fn ($id) => (int) $id)->all();
            $items = GroupRelease::query()->with('group')->whereIn('id', $ids)->get();

            foreach ($items as $item) {
                $preview = $this->resolveReleasePreview((int) $item->id, $viewer, '');
                if ($preview) {
                    $map->put($this->entityMapKey(WorldRelation::TYPE_RELEASE, (int) $item->id), $preview);
                }
            }
        }

        if ($grouped->has(WorldRelation::TYPE_CARD)) {
            $ids = $grouped[WorldRelation::TYPE_CARD]->pluck('related_id')->map(fn ($id) => (int) $id)->all();
            $items = NovaCard::query()->with(['user.profile', 'category'])->whereIn('id', $ids)->get();

            foreach ($items as $item) {
                $preview = $this->resolveCardPreview((int) $item->id, $viewer, '');
                if ($preview) {
                    $map->put($this->entityMapKey(WorldRelation::TYPE_CARD, (int) $item->id), $preview);
                }
            }
        }

        return $map;
    }

    private function mapWorldCard(World $world, string $phase): array
    {
        $detail = $this->mapWorldDetail($world);

        return [
            'id' => $detail['id'],
            'title' => $detail['title'],
            'teaser_title' => $detail['teaser_title'],
            'slug' => $detail['slug'],
            'tagline' => $detail['tagline'],
            'summary' => $detail['teaser_summary'] ?: $detail['summary'],
            'cover_url' => $detail['teaser_image_url'] ?: $detail['cover_url'],
            'theme' => $detail['theme'],
            'type' => $detail['type'],
            'status' => $detail['status'],
            'phase' => $phase,
            'badge_label' => $detail['badge_label'],
            'campaign_label' => $detail['campaign_label'],
            'icon_name' => $detail['icon_name'],
            'timeframe_label' => $detail['timeframe_label'],
            'promotion_window_label' => $detail['promotion_window_label'],
            'starts_at' => $detail['starts_at'],
            'ends_at' => $detail['ends_at'],
            'edition_year' => $detail['edition_year'],
            'edition_label' => $detail['edition_label'],
            'is_recurring' => $detail['is_recurring'],
            'family_title' => $detail['family_title'],
            'family_url' => $detail['family_url'],
            'edition_url' => $detail['edition_url'],
            'is_canonical_edition' => $detail['is_canonical_edition'],
            'public_url' => $detail['public_url'],
            'cta_label' => $detail['cta_label'],
            'challenge_cta_label' => $detail['challenge_cta_label'],
            'challenge_cta_url' => $detail['challenge_cta_url'],
            'is_featured' => $detail['is_featured'],
            'is_active_campaign' => $detail['is_active_campaign'],
            'is_homepage_featured' => $detail['is_homepage_featured'],
            'campaign_state' => $detail['campaign_state'],
            'campaign_state_label' => $detail['campaign_state_label'],
            'status_badges' => $detail['status_badges'],
            'live_submission_count' => $detail['live_submission_count'],
            'featured_submission_count' => $detail['featured_submission_count'],
            'relation_count' => $detail['relation_count'],
        ];
    }

    private function mapWorldDetail(World $world): array
    {
        $world->loadMissing(['linkedChallenge.group', 'worldRelations', 'recapArticle.author.profile', 'recapArticle.category', 'publishedWebStory']);
        $theme = $this->themePayload($world);
        $familyTitle = $this->recurrenceFamilyLabel($world);
        $familyUrl = $this->familyUrlForWorld($world);
        $editionUrl = $this->editionUrlForWorld($world);
        $isCanonicalEdition = $this->isCanonicalSurfaceWorld($world);
        $linkedChallenge = $this->preferredLinkedChallenge($world);
        $linkedChallengeState = $linkedChallenge ? $this->challengeLifecycleStateForWorld($world, $linkedChallenge) : null;
        $linkedChallengeStory = $linkedChallenge ? $this->linkedChallengeStoryPayload($world, $linkedChallenge, $linkedChallengeState) : null;
        $recapArticle = $this->recapArticlePayload($world, $linkedChallengeStory);
        $recapPrimaryCta = $this->recapPrimaryCta($world, $recapArticle);

        return [
            'id' => (int) $world->id,
            'title' => (string) $world->title,
            'slug' => (string) $world->slug,
            'tagline' => (string) ($world->tagline ?? ''),
            'summary' => (string) ($world->summary ?? ''),
            'teaser_title' => (string) ($world->teaser_title ?? ''),
            'teaser_summary' => (string) ($world->teaser_summary ?? ''),
            'description' => (string) ($world->description ?? ''),
            'cover_url' => $world->coverUrl(),
            'teaser_image_url' => $world->teaserImageUrl(),
            'recap_cover_url' => $world->recapCoverUrl(),
            'type' => (string) $world->type,
            'status' => (string) $world->status,
            'theme' => $theme,
            'icon_name' => $this->resolvedIconName($world, $theme),
            'badge_label' => (string) ($world->badge_label ?? ''),
            'campaign_label' => (string) ($world->campaign_label ?? ''),
            'badge_description' => (string) ($world->badge_description ?? ''),
            'badge_url' => (string) ($world->badge_url ?? ''),
            'cta_label' => $recapPrimaryCta['label'] ?? (string) ($world->cta_label ?? ''),
            'challenge_cta_label' => $linkedChallenge ? $this->linkedChallengePrimaryCtaLabel($linkedChallengeState, $linkedChallengeStory) : null,
            'challenge_cta_url' => $linkedChallenge ? $this->linkedChallengePrimaryUrl($world, $linkedChallenge, $linkedChallengeState, $linkedChallengeStory) : null,
            'cta_url' => $recapPrimaryCta['url'] ?? (string) ($world->cta_url ?? ''),
            'starts_at' => optional($world->starts_at)?->toIso8601String(),
            'ends_at' => optional($world->ends_at)?->toIso8601String(),
            'promotion_starts_at' => optional($world->promotion_starts_at)?->toIso8601String(),
            'promotion_ends_at' => optional($world->promotion_ends_at)?->toIso8601String(),
            'timeframe_label' => $this->timeframeLabel($world),
            'promotion_window_label' => $this->promotionWindowLabel($world),
            'related_tags' => array_values(array_map('strval', $world->related_tags_json ?? [])),
            'recurrence_key' => (string) ($world->recurrence_key ?? ''),
            'edition_year' => $world->edition_year,
            'edition_label' => $world->edition_year ? ('Edition ' . $world->edition_year) : null,
            'is_recurring' => (bool) $world->is_recurring,
            'family_title' => $familyTitle,
            'family_slug' => $world->recurrence_key ?: null,
            'family_url' => $familyUrl,
            'edition_url' => $editionUrl,
            'is_canonical_edition' => $isCanonicalEdition,
            'is_featured' => (bool) $world->is_featured,
            'is_active_campaign' => (bool) $world->is_active_campaign,
            'is_homepage_featured' => (bool) $world->is_homepage_featured,
            'campaign_priority' => $world->campaign_priority,
            'campaign_state' => $this->campaignStateKey($world),
            'campaign_state_label' => $this->campaignStateLabel($world),
            'status_badges' => $this->statusBadges($world, $linkedChallenge),
            'recap_status' => (string) ($world->recap_status ?: World::RECAP_STATUS_DRAFT),
            'recap_title' => (string) ($world->recap_title ?? ''),
            'recap_summary' => (string) ($world->recap_summary ?? ''),
            'recap_cover_url' => $world->recapCoverUrl(),
            'recap_published_at' => optional($world->recap_published_at)?->toIso8601String(),
            'has_recap' => $world->hasPublishedRecap(),
            'recap_article' => $recapArticle,
            'live_submission_count' => (int) ($world->live_submission_count ?? $world->worldSubmissions()->where('status', WorldSubmission::STATUS_LIVE)->count()),
            'featured_submission_count' => (int) ($world->featured_submission_count ?? $world->worldSubmissions()->where('status', WorldSubmission::STATUS_LIVE)->where('is_featured', true)->count()),
            'rewarded_contributor_count' => (int) $world->worldRewardGrants()->count(),
            'relation_count' => (int) ($world->world_relations_count ?? $world->worldRelations()->count()),
            'public_url' => $this->publicUrlForWorld($world),
            'published_web_story' => $this->publishedWebStoryPayload($world),
        ];
    }

    private function publishedWebStoryPayload(World $world): ?array
    {
        $story = $world->publishedWebStory;

        if (! $story instanceof WorldWebStory) {
            return null;
        }

        return [
            'id' => (int) $story->id,
            'slug' => (string) $story->slug,
            'title' => (string) $story->title,
            'excerpt' => (string) ($story->excerpt ?? ''),
            'poster_portrait_url' => $story->posterPortraitUrl(),
            'url' => $story->publicUrl(),
            'published_at' => optional($story->published_at)?->toIso8601String(),
        ];
    }

    private function mapStudioListItem(World $world): array
    {
        $linkedChallenge = $this->preferredLinkedChallenge($world);

        return [
            'id' => (int) $world->id,
            'title' => (string) $world->title,
            'slug' => (string) $world->slug,
            'status' => (string) $world->status,
            'type' => (string) $world->type,
            'is_recurring' => (bool) $world->is_recurring,
            'recurrence_key' => (string) ($world->recurrence_key ?? ''),
            'edition_year' => $world->edition_year,
            'theme_key' => (string) ($world->theme_key ?? ''),
            'summary' => Str::limit(trim(strip_tags((string) ($world->summary ?: $world->description ?: ''))), 120),
            'timeframe_label' => $this->timeframeLabel($world),
            'promotion_window_label' => $this->promotionWindowLabel($world),
            'relation_count' => (int) ($world->world_relations_count ?? 0),
            'live_submission_count' => (int) ($world->live_submission_count ?? 0),
            'is_featured' => (bool) $world->is_featured,
            'is_active_campaign' => (bool) $world->is_active_campaign,
            'is_homepage_featured' => (bool) $world->is_homepage_featured,
            'campaign_priority' => $world->campaign_priority,
            'campaign_state' => $this->campaignStateKey($world),
            'campaign_state_label' => $this->campaignStateLabel($world),
            'status_badges' => $this->statusBadges($world, $linkedChallenge),
            'edit_url' => route('studio.worlds.edit', ['world' => $world]),
            'preview_url' => route('studio.worlds.preview', ['world' => $world]),
            'public_url' => $this->publicUrlForWorld($world),
        ];
    }

    private function publicSurfaceQuery(): Builder
    {
        return World::query()->with(['linkedChallenge.group'])->withCount([
            'worldRelations',
            'worldSubmissions as live_submission_count' => fn (Builder $builder): Builder => $builder->where('status', WorldSubmission::STATUS_LIVE),
            'worldSubmissions as featured_submission_count' => fn (Builder $builder): Builder => $builder->where('status', WorldSubmission::STATUS_LIVE)->where('is_featured', true),
        ]);
    }

    private function currentSurfaceQuery(): Builder
    {
        return $this->publicSurfaceQuery()
            ->current()
            ->orderByDesc('is_active_campaign')
            ->orderByDesc('is_homepage_featured')
            ->orderByRaw('COALESCE(campaign_priority, 0) DESC')
            ->orderByDesc('is_featured')
            ->orderBy('starts_at')
            ->orderBy('title');
    }

    private function upcomingSurfaceQuery(): Builder
    {
        return $this->publicSurfaceQuery()
            ->published()
            ->where(function (Builder $builder): void {
                $builder->where(function (Builder $upcoming): void {
                    $upcoming->whereNotNull('starts_at')
                        ->where('starts_at', '>', now());
                })->orWhere(function (Builder $campaign): void {
                    $campaign->where('is_active_campaign', true)
                        ->whereRaw('COALESCE(promotion_starts_at, starts_at) > ?', [now()->toDateTimeString()]);
                });
            })
            ->orderByDesc('is_homepage_featured')
            ->orderByRaw('COALESCE(campaign_priority, 0) DESC')
            ->orderByRaw('COALESCE(promotion_starts_at, starts_at) ASC')
            ->orderBy('title');
    }

    private function featuredSurfaceQuery(array $excludeIds = []): Builder
    {
        return $this->publicSurfaceQuery()
            ->publiclyVisible()
            ->where(function (Builder $builder): void {
                $builder->where('is_featured', true)
                    ->orWhere('is_homepage_featured', true);
            })
            ->when($excludeIds !== [], fn (Builder $builder): Builder => $builder->whereNotIn('id', $excludeIds))
            ->orderByDesc('is_homepage_featured')
            ->orderByRaw('COALESCE(campaign_priority, 0) DESC')
            ->orderByDesc('is_featured')
            ->orderByDesc('published_at');
    }

    private function archiveSurfaceQuery(): Builder
    {
        return $this->publicSurfaceQuery()
            ->archive()
            ->orderByDesc('ends_at')
            ->orderByDesc('published_at');
    }

    private function primarySpotlightWorld(): ?World
    {
        return $this->firstCanonicalWorld(
            $this->publicSurfaceQuery()
                ->campaignActive()
                ->homepageFeatured()
                ->orderByRaw('COALESCE(campaign_priority, 0) DESC')
                ->orderByDesc('is_featured')
                ->orderBy('title')
                ->limit(12)
        )
            ?? $this->firstCanonicalWorld(
                $this->publicSurfaceQuery()
                    ->campaignActive()
                    ->orderByDesc('is_homepage_featured')
                    ->orderByRaw('COALESCE(campaign_priority, 0) DESC')
                    ->orderByDesc('is_featured')
                    ->orderBy('title')
                    ->limit(12)
            )
            ?? $this->firstCanonicalWorld(
                $this->publicSurfaceQuery()
                    ->current()
                    ->where(function (Builder $builder): void {
                        $builder->where('is_featured', true)
                            ->orWhere('is_homepage_featured', true);
                    })
                    ->orderByDesc('is_homepage_featured')
                    ->orderByRaw('COALESCE(campaign_priority, 0) DESC')
                    ->orderByDesc('is_featured')
                    ->orderBy('title')
                    ->limit(12)
            )
            ?? $this->firstCanonicalWorld(
                $this->publicSurfaceQuery()
                    ->campaignUpcoming()
                    ->homepageFeatured()
                    ->orderByRaw('COALESCE(campaign_priority, 0) DESC')
                    ->orderByRaw('COALESCE(promotion_starts_at, starts_at) ASC')
                    ->limit(12)
            )
            ?? $this->firstCanonicalWorld(
                $this->publicSurfaceQuery()
                    ->campaignUpcoming()
                    ->orderByDesc('is_homepage_featured')
                    ->orderByRaw('COALESCE(campaign_priority, 0) DESC')
                    ->orderByRaw('COALESCE(promotion_starts_at, starts_at) ASC')
                    ->limit(12)
            );
    }

    private function homepageSecondaryQuery(int $excludeWorldId): Builder
    {
        return $this->publicSurfaceQuery()
            ->published()
            ->where('id', '!=', $excludeWorldId)
            ->where(function (Builder $builder): void {
                $builder->where(function (Builder $active): void {
                    $active->where('is_active_campaign', true)
                        ->where(function (Builder $window): void {
                            $window->whereRaw('COALESCE(promotion_ends_at, ends_at) IS NULL')
                                ->orWhereRaw('COALESCE(promotion_ends_at, ends_at) >= ?', [now()->toDateTimeString()]);
                        });
                })->orWhere(function (Builder $featured): void {
                    $featured->where('is_featured', true)
                        ->current();
                });
            })
            ->orderByDesc('is_active_campaign')
            ->orderByDesc('is_homepage_featured')
            ->orderByRaw('COALESCE(campaign_priority, 0) DESC')
            ->orderByDesc('is_featured')
            ->orderByRaw('COALESCE(promotion_starts_at, starts_at) ASC')
            ->orderBy('title');
    }

    private function resolveHomepageSupportingItem(World $world, ?User $viewer = null): ?array
    {
        $relation = $world->worldRelations
            ->first(fn (WorldRelation $item): bool => in_array((string) $item->related_type, [WorldRelation::TYPE_CHALLENGE, WorldRelation::TYPE_NEWS, WorldRelation::TYPE_EVENT], true));

        if (! $relation) {
            return null;
        }

        return $this->resolveEntityPreview((string) $relation->related_type, (int) $relation->related_id, $viewer, (string) ($relation->context_label ?? ''));
    }

    private function firstCanonicalWorld(Builder $query): ?World
    {
        return $query
            ->get()
            ->first(fn (World $world): bool => $this->isCanonicalSurfaceWorld($world));
    }

    private function campaignStateKey(World $world): string
    {
        if ($world->isActiveCampaign()) {
            return 'live_now';
        }

        if ($world->isUpcomingCampaign() || ($world->starts_at && $world->starts_at->isFuture())) {
            return 'upcoming';
        }

        if ((string) $world->status === World::STATUS_ARCHIVED || ($world->ends_at && $world->ends_at->isPast())) {
            return 'archived';
        }

        if ($world->isCurrent()) {
            return 'current';
        }

        return (string) $world->status;
    }

    private function campaignStateLabel(World $world): string
    {
        return match ($this->campaignStateKey($world)) {
            'live_now' => 'Live now',
            'upcoming' => 'Upcoming',
            'archived' => 'Archived',
            'current' => 'Current',
            World::STATUS_PUBLISHED => 'Published',
            World::STATUS_ARCHIVED => 'Archived',
            default => 'Draft',
        };
    }

    private function statusBadges(World $world, ?GroupChallenge $linkedChallenge = null): array
    {
        $badges = [];

        if ($world->isActiveCampaign()) {
            $badges[] = ['label' => 'Live now', 'tone' => 'emerald'];
        } elseif ($world->isUpcomingCampaign() || ($world->starts_at && $world->starts_at->isFuture())) {
            $badges[] = ['label' => 'Upcoming', 'tone' => 'sky'];
        } elseif ((string) $world->status === World::STATUS_ARCHIVED || ($world->ends_at && $world->ends_at->isPast())) {
            $badges[] = ['label' => 'Archived', 'tone' => 'amber'];
        }

        if ($world->isEndingSoon()) {
            $badges[] = ['label' => 'Ending soon', 'tone' => 'amber'];
        }

        if ((bool) $world->is_homepage_featured || (bool) $world->is_featured) {
            $badges[] = ['label' => 'Featured', 'tone' => 'rose'];
        }

        if ($world->hasPublishedRecap()) {
            $badges[] = ['label' => 'Recap live', 'tone' => 'sky'];
        }

        if ($linkedChallenge) {
            $challengeState = $this->challengeLifecycleStateForWorld($world, $linkedChallenge);
            if (in_array($challengeState['key'], ['upcoming', 'open', 'voting', 'judging', 'winners_announced'], true)) {
                $badges[] = ['label' => $challengeState['label'], 'tone' => $challengeState['tone']];
            }
        }

        return collect($badges)
            ->unique('label')
            ->values()
            ->all();
    }

    public function linkedWorldForChallenge(GroupChallenge $challenge): ?array
    {
        $world = $this->firstCanonicalWorld(
            $this->publicSurfaceQuery()
                ->publiclyVisible()
                ->where('linked_challenge_id', (int) $challenge->id)
                ->orderByDesc('is_active_campaign')
                ->orderByDesc('published_at')
                ->limit(12)
        );

        return $world ? $this->mapWorldCard($world, $this->phaseForWorld($world)) : null;
    }

    private function preferredLinkedChallenge(World $world, ?User $viewer = null): ?GroupChallenge
    {
        $world->loadMissing(['linkedChallenge.group', 'worldRelations']);

        $linkedChallenge = $world->linkedChallenge;

        if ($linkedChallenge && $linkedChallenge->group && $linkedChallenge->canBeViewedBy($viewer)) {
            return $linkedChallenge;
        }

        $fallbackChallengeId = $world->worldRelations
            ->first(fn (WorldRelation $relation): bool => (string) $relation->related_type === WorldRelation::TYPE_CHALLENGE)?->related_id;

        if (! $fallbackChallengeId) {
            return null;
        }

        $fallback = GroupChallenge::query()->with('group')->find((int) $fallbackChallengeId);

        if (! $fallback || ! $fallback->group || ! $fallback->canBeViewedBy($viewer)) {
            return null;
        }

        return $fallback;
    }

    private function linkedChallengePanelPayload(World $world, ?User $viewer = null, ?GroupChallenge $linkedChallenge = null): ?array
    {
        if (! (bool) ($world->show_linked_challenge_section ?? true)) {
            return null;
        }

        $challenge = $linkedChallenge ?? $this->preferredLinkedChallenge($world, $viewer);

        if (! $challenge) {
            return null;
        }

        $state = $this->challengeLifecycleStateForWorld($world, $challenge);
        $story = $this->linkedChallengeStoryPayload($world, $challenge, $state);
        $summary = trim((string) ($world->challenge_teaser_override ?: $challenge->summary ?: $challenge->description ?: ''));

        return [
            'id' => (int) $challenge->id,
            'title' => (string) $challenge->title,
            'summary' => $summary !== '' ? Str::limit($summary, 220) : null,
            'cover_url' => $challenge->coverUrl(),
            'url' => $this->challengeUrl($challenge),
            'challenge_url' => $this->challengeUrl($challenge),
            'group' => $challenge->group ? [
                'name' => (string) $challenge->group->name,
                'url' => $challenge->group->publicUrl(),
            ] : null,
            'status' => (string) $challenge->status,
            'state' => $state['key'],
            'state_label' => $state['label'],
            'state_tone' => $state['tone'],
            'cta_label' => $this->linkedChallengePrimaryCtaLabel($state, $story),
            'cta_url' => $this->linkedChallengePrimaryUrl($world, $challenge, $state, $story),
            'timeframe_label' => $this->challengeTimeframeLabel($challenge),
            'entry_count' => (int) ($challenge->artwork_links_count ?? $challenge->artworkLinks()->count()),
            'has_winner' => $this->challengeOutcomeArtworkIds($challenge, GroupChallengeOutcome::TYPE_WINNER)->isNotEmpty(),
            'show_entries' => (bool) ($world->show_linked_challenge_entries ?? true),
            'show_winners' => (bool) ($world->show_linked_challenge_winners ?? true),
            'show_finalists' => (bool) ($world->show_linked_challenge_finalists ?? true),
            'supports_finalists' => true,
            'story' => $story,
        ];
    }

    private function challengeLifecycleStateForWorld(World $world, GroupChallenge $challenge): array
    {
        $state = $this->challengeLifecycleState($challenge);

        if ((string) $world->status === World::STATUS_ARCHIVED || ($world->ends_at && $world->ends_at->isPast())) {
            return [
                'key' => 'closed',
                'label' => $state['key'] === 'winners_announced' ? 'Winners announced' : 'Challenge closed',
                'tone' => $state['key'] === 'winners_announced' ? 'amber' : 'slate',
                'cta_label' => 'View challenge recap',
            ];
        }

        return $state;
    }

    private function linkedChallengeEntriesPayload(World $world, GroupChallenge $challenge, ?User $viewer = null): ?array
    {
        if (! (bool) ($world->show_linked_challenge_entries ?? true)) {
            return null;
        }

        $state = $this->challengeLifecycleStateForWorld($world, $challenge);
        $winnerIds = $this->challengeOutcomeArtworkIds($challenge, GroupChallengeOutcome::TYPE_WINNER)->all();
        $finalistIds = $this->challengeOutcomeArtworkIds($challenge, GroupChallengeOutcome::TYPE_FINALIST)->all();
        $hiddenArtworkIds = $this->hiddenLinkedChallengeArtworkIds($world);
        $items = $this->visibleChallengeArtworkQuery($challenge, $viewer)
            ->when($hiddenArtworkIds !== [], fn (Builder $builder): Builder => $builder->whereNotIn('artworks.id', $hiddenArtworkIds))
            ->orderBy('group_challenge_artworks.sort_order')
            ->limit(12)
            ->get()
            ->map(function (Artwork $artwork) use ($state, $winnerIds, $finalistIds): array {
                $status = 'entry';

                if (in_array((int) $artwork->id, $winnerIds, true)) {
                    $status = 'winner';
                } elseif (in_array((int) $artwork->id, $finalistIds, true)) {
                    $status = 'finalist';
                }

                return $this->mapLinkedChallengeArtwork($artwork, $status, $state['key']);
            })
            ->all();

        if ($items === []) {
            return null;
        }

        return [
            'title' => 'Challenge entries',
            'description' => $state['key'] === 'closed'
                ? 'Entries from the linked challenge remain visible here so the world recap preserves the full field of work.'
                : 'Entries pulled directly from the linked challenge so the world stays current without duplicating editorial relations.',
            'hidden_count' => count($hiddenArtworkIds),
            'items' => $items,
        ];
    }

    private function linkedChallengeWinnersPayload(World $world, GroupChallenge $challenge, ?User $viewer = null): ?array
    {
        if (! (bool) ($world->show_linked_challenge_winners ?? true)) {
            return null;
        }

        $state = $this->challengeLifecycleStateForWorld($world, $challenge);
        $items = $this->linkedChallengeOutcomeItems($challenge, GroupChallengeOutcome::TYPE_WINNER, $viewer, $state['key']);

        if ($items->isEmpty()) {
            return null;
        }

        return [
            'title' => $items->count() === 1 ? 'Challenge winner' : 'Challenge winners',
            'description' => $state['key'] === 'winners_announced'
                ? 'The linked challenge has published results, and those winners are being carried into the world automatically.'
                : 'This world is carrying the linked challenge result forward so the campaign recap stays visible here too.',
            'item' => $items->first(),
            'items' => $items->all(),
        ];
    }

    private function linkedChallengeFinalistsPayload(World $world, GroupChallenge $challenge, ?User $viewer = null): ?array
    {
        if (! (bool) ($world->show_linked_challenge_finalists ?? true)) {
            return null;
        }

        $state = $this->challengeLifecycleStateForWorld($world, $challenge);
        $items = $this->linkedChallengeOutcomeItems($challenge, GroupChallengeOutcome::TYPE_FINALIST, $viewer, $state['key']);

        if ($items->isEmpty()) {
            return null;
        }

        return [
            'title' => 'Challenge finalists',
            'description' => $state['key'] === 'closed'
                ? 'Finalists from the linked challenge remain visible here so the archived world keeps the complete recap in view.'
                : 'Finalists from the linked challenge are being pulled directly into the world so the campaign recap reflects the full result set.',
            'items' => $items->all(),
        ];
    }

    private function linkedChallengeStoryPayload(World $world, GroupChallenge $challenge, ?array $state = null): ?array
    {
        $world->loadMissing('worldRelations');

        $newsRelations = $world->worldRelations
            ->where('related_type', WorldRelation::TYPE_NEWS)
            ->values();

        if ($newsRelations->isEmpty()) {
            return null;
        }

        $state = $state ?? $this->challengeLifecycleStateForWorld($world, $challenge);
        $intent = in_array($state['key'] ?? null, ['winners_announced', 'closed'], true) ? 'recap' : 'announcement';

        $candidate = $newsRelations
            ->map(function (WorldRelation $relation) use ($intent): ?array {
                $preview = $this->resolveNewsPreview(
                    (int) $relation->related_id,
                    trim((string) ($relation->context_label ?? '')),
                );

                if (! $preview) {
                    return null;
                }

                return [
                    'preview' => $preview,
                    'score' => $this->linkedChallengeStoryScore($preview, $relation, $intent),
                    'sort_key' => sprintf(
                        '%06d:%06d:%09d',
                        999999 - $this->linkedChallengeStoryScore($preview, $relation, $intent),
                        (bool) $relation->is_featured ? 0 : 1,
                        (int) $relation->sort_order,
                    ),
                ];
            })
            ->filter()
            ->sortBy('sort_key')
            ->first();

        if (! $candidate) {
            return null;
        }

        if ($intent === 'recap' && (int) ($candidate['score'] ?? 0) < 25) {
            return null;
        }

        return array_merge($candidate['preview'], [
            'intent' => $intent,
            'eyebrow' => $intent === 'recap' ? 'Challenge recap' : 'Challenge story',
            'cta_label' => $intent === 'recap' ? 'Read recap' : 'Read story',
        ]);
    }

    private function linkedChallengeStoryScore(array $preview, WorldRelation $relation, string $intent): int
    {
        $haystack = Str::lower(implode(' ', array_filter([
            (string) ($relation->context_label ?? ''),
            (string) ($preview['title'] ?? ''),
            (string) ($preview['subtitle'] ?? ''),
            (string) ($preview['description'] ?? ''),
        ])));

        $score = (bool) $relation->is_featured ? 18 : 0;
        $keywords = $intent === 'recap'
            ? [
                'recap' => 40,
                'results' => 36,
                'winner' => 34,
                'winners' => 34,
                'finalist' => 30,
                'finalists' => 30,
                'roundup' => 22,
                'highlights' => 18,
            ]
            : [
                'challenge' => 22,
                'announcement' => 20,
                'announce' => 20,
                'launch' => 18,
                'opens' => 16,
                'open' => 12,
                'submissions' => 14,
                'join' => 12,
                'call for entries' => 20,
            ];

        foreach ($keywords as $keyword => $weight) {
            if (Str::contains($haystack, $keyword)) {
                $score += $weight;
            }
        }

        return $score;
    }

    private function linkedChallengePrimaryCtaLabel(?array $state, ?array $story): ?string
    {
        if (! $state) {
            return null;
        }

        if (($story['intent'] ?? null) !== 'recap') {
            return $state['cta_label'] ?? null;
        }

        return match ($state['key'] ?? null) {
            'winners_announced' => 'Read results recap',
            'closed' => 'View challenge recap',
            default => $state['cta_label'] ?? null,
        };
    }

    private function linkedChallengePrimaryUrl(World $world, GroupChallenge $challenge, ?array $state = null, ?array $story = null): string
    {
        $state = $state ?? $this->challengeLifecycleStateForWorld($world, $challenge);
        $story = $story ?? $this->linkedChallengeStoryPayload($world, $challenge, $state);

        if (($story['intent'] ?? null) === 'recap' && in_array($state['key'] ?? null, ['winners_announced', 'closed'], true)) {
            return (string) $story['url'];
        }

        return $this->challengeUrl($challenge);
    }

    private function visibleChallengeArtworkQuery(GroupChallenge $challenge, ?User $viewer = null): Builder
    {
        $query = Artwork::query()
            ->select('artworks.*', 'group_challenge_artworks.sort_order as challenge_sort_order')
            ->join('group_challenge_artworks', function ($join) use ($challenge): void {
                $join->on('group_challenge_artworks.artwork_id', '=', 'artworks.id')
                    ->where('group_challenge_artworks.group_challenge_id', '=', $challenge->id);
            })
            ->with(['user.profile', 'categories.contentType', 'stats'])
            ->catalogVisible();

        $this->maturity->applyViewerFilter($query, $viewer);

        return $query;
    }

    private function mapLinkedChallengeArtwork(Artwork $artwork, string $status = 'entry', string $stateKey = 'open', ?string $statusLabelOverride = null, ?string $note = null, ?int $position = null): array
    {
        $resource = ArtworkListResource::make($artwork)->toArray(request());
        $views = (int) ($artwork->stats?->views ?? 0);
        $statusLabel = $statusLabelOverride ?: match ($status) {
            'winner' => $stateKey === 'winners_announced' ? 'Winner announced' : 'Winner',
            'finalist' => 'Finalist',
            'runner_up' => 'Runner-up',
            'honorable_mention' => 'Honorable Mention',
            'featured' => 'Featured',
            default => 'Challenge entry',
        };
        $contextLabel = match ($status) {
            'winner' => 'Linked challenge winner',
            'finalist' => 'Linked challenge finalist',
            'runner_up' => 'Linked challenge runner-up',
            'honorable_mention' => 'Linked challenge honorable mention',
            'featured' => 'Linked challenge featured entry',
            default => 'Linked challenge entry',
        };

        return [
            'id' => (int) $artwork->id,
            'title' => (string) ($resource['title'] ?? $artwork->title ?? 'Untitled artwork'),
            'subtitle' => (string) ($resource['author']['name'] ?? ''),
            'description' => Str::limit(trim(strip_tags((string) ($note ?: $artwork->description ?? ''))), 120),
            'url' => (string) ($resource['urls']['canonical'] ?? route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug ?: Str::slug((string) $artwork->title)])),
            'image' => $resource['thumbnail_url'] ?? $artwork->thumbUrl('md'),
            'status' => $status,
            'status_label' => $statusLabel,
            'context_label' => $contextLabel,
            'meta' => array_values(array_filter([
                $position ? 'Place ' . $position : null,
                $resource['category']['name'] ?? null,
                $views > 0 ? number_format($views) . ' views' : null,
            ])),
        ];
    }

    private function challengeLifecycleState(GroupChallenge $challenge): array
    {
        $now = now();
        $status = (string) $challenge->status;
        $startsAt = $challenge->start_at;
        $endsAt = $challenge->end_at;
        $hasWinner = $this->challengeOutcomeArtworkIds($challenge, GroupChallengeOutcome::TYPE_WINNER)->isNotEmpty();

        if ($status === GroupChallenge::STATUS_DRAFT) {
            return ['key' => 'draft', 'label' => 'Challenge draft', 'tone' => 'slate', 'cta_label' => 'Preview challenge'];
        }

        if ($hasWinner) {
            return ['key' => 'winners_announced', 'label' => 'Winners announced', 'tone' => 'amber', 'cta_label' => 'See results'];
        }

        if ($status === GroupChallenge::STATUS_ARCHIVED) {
            return ['key' => 'closed', 'label' => 'Challenge closed', 'tone' => 'slate', 'cta_label' => 'View challenge recap'];
        }

        if ($status === GroupChallenge::STATUS_ENDED || ($endsAt && $endsAt->isPast())) {
            if ((string) ($challenge->judging_mode ?? '') === 'community_vote') {
                return ['key' => 'voting', 'label' => 'Voting live', 'tone' => 'sky', 'cta_label' => 'View entries'];
            }

            return ['key' => 'judging', 'label' => 'Judging now', 'tone' => 'violet', 'cta_label' => 'Track challenge'];
        }

        if ($startsAt && $startsAt->isFuture()) {
            return ['key' => 'upcoming', 'label' => 'Challenge upcoming', 'tone' => 'sky', 'cta_label' => 'Challenge opens soon'];
        }

        if (in_array($status, [GroupChallenge::STATUS_ACTIVE, GroupChallenge::STATUS_PUBLISHED], true) || ! $startsAt || $startsAt->lte($now)) {
            return ['key' => 'open', 'label' => 'Challenge open', 'tone' => 'emerald', 'cta_label' => 'Join challenge'];
        }

        return ['key' => 'closed', 'label' => 'Challenge closed', 'tone' => 'slate', 'cta_label' => 'View challenge recap'];
    }

    private function challengeOutcomeArtworkIds(GroupChallenge $challenge, string $type): SupportCollection
    {
        $challenge->loadMissing('outcomes');

        $ids = $challenge->outcomes
            ->where('outcome_type', $type)
            ->pluck('artwork_id')
            ->map(fn ($id): int => (int) $id)
            ->filter(fn (int $id): bool => $id > 0)
            ->values();

        if ($type === GroupChallengeOutcome::TYPE_WINNER && $ids->isEmpty() && (int) ($challenge->featured_artwork_id ?? 0) > 0) {
            return collect([(int) $challenge->featured_artwork_id]);
        }

        return $ids;
    }

    private function linkedChallengeOutcomeItems(GroupChallenge $challenge, string $type, ?User $viewer = null, string $stateKey = 'open'): SupportCollection
    {
        $artworkIds = $this->challengeOutcomeArtworkIds($challenge, $type);

        if ($artworkIds->isEmpty()) {
            return collect();
        }

        $challenge->loadMissing('outcomes');
        $artworks = $this->visibleChallengeArtworkQuery($challenge, $viewer)
            ->whereIn('artworks.id', $artworkIds->all())
            ->get()
            ->keyBy(fn (Artwork $artwork): int => (int) $artwork->id);

        $outcomes = $challenge->outcomes
            ->where('outcome_type', $type)
            ->values();

        if ($outcomes->isEmpty() && $type === GroupChallengeOutcome::TYPE_WINNER && (int) ($challenge->featured_artwork_id ?? 0) > 0) {
            $artwork = $artworks->get((int) $challenge->featured_artwork_id);

            return $artwork
                ? collect([$this->mapLinkedChallengeArtwork($artwork, 'winner', $stateKey)])
                : collect();
        }

        return $outcomes
            ->map(function (GroupChallengeOutcome $outcome) use ($artworks, $stateKey, $type): ?array {
                $artwork = $artworks->get((int) $outcome->artwork_id);

                if (! $artwork) {
                    return null;
                }

                return $this->mapLinkedChallengeArtwork(
                    $artwork,
                    $type,
                    $stateKey,
                    $outcome->title_override,
                    $outcome->note,
                    $outcome->position,
                );
            })
            ->filter()
            ->values();
    }

    private function challengeTimeframeLabel(GroupChallenge $challenge): ?string
    {
        if ($challenge->start_at && $challenge->end_at) {
            return $challenge->start_at->format('M j') . ' - ' . $challenge->end_at->format('M j, Y');
        }

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

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

        return null;
    }

    private function challengeUrl(GroupChallenge $challenge): string
    {
        return route('groups.challenges.show', ['group' => $challenge->group, 'challenge' => $challenge]);
    }

    private function normalizeLinkedChallengeId(mixed $value): ?int
    {
        $id = (int) $value;

        return $id > 0 ? $id : null;
    }

    private function normalizeArtworkIdList(mixed $value): array
    {
        return collect((array) $value)
            ->map(fn ($id): int => (int) $id)
            ->filter(fn (int $id): bool => $id > 0)
            ->unique()
            ->values()
            ->all();
    }

    private function hiddenLinkedChallengeArtworkIds(World $world): array
    {
        return $this->normalizeArtworkIdList($world->hidden_linked_challenge_artwork_ids_json ?? []);
    }

    private function promotionWindowLabel(World $world): ?string
    {
        if (! $world->promotion_starts_at && ! $world->promotion_ends_at) {
            return null;
        }

        if ($world->promotion_starts_at && $world->promotion_ends_at) {
            return 'Promotion ' . $world->promotion_starts_at->format('d M Y') . ' - ' . $world->promotion_ends_at->format('d M Y');
        }

        if ($world->promotion_starts_at) {
            return 'Promotion starts ' . $world->promotion_starts_at->format('d M Y');
        }

        return 'Promoted through ' . $world->promotion_ends_at?->format('d M Y');
    }

    private function paginationMeta(LengthAwarePaginator $paginator): array
    {
        return [
            'current_page' => $paginator->currentPage(),
            'last_page' => $paginator->lastPage(),
            'per_page' => $paginator->perPage(),
            'total' => $paginator->total(),
            'from' => $paginator->firstItem(),
            'to' => $paginator->lastItem(),
        ];
    }

    private function resolveSlug(string $title, World $world, array $data): string
    {
        $requested = trim(Str::slug((string) ($data['slug'] ?? '')));
        $base = $requested !== '' ? $requested : Str::slug($title);
        $slug = $base !== '' ? $base : 'world';
        $counter = 2;

        while (World::query()
            ->where('slug', $slug)
            ->when($world->exists, fn (Builder $builder) => $builder->where('id', '!=', $world->id))
            ->exists()) {
            $slug = Str::limit($base !== '' ? $base : 'world', 170, '') . '-' . $counter;
            $counter++;
        }

        return $slug;
    }

    private function normalizePublishedAt(string $status, mixed $value): ?Carbon
    {
        if ($status === World::STATUS_PUBLISHED) {
            return $value ? Carbon::parse((string) $value) : now();
        }

        return $value ? Carbon::parse((string) $value) : null;
    }

    private function normalizeRecapArticleId(mixed $value): ?int
    {
        $articleId = (int) $value;

        return $articleId > 0 ? $articleId : null;
    }

    private function normalizeRecapStatsSnapshot(mixed $value): ?array
    {
        if (! is_array($value)) {
            return null;
        }

        return [
            'captured_at' => is_string($value['captured_at'] ?? null) ? $value['captured_at'] : now()->toIso8601String(),
            'summary' => array_map(fn ($item): int => (int) $item, (array) ($value['summary'] ?? [])),
        ];
    }

    private function normalizeSectionOrder(iterable $sectionOrder): array
    {
        $valid = array_keys((array) config('worlds.sections', []));

        return collect($sectionOrder)
            ->map(fn ($key) => trim((string) $key))
            ->filter(fn (string $key) => in_array($key, $valid, true))
            ->unique()
            ->values()
            ->all();
    }

    private function normalizeSectionVisibility(iterable $visibility): array
    {
        $valid = array_keys((array) config('worlds.sections', []));

        return collect($visibility)
            ->mapWithKeys(fn ($value, $key): array => [(string) $key => (bool) $value])
            ->filter(fn (bool $enabled, string $key): bool => in_array($key, $valid, true))
            ->all();
    }

    private function nullableText(mixed $value): ?string
    {
        $text = trim((string) ($value ?? ''));

        return $text === '' ? null : $text;
    }

    private function themePayload(World $world): array
    {
        $preset = $this->resolvedThemePreset($world);

        return [
            'key' => $world->theme_key,
            'label' => (string) ($preset['label'] ?? Str::headline((string) ($world->theme_key ?: $world->type))),
            'accent_color' => $world->accent_color ?: ($preset['accent_color'] ?? '#38bdf8'),
            'accent_color_secondary' => $world->accent_color_secondary ?: ($preset['accent_color_secondary'] ?? '#0f172a'),
            'background_motif' => $world->background_motif ?: ($preset['background_motif'] ?? 'atmosphere'),
            'icon_name' => $this->resolvedIconName($world, $preset),
        ];
    }

    private function resolvedIconName(World $world, ?array $theme = null): string
    {
        $theme ??= $this->resolvedThemePreset($world);

        $icon = $this->supportedIconName($world->icon_name ?? null);
        if ($icon !== null) {
            return $icon;
        }

        $themeIcon = $this->supportedIconName($theme['icon_name'] ?? null);
        if ($themeIcon !== null) {
            return $themeIcon;
        }

        return 'fa-solid fa-globe';
    }

    private function resolvedThemePreset(World $world): array
    {
        $themeKey = trim((string) ($world->theme_key ?? ''));
        if ($themeKey !== '') {
            $preset = (array) config('worlds.themes.' . $themeKey, []);
            if ($preset !== []) {
                return $preset;
            }
        }

        $typeKey = trim((string) $world->type);

        return (array) config('worlds.themes.' . $typeKey, []);
    }

    private function supportedIconName(mixed $icon): ?string
    {
        $value = trim((string) ($icon ?? ''));

        if ($value === '' || ! Str::startsWith($value, 'fa-')) {
            return null;
        }

        return $value;
    }

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

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

        if ($world->ends_at) {
            return 'Through ' . $world->ends_at->format('d M Y');
        }

        return $world->edition_year ? 'Edition ' . $world->edition_year : null;
    }

    private function familyEditionsForWorld(World $world): SupportCollection
    {
        $recurrenceKey = trim((string) ($world->recurrence_key ?? ''));

        if ($recurrenceKey === '') {
            return collect();
        }

        return $this->familyEditionsForRecurrenceKey($recurrenceKey);
    }

    private function familyEditionsForRecurrenceKey(string $recurrenceKey): SupportCollection
    {
        if (! array_key_exists($recurrenceKey, $this->recurrenceEditionCache)) {
            $this->recurrenceEditionCache[$recurrenceKey] = $this->publicSurfaceQuery()
                ->publiclyVisible()
                ->where('recurrence_key', $recurrenceKey)
                ->orderByDesc('edition_year')
                ->orderByDesc('starts_at')
                ->orderByDesc('published_at')
                ->get();
        }

        return collect($this->recurrenceEditionCache[$recurrenceKey]);
    }

    private function canonicalEditionForWorld(World $world): ?World
    {
        $recurrenceKey = trim((string) ($world->recurrence_key ?? ''));

        if ($recurrenceKey === '') {
            return null;
        }

        return $this->canonicalEditionForRecurrenceKey($recurrenceKey);
    }

    private function canonicalEditionForRecurrenceKey(string $recurrenceKey): ?World
    {
        if (! array_key_exists($recurrenceKey, $this->recurrenceCanonicalCache)) {
            $this->recurrenceCanonicalCache[$recurrenceKey] = $this->selectCanonicalEdition($this->familyEditionsForRecurrenceKey($recurrenceKey));
        }

        return $this->recurrenceCanonicalCache[$recurrenceKey];
    }

    private function selectCanonicalEdition(SupportCollection $editions): ?World
    {
        return $editions
            ->sortBy([
                fn (World $edition): int => (string) $edition->status === World::STATUS_PUBLISHED ? 0 : 1,
                fn (World $edition): int => $edition->isCurrent() ? 0 : 1,
                fn (World $edition): int => -1 * (int) ($edition->edition_year ?? 0),
                fn (World $edition): int => -1 * ($edition->starts_at?->getTimestamp() ?? $edition->published_at?->getTimestamp() ?? 0),
                fn (World $edition): int => -1 * (int) $edition->id,
            ])
            ->first();
    }

    private function filterCanonicalSurfaceWorlds(SupportCollection $worlds): SupportCollection
    {
        return $worlds
            ->filter(fn (World $world): bool => $this->isCanonicalSurfaceWorld($world))
            ->values();
    }

    private function isCanonicalSurfaceWorld(World $world): bool
    {
        $recurrenceKey = trim((string) ($world->recurrence_key ?? ''));

        if (! $world->is_recurring || $recurrenceKey === '') {
            return true;
        }

        return (int) ($this->canonicalEditionForRecurrenceKey($recurrenceKey)?->id ?? 0) === (int) $world->id;
    }

    private function publicUrlForWorld(World $world): string
    {
        $recurrenceKey = trim((string) ($world->recurrence_key ?? ''));

        if (! $world->is_recurring || $recurrenceKey === '') {
            return route('worlds.show', ['world' => $world->slug]);
        }

        if ($this->isCanonicalSurfaceWorld($world)) {
            return route('worlds.show', ['world' => $recurrenceKey]);
        }

        if ($world->edition_year !== null) {
            return route('worlds.editions.show', ['world' => $recurrenceKey, 'year' => $world->edition_year]);
        }

        return route('worlds.show', ['world' => $recurrenceKey]);
    }

    private function publicPathForWorld(World $world): string
    {
        $recurrenceKey = trim((string) ($world->recurrence_key ?? ''));

        if (! $world->is_recurring || $recurrenceKey === '') {
            return route('worlds.show', ['world' => $world->slug], false);
        }

        if ($this->isCanonicalSurfaceWorld($world)) {
            return route('worlds.show', ['world' => $recurrenceKey], false);
        }

        if ($world->edition_year !== null) {
            return route('worlds.editions.show', ['world' => $recurrenceKey, 'year' => $world->edition_year], false);
        }

        return route('worlds.show', ['world' => $recurrenceKey], false);
    }

    private function familyUrlForWorld(World $world): ?string
    {
        $recurrenceKey = trim((string) ($world->recurrence_key ?? ''));

        return $recurrenceKey !== '' ? route('worlds.show', ['world' => $recurrenceKey]) : null;
    }

    private function editionUrlForWorld(World $world): ?string
    {
        $recurrenceKey = trim((string) ($world->recurrence_key ?? ''));

        if (! $world->is_recurring || $recurrenceKey === '' || $world->edition_year === null) {
            return null;
        }

        return route('worlds.editions.show', ['world' => $recurrenceKey, 'year' => $world->edition_year]);
    }

    private function recurrenceFamilyLabel(World $world): ?string
    {
        $recurrenceKey = trim((string) ($world->recurrence_key ?? ''));

        if ($recurrenceKey === '') {
            return null;
        }

        return Str::title(str_replace('-', ' ', $recurrenceKey));
    }

    private function mapRecurringFamilySummary(World $world): ?array
    {
        $recurrenceKey = trim((string) ($world->recurrence_key ?? ''));

        if ($recurrenceKey === '') {
            return null;
        }

        return $this->buildRecurringFamilySummary($recurrenceKey, $this->familyEditionsForRecurrenceKey($recurrenceKey));
    }

    private function recurringFamilyIndexPayload(int $limit = 8): array
    {
        return $this->publicSurfaceQuery()
            ->publiclyVisible()
            ->whereNotNull('recurrence_key')
            ->get()
            ->filter(fn (World $world): bool => trim((string) ($world->recurrence_key ?? '')) !== '')
            ->groupBy(fn (World $world): string => (string) $world->recurrence_key)
            ->map(fn (SupportCollection $editions, string $recurrenceKey): array => $this->buildRecurringFamilySummary($recurrenceKey, $editions))
            ->sortBy([
                fn (array $family): int => match ((string) ($family['current_world']['campaign_state'] ?? '')) {
                    'live_now' => 0,
                    'upcoming' => 1,
                    default => 2,
                },
                fn (array $family): int => -1 * (int) ($family['current_world']['campaign_priority'] ?? 0),
                fn (array $family): int => -1 * (int) ($family['latest_edition_year'] ?? 0),
                fn (array $family): string => Str::lower((string) ($family['title'] ?? '')),
            ])
            ->take($limit)
            ->values()
            ->all();
    }

    private function buildRecurringFamilySummary(string $recurrenceKey, SupportCollection $editions): array
    {
        $canonicalEdition = $this->canonicalEditionForRecurrenceKey($recurrenceKey);
        $otherEditions = $editions
            ->reject(fn (World $edition): bool => (int) $edition->id === (int) ($canonicalEdition?->id ?? 0))
            ->sortBy([
                fn (World $edition): int => -1 * (int) ($edition->edition_year ?? 0),
                fn (World $edition): int => -1 * ($edition->starts_at?->getTimestamp() ?? $edition->published_at?->getTimestamp() ?? 0),
                fn (World $edition): int => -1 * (int) $edition->id,
            ])
            ->values();

        $currentWorld = $canonicalEdition ? $this->mapWorldCard($canonicalEdition, $this->phaseForWorld($canonicalEdition)) : null;

        return [
            'id' => 'family-' . $recurrenceKey,
            'key' => $recurrenceKey,
            'title' => Str::title(str_replace('-', ' ', $recurrenceKey)),
            'public_url' => route('worlds.show', ['world' => $recurrenceKey]),
            'current_world' => $currentWorld,
            'theme' => $currentWorld['theme'] ?? null,
            'summary' => $currentWorld['summary'] ?? null,
            'tagline' => $currentWorld['tagline'] ?? null,
            'cover_url' => $currentWorld['cover_url'] ?? null,
            'latest_edition_year' => $canonicalEdition?->edition_year,
            'edition_count' => $editions->count(),
            'archive_count' => $otherEditions->count(),
            'years' => $editions->pluck('edition_year')->filter()->map(fn ($year): int => (int) $year)->unique()->sortDesc()->values()->all(),
            'previous_editions' => $otherEditions
                ->take(3)
                ->map(fn (World $edition): array => [
                    'id' => (int) $edition->id,
                    'title' => (string) $edition->title,
                    'edition_year' => $edition->edition_year,
                    'public_url' => $this->publicUrlForWorld($edition),
                ])
                ->all(),
        ];
    }

    private function adjacentEditionForWorld(World $world, string $direction): ?World
    {
        $familyEditions = $this->familyEditionsForWorld($world);

        if ($familyEditions->isEmpty() || $world->edition_year === null) {
            return null;
        }

        $currentYear = (int) $world->edition_year;

        if ($direction === 'previous') {
            return $familyEditions
                ->filter(fn (World $edition): bool => $edition->edition_year !== null && (int) $edition->edition_year < $currentYear)
                ->sortByDesc([
                    fn (World $edition): int => (int) ($edition->edition_year ?? 0),
                    fn (World $edition): int => $edition->starts_at?->getTimestamp() ?? $edition->published_at?->getTimestamp() ?? 0,
                    fn (World $edition): int => (int) $edition->id,
                ])
                ->first();
        }

        return $familyEditions
            ->filter(fn (World $edition): bool => $edition->edition_year !== null && (int) $edition->edition_year > $currentYear)
            ->sortBy([
                fn (World $edition): int => (int) ($edition->edition_year ?? 0),
                fn (World $edition): int => $edition->starts_at?->getTimestamp() ?? $edition->published_at?->getTimestamp() ?? 0,
                fn (World $edition): int => (int) $edition->id,
            ])
            ->first();
    }

    private function archiveNoticePayload(World $world, ?World $canonicalEdition): ?array
    {
        $familyTitle = $this->recurrenceFamilyLabel($world);

        if ($familyTitle === null) {
            return null;
        }

        if ($canonicalEdition && (int) $canonicalEdition->id !== (int) $world->id) {
            return [
                'eyebrow' => 'Archived edition',
                'title' => sprintf('You are viewing the %s archived edition of %s.', (string) ($world->edition_year ?? 'previous'), $familyTitle),
                'description' => $world->hasPublishedRecap()
                    ? 'Past editions remain public as part of the recurring campaign archive, and this edition now carries a published recap.'
                    : 'Past editions remain public as part of the recurring campaign archive.',
                'current_edition' => $this->mapWorldCard($canonicalEdition, $this->phaseForWorld($canonicalEdition)),
            ];
        }

        if ((string) $world->status === World::STATUS_ARCHIVED || ($world->ends_at && $world->ends_at->isPast())) {
            return [
                'eyebrow' => 'Archive edition',
                'title' => sprintf('You are viewing the latest archived edition of %s.', $familyTitle),
                'description' => $world->hasPublishedRecap()
                    ? 'No newer public edition is live right now, but this archived edition now preserves its highlights as a published recap.'
                    : 'No newer public edition is live right now, but the family archive remains readable and linked together.',
                'current_edition' => null,
            ];
        }

        return null;
    }

    private function phaseForWorld(World $world): string
    {
        if ($world->isActiveCampaign()) {
            return 'active';
        }

        if ($world->isUpcomingCampaign() || ($world->starts_at && $world->starts_at->isFuture())) {
            return 'upcoming';
        }

        if ((string) $world->status === World::STATUS_ARCHIVED || ($world->ends_at && $world->ends_at->isPast())) {
            return 'archive';
        }

        return 'featured';
    }

    private function duplicateTitle(World $world): string
    {
        $title = trim((string) $world->title);

        return $title === '' ? 'World copy' : $title . ' Copy';
    }

    private function nextEditionYear(World $world): int
    {
        return max((int) now()->year, (int) ($world->edition_year ?? now()->year)) + 1;
    }

    private function nextEditionTitle(World $world): string
    {
        $nextYear = $this->nextEditionYear($world);
        $title = $this->stripTrailingEditionYear(trim((string) $world->title));

        if ($title === '') {
            return 'World ' . $nextYear;
        }

        return $title . ' ' . $nextYear;
    }

    private function nextEditionSlug(World $world): string
    {
        $nextYear = $this->nextEditionYear($world);
        $slug = $this->inferredRecurrenceKey($world);

        if ($slug === '') {
            return 'world-' . $nextYear;
        }

        return $slug . '-' . $nextYear;
    }

    private function inferredRecurrenceKey(World $world): string
    {
        $recurrenceKey = trim((string) ($world->recurrence_key ?? ''));

        if ($recurrenceKey !== '') {
            return $recurrenceKey;
        }

        $slug = Str::slug($this->stripTrailingEditionYear(trim((string) $world->slug)));
        if ($slug !== '') {
            return $slug;
        }

        $title = Str::slug($this->stripTrailingEditionYear(trim((string) $world->title)));

        return $title !== '' ? $title : 'world';
    }

    private function stripTrailingEditionYear(string $value): string
    {
        $trimmed = trim($value);

        if ($trimmed === '') {
            return '';
        }

        return trim((string) preg_replace('/(?:[\s-]+)(?:19|20)\d{2}$/', '', $trimmed));
    }

    private function searchArtworks(string $query): array
    {
        return Artwork::query()
            ->with(['user.profile', 'group', 'categories.contentType'])
            ->leftJoin('artwork_stats', 'artwork_stats.artwork_id', '=', 'artworks.id')
            ->select('artworks.*')
            ->where('artwork_status', 'published')
            ->where('visibility', Artwork::VISIBILITY_PUBLIC)
            ->when($query !== '', function (Builder $builder) use ($query): void {
                $builder->where(function (Builder $nested) use ($query): void {
                    $like = '%' . $query . '%';

                    $nested->where('title', 'like', $like)
                        ->orWhere('slug', 'like', $like)
                        ->orWhere('description', 'like', $like)
                        ->orWhereHas('user', function (Builder $userQuery) use ($like): void {
                            $userQuery->where('username', 'like', $like)
                                ->orWhere('name', 'like', $like);
                        })
                        ->orWhereHas('group', function (Builder $groupQuery) use ($like): void {
                            $groupQuery->where('name', 'like', $like)
                                ->orWhere('slug', 'like', $like);
                        })
                        ->orWhereHas('categories', function (Builder $categoryQuery) use ($like): void {
                            $categoryQuery->where('name', 'like', $like)
                                ->orWhere('slug', 'like', $like)
                                ->orWhereHas('contentType', function (Builder $contentTypeQuery) use ($like): void {
                                    $contentTypeQuery->where('name', 'like', $like)
                                        ->orWhere('slug', 'like', $like);
                                });
                        });
                });
            })
                        ->orderByRaw('COALESCE(artwork_stats.views, 0) DESC')
                        ->orderByDesc('artworks.published_at')
            ->limit(8)
            ->get()
            ->map(fn (Artwork $artwork): ?array => $this->resolveArtworkPreview((int) $artwork->id, ''))
            ->filter()
            ->values()
            ->all();
    }

    private function searchCollections(string $query, ?User $viewer): array
    {
        return Collection::query()
            ->with(['user.profile', 'coverArtwork'])
            ->public()
            ->when($query !== '', function (Builder $builder) use ($query): void {
                $builder->where(function (Builder $nested) use ($query): void {
                    $nested->where('title', 'like', '%' . $query . '%')
                        ->orWhere('slug', 'like', '%' . $query . '%')
                        ->orWhere('summary', 'like', '%' . $query . '%');
                });
            })
            ->orderByDesc('followers_count')
            ->limit(8)
            ->get()
            ->map(fn (Collection $collection): ?array => $this->resolveCollectionPreview((int) $collection->id, $viewer, ''))
            ->filter()
            ->values()
            ->all();
    }

    private function searchUsers(string $query): array
    {
        return User::query()
            ->with(['profile', 'statistics'])
            ->when($query !== '', function (Builder $builder) use ($query): void {
                $builder->where(function (Builder $nested) use ($query): void {
                    $nested->where('username', 'like', '%' . $query . '%')
                        ->orWhere('name', 'like', '%' . $query . '%');
                });
            })
            ->orderByDesc('nova_featured_creator')
            ->orderBy('username')
            ->limit(8)
            ->get()
            ->map(fn (User $user): ?array => $this->resolveUserPreview((int) $user->id, ''))
            ->filter()
            ->values()
            ->all();
    }

    private function searchGroups(string $query, ?User $viewer): array
    {
        return Group::query()
            ->with(['owner.profile', 'recruitmentProfile', 'discoveryMetric', 'members', 'badges'])
            ->public()
            ->when($query !== '', function (Builder $builder) use ($query): void {
                $builder->where(function (Builder $nested) use ($query): void {
                    $nested->where('name', 'like', '%' . $query . '%')
                        ->orWhere('slug', 'like', '%' . $query . '%')
                        ->orWhere('headline', 'like', '%' . $query . '%');
                });
            })
            ->orderByDesc('followers_count')
            ->limit(8)
            ->get()
            ->map(fn (Group $group): ?array => $this->resolveGroupPreview((int) $group->id, $viewer, ''))
            ->filter()
            ->values()
            ->all();
    }

    private function searchNews(string $query): array
    {
        return NewsArticle::query()
            ->with(['author.profile', 'category'])
            ->published()
            ->when($query !== '', function (Builder $builder) use ($query): void {
                $builder->where(function (Builder $nested) use ($query): void {
                    $nested->where('title', 'like', '%' . $query . '%')
                        ->orWhere('slug', 'like', '%' . $query . '%')
                        ->orWhere('excerpt', 'like', '%' . $query . '%');
                });
            })
            ->orderByDesc('published_at')
            ->limit(8)
            ->get()
            ->map(fn (NewsArticle $article): ?array => $this->resolveNewsPreview((int) $article->id, ''))
            ->filter()
            ->values()
            ->all();
    }

    private function searchChallenges(string $query, ?User $viewer): array
    {
        return GroupChallenge::query()
            ->with('group')
            ->when($query !== '', fn (Builder $builder): Builder => $builder->where('title', 'like', '%' . $query . '%'))
            ->orderByDesc('start_at')
            ->limit(8)
            ->get()
            ->map(fn (GroupChallenge $challenge): ?array => $this->resolveChallengePreview((int) $challenge->id, $viewer, ''))
            ->filter()
            ->values()
            ->all();
    }

    private function searchEvents(string $query, ?User $viewer): array
    {
        return GroupEvent::query()
            ->with('group')
            ->when($query !== '', fn (Builder $builder): Builder => $builder->where('title', 'like', '%' . $query . '%'))
            ->orderByDesc('start_at')
            ->limit(8)
            ->get()
            ->map(fn (GroupEvent $event): ?array => $this->resolveEventPreview((int) $event->id, $viewer, ''))
            ->filter()
            ->values()
            ->all();
    }

    private function searchReleases(string $query, ?User $viewer): array
    {
        return GroupRelease::query()
            ->with('group')
            ->when($query !== '', fn (Builder $builder): Builder => $builder->where('title', 'like', '%' . $query . '%'))
            ->orderByDesc('published_at')
            ->limit(8)
            ->get()
            ->map(fn (GroupRelease $release): ?array => $this->resolveReleasePreview((int) $release->id, $viewer, ''))
            ->filter()
            ->values()
            ->all();
    }

    private function searchCards(string $query, ?User $viewer): array
    {
        return NovaCard::query()
            ->with(['user.profile', 'category'])
            ->when($query !== '', function (Builder $builder) use ($query): void {
                $builder->where(function (Builder $nested) use ($query): void {
                    $nested->where('title', 'like', '%' . $query . '%')
                        ->orWhere('slug', 'like', '%' . $query . '%')
                        ->orWhere('description', 'like', '%' . $query . '%');
                });
            })
            ->orderByDesc('published_at')
            ->limit(8)
            ->get()
            ->map(fn (NovaCard $card): ?array => $this->resolveCardPreview((int) $card->id, $viewer, ''))
            ->filter()
            ->values()
            ->all();
    }

    public function resolveEntityPreview(string $type, int $entityId, ?User $viewer = null, string $contextLabel = ''): ?array
    {
        return match ($type) {
            WorldRelation::TYPE_ARTWORK => $this->resolveArtworkPreview($entityId, $contextLabel),
            WorldRelation::TYPE_COLLECTION => $this->resolveCollectionPreview($entityId, $viewer, $contextLabel),
            WorldRelation::TYPE_USER => $this->resolveUserPreview($entityId, $contextLabel),
            WorldRelation::TYPE_GROUP => $this->resolveGroupPreview($entityId, $viewer, $contextLabel),
            WorldRelation::TYPE_NEWS => $this->resolveNewsPreview($entityId, $contextLabel),
            WorldRelation::TYPE_CHALLENGE => $this->resolveChallengePreview($entityId, $viewer, $contextLabel),
            WorldRelation::TYPE_EVENT => $this->resolveEventPreview($entityId, $viewer, $contextLabel),
            WorldRelation::TYPE_RELEASE => $this->resolveReleasePreview($entityId, $viewer, $contextLabel),
            WorldRelation::TYPE_CARD => $this->resolveCardPreview($entityId, $viewer, $contextLabel),
            default => null,
        };
    }

    public function previewArtwork(Artwork $artwork, string $contextLabel = ''): ?array
    {
        if ((string) $artwork->artwork_status !== 'published' || (string) $artwork->visibility !== Artwork::VISIBILITY_PUBLIC) {
            return null;
        }

        $artwork->loadMissing(['user.profile', 'categories.contentType', 'stats']);
        $resource = ArtworkListResource::make($artwork)->toArray(request());
        $views = (int) ($artwork->stats?->views ?? 0);

        return [
            'id' => (int) $artwork->id,
            'entity_type' => WorldRelation::TYPE_ARTWORK,
            'entity_label' => (string) (config('worlds.relation_types.artwork') ?? 'Artwork'),
            'title' => (string) ($resource['title'] ?? 'Untitled artwork'),
            'subtitle' => (string) ($resource['author']['name'] ?? ''),
            'description' => Str::limit(trim(strip_tags((string) ($artwork->description ?? ''))), 120),
            'url' => (string) ($resource['urls']['canonical'] ?? route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug ?: Str::slug((string) $artwork->title)])),
            'image' => $resource['thumbnail_url'] ?? $artwork->thumbUrl('md'),
            'avatar' => null,
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Featured artwork',
            'meta' => array_values(array_filter([
                $resource['category']['name'] ?? null,
                $views > 0 ? number_format($views) . ' views' : null,
            ])),
        ];
    }

    public function previewCollection(Collection $collection, ?User $viewer = null, string $contextLabel = ''): ?array
    {
        $collection->loadMissing(['user.profile', 'group', 'coverArtwork']);

        if (! $collection->canBeViewedBy($viewer) || ! $collection->user?->username) {
            return null;
        }

        $payload = $this->collections->mapCollectionCardPayloads([$collection], false, $viewer)[0] ?? null;

        if (! $payload) {
            return null;
        }

        return array_merge($payload, [
            'entity_type' => WorldRelation::TYPE_COLLECTION,
            'entity_label' => (string) (config('worlds.relation_types.collection') ?? 'Collection'),
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Curated collection',
        ]);
    }

    public function previewUser(User $user, string $contextLabel = ''): ?array
    {
        $user->loadMissing(['profile', 'statistics']);

        if (! $user->username) {
            return null;
        }

        return [
            'id' => (int) $user->id,
            'entity_type' => WorldRelation::TYPE_USER,
            'entity_label' => (string) (config('worlds.relation_types.user') ?? 'Creator'),
            'title' => (string) ($user->name ?: $user->username),
            'subtitle' => $user->username ? '@' . $user->username : null,
            'description' => Str::limit((string) ($user->profile?->description ?? $user->profile?->about ?? ''), 140),
            'url' => route('profile.show', ['username' => strtolower((string) $user->username)]),
            'image' => $user->cover_hash && $user->cover_ext ? CoverUrl::forUser($user->cover_hash, $user->cover_ext, $user->updated_at?->timestamp) : null,
            'avatar' => AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash, 128),
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Featured creator',
            'meta' => array_values(array_filter([
                $user->nova_featured_creator ? 'Featured creator' : null,
                (int) $user->followers_count > 0 ? number_format((int) $user->followers_count) . ' followers' : null,
            ])),
        ];
    }

    public function previewGroup(Group $group, ?User $viewer = null, string $contextLabel = ''): ?array
    {
        $group->loadMissing(['owner.profile', 'recruitmentProfile', 'discoveryMetric', 'members', 'badges']);

        if (! $group->canBeViewedBy($viewer)) {
            return null;
        }

        $card = $this->groups->mapGroupCard($group, $viewer);

        return array_merge($card, [
            'entity_type' => WorldRelation::TYPE_GROUP,
            'entity_label' => (string) (config('worlds.relation_types.group') ?? 'Group'),
            'title' => (string) ($card['name'] ?? $group->name),
            'subtitle' => (string) (($card['owner']['name'] ?? null) ?: 'Group'),
            'description' => (string) (($card['bio_excerpt'] ?? null) ?: ($card['headline'] ?? '')),
            'url' => route('groups.show', ['group' => $group->slug]),
            'image' => $card['banner_url'] ?? null,
            'avatar' => $card['avatar_url'] ?? null,
            'meta' => array_values(array_filter([
                (bool) ($card['is_verified'] ?? false) ? 'Verified group' : null,
                ((int) data_get($card, 'counts.followers', 0)) > 0 ? number_format((int) data_get($card, 'counts.followers', 0)) . ' followers' : null,
                ((int) data_get($card, 'counts.artworks', 0)) > 0 ? number_format((int) data_get($card, 'counts.artworks', 0)) . ' artworks' : null,
            ])),
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Featured group',
        ]);
    }

    public function previewNews(NewsArticle $article, string $contextLabel = ''): ?array
    {
        return [
            'id' => (int) $article->id,
            'entity_type' => WorldRelation::TYPE_NEWS,
            'entity_label' => (string) (config('worlds.relation_types.news') ?? 'News article'),
            'title' => (string) $article->title,
            'subtitle' => (string) ($article->category?->name ?? 'News'),
            'description' => Str::limit((string) ($article->excerpt ?: strip_tags((string) $article->content)), 160),
            'url' => route('news.show', ['slug' => $article->slug]),
            'image' => $article->cover_url,
            'avatar' => $article->author ? AvatarUrl::forUser((int) $article->author->id, $article->author->profile?->avatar_hash, 72) : null,
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Related story',
            'meta' => array_values(array_filter([
                $article->published_at?->format('d M Y'),
                (string) ($article->type_label ?? ''),
            ])),
        ];
    }

    public function previewChallenge(GroupChallenge $challenge, ?User $viewer = null, string $contextLabel = ''): ?array
    {
        $challenge->loadMissing(['group', 'outcomes']);

        if (! $challenge->group || ! $challenge->canBeViewedBy($viewer)) {
            return null;
        }

        $winnerIds = $this->challengeOutcomeArtworkIds($challenge, GroupChallengeOutcome::TYPE_WINNER)->all();
        $finalistIds = $this->challengeOutcomeArtworkIds($challenge, GroupChallengeOutcome::TYPE_FINALIST)->all();
        $entryPreviewItems = $this->visibleChallengeArtworkQuery($challenge, $viewer)
            ->orderBy('group_challenge_artworks.sort_order')
            ->limit(12)
            ->get()
            ->map(function (Artwork $artwork) use ($winnerIds, $finalistIds): array {
                $status = 'entry';

                if (in_array((int) $artwork->id, $winnerIds, true)) {
                    $status = 'winner';
                } elseif (in_array((int) $artwork->id, $finalistIds, true)) {
                    $status = 'finalist';
                }

                return [
                    'id' => (int) $artwork->id,
                    'title' => (string) $artwork->title,
                    'image' => $artwork->thumbUrl('sm'),
                    'status' => $status,
                ];
            })
            ->all();

        return [
            'id' => (int) $challenge->id,
            'entity_type' => WorldRelation::TYPE_CHALLENGE,
            'entity_label' => (string) (config('worlds.relation_types.challenge') ?? 'Challenge'),
            'title' => (string) $challenge->title,
            'subtitle' => (string) $challenge->group->name,
            'description' => Str::limit((string) ($challenge->summary ?: $challenge->description ?: ''), 140),
            'url' => route('groups.challenges.show', ['group' => $challenge->group, 'challenge' => $challenge]),
            'image' => $challenge->coverUrl(),
            'avatar' => $challenge->group->avatarUrl(),
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Join the challenge',
            'meta' => array_values(array_filter([
                $challenge->start_at?->format('d M Y'),
                Str::headline((string) $challenge->status),
            ])),
            'judging_mode' => (string) ($challenge->judging_mode ?? ''),
            'entry_preview_items' => $entryPreviewItems,
        ];
    }

    private function resolveArtworkPreview(int $entityId, string $contextLabel): ?array
    {
        $artwork = Artwork::query()->with(['user.profile', 'categories.contentType', 'stats'])->find($entityId);

        if (! $artwork) {
            return null;
        }

        return $this->previewArtwork($artwork, $contextLabel);
    }

    private function resolveCollectionPreview(int $entityId, ?User $viewer, string $contextLabel): ?array
    {
        $collection = Collection::query()->with(['user.profile', 'group', 'coverArtwork'])->find($entityId);

        if (! $collection) {
            return null;
        }

        return $this->previewCollection($collection, $viewer, $contextLabel);
    }

    private function resolveUserPreview(int $entityId, string $contextLabel): ?array
    {
        $user = User::query()->with(['profile', 'statistics'])->find($entityId);

        if (! $user) {
            return null;
        }

        return $this->previewUser($user, $contextLabel);
    }

    private function resolveGroupPreview(int $entityId, ?User $viewer, string $contextLabel): ?array
    {
        $group = Group::query()->with(['owner.profile', 'recruitmentProfile', 'discoveryMetric', 'members', 'badges'])->find($entityId);

        if (! $group) {
            return null;
        }

        return $this->previewGroup($group, $viewer, $contextLabel);
    }

    private function resolveNewsPreview(int $entityId, string $contextLabel): ?array
    {
        $article = NewsArticle::query()->with(['author.profile', 'category'])->published()->find($entityId);

        if (! $article) {
            return null;
        }

        return $this->previewNews($article, $contextLabel);
    }

    private function resolveChallengePreview(int $entityId, ?User $viewer, string $contextLabel): ?array
    {
        $challenge = GroupChallenge::query()->with(['group', 'outcomes'])->find($entityId);

        if (! $challenge) {
            return null;
        }

        return $this->previewChallenge($challenge, $viewer, $contextLabel);
    }

    private function resolveEventPreview(int $entityId, ?User $viewer, string $contextLabel): ?array
    {
        $event = GroupEvent::query()->with('group')->find($entityId);

        if (! $event || ! $event->group || ! $event->canBeViewedBy($viewer)) {
            return null;
        }

        return [
            'id' => (int) $event->id,
            'entity_type' => WorldRelation::TYPE_EVENT,
            'entity_label' => (string) (config('worlds.relation_types.event') ?? 'Event'),
            'title' => (string) $event->title,
            'subtitle' => (string) $event->group->name,
            'description' => Str::limit((string) ($event->summary ?: $event->description ?: ''), 140),
            'url' => route('groups.events.show', ['group' => $event->group, 'event' => $event]),
            'image' => $event->coverUrl(),
            'avatar' => $event->group->avatarUrl(),
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Related event',
            'meta' => array_values(array_filter([
                $event->start_at?->format('d M Y H:i'),
                $event->location,
            ])),
        ];
    }

    private function resolveReleasePreview(int $entityId, ?User $viewer, string $contextLabel): ?array
    {
        $release = GroupRelease::query()->with('group')->find($entityId);

        if (! $release || ! $release->group || ! $release->canBeViewedBy($viewer)) {
            return null;
        }

        return [
            'id' => (int) $release->id,
            'entity_type' => WorldRelation::TYPE_RELEASE,
            'entity_label' => (string) (config('worlds.relation_types.release') ?? 'Release'),
            'title' => (string) $release->title,
            'subtitle' => (string) $release->group->name,
            'description' => Str::limit((string) ($release->summary ?: $release->description ?: ''), 140),
            'url' => route('groups.releases.show', ['group' => $release->group, 'release' => $release]),
            'image' => $release->coverUrl(),
            'avatar' => $release->group->avatarUrl(),
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Release spotlight',
            'meta' => array_values(array_filter([
                $release->published_at?->format('d M Y'),
                Str::headline((string) $release->status),
            ])),
        ];
    }

    private function resolveCardPreview(int $entityId, ?User $viewer, string $contextLabel): ?array
    {
        $card = NovaCard::query()->with(['user.profile', 'category'])->find($entityId);

        if (! $card || ! $card->canBeViewedBy($viewer)) {
            return null;
        }

        return [
            'id' => (int) $card->id,
            'entity_type' => WorldRelation::TYPE_CARD,
            'entity_label' => (string) (config('worlds.relation_types.card') ?? 'Card'),
            'title' => (string) ($card->title ?: 'Untitled card'),
            'subtitle' => (string) ($card->category?->name ?? ''),
            'description' => Str::limit((string) ($card->description ?? $card->quote_text ?? ''), 140),
            'url' => $card->publicUrl(),
            'image' => $card->previewUrl(),
            'avatar' => $card->user ? AvatarUrl::forUser((int) $card->user->id, $card->user->profile?->avatar_hash, 72) : null,
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Themed card',
            'meta' => array_values(array_filter([
                Str::headline((string) $card->format),
                (int) $card->likes_count > 0 ? number_format((int) $card->likes_count) . ' likes' : null,
            ])),
        ];
    }

    private function entityMapKey(string $type, int $id): string
    {
        return $type . ':' . $id;
    }

    private function deleteWorldMediaIfReplaced(string $originalPath, string $currentPath): void
    {
        $trimmedOriginal = ltrim(trim($originalPath), '/');
        $trimmedCurrent = ltrim(trim($currentPath), '/');

        if ($trimmedOriginal === '' || $trimmedOriginal === $trimmedCurrent || ! Str::startsWith($trimmedOriginal, 'worlds/media/')) {
            return;
        }

        Storage::disk((string) config('covers.disk', 's3'))->delete($trimmedOriginal);
    }
}