<?php

declare(strict_types=1);

namespace App\Services\Uploads;

use App\Jobs\AnalyzeArtworkAiAssistJob;
use App\Jobs\AutoTagArtworkJob;
use App\Jobs\DetectArtworkMaturityJob;
use App\Jobs\GenerateArtworkEmbeddingJob;
use App\Models\Artwork;
use App\Models\UploadBatch;
use App\Models\UploadBatchItem;
use App\Models\User;
use App\Services\Artworks\ArtworkDraftService;
use App\Services\Artworks\ArtworkPublicationService;
use App\Services\Maturity\ArtworkMaturityService;
use App\Services\TagService;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

final class UploadQueueService
{
    public function __construct(
        private readonly ArtworkDraftService $artworkDrafts,
        private readonly ArtworkPublicationService $publication,
        private readonly TagService $tags,
    ) {
    }

    public function createBatch(User $user, array $files, array $defaults = [], ?string $name = null): UploadBatch
    {
        $normalizedFiles = collect($files)
            ->map(fn (mixed $file): array => [
                'name' => trim((string) data_get($file, 'name', '')),
            ])
            ->filter(fn (array $file): bool => $file['name'] !== '')
            ->values();

        if ($normalizedFiles->isEmpty()) {
            throw ValidationException::withMessages([
                'files' => ['Choose at least one file for the upload queue.'],
            ]);
        }

        if ($normalizedFiles->count() > 50) {
            throw ValidationException::withMessages([
                'files' => ['Bulk upload is limited to 50 files per batch in v1.'],
            ]);
        }

        $normalizedDefaults = $this->normalizeDefaults($defaults);

        $batch = DB::transaction(function () use ($user, $normalizedFiles, $normalizedDefaults, $name): UploadBatch {
            $batch = UploadBatch::query()->create([
                'user_id' => (int) $user->id,
                'name' => $this->normalizeBatchName($name, $normalizedFiles->count()),
                'status' => UploadBatch::STATUS_UPLOADING,
                'total_items' => $normalizedFiles->count(),
                'defaults_json' => $normalizedDefaults === [] ? null : $normalizedDefaults,
            ]);

            foreach ($normalizedFiles as $file) {
                $draft = $this->artworkDrafts->createDraft(
                    $user,
                    $this->titleFromFilename($file['name']),
                    null,
                    Arr::get($normalizedDefaults, 'category_id'),
                    (bool) Arr::get($normalizedDefaults, 'is_mature', false),
                    Arr::get($normalizedDefaults, 'group')
                );

                $artwork = Artwork::query()->findOrFail($draft->artworkId);

                $artwork->forceFill([
                    'visibility' => Arr::get($normalizedDefaults, 'visibility', Artwork::VISIBILITY_PUBLIC),
                    'is_public' => false,
                    'artwork_status' => 'draft',
                ])->saveQuietly();

                $defaultTags = (array) Arr::get($normalizedDefaults, 'tags', []);
                if ($defaultTags !== []) {
                    $this->tags->syncStudioTags($artwork, $defaultTags);
                }

                $batch->items()->create([
                    'user_id' => (int) $user->id,
                    'artwork_id' => (int) $artwork->id,
                    'original_filename' => $file['name'],
                    'status' => UploadBatchItem::STATUS_UPLOADED,
                    'processing_stage' => UploadBatchItem::STAGE_QUEUED,
                    'metadata_completeness' => $this->metadataCompleteness($artwork, false, false),
                    'is_ready_to_publish' => false,
                ]);
            }

            return $batch;
        });

        return $this->refreshBatch($batch->id);
    }

