import { EventEmitter } from 'node:events'
import WebSocket, { RawData } from 'ws'

export interface OrderbookLevel {
  price: number
  size: number
}

export interface OrderbookState {
  bids: OrderbookLevel[]
  asks: OrderbookLevel[]
  bestBid: number | null
  bestAsk: number | null
  midPrice: number | null
  spreadBps: number | null
  updatedAt: number
}

interface BybitObMessage {
  topic?: string
  type?: string
  data?: {
    s?: string
    b?: [string, string][]
    a?: [string, string][]
    u?: number
    seq?: number
  }
  ts?: number
}

const OB_URL = 'wss://stream.bybit.com/v5/public/linear'

export class BybitOrderbook extends EventEmitter {
  private socket?: WebSocket
  private pingTimer?: NodeJS.Timeout
  private reconnectTimer?: NodeJS.Timeout
  private stopped = false
  private readonly book = new Map<number, number>()
  private readonly bookSide = new Map<number, 'bid' | 'ask'>()
  private bids: OrderbookLevel[] = []
  private asks: OrderbookLevel[] = []

  start(): void {
    this.stopped = false
    const socket = new WebSocket(OB_URL)
    this.socket = socket

    socket.on('open', () => {
      socket.send(JSON.stringify({ op: 'subscribe', args: ['orderbook.50.BTCUSDT'] }))
      this.pingTimer = setInterval(() => {
        if (socket.readyState === WebSocket.OPEN) {
          socket.send(JSON.stringify({ op: 'ping' }))
        }
      }, 20_000)
      this.emit('ready')
    })

    socket.on('message', (payload: RawData) => {
      try {
        const msg = JSON.parse(payload.toString()) as BybitObMessage
        if (msg.topic !== 'orderbook.50.BTCUSDT' || !msg.data) return

        if (msg.type === 'snapshot') {
          this.book.clear()
          this.bookSide.clear()
          for (const [p, s] of msg.data.b ?? []) {
            const price = Number(p), size = Number(s)
            this.book.set(price, size)
            this.bookSide.set(price, 'bid')
          }
          for (const [p, s] of msg.data.a ?? []) {
            const price = Number(p), size = Number(s)
            this.book.set(price, size)
            this.bookSide.set(price, 'ask')
          }
        } else {
          for (const [p, s] of msg.data.b ?? []) {
            const price = Number(p), size = Number(s)
            if (size === 0) { this.book.delete(price); this.bookSide.delete(price) }
            else { this.book.set(price, size); this.bookSide.set(price, 'bid') }
          }
          for (const [p, s] of msg.data.a ?? []) {
            const price = Number(p), size = Number(s)
            if (size === 0) { this.book.delete(price); this.bookSide.delete(price) }
            else { this.book.set(price, size); this.bookSide.set(price, 'ask') }
          }
        }

        this.rebuildSorted()
        this.emit('update', this.getState())
      } catch (error) {
        this.emit('error', error)
      }
    })

    socket.on('error', (error: Error) => this.emit('error', error))
    socket.on('close', () => {
      if (this.pingTimer) { clearInterval(this.pingTimer); this.pingTimer = undefined }
      if (!this.stopped) {
        this.reconnectTimer = setTimeout(() => this.start(), 3_000)
      }
    })
  }

  stop(): void {
    this.stopped = true
    if (this.pingTimer) { clearInterval(this.pingTimer); this.pingTimer = undefined }
    if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = undefined }
    this.socket?.close()
  }

  getState(): OrderbookState {
    const bestBid = this.bids.length > 0 ? this.bids[0].price : null
    const bestAsk = this.asks.length > 0 ? this.asks[0].price : null
    const midPrice = bestBid != null && bestAsk != null ? (bestBid + bestAsk) / 2 : null
    const spreadBps = bestBid != null && bestAsk != null && midPrice != null
      ? Math.round(((bestAsk - bestBid) / midPrice) * 10_000 * 100) / 100
      : null
    return { bids: this.bids, asks: this.asks, bestBid, bestAsk, midPrice, spreadBps, updatedAt: Date.now() }
  }

  private rebuildSorted(): void {
    const bidEntries: OrderbookLevel[] = []
    const askEntries: OrderbookLevel[] = []
    for (const [price, size] of this.book.entries()) {
      const side = this.bookSide.get(price)
      if (side === 'bid') bidEntries.push({ price, size })
      else if (side === 'ask') askEntries.push({ price, size })
    }
    this.bids = bidEntries.sort((a, b) => b.price - a.price)
    this.asks = askEntries.sort((a, b) => a.price - b.price)
  }
}

export interface SimFill {
  avgPrice: number
  filledSize: number
  slippage: number
  levels: number
}

export function simulateMarketFill(
  book: OrderbookLevel[],
  sizeBtc: number,
  referencePrice: number
): SimFill | null {
  let remaining = sizeBtc
  let cost = 0
  let levels = 0

  for (const level of book) {
    if (remaining <= 0) break
    const take = Math.min(remaining, level.size)
    cost += take * level.price
    remaining -= take
    levels++
  }

  if (remaining > 0.0001) return null

  const filledSize = sizeBtc - remaining
  const avgPrice = cost / filledSize
  const slippage = Math.round(Math.abs(avgPrice - referencePrice) / referencePrice * 10_000 * 100) / 100

  return { avgPrice: Math.round(avgPrice * 10) / 10, filledSize, slippage, levels }
}
