/**
 * Unified audio player interface used by ArrowGame.
 *
 * Two implementations:
 * - LocalAudioPlayer: wraps HTMLAudioElement (existing behaviour)
 * - YouTubeAudioPlayer: wraps the YouTube IFrame Player API
 */

export interface AudioPlayer {
  play(): Promise<void>
  pause(): void
  destroy(): void
  /** Current playback position in seconds (from the game's perspective, offset-adjusted). */
  readonly currentTime: number
  /** Total duration of the playable segment in seconds. */
  readonly duration: number
  /** True once the player can begin playback without further buffering. */
  readonly ready: boolean
  /** Register a callback for when playback reaches the end. Returns an unsubscribe fn. */
  onEnded(cb: () => void): () => void
}

// ---------------------------------------------------------------------------
// Local HTML5 Audio
// ---------------------------------------------------------------------------

export function createLocalAudioPlayer(url: string): AudioPlayer {
  const audio = new Audio(url)
  audio.preload = 'auto'
  let isReady = false
  const endedCbs = new Set<() => void>()

  audio.addEventListener('canplaythrough', () => { isReady = true })
  audio.addEventListener('ended', () => { for (const cb of endedCbs) cb() })

  return {
    play: () => audio.play(),
    pause: () => audio.pause(),
    destroy() {
      audio.pause()
      audio.src = ''
      endedCbs.clear()
    },
    get currentTime() { return audio.currentTime },
    get duration() { return audio.duration || 0 },
    get ready() { return isReady },
    onEnded(cb) {
      endedCbs.add(cb)
      return () => { endedCbs.delete(cb) }
    },
  }
}

// ---------------------------------------------------------------------------
// YouTube IFrame Player
// ---------------------------------------------------------------------------

/** Global promise that resolves once the YT IFrame API script is loaded. */
let ytApiReady: Promise<void> | null = null

export function loadYouTubeApi(): Promise<void> {
  if (ytApiReady) return ytApiReady
  ytApiReady = new Promise<void>((resolve) => {
    // API already loaded (e.g. from a previous session)?
    if (window.YT?.Player) { resolve(); return }

    // The API calls this global when ready
    const prev = window.onYouTubeIframeAPIReady
    window.onYouTubeIframeAPIReady = () => {
      prev?.()
      resolve()
    }

    const tag = document.createElement('script')
    tag.src = 'https://www.youtube.com/iframe_api'
    document.head.appendChild(tag)
  })
  return ytApiReady
}

export interface YouTubeAudioPlayerOptions {
  videoId: string
  /** Seconds into the YouTube video where the game audio's t=0 begins. */
  offset: number
  /** DOM element to mount the hidden iframe into. */
  host: HTMLElement
}

export async function createYouTubeAudioPlayer(
  opts: YouTubeAudioPlayerOptions,
): Promise<AudioPlayer> {
  await loadYouTubeApi()

  const { videoId, offset, host } = opts
  const endedCbs = new Set<() => void>()
  let isReady = false
  let destroyed = false
  // Duration of the playable segment (from offset to end of video, or video duration - offset)
  let segmentDuration = 0

  const player = await new Promise<YT.Player>((resolve) => {
    const div = document.createElement('div')
    host.appendChild(div)

    const p = new YT.Player(div, {
      videoId,
      width: '100%',
      height: '100%',
      playerVars: {
        autoplay: 0,
        controls: 0,
        disablekb: 1,
        fs: 0,
        modestbranding: 1,
        rel: 0,
        playsinline: 1,
        // Start near the offset so buffering is faster
        start: Math.max(0, Math.floor(offset) - 1),
      },
      events: {
        onReady() {
          segmentDuration = Math.max(0, p.getDuration() - offset)
          isReady = true
          resolve(p)
        },
        onStateChange(e: YT.OnStateChangeEvent) {
          if (e.data === YT.PlayerState.ENDED) {
            for (const cb of endedCbs) cb()
          }
        },
      },
    })
  })

  return {
    async play() {
      if (destroyed) return
      // Seek to the correct position before playing.
      // If we haven't started yet, seek to the offset.
      const cur = player.getCurrentTime()
      if (cur < offset - 0.5) {
        player.seekTo(offset, true)
      }
      player.playVideo()
      // Wait until actually playing (may need to buffer)
      await new Promise<void>((resolve) => {
        const check = () => {
          if (destroyed) { resolve(); return }
          if (player.getPlayerState() === YT.PlayerState.PLAYING) {
            resolve()
          } else {
            setTimeout(check, 50)
          }
        }
        // If already playing, resolve immediately
        if (player.getPlayerState() === YT.PlayerState.PLAYING) {
          resolve()
        } else {
          check()
        }
      })
    },
    pause() {
      if (!destroyed) player.pauseVideo()
    },
    destroy() {
      destroyed = true
      endedCbs.clear()
      try { player.destroy() } catch { /* ignore */ }
    },
    get currentTime() {
      if (destroyed) return 0
      return Math.max(0, player.getCurrentTime() - offset)
    },
    get duration() { return segmentDuration },
    get ready() { return isReady },
    onEnded(cb) {
      endedCbs.add(cb)
      return () => { endedCbs.delete(cb) }
    },
  }
}
