import React, { useCallback, useEffect, useRef, useState } from 'react'
import { t } from 'i18next'
import {
  RiPauseCircleFill,
  RiPlayLargeFill,
} from '@remixicon/react'
import Toast from '@/app/components/base/toast'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
import cn from '@/utils/classnames'

type AudioPlayerProps = {
  src: string
}

const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
  const [isPlaying, setIsPlaying] = useState(false)
  const [currentTime, setCurrentTime] = useState(0)
  const [duration, setDuration] = useState(0)
  const [waveformData, setWaveformData] = useState<number[]>([])
  const [bufferedTime, setBufferedTime] = useState(0)
  const audioRef = useRef<HTMLAudioElement>(null)
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const [hasStartedPlaying, setHasStartedPlaying] = useState(false)
  const [hoverTime, setHoverTime] = useState(0)
  const [isAudioAvailable, setIsAudioAvailable] = useState(true)
  const { theme } = useTheme()

  useEffect(() => {
    const audio = audioRef.current
    if (!audio)
      return

    const handleError = () => {
      setIsAudioAvailable(false)
    }

    const setAudioData = () => {
      setDuration(audio.duration)
    }

    const setAudioTime = () => {
      setCurrentTime(audio.currentTime)
    }

    const handleProgress = () => {
      if (audio.buffered.length > 0)
        setBufferedTime(audio.buffered.end(audio.buffered.length - 1))
    }

    const handleEnded = () => {
      setIsPlaying(false)
    }

    audio.addEventListener('loadedmetadata', setAudioData)
    audio.addEventListener('timeupdate', setAudioTime)
    audio.addEventListener('progress', handleProgress)
    audio.addEventListener('ended', handleEnded)
    audio.addEventListener('error', handleError)

    // Preload audio metadata
    audio.load()

    // Delayed generation of waveform data
    // eslint-disable-next-line ts/no-use-before-define
    const timer = setTimeout(() => generateWaveformData(src), 1000)

    return () => {
      audio.removeEventListener('loadedmetadata', setAudioData)
      audio.removeEventListener('timeupdate', setAudioTime)
      audio.removeEventListener('progress', handleProgress)
      audio.removeEventListener('ended', handleEnded)
      audio.removeEventListener('error', handleError)
      clearTimeout(timer)
    }
  }, [src])

  const generateWaveformData = async (audioSrc: string) => {
    if (!window.AudioContext && !(window as any).webkitAudioContext) {
      setIsAudioAvailable(false)
      Toast.notify({
        type: 'error',
        message: 'Web Audio API is not supported in this browser',
      })
      return null
    }

    const url = new URL(src)
    const isHttp = url.protocol === 'http:' || url.protocol === 'https:'
    if (!isHttp) {
      setIsAudioAvailable(false)
      return null
    }

    const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
    const samples = 70

    try {
      const response = await fetch(audioSrc, { mode: 'cors' })
      if (!response || !response.ok) {
        setIsAudioAvailable(false)
        return null
      }

      const arrayBuffer = await response.arrayBuffer()
      const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
      const channelData = audioBuffer.getChannelData(0)
      const blockSize = Math.floor(channelData.length / samples)
      const waveformData: number[] = []

      for (let i = 0; i < samples; i++) {
        let sum = 0
        for (let j = 0; j < blockSize; j++)
          sum += Math.abs(channelData[i * blockSize + j])

        // Apply nonlinear scaling to enhance small amplitudes
        waveformData.push((sum / blockSize) * 5)
      }

      // Normalized waveform data
      const maxAmplitude = Math.max(...waveformData)
      const normalizedWaveform = waveformData.map(amp => amp / maxAmplitude)

      setWaveformData(normalizedWaveform)
      setIsAudioAvailable(true)
    }
    catch {
      const waveform: number[] = []
      let prevValue = Math.random()

      for (let i = 0; i < samples; i++) {
        const targetValue = Math.random()
        const interpolatedValue = prevValue + (targetValue - prevValue) * 0.3
        waveform.push(interpolatedValue)
        prevValue = interpolatedValue
      }

      const maxAmplitude = Math.max(...waveform)
      const randomWaveform = waveform.map(amp => amp / maxAmplitude)

      setWaveformData(randomWaveform)
      setIsAudioAvailable(true)
    }
    finally {
      await audioContext.close()
    }
  }

  const togglePlay = useCallback(() => {
    const audio = audioRef.current
    if (audio && isAudioAvailable) {
      if (isPlaying) {
        setHasStartedPlaying(false)
        audio.pause()
      }
      else {
        setHasStartedPlaying(true)
        audio.play().catch(error => console.error('Error playing audio:', error))
      }

      setIsPlaying(!isPlaying)
    }
    else {
      Toast.notify({
        type: 'error',
        message: 'Audio element not found',
      })
      setIsAudioAvailable(false)
    }
  }, [isAudioAvailable, isPlaying])

  const handleCanvasInteraction = useCallback((e: React.MouseEvent | React.TouchEvent) => {
    e.preventDefault()

    const getClientX = (event: React.MouseEvent | React.TouchEvent): number => {
      if ('touches' in event)
        return event.touches[0].clientX
      return event.clientX
    }

    const updateProgress = (clientX: number) => {
      const canvas = canvasRef.current
      const audio = audioRef.current
      if (!canvas || !audio)
        return

      const rect = canvas.getBoundingClientRect()
      const percent = Math.min(Math.max(0, clientX - rect.left), rect.width) / rect.width
      const newTime = percent * duration

      // Removes the buffer check, allowing drag to any location
      audio.currentTime = newTime
      setCurrentTime(newTime)

      if (!isPlaying) {
        setIsPlaying(true)
        audio.play().catch((error) => {
          Toast.notify({
            type: 'error',
            message: `Error playing audio: ${error}`,
          })
          setIsPlaying(false)
        })
      }
    }

    updateProgress(getClientX(e))
  }, [duration, isPlaying])

  const formatTime = (time: number) => {
    const minutes = Math.floor(time / 60)
    const seconds = Math.floor(time % 60)
    return `${minutes}:${seconds.toString().padStart(2, '0')}`
  }

  const drawWaveform = useCallback(() => {
    const canvas = canvasRef.current
    if (!canvas)
      return

    const ctx = canvas.getContext('2d')
    if (!ctx)
      return

    const width = canvas.width
    const height = canvas.height
    const data = waveformData

    ctx.clearRect(0, 0, width, height)

    const barWidth = width / data.length
    const playedWidth = (currentTime / duration) * width
    const cornerRadius = 2

    // Draw waveform bars
    data.forEach((value, index) => {
      let color

      if (index * barWidth <= playedWidth)
        color = theme === Theme.light ? '#296DFF' : '#84ABFF'
      else if ((index * barWidth / width) * duration <= hoverTime)
        color = theme === Theme.light ? 'rgba(21,90,239,.40)' : 'rgba(200, 206, 218, 0.28)'
      else
        color = theme === Theme.light ? 'rgba(21,90,239,.20)' : 'rgba(200, 206, 218, 0.14)'

      const barHeight = value * height
      const rectX = index * barWidth
      const rectY = (height - barHeight) / 2
      const rectWidth = barWidth * 0.5
      const rectHeight = barHeight

      ctx.lineWidth = 1
      ctx.fillStyle = color
      if (ctx.roundRect) {
        ctx.beginPath()
        ctx.roundRect(rectX, rectY, rectWidth, rectHeight, cornerRadius)
        ctx.fill()
      }
      else {
        ctx.fillRect(rectX, rectY, rectWidth, rectHeight)
      }
    })
  }, [currentTime, duration, hoverTime, theme, waveformData])

  useEffect(() => {
    drawWaveform()
  }, [drawWaveform, bufferedTime, hasStartedPlaying])

  const handleMouseMove = useCallback((e: React.MouseEvent) => {
    const canvas = canvasRef.current
    const audio = audioRef.current
    if (!canvas || !audio)
      return

    const rect = canvas.getBoundingClientRect()
    const percent = Math.min(Math.max(0, e.clientX - rect.left), rect.width) / rect.width
    const time = percent * duration

    // Check if the hovered position is within a buffered range before updating hoverTime
    for (let i = 0; i < audio.buffered.length; i++) {
      if (time >= audio.buffered.start(i) && time <= audio.buffered.end(i)) {
        setHoverTime(time)
        break
      }
    }
  }, [duration])

  return (
    <div className='flex h-9 min-w-[240px] max-w-[420px] items-end gap-2 rounded-[10px] border border-components-panel-border-subtle bg-components-chat-input-audio-bg-alt p-2 shadow-xs backdrop-blur-sm'>
      <audio ref={audioRef} src={src} preload="auto"/>
      <button type="button" className='inline-flex shrink-0 cursor-pointer items-center justify-center border-none text-text-accent transition-all hover:text-text-accent-secondary disabled:text-components-button-primary-bg-disabled' onClick={togglePlay} disabled={!isAudioAvailable}>
        {isPlaying
          ? (
            <RiPauseCircleFill className='h-5 w-5' />
          )
          : (
            <RiPlayLargeFill className='h-5 w-5' />
          )}
      </button>
      <div className={cn(isAudioAvailable && 'grow')} hidden={!isAudioAvailable}>
        <div className='flex h-8 items-center justify-center'>
          <canvas
            ref={canvasRef}
            className='relative flex h-6 w-full grow cursor-pointer items-center justify-center'
            onClick={handleCanvasInteraction}
            onMouseMove={handleMouseMove}
            onMouseDown={handleCanvasInteraction}
          />
          <div className='system-xs-medium inline-flex min-w-[50px] items-center justify-center text-text-accent-secondary'>
            <span className='rounded-[10px] px-0.5 py-1'>{formatTime(duration)}</span>
          </div>
        </div>
      </div>
      <div className='absolute left-0 top-0 flex h-full w-full items-center justify-center text-text-quaternary' hidden={isAudioAvailable}>{t('common.operation.audioSourceUnavailable')}</div>
    </div>
  )
}

export default AudioPlayer
