<?php

declare(strict_types=1);

namespace App\Services\News;

use App\Models\Artwork;
use App\Models\Collection;
use App\Models\Group;
use App\Models\GroupChallenge;
use App\Models\GroupEvent;
use App\Models\GroupProject;
use App\Models\GroupRelease;
use App\Models\User;
use App\Support\AvatarUrl;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use cPad\Plugins\News\Models\NewsArticle;
use cPad\Plugins\News\Models\NewsArticleRelation;
use cPad\Plugins\News\Models\NewsCategory;
use cPad\Plugins\News\Models\NewsTag;

final class NewsService
{
    public const RELATION_GROUP = 'group';
    public const RELATION_ARTWORK = 'artwork';
    public const RELATION_COLLECTION = 'collection';
    public const RELATION_RELEASE = 'release';
    public const RELATION_PROJECT = 'project';
    public const RELATION_CHALLENGE = 'challenge';
    public const RELATION_EVENT = 'event';
    public const RELATION_USER = 'user';

    public const RELATION_LABELS = [
        self::RELATION_GROUP => 'Group',
        self::RELATION_ARTWORK => 'Artwork',
        self::RELATION_COLLECTION => 'Collection',
        self::RELATION_RELEASE => 'Release',
        self::RELATION_PROJECT => 'Project',
        self::RELATION_CHALLENGE => 'Challenge',
        self::RELATION_EVENT => 'Event',
        self::RELATION_USER => 'Profile',
    ];

    public function articleTypeOptions(): array
    {
        return \collect(NewsArticle::TYPE_LABELS)
            ->map(fn (string $label, string $value): array => ['value' => $value, 'label' => $label])
            ->values()
            ->all();
    }

    public function editorialStatusOptions(): array
    {
        return [
            ['value' => NewsArticle::EDITORIAL_STATUS_DRAFT, 'label' => 'Draft'],
            ['value' => NewsArticle::EDITORIAL_STATUS_IN_REVIEW, 'label' => 'In review'],
            ['value' => NewsArticle::EDITORIAL_STATUS_SCHEDULED, 'label' => 'Scheduled'],
            ['value' => NewsArticle::EDITORIAL_STATUS_PUBLISHED, 'label' => 'Published'],
            ['value' => NewsArticle::EDITORIAL_STATUS_ARCHIVED, 'label' => 'Archived'],
        ];
    }

    public function relationTypeOptions(): array
    {
        return \collect(self::RELATION_LABELS)
            ->map(fn (string $label, string $value): array => ['value' => $value, 'label' => $label])
            ->values()
            ->all();
    }

    public function categoryOptions(): array
    {
        return NewsCategory::query()
            ->ordered()
            ->get(['id', 'name'])
            ->map(fn (NewsCategory $category): array => [
                'id' => (int) $category->id,
                'name' => (string) $category->name,
            ])
            ->all();
    }

    public function tagOptions(): array
    {
        return NewsTag::query()
            ->orderBy('name')
            ->get(['id', 'name'])
            ->map(fn (NewsTag $tag): array => [
                'id' => (int) $tag->id,
                'name' => (string) $tag->name,
            ])
            ->all();
    }

    public function sidebarData(): array
    {
        return [
            'categories' => NewsCategory::active()->withCount('publishedArticles')->ordered()->get(),
            'trending' => NewsArticle::published()
                ->with('category')
                ->orderByDesc('views')
                ->limit(config('news.trending_limit', 5))
                ->get(['id', 'title', 'slug', 'views', 'published_at', 'category_id', 'type']),
            'tags' => NewsTag::whereHas('articles', fn ($query) => $query->published())->orderBy('name')->get(),
        ];
    }

