<?php

declare(strict_types=1);

namespace App\Services;

use App\Events\Collections\CollectionArtworkAttached;
use App\Events\Collections\CollectionArtworkRemoved;
use App\Events\Collections\CollectionCreated;
use App\Events\Collections\CollectionDeleted;
use App\Events\Collections\CollectionFeatured;
use App\Events\Collections\CollectionUnfeatured;
use App\Events\Collections\CollectionShared;
use App\Events\Collections\CollectionUpdated;
use App\Events\Collections\SmartCollectionRulesUpdated;
use App\Models\Artwork;
use App\Models\Collection;
use App\Models\Group;
use App\Models\User;
use App\Services\Maturity\ArtworkMaturityService;
use App\Support\AvatarUrl;
use App\Services\ThumbnailPresenter;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use Illuminate\Validation\ValidationException;

class CollectionService
{
    public function __construct(
        private readonly SmartCollectionService $smartCollections,
        private readonly CollectionCollaborationService $collaborators,
        private readonly GroupMembershipService $groupMembers,
        private readonly ArtworkMaturityService $maturity,
    ) {
    }

    public function getLayoutModuleDefinitions(): array
    {
        return $this->normalizeLayoutModules(null, Collection::TYPE_PERSONAL, true, false);
    }

    public function makeUniqueSlugForUser(User $user, string $source, ?int $ignoreCollectionId = null, ?Group $group = null): string
    {
        $base = Str::slug(Str::limit($source, 140, ''));
        $base = $base !== '' ? $base : 'collection';
        $slug = $base;
        $suffix = 2;

        while ($this->slugExistsForUser($user, $slug, $ignoreCollectionId, $group)) {
            $slug = Str::limit($base, 132, '');
            $slug = rtrim($slug, '-');
            $slug .= '-' . $suffix;
            $suffix++;
        }

        return $slug;
    }

    public function createCollection(User $user, array $attributes): Collection
    {
        return DB::transaction(function () use ($user, $attributes): Collection {
            $mode = (string) ($attributes['mode'] ?? Collection::MODE_MANUAL);
            $type = (string) ($attributes['type'] ?? Collection::TYPE_PERSONAL);
            $ownership = $this->resolveOwnershipContext($user, $attributes, null, $type);
            $smartRules = $mode === Collection::MODE_SMART
                ? $this->smartCollections->sanitizeRules($attributes['smart_rules_json'] ?? null)
                : null;
            $allowComments = array_key_exists('allow_comments', $attributes) ? (bool) $attributes['allow_comments'] : true;
            $allowSubmissions = (bool) ($attributes['allow_submissions'] ?? false);

            $collection = new Collection();
            $collection->user()->associate($ownership['owner_user']);
            $collection->group()->associate($ownership['group']);
            $collection->managed_by_user_id = $ownership['managed_by_user_id'];
            $collection->title = (string) $attributes['title'];
            $collection->slug = $this->makeUniqueSlugForUser($ownership['owner_user'], (string) ($attributes['slug'] ?? $attributes['title']), null, $ownership['group']);
            $collection->lifecycle_state = (string) ($attributes['lifecycle_state'] ?? Collection::LIFECYCLE_DRAFT);
            $collection->type = $type;
            $collection->editorial_owner_mode = $ownership['editorial_owner_mode'];
            $collection->editorial_owner_user_id = $ownership['editorial_owner_user_id'];
            $collection->editorial_owner_label = $ownership['editorial_owner_label'];
            $collection->description = $attributes['description'] ?? null;
            $collection->subtitle = $attributes['subtitle'] ?? null;
            $collection->summary = $attributes['summary'] ?? null;
            $collection->collaboration_mode = (string) ($attributes['collaboration_mode'] ?? Collection::COLLABORATION_CLOSED);
            $collection->allow_submissions = $allowSubmissions;
            $collection->allow_comments = $allowComments;
            $collection->allow_saves = array_key_exists('allow_saves', $attributes) ? (bool) $attributes['allow_saves'] : true;
            $collection->moderation_status = Collection::MODERATION_ACTIVE;
            $collection->visibility = (string) ($attributes['visibility'] ?? Collection::VISIBILITY_PUBLIC);
            $collection->mode = $mode;
            $collection->sort_mode = (string) ($attributes['sort_mode'] ?? ($mode === Collection::MODE_SMART ? Collection::SORT_NEWEST : Collection::SORT_MANUAL));
            $collection->event_key = $attributes['event_key'] ?? null;
            $collection->event_label = $attributes['event_label'] ?? null;
            $collection->season_key = $attributes['season_key'] ?? null;
            $collection->banner_text = $attributes['banner_text'] ?? null;
            $collection->badge_label = $attributes['badge_label'] ?? null;
            $collection->spotlight_style = $attributes['spotlight_style'] ?? Collection::SPOTLIGHT_STYLE_DEFAULT;
            $collection->quality_score = null;
            $collection->ranking_score = null;
            $collection->analytics_enabled = array_key_exists('analytics_enabled', $attributes) ? (bool) $attributes['analytics_enabled'] : true;
            $collection->presentation_style = (string) ($attributes['presentation_style'] ?? Collection::PRESENTATION_STANDARD);
            $collection->emphasis_mode = (string) ($attributes['emphasis_mode'] ?? Collection::EMPHASIS_BALANCED);
            $collection->theme_token = $attributes['theme_token'] ?? null;
            $collection->series_key = $attributes['series_key'] ?? null;
            $collection->series_title = $attributes['series_title'] ?? null;
            $collection->series_description = $attributes['series_description'] ?? null;
            $collection->series_order = $attributes['series_order'] ?? null;
            $collection->campaign_key = $attributes['campaign_key'] ?? null;
            $collection->campaign_label = $attributes['campaign_label'] ?? null;
            $collection->commercial_eligibility = (bool) ($attributes['commercial_eligibility'] ?? false);
            $collection->promotion_tier = $attributes['promotion_tier'] ?? null;
            $collection->sponsorship_label = $attributes['sponsorship_label'] ?? null;
            $collection->partner_label = $attributes['partner_label'] ?? null;
            $collection->monetization_ready_status = $attributes['monetization_ready_status'] ?? null;
            $collection->brand_safe_status = $attributes['brand_safe_status'] ?? null;
            $collection->editorial_notes = $attributes['editorial_notes'] ?? null;
            $collection->staff_commercial_notes = $attributes['staff_commercial_notes'] ?? null;
            $collection->archived_at = $attributes['archived_at'] ?? null;
            $collection->expired_at = $attributes['expired_at'] ?? null;
            $collection->history_count = 0;
            $collection->cover_artwork_id = null;
            $collection->artworks_count = 0;
            $collection->comments_count = 0;
            $collection->views_count = 0;
            $collection->likes_count = 0;
            $collection->followers_count = 0;
            $collection->shares_count = 0;
            $collection->saves_count = 0;
            $collection->collaborators_count = 1;
            $collection->smart_rules_json = $smartRules;
            $collection->layout_modules_json = $this->normalizeLayoutModules($attributes['layout_modules_json'] ?? null, $type, $allowComments, $allowSubmissions, false);
            $collection->profile_order = $this->nextProfileOrder($ownership['owner_user'], $ownership['group']);
            $collection->last_activity_at = now();
            $collection->published_at = $this->resolvePublishedAt($attributes);
            $collection->unpublished_at = $this->resolveUnpublishedAt($attributes);
            $collection->save();

            $this->collaborators->ensureOwnerMembership($collection);
            $this->collaborators->ensureManagerMembership($collection, $user);

            if ($collection->isSmart()) {
                $this->syncSmartCollectionState($collection);
            }

            $fresh = $this->postMutationSync($collection->fresh());
            app(CollectionHistoryService::class)->record($fresh, $user, 'created', 'Collection created.', null, [
                'title' => $fresh->title,
                'type' => $fresh->type,
                'lifecycle_state' => $fresh->lifecycle_state,
            ]);

            event(new CollectionCreated($fresh));

            return $fresh;
        });
    }

