import { ref, reactive, computed, onUnmounted } from 'vue'
import type { PreviewChatMessage, DialogueAPI, DialogueReport, HistoricalDialogueMessage } from '@/types/englishSpeaking'
import { createDialogueApi, DialogueApiError } from '../services/llmService'
import { buildSpeakingWsUrl } from '../services/speakingApiConfig'

export function useDialogueEngine() {
  const messages = ref<PreviewChatMessage[]>([])
  const sessionId = ref<string | null>(null)
  const expiresAt = ref<string | null>(null)
  const currentRound = ref(1)
  const totalRounds = ref<number>(3)
  const isComplete = ref(false)
  const countdownSeconds = ref<number | null>(null)

  const api: DialogueAPI = createDialogueApi()
  let currentAbortController: AbortController | null = null
  let countdownTimer: ReturnType<typeof setInterval> | null = null

  const isProcessing = computed(() => messages.value.some(m => m.status === 'loading'))
  const canRecord = computed(() => !isProcessing.value && !isComplete.value)
  const isFinalRound = computed(() => currentRound.value >= totalRounds.value)

  // ==================== Session Attach ====================

  const greetingInflight = ref(false)
  let greetingAbortController: AbortController | null = null

  /** 仅附加已建好的 session 到 engine,不发任何网络请求。
   *  时间和轮次是两个并存的结束条件,先满足谁就结束。
   *  expiresAt 由后端在 createSession 时基于 durationMinutes 计算下发,前端只读 ——
   *  保证关页面/刷新/换设备重进都能接续同一个截止时刻,倒计时不暂停。
   */
  function toPreviewMessage(message: HistoricalDialogueMessage): PreviewChatMessage {
    return {
      id: message.id,
      role: message.role,
      content: message.content,
      timestamp: new Date(),
      status: 'done',
      turnId: message.clientTurnId ?? undefined,
      audioUrl: message.audioUrl ?? undefined,
    }
  }

  function attachSession(info: {
    sessionId: string
    expiresAt?: string | null
    totalRounds: number
    currentRound?: number
    messages?: HistoricalDialogueMessage[]
  }) {
    sessionId.value = info.sessionId
    expiresAt.value = info.expiresAt ?? null
    totalRounds.value = info.totalRounds
    currentRound.value = info.currentRound ?? 1
    messages.value = (info.messages ?? []).map(toPreviewMessage)
    if (info.expiresAt) startCountdown(info.expiresAt)
  }

  /** 执行开场白生成的私有帮手:push loading 占位 AI 消息 → 请求 /greeting → 成功填内容,失败标 error。
   *  @param turnId 本次生成的轮次 ID,用于后端幂等性查找。
   */
  async function _runGreeting(turnId: string) {
    if (!sessionId.value || greetingInflight.value) return
    greetingInflight.value = true
    greetingAbortController = new AbortController()

    const aiMsg = reactive<PreviewChatMessage>({
      id: crypto.randomUUID(),
      role: 'ai',
      content: '',
      timestamp: new Date(),
      status: 'loading',
      turnId,
    })
    messages.value.push(aiMsg)

    try {
      const { aiMessage } = await api.generateGreeting(sessionId.value, turnId, greetingAbortController.signal)
      aiMsg.content = aiMessage
      aiMsg.status = 'done'
    } catch (err: unknown) {
      if (err instanceof Error && err.name === 'AbortError') return  // 组件卸载:不改 UI
      aiMsg.status = 'error'
      const status = err instanceof DialogueApiError ? err.status : undefined
      const raw = err instanceof Error ? err.message : undefined
      aiMsg.error = friendlyErrorMessage(raw)
      aiMsg.recovery = classifyError(raw, status, 'ai')
    } finally {
      greetingInflight.value = false
      greetingAbortController = null
    }
  }

  /** 触发开场白生成:分配新的 turnId,调用 _runGreeting。 */
  async function generateGreeting() {
    await _runGreeting(crypto.randomUUID())
  }

  /** 重试失败的开场白:移除 error 消息,复用失败消息的 turnId 再走一遍(后端幂等性查找)。
   *  如果老消息没有 turnId(不应该发生,但防守性),分配新的。 */
  async function retryGreeting() {
    if (greetingInflight.value) return
    const firstAi = messages.value.find(m => m.role === 'ai')
    if (firstAi?.status !== 'error' || firstAi.recovery === 'restart') return
    const reuseTurnId = firstAi.turnId ?? crypto.randomUUID()
    messages.value = messages.value.filter(m => m.id !== firstAi.id)
    await _runGreeting(reuseTurnId)
  }

  // ==================== Send Message ====================

  async function sendStudentMessage(audioBlob: Blob, turnId: string) {
    if (!sessionId.value || isProcessing.value) return

    // Add student message (loading)
    const studentMsg = reactive<PreviewChatMessage>({
      id: crypto.randomUUID(),
      role: 'student',
      content: '',
      timestamp: new Date(),
      status: 'loading',
      audioBlob,
      turnId,
    })
    messages.value.push(studentMsg)

    // Add AI message placeholder
    const aiMsg = reactive<PreviewChatMessage>({
      id: crypto.randomUUID(),
      role: 'ai',
      content: '',
      timestamp: new Date(),
      status: 'loading',
      turnId,
    })

    currentAbortController = new AbortController()

    try {
      const generator = api.speak(sessionId.value, audioBlob, currentAbortController.signal, turnId)

      for await (const event of generator) {
        if (event.type === 'transcript') {
          studentMsg.content = event.text
          studentMsg.status = 'done'
          // Now push AI message placeholder
          messages.value.push(aiMsg)
        } else if (event.type === 'token') {
          aiMsg.content += event.text
        } else if (event.type === 'done') {
          aiMsg.status = 'done'
          isComplete.value = event.isComplete

          if (!event.isComplete) {
            currentRound.value++
          }
        }
      }

      // If student message never got transcript, mark done with fallback
      if (studentMsg.status === 'loading') {
        studentMsg.status = 'done'
      }
      if (aiMsg.status === 'loading') {
        aiMsg.status = 'done'
      }
    } catch (err: any) {
      if (err.name === 'AbortError') return
      const status: number | undefined = err instanceof DialogueApiError ? err.status : undefined
      if (studentMsg.status === 'loading') {
        studentMsg.status = 'error'
        studentMsg.error = friendlyErrorMessage(err.message)
        studentMsg.recovery = classifyError(err.message, status, 'student')
      } else if (aiMsg.status === 'loading') {
        aiMsg.status = 'error'
        aiMsg.error = friendlyErrorMessage(err.message)
        aiMsg.recovery = classifyError(err.message, status, 'ai')
      }
    } finally {
      currentAbortController = null
    }
  }

  // ==================== Retry / Regenerate ====================

  async function retryMessage(messageId: string) {
    const msg = messages.value.find(m => m.id === messageId)
    if (!msg || msg.status !== 'error') return

    if (msg.role === 'student' && msg.audioBlob && msg.turnId) {
      // Remove the failed student message and any subsequent AI message
      const idx = messages.value.indexOf(msg)
      messages.value.splice(idx)
      await sendStudentMessage(msg.audioBlob, msg.turnId)
    }
  }

  async function regenerateAiMessage(messageId: string) {
    const msg = messages.value.find(m => m.id === messageId)
    if (!msg || msg.role !== 'ai' || msg.status !== 'error') return

    // Find the student message before this AI message
    const idx = messages.value.indexOf(msg)
    const prevStudent = messages.value.slice(0, idx).reverse().find(m => m.role === 'student')
    if (!prevStudent?.audioBlob || !sessionId.value || !prevStudent.turnId) return

    // Remove the failed AI message
    messages.value.splice(idx, 1)

    // Re-add AI placeholder and stream
    const aiMsg = reactive<PreviewChatMessage>({
      id: crypto.randomUUID(),
      role: 'ai',
      content: '',
      timestamp: new Date(),
      status: 'loading',
      turnId: prevStudent.turnId,
    })
    messages.value.push(aiMsg)

    currentAbortController = new AbortController()

    try {
      const generator = api.speak(
        sessionId.value, prevStudent.audioBlob,
        currentAbortController.signal, prevStudent.turnId,
      )

      for await (const event of generator) {
        if (event.type === 'transcript') {
          // Skip transcript on regenerate, student message already exists
        } else if (event.type === 'token') {
          aiMsg.content += event.text
        } else if (event.type === 'done') {
          aiMsg.status = 'done'
          isComplete.value = event.isComplete
          if (!event.isComplete) currentRound.value++
        }
      }

      if (aiMsg.status === 'loading') aiMsg.status = 'done'
    } catch (err: any) {
      if (err.name === 'AbortError') return
      aiMsg.status = 'error'
      aiMsg.error = friendlyErrorMessage(err.message)
      const status = err instanceof DialogueApiError ? err.status : undefined
      aiMsg.recovery = classifyError(err.message, status, 'ai')
    } finally {
      currentAbortController = null
    }
  }

  // ==================== Report ====================

  async function waitForReport(sessionId: string): Promise<DialogueReport> {
    const maxAttempts = 30
    const intervalMs = 2000
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      const report = await api.getReport(sessionId)
      if (report.status === 'ready' || report.status === 'failed' || report.status === 'incomplete') {
        return report
      }
      await new Promise(resolve => setTimeout(resolve, intervalMs))
    }
    throw new Error('Report generation timed out')
  }

  async function getReport(): Promise<DialogueReport> {
    if (!sessionId.value) return Promise.reject(new Error('No session'))

    const report = await waitForReport(sessionId.value!)
    return report
  }

  async function completeSession(): Promise<void> {
    if (!sessionId.value) return
    await api.completeSession(sessionId.value)
  }

  // ==================== Countdown ====================

  function startCountdown(expiresAtStr: string) {
    stopCountdown()

    const update = () => {
      const remaining = Math.max(0, Math.floor((new Date(expiresAtStr).getTime() - Date.now()) / 1000))
      countdownSeconds.value = remaining
      if (remaining <= 0) {
        stopCountdown()
        isComplete.value = true
      }
    }

    update()
    countdownTimer = setInterval(update, 1000)
  }

  function stopCountdown() {
    if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
    countdownSeconds.value = null
  }

  // ==================== Abort ====================

  function abort() {
    currentAbortController?.abort()
    currentAbortController = null
  }

  // ==================== Streaming Speak (WebSocket) ====================

  /** 流式开始：打开 WS，返回 { pushChunk, commit, abort }；占位消息在 commit() 时才 push */
  function beginStudentStream(opts: {
    sampleRate: number
    bits?: number
    channels?: number
  }) {
    if (!sessionId.value || isProcessing.value) return null

    const turnId = crypto.randomUUID()

    // No messages pushed yet. They are pushed in commit().
    let studentMsg: PreviewChatMessage | null = null
    let aiMsg: PreviewChatMessage | null = null

    const wsUrl = buildSpeakingWsUrl('/speak-stream')
    const ws = new WebSocket(wsUrl)
    ws.binaryType = 'arraybuffer'

    let aborted = false
    let committed = false
    let chunkQueue: ArrayBuffer[] = []
    let open = false

    const finalizeError = (raw: string) => {
      const text = friendlyErrorMessage(raw)
      if (studentMsg && studentMsg.status === 'loading') {
        studentMsg.status = 'error'
        studentMsg.error = text
        studentMsg.recovery = classifyError(raw, undefined, 'student')
      } else if (aiMsg && aiMsg.status === 'loading') {
        aiMsg.status = 'error'
        aiMsg.error = text
        aiMsg.recovery = classifyError(raw, undefined, 'ai')
      }
    }

    ws.onopen = () => {
      open = true
      ws.send(JSON.stringify({
        type: 'start',
        sessionId: sessionId.value,
        turnId,
        sampleRate: opts.sampleRate,
        bits: opts.bits ?? 16,
        channels: opts.channels ?? 1,
      }))
      for (const c of chunkQueue) ws.send(c)
      chunkQueue = []
    }

    ws.onmessage = (e: MessageEvent) => {
      if (!committed) return  // tokens shouldn't arrive before commit; defensive
      try {
        const data = JSON.parse(e.data)
        if (data.type === 'transcript' && studentMsg) {
          studentMsg.content = data.text
          studentMsg.status = 'done'
        }
        else if (data.type === 'token' && aiMsg) {
          aiMsg.content += data.content
        }
        else if (data.type === 'done') {
          if (aiMsg) aiMsg.status = 'done'
          else if (studentMsg && studentMsg.status === 'loading') studentMsg.status = 'done'
          isComplete.value = !!data.isComplete
          if (!data.isComplete) currentRound.value++
          ws.close()
        }
        else if (data.type === 'error') {
          finalizeError(data.message)
          ws.close()
        }
      } catch { /* ignore */ }
    }

    ws.onerror = () => {
      if (!aborted && committed) finalizeError('WebSocket error')
    }
    ws.onclose = () => {
      if (!committed || aborted) return
      if (studentMsg?.status === 'loading') finalizeError('Connection closed')
      else if (aiMsg?.status === 'loading') finalizeError('Connection closed')
    }

    const pushChunk = (chunk: ArrayBuffer) => {
      if (aborted) return
      if (open && ws.readyState === WebSocket.OPEN) ws.send(chunk)
      else chunkQueue.push(chunk)
    }

    const commit = (blob: Blob) => {
      if (committed || aborted) return
      committed = true

      studentMsg = reactive<PreviewChatMessage>({
        id: crypto.randomUUID(),
        role: 'student',
        content: '',
        timestamp: new Date(),
        status: 'loading',
        audioBlob: blob,
        turnId,
      })
      messages.value.push(studentMsg)

      if (!isFinalRound.value) {
        aiMsg = reactive<PreviewChatMessage>({
          id: crypto.randomUUID(),
          role: 'ai',
          content: '',
          timestamp: new Date(),
          status: 'loading',
          turnId,
        })
        messages.value.push(aiMsg)
      }

      if (open && ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: 'stop' }))
      } else {
        ws.close()
      }
    }

    const abort = () => {
      aborted = true
      try { ws.close() } catch { /* ignore */ }
      // No messages were pushed (commit not called) → nothing to clean up.
    }

    return { turnId, pushChunk, commit, abort }
  }

  /**
   * 流式失败时的 HTTP fallback：用完整 audioBlob 走旧 /speak 路径。
   * 会把 beginStudentStream 已 push 的占位消息回收（避免重复）。
   */
  async function streamFallback(audioBlob: Blob, studentMsgId: string, aiMsgId: string, turnId: string) {
    // 移除占位消息
    messages.value = messages.value.filter(m => m.id !== studentMsgId && m.id !== aiMsgId)
    // 走旧流程
    await sendStudentMessage(audioBlob, turnId)
  }

  /**
   * 丢弃当前轮次的所有消息（student + ai），用于"重录"按钮。
   * 通过 turnId 精确定位同一轮的两条消息并一并移除。
   */
  function discardCurrentTurn(messageId: string) {
    const msg = messages.value.find(m => m.id === messageId)
    if (!msg) return
    const turnId = msg.turnId
    if (!turnId) {
      // Defensive: messages without turnId (legacy / greeting) can't be turn-grouped.
      messages.value = messages.value.filter(m => m.id !== messageId)
      return
    }
    // Splice out both rows of the turn (student + ai) sharing this turnId.
    messages.value = messages.value.filter(m => m.turnId !== turnId)
  }

  // ==================== Cleanup ====================

  onUnmounted(() => {
    abort()
    greetingAbortController?.abort()
    stopCountdown()
  })

  return {
    messages,
    sessionId,
    currentRound,
    totalRounds,
    isComplete,
    isProcessing,
    canRecord,
    countdownSeconds,
    greetingInflight,
    isFinalRound,

    attachSession,
    generateGreeting,
    retryGreeting,
    sendStudentMessage,
    beginStudentStream,
    streamFallback,
    retryMessage,
    regenerateAiMessage,
    discardCurrentTurn,
    getReport,
    completeSession,
    abort,
  }
}

// ==================== Helpers ====================

/** 把后端英文错误转成面向用户的中文友好文案 */
function friendlyErrorMessage(raw: string | undefined): string {
  const map: Record<string, string> = {
    'No speech detected': '没听清，请再说一次',
    'Session not found': '会话已失效，请刷新页面重新开始',
    'Session is not active': '会话已结束',
    'sessionId required': '会话参数缺失',
    'First message must be start': '协议异常，请刷新重试',
    'Invalid start payload': '协议异常，请刷新重试',
    'Internal error': '服务暂时不可用，请稍后重试',
    'WebSocket error': '网络连接异常',
    'Connection closed': '连接中断，请重试',
    'Aborted': '已取消',
  }
  if (!raw) return '请求失败，请重试'
  return map[raw] || raw
}

export type Recovery = 'retry' | 'rerecord' | 'restart'

export function classifyError(
  raw: string | undefined,
  status: number | undefined,
  role: 'student' | 'ai',
): Recovery {
  if (status === 404 || status === 409) return 'restart'
  if (raw === 'Session not found' || raw === 'Session is not active') return 'restart'
  if (raw === 'No speech detected' && role === 'student') return 'rerecord'
  return 'retry'
}