    public function studioListing(array $filters = []): array
    {
        $query = NewsArticle::query()
            ->with(['author:id,username,name', 'category:id,name,slug', 'tags:id,name,slug'])
            ->editorialOrder();

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

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

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

        if ($categoryId > 0) {
            $query->where('category_id', $categoryId);
        }

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

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

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

    public function mapStudioArticle(NewsArticle $article, ?User $viewer = null): array
    {
        $article->loadMissing(['author.profile', 'category', 'tags', 'relatedEntities']);

        return [
            'id' => (int) $article->id,
            'title' => (string) $article->title,
            'slug' => (string) $article->slug,
            'excerpt' => (string) ($article->excerpt ?? ''),
            'content' => (string) ($article->content ?? ''),
            'cover_image' => (string) ($article->cover_image ?? ''),
            'cover_url' => $article->cover_url,
            'type' => (string) ($article->type ?? NewsArticle::TYPE_ANNOUNCEMENT),
            'editorial_status' => (string) ($article->editorial_status ?? NewsArticle::EDITORIAL_STATUS_DRAFT),
            'published_at' => \optional($article->published_at)?->toIso8601String(),
            'is_featured' => (bool) $article->is_featured,
            'is_pinned' => (bool) ($article->is_pinned ?? false),
            'category_id' => $article->category_id ? (int) $article->category_id : null,
            'author_id' => (int) $article->author_id,
            'author' => $article->author ? $this->mapUserLookupResult($article->author) : null,
            'tag_ids' => $article->tags->pluck('id')->map(fn (mixed $id): int => (int) $id)->all(),
            'meta_title' => (string) ($article->meta_title ?? ''),
            'meta_description' => (string) ($article->meta_description ?? ''),
            'meta_keywords' => (string) ($article->meta_keywords ?? ''),
            'canonical_url' => (string) ($article->canonical_url ?? ''),
            'og_title' => (string) ($article->og_title ?? ''),
            'og_description' => (string) ($article->og_description ?? ''),
            'og_image' => (string) ($article->og_image ?? ''),
            'relations' => $article->relatedEntities
                ->map(fn (NewsArticleRelation $relation): array => [
                    'entity_type' => (string) $relation->entity_type,
                    'entity_id' => (int) $relation->entity_id,
                    'context_label' => (string) ($relation->context_label ?? ''),
                    'preview' => $this->resolveEntityPreview((string) $relation->entity_type, (int) $relation->entity_id, $viewer),
                ])
                ->values()
                ->all(),
        ];
    }

    public function storeArticle(User $editor, array $data): NewsArticle
    {
        $article = new NewsArticle();
        $article->author_id = (int) ($data['author_id'] ?? $editor->id);

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

    public function updateArticle(NewsArticle $article, User $editor, array $data): NewsArticle
    {
        return $this->persistArticle($article, $editor, $data);
    }

    public function publish(NewsArticle $article): NewsArticle
    {
        $article->forceFill([
            'editorial_status' => NewsArticle::EDITORIAL_STATUS_PUBLISHED,
            'status' => 'published',
            'published_at' => $article->published_at ?? \now(),
        ])->save();

        return $article->fresh(['author', 'category', 'tags', 'relatedEntities']);
    }

    public function archive(NewsArticle $article): NewsArticle
    {
        $article->forceFill([
            'editorial_status' => NewsArticle::EDITORIAL_STATUS_ARCHIVED,
            'status' => 'draft',
        ])->save();

        return $article->fresh(['author', 'category', 'tags', 'relatedEntities']);
    }

    public function toggleFeature(NewsArticle $article): NewsArticle
    {
        $article->forceFill(['is_featured' => ! $article->is_featured])->save();

        return $article->fresh(['author', 'category', 'tags', 'relatedEntities']);
    }

    public function togglePin(NewsArticle $article): NewsArticle
    {
        $article->forceFill(['is_pinned' => ! (bool) $article->is_pinned])->save();

        return $article->fresh(['author', 'category', 'tags', 'relatedEntities']);
    }

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

        return match ($type) {
            self::RELATION_GROUP => $this->searchGroups($query, $viewer),
            self::RELATION_ARTWORK => $this->searchArtworks($query),
            self::RELATION_COLLECTION => $this->searchCollections($query, $viewer),
            self::RELATION_RELEASE => $this->searchReleases($query, $viewer),
            self::RELATION_PROJECT => $this->searchProjects($query, $viewer),
            self::RELATION_CHALLENGE => $this->searchChallenges($query, $viewer),
            self::RELATION_EVENT => $this->searchEvents($query, $viewer),
            self::RELATION_USER => $this->searchUsers($query),
            default => [],
        };
    }

    public function resolveRelatedEntities(NewsArticle $article, ?User $viewer = null): array
    {
        $article->loadMissing('relatedEntities');

        return $article->relatedEntities
            ->map(fn (NewsArticleRelation $relation): ?array => $this->resolveEntityPreview((string) $relation->entity_type, (int) $relation->entity_id, $viewer, (string) ($relation->context_label ?? '')))
            ->filter()
            ->values()
            ->all();
    }

    public function syncRelations(NewsArticle $article, array $relations): void
    {
        $normalized = \collect($relations)
            ->map(function (array $relation): ?array {
                $entityType = trim(Str::lower((string) ($relation['entity_type'] ?? '')));
                $entityId = (int) ($relation['entity_id'] ?? 0);

                if (! array_key_exists($entityType, self::RELATION_LABELS) || $entityId < 1) {
                    return null;
                }

                return [
                    'entity_type' => $entityType,
                    'entity_id' => $entityId,
                    'context_label' => Str::limit(trim((string) ($relation['context_label'] ?? '')), 120, ''),
                ];
            })
            ->filter()
            ->unique(fn (array $relation): string => $relation['entity_type'] . ':' . $relation['entity_id'])
            ->values();

        $article->relatedEntities()->delete();

        foreach ($normalized as $index => $relation) {
            $article->relatedEntities()->create([
                'entity_type' => $relation['entity_type'],
                'entity_id' => $relation['entity_id'],
                'context_label' => $relation['context_label'] !== '' ? $relation['context_label'] : null,
                'sort_order' => $index,
            ]);
        }
    }

    private function persistArticle(NewsArticle $article, User $editor, array $data): NewsArticle
    {
        $title = trim((string) ($data['title'] ?? $article->title ?? 'Untitled News Article'));
        if ($title === '') {
            $title = 'Untitled News Article';
        }

        $editorialStatus = $this->normalizeEditorialStatus((string) ($data['editorial_status'] ?? $article->editorial_status ?? NewsArticle::EDITORIAL_STATUS_DRAFT));
        $publishedAt = $this->normalizePublishedAt($editorialStatus, $data['published_at'] ?? $article->published_at);
        $authorId = (int) ($data['author_id'] ?? $article->author_id ?? $editor->id);

        $article->fill([
            'title' => $title,
            'slug' => $this->resolveSlug($title, $article, $data),
            'excerpt' => $this->nullableText($data['excerpt'] ?? null),
            'content' => (string) ($data['content'] ?? ''),
            'cover_image' => $this->nullableText($data['cover_image'] ?? null),
            'type' => (string) ($data['type'] ?? NewsArticle::TYPE_ANNOUNCEMENT),
            'author_id' => $authorId,
            'category_id' => ! empty($data['category_id']) ? (int) $data['category_id'] : null,
            'editorial_status' => $editorialStatus,
            'status' => $this->legacyStatusFor($editorialStatus),
            'published_at' => $publishedAt,
            'is_featured' => (bool) ($data['is_featured'] ?? false),
            'is_pinned' => (bool) ($data['is_pinned'] ?? false),
            'meta_title' => $this->nullableText($data['meta_title'] ?? null),
            'meta_description' => $this->nullableText($data['meta_description'] ?? null),
            'meta_keywords' => $this->nullableText($data['meta_keywords'] ?? null),
            'canonical_url' => $this->nullableText($data['canonical_url'] ?? null),
            'og_title' => $this->nullableText($data['og_title'] ?? null),
            'og_description' => $this->nullableText($data['og_description'] ?? null),
            'og_image' => $this->nullableText($data['og_image'] ?? null),
        ]);

        $article->save();

        $article->tags()->sync(\collect($data['tag_ids'] ?? [])->map(fn (mixed $id): int => (int) $id)->filter()->all());
        $this->syncRelations($article, $data['relations'] ?? []);

        return $article->fresh(['author.profile', 'category', 'tags', 'relatedEntities']);
    }

    private function mapStudioListItem(NewsArticle $article): array
    {
        return [
            'id' => (int) $article->id,
            'title' => (string) $article->title,
            'slug' => (string) $article->slug,
            'type' => (string) ($article->type ?? NewsArticle::TYPE_ANNOUNCEMENT),
            'type_label' => (string) $article->type_label,
            'editorial_status' => (string) ($article->editorial_status ?? NewsArticle::EDITORIAL_STATUS_DRAFT),
            'published_at' => \optional($article->published_at)?->toIso8601String(),
            'cover_url' => $article->cover_url,
            'author_name' => (string) ($article->author?->name ?? 'Skinbase'),
            'category_name' => (string) ($article->category?->name ?? ''),
            'is_featured' => (bool) $article->is_featured,
            'is_pinned' => (bool) ($article->is_pinned ?? false),
            'views' => (int) $article->views,
            'edit_url' => route('studio.news.edit', ['article' => $article->id]),
            'preview_url' => route('studio.news.preview', ['article' => $article->id]),
            'public_url' => route('news.show', ['slug' => $article->slug]),
        ];
    }

    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, NewsArticle $article, array $data): string
    {
        $requested = trim(Str::slug((string) ($data['slug'] ?? '')));

        if ($requested !== '' && $requested !== (string) $article->slug) {
            return NewsArticle::generateUniqueSlug($requested, $article->exists ? (int) $article->id : null);
        }

        if ($article->exists && trim((string) $article->slug) !== '') {
            return (string) $article->slug;
        }

        return NewsArticle::generateUniqueSlug($title, $article->exists ? (int) $article->id : null);
    }

