/**
 * UploadWizard – refactored orchestrator
 *
 * A 3-step upload wizard that delegates:
 *   - Machine state    → useUploadMachine
 *   - File validation  → useFileValidation
 *   - Vision AI tags   → useVisionTags
 *   - Step rendering   → Step1FileUpload / Step2Details / Step3Publish
 *   - Reusable UI      → ContentTypeSelector / CategorySelector / UploadSidebar …
 */
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'

import useUploadMachine, { machineStates } from '../../hooks/upload/useUploadMachine'
import useFileValidation from '../../hooks/upload/useFileValidation'
import useVisionTags from '../../hooks/upload/useVisionTags'

import StudioStatusBar from './StudioStatusBar'
import UploadOverlay from './UploadOverlay'
import UploadActions from './UploadActions'
import PublishPanel from './PublishPanel'
import Step1FileUpload from './steps/Step1FileUpload'
import Step2Details from './steps/Step2Details'
import Step3Publish from './steps/Step3Publish'

import {
  buildCategoryTree,
  getContentTypeValue,
  getProcessingTransparencyLabel,
} from '../../lib/uploadUtils'

// ─── Wizard step config ───────────────────────────────────────────────────────
const wizardSteps = [
  { key: 'upload', label: 'Upload' },
  { key: 'details', label: 'Details' },
  { key: 'publish', label: 'Publish' },
]

function createInitialMetadata(initialGroupSlug = '', currentUserId = null, contributorOptionsByGroup = {}) {
  const normalizedGroupSlug = String(initialGroupSlug || '').trim()
  const contributors = Array.isArray(contributorOptionsByGroup?.[normalizedGroupSlug])
    ? contributorOptionsByGroup[normalizedGroupSlug]
    : []
  const defaultPrimaryAuthor = contributors.some((user) => Number(user.id) === Number(currentUserId))
    ? Number(currentUserId)
    : Number(contributors[0]?.id || 0) || null

  return {
    title: '',
    rootCategoryId: '',
    subCategoryId: '',
    tags: [],
    description: '',
    isMature: false,
    rightsAccepted: false,
    contentType: '',
    group: normalizedGroupSlug,
    primaryAuthorUserId: defaultPrimaryAuthor,
    contributorUserIds: [],
    contributorCredits: {},
  }
}

function normalizeContributorCredits(contributorIds = [], contributorCredits = {}) {
  const normalized = {}
  const ids = Array.isArray(contributorIds)
    ? contributorIds.map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0)
    : []

  ids.forEach((id) => {
    const current = contributorCredits?.[id] || contributorCredits?.[String(id)] || {}
    normalized[id] = {
      creditRole: typeof current.creditRole === 'string' ? current.creditRole : '',
      isPrimary: Boolean(current.isPrimary),
    }
  })

  const leadIds = Object.entries(normalized)
    .filter(([, value]) => value.isPrimary)
    .map(([id]) => Number(id))

  if (leadIds.length > 1) {
    leadIds.slice(1).forEach((id) => {
      normalized[id] = {
        ...normalized[id],
        isPrimary: false,
      }
    })
  }

  return normalized
}

// ─── Helpers ─────────────────────────────────────────────────────────────────
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
}

