import { upperCaseSymbols } from '../handy.ts'
import { BookChange, Exchange, BookTicker, Trade, DerivativeTicker } from '../types.ts'
import { Mapper, PendingTickerInfoHelper } from './mapper.ts'

export class CryptoComTradesMapper implements Mapper<'crypto-com', Trade> {
  constructor(private readonly _exchange: Exchange) {}
  canHandle(message: CryptoComTradeMessage) {
    return message.result !== undefined && message.result.channel === 'trade'
  }

  getFilters(symbols?: string[]) {
    symbols = upperCaseSymbols(symbols)

    return [
      {
        channel: 'trade',
        symbols
      } as const
    ]
  }

  *map(message: CryptoComTradeMessage, localTimestamp: Date): IterableIterator<Trade> {
    message.result.data.reverse()

    for (const item of message.result.data) {
      const trade: Trade = {
        type: 'trade',
        symbol: message.result.instrument_name,
        exchange: this._exchange,
        id: item.d.toString(),
        price: Number(item.p),
        amount: Number(item.q),
        side: item.s === 'BUY' ? 'buy' : 'sell',
        timestamp: new Date(item.t),
        localTimestamp
      }

      yield trade
    }
  }
}

export class CryptoComBookChangeMapper implements Mapper<'crypto-com', BookChange> {
  constructor(protected readonly _exchange: Exchange) {}

  canHandle(message: CryptoComBookMessage) {
    return message.result !== undefined && message.result.channel.startsWith('book')
  }

  getFilters(symbols?: string[]) {
    symbols = upperCaseSymbols(symbols)
    return [
      {
        channel: 'book',
        symbols
      } as const
    ]
  }

  *map(message: CryptoComBookMessage, localTimestamp: Date) {
    if (message.result.data === undefined || message.result.data[0] === undefined) {
      return
    }

    const bids = (message.result.channel === 'book' ? message.result.data[0].bids : message.result.data[0].update.bids) || []
    const asks = (message.result.channel === 'book' ? message.result.data[0].asks : message.result.data[0].update.asks) || []

    yield {
      type: 'book_change',
      symbol: message.result.instrument_name,
      exchange: this._exchange,
      isSnapshot: message.result.channel === 'book',
      bids: bids.map(this._mapBookLevel),
      asks: asks.map(this._mapBookLevel),
      timestamp: new Date(message.result.data[0].t),
      localTimestamp
    } as const
  }

  private _mapBookLevel(level: [number | string, number | string]) {
    return { price: Number(level[0]), amount: Number(level[1]) }
  }
}

export class CryptoComBookTickerMapper implements Mapper<'crypto-com', BookTicker> {
  constructor(protected readonly _exchange: Exchange) {}

  canHandle(message: CryptoComTickerMessage) {
    return message.result !== undefined && message.result.channel === 'ticker'
  }

  getFilters(symbols?: string[]) {
    symbols = upperCaseSymbols(symbols)
    return [
      {
        channel: 'ticker',
        symbols
      } as const
    ]
  }

  *map(message: CryptoComTickerMessage, localTimestamp: Date) {
    for (const item of message.result.data) {
      const bookTicker: BookTicker = {
        type: 'book_ticker',
        symbol: message.result.instrument_name,
        exchange: this._exchange,

        askAmount: item.ks !== undefined && item.ks !== null ? Number(item.ks) : undefined,
        askPrice: item.k !== undefined && item.k !== null ? Number(item.k) : undefined,
        bidPrice: item.b !== undefined && item.b !== null ? Number(item.b) : undefined,
        bidAmount: item.bs !== undefined && item.bs !== null ? Number(item.bs) : undefined,
        timestamp: new Date(item.t),
        localTimestamp: localTimestamp
      }

      yield bookTicker
    }
  }
}

export class CryptoComDerivativeTickerMapper implements Mapper<'crypto-com', DerivativeTicker> {
  private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
  private readonly _indexPrices = new Map<string, number>()

  constructor(protected readonly exchange: Exchange) {}

  canHandle(message: CryptoComDerivativesTickerMessage | CryptoComIndexMessage | CryptoComMarkPriceMessage | CryptoComFundingMessage) {
    if (message.result === undefined) {
      return false
    }

    if (message.result.instrument_name === undefined) {
      return false
    }

    // spot symbols
    if (message.result.instrument_name.includes('_')) {
      return false
    }
    // options
    if (message.result.instrument_name.split('-').length === 3) {
      return false
    }

    return (
      message.result.channel === 'ticker' ||
      message.result.channel === 'index' ||
      message.result.channel === 'mark' ||
      message.result.channel === 'funding'
    )
  }

