package okgroup

import (
	"encoding/json"
	"errors"
	"fmt"
	"hash/crc32"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gorilla/websocket"
	"github.com/thrasher-corp/gocryptotrader/common/crypto"
	"github.com/thrasher-corp/gocryptotrader/currency"
	exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
	"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
	"github.com/thrasher-corp/gocryptotrader/exchanges/order"
	"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
	"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
	"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
	"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
	"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
	"github.com/thrasher-corp/gocryptotrader/log"
)

// List of all websocket channels to subscribe to
const (
	// Orderbook events
	okGroupWsOrderbookUpdate  = "update"
	okGroupWsOrderbookPartial = "partial"
	// API subsections
	okGroupWsSwapSubsection    = "swap/"
	okGroupWsIndexSubsection   = "index/"
	okGroupWsFuturesSubsection = "futures/"
	okGroupWsSpotSubsection    = "spot/"
	// Shared API endpoints
	okGroupWsCandle         = "candle"
	okGroupWsCandle60s      = okGroupWsCandle + "60s"
	okGroupWsCandle180s     = okGroupWsCandle + "180s"
	okGroupWsCandle300s     = okGroupWsCandle + "300s"
	okGroupWsCandle900s     = okGroupWsCandle + "900s"
	okGroupWsCandle1800s    = okGroupWsCandle + "1800s"
	okGroupWsCandle3600s    = okGroupWsCandle + "3600s"
	okGroupWsCandle7200s    = okGroupWsCandle + "7200s"
	okGroupWsCandle14400s   = okGroupWsCandle + "14400s"
	okGroupWsCandle21600s   = okGroupWsCandle + "21600"
	okGroupWsCandle43200s   = okGroupWsCandle + "43200s"
	okGroupWsCandle86400s   = okGroupWsCandle + "86400s"
	okGroupWsCandle604900s  = okGroupWsCandle + "604800s"
	okGroupWsTicker         = "ticker"
	okGroupWsTrade          = "trade"
	okGroupWsDepth          = "depth"
	okGroupWsDepth5         = "depth5"
	okGroupWsAccount        = "account"
	okGroupWsMarginAccount  = "margin_account"
	okGroupWsOrder          = "order"
	okGroupWsFundingRate    = "funding_rate"
	okGroupWsPriceRange     = "price_range"
	okGroupWsMarkPrice      = "mark_price"
	okGroupWsPosition       = "position"
	okGroupWsEstimatedPrice = "estimated_price"
	// Spot endpoints
	okGroupWsSpotTicker        = okGroupWsSpotSubsection + okGroupWsTicker
	okGroupWsSpotCandle60s     = okGroupWsSpotSubsection + okGroupWsCandle60s
	okGroupWsSpotCandle180s    = okGroupWsSpotSubsection + okGroupWsCandle180s
	okGroupWsSpotCandle300s    = okGroupWsSpotSubsection + okGroupWsCandle300s
	okGroupWsSpotCandle900s    = okGroupWsSpotSubsection + okGroupWsCandle900s
	okGroupWsSpotCandle1800s   = okGroupWsSpotSubsection + okGroupWsCandle1800s
	okGroupWsSpotCandle3600s   = okGroupWsSpotSubsection + okGroupWsCandle3600s
	okGroupWsSpotCandle7200s   = okGroupWsSpotSubsection + okGroupWsCandle7200s
	okGroupWsSpotCandle14400s  = okGroupWsSpotSubsection + okGroupWsCandle14400s
	okGroupWsSpotCandle21600s  = okGroupWsSpotSubsection + okGroupWsCandle21600s
	okGroupWsSpotCandle43200s  = okGroupWsSpotSubsection + okGroupWsCandle43200s
	okGroupWsSpotCandle86400s  = okGroupWsSpotSubsection + okGroupWsCandle86400s
	okGroupWsSpotCandle604900s = okGroupWsSpotSubsection + okGroupWsCandle604900s
	okGroupWsSpotTrade         = okGroupWsSpotSubsection + okGroupWsTrade
	okGroupWsSpotDepth         = okGroupWsSpotSubsection + okGroupWsDepth
	okGroupWsSpotDepth5        = okGroupWsSpotSubsection + okGroupWsDepth5
	okGroupWsSpotAccount       = okGroupWsSpotSubsection + okGroupWsAccount
	okGroupWsSpotMarginAccount = okGroupWsSpotSubsection + okGroupWsMarginAccount
	okGroupWsSpotOrder         = okGroupWsSpotSubsection + okGroupWsOrder
	// Swap endpoints
	okGroupWsSwapTicker        = okGroupWsSwapSubsection + okGroupWsTicker
	okGroupWsSwapCandle60s     = okGroupWsSwapSubsection + okGroupWsCandle60s
	okGroupWsSwapCandle180s    = okGroupWsSwapSubsection + okGroupWsCandle180s
	okGroupWsSwapCandle300s    = okGroupWsSwapSubsection + okGroupWsCandle300s
	okGroupWsSwapCandle900s    = okGroupWsSwapSubsection + okGroupWsCandle900s
	okGroupWsSwapCandle1800s   = okGroupWsSwapSubsection + okGroupWsCandle1800s
	okGroupWsSwapCandle3600s   = okGroupWsSwapSubsection + okGroupWsCandle3600s
	okGroupWsSwapCandle7200s   = okGroupWsSwapSubsection + okGroupWsCandle7200s
	okGroupWsSwapCandle14400s  = okGroupWsSwapSubsection + okGroupWsCandle14400s
	okGroupWsSwapCandle21600s  = okGroupWsSwapSubsection + okGroupWsCandle21600s
	okGroupWsSwapCandle43200s  = okGroupWsSwapSubsection + okGroupWsCandle43200s
	okGroupWsSwapCandle86400s  = okGroupWsSwapSubsection + okGroupWsCandle86400s
	okGroupWsSwapCandle604900s = okGroupWsSwapSubsection + okGroupWsCandle604900s
	okGroupWsSwapTrade         = okGroupWsSwapSubsection + okGroupWsTrade
	okGroupWsSwapDepth         = okGroupWsSwapSubsection + okGroupWsDepth
	okGroupWsSwapDepth5        = okGroupWsSwapSubsection + okGroupWsDepth5
	okGroupWsSwapFundingRate   = okGroupWsSwapSubsection + okGroupWsFundingRate
	okGroupWsSwapPriceRange    = okGroupWsSwapSubsection + okGroupWsPriceRange
	okGroupWsSwapMarkPrice     = okGroupWsSwapSubsection + okGroupWsMarkPrice
	okGroupWsSwapPosition      = okGroupWsSwapSubsection + okGroupWsPosition
	okGroupWsSwapAccount       = okGroupWsSwapSubsection + okGroupWsAccount
	okGroupWsSwapOrder         = okGroupWsSwapSubsection + okGroupWsOrder
	// Index endpoints
	okGroupWsIndexTicker        = okGroupWsIndexSubsection + okGroupWsTicker
	okGroupWsIndexCandle60s     = okGroupWsIndexSubsection + okGroupWsCandle60s
	okGroupWsIndexCandle180s    = okGroupWsIndexSubsection + okGroupWsCandle180s
	okGroupWsIndexCandle300s    = okGroupWsIndexSubsection + okGroupWsCandle300s
	okGroupWsIndexCandle900s    = okGroupWsIndexSubsection + okGroupWsCandle900s
	okGroupWsIndexCandle1800s   = okGroupWsIndexSubsection + okGroupWsCandle1800s
	okGroupWsIndexCandle3600s   = okGroupWsIndexSubsection + okGroupWsCandle3600s
	okGroupWsIndexCandle7200s   = okGroupWsIndexSubsection + okGroupWsCandle7200s
	okGroupWsIndexCandle14400s  = okGroupWsIndexSubsection + okGroupWsCandle14400s
	okGroupWsIndexCandle21600s  = okGroupWsIndexSubsection + okGroupWsCandle21600s
	okGroupWsIndexCandle43200s  = okGroupWsIndexSubsection + okGroupWsCandle43200s
	okGroupWsIndexCandle86400s  = okGroupWsIndexSubsection + okGroupWsCandle86400s
	okGroupWsIndexCandle604900s = okGroupWsIndexSubsection + okGroupWsCandle604900s
	// Futures endpoints
	okGroupWsFuturesTicker         = okGroupWsFuturesSubsection + okGroupWsTicker
	okGroupWsFuturesCandle60s      = okGroupWsFuturesSubsection + okGroupWsCandle60s
	okGroupWsFuturesCandle180s     = okGroupWsFuturesSubsection + okGroupWsCandle180s
	okGroupWsFuturesCandle300s     = okGroupWsFuturesSubsection + okGroupWsCandle300s
	okGroupWsFuturesCandle900s     = okGroupWsFuturesSubsection + okGroupWsCandle900s
	okGroupWsFuturesCandle1800s    = okGroupWsFuturesSubsection + okGroupWsCandle1800s
	okGroupWsFuturesCandle3600s    = okGroupWsFuturesSubsection + okGroupWsCandle3600s
	okGroupWsFuturesCandle7200s    = okGroupWsFuturesSubsection + okGroupWsCandle7200s
	okGroupWsFuturesCandle14400s   = okGroupWsFuturesSubsection + okGroupWsCandle14400s
	okGroupWsFuturesCandle21600s   = okGroupWsFuturesSubsection + okGroupWsCandle21600s
	okGroupWsFuturesCandle43200s   = okGroupWsFuturesSubsection + okGroupWsCandle43200s
	okGroupWsFuturesCandle86400s   = okGroupWsFuturesSubsection + okGroupWsCandle86400s
	okGroupWsFuturesCandle604900s  = okGroupWsFuturesSubsection + okGroupWsCandle604900s
	okGroupWsFuturesTrade          = okGroupWsFuturesSubsection + okGroupWsTrade
	okGroupWsFuturesEstimatedPrice = okGroupWsFuturesSubsection + okGroupWsTrade
	okGroupWsFuturesPriceRange     = okGroupWsFuturesSubsection + okGroupWsPriceRange
	okGroupWsFuturesDepth          = okGroupWsFuturesSubsection + okGroupWsDepth
	okGroupWsFuturesDepth5         = okGroupWsFuturesSubsection + okGroupWsDepth5
	okGroupWsFuturesMarkPrice      = okGroupWsFuturesSubsection + okGroupWsMarkPrice
	okGroupWsFuturesAccount        = okGroupWsFuturesSubsection + okGroupWsAccount
	okGroupWsFuturesPosition       = okGroupWsFuturesSubsection + okGroupWsPosition
	okGroupWsFuturesOrder          = okGroupWsFuturesSubsection + okGroupWsOrder

	okGroupWsRateLimit = 30

	allowableIterations = 25
	delimiterColon      = ":"
	delimiterDash       = "-"

	maxConnByteLen = 4096
)