    public function listPayload(User $user, array $filters = []): array
    {
        $requestedBatchId = (int) ($filters['batch_id'] ?? 0);
        $statusFilter = Str::lower(trim((string) ($filters['status'] ?? 'all')));
        $sort = Str::lower(trim((string) ($filters['sort'] ?? 'newest')));

        $recentBatches = UploadBatch::query()
            ->where('user_id', (int) $user->id)
            ->latest('id')
            ->limit(12)
            ->get();

        $currentBatch = $requestedBatchId > 0
            ? $recentBatches->firstWhere('id', $requestedBatchId)
            : $recentBatches->first();

        if ($currentBatch) {
            $currentBatch = $this->refreshBatch((int) $currentBatch->id);
        }

        $items = $currentBatch
            ? $currentBatch->items
                ->loadMissing(['artwork.categories.contentType', 'artwork.tags'])
                ->map(fn (UploadBatchItem $item): array => $this->presentItem($this->refreshItem((int) $item->id)))
            : collect();

        $filteredItems = $this->applyFiltersAndSorting($items, $statusFilter, $sort)->values();
        $recentBatchPayloads = $recentBatches
            ->map(fn (UploadBatch $batch): UploadBatch => $batch->relationLoaded('items') ? $batch : $this->refreshBatch((int) $batch->id))
            ->map(fn (UploadBatch $batch): array => $this->presentBatch($batch))
            ->values()
            ->all();

        return [
            'filters' => [
                'batch_id' => $currentBatch?->id,
                'status' => $statusFilter,
                'sort' => $sort,
            ],
            'batches' => $recentBatchPayloads,
            'current_batch' => $currentBatch ? $this->presentBatch($currentBatch) : null,
            'items' => $filteredItems->all(),
            'status_options' => [
                ['value' => 'all', 'label' => 'All'],
                ['value' => UploadBatchItem::STATUS_PROCESSING, 'label' => 'Processing'],
                ['value' => UploadBatchItem::STATUS_NEEDS_METADATA, 'label' => 'Needs metadata'],
                ['value' => UploadBatchItem::STATUS_NEEDS_REVIEW, 'label' => 'Needs review'],
                ['value' => UploadBatchItem::STATUS_READY, 'label' => 'Ready'],
                ['value' => UploadBatchItem::STATUS_FAILED, 'label' => 'Failed'],
                ['value' => UploadBatchItem::STATUS_PUBLISHED, 'label' => 'Published'],
            ],
            'sort_options' => [
                ['value' => 'newest', 'label' => 'Newest first'],
                ['value' => 'oldest', 'label' => 'Oldest first'],
                ['value' => 'filename', 'label' => 'Filename'],
                ['value' => 'status', 'label' => 'Status'],
                ['value' => 'ready', 'label' => 'Ready first'],
                ['value' => 'failed', 'label' => 'Failed first'],
            ],
        ];
    }

    public function markItemProcessingQueued(int $itemId): UploadBatchItem
    {
        $item = $this->itemQuery()->findOrFail($itemId);

        $item->forceFill([
            'status' => UploadBatchItem::STATUS_PROCESSING,
            'processing_stage' => UploadBatchItem::STAGE_THUMBNAILS,
            'error_code' => null,
            'error_message' => null,
            'uploaded_at' => $item->uploaded_at ?: now(),
        ])->save();

        $this->refreshBatch((int) $item->upload_batch_id);

        return $item->fresh(['artwork.categories.contentType', 'artwork.tags']) ?? $item;
    }

    public function markItemMediaProcessed(int $itemId): UploadBatchItem
    {
        $item = $this->itemQuery()->findOrFail($itemId);

        $item->forceFill([
            'processing_stage' => UploadBatchItem::STAGE_MATURITY_CHECK,
            'error_code' => null,
            'error_message' => null,
            'processed_at' => now(),
        ])->save();

        $refreshed = $this->refreshItem((int) $item->id);
        $this->refreshBatch((int) $item->upload_batch_id);

        return $refreshed;
    }

