'use client'

import React, { createContext, useContext, useReducer, useRef, useEffect, useCallback, useState } from 'react'
import { getTrackBlob } from '@/lib/offline-db'
import { precacheUpcoming } from '@/lib/precache'

interface TrackBase {
  id: string
  title: string
  artist: string
  album: string
  albumId: string
  duration: number
  coverArt: string
}

export interface MusicTrack extends TrackBase {
  type?: 'music'
}

export interface RadioTrack extends TrackBase {
  type: 'radio'
  streamUrl: string
}

export type Track = MusicTrack | RadioTrack

export function isRadioTrack(track: Track): track is RadioTrack {
  return track.type === 'radio'
}

interface PlayerState {
  currentTrack: Track | null
  queue: Track[]
  queueIndex: number
  isPlaying: boolean
  progress: number
  duration: number
  volume: number
  isFullScreen: boolean
  shuffleEnabled: boolean
  originalQueue: Track[] | null
}

type PlayerAction =
  | { type: 'PLAY_TRACK'; track: Track; queue?: Track[]; index?: number }
  | { type: 'TOGGLE_PLAY' }
  | { type: 'SET_PLAYING'; playing: boolean }
  | { type: 'SET_PROGRESS'; progress: number }
  | { type: 'SET_DURATION'; duration: number }
  | { type: 'SET_VOLUME'; volume: number }
  | { type: 'NEXT_TRACK' }
  | { type: 'PREV_TRACK' }
  | { type: 'SET_FULLSCREEN'; open: boolean }
  | { type: 'ADD_TO_QUEUE'; track: Track }
  | { type: 'ADD_ALL_TO_QUEUE'; tracks: Track[] }
  | { type: 'REMOVE_FROM_QUEUE'; index: number }
  | { type: 'MOVE_IN_QUEUE'; from: number; to: number }
  | { type: 'JUMP_TO'; index: number }
  | { type: 'CLEAR_QUEUE' }
  | { type: 'TOGGLE_SHUFFLE' }

const initialState: PlayerState = {
  currentTrack: null,
  queue: [],
  queueIndex: -1,
  isPlaying: false,
  progress: 0,
  duration: 0,
  volume: 0.8,
  isFullScreen: false,
  shuffleEnabled: false,
  originalQueue: null,
}