// orderbookMutex Ensures if two entries arrive at once, only one can be
// processed at a time
var orderbookMutex sync.Mutex

var defaultSpotSubscribedChannels = []string{okGroupWsSpotDepth,
	okGroupWsSpotCandle300s,
	okGroupWsSpotTicker,
	okGroupWsSpotTrade}

var defaultFuturesSubscribedChannels = []string{okGroupWsFuturesDepth,
	okGroupWsFuturesCandle300s,
	okGroupWsFuturesTicker,
	okGroupWsFuturesTrade}

var defaultIndexSubscribedChannels = []string{okGroupWsIndexCandle300s,
	okGroupWsIndexTicker}

var defaultSwapSubscribedChannels = []string{okGroupWsSwapDepth,
	okGroupWsSwapCandle300s,
	okGroupWsSwapTicker,
	okGroupWsSwapTrade,
	okGroupWsSwapFundingRate,
	okGroupWsSwapMarkPrice}

// WsConnect initiates a websocket connection
func (o *OKGroup) WsConnect() error {
	if !o.Websocket.IsEnabled() || !o.IsEnabled() {
		return errors.New(stream.WebsocketNotEnabled)
	}
	var dialer websocket.Dialer
	dialer.ReadBufferSize = 8192
	dialer.WriteBufferSize = 8192
	err := o.Websocket.Conn.Dial(&dialer, http.Header{})
	if err != nil {
		return err
	}
	if o.Verbose {
		log.Debugf(log.ExchangeSys, "Successful connection to %v\n",
			o.Websocket.GetWebsocketURL())
	}

	go o.WsReadData()
	if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		err = o.WsLogin()
		if err != nil {
			log.Errorf(log.ExchangeSys,
				"%v - authentication failed: %v\n",
				o.Name,
				err)
		}
	}

	return nil
}