    public function markItemFailed(int $itemId, ?string $errorCode, ?string $errorMessage): UploadBatchItem
    {
        $item = $this->itemQuery()->findOrFail($itemId);

        $item->forceFill([
            'status' => UploadBatchItem::STATUS_FAILED,
            'error_code' => $this->nullableString($errorCode),
            'error_message' => $this->nullableText($errorMessage),
            'is_ready_to_publish' => false,
        ])->save();

        $this->refreshBatch((int) $item->upload_batch_id);

        return $item->fresh(['artwork.categories.contentType', 'artwork.tags']) ?? $item;
    }

    public function markItemFailedForUser(User $user, int $itemId, ?string $errorCode, ?string $errorMessage): UploadBatchItem
    {
        $item = $this->ownedItems($user, [$itemId])->first();
        if (! $item) {
            throw ValidationException::withMessages([
                'item' => ['Upload queue item not found.'],
            ]);
        }

        return $this->markItemFailed((int) $item->id, $errorCode, $errorMessage);
    }

    public function refreshItem(int $itemId): UploadBatchItem
    {
        $item = $this->itemQuery()->findOrFail($itemId);
        $state = $this->evaluateItemState($item);

        $item->forceFill([
            'status' => $state['status'],
            'processing_stage' => $state['processing_stage'],
            'metadata_completeness' => $state['metadata_completeness'],
            'is_ready_to_publish' => $state['is_ready_to_publish'],
            'published_at' => $state['published_at'],
            'processed_at' => $state['processed_at'],
        ])->saveQuietly();

        return $item->fresh(['artwork.categories.contentType', 'artwork.tags']) ?? $item;
    }

    public function refreshBatch(int $batchId): UploadBatch
    {
        $batch = UploadBatch::query()
            ->with(['items.artwork.categories.contentType', 'items.artwork.tags'])
            ->findOrFail($batchId);

        $items = $batch->items->map(fn (UploadBatchItem $item): UploadBatchItem => $this->refreshItem((int) $item->id));
        $summary = $this->summarizeItems($items);

        $batch->forceFill([
            'status' => $summary['status'],
            'total_items' => $summary['total_items'],
            'processed_items' => $summary['processed_items'],
            'failed_items' => $summary['failed_items'],
            'published_items' => $summary['published_items'],
        ])->saveQuietly();

        return $batch->fresh(['items.artwork.categories.contentType', 'items.artwork.tags']) ?? $batch;
    }

    public function bulkAction(User $user, string $action, array $itemIds, array $params = []): array
    {
        $items = $this->ownedItems($user, $itemIds);

        if ($items->isEmpty()) {
            return [
                'success' => 0,
                'failed' => count($itemIds),
                'errors' => ['No owned upload queue items were found.'],
            ];
        }

        $success = 0;
        $failed = 0;
        $errors = [];

        foreach ($items as $item) {
            try {
                match ($action) {
                    'publish' => $this->publishItem($item),
                    'delete' => $this->deleteItem($item),
                    'apply_category' => $this->applyCategory($item, (int) ($params['category_id'] ?? 0)),
                    'apply_tags' => $this->applyTags($item, (array) ($params['tags'] ?? [])),
                    'set_visibility' => $this->setVisibility($item, (string) ($params['visibility'] ?? '')),
                    'generate_ai' => $this->retryProcessing($item),
                    default => throw ValidationException::withMessages([
                        'action' => ['Unsupported upload queue action.'],
                    ]),
                };

                $success++;
            } catch (ValidationException $exception) {
                $failed++;
                $errors[] = collect($exception->errors())->flatten()->first() ?: 'Action failed.';
            } catch (\Throwable $exception) {
                $failed++;
                $errors[] = $exception->getMessage();
            }
        }

        $batchIds = $items->pluck('upload_batch_id')->unique()->filter()->all();
        foreach ($batchIds as $batchId) {
            $this->refreshBatch((int) $batchId);
        }

        return [
            'success' => $success,
            'failed' => $failed,
            'errors' => $errors,
        ];
    }