    public function updateCollection(Collection $collection, array $attributes, ?User $actor = null): Collection
    {
        return DB::transaction(function () use ($collection, $attributes, $actor): Collection {
            $originalFeatured = (bool) $collection->is_featured;
            $originalSmartRules = $collection->smart_rules_json;
            $slugSource = (string) ($attributes['slug'] ?? $attributes['title'] ?? $collection->slug);
            $mode = (string) ($attributes['mode'] ?? $collection->mode ?? Collection::MODE_MANUAL);
            $type = (string) ($attributes['type'] ?? $collection->type);
            $manager = $actor ?? $collection->user;
            $ownership = $this->resolveOwnershipContext($manager, $attributes, $collection, $type);
            $smartRules = $mode === Collection::MODE_SMART
                ? $this->smartCollections->sanitizeRules($attributes['smart_rules_json'] ?? $collection->smart_rules_json)
                : null;

            $coverArtworkId = $mode === Collection::MODE_SMART
                ? null
                : ($attributes['cover_artwork_id'] ?? $collection->cover_artwork_id);

            $visibility = (string) ($attributes['visibility'] ?? $collection->visibility);
            $isFeatureable = $visibility === Collection::VISIBILITY_PUBLIC;
            $allowSubmissions = array_key_exists('allow_submissions', $attributes) ? (bool) $attributes['allow_submissions'] : $collection->allow_submissions;
            $allowComments = array_key_exists('allow_comments', $attributes) ? (bool) $attributes['allow_comments'] : $collection->allow_comments;

            $collection->user()->associate($ownership['owner_user']);
            $collection->group()->associate($ownership['group']);

            $collection->fill([
                'title' => (string) ($attributes['title'] ?? $collection->title),
                'slug' => $this->makeUniqueSlugForUser($ownership['owner_user'], $slugSource, (int) $collection->id, $ownership['group']),
                'lifecycle_state' => (string) ($attributes['lifecycle_state'] ?? $collection->lifecycle_state),
                'type' => $type,
                'managed_by_user_id' => $ownership['managed_by_user_id'],
                'editorial_owner_mode' => $ownership['editorial_owner_mode'],
                'editorial_owner_user_id' => $ownership['editorial_owner_user_id'],
                'editorial_owner_label' => $ownership['editorial_owner_label'],
                'description' => $attributes['description'] ?? null,
                'subtitle' => $attributes['subtitle'] ?? null,
                'summary' => $attributes['summary'] ?? null,
                'collaboration_mode' => (string) ($attributes['collaboration_mode'] ?? $collection->collaboration_mode),
                'allow_submissions' => $allowSubmissions,
                'allow_comments' => $allowComments,
                'allow_saves' => array_key_exists('allow_saves', $attributes) ? (bool) $attributes['allow_saves'] : $collection->allow_saves,
                'visibility' => $visibility,
                'mode' => $mode,
                'sort_mode' => (string) ($attributes['sort_mode'] ?? $collection->sort_mode),
                'cover_artwork_id' => $coverArtworkId,
                'smart_rules_json' => $smartRules,
                'layout_modules_json' => $this->normalizeLayoutModules($attributes['layout_modules_json'] ?? $collection->layout_modules_json, $type, $allowComments, $allowSubmissions, false),
                'event_key' => $attributes['event_key'] ?? $collection->event_key,
                'event_label' => $attributes['event_label'] ?? $collection->event_label,
                'season_key' => $attributes['season_key'] ?? $collection->season_key,
                'banner_text' => $attributes['banner_text'] ?? $collection->banner_text,
                'badge_label' => $attributes['badge_label'] ?? $collection->badge_label,
                'spotlight_style' => $attributes['spotlight_style'] ?? $collection->spotlight_style,
                'analytics_enabled' => array_key_exists('analytics_enabled', $attributes) ? (bool) $attributes['analytics_enabled'] : $collection->analytics_enabled,
                'presentation_style' => $attributes['presentation_style'] ?? $collection->presentation_style,
                'emphasis_mode' => $attributes['emphasis_mode'] ?? $collection->emphasis_mode,
                'theme_token' => $attributes['theme_token'] ?? $collection->theme_token,
                'series_key' => $attributes['series_key'] ?? $collection->series_key,
                'series_title' => $attributes['series_title'] ?? $collection->series_title,
                'series_description' => $attributes['series_description'] ?? $collection->series_description,
                'series_order' => $attributes['series_order'] ?? $collection->series_order,
                'campaign_key' => $attributes['campaign_key'] ?? $collection->campaign_key,
                'campaign_label' => $attributes['campaign_label'] ?? $collection->campaign_label,
                'commercial_eligibility' => array_key_exists('commercial_eligibility', $attributes) ? (bool) $attributes['commercial_eligibility'] : $collection->commercial_eligibility,
                'promotion_tier' => $attributes['promotion_tier'] ?? $collection->promotion_tier,
                'sponsorship_label' => $attributes['sponsorship_label'] ?? $collection->sponsorship_label,
                'partner_label' => $attributes['partner_label'] ?? $collection->partner_label,
                'monetization_ready_status' => $attributes['monetization_ready_status'] ?? $collection->monetization_ready_status,
                'brand_safe_status' => $attributes['brand_safe_status'] ?? $collection->brand_safe_status,
                'editorial_notes' => $attributes['editorial_notes'] ?? $collection->editorial_notes,
                'staff_commercial_notes' => $attributes['staff_commercial_notes'] ?? $collection->staff_commercial_notes,
                'published_at' => $this->resolvePublishedAt($attributes, $collection->published_at),
                'unpublished_at' => $this->resolveUnpublishedAt($attributes, $collection->unpublished_at),
                'archived_at' => $attributes['archived_at'] ?? $collection->archived_at,
                'expired_at' => $attributes['expired_at'] ?? $collection->expired_at,
                'last_activity_at' => now(),
            ]);

            if (! $isFeatureable) {
                $collection->is_featured = false;
                $collection->featured_at = null;
            }

            $collection->save();

            $this->collaborators->ensureOwnerMembership($collection);
            $this->collaborators->ensureManagerMembership($collection, $manager);

            if ($collection->isSmart()) {
                $this->syncSmartCollectionState($collection);
            } else {
                $collection->syncArtworksCount();
            }

            $fresh = $this->postMutationSync($collection->fresh());

            if ($originalFeatured && ! $fresh->is_featured) {
                event(new CollectionUnfeatured($fresh));
            }

            if ($fresh->isSmart() && $originalSmartRules !== $fresh->smart_rules_json) {
                event(new SmartCollectionRulesUpdated($fresh));
            }

            app(CollectionHistoryService::class)->record($fresh, $manager, 'updated', 'Collection settings updated.', [
                'title' => $collection->getOriginal('title'),
                'visibility' => $collection->getOriginal('visibility'),
                'lifecycle_state' => $collection->getOriginal('lifecycle_state'),
            ], [
                'title' => $fresh->title,
                'visibility' => $fresh->visibility,
                'lifecycle_state' => $fresh->lifecycle_state,
            ]);

            event(new CollectionUpdated($fresh));

            return $fresh;
        });
    }

    public function deleteCollection(Collection $collection): void
    {
        DB::transaction(function () use ($collection): void {
            DB::table('collection_artwork')
                ->where('collection_id', $collection->id)
                ->delete();

            $collection->forceFill([
                'cover_artwork_id' => null,
                'artworks_count' => 0,
            ])->save();

            app(CollectionHistoryService::class)->record($collection->fresh(), null, 'deleted', 'Collection deleted.');
            event(new CollectionDeleted($collection->fresh()));
            $collection->delete();
        });
    }

    /**
     * @param array<int, int|string> $artworkIds
     */
    public function attachArtworks(Collection $collection, User $owner, array $artworkIds): Collection
    {
        if ($collection->isSmart()) {
            throw ValidationException::withMessages([
                'collection' => 'Smart collections update from rules and do not accept manual attachments.',
            ]);
        }

        $normalizedIds = collect($artworkIds)
            ->map(static fn ($id) => (int) $id)
            ->filter(static fn (int $id) => $id > 0)
            ->unique()
            ->values();

        if ($normalizedIds->isEmpty()) {
            throw ValidationException::withMessages([
                'artwork_ids' => 'Select at least one artwork to add.',
            ]);
        }

        $validIds = Artwork::query()
            ->whereIn('user_id', $this->contributorIds($collection))
            ->whereIn('id', $normalizedIds)
            ->whereNull('deleted_at')
            ->pluck('id');

        if ($validIds->count() !== $normalizedIds->count()) {
            throw ValidationException::withMessages([
                'artwork_ids' => 'You can only add artworks from approved collection contributors.',
            ]);
        }

        DB::transaction(function () use ($collection, $owner, $validIds): void {
            $this->attachArtworkIds($collection, $validIds->all());
            app(CollectionHistoryService::class)->record($collection->fresh(), $owner, 'artworks_attached', 'Artworks added to collection.', null, [
                'artwork_ids' => $validIds->values()->all(),
            ]);

            event(new CollectionArtworkAttached($collection->fresh(), $validIds->values()->all()));
        });

        return $collection->fresh();
    }