// ─── Component ────────────────────────────────────────────────────────────────
export default function UploadWizard({
  onValidationStateChange,
  initialDraftId = null,
  chunkSize,
  chunkRequestTimeoutMs,
  contentTypes = [],
  suggestedTags = [],
  groupOptions = [],
  contributorOptionsByGroup = {},
  initialGroupSlug = '',
  currentUserId = null,
}) {
  const [notices, setNotices] = useState([])
  // ── UI state ──────────────────────────────────────────────────────────────
  const [activeStep, setActiveStep] = useState(1)
  const [showRestoredBanner, setShowRestoredBanner] = useState(Boolean(initialDraftId))
  const [isUploadLocked, setIsUploadLocked] = useState(false)
  const [resolvedArtworkId, setResolvedArtworkId] = useState(() => {
    const parsed = Number(initialDraftId)
    return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null
  })

  // ── Publish options (Studio) ──────────────────────────────────────────────
  const [publishMode, setPublishMode] = useState('now')   // 'now' | 'schedule'
  const [scheduledAt, setScheduledAt] = useState(null)    // UTC ISO or null
  const [visibility, setVisibility] = useState('public')  // 'public'|'unlisted'|'private'
  const [showMobilePublishPanel, setShowMobilePublishPanel] = useState(false)
  const userTimezone = useMemo(() => {
    try { return Intl.DateTimeFormat().resolvedOptions().timeZone } catch { return 'UTC' }
  }, [])

  // ── File + screenshot state ───────────────────────────────────────────────
  const [primaryFile, setPrimaryFile] = useState(null)
  const [screenshots, setScreenshots] = useState([])
  const [selectedScreenshotIndex, setSelectedScreenshotIndex] = useState(0)

  // ── Metadata state ────────────────────────────────────────────────────────
  const [metadata, setMetadata] = useState(() => createInitialMetadata(initialGroupSlug, currentUserId, contributorOptionsByGroup))

  // ── Refs ──────────────────────────────────────────────────────────────────
  const prefersReducedMotion = useReducedMotion()
  const stepContentRef = useRef(null)
  const stepHeadingRef = useRef(null)
  const hasAutoAdvancedRef = useRef(false)

  const quickTransition = prefersReducedMotion ? { duration: 0 } : { duration: 0.2, ease: 'easeOut' }

  // ── File validation hook ──────────────────────────────────────────────────
  const {
    primaryType,
    primaryErrors,
    primaryWarnings,
    fileMetadata,
    primaryPreviewUrl,
    screenshotErrors,
    screenshotPerFileErrors,
  } = useFileValidation(primaryFile, screenshots)

  const isArchive = primaryType === 'archive'

  const canStartUpload = isValidForUpload(primaryFile, primaryErrors, isArchive, screenshotErrors)

  useEffect(() => {
    if (!Array.isArray(screenshots) || screenshots.length === 0) {
      setSelectedScreenshotIndex(0)
      return
    }

    setSelectedScreenshotIndex((prev) => {
      if (!Number.isFinite(prev) || prev < 0) return 0
      return Math.min(prev, screenshots.length - 1)
    })
  }, [screenshots])

  // ── Machine hook ──────────────────────────────────────────────────────────
  const {
    machine,
    runUploadFlow,
    handleCancel,
    handlePublish,
    handleRetry,
    resetMachine,
    abortAllRequests,
    clearPolling,
  } = useUploadMachine({
    primaryFile,
    screenshots,
    selectedScreenshotIndex,
    canStartUpload,
    primaryType,
    isArchive,
    initialDraftId,
    metadata,
    chunkSize,
    chunkRequestTimeoutMs,
    onArtworkCreated: (id) => setResolvedArtworkId(id),
    onNotice: (notice) => {
      if (!notice?.message) return
      const normalizedType = ['success', 'warning', 'error'].includes(String(notice.type || '').toLowerCase())
        ? String(notice.type).toLowerCase()
        : 'error'
      const id = `${Date.now()}-${Math.random().toString(16).slice(2)}`
      setNotices((prev) => [...prev, { id, type: normalizedType, message: String(notice.message) }])
      window.setTimeout(() => {
        setNotices((prev) => prev.filter((item) => item.id !== id))
      }, 4500)
    },
  })

  // ── Upload-ready flag (needed before vision hook) ─────────────────────────
  const uploadReady = (
    machine.state === machineStates.ready_to_publish ||
    machine.processingStatus === 'ready' ||
    machine.state === machineStates.complete
  )

  // ── Vision tags hook – fires on upload completion, not step change ──────────
  // Starts fetching AI tag suggestions while the user is still on Step 1,
  // so results are ready (or partially ready) by the time Step 2 opens.
  const { visionSuggestedTags } = useVisionTags(resolvedArtworkId, uploadReady)

  // ── Category tree computation ─────────────────────────────────────────────
  const categoryTreeByType = useMemo(() => {
    const result = {}
    const list = Array.isArray(contentTypes) ? contentTypes : []
    list.forEach((type) => {
      const value = getContentTypeValue(type)
      if (!value) return
      result[value] = buildCategoryTree([type])
    })
    return result
  }, [contentTypes])

  const filteredCategoryTree = useMemo(() => {
    const selected = String(metadata.contentType || '')
    if (!selected) return []
    return categoryTreeByType[selected] || []
  }, [categoryTreeByType, metadata.contentType])

  const selectedGroupOption = useMemo(() => {
    const selectedSlug = String(metadata.group || '')
    if (!selectedSlug) return null
    return (Array.isArray(groupOptions) ? groupOptions : []).find((group) => String(group.slug || '') === selectedSlug) || null
  }, [groupOptions, metadata.group])

  const reviewSubmissionMode = Boolean(
    selectedGroupOption &&
    !selectedGroupOption?.permissions?.can_publish_artworks &&
    selectedGroupOption?.permissions?.can_submit_artwork_for_review
  )

  const publishActionLabel = reviewSubmissionMode
    ? 'Submit for review'
    : (publishMode === 'schedule' ? 'Schedule publish' : 'Publish now')

  const currentContributorOptions = useMemo(() => {
    const selectedSlug = String(metadata.group || '')
    return Array.isArray(contributorOptionsByGroup?.[selectedSlug]) ? contributorOptionsByGroup[selectedSlug] : []
  }, [contributorOptionsByGroup, metadata.group])

  const allRootCategoryOptions = useMemo(() => {
    const items = []
    Object.entries(categoryTreeByType).forEach(([contentTypeValue, roots]) => {
      roots.forEach((root) => items.push({ ...root, contentTypeValue }))
    })
    return items
  }, [categoryTreeByType])

  const selectedRootCategory = useMemo(() => {
    const id = String(metadata.rootCategoryId || '')
    if (!id) return null
    return (
      filteredCategoryTree.find((r) => String(r.id) === id) ||
      allRootCategoryOptions.find((r) => String(r.id) === id) ||
      null
    )
  }, [filteredCategoryTree, allRootCategoryOptions, metadata.rootCategoryId])

  const requiresSubCategory = Boolean(
    selectedRootCategory &&
    Array.isArray(selectedRootCategory.children) &&
    selectedRootCategory.children.length > 0
  )

  useEffect(() => {
    const selectedSlug = String(metadata.group || '')
    if (!selectedSlug) {
      if (metadata.primaryAuthorUserId || metadata.contributorUserIds.length > 0 || Object.keys(metadata.contributorCredits || {}).length > 0) {
        setMetadata((current) => ({ ...current, primaryAuthorUserId: null, contributorUserIds: [], contributorCredits: {} }))
      }
      return
    }

    const validGroup = (Array.isArray(groupOptions) ? groupOptions : []).some((group) => String(group.slug || '') === selectedSlug)
    if (!validGroup) {
      setMetadata((current) => ({ ...current, group: '', primaryAuthorUserId: null, contributorUserIds: [], contributorCredits: {} }))
      return
    }

    const validContributorIds = currentContributorOptions.map((user) => Number(user.id)).filter((id) => Number.isFinite(id) && id > 0)
    const nextPrimaryAuthorId = validContributorIds.includes(Number(metadata.primaryAuthorUserId))
      ? Number(metadata.primaryAuthorUserId)
      : (validContributorIds.includes(Number(currentUserId)) ? Number(currentUserId) : (validContributorIds[0] || null))
    const nextContributorIds = (Array.isArray(metadata.contributorUserIds) ? metadata.contributorUserIds : [])
      .map((id) => Number(id))
      .filter((id) => validContributorIds.includes(id) && id !== nextPrimaryAuthorId)
    const nextContributorCredits = normalizeContributorCredits(nextContributorIds, metadata.contributorCredits)

    const currentPrimary = metadata.primaryAuthorUserId ? Number(metadata.primaryAuthorUserId) : null
    const currentContributors = (Array.isArray(metadata.contributorUserIds) ? metadata.contributorUserIds : []).map((id) => Number(id))
    const contributorsChanged = nextContributorIds.length !== currentContributors.length || nextContributorIds.some((id, index) => id !== currentContributors[index])
    const contributorCreditsChanged = JSON.stringify(nextContributorCredits) !== JSON.stringify(normalizeContributorCredits(currentContributors, metadata.contributorCredits))

    if (currentPrimary !== nextPrimaryAuthorId || contributorsChanged || contributorCreditsChanged) {
      setMetadata((current) => ({
        ...current,
        primaryAuthorUserId: nextPrimaryAuthorId,
        contributorUserIds: nextContributorIds,
        contributorCredits: nextContributorCredits,
      }))
    }
  }, [groupOptions, currentContributorOptions, currentUserId, metadata.group, metadata.primaryAuthorUserId, metadata.contributorUserIds, metadata.contributorCredits])

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

  const detailsValid = Object.keys(metadataErrors).length === 0

  // ── Merged AI + manual suggested tags ────────────────────────────────────
  const mergedSuggestedTags = useMemo(() => {
    const map = new Map()
    const addTag = (item) => {
      if (!item) return
      const key = String(item?.slug || item?.tag || item?.name || item).trim().toLowerCase()
      if (!key || map.has(key)) return
      map.set(key, typeof item === 'string' ? item : {
        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(map.values())
  }, [suggestedTags, visionSuggestedTags])

  // ── Derived flags ─────────────────────────────────────────────────────────
  const highestUnlockedStep = uploadReady ? (detailsValid ? 3 : 2) : 1
  const showProgress = ![machineStates.idle, machineStates.cancelled].includes(machine.state)
  const processingLabel = getProcessingTransparencyLabel(machine.processingStatus, machine.state)
  const showOverlay = ['initializing', 'uploading', 'finishing', 'processing', 'error'].includes(machine.state)

  const hasTitle = Boolean(String(metadata.title || '').trim())
  const hasCompleteCategory = Boolean(
    metadata.rootCategoryId && (!requiresSubCategory || metadata.subCategoryId)
  )
  const hasTag = Array.isArray(metadata.tags) && metadata.tags.length > 0
  const hasRequiredScreenshot = !isArchive || screenshots.length > 0

  const canPublish = useMemo(() => (
    uploadReady &&
    hasTitle &&
    hasCompleteCategory &&
    hasTag &&
    hasRequiredScreenshot &&
    metadata.rightsAccepted &&
    machine.state !== machineStates.publishing
  ), [uploadReady, hasTitle, hasCompleteCategory, hasTag, hasRequiredScreenshot, metadata.rightsAccepted, machine.state])

  const canScheduleSubmit = useMemo(() => {
    if (!canPublish) return false
    if (reviewSubmissionMode) return true
    if (publishMode === 'schedule') return Boolean(scheduledAt)
    return true
  }, [canPublish, reviewSubmissionMode, publishMode, scheduledAt])

  // ── Validation surface for parent ────────────────────────────────────────
  const validationErrors = useMemo(
    () => [...primaryErrors, ...screenshotErrors],
    [primaryErrors, screenshotErrors]
  )

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

  // ── Auto-advance to step 2 after upload complete ──────────────────────────
  useEffect(() => {
    if (uploadReady && activeStep === 1 && !hasAutoAdvancedRef.current) {
      hasAutoAdvancedRef.current = true
      setIsUploadLocked(true)
      setActiveStep(2)
    }
  }, [uploadReady, activeStep])

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

  // ── Step scroll + focus ───────────────────────────────────────────────────
  useEffect(() => {
    if (!stepContentRef.current) return
    stepContentRef.current.scrollIntoView({
      behavior: prefersReducedMotion ? 'auto' : 'smooth',
      block: 'start',
    })
    window.setTimeout(() => {
      stepHeadingRef.current?.focus?.({ preventScroll: true })
    }, 0)
  }, [activeStep, prefersReducedMotion])

  // ── Cleanup ───────────────────────────────────────────────────────────────
  useEffect(() => {
    return () => {
      abortAllRequests()
      clearPolling()
    }
  }, [abortAllRequests, clearPolling])
  // ── ESC key closes mobile drawer (spec §7) ─────────────────────────────
  useEffect(() => {
    if (!showMobilePublishPanel) return
    const handler = (e) => { if (e.key === 'Escape') setShowMobilePublishPanel(false) }
    document.addEventListener('keydown', handler)
    return () => document.removeEventListener('keydown', handler)
  }, [showMobilePublishPanel])
  // ── Metadata helpers ──────────────────────────────────────────────────────
  const setMeta = useCallback((patch) => setMetadata((prev) => ({ ...prev, ...patch })), [])

  // ── Full reset ────────────────────────────────────────────────────────────
  const handleReset = useCallback(() => {
    resetMachine()
    setPrimaryFile(null)
    setScreenshots([])
    setSelectedScreenshotIndex(0)
    setMetadata(createInitialMetadata(initialGroupSlug, currentUserId, contributorOptionsByGroup))
    setIsUploadLocked(false)
    hasAutoAdvancedRef.current = false
    setPublishMode('now')
    setScheduledAt(null)
    setVisibility('public')
    setShowMobilePublishPanel(false)
    setResolvedArtworkId(() => {
      const parsed = Number(initialDraftId)
      return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null
    })
    setActiveStep(1)
  }, [resetMachine, initialDraftId, initialGroupSlug, currentUserId, contributorOptionsByGroup])

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

  // ── Step content renderer ─────────────────────────────────────────────────
  const renderStepContent = () => {
    // Complete / success screen
    if (machine.state === machineStates.complete) {
      const wasScheduled = machine.lastAction === 'schedule'
      const studioArtworksUrl = '/studio/artworks'
      const studioArtworkUrl = resolvedArtworkId
        ? `/studio/artworks/${resolvedArtworkId}/edit`
        : studioArtworksUrl
      return (
        <motion.div
          initial={prefersReducedMotion ? false : { opacity: 0, scale: 0.98 }}
          animate={{ opacity: 1, scale: 1 }}
          transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.28, ease: 'easeOut' }}
          className={`rounded-2xl p-8 text-center ${wasScheduled ? 'ring-1 ring-violet-300/25 bg-violet-500/8' : 'ring-1 ring-emerald-300/25 bg-emerald-500/8'}`}
        >
          <motion.div
            initial={prefersReducedMotion ? false : { scale: 0.7, opacity: 0 }}
            animate={{ scale: 1, opacity: 1 }}
            transition={prefersReducedMotion ? { duration: 0 } : { delay: 0.1, duration: 0.26, ease: 'backOut' }}
            className={`mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full text-2xl`}
          >
            {wasScheduled ? '🕐' : '🎉'}
          </motion.div>

          <h3 className="text-xl font-semibold text-white">
            {wasScheduled ? 'Artwork scheduled!' : 'Your artwork is live!'}
          </h3>
          <p className="mt-2 text-sm text-white/65">
            {wasScheduled
              ? scheduledAt
                ? `Will publish on ${new Intl.DateTimeFormat('en-GB', { timeZone: userTimezone, weekday: 'short', day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date(scheduledAt))}`
                : 'Your artwork is scheduled for future publishing.'
              : 'It has been published and is now visible to the community.'}
          </p>

            <div className="mt-6 flex flex-wrap justify-center gap-3">
            {!wasScheduled && (
              <a
                href={resolvedArtworkId
                  ? `/art/${resolvedArtworkId}${machine.slug ? `/${machine.slug}` : ''}`
                  : '/'}
                className="rounded-lg ring-1 ring-emerald-300/45 bg-emerald-400/20 px-4 py-2 text-sm font-medium text-emerald-50 hover:bg-emerald-400/30 transition"
              >
                View artwork
              </a>
            )}
            <a
              href={studioArtworksUrl}
              className="rounded-lg ring-1 ring-sky-300/35 bg-sky-400/12 px-4 py-2 text-sm font-medium text-sky-50 hover:bg-sky-400/20 transition"
            >
              View in studio
            </a>
            <a
              href={studioArtworkUrl}
              className="rounded-lg ring-1 ring-white/20 bg-white/8 px-4 py-2 text-sm font-medium text-white hover:bg-white/15 transition"
            >
              Edit artwork in studio
            </a>
            <button
              type="button"
              onClick={handleReset}
              className="rounded-lg ring-1 ring-white/20 bg-white/8 px-4 py-2 text-sm text-white hover:bg-white/15 transition"
            >
              Upload another
            </button>
          </div>
        </motion.div>
      )
    }

    if (activeStep === 1) {
      return (
        <Step1FileUpload
          headingRef={stepHeadingRef}
          primaryFile={primaryFile}
          primaryPreviewUrl={primaryPreviewUrl}
          primaryErrors={primaryErrors}
          primaryWarnings={primaryWarnings}
          fileMetadata={fileMetadata}
          fileSelectionLocked={isUploadLocked}
          onPrimaryFileChange={setPrimaryFile}
          isArchive={isArchive}
          screenshots={screenshots}
          selectedScreenshotIndex={selectedScreenshotIndex}
          screenshotErrors={screenshotErrors}
          screenshotPerFileErrors={screenshotPerFileErrors}
          onScreenshotsChange={setScreenshots}
          onSelectedScreenshotChange={setSelectedScreenshotIndex}
          machine={machine}
        />
      )
    }

    if (activeStep === 2) {
      return (
        <Step2Details
          headingRef={stepHeadingRef}
          primaryFile={primaryFile}
          primaryPreviewUrl={primaryPreviewUrl}
          isArchive={isArchive}
          fileMetadata={fileMetadata}
          screenshots={screenshots}
          selectedScreenshotIndex={selectedScreenshotIndex}
          onSelectedScreenshotChange={setSelectedScreenshotIndex}
          contentTypes={contentTypes}
          metadata={metadata}
          metadataErrors={metadataErrors}
          filteredCategoryTree={filteredCategoryTree}
          allRootCategoryOptions={allRootCategoryOptions}
          requiresSubCategory={requiresSubCategory}
          onContentTypeChange={(value) => setMeta({ contentType: value, rootCategoryId: '', subCategoryId: '' })}
          onRootCategoryChange={(rootId) => setMeta({ rootCategoryId: rootId, subCategoryId: '' })}
          onSubCategoryChange={(subId) => setMeta({ subCategoryId: subId })}
          groupOptions={groupOptions}
          currentContributorOptions={currentContributorOptions}
          onGroupChange={(groupSlug) => setMeta({ group: groupSlug })}
          onPrimaryAuthorChange={(authorId) => setMeta({ primaryAuthorUserId: authorId ? Number(authorId) : null })}
          onContributorToggle={(contributorId) => setMetadata((current) => {
            const normalizedId = Number(contributorId)
            const nextIds = new Set((Array.isArray(current.contributorUserIds) ? current.contributorUserIds : []).map((id) => Number(id)).filter((id) => id !== Number(current.primaryAuthorUserId)))
            if (nextIds.has(normalizedId)) {
              nextIds.delete(normalizedId)
            } else {
              nextIds.add(normalizedId)
            }

            const contributorUserIds = Array.from(nextIds).filter((id) => id !== Number(current.primaryAuthorUserId))

            return {
              ...current,
              contributorUserIds,
              contributorCredits: normalizeContributorCredits(contributorUserIds, current.contributorCredits),
            }
          })}
          onContributorRoleChange={(contributorId, creditRole) => setMetadata((current) => {
            const contributorUserIds = (Array.isArray(current.contributorUserIds) ? current.contributorUserIds : []).map((id) => Number(id))
            if (!contributorUserIds.includes(Number(contributorId))) return current

            const contributorCredits = normalizeContributorCredits(contributorUserIds, current.contributorCredits)

            return {
              ...current,
              contributorCredits: {
                ...contributorCredits,
                [Number(contributorId)]: {
                  ...(contributorCredits[Number(contributorId)] || { isPrimary: false }),
                  creditRole,
                },
              },
            }
          })}
          onContributorPrimaryChange={(contributorId) => setMetadata((current) => {
            const contributorUserIds = (Array.isArray(current.contributorUserIds) ? current.contributorUserIds : []).map((id) => Number(id))
            const contributorCredits = normalizeContributorCredits(contributorUserIds, current.contributorCredits)

            contributorUserIds.forEach((id) => {
              contributorCredits[id] = {
                ...(contributorCredits[id] || { creditRole: '' }),
                isPrimary: id === Number(contributorId),
              }
            })

            return {
              ...current,
              contributorCredits,
            }
          })}
          suggestedTags={mergedSuggestedTags}
          publishMode={publishMode}
          scheduledAt={scheduledAt}
          timezone={userTimezone}
          onPublishModeChange={setPublishMode}
          onScheduleAt={setScheduledAt}
          onChangeTitle={(value) => setMeta({ title: value })}
          onChangeTags={(value) => setMeta({ tags: value })}
          onChangeDescription={(value) => setMeta({ description: value })}
          onToggleMature={(value) => setMeta({ isMature: Boolean(value) })}
          onToggleRights={(value) => setMeta({ rightsAccepted: Boolean(value) })}
        />
      )
    }

    return (
      <Step3Publish
        headingRef={stepHeadingRef}
        primaryFile={primaryFile}
        primaryPreviewUrl={primaryPreviewUrl}
        isArchive={isArchive}
        screenshots={screenshots}
        selectedScreenshotIndex={selectedScreenshotIndex}
        onSelectedScreenshotChange={setSelectedScreenshotIndex}
        fileMetadata={fileMetadata}
        metadata={metadata}
        canPublish={canPublish}
        uploadReady={uploadReady}
        publishMode={publishMode}
        scheduledAt={scheduledAt}
        timezone={userTimezone}
        visibility={visibility}
        actionLabel={publishActionLabel}
        showScheduleSummary={!reviewSubmissionMode}
        onVisibilityChange={setVisibility}
        selectedGroup={selectedGroupOption}
        currentContributorOptions={currentContributorOptions}
        allRootCategoryOptions={allRootCategoryOptions}
        filteredCategoryTree={filteredCategoryTree}
      />
    )
  }

  // ── Action bar helpers ────────────────────────────────────────────────────
  const disableReason = (() => {
    if (activeStep === 1) return validationErrors[0] || machine.error || 'Complete upload requirements first.'
    if (activeStep === 2) return metadataErrors.title || metadataErrors.contentType || metadataErrors.category || metadataErrors.rights || metadataErrors.tags || 'Complete required metadata.'
    return machine.error || 'Publish is available when upload is ready and rights are confirmed.'
  })()

  // ─────────────────────────────────────────────────────────────────────────
  return (
    <section
      ref={stepContentRef}
      className="space-y-5 pb-32 text-white lg:pb-8"
      data-is-archive={isArchive ? 'true' : 'false'}
    >
      {notices.length > 0 && (
        <div className="fixed right-4 top-4 z-[70] w-[min(92vw,420px)] space-y-2">
          {notices.map((notice) => (
            <div
              key={notice.id}
              role="alert"
              aria-live="polite"
              className={[
                'rounded-xl border px-4 py-3 text-sm shadow-lg backdrop-blur',
                notice.type === 'success'
                  ? 'border-emerald-400/45 bg-emerald-500/12 text-emerald-100'
                  : notice.type === 'warning'
                    ? 'border-amber-400/45 bg-amber-500/12 text-amber-100'
                    : 'border-red-400/45 bg-red-500/12 text-red-100',
              ].join(' ')}
            >
              {notice.message}
            </div>
          ))}
        </div>
      )}

      {/* Restored draft banner */}
      {showRestoredBanner && (
        <div className="rounded-2xl border border-sky-300/20 bg-sky-500/10 px-4 py-3 text-sm text-sky-100 shadow-[0_14px_44px_rgba(14,165,233,0.10)]">
          <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="shrink-0 rounded-md ring-1 ring-sky-200/35 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>
      )}

      {/* ── Studio Status Bar (sticky step header + progress) ────────────── */}
      <StudioStatusBar
        steps={wizardSteps}
        activeStep={activeStep}
        highestUnlockedStep={highestUnlockedStep}
        machineState={machine.state}
        progress={machine.progress}
        showProgress={showProgress}
        onStepClick={goToStep}
      />

      {/* ── Main body: two-column on desktop ─────────────────────────────── */}
      <div className="flex flex-col gap-5 lg:flex-row lg:items-start lg:gap-8">
        {/* Left / main column: step content */}
        <div className="min-w-0 flex-1">
          {/* Step content + centered progress overlay */}
          <div className="relative">
            <AnimatePresence mode="wait" initial={false}>
              <motion.div
                key={`step-${activeStep}`}
                initial={prefersReducedMotion ? false : { opacity: 0, y: 8 }}
                animate={{ opacity: 1, y: 0 }}
                exit={prefersReducedMotion ? {} : { opacity: 0, y: -8 }}
                transition={quickTransition}
              >
                {renderStepContent()}
              </motion.div>
            </AnimatePresence>

            <UploadOverlay
              machineState={machine.state}
              progress={machine.progress}
              processingLabel={processingLabel}
              error={machine.error}
              onRetry={() => handleRetry(canPublish, { mode: publishMode, publishAt: scheduledAt, timezone: userTimezone, visibility })}
              onReset={handleReset}
            />
          </div>

          {/* Wizard action bar (nav: back/next/start/retry) */}
          {machine.state !== machineStates.complete && (
          <div className="mt-5">
            <UploadActions
              step={activeStep}
              canStart={canStartUpload && [machineStates.idle, machineStates.error, machineStates.cancelled].includes(machine.state)}
              canContinue={detailsValid}
              canPublish={canScheduleSubmit}
              canGoBack={activeStep > 1 && machine.state !== machineStates.complete}
              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={[machineStates.uploading, machineStates.initializing].includes(machine.state)}
              isProcessing={[machineStates.processing, machineStates.finishing].includes(machine.state)}
              isPublishing={machine.state === machineStates.publishing}
              isCancelling={machine.isCancelling}
              disableReason={disableReason}
              onStart={runUploadFlow}
              onContinue={() => detailsValid && setActiveStep(3)}
              onPublish={() => handlePublish(canPublish, { mode: reviewSubmissionMode ? 'now' : publishMode, publishAt: reviewSubmissionMode ? null : scheduledAt, timezone: userTimezone, visibility, action: reviewSubmissionMode ? 'submit_review' : 'publish' })}
              onBack={() => setActiveStep((s) => Math.max(1, s - 1))}
              onCancel={handleCancel}
              onReset={handleReset}
              onRetry={() => handleRetry(canPublish, { mode: reviewSubmissionMode ? 'now' : publishMode, publishAt: reviewSubmissionMode ? null : scheduledAt, timezone: userTimezone, visibility, action: reviewSubmissionMode ? 'submit_review' : 'publish' })}
              onSaveDraft={() => {}}
              showSaveDraft={activeStep === 2}
              resetLabel={isUploadLocked ? 'Reset upload' : 'Reset'}
              publishLabel={publishActionLabel}
              mobileSticky
            />
          </div>
          )}
        </div>

        {/* Right column: PublishPanel (sticky sidebar on lg+, Step 2+ only) */}
        {(primaryFile || resolvedArtworkId) && machine.state !== machineStates.complete && activeStep > 1 && (
          <div className="hidden shrink-0 lg:block lg:w-80 xl:w-[22rem] lg:sticky lg:top-20 lg:self-start">
            <PublishPanel
              primaryPreviewUrl={primaryPreviewUrl}
              isArchive={isArchive}
              screenshots={screenshots}
              selectedScreenshotIndex={selectedScreenshotIndex}
              onSelectedScreenshotChange={setSelectedScreenshotIndex}
              metadata={metadata}
              machineState={machine.state}
              uploadReady={uploadReady}
              canPublish={canPublish}
              isPublishing={machine.state === machineStates.publishing}
              isArchiveRequiresScreenshot={isArchive}
              publishMode={publishMode}
              scheduledAt={scheduledAt}
              timezone={userTimezone}
              visibility={visibility}
              actionLabel={publishActionLabel}
              showScheduleControls={!reviewSubmissionMode}
              showRightsConfirmation={activeStep === 3}
              showVisibility={false}
              onPublishModeChange={setPublishMode}
              onScheduleAt={setScheduledAt}
              onVisibilityChange={setVisibility}
              onToggleRights={(checked) => setMeta({ rightsAccepted: Boolean(checked) })}
              onPublish={() => handlePublish(canPublish, { mode: reviewSubmissionMode ? 'now' : publishMode, publishAt: reviewSubmissionMode ? null : scheduledAt, timezone: userTimezone, visibility, action: reviewSubmissionMode ? 'submit_review' : 'publish' })}
              onCancel={handleCancel}
              onGoToStep={goToStep}
              allRootCategoryOptions={allRootCategoryOptions}
            />
          </div>
        )}
      </div>

      {/* ── Mobile: floating "Publish" button that opens bottom sheet ────── */}
      {(primaryFile || resolvedArtworkId) && machine.state !== machineStates.complete && activeStep > 1 && (
        <div className="fixed bottom-4 right-4 z-30 lg:hidden">
          <button
            type="button"
            aria-label="Open publish panel"
            onClick={() => setShowMobilePublishPanel((v) => !v)}
            className="flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-500 px-4 py-2.5 text-sm font-semibold text-white shadow-[0_18px_50px_rgba(14,165,233,0.35)] transition hover:bg-sky-400 active:scale-95"
          >
            <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
              <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
            </svg>
            {reviewSubmissionMode ? 'Review' : 'Publish'}
            {!canPublish && (
              <span className="ml-1 rounded-full bg-white/20 px-1.5 text-[10px]">
                {[
                  ...(!uploadReady ? [1] : []),
                  ...(hasTitle ? [] : [1]),
                  ...(hasCompleteCategory ? [] : [1]),
                  ...(hasTag ? [] : [1]),
                  ...(hasRequiredScreenshot ? [] : [1]),
                  ...(metadata.rightsAccepted ? [] : [1]),
                ].length}
              </span>
            )}
          </button>
        </div>
      )}

      {/* ── Mobile Publish panel bottom-sheet overlay ────────────────────── */}
      <AnimatePresence>
        {showMobilePublishPanel && (
          <>
            {/* Backdrop */}
            <motion.div
              key="mobile-panel-backdrop"
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              transition={{ duration: 0.2 }}
              className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm lg:hidden"
              onClick={() => setShowMobilePublishPanel(false)}
            />
            {/* Sheet */}
            <motion.div
              key="mobile-panel-sheet"
              initial={{ y: '100%' }}
              animate={{ y: 0 }}
              exit={{ y: '100%' }}
              transition={prefersReducedMotion ? { duration: 0 } : { type: 'spring', damping: 30, stiffness: 300 }}
              className="fixed bottom-0 left-0 right-0 z-50 max-h-[80vh] overflow-y-auto rounded-t-2xl bg-slate-900 ring-1 ring-white/10 p-5 pb-8 lg:hidden"
            >
              <div className="mx-auto mb-4 h-1 w-12 rounded-full bg-white/20" aria-hidden="true" />
              <PublishPanel
                primaryPreviewUrl={primaryPreviewUrl}
                isArchive={isArchive}
                screenshots={screenshots}
                selectedScreenshotIndex={selectedScreenshotIndex}
                onSelectedScreenshotChange={setSelectedScreenshotIndex}
                metadata={metadata}
                machineState={machine.state}
                uploadReady={uploadReady}
                canPublish={canPublish}
                isPublishing={machine.state === machineStates.publishing}
                isArchiveRequiresScreenshot={isArchive}
                publishMode={publishMode}
                scheduledAt={scheduledAt}
                timezone={userTimezone}
                visibility={visibility}
                actionLabel={publishActionLabel}
                showScheduleControls={!reviewSubmissionMode}
                showRightsConfirmation={activeStep === 3}
                showVisibility={false}
                onPublishModeChange={setPublishMode}
                onScheduleAt={setScheduledAt}
                onVisibilityChange={setVisibility}
                onToggleRights={(checked) => setMeta({ rightsAccepted: Boolean(checked) })}
                onPublish={() => {
                  setShowMobilePublishPanel(false)
                  handlePublish(canPublish, { mode: reviewSubmissionMode ? 'now' : publishMode, publishAt: reviewSubmissionMode ? null : scheduledAt, timezone: userTimezone, visibility, action: reviewSubmissionMode ? 'submit_review' : 'publish' })
                }}
                onCancel={() => {
                  setShowMobilePublishPanel(false)
                  handleCancel()
                }}
                onGoToStep={(s) => {
                  setShowMobilePublishPanel(false)
                  goToStep(s)
                }}
                allRootCategoryOptions={allRootCategoryOptions}
              />
            </motion.div>
          </>
        )}
      </AnimatePresence>
    </section>
  )
}