    public function retryProcessingForUser(User $user, int $itemId): UploadBatchItem
    {
        $item = $this->ownedItems($user, [$itemId])->first();
        if (! $item) {
            throw ValidationException::withMessages([
                'item' => ['Upload queue item not found.'],
            ]);
        }

        return $this->retryProcessing($item);
    }

    private function retryProcessing(UploadBatchItem $item): UploadBatchItem
    {
        $artwork = $item->artwork;
        if (! $artwork || trim((string) ($artwork->hash ?? '')) === '' || trim((string) ($artwork->file_path ?? '')) === '') {
            throw ValidationException::withMessages([
                'item' => ['This item cannot be retried safely. Re-upload the original file instead.'],
            ]);
        }

        $item->forceFill([
            'status' => UploadBatchItem::STATUS_PROCESSING,
            'processing_stage' => UploadBatchItem::STAGE_MATURITY_CHECK,
            'error_code' => null,
            'error_message' => null,
            'is_ready_to_publish' => false,
        ])->save();

        AutoTagArtworkJob::dispatch((int) $artwork->id, (string) $artwork->hash)->afterCommit();
        DetectArtworkMaturityJob::dispatch((int) $artwork->id, (string) $artwork->hash)->afterCommit();
        GenerateArtworkEmbeddingJob::dispatch((int) $artwork->id, (string) $artwork->hash)->afterCommit();
        AnalyzeArtworkAiAssistJob::dispatch((int) $artwork->id, true)->afterCommit();

        return $this->refreshItem((int) $item->id);
    }

    private function publishItem(UploadBatchItem $item): void
    {
        $item = $this->refreshItem((int) $item->id);
        if (! $item->is_ready_to_publish || ! $item->artwork) {
            throw ValidationException::withMessages([
                'item' => ['Only ready queue items can be published.'],
            ]);
        }

        $artwork = $item->artwork;
        $artwork->forceFill([
            'is_approved' => true,
            'visibility' => $artwork->visibility ?: Artwork::VISIBILITY_PUBLIC,
        ])->saveQuietly();

        $this->publication->publishNow($artwork->fresh() ?? $artwork);

        $item->forceFill([
            'status' => UploadBatchItem::STATUS_PUBLISHED,
            'processing_stage' => UploadBatchItem::STAGE_FINALIZED,
            'is_ready_to_publish' => false,
            'published_at' => now(),
        ])->save();
    }

    private function deleteItem(UploadBatchItem $item): void
    {
        if ($item->artwork && ! $item->artwork->trashed()) {
            $item->artwork->delete();
        }

        $item->forceFill([
            'status' => UploadBatchItem::STATUS_DELETED,
            'processing_stage' => UploadBatchItem::STAGE_FINALIZED,
            'is_ready_to_publish' => false,
        ])->save();
    }

    private function applyCategory(UploadBatchItem $item, int $categoryId): void
    {
        if ($categoryId <= 0 || ! $item->artwork) {
            throw ValidationException::withMessages([
                'category_id' => ['Choose a valid category.'],
            ]);
        }

        $item->artwork->categories()->sync([$categoryId]);
        $this->refreshItem((int) $item->id);
    }

    private function applyTags(UploadBatchItem $item, array $tags): void
    {
        if (! $item->artwork) {
            throw ValidationException::withMessages([
                'tags' => ['Artwork not found for this queue item.'],
            ]);
        }

        $normalizedTags = collect($tags)
            ->map(fn (mixed $tag): string => trim((string) $tag))
            ->filter()
            ->values();

        if ($normalizedTags->isEmpty()) {
            throw ValidationException::withMessages([
                'tags' => ['Enter at least one tag to apply.'],
            ]);
        }

        $mergedTags = collect($item->artwork->tags()->pluck('tags.slug')->all())
            ->merge($normalizedTags)
            ->map(fn (string $tag): string => trim($tag))
            ->filter()
            ->unique()
            ->values()
            ->all();

        $this->tags->syncStudioTags($item->artwork->fresh(['tags']) ?? $item->artwork, $mergedTags);
        $this->refreshItem((int) $item->id);
    }

