import { useCallback, useReducer, useRef } from 'react'
import { emitUploadEvent } from '../../lib/uploadAnalytics'
import * as uploadEndpoints from '../../lib/uploadEndpoints'
import { mapUploadErrorNotice, mapUploadResultNotice } from '../../lib/uploadNotices'

// ─── Constants ──────────────────────────────────────────────────────────────
const DEFAULT_CHUNK_SIZE_BYTES = 5 * 1024 * 1024
const POLL_INTERVAL_MS = 2000

// ─── State machine ───────────────────────────────────────────────────────────
export 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,
  slug: 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: '', slug: action.slug ?? state.slug }
    case 'SCHEDULED':
      return { ...state, state: machineStates.complete, error: '', lastAction: 'schedule', slug: action.slug ?? state.slug }
    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
  }
}

// ─── Helpers ─────────────────────────────────────────────────────────────────
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()
  return direct || 'processing'
}

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

// ─── Hook ─────────────────────────────────────────────────────────────────────
/**
 * useUploadMachine
 *
 * Manages the full upload state machine lifecycle:
 * init → chunk → finish → poll → publish
 *
 * @param {object} opts
 * @param {File|null}   opts.primaryFile
 * @param {boolean}     opts.canStartUpload
 * @param {string}      opts.primaryType        'image' | 'archive' | 'unknown'
 * @param {boolean}     opts.isArchive
 * @param {number|null} opts.initialDraftId
 * @param {object}      opts.metadata           { title, description, tags, rightsAccepted, ... }
 * @param {number}      [opts.chunkSize]
 * @param {function}    [opts.onArtworkCreated] called with artworkId after draft creation
 */
