import WebSocket from 'ws'
import { EventEmitter } from 'events'
import { AbiCoder, Wallet, keccak256, getBytes } from 'ethers'
import {
  AsterWsClientEmitter,
  AsterWsConfig,
  AsterStreamArg,
  AsterTicker,
  AsterTrade,
  AsterKline,
  AsterAuthConfig,
} from './types'

export class AsterWsClient extends (EventEmitter as { new (): AsterWsClientEmitter }) {
  private ws: WebSocket | null = null
  private cfg: Required<AsterWsConfig>
  private pingTimer: NodeJS.Timeout | null = null
  private pongTimer: NodeJS.Timeout | null = null
  private reconnectTimer: NodeJS.Timeout | null = null
  private subs: AsterStreamArg[] = []
  private auth?: AsterAuthConfig
  private listenKey?: string
  private userStreamUrl?: string

  constructor(cfg: AsterWsConfig, auth?: AsterAuthConfig) {
    super()
    this.cfg = {
      wsUrl: cfg.wsUrl,
      pingIntervalMs: cfg.pingIntervalMs ?? 30000,
      pongTimeoutMs: cfg.pongTimeoutMs ?? 10000,
      autoReconnect: cfg.autoReconnect ?? true,
      reconnectIntervalMs: cfg.reconnectIntervalMs ?? 5000,
    }
    this.auth = auth
    if (!this.cfg.wsUrl.includes('/ws/') && !this.cfg.wsUrl.includes('/stream')) {
      this.cfg.wsUrl = this.cfg.wsUrl.replace(/\/$/, '') + '/ws/'
    }
  }

  /**
   * 对行情/深度：使用 cfg.wsUrl（/ws 或 /stream）。
   * 对账户流：使用 setUserStream(listenKey, baseWsUrl) 设定 userStreamUrl 后调用 connectUserStream()。
   */
  connect(): void {
    if (this.ws) return
    this.ws = new WebSocket(this.cfg.wsUrl)
    this.ws.on('open', () => {
      this.emit('open')
      this.startPing()
      if (this.subs.length > 0) this.subscribe(this.subs)
    })
    this.ws.on('message', buf => this.handleMessage(buf.toString()))
    this.ws.on('pong', () => this.onPong())
    this.ws.on('close', (code, reasonBuf) => {
      const reason = reasonBuf.toString()
      this.emit('close', code, reason)
      this.stopPing()
      this.ws = null
      if (this.cfg.autoReconnect) this.scheduleReconnect()
    })
    this.ws.on('error', err => this.emit('error', err as any))
  }

  /** 设定账户 user stream 连接地址：wss://fstream.asterdex.com/ws/<listenKey> */
  setUserStream(listenKey: string, baseWsUrl: string) {
    this.listenKey = listenKey
    const base = baseWsUrl.replace(/\/$/, '')
    // 文档使用 /ws/<listenKey>
    this.userStreamUrl = `${base}/ws/${listenKey}`
  }

  /** 连接账户 user stream（需要先 setUserStream）*/
  connectUserStream(): void {
    if (!this.userStreamUrl) throw new Error('userStreamUrl not set')
    const ws = new WebSocket(this.userStreamUrl)
    // 独立的用户流连接：仅转发 raw/账户类事件
    ws.on('open', () => {
      this.emit('open')
      // 心跳
      const t = setInterval(() => {
        try {
          ws.ping()
        } catch {}
      }, 30000)
      ;(ws as any).__hb = t
    })
    ws.on('message', buf => this.handleUserMessage(buf.toString()))
    ws.on('close', (code, reasonBuf) => {
      try {
        clearInterval((ws as any).__hb)
      } catch {}
      this.emit('close', code, reasonBuf.toString())
      if (this.cfg.autoReconnect) this.scheduleReconnect()
    })
    ws.on('error', err => this.emit('error', err as any))
  }

  disconnect(): void {
    this.stopPing()
    if (this.ws) {
      this.ws.close()
      this.ws = null
    }
  }

  subscribe(args: AsterStreamArg[] | AsterStreamArg): void {
    const list = Array.isArray(args) ? args : [args]
    for (const a of list) {
      const key = JSON.stringify(a)
      if (!this.subs.find(x => JSON.stringify(x) === key)) this.subs.push(a)
    }
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return
    const streamNames = list.map(arg => {
      const symbol = arg.symbol?.toLowerCase() || 'btcusdt'
      const channel = arg.channel || 'aggTrade'
      return `${symbol}@${channel}`
    })
    const payload = { method: 'SUBSCRIBE', params: streamNames, id: Date.now() }
    this.ws.send(JSON.stringify(payload))
  }

  unsubscribe(args: AsterStreamArg[] | AsterStreamArg): void {
    const list = Array.isArray(args) ? args : [args]
    this.subs = this.subs.filter(s => !list.find(x => JSON.stringify(x) === JSON.stringify(s)))
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return
    const streamNames = list.map(arg => {
      const symbol = arg.symbol?.toLowerCase() || 'btcusdt'
      const channel = arg.channel || 'aggTrade'
      return `${symbol}@${channel}`
    })
    const payload = { method: 'UNSUBSCRIBE', params: streamNames, id: Date.now() }
    this.ws.send(JSON.stringify(payload))
  }

