<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Artwork;
use App\Models\Collection;
use Carbon\CarbonInterface;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Str;

class CollectionAiCurationService
{
    public function __construct(
        private readonly SmartCollectionService $smartCollections,
        private readonly CollectionCampaignService $campaigns,
        private readonly CollectionRecommendationService $recommendations,
    ) {
    }

    public function suggestTitle(Collection $collection, array $draft = []): array
    {
        $context = $this->buildContext($collection, $draft);
        $theme = $context['primary_theme'] ?: $context['top_category'] ?: $context['event_label'] ?: 'Curated Highlights';

        $primary = match ($context['type']) {
            Collection::TYPE_EDITORIAL => 'Staff Picks: ' . $theme,
            Collection::TYPE_COMMUNITY => ($context['allow_submissions'] ? 'Community Picks: ' : '') . $theme,
            default => $theme,
        };

        $alternatives = array_values(array_unique(array_filter([
            $primary,
            $theme . ' Showcase',
            $context['event_label'] ? $context['event_label'] . ': ' . $theme : null,
            $context['type'] === Collection::TYPE_EDITORIAL ? $theme . ' Editorial' : $theme . ' Collection',
        ])));

        return [
            'title' => $alternatives[0] ?? $primary,
            'alternatives' => array_slice($alternatives, 1, 3),
            'rationale' => sprintf(
                'Built from the strongest recurring theme%s across %d artworks.',
                $context['primary_theme'] ? ' (' . $context['primary_theme'] . ')' : '',
                $context['artworks_count']
            ),
            'source' => 'heuristic-ai',
        ];
    }

    public function suggestSummary(Collection $collection, array $draft = []): array
    {
        $context = $this->buildContext($collection, $draft);
        $typeLabel = match ($context['type']) {
            Collection::TYPE_EDITORIAL => 'editorial',
            Collection::TYPE_COMMUNITY => 'community',
            default => 'curated',
        };

        $themePart = $context['theme_sentence'] !== ''
            ? ' focused on ' . $context['theme_sentence']
            : '';

        $creatorPart = $context['creator_count'] > 1
            ? sprintf(' featuring work from %d creators', $context['creator_count'])
            : ' featuring a tightly selected set of pieces';

        $summary = sprintf(
            'A %s collection%s with %d artworks%s.',
            $typeLabel,
            $themePart,
            $context['artworks_count'],
            $creatorPart
        );

        $seo = sprintf(
            '%s on Skinbase: %d curated artworks%s.',
            $this->draftString($collection, $draft, 'title') ?: $collection->title,
            $context['artworks_count'],
            $context['theme_sentence'] !== '' ? ' exploring ' . $context['theme_sentence'] : ''
        );

        return [
            'summary' => Str::limit($summary, 220, ''),
            'seo_description' => Str::limit($seo, 155, ''),
            'rationale' => 'Summarised from the collection type, artwork count, creator mix, and recurring artwork themes.',
            'source' => 'heuristic-ai',
        ];
    }

    public function suggestCover(Collection $collection, array $draft = []): array
    {
        $artworks = $this->candidateArtworks($collection, $draft, 24);

        /** @var Artwork|null $winner */
        $winner = $artworks
            ->sortByDesc(fn (Artwork $artwork) => $this->coverScore($artwork))
            ->first();

        if (! $winner) {
            return [
                'artwork' => null,
                'rationale' => 'Add or match a few artworks first so the assistant has something to rank.',
                'source' => 'heuristic-ai',
            ];
        }

        $stats = $winner->stats;

        return [
            'artwork' => [
                'id' => (int) $winner->id,
                'title' => (string) $winner->title,
                'thumb' => $winner->thumbUrl('md'),
                'url' => route('art.show', [
                    'id' => $winner->id,
                    'slug' => Str::slug((string) ($winner->slug ?: $winner->title)) ?: (string) $winner->id,
                ]),
            ],
            'rationale' => sprintf(
                'Ranked highest for cover impact based on engagement, recency, and display-friendly proportions (%dx%d, %d views, %d likes).',
                (int) ($winner->width ?? 0),
                (int) ($winner->height ?? 0),
                (int) ($stats?->views ?? $winner->view_count ?? 0),
                (int) ($stats?->favorites ?? $winner->favourite_count ?? 0),
            ),
            'source' => 'heuristic-ai',
        ];
    }

