import { useEffect, useRef, useState } from 'react'
import { LoaderCircle, Mic, Paperclip, Plus, Send, Square } from 'lucide-react'
import clsx from 'clsx'
import toast from 'react-hot-toast'

import { audioApi, getApiErrorMessage } from '../../api/client'

const SAMPLE_RATE = 16000
const MIN_RECORDING_SECONDS = 0.5
const TARGET_PEAK = 0.92
const LOW_INPUT_LEVEL = 0.015

function floatTo16BitPCM(view, offset, input) {
  for (let i = 0; i < input.length; i += 1) {
    const sample = Math.max(-1, Math.min(1, input[i]))
    view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true)
    offset += 2
  }
}

function encodeWav(samples, sampleRate) {
  const buffer = new ArrayBuffer(44 + samples.length * 2)
  const view = new DataView(buffer)
  const writeString = (offset, value) => {
    for (let i = 0; i < value.length; i += 1) {
      view.setUint8(offset + i, value.charCodeAt(i))
    }
  }

  writeString(0, 'RIFF')
  view.setUint32(4, 36 + samples.length * 2, true)
  writeString(8, 'WAVE')
  writeString(12, 'fmt ')
  view.setUint32(16, 16, true)
  view.setUint16(20, 1, true)
  view.setUint16(22, 1, true)
  view.setUint32(24, sampleRate, true)
  view.setUint32(28, sampleRate * 2, true)
  view.setUint16(32, 2, true)
  view.setUint16(34, 16, true)
  writeString(36, 'data')
  view.setUint32(40, samples.length * 2, true)
  floatTo16BitPCM(view, 44, samples)
  return new Blob([buffer], { type: 'audio/wav' })
}

function downsampleBuffer(buffer, inputSampleRate, outputSampleRate) {
  if (outputSampleRate >= inputSampleRate) return buffer

  const sampleRateRatio = inputSampleRate / outputSampleRate
  const newLength = Math.round(buffer.length / sampleRateRatio)
  const result = new Float32Array(newLength)
  let offsetResult = 0
  let offsetBuffer = 0

  while (offsetResult < result.length) {
    const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio)
    let accum = 0
    let count = 0
    for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i += 1) {
      accum += buffer[i]
      count += 1
    }
    result[offsetResult] = count > 0 ? accum / count : 0
    offsetResult += 1
    offsetBuffer = nextOffsetBuffer
  }

  return result
}

function normalizeSamples(samples, targetPeak = TARGET_PEAK) {
  let peak = 0
  for (let i = 0; i < samples.length; i += 1) {
    peak = Math.max(peak, Math.abs(samples[i]))
  }
  if (peak === 0) return samples

  const gain = Math.min(targetPeak / peak, 8)
  if (gain <= 1.05) return samples

  const normalized = new Float32Array(samples.length)
  for (let i = 0; i < samples.length; i += 1) {
    normalized[i] = Math.max(-1, Math.min(1, samples[i] * gain))
  }
  return normalized
}

function getPeak(samples) {
  let peak = 0
  for (let i = 0; i < samples.length; i += 1) {
    peak = Math.max(peak, Math.abs(samples[i]))
  }
  return peak
}

function mixToMono(audioBuffer) {
  const channelCount = audioBuffer.numberOfChannels
  if (channelCount === 1) return new Float32Array(audioBuffer.getChannelData(0))

  const mono = new Float32Array(audioBuffer.length)
  for (let channel = 0; channel < channelCount; channel += 1) {
    const data = audioBuffer.getChannelData(channel)
    for (let i = 0; i < data.length; i += 1) {
      mono[i] += data[i] / channelCount
    }
  }
  return mono
}

async function decodeBlobToMonoWav(blob) {
  const AudioContextClass = window.AudioContext || window.webkitAudioContext
  const audioContext = new AudioContextClass()
  try {
    if (audioContext.state === 'suspended') await audioContext.resume()
    const arrayBuffer = await blob.arrayBuffer()
    const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
    const mono = mixToMono(audioBuffer)
    const downsampled = downsampleBuffer(mono, audioBuffer.sampleRate, SAMPLE_RATE)
    const peakBefore = getPeak(downsampled)
    const normalized = normalizeSamples(downsampled)
    return {
      wavBlob: encodeWav(normalized, SAMPLE_RATE),
      durationSeconds: normalized.length / SAMPLE_RATE,
      sampleCount: normalized.length,
      peakBefore,
    }
  } finally {
    if (audioContext.state !== 'closed') await audioContext.close()
  }
}