export default function useUploadMachine({
  primaryFile,
  canStartUpload,
  primaryType,
  isArchive,
  initialDraftId = null,
  metadata,
  chunkSize = DEFAULT_CHUNK_SIZE_BYTES,
  onArtworkCreated,
  onNotice,
}) {
  const [machine, dispatchMachine] = useReducer(machineReducer, initialMachineState)

  const pollingTimerRef = useRef(null)
  const requestControllersRef = useRef(new Set())
  const publishLockRef = useRef(false)

  // Resolved artwork id (draft) created at the start of the upload
  const resolvedArtworkIdRef = useRef(
    (() => {
      const parsed = Number(initialDraftId)
      return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null
    })()
  )

  const effectiveChunkSize = (() => {
    const parsed = Number(chunkSize)
    return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : DEFAULT_CHUNK_SIZE_BYTES
  })()

  // ── Controller registry ────────────────────────────────────────────────────
  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((c) => c.abort())
    requestControllersRef.current.clear()
  }, [])

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

  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 pollProcessing = useCallback(async (sessionId, uploadToken) => {
    if (!sessionId) return
    try {
      const statusController = registerController()
      const payload = await fetchProcessingStatus(sessionId, uploadToken, statusController.signal)
      unregisterController(statusController)

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

      if (isReadyToPublishStatus(processingValue)) {
        dispatchMachine({ type: 'READY_TO_PUBLISH' })
        clearPolling()
      } 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 })
        onNotice?.({ type: 'error', message: failureMessage })
        clearPolling()
      }
    } catch (error) {
      if (error?.name === 'CanceledError' || error?.code === 'ERR_CANCELED') return
      const notice = mapUploadErrorNotice(error, 'Processing status check failed.')
      dispatchMachine({ type: 'ERROR', error: notice.message })
      onNotice?.(notice)
      emitUploadEvent('upload_error', { stage: 'processing_poll', message: notice.message })
      clearPolling()
    }
  }, [fetchProcessingStatus, registerController, unregisterController, clearPolling, onNotice])

  const startPolling = useCallback((sessionId, uploadToken) => {
    clearPolling()
    pollProcessing(sessionId, uploadToken)
    pollingTimerRef.current = window.setInterval(() => {
      pollProcessing(sessionId, uploadToken)
    }, POLL_INTERVAL_MS)
  }, [clearPolling, pollProcessing])

  // ── Core upload flow ───────────────────────────────────────────────────────
  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 {
      // 1. Create or reuse the artwork draft
      let artworkIdForUpload = resolvedArtworkIdRef.current
      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)
        resolvedArtworkIdRef.current = artworkIdForUpload
        onArtworkCreated?.(artworkIdForUpload)
      }

      // 2. Init upload session
      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' })

      // 3. Chunked upload
      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 })
      }

      // 4. Finish + start processing
      dispatchMachine({ type: 'FINISH_START' })

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

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

      const finishNotice = mapUploadResultNotice(finishResponse?.data || {}, {
        fallbackType: finishStatus === 'queued' ? 'warning' : 'success',
        fallbackMessage: finishStatus === 'queued'
          ? 'Upload received. Processing is queued.'
          : 'Upload completed successfully.',
      })
      onNotice?.(finishNotice)

      if (isReadyToPublishStatus(finishStatus)) {
        dispatchMachine({ type: 'READY_TO_PUBLISH' })
      } else {
        startPolling(sessionId, uploadToken)
      }

      emitUploadEvent('upload_complete', { session_id: sessionId, artwork_id: artworkIdForUpload })
    } catch (error) {
      if (error?.name === 'CanceledError' || error?.code === 'ERR_CANCELED') return
      const notice = mapUploadErrorNotice(error, 'Upload failed.')
      dispatchMachine({ type: 'ERROR', error: notice.message })
      onNotice?.(notice)
      emitUploadEvent('upload_error', { stage: 'upload_flow', message: notice.message })
    }
  }, [
    primaryFile,
    canStartUpload,
    primaryType,
    isArchive,
    metadata,
    effectiveChunkSize,
    registerController,
    unregisterController,
    clearPolling,
    startPolling,
    onArtworkCreated,
    onNotice,
  ])

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

    try {
      const { sessionId, uploadToken } = machine
      if (sessionId) {
        await window.axios.post(
          uploadEndpoints.cancel(),
          { session_id: sessionId, upload_token: uploadToken },
          { headers: uploadToken ? { 'X-Upload-Token': uploadToken } : undefined }
        )
      }
      dispatchMachine({ type: 'CANCELLED' })
      onNotice?.({ type: 'warning', message: 'Upload cancelled.' })
      emitUploadEvent('upload_cancel', { session_id: machine.sessionId || null })
    } catch (error) {
      const notice = mapUploadErrorNotice(error, 'Cancel failed.')
      dispatchMachine({ type: 'ERROR', error: notice.message })
      onNotice?.(notice)
      emitUploadEvent('upload_error', { stage: 'cancel', message: notice.message })
    }
  }, [machine, abortAllRequests, clearPolling, onNotice])

  // ── Publish ────────────────────────────────────────────────────────────────
  /**
   * handlePublish
   *
   * @param {boolean} canPublish
   * @param {{ mode?: 'now'|'schedule', publishAt?: string|null, timezone?: string, visibility?: string }} [opts]
   */
  const handlePublish = useCallback(async (canPublish, opts = {}) => {
    if (!canPublish || publishLockRef.current) return

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

    const { mode = 'now', publishAt = null, timezone = null, visibility = 'public' } = opts

    const resolvedCategoryId = metadata.subCategoryId || metadata.rootCategoryId || null

    const buildPayload = () => ({
      title: String(metadata.title || '').trim() || undefined,
      description: String(metadata.description || '').trim() || null,
      category: resolvedCategoryId ? String(resolvedCategoryId) : null,
      tags: Array.isArray(metadata.tags) ? metadata.tags : [],
      mode,
      ...(mode === 'schedule' && publishAt ? { publish_at: publishAt } : {}),
      ...(timezone ? { timezone } : {}),
      visibility,
    })

    try {
      const publishTargetId =
        resolvedArtworkIdRef.current || initialDraftId || machine.sessionId

      if (resolvedArtworkIdRef.current && resolvedArtworkIdRef.current > 0) {
        const publishController = registerController()
        const publishRes = await window.axios.post(
          uploadEndpoints.publish(String(resolvedArtworkIdRef.current)),
          buildPayload(),
          { signal: publishController.signal }
        )
        unregisterController(publishController)
        const publishedSlug = publishRes?.data?.slug ?? null
        dispatchMachine({ type: mode === 'schedule' ? 'SCHEDULED' : 'PUBLISH_SUCCESS', slug: publishedSlug })
        onNotice?.(mapUploadResultNotice(publishRes?.data || {}, {
          fallbackType: 'success',
          fallbackMessage: mode === 'schedule' ? 'Artwork scheduled successfully.' : 'Artwork published successfully.',
        }))
        emitUploadEvent('upload_publish', { id: publishTargetId, mode })
        return
      }

      if (!publishTargetId) throw new Error('Missing publish id.')

      const publishController = registerController()
      const publishRes2 = await window.axios.post(
        uploadEndpoints.publish(publishTargetId),
        buildPayload(),
        { signal: publishController.signal }
      )
      unregisterController(publishController)
      const publishedSlug2 = publishRes2?.data?.slug ?? null
      dispatchMachine({ type: mode === 'schedule' ? 'SCHEDULED' : 'PUBLISH_SUCCESS', slug: publishedSlug2 })
      onNotice?.(mapUploadResultNotice(publishRes2?.data || {}, {
        fallbackType: 'success',
        fallbackMessage: mode === 'schedule' ? 'Artwork scheduled successfully.' : 'Artwork published successfully.',
      }))
      emitUploadEvent('upload_publish', { id: publishTargetId, mode })
    } catch (error) {
      if (error?.name === 'CanceledError' || error?.code === 'ERR_CANCELED') return
      const notice = mapUploadErrorNotice(error, 'Publish failed.')
      dispatchMachine({ type: 'ERROR', error: notice.message })
      onNotice?.(notice)
      emitUploadEvent('upload_error', { stage: 'publish', message: notice.message })
    } finally {
      publishLockRef.current = false
    }
  }, [machine, initialDraftId, metadata, registerController, unregisterController, onNotice])

  // ── Reset ──────────────────────────────────────────────────────────────────
  const resetMachine = useCallback(() => {
    clearPolling()
    abortAllRequests()
    resolvedArtworkIdRef.current = (() => {
      const parsed = Number(initialDraftId)
      return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null
    })()
    publishLockRef.current = false
    dispatchMachine({ type: 'RESET_MACHINE' })
  }, [clearPolling, abortAllRequests, initialDraftId])

  // ── Retry ──────────────────────────────────────────────────────────────────
  /**
   * handleRetry
   *
   * Re-attempts the last action. When the last action was a publish/schedule,
   * opts must be forwarded so scheduled-publish options are not lost on retry.
   *
   * @param {boolean} canPublish
   * @param {{ mode?: string, publishAt?: string|null, timezone?: string, visibility?: string }} [opts]
   */
  const handleRetry = useCallback((canPublish, opts = {}) => {
    clearPolling()
    abortAllRequests()
    if (machine.lastAction === 'publish') {
      handlePublish(canPublish, opts)
      return
    }
    runUploadFlow()
  }, [machine.lastAction, handlePublish, runUploadFlow, clearPolling, abortAllRequests])

  // ── Cleanup on unmount ─────────────────────────────────────────────────────
  // (callers should call resetMachine or abortAllRequests on unmount if needed)

  return {
    machine,
    dispatchMachine,
    resolvedArtworkId: resolvedArtworkIdRef.current,
    runUploadFlow,
    handleCancel,
    handlePublish,
    handleRetry,
    resetMachine,
    clearPolling,
    abortAllRequests,
    startPolling,
  }
}