// WsLogin sends a login request to websocket to enable access to authenticated endpoints
func (o *OKGroup) WsLogin() error {
	o.Websocket.SetCanUseAuthenticatedEndpoints(true)
	unixTime := time.Now().UTC().Unix()
	signPath := "/users/self/verify"
	hmac := crypto.GetHMAC(crypto.HashSHA256,
		[]byte(strconv.FormatInt(unixTime, 10)+http.MethodGet+signPath),
		[]byte(o.API.Credentials.Secret),
	)
	base64 := crypto.Base64Encode(hmac)
	request := WebsocketEventRequest{
		Operation: "login",
		Arguments: []string{
			o.API.Credentials.Key,
			o.API.Credentials.ClientID,
			strconv.FormatInt(unixTime, 10),
			base64,
		},
	}
	_, err := o.Websocket.Conn.SendMessageReturnResponse("login", request)
	if err != nil {
		o.Websocket.SetCanUseAuthenticatedEndpoints(false)
		return err
	}
	return nil
}

// WsReadData receives and passes on websocket messages for processing
func (o *OKGroup) WsReadData() {
	o.Websocket.Wg.Add(1)
	defer o.Websocket.Wg.Done()

	for {
		resp := o.Websocket.Conn.ReadMessage()
		if resp.Raw == nil {
			return
		}
		err := o.WsHandleData(resp.Raw)
		if err != nil {
			o.Websocket.DataHandler <- err
		}
	}
}