    private function normalizeEditorialStatus(string $status): string
    {
        return in_array($status, array_column($this->editorialStatusOptions(), 'value'), true)
            ? $status
            : NewsArticle::EDITORIAL_STATUS_DRAFT;
    }

    private function normalizePublishedAt(string $editorialStatus, mixed $value): ?Carbon
    {
        if ($editorialStatus === NewsArticle::EDITORIAL_STATUS_PUBLISHED) {
            return $value ? Carbon::parse((string) $value) : \now();
        }

        if ($editorialStatus === NewsArticle::EDITORIAL_STATUS_SCHEDULED) {
            return $value ? Carbon::parse((string) $value) : \now()->addHour();
        }

        if ($value instanceof Carbon) {
            return $value;
        }

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

    private function legacyStatusFor(string $editorialStatus): string
    {
        return match ($editorialStatus) {
            NewsArticle::EDITORIAL_STATUS_PUBLISHED => 'published',
            NewsArticle::EDITORIAL_STATUS_SCHEDULED => 'scheduled',
            default => 'draft',
        };
    }

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

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

    private function searchGroups(string $query, ?User $viewer): array
    {
        return Group::query()
            ->with('owner')
            ->where('visibility', Group::VISIBILITY_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 searchArtworks(string $query): array
    {
        return Artwork::query()
            ->with(['user.profile'])
            ->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 {
                    $nested->where('title', 'like', '%' . $query . '%')
                        ->orWhere('slug', 'like', '%' . $query . '%')
                        ->orWhere('description', 'like', '%' . $query . '%');
                });
            })
            ->orderByDesc('views')
            ->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', '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 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 searchProjects(string $query, ?User $viewer): array
    {
        return GroupProject::query()
            ->with('group')
            ->when($query !== '', fn (Builder $builder): Builder => $builder->where('title', 'like', '%' . $query . '%'))
            ->orderByDesc('updated_at')
            ->limit(8)
            ->get()
            ->map(fn (GroupProject $project): ?array => $this->resolveProjectPreview((int) $project->id, $viewer, ''))
            ->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 searchUsers(string $query): array
    {
        return User::query()
            ->with('profile')
            ->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 . '%');
                });
            })
            ->orderBy('username')
            ->limit(8)
            ->get()
            ->map(fn (User $user): array => $this->mapUserLookupResult($user))
            ->values()
            ->all();
    }

    private function resolveEntityPreview(string $type, int $entityId, ?User $viewer = null, string $contextLabel = ''): ?array
    {
        return match ($type) {
            self::RELATION_GROUP => $this->resolveGroupPreview($entityId, $viewer, $contextLabel),
            self::RELATION_ARTWORK => $this->resolveArtworkPreview($entityId, $contextLabel),
            self::RELATION_COLLECTION => $this->resolveCollectionPreview($entityId, $viewer, $contextLabel),
            self::RELATION_RELEASE => $this->resolveReleasePreview($entityId, $viewer, $contextLabel),
            self::RELATION_PROJECT => $this->resolveProjectPreview($entityId, $viewer, $contextLabel),
            self::RELATION_CHALLENGE => $this->resolveChallengePreview($entityId, $viewer, $contextLabel),
            self::RELATION_EVENT => $this->resolveEventPreview($entityId, $viewer, $contextLabel),
            self::RELATION_USER => $this->resolveUserPreview($entityId, $contextLabel),
            default => null,
        };
    }

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

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

        return [
            'id' => (int) $group->id,
            'entity_type' => self::RELATION_GROUP,
            'entity_label' => self::RELATION_LABELS[self::RELATION_GROUP],
            'title' => (string) $group->name,
            'subtitle' => '@' . $group->slug,
            'description' => Str::limit((string) ($group->headline ?: $group->bio ?: ''), 120),
            'url' => $group->publicUrl(),
            'image' => $group->bannerUrl(),
            'avatar' => $group->avatarUrl(),
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Related Group',
            'meta' => array_values(array_filter([
                (int) $group->artworks_count > 0 ? number_format((int) $group->artworks_count) . ' artworks' : null,
                (int) $group->followers_count > 0 ? number_format((int) $group->followers_count) . ' followers' : null,
            ])),
        ];
    }

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

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

        return [
            'id' => (int) $artwork->id,
            'entity_type' => self::RELATION_ARTWORK,
            'entity_label' => self::RELATION_LABELS[self::RELATION_ARTWORK],
            'title' => (string) ($artwork->title ?: 'Untitled artwork'),
            'subtitle' => $artwork->user?->username ? '@' . $artwork->user->username : null,
            'description' => Str::limit(trim(strip_tags((string) ($artwork->description ?? ''))), 120),
            'url' => route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug ?: Str::slug((string) $artwork->title)]),
            'image' => $artwork->thumbUrl('lg') ?? $artwork->thumbUrl('md'),
            'avatar' => null,
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Mentioned artwork',
            'meta' => array_values(array_filter([
                (int) $artwork->views > 0 ? number_format((int) $artwork->views) . ' views' : null,
                $artwork->categories()->first()?->name,
            ])),
        ];
    }

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

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

        return [
            'id' => (int) $collection->id,
            'entity_type' => self::RELATION_COLLECTION,
            'entity_label' => self::RELATION_LABELS[self::RELATION_COLLECTION],
            'title' => (string) $collection->title,
            'subtitle' => '@' . $collection->user->username,
            'description' => Str::limit((string) ($collection->summary ?: $collection->description ?: ''), 120),
            'url' => route('profile.collections.show', ['username' => $collection->user->username, 'slug' => $collection->slug]),
            'image' => $collection->coverArtwork?->thumbUrl('lg') ?? $collection->coverArtwork?->thumbUrl('md'),
            'avatar' => null,
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Featured collection',
            'meta' => array_values(array_filter([
                (int) $collection->artworks_count > 0 ? number_format((int) $collection->artworks_count) . ' items' : null,
                (int) $collection->followers_count > 0 ? number_format((int) $collection->followers_count) . ' followers' : null,
            ])),
        ];
    }

    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' => self::RELATION_RELEASE,
            'entity_label' => self::RELATION_LABELS[self::RELATION_RELEASE],
            'title' => (string) $release->title,
            'subtitle' => (string) $release->group->name,
            'description' => Str::limit((string) ($release->summary ?: $release->description ?: ''), 120),
            'url' => route('groups.releases.show', ['group' => $release->group, 'release' => $release]),
            'image' => $release->coverUrl(),
            'avatar' => $release->group->avatarUrl(),
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Featured release',
            'meta' => array_values(array_filter([
                $release->published_at?->format('d M Y'),
                Str::headline((string) $release->status),
            ])),
        ];
    }

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

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

        return [
            'id' => (int) $project->id,
            'entity_type' => self::RELATION_PROJECT,
            'entity_label' => self::RELATION_LABELS[self::RELATION_PROJECT],
            'title' => (string) $project->title,
            'subtitle' => (string) $project->group->name,
            'description' => Str::limit((string) ($project->summary ?: $project->description ?: ''), 120),
            'url' => route('groups.projects.show', ['group' => $project->group, 'project' => $project]),
            'image' => $project->coverUrl(),
            'avatar' => $project->group->avatarUrl(),
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Related project',
            'meta' => array_values(array_filter([
                Str::headline((string) $project->status),
                $project->target_date?->format('d M Y'),
            ])),
        ];
    }

    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' => self::RELATION_CHALLENGE,
            'entity_label' => self::RELATION_LABELS[self::RELATION_CHALLENGE],
            'title' => (string) $challenge->title,
            'subtitle' => (string) $challenge->group->name,
            'description' => Str::limit((string) ($challenge->summary ?: $challenge->description ?: ''), 120),
            'url' => route('groups.challenges.show', ['group' => $challenge->group, 'challenge' => $challenge]),
            'image' => $challenge->coverUrl(),
            'avatar' => $challenge->group->avatarUrl(),
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Join this 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' => self::RELATION_EVENT,
            'entity_label' => self::RELATION_LABELS[self::RELATION_EVENT],
            'title' => (string) $event->title,
            'subtitle' => (string) $event->group->name,
            'description' => Str::limit((string) ($event->summary ?: $event->description ?: ''), 120),
            'url' => route('groups.events.show', ['group' => $event->group, 'event' => $event]),
            'image' => $event->coverUrl(),
            'avatar' => $event->group->avatarUrl(),
            'context_label' => $contextLabel !== '' ? $contextLabel : 'Upcoming event',
            'meta' => array_values(array_filter([
                $event->start_at?->format('d M Y H:i'),
                Str::headline((string) $event->event_type),
            ])),
        ];
    }

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

        if (! $user || trim((string) $user->username) === '') {
            return null;
        }

        return $this->mapUserLookupResult($user, $contextLabel !== '' ? $contextLabel : 'Meet the creator');
    }

    private function mapUserLookupResult(User $user, string $contextLabel = 'Profile'): array
    {
        return [
            'id' => (int) $user->id,
            'entity_type' => self::RELATION_USER,
            'entity_label' => self::RELATION_LABELS[self::RELATION_USER],
            'title' => (string) ($user->name ?: $user->username),
            'subtitle' => $user->username ? '@' . $user->username : null,
            'description' => Str::limit(trim((string) ($user->profile?->bio ?? '')), 120),
            'url' => $user->username ? route('profile.show', ['username' => $user->username]) : null,
            'image' => null,
            'avatar' => AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash ?? null, 96),
            'context_label' => $contextLabel,
            'meta' => [],
        ];
    }
}