import { useEffect, useRef } from 'react'

export type RealtimeEvent = {
  eventId: number
  version: number
  type:
    | 'admin_notification.changed'
    | 'order.changed'
    | 'work_order.changed'
    | 'worker_finance_request.changed'
    | 'worker_wallet.changed'
  entityId: number
  scopes: string[]
  operation: 'upsert' | 'remove' | 'refresh'
  data?: unknown
  occurredAt: string
}

type UseRealtimeEventsOptions = {
  url: string
  token: string
  onEvent: (event: RealtimeEvent) => void
  onConnected?: () => void
  /** 鉴权失效（401/403）时触发：连接停止重连，调用方应清理会话并跳转登录页。 */
  onAuthExpired?: () => void
}

const RECONNECT_DELAY_MS = 3_000

/** 通过 fetch 保持带 Bearer 鉴权的 SSE 连接，避免把登录 token 暴露到 URL。 */
export function useRealtimeEvents({
  url,
  token,
  onEvent,
  onConnected,
  onAuthExpired,
}: UseRealtimeEventsOptions) {
  const onEventRef = useRef(onEvent)
  const onConnectedRef = useRef(onConnected)
  const onAuthExpiredRef = useRef(onAuthExpired)
  onEventRef.current = onEvent
  onConnectedRef.current = onConnected
  onAuthExpiredRef.current = onAuthExpired

  useEffect(() => {
    if (!token) return

    let stopped = false
    let abortController: AbortController | null = null
    let reconnectTimer = 0
    let lastEventId = 0

    const connect = () => {
      if (stopped || document.visibilityState !== 'visible') return
      abortController?.abort()
      abortController = new AbortController()

      const headers: Record<string, string> = {
        Accept: 'text/event-stream',
        Authorization: `Bearer ${token}`,
      }
      if (lastEventId > 0) headers['Last-Event-ID'] = String(lastEventId)
      let authExpired = false
      void fetch(url, {
        method: 'GET',
        headers,
        cache: 'no-store',
        signal: abortController.signal,
      })
        .then(async (response) => {
          if (response.status === 401 || response.status === 403) {
            // 会话已失效：重连只会无限 401，交给调用方清理会话跳登录。
            authExpired = true
            onAuthExpiredRef.current?.()
            return
          }
          if (!response.ok || !response.body) {
            throw new Error(`realtime_stream_${response.status}`)
          }
          onConnectedRef.current?.()
          await readSseStream(response.body, (eventName, data) => {
            if (eventName !== 'realtime') return
            const event = normalizeRealtimeEvent(data)
            if (event) {
              if (event.eventId <= lastEventId) return
              lastEventId = Math.max(lastEventId, event.eventId)
              onEventRef.current(event)
            }
          })
        })
        .catch(() => undefined)
        .finally(() => {
          if (authExpired || stopped || document.visibilityState !== 'visible') return
          reconnectTimer = window.setTimeout(connect, RECONNECT_DELAY_MS)
        })
    }

    const handleVisibilityChange = () => {
      if (document.visibilityState === 'visible') {
        connect()
      } else {
        abortController?.abort()
        window.clearTimeout(reconnectTimer)
      }
    }

    document.addEventListener('visibilitychange', handleVisibilityChange)
    connect()
    return () => {
      stopped = true
      document.removeEventListener('visibilitychange', handleVisibilityChange)
      abortController?.abort()
      window.clearTimeout(reconnectTimer)
    }
  }, [token, url])
}

async function readSseStream(
  stream: ReadableStream<Uint8Array>,
  onMessage: (eventName: string, data: string) => void,
) {
  const reader = stream.getReader()
  const decoder = new TextDecoder()
  let buffer = ''
  let eventName = 'message'
  let dataLines: string[] = []

  function dispatch() {
    if (dataLines.length > 0) {
      onMessage(eventName, dataLines.join('\n'))
    }
    eventName = 'message'
    dataLines = []
  }

  function parseLine(line: string) {
    if (!line) {
      dispatch()
      return
    }
    if (line.startsWith(':')) return
    const separatorIndex = line.indexOf(':')
    const field = separatorIndex >= 0 ? line.slice(0, separatorIndex) : line
    const value = separatorIndex >= 0 ? line.slice(separatorIndex + 1).replace(/^ /, '') : ''
    if (field === 'event') eventName = value || 'message'
    if (field === 'data') dataLines.push(value)
  }

  try {
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      buffer += decoder.decode(value, { stream: true })
      const lines = buffer.split(/\r?\n/)
      buffer = lines.pop() || ''
      lines.forEach(parseLine)
    }
    buffer += decoder.decode()
    if (buffer) parseLine(buffer)
    dispatch()
  } finally {
    reader.releaseLock()
  }
}

function normalizeRealtimeEvent(value: string): RealtimeEvent | null {
  try {
    const parsed = JSON.parse(value) as Partial<RealtimeEvent>
    const type = String(parsed.type || '')
    if (
      ![
        'admin_notification.changed',
        'order.changed',
        'work_order.changed',
        'worker_finance_request.changed',
        'worker_wallet.changed',
      ].includes(type)
    ) {
      return null
    }
    return {
      eventId: Number(parsed.eventId || 0),
      version: Number(parsed.version || parsed.eventId || 0),
      type: type as RealtimeEvent['type'],
      entityId: Number(parsed.entityId || 0),
      scopes: Array.isArray(parsed.scopes)
        ? parsed.scopes.map((item) => String(item || '')).filter(Boolean)
        : [],
      operation:
        parsed.operation === 'upsert' || parsed.operation === 'remove'
          ? parsed.operation
          : 'refresh',
      data: parsed.data,
      occurredAt: String(parsed.occurredAt || ''),
    }
  } catch {
    return null
  }
}
