package poloniex

import (
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	gws "github.com/gorilla/websocket"
	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/common/crypto"
	"github.com/thrasher-corp/gocryptotrader/currency"
	"github.com/thrasher-corp/gocryptotrader/encoding/json"
	"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
	"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
	"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/request"
	"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
	"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
	"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
	"github.com/thrasher-corp/gocryptotrader/log"
)

const (
	poloniexWebsocketAddress = "wss://api2.poloniex.com"
	wsAccountNotificationID  = 1000
	wsTickerDataID           = 1002
	ws24HourExchangeVolumeID = 1003
	wsHeartbeat              = 1010

	accountNotificationBalanceUpdate     = "b"
	accountNotificationOrderUpdate       = "o"
	accountNotificationPendingOrder      = "p"
	accountNotificationOrderLimitCreated = "n"
	accountNotificationTrades            = "t"
	accountNotificationKilledOrder       = "k"
	accountNotificationMarginPosition    = "m"

	orderbookInitial = "i"
	orderbookUpdate  = "o"
	tradeUpdate      = "t"
)

var (
	errNotEnoughData        = errors.New("element length not adequate to process")
	errTypeAssertionFailure = errors.New("type assertion failure")
	errIDNotFoundInPairMap  = errors.New("id not associated with currency pair map")
	errIDNotFoundInCodeMap  = errors.New("id not associated with currency code map")
)

// WsConnect initiates a websocket connection
func (e *Exchange) WsConnect() error {
	ctx := context.TODO()
	if !e.Websocket.IsEnabled() || !e.IsEnabled() {
		return websocket.ErrWebsocketNotEnabled
	}
	var dialer gws.Dialer
	err := e.Websocket.Conn.Dial(ctx, &dialer, http.Header{})
	if err != nil {
		return err
	}

	err = e.loadCurrencyDetails(ctx)
	if err != nil {
		return err
	}

	e.Websocket.Wg.Add(1)
	go e.wsReadData(ctx)

	return nil
}

// TODO: Create routine to refresh list every day/week(?) for production
func (e *Exchange) loadCurrencyDetails(ctx context.Context) error {
	if e.details.isInitial() {
		ticks, err := e.GetTicker(ctx)
		if err != nil {
			return err
		}
		err = e.details.loadPairs(ticks)
		if err != nil {
			return err
		}

		currs, err := e.GetCurrencies(ctx)
		if err != nil {
			return err
		}

		err = e.details.loadCodes(currs)
		if err != nil {
			return err
		}
	}
	return nil
}

// wsReadData handles data from the websocket connection
func (e *Exchange) wsReadData(ctx context.Context) {
	defer e.Websocket.Wg.Done()
	for {
		resp := e.Websocket.Conn.ReadMessage()
		if resp.Raw == nil {
			return
		}
		err := e.wsHandleData(ctx, resp.Raw)
		if err != nil {
			e.Websocket.DataHandler <- fmt.Errorf("%s: %w", e.Name, err)
		}
	}
}