    public function suggestGrouping(Collection $collection, array $draft = []): array
    {
        $artworks = $this->candidateArtworks($collection, $draft, 36);
        $themeBuckets = [];

        foreach ($artworks as $artwork) {
            $tag = $artwork->tags
                ->sortByDesc(fn ($item) => $item->pivot?->source === 'ai' ? 1 : 0)
                ->first();

            $label = $tag?->name
                ?: ($artwork->categories->first()?->name)
                ?: ($artwork->stats?->views ? 'Popular highlights' : 'Curated picks');

            if (! isset($themeBuckets[$label])) {
                $themeBuckets[$label] = [
                    'label' => $label,
                    'artwork_ids' => [],
                ];
            }

            if (count($themeBuckets[$label]['artwork_ids']) < 5) {
                $themeBuckets[$label]['artwork_ids'][] = (int) $artwork->id;
            }
        }

        $groups = collect($themeBuckets)
            ->map(fn (array $bucket) => [
                'label' => $bucket['label'],
                'artwork_ids' => $bucket['artwork_ids'],
                'count' => count($bucket['artwork_ids']),
            ])
            ->sortByDesc('count')
            ->take(4)
            ->values()
            ->all();

        return [
            'groups' => $groups,
            'rationale' => $groups !== []
                ? 'Grouped by the strongest recurring artwork themes so the collection can be split into cleaner sections.'
                : 'No strong theme groups were found yet.',
            'source' => 'heuristic-ai',
        ];
    }

    public function suggestRelatedArtworks(Collection $collection, array $draft = []): array
    {
        $seedArtworks = $this->candidateArtworks($collection, $draft, 24);
        $tagSlugs = $seedArtworks
            ->flatMap(fn (Artwork $artwork) => $artwork->tags->pluck('slug'))
            ->filter()
            ->unique()
            ->values();
        $categoryIds = $seedArtworks
            ->flatMap(fn (Artwork $artwork) => $artwork->categories->pluck('id'))
            ->filter()
            ->unique()
            ->values();
        $attachedIds = $collection->artworks()->pluck('artworks.id')->map(static fn ($id) => (int) $id)->all();

        $candidates = Artwork::query()
            ->with(['tags', 'categories'])
            ->where('user_id', $collection->user_id)
            ->whereNotIn('id', $attachedIds)
            ->where(function ($query) use ($tagSlugs, $categoryIds): void {
                if ($tagSlugs->isNotEmpty()) {
                    $query->orWhereHas('tags', fn ($tagQuery) => $tagQuery->whereIn('slug', $tagSlugs->all()));
                }

                if ($categoryIds->isNotEmpty()) {
                    $query->orWhereHas('categories', fn ($categoryQuery) => $categoryQuery->whereIn('categories.id', $categoryIds->all()));
                }
            })
            ->latest('published_at')
            ->limit(18)
            ->get()
            ->map(function (Artwork $artwork) use ($tagSlugs, $categoryIds): array {
                $sharedTags = $artwork->tags->pluck('slug')->intersect($tagSlugs)->values();
                $sharedCategories = $artwork->categories->pluck('id')->intersect($categoryIds)->values();
                $score = ($sharedTags->count() * 3) + ($sharedCategories->count() * 2);

                return [
                    'id' => (int) $artwork->id,
                    'title' => (string) $artwork->title,
                    'thumb' => $artwork->thumbUrl('sm'),
                    'score' => $score,
                    'shared_tags' => $sharedTags->take(3)->values()->all(),
                    'shared_categories' => $sharedCategories->count(),
                ];
            })
            ->sortByDesc('score')
            ->take(6)
            ->values()
            ->all();

        return [
            'artworks' => $candidates,
            'rationale' => $candidates !== []
                ? 'Suggested from your unassigned artworks that overlap most with the collection’s current themes and categories.'
                : 'No closely related unassigned artworks were found in your gallery yet.',
            'source' => 'heuristic-ai',
        ];
    }

