import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import UploadDropzone from './UploadDropzone'
import ScreenshotUploader from './ScreenshotUploader'
import UploadSidebar from './UploadSidebar'
import UploadProgress from './UploadProgress'
import UploadActions from './UploadActions'
import UploadStepper from './UploadStepper'
import { emitUploadEvent } from '../../lib/uploadAnalytics'
import * as uploadEndpoints from '../../lib/uploadEndpoints'
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'

const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp'])
const IMAGE_MIME = new Set(['image/jpeg', 'image/png', 'image/webp'])
const ARCHIVE_EXTENSIONS = new Set(['zip', 'rar', '7z', 'tar', 'gz'])
const ARCHIVE_MIME = new Set([
  'application/zip',
  'application/x-zip-compressed',
  'application/x-rar-compressed',
  'application/vnd.rar',
  'application/x-7z-compressed',
  'application/x-tar',
  'application/gzip',
  'application/x-gzip',
  'application/octet-stream',
])

const PRIMARY_IMAGE_MAX_BYTES = 50 * 1024 * 1024
const PRIMARY_ARCHIVE_MAX_BYTES = 200 * 1024 * 1024
const SCREENSHOT_MAX_BYTES = 10 * 1024 * 1024
const DEFAULT_CHUNK_SIZE_BYTES = 5 * 1024 * 1024
const POLL_INTERVAL_MS = 2000

const wizardSteps = [
  { key: 'upload', label: 'Upload' },
  { key: 'details', label: 'Details' },
  { key: 'publish', label: 'Publish' },
]

function getExtension(fileName = '') {
  const parts = String(fileName).toLowerCase().split('.')
  return parts.length > 1 ? parts.pop() : ''
}

function detectFileType(file) {
  if (!file) return 'unknown'
  const extension = getExtension(file.name)
  const mime = String(file.type || '').toLowerCase()

  if (IMAGE_MIME.has(mime) || IMAGE_EXTENSIONS.has(extension)) return 'image'
  if (ARCHIVE_MIME.has(mime) || ARCHIVE_EXTENSIONS.has(extension)) return 'archive'
  return 'unsupported'
}

function formatBytes(bytes) {
  if (!Number.isFinite(bytes) || bytes <= 0) return '—'
  if (bytes < 1024) return `${bytes} B`
  const kb = bytes / 1024
  if (kb < 1024) return `${kb.toFixed(1)} KB`
  const mb = kb / 1024
  if (mb < 1024) return `${mb.toFixed(1)} MB`
  return `${(mb / 1024).toFixed(2)} GB`
}

function readImageDimensions(file) {
  return new Promise((resolve, reject) => {
    const blobUrl = URL.createObjectURL(file)
    const image = new Image()

    image.onload = () => {
      resolve({ width: image.naturalWidth, height: image.naturalHeight })
      URL.revokeObjectURL(blobUrl)
    }

    image.onerror = () => {
      reject(new Error('image_read_failed'))
      URL.revokeObjectURL(blobUrl)
    }

    image.src = blobUrl
  })
}

function buildCategoryTree(contentTypes = []) {
  const rootsById = new Map()

  contentTypes.forEach((type) => {
    const categories = Array.isArray(type?.categories) ? type.categories : []
    categories.forEach((category) => {
      if (!category?.id) return
      if (!rootsById.has(String(category.id))) {
        rootsById.set(String(category.id), {
          id: String(category.id),
          name: category.name || `Category ${category.id}`,
          children: [],
        })
      }

      const root = rootsById.get(String(category.id))
      const children = Array.isArray(category?.children) ? category.children : []
      children.forEach((child) => {
        if (!child?.id) return
        const exists = root.children.some((item) => String(item.id) === String(child.id))
        if (!exists) {
          root.children.push({ id: String(child.id), name: child.name || `Subcategory ${child.id}` })
        }
      })
    })
  })

  return Array.from(rootsById.values())
}

function getContentTypeValue(type) {
  if (!type) return ''
  return String(type.id ?? type.key ?? type.slug ?? type.name ?? '')
}

function getContentTypeVisualKey(type) {
  const raw = String(type?.slug || type?.name || type?.key || '').toLowerCase()
  const normalized = raw.replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '')
  if (normalized.includes('wallpaper')) return 'wallpapers'
  if (normalized.includes('skin')) return 'skins'
  if (normalized.includes('photo')) return 'photography'
  return 'other'
}

export async function validatePrimaryFile(file) {
  const errors = []
  const warnings = []
  const type = detectFileType(file)

  if (!file) {
    return {
      type: 'unknown',
      errors: [],
      warnings,
      metadata: { resolution: '—', size: '—', type: '—' },
      previewUrl: '',
    }
  }

  const metadata = {
    resolution: '—',
    size: formatBytes(file.size),
    type: file.type || getExtension(file.name) || 'unknown',
  }

  if (type === 'unsupported') {
    errors.push('Unsupported file type. Use image (jpg/jpeg/png/webp) or archive (zip/rar/7z/tar/gz).')
  }

  if (type === 'image') {
    if (file.size > PRIMARY_IMAGE_MAX_BYTES) {
      errors.push('Image exceeds 50MB maximum size.')
    }

    try {
      const dimensions = await readImageDimensions(file)
      metadata.resolution = `${dimensions.width} × ${dimensions.height}`
      if (dimensions.width < 800 || dimensions.height < 600) {
        errors.push('Image resolution must be at least 800×600.')
      }
    } catch {
      errors.push('Unable to read image resolution.')
    }
  }

  if (type === 'archive') {
    metadata.resolution = 'n/a'
    if (file.size > PRIMARY_ARCHIVE_MAX_BYTES) {
      errors.push('Archive exceeds 200MB maximum size.')
    }
    warnings.push('Archive upload requires at least one valid screenshot.')
  }

  const previewUrl = type === 'image' ? URL.createObjectURL(file) : ''

  return {
    type,
    errors,
    warnings,
    metadata,
    previewUrl,
  }
}