func (e *Exchange) wsHandleData(ctx context.Context, respRaw []byte) error {
	var result any
	err := json.Unmarshal(respRaw, &result)
	if err != nil {
		return err
	}

	data, ok := result.([]any)
	if !ok {
		return fmt.Errorf("%w data is not []any",
			errTypeAssertionFailure)
	}

	if len(data) == 0 {
		return nil
	}
	if len(data) == 2 {
		// subscription acknowledgement
		// TODO: Add in subscriber ack
		return nil
	}

	channelID, ok := data[0].(float64)
	if !ok {
		return fmt.Errorf("%w channel id is not of type float64",
			errTypeAssertionFailure)
	}
	switch channelID {
	case ws24HourExchangeVolumeID, wsHeartbeat:
		return nil
	case wsAccountNotificationID:
		var notificationsArray []any
		notificationsArray, ok = data[2].([]any)
		if !ok {
			return fmt.Errorf("%w account notification is not a []any",
				errTypeAssertionFailure)
		}
		for i := range notificationsArray {
			var notification []any
			notification, ok = (notificationsArray[i]).([]any)
			if !ok {
				return fmt.Errorf("%w notification array element is not a []any",
					errTypeAssertionFailure)
			}
			var updateType string
			updateType, ok = notification[0].(string)
			if !ok {
				return fmt.Errorf("%w update type is not a string",
					errTypeAssertionFailure)
			}

			switch updateType {
			case accountNotificationPendingOrder:
				err = e.processAccountPendingOrder(notification)
				if err != nil {
					return fmt.Errorf("account notification pending order: %w", err)
				}
			case accountNotificationOrderUpdate:
				err = e.processAccountOrderUpdate(notification)
				if err != nil {
					return fmt.Errorf("account notification order update: %w", err)
				}
			case accountNotificationOrderLimitCreated:
				err = e.processAccountOrderLimit(notification)
				if err != nil {
					return fmt.Errorf("account notification limit order creation: %w", err)
				}
			case accountNotificationBalanceUpdate:
				err = e.processAccountBalanceUpdate(ctx, notification)
				if err != nil {
					return fmt.Errorf("account notification balance update: %w", err)
				}
			case accountNotificationTrades:
				err = e.processAccountTrades(notification)
				if err != nil {
					return fmt.Errorf("account notification trades: %w", err)
				}
			case accountNotificationKilledOrder:
				err = e.processAccountKilledOrder(notification)
				if err != nil {
					return fmt.Errorf("account notification killed order: %w", err)
				}
			case accountNotificationMarginPosition:
				err = e.processAccountMarginPosition(notification)
				if err != nil {
					return fmt.Errorf("account notification margin position: %w", err)
				}
			default:
				return fmt.Errorf("unhandled account update: %s", string(respRaw))
			}
		}
		return nil
	case wsTickerDataID:
		err = e.wsHandleTickerData(data)
		if err != nil {
			return fmt.Errorf("websocket ticker process: %w", err)
		}
		return nil
	}

	priceAggBook, ok := data[2].([]any)
	if !ok {
		return fmt.Errorf("%w price aggregated book not []any",
			errTypeAssertionFailure)
	}

	for x := range priceAggBook {
		subData, ok := priceAggBook[x].([]any)
		if !ok {
			return fmt.Errorf("%w price aggregated book element not []any",
				errTypeAssertionFailure)
		}

		updateIdent, ok := subData[0].(string)
		if !ok {
			return fmt.Errorf("%w update identifier not a string",
				errTypeAssertionFailure)
		}

		switch updateIdent {
		case orderbookInitial:
			err = e.WsProcessOrderbookSnapshot(subData)
			if err != nil {
				return fmt.Errorf("websocket process orderbook snapshot: %w", err)
			}
		case orderbookUpdate:
			var pair currency.Pair
			pair, err = e.details.GetPair(channelID)
			if err != nil {
				return err
			}
			var seqNo float64
			seqNo, ok = data[1].(float64)
			if !ok {
				return fmt.Errorf("%w sequence number is not a float64",
					errTypeAssertionFailure)
			}
			err = e.WsProcessOrderbookUpdate(seqNo, subData, pair)
			if err != nil {
				return fmt.Errorf("websocket process orderbook update: %w", err)
			}
		case tradeUpdate:
			err = e.processTrades(channelID, subData)
			if err != nil {
				return fmt.Errorf("websocket process trades update: %w", err)
			}
		default:
			e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{
				Message: e.Name + websocket.UnhandledMessage + string(respRaw),
			}
		}
	}
	return nil
}

