<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Uploads\UploadFinishRequest;
use App\Http\Requests\Uploads\UploadInitRequest;
use App\Http\Requests\Uploads\UploadChunkRequest;
use App\Http\Requests\Uploads\UploadCancelRequest;
use App\Http\Requests\Uploads\UploadStatusRequest;
use App\Jobs\GenerateDerivativesJob;
use App\Jobs\AnalyzeArtworkAiAssistJob;
use App\Jobs\IndexArtworkJob;
use App\Jobs\AutoTagArtworkJob;
use App\Jobs\DetectArtworkMaturityJob;
use App\Jobs\GenerateArtworkEmbeddingJob;
use App\Repositories\Uploads\UploadSessionRepository;
use App\Services\Uploads\UploadChunkService;
use App\Services\Uploads\UploadCancelService;
use App\Services\Uploads\UploadAuditService;
use App\Services\Uploads\UploadPipelineService;
use App\Services\Uploads\UploadQuotaService;
use App\Services\Uploads\UploadQueueService;
use App\Services\Uploads\UploadSessionStatus;
use App\Services\Uploads\UploadStatusService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use App\Services\Upload\Contracts\UploadDraftServiceInterface;
use Carbon\Carbon;
use App\Uploads\Jobs\VirusScanJob;
use App\Uploads\Services\PublishService;
use App\Services\Activity\UserActivityService;
use App\Services\ArtworkAttributionService;
use App\Services\Maturity\ArtworkMaturityService;
use App\Uploads\Exceptions\UploadNotFoundException;
use App\Uploads\Exceptions\UploadOwnershipException;
use App\Uploads\Exceptions\UploadPublishValidationException;
use App\Uploads\Services\ArchiveInspectorService;
use App\Uploads\Services\DraftQuotaService;
use App\Uploads\Exceptions\DraftQuotaException;
use App\Models\Artwork;
use App\Models\Group;
use App\Services\GroupArtworkReviewService;
use App\Services\Worlds\WorldSubmissionService;
use Illuminate\Support\Str;

final class UploadController extends Controller
{
    public function init(
        UploadInitRequest $request,
        UploadPipelineService $pipeline,
        UploadQuotaService $quota,
        UploadAuditService $audit
    )
    {
        $user = $request->user();

        try {
            $quota->enforce($user->id);
        } catch (Throwable $e) {
            return response()->json([
                'message' => $e->getMessage(),
            ], Response::HTTP_TOO_MANY_REQUESTS);
        }

        $result = $pipeline->initSession($user->id, (string) $request->ip());

        $audit->log($user->id, 'upload_init_issued', (string) $request->ip(), [
            'session_id' => $result->sessionId,
        ]);

        return response()->json([
            'session_id' => $result->sessionId,
            'upload_token' => $result->token,
            'status' => $result->status,
        ], Response::HTTP_OK);
    }