export async function validateScreenshots(files, isArchive) {
  if (!isArchive) {
    return { errors: [], perFileErrors: [] }
  }

  const errors = []
  const perFileErrors = Array.from({ length: files.length }, () => '')

  if (files.length < 1) {
    errors.push('At least one screenshot is required for archives.')
  }

  if (files.length > 5) {
    errors.push('Maximum 5 screenshots are allowed.')
  }

  await Promise.all(files.map(async (file, index) => {
    const typeErrors = []
    const extension = getExtension(file.name)
    const mime = String(file.type || '').toLowerCase()

    if (!IMAGE_MIME.has(mime) && !IMAGE_EXTENSIONS.has(extension)) {
      typeErrors.push('Must be JPG, PNG, or WEBP.')
    }

    if (file.size > SCREENSHOT_MAX_BYTES) {
      typeErrors.push('Must be 10MB or less.')
    }

    if (typeErrors.length === 0) {
      try {
        const dimensions = await readImageDimensions(file)
        if (dimensions.width < 1280 || dimensions.height < 720) {
          typeErrors.push('Minimum resolution is 1280×720.')
        }
      } catch {
        typeErrors.push('Could not read screenshot resolution.')
      }
    }

    if (typeErrors.length > 0) {
      perFileErrors[index] = typeErrors.join(' ')
    }
  }))

  if (perFileErrors.some(Boolean)) {
    errors.push('One or more screenshots are invalid.')
  }

  return { errors, perFileErrors }
}

const machineStates = {
  idle: 'idle',
  initializing: 'initializing',
  uploading: 'uploading',
  finishing: 'finishing',
  processing: 'processing',
  ready_to_publish: 'ready_to_publish',
  publishing: 'publishing',
  complete: 'complete',
  error: 'error',
  cancelled: 'cancelled',
}

const initialMachineState = {
  state: machineStates.idle,
  progress: 0,
  sessionId: null,
  uploadToken: null,
  processingStatus: null,
  isCancelling: false,
  error: '',
  lastAction: null,
}

function machineReducer(state, action) {
  switch (action.type) {
    case 'INIT_START':
      return { ...state, state: machineStates.initializing, progress: 0, error: '', isCancelling: false, lastAction: 'start' }
    case 'INIT_SUCCESS':
      return { ...state, sessionId: action.sessionId, uploadToken: action.uploadToken, error: '' }
    case 'UPLOAD_START':
      return { ...state, state: machineStates.uploading, progress: 1, error: '' }
    case 'UPLOAD_PROGRESS':
      return { ...state, progress: Math.max(1, Math.min(95, action.progress)), error: '' }
    case 'FINISH_START':
      return { ...state, state: machineStates.finishing, progress: Math.max(state.progress, 96), error: '' }
    case 'FINISH_SUCCESS':
      return { ...state, state: machineStates.processing, progress: 100, processingStatus: action.processingStatus ?? 'processing', error: '' }
    case 'PROCESSING_STATUS':
      return { ...state, processingStatus: action.processingStatus ?? state.processingStatus, error: '' }
    case 'READY_TO_PUBLISH':
      return { ...state, state: machineStates.ready_to_publish, processingStatus: 'ready', error: '' }
    case 'PUBLISH_START':
      return { ...state, state: machineStates.publishing, error: '', lastAction: 'publish' }
    case 'PUBLISH_SUCCESS':
      return { ...state, state: machineStates.complete, error: '' }
    case 'CANCEL_START':
      return { ...state, isCancelling: true, error: '', lastAction: 'cancel' }
    case 'CANCELLED':
      return { ...state, state: machineStates.cancelled, isCancelling: false, error: '' }
    case 'ERROR':
      return { ...state, state: machineStates.error, isCancelling: false, error: action.error || 'Upload failed.' }
    case 'RESET_MACHINE':
      return { ...initialMachineState }
    default:
      return state
  }
}

function toPercent(loaded, total) {
  if (!Number.isFinite(total) || total <= 0) return 0
  return Math.max(0, Math.min(100, Math.round((loaded / total) * 100)))
}

function getProcessingValue(payload) {
  const direct = String(payload?.processing_state || payload?.status || '').toLowerCase()
  if (direct) return direct
  return 'processing'
}

function isReadyToPublishStatus(status) {
  const normalized = String(status || '').toLowerCase()
  return ['ready', 'processed', 'publish_ready', 'published', 'complete'].includes(normalized)
}

function getProcessingTransparencyLabel(processingStatus, machineState) {
  if (!['processing', 'finishing', 'publishing'].includes(machineState)) return ''

  const normalized = String(processingStatus || '').toLowerCase()
  if (normalized === 'generating_preview') return 'Generating preview'

  if (['processed', 'ready', 'published', 'queued', 'publish_ready'].includes(normalized)) {
    return 'Preparing for publish'
  }

  return 'Analyzing content'
}