    private function setVisibility(UploadBatchItem $item, string $visibility): void
    {
        if (! in_array($visibility, [Artwork::VISIBILITY_PUBLIC, Artwork::VISIBILITY_UNLISTED, Artwork::VISIBILITY_PRIVATE], true) || ! $item->artwork) {
            throw ValidationException::withMessages([
                'visibility' => ['Choose a valid visibility.'],
            ]);
        }

        $item->artwork->forceFill([
            'visibility' => $visibility,
            'is_public' => false,
        ])->saveQuietly();

        $this->refreshItem((int) $item->id);
    }

    private function ownedItems(User $user, array $itemIds): EloquentCollection
    {
        $normalizedIds = collect($itemIds)
            ->map(fn (mixed $id): int => (int) $id)
            ->filter(fn (int $id): bool => $id > 0)
            ->values()
            ->all();

        return $this->itemQuery()
            ->where('user_id', (int) $user->id)
            ->whereIn('id', $normalizedIds)
            ->get();
    }

    private function itemQuery()
    {
        return UploadBatchItem::query()->with(['artwork.categories.contentType', 'artwork.tags']);
    }

    private function applyFiltersAndSorting(Collection $items, string $statusFilter, string $sort): Collection
    {
        $filtered = $statusFilter === 'all'
            ? $items
            : $items->filter(fn (array $item): bool => (string) ($item['status'] ?? '') === $statusFilter);

        return match ($sort) {
            'oldest' => $filtered->sortBy('created_at'),
            'filename' => $filtered->sortBy(fn (array $item): string => Str::lower((string) ($item['original_filename'] ?? ''))),
            'status' => $filtered->sortBy(fn (array $item): string => Str::lower((string) ($item['status'] ?? ''))),
            'ready' => $filtered->sortByDesc(fn (array $item): int => (int) ($item['is_ready_to_publish'] ?? false)),
            'failed' => $filtered->sortByDesc(fn (array $item): int => (int) ((string) ($item['status'] ?? '') === UploadBatchItem::STATUS_FAILED)),
            default => $filtered->sortByDesc('created_at'),
        };
    }

    private function summarizeItems(EloquentCollection $items): array
    {
        $statusCounts = $items->groupBy('status')->map->count();
        $activeCount = (int) ($statusCounts[UploadBatchItem::STATUS_UPLOADED] ?? 0)
            + (int) ($statusCounts[UploadBatchItem::STATUS_PROCESSING] ?? 0);
        $failedCount = (int) ($statusCounts[UploadBatchItem::STATUS_FAILED] ?? 0);
        $publishedCount = (int) ($statusCounts[UploadBatchItem::STATUS_PUBLISHED] ?? 0);
        $processedCount = $items->filter(fn (UploadBatchItem $item): bool => in_array((string) $item->status, [
            UploadBatchItem::STATUS_NEEDS_METADATA,
            UploadBatchItem::STATUS_NEEDS_REVIEW,
            UploadBatchItem::STATUS_READY,
            UploadBatchItem::STATUS_FAILED,
            UploadBatchItem::STATUS_PUBLISHED,
            UploadBatchItem::STATUS_DELETED,
        ], true))->count();

        $status = UploadBatch::STATUS_COMPLETED;
        if ($activeCount > 0) {
            $status = $statusCounts[UploadBatchItem::STATUS_PROCESSING] ?? 0
                ? UploadBatch::STATUS_PROCESSING
                : UploadBatch::STATUS_UPLOADING;
        } elseif ($failedCount > 0) {
            $status = UploadBatch::STATUS_COMPLETED_WITH_ERRORS;
        }

        return [
            'status' => $status,
            'total_items' => $items->count(),
            'processed_items' => $processedCount,
            'failed_items' => $failedCount,
            'published_items' => $publishedCount,
            'ready_items' => (int) ($statusCounts[UploadBatchItem::STATUS_READY] ?? 0),
            'processing_items' => $activeCount,
            'needs_metadata_items' => (int) ($statusCounts[UploadBatchItem::STATUS_NEEDS_METADATA] ?? 0),
            'needs_review_items' => (int) ($statusCounts[UploadBatchItem::STATUS_NEEDS_REVIEW] ?? 0),
        ];
    }

