import { onUnmounted, ref, type Ref } from 'vue'

/**
 * 与 SimpleLite `/projection/stream`（SSE）通信的轻量封装。
 *
 * 服务器以 `text/event-stream` 推送 JSON 行：
 *   ```
 *   event: car-state
 *   data: { "kind": "car-state", "tick": 123, "payload": { ... } }
 *   ```
 *
 * 离线 / 未启用时返回的 `connected = false`，调用方可继续用 3s 轮询兜底。
 */

export interface StreamEvent {
  kind: string
  tick?: number
  payload?: unknown
}

export interface ProjectionStreamOptions {
  /** SSE URL。默认走 axios baseURL `/api` + `/sl/projection/stream`。 */
  url?: string
  /** 挂载时自动 connect。默认 true。 */
  autoConnect?: boolean
  /** 断线后重连延时（ms）。默认 3000。 */
  reconnectDelayMs?: number
  /**
   * 额外要订阅的命名事件。这些会和 {@link KNOWN_EVENT_KINDS} 合并后注册到 EventSource。
   * 当后端新增 broadcast 事件名时，可不改动 composable，直接通过此参数补充。
   */
  extraEventKinds?: readonly string[]
}

/**
 * SimpleLite 当前后端会广播的全部 SSE 事件名。新增后端事件时务必同步追加，
 * 否则 EventSource 拿不到该事件 —— 这是历史上 alarm / object-* 被静默丢弃的根因。
 */
export const KNOWN_EVENT_KINDS = [
  'snapshot-tick',
  'selection-detail',
  'alarm',
  'object-created',
  'object-deleted',
  'object-patched',
  'object-batch-changed',
  'pick-result',
  'project-saved',
  'project-loaded',
  'car-state',
  'mission-status',
  'monitor-config-updated',
  'workspace-shortcut',
  'viewport-style-updated'
] as const

type Listener = (event: StreamEvent) => void

export function useProjectionStream(opts: ProjectionStreamOptions = {}) {
  const apiBase = (import.meta.env.VITE_API_BASE as string | undefined) ?? '/api'
  const defaultUrl = `${apiBase}/sl/projection/stream`
  const url = opts.url ?? defaultUrl
  const reconnectDelay = opts.reconnectDelayMs ?? 3000
  // 与 api/* 模块统一走 VITE_USE_MOCK 开关（替代旧的 VITE_PROJECTION_MOCK）。
  const isMock = import.meta.env.VITE_USE_MOCK === 'true'

  const connected = ref(false)
  const lastEvent: Ref<StreamEvent | null> = ref(null)
  const listeners = new Set<Listener>()

  let es: EventSource | null = null
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null
  let mockTimer: ReturnType<typeof setInterval> | null = null
  let mockTick = 0
  let stopped = false

  function connect() {
    if (stopped) return

    // Mock 模式：不发 EventSource（无后端可连），改用本地心跳广播
    // `snapshot-tick`，让订阅方的"实时刷新"路径同样可被验证。
    if (isMock) {
      if (mockTimer) return
      connected.value = true
      mockTimer = setInterval(() => {
        mockTick++
        const event: StreamEvent = {
          kind: 'snapshot-tick',
          tick: mockTick,
          payload: { reason: 'mock', tick: mockTick }
        }
        lastEvent.value = event
        listeners.forEach((cb) => {
          try { cb(event) } catch { /* swallow */ }
        })
      }, 5000)
      return
    }

    if (es) return
    try {
      // withCredentials：让浏览器把 httpOnly 鉴权 Cookie (simple.auth.token) 随 SSE 一起带上，
      // 配合后端给 /api/sl/* 反代加的鉴权（EventSource 无法设置 Authorization header，只能靠 Cookie）。
      es = new EventSource(url, { withCredentials: true })
    } catch {
      scheduleReconnect()
      return
    }

    es.onopen = () => {
      connected.value = true
    }

    es.onerror = () => {
      connected.value = false
      closeSocket()
      scheduleReconnect()
    }

    es.onmessage = (msg) => {
      handleEventLine(undefined, msg.data)
    }

    // 命名事件：EventSource 默认只把"无 event: 头"的消息派发给 onmessage，
    // 任何 `event: foo` 都必须通过 addEventListener('foo', ...) 才能收到。
    // 必须覆盖 SimpleLite 后端可能广播的所有事件名 —— 漏一个就等于该事件被丢弃。
    // 当前后端事件源：
    //   - ProjectionStreamModule: snapshot-tick (1Hz 心跳/计数)
    //   - SimpleUI / ReflectionApiController: selection-detail
    //   - AlarmStreamService: alarm
    //   - MapEditApiController: object-created / object-deleted / object-patched
    //                           / object-batch-changed / pick-result
    //                           / project-saved / project-loaded
    //   - 保留 car-state / mission-status 占位以兼容未来事件型推送
    const allKinds = new Set<string>([...KNOWN_EVENT_KINDS, ...(opts.extraEventKinds ?? [])])
    for (const kind of allKinds) {
      es.addEventListener(kind, (msg: MessageEvent) => {
        handleEventLine(kind, msg.data)
      })
    }
  }

  function handleEventLine(kindHint: string | undefined, raw: unknown) {
    if (typeof raw !== 'string' || !raw) return
    try {
      const parsed = JSON.parse(raw)
      const event: StreamEvent =
        typeof parsed === 'object' && parsed !== null && 'kind' in parsed
          ? (parsed as StreamEvent)
          : { kind: kindHint ?? 'message', payload: parsed }
      lastEvent.value = event
      listeners.forEach((cb) => {
        try { cb(event) } catch { /* swallow */ }
      })
    } catch {
      // ignore malformed payload
    }
  }

  function closeSocket() {
    if (es) {
      try { es.close() } catch { /* ignore */ }
      es = null
    }
  }

  function scheduleReconnect() {
    if (stopped) return
    if (reconnectTimer) return
    reconnectTimer = setTimeout(() => {
      reconnectTimer = null
      connect()
    }, reconnectDelay)
  }

  function disconnect() {
    stopped = true
    if (reconnectTimer) {
      clearTimeout(reconnectTimer)
      reconnectTimer = null
    }
    if (mockTimer) {
      clearInterval(mockTimer)
      mockTimer = null
    }
    closeSocket()
    connected.value = false
  }

  function on(cb: Listener) { listeners.add(cb) }
  function off(cb: Listener) { listeners.delete(cb) }

  if (opts.autoConnect !== false) connect()

  onUnmounted(() => {
    disconnect()
    listeners.clear()
  })

  return { connected, lastEvent, connect, disconnect, on, off }
}