// WsHandleData will read websocket raw data and pass to appropriate handler
func (o *OKGroup) WsHandleData(respRaw []byte) error {
	var dataResponse WebsocketDataResponse
	err := json.Unmarshal(respRaw, &dataResponse)
	if err != nil {
		return err
	}
	if len(dataResponse.Data) > 0 {
		switch o.GetWsChannelWithoutOrderType(dataResponse.Table) {
		case okGroupWsCandle60s, okGroupWsCandle180s, okGroupWsCandle300s,
			okGroupWsCandle900s, okGroupWsCandle1800s, okGroupWsCandle3600s,
			okGroupWsCandle7200s, okGroupWsCandle14400s, okGroupWsCandle21600s,
			okGroupWsCandle43200s, okGroupWsCandle86400s, okGroupWsCandle604900s:
			return o.wsProcessCandles(respRaw)
		case okGroupWsDepth, okGroupWsDepth5:
			return o.WsProcessOrderBook(respRaw)
		case okGroupWsTicker:
			return o.wsProcessTickers(respRaw)
		case okGroupWsTrade:
			return o.wsProcessTrades(respRaw)
		case okGroupWsOrder:
			return o.wsProcessOrder(respRaw)
		}
		o.Websocket.DataHandler <- stream.UnhandledMessageWarning{
			Message: o.Name + stream.UnhandledMessage + string(respRaw),
		}
		return nil
	}

	var errorResponse WebsocketErrorResponse
	err = json.Unmarshal(respRaw, &errorResponse)
	if err == nil && errorResponse.ErrorCode > 0 {
		return fmt.Errorf("%v error - %v message: %s ",
			o.Name,
			errorResponse.ErrorCode,
			errorResponse.Message)
	}
	var eventResponse WebsocketEventResponse
	err = json.Unmarshal(respRaw, &eventResponse)
	if err == nil && eventResponse.Event != "" {
		if eventResponse.Event == "login" {
			if o.Websocket.Match.Incoming("login") {
				o.Websocket.SetCanUseAuthenticatedEndpoints(eventResponse.Success)
			}
		}
		if o.Verbose {
			log.Debug(log.ExchangeSys,
				o.Name+" - "+eventResponse.Event+" on channel: "+eventResponse.Channel)
		}
	}
	return nil
}

// StringToOrderStatus converts order status IDs to internal types
func StringToOrderStatus(num int64) (order.Status, error) {
	switch num {
	case -2:
		return order.Rejected, nil
	case -1:
		return order.Cancelled, nil
	case 0:
		return order.Active, nil
	case 1:
		return order.PartiallyFilled, nil
	case 2:
		return order.Filled, nil
	case 3:
		return order.New, nil
	case 4:
		return order.PendingCancel, nil
	default:
		return order.UnknownStatus, fmt.Errorf("%v not recognised as order status", num)
	}
}

func (o *OKGroup) wsProcessOrder(respRaw []byte) error {
	var resp WebsocketSpotOrderResponse
	err := json.Unmarshal(respRaw, &resp)
	if err != nil {
		return err
	}
	for i := range resp.Data {
		var oType order.Type
		var oSide order.Side
		var oStatus order.Status
		oType, err = order.StringToOrderType(resp.Data[i].Type)
		if err != nil {
			o.Websocket.DataHandler <- order.ClassificationError{
				Exchange: o.Name,
				OrderID:  resp.Data[i].OrderID,
				Err:      err,
			}
		}
		oSide, err = order.StringToOrderSide(resp.Data[i].Side)
		if err != nil {
			o.Websocket.DataHandler <- order.ClassificationError{
				Exchange: o.Name,
				OrderID:  resp.Data[i].OrderID,
				Err:      err,
			}
		}
		oStatus, err = StringToOrderStatus(resp.Data[i].State)
		if err != nil {
			o.Websocket.DataHandler <- order.ClassificationError{
				Exchange: o.Name,
				OrderID:  resp.Data[i].OrderID,
				Err:      err,
			}
		}

		pair, err := currency.NewPairFromString(resp.Data[i].InstrumentID)
		if err != nil {
			o.Websocket.DataHandler <- order.ClassificationError{
				Exchange: o.Name,
				OrderID:  resp.Data[i].OrderID,
				Err:      err,
			}
		}

		o.Websocket.DataHandler <- &order.Detail{
			ImmediateOrCancel: resp.Data[i].OrderType == 3,
			FillOrKill:        resp.Data[i].OrderType == 2,
			PostOnly:          resp.Data[i].OrderType == 1,
			Price:             resp.Data[i].Price,
			Amount:            resp.Data[i].Size,
			ExecutedAmount:    resp.Data[i].LastFillQty,
			RemainingAmount:   resp.Data[i].Size - resp.Data[i].LastFillQty,
			Exchange:          o.Name,
			ID:                resp.Data[i].OrderID,
			Type:              oType,
			Side:              oSide,
			Status:            oStatus,
			AssetType:         o.GetAssetTypeFromTableName(resp.Table),
			Date:              resp.Data[i].CreatedAt,
			Pair:              pair,
		}
	}
	return nil
}

