/**
 * 现货行情 WS（连接 VITE_WS_SPOT_URL，协议与 Flutter SpotWsClient 一致）
 * 对外暴露与 useMarketWs 相同的回调接口，SpotView.vue 可直接替换。
 *
 * 协议要点：
 *  - 订阅：{ event: 'sub', params: { channel } }，逐个发送
 *  - 心跳：服务端 → { ping: ts }，客户端回 → { pong: ts }
 *  - K 线历史请求：{ event: 'req', params: { channel, cb_id, pageSize, endIdx } }
 *  - K 线历史响应：event_rep === 'req'，data 为数组，time 字段单位可能为秒
 *  - Channel 命名：market_{sym}_{ticker|depth|trade_ticker|kline_1min}
 */
import type { KlineResolution } from '@/stores/futures'

const WS_URL = import.meta.env.VITE_WS_SPOT_URL as string

/** 现货 WS K 线 period 后缀（与 Flutter spotWsKlinePeriod 一致） */
const SPOT_INTERVAL: Record<KlineResolution, string> = {
  '1': '1min', '5': '5min', '15': '15min', '30': '30min',
  '60': '60min', '240': '4h', '1D': '1day', '1W': '1week', '1M': '1month', '3M': '3month',
}

export const BAR_MS: Record<KlineResolution, number> = {
  '1':   60_000,
  '5':   300_000,
  '15':  900_000,
  '30':  1_800_000,
  '60':  3_600_000,
  '240': 14_400_000,
  '1D':  86_400_000,
  '1W':  604_800_000,
  '1M':  2_592_000_000,
  '3M':  7_776_000_000,
}
export const BAR_COUNT = 200

export interface WsKlineBar {
  timestamp: number
  open: number; high: number; low: number; close: number; volume: number
}

export interface WsTickerData {
  close: string; open: string; high: string; low: string
  volume: string; turnover: string
}

export interface WsTradeItem {
  price: string; qty: string; time: number; isSell: boolean
}

export interface WsBookData {
  bids: [string, string][]; asks: [string, string][]
}

export interface MarketWsCallbacks {
  onConnected?: () => void
  onKline?: (bar: WsKlineBar) => void
  onKlineHistory?: (bars: WsKlineBar[]) => void
  onKlineHistoryMore?: (bars: WsKlineBar[]) => void
  onTicker?: (data: WsTickerData) => void
  onTrade?: (item: WsTradeItem) => void
  onBook?: (data: WsBookData) => void
}

function num(v: unknown): number {
  const x = Number(v)
  return Number.isNaN(x) ? 0 : x
}

function toMs(ts: unknown): number {
  const v = Number(ts)
  if (!v || !Number.isFinite(v)) return 0
  return v < 10_000_000_000 ? v * 1000 : v
}

function parseDepthLevels(raw: unknown): [string, string][] {
  if (!Array.isArray(raw)) return []
  const out: [string, string][] = []
  for (const row of raw) {
    if (Array.isArray(row) && row.length >= 2) {
      const p = Number(row[0]), q = Number(row[1])
      if (p > 0 && q > 0 && Number.isFinite(p) && Number.isFinite(q)) {
        out.push([String(p), String(q)])
      }
    } else if (row && typeof row === 'object') {
      const o = row as Record<string, unknown>
      const p = Number(o.price ?? o.limitPrice ?? o.entrustPrice ?? o.p)
      const q = Number(o.quantity ?? o.amount ?? o.qty ?? o.q ?? o.a)
      if (p > 0 && q > 0 && Number.isFinite(p) && Number.isFinite(q)) {
        out.push([String(p), String(q)])
      }
    }
  }
  return out
}