    public function suggestTags(Collection $collection, array $draft = []): array
    {
        $context = $this->buildContext($collection, $draft);
        $artworks = $this->candidateArtworks($collection, $draft, 24);
        $tags = collect([
            $context['primary_theme'],
            $context['top_category'],
            $context['event_label'] !== '' ? Str::slug($context['event_label']) : null,
            $collection->type === Collection::TYPE_EDITORIAL ? 'staff-picks' : null,
            $collection->type === Collection::TYPE_COMMUNITY ? 'community-curation' : null,
        ])
            ->filter()
            ->merge(
                $artworks->flatMap(fn (Artwork $artwork) => $artwork->tags->pluck('slug'))
                    ->filter()
                    ->countBy()
                    ->sortDesc()
                    ->keys()
                    ->take(4)
            )
            ->map(fn ($value) => Str::of((string) $value)->replace('-', ' ')->trim()->lower()->value())
            ->filter()
            ->unique()
            ->take(6)
            ->values()
            ->all();

        return [
            'tags' => $tags,
            'rationale' => 'Suggested from recurring artwork themes, categories, and collection type signals.',
            'source' => 'heuristic-ai',
        ];
    }

    public function suggestSeoDescription(Collection $collection, array $draft = []): array
    {
        $summary = $this->suggestSummary($collection, $draft);
        $title = $this->draftString($collection, $draft, 'title') ?: $collection->title;
        $label = match ($collection->type) {
            Collection::TYPE_EDITORIAL => 'Staff Pick',
            Collection::TYPE_COMMUNITY => 'Community Collection',
            default => 'Collection',
        };

        return [
            'description' => Str::limit(sprintf('%s: %s. %s', $label, $title, $summary['seo_description']), 155, ''),
            'rationale' => 'Optimised for social previews and search snippets using the title, type, and strongest collection theme.',
            'source' => 'heuristic-ai',
        ];
    }

    public function detectWeakMetadata(Collection $collection, array $draft = []): array
    {
        $context = $this->buildContext($collection, $draft);
        $issues = [];
        $title = $this->draftString($collection, $draft, 'title') ?: (string) $collection->title;
        $summary = $this->draftString($collection, $draft, 'summary');
        $description = $this->draftString($collection, $draft, 'description');
        $metadataScore = (float) ($collection->metadata_completeness_score ?? 0);
        $coverArtworkId = $draft['cover_artwork_id'] ?? $collection->cover_artwork_id;

        if ($title === '' || Str::length($title) < 12 || preg_match('/^(untitled|new collection|my collection)/i', $title) === 1) {
            $issues[] = $this->metadataIssue(
                'title',
                'high',
                'Title needs more specificity',
                'Use a more descriptive title that signals the theme, series, or purpose of the collection.'
            );
        }

        if ($summary === null || Str::length($summary) < 60) {
            $issues[] = $this->metadataIssue(
                'summary',
                'high',
                'Summary is missing or too short',
                'Add a concise summary that explains what ties these artworks together and why the collection matters.'
            );
        }

        if ($description === null || Str::length(strip_tags($description)) < 140) {
            $issues[] = $this->metadataIssue(
                'description',
                'medium',
                'Description lacks depth',
                'Expand the description with context, mood, creator intent, or campaign framing so the collection reads as deliberate curation.'
            );
        }

        if (! $coverArtworkId) {
            $issues[] = $this->metadataIssue(
                'cover',
                'medium',
                'No explicit cover artwork is set',
                'Choose a cover artwork so the collection has a stronger visual anchor across profile, saved-library, and programming surfaces.'
            );
        }

        if (($context['primary_theme'] ?? null) === null && ($context['top_category'] ?? null) === null) {
            $issues[] = $this->metadataIssue(
                'theme',
                'medium',
                'Theme signals are weak',
                'Add or retag a few representative artworks so the collection has a clearer theme for discovery and recommendations.'
            );
        }

        if ($metadataScore > 0 && $metadataScore < 65) {
            $issues[] = $this->metadataIssue(
                'metadata_score',
                'medium',
                'Metadata completeness is below the recommended threshold',
                sprintf('The current metadata completeness score is %.1f. Tightening title, summary, description, and cover selection should improve it.', $metadataScore)
            );
        }

        return [
            'status' => $issues === [] ? 'healthy' : 'needs_work',
            'issues' => $issues,
            'rationale' => $issues === []
                ? 'The collection metadata is strong enough for creator-facing surfaces and AI assistance did not find obvious weak spots.'
                : 'Detected from title specificity, metadata coverage, theme clarity, and cover readiness heuristics.',
            'source' => 'heuristic-ai',
        ];
    }