// wsProcessTickers converts ticker data and sends it to the datahandler
func (o *OKGroup) wsProcessTickers(respRaw []byte) error {
	var response WebsocketTickerData
	err := json.Unmarshal(respRaw, &response)
	if err != nil {
		return err
	}
	a := o.GetAssetTypeFromTableName(response.Table)
	for i := range response.Data {
		f := strings.Split(response.Data[i].InstrumentID, delimiterDash)

		var c currency.Pair
		switch a {
		case asset.Futures, asset.PerpetualSwap:
			c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1],
				f[2],
				currency.UnderscoreDelimiter)
		default:
			c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
		}

		baseVolume := response.Data[i].BaseVolume24h
		if response.Data[i].ContractVolume24h != 0 {
			baseVolume = response.Data[i].ContractVolume24h
		}

		quoteVolume := response.Data[i].QuoteVolume24h
		if response.Data[i].TokenVolume24h != 0 {
			quoteVolume = response.Data[i].TokenVolume24h
		}

		o.Websocket.DataHandler <- &ticker.Price{
			ExchangeName: o.Name,
			Open:         response.Data[i].Open24h,
			Close:        response.Data[i].Last,
			Volume:       baseVolume,
			QuoteVolume:  quoteVolume,
			High:         response.Data[i].High24h,
			Low:          response.Data[i].Low24h,
			Bid:          response.Data[i].BestBid,
			Ask:          response.Data[i].BestAsk,
			Last:         response.Data[i].Last,
			AssetType:    o.GetAssetTypeFromTableName(response.Table),
			Pair:         c,
			LastUpdated:  response.Data[i].Timestamp,
		}
	}
	return nil
}

// wsProcessTrades converts trade data and sends it to the datahandler
func (o *OKGroup) wsProcessTrades(respRaw []byte) error {
	if !o.IsSaveTradeDataEnabled() {
		return nil
	}
	var response WebsocketTradeResponse
	err := json.Unmarshal(respRaw, &response)
	if err != nil {
		return err
	}

	a := o.GetAssetTypeFromTableName(response.Table)
	var trades []trade.Data
	for i := range response.Data {
		f := strings.Split(response.Data[i].InstrumentID, delimiterDash)

		var c currency.Pair
		switch a {
		case asset.Futures, asset.PerpetualSwap:
			c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1],
				f[2],
				currency.UnderscoreDelimiter)
		default:
			c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
		}

		tSide, err := order.StringToOrderSide(response.Data[i].Side)
		if err != nil {
			o.Websocket.DataHandler <- order.ClassificationError{
				Exchange: o.Name,
				Err:      err,
			}
		}

		amount := response.Data[i].Size
		if response.Data[i].Quantity != 0 {
			amount = response.Data[i].Quantity
		}
		trades = append(trades, trade.Data{
			Amount:       amount,
			AssetType:    o.GetAssetTypeFromTableName(response.Table),
			CurrencyPair: c,
			Exchange:     o.Name,
			Price:        response.Data[i].Price,
			Side:         tSide,
			Timestamp:    response.Data[i].Timestamp,
			TID:          response.Data[i].TradeID,
		})
	}
	return trade.AddTradesToBuffer(o.Name, trades...)
}

// wsProcessCandles converts candle data and sends it to the data handler
func (o *OKGroup) wsProcessCandles(respRaw []byte) error {
	var response WebsocketCandleResponse
	err := json.Unmarshal(respRaw, &response)
	if err != nil {
		return err
	}

	a := o.GetAssetTypeFromTableName(response.Table)
	for i := range response.Data {
		f := strings.Split(response.Data[i].InstrumentID, delimiterDash)

		var c currency.Pair
		switch a {
		case asset.Futures, asset.PerpetualSwap:
			c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1],
				f[2],
				currency.UnderscoreDelimiter)
		default:
			c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
		}

		timeData, err := time.Parse(time.RFC3339Nano,
			response.Data[i].Candle[0])
		if err != nil {
			return fmt.Errorf("%v Time data could not be parsed: %v",
				o.Name,
				response.Data[i].Candle[0])
		}

		candleIndex := strings.LastIndex(response.Table, okGroupWsCandle)
		candleInterval := response.Table[candleIndex+len(okGroupWsCandle):]

		klineData := stream.KlineData{
			AssetType: o.GetAssetTypeFromTableName(response.Table),
			Pair:      c,
			Exchange:  o.Name,
			Timestamp: timeData,
			Interval:  candleInterval,
		}
		klineData.OpenPrice, err = strconv.ParseFloat(response.Data[i].Candle[1], 64)
		if err != nil {
			return err
		}
		klineData.HighPrice, err = strconv.ParseFloat(response.Data[i].Candle[2], 64)
		if err != nil {
			return err
		}
		klineData.LowPrice, err = strconv.ParseFloat(response.Data[i].Candle[3], 64)
		if err != nil {
			return err
		}
		klineData.ClosePrice, err = strconv.ParseFloat(response.Data[i].Candle[4], 64)
		if err != nil {
			return err
		}
		klineData.Volume, err = strconv.ParseFloat(response.Data[i].Candle[5], 64)
		if err != nil {
			return err
		}
		o.Websocket.DataHandler <- klineData
	}
	return nil
}