    public function finish(
        UploadFinishRequest $request,
        UploadPipelineService $pipeline,
        UploadSessionRepository $sessions,
        UploadAuditService $audit,
        UploadQueueService $queue
    ) {
        $user = $request->user();
        $sessionId = (string) $request->validated('session_id');
        $artworkId = (int) $request->validated('artwork_id');
        $batchItemId = (int) $request->validated('batch_item_id', 0);
        $originalFileName = $request->validated('file_name');
        $archiveSessionId = $request->validated('archive_session_id');
        $archiveOriginalFileName = $request->validated('archive_file_name');
        $additionalScreenshotSessions = collect($request->validated('additional_screenshot_sessions', []))
            ->filter(fn ($payload) => is_array($payload) && is_string($payload['session_id'] ?? null))
            ->values();

        $session = $sessions->getOrFail($sessionId);

        $request->artwork();
        $request->batchItem();

        $failResponse = function (int $statusCode, string $message, ?string $reason = null) use ($queue, $user, $batchItemId) {
            if ($batchItemId > 0) {
                $queue->markItemFailedForUser($user, $batchItemId, $reason ?? 'upload_failed', $message);
            }

            return response()->json(array_filter([
                'message' => $message,
                'reason' => $reason,
            ], static fn (mixed $value): bool => $value !== null), $statusCode);
        };

        $validated = $pipeline->validateAndHash($sessionId);
        if (! $validated->validation->ok || ! $validated->hash) {
            return $failResponse(
                Response::HTTP_UNPROCESSABLE_ENTITY,
                'Upload validation failed.',
                $validated->validation->reason
            );
        }

        if ($pipeline->originalHashExists($validated->hash)) {
            if ($batchItemId > 0) {
                $queue->markItemFailedForUser($user, $batchItemId, 'duplicate_hash', 'Duplicate upload is not allowed. This file already exists.');
            }

            return response()->json([
                'message' => 'Duplicate upload is not allowed. This file already exists.',
                'reason' => 'duplicate_hash',
                'hash' => $validated->hash,
            ], Response::HTTP_CONFLICT);
        }

        $scan = $pipeline->scan($sessionId);
        if (! $scan->ok) {
            return $failResponse(
                Response::HTTP_UNPROCESSABLE_ENTITY,
                'Upload scan failed.',
                $scan->reason
            );
        }

        $validatedArchive = null;
        if (is_string($archiveSessionId) && trim($archiveSessionId) !== '') {
            $validatedArchive = $pipeline->validateAndHashArchive($archiveSessionId);
            if (! $validatedArchive->validation->ok || ! $validatedArchive->hash) {
                return $failResponse(
                    Response::HTTP_UNPROCESSABLE_ENTITY,
                    'Archive validation failed.',
                    $validatedArchive->validation->reason
                );
            }

            $archiveScan = $pipeline->scan($archiveSessionId);
            if (! $archiveScan->ok) {
                return $failResponse(
                    Response::HTTP_UNPROCESSABLE_ENTITY,
                    'Archive scan failed.',
                    $archiveScan->reason
                );
            }
        }

        $validatedAdditionalScreenshots = [];
        foreach ($additionalScreenshotSessions as $payload) {
            $screenshotSessionId = (string) ($payload['session_id'] ?? '');
            if ($screenshotSessionId === '') {
                continue;
            }

            $validatedScreenshot = $pipeline->validateAndHash($screenshotSessionId);
            if (! $validatedScreenshot->validation->ok || ! $validatedScreenshot->hash) {
                return $failResponse(
                    Response::HTTP_UNPROCESSABLE_ENTITY,
                    'Screenshot validation failed.',
                    $validatedScreenshot->validation->reason
                );
            }

            $screenshotScan = $pipeline->scan($screenshotSessionId);
            if (! $screenshotScan->ok) {
                return $failResponse(
                    Response::HTTP_UNPROCESSABLE_ENTITY,
                    'Screenshot scan failed.',
                    $screenshotScan->reason
                );
            }

            $validatedAdditionalScreenshots[] = [
                'session_id' => $screenshotSessionId,
                'hash' => $validatedScreenshot->hash,
                'file_name' => is_string($payload['file_name'] ?? null) ? $payload['file_name'] : null,
            ];
        }

        try {
            $status = DB::transaction(function () use ($pipeline, $sessionId, $validated, $artworkId, $originalFileName, $archiveSessionId, $validatedArchive, $archiveOriginalFileName, $validatedAdditionalScreenshots, $queue, $batchItemId) {
                if ((bool) config('uploads.queue_derivatives', false)) {
                    GenerateDerivativesJob::dispatch(
                        $sessionId,
                        $validated->hash,
                        $artworkId,
                        is_string($originalFileName) ? $originalFileName : null,
                        is_string($archiveSessionId) ? $archiveSessionId : null,
                        $validatedArchive?->hash,
                        is_string($archiveOriginalFileName) ? $archiveOriginalFileName : null,
                        $validatedAdditionalScreenshots,
                        $batchItemId > 0 ? $batchItemId : null
                    )->afterCommit();

                    if ($batchItemId > 0) {
                        $queue->markItemProcessingQueued($batchItemId);
                    }

                    return 'queued';
                }

                $pipeline->processAndPublish(
                    $sessionId,
                    $validated->hash,
                    $artworkId,
                    is_string($originalFileName) ? $originalFileName : null,
                    is_string($archiveSessionId) ? $archiveSessionId : null,
                    $validatedArchive?->hash,
                    is_string($archiveOriginalFileName) ? $archiveOriginalFileName : null,
                    $validatedAdditionalScreenshots
                );

                if ($batchItemId > 0) {
                    $queue->markItemMediaProcessed($batchItemId);
                }

                // Derivatives are available now; dispatch AI auto-tagging.
                AutoTagArtworkJob::dispatch($artworkId, $validated->hash)->afterCommit();
                DetectArtworkMaturityJob::dispatch($artworkId, $validated->hash)->afterCommit();
                GenerateArtworkEmbeddingJob::dispatch($artworkId, $validated->hash)->afterCommit();
                AnalyzeArtworkAiAssistJob::dispatch($artworkId)->afterCommit();
                return UploadSessionStatus::PROCESSED;
            });

            $audit->log($user->id, 'upload_finished', $session->ip, [
                'session_id' => $sessionId,
                'hash' => $validated->hash,
                'artwork_id' => $artworkId,
                'status' => $status,
                'archive_session_id' => is_string($archiveSessionId) ? $archiveSessionId : null,
                'additional_screenshot_session_ids' => array_values(array_map(static fn (array $payload): string => (string) $payload['session_id'], $validatedAdditionalScreenshots)),
            ]);

            return response()->json([
                'artwork_id' => $artworkId,
                'status' => $status,
            ], Response::HTTP_OK);
        } catch (Throwable $e) {
            Log::error('Upload finish failed', [
                'session_id' => $sessionId,
                'artwork_id' => $artworkId,
                'error' => $e->getMessage(),
            ]);

            if ($batchItemId > 0) {
                $queue->markItemFailedForUser($user, $batchItemId, 'upload_finish_failed', $e->getMessage());
            }

            return response()->json([
                'message' => 'Upload finish failed.',
            ], Response::HTTP_INTERNAL_SERVER_ERROR);
        }
    }

