import type { Ref } from 'react'
import type { VoiceRecorder } from './recorder'
import type { SpeechToTextTarget } from './types'
import { cn } from '@langgenius/dify-ui/cn'
import { useCallback, useEffect, useEffectEvent, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { transcribeAudio } from './api'
import s from './index.module.css'
import { startVoiceRecorder } from './recorder'

const MAX_RECORDING_DURATION = 600

type VoiceInputStatus = 'starting' | 'recording' | 'converting'

type VoiceInputProps = {
  ref?: Ref<HTMLDivElement>
  onConverted: (text: string) => void
  onCancel: () => void
  onBeforeTranscribe?: () => Promise<unknown>
  onError?: () => void
  onStartError?: (error: unknown) => void
  target: SpeechToTextTarget
}

function VoiceInput({
  ref,
  onCancel,
  onBeforeTranscribe,
  onConverted,
  onError,
  onStartError,
  target,
}: VoiceInputProps) {
  const { t } = useTranslation()
  const recorderRef = useRef<VoiceRecorder | null>(null)
  const canvasRef = useRef<HTMLCanvasElement | null>(null)
  const canvasContextRef = useRef<CanvasRenderingContext2D | null>(null)
  const canvasSizeRef = useRef({ height: 0, width: 0 })
  const drawRecordIdRef = useRef<number | null>(null)
  const mountedRef = useRef(true)
  const stopRequestedRef = useRef(false)
  const cancelledRef = useRef(false)
  const setupAbortControllerRef = useRef<AbortController | null>(null)
  const transcriptionAbortControllerRef = useRef<AbortController | null>(null)
  const [duration, setDuration] = useState(0)
  const [status, setStatus] = useState<VoiceInputStatus>('starting')
  const handleRecorderStartError = useEffectEvent((error: unknown) => {
    onStartError?.(error)
    onCancel()
  })

  const stopDrawing = useCallback(() => {
    if (drawRecordIdRef.current !== null) cancelAnimationFrame(drawRecordIdRef.current)
    drawRecordIdRef.current = null
    const { height, width } = canvasSizeRef.current
    canvasContextRef.current?.clearRect(0, 0, width, height)
  }, [])

  const drawRecord = useCallback(() => {
    const recorder = recorderRef.current
    const context = canvasContextRef.current
    const { height, width } = canvasSizeRef.current
    if (!recorder || !context || !height || !width) return

    const frequencyData = new Uint8Array(recorder.analyser.frequencyBinCount)
    recorder.analyser.getByteFrequencyData(frequencyData)
    const lineCount = Math.max(1, Math.floor(width / 3))
    const sampleStep = Math.max(1, Math.floor(frequencyData.length / lineCount))

    context.clearRect(0, 0, width, height)
    context.beginPath()
    for (let index = 0; index < lineCount; index++) {
      const amplitude = frequencyData[index * sampleStep] ?? 0
      const lineHeight = Math.max(1, (amplitude / 255) * height)
      const x = index * 3
      if (context.roundRect) context.roundRect(x, height - lineHeight, 2, lineHeight, [1, 1, 0, 0])
      else context.rect(x, height - lineHeight, 2, lineHeight)
    }
    context.fill()
    context.closePath()
    drawRecordIdRef.current = requestAnimationFrame(drawRecord)
  }, [])

  const handleStopRecorder = useCallback(async () => {
    const recorder = recorderRef.current
    if (!recorder || status !== 'recording' || stopRequestedRef.current) return

    stopRequestedRef.current = true
    setStatus('converting')
    stopDrawing()
    let mp3Blob: Blob
    try {
      mp3Blob = await recorder.stop()
    } catch {
      if (mountedRef.current && !cancelledRef.current) {
        onError?.()
        onCancel()
      }
      return
    }
    if (cancelledRef.current) return

    try {
      await onBeforeTranscribe?.()
    } catch {
      if (mountedRef.current && !cancelledRef.current) onCancel()
      return
    }
    if (cancelledRef.current) return

    const file = new File([mp3Blob], 'temp.mp3', { type: 'audio/mp3' })
    const abortController = new AbortController()
    transcriptionAbortControllerRef.current = abortController
    try {
      const audioResponse = await transcribeAudio(target, file, abortController.signal)
      if (mountedRef.current && !cancelledRef.current) onConverted(audioResponse.text)
    } catch {
      if (mountedRef.current && !cancelledRef.current) onError?.()
    } finally {
      transcriptionAbortControllerRef.current = null
      if (mountedRef.current && !cancelledRef.current) onCancel()
    }
  }, [onBeforeTranscribe, onCancel, onConverted, onError, status, stopDrawing, target])

  const handleCancel = useCallback(() => {
    cancelledRef.current = true
    setupAbortControllerRef.current?.abort()
    transcriptionAbortControllerRef.current?.abort()
    void recorderRef.current?.cancel()
    onCancel()
  }, [onCancel])

  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return
    const devicePixelRatio = window.devicePixelRatio || 1
    const { height, width } = canvas.getBoundingClientRect()
    canvas.width = devicePixelRatio * width
    canvas.height = devicePixelRatio * height
    canvasSizeRef.current = { height, width }
    const context = canvas.getContext('2d')
    if (!context) return
    context.scale(devicePixelRatio, devicePixelRatio)
    context.fillStyle = 'rgba(209, 224, 255, 1)'
    canvasContextRef.current = context
  }, [])

  useEffect(() => {
    mountedRef.current = true
    cancelledRef.current = false
    let effectCancelled = false
    const abortController = new AbortController()
    setupAbortControllerRef.current = abortController
    void startVoiceRecorder(abortController.signal)
      .then((recorder) => {
        if (setupAbortControllerRef.current === abortController)
          setupAbortControllerRef.current = null
        if (effectCancelled) {
          void recorder.cancel()
          return
        }
        recorderRef.current = recorder
        setStatus('recording')
        drawRecord()
      })
      .catch((error: unknown) => {
        if (setupAbortControllerRef.current === abortController)
          setupAbortControllerRef.current = null
        if (effectCancelled || abortController.signal.aborted) return
        handleRecorderStartError(error)
      })

    return () => {
      effectCancelled = true
      mountedRef.current = false
      cancelledRef.current = true
      abortController.abort()
      transcriptionAbortControllerRef.current?.abort()
      stopDrawing()
      void recorderRef.current?.cancel()
    }
  }, [drawRecord, stopDrawing])

  useEffect(() => {
    if (status !== 'recording') return
    const intervalId = window.setInterval(() => {
      setDuration((currentDuration) => currentDuration + 1)
    }, 1000)
    return () => window.clearInterval(intervalId)
  }, [status])

  useEffect(() => {
    if (duration >= MAX_RECORDING_DURATION && status === 'recording') void handleStopRecorder()
  }, [duration, handleStopRecorder, status])

  const minutes = Math.floor(duration / 60)
    .toString()
    .padStart(2, '0')
  const seconds = (duration % 60).toString().padStart(2, '0')
  const isStarting = status === 'starting'
  const isRecording = status === 'recording'
  const isConverting = status === 'converting'

  return (
    <div ref={ref} className={cn(s.wrapper, 'absolute inset-0 rounded-xl')}>
      <div className="absolute inset-[1.5px] flex items-center overflow-hidden rounded-[10.5px] bg-primary-25 py-[14px] pr-[6.5px] pl-[14.5px]">
        <canvas ref={canvasRef} className="absolute bottom-0 left-0 h-4 w-full" />
        {(isStarting || isConverting) && (
          <div
            className="mr-2 i-ri-loader-2-line size-4 animate-spin text-primary-700"
            aria-hidden="true"
          />
        )}
        <div className="grow">
          {isStarting && (
            <div className="text-sm text-gray-500" role="status">
              {t(($) => $['voiceInput.starting'], { ns: 'common' })}
            </div>
          )}
          {isRecording && (
            <div className="text-sm text-gray-500">
              {t(($) => $['voiceInput.speaking'], { ns: 'common' })}
            </div>
          )}
          {isConverting && (
            <div className={cn(s.convert, 'text-sm')} role="status">
              {t(($) => $['voiceInput.converting'], { ns: 'common' })}
            </div>
          )}
        </div>
        {isRecording && (
          <button
            type="button"
            className="mr-1 flex size-8 items-center justify-center rounded-lg outline-hidden hover:bg-primary-100 focus-visible:ring-2 focus-visible:ring-state-accent-solid"
            aria-label={t(($) => $['voiceInput.stop'], { ns: 'common' })}
            onClick={handleStopRecorder}
          >
            <span className="i-ri-stop-circle-line size-5 text-primary-600" aria-hidden="true" />
          </button>
        )}
        {(isStarting || isConverting) && (
          <button
            type="button"
            className="mr-1 flex size-8 items-center justify-center rounded-lg outline-hidden hover:bg-gray-200 focus-visible:ring-2 focus-visible:ring-state-accent-solid"
            aria-label={t(($) => $['operation.cancel'], { ns: 'common' })}
            onClick={handleCancel}
          >
            <span className="i-ri-close-line size-4 text-gray-500" aria-hidden="true" />
          </button>
        )}
        <div
          className={cn(
            'w-[45px] pl-1 text-xs font-medium',
            duration > 500 ? 'text-text-destructive' : 'text-text-secondary',
          )}
          data-testid="voice-input-timer"
        >
          {`${minutes}:${seconds}`}
        </div>
      </div>
    </div>
  )
}

export default VoiceInput