func (e *Exchange) wsHandleTickerData(data []any) error {
	tickerData, ok := data[2].([]any)
	if !ok {
		return fmt.Errorf("%w ticker data is not []any",
			errTypeAssertionFailure)
	}

	currencyID, ok := tickerData[0].(float64)
	if !ok {
		return fmt.Errorf("%w currency ID not float64", errTypeAssertionFailure)
	}

	pair, err := e.details.GetPair(currencyID)
	if err != nil {
		return err
	}

	enabled, err := e.GetEnabledPairs(asset.Spot)
	if err != nil {
		return err
	}

	if !enabled.Contains(pair, true) {
		return nil
	}

	tlp, ok := tickerData[1].(string)
	if !ok {
		return fmt.Errorf("%w last price not string", errTypeAssertionFailure)
	}

	lastPrice, err := strconv.ParseFloat(tlp, 64)
	if err != nil {
		return err
	}

	la, ok := tickerData[2].(string)
	if !ok {
		return fmt.Errorf("%w lowest ask price not string",
			errTypeAssertionFailure)
	}

	lowestAsk, err := strconv.ParseFloat(la, 64)
	if err != nil {
		return err
	}

	hb, ok := tickerData[3].(string)
	if !ok {
		return fmt.Errorf("%w highest bid price not string",
			errTypeAssertionFailure)
	}

	highestBid, err := strconv.ParseFloat(hb, 64)
	if err != nil {
		return err
	}

	bcv, ok := tickerData[5].(string)
	if !ok {
		return fmt.Errorf("%w base currency volume not string",
			errTypeAssertionFailure)
	}

	baseCurrencyVolume24H, err := strconv.ParseFloat(bcv, 64)
	if err != nil {
		return err
	}

	qcv, ok := tickerData[6].(string)
	if !ok {
		return fmt.Errorf("%w quote currency volume not string",
			errTypeAssertionFailure)
	}

	quoteCurrencyVolume24H, err := strconv.ParseFloat(qcv, 64)
	if err != nil {
		return err
	}

	// Unused variables below, can add later if needed:
	// percentageChange, ok := tickerData[4].(string)
	// Not integrating isFrozen with currency details as this will slow down
	// the sync RW mutex (can use REST calls for now).
	// isFrozen, ok := tickerData[7].(float64) // == 1 means it is frozen
	// highestTradeIn24Hm, ok := tickerData[8].(string)
	// lowestTradePrice24H, ok := tickerData[9].(string)

	e.Websocket.DataHandler <- &ticker.Price{
		ExchangeName: e.Name,
		Volume:       baseCurrencyVolume24H,
		QuoteVolume:  quoteCurrencyVolume24H,
		High:         highestBid,
		Low:          lowestAsk,
		Bid:          highestBid,
		Ask:          lowestAsk,
		Last:         lastPrice,
		AssetType:    asset.Spot,
		Pair:         pair,
	}
	return nil
}

// WsProcessOrderbookSnapshot processes a new orderbook snapshot into a local
// of orderbooks
func (e *Exchange) WsProcessOrderbookSnapshot(data []any) error {
	subDataMap, ok := data[1].(map[string]any)
	if !ok {
		return fmt.Errorf("%w subData element is not map[string]any",
			errTypeAssertionFailure)
	}

	pMap, ok := subDataMap["currencyPair"]
	if !ok {
		return errors.New("could not find currency pair in map")
	}

	pair, ok := pMap.(string)
	if !ok {
		return fmt.Errorf("%w subData element is not map[string]any",
			errTypeAssertionFailure)
	}

	if len(data) < 3 {
		return fmt.Errorf("%w for pair %v", errNotEnoughData, pair)
	}

	ts, ok := data[2].(string)
	if !ok {
		return common.GetTypeAssertError("string", data[2], "timestamp string")
	}

	tsMilli, err := strconv.ParseInt(ts, 10, 64)
	if err != nil {
		return err
	}

	oMap, ok := subDataMap["orderBook"]
	if !ok {
		return errors.New("could not find orderbook data in map")
	}

	ob, ok := oMap.([]any)
	if !ok {
		return fmt.Errorf("%w orderbook data is not []any",
			errTypeAssertionFailure)
	}

	if len(ob) != 2 {
		return errNotEnoughData
	}

	askData, ok := ob[0].(map[string]any)
	if !ok {
		return fmt.Errorf("%w ask data is not map[string]any",
			errTypeAssertionFailure)
	}

	bidData, ok := ob[1].(map[string]any)
	if !ok {
		return fmt.Errorf("%w bid data is not map[string]any",
			errTypeAssertionFailure)
	}

	var book orderbook.Book
	book.Asks = make(orderbook.Levels, 0, len(askData))
	for price, volume := range askData {
		var p float64
		p, err = strconv.ParseFloat(price, 64)
		if err != nil {
			return err
		}
		v, ok := volume.(string)
		if !ok {
			return fmt.Errorf("%w ask volume data not string",
				errTypeAssertionFailure)
		}
		var a float64
		a, err = strconv.ParseFloat(v, 64)
		if err != nil {
			return err
		}
		book.Asks = append(book.Asks, orderbook.Level{Price: p, Amount: a})
	}

	book.Bids = make(orderbook.Levels, 0, len(bidData))
	for price, volume := range bidData {
		var p float64
		p, err = strconv.ParseFloat(price, 64)
		if err != nil {
			return err
		}
		v, ok := volume.(string)
		if !ok {
			return fmt.Errorf("%w bid volume data not string",
				errTypeAssertionFailure)
		}
		var a float64
		a, err = strconv.ParseFloat(v, 64)
		if err != nil {
			return err
		}
		book.Bids = append(book.Bids, orderbook.Level{Price: p, Amount: a})
	}

	// Both sides are completely out of order - sort needs to be used
	book.Asks.SortAsks()
	book.Bids.SortBids()
	book.Asset = asset.Spot
	book.ValidateOrderbook = e.ValidateOrderbook
	book.LastUpdated = time.UnixMilli(tsMilli)
	book.Pair, err = currency.NewPairFromString(pair)
	if err != nil {
		return err
	}
	book.Exchange = e.Name

	return e.Websocket.Orderbook.LoadSnapshot(&book)
}