    public function suggestStaleRefresh(Collection $collection, array $draft = []): array
    {
        $context = $this->buildContext($collection, $draft);
        $referenceAt = $this->staleReferenceAt($collection);
        $daysSinceRefresh = $referenceAt?->diffInDays(now()) ?? null;
        $isStale = $daysSinceRefresh !== null && ($daysSinceRefresh >= 45 || (float) ($collection->freshness_score ?? 0) < 40);
        $actions = [];

        if ($isStale) {
            $actions[] = [
                'key' => 'refresh_summary',
                'label' => 'Refresh the summary and description',
                'reason' => 'A quick metadata pass helps older collections feel current again when they resurface in search or recommendations.',
            ];

            if (! $collection->cover_artwork_id) {
                $actions[] = [
                    'key' => 'set_cover',
                    'label' => 'Choose a stronger cover artwork',
                    'reason' => 'A defined cover is the fastest way to make a stale collection feel newly curated.',
                ];
            }

            if (($context['artworks_count'] ?? 0) < 6) {
                $actions[] = [
                    'key' => 'add_recent_artworks',
                    'label' => 'Add newer artworks to the set',
                    'reason' => 'The collection is still relatively small, so a few fresh pieces would meaningfully improve recency and depth.',
                ];
            } else {
                $actions[] = [
                    'key' => 'resequence_highlights',
                    'label' => 'Resequence the leading artworks',
                    'reason' => 'Reordering the strongest pieces can refresh the collection without changing its core theme.',
                ];
            }

            if (($context['primary_theme'] ?? null) === null) {
                $actions[] = [
                    'key' => 'tighten_theme',
                    'label' => 'Clarify the collection theme',
                    'reason' => 'The current artwork set does not emit a strong recurring theme, so a tighter selection would improve discovery quality.',
                ];
            }
        }

        return [
            'stale' => $isStale,
            'days_since_refresh' => $daysSinceRefresh,
            'last_active_at' => $referenceAt?->toIso8601String(),
            'actions' => $actions,
            'rationale' => $isStale
                ? 'Detected from freshness, recent activity, and current collection depth.'
                : 'The collection has recent enough activity that a refresh is not urgent right now.',
            'source' => 'heuristic-ai',
        ];
    }