// WsProcessOrderBook Validates the checksum and updates internal orderbook values
func (o *OKGroup) WsProcessOrderBook(respRaw []byte) error {
	var response WebsocketOrderBooksData
	err := json.Unmarshal(respRaw, &response)
	if err != nil {
		return err
	}
	orderbookMutex.Lock()
	defer orderbookMutex.Unlock()
	a := o.GetAssetTypeFromTableName(response.Table)
	for i := range response.Data {
		f := strings.Split(response.Data[i].InstrumentID, delimiterDash)

		var c currency.Pair
		switch a {
		case asset.Futures, asset.PerpetualSwap:
			c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1],
				f[2],
				currency.UnderscoreDelimiter)
		default:
			c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
		}

		if response.Action == okGroupWsOrderbookPartial {
			err := o.WsProcessPartialOrderBook(&response.Data[i], c, a)
			if err != nil {
				err2 := o.wsResubscribeToOrderbook(&response)
				if err2 != nil {
					o.Websocket.DataHandler <- err2
				}
				return err
			}
		} else if response.Action == okGroupWsOrderbookUpdate {
			if len(response.Data[i].Asks) == 0 && len(response.Data[i].Bids) == 0 {
				return nil
			}
			err := o.WsProcessUpdateOrderbook(&response.Data[i], c, a)
			if err != nil {
				err2 := o.wsResubscribeToOrderbook(&response)
				if err2 != nil {
					o.Websocket.DataHandler <- err2
				}
				return err
			}
		}
	}
	return nil
}

func (o *OKGroup) wsResubscribeToOrderbook(response *WebsocketOrderBooksData) error {
	a := o.GetAssetTypeFromTableName(response.Table)
	for i := range response.Data {
		f := strings.Split(response.Data[i].InstrumentID, delimiterDash)

		var c currency.Pair
		switch a {
		case asset.Futures, asset.PerpetualSwap:
			c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterDash)
		default:
			c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
		}

		channelToResubscribe := &stream.ChannelSubscription{
			Channel:  response.Table,
			Currency: c,
			Asset:    a,
		}
		err := o.Websocket.ResubscribeToChannel(channelToResubscribe)
		if err != nil {
			return fmt.Errorf("%s resubscribe to orderbook error %s", o.Name, err)
		}
	}
	return nil
}

// AppendWsOrderbookItems adds websocket orderbook data bid/asks into an
// orderbook item array
func (o *OKGroup) AppendWsOrderbookItems(entries [][]interface{}) ([]orderbook.Item, error) {
	var items []orderbook.Item
	for j := range entries {
		amount, err := strconv.ParseFloat(entries[j][1].(string), 64)
		if err != nil {
			return nil, err
		}
		price, err := strconv.ParseFloat(entries[j][0].(string), 64)
		if err != nil {
			return nil, err
		}
		items = append(items, orderbook.Item{Amount: amount, Price: price})
	}
	return items, nil
}

// WsProcessPartialOrderBook takes websocket orderbook data and creates an
// orderbook Calculates checksum to ensure it is valid
func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketOrderBook, instrument currency.Pair, a asset.Item) error {
	signedChecksum := o.CalculatePartialOrderbookChecksum(wsEventData)
	if signedChecksum != wsEventData.Checksum {
		return fmt.Errorf("%s channel: %s. Orderbook partial for %v checksum invalid",
			o.Name,
			a,
			instrument)
	}
	if o.Verbose {
		log.Debugf(log.ExchangeSys,
			"%s passed checksum for instrument %s",
			o.Name,
			instrument)
	}

	asks, err := o.AppendWsOrderbookItems(wsEventData.Asks)
	if err != nil {
		return err
	}

	bids, err := o.AppendWsOrderbookItems(wsEventData.Bids)
	if err != nil {
		return err
	}

	newOrderBook := orderbook.Base{
		Asks:            asks,
		Bids:            bids,
		Asset:           a,
		LastUpdated:     wsEventData.Timestamp,
		Pair:            instrument,
		Exchange:        o.Name,
		VerifyOrderbook: o.CanVerifyOrderbook,
	}
	return o.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
}

// WsProcessUpdateOrderbook updates an existing orderbook using websocket data
// After merging WS data, it will sort, validate and finally update the existing
// orderbook
func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketOrderBook, instrument currency.Pair, a asset.Item) error {
	update := buffer.Update{
		Asset:      a,
		Pair:       instrument,
		UpdateTime: wsEventData.Timestamp,
	}

	var err error
	update.Asks, err = o.AppendWsOrderbookItems(wsEventData.Asks)
	if err != nil {
		return err
	}
	update.Bids, err = o.AppendWsOrderbookItems(wsEventData.Bids)
	if err != nil {
		return err
	}

	err = o.Websocket.Orderbook.Update(&update)
	if err != nil {
		return err
	}

	updatedOb, err := o.Websocket.Orderbook.GetOrderbook(instrument, a)
	if err != nil {
		return err
	}
	checksum := o.CalculateUpdateOrderbookChecksum(updatedOb)

	if checksum != wsEventData.Checksum {
		// re-sub
		log.Warnf(log.ExchangeSys, "%s checksum failure for item %s",
			o.Name,
			wsEventData.InstrumentID)
		return errors.New("checksum failed")
	}
	return nil
}