export default function UploadWizard({
  onValidationStateChange,
  initialDraftId = null,
  chunkSize = DEFAULT_CHUNK_SIZE_BYTES,
  contentTypes = [],
  suggestedTags = [],
}) {
  const [activeStep, setActiveStep] = useState(1)
  const [showRestoredBanner, setShowRestoredBanner] = useState(Boolean(initialDraftId))

  const [primaryFile, setPrimaryFile] = useState(null)
  const [primaryPreviewUrl, setPrimaryPreviewUrl] = useState('')
  const [primaryType, setPrimaryType] = useState('unknown')
  const [primaryWarnings, setPrimaryWarnings] = useState([])
  const [primaryErrors, setPrimaryErrors] = useState([])
  const [fileMetadata, setFileMetadata] = useState({ resolution: '—', size: '—', type: '—' })

  const [screenshots, setScreenshots] = useState([])
  const [screenshotErrors, setScreenshotErrors] = useState([])
  const [screenshotPerFileErrors, setScreenshotPerFileErrors] = useState([])

  const [metadata, setMetadata] = useState({
    title: '',
    rootCategoryId: '',
    subCategoryId: '',
    tags: [],
    description: '',
    rightsAccepted: false,
    contentType: '',
  })
  const [visionSuggestedTags, setVisionSuggestedTags] = useState([])
  const [visionDebug, setVisionDebug] = useState({
    enabled: null,
    queueConnection: '',
    queuedJobs: 0,
    failedJobs: 0,
    triggered: false,
    aiTagCount: 0,
    totalTagCount: 0,
    lastError: '',
  })
  const [isUploadLocked, setIsUploadLocked] = useState(false)
  const [resolvedArtworkId, setResolvedArtworkId] = useState(() => {
    const parsed = Number(initialDraftId)
    return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null
  })

  const [machine, dispatchMachine] = useReducer(machineReducer, initialMachineState)
  const prefersReducedMotion = useReducedMotion()
  const stepContentRef = useRef(null)
  const stepHeadingRef = useRef(null)

  const primaryValidationRunRef = useRef(0)
  const screenshotValidationRunRef = useRef(0)
  const pollingTimerRef = useRef(null)
  const requestControllersRef = useRef(new Set())
  const publishLockRef = useRef(false)
  const hasAutoAdvancedRef = useRef(false)
  const lastVisionFetchAtRef = useRef(0)

  const effectiveChunkSize = useMemo(() => {
    const parsed = Number(chunkSize)
    if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_CHUNK_SIZE_BYTES
    return Math.floor(parsed)
  }, [chunkSize])

  const quickTransition = prefersReducedMotion ? { duration: 0 } : { duration: 0.2, ease: 'easeOut' }
  const categoryTreeByType = useMemo(() => {
    const next = {}
    const list = Array.isArray(contentTypes) ? contentTypes : []
    list.forEach((type) => {
      const value = getContentTypeValue(type)
      if (!value) return
      next[value] = buildCategoryTree([type])
    })
    return next
  }, [contentTypes])
  const filteredCategoryTree = useMemo(() => {
    const selected = String(metadata.contentType || '')
    if (!selected) return []
    return categoryTreeByType[selected] || []
  }, [categoryTreeByType, metadata.contentType])
  const selectedRootCategory = useMemo(() => {
    return filteredCategoryTree.find((root) => String(root.id) === String(metadata.rootCategoryId || '')) || null
  }, [filteredCategoryTree, metadata.rootCategoryId])
  const requiresSubCategory = Boolean(selectedRootCategory && Array.isArray(selectedRootCategory.children) && selectedRootCategory.children.length > 0)
  const stepProgressPercent = useMemo(() => {
    if (activeStep === 1) return 33
    if (activeStep === 2) return 66
    return 100
  }, [activeStep])

  const isArchive = primaryType === 'archive'
  const processingTransparencyLabel = useMemo(() => getProcessingTransparencyLabel(machine.processingStatus, machine.state), [machine.processingStatus, machine.state])

  const uploadReady = useMemo(() => {
    return machine.state === machineStates.ready_to_publish || machine.processingStatus === 'ready' || machine.state === machineStates.complete
  }, [machine.state, machine.processingStatus])

  const metadataErrors = useMemo(() => {
    const next = {}
    if (!String(metadata.title || '').trim()) next.title = 'Title is required.'
    if (!metadata.contentType) next.contentType = 'Content type is required.'
    if (!metadata.rootCategoryId) next.category = 'Root category is required.'
    if (metadata.rootCategoryId && requiresSubCategory && !metadata.subCategoryId) next.category = 'Subcategory is required for the selected root category.'
    if (!metadata.rightsAccepted) next.rights = 'Rights confirmation is required to continue.'
    return next
  }, [metadata, requiresSubCategory])

  const detailsValid = Object.keys(metadataErrors).length === 0
  const mergedSuggestedTags = useMemo(() => {
    const normalized = new Map()
    const addTag = (item) => {
      if (!item) return
      const key = String(item?.slug || item?.tag || item?.name || item).trim().toLowerCase()
      if (!key) return
      if (!normalized.has(key)) {
        if (typeof item === 'string') {
          normalized.set(key, item)
        } else {
          normalized.set(key, {
            id: item.id ?? key,
            name: item.name || item.tag || item.slug || key,
            slug: item.slug || item.tag || key,
            usage_count: Number(item.usage_count || 0),
            is_ai: Boolean(item.is_ai || item.source === 'ai'),
            source: item.source || (item.is_ai ? 'ai' : 'manual'),
          })
        }
      }
    }

    ;(Array.isArray(suggestedTags) ? suggestedTags : []).forEach(addTag)
    ;(Array.isArray(visionSuggestedTags) ? visionSuggestedTags : []).forEach(addTag)
    return Array.from(normalized.values())
  }, [suggestedTags, visionSuggestedTags])
  const highestUnlockedStep = uploadReady ? (detailsValid ? 3 : 2) : 1
  const showProgress = machine.state !== machineStates.idle && machine.state !== machineStates.cancelled
  const canStartUpload = isValidForUpload(primaryFile, primaryErrors, isArchive, screenshotErrors)

  const canPublish = useMemo(() => {
    return uploadReady && metadata.rightsAccepted && machine.state !== machineStates.publishing
  }, [uploadReady, metadata.rightsAccepted, machine.state])

  const fileSelectionLocked = isUploadLocked

  useEffect(() => {
    if (uploadReady && activeStep === 1 && !hasAutoAdvancedRef.current) {
      hasAutoAdvancedRef.current = true
      setIsUploadLocked(true)
      setActiveStep(2)
    }
  }, [uploadReady, activeStep])

  useEffect(() => {
    if (uploadReady) {
      setIsUploadLocked(true)
    }
  }, [uploadReady])

  useEffect(() => {
    if (!stepContentRef.current) return
    if (typeof stepContentRef.current.scrollIntoView === 'function') {
      stepContentRef.current.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth', block: 'start' })
    }

    window.setTimeout(() => {
      if (stepHeadingRef.current && typeof stepHeadingRef.current.focus === 'function') {
        stepHeadingRef.current.focus({ preventScroll: true })
      }
    }, 0)
  }, [activeStep, prefersReducedMotion])

  useEffect(() => {
    return () => {
      if (primaryPreviewUrl) URL.revokeObjectURL(primaryPreviewUrl)
      requestControllersRef.current.forEach((controller) => controller.abort())
      requestControllersRef.current.clear()
      if (pollingTimerRef.current) {
        window.clearInterval(pollingTimerRef.current)
        pollingTimerRef.current = null
      }
    }
  }, [primaryPreviewUrl])

  const registerController = useCallback(() => {
    const controller = new AbortController()
    requestControllersRef.current.add(controller)
    return controller
  }, [])

  const unregisterController = useCallback((controller) => {
    if (!controller) return
    requestControllersRef.current.delete(controller)
  }, [])

  const abortAllRequests = useCallback(() => {
    requestControllersRef.current.forEach((controller) => controller.abort())
    requestControllersRef.current.clear()
  }, [])

  const clearPolling = useCallback(() => {
    if (pollingTimerRef.current) {
      window.clearInterval(pollingTimerRef.current)
      pollingTimerRef.current = null
    }
  }, [])

  useEffect(() => {
    let cancelled = false
    primaryValidationRunRef.current += 1
    const runId = primaryValidationRunRef.current

    ;(async () => {
      const result = await validatePrimaryFile(primaryFile)
      if (cancelled || runId !== primaryValidationRunRef.current) return

      setPrimaryType(result.type)
      setPrimaryWarnings(result.warnings)
      setPrimaryErrors(result.errors)
      setFileMetadata(result.metadata)

      setPrimaryPreviewUrl((current) => {
        if (current) URL.revokeObjectURL(current)
        return result.previewUrl
      })
    })()

    return () => {
      cancelled = true
    }
  }, [primaryFile])

  useEffect(() => {
    let cancelled = false
    screenshotValidationRunRef.current += 1
    const runId = screenshotValidationRunRef.current

    ;(async () => {
      const result = await validateScreenshots(screenshots, isArchive)
      if (cancelled || runId !== screenshotValidationRunRef.current) return
      setScreenshotErrors(result.errors)
      setScreenshotPerFileErrors(result.perFileErrors)
    })()

    return () => {
      cancelled = true
    }
  }, [screenshots, isArchive])

  useEffect(() => {
    if (!isArchive) {
      setScreenshots([])
      setScreenshotErrors([])
      setScreenshotPerFileErrors([])
    }
  }, [isArchive])

  const validationErrors = useMemo(() => [...primaryErrors, ...screenshotErrors], [primaryErrors, screenshotErrors])

  useEffect(() => {
    if (typeof onValidationStateChange === 'function') {
      onValidationStateChange({
        isValid: canStartUpload,
        validationErrors,
        isArchive,
      })
    }
  }, [canStartUpload, validationErrors, isArchive, onValidationStateChange])

  const fetchProcessingStatus = useCallback(async (sessionId, uploadToken, signal) => {
    const response = await window.axios.get(uploadEndpoints.status(sessionId), {
      signal,
      headers: uploadToken ? { 'X-Upload-Token': uploadToken } : undefined,
      params: uploadToken ? { upload_token: uploadToken } : undefined,
    })
    return response.data || {}
  }, [])

  const runUploadFlow = useCallback(async () => {
    if (!primaryFile || !canStartUpload) return

    clearPolling()

    dispatchMachine({ type: 'INIT_START' })
    emitUploadEvent('upload_start', {
      file_name: primaryFile.name,
      file_size: primaryFile.size,
      file_type: primaryType,
      is_archive: isArchive,
    })

    try {
      let artworkIdForUpload = resolvedArtworkId
      if (!artworkIdForUpload) {
        const derivedTitle = String(metadata.title || '').trim() || String(primaryFile.name || '').replace(/\.[^.]+$/, '') || 'Untitled upload'
        const draftResponse = await window.axios.post('/api/artworks', {
          title: derivedTitle,
          description: String(metadata.description || '').trim() || null,
          category: metadata.subCategoryId || metadata.rootCategoryId || null,
          tags: Array.isArray(metadata.tags) ? metadata.tags.join(', ') : '',
          license: Boolean(metadata.rightsAccepted),
        })

        const draftIdCandidate = Number(draftResponse?.data?.artwork_id ?? draftResponse?.data?.id)
        if (!Number.isFinite(draftIdCandidate) || draftIdCandidate <= 0) {
          throw new Error('Unable to create upload draft before finishing upload.')
        }

        artworkIdForUpload = Math.floor(draftIdCandidate)
        setResolvedArtworkId(artworkIdForUpload)
      }

      const initController = registerController()
      const initResponse = await window.axios.post(uploadEndpoints.init(), { client: 'web' }, { signal: initController.signal })
      unregisterController(initController)

      const sessionId = initResponse?.data?.session_id
      const uploadToken = initResponse?.data?.upload_token
      if (!sessionId || !uploadToken) {
        throw new Error('Upload session initialization returned an invalid payload.')
      }

      dispatchMachine({ type: 'INIT_SUCCESS', sessionId, uploadToken })
      dispatchMachine({ type: 'UPLOAD_START' })

      let uploaded = 0
      const totalSize = primaryFile.size

      while (uploaded < totalSize) {
        const nextOffset = Math.min(uploaded + effectiveChunkSize, totalSize)
        const blob = primaryFile.slice(uploaded, nextOffset)

        const payload = new FormData()
        payload.append('session_id', sessionId)
        payload.append('offset', String(uploaded))
        payload.append('chunk_size', String(blob.size))
        payload.append('total_size', String(totalSize))
        payload.append('upload_token', uploadToken)
        payload.append('chunk', blob)

        const chunkController = registerController()
        const chunkResponse = await window.axios.post(uploadEndpoints.chunk(), payload, {
          signal: chunkController.signal,
          headers: { 'X-Upload-Token': uploadToken },
        })
        unregisterController(chunkController)

        const receivedBytes = Number(chunkResponse?.data?.received_bytes ?? nextOffset)
        uploaded = Math.max(nextOffset, Number.isFinite(receivedBytes) ? receivedBytes : nextOffset)
        const progress = chunkResponse?.data?.progress ?? toPercent(uploaded, totalSize)
        dispatchMachine({ type: 'UPLOAD_PROGRESS', progress })
      }

      dispatchMachine({ type: 'FINISH_START' })

      const finishController = registerController()
      const finishResponse = await window.axios.post(uploadEndpoints.finish(), {
        session_id: sessionId,
        upload_token: uploadToken,
        artwork_id: artworkIdForUpload,
      }, {
        signal: finishController.signal,
        headers: { 'X-Upload-Token': uploadToken },
      })
      unregisterController(finishController)

      const finishStatus = getProcessingValue(finishResponse?.data || {})
      dispatchMachine({ type: 'FINISH_SUCCESS', processingStatus: finishStatus })
      if (isReadyToPublishStatus(finishStatus)) {
        dispatchMachine({ type: 'READY_TO_PUBLISH' })
      }

      emitUploadEvent('upload_complete', {
        session_id: sessionId,
        artwork_id: artworkIdForUpload,
      })
    } catch (error) {
      if (error?.name === 'CanceledError' || error?.code === 'ERR_CANCELED') return

      const message = error?.response?.data?.message || error?.message || 'Upload failed.'
      dispatchMachine({ type: 'ERROR', error: message })
      emitUploadEvent('upload_error', { stage: 'upload_flow', message })
    }
  }, [primaryFile, canStartUpload, primaryType, isArchive, resolvedArtworkId, metadata, registerController, unregisterController, clearPolling, effectiveChunkSize])

  const pollProcessing = useCallback(async () => {
    if (!machine.sessionId) return

    try {
      const statusController = registerController()
      const payload = await fetchProcessingStatus(machine.sessionId, machine.uploadToken, statusController.signal)
      unregisterController(statusController)

      const processingValue = getProcessingValue(payload)
      dispatchMachine({ type: 'PROCESSING_STATUS', processingStatus: processingValue })

      if (isReadyToPublishStatus(processingValue)) {
        dispatchMachine({ type: 'READY_TO_PUBLISH' })
      } else if (processingValue === 'rejected' || processingValue === 'error' || payload?.failure_reason) {
        const failureMessage = payload?.failure_reason || payload?.message || `Processing ended with status: ${processingValue}`
        dispatchMachine({ type: 'ERROR', error: failureMessage })
      }
    } catch (error) {
      if (error?.name === 'CanceledError' || error?.code === 'ERR_CANCELED') return
      const message = error?.response?.data?.message || 'Processing status check failed.'
      dispatchMachine({ type: 'ERROR', error: message })
      emitUploadEvent('upload_error', { stage: 'processing_poll', message })
    }
  }, [machine.sessionId, machine.uploadToken, fetchProcessingStatus, registerController, unregisterController])

  const fetchVisionSuggestedTags = useCallback(async () => {
    if (!resolvedArtworkId || resolvedArtworkId <= 0) return

    const now = Date.now()
    if (now - lastVisionFetchAtRef.current < 3000) return
    lastVisionFetchAtRef.current = now

    try {
      const response = await window.axios.get(`/api/artworks/${resolvedArtworkId}/tags`, {
        params: { trigger: 1 },
      })
      const payload = response?.data || {}
      if (payload?.vision_enabled === false) {
        setVisionSuggestedTags([])
        setVisionDebug((current) => ({
          ...current,
          enabled: false,
          lastError: '',
        }))
        return
      }

      const aiTags = Array.isArray(payload?.ai_tags) ? payload.ai_tags : []
      setVisionSuggestedTags(aiTags)
      const debug = payload?.debug || {}
      setVisionDebug({
        enabled: Boolean(payload?.vision_enabled),
        queueConnection: String(debug?.queue_connection || ''),
        queuedJobs: Number(debug?.queued_jobs || 0),
        failedJobs: Number(debug?.failed_jobs || 0),
        triggered: Boolean(debug?.triggered),
        aiTagCount: Number(debug?.ai_tag_count || aiTags.length || 0),
        totalTagCount: Number(debug?.total_tag_count || 0),
        lastError: '',
      })
      if (typeof window !== 'undefined') {
        window.console?.debug?.('[upload][vision-tags]', {
          artworkId: resolvedArtworkId,
          aiTags: aiTags.map((tag) => tag?.slug || tag?.name || tag),
          debug,
        })
      }
    } catch (error) {
      if (error?.response?.status === 404 || error?.response?.status === 403) return
      setVisionDebug((current) => ({
        ...current,
        lastError: error?.response?.data?.message || error?.message || 'Vision tag fetch failed.',
      }))
    }
  }, [resolvedArtworkId])

  useEffect(() => {
    if (!resolvedArtworkId || activeStep < 2) return

    fetchVisionSuggestedTags()
    const timer = window.setInterval(() => {
      fetchVisionSuggestedTags()
    }, 4000)

    return () => window.clearInterval(timer)
  }, [resolvedArtworkId, activeStep, fetchVisionSuggestedTags])

  useEffect(() => {
    if (machine.state !== machineStates.processing) {
      clearPolling()
      return
    }

    pollProcessing()
    clearPolling()
    pollingTimerRef.current = window.setInterval(() => {
      pollProcessing()
    }, POLL_INTERVAL_MS)

    return () => {
      clearPolling()
    }
  }, [machine.state, pollProcessing, clearPolling])

  const handleCancel = useCallback(async () => {
    dispatchMachine({ type: 'CANCEL_START' })
    clearPolling()
    abortAllRequests()

    try {
      if (machine.sessionId) {
        await window.axios.post(uploadEndpoints.cancel(), {
          session_id: machine.sessionId,
          upload_token: machine.uploadToken,
        }, {
          headers: machine.uploadToken ? { 'X-Upload-Token': machine.uploadToken } : undefined,
        })
      }

      dispatchMachine({ type: 'CANCELLED' })
      emitUploadEvent('upload_cancel', { session_id: machine.sessionId || null })
    } catch (error) {
      const message = error?.response?.data?.message || 'Cancel failed.'
      dispatchMachine({ type: 'ERROR', error: message })
      emitUploadEvent('upload_error', { stage: 'cancel', message })
    }
  }, [machine.sessionId, machine.uploadToken, abortAllRequests, clearPolling])

  const handlePublish = useCallback(async () => {
    if (!canPublish || publishLockRef.current) return

    publishLockRef.current = true
    dispatchMachine({ type: 'PUBLISH_START' })

    try {
      const publishTargetId = resolvedArtworkId || initialDraftId || machine.sessionId
      const publishPayload = {
        title: String(metadata.title || '').trim() || undefined,
        description: String(metadata.description || '').trim() || null,
      }

      if (!machine.sessionId) {
        if (!publishTargetId) throw new Error('Missing publish id.')
        const publishController = registerController()
        await window.axios.post(uploadEndpoints.publish(publishTargetId), publishPayload, { signal: publishController.signal })
        unregisterController(publishController)
      } else {
        if (!publishTargetId) throw new Error('Missing publish id.')
        const publishController = registerController()
        await window.axios.post(uploadEndpoints.publish(publishTargetId), publishPayload, { signal: publishController.signal })
        unregisterController(publishController)
      }

      dispatchMachine({ type: 'PUBLISH_SUCCESS' })
      emitUploadEvent('upload_publish', { id: publishTargetId })
    } catch (error) {
      if (error?.name === 'CanceledError' || error?.code === 'ERR_CANCELED') return
      const message = error?.response?.data?.message || error?.message || 'Publish failed.'
      dispatchMachine({ type: 'ERROR', error: message })
      emitUploadEvent('upload_error', { stage: 'publish', message })
    } finally {
      publishLockRef.current = false
    }
  }, [canPublish, machine.sessionId, initialDraftId, resolvedArtworkId, metadata.title, metadata.description, registerController, unregisterController])

  const handleReset = useCallback(() => {
    clearPolling()
    abortAllRequests()
    setPrimaryFile(null)
    setScreenshots([])
    setMetadata({
      title: '',
      rootCategoryId: '',
      subCategoryId: '',
      tags: [],
      description: '',
      rightsAccepted: false,
      contentType: '',
    })
    setVisionSuggestedTags([])
    setResolvedArtworkId(() => {
      const parsed = Number(initialDraftId)
      return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null
    })
    setIsUploadLocked(false)
    hasAutoAdvancedRef.current = false
    setActiveStep(1)
    dispatchMachine({ type: 'RESET_MACHINE' })
  }, [abortAllRequests, clearPolling])

  const handleRetry = useCallback(() => {
    clearPolling()
    abortAllRequests()
    if (machine.lastAction === 'publish') {
      handlePublish()
      return
    }
    runUploadFlow()
  }, [machine.lastAction, handlePublish, runUploadFlow, clearPolling, abortAllRequests])

  const goToStep = useCallback((step) => {
    if (step < 1 || step > highestUnlockedStep) return
    setActiveStep(step)
  }, [highestUnlockedStep])

  const statusBadges = [
    { label: 'Scan OK', ok: uploadReady },
    { label: 'Preview OK', ok: Boolean(primaryPreviewUrl) || (isArchive && screenshots.length > 0) },
    { label: 'AI tags OK', ok: uploadReady },
  ]

  const renderStepContent = () => {
    if (machine.state === machineStates.complete) {
      return (
        <motion.div
          initial={prefersReducedMotion ? false : { opacity: 0, y: 6 }}
          animate={prefersReducedMotion ? {} : { opacity: 1, y: 0 }}
          transition={quickTransition}
          className="rounded-2xl border border-emerald-300/30 bg-emerald-500/8 p-7 text-emerald-100"
        >
          <motion.div
            initial={prefersReducedMotion ? false : { scale: 0.8, opacity: 0 }}
            animate={prefersReducedMotion ? {} : { scale: 1, opacity: 1 }}
            transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.24, ease: 'easeOut' }}
            className="inline-flex items-center gap-2 rounded-full border border-emerald-300/45 bg-emerald-500/15 px-3 py-1 text-xs font-medium"
          >
            <span aria-hidden="true">✓</span>
            <span>Your artwork is live 🎉</span>
          </motion.div>
          <p className="mt-3 text-sm text-emerald-100/90">You can view it now or upload another item.</p>
          <div className="mt-4 flex flex-wrap gap-2">
            <a href={resolvedArtworkId ? `/artwork/${resolvedArtworkId}` : '/'} className="rounded-lg border border-emerald-300/45 bg-emerald-400/20 px-3 py-2 text-sm font-medium text-emerald-50 hover:bg-emerald-400/30">View artwork</a>
            <button type="button" onClick={handleReset} className="rounded-lg border border-white/25 bg-white/10 px-3 py-2 text-sm text-white hover:bg-white/20">Upload another</button>
          </div>
        </motion.div>
      )
    }

    if (activeStep === 1) {
      return (
        <div className="max-w-4xl mx-auto px-4 py-6">
          <div className="bg-panel/80 backdrop-blur rounded-2xl shadow-xl shadow-black/40 ring-1 ring-white/10 p-6 space-y-5">
            <div className="rounded-xl bg-white/5 px-4 py-3 ring-1 ring-white/10">
              <h2 ref={stepHeadingRef} tabIndex={-1} className="text-lg font-semibold text-white focus:outline-none">Upload your artwork</h2>
              <p className="mt-1 text-sm text-white/65">Select your file, satisfy requirements, and complete secure upload processing.</p>
            </div>

            {fileSelectionLocked && (
              <div className="rounded-lg bg-amber-500/10 px-3 py-2 text-xs text-amber-100 ring-1 ring-amber-300/35">
                File is locked after upload. Reset to change.
              </div>
            )}

            <div className="space-y-5">
            <UploadDropzone
              title="Upload your artwork file"
              description="Drag and drop or click to browse. Validation runs immediately before upload starts."
              fileName={primaryFile?.name || ''}
              previewUrl={primaryPreviewUrl}
              fileMeta={fileMetadata}
              fileHint="Awaiting file selection"
              invalid={primaryErrors.length > 0}
              errors={primaryErrors}
              showLooksGood={Boolean(primaryFile) && primaryErrors.length === 0}
              looksGoodText="Looks good"
              locked={fileSelectionLocked}
              onPrimaryFileChange={(file) => {
                if (fileSelectionLocked) return
                setPrimaryFile(file || null)
              }}
            />

            <ScreenshotUploader
              title="Archive screenshots"
              description="We need at least 1 screenshot to generate thumbnails and analyze content."
              visible={isArchive}
              files={screenshots}
              min={1}
              max={5}
              perFileErrors={screenshotPerFileErrors}
              errors={screenshotErrors}
              invalid={isArchive && screenshotErrors.length > 0}
              showLooksGood={isArchive && screenshots.length > 0 && screenshotErrors.length === 0}
              looksGoodText="Looks good"
              onFilesChange={(nextFiles) => setScreenshots(nextFiles)}
            />

            {showProgress && (
              <UploadProgress
                title="Upload progress"
                description="Upload and processing status"
                status={machine.state === machineStates.ready_to_publish ? 'Ready' : machine.state === machineStates.uploading ? 'Uploading' : machine.state === machineStates.processing ? 'Processing' : 'Idle'}
                progress={machine.progress}
                state={machine.state}
                processingStatus={machine.processingStatus}
                isCancelling={machine.isCancelling}
                error={machine.error}
                processingLabel={processingTransparencyLabel}
                onRetry={handleRetry}
                onReset={handleReset}
              />
            )}
            </div>
          </div>
        </div>
      )
    }

    if (activeStep === 2) {
      return (
        <div className="max-w-4xl mx-auto px-4 py-6">
          <div className="bg-panel/80 backdrop-blur rounded-2xl shadow-xl shadow-black/40 ring-1 ring-white/10 p-6 space-y-5">
            <div className="rounded-xl bg-white/5 px-4 py-3 ring-1 ring-white/10">
              <h2 ref={stepHeadingRef} tabIndex={-1} className="text-lg font-semibold text-white focus:outline-none">Add details</h2>
              <p className="mt-1 text-sm text-white/65">Complete required metadata and rights confirmation before publishing.</p>
            </div>

            <div className="mb-6 rounded-xl border border-white/8 bg-white/4 p-4">
              <p className="text-xs uppercase tracking-wide text-white/55">Uploaded asset</p>
              <div className="mt-2 flex items-center gap-3">
                {primaryPreviewUrl && !isArchive ? (
                  <img src={primaryPreviewUrl} alt="Uploaded artwork thumbnail" className="h-20 w-20 rounded-lg border border-white/50 object-cover" />
                ) : (
                  <div className="grid h-20 w-20 place-items-center rounded-lg border border-white/15 bg-white/5 text-white/60">📦</div>
                )}
                <div className="min-w-0">
                  <p className="truncate text-sm font-medium text-white">{primaryFile?.name || 'Primary file selected'}</p>
                  <p className="text-xs text-white/60">{isArchive ? `${screenshots.length} screenshot(s)` : fileMetadata.resolution}</p>
                </div>
              </div>
            </div>

            <div className="rounded-2xl border border-white/10 bg-gradient-to-br from-white/5 via-white/0 to-white/5 p-4 sm:p-5">
              <div className="flex flex-wrap items-center justify-between gap-2">
                <div>
                  <div className="text-sm font-semibold text-white">Content type, category & subcategory</div>
                  <div className="text-xs text-white/60">Select in order: type → category → subcategory (when available).</div>
                </div>
                <span className="rounded-full border border-sky-400/40 bg-sky-400/10 px-3 py-1 text-[11px] uppercase tracking-[0.2em] text-sky-200">Type</span>
              </div>

              {Array.isArray(contentTypes) && contentTypes.length > 0 ? (
                <div className="mt-4">
                  <div className="flex gap-4 overflow-x-auto py-2 no-scrollbar">
                    {contentTypes.map((ct) => {
                      const typeValue = getContentTypeValue(ct)
                      const active = String(typeValue) === String(metadata.contentType || '')
                      const visualKey = getContentTypeVisualKey(ct)
                      const iconPath = `/gfx/mascot_${visualKey}.webp`
                      return (
                        <button
                          key={typeValue || ct.name}
                          type="button"
                          onClick={() => {
                            setMetadata((current) => ({
                              ...current,
                              contentType: String(typeValue),
                              rootCategoryId: '',
                              subCategoryId: '',
                            }))
                          }}
                          className={`group flex flex-col items-center gap-2 min-w-[104px] rounded-xl border px-3 py-2 transition transform ${active
                            ? 'border-emerald-400/70 bg-emerald-400/15 shadow-lg shadow-emerald-900/30 scale-105'
                            : 'border-white/10 bg-white/[0.03] hover:border-white/25 hover:bg-white/[0.06] hover:-translate-y-1 hover:scale-105'} focus:outline-none focus:ring-2 focus:ring-emerald-400/30`}
                          aria-pressed={active}
                        >
                          <div className="h-20 w-20 rounded-full flex items-center justify-center overflow-hidden transform transition-transform duration-150 scale-110">
                            <img
                              src={iconPath}
                              alt={`${ct.name || 'Content type'} mascot`}
                              className={`h-full w-full object-contain transition-all duration-150 ${active ? 'grayscale-0 opacity-100' : 'grayscale opacity-40 group-hover:grayscale-0 group-hover:opacity-90'}`}
                              onError={(event) => {
                                if (event.currentTarget.src.includes('mascot_other.webp')) return
                                event.currentTarget.src = '/gfx/mascot_other.webp'
                              }}
                            />
                          </div>
                          <div className={`text-sm font-semibold ${active ? 'text-white' : 'text-white/65'}`}>{ct.name || 'Type'}</div>
                        </button>
                      )
                    })}
                  </div>
                </div>
              ) : (
                <div className="mt-4 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/60">
                  No content types available.
                </div>
              )}
              {metadataErrors.contentType && <p className="mt-2 text-xs text-red-200">{metadataErrors.contentType}</p>}

              <div className="mt-5 rounded-xl border border-white/10 bg-white/5 p-4">
                <div className="text-xs uppercase tracking-wide text-white/60">Category</div>
                {!metadata.contentType ? (
                  <div className="mt-2 text-sm text-white/60">Choose a content type to load categories.</div>
                ) : filteredCategoryTree.length === 0 ? (
                  <div className="mt-2 text-sm text-white/60">No categories available for this content type.</div>
                ) : (
                  <div className="mt-3 flex flex-wrap gap-2">
                    {filteredCategoryTree.map((root) => {
                      const active = String(root.id) === String(metadata.rootCategoryId || '')
                      return (
                        <button
                          key={root.id}
                          type="button"
                          onClick={() => {
                            setMetadata((current) => ({
                              ...current,
                              rootCategoryId: String(root.id),
                              subCategoryId: '',
                            }))
                          }}
                                  className={`rounded-full border px-4 py-2 text-sm transition ${active
                                    ? 'border-purple-600/90 bg-purple-700/35 text-white shadow-lg'
                                    : 'border-white/10 bg-white/5 text-white/70 hover:border-purple-300/50 hover:bg-purple-400/10'}`}
                          aria-pressed={active}
                        >
                          {root.name}
                        </button>
                      )
                    })}
                  </div>
                )}
              </div>

              {requiresSubCategory && (
                <div className="mt-4 rounded-xl border border-white/10 bg-white/5 p-4">
                  <div className="text-xs uppercase tracking-wide text-white/60">Subcategory</div>
                  <div className="mt-3 flex flex-wrap gap-2">
                    {selectedRootCategory.children.map((sub) => {
                      const active = String(sub.id) === String(metadata.subCategoryId || '')
                      return (
                        <button
                          key={sub.id}
                          type="button"
                          onClick={() => {
                            setMetadata((current) => ({
                              ...current,
                              subCategoryId: String(sub.id),
                            }))
                          }}
                          className={`rounded-full border px-4 py-2 text-sm transition ${active
                            ? 'border-cyan-600/90 bg-cyan-700/35 text-white shadow-lg'
                            : 'border-white/10 bg-white/5 text-white/70 hover:border-cyan-300/50 hover:bg-cyan-400/10'}`}
                          aria-pressed={active}
                        >
                          {sub.name}
                        </button>
                      )
                    })}
                  </div>
                </div>
              )}

              {metadataErrors.category && <p className="mt-2 text-xs text-red-200">{metadataErrors.category}</p>}
            </div>

            <UploadSidebar
              showHeader={false}
              metadata={metadata}
              suggestedTags={mergedSuggestedTags}
              errors={metadataErrors}
              onChangeTitle={(value) => setMetadata((current) => ({ ...current, title: value }))}
              onChangeTags={(value) => setMetadata((current) => ({ ...current, tags: value }))}
              onChangeDescription={(value) => setMetadata((current) => ({ ...current, description: value }))}
              onToggleRights={(value) => setMetadata((current) => ({ ...current, rightsAccepted: Boolean(value) }))}
            />

            {resolvedArtworkId ? (
              <div className="rounded-xl border border-white/10 bg-white/[0.03] p-3 text-xs text-white/70">
                <div className="font-medium text-white/85">Vision debug</div>
                <div className="mt-1">
                  enabled: {String(visionDebug.enabled)} · queue: {visionDebug.queueConnection || 'n/a'} · ai tags: {visionDebug.aiTagCount} · total tags: {visionDebug.totalTagCount}
                </div>
                <div className="mt-1">
                  queued jobs: {visionDebug.queuedJobs} · failed jobs: {visionDebug.failedJobs} · trigger sent: {String(visionDebug.triggered)}
                </div>
                {visionDebug.lastError ? <div className="mt-1 text-red-300">error: {visionDebug.lastError}</div> : null}
              </div>
            ) : null}
          </div>
        </div>
      )
    }

    return (
      <div className="rounded-2xl border border-white/8 bg-slate-900/70 p-6 sm:p-7">
        <div className="mb-6 rounded-xl border border-white/50 bg-white/5 px-4 py-3">
          <h2 ref={stepHeadingRef} tabIndex={-1} className="text-lg font-semibold text-white focus:outline-none">Review & publish</h2>
          <p className="mt-1 text-sm text-white/65">Review your submission and publish when all checks are ready.</p>
        </div>

        <div className="rounded-xl border border-white/8 bg-white/4 p-5">
          <div className="flex flex-col gap-4 sm:flex-row">
            <div className="w-full sm:w-40">
                {primaryPreviewUrl && !isArchive ? (
                <img src={primaryPreviewUrl} alt="Review preview" className="h-32 w-full rounded-lg border border-white/50 object-cover" />
              ) : (
                <div className="grid h-32 w-full place-items-center rounded-lg border border-white/50 bg-black/25 text-white/60">Archive</div>
              )}
            </div>
            <div className="min-w-0 flex-1 space-y-2">
              <p className="text-base font-semibold text-white">{metadata.title || 'Untitled artwork'}</p>
              <p className="text-sm text-white/75">
                Category: {metadata.rootCategoryId || '—'} / {metadata.subCategoryId || '—'}
              </p>
              <p className="text-sm text-white/75">Tags: {(metadata.tags || []).length}</p>
              <div className="flex flex-wrap gap-2">
                <span className={`rounded-full border px-2.5 py-1 text-xs ${metadata.rightsAccepted ? 'border-emerald-300/45 bg-emerald-500/15 text-emerald-100' : 'border-white/20 bg-white/5 text-white/70'}`}>
                  {metadata.rightsAccepted ? '✓ Rights confirmed' : 'Rights not confirmed'}
                </span>
                <span className={`rounded-full border px-2.5 py-1 text-xs ${canPublish ? 'border-emerald-300/45 bg-emerald-500/15 text-emerald-100' : 'border-white/20 bg-white/5 text-white/70'}`}>
                  {canPublish ? 'Ready to publish' : 'Waiting for requirements'}
                </span>
              </div>
              {metadata.description && <p className="line-clamp-3 text-sm text-white/70">{metadata.description}</p>}
            </div>
          </div>

          <div className="mt-4 flex flex-wrap gap-2">
            {statusBadges.map((badge) => (
              <span key={badge.label} className={`rounded-full border px-2.5 py-1 text-xs ${badge.ok ? 'border-emerald-300/45 bg-emerald-500/15 text-emerald-100' : 'border-white/20 bg-white/5 text-white/60'}`}>
                {badge.ok ? '✓' : '•'} {badge.label}
              </span>
            ))}
          </div>
        </div>
      </div>
    )
  }

  return (
    <section ref={stepContentRef} className="space-y-8 text-white" data-is-archive={isArchive ? 'true' : 'false'}>
      {showRestoredBanner && (
        <div className="rounded-xl border border-sky-300/30 bg-sky-500/10 px-4 py-2 text-sm text-sky-100">
          <div className="flex items-center justify-between gap-3">
            <span>Draft restored. Continue from your previous upload session.</span>
            <button
              type="button"
              onClick={() => setShowRestoredBanner(false)}
              className="rounded-md border border-sky-200/40 bg-sky-500/15 px-2 py-1 text-xs text-sky-100 hover:bg-sky-500/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/70"
            >
              Dismiss
            </button>
          </div>
        </div>
      )}

      <UploadStepper
        steps={wizardSteps}
        activeStep={activeStep}
        highestUnlockedStep={highestUnlockedStep}
        onStepClick={goToStep}
      />

      <div className="-mt-4 rounded-full bg-white/8 p-0.5">
        <motion.div
          className="h-1.5 rounded-full bg-gradient-to-r from-sky-400/90 via-cyan-300/90 to-emerald-300/90"
          animate={{ width: `${stepProgressPercent}%` }}
          transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.24, ease: 'easeOut' }}
        />
      </div>

      <AnimatePresence mode="wait" initial={false}>
        <motion.div
          key={`wizard-step-${activeStep}`}
          initial={prefersReducedMotion ? false : { opacity: 0, y: 6 }}
          animate={prefersReducedMotion ? {} : { opacity: 1, y: 0 }}
          exit={prefersReducedMotion ? {} : { opacity: 0, y: -6 }}
          transition={quickTransition}
        >
          {renderStepContent()}
        </motion.div>
      </AnimatePresence>

      <UploadActions
        step={activeStep}
        canStart={canStartUpload && [machineStates.idle, machineStates.error, machineStates.cancelled].includes(machine.state)}
        canContinue={detailsValid}
        canPublish={canPublish}
        canGoBack={activeStep > 1}
        canReset={Boolean(primaryFile || screenshots.length || metadata.title || metadata.description || metadata.tags.length)}
        canCancel={activeStep === 1 && [machineStates.initializing, machineStates.uploading, machineStates.finishing, machineStates.processing].includes(machine.state)}
        canRetry={machine.state === machineStates.error}
        isUploading={machine.state === machineStates.uploading || machine.state === machineStates.initializing}
        isProcessing={machine.state === machineStates.processing || machine.state === machineStates.finishing}
        isPublishing={machine.state === machineStates.publishing}
        isCancelling={machine.isCancelling}
        disableReason={activeStep === 1 ? (validationErrors[0] || machine.error || 'Complete upload requirements first.') : activeStep === 2 ? (metadataErrors.title || metadataErrors.contentType || metadataErrors.category || metadataErrors.rights || 'Complete required metadata.') : (machine.error || 'Publish is available when upload is ready and rights are confirmed.')}
        onStart={runUploadFlow}
        onContinue={() => detailsValid && setActiveStep(3)}
        onPublish={handlePublish}
        onBack={() => setActiveStep((current) => Math.max(1, current - 1))}
        onCancel={handleCancel}
        onReset={handleReset}
        onRetry={handleRetry}
        onSaveDraft={() => {}}
        showSaveDraft={activeStep === 2}
        resetLabel={fileSelectionLocked ? 'Reset upload' : 'Reset'}
        mobileSticky
      />
    </section>
  )
}

function isValidForUpload(primaryFile, primaryErrors, isArchive, screenshotErrors) {
  if (!primaryFile) return false
  if (primaryErrors.length > 0) return false
  if (isArchive && screenshotErrors.length > 0) return false
  return true
}
