import { buildArticleWsUrl } from './speakingApiConfig'
import type { ArticleStreamAck } from '@/types/articleReading'

interface OpenArticleParagraphStreamOptions {
  sessionId: string
  paragraphIdx: number
  sampleRate: number
}

export interface ArticleParagraphStream {
  pushChunk(chunk: ArrayBuffer): void
  stop(): Promise<ArticleStreamAck>
  abort(reason?: string): void
}

/**
 * 段落录音上行：start JSON → 0..n 个 binary PCM 帧 → stop JSON → 等 ack。
 *
 * 录音器常常早于 socket open 就吐出 chunk，所以 CONNECTING 期间入队而非丢弃，
 * 否则开头的音频会缺失。没有 HTTP/文件兜底：断掉的录音直接作废，由 UI 提供重录。
 */
export function openArticleParagraphStream(
  options: OpenArticleParagraphStreamOptions,
): ArticleParagraphStream {
  const path = `/session/${encodeURIComponent(options.sessionId)}/paragraph/${options.paragraphIdx}/stream`
  const socket = new WebSocket(buildArticleWsUrl(path))
  const queuedChunks: ArrayBuffer[] = []
  let stopRequested = false
  let settled = false
  let resolveAck!: (ack: ArticleStreamAck) => void
  let rejectAck!: (error: Error) => void
  const ackPromise = new Promise<ArticleStreamAck>((resolve, reject) => {
    resolveAck = resolve
    rejectAck = reject
  })
  // stop() 之前没人持有 ackPromise，Node/浏览器会把提前的 reject 记为未处理拒绝
  ackPromise.catch(() => {})

  function rejectOnce(message: string) {
    if (settled) return
    settled = true
    rejectAck(new Error(message))
  }

  function sendStop() {
    socket.send(JSON.stringify({ type: 'stop' }))
  }

  socket.onopen = () => {
    socket.send(JSON.stringify({
      type: 'start',
      sampleRate: options.sampleRate,
      bits: 16,
      channels: 1,
    }))
    for (const chunk of queuedChunks) socket.send(chunk)
    queuedChunks.length = 0
    if (stopRequested) sendStop()
  }

  socket.onmessage = event => {
    let message: ArticleStreamAck | { type: 'error'; message: string }
    try {
      message = JSON.parse(String(event.data)) as ArticleStreamAck | { type: 'error'; message: string }
    } catch {
      rejectOnce('Article recording returned invalid JSON')
      socket.close()
      return
    }
    if (message.type === 'error') {
      rejectOnce(message.message)
      socket.close()
      return
    }
    if (message.type === 'ack' && message.status === 'generating' && !settled) {
      settled = true
      resolveAck(message)
      return
    }
    rejectOnce('Article recording returned an invalid acknowledgement')
    socket.close()
  }

  socket.onerror = () => rejectOnce('Article recording connection failed')
  socket.onclose = () => {
    if (!settled) rejectOnce('Article recording connection closed before acknowledgement')
  }

  return {
    pushChunk(chunk) {
      if (stopRequested) throw new Error('Cannot push PCM after stop')
      if (socket.readyState === WebSocket.OPEN) socket.send(chunk)
      else if (socket.readyState === WebSocket.CONNECTING) queuedChunks.push(chunk)
      else rejectOnce('Article recording connection is not open')
    },
    stop() {
      if (!stopRequested) {
        stopRequested = true
        if (socket.readyState === WebSocket.OPEN) sendStop()
        else if (socket.readyState !== WebSocket.CONNECTING) {
          rejectOnce('Article recording connection is not open')
        }
      }
      return ackPromise
    },
    abort(reason = 'Article recording aborted') {
      rejectOnce(reason)
      socket.close()
    },
  }
}