    private function evaluateItemState(UploadBatchItem $item): array
    {
        $artwork = $item->artwork;
        $isDeleted = (string) $item->status === UploadBatchItem::STATUS_DELETED || ($artwork?->trashed() ?? false);
        $isPublished = $artwork
            && (string) ($artwork->artwork_status ?? '') === 'published'
            && $artwork->published_at !== null;
        $hasProcessedMedia = $artwork
            && trim((string) ($artwork->file_name ?? '')) !== ''
            && trim((string) ($artwork->file_name ?? '')) !== 'pending'
            && trim((string) ($artwork->file_path ?? '')) !== ''
            && trim((string) ($artwork->hash ?? '')) !== '';
        $title = trim((string) ($artwork?->title ?? ''));
        $hasTitle = $title !== '';
        $hasCategory = (bool) $artwork?->categories?->first();
        $maturityStatus = Str::lower((string) ($artwork?->maturity_status ?? ArtworkMaturityService::STATUS_CLEAR));
        $maturityAiStatus = Str::lower((string) ($artwork?->maturity_ai_status ?? ArtworkMaturityService::AI_STATUS_NOT_REQUESTED));
        $aiStatus = Str::lower((string) ($artwork?->ai_status ?? ''));
        $visionEnabled = (bool) config('vision.enabled', true);

        $maturityPending = $visionEnabled && in_array($maturityAiStatus, [
            ArtworkMaturityService::AI_STATUS_PENDING,
            ArtworkMaturityService::AI_STATUS_NOT_REQUESTED,
        ], true);
        $maturityFailed = $visionEnabled && $maturityAiStatus === ArtworkMaturityService::AI_STATUS_FAILED;
        $needsReview = $maturityStatus === ArtworkMaturityService::STATUS_SUSPECTED || $maturityFailed;
        $needsMetadata = ! $hasTitle || ! $hasCategory;
        $blockingUploadFailure = ! $hasProcessedMedia && ($this->nullableString($item->error_code) !== null || $this->nullableText($item->error_message) !== null);
        $isReadyToPublish = ! $isDeleted
            && ! $isPublished
            && ! $blockingUploadFailure
            && $hasProcessedMedia
            && ! $maturityPending
            && ! $needsReview
            && ! $needsMetadata
            && $artwork
            && (int) $artwork->user_id === (int) $item->user_id;

        $status = match (true) {
            $isDeleted => UploadBatchItem::STATUS_DELETED,
            $isPublished => UploadBatchItem::STATUS_PUBLISHED,
            $blockingUploadFailure => UploadBatchItem::STATUS_FAILED,
            ! $hasProcessedMedia || $maturityPending => UploadBatchItem::STATUS_PROCESSING,
            $needsReview => UploadBatchItem::STATUS_NEEDS_REVIEW,
            $needsMetadata => UploadBatchItem::STATUS_NEEDS_METADATA,
            $isReadyToPublish => UploadBatchItem::STATUS_READY,
            default => UploadBatchItem::STATUS_PROCESSING,
        };

        $processingStage = match (true) {
            in_array($status, [UploadBatchItem::STATUS_DELETED, UploadBatchItem::STATUS_PUBLISHED, UploadBatchItem::STATUS_FAILED], true) => UploadBatchItem::STAGE_FINALIZED,
            ! $hasProcessedMedia && (string) $item->processing_stage === UploadBatchItem::STAGE_QUEUED => UploadBatchItem::STAGE_QUEUED,
            ! $hasProcessedMedia => UploadBatchItem::STAGE_THUMBNAILS,
            $maturityPending => UploadBatchItem::STAGE_MATURITY_CHECK,
            in_array($aiStatus, ['queued', 'processing'], true) => UploadBatchItem::STAGE_METADATA_SUGGESTIONS,
            default => UploadBatchItem::STAGE_FINALIZED,
        };

        return [
            'status' => $status,
            'processing_stage' => $processingStage,
            'metadata_completeness' => $this->metadataCompleteness($artwork, $hasProcessedMedia, ! $maturityPending && ! $maturityFailed),
            'is_ready_to_publish' => $isReadyToPublish,
            'published_at' => $isPublished ? ($item->published_at ?: now()) : null,
            'processed_at' => $hasProcessedMedia ? ($item->processed_at ?: now()) : $item->processed_at,
        ];
    }