    public function suggestCampaignFit(Collection $collection, array $draft = []): array
    {
        $context = $this->buildContext($collection, $draft);
        $campaignSummary = $this->campaigns->campaignSummary($collection);
        $seasonKey = $this->draftString($collection, $draft, 'season_key') ?: (string) ($collection->season_key ?? '');
        $eventKey = $this->draftString($collection, $draft, 'event_key') ?: (string) ($collection->event_key ?? '');
        $eventLabel = $this->draftString($collection, $draft, 'event_label') ?: (string) ($collection->event_label ?? '');

        $candidates = Collection::query()
            ->public()
            ->where('id', '!=', $collection->id)
            ->whereNotNull('campaign_key')
            ->with(['user:id,username,name'])
            ->orderByDesc('ranking_score')
            ->orderByDesc('saves_count')
            ->orderByDesc('updated_at')
            ->limit(36)
            ->get()
            ->map(function (Collection $candidate) use ($collection, $context, $seasonKey, $eventKey): array {
                $score = 0;
                $reasons = [];

                if ((string) $candidate->type === (string) $collection->type) {
                    $score += 4;
                    $reasons[] = 'Matches the same collection type.';
                }

                if ($seasonKey !== '' && $candidate->season_key === $seasonKey) {
                    $score += 5;
                    $reasons[] = 'Shares the same seasonal window.';
                }

                if ($eventKey !== '' && $candidate->event_key === $eventKey) {
                    $score += 6;
                    $reasons[] = 'Shares the same event context.';
                }

                if ((int) $candidate->user_id === (int) $collection->user_id) {
                    $score += 2;
                    $reasons[] = 'Comes from the same curator account.';
                }

                if ($context['top_category'] !== null && filled($candidate->theme_token) && Str::contains(mb_strtolower((string) $candidate->theme_token), mb_strtolower((string) $context['top_category']))) {
                    $score += 2;
                    $reasons[] = 'Theme token overlaps with the collection category.';
                }

                if ((float) ($candidate->ranking_score ?? 0) >= 70) {
                    $score += 1;
                    $reasons[] = 'Campaign is represented by a strong-performing collection.';
                }

                return [
                    'score' => $score,
                    'candidate' => $candidate,
                    'reasons' => $reasons,
                ];
            })
            ->filter(fn (array $item): bool => $item['score'] > 0)
            ->sortByDesc(fn (array $item): string => sprintf('%08d-%s', $item['score'], optional($item['candidate']->updated_at)?->timestamp ?? 0))
            ->groupBy(fn (array $item): string => (string) $item['candidate']->campaign_key)
            ->map(function ($items, string $campaignKey): array {
                $top = collect($items)->sortByDesc('score')->first();
                $candidate = $top['candidate'];

                return [
                    'campaign_key' => $campaignKey,
                    'campaign_label' => $candidate->campaign_label ?: Str::headline(str_replace(['_', '-'], ' ', $campaignKey)),
                    'score' => (int) $top['score'],
                    'reasons' => array_values(array_unique($top['reasons'])),
                    'sample_collection' => [
                        'id' => (int) $candidate->id,
                        'title' => (string) $candidate->title,
                        'slug' => (string) $candidate->slug,
                    ],
                ];
            })
            ->sortByDesc('score')
            ->take(4)
            ->values()
            ->all();

        if ($candidates === [] && ($seasonKey !== '' || $eventKey !== '' || $eventLabel !== '')) {
            $fallbackKey = $collection->campaign_key ?: ($eventKey !== '' ? Str::slug($eventKey) : ($seasonKey !== '' ? $seasonKey . '-editorial' : Str::slug($eventLabel)));
            if ($fallbackKey !== '') {
                $candidates[] = [
                    'campaign_key' => $fallbackKey,
                    'campaign_label' => $collection->campaign_label ?: ($eventLabel !== '' ? $eventLabel : Str::headline(str_replace(['_', '-'], ' ', $fallbackKey))),
                    'score' => 72,
                    'reasons' => array_values(array_filter([
                        $seasonKey !== '' ? 'Season metadata is already present and can anchor a campaign.' : null,
                        $eventLabel !== '' ? 'Event labeling is already specific enough for campaign framing.' : null,
                        'Surface suggestions indicate this collection is promotable once reviewed.',
                    ])),
                    'sample_collection' => null,
                ];
            }
        }

        return [
            'current_context' => [
                'campaign_key' => $collection->campaign_key,
                'campaign_label' => $collection->campaign_label,
                'event_key' => $eventKey !== '' ? $eventKey : null,
                'event_label' => $eventLabel !== '' ? $eventLabel : null,
                'season_key' => $seasonKey !== '' ? $seasonKey : null,
            ],
            'eligibility' => $campaignSummary['eligibility'] ?? [],
            'recommended_surfaces' => $campaignSummary['recommended_surfaces'] ?? [],
            'fits' => $candidates,
            'rationale' => $candidates !== []
                ? 'Suggested from existing campaign-aware collections with overlapping type, season, event, and performance context.'
                : 'No strong campaign fits were detected yet; tighten seasonal or event metadata first.',
            'source' => 'heuristic-ai',
        ];
    }