export default function ChatInput({ onSend, onUploadClick, isLoading, disabled, disabledReason }) {
  const [text, setText] = useState('')
  const [isRecording, setIsRecording] = useState(false)
  const [isTranscribing, setIsTranscribing] = useState(false)
  const [inputLevel, setInputLevel] = useState(0)
  const textareaRef = useRef(null)
  const streamRef = useRef(null)
  const mediaRecorderRef = useRef(null)
  const chunksRef = useRef([])
  const monitorAudioContextRef = useRef(null)
  const analyserRef = useRef(null)
  const monitorFrameRef = useRef(null)

  useEffect(() => {
    const el = textareaRef.current
    if (!el) return
    el.style.height = 'auto'
    el.style.height = Math.min(el.scrollHeight, 200) + 'px'
  }, [text])

  useEffect(() => () => {
    if (mediaRecorderRef.current?.state && mediaRecorderRef.current.state !== 'inactive') {
      mediaRecorderRef.current.stop()
    }
    if (monitorFrameRef.current) cancelAnimationFrame(monitorFrameRef.current)
    if (monitorAudioContextRef.current && monitorAudioContextRef.current.state !== 'closed') {
      monitorAudioContextRef.current.close().catch(() => {})
    }
    if (streamRef.current) {
      streamRef.current.getTracks().forEach((track) => track.stop())
    }
  }, [])

  const submit = () => {
    const q = text.trim()
    if (!q || isLoading || disabled || isRecording || isTranscribing) return
    onSend(q)
    setText('')
    if (textareaRef.current) textareaRef.current.style.height = 'auto'
  }

  const handleKey = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault()
      submit()
    }
  }

  const stopInputMonitor = async () => {
    if (monitorFrameRef.current) {
      cancelAnimationFrame(monitorFrameRef.current)
      monitorFrameRef.current = null
    }
    analyserRef.current = null
    if (monitorAudioContextRef.current && monitorAudioContextRef.current.state !== 'closed') {
      await monitorAudioContextRef.current.close()
    }
    monitorAudioContextRef.current = null
    setInputLevel(0)
  }

  const stopStream = async () => {
    await stopInputMonitor()
    if (!streamRef.current) return
    streamRef.current.getTracks().forEach((track) => track.stop())
    streamRef.current = null
  }

  const startInputMonitor = async (stream) => {
    const AudioContextClass = window.AudioContext || window.webkitAudioContext
    const audioContext = new AudioContextClass()
    if (audioContext.state === 'suspended') await audioContext.resume()

    const source = audioContext.createMediaStreamSource(stream)
    const analyser = audioContext.createAnalyser()
    analyser.fftSize = 2048
    source.connect(analyser)

    monitorAudioContextRef.current = audioContext
    analyserRef.current = analyser

    const buffer = new Float32Array(analyser.fftSize)
    const tick = () => {
      if (!analyserRef.current) return
      analyserRef.current.getFloatTimeDomainData(buffer)
      let sumSquares = 0
      for (let i = 0; i < buffer.length; i += 1) {
        sumSquares += buffer[i] * buffer[i]
      }
      const rms = Math.sqrt(sumSquares / buffer.length)
      setInputLevel(Math.min(1, rms * 12))
      monitorFrameRef.current = requestAnimationFrame(tick)
    }
    tick()
  }

  const transcribeWavBlob = async (blob) => {
    const formData = new FormData()
    formData.append('file', new File([blob], 'recording.wav', { type: 'audio/wav' }))

    setIsTranscribing(true)
    try {
      const result = await audioApi.transcribe(formData)
      if (!result.text?.trim()) {
        toast.error('No speech was detected in that recording')
        return
      }
      setText((prev) => [prev.trim(), result.text.trim()].filter(Boolean).join(prev.trim() ? '\n' : ''))
      toast.success('Transcript added to the message box')
    } catch (err) {
      toast.error(getApiErrorMessage(err, 'Failed to transcribe audio'))
    } finally {
      setIsTranscribing(false)
    }
  }

  const handleRecordedBlob = async (blob) => {
    try {
      const { wavBlob, durationSeconds, sampleCount, peakBefore } = await decodeBlobToMonoWav(blob)
      if (sampleCount === 0) {
        toast.error('Recording was empty')
        return
      }
      if (durationSeconds < MIN_RECORDING_SECONDS) {
        toast.error('Recording is too short. Please speak for a bit longer.')
        return
      }
      if (peakBefore < LOW_INPUT_LEVEL) {
        toast.error('The selected browser microphone is almost silent. Check the browser mic permission and selected input device.')
        return
      }
      await transcribeWavBlob(wavBlob)
    } catch (err) {
      toast.error(err?.message || 'Could not process the microphone recording')
    }
  }

  const toggleRecording = async () => {
    if (disabled || isLoading || isTranscribing) return

    if (isRecording) {
      mediaRecorderRef.current?.stop()
      setIsRecording(false)
      return
    }

    if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
      toast.error('Your browser does not support microphone recording')
      return
    }

    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
      streamRef.current = stream
      await startInputMonitor(stream)

      const track = stream.getAudioTracks()[0]
      const label = track?.label?.trim()
      if (label) {
        toast.success(`Using microphone: ${label}`)
      } else {
        toast.success('Recording started. Press the mic again to stop.')
      }

      const mimeType = [
        'audio/webm;codecs=opus',
        'audio/webm',
        'audio/ogg;codecs=opus',
      ].find((type) => MediaRecorder.isTypeSupported(type))

      if (!mimeType) {
        await stopStream()
        toast.error('This browser cannot record in a supported audio format')
        return
      }

      const recorder = new MediaRecorder(stream, { mimeType })
      mediaRecorderRef.current = recorder
      chunksRef.current = []

      recorder.ondataavailable = (event) => {
        if (event.data?.size) chunksRef.current.push(event.data)
      }
      recorder.onerror = async () => {
        await stopStream()
        setIsRecording(false)
        toast.error('Microphone recording failed')
      }
      recorder.onstop = async () => {
        const blob = new Blob(chunksRef.current, { type: recorder.mimeType || mimeType })
        chunksRef.current = []
        await stopStream()
        await handleRecordedBlob(blob)
      }

      recorder.start()
      setIsRecording(true)
    } catch (err) {
      await stopStream()
      toast.error(err?.message || 'Could not access the microphone')
    }
  }

  return (
    <div className="border border-white/8 bg-surface-2 rounded-2xl shadow-xl overflow-hidden transition-all duration-150 focus-within:border-accent/30 focus-within:shadow-accent/5">
      <textarea
        ref={textareaRef}
        value={text}
        onChange={(e) => setText(e.target.value)}
        onKeyDown={handleKey}
        disabled={disabled || isLoading || isTranscribing}
        placeholder={disabled && disabledReason ? disabledReason : 'Ask a medical question...'}
        rows={1}
        className="w-full bg-transparent text-white text-sm placeholder-gray-600 px-4 pt-4 pb-2 resize-none outline-none leading-relaxed disabled:opacity-50"
      />

      <div className="flex items-center justify-between px-3 pb-3 pt-1">
        <div className="flex items-center gap-1">
          <button
            onClick={onUploadClick}
            title="Upload PDF / document"
            className={clsx(
              'flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-all duration-150',
              'text-gray-400 hover:text-accent hover:bg-accent/10 border border-white/5 hover:border-accent/20'
            )}
          >
            <Plus size={13} />
            <Paperclip size={12} />
          </button>
          <button
            onClick={toggleRecording}
            disabled={disabled || isLoading || isTranscribing}
            title={
              isTranscribing
                ? 'Transcribing audio...'
                : isRecording
                  ? 'Stop recording'
                  : 'Record audio for transcription'
            }
            className={clsx(
              'flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-all duration-150 border',
              isRecording
                ? 'text-red-300 bg-red-500/10 border-red-400/30 hover:bg-red-500/15'
                : 'text-gray-400 hover:text-accent hover:bg-accent/10 border-white/5 hover:border-accent/20',
              (disabled || isLoading || isTranscribing) && 'opacity-60 cursor-not-allowed'
            )}
          >
            {isTranscribing
              ? <LoaderCircle size={13} className="animate-spin" />
              : <Mic size={13} className={isRecording ? 'animate-pulse' : ''} />
            }
          </button>
        </div>

        <div className="flex items-center gap-2">
          {(text || isRecording || isTranscribing) && (
            <span className="text-[10px] text-gray-600 select-none">
              {isRecording
                ? 'Recording... press mic to stop'
                : isTranscribing
                  ? 'Transcribing audio...'
                  : 'Enter send | Shift+Enter newline'}
            </span>
          )}
          <button
            onClick={submit}
            disabled={!text.trim() || disabled || isRecording || isTranscribing}
            title={disabled && disabledReason ? disabledReason : (isLoading ? 'Processing...' : 'Send')}
            className={clsx(
              'w-8 h-8 rounded-lg flex items-center justify-center transition-all duration-150',
              text.trim() && !disabled && !isRecording && !isTranscribing
                ? 'bg-accent hover:bg-accent-hover text-white shadow-md shadow-accent/20 active:scale-95'
                : 'bg-surface-3 text-gray-600 cursor-not-allowed'
            )}
          >
            {isLoading
              ? <Square size={12} className="fill-current" />
              : <Send size={13} className={text.trim() ? '' : 'opacity-40'} />
            }
          </button>
        </div>
      </div>

      {(isRecording || inputLevel > 0) && (
        <div className="px-4 pb-3">
          <div className="flex items-center gap-2">
            <div className="h-1.5 flex-1 rounded-full bg-surface-3 overflow-hidden">
              <div
                className={clsx(
                  'h-full transition-[width] duration-100',
                  inputLevel < LOW_INPUT_LEVEL ? 'bg-amber-400/80' : 'bg-emerald-400/90'
                )}
                style={{ width: `${Math.max(4, inputLevel * 100)}%` }}
              />
            </div>
            <span className="text-[10px] text-gray-500 w-12 text-right">
              {inputLevel < LOW_INPUT_LEVEL ? 'Low mic' : 'Mic ok'}
            </span>
          </div>
        </div>
      )}

      {disabled && disabledReason && (
        <div className="px-4 pb-3 text-[11px] text-amber-300/80">
          {disabledReason}
        </div>
      )}
    </div>
  )
}