    /**
     * @param array<int, int> $artworkIds
     */
    public function attachArtworkIds(Collection $collection, array $artworkIds): void
    {
        $newIds = collect($artworkIds)
            ->map(static fn ($id) => (int) $id)
            ->filter(static fn (int $id) => $id > 0)
            ->unique()
            ->values();

        if ($newIds->isEmpty()) {
            return;
        }

        $existingIds = DB::table('collection_artwork')
            ->where('collection_id', $collection->id)
            ->whereIn('artwork_id', $newIds)
            ->pluck('artwork_id');

        $newIds = $newIds->diff($existingIds)->values();
        if ($newIds->isEmpty()) {
            return;
        }

        $nextOrder = (int) (DB::table('collection_artwork')
            ->where('collection_id', $collection->id)
            ->max('order_num') ?? -1) + 1;

        $rows = $newIds->map(function (int $artworkId) use (&$nextOrder, $collection) {
            return [
                'collection_id' => $collection->id,
                'artwork_id' => $artworkId,
                'order_num' => $nextOrder++,
                'created_at' => now(),
                'updated_at' => now(),
            ];
        })->all();

        DB::table('collection_artwork')->insert($rows);

        $this->syncCollectionArtworkState($collection);
    }

    public function removeArtwork(Collection $collection, Artwork $artwork): Collection
    {
        if ($collection->isSmart()) {
            throw ValidationException::withMessages([
                'collection' => 'Smart collections update from rules and do not support manual removals.',
            ]);
        }

        DB::transaction(function () use ($collection, $artwork): void {
            DB::table('collection_artwork')
                ->where('collection_id', $collection->id)
                ->where('artwork_id', $artwork->id)
                ->delete();

            if ((int) $collection->cover_artwork_id === (int) $artwork->id) {
                $collection->cover_artwork_id = null;
                $collection->save();
            }

            $this->normalizeArtworkOrder($collection);
            $this->syncCollectionArtworkState($collection);
            app(CollectionHistoryService::class)->record($collection->fresh(), null, 'artwork_removed', 'Artwork removed from collection.', [
                'artwork_id' => (int) $artwork->id,
            ]);

            event(new CollectionArtworkRemoved($collection->fresh(), (int) $artwork->id));
        });

        return $collection->fresh();
    }

    /**
     * @param array<int, int|string> $orderedArtworkIds
     */
    public function reorderArtworks(Collection $collection, array $orderedArtworkIds): Collection
    {
        if ($collection->isSmart()) {
            throw ValidationException::withMessages([
                'collection' => 'Smart collections use rule-based ordering and cannot be manually reordered.',
            ]);
        }

        $normalizedIds = collect($orderedArtworkIds)
            ->map(static fn ($id) => (int) $id)
            ->filter(static fn (int $id) => $id > 0)
            ->values();

        $currentIds = DB::table('collection_artwork')
            ->where('collection_id', $collection->id)
            ->orderBy('order_num')
            ->pluck('artwork_id')
            ->map(static fn ($id) => (int) $id)
            ->values();

        if ($normalizedIds->count() !== $currentIds->count() || $normalizedIds->diff($currentIds)->isNotEmpty() || $currentIds->diff($normalizedIds)->isNotEmpty()) {
            throw ValidationException::withMessages([
                'ordered_artwork_ids' => 'The submitted artwork order is invalid for this collection.',
            ]);
        }

        DB::transaction(function () use ($collection, $normalizedIds): void {
            foreach ($normalizedIds as $index => $artworkId) {
                DB::table('collection_artwork')
                    ->where('collection_id', $collection->id)
                    ->where('artwork_id', $artworkId)
                    ->update([
                        'order_num' => $index,
                        'updated_at' => now(),
                    ]);
            }

            $this->syncCollectionArtworkState($collection);
            app(CollectionHistoryService::class)->record($collection->fresh(), null, 'artworks_reordered', 'Artwork order updated.', null, [
                'ordered_artwork_ids' => $normalizedIds->all(),
            ]);
        });

        return $collection->fresh();
    }

    public function getProfileCollections(User $profileOwner, ?User $viewer, int $limit = 12): EloquentCollection
    {
        $ownerView = $viewer && (int) $viewer->id === (int) $profileOwner->id;

        $query = Collection::query()
            ->ownedBy((int) $profileOwner->id)
            ->with([
                'user:id,username,name',
                'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
            ])
            ->orderByDesc('is_featured')
            ->orderByRaw('CASE WHEN profile_order IS NULL THEN 1 ELSE 0 END')
            ->orderBy('profile_order')
            ->orderByDesc('featured_at')
            ->orderByDesc('updated_at')
            ->limit($limit);

        if (! $ownerView) {
            $query->visibleOnProfile();
        }

        return $query->get();
    }

    public function getCollectionDetailArtworks(Collection $collection, bool $ownerView, int $perPage = 24, ?User $viewer = null): LengthAwarePaginator
    {
        if ($collection->isSmart()) {
            return $this->smartCollections->resolveArtworks($collection, $ownerView, $perPage);
        }

        $viewer ??= $ownerView ? null : request()->user();

        $query = $collection->artworks()
            ->with([
                'user:id,name,username',
                'stats:artwork_id,views,downloads,favorites',
                'categories' => function ($query) {
                    $query->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order')
                        ->with(['contentType:id,slug,name']);
                },
            ])
            ->whereNull('artworks.deleted_at')
            ->select('artworks.*');

        if (! $ownerView) {
            $query->where('artworks.is_public', true)
                ->where('artworks.is_approved', true)
                ->whereNotNull('artworks.published_at')
                ->where('artworks.published_at', '<=', now());

            if ($this->viewerShouldHideMature($viewer)) {
                $query->whereRaw('COALESCE(artworks.is_mature, 0) = 0')
                    ->whereRaw("COALESCE(artworks.maturity_status, 'clear') != ?", [ArtworkMaturityService::STATUS_SUSPECTED]);
            }
        }

        $query = match ($collection->sort_mode) {
            Collection::SORT_NEWEST => $query->orderByDesc('artworks.published_at'),
            Collection::SORT_OLDEST => $query->orderBy('artworks.published_at'),
            Collection::SORT_POPULAR => $query->orderByDesc('artworks.view_count')->orderByPivot('order_num'),
            default => $query->orderByPivot('order_num'),
        };

        return $query->paginate($perPage)->withQueryString();
    }

    public function getAvailableArtworkOptions(Collection $collection, User $owner, ?string $search = null, int $limit = 36): array
    {
        if ($collection->isSmart()) {
            return [];
        }

        $attachedIds = DB::table('collection_artwork')
            ->where('collection_id', $collection->id)
            ->pluck('artwork_id');

        $query = Artwork::query()
            ->with([
                'stats:artwork_id,views,downloads,favorites',
                'categories' => function ($query) {
                    $query->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order')
                        ->with(['contentType:id,slug,name']);
                },
            ])
            ->when(
                (int) ($collection->group_id ?? 0) > 0,
                fn ($builder) => $builder->where('group_id', (int) $collection->group_id),
                fn ($builder) => $builder->whereIn('user_id', $this->contributorIds($collection))
            )
            ->whereNull('deleted_at')
            ->whereNotIn('id', $attachedIds)
            ->orderByDesc('published_at')
            ->orderByDesc('id')
            ->limit($limit);

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

        return $query->get()->map(fn (Artwork $artwork) => $this->mapArtworkPayload($artwork))->all();
    }