// CalculatePartialOrderbookChecksum alternates over the first 25 bid and ask
// entries from websocket data. The checksum is made up of the price and the
// quantity with a semicolon (:) deliminating them. This will also work when
// there are less than 25 entries (for whatever reason)
// eg Bid:Ask:Bid:Ask:Ask:Ask
func (o *OKGroup) CalculatePartialOrderbookChecksum(orderbookData *WebsocketOrderBook) int32 {
	var checksum strings.Builder
	for i := 0; i < allowableIterations; i++ {
		if len(orderbookData.Bids)-1 >= i {
			checksum.WriteString(orderbookData.Bids[i][0].(string) +
				delimiterColon +
				orderbookData.Bids[i][1].(string) +
				delimiterColon)
		}
		if len(orderbookData.Asks)-1 >= i {
			checksum.WriteString(orderbookData.Asks[i][0].(string) +
				delimiterColon +
				orderbookData.Asks[i][1].(string) +
				delimiterColon)
		}
	}
	checksumStr := strings.TrimSuffix(checksum.String(), delimiterColon)
	return int32(crc32.ChecksumIEEE([]byte(checksumStr)))
}

// CalculateUpdateOrderbookChecksum alternates over the first 25 bid and ask
// entries of a merged orderbook. The checksum is made up of the price and the
// quantity with a semicolon (:) deliminating them. This will also work when
// there are less than 25 entries (for whatever reason)
// eg Bid:Ask:Bid:Ask:Ask:Ask
func (o *OKGroup) CalculateUpdateOrderbookChecksum(orderbookData *orderbook.Base) int32 {
	var checksum strings.Builder
	for i := 0; i < allowableIterations; i++ {
		if len(orderbookData.Bids)-1 >= i {
			price := strconv.FormatFloat(orderbookData.Bids[i].Price, 'f', -1, 64)
			amount := strconv.FormatFloat(orderbookData.Bids[i].Amount, 'f', -1, 64)
			checksum.WriteString(price + delimiterColon + amount + delimiterColon)
		}
		if len(orderbookData.Asks)-1 >= i {
			price := strconv.FormatFloat(orderbookData.Asks[i].Price, 'f', -1, 64)
			amount := strconv.FormatFloat(orderbookData.Asks[i].Amount, 'f', -1, 64)
			checksum.WriteString(price + delimiterColon + amount + delimiterColon)
		}
	}
	checksumStr := strings.TrimSuffix(checksum.String(), delimiterColon)
	return int32(crc32.ChecksumIEEE([]byte(checksumStr)))
}

// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be
// handled by ManageSubscriptions()
func (o *OKGroup) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
	var subscriptions []stream.ChannelSubscription
	assets := o.GetAssetTypes()
	for x := range assets {
		pairs, err := o.GetEnabledPairs(assets[x])
		if err != nil {
			return nil, err
		}

		switch assets[x] {
		case asset.Spot:
			channels := defaultSpotSubscribedChannels
			if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
				channels = append(channels,
					okGroupWsSpotMarginAccount,
					okGroupWsSpotAccount,
					okGroupWsSpotOrder)
			}

			for i := range pairs {
				p, err := o.FormatExchangeCurrency(pairs[i], asset.Spot)
				if err != nil {
					return nil, err
				}
				for y := range channels {
					subscriptions = append(subscriptions,
						stream.ChannelSubscription{
							Channel:  channels[y],
							Currency: p,
							Asset:    asset.Spot,
						})
				}
			}
		case asset.Futures:
			channels := defaultFuturesSubscribedChannels
			if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
				channels = append(channels,
					okGroupWsFuturesAccount,
					okGroupWsFuturesPosition,
					okGroupWsFuturesOrder)
			}
			var futuresAccountPairs currency.Pairs
			var futuresAccountCodes currency.Currencies

			for i := range pairs {
				p, err := o.FormatExchangeCurrency(pairs[i], asset.Futures)
				if err != nil {
					return nil, err
				}
				for y := range channels {
					if channels[y] == okGroupWsFuturesAccount {
						currencyString := strings.Split(pairs[i].String(),
							currency.UnderscoreDelimiter)[0]
						newP, err := currency.NewPairFromString(currencyString)
						if err != nil {
							return nil, err
						}

						if !futuresAccountCodes.Contains(newP.Base) {
							// subscribe to coin-margin futures trading mode
							subscriptions = append(subscriptions,
								stream.ChannelSubscription{
									Channel:  channels[y],
									Currency: currency.NewPair(newP.Base, currency.Code{}),
									Asset:    asset.Futures,
								})
							futuresAccountCodes = append(futuresAccountCodes, newP.Base)
						}

						if newP.Quote != currency.USDT {
							// Only allows subscription to USDT margined pair
							continue
						}

						if !futuresAccountPairs.Contains(newP, true) {
							subscriptions = append(subscriptions,
								stream.ChannelSubscription{
									Channel:  channels[y],
									Currency: newP,
									Asset:    asset.Futures,
								})
							futuresAccountPairs = futuresAccountPairs.Add(newP)
						}

						continue
					}
					subscriptions = append(subscriptions,
						stream.ChannelSubscription{
							Channel:  channels[y],
							Currency: p,
							Asset:    asset.Futures,
						})
				}
			}
		case asset.PerpetualSwap:
			channels := defaultSwapSubscribedChannels
			if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
				channels = append(channels,
					okGroupWsSwapAccount,
					okGroupWsSwapPosition,
					okGroupWsSwapOrder)
			}
			for i := range pairs {
				p, err := o.FormatExchangeCurrency(pairs[i], asset.PerpetualSwap)
				if err != nil {
					return nil, err
				}
				for y := range channels {
					subscriptions = append(subscriptions,
						stream.ChannelSubscription{
							Channel:  channels[y],
							Currency: p,
							Asset:    asset.PerpetualSwap,
						})
				}
			}
		case asset.Index:
			for i := range pairs {
				p, err := o.FormatExchangeCurrency(pairs[i], asset.Index)
				if err != nil {
					return nil, err
				}
				for y := range defaultIndexSubscribedChannels {
					subscriptions = append(subscriptions,
						stream.ChannelSubscription{
							Channel:  defaultIndexSubscribedChannels[y],
							Currency: p,
							Asset:    asset.Index,
						})
				}
			}
		default:
			o.Websocket.DataHandler <- errors.New("unhandled asset type")
		}
	}
	return subscriptions, nil
}