// WsProcessOrderbookUpdate processes new orderbook updates
func (e *Exchange) WsProcessOrderbookUpdate(sequenceNumber float64, data []any, pair currency.Pair) error {
	if len(data) < 5 {
		return errNotEnoughData
	}

	ps, ok := data[2].(string)
	if !ok {
		return fmt.Errorf("%w price not string", errTypeAssertionFailure)
	}
	price, err := strconv.ParseFloat(ps, 64)
	if err != nil {
		return err
	}
	vs, ok := data[3].(string)
	if !ok {
		return fmt.Errorf("%w volume not string", errTypeAssertionFailure)
	}
	volume, err := strconv.ParseFloat(vs, 64)
	if err != nil {
		return err
	}
	bs, ok := data[1].(float64)
	if !ok {
		return fmt.Errorf("%w buysell not float64", errTypeAssertionFailure)
	}

	ts, ok := data[4].(string)
	if !ok {
		return common.GetTypeAssertError("string", data[2], "timestamp string")
	}

	tsMilli, err := strconv.ParseInt(ts, 10, 64)
	if err != nil {
		return err
	}

	update := &orderbook.Update{
		Pair:       pair,
		Asset:      asset.Spot,
		UpdateID:   int64(sequenceNumber),
		UpdateTime: time.UnixMilli(tsMilli),
	}
	if bs == 1 {
		update.Bids = []orderbook.Level{{Price: price, Amount: volume}}
	} else {
		update.Asks = []orderbook.Level{{Price: price, Amount: volume}}
	}
	return e.Websocket.Orderbook.Update(update)
}

// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (e *Exchange) GenerateDefaultSubscriptions() (subscription.List, error) {
	enabledPairs, err := e.GetEnabledPairs(asset.Spot)
	if err != nil {
		return nil, err
	}

	subscriptions := make(subscription.List, 0, len(enabledPairs))
	subscriptions = append(subscriptions, &subscription.Subscription{
		Channel: strconv.FormatInt(wsTickerDataID, 10),
	})

	if e.IsWebsocketAuthenticationSupported() {
		subscriptions = append(subscriptions, &subscription.Subscription{
			Channel: strconv.FormatInt(wsAccountNotificationID, 10),
		})
	}

	for j := range enabledPairs {
		enabledPairs[j].Delimiter = currency.UnderscoreDelimiter
		subscriptions = append(subscriptions, &subscription.Subscription{
			Channel: "orderbook",
			Pairs:   currency.Pairs{enabledPairs[j]},
			Asset:   asset.Spot,
		})
	}
	return subscriptions, nil
}

// Subscribe sends a websocket message to receive data from the channel
func (e *Exchange) Subscribe(subs subscription.List) error {
	ctx := context.TODO()
	return e.manageSubs(ctx, subs, wsSubscribeOp)
}

