'use client' import { useState, useRef, useCallback } from 'react' import { getErrorMessage } from '@/lib/guards' import type { FolderEntry } from '@/lib/client-types' import type { CheckUploadResult, DriveAction, FileDiff, GitModelMode, } from '@/lib/types' import { checkFolderDiffs, stageUpload, uploadDrive, uploadGit } from '@/lib/upload-api' type UploadLogDetails = Record function formatElapsed(startedAt: number) { return `${((performance.now() - startedAt) / 1000).toFixed(1)}s` } function logUpload(level: 'INFO' | 'ERROR', step: string, action: string, startedAt: number, details?: UploadLogDetails) { const log = level === 'ERROR' ? console.error : console.info log(`[${level}] ${step} -> ${action} | Timer: ${formatElapsed(startedAt)}`, details || '') } function startTimedLog(step: string, action: string, details?: UploadLogDetails) { const startedAt = performance.now() logUpload('INFO', step, `${action} started`, startedAt, details) const interval = window.setInterval(() => { logUpload('INFO', step, `${action} running`, startedAt, details) }, 10_000) return (status: 'done' | 'failed' | 'cancelled' = 'done', extra?: UploadLogDetails) => { window.clearInterval(interval) logUpload(status === 'failed' ? 'ERROR' : 'INFO', step, `${action} ${status}`, startedAt, { ...details, ...extra }) } } interface UseUploadOrchestratorParams { secret: string setSecretError: (err: string | null) => void entries: FolderEntry[] updateEntry: (index: number, patch: Partial) => void resetEntries: () => void } export function useUploadOrchestrator({ secret, setSecretError, entries, updateEntry, resetEntries, }: UseUploadOrchestratorParams) { const [isUploading, setIsUploading] = useState(false) const [isChecking, setIsChecking] = useState(false) const [isResolvingDriveError, setIsResolvingDriveError] = useState(false) const [globalError, setGlobalError] = useState(null) const [overwriteConfirm, setOverwriteConfirm] = useState<{ folderName: string diffs: FileDiff[] } | null>(null) const [noChangesFolder, setNoChangesFolder] = useState(null) const [driveError, setDriveError] = useState<{ error: string folderIndex: number } | null>(null) const abortRef = useRef(null) const checkResultRef = useRef({ exists: false, diffs: [] }) const uploadActionRef = useRef(false) const stagingIdRef = useRef(null) const secretRef = useRef(secret) secretRef.current = secret const entriesRef = useRef(entries) entriesRef.current = entries const pushGit = useCallback(async (index: number, signal?: AbortSignal) => { const stagingId = stagingIdRef.current if (!stagingId) { updateEntry(index, { status: 'error', error: 'Preparation serveur introuvable' }) return } const folderName = entriesRef.current[index]?.folderName const endGitLog = startTimedLog('Git', 'Upload', { folderName, stagingId }) let gitResult: Awaited> try { gitResult = await uploadGit( stagingId, secretRef.current, (pct) => updateEntry(index, { progress: 50 + Math.round(pct / 2) }), signal, ) endGitLog(gitResult.success ? 'done' : 'failed', { error: gitResult.error }) } catch (err) { endGitLog(signal?.aborted ? 'cancelled' : 'failed', { error: getErrorMessage(err), }) throw err } updateEntry(index, { status: gitResult.success ? 'success' : 'error', progress: gitResult.success ? 100 : 0, error: gitResult.success ? undefined : gitResult.error, uploadWarning: gitResult.success ? gitResult.warning : undefined, filename: gitResult.filename, }) }, [updateEntry]) const proceedUpload = useCallback(async () => { if (uploadActionRef.current) return uploadActionRef.current = true setOverwriteConfirm(null) setIsChecking(false) setIsUploading(true) setGlobalError(null) const controller = new AbortController() abortRef.current = controller try { const currentEntries = entriesRef.current for (let i = 0; i < currentEntries.length; i++) { if (currentEntries[i].status === 'success') continue if (controller.signal.aborted) break const folderEntry = currentEntries[i] const driveAction: DriveAction = checkResultRef.current.exists ? 'replace' : 'new' const stagingId = stagingIdRef.current if (!stagingId) { updateEntry(i, { status: 'error', error: 'Preparation serveur introuvable' }) return } updateEntry(i, { status: 'uploading', progress: 1, error: undefined, uploadWarning: undefined, driveStatus: 'uploading', driveError: undefined, }) const endDriveLog = startTimedLog('Drive', 'Upload', { folderName: folderEntry.folderName, stagingId, action: driveAction, }) let driveResult: Awaited> try { driveResult = await uploadDrive( stagingId, secretRef.current, driveAction, controller.signal, ) endDriveLog(driveResult.success ? 'done' : 'failed', { error: driveResult.error }) } catch (err) { endDriveLog(controller.signal.aborted ? 'cancelled' : 'failed', { error: getErrorMessage(err), }) throw err } if (!driveResult.success) { updateEntry(i, { driveStatus: 'error', driveError: driveResult.error }) setDriveError({ error: driveResult.error || 'Erreur inconnue', folderIndex: i }) return } updateEntry(i, { driveStatus: 'success', progress: 50 }) await pushGit(i, controller.signal) } } finally { abortRef.current = null setIsUploading(false) uploadActionRef.current = false } }, [updateEntry, pushGit]) const handleUpload = useCallback(async (gitModelMode: GitModelMode) => { if (uploadActionRef.current || isChecking || isUploading) return if (!secretRef.current.trim()) { setSecretError("La cle d'acces est requise") return } if (entriesRef.current.length === 0) return uploadActionRef.current = true setIsChecking(true) setSecretError(null) setGlobalError(null) const folder = entriesRef.current[0] const controller = new AbortController() abortRef.current = controller try { const endStageLog = startTimedLog('Verification', 'Staging', { folderName: folder.folderName, files: 1 + folder.textures.length, modelSize: folder.modelFile.size, gitModelMode, }) let staged: Awaited> try { staged = await stageUpload(folder, gitModelMode, secretRef.current, controller.signal) endStageLog('done', { stagingId: staged.stagingId, filesCount: staged.filesCount }) } catch (err) { endStageLog(controller.signal.aborted ? 'cancelled' : 'failed', { error: getErrorMessage(err), }) throw err } stagingIdRef.current = staged.stagingId const endCheckLog = startTimedLog('Verification', 'GitHub diff', { folderName: folder.folderName, stagingId: staged.stagingId, }) let check: CheckUploadResult try { check = await checkFolderDiffs( staged.stagingId, secretRef.current, controller.signal, ) endCheckLog('done', { exists: check.exists, diffs: check.diffs.length }) } catch (err) { endCheckLog(controller.signal.aborted ? 'cancelled' : 'failed', { error: getErrorMessage(err), }) throw err } checkResultRef.current = check updateEntry(0, { uploadWarning: check.warning }) if (check.exists) { if (check.diffs.length === 0) { setNoChangesFolder(folder.folderName) uploadActionRef.current = false setIsChecking(false) abortRef.current = null return } setOverwriteConfirm({ folderName: folder.folderName, diffs: check.diffs }) uploadActionRef.current = false setIsChecking(false) abortRef.current = null return } } catch (err) { const message = getErrorMessage(err) setGlobalError(message) uploadActionRef.current = false setIsChecking(false) abortRef.current = null return } uploadActionRef.current = false abortRef.current = null await proceedUpload() }, [setSecretError, proceedUpload, isChecking, isUploading]) const handleDriveContinue = useCallback(async () => { if (!driveError || uploadActionRef.current) return uploadActionRef.current = true setIsResolvingDriveError(true) const idx = driveError.folderIndex setDriveError(null) try { updateEntry(idx, { driveStatus: 'skipped' }) const signal = abortRef.current?.signal await pushGit(idx, signal) const currentEntries = entriesRef.current for (let i = idx + 1; i < currentEntries.length; i++) { if (currentEntries[i].status === 'success') continue if (abortRef.current?.signal.aborted) break updateEntry(i, { status: 'uploading', progress: 0, error: undefined, uploadWarning: undefined, driveStatus: 'skipped', }) await pushGit(i, abortRef.current?.signal) } } finally { abortRef.current = null setIsUploading(false) setIsResolvingDriveError(false) uploadActionRef.current = false } }, [driveError, updateEntry, pushGit]) const handleDriveCancel = useCallback(() => { if (!driveError) return const idx = driveError.folderIndex setDriveError(null) updateEntry(idx, { status: 'error', progress: 0, error: 'Upload annule (Drive echoue)', driveStatus: 'error', }) abortRef.current?.abort() abortRef.current = null uploadActionRef.current = false setIsChecking(false) setIsResolvingDriveError(false) setIsUploading(false) }, [driveError, updateEntry]) const handleCancel = useCallback(() => { if (isChecking) { abortRef.current?.abort() abortRef.current = null uploadActionRef.current = false setIsChecking(false) return } abortRef.current?.abort() abortRef.current = null uploadActionRef.current = false setIsResolvingDriveError(false) setIsUploading(false) stagingIdRef.current = null }, [isChecking]) const handleReset = useCallback(() => { resetEntries() setGlobalError(null) setIsChecking(false) setIsUploading(false) setIsResolvingDriveError(false) setDriveError(null) checkResultRef.current = { exists: false, diffs: [] } uploadActionRef.current = false stagingIdRef.current = null }, [resetEntries]) return { isUploading, isChecking, isResolvingDriveError, globalError, setGlobalError, overwriteConfirm, setOverwriteConfirm, noChangesFolder, setNoChangesFolder, driveError, handleUpload, handleOverwriteCancel: () => { setOverwriteConfirm(null) uploadActionRef.current = false setIsChecking(false) }, handleDriveContinue, handleDriveCancel, handleCancel, handleReset, proceedUpload, } }