import { asNumberIfValid } from '../handy'
import { BookChange, BookTicker, DerivativeTicker, Liquidation, OptionSummary, Trade } from '../types'
import { Mapper, PendingTickerInfoHelper } from './mapper'

// https://docs.deribit.com/v2/#subscriptions

function deribitCasing(symbols?: string[]) {
  if (symbols !== undefined) {
    return symbols.map((symbol) => {
      if (symbol.endsWith('-C') || symbol.endsWith('-P')) {
        const parts = symbol.split('-')
        if (parts[2] !== undefined && parts[2].toUpperCase().includes('D')) {
          parts[2] = parts[2].replace('D', 'd')
          return parts.join('-')
        } else {
          return symbol.toUpperCase()
        }
      } else {
        return symbol.toUpperCase()
      }
    })
  }

  return
}

export const deribitTradesMapper: Mapper<'deribit', Trade> = {
  canHandle(message: any) {
    const channel = message.params !== undefined ? (message.params.channel as string | undefined) : undefined
    if (channel === undefined) {
      return false
    }

    return channel.startsWith('trades')
  },

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

    return [
      {
        channel: 'trades',
        symbols
      }
    ]
  },

  *map(message: DeribitTradesMessage, localTimestamp: Date): IterableIterator<Trade> {
    for (const deribitTrade of message.params.data) {
      yield {
        type: 'trade',
        symbol: deribitTrade.instrument_name.toUpperCase(),
        exchange: 'deribit',
        id: deribitTrade.trade_id,
        price: deribitTrade.price,
        amount: deribitTrade.amount,
        side: deribitTrade.direction,
        timestamp: new Date(deribitTrade.timestamp),
        localTimestamp: localTimestamp
      }
    }
  }
}

const mapBookLevel = (level: DeribitBookLevel) => {
  const price = level[1]
  const amount = level[0] === 'delete' ? 0 : level[2]

  return { price, amount }
}

export const deribitBookChangeMapper: Mapper<'deribit', BookChange> = {
  canHandle(message: any) {
    const channel = message.params && (message.params.channel as string | undefined)
    if (channel === undefined) {
      return false
    }

    return channel.startsWith('book')
  },

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

    return [
      {
        channel: 'book',
        symbols
      }
    ]
  },

  *map(message: DeribitBookMessage, localTimestamp: Date): IterableIterator<BookChange> {
    const deribitBookChange = message.params.data
    // snapshots do not have prev_change_id set
    const isSnapshot =
      (deribitBookChange.type !== undefined && deribitBookChange.type === 'snapshot') ||
      deribitBookChange.prev_change_id === undefined ||
      deribitBookChange.prev_change_id === 0

    yield {
      type: 'book_change',
      symbol: deribitBookChange.instrument_name.toUpperCase(),
      exchange: 'deribit',
      isSnapshot,
      bids: deribitBookChange.bids.map(mapBookLevel),
      asks: deribitBookChange.asks.map(mapBookLevel),
      timestamp: new Date(deribitBookChange.timestamp),
      localTimestamp: localTimestamp
    }
  }
}

export class DeribitDerivativeTickerMapper implements Mapper<'deribit', DerivativeTicker> {
  private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()

  canHandle(message: any) {
    const channel = message.params && (message.params.channel as string | undefined)
    if (channel === undefined) {
      return false
    }

    return channel.startsWith('ticker') && (message.params.data.greeks === undefined || message.params.data.combo_state === 'active')
  }

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

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