    public function getCollectionOptionsForArtwork(User $owner, Artwork $artwork): array
    {
        $isPersonalArtwork = (int) ($artwork->group_id ?? 0) < 1;
        $group = $artwork->group;

        if ($isPersonalArtwork && (int) $artwork->user_id !== (int) $owner->id) {
            throw ValidationException::withMessages([
                'artwork_id' => 'You can only manage collections for your own artworks.',
            ]);
        }

        if (! $isPersonalArtwork && (! $group || ! $group->canManageCollections($owner))) {
            throw ValidationException::withMessages([
                'artwork_id' => 'You can only manage collections for groups you can edit.',
            ]);
        }

        $collections = Collection::query()
            ->with('group')
            ->when(
                ! $isPersonalArtwork,
                fn ($query) => $query->where('group_id', (int) $artwork->group_id),
                fn ($query) => $query->ownedBy((int) $owner->id)
            )
            ->where('mode', Collection::MODE_MANUAL)
            ->orderByDesc('updated_at')
            ->get(['id', 'user_id', 'group_id', 'title', 'slug', 'visibility', 'mode', 'artworks_count', 'updated_at']);

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

        $attachedCollectionIds = DB::table('collection_artwork')
            ->where('artwork_id', $artwork->id)
            ->whereIn('collection_id', $collections->pluck('id')->all())
            ->pluck('collection_id')
            ->map(static fn ($id) => (int) $id)
            ->all();

        return $collections->map(function (Collection $collection) use ($attachedCollectionIds, $owner) {
            $alreadyAttached = in_array((int) $collection->id, $attachedCollectionIds, true);

            $publicUrl = route('profile.collections.show', [
                'username' => strtolower((string) $owner->username),
                'slug' => $collection->slug,
            ]);

            return [
                'id' => (int) $collection->id,
                'title' => (string) $collection->title,
                'type' => (string) $collection->type,
                'slug' => (string) $collection->slug,
                'visibility' => (string) $collection->visibility,
                'mode' => (string) $collection->mode,
                'artworks_count' => (int) $collection->artworks_count,
                'already_attached' => $alreadyAttached,
                'attach_url' => route('settings.collections.artworks.attach', ['collection' => $collection->id]),
                'manage_url' => route('settings.collections.show', ['collection' => $collection->id]),
                'public_url' => $publicUrl,
            ];
        })->all();
    }

    public function getSubmissionArtworkOptions(User $user, int $limit = 24): array
    {
        return Artwork::query()
            ->with([
                'stats:artwork_id,views,downloads,favorites',
                'categories' => function ($query) {
                    $query->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order')
                        ->with(['contentType:id,slug,name']);
                },
            ])
            ->where('user_id', $user->id)
            ->whereNull('deleted_at')
            ->orderByDesc('published_at')
            ->orderByDesc('id')
            ->limit(max(1, min($limit, 36)))
            ->get()
            ->map(fn (Artwork $artwork) => $this->mapArtworkPayload($artwork))
            ->all();
    }

    public function getSavedCollectionsForUser(User $user, int $limit = 48): EloquentCollection
    {
        return Collection::query()
            ->public()
            ->select(
                'collections.*',
                'collection_saves.created_at as saved_at',
                'collection_saves.last_viewed_at as saved_last_viewed_at',
                'collection_saves.save_context as saved_context',
                'collection_saves.save_context_meta_json as saved_context_meta_json'
            )
            ->join('collection_saves', 'collection_saves.collection_id', '=', 'collections.id')
            ->where('collection_saves.user_id', $user->id)
            ->with([
                'user:id,username,name',
                'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
            ])
            ->orderByDesc('collection_saves.created_at')
            ->limit(max(1, min($limit, 60)))
            ->get();
    }

    public function featureCollection(Collection $collection): Collection
    {
        if (! $collection->isFeatureablePublicly()) {
            throw ValidationException::withMessages([
                'collection' => 'Only public collections can be featured.',
            ]);
        }

        return DB::transaction(function () use ($collection): Collection {
            $fresh = $collection->fresh();

            if (! $fresh->is_featured) {
                $featuredCount = Collection::query()
                    ->ownedBy((int) $fresh->user_id)
                    ->where('is_featured', true)
                    ->count();

                if ($featuredCount >= $this->featuredLimit()) {
                    throw ValidationException::withMessages([
                        'collection' => sprintf('You can feature up to %d collections.', $this->featuredLimit()),
                    ]);
                }
            }

            $fresh->forceFill([
                'is_featured' => true,
                'featured_at' => now(),
                'last_activity_at' => now(),
            ])->save();

            $fresh = $this->postMutationSync($fresh->fresh());
            app(CollectionHistoryService::class)->record($fresh, null, 'featured', 'Collection marked as featured.');

            event(new CollectionFeatured($fresh));

            return $fresh;
        });
    }

    public function unfeatureCollection(Collection $collection): Collection
    {
        $collection->forceFill([
            'is_featured' => false,
            'featured_at' => null,
        ])->save();

        $collection = $this->postMutationSync($collection->fresh());
        app(CollectionHistoryService::class)->record($collection, null, 'unfeatured', 'Collection removed from featured state.');

        event(new CollectionUnfeatured($collection));

        return $collection;
    }

    public function syncCollectionPublicState(Collection $collection, array $attributes): Collection
    {
        $updates = [];

        foreach (['allow_comments', 'allow_submissions', 'allow_saves', 'moderation_status'] as $key) {
            if (array_key_exists($key, $attributes)) {
                $updates[$key] = $attributes[$key];
            }
        }

        if ($updates === []) {
            return $collection->fresh();
        }

        if (($updates['moderation_status'] ?? $collection->moderation_status) !== Collection::MODERATION_ACTIVE) {
            $updates['is_featured'] = false;
            $updates['featured_at'] = null;
        }

        $updates['updated_at'] = now();
        $updates['last_activity_at'] = now();

        $collection->forceFill($updates)->save();

        $collection = $this->postMutationSync($collection->fresh());
        app(CollectionHistoryService::class)->record($collection, null, 'moderation_updated', 'Collection public state updated.', null, $updates);

        return $collection;
    }

    /**
     * @param array<int, int|string> $collectionIds
     */
    public function reorderProfileCollections(User $owner, array $collectionIds): void
    {
        $normalizedIds = collect($collectionIds)
            ->map(static fn ($id) => (int) $id)
            ->filter(static fn (int $id) => $id > 0)
            ->values();

        $ownedIds = Collection::query()
            ->ownedBy((int) $owner->id)
            ->orderBy('id')
            ->pluck('id')
            ->map(static fn ($id) => (int) $id)
            ->values();

        if ($normalizedIds->count() !== $ownedIds->count() || $normalizedIds->diff($ownedIds)->isNotEmpty() || $ownedIds->diff($normalizedIds)->isNotEmpty()) {
            throw ValidationException::withMessages([
                'collection_ids' => 'The submitted profile order is invalid.',
            ]);
        }

        DB::transaction(function () use ($normalizedIds): void {
            foreach ($normalizedIds as $index => $collectionId) {
                DB::table('collections')
                    ->where('id', $collectionId)
                    ->update([
                        'profile_order' => $index,
                        'updated_at' => now(),
                    ]);
            }
        });
    }

    public function previewSmartCollection(User $owner, array $rules, int $perPage = 12): array
    {
        $sanitized = $this->smartCollections->sanitizeRules($rules);
        $paginator = $this->smartCollections->preview($owner, $sanitized, true, $perPage);

        return [
            'rules' => $sanitized,
            'summary' => $this->smartCollections->smartSummary($sanitized),
            'count' => $paginator->total(),
            'artworks' => $this->mapArtworkPaginator($paginator),
        ];
    }

    public function getSmartRuleOptions(User $owner): array
    {
        return $this->smartCollections->ruleOptionsForOwner($owner);
    }

    public function recordView(Collection $collection): Collection
    {
        DB::table('collections')
            ->where('id', $collection->id)
            ->update([
                'views_count' => DB::raw('views_count + 1'),
                'updated_at' => now(),
            ]);

        $fresh = $collection->fresh();
        if ($fresh->supportsAnalytics()) {
            app(CollectionAnalyticsService::class)->snapshot($fresh);
        }

        return $fresh;
    }

    public function recordShare(Collection $collection, ?User $actor = null): Collection
    {
        DB::table('collections')
            ->where('id', $collection->id)
            ->update([
                'shares_count' => DB::raw('shares_count + 1'),
                'last_activity_at' => now(),
                'updated_at' => now(),
            ]);

        $fresh = $this->postMutationSync($collection->fresh());
        event(new CollectionShared($fresh, $actor?->id));

        return $fresh;
    }