// Unsubscribe sends a websocket message to stop receiving data from the channel
func (e *Exchange) Unsubscribe(subs subscription.List) error {
	ctx := context.TODO()
	return e.manageSubs(ctx, subs, wsUnsubscribeOp)
}

func (e *Exchange) manageSubs(ctx context.Context, subs subscription.List, op wsOp) error {
	var creds *accounts.Credentials
	if e.IsWebsocketAuthenticationSupported() {
		var err error
		creds, err = e.GetCredentials(ctx)
		if err != nil {
			return err
		}
	}

	var errs error
	for _, s := range subs {
		var err error
		if creds != nil && strings.EqualFold(strconv.FormatInt(wsAccountNotificationID, 10), s.Channel) {
			err = e.wsSendAuthorisedCommand(ctx, creds.Secret, creds.Key, op)
		} else {
			req := wsCommand{Command: op}
			if strings.EqualFold(strconv.FormatInt(wsTickerDataID, 10), s.Channel) {
				req.Channel = wsTickerDataID
			} else {
				if len(s.Pairs) != 1 {
					return subscription.ErrNotSinglePair
				}
				req.Channel = s.Pairs[0].String()
			}
			err = e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, req)
		}
		if err == nil {
			if op == wsSubscribeOp {
				err = e.Websocket.AddSuccessfulSubscriptions(e.Websocket.Conn, s)
			} else {
				err = e.Websocket.RemoveSubscriptions(e.Websocket.Conn, s)
			}
		}
		if err != nil {
			errs = common.AppendError(errs, err)
		}
	}
	return errs
}

func (e *Exchange) wsSendAuthorisedCommand(ctx context.Context, secret, key string, op wsOp) error {
	nonce := fmt.Sprintf("nonce=%v", time.Now().UnixNano())
	hmac, err := crypto.GetHMAC(crypto.HashSHA512, []byte(nonce), []byte(secret))
	if err != nil {
		return err
	}
	req := wsAuthorisationRequest{
		Command: op,
		Channel: 1000,
		Sign:    hex.EncodeToString(hmac),
		Key:     key,
		Payload: nonce,
	}
	return e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, req)
}

func (e *Exchange) processAccountMarginPosition(notification []any) error {
	if len(notification) < 5 {
		return errNotEnoughData
	}

	orderID, ok := notification[1].(float64)
	if !ok {
		return fmt.Errorf("%w order id not float64", errTypeAssertionFailure)
	}

	currencyID, ok := notification[2].(float64)
	if !ok {
		return fmt.Errorf("%w currency id not float64", errTypeAssertionFailure)
	}
	code, err := e.details.GetCode(currencyID)
	if err != nil {
		return err
	}

	a, ok := notification[3].(string)
	if !ok {
		return fmt.Errorf("%w amount not string", errTypeAssertionFailure)
	}

	amount, err := strconv.ParseFloat(a, 64)
	if err != nil {
		return err
	}

	// null returned so ok check is not needed
	clientOrderID, _ := notification[4].(string)

	// Temp struct for margin position changes
	e.Websocket.DataHandler <- struct {
		OrderID       string
		Code          currency.Code
		Amount        float64
		ClientOrderID string
	}{
		OrderID:       strconv.FormatFloat(orderID, 'f', -1, 64),
		Code:          code,
		Amount:        amount,
		ClientOrderID: clientOrderID,
	}

	return nil
}

