/**
 * 现货行情 WebSocket（与 App spot_ws_client 协议对齐）
 * 订阅：{ event: 'sub', params: { channel: 'market_btcusdt_ticker' } }
 * 心跳：服务端 ping → 客户端 { pong: ts }
 */
const WS_BASE = import.meta.env.VITE_WS_SPOT_URL as string

export interface SpotWsTicker {
  last: number
  open: number
  high: number
  low: number
  vol: number
  amount: number
  changePct: number
  buyOne?: number
  sellOne?: number
}

export type DepthLevel = [number, number]

export interface SpotWsTrade {
  price: number
  vol: number
  side: 'BUY' | 'SELL'
  time: number
}

export interface SpotWsCallbacks {
  onTicker?: (t: SpotWsTicker) => void
  onDepth?: (asks: DepthLevel[], bids: DepthLevel[]) => void
  onTrade?: (t: SpotWsTrade) => void
}

function slug(sym: string): string {
  return sym.toUpperCase().replace(/[/\s-]/g, '').toLowerCase()
}

function numFromTick(t: Record<string, unknown>, keys: string[]): number {
  for (const k of keys) {
    const v = t[k]
    if (v === undefined || v === null || v === '') continue
    const n = Number(v)
    if (!Number.isNaN(n)) return n
  }
  return 0
}

function normalizeRose(rose: unknown, open: number, close: number): number {
  if (rose !== undefined && rose !== null && rose !== '') {
    const s = String(rose).replace('%', '')
    const n = Number(s)
    if (!Number.isNaN(n)) {
      return Math.abs(n) < 1 && String(rose).includes('.') ? n * 100 : n
    }
  }
  if (open > 0 && close > 0) {
    return ((close - open) / open) * 100
  }
  return 0
}

function parseDepthSide(raw: unknown): DepthLevel[] {
  if (!Array.isArray(raw)) return []
  const out: DepthLevel[] = []
  for (const row of raw) {
    if (Array.isArray(row) && row.length >= 2) {
      const p = Number(row[0])
      const q = Number(row[1])
      if (!Number.isNaN(p) && !Number.isNaN(q)) out.push([p, q])
      continue
    }
    if (row && typeof row === 'object') {
      const o = row as Record<string, unknown>
      const p = Number(o.price ?? o.p)
      const q = Number(o.quantity ?? o.vol ?? o.amount ?? o.q)
      if (!Number.isNaN(p) && !Number.isNaN(q)) out.push([p, q])
    }
  }
  return out
}

function depthFromTick(tick: Record<string, unknown>): { asks: DepthLevel[]; bids: DepthLevel[] } {
  let asks = parseDepthSide(tick.asks ?? tick.sells)
  let bids = parseDepthSide(tick.bids ?? tick.buys)
  const inner = tick.data
  if (inner && typeof inner === 'object') {
    const d = inner as Record<string, unknown>
    if (!asks.length) {
      asks = parseDepthSide(d.asks ?? d.sells)
    }
    if (!bids.length) {
      bids = parseDepthSide(d.bids ?? d.buys)
    }
  }
  return { asks, bids }
}

export interface UseSpotWsOptions {
  /** 为 true 时仅订阅 market_{symbol}_ticker */
  tickerOnly?: boolean
  /** 为 true 时仅订阅 market_{symbol}_trade_ticker（IDO 等场景） */
  tradeTickerOnly?: boolean
}