  private handleMessage(text: string) {
    let msg: any
    try {
      msg = JSON.parse(text)
    } catch {}
    if (!msg) return
    this.emit('raw', msg)
    if (msg.id && msg.result !== undefined) return
    if (msg.code !== undefined) return
    if (msg.stream && msg.data) {
      const streamName = msg.stream
      const data = msg.data
      if (streamName.includes('@aggTrade')) {
        const trade: AsterTrade = {
          symbol: data.s || data.symbol,
          price: Number(data.p || data.price),
          size: Number(data.q || data.quantity),
          side: data.m ? 'sell' : 'buy',
          ts: Number(data.T || data.timestamp || Date.now()),
        }
        if (trade.symbol && Number.isFinite(trade.price)) this.emit('trade', trade)
        return
      }
      if (streamName.includes('@markPrice')) {
        const ticker: AsterTicker = {
          symbol: data.s || data.symbol,
          price: Number(data.p || data.price),
          ts: Number(data.E || data.timestamp || Date.now()),
        }
        if (ticker.symbol && Number.isFinite(ticker.price)) this.emit('ticker', ticker)
        return
      }
    }
    if (msg.e) {
      const eventType = msg.e
      if (eventType === 'aggTrade') {
        const trade: AsterTrade = {
          symbol: msg.s,
          price: Number(msg.p),
          size: Number(msg.q),
          side: msg.m ? 'sell' : 'buy',
          ts: Number(msg.T),
        }
        if (trade.symbol && Number.isFinite(trade.price)) this.emit('trade', trade)
        return
      }
      if (eventType === 'markPriceUpdate') {
        const ticker: AsterTicker = { symbol: msg.s, price: Number(msg.p), ts: Number(msg.E) }
        if (ticker.symbol && Number.isFinite(ticker.price)) this.emit('ticker', ticker)
        return
      }
    }
  }

  // 账户 user stream 消息处理（ACCOUNT_UPDATE / ORDER_TRADE_UPDATE / ACCOUNT_CONFIG_UPDATE 等）
  private handleUserMessage(text: string) {
    let msg: any
    try {
      msg = JSON.parse(text)
    } catch {}
    if (!msg) return
    this.emit('raw', msg)
    if (msg.code !== undefined) {
      // -1125 INVALID_LISTEN_KEY or other errors
      const code = Number(msg.code)
      if (code === -1125) {
        this.emit('ws_error', { code: 'INVALID_LISTEN_KEY', detail: msg, ts: Date.now() })
        return
      }
      this.emit('ws_error', { code, detail: msg, ts: Date.now() })
      return
    }
    const ev = msg.e || msg.eventType
    if (!ev) return
    if (ev === 'ACCOUNT_UPDATE') {
      // 标准化为 account_info + account_positions
      const a = msg.a || {}
      const balances = Array.isArray(a.B) ? a.B : []
      const positions = Array.isArray(a.P) ? a.P : []
      this.emit('balance', { channel: 'account_balance', data: balances, ts: Number(msg.E || Date.now()) })
      this.emit('account_positions', { channel: 'account_positions', data: positions, ts: Number(msg.E || Date.now()) })
      // 也可以综合成 account_info 概要
      const pc = positions.filter((p: any) => Number(p.pa || p.positionAmt || 0) !== 0).length
      this.emit('account_info', {
        channel: 'account_info',
        data: { pc, t: Number(msg.E || Date.now()) },
        ts: Number(msg.E || Date.now()),
      })
      return
    }
    if (ev === 'ORDER_TRADE_UPDATE') {
      const o = msg.o || {}
      this.emit('orders', { channel: 'orders', data: o, ts: Number(msg.E || Date.now()) })
      return
    }
    if (ev === 'ACCOUNT_CONFIG_UPDATE') {
      this.emit('account_info', {
        channel: 'account_info',
        data: { t: Number(msg.E || Date.now()), ac: msg.ac, ai: msg.ai },
        ts: Number(msg.E || Date.now()),
      })
      return
    }
    if (ev === 'listenKeyExpired') {
      this.emit('ws_error', { code: 'listenKeyExpired', ts: Date.now() })
      return
    }
  }

  private startPing() {
    this.stopPing()
    this.pingTimer = setInterval(() => {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        try {
          this.ws.pong()
        } catch {}
      }
    }, 60000)
  }
  private stopPing() {
    if (this.pingTimer) clearInterval(this.pingTimer)
    this.pingTimer = null
    if (this.pongTimer) clearTimeout(this.pongTimer)
    this.pongTimer = null
  }
  private onPong() {
    if (this.pongTimer) clearTimeout(this.pongTimer)
    this.pongTimer = null
  }
  private scheduleReconnect() {
    if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
    this.reconnectTimer = setTimeout(() => this.connect(), this.cfg.reconnectIntervalMs)
  }
}