func (e *Exchange) processAccountPendingOrder(notification []any) error {
	if len(notification) < 7 {
		return errNotEnoughData
	}

	orderID, ok := notification[1].(float64)
	if !ok {
		return fmt.Errorf("%w order id not float64", errTypeAssertionFailure)
	}

	currencyID, ok := notification[2].(float64)
	if !ok {
		return fmt.Errorf("%w currency id not float64", errTypeAssertionFailure)
	}
	pair, err := e.details.GetPair(currencyID)
	if err != nil {
		if !errors.Is(err, errIDNotFoundInPairMap) {
			return err
		}
		log.Errorf(log.WebsocketMgr,
			"%s - Unknown currency pair ID. Currency will appear as the pair ID: '%v'",
			e.Name,
			currencyID)
	}

	price, ok := notification[3].(string)
	if !ok {
		return fmt.Errorf("%w price not string", errTypeAssertionFailure)
	}
	orderPrice, err := strconv.ParseFloat(price, 64)
	if err != nil {
		return err
	}
	amount, ok := notification[4].(string)
	if !ok {
		return fmt.Errorf("%w amount not string", errTypeAssertionFailure)
	}
	orderAmount, err := strconv.ParseFloat(amount, 64)
	if err != nil {
		return err
	}
	side, ok := notification[5].(string)
	if !ok {
		return fmt.Errorf("%w order type not string", errTypeAssertionFailure)
	}
	orderSide := order.Buy
	if side == "0" {
		orderSide = order.Sell
	}

	// null returned so ok check is not needed
	clientOrderID, _ := notification[6].(string)

	e.Websocket.DataHandler <- &order.Detail{
		Exchange:        e.Name,
		OrderID:         strconv.FormatFloat(orderID, 'f', -1, 64),
		Pair:            pair,
		AssetType:       asset.Spot,
		Side:            orderSide,
		Price:           orderPrice,
		Amount:          orderAmount,
		RemainingAmount: orderAmount,
		ClientOrderID:   clientOrderID,
		Status:          order.Pending,
	}
	return nil
}

func (e *Exchange) processAccountOrderUpdate(notification []any) error {
	if len(notification) < 5 {
		return errNotEnoughData
	}

	orderID, ok := notification[1].(float64)
	if !ok {
		return fmt.Errorf("%w order id not float64", errTypeAssertionFailure)
	}

	a, ok := notification[2].(string)
	if !ok {
		return fmt.Errorf("%w amount not string", errTypeAssertionFailure)
	}
	amount, err := strconv.ParseFloat(a, 64)
	if err != nil {
		return err
	}

	oType, ok := notification[3].(string)
	if !ok {
		return fmt.Errorf("%w order type not string", errTypeAssertionFailure)
	}

	var oStatus order.Status
	var cancelledAmount float64
	if oType == "c" {
		if len(notification) < 6 {
			return errNotEnoughData
		}
		cancel, ok := notification[5].(string)
		if !ok {
			return fmt.Errorf("%w cancel amount not string", errTypeAssertionFailure)
		}

		cancelledAmount, err = strconv.ParseFloat(cancel, 64)
		if err != nil {
			return err
		}

		if amount > 0 {
			oStatus = order.PartiallyCancelled
		} else {
			oStatus = order.Cancelled
		}
	} else {
		if amount > 0 {
			oStatus = order.PartiallyFilled
		} else {
			oStatus = order.Filled
		}
	}

	// null returned so ok check is not needed
	clientOrderID, _ := notification[4].(string)

	e.Websocket.DataHandler <- &order.Detail{
		Exchange:        e.Name,
		RemainingAmount: cancelledAmount,
		Amount:          amount + cancelledAmount,
		ExecutedAmount:  amount,
		OrderID:         strconv.FormatFloat(orderID, 'f', -1, 64),
		Type:            order.Limit,
		Status:          oStatus,
		AssetType:       asset.Spot,
		ClientOrderID:   clientOrderID,
	}
	return nil
}