function playerReducer(state: PlayerState, action: PlayerAction): PlayerState {
  switch (action.type) {
    case 'PLAY_TRACK': {
      const queue = action.queue || [action.track]
      const index = action.index ?? 0
      return { ...state, currentTrack: action.track, queue, queueIndex: index, isPlaying: true, progress: 0, shuffleEnabled: false, originalQueue: null }
    }
    case 'TOGGLE_PLAY':
      return { ...state, isPlaying: !state.isPlaying }
    case 'SET_PLAYING':
      return { ...state, isPlaying: action.playing }
    case 'SET_PROGRESS':
      return { ...state, progress: action.progress }
    case 'SET_DURATION':
      return { ...state, duration: action.duration }
    case 'SET_VOLUME':
      return { ...state, volume: action.volume }
    case 'NEXT_TRACK': {
      const next = state.queueIndex + 1
      if (next >= state.queue.length) return { ...state, isPlaying: false }
      return { ...state, currentTrack: state.queue[next], queueIndex: next, isPlaying: true, progress: 0 }
    }
    case 'PREV_TRACK': {
      // If > 3s in, restart current track
      if (state.progress > 3) return { ...state, progress: 0 }
      const prev = state.queueIndex - 1
      if (prev < 0) return { ...state, progress: 0 }
      return { ...state, currentTrack: state.queue[prev], queueIndex: prev, isPlaying: true, progress: 0 }
    }
    case 'SET_FULLSCREEN':
      return { ...state, isFullScreen: action.open }
    case 'ADD_TO_QUEUE': {
      // Insert after current track so it plays next
      const insertAt = state.queueIndex + 1
      const q2 = [...state.queue]
      q2.splice(insertAt, 0, action.track)
      // If shuffle is on, also add to originalQueue at end
      const origQ = state.originalQueue ? [...state.originalQueue, action.track] : null
      return { ...state, queue: q2, originalQueue: origQ }
    }
    case 'ADD_ALL_TO_QUEUE': {
      const insertAt = state.queueIndex + 1
      const q3 = [...state.queue]
      q3.splice(insertAt, 0, ...action.tracks)
      const origQ2 = state.originalQueue ? [...state.originalQueue, ...action.tracks] : null
      return { ...state, queue: q3, originalQueue: origQ2 }
    }
    case 'REMOVE_FROM_QUEUE': {
      const newQueue = [...state.queue]
      newQueue.splice(action.index, 1)
      let newIndex = state.queueIndex
      if (action.index < state.queueIndex) {
        newIndex--
      } else if (action.index === state.queueIndex) {
        // Removing current track: play next (or clamp)
        const clamped = Math.min(newIndex, newQueue.length - 1)
        if (clamped < 0) return { ...state, queue: [], queueIndex: -1, currentTrack: null, isPlaying: false }
        return { ...state, queue: newQueue, queueIndex: clamped, currentTrack: newQueue[clamped], progress: 0 }
      }
      return { ...state, queue: newQueue, queueIndex: newIndex }
    }
    case 'MOVE_IN_QUEUE': {
      const q = [...state.queue]
      const [moved] = q.splice(action.from, 1)
      q.splice(action.to, 0, moved)
      // Track the currently-playing song's new position
      let idx = state.queueIndex
      if (action.from === idx) {
        idx = action.to
      } else {
        if (action.from < idx) idx--
        if (action.to <= idx) idx++
      }
      return { ...state, queue: q, queueIndex: idx }
    }
    case 'JUMP_TO': {
      if (action.index < 0 || action.index >= state.queue.length) return state
      return { ...state, currentTrack: state.queue[action.index], queueIndex: action.index, isPlaying: true, progress: 0 }
    }
    case 'CLEAR_QUEUE': {
      // Keep current track, remove everything after it
      if (state.queueIndex < 0 || !state.currentTrack) return { ...state, queue: [], queueIndex: -1 }
      return { ...state, queue: [state.currentTrack], queueIndex: 0 }
    }
    case 'TOGGLE_SHUFFLE': {
      if (state.shuffleEnabled) {
        // OFF: restore original order, find current track in it
        if (!state.originalQueue) return { ...state, shuffleEnabled: false }
        const currentId = state.currentTrack?.id
        const idx = state.originalQueue.findIndex((t) => t.id === currentId)
        return { ...state, shuffleEnabled: false, queue: state.originalQueue, queueIndex: idx >= 0 ? idx : 0, originalQueue: null }
      } else {
        // ON: save original queue, Fisher-Yates shuffle items after current index
        const shuffled = [...state.queue]
        for (let i = shuffled.length - 1; i > state.queueIndex + 1; i--) {
          const j = state.queueIndex + 1 + Math.floor(Math.random() * (i - state.queueIndex))
          ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
        }
        return { ...state, shuffleEnabled: true, originalQueue: [...state.queue], queue: shuffled }
      }
    }
    default:
      return state
  }
}

export interface AudioOutputDevice {
  deviceId: string
  label: string
}

interface MusicContextValue {
  state: PlayerState
  playTrack: (track: Track, queue?: Track[], index?: number) => void
  togglePlay: () => void
  seek: (time: number) => void
  setVolume: (volume: number) => void
  nextTrack: () => void
  prevTrack: () => void
  setFullScreen: (open: boolean) => void
  addToQueue: (track: Track) => void
  addAllToQueue: (tracks: Track[]) => void
  removeFromQueue: (index: number) => void
  moveInQueue: (from: number, to: number) => void
  jumpTo: (index: number) => void
  clearQueue: () => void
  toggleShuffle: () => void
  outputDevices: AudioOutputDevice[]
  currentOutputId: string
  setOutputDevice: (deviceId: string) => void
}

const MusicContext = createContext<MusicContextValue | null>(null)

export function useMusicPlayer() {
  const ctx = useContext(MusicContext)
  if (!ctx) throw new Error('useMusicPlayer must be used within MusicProvider')
  return ctx
}