    public function suggestRelatedCollectionsToLink(Collection $collection, array $draft = []): array
    {
        $alreadyLinkedIds = $collection->manualRelatedCollections()->pluck('collections.id')->map(static fn ($id): int => (int) $id)->all();
        $candidates = $this->recommendations->relatedPublicCollections($collection, 8)
            ->reject(fn (Collection $candidate): bool => in_array((int) $candidate->id, $alreadyLinkedIds, true))
            ->take(6)
            ->values();

        $suggestions = $candidates->map(function (Collection $candidate) use ($collection): array {
            $reasons = [];

            if ((string) $candidate->type === (string) $collection->type) {
                $reasons[] = 'Matches the same collection type.';
            }

            if ((int) $candidate->user_id === (int) $collection->user_id) {
                $reasons[] = 'Owned by the same curator.';
            }

            if (filled($collection->campaign_key) && $candidate->campaign_key === $collection->campaign_key) {
                $reasons[] = 'Shares the same campaign context.';
            }

            if (filled($collection->event_key) && $candidate->event_key === $collection->event_key) {
                $reasons[] = 'Shares the same event context.';
            }

            if (filled($collection->season_key) && $candidate->season_key === $collection->season_key) {
                $reasons[] = 'Shares the same seasonal framing.';
            }

            if ($reasons === []) {
                $reasons[] = 'Ranks as a closely related public collection based on the platform recommendation model.';
            }

            return [
                'id' => (int) $candidate->id,
                'title' => (string) $candidate->title,
                'slug' => (string) $candidate->slug,
                'owner' => $candidate->displayOwnerName(),
                'reasons' => $reasons,
                'link_type' => (int) $candidate->user_id === (int) $collection->user_id ? 'same_curator' : 'discovery_adjacent',
            ];
        })->all();

        return [
            'linked_collection_ids' => $alreadyLinkedIds,
            'suggestions' => $suggestions,
            'rationale' => $suggestions !== []
                ? 'Suggested from the related-public-collections model after excluding collections already linked manually.'
                : 'No additional related public collections were strong enough to suggest right now.',
            'source' => 'heuristic-ai',
        ];
    }

    public function explainSmartRules(Collection $collection, array $draft = []): array
    {
        $rules = is_array($draft['smart_rules_json'] ?? null)
            ? $draft['smart_rules_json']
            : $collection->smart_rules_json;

        if (! is_array($rules)) {
            return [
                'explanation' => 'This collection is currently curated manually, so there are no smart rules to explain.',
                'source' => 'heuristic-ai',
            ];
        }

        $summary = $this->smartCollections->smartSummary($rules);
        $preview = $this->smartCollections->preview($collection->user, $rules, true, 6);

        return [
            'explanation' => sprintf('%s The current rule set matches %d artworks in the preview.', $summary, $preview->total()),
            'rationale' => 'Translated from the active smart rule JSON into a human-readable curator summary.',
            'source' => 'heuristic-ai',
        ];
    }

    public function suggestSplitThemes(Collection $collection, array $draft = []): array
    {
        $grouping = $this->suggestGrouping($collection, $draft);
        $groups = collect($grouping['groups'] ?? [])->take(2)->values();

        if ($groups->count() < 2) {
            return [
                'splits' => [],
                'rationale' => 'There are not enough distinct recurring themes yet to justify splitting this collection into two focused sets.',
                'source' => 'heuristic-ai',
            ];
        }

        return [
            'splits' => $groups->map(function (array $group, int $index) use ($collection): array {
                $label = (string) ($group['label'] ?? ('Theme ' . ($index + 1)));

                return [
                    'label' => $label,
                    'title' => trim(sprintf('%s: %s', $collection->title, $label)),
                    'artwork_ids' => array_values(array_map('intval', $group['artwork_ids'] ?? [])),
                    'count' => (int) ($group['count'] ?? 0),
                ];
            })->all(),
            'rationale' => 'Suggested from the two strongest artwork theme clusters so you can split the collection into clearer destination pages.',
            'source' => 'heuristic-ai',
        ];
    }

    public function suggestMergeIdea(Collection $collection, array $draft = []): array
    {
        $related = $this->suggestRelatedArtworks($collection, $draft);
        $context = $this->buildContext($collection, $draft);
        $artworks = collect($related['artworks'] ?? [])->take(3)->values();

        if ($artworks->isEmpty()) {
            return [
                'idea' => null,
                'rationale' => 'No closely related artworks were found that would strengthen a merged follow-up collection yet.',
                'source' => 'heuristic-ai',
            ];
        }

        $theme = $context['primary_theme'] ?: $context['top_category'] ?: 'Extended Showcase';

        return [
            'idea' => [
                'title' => sprintf('%s Extended', Str::title((string) $theme)),
                'summary' => sprintf('A follow-up collection idea that combines the current theme with %d closely related artworks from the same gallery.', $artworks->count()),
                'related_artwork_ids' => $artworks->pluck('id')->map(static fn ($id) => (int) $id)->all(),
            ],
            'rationale' => 'Suggested as a merge or spin-out concept using the current theme and the strongest related artworks not already attached.',
            'source' => 'heuristic-ai',
        ];
    }