func (e *Exchange) processAccountOrderLimit(notification []any) error {
	if len(notification) != 9 {
		return errNotEnoughData
	}

	currencyID, ok := notification[1].(float64)
	if !ok {
		return fmt.Errorf("%w currency ID not string", errTypeAssertionFailure)
	}
	pair, err := e.details.GetPair(currencyID)
	if err != nil {
		if !errors.Is(err, errIDNotFoundInPairMap) {
			return err
		}
		log.Errorf(log.WebsocketMgr,
			"%s - Unknown currency pair ID. Currency will appear as the pair ID: '%v'",
			e.Name,
			currencyID)
	}

	orderID, ok := notification[2].(float64)
	if !ok {
		return fmt.Errorf("%w order ID not float64", errTypeAssertionFailure)
	}

	side, ok := notification[3].(string)
	if !ok {
		return fmt.Errorf("%w order type not string", errTypeAssertionFailure)
	}
	orderSide := order.Buy
	if side == "0" {
		orderSide = order.Sell
	}

	rate, ok := notification[4].(string)
	if !ok {
		return fmt.Errorf("%w rate not string", errTypeAssertionFailure)
	}
	orderPrice, err := strconv.ParseFloat(rate, 64)
	if err != nil {
		return err
	}
	amount, ok := notification[5].(string)
	if !ok {
		return fmt.Errorf("%w amount not string", errTypeAssertionFailure)
	}
	orderAmount, err := strconv.ParseFloat(amount, 64)
	if err != nil {
		return err
	}

	ts, ok := notification[6].(string)
	if !ok {
		return fmt.Errorf("%w time not string", errTypeAssertionFailure)
	}

	var timeParse time.Time
	timeParse, err = time.Parse(time.DateTime, ts)
	if err != nil {
		return err
	}

	origAmount, ok := notification[7].(string)
	if !ok {
		return fmt.Errorf("%w original amount not string", errTypeAssertionFailure)
	}
	origOrderAmount, err := strconv.ParseFloat(origAmount, 64)
	if err != nil {
		return err
	}

	// null returned so ok check is not needed
	clientOrderID, _ := notification[8].(string)
	e.Websocket.DataHandler <- &order.Detail{
		Exchange:        e.Name,
		Price:           orderPrice,
		RemainingAmount: orderAmount,
		ExecutedAmount:  origOrderAmount - orderAmount,
		Amount:          origOrderAmount,
		OrderID:         strconv.FormatFloat(orderID, 'f', -1, 64),
		Type:            order.Limit,
		Side:            orderSide,
		Status:          order.New,
		AssetType:       asset.Spot,
		Date:            timeParse,
		Pair:            pair,
		ClientOrderID:   clientOrderID,
	}
	return nil
}

func (e *Exchange) processAccountBalanceUpdate(ctx context.Context, notification []any) error {
	if len(notification) < 4 {
		return errNotEnoughData
	}

	currencyID, ok := notification[1].(float64)
	if !ok {
		return fmt.Errorf("%w currency ID not float64", errTypeAssertionFailure)
	}
	curr, err := e.details.GetCode(currencyID)
	if err != nil {
		return err
	}

	walletType, ok := notification[2].(string)
	if !ok {
		return fmt.Errorf("%w wallet addr not string", errTypeAssertionFailure)
	}

	a, ok := notification[3].(string)
	if !ok {
		return fmt.Errorf("%w amount not string", errTypeAssertionFailure)
	}
	amount, err := strconv.ParseFloat(a, 64)
	if err != nil {
		return err
	}

	bal := accounts.Balance{
		Currency: curr,
		Total:    amount,
		Free:     amount,
	}

	id := deriveWalletType(walletType)
	subAccts := accounts.SubAccounts{accounts.NewSubAccount(asset.Spot, id)}
	subAccts[0].Balances.Set(curr, bal)

	if err := e.Accounts.Save(ctx, subAccts, true); err != nil {
		return err
	}
	e.Websocket.DataHandler <- subAccts
	return nil
}

func deriveWalletType(s string) string {
	switch s {
	case "e":
		return "exchange"
	case "m":
		return "margin"
	case "l":
		return "lending"
	default:
		return "unknown"
	}
}