    private function presentBatch(UploadBatch $batch): array
    {
        $summary = $this->summarizeItems($batch->items);

        return [
            'id' => (int) $batch->id,
            'name' => $batch->name,
            'status' => $summary['status'],
            'total_items' => $summary['total_items'],
            'processed_items' => $summary['processed_items'],
            'failed_items' => $summary['failed_items'],
            'published_items' => $summary['published_items'],
            'ready_items' => $summary['ready_items'],
            'processing_items' => $summary['processing_items'],
            'needs_metadata_items' => $summary['needs_metadata_items'],
            'needs_review_items' => $summary['needs_review_items'],
            'defaults' => $batch->defaults_json ?? [],
            'created_at' => $this->iso($batch->created_at),
            'updated_at' => $this->iso($batch->updated_at),
        ];
    }

    private function presentItem(UploadBatchItem $item): array
    {
        $artwork = $item->artwork;
        $state = $this->evaluateItemState($item);
        $maturityStatus = Str::lower((string) ($artwork?->maturity_status ?? ArtworkMaturityService::STATUS_CLEAR));
        $maturityAiStatus = Str::lower((string) ($artwork?->maturity_ai_status ?? ArtworkMaturityService::AI_STATUS_NOT_REQUESTED));
        $missing = [];

        if (! $artwork || trim((string) ($artwork->file_path ?? '')) === '' || trim((string) ($artwork->hash ?? '')) === '') {
            $missing[] = 'Processing incomplete';
        }
        if (! $artwork || trim((string) ($artwork->title ?? '')) === '') {
            $missing[] = 'Missing title';
        }
        if (! $artwork?->categories?->first()) {
            $missing[] = 'Missing category';
        }
        if ($maturityStatus === ArtworkMaturityService::STATUS_SUSPECTED) {
            $missing[] = 'Needs maturity review';
        } elseif ((bool) config('vision.enabled', true) && in_array($maturityAiStatus, [ArtworkMaturityService::AI_STATUS_PENDING, ArtworkMaturityService::AI_STATUS_NOT_REQUESTED], true)) {
            $missing[] = 'Maturity analysis pending';
        } elseif ((bool) config('vision.enabled', true) && $maturityAiStatus === ArtworkMaturityService::AI_STATUS_FAILED) {
            $missing[] = 'Maturity check failed';
        }

        $canRetry = ! empty($artwork?->hash)
            && ! empty($artwork?->file_path)
            && ($state['status'] === UploadBatchItem::STATUS_FAILED || $maturityAiStatus === ArtworkMaturityService::AI_STATUS_FAILED);

        return [
            'id' => (int) $item->id,
            'batch_id' => (int) $item->upload_batch_id,
            'artwork_id' => $artwork?->id,
            'title' => $artwork?->title ?: $this->titleFromFilename((string) $item->original_filename),
            'original_filename' => $item->original_filename,
            'status' => $state['status'],
            'processing_stage' => $state['processing_stage'],
            'metadata_completeness' => $state['metadata_completeness'],
            'metadata_label' => $state['metadata_completeness'] . '% complete',
            'is_ready_to_publish' => $state['is_ready_to_publish'],
            'error_code' => $item->error_code,
            'error_message' => $item->error_message,
            'missing' => $missing,
            'thumbnail_url' => $artwork?->thumbUrl('sm'),
            'visibility' => $artwork?->visibility,
            'maturity_status' => $maturityStatus,
            'maturity_ai_status' => $maturityAiStatus,
            'ai_status' => Str::lower((string) ($artwork?->ai_status ?? '')) ?: null,
            'created_at' => $this->iso($item->created_at),
            'updated_at' => $this->iso($item->updated_at),
            'published_at' => $this->iso($item->published_at),
            'edit_url' => $artwork ? route('studio.artworks.edit', ['id' => $artwork->id]) : null,
            'public_url' => $artwork && $artwork->published_at ? route('art.show', ['id' => $artwork->id]) : null,
            'actions' => [
                'can_edit' => $artwork !== null && $state['status'] !== UploadBatchItem::STATUS_DELETED,
                'can_publish' => $state['is_ready_to_publish'],
                'can_delete' => ! in_array($state['status'], [UploadBatchItem::STATUS_DELETED, UploadBatchItem::STATUS_PUBLISHED], true),
                'can_retry_processing' => $canRetry,
                'can_generate_ai' => $artwork !== null && trim((string) ($artwork->hash ?? '')) !== '',
            ],
        ];
    }