    public function mapCollectionCardPayloads(iterable $collections, bool $ownerView = false, ?User $viewer = null): array
    {
        $viewer ??= $ownerView ? null : request()->user();
        $collectionList = $collections instanceof EloquentCollection
            ? $collections
            : new EloquentCollection(is_array($collections) ? $collections : iterator_to_array($collections));

        $collectionIds = $collectionList->pluck('id')->map(static fn ($id) => (int) $id)->all();
        $hideMatureCovers = ! $ownerView && $this->viewerShouldHideMature($viewer);

        $firstArtworkMap = $this->firstArtworkMapForCollections(
            $collectionIds,
            ! $ownerView,
            $hideMatureCovers,
        );

        $savedCollectionIds = $viewer && ! $ownerView && $collectionIds !== []
            ? DB::table('collection_saves')
                ->where('user_id', $viewer->id)
                ->whereIn('collection_id', $collectionIds)
                ->pluck('collection_id')
                ->map(static fn ($id) => (int) $id)
                ->all()
            : [];

        return $collectionList->map(function (Collection $collection) use ($ownerView, $viewer, $firstArtworkMap, $savedCollectionIds) {
            $resolvedCover = $collection->isSmart()
                ? $this->smartCollections->firstArtwork($collection, $ownerView)
                : $collection->resolvedCoverArtwork(! $ownerView, ! $ownerView && $this->viewerShouldHideMature($viewer));
            $fallbackCover = $firstArtworkMap->get((int) $collection->id);
            $cover = $this->eligibleCoverArtwork($resolvedCover, ! $ownerView, ! $ownerView && $this->viewerShouldHideMature($viewer))
                ? $resolvedCover
                : $fallbackCover;
            $summary = $collection->summary ?? $collection->description;
            $isSaved = in_array((int) $collection->id, $savedCollectionIds, true);
            $canSave = ! $ownerView && $viewer && $collection->canBeSavedBy($viewer);

            return [
                'id' => $collection->id,
                'title' => $collection->title,
                'subtitle' => $collection->subtitle,
                'slug' => $collection->slug,
                'type' => $collection->type,
                'lifecycle_state' => $collection->lifecycle_state,
                'workflow_state' => $collection->workflow_state,
                'readiness_state' => $collection->readiness_state,
                'health_state' => $collection->health_state,
                'health_flags' => is_array($collection->health_flags_json) ? $collection->health_flags_json : [],
                'program_key' => $collection->program_key,
                'partner_key' => $collection->partner_key,
                'trust_tier' => $collection->trust_tier,
                'experiment_key' => $collection->experiment_key,
                'experiment_treatment' => $collection->experiment_treatment,
                'placement_variant' => $collection->placement_variant,
                'ranking_mode_variant' => $collection->ranking_mode_variant,
                'collection_pool_version' => $collection->collection_pool_version,
                'test_label' => $collection->test_label,
                'recommendation_tier' => $collection->recommendation_tier,
                'ranking_bucket' => $collection->ranking_bucket,
                'search_boost_tier' => $collection->search_boost_tier,
                'event_key' => $collection->event_key,
                'event_label' => $collection->event_label,
                'season_key' => $collection->season_key,
                'badge_label' => $collection->badge_label,
                'banner_text' => $collection->banner_text,
                'spotlight_style' => $collection->spotlight_style,
                'quality_score' => $collection->quality_score !== null ? (float) $collection->quality_score : null,
                'ranking_score' => $collection->ranking_score !== null ? (float) $collection->ranking_score : null,
                'metadata_completeness_score' => $collection->metadata_completeness_score !== null ? (float) $collection->metadata_completeness_score : null,
                'editorial_readiness_score' => $collection->editorial_readiness_score !== null ? (float) $collection->editorial_readiness_score : null,
                'freshness_score' => $collection->freshness_score !== null ? (float) $collection->freshness_score : null,
                'engagement_score' => $collection->engagement_score !== null ? (float) $collection->engagement_score : null,
                'health_score' => $collection->health_score !== null ? (float) $collection->health_score : null,
                'placement_eligibility' => (bool) $collection->placement_eligibility,
                'canonical_collection_id' => $collection->canonical_collection_id ? (int) $collection->canonical_collection_id : null,
                'duplicate_cluster_key' => $collection->duplicate_cluster_key,
                'analytics_enabled' => (bool) $collection->analytics_enabled,
                'presentation_style' => $collection->presentation_style,
                'emphasis_mode' => $collection->emphasis_mode,
                'theme_token' => $collection->theme_token,
                'series_key' => $collection->series_key,
                'series_title' => $collection->series_title,
                'series_description' => $collection->series_description,
                'series_order' => $collection->series_order,
                'campaign_key' => $collection->campaign_key,
                'campaign_label' => $collection->campaign_label,
                'commercial_eligibility' => (bool) $collection->commercial_eligibility,
                'promotion_tier' => $collection->promotion_tier,
                'sponsorship_label' => $collection->sponsorship_label,
                'sponsorship_state' => $collection->sponsorship_state,
                'partner_label' => $collection->partner_label,
                'ownership_domain' => $collection->ownership_domain,
                'commercial_review_state' => $collection->commercial_review_state,
                'legal_review_state' => $collection->legal_review_state,
                'editorial_notes' => $collection->editorial_notes,
                'staff_commercial_notes' => $collection->staff_commercial_notes,
                'description' => $collection->description,
                'summary' => $collection->summary,
                'description_excerpt' => Str::limit((string) ($summary ?? ''), 120),
                'owner' => $this->mapCollectionOwnerPayload($collection),
                'artworks_count' => (int) $collection->artworks_count,
                'comments_count' => (int) $collection->comments_count,
                'visibility' => $collection->visibility,
                'mode' => $collection->mode,
                'collaboration_mode' => $collection->collaboration_mode,
                'allow_submissions' => (bool) $collection->allow_submissions,
                'allow_comments' => (bool) $collection->allow_comments,
                'allow_saves' => (bool) $collection->allow_saves,
                'is_featured' => (bool) $collection->is_featured,
                'views_count' => (int) $collection->views_count,
                'likes_count' => (int) $collection->likes_count,
                'followers_count' => (int) $collection->followers_count,
                'shares_count' => (int) $collection->shares_count,
                'saves_count' => (int) $collection->saves_count,
                'collaborators_count' => (int) $collection->collaborators_count,
                'updated_at' => optional($collection->updated_at)?->toISOString(),
                'last_activity_at' => optional($collection->last_activity_at)?->toISOString(),
                'featured_at' => optional($collection->featured_at)?->toISOString(),
                'last_health_check_at' => optional($collection->last_health_check_at)?->toISOString(),
                'last_recommendation_refresh_at' => optional($collection->last_recommendation_refresh_at)?->toISOString(),
                'smart_summary' => $collection->isSmart() ? $this->smartCollections->smartSummary($collection->smart_rules_json) : null,
                'cover_image' => $cover ? $this->mapArtworkThumb($cover) : null,
                'cover_image_maturity' => ! $ownerView && $cover ? $this->maturity->presentation($cover, $viewer) : null,
                'cover_artwork_id' => $cover?->id,
                'saved' => $isSaved,
                'save_url' => $canSave ? route('collections.save', ['collection' => $collection->id]) : null,
                'unsave_url' => $isSaved && ! $ownerView && $viewer ? route('collections.unsave', ['collection' => $collection->id]) : null,
                'login_url' => ! $ownerView && ! $viewer ? route('login') : null,
                'url' => route('profile.collections.show', [
                    'username' => strtolower((string) $collection->user->username),
                    'slug' => $collection->slug,
                ]),
                'manage_url' => $ownerView ? route('settings.collections.show', ['collection' => $collection->id]) : null,
                'edit_url' => $ownerView ? route('settings.collections.edit', ['collection' => $collection->id]) : null,
                'delete_url' => $ownerView ? route('settings.collections.destroy', ['collection' => $collection->id]) : null,
                'feature_url' => $ownerView ? route('settings.collections.feature', ['collection' => $collection->id]) : null,
                'unfeature_url' => $ownerView ? route('settings.collections.unfeature', ['collection' => $collection->id]) : null,
            ];
        })->all();
    }