func (e *Exchange) processAccountTrades(notification []any) error {
	if len(notification) < 11 {
		return errNotEnoughData
	}

	tradeID, ok := notification[1].(float64)
	if !ok {
		return fmt.Errorf("%w tradeID not float64", errTypeAssertionFailure)
	}

	r, ok := notification[2].(string)
	if !ok {
		return fmt.Errorf("%w rate not string", errTypeAssertionFailure)
	}
	rate, err := strconv.ParseFloat(r, 64)
	if err != nil {
		return err
	}

	a, ok := notification[3].(string)
	if !ok {
		return fmt.Errorf("%w amount not string", errTypeAssertionFailure)
	}
	amount, err := strconv.ParseFloat(a, 64)
	if err != nil {
		return err
	}

	// notification[4].(string) is the fee multiplier
	// notification[5].(string) is the funding type 0 (exchange wallet),
	// 1 (borrowed funds), 2 (margin funds), or 3 (lending funds)

	orderID, ok := notification[6].(float64)
	if !ok {
		return fmt.Errorf("%w orderID not float64", errTypeAssertionFailure)
	}

	fee, ok := notification[7].(string)
	if !ok {
		return fmt.Errorf("%w fee not string", errTypeAssertionFailure)
	}
	totalFee, err := strconv.ParseFloat(fee, 64)
	if err != nil {
		return err
	}

	t, ok := notification[8].(string)
	if !ok {
		return fmt.Errorf("%w time not string", errTypeAssertionFailure)
	}
	timeParse, err := time.Parse(time.DateTime, t)
	if err != nil {
		return err
	}

	// null returned so ok check is not needed
	clientOrderID, _ := notification[9].(string)

	tt, ok := notification[10].(string)
	if !ok {
		return fmt.Errorf("%w time not string", errTypeAssertionFailure)
	}
	tradeTotal, err := strconv.ParseFloat(tt, 64)
	if err != nil {
		return err
	}

	e.Websocket.DataHandler <- &order.Detail{
		Exchange: e.Name,
		OrderID:  strconv.FormatFloat(orderID, 'f', -1, 64),
		Fee:      totalFee,
		Trades: []order.TradeHistory{{
			Price:     rate,
			Amount:    amount,
			Fee:       totalFee,
			Exchange:  e.Name,
			TID:       strconv.FormatFloat(tradeID, 'f', -1, 64),
			Timestamp: timeParse,
			Total:     tradeTotal,
		}},
		AssetType:     asset.Spot,
		ClientOrderID: clientOrderID,
	}
	return nil
}

func (e *Exchange) processAccountKilledOrder(notification []any) error {
	if len(notification) < 3 {
		return errNotEnoughData
	}

	orderID, ok := notification[1].(float64)
	if !ok {
		return fmt.Errorf("%w order ID not float64", errTypeAssertionFailure)
	}

	// null returned so ok check is not needed
	clientOrderID, _ := notification[2].(string)

	e.Websocket.DataHandler <- &order.Detail{
		Exchange:      e.Name,
		OrderID:       strconv.FormatFloat(orderID, 'f', -1, 64),
		Status:        order.Cancelled,
		AssetType:     asset.Spot,
		ClientOrderID: clientOrderID,
	}
	return nil
}

func (e *Exchange) processTrades(currencyID float64, subData []any) error {
	if !e.IsSaveTradeDataEnabled() {
		return nil
	}
	pair, err := e.details.GetPair(currencyID)
	if err != nil {
		return err
	}

	if len(subData) != 6 {
		return errNotEnoughData
	}

	var tradeID string
	switch tradeIDData := subData[1].(type) { // tradeID type intermittently changes
	case string:
		tradeID = tradeIDData
	case float64:
		tradeID = strconv.FormatFloat(tradeIDData, 'f', -1, 64)
	default:
		return fmt.Errorf("unhandled type for websocket trade update: %v",
			tradeIDData)
	}

	orderSide, ok := subData[2].(float64)
	if !ok {
		return fmt.Errorf("%w order side not float64",
			errTypeAssertionFailure)
	}

	side := order.Buy
	if orderSide != 1 {
		side = order.Sell
	}

	v, ok := subData[3].(string)
	if !ok {
		return fmt.Errorf("%w volume not string",
			errTypeAssertionFailure)
	}
	volume, err := strconv.ParseFloat(v, 64)
	if err != nil {
		return err
	}
	rate, ok := subData[4].(string)
	if !ok {
		return fmt.Errorf("%w rate not string", errTypeAssertionFailure)
	}
	price, err := strconv.ParseFloat(rate, 64)
	if err != nil {
		return err
	}
	timestamp, ok := subData[5].(float64)
	if !ok {
		return fmt.Errorf("%w time not float64", errTypeAssertionFailure)
	}

	return e.AddTradesToBuffer(trade.Data{
		TID:          tradeID,
		Exchange:     e.Name,
		CurrencyPair: pair,
		AssetType:    asset.Spot,
		Side:         side,
		Price:        price,
		Amount:       volume,
		Timestamp:    time.Unix(int64(timestamp), 0),
	})
}