export function MusicProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(playerReducer, initialState)
  const audioRef = useRef<HTMLAudioElement | null>(null)
  // Guard against pause events fired when changing audio.src during track transitions
  const transitioningRef = useRef(false)

  // Create audio element on mount (client only)
  useEffect(() => {
    const audio = new Audio()
    audio.preload = 'auto'
    audioRef.current = audio

    const onTimeUpdate = () => dispatch({ type: 'SET_PROGRESS', progress: audio.currentTime })
    const onDurationChange = () => dispatch({ type: 'SET_DURATION', duration: audio.duration || 0 })
    const onEnded = () => {
      transitioningRef.current = true
      dispatch({ type: 'NEXT_TRACK' })
    }
    const onPause = () => {
      // Ignore pause events during track transitions (browser fires pause when src changes)
      if (transitioningRef.current) return
      dispatch({ type: 'SET_PLAYING', playing: false })
    }
    const onPlay = () => dispatch({ type: 'SET_PLAYING', playing: true })

    audio.addEventListener('timeupdate', onTimeUpdate)
    audio.addEventListener('durationchange', onDurationChange)
    audio.addEventListener('ended', onEnded)
    audio.addEventListener('pause', onPause)
    audio.addEventListener('play', onPlay)

    return () => {
      audio.removeEventListener('timeupdate', onTimeUpdate)
      audio.removeEventListener('durationchange', onDurationChange)
      audio.removeEventListener('ended', onEnded)
      audio.removeEventListener('pause', onPause)
      audio.removeEventListener('play', onPlay)
      audio.pause()
      audio.src = ''
    }
  }, [])

  // Retry play with exponential backoff (handles mobile autoplay restrictions)
  const playWithRetry = useCallback((audio: HTMLAudioElement, attempts = 3) => {
    audio.play().catch((err) => {
      if (attempts > 1) {
        setTimeout(() => playWithRetry(audio, attempts - 1), 500)
      } else {
        console.warn('Autoplay blocked after retries:', err.message)
      }
    })
  }, [])

  // Track blob URL for cleanup
  const blobUrlRef = useRef<string | null>(null)

  // When currentTrack changes, update audio src (offline-first)
  useEffect(() => {
    const audio = audioRef.current
    if (!audio || !state.currentTrack) return
    const trackId = state.currentTrack.id
    transitioningRef.current = true

    // Revoke previous blob URL
    if (blobUrlRef.current) {
      URL.revokeObjectURL(blobUrlRef.current)
      blobUrlRef.current = null
    }

    // Radio streams: use streamUrl directly, skip offline/IndexedDB
    if (isRadioTrack(state.currentTrack)) {
      audio.src = state.currentTrack.streamUrl
      playWithRetry(audio)
      requestAnimationFrame(() => { transitioningRef.current = false })
      return
    }

    // Music: try offline first, fall back to streaming
    getTrackBlob(trackId).then((blob) => {
      // Guard: track may have changed while we awaited
      if (state.currentTrack?.id !== trackId) return
      if (blob) {
        const url = URL.createObjectURL(blob)
        blobUrlRef.current = url
        audio.src = url
      } else {
        audio.src = `/api/music/stream/${trackId}`
      }
      playWithRetry(audio)
      requestAnimationFrame(() => { transitioningRef.current = false })
    }).catch(() => {
      // IndexedDB unavailable, fall back to streaming
      if (state.currentTrack?.id !== trackId) return
      audio.src = `/api/music/stream/${trackId}`
      playWithRetry(audio)
      requestAnimationFrame(() => { transitioningRef.current = false })
    })
  }, [state.currentTrack?.id, playWithRetry]) // eslint-disable-line react-hooks/exhaustive-deps

  // Sync play/pause
  useEffect(() => {
    const audio = audioRef.current
    if (!audio || !state.currentTrack) return
    if (state.isPlaying) {
      playWithRetry(audio)
    } else {
      audio.pause()
    }
  }, [state.isPlaying, state.currentTrack, playWithRetry])

  // Sync volume
  useEffect(() => {
    if (audioRef.current) audioRef.current.volume = state.volume
  }, [state.volume])

  // MediaSession API
  useEffect(() => {
    if (!state.currentTrack || !('mediaSession' in navigator)) return
    navigator.mediaSession.metadata = new MediaMetadata({
      title: state.currentTrack.title,
      artist: state.currentTrack.artist,
      album: state.currentTrack.album,
      artwork: state.currentTrack.coverArt
        ? [{ src: `/api/music/cover/${state.currentTrack.coverArt}?size=300`, sizes: '300x300', type: 'image/jpeg' }]
        : [],
    })
    navigator.mediaSession.setActionHandler('play', () => dispatch({ type: 'SET_PLAYING', playing: true }))
    navigator.mediaSession.setActionHandler('pause', () => dispatch({ type: 'SET_PLAYING', playing: false }))
    navigator.mediaSession.setActionHandler('previoustrack', () => dispatch({ type: 'PREV_TRACK' }))
    navigator.mediaSession.setActionHandler('nexttrack', () => dispatch({ type: 'NEXT_TRACK' }))
  }, [state.currentTrack])

  // Pre-cache next 3 tracks in queue when current track changes (skip for radio)
  useEffect(() => {
    if (state.queueIndex < 0 || state.queue.length <= state.queueIndex + 1) return
    if (state.currentTrack && isRadioTrack(state.currentTrack)) return

    const ac = new AbortController()
    const delay = setTimeout(() => {
      precacheUpcoming(state.queue, state.queueIndex, 3, ac.signal)
    }, 2000)

    return () => {
      clearTimeout(delay)
      ac.abort()
    }
  }, [state.queueIndex, state.queue])

  // Keyboard shortcut: Space = play/pause (only when not in input)
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.code === 'Space' && !['INPUT', 'TEXTAREA', 'SELECT'].includes((e.target as HTMLElement).tagName)) {
        e.preventDefault()
        dispatch({ type: 'TOGGLE_PLAY' })
      }
    }
    window.addEventListener('keydown', handler)
    return () => window.removeEventListener('keydown', handler)
  }, [])

  // Audio output device selection
  const [outputDevices, setOutputDevices] = useState<AudioOutputDevice[]>([])
  const [currentOutputId, setCurrentOutputId] = useState('')

  useEffect(() => {
    if (typeof navigator === 'undefined' || !navigator.mediaDevices?.enumerateDevices) return

    const enumerate = () => {
      navigator.mediaDevices.enumerateDevices().then((devices) => {
        const outputs = devices
          .filter((d) => d.kind === 'audiooutput')
          .map((d) => ({
            deviceId: d.deviceId,
            label: d.label || (d.deviceId === 'default' ? 'Default' : `Device ${d.deviceId.slice(0, 6)}`),
          }))
        setOutputDevices(outputs)
      }).catch(() => {})
    }

    enumerate()
    navigator.mediaDevices.addEventListener('devicechange', enumerate)
    return () => navigator.mediaDevices.removeEventListener('devicechange', enumerate)
  }, [])

  const setOutputDevice = useCallback((deviceId: string) => {
    const audio = audioRef.current as HTMLAudioElement & { setSinkId?: (id: string) => Promise<void> }
    if (!audio?.setSinkId) return
    audio.setSinkId(deviceId).then(() => {
      setCurrentOutputId(deviceId)
    }).catch((err) => {
      console.warn('Failed to set audio output:', err.message)
    })
  }, [])

  const playTrack = useCallback((track: Track, queue?: Track[], index?: number) => {
    dispatch({ type: 'PLAY_TRACK', track, queue, index })
  }, [])

  const togglePlay = useCallback(() => dispatch({ type: 'TOGGLE_PLAY' }), [])

  const seek = useCallback((time: number) => {
    if (audioRef.current) {
      audioRef.current.currentTime = time
      dispatch({ type: 'SET_PROGRESS', progress: time })
    }
  }, [])

  const setVolume = useCallback((v: number) => dispatch({ type: 'SET_VOLUME', volume: v }), [])
  const nextTrack = useCallback(() => dispatch({ type: 'NEXT_TRACK' }), [])
  const prevTrack = useCallback(() => dispatch({ type: 'PREV_TRACK' }), [])
  const setFullScreen = useCallback((open: boolean) => dispatch({ type: 'SET_FULLSCREEN', open }), [])
  const addToQueue = useCallback((track: Track) => dispatch({ type: 'ADD_TO_QUEUE', track }), [])
  const addAllToQueue = useCallback((tracks: Track[]) => dispatch({ type: 'ADD_ALL_TO_QUEUE', tracks }), [])
  const removeFromQueue = useCallback((index: number) => dispatch({ type: 'REMOVE_FROM_QUEUE', index }), [])
  const moveInQueue = useCallback((from: number, to: number) => dispatch({ type: 'MOVE_IN_QUEUE', from, to }), [])
  const jumpTo = useCallback((index: number) => dispatch({ type: 'JUMP_TO', index }), [])
  const clearQueue = useCallback(() => dispatch({ type: 'CLEAR_QUEUE' }), [])
  const toggleShuffle = useCallback(() => dispatch({ type: 'TOGGLE_SHUFFLE' }), [])

  return (
    <MusicContext.Provider value={{
      state,
      playTrack,
      togglePlay,
      seek,
      setVolume,
      nextTrack,
      prevTrack,
      setFullScreen,
      addToQueue,
      addAllToQueue,
      removeFromQueue,
      moveInQueue,
      jumpTo,
      clearQueue,
      toggleShuffle,
      outputDevices,
      currentOutputId,
      setOutputDevice,
    }}>
      {children}
    </MusicContext.Provider>
  )
}