    public function mapCollectionDetailPayload(Collection $collection, bool $ownerView = false, ?User $viewer = null): array
    {
        $viewer ??= $ownerView ? null : request()->user();
        $hideMatureCovers = ! $ownerView && $this->viewerShouldHideMature($viewer);
        $cover = $collection->isSmart()
            ? $this->smartCollections->firstArtwork($collection, $ownerView)
            : $collection->resolvedCoverArtwork(! $ownerView, $hideMatureCovers);

        if (! $this->eligibleCoverArtwork($cover, ! $ownerView, $hideMatureCovers)) {
            $cover = $this->firstArtworkMapForCollections([(int) $collection->id], ! $ownerView, $hideMatureCovers)
                ->get((int) $collection->id);
        }

        return [
            'id' => $collection->id,
            'title' => $collection->title,
            'subtitle' => $collection->subtitle,
            'slug' => $collection->slug,
            'type' => $collection->type,
            'lifecycle_state' => $collection->lifecycle_state,
            'workflow_state' => $collection->workflow_state,
            'readiness_state' => $collection->readiness_state,
            'health_state' => $collection->health_state,
            'health_flags' => is_array($collection->health_flags_json) ? $collection->health_flags_json : [],
            'collaboration_mode' => $collection->collaboration_mode,
            'allow_submissions' => (bool) $collection->allow_submissions,
            'allow_comments' => (bool) $collection->allow_comments,
            'allow_saves' => (bool) $collection->allow_saves,
            'moderation_status' => $collection->moderation_status,
            'event_key' => $collection->event_key,
            'event_label' => $collection->event_label,
            'season_key' => $collection->season_key,
            'banner_text' => $collection->banner_text,
            'badge_label' => $collection->badge_label,
            'spotlight_style' => $collection->spotlight_style,
            'quality_score' => $collection->quality_score !== null ? (float) $collection->quality_score : null,
            'ranking_score' => $collection->ranking_score !== null ? (float) $collection->ranking_score : null,
            'metadata_completeness_score' => $collection->metadata_completeness_score !== null ? (float) $collection->metadata_completeness_score : null,
            'editorial_readiness_score' => $collection->editorial_readiness_score !== null ? (float) $collection->editorial_readiness_score : null,
            'freshness_score' => $collection->freshness_score !== null ? (float) $collection->freshness_score : null,
            'engagement_score' => $collection->engagement_score !== null ? (float) $collection->engagement_score : null,
            'health_score' => $collection->health_score !== null ? (float) $collection->health_score : null,
            'placement_eligibility' => (bool) $collection->placement_eligibility,
            'analytics_enabled' => (bool) $collection->analytics_enabled,
            'presentation_style' => $collection->presentation_style,
            'emphasis_mode' => $collection->emphasis_mode,
            'theme_token' => $collection->theme_token,
            'series_key' => $collection->series_key,
            'series_title' => $collection->series_title,
            'series_description' => $collection->series_description,
            'series_order' => $collection->series_order,
            'campaign_key' => $collection->campaign_key,
            'campaign_label' => $collection->campaign_label,
            'commercial_eligibility' => (bool) $collection->commercial_eligibility,
            'promotion_tier' => $collection->promotion_tier,
            'sponsorship_label' => $collection->sponsorship_label,
            'sponsorship_state' => $collection->sponsorship_state,
            'partner_label' => $collection->partner_label,
            'ownership_domain' => $collection->ownership_domain,
            'commercial_review_state' => $collection->commercial_review_state,
            'legal_review_state' => $collection->legal_review_state,
            'partner_key' => $collection->partner_key,
            'program_key' => $collection->program_key,
            'trust_tier' => $collection->trust_tier,
            'experiment_key' => $collection->experiment_key,
            'experiment_treatment' => $collection->experiment_treatment,
            'placement_variant' => $collection->placement_variant,
            'ranking_mode_variant' => $collection->ranking_mode_variant,
            'collection_pool_version' => $collection->collection_pool_version,
            'test_label' => $collection->test_label,
            'recommendation_tier' => $collection->recommendation_tier,
            'ranking_bucket' => $collection->ranking_bucket,
            'search_boost_tier' => $collection->search_boost_tier,
            'monetization_ready_status' => $collection->monetization_ready_status,
            'brand_safe_status' => $collection->brand_safe_status,
            'editorial_notes' => $collection->editorial_notes,
            'staff_commercial_notes' => $collection->staff_commercial_notes,
            'description' => $collection->description,
            'summary' => $collection->summary,
            'owner' => $this->mapCollectionOwnerPayload($collection),
            'visibility' => $collection->visibility,
            'mode' => $collection->mode,
            'sort_mode' => $collection->sort_mode,
            'canonical_collection_id' => $collection->canonical_collection_id ? (int) $collection->canonical_collection_id : null,
            'duplicate_cluster_key' => $collection->duplicate_cluster_key,
            'artworks_count' => (int) $collection->artworks_count,
            'comments_count' => (int) $collection->comments_count,
            'is_featured' => (bool) $collection->is_featured,
            'views_count' => (int) $collection->views_count,
            'likes_count' => (int) $collection->likes_count,
            'followers_count' => (int) $collection->followers_count,
            'shares_count' => (int) $collection->shares_count,
            'saves_count' => (int) $collection->saves_count,
            'collaborators_count' => (int) $collection->collaborators_count,
            'updated_at' => optional($collection->updated_at)?->toISOString(),
            'last_activity_at' => optional($collection->last_activity_at)?->toISOString(),
            'featured_at' => optional($collection->featured_at)?->toISOString(),
            'last_health_check_at' => optional($collection->last_health_check_at)?->toISOString(),
            'last_recommendation_refresh_at' => optional($collection->last_recommendation_refresh_at)?->toISOString(),
            'published_at' => optional($collection->published_at)?->toISOString(),
            'unpublished_at' => optional($collection->unpublished_at)?->toISOString(),
            'archived_at' => optional($collection->archived_at)?->toISOString(),
            'expired_at' => optional($collection->expired_at)?->toISOString(),
            'history_count' => (int) $collection->history_count,
            'cover_image' => $cover ? $this->mapArtworkThumb($cover) : null,
            'cover_image_maturity' => ! $ownerView && $cover ? $this->maturity->presentation($cover, $viewer) : null,
            'cover_artwork_id' => $cover?->id,
            'smart_rules_json' => $collection->smart_rules_json,
            'layout_modules' => $this->normalizeLayoutModules($collection->layout_modules_json, $collection->type, (bool) $collection->allow_comments, (bool) $collection->allow_submissions),
            'smart_summary' => $collection->isSmart() ? $this->smartCollections->smartSummary($collection->smart_rules_json) : null,
            'can_publicly_engage' => $collection->isPubliclyEngageable(),
            'public_url' => route('profile.collections.show', [
                'username' => strtolower((string) $collection->user->username),
                'slug' => $collection->slug,
            ]),
        ];
    }

    public function mapArtworkPaginator(LengthAwarePaginator $paginator): array
    {
        return [
            'data' => collect($paginator->items())
                ->map(fn (Artwork $artwork) => $this->mapArtworkPayload($artwork))
                ->values()
                ->all(),
            'meta' => [
                'current_page' => $paginator->currentPage(),
                'last_page' => $paginator->lastPage(),
                'per_page' => $paginator->perPage(),
                'total' => $paginator->total(),
            ],
            'links' => [
                'next' => $paginator->nextPageUrl(),
                'prev' => $paginator->previousPageUrl(),
            ],
        ];
    }

    public function mapAttachedArtworks(Collection $collection): array
    {
        if ($collection->isSmart()) {
            return [];
        }

        $artworks = $collection->artworks()
            ->with([
                'stats:artwork_id,views,downloads,favorites',
                'categories' => function ($query) {
                    $query->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order')
                        ->with(['contentType:id,slug,name']);
                },
            ])
            ->whereNull('artworks.deleted_at')
            ->select('artworks.*')
            ->get();

        return $artworks->map(fn (Artwork $artwork) => $this->mapArtworkPayload($artwork, [
            'pivot_order' => (int) ($artwork->pivot?->order_num ?? 0),
            'can_remove' => true,
            'remove_url' => route('settings.collections.artworks.remove', [
                'collection' => $collection->id,
                'artwork' => $artwork->id,
            ]),
        ]))->all();
    }

