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

import type { TapeTrade, TradeSide } from '../../core/types.js'

interface CoinbaseMatchMessage {
  type: 'match' | 'last_match' | 'subscriptions'
  trade_id?: number
  side?: 'buy' | 'sell'
  size?: string
  price?: string
  product_id?: string
  time?: string
}

const COINBASE_URL = 'wss://ws-feed.exchange.coinbase.com'

function normalizeAggressorSide(makerSide: 'buy' | 'sell'): TradeSide {
  return makerSide === 'buy' ? 'sell' : 'buy'
}

export class CoinbaseTradeStreamer extends EventEmitter {
  readonly name = 'coinbase'

  private socket?: WebSocket
  private reconnectTimer?: NodeJS.Timeout
  private stopped = false

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

    socket.on('open', () => {
      socket.send(
        JSON.stringify({
          type: 'subscribe',
          product_ids: ['BTC-USD'],
          channels: ['matches']
        })
      )
    })

    socket.on('message', (payload: RawData) => {
      try {
        const message = JSON.parse(payload.toString()) as CoinbaseMatchMessage

        if (message.type === 'subscriptions') {
          this.emit('discover', { stream: 'spot', count: 1, symbols: ['BTC-USD'] })
          this.emit('ready', { stream: 'spot', symbolCount: 1, url: COINBASE_URL })
          return
        }

        if (message.type !== 'match' && message.type !== 'last_match') {
          return
        }

        const price = Number(message.price)
        const size = Number(message.size)
        const timestamp = Date.parse(String(message.time))

        if (!Number.isFinite(price) || !Number.isFinite(size) || !Number.isFinite(timestamp) || price <= 0 || size <= 0) {
          return
        }

        const trade: TapeTrade = {
          exchange: 'COINBASE',
          exchangeClass: 'cex',
          instrumentType: 'spot',
          symbol: String(message.product_id),
          timestamp,
          price,
          size,
          notionalUsd: price * size,
          side: normalizeAggressorSide(message.side as 'buy' | 'sell'),
          tradeId: String(message.trade_id),
          raw: message
        }

        this.emit('trade', trade)
      } catch (error) {
        this.emit('error', error)
      }
    })

    socket.on('error', (error: Error) => this.emit('error', error))
    socket.on('close', () => this.handleClose())
  }

  stop(): void {
    this.stopped = true

    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer)
      this.reconnectTimer = undefined
    }

    this.socket?.close()
    this.socket = undefined
  }

  private handleClose(): void {
    this.socket = undefined

    if (this.stopped) {
      return
    }

    this.reconnectTimer = setTimeout(() => this.start(), 3_000)
    this.emit('warn', 'Reconnecting spot stream in 3s')
  }
}