export function useSpotWs(callbacks: SpotWsCallbacks, options?: UseSpotWsOptions) {
  let ws: WebSocket | null = null
  let destroyed = false
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null
  let curSlug = ''
  let reconnectAttempt = 0
  let sessionUid = ''

  function wsChannels(): string[] {
    if (!curSlug) return []
    const base = `market_${curSlug}`
    if (options?.tradeTickerOnly) {
      return [`${base}_trade_ticker`]
    }
    if (options?.tickerOnly) {
      return [`${base}_ticker`]
    }
    return [`${base}_ticker`, `${base}_depth`, `${base}_trade_ticker`]
  }

  function buildUrl(): string {
    const q = new URLSearchParams({ clientType: 'web' })
    if (sessionUid) {
      q.set('uid', sessionUid)
    }
    const sep = WS_BASE.includes('?') ? '&' : '?'
    return `${WS_BASE}${sep}${q.toString()}`
  }

  function send(obj: object) {
    if (ws?.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(obj))
    }
  }

  function unsubAll() {
    for (const ch of wsChannels()) {
      send({ event: 'unsub', params: { channel: ch } })
    }
  }

  function subAll() {
    for (const ch of wsChannels()) {
      send({ event: 'sub', params: { channel: ch } })
    }
  }

  function routeMsg(raw: string) {
    let msg: Record<string, unknown>
    try {
      msg = JSON.parse(raw) as Record<string, unknown>
    } catch {
      return
    }
    if (msg.ping !== undefined) {
      send({ pong: msg.ping })
      return
    }
    const ch = String(msg.channel ?? msg.ch ?? '')
    const tick = (msg.tick ?? msg.data) as Record<string, unknown> | undefined
    if (!ch || !tick || typeof tick !== 'object') return

    if (ch.includes('_ticker') && !ch.includes('trade_ticker')) {
      const open = numFromTick(tick, ['open', 'o'])
      const close = numFromTick(tick, ['close', 'last', 'price', 'c', 'latest'])
      callbacks.onTicker?.({
        last: close,
        open,
        high: numFromTick(tick, ['high', 'h']),
        low: numFromTick(tick, ['low', 'l']),
        vol: numFromTick(tick, ['vol', 'volume']),
        amount: numFromTick(tick, ['amount', 'turnover']),
        changePct: normalizeRose(tick.rose, open, close),
        buyOne: numFromTick(tick, ['buyOne', 'bid']) || undefined,
        sellOne: numFromTick(tick, ['sellOne', 'ask']) || undefined,
      })
      return
    }
    if (ch.includes('_depth')) {
      const { asks, bids } = depthFromTick(tick)
      callbacks.onDepth?.(asks, bids)
      return
    }
    if (ch.includes('trade_ticker')) {
      const price = numFromTick(tick, ['price', 'p', 'close', 'last', 'c'])
      callbacks.onTrade?.({
        price,
        vol: numFromTick(tick, ['vol', 'quantity', 'q']),
        side: String(tick.side ?? tick.S ?? '').toUpperCase() === 'SELL' ? 'SELL' : 'BUY',
        time: Number(tick.time ?? tick.T ?? tick.ts ?? 0),
      })
      if (price > 0) {
        const open = numFromTick(tick, ['open', 'o'])
        callbacks.onTicker?.({
          last: price,
          open,
          high: numFromTick(tick, ['high', 'h']),
          low: numFromTick(tick, ['low', 'l']),
          vol: numFromTick(tick, ['vol', 'volume']),
          amount: numFromTick(tick, ['amount', 'turnover']),
          changePct: normalizeRose(tick.rose, open, price),
        })
      }
    }
  }

  function backoffMs(): number {
    const base = Math.min(30_000, 1000 * Math.pow(2, reconnectAttempt))
    return reconnectAttempt >= 50 ? 120_000 : base
  }

  function doConnect() {
    if (destroyed || !WS_BASE) return
    ws = new WebSocket(buildUrl())
    ws.onopen = () => {
      reconnectAttempt = 0
      subAll()
    }
    ws.onmessage = (e) => routeMsg(String(e.data))
    ws.onerror = () => {}
    ws.onclose = () => {
      if (destroyed) return
      reconnectAttempt++
      reconnectTimer = setTimeout(doConnect, backoffMs())
    }
  }

  function connect(symbol: string, uid?: string) {
    sessionUid = uid ?? ''
    const next = slug(symbol)
    destroyed = false
    if (reconnectTimer !== null) {
      clearTimeout(reconnectTimer)
      reconnectTimer = null
    }
    if (ws) {
      try {
        unsubAll()
      } catch {
        /* ignore */
      }
      ws.onclose = null
      ws.close()
      ws = null
    }
    curSlug = next
    doConnect()
  }

  function disconnect() {
    destroyed = true
    reconnectAttempt = 0
    if (reconnectTimer !== null) {
      clearTimeout(reconnectTimer)
      reconnectTimer = null
    }
    if (ws) {
      try {
        unsubAll()
      } catch {
        /* ignore */
      }
      ws.onclose = null
      ws.close()
      ws = null
    }
    curSlug = ''
  }

  return { connect, disconnect }
}

export { slug as spotWsSlug }