    private function syncCollectionArtworkState(Collection $collection): void
    {
        $collection->refresh();
        $collection->syncArtworksCount();

        if ($collection->cover_artwork_id !== null) {
            $coverStillAttached = DB::table('collection_artwork')
                ->where('collection_id', $collection->id)
                ->where('artwork_id', $collection->cover_artwork_id)
                ->exists();

            if (! $coverStillAttached) {
                $collection->forceFill(['cover_artwork_id' => null])->save();
            }
        }

        $collection->forceFill([
            'last_activity_at' => now(),
        ])->save();

        $this->postMutationSync($collection->fresh());
    }

    private function postMutationSync(Collection $collection): Collection
    {
        $fresh = app(CollectionLifecycleService::class)->syncState($collection->fresh());
        $fresh = app(CollectionQualityService::class)->sync($fresh);
        $fresh = app(CollectionHealthService::class)->refresh($fresh, null, 'post-mutation');
        $fresh = app(CollectionRankingService::class)->refresh($fresh);

        if ($fresh->supportsAnalytics()) {
            app(CollectionAnalyticsService::class)->snapshot($fresh);
        }

        return $fresh;
    }

    private function normalizeArtworkOrder(Collection $collection): void
    {
        $ids = DB::table('collection_artwork')
            ->where('collection_id', $collection->id)
            ->orderBy('order_num')
            ->orderBy('id')
            ->pluck('artwork_id');

        foreach ($ids as $index => $artworkId) {
            DB::table('collection_artwork')
                ->where('collection_id', $collection->id)
                ->where('artwork_id', $artworkId)
                ->update([
                    'order_num' => $index,
                    'updated_at' => now(),
                ]);
        }
    }

    /**
     * @param array<int, int> $collectionIds
     * @return SupportCollection<int, Artwork>
     */
    private function firstArtworkMapForCollections(array $collectionIds, bool $publicOnly, bool $hideMature = false): SupportCollection
    {
        if ($collectionIds === []) {
            return collect();
        }

        $rows = DB::table('collection_artwork as ca')
            ->join('artworks as a', 'a.id', '=', 'ca.artwork_id')
            ->whereIn('ca.collection_id', $collectionIds)
            ->whereNull('a.deleted_at')
            ->when($publicOnly, function ($query): void {
                $query->where('a.is_public', true)
                    ->where('a.is_approved', true)
                    ->whereNotNull('a.published_at')
                    ->where('a.published_at', '<=', now());
            })
            ->when($hideMature, function ($query): void {
                $query->whereRaw('COALESCE(a.is_mature, 0) = 0')
                    ->whereRaw("COALESCE(a.maturity_status, 'clear') != ?", ['suspected']);
            })
            ->orderBy('ca.collection_id')
            ->orderBy('ca.order_num')
            ->select(['ca.collection_id', 'a.id'])
            ->get();

        $firstIdsByCollection = $rows
            ->unique('collection_id')
            ->pluck('id', 'collection_id');

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

        $artworks = Artwork::query()
            ->whereIn('id', $firstIdsByCollection->values()->all())
            ->get()
            ->keyBy('id');

        return $firstIdsByCollection->map(fn ($artworkId) => $artworks->get($artworkId))->filter();
    }

    private function mapArtworkPayload(Artwork $artwork, array $extra = []): array
    {
        $category = $artwork->categories->first();
        $contentType = $category?->contentType;
        $stats = $artwork->stats;

        return $this->maturity->decoratePayload(array_merge([
            'id' => $artwork->id,
            'title' => $artwork->title,
            'slug' => $artwork->slug,
            'thumb' => $this->mapArtworkThumb($artwork),
            'url' => route('art.show', ['id' => $artwork->id, 'slug' => Str::slug((string) ($artwork->slug ?: $artwork->title)) ?: (string) $artwork->id]),
            'width' => $artwork->width,
            'height' => $artwork->height,
            'content_type' => $contentType?->name,
            'content_type_slug' => $contentType?->slug,
            'category' => $category?->name,
            'category_slug' => $category?->slug,
            'views' => (int) ($stats?->views ?? $artwork->view_count ?? 0),
            'downloads' => (int) ($stats?->downloads ?? 0),
            'likes' => (int) ($stats?->favorites ?? $artwork->favourite_count ?? 0),
            'published_at' => optional($artwork->published_at)?->toISOString(),
            'is_public' => (bool) $artwork->is_public,
            'is_approved' => (bool) $artwork->is_approved,
            'author' => $artwork->relationLoaded('user') && $artwork->user ? [
                'id' => (int) $artwork->user->id,
                'name' => $artwork->user->name,
                'username' => $artwork->user->username,
                'profile_url' => route('profile.show', ['username' => strtolower((string) $artwork->user->username)]),
            ] : null,
        ], $extra), $artwork, request()->user());
    }

    private function normalizeLayoutModules(?array $modules, string $type, bool $allowComments, bool $allowSubmissions, bool $includePresentation = true): array
    {
        $definitions = config('collections.layout_modules', []);
        $submitted = collect($modules ?? [])->values();
        $submittedByKey = $submitted
            ->filter(fn ($item) => is_array($item) && isset($item['key']))
            ->keyBy(fn ($item) => (string) $item['key']);
        $submittedOrder = $submitted
            ->filter(fn ($item) => is_array($item) && isset($item['key']))
            ->pluck('key')
            ->values()
            ->flip();
        $submittedCount = $submittedOrder->count();

        $normalized = [];

        foreach ($definitions as $key => $definition) {
            $input = $submittedByKey->get($key, []);
            $slots = $definition['slots'] ?? ['main'];
            $slot = (string) ($input['slot'] ?? $definition['default_slot'] ?? $slots[0]);

            if (! in_array($slot, $slots, true)) {
                $slot = (string) ($definition['default_slot'] ?? $slots[0]);
            }

            $locked = (bool) ($definition['locked'] ?? false);
            $enabled = array_key_exists('enabled', $input)
                ? (bool) $input['enabled']
                : $this->defaultEnabledForLayoutModule((string) $key, $type, $allowComments, $allowSubmissions);

            if ($locked) {
                $enabled = true;
            }

            $normalized[] = [
                'key' => (string) $key,
                'label' => (string) ($definition['label'] ?? Str::headline((string) $key)),
                'description' => (string) ($definition['description'] ?? ''),
                'slot' => $slot,
                'slots' => array_values($slots),
                'enabled' => $enabled,
                'locked' => $locked,
                '_order' => (int) ($submittedOrder->get($key, $submittedCount + count($normalized))),
            ];
        }

        usort($normalized, static fn (array $left, array $right): int => $left['_order'] <=> $right['_order']);

        return array_map(function (array $module) use ($includePresentation): array {
            unset($module['_order']);

            if (! $includePresentation) {
                return [
                    'key' => $module['key'],
                    'slot' => $module['slot'],
                    'enabled' => $module['enabled'],
                ];
            }

            return $module;
        }, $normalized);
    }

    private function defaultEnabledForLayoutModule(string $key, string $type, bool $allowComments, bool $allowSubmissions): bool
    {
        return match ($key) {
            'intro_block' => true,
            'artwork_grid' => true,
            'featured_artworks' => false,
            'discussion' => $allowComments,
            'submissions' => $allowSubmissions,
            'editorial_note' => $type === Collection::TYPE_EDITORIAL,
            default => true,
        };
    }

