import got from 'got'
import { Exchange } from '../types'

import { ExchangeDetailsBase } from './exchangedetails'

type AvailableSymbol = ExchangeDetailsBase<any>['availableSymbols'][number]

const marketInfoAPIs: { [key in Exchange]?: string } = {
  binance: 'https://api.binance.com/api/v3/exchangeInfo',
  'binance-futures': 'https://fapi.binance.com/fapi/v1/exchangeInfo',
  'binance-delivery': 'https://dapi.binance.com/dapi/v1/exchangeInfo',
  okex: 'https://www.okex.com/api/spot/v3/instruments',
  'okex-swap': 'https://www.okex.com/api/swap/v3/instruments',
  'okex-futures': 'https://www.okex.com/api/futures/v3/instruments',
  huobi: 'https://api.huobi.pro/v1/common/symbols',
  'huobi-dm-swap': 'https://api.hbdm.com/swap-api/v1/swap_contract_info',
  'huobi-dm': 'https://api.hbdm.com/api/v1/contract_contract_info'
}

async function getRawMarketInfo<T extends Exchange>(exchange: T) {
  const url = marketInfoAPIs[exchange]
  if (!url) return
  const info: any = await got.get(url!).json()
  return { exchange, info } as MarketInfoResponse<T>
}

export async function getMarketInfo(exchange: Exchange) {
  const resp = await getRawMarketInfo(exchange)
  if (!resp) return

  const database = new MarketInfoDatabase()
  switch (resp.exchange) {
    case 'binance': {
      const symbols = resp.info.symbols
      symbols.forEach((symbol) => {
        const id = symbol.symbol
        database.add({
          id,
          type: 'spot',
          base: symbol.baseAsset,
          quote: symbol.quoteAsset,
          precision: getBinancePrecisionFromFilters(symbol.filters)
        })
      })

      break
    }
    case 'binance-futures': // all perpetual
    case 'binance-delivery': {
      // some perpetual, some delivery
      const symbols: MergeArrayElement<typeof resp.info.symbols> = resp.info.symbols
      symbols.forEach((symbol) => {
        const id = symbol.symbol
        let multiplier = 1
        let contractType: 'perpetual' | 'future' = 'perpetual'
        if ('contractSize' in symbol) {
          multiplier = symbol.contractSize
          contractType = symbol.contractType === 'PERPETUAL' ? 'perpetual' : 'future'
        }

        let contract: ContractInfo
        if (exchange === 'binance-delivery') {
          contract = {
            isInverse: true,
            unit: symbol.quoteAsset,
            multiplier
          }
        } else {
          contract = {
            isInverse: false,
            unit: symbol.baseAsset,
            multiplier
          }
        }

        database.add({
          id,
          type: contractType,
          base: symbol.baseAsset,
          quote: symbol.quoteAsset,
          precision: {
            price: symbol.pricePrecision,
            amount: symbol.quantityPrecision
          },
          contract
        })
      })

      break
    }

    case 'okex': {
      const symbols = resp.info
      symbols.forEach((symbol) => {
        database.add({
          id: symbol.instrument_id,
          type: 'spot',
          base: symbol.base_currency,
          quote: symbol.quote_currency,
          precision: {
            price: getDecimalPlaces(symbol.tick_size),
            amount: getDecimalPlaces(symbol.size_increment)
          }
        })
      })

      break
    }

    case 'okex-swap':
    case 'okex-futures': {
      const contractType = resp.exchange === 'okex-swap' ? 'perpetual' : 'future'
      const symbols: MergeArrayElement<typeof resp.info> = resp.info
      symbols.forEach((symbol) => {
        const id = symbol.instrument_id
        const contract: ContractInfo = {
          isInverse: symbol.is_inverse === 'true',
          multiplier: Number(symbol.contract_val),
          unit: symbol.contract_val_currency
        }

        database.add({
          id,
          type: contractType,
          base: symbol.base_currency,
          quote: symbol.quote_currency,
          precision: {
            price: getDecimalPlaces(symbol.tick_size),
            amount: 0 // okex contract is traded in "cont", which is integer
          },
          contract
        })
      })

      break
    }

    case 'huobi': {
      const symbols = resp.info.data
      symbols.forEach((symbol) => {
        database.add({
          id: symbol.symbol,
          type: 'spot',
          base: symbol['base-currency'],
          quote: symbol['quote-currency'],
          precision: {
            price: symbol['price-precision'],
            amount: symbol['amount-precision']
          }
        })
      })

      break
    }

    case 'huobi-dm-swap':
    case 'huobi-dm': {
      const contractType = resp.exchange === 'huobi-dm' ? 'future' : 'perpetual'
      const symbols: MergeArrayElement<typeof resp.info.data> = resp.info.data
      symbols.forEach((symbol) => {
        const id = getHuobiSymbolKey(symbol)
        database.add({
          id,
          type: contractType,
          base: symbol.symbol,
          quote: 'USD',
          precision: {
            price: getDecimalPlaces(symbol.price_tick),
            amount: 0
          },
          contract: {
            isInverse: true,
            multiplier: symbol.contract_size,
            unit: 'USD'
          }
        })
      })

      break
    }
  }

  return database
}

class MarketInfoDatabase {
  db: Map<string, MarketInfoRecord> = new Map()
  assetInfo: Map<string, { base: string; quote: string }> = new Map()

  count = 0