    public function chunk(UploadChunkRequest $request, UploadChunkService $chunks)
    {
        $user = $request->user();
        $chunkFile = $request->file('chunk');

        // Debug: log uploaded file object details to help diagnose missing chunk
        try {
            if (! $chunkFile) {
                logger()->warning('Chunk upload: no file present on request', [
                    'session_id' => (string) $request->input('session_id'),
                    'headers' => $request->headers->all(),
                ]);
            } else {
                logger()->warning('Chunk upload file details', [
                    'session_id' => (string) $request->input('session_id'),
                    'client_name' => $chunkFile->getClientOriginalName() ?? null,
                    'client_size' => $chunkFile->getSize() ?? null,
                    'error' => $chunkFile->getError(),
                    'realpath' => $chunkFile->getRealPath(),
                ]);
            }
        } catch (\Throwable $e) {
            logger()->warning('Chunk upload debug logging failed', ['error' => $e->getMessage()]);
        }

        try {
            // Use getPathname() — this returns the PHP temp filename even when
            // getRealPath() may be false (platform/stream wrappers can cause
            // getRealPath() to return false). getPathname() is safe for reading
            // the uploaded chunk file.
            $chunkPath = $chunkFile ? $chunkFile->getPathname() : '';

            $result = $chunks->appendChunk(
                (string) $request->input('session_id'),
                (string) $chunkPath,
                (int) $request->input('offset'),
                (int) $request->input('chunk_size'),
                (int) $request->input('total_size'),
                (int) $user->id,
                (string) $request->ip()
            );

            return response()->json([
                'session_id' => $result->sessionId,
                'status' => $result->status,
                'received_bytes' => $result->receivedBytes,
                'total_bytes' => $result->totalBytes,
                'progress' => $result->progress,
            ], Response::HTTP_OK);
        } catch (\Throwable $e) {
            logger()->warning('Upload chunk failed', [
                'session_id' => (string) $request->input('session_id'),
                'error' => $e->getMessage(),
            ]);

            // Include the underlying error message in the response during debugging
            // so the frontend can show a useful description. Remove or hide this
            // in production if you prefer more generic errors.
            return response()->json([
                'message' => 'Upload chunk failed.',
                'error' => $e->getMessage(),
            ], Response::HTTP_UNPROCESSABLE_ENTITY);
        }
    }