  getFilters(symbols?: string[]) {
    symbols = upperCaseSymbols(symbols)

    let indexes: string[] = []
    if (symbols !== undefined) {
      indexes = [...new Set(symbols.map((s) => `${s.split('-')[0]}-INDEX`))]
    }
    const filters = [
      {
        channel: 'ticker',
        symbols
      } as const,
      {
        channel: 'index',
        symbols: indexes
      } as const,
      {
        channel: 'mark',
        symbols
      } as const,
      {
        channel: 'funding',
        symbols
      } as const,
      {
        channel: 'estimatedfunding',
        symbols
      } as const
    ]

    return filters
  }

  *map(
    message:
      | CryptoComDerivativesTickerMessage
      | CryptoComIndexMessage
      | CryptoComMarkPriceMessage
      | CryptoComFundingMessage
      | CryptoComEstFundingMessage,
    localTimestamp: Date
  ): IterableIterator<DerivativeTicker> {
    if (message.result.channel === 'index') {
      this._indexPrices.set(message.result.instrument_name.split('-')[0], Number(message.result.data[0].v))
      return
    }

    const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(message.result.instrument_name, this.exchange)

    const lastIndexPrice = this._indexPrices.get(message.result.instrument_name.split('-')[0])
    if (lastIndexPrice !== undefined) {
      pendingTickerInfo.updateIndexPrice(lastIndexPrice)
    }

    if (message.result.channel === 'ticker') {
      if (message.result.data[0].a !== null && message.result.data[0].a !== undefined) {
        pendingTickerInfo.updateLastPrice(Number(message.result.data[0].a))
      }
      if (message.result.data[0].oi !== null && message.result.data[0].oi !== undefined) {
        pendingTickerInfo.updateOpenInterest(Number(message.result.data[0].oi))
      }
    }

    if (message.result.channel === 'mark') {
      if (message.result.data[0].v !== null && message.result.data[0].v !== undefined) {
        pendingTickerInfo.updateMarkPrice(Number(message.result.data[0].v))
      }
    }

    if (message.result.channel === 'funding') {
      if (message.result.data[0].v !== null && message.result.data[0].v !== undefined) {
        pendingTickerInfo.updateFundingRate(Number(message.result.data[0].v))
        const nextFundingTimestamp = new Date(message.result.data[0].t)
        nextFundingTimestamp.setUTCHours(nextFundingTimestamp.getUTCHours() + 1)
        nextFundingTimestamp.setUTCMinutes(0, 0, 0)
        pendingTickerInfo.updateFundingTimestamp(nextFundingTimestamp)
      }
    }
    if (message.result.channel === 'estimatedfunding') {
      if (message.result.data[0] && message.result.data[0].v !== null && message.result.data[0].v !== undefined) {
        pendingTickerInfo.updatePredictedFundingRate(Number(message.result.data[0].v))
      }
    }

    pendingTickerInfo.updateTimestamp(new Date(message.result.data[0].t))

    if (pendingTickerInfo.hasChanged()) {
      yield pendingTickerInfo.getSnapshot(localTimestamp)
    }
  }
}

type CryptoComTradeMessage =
  | {
      method: 'subscribe'
      result: {
        instrument_name: 'ETH_CRO' // instrument_name
        subscription: 'trade.ETH_CRO'
        channel: 'trade'
        data: [
          {
            p: 162.12 // price
            q: 11.085 // quantity
            s: 'BUY' // side
            d: 1210447366 // trade id
            t: 1587523078844 // trade time
            dataTime: 0 // please ignore this field
          }
        ]
      }
    }
  | {
      id: -1
      code: 0
      method: 'subscribe'
      result: {
        channel: 'trade'
        subscription: 'trade.BTCUSD-PERP'
        instrument_name: 'BTCUSD-PERP'
        data: [{ d: '4611686018439397540'; t: 1653992578435; p: '31603.5'; q: '0.1000'; s: 'BUY'; i: 'BTCUSD-PERP' }]
      }
    }