  add(record: MarketInfoRecord) {
    this.count++
    this.assetInfo.set(record.id.toLowerCase(), { base: record.base, quote: record.quote })
    const key = `${record.base}_${record.quote}_${record.type}`.toLowerCase()
    if (this.db.has(key)) {
      const prevRecord = this.db.get(key)!
      const isEqual = compareMarketInfoRecord(prevRecord, record)
      if (!isEqual) {
        throw Error('wierd case')
      }
    } else {
      this.db.set(key, record)
    }
  }

  // @ts-ignore
  mixin(exchange: Exchange, symbol: AvailableSymbol) {
    const assetInfo = this.assetInfo.get(symbol.id.toLowerCase())!
    if (!assetInfo) {
      console.warn(`cannot find asset info for ${exchange}:${symbol.id}`)
      return symbol
    }

    const key = `${assetInfo.base}_${assetInfo.quote}_${symbol.type}`.toLowerCase()
    const record = this.db.get(key)
    if (record) {
      symbol.market = {
        base: record.base,
        quote: record.quote,
        precision: record.precision,
        contract: record.contract
      }
    } else {
      console.warn(`cannot find market info for ${exchange}:${symbol.id}`)
    }
    return symbol
  }
}

function getBinancePrecisionFromFilters(filters: BinanceSpot.Symbol['filters']) {
  const priceFilter = filters.find((filter) => filter.filterType === 'PRICE_FILTER')!
  const lotSize = filters.find((filter) => filter.filterType === 'LOT_SIZE')!

  const price = getDecimalPlaces(priceFilter.tickSize)
  const amount = getDecimalPlaces(lotSize.stepSize)
  return { price, amount }
}

function compareMarketInfoRecord(a: MarketInfoRecord, b: MarketInfoRecord) {
  const symbolInfoIsEqual =
    a.base === b.base &&
    a.quote === b.quote &&
    a.type === b.type &&
    a.precision.price === b.precision.price &&
    a.precision.amount === b.precision.amount
  if (a.contract && b.contract) {
    const contractInfoIsEqual =
      a.contract.isInverse === b.contract.isInverse &&
      a.contract.multiplier === b.contract.multiplier &&
      a.contract.unit === b.contract.unit
    return contractInfoIsEqual && symbolInfoIsEqual
  } else {
    // this is impossible
    throw Error('missing contract field')
  }
}

type RawMarketInfoMap = {
  okex: OkexSpot.MarketInfo
  'okex-swap': OkexSwap.MarketInfo
  'okex-futures': OkexFutures.MarketInfo
  huobi: HuobiSpot.MarketInfo
  'huobi-dm': HuobiDm.MarketInfo
  'huobi-dm-swap': HuobiDmSwap.MarketInfo
  binance: BinanceSpot.MarketInfo
  'binance-futures': BinanceFutures.MarketInfo
  'binance-delivery': BinanceDelivery.MarketInfo
}

type MarketInfoResponse<T extends Exchange> = T extends keyof RawMarketInfoMap ? { exchange: T; info: RawMarketInfoMap[T] } : undefined

type ContractInfo = {
  isInverse: boolean
  multiplier: number
  unit: string
}

export interface MarketInfo {
  base: string
  quote: string
  precision: {
    price: number
    amount: number
  }
  contract?: ContractInfo
}

interface MarketInfoRecord extends MarketInfo {
  id: string
  type: 'spot' | 'perpetual' | 'future'
}

type MergeArrayElement<T> = Array<T extends Array<any> ? T[number] : never>

function getDecimalPlaces(x: number | string) {
  if (typeof x === 'string') x = Number(x)
  const str = String(x)
  if (str.includes('e')) {
    if (str.includes('e-')) {
      return Number(str.split('e-')[1])
    } else {
      return 0
    }
  } else {
    const decimals = str.split('.')[1]
    return decimals ? decimals.length : 0
  }
}

function getHuobiSymbolKey(symbol: HuobiDm.Symbol | HuobiDmSwap.Symbol) {
  if ('contract_type' in symbol) {
    switch (symbol.contract_type) {
      case 'this_week':
        return `${symbol.symbol}_CW`
      case 'next_week':
        return `${symbol.symbol}_NW`
      case 'quarter':
        return `${symbol.symbol}_CQ`
      case 'next_quarter':
        return `${symbol.symbol}_NQ`
    }
  } else {
    return symbol.contract_code
  }
}

/*
function getRecordKeyForSymbol(exchange: Exchange, symbol: AvailableSymbol) {
  let symbolId = symbol.id
  const type = symbol.type

  switch (exchange) {
    case 'binance-futures': {
      // "btcusdt"
      return `${symbolId.slice(0, -4)}_usdt_${type}`.toLowerCase()
    }
    case 'binance-delivery': {
      // "btcusd_perp" or "btcusd_210326"
      symbolId = symbolId.slice(0, symbolId.indexOf('_'))
      return `${symbolId.slice(0, -3)}_usd_${type}`.toLowerCase()
    }
    case 'okex-swap':
    case 'okex-futures': {
      const parts = symbolId.split('-')
      return `${parts[0]}_${parts[1]}_${type}`.toLowerCase()
    }
    case 'huobi-dm': {
      // "BTC_CW"
      return `${symbolId.slice(0, -3)}_usd_${type}`.toLowerCase()
    }
    case 'huobi-dm-swap': {
      // "BTC-USD"
      return `${symbolId.replace('-', '_')}_${type}`.toLowerCase()
    }
  }

  return
}
*/