  *map(message: DeribitTickerMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> {
    const deribitTicker = message.params.data
    const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(deribitTicker.instrument_name, 'deribit')

    pendingTickerInfo.updateFundingRate(deribitTicker.current_funding)
    pendingTickerInfo.updateIndexPrice(deribitTicker.index_price)
    pendingTickerInfo.updateMarkPrice(deribitTicker.mark_price)
    pendingTickerInfo.updateOpenInterest(deribitTicker.open_interest)
    pendingTickerInfo.updateLastPrice(deribitTicker.last_price)
    pendingTickerInfo.updateTimestamp(new Date(deribitTicker.timestamp))

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

export class DeribitOptionSummaryMapper implements Mapper<'deribit', OptionSummary> {
  getFilters(symbols?: string[]) {
    symbols = deribitCasing(symbols)

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

  canHandle(message: any) {
    const channel = message.params && message.params.channel
    if (channel === undefined) {
      return false
    }

    return (
      channel.startsWith('ticker') &&
      (message.params.data.instrument_name.endsWith('-P') || message.params.data.instrument_name.endsWith('-C'))
    )
  }

  *map(message: DeribitOptionTickerMessage, localTimestamp: Date) {
    //MATIC_USDC-9MAR24-1d02-C
    const optionInfo = message.params.data

    //e.g., BTC-8JUN20-8750-P
    const symbolParts = optionInfo.instrument_name.split('-')

    const isPut = symbolParts[3] === 'P'

    let strikePriceString = symbolParts[2]
    if (strikePriceString.includes('d')) {
      strikePriceString = strikePriceString.replace('d', '.')
    }
    const strikePrice = Number(strikePriceString)
    const expirationDate = new Date(symbolParts[1] + 'Z')
    expirationDate.setUTCHours(8)

    const optionSummary: OptionSummary = {
      type: 'option_summary',
      symbol: optionInfo.instrument_name.toUpperCase(),
      exchange: 'deribit',
      optionType: isPut ? 'put' : 'call',
      strikePrice,
      expirationDate,

      bestBidPrice: asNumberIfValid(optionInfo.best_bid_price),
      bestBidAmount: asNumberIfValid(optionInfo.best_bid_amount),
      bestBidIV: asNumberIfValid(optionInfo.bid_iv),

      bestAskPrice: asNumberIfValid(optionInfo.best_ask_price),
      bestAskAmount: asNumberIfValid(optionInfo.best_ask_amount),
      bestAskIV: asNumberIfValid(optionInfo.ask_iv),

      lastPrice: asNumberIfValid(optionInfo.last_price),
      openInterest: optionInfo.open_interest,

      markPrice: optionInfo.mark_price,
      markIV: optionInfo.mark_iv,

      delta: optionInfo.greeks.delta,
      gamma: optionInfo.greeks.gamma,
      vega: optionInfo.greeks.vega,
      theta: optionInfo.greeks.theta,
      rho: optionInfo.greeks.rho,

      underlyingPrice: optionInfo.underlying_price,
      underlyingIndex: optionInfo.underlying_index,

      timestamp: new Date(optionInfo.timestamp),
      localTimestamp: localTimestamp
    }

    yield optionSummary
  }
}

export const deribitLiquidationsMapper: Mapper<'deribit', Liquidation> = {
  canHandle(message: any) {
    const channel = message.params !== undefined ? (message.params.channel as string | undefined) : undefined
    if (channel === undefined) {
      return false
    }

    return channel.startsWith('trades')
  },

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

    return [
      {
        channel: 'trades',
        symbols
      }
    ]
  },

  *map(message: DeribitTradesMessage, localTimestamp: Date): IterableIterator<Liquidation> {
    for (const deribitTrade of message.params.data) {
      if (deribitTrade.liquidation !== undefined) {
        let side
        // "T" when liquidity taker side was under liquidation
        if (deribitTrade.liquidation === 'T') {
          side = deribitTrade.direction
        } else {
          // "M" when maker (passive) side of trade was under liquidation
          side = deribitTrade.direction === 'buy' ? ('sell' as const) : ('buy' as const)
        }
        yield {
          type: 'liquidation',
          symbol: deribitTrade.instrument_name.toUpperCase(),
          exchange: 'deribit',
          id: deribitTrade.trade_id,
          price: deribitTrade.price,
          amount: deribitTrade.amount,
          side,
          timestamp: new Date(deribitTrade.timestamp),
          localTimestamp: localTimestamp
        }
      }
    }
  }
}

export const deribitBookTickerMapper: Mapper<'deribit', BookTicker> = {
  canHandle(message: any) {
    const channel = message.params !== undefined ? (message.params.channel as string | undefined) : undefined
    if (channel === undefined) {
      return false
    }

    return channel.startsWith('ticker')
  },

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

    return [
      {
        channel: 'ticker',
        symbols
      }
    ]
  },

  *map(message: DeribitTickerMessage, localTimestamp: Date): IterableIterator<BookTicker> {
    const deribitTicker = message.params.data

    const ticker: BookTicker = {
      type: 'book_ticker',
      symbol: deribitTicker.instrument_name.toUpperCase(),
      exchange: 'deribit',

      askAmount: asNumberIfValid(deribitTicker.best_ask_amount),
      askPrice: asNumberIfValid(deribitTicker.best_ask_price),
      bidPrice: asNumberIfValid(deribitTicker.best_bid_price),
      bidAmount: asNumberIfValid(deribitTicker.best_bid_amount),

      timestamp: new Date(deribitTicker.timestamp),
      localTimestamp: localTimestamp
    }

    yield ticker
  }
}

type DeribitMessage = {
  params: {
    channel: string
  }
}

type DeribitTradesMessage = DeribitMessage & {
  params: {
    data: {
      trade_id: string
      instrument_name: string
      timestamp: number
      direction: 'buy' | 'sell'
      price: number
      amount: number
      trade_seq: number
      liquidation?: 'M' | 'T' | 'MT'
    }[]
  }
}

type DeribitBookLevel = ['new' | 'change' | 'delete', number, number]

type DeribitBookMessage = DeribitMessage & {
  params: {
    data: {
      timestamp: number
      instrument_name: string
      prev_change_id?: number
      bids: DeribitBookLevel[]
      asks: DeribitBookLevel[]
      type?: 'snapshot' | 'change'
    }
  }
}

type DeribitTickerMessage = DeribitMessage & {
  params: {
    data: {
      timestamp: number
      open_interest: number
      last_price: number | undefined
      mark_price: number
      instrument_name: string
      index_price: number
      current_funding?: number
      funding_8h?: number
      best_bid_price: number | undefined
      best_bid_amount: number | undefined
      best_ask_price: number | undefined
      best_ask_amount: number | undefined
    }
  }
}

type DeribitOptionTickerMessage = DeribitTickerMessage & {
  params: {
    data: {
      underlying_price: number
      underlying_index: string
      timestamp: number
      open_interest: number
      mark_price: number
      mark_iv: number
      last_price: number | null
      greeks: { vega: number; theta: number; rho: number; gamma: number; delta: number }
      bid_iv: number | undefined
      ask_iv: number | undefined
    }
  }
}