// Subscribe sends a websocket message to receive data from the channel
func (o *OKGroup) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
	return o.handleSubscriptions("subscribe", channelsToSubscribe)
}

// Unsubscribe sends a websocket message to stop receiving data from the channel
func (o *OKGroup) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
	return o.handleSubscriptions("unsubscribe", channelsToUnsubscribe)
}

func (o *OKGroup) handleSubscriptions(operation string, subs []stream.ChannelSubscription) error {
	request := WebsocketEventRequest{
		Operation: operation,
	}

	var channels []stream.ChannelSubscription
	for i := 0; i < len(subs); i++ {
		// Temp type to evaluate max byte len after a marshal on batched unsubs
		temp := WebsocketEventRequest{
			Operation: operation,
		}
		temp.Arguments = make([]string, len(request.Arguments))
		copy(temp.Arguments, request.Arguments)

		arg := subs[i].Channel + delimiterColon
		if strings.EqualFold(subs[i].Channel, okGroupWsSpotAccount) {
			arg += subs[i].Currency.Base.String()
		} else {
			arg += subs[i].Currency.String()
		}

		temp.Arguments = append(temp.Arguments, arg)
		chunk, err := json.Marshal(request)
		if err != nil {
			return err
		}

		if len(chunk) > maxConnByteLen {
			// If temp chunk exceeds max byte length determined by the exchange,
			// commit last payload.
			i-- // reverse position in range to reuse channel unsubscription on
			// next iteration
			err = o.Websocket.Conn.SendJSONMessage(request)
			if err != nil {
				return err
			}

			if operation == "unsubscribe" {
				o.Websocket.RemoveSuccessfulUnsubscriptions(channels...)
			} else {
				o.Websocket.AddSuccessfulSubscriptions(channels...)
			}

			// Drop prior unsubs and chunked payload args on successful unsubscription
			channels = nil
			request.Arguments = nil
			continue
		}
		// Add pending chained items
		channels = append(channels, subs[i])
		request.Arguments = temp.Arguments
	}

	// Commit left overs to payload
	err := o.Websocket.Conn.SendJSONMessage(request)
	if err != nil {
		return err
	}

	if operation == "unsubscribe" {
		o.Websocket.RemoveSuccessfulUnsubscriptions(channels...)
	} else {
		o.Websocket.AddSuccessfulSubscriptions(channels...)
	}
	return nil
}

// GetWsChannelWithoutOrderType takes WebsocketDataResponse.Table and returns
// The base channel name eg receive "spot/depth5:BTC-USDT" return "depth5"
func (o *OKGroup) GetWsChannelWithoutOrderType(table string) string {
	index := strings.Index(table, "/")
	if index == -1 {
		return table
	}
	channel := table[index+1:]
	index = strings.Index(channel, ":")
	// Some events do not contain a currency
	if index == -1 {
		return channel
	}

	return channel[:index]
}

// GetAssetTypeFromTableName gets the asset type from the table name
// eg "spot/ticker:BTCUSD" results in "SPOT"
func (o *OKGroup) GetAssetTypeFromTableName(table string) asset.Item {
	assetIndex := strings.Index(table, "/")
	switch table[:assetIndex] {
	case asset.Futures.String():
		return asset.Futures
	case asset.Spot.String():
		return asset.Spot
	case "swap":
		return asset.PerpetualSwap
	case asset.Index.String():
		return asset.Index
	default:
		log.Warnf(log.ExchangeSys, "%s unhandled asset type %s",
			o.Name,
			table[:assetIndex])
		return asset.Item(table[:assetIndex])
	}
}