    private function metadataCompleteness(?Artwork $artwork, bool $hasProcessedMedia, bool $maturityReady): int
    {
        $checks = [
            $hasProcessedMedia,
            trim((string) ($artwork?->title ?? '')) !== '',
            (bool) $artwork?->categories?->first(),
            $maturityReady,
        ];

        return (int) round((collect($checks)->filter()->count() / count($checks)) * 100);
    }

    private function normalizeDefaults(array $defaults): array
    {
        $visibility = (string) ($defaults['visibility'] ?? Artwork::VISIBILITY_PUBLIC);
        if (! in_array($visibility, [Artwork::VISIBILITY_PUBLIC, Artwork::VISIBILITY_UNLISTED, Artwork::VISIBILITY_PRIVATE], true)) {
            $visibility = Artwork::VISIBILITY_PUBLIC;
        }

        return array_filter([
            'category_id' => ($categoryId = (int) ($defaults['category_id'] ?? 0)) > 0 ? $categoryId : null,
            'tags' => collect((array) ($defaults['tags'] ?? []))
                ->map(fn (mixed $tag): string => trim((string) $tag))
                ->filter()
                ->values()
                ->all(),
            'visibility' => $visibility,
            'is_mature' => (bool) ($defaults['is_mature'] ?? false),
            'group' => $this->nullableString($defaults['group'] ?? null),
        ], static fn (mixed $value): bool => $value !== null && $value !== []);
    }

    private function titleFromFilename(string $filename): string
    {
        $base = pathinfo($filename, PATHINFO_FILENAME);
        $normalized = Str::of($base)
            ->replace(['_', '-'], ' ')
            ->squish()
            ->trim();

        return (string) ($normalized !== '' ? Str::limit((string) $normalized, 255, '') : 'Untitled artwork');
    }

    private function normalizeBatchName(?string $name, int $count): string
    {
        $normalized = trim((string) $name);
        if ($normalized !== '') {
            return Str::limit($normalized, 160, '');
        }

        return 'Upload Queue ' . now()->format('M j, Y g:i A') . ' (' . $count . ')';
    }

    private function iso(CarbonInterface|string|null $value): ?string
    {
        if ($value instanceof CarbonInterface) {
            return $value->toIso8601String();
        }

        if (is_string($value) && trim($value) !== '') {
            return $value;
        }

        return null;
    }

    private function nullableString(mixed $value): ?string
    {
        $normalized = trim((string) $value);

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

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

        return $normalized === '' ? null : Str::limit($normalized, 65535, '');
    }
}