<?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\GroupEvent;
use App\Models\GroupRelease;
use App\Models\NovaCard;
use App\Models\User;
use App\Models\World;
use App\Models\WorldRelation;
use App\Services\CollectionService;
use App\Services\GroupCardService;
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\Storage;
use Illuminate\Support\Str;
use cPad\Plugins\News\Models\NewsArticle;

final class WorldService
{
    public function __construct(
        private readonly CollectionService $collections,
        private readonly GroupCardService $groups,
        private readonly WorldSubmissionService $submissions,
    ) {
    }

    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')->orderByDesc('is_featured')->orderByRaw("CASE WHEN status = 'published' THEN 0 WHEN status = 'draft' THEN 1 ELSE 2 END")->orderByDesc('starts_at')->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']);

        return [
            'id' => (int) $world->id,
            'title' => (string) $world->title,
            'slug' => (string) $world->slug,
            'tagline' => (string) ($world->tagline ?? ''),
            'summary' => (string) ($world->summary ?? ''),
            'description' => (string) ($world->description ?? ''),
            'cover_path' => (string) ($world->cover_path ?? ''),
            'cover_url' => $world->coverUrl(),
            '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(),
            '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_recurring' => (bool) $world->is_recurring,
            'recurrence_key' => (string) ($world->recurrence_key ?? ''),
            'recurrence_rule' => (string) ($world->recurrence_rule ?? ''),
            'edition_year' => $world->edition_year,
            'cta_label' => (string) ($world->cta_label ?? ''),
            'cta_url' => (string) ($world->cta_url ?? ''),
            'badge_label' => (string) ($world->badge_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(),
            '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,
            'related_tags_json' => array_values(array_map('strval', $world->related_tags_json ?? [])),
            'section_order_json' => $world->sectionOrder(),
            'section_visibility_json' => $world->sectionVisibility(),
            '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),
            '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]),
                '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
    {
        $source->loadMissing('worldRelations');

        $data = [
            'title' => $asNewEdition ? $this->nextEditionTitle($source) : $this->duplicateTitle($source),
            'slug' => $asNewEdition ? $this->nextEditionSlug($source) : $source->slug . '-copy',
            'tagline' => $source->tagline,
            'summary' => $source->summary,
            'description' => $source->description,
            'cover_path' => $source->cover_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,
            'accepts_submissions' => (bool) $source->accepts_submissions,
            'participation_mode' => (string) ($source->participation_mode ?: World::PARTICIPATION_MODE_CLOSED),
            'submission_starts_at' => $source->submission_starts_at,
            'submission_ends_at' => $source->submission_ends_at,
            '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_recurring' => $asNewEdition ? true : $source->is_recurring,
            'recurrence_key' => $asNewEdition ? ($source->recurrence_key ?: Str::slug((string) $source->slug)) : $source->recurrence_key,
            'recurrence_rule' => $source->recurrence_rule,
            'edition_year' => $asNewEdition ? $this->nextEditionYear($source) : $source->edition_year,
            'cta_label' => $source->cta_label,
            'cta_url' => $source->cta_url,
            'badge_label' => $source->badge_label,
            'badge_description' => $source->badge_description,
            'submission_guidelines' => $source->submission_guidelines,
            'badge_url' => $source->badge_url,
            'seo_title' => $source->seo_title,
            'seo_description' => $source->seo_description,
            'og_image_path' => $source->og_image_path,
            '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' => $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 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']);
    }

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

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

    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
    {
        $featuredWorld = World::query()->current()->where('is_featured', true)->latest('starts_at')->first()
            ?? World::query()->published()->where('is_featured', true)->latest('published_at')->first();

        $current = World::query()->current()->orderByDesc('is_featured')->orderBy('starts_at')->limit(12)->get();
        $upcoming = World::query()->upcoming()->orderBy('starts_at')->limit(12)->get();
        $archive = World::query()->archive()->orderByDesc('ends_at')->orderByDesc('published_at')->limit(18)->get();

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

    public function publicShowPayload(World $world, ?User $viewer = null): array
    {
        $world->loadMissing(['createdBy.profile', 'parentWorld', 'archiveEditions', 'worldRelations']);

        $sections = $this->resolveSections($world, $viewer);
        $archiveEditions = $world->archiveEditions
            ->filter(fn (World $edition): bool => $edition->isPubliclyVisible())
            ->sortByDesc(fn (World $edition): int => (int) ($edition->edition_year ?? 0))
            ->values()
            ->map(fn (World $edition): array => $this->mapWorldCard($edition, 'archive'))
            ->all();

        $relatedWorlds = World::query()
            ->publiclyVisible()
            ->when($world->recurrence_key, fn (Builder $builder) => $builder->where('recurrence_key', $world->recurrence_key))
            ->where('id', '!=', $world->id)
            ->orderByDesc('starts_at')
            ->limit(3)
            ->get()
            ->map(fn (World $item): array => $this->mapWorldCard($item, $item->isCurrent() ? 'active' : 'archive'))
            ->all();

        return [
            'world' => $this->mapWorldDetail($world),
            'sections' => $sections,
            'communitySubmissions' => $this->submissions->publicSectionPayload($world, $viewer),
            'archiveEditions' => $archiveEditions,
            'relatedWorlds' => $relatedWorlds,
        ];
    }

    public function homepageSpotlight(?User $viewer = null): ?array
    {
        $world = World::query()->current()->where('is_featured', true)->latest('starts_at')->first()
            ?? World::query()->current()->latest('starts_at')->first()
            ?? World::query()->upcoming()->where('is_featured', true)->orderBy('starts_at')->first();

        if (! $world) {
            return null;
        }

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

        return [
            'id' => $payload['id'],
            'title' => $payload['title'],
            'tagline' => $payload['tagline'],
            'summary' => $payload['summary'],
            'cover_url' => $payload['cover_url'],
            'icon_name' => $payload['icon_name'],
            'badge_label' => $payload['badge_label'],
            'timeframe_label' => $payload['timeframe_label'],
            'theme' => $payload['theme'],
            'cta_label' => $payload['cta_label'] ?: 'Explore world',
            'cta_url' => $payload['public_url'],
            'public_url' => $payload['public_url'],
        ];
    }

    private function persist(World $world, User $editor, array $data): World
    {
        $originalCoverPath = (string) ($world->cover_path ?? '');
        $originalOgImagePath = (string) ($world->og_image_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),
            'description' => $this->nullableText($data['description'] ?? null),
            'cover_path' => $this->nullableText($data['cover_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,
            '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_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),
            '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),
            '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,
            'published_at' => $publishedAt,
        ]);

        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($originalOgImagePath, (string) ($world->og_image_path ?? ''));

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

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

    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'],
            'slug' => $detail['slug'],
            'tagline' => $detail['tagline'],
            'summary' => $detail['summary'],
            'cover_url' => $detail['cover_url'],
            'theme' => $detail['theme'],
            'type' => $detail['type'],
            'status' => $detail['status'],
            'phase' => $phase,
            'badge_label' => $detail['badge_label'],
            'icon_name' => $detail['icon_name'],
            'timeframe_label' => $detail['timeframe_label'],
            'starts_at' => $detail['starts_at'],
            'ends_at' => $detail['ends_at'],
            'public_url' => $detail['public_url'],
            'cta_label' => $detail['cta_label'],
            'is_featured' => $detail['is_featured'],
        ];
    }

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

        return [
            'id' => (int) $world->id,
            'title' => (string) $world->title,
            'slug' => (string) $world->slug,
            'tagline' => (string) ($world->tagline ?? ''),
            'summary' => (string) ($world->summary ?? ''),
            'description' => (string) ($world->description ?? ''),
            'cover_url' => $world->coverUrl(),
            'type' => (string) $world->type,
            'status' => (string) $world->status,
            'theme' => $theme,
            'icon_name' => $this->resolvedIconName($world, $theme),
            'badge_label' => (string) ($world->badge_label ?? ''),
            'badge_description' => (string) ($world->badge_description ?? ''),
            'badge_url' => (string) ($world->badge_url ?? ''),
            'cta_label' => (string) ($world->cta_label ?? ''),
            'cta_url' => (string) ($world->cta_url ?? ''),
            'starts_at' => optional($world->starts_at)?->toIso8601String(),
            'ends_at' => optional($world->ends_at)?->toIso8601String(),
            'timeframe_label' => $this->timeframeLabel($world),
            'related_tags' => array_values(array_map('strval', $world->related_tags_json ?? [])),
            'recurrence_key' => (string) ($world->recurrence_key ?? ''),
            'edition_year' => $world->edition_year,
            'is_recurring' => (bool) $world->is_recurring,
            'is_featured' => (bool) $world->is_featured,
            'public_url' => $world->publicUrl(),
        ];
    }

    private function mapStudioListItem(World $world): array
    {
        return [
            'id' => (int) $world->id,
            'title' => (string) $world->title,
            'slug' => (string) $world->slug,
            'status' => (string) $world->status,
            'type' => (string) $world->type,
            'theme_key' => (string) ($world->theme_key ?? ''),
            'summary' => Str::limit(trim(strip_tags((string) ($world->summary ?: $world->description ?: ''))), 120),
            'timeframe_label' => $this->timeframeLabel($world),
            'relation_count' => (int) ($world->world_relations_count ?? 0),
            'is_featured' => (bool) $world->is_featured,
            'edit_url' => route('studio.worlds.edit', ['world' => $world]),
            'preview_url' => route('studio.worlds.preview', ['world' => $world]),
            'public_url' => $world->publicUrl(),
        ];
    }

    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 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 = (array) config('worlds.themes.' . $world->theme_key, []);

        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 ??= (array) config('worlds.themes.' . $world->theme_key, []);

        $icon = trim((string) ($world->icon_name ?? ''));
        if ($icon !== '') {
            return $icon;
        }

        $themeIcon = trim((string) ($theme['icon_name'] ?? ''));
        if ($themeIcon !== '') {
            return $themeIcon;
        }

        return 'fa-solid fa-sparkles';
    }

    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 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 = trim((string) $world->title);

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

        if ($world->edition_year && str_contains($title, (string) $world->edition_year)) {
            return str_replace((string) $world->edition_year, (string) $nextYear, $title);
        }

        return $title . ' ' . $nextYear;
    }

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

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

        if ($world->edition_year && str_contains($slug, (string) $world->edition_year)) {
            return str_replace((string) $world->edition_year, (string) $nextYear, $slug);
        }

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

    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,
        };
    }

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

        if (! $artwork || (string) $artwork->artwork_status !== 'published' || (string) $artwork->visibility !== Artwork::VISIBILITY_PUBLIC) {
            return null;
        }

        $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,
            ])),
        ];
    }

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

        if (! $collection || ! $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',
        ]);
    }

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

        if (! $user || ! $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,
            ])),
        ];
    }

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

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

        return array_merge($this->groups->mapGroupCard($group, $viewer), [
            'entity_type' => WorldRelation::TYPE_GROUP,
            'entity_label' => (string) (config('worlds.relation_types.group') ?? 'Group'),
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Featured group',
        ]);
    }

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

        if (! $article) {
            return null;
        }

        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 ?? ''),
            ])),
        ];
    }

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

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

        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),
            ])),
        ];
    }

    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);
    }
}