    public function status(string $id, UploadStatusRequest $request, UploadStatusService $statusService, UploadAuditService $audit)
    {
        $user = $request->user();
        $payload = $statusService->get($id);

        $audit->log($user->id, 'upload_status_checked', (string) $request->ip(), [
            'session_id' => $id,
            'status' => $payload['status'],
        ]);

        return response()->json([
            'session_id' => $payload['session_id'],
            'status' => $payload['status'],
            'progress' => $payload['progress'],
            'failure_reason' => $payload['failure_reason'],
            'received_bytes' => $payload['received_bytes'] ?? 0,
        ], Response::HTTP_OK);
    }

    public function cancel(UploadCancelRequest $request, UploadCancelService $cancel)
    {
        $user = $request->user();

        try {
            $result = $cancel->cancel(
                (string) $request->input('session_id'),
                (int) $user->id,
                (string) $request->ip()
            );

            return response()->json([
                'session_id' => $result['session_id'],
                'status' => $result['status'],
            ], Response::HTTP_OK);
        } catch (\Throwable $e) {
            logger()->warning('Upload cancel failed', [
                'session_id' => (string) $request->input('session_id'),
                'error' => $e->getMessage(),
            ]);

            return response()->json([
                'message' => 'Upload cancel failed.',
            ], Response::HTTP_INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * Preload an upload draft: validate main file, create draft and store files.
     *
     * Returns JSON: { upload_id, status, expires_at }
     */
    public function preload(Request $request, UploadDraftServiceInterface $draftService, ArchiveInspectorService $archiveInspector, DraftQuotaService $draftQuotaService)
    {
        $user = $request->user();

        $request->validate([
            'main' => ['required', 'file'],
            'screenshots' => ['sometimes', 'array'],
            'screenshots.*' => ['file', 'image', 'max:5120'],
        ]);

        $main = $request->file('main');

        // Detect type from mime
        $mime = (string) $main->getClientMimeType();
        $type = null;
        if (str_starts_with($mime, 'image/')) {
            $type = 'image';
        } elseif (in_array($mime, ['application/zip', 'application/x-zip-compressed', 'application/x-tar', 'application/x-gzip', 'application/x-rar-compressed', 'application/octet-stream'])) {
            $type = 'archive';
        }

        if ($type === null) {
            return response()->json([
                'message' => 'Invalid main file type.',
                'errors' => [
                    'main' => ['The main file must be an image or archive.'],
                ],
            ], Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        if ($type === 'archive') {
            $validator = Validator::make($request->all(), [
                'screenshots' => ['required', 'array', 'min:1'],
                'screenshots.*' => ['file', 'image', 'max:5120'],
            ]);

            if ($validator->fails()) {
                return response()->json([
                    'message' => 'The given data was invalid.',
                    'errors' => $validator->errors(),
                ], Response::HTTP_UNPROCESSABLE_ENTITY);
            }

            $inspection = $archiveInspector->inspect((string) $main->getPathname());
            if (! $inspection->valid) {
                return response()->json([
                    'message' => 'Archive inspection failed.',
                    'reason' => $inspection->reason,
                    'stats' => $inspection->stats,
                ], Response::HTTP_UNPROCESSABLE_ENTITY);
            }
        }

        $incomingFiles = [$main];
        if ($type === 'archive' && $request->hasFile('screenshots')) {
            foreach ($request->file('screenshots') as $screenshot) {
                $incomingFiles[] = $screenshot;
            }
        }

        $mainHash = $draftService->calculateHash((string) $main->getPathname());

        try {
            $warnings = $draftQuotaService->assertCanCreateDraft($user, [
                'files' => $incomingFiles,
                'main_hash' => $mainHash,
            ]);
        } catch (DraftQuotaException $e) {
            return response()->json([
                'message' => $e->machineCode(),
                'code' => $e->machineCode(),
            ], $e->httpStatus());
        }

        // Create draft record (meta-only) and store main file via service
        $draft = $draftService->createDraft(['user_id' => $user->id, 'type' => $type]);

        try {
            $mainInfo = $draftService->storeMainFile($draft['id'], $main);

            // If archive, allow optional screenshots to be uploaded in the same request
            if ($type === 'archive' && $request->hasFile('screenshots')) {
                foreach ($request->file('screenshots') as $ss) {
                    try {
                        $draftService->storeScreenshot($draft['id'], $ss);
                    } catch (Throwable $e) {
                        // Keep controller thin: log and continue
                        logger()->warning('Screenshot store failed during preload', ['error' => $e->getMessage(), 'draft' => $draft['id']]);
                    }
                }
            }

            // Set expiration (default 7 days) and return info
            $ttlDays = (int) config('uploads.draft_ttl_days', 7);
            $expiresAt = Carbon::now()->addDays($ttlDays);
            $draftService->setExpiration($draft['id'], $expiresAt);

            VirusScanJob::dispatch($draft['id']);

            $response = [
                'upload_id' => $draft['id'],
                'status' => 'draft',
                'expires_at' => $expiresAt->toISOString(),
            ];

            if (! empty($warnings)) {
                $response['warnings'] = array_values($warnings);
            }

            return response()->json($response, Response::HTTP_OK);
        } catch (Throwable $e) {
            logger()->error('Upload preload failed', ['error' => $e->getMessage()]);
            return response()->json(['message' => 'Preload failed.'], Response::HTTP_INTERNAL_SERVER_ERROR);
        }
    }

    public function autosave(string $id, Request $request)
    {
        $user = $request->user();

        $upload = DB::table('uploads')->where('id', $id)->first();
        if (! $upload) {
            return response()->json(['message' => 'Upload draft not found.'], Response::HTTP_NOT_FOUND);
        }

        if ((int) $upload->user_id !== (int) $user->id) {
            return response()->json(['message' => 'Forbidden.'], Response::HTTP_FORBIDDEN);
        }

        if ((string) $upload->status !== 'draft') {
            return response()->json([
                'message' => 'Only draft uploads can be autosaved.',
            ], Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $validated = $request->validate([
            'title' => ['nullable', 'string', 'max:255'],
            'category_id' => ['nullable', 'exists:categories,id'],
            'description' => ['nullable', 'string'],
            'tags' => ['nullable', 'array'],
            'license' => ['nullable', 'string'],
            'nsfw' => ['nullable', 'boolean'],
        ]);

        $updates = [];
        foreach (['title', 'category_id', 'description', 'tags', 'license', 'nsfw'] as $field) {
            if (array_key_exists($field, $validated)) {
                $updates[$field] = $validated[$field];
            }
        }

        $dirty = [];
        foreach ($updates as $field => $value) {
            $current = $upload->{$field} ?? null;

            if ($field === 'tags') {
                $current = $current ? json_decode((string) $current, true) : null;
            }

            if ($field === 'nsfw') {
                $current = is_null($current) ? null : (bool) $current;
                $value = is_null($value) ? null : (bool) $value;
            }

            if ($current !== $value) {
                $dirty[$field] = $value;
            }
        }

        if (array_key_exists('tags', $dirty)) {
            $dirty['tags'] = json_encode($dirty['tags']);
        }

        if (! empty($dirty)) {
            $dirty['updated_at'] = now();
            DB::table('uploads')->where('id', $id)->update($dirty);
            $upload = DB::table('uploads')->where('id', $id)->first();
        }

        return response()->json([
            'success' => true,
            'updated_at' => (string) ($upload->updated_at ?? now()->toDateTimeString()),
        ], Response::HTTP_OK);
    }

    public function processingStatus(string $id, Request $request)
    {
        $user = $request->user();

        $upload = DB::table('uploads')->where('id', $id)->first();
        if (! $upload) {
            return response()->json(['message' => 'Upload not found.'], Response::HTTP_NOT_FOUND);
        }

        if ((int) $upload->user_id !== (int) $user->id) {
            return response()->json(['message' => 'Forbidden.'], Response::HTTP_FORBIDDEN);
        }

        $status = (string) ($upload->status ?? 'draft');
        $isScanned = (bool) ($upload->is_scanned ?? false);
        $previewReady = ! empty($upload->preview_path);
        $hasTags = (bool) ($upload->has_tags ?? false);
        $processingState = (string) ($upload->processing_state ?? 'pending_scan');

        return response()->json([
            'id' => (string) $upload->id,
            'status' => $status,
            'is_scanned' => $isScanned,
            'preview_ready' => $previewReady,
            'has_tags' => $hasTags,
            'processing_state' => $processingState,
        ], Response::HTTP_OK);
    }

    public function publish(string $id, Request $request, PublishService $publishService, ArtworkAttributionService $attribution, ArtworkMaturityService $maturity, WorldSubmissionService $submissions)
    {
        $user = $request->user();

        $validated = $request->validate([
            'title'       => ['nullable', 'string', 'max:150'],
            'description' => ['nullable', 'string'],
            'category'    => ['nullable', 'integer', 'exists:categories,id'],
            'tags'        => ['nullable', 'array', 'max:' . (int) config('tags.max_user_tags', 30)],
            'tags.*'      => ['string', 'max:64'],
            'is_mature'   => ['nullable', 'boolean'],
            'nsfw'        => ['nullable', 'boolean'],
            // Scheduled-publishing fields
            'mode'        => ['nullable', 'string', 'in:now,schedule'],
            'publish_at'  => ['nullable', 'string', 'date'],
            'timezone'    => ['nullable', 'string', 'max:64'],
            'visibility'  => ['nullable', 'string', 'in:public,unlisted,private'],
            'group' => ['nullable', 'string', 'max:90'],
            'primary_author_user_id' => ['nullable', 'integer', 'min:1'],
            'contributor_user_ids' => ['nullable', 'array', 'max:20'],
            'contributor_user_ids.*' => ['integer', 'min:1'],
            'contributor_credits' => ['nullable', 'array', 'max:20'],
            'contributor_credits.*.user_id' => ['required', 'integer', 'min:1'],
            'contributor_credits.*.credit_role' => ['nullable', 'string', 'max:80'],
            'contributor_credits.*.is_primary' => ['nullable', 'boolean'],
            'world_submissions' => ['nullable', 'array', 'max:12'],
            'world_submissions.*.world_id' => ['required', 'integer', 'exists:worlds,id'],
            'world_submissions.*.note' => ['nullable', 'string', 'max:1000'],
        ]);

        $mode       = $validated['mode'] ?? 'now';
        $visibility = $validated['visibility'] ?? 'public';

        // Resolve the UTC publish_at datetime for schedule mode
        $publishAt = null;
        if ($mode === 'schedule' && ! empty($validated['publish_at'])) {
            try {
                $publishAt = \Carbon\Carbon::parse($validated['publish_at'])->utc();
                // Must be at least 1 minute in the future (server-side guard)
                if ($publishAt->lte(now()->addMinute())) {
                    return response()->json([
                        'message' => 'Scheduled publish time must be at least 1 minute in the future.',
                    ], Response::HTTP_UNPROCESSABLE_ENTITY);
                }
            } catch (\Throwable) {
                return response()->json(['message' => 'Invalid publish_at datetime.'], Response::HTTP_UNPROCESSABLE_ENTITY);
            }
        }

        if (ctype_digit($id)) {
            $artworkId = (int) $id;
            $artwork = Artwork::query()->find($artworkId);
            if (! $artwork) {
                return response()->json(['message' => 'Artwork not found.'], Response::HTTP_NOT_FOUND);
            }

            if ((int) $artwork->user_id !== (int) $user->id) {
                return response()->json(['message' => 'Forbidden.'], Response::HTTP_FORBIDDEN);
            }

            $title = trim((string) ($validated['title'] ?? $artwork->title ?? ''));
            if ($title === '') {
                $title = 'Untitled artwork';
            }

            $slugBase = Str::slug($title);
            if ($slugBase === '') {
                $slugBase = 'artwork';
            }

            $artwork->title = $title;
            if (array_key_exists('description', $validated)) {
                $artwork->description = $validated['description'];
            }
            if (array_key_exists('is_mature', $validated) || array_key_exists('nsfw', $validated)) {
                $artwork->is_mature = (bool) ($validated['is_mature'] ?? $validated['nsfw'] ?? false);
            }
            $artwork->slug             = Str::limit($slugBase, 160, '');
            $artwork->artwork_timezone = $validated['timezone'] ?? null;
            $artwork->uploaded_by_user_id = $artwork->uploaded_by_user_id ?: (int) $user->id;
            $artwork->primary_author_user_id = $artwork->primary_author_user_id ?: (int) $user->id;

            // Sync category if provided
            $categoryId = isset($validated['category']) ? (int) $validated['category'] : null;
            if ($categoryId && \App\Models\Category::where('id', $categoryId)->exists()) {
                $artwork->categories()->sync([$categoryId]);
            }

            // Sync tags if provided
            if (!empty($validated['tags']) && is_array($validated['tags'])) {
                $tagIds = [];
                foreach ($validated['tags'] as $tagSlug) {
                    $tag = \App\Models\Tag::firstOrCreate(
                        ['slug' => Str::slug($tagSlug)],
                        ['name' => $tagSlug, 'is_active' => true, 'usage_count' => 0]
                    );
                    $tagIds[$tag->id] = ['source' => 'user', 'confidence' => 1.0];
                }
                $artwork->tags()->sync($tagIds);
            }

            $artwork->save();
            $maturity->applyUploaderDeclaration($artwork, (bool) $artwork->is_mature);
            $artwork = $attribution->apply($artwork->fresh(['group.members']), $user, $validated);
            $submissions->syncForArtwork($artwork->fresh(), $user, (array) ($validated['world_submissions'] ?? []));

            if ($mode === 'schedule' && $publishAt) {
                // Scheduled: store publish_at but don't make public yet
                $artwork->visibility     = $visibility;
                $artwork->is_public      = false;
                $artwork->is_approved    = true;
                $artwork->publish_at     = $publishAt;
                $artwork->artwork_status = 'scheduled';
                $artwork->published_at   = null;
                $artwork->save();

            IndexArtworkJob::dispatch((int) $artwork->id);

                return response()->json([
                    'success'      => true,
                    'artwork_id'   => (int) $artwork->id,
                    'status'       => 'scheduled',
                    'slug'         => (string) $artwork->slug,
                    'publish_at'   => $publishAt->toISOString(),
                    'published_at' => null,
                ], Response::HTTP_OK);
            }

            // Publish immediately
            $artwork->visibility     = $visibility;
            $artwork->is_public      = ($visibility !== 'private');
            $artwork->is_approved    = true;
            $artwork->published_at   = now();
            $artwork->artwork_status = 'published';
            $artwork->publish_at     = null;
            $artwork->save();

            IndexArtworkJob::dispatch((int) $artwork->id);

            // Record upload activity event
            try {
                \App\Models\ActivityEvent::record(
                    actorId:    (int) $user->id,
                    type:       \App\Models\ActivityEvent::TYPE_UPLOAD,
                    targetType: \App\Models\ActivityEvent::TARGET_ARTWORK,
                    targetId:   (int) $artwork->id,
                );
            } catch (\Throwable) {}

            try {
                app(UserActivityService::class)->logUpload((int) $user->id, (int) $artwork->id);
            } catch (\Throwable) {}

            return response()->json([
                'success'      => true,
                'artwork_id'   => (int) $artwork->id,
                'status'       => 'published',
                'slug'         => (string) $artwork->slug,
                'published_at' => optional($artwork->published_at)->toISOString(),
            ], Response::HTTP_OK);
        }

        try {
            $upload = $publishService->publish($id, $user);

            return response()->json([
                'success'      => true,
                'upload_id'    => (string) $upload->id,
                'status'       => (string) $upload->status,
                'published_at' => optional($upload->published_at)->toISOString(),
                'final_path'   => (string) ($upload->final_path ?? ''),
            ], Response::HTTP_OK);
        } catch (UploadOwnershipException $e) {
            return response()->json(['message' => $e->getMessage()], Response::HTTP_FORBIDDEN);
        } catch (UploadNotFoundException $e) {
            return response()->json(['message' => $e->getMessage()], Response::HTTP_NOT_FOUND);
        } catch (UploadPublishValidationException $e) {
            return response()->json(['message' => $e->getMessage()], Response::HTTP_UNPROCESSABLE_ENTITY);
        }
    }

    public function submitForReview(string $id, Request $request, GroupArtworkReviewService $reviews, WorldSubmissionService $submissions)
    {
        $user = $request->user();

        $validated = $request->validate([
            'title' => ['nullable', 'string', 'max:150'],
            'description' => ['nullable', 'string'],
            'category' => ['nullable', 'integer', 'exists:categories,id'],
            'tags' => ['nullable', 'array', 'max:' . (int) config('tags.max_user_tags', 30)],
            'tags.*' => ['string', 'max:64'],
            'is_mature' => ['nullable', 'boolean'],
            'nsfw' => ['nullable', 'boolean'],
            'timezone' => ['nullable', 'string', 'max:64'],
            'visibility' => ['nullable', 'string', 'in:public,unlisted,private'],
            'group' => ['required', 'string', 'max:90'],
            'primary_author_user_id' => ['nullable', 'integer', 'min:1'],
            'contributor_user_ids' => ['nullable', 'array', 'max:20'],
            'contributor_user_ids.*' => ['integer', 'min:1'],
            'contributor_credits' => ['nullable', 'array', 'max:20'],
            'contributor_credits.*.user_id' => ['required', 'integer', 'min:1'],
            'contributor_credits.*.credit_role' => ['nullable', 'string', 'max:80'],
            'contributor_credits.*.is_primary' => ['nullable', 'boolean'],
            'world_submissions' => ['nullable', 'array', 'max:12'],
            'world_submissions.*.world_id' => ['required', 'integer', 'exists:worlds,id'],
            'world_submissions.*.note' => ['nullable', 'string', 'max:1000'],
        ]);

        if (! ctype_digit($id)) {
            return response()->json(['message' => 'Artwork review submission requires an artwork draft id.'], Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $artwork = Artwork::query()->find((int) $id);
        if (! $artwork) {
            return response()->json(['message' => 'Artwork not found.'], Response::HTTP_NOT_FOUND);
        }

        if ((int) $artwork->user_id !== (int) $user->id && (int) ($artwork->uploaded_by_user_id ?? 0) !== (int) $user->id) {
            return response()->json(['message' => 'Forbidden.'], Response::HTTP_FORBIDDEN);
        }

        $group = Group::query()->with('members')->where('slug', (string) $validated['group'])->first();
        if (! $group) {
            return response()->json(['message' => 'Group not found.'], Response::HTTP_NOT_FOUND);
        }

        $artwork = $reviews->submit($group, $artwork, $user, $validated);
        $submissions->syncForArtwork($artwork->fresh(), $user, (array) ($validated['world_submissions'] ?? []));

        return response()->json([
            'success' => true,
            'artwork_id' => (int) $artwork->id,
            'status' => 'submitted_for_review',
            'group_review_status' => (string) $artwork->group_review_status,
        ], Response::HTTP_OK);
    }
}