    private function resolveOwnershipContext(User $actor, array $attributes, ?Collection $collection, string $type): array
    {
        if ($type !== Collection::TYPE_EDITORIAL) {
            $group = $this->resolveGroupContext($actor, $attributes, $collection);
            $ownerUser = $collection && ! $collection->hasSystemEditorialOwner() && (int) $collection->user_id !== (int) $actor->id
                ? $collection->user
                : ($group?->owner ?: $actor);

            $managedByUserId = $group && (int) $ownerUser->id !== (int) $actor->id
                ? (int) $actor->id
                : null;

            return [
                'owner_user' => $ownerUser,
                'group' => $group,
                'managed_by_user_id' => $managedByUserId,
                'editorial_owner_mode' => Collection::EDITORIAL_OWNER_CREATOR,
                'editorial_owner_user_id' => null,
                'editorial_owner_label' => null,
            ];
        }

        $ownerMode = (string) ($attributes['editorial_owner_mode'] ?? $collection?->editorial_owner_mode ?? Collection::EDITORIAL_OWNER_CREATOR);
        $ownerUser = $actor;
        $managedByUserId = null;
        $editorialOwnerUserId = null;
        $editorialOwnerLabel = null;

        if ($ownerMode === Collection::EDITORIAL_OWNER_STAFF_ACCOUNT) {
            $ownerUsername = trim((string) ($attributes['editorial_owner_username'] ?? ''));
            $target = User::query()->whereRaw('LOWER(username) = ?', [Str::lower($ownerUsername)])->first();

            if (! $target || ! ($target->isAdmin() || $target->isModerator())) {
                throw ValidationException::withMessages([
                    'editorial_owner_username' => 'The editorial owner must be an admin or moderator account.',
                ]);
            }

            $ownerUser = $target;
            $managedByUserId = (int) $target->id !== (int) $actor->id ? (int) $actor->id : null;
            $editorialOwnerUserId = (int) $target->id;
        }

        if ($ownerMode === Collection::EDITORIAL_OWNER_SYSTEM) {
            $systemLabel = trim((string) ($attributes['editorial_owner_label'] ?? config('collections.editorial.system_owner_label', 'Skinbase Editorial')));
            $systemOwnerUsername = trim((string) config('collections.editorial.system_owner_username', ''));
            $systemOwner = $systemOwnerUsername !== ''
                ? User::query()->whereRaw('LOWER(username) = ?', [Str::lower($systemOwnerUsername)])->first()
                : null;

            if ($systemOwner) {
                $ownerUser = $systemOwner;
                $editorialOwnerUserId = (int) $systemOwner->id;
                $managedByUserId = (int) $systemOwner->id !== (int) $actor->id ? (int) $actor->id : null;
            }

            $editorialOwnerLabel = $systemLabel !== '' ? $systemLabel : 'Skinbase Editorial';
        }

        return [
            'owner_user' => $ownerUser,
            'group' => null,
            'managed_by_user_id' => $managedByUserId,
            'editorial_owner_mode' => $ownerMode,
            'editorial_owner_user_id' => $editorialOwnerUserId,
            'editorial_owner_label' => $editorialOwnerLabel,
        ];
    }

    private function mapCollectionOwnerPayload(Collection $collection): array
    {
        if ((int) ($collection->group_id ?? 0) > 0) {
            $group = $collection->relationLoaded('group') ? $collection->group : $collection->group()->with('owner.profile')->first();

            return [
                'name' => $group?->name ?: 'Skinbase Group',
                'username' => null,
                'profile_url' => $group ? $group->publicUrl() : null,
                'is_system' => false,
                'mode' => 'group',
                'managed_by_user_id' => $collection->managed_by_user_id ? (int) $collection->managed_by_user_id : null,
                'avatar_url' => $group?->avatarUrl(),
                'group' => $group ? [
                    'id' => (int) $group->id,
                    'slug' => (string) $group->slug,
                    'name' => (string) $group->name,
                    'public_url' => $group->publicUrl(),
                ] : null,
            ];
        }

        $owner = $collection->relationLoaded('user') ? $collection->user : $collection->user()->first();
        $username = $collection->displayOwnerUsername();
        $avatarUrl = null;

        if (! $collection->hasSystemEditorialOwner() && $owner && $owner->relationLoaded('profile')) {
            $avatarUrl = AvatarUrl::forUser((int) $owner->id, $owner->profile?->avatar_hash, 128);
        }

        return [
            'name' => $collection->displayOwnerName(),
            'username' => $username,
            'profile_url' => $username ? route('profile.tab', ['username' => strtolower($username), 'tab' => 'collections']) : null,
            'is_system' => $collection->hasSystemEditorialOwner(),
            'mode' => $collection->editorial_owner_mode,
            'managed_by_user_id' => $collection->managed_by_user_id ? (int) $collection->managed_by_user_id : null,
            'avatar_url' => $avatarUrl,
            'group' => null,
        ];
    }

    private function mapArtworkThumb(Artwork $artwork): ?string
    {
        $presented = ThumbnailPresenter::present($artwork, 'md');

        return $presented['url'] ?? $artwork->thumbUrl('md');
    }

    private function eligibleCoverArtwork(?Artwork $artwork, bool $publicOnly, bool $hideMature): bool
    {
        if (! $artwork) {
            return false;
        }

        if ($publicOnly && (! (bool) $artwork->is_public || ! (bool) $artwork->is_approved || $artwork->published_at === null || $artwork->published_at->gt(now()))) {
            return false;
        }

        if (! $hideMature) {
            return true;
        }

        return ! (bool) $artwork->is_mature
            && (string) ($artwork->maturity_status ?? ArtworkMaturityService::STATUS_CLEAR) !== ArtworkMaturityService::STATUS_SUSPECTED;
    }

    private function viewerShouldHideMature(?User $viewer): bool
    {
        return $this->maturity->viewerPreferences($viewer)['visibility'] === ArtworkMaturityService::VIEW_HIDE;
    }

    private function slugExistsForUser(User $user, string $slug, ?int $ignoreCollectionId = null, ?Group $group = null): bool
    {
        return Collection::query()
            ->when($group !== null,
                fn ($query) => $query->where('group_id', $group->id),
                fn ($query) => $query->whereNull('group_id')->where('user_id', $user->id)
            )
            ->where('slug', $slug)
            ->when($ignoreCollectionId !== null, fn ($query) => $query->where('id', '!=', $ignoreCollectionId))
            ->withTrashed()
            ->exists();
    }

    private function featuredLimit(): int
    {
        return max(1, (int) config('collections.featured_limit', 3));
    }

    private function nextProfileOrder(User $user, ?Group $group = null): int
    {
        return (int) (Collection::query()
            ->when($group !== null,
                fn ($query) => $query->where('group_id', $group->id),
                fn ($query) => $query->ownedBy((int) $user->id)
            )
            ->max('profile_order') ?? -1) + 1;
    }

    private function resolveGroupContext(User $actor, array $attributes, ?Collection $collection = null): ?Group
    {
        $groupIdentifier = $attributes['group'] ?? $attributes['group_id'] ?? ($collection?->group_id ? (string) $collection->group_id : null);

        if ($groupIdentifier === null || $groupIdentifier === '') {
            return null;
        }

        $group = is_numeric($groupIdentifier)
            ? Group::query()->with('members')->findOrFail((int) $groupIdentifier)
            : Group::query()->with('members')->where('slug', (string) $groupIdentifier)->firstOrFail();

        if (! $group->canManageCollections($actor) && ! $actor->isAdmin()) {
            throw ValidationException::withMessages([
                'group' => 'You are not allowed to manage collections for this group.',
            ]);
        }

        return $group;
    }

    private function resolvePublishedAt(array $attributes, mixed $fallback = null): ?Carbon
    {
        if (! array_key_exists('published_at', $attributes)) {
            return $fallback instanceof Carbon ? $fallback : ($fallback ? Carbon::parse((string) $fallback) : now());
        }

        $value = $attributes['published_at'];

        if ($value === null || $value === '') {
            return null;
        }

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

    private function resolveUnpublishedAt(array $attributes, mixed $fallback = null): ?Carbon
    {
        if (! array_key_exists('unpublished_at', $attributes)) {
            return $fallback instanceof Carbon ? $fallback : ($fallback ? Carbon::parse((string) $fallback) : null);
        }

        $value = $attributes['unpublished_at'];

        if ($value === null || $value === '') {
            return null;
        }

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

    /**
     * @return array<int, int>
     */
    private function contributorIds(Collection $collection): array
    {
        if ((int) ($collection->group_id ?? 0) > 0) {
            $group = $collection->relationLoaded('group') ? $collection->group : $collection->group()->with('members')->first();

            return $group ? $this->groupMembers->activeContributorIds($group) : [];
        }

        return $collection->isCollaborative()
            ? $this->collaborators->activeContributorIds($collection)
            : [(int) $collection->user_id];
    }

    private function syncSmartCollectionState(Collection $collection): void
    {
        $count = $this->smartCollections->countMatching($collection, null, true);

        $collection->forceFill([
            'artworks_count' => $count,
            'cover_artwork_id' => null,
        ])->save();
    }
}
