import { useEffect, useMemo, useRef, useState } from 'react'
import './App.css'
import TranscriptPanel from './components/TranscriptPanel'
import MeetingList from './components/MeetingList'
import { createMeeting, deleteMeetings, endMeeting, fetchMeeting, fetchMeetings } from './lib/api'

function App() {
  const [isRecording, setIsRecording] = useState(false)
  const [isEditMode, setIsEditMode] = useState(false)
  const [currentMeetingId, setCurrentMeetingId] = useState<number | null>(null)
  const [transcriptLines, setTranscriptLines] = useState<
    { id: number; ts: string; text: string; isFinal: boolean }[]
  >([])
  const [meetingsList, setMeetingsList] = useState<
    { id: number; started_at: string; ended_at: string | null; title: string | null }[]
  >([])
  const [selectedMeetingIds, setSelectedMeetingIds] = useState<Set<number>>(
    new Set()
  )
  const [errorMessage, setErrorMessage] = useState<string | null>(null)
  const [micLevel, setMicLevel] = useState(0)
  const lineIdRef = useRef(1)
  const meetingIdRef = useRef<number | null>(null)
  const recognitionRef = useRef<SpeechRecognition | null>(null)
  const isRecordingRef = useRef(false)
  const stopRequestedRef = useRef(false)
  const maxLinesRef = useRef(500)
  const micLevelRef = useRef(0)
  const pendingTranscriptRef = useRef('')
  const lastAutoFinalAtRef = useRef(0)
  const lastAutoFinalTextRef = useRef('')
  const lastVoiceAtRef = useRef(0)
  const lastSilenceFinalAtRef = useRef(0)
  const lastResultAtRef = useRef(0)
  const noResultTimerRef = useRef<number | null>(null)
  const resetPendingRef = useRef(false)
  const lastFinalTextRef = useRef('')
  const lastFinalAtRef = useRef(0)
  const audioContextRef = useRef<AudioContext | null>(null)
  const hasSpeechRecognition = useMemo(() => {
    return 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window
  }, [])
  const analyserRef = useRef<AnalyserNode | null>(null)
  const mediaStreamRef = useRef<MediaStream | null>(null)
  const meterRafRef = useRef<number | null>(null)

  useEffect(() => {
    fetchMeetings()
      .then(setMeetingsList)
      .catch((err) => setErrorMessage(err.message))
  }, [])

  const clearNoResultTimer = () => {
    if (noResultTimerRef.current !== null) {
      window.clearTimeout(noResultTimerRef.current)
      noResultTimerRef.current = null
    }
  }

  const scheduleNoResultReset = () => {
    clearNoResultTimer()
    if (!isRecordingRef.current || stopRequestedRef.current) return
    noResultTimerRef.current = window.setTimeout(() => {
      noResultTimerRef.current = null
      if (!isRecordingRef.current || stopRequestedRef.current) return
      const now = Date.now()
      if (now - lastResultAtRef.current < 1500) return
      if (resetPendingRef.current) return
      resetPendingRef.current = true
      try {
        recognitionRef.current?.stop()
      } catch {
        // ignore stop errors
      }
      window.setTimeout(() => {
        resetPendingRef.current = false
        if (isRecordingRef.current && !stopRequestedRef.current) {
          startRecognition()
        }
      }, 300)
    }, 1500)
  }
  const appendFinalLine = (text: string) => {
    const trimmed = text.trim()
    if (!trimmed) return
    const tokenCount = trimmed.split(/\s+/).filter(Boolean).length
    if (tokenCount < 2) return
    const now = Date.now()
    if (now - lastFinalAtRef.current < 1200) {
      const last = lastFinalTextRef.current
      if (last && (last.includes(trimmed) || trimmed.includes(last))) {
        return
      }
    }
    const ts = new Date().toISOString()
    setTranscriptLines((prev) => {
      const next = [...prev, { id: lineIdRef.current++, ts, text: trimmed, isFinal: true }]
      const overflow = next.length - maxLinesRef.current
      return overflow > 0 ? next.slice(overflow) : next
    })
    lastFinalTextRef.current = trimmed
    lastFinalAtRef.current = now
  }

  const startRecognition = () => {
    const SpeechRecognitionConstructor =
      window.SpeechRecognition || window.webkitSpeechRecognition
    if (!SpeechRecognitionConstructor) {
      setErrorMessage('이 브라우저에서는 STT를 지원하지 않습니다. Chrome을 사용해 주세요.')
      return
    }
    const recognition = recognitionRef.current ?? new SpeechRecognitionConstructor()
    recognition.lang = 'ko-KR'
    recognition.interimResults = true
    recognition.continuous = true
    recognition.maxAlternatives = 1

    recognition.onresult = (event) => {
      lastResultAtRef.current = Date.now()
      for (let i = event.resultIndex; i < event.results.length; i += 1) {
        const result = event.results[i]
        if (!result || !result[0]) continue
        const transcript = result[0].transcript
        if (!transcript) continue
        pendingTranscriptRef.current = transcript
        if (result.isFinal) {
          appendFinalLine(transcript)
        }
      }
      scheduleNoResultReset()
    }

    recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
      const errorCode = event?.error
      if (errorCode === 'aborted' || errorCode === 'no-speech') {
        return
      }
      setErrorMessage('음성 인식 중 오류가 발생했습니다.')
    }

    recognition.onend = () => {
      if (isRecordingRef.current && !stopRequestedRef.current) {
        window.setTimeout(() => {
          startRecognition()
        }, 200)
      }
    }

    recognitionRef.current = recognition
    try {
      recognition.start()
    } catch {
      // ignore start errors
    }
    scheduleNoResultReset()
  }

  useEffect(() => {
    return () => {
      stopMeter()
    }
  }, [])

  const startMeter = async () => {
    if (meterRafRef.current !== null) return
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
      mediaStreamRef.current = stream
      const AudioCtx = window.AudioContext || (window as typeof window & {
        webkitAudioContext?: typeof AudioContext
      }).webkitAudioContext
      if (!AudioCtx) return
      const audioContext = new AudioCtx()
      audioContextRef.current = audioContext
      const analyser = audioContext.createAnalyser()
      analyser.fftSize = 2048
      analyserRef.current = analyser
      const source = audioContext.createMediaStreamSource(stream)
      source.connect(analyser)
      const data = new Uint8Array(analyser.fftSize)
      const loop = () => {
        if (!analyserRef.current) return
        analyserRef.current.getByteTimeDomainData(data)
        let sum = 0
        for (let i = 0; i < data.length; i += 1) {
          const v = (data[i] - 128) / 128
          sum += v * v
        }
        const rms = Math.sqrt(sum / data.length)
        const scaled = Math.log10(1 + rms * 120) / Math.log10(121)
        const level = Math.min(1, Math.max(0, scaled))
        const smooth = level * 0.5 + micLevelRef.current * 0.5
        micLevelRef.current = smooth
        setMicLevel(smooth)
        if (isRecordingRef.current) {
          const percent = smooth * 100
          const now = Date.now()
          if (percent >= 8) {
            lastVoiceAtRef.current = now
          }
          const silenceMs = now - lastVoiceAtRef.current
          if (
            silenceMs >= 900 &&
            pendingTranscriptRef.current &&
            now - lastSilenceFinalAtRef.current > 1200 &&
            pendingTranscriptRef.current !== lastAutoFinalTextRef.current
          ) {
            appendFinalLine(pendingTranscriptRef.current)
            lastSilenceFinalAtRef.current = now
            lastAutoFinalAtRef.current = now
            lastAutoFinalTextRef.current = pendingTranscriptRef.current
            pendingTranscriptRef.current = ''
          }
        }
        meterRafRef.current = window.requestAnimationFrame(loop)
      }
      meterRafRef.current = window.requestAnimationFrame(loop)
    } catch (err) {
      setErrorMessage((err as Error).message)
    }
  }

  const stopMeter = () => {
    if (meterRafRef.current !== null) {
      window.cancelAnimationFrame(meterRafRef.current)
      meterRafRef.current = null
    }
    if (analyserRef.current) {
      analyserRef.current.disconnect()
      analyserRef.current = null
    }
    if (audioContextRef.current) {
      audioContextRef.current.close().catch(() => undefined)
      audioContextRef.current = null
    }
    if (mediaStreamRef.current) {
      mediaStreamRef.current.getTracks().forEach((track) => track.stop())
      mediaStreamRef.current = null
    }
    setMicLevel(0)
    micLevelRef.current = 0
    pendingTranscriptRef.current = ''
    lastAutoFinalAtRef.current = 0
    lastAutoFinalTextRef.current = ''
    lastVoiceAtRef.current = 0
    lastSilenceFinalAtRef.current = 0
    clearNoResultTimer()
  }

  const handleStart = async () => {
    setErrorMessage(null)
    lineIdRef.current = 1
    setTranscriptLines([])
    meetingIdRef.current = null
    setCurrentMeetingId(null)
    setIsRecording(true)
    isRecordingRef.current = true
    stopRequestedRef.current = false
    lastResultAtRef.current = 0
    resetPendingRef.current = false
    lastVoiceAtRef.current = 0
    lastSilenceFinalAtRef.current = 0
    scheduleNoResultReset()
    pendingTranscriptRef.current = ''
    lastAutoFinalAtRef.current = 0
    lastAutoFinalTextRef.current = ''
    if (hasSpeechRecognition) {
      startRecognition()
    }
    void startMeter()
    try {
      const result = await createMeeting(new Date().toISOString())
      meetingIdRef.current = result.id
      setCurrentMeetingId(result.id)
    } catch (err) {
      setErrorMessage((err as Error).message)
    }
  }

  const handleStop = async () => {
    setErrorMessage(null)
    setIsRecording(false)
    isRecordingRef.current = false
    stopRequestedRef.current = true
    lastVoiceAtRef.current = 0
    lastSilenceFinalAtRef.current = 0
    clearNoResultTimer()
    pendingTranscriptRef.current = ''
    recognitionRef.current?.stop()
    stopMeter()
    try {
      if (meetingIdRef.current) {
        await endMeeting(meetingIdRef.current, new Date().toISOString())
        const list = await fetchMeetings()
        setMeetingsList(list)
      }
    } catch (err) {
      setErrorMessage((err as Error).message)
    }
  }

  const handleSelectMeeting = async (id: number) => {
    setErrorMessage(null)
    try {
      const data = await fetchMeeting(id)
      meetingIdRef.current = id
      setCurrentMeetingId(id)
      lineIdRef.current = 1
      setTranscriptLines(
        data.utterances.map((utterance) => ({
          id: lineIdRef.current++,
          ts: utterance.ts,
          text: utterance.text,
          isFinal: true,
        }))
      )
    } catch (err) {
      setErrorMessage((err as Error).message)
    }
  }

  const handleToggleEdit = () => {
    setIsEditMode(true)
  }

  const handleCancelEdit = () => {
    setIsEditMode(false)
    setSelectedMeetingIds(new Set())
  }

  const handleToggleSelect = (id: number) => {
    const next = new Set(selectedMeetingIds)
    if (next.has(id)) {
      next.delete(id)
    } else {
      next.add(id)
    }
    setSelectedMeetingIds(next)
  }

  const handleToggleAll = () => {
    if (meetingsList.length === 0) return
    if (selectedMeetingIds.size === meetingsList.length) {
      setSelectedMeetingIds(new Set())
      return
    }
    setSelectedMeetingIds(new Set(meetingsList.map((meeting) => meeting.id)))
  }

  const handleDelete = async () => {
    if (selectedMeetingIds.size === 0) return
    setErrorMessage(null)
    const ids = Array.from(selectedMeetingIds)
    try {
      await deleteMeetings(ids)
      const updated = meetingsList.filter((meeting) => !selectedMeetingIds.has(meeting.id))
      setMeetingsList(updated)
      setSelectedMeetingIds(new Set())
      setIsEditMode(false)
      if (currentMeetingId && ids.includes(currentMeetingId)) {
        setCurrentMeetingId(null)
        meetingIdRef.current = null
        setTranscriptLines([])
      }
    } catch (err) {
      setErrorMessage((err as Error).message)
    }
  }

  return (
    <div className="app">
      <div className="left-panel">
        {errorMessage && <div className="error-banner">{errorMessage}</div>}
        <TranscriptPanel
          transcriptLines={transcriptLines}
          interimText=""
          isRecording={isRecording}
        />
        <div className="controls">
          <button
            type="button"
            className={`record-btn ${isRecording ? 'recording' : ''}`}
            onClick={handleStart}
            disabled={isRecording || !hasSpeechRecognition}
          >
            녹음
          </button>
          <button type="button" className="stop-btn" onClick={handleStop} disabled={!isRecording}>
            중지
          </button>
          <div className="mic-meter" aria-hidden="true">
            <div
              className="mic-meter-bar"
              style={{ width: `${Math.round(micLevel * 100)}%` }}
            />
            <span className="mic-meter-label">{Math.round(micLevel * 100)}%</span>
          </div>
        </div>
      </div>
      <div className="right-panel">
        <div className="panel-title">대화 리스트</div>
        <MeetingList
          meetings={meetingsList}
          isEditMode={isEditMode}
          selectedIds={selectedMeetingIds}
          allSelected={meetingsList.length > 0 && selectedMeetingIds.size === meetingsList.length}
          onToggleSelect={handleToggleSelect}
          onToggleAll={handleToggleAll}
          onSelectMeeting={handleSelectMeeting}
        />
        <div className="list-controls">
          {!isEditMode ? (
            <button type="button" className="edit-btn" onClick={handleToggleEdit}>
              편집
            </button>
          ) : (
            <>
              <button type="button" className="delete-btn" onClick={handleDelete}>
                삭제
              </button>
              <button type="button" className="cancel-btn" onClick={handleCancelEdit}>
                취소
              </button>
            </>
          )}
        </div>
      </div>
    </div>
  )
}

export default App