export function useSpotMarketWs(callbacks: MarketWsCallbacks) {
  let ws: WebSocket | null = null
  let destroyed = false
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null
  let reconnectAttempt = 0

  let curSlug = ''
  let curResolution: KlineResolution = '60'
  let lastKlineReqIsMore = false

  function klineChannel(): string {
    return `market_${curSlug}_kline_${SPOT_INTERVAL[curResolution] ?? '60min'}`
  }

  function allChannels(): string[] {
    return [
      `market_${curSlug}_ticker`,
      `market_${curSlug}_depth`,
      `market_${curSlug}_trade_ticker`,
      klineChannel(),
    ]
  }

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

  function subOne(channel: string) {
    send({ event: 'sub', params: { channel } })
  }

  function unsubOne(channel: string) {
    send({ event: 'unsub', params: { channel } })
  }

  function subAll() {
    for (const ch of allChannels()) subOne(ch)
  }

  function unsubAll(channels?: string[]) {
    for (const ch of channels ?? allChannels()) unsubOne(ch)
  }

  function sendKlineReq(endIdx: number, isMore: boolean) {
    lastKlineReqIsMore = isMore
    send({
      event: 'req',
      params: {
        channel: klineChannel(),
        cb_id: curSlug,
        pageSize: 300,
        endIdx: endIdx > 0 ? endIdx : 0,
      },
    })
  }

  function routeMsg(raw: string) {
    let msg: Record<string, unknown>
    try { msg = JSON.parse(raw) as Record<string, unknown> } catch { return }

    // Server heartbeat: server → {ping: ts}, client → {pong: ts}
    if (msg.ping !== undefined) {
      send({ pong: msg.ping })
      return
    }

    // K-line history response
    if (msg.event_rep === 'req') {
      const dataArr = msg.data
      if (!Array.isArray(dataArr)) return
      const bars: WsKlineBar[] = dataArr
        .filter((d): d is Record<string, unknown> => !!d && typeof d === 'object')
        .map(d => ({
          timestamp: toMs(d.time ?? d.id),
          open: num(d.open),
          high: num(d.high),
          low: num(d.low),
          close: num(d.close),
          volume: num(d.vol ?? d.volume),
        }))
        .filter(b => b.timestamp > 0)
        .sort((a, b) => a.timestamp - b.timestamp)
      if (lastKlineReqIsMore) {
        callbacks.onKlineHistoryMore?.(bars)
      } else {
        callbacks.onKlineHistory?.(bars)
      }
      return
    }

    const ch = String(msg.channel ?? msg.ch ?? '')
    // For real-time pushes tick is always an object, not an array
    const rawTick = msg.tick ?? msg.data
    const tick = rawTick && !Array.isArray(rawTick) && typeof rawTick === 'object'
      ? rawTick as Record<string, unknown>
      : undefined
    if (!ch) return

    // Ticker (excludes trade_ticker)
    if (ch.endsWith('_ticker') && !ch.includes('trade_ticker')) {
      if (!tick) return
      const close = num(tick.close ?? tick.last ?? tick.price ?? tick.c ?? tick.latest ?? tick.es_price)
      const open  = num(tick.open  ?? tick.o)
      const high  = num(tick.high  ?? tick.h)
      const low   = num(tick.low   ?? tick.l)
      const vol   = num(tick.vol   ?? tick.volume ?? tick.v ?? tick.baseVolume)
      const turn  = num(tick.amount ?? tick.quoteVolume ?? tick.q ?? tick.turnover)
      callbacks.onTicker?.({
        close: String(close), open: String(open), high: String(high), low: String(low),
        volume: String(vol), turnover: String(turn),
      })
      return
    }

    // Depth (order book)
    if (ch.endsWith('_depth')) {
      if (!tick) return
      let t: Record<string, unknown> = tick
      // Inner data may be nested under tick.data
      if (!t.bids && !t.buys && !t.asks && !t.sells
          && t.data && typeof t.data === 'object' && !Array.isArray(t.data)) {
        t = t.data as Record<string, unknown>
      }
      const asks = parseDepthLevels(t.asks ?? t.sells)
      const bids = parseDepthLevels(t.bids ?? t.buys)
      if (asks.length || bids.length) {
        callbacks.onBook?.({ asks, bids })
      }
      return
    }

    // Trades (trade_ticker channel)
    if (ch.includes('trade_ticker')) {
      if (!tick) return
      const data = tick.data
      const items: unknown[] = Array.isArray(data) ? data : [tick]
      for (const item of items) {
        if (!item || typeof item !== 'object') continue
        const one = item as Record<string, unknown>
        const ts     = toMs(one.time ?? one.ts ?? one.T)
        const price  = num(one.price ?? one.p)
        const qty    = num(one.vol ?? one.quantity ?? one.q)
        const isSell = String(one.side ?? one.S ?? '').toUpperCase() === 'SELL'
        callbacks.onTrade?.({ price: String(price), qty: String(qty), time: ts, isSell })
      }
      return
    }

    // K-line real-time bar
    if (ch.includes('_kline_')) {
      if (!tick) return
      const ts = toMs(tick.time ?? tick.id)
      if (!ts) return
      callbacks.onKline?.({
        timestamp: ts,
        open: num(tick.open),
        high: num(tick.high),
        low: num(tick.low),
        close: num(tick.close),
        volume: num(tick.vol ?? tick.volume),
      })
      return
    }
  }

  function backoffMs(): number {
    return Math.min(30_000, 1000 * 2 ** Math.min(reconnectAttempt, 5))
  }

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

  function connect(sym: string, resolution: KlineResolution) {
    destroyed = false
    curSlug = sym.toUpperCase().replace(/[/\s-]/g, '').toLowerCase()
    curResolution = resolution
    if (reconnectTimer !== null) { clearTimeout(reconnectTimer); reconnectTimer = null }
    if (ws) { ws.onclose = null; ws.close(); ws = null }
    doConnect()
  }

  function changeSymbol(sym: string) {
    connect(sym, curResolution)
  }

  function changeResolution(resolution: KlineResolution) {
    if (curResolution === resolution) return
    const oldKline = klineChannel()
    curResolution = resolution
    unsubOne(oldKline)
    subOne(klineChannel())
    sendKlineReq(0, false)
  }

  function requestKlines() {
    sendKlineReq(0, false)
  }

  // endIdx for spot WS is in seconds; `to` from chart loader is in ms
  function requestKlinesRange(_from: number, to: number) {
    sendKlineReq(Math.floor(to / 1000), true)
  }

  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, changeSymbol, changeResolution, requestKlines, requestKlinesRange, disconnect }
}