    private function buildContext(Collection $collection, array $draft = []): array
    {
        $artworks = $this->candidateArtworks($collection, $draft, 36);
        $themes = $this->topThemes($artworks);
        $categories = $artworks
            ->map(fn (Artwork $artwork) => $artwork->categories->first()?->name)
            ->filter()
            ->countBy()
            ->sortDesc();

        return [
            'type' => $this->draftString($collection, $draft, 'type') ?: $collection->type,
            'allow_submissions' => array_key_exists('allow_submissions', $draft)
                ? (bool) $draft['allow_submissions']
                : (bool) $collection->allow_submissions,
            'artworks_count' => max(1, $artworks->count()),
            'creator_count' => max(1, $artworks->pluck('user_id')->filter()->unique()->count()),
            'primary_theme' => $themes->keys()->first(),
            'theme_sentence' => $themes->keys()->take(2)->implode(' and '),
            'top_category' => $categories->keys()->first(),
            'event_label' => $this->draftString($collection, $draft, 'event_label') ?: (string) ($collection->event_label ?? ''),
        ];
    }

    /**
     * @return SupportCollection<int, Artwork>
     */
    private function candidateArtworks(Collection $collection, array $draft = [], int $limit = 24): SupportCollection
    {
        $mode = $this->draftString($collection, $draft, 'mode') ?: $collection->mode;
        $smartRules = is_array($draft['smart_rules_json'] ?? null)
            ? $draft['smart_rules_json']
            : $collection->smart_rules_json;

        if ($mode === Collection::MODE_SMART && is_array($smartRules)) {
            return $this->smartCollections
                ->preview($collection->user, $smartRules, true, max(6, $limit))
                ->getCollection()
                ->loadMissing(['tags', 'categories.contentType', 'stats']);
        }

        return $collection->artworks()
            ->with(['tags', 'categories.contentType', 'stats'])
            ->whereNull('artworks.deleted_at')
            ->select('artworks.*')
            ->limit(max(6, $limit))
            ->get();
    }

    /**
     * @return SupportCollection<string, float>
     */
    private function topThemes(SupportCollection $artworks): SupportCollection
    {
        return $artworks
            ->flatMap(function (Artwork $artwork): array {
                return $artwork->tags->map(function ($tag): array {
                    return [
                        'label' => (string) $tag->name,
                        'weight' => $tag->pivot?->source === 'ai' ? 1.25 : 1.0,
                    ];
                })->all();
            })
            ->groupBy('label')
            ->map(fn (SupportCollection $items) => (float) $items->sum('weight'))
            ->sortDesc()
            ->take(6);
    }

    private function coverScore(Artwork $artwork): float
    {
        $stats = $artwork->stats;
        $views = (int) ($stats?->views ?? $artwork->view_count ?? 0);
        $likes = (int) ($stats?->favorites ?? $artwork->favourite_count ?? 0);
        $downloads = (int) ($stats?->downloads ?? 0);
        $width = max(1, (int) ($artwork->width ?? 1));
        $height = max(1, (int) ($artwork->height ?? 1));
        $ratio = $width / $height;
        $ratioBonus = $ratio >= 1.1 && $ratio <= 1.8 ? 40 : 0;
        $freshness = $artwork->published_at ? max(0, 30 - min(30, $artwork->published_at->diffInDays(now()))) : 0;

        return ($likes * 8) + ($downloads * 5) + ($views * 0.05) + $ratioBonus + $freshness;
    }

    private function draftString(Collection $collection, array $draft, string $key): ?string
    {
        if (! array_key_exists($key, $draft)) {
            return $collection->{$key} !== null ? (string) $collection->{$key} : null;
        }

        $value = $draft[$key];
        if ($value === null) {
            return null;
        }

        return trim((string) $value);
    }

    /**
     * @return array{key:string,severity:string,label:string,detail:string}
     */
    private function metadataIssue(string $key, string $severity, string $label, string $detail): array
    {
        return [
            'key' => $key,
            'severity' => $severity,
            'label' => $label,
            'detail' => $detail,
        ];
    }

    private function staleReferenceAt(Collection $collection): ?CarbonInterface
    {
        return $collection->last_activity_at
            ?? $collection->updated_at
            ?? $collection->published_at;
    }
}