type CryptoComBookMessage =
  | {
      code: 0
      method: 'subscribe'
      result: {
        instrument_name: 'ETH_CRO'
        subscription: 'book.ETH_CRO.150'
        channel: 'book'
        depth: 150
        data: [
          {
            bids: [number, number][]
            asks: [number, number][]
            t: 1659311999933
            s: 788293808
          }
        ]
      }
    }
  | {
      code: 0
      method: 'subscribe'
      result: {
        instrument_name: 'DOT_USDT'
        subscription: 'book.DOT_USDT.150'
        channel: 'book.update'
        depth: 150
        data: [
          {
            update: { bids: [number, number][]; asks: [number, number][] }
            t: 1659312000046
            s: 763793123
          }
        ]
      }
    }
  | {
      id: -1
      code: 0
      method: 'subscribe'
      result: {
        channel: 'book.update'
        subscription: 'book.BTCUSD-PERP.50'
        instrument_name: 'BTCUSD-PERP'
        depth: 50
        data: [
          {
            update: { asks: [string, string][]; bids: [string, string][] }
            t: 1653992578436
            tt: 1653992578428
            u: 72560693920
            pu: 72560688000
            cs: 380529173
          }
        ]
      }
    }

type CryptoComTickerMessage =
  | {
      code: 0
      method: 'subscribe'
      result: {
        instrument_name: 'GODS_USDT'
        subscription: 'ticker.GODS_USDT'
        channel: 'ticker'
        data: [
          {
            i: 'GODS_USDT'
            b: 0.4262
            bs?: 0.1
            k: 0.4272
            ks?: 0.2
            a: 0.4272
            t: 1659311999946
            v: 100623.01
            vv: 42986.1541
            h: 0.4624
            l: 0.4229
            c: -0.0062
            pc: -1.4302
          }
        ]
      }
    }
  | CryptoComDerivativesTickerMessage

type CryptoComDerivativesTickerMessage =
  | {
      id: -1
      code: 0
      method: 'subscribe'
      result: {
        channel: 'ticker'
        instrument_name: 'BTCUSD-PERP'
        subscription: 'ticker.BTCUSD-PERP'
        data: [
          {
            h: '32222.5'
            l: '30240.0'
            a: '31611.0'
            c: '0.0320'
            b: '31613.0'
            bs?: '0.1000'
            k: '31613.5'
            ks?: '0.2000'
            i: 'BTCUSD-PERP'
            v: '13206.4884'
            vv: '433945264.39'
            oi: '318.5162'
            t: 1653992543383
          }
        ]
      }
    }
  | {
      id: 2
      method: 'subscribe'
      code: 0
      result: {
        instrument_name: 'ESUSD-PERP'
        subscription: 'ticker.ESUSD-PERP'
        channel: 'ticker'
        data: [
          {
            h: '0.09625'
            l: '0.09230'
            a: '0.09481'
            c: '-0.0038'
            b: '0.09451'
            bs: '1'
            k: '0.09452'
            ks: '8461'
            i: 'ESUSD-PERP'
            v: '115'
            vv: '10.84'
            oi: '78522'
            t: 1765238404604
          }
        ]
      }
    }
  | {
      id: -1
      code: 0
      method: 'subscribe'
      result: {
        channel: 'ticker'
        instrument_name: 'MATIC_USD'
        subscription: 'ticker.MATIC_USD'
        id: 1
        data: [
          {
            h: '1.24383'
            l: '1.18086'
            a: '1.19604'
            c: '-0.0315'
            b: '1.19591'
            bs: '0.1'
            k: '1.19643'
            ks: '0.7'
            i: 'MATIC_USD'
            v: '854908.9'
            vv: '1043976.96'
            oi: '0'
            t: 1677628802241
          }
        ]
      }
    }

type CryptoComIndexMessage = {
  id: -1
  method: 'subscribe'
  code: 0
  result: {
    instrument_name: 'BTCUSD-INDEX'
    subscription: 'index.BTCUSD-INDEX'
    channel: 'index'
    data: [{ v: '31601.35'; t: 1653992545000 }]
  }
}

type CryptoComMarkPriceMessage = {
  id: 1
  method: 'subscribe'
  code: 0
  result: {
    instrument_name: 'BTCUSD-PERP'
    subscription: 'mark.BTCUSD-PERP'
    channel: 'mark'
    data: [{ v: '31606.3'; t: 1653992543000 }]
  }
}

type CryptoComFundingMessage = {
  id: -1
  method: 'subscribe'
  code: 0
  result: {
    instrument_name: 'BTCUSD-PERP'
    subscription: 'funding.BTCUSD-PERP'
    channel: 'funding'
    data: [{ v: '0.00000700'; t: 1653992579000 }]
  }
}

type CryptoComEstFundingMessage = {
  id: 1
  method: 'subscribe'
  code: 0
  result: {
    instrument_name: 'AAVEUSD-PERP'
    subscription: 'estimatedfunding.AAVEUSD-PERP'
    channel: 'estimatedfunding'
    data: [{ v: '0.000039493'; t: 1727308799000 }]
  }
}
