package hitbtc

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

	"github.com/Masterminds/sprig/v3"
	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/websocket"
	"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
	"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
	"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 (
	hitbtcWebsocketAddress = "wss://api.hitbtc.com/api/2/ws"
	rpcVersion             = "2.0"
	errAuthFailed          = 1002
)

var subscriptionNames = map[string]string{
	subscription.TickerChannel:    "Ticker",
	subscription.OrderbookChannel: "Orderbook",
	subscription.CandlesChannel:   "Candles",
	subscription.AllTradesChannel: "Trades",
	subscription.MyAccountChannel: "Reports",
}

var defaultSubscriptions = subscription.List{
	{Enabled: true, Asset: asset.Spot, Channel: subscription.TickerChannel},
	{Enabled: true, Asset: asset.Spot, Channel: subscription.OrderbookChannel},
	{Enabled: true, Asset: asset.Spot, Channel: subscription.CandlesChannel, Interval: kline.ThirtyMin, Levels: 100},
	{Enabled: true, Asset: asset.Spot, Channel: subscription.AllTradesChannel, Levels: 100},
	{Enabled: true, Asset: asset.Spot, Channel: subscription.MyAccountChannel, Authenticated: true},
}

// WsConnect starts a new connection with the websocket API
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
	}

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

	if e.Websocket.CanUseAuthenticatedEndpoints() {
		err = e.wsLogin(ctx)
		if err != nil {
			log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", e.Name, err)
		}
	}

	return nil
}

// wsReadData receives and passes on websocket messages for processing
func (e *Exchange) wsReadData() {
	defer e.Websocket.Wg.Done()

	for {
		resp := e.Websocket.Conn.ReadMessage()
		if resp.Raw == nil {
			return
		}

		err := e.wsHandleData(resp.Raw)
		if err != nil {
			e.Websocket.DataHandler <- err
		}
	}
}

func (e *Exchange) wsGetTableName(respRaw []byte) (string, error) {
	var init capture
	err := json.Unmarshal(respRaw, &init)
	if err != nil {
		return "", err
	}
	if init.Error.Code == errAuthFailed {
		e.Websocket.SetCanUseAuthenticatedEndpoints(false)
	}
	if init.ID != "" {
		if e.Websocket.Match.IncomingWithData(init.ID, respRaw) {
			return "", nil
		}
	}
	if init.Error.Message != "" || init.Error.Code != 0 {
		return "", fmt.Errorf("code: %d, Message: %s",
			init.Error.Code,
			init.Error.Message)
	}
	if _, ok := init.Result.(bool); ok {
		return "", nil
	}
	if init.Method != "" {
		return init.Method, nil
	}
	switch resultType := init.Result.(type) {
	case map[string]any:
		if reportType, ok := resultType["reportType"].(string); ok {
			return reportType, nil
		}
		// check for ids - means it was a specific request
		// and can't go through normal processing
		if responseID, ok := resultType["id"].(string); ok {
			if responseID != "" {
				return "", nil
			}
		}
	case []any:
		if len(resultType) == 0 {
			e.Websocket.DataHandler <- fmt.Sprintf("No data returned. ID: %v", init.ID)
			return "", nil
		}

		data, ok := resultType[0].(map[string]any)
		if !ok {
			return "", errors.New("unable to type assert data")
		}
		if _, ok := data["clientOrderId"]; ok {
			return "order", nil
		} else if _, ok := data["available"]; ok {
			return "trading", nil
		}
	}
	e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{Message: e.Name + websocket.UnhandledMessage + string(respRaw)}
	return "", nil
}

func (e *Exchange) wsHandleData(respRaw []byte) error {
	name, err := e.wsGetTableName(respRaw)
	if err != nil {
		return err
	}
	switch name {
	case "":
		return nil
	case "ticker":
		var wsTicker WsTicker
		err := json.Unmarshal(respRaw, &wsTicker)
		if err != nil {
			return err
		}

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

		format, err := e.GetPairFormat(asset.Spot, true)
		if err != nil {
			return err
		}

		p, err := currency.NewPairFromFormattedPairs(wsTicker.Params.Symbol,
			pairs,
			format)
		if err != nil {
			return err
		}

		e.Websocket.DataHandler <- &ticker.Price{
			ExchangeName: e.Name,
			Open:         wsTicker.Params.Open,
			Volume:       wsTicker.Params.Volume,
			QuoteVolume:  wsTicker.Params.VolumeQuote,
			High:         wsTicker.Params.High,
			Low:          wsTicker.Params.Low,
			Bid:          wsTicker.Params.Bid,
			Ask:          wsTicker.Params.Ask,
			Last:         wsTicker.Params.Last,
			LastUpdated:  wsTicker.Params.Timestamp,
			AssetType:    asset.Spot,
			Pair:         p,
		}
	case "snapshotOrderbook":
		var obSnapshot WsOrderbook
		err := json.Unmarshal(respRaw, &obSnapshot)
		if err != nil {
			return err
		}
		err = e.WsProcessOrderbookSnapshot(&obSnapshot)
		if err != nil {
			return err
		}
	case "updateOrderbook":
		var obUpdate WsOrderbook
		err := json.Unmarshal(respRaw, &obUpdate)
		if err != nil {
			return err
		}
		err = e.WsProcessOrderbookUpdate(&obUpdate)
		if err != nil {
			return err
		}
	case "snapshotTrades", "updateTrades":
		if !e.IsSaveTradeDataEnabled() {
			return nil
		}
		var tradeSnapshot WsTrade
		err := json.Unmarshal(respRaw, &tradeSnapshot)
		if err != nil {
			return err
		}
		var trades []trade.Data
		p, err := currency.NewPairFromString(tradeSnapshot.Params.Symbol)
		if err != nil {
			return &order.ClassificationError{
				Exchange: e.Name,
				Err:      err,
			}
		}
		for i := range tradeSnapshot.Params.Data {
			side, err := order.StringToOrderSide(tradeSnapshot.Params.Data[i].Side)
			if err != nil {
				return &order.ClassificationError{
					Exchange: e.Name,
					Err:      err,
				}
			}
			trades = append(trades, trade.Data{
				Timestamp:    tradeSnapshot.Params.Data[i].Timestamp,
				Exchange:     e.Name,
				CurrencyPair: p,
				AssetType:    asset.Spot,
				Price:        tradeSnapshot.Params.Data[i].Price,
				Amount:       tradeSnapshot.Params.Data[i].Quantity,
				Side:         side,
				TID:          strconv.FormatInt(tradeSnapshot.Params.Data[i].ID, 10),
			})
		}
		return trade.AddTradesToBuffer(trades...)
	case "activeOrders":
		var o wsActiveOrdersResponse
		err := json.Unmarshal(respRaw, &o)
		if err != nil {
			return err
		}
		for i := range o.Params {
			err = e.wsHandleOrderData(&o.Params[i])
			if err != nil {
				return err
			}
		}
	case "trading":
		var trades WsGetTradingBalanceResponse
		err := json.Unmarshal(respRaw, &trades)
		if err != nil {
			return err
		}
		e.Websocket.DataHandler <- trades
	case "report":
		var o wsReportResponse
		err := json.Unmarshal(respRaw, &o)
		if err != nil {
			return err
		}
		err = e.wsHandleOrderData(&o.OrderData)
		if err != nil {
			return err
		}
	case "order":
		var o wsActiveOrderRequestResponse
		err := json.Unmarshal(respRaw, &o)
		if err != nil {
			return err
		}
		for i := range o.OrderData {
			err = e.wsHandleOrderData(&o.OrderData[i])
			if err != nil {
				return err
			}
		}
	case "replaced", "canceled", "new":
		var o wsOrderResponse
		err := json.Unmarshal(respRaw, &o)
		if err != nil {
			return err
		}
		err = e.wsHandleOrderData(&o.OrderData)
		if err != nil {
			return err
		}
	default:
		e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{Message: e.Name + websocket.UnhandledMessage + string(respRaw)}
		return nil
	}
	return nil
}

// WsProcessOrderbookSnapshot processes a full orderbook snapshot to a local cache
func (e *Exchange) WsProcessOrderbookSnapshot(ob *WsOrderbook) error {
	if len(ob.Params.Bid) == 0 || len(ob.Params.Ask) == 0 {
		return errors.New("no orderbooks to process")
	}

	newOrderBook := orderbook.Book{
		Bids: make(orderbook.Levels, len(ob.Params.Bid)),
		Asks: make(orderbook.Levels, len(ob.Params.Ask)),
	}
	for i := range ob.Params.Bid {
		newOrderBook.Bids[i] = orderbook.Level{
			Amount: ob.Params.Bid[i].Size,
			Price:  ob.Params.Bid[i].Price,
		}
	}
	for i := range ob.Params.Ask {
		newOrderBook.Asks[i] = orderbook.Level{
			Amount: ob.Params.Ask[i].Size,
			Price:  ob.Params.Ask[i].Price,
		}
	}

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

	format, err := e.GetPairFormat(asset.Spot, true)
	if err != nil {
		return err
	}

	p, err := currency.NewPairFromFormattedPairs(ob.Params.Symbol,
		pairs,
		format)
	if err != nil {
		e.Websocket.DataHandler <- err
		return err
	}

	newOrderBook.Asset = asset.Spot
	newOrderBook.Pair = p
	newOrderBook.Exchange = e.Name
	newOrderBook.ValidateOrderbook = e.ValidateOrderbook
	newOrderBook.LastUpdated = ob.Params.Timestamp

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

func (e *Exchange) wsHandleOrderData(o *wsOrderData) error {
	var trades []order.TradeHistory
	if o.TradeID > 0 {
		trades = append(trades, order.TradeHistory{
			Price:     o.TradePrice,
			Amount:    o.TradeQuantity,
			Fee:       o.TradeFee,
			Exchange:  e.Name,
			TID:       strconv.FormatFloat(o.TradeID, 'f', -1, 64),
			Timestamp: o.UpdatedAt,
		})
	}
	oType, err := order.StringToOrderType(o.Type)
	if err != nil {
		e.Websocket.DataHandler <- order.ClassificationError{
			Exchange: e.Name,
			OrderID:  o.ID,
			Err:      err,
		}
	}
	o.Status = strings.Replace(o.Status, "canceled", "cancelled", 1)
	oStatus, err := order.StringToOrderStatus(o.Status)
	if err != nil {
		e.Websocket.DataHandler <- order.ClassificationError{
			Exchange: e.Name,
			OrderID:  o.ID,
			Err:      err,
		}
	}
	oSide, err := order.StringToOrderSide(o.Side)
	if err != nil {
		e.Websocket.DataHandler <- order.ClassificationError{
			Exchange: e.Name,
			OrderID:  o.ID,
			Err:      err,
		}
	}

	p, err := currency.NewPairFromString(o.Symbol)
	if err != nil {
		e.Websocket.DataHandler <- order.ClassificationError{
			Exchange: e.Name,
			OrderID:  o.ID,
			Err:      err,
		}
	}

	var a asset.Item
	a, err = e.GetPairAssetType(p)
	if err != nil {
		return err
	}
	e.Websocket.DataHandler <- &order.Detail{
		Price:           o.Price,
		Amount:          o.Quantity,
		ExecutedAmount:  o.CumQuantity,
		RemainingAmount: o.Quantity - o.CumQuantity,
		Exchange:        e.Name,
		OrderID:         o.ID,
		Type:            oType,
		Side:            oSide,
		Status:          oStatus,
		AssetType:       a,
		Date:            o.CreatedAt,
		LastUpdated:     o.UpdatedAt,
		Pair:            p,
		Trades:          trades,
	}
	return nil
}

// WsProcessOrderbookUpdate updates a local cache
func (e *Exchange) WsProcessOrderbookUpdate(update *WsOrderbook) error {
	if len(update.Params.Bid) == 0 && len(update.Params.Ask) == 0 {
		// Periodically HitBTC sends empty updates which includes a sequence
		// can return this as nil.
		return nil
	}

	bids := make(orderbook.Levels, len(update.Params.Bid))
	for i := range update.Params.Bid {
		bids[i] = orderbook.Level{
			Price:  update.Params.Bid[i].Price,
			Amount: update.Params.Bid[i].Size,
		}
	}

	asks := make(orderbook.Levels, len(update.Params.Ask))
	for i := range update.Params.Ask {
		asks[i] = orderbook.Level{
			Price:  update.Params.Ask[i].Price,
			Amount: update.Params.Ask[i].Size,
		}
	}

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

	format, err := e.GetPairFormat(asset.Spot, true)
	if err != nil {
		return err
	}

	p, err := currency.NewPairFromFormattedPairs(update.Params.Symbol,
		pairs,
		format)
	if err != nil {
		return err
	}

	return e.Websocket.Orderbook.Update(&orderbook.Update{
		Asks:       asks,
		Bids:       bids,
		Pair:       p,
		UpdateID:   update.Params.Sequence,
		Asset:      asset.Spot,
		UpdateTime: update.Params.Timestamp,
	})
}

// generateSubscriptions returns a list of subscriptions from the configured subscriptions feature
func (e *Exchange) generateSubscriptions() (subscription.List, error) {
	return e.Features.Subscriptions.ExpandTemplates(e)
}

// GetSubscriptionTemplate returns a subscription channel template
func (e *Exchange) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) {
	return template.New("master.tmpl").Funcs(sprig.FuncMap()).Funcs(template.FuncMap{
		"subToReq":        subToReq,
		"isSymbolChannel": isSymbolChannel,
	}).Parse(subTplText)
}

const (
	subscribeOp   = "subscribe"
	unsubscribeOp = "unsubscribe"
)

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

// 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.ParallelChanOp(ctx, subs, func(ctx context.Context, subs subscription.List) error { return e.manageSubs(ctx, unsubscribeOp, subs) }, 1)
}

func (e *Exchange) manageSubs(ctx context.Context, op string, subs subscription.List) error {
	var errs error
	subs, errs = subs.ExpandTemplates(e)
	for _, s := range subs {
		r := WsRequest{
			JSONRPCVersion: rpcVersion,
			ID:             e.MessageID(),
		}
		if err := json.Unmarshal([]byte(s.QualifiedChannel), &r); err != nil {
			errs = common.AppendError(errs, err)
			continue
		}
		r.Method = op + r.Method
		err := e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, r) // v2 api does not return an ID with errors, so we don't use ReturnResponse
		if err == nil {
			if op == subscribeOp {
				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
}

// Unsubscribe sends a websocket message to stop receiving data from the channel
func (e *Exchange) wsLogin(ctx context.Context) error {
	if !e.IsWebsocketAuthenticationSupported() {
		return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", e.Name)
	}
	creds, err := e.GetCredentials(ctx)
	if err != nil {
		return err
	}
	e.Websocket.SetCanUseAuthenticatedEndpoints(true)
	n := strconv.FormatInt(time.Now().Unix(), 10)
	hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte(n), []byte(creds.Secret))
	if err != nil {
		return err
	}

	req := WsLoginRequest{
		Method: "login",
		Params: WsLoginData{
			Algo:      "HS256",
			PKey:      creds.Key,
			Nonce:     n,
			Signature: hex.EncodeToString(hmac),
		},
		ID: e.MessageID(),
	}

	err = e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, req)
	if err != nil {
		e.Websocket.SetCanUseAuthenticatedEndpoints(false)
		return err
	}

	return nil
}

// wsPlaceOrder sends a websocket message to submit an order
func (e *Exchange) wsPlaceOrder(ctx context.Context, pair currency.Pair, side string, price, quantity float64) (*WsSubmitOrderSuccessResponse, error) {
	if !e.Websocket.CanUseAuthenticatedEndpoints() {
		return nil, fmt.Errorf("%v not authenticated, cannot place order", e.Name)
	}

	id := e.MessageID()
	fPair, err := e.FormatExchangeCurrency(pair, asset.Spot)
	if err != nil {
		return nil, err
	}

	req := WsSubmitOrderRequest{
		Method: "newOrder",
		Params: WsSubmitOrderRequestData{
			ClientOrderID: id,
			Symbol:        fPair.String(),
			Side:          strings.ToLower(side),
			Price:         price,
			Quantity:      quantity,
		},
		ID: id,
	}
	resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, id, req)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	var response WsSubmitOrderSuccessResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	if response.Error.Code > 0 || response.Error.Message != "" {
		return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
	}
	return &response, nil
}

// wsCancelOrder sends a websocket message to cancel an order
func (e *Exchange) wsCancelOrder(ctx context.Context, clientOrderID string) (*WsCancelOrderResponse, error) {
	if !e.Websocket.CanUseAuthenticatedEndpoints() {
		return nil, fmt.Errorf("%v not authenticated, cannot place order", e.Name)
	}
	req := WsCancelOrderRequest{
		Method: "cancelOrder",
		Params: WsCancelOrderRequestData{
			ClientOrderID: clientOrderID,
		},
		ID: e.MessageID(),
	}
	resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	var response WsCancelOrderResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	if response.Error.Code > 0 || response.Error.Message != "" {
		return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
	}
	return &response, nil
}

// wsReplaceOrder sends a websocket message to replace an order
func (e *Exchange) wsReplaceOrder(ctx context.Context, clientOrderID string, quantity, price float64) (*WsReplaceOrderResponse, error) {
	if !e.Websocket.CanUseAuthenticatedEndpoints() {
		return nil, fmt.Errorf("%v not authenticated, cannot place order", e.Name)
	}
	req := WsReplaceOrderRequest{
		Method: "cancelReplaceOrder",
		Params: WsReplaceOrderRequestData{
			ClientOrderID:   clientOrderID,
			RequestClientID: strconv.FormatInt(time.Now().Unix(), 10),
			Quantity:        quantity,
			Price:           price,
		},
		ID: e.MessageID(),
	}
	resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	var response WsReplaceOrderResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	if response.Error.Code > 0 || response.Error.Message != "" {
		return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
	}
	return &response, nil
}

// wsGetActiveOrders sends a websocket message to get all active orders
func (e *Exchange) wsGetActiveOrders(ctx context.Context) (*wsActiveOrdersResponse, error) {
	if !e.Websocket.CanUseAuthenticatedEndpoints() {
		return nil, fmt.Errorf("%v not authenticated, cannot get active orders", e.Name)
	}
	req := WsReplaceOrderRequest{
		Method: "getOrders",
		Params: WsReplaceOrderRequestData{},
		ID:     e.MessageID(),
	}
	resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	var response wsActiveOrdersResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	if response.Error.Code > 0 || response.Error.Message != "" {
		return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
	}
	return &response, nil
}

// wsGetTradingBalance sends a websocket message to get trading balance
func (e *Exchange) wsGetTradingBalance(ctx context.Context) (*WsGetTradingBalanceResponse, error) {
	if !e.Websocket.CanUseAuthenticatedEndpoints() {
		return nil, fmt.Errorf("%v not authenticated, cannot place order", e.Name)
	}
	req := WsReplaceOrderRequest{
		Method: "getTradingBalance",
		Params: WsReplaceOrderRequestData{},
		ID:     e.MessageID(),
	}
	resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	var response WsGetTradingBalanceResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	if response.Error.Code > 0 || response.Error.Message != "" {
		return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
	}
	return &response, nil
}

// wsGetCurrencies sends a websocket message to get trading balance
func (e *Exchange) wsGetCurrencies(ctx context.Context, currencyItem currency.Code) (*WsGetCurrenciesResponse, error) {
	req := WsGetCurrenciesRequest{
		Method: "getCurrency",
		Params: WsGetCurrenciesRequestParameters{
			Currency: currencyItem,
		},
		ID: e.MessageID(),
	}
	resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	var response WsGetCurrenciesResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	if response.Error.Code > 0 || response.Error.Message != "" {
		return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
	}
	return &response, nil
}

// wsGetSymbols sends a websocket message to get trading balance
func (e *Exchange) wsGetSymbols(ctx context.Context, c currency.Pair) (*WsGetSymbolsResponse, error) {
	fPair, err := e.FormatExchangeCurrency(c, asset.Spot)
	if err != nil {
		return nil, err
	}

	req := WsGetSymbolsRequest{
		Method: "getSymbol",
		Params: WsGetSymbolsRequestParameters{
			Symbol: fPair.String(),
		},
		ID: e.MessageID(),
	}
	resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	var response WsGetSymbolsResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	if response.Error.Code > 0 || response.Error.Message != "" {
		return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
	}
	return &response, nil
}

// wsGetSymbols sends a websocket message to get trading balance
func (e *Exchange) wsGetTrades(ctx context.Context, c currency.Pair, limit int64, sort, by string) (*WsGetTradesResponse, error) {
	fPair, err := e.FormatExchangeCurrency(c, asset.Spot)
	if err != nil {
		return nil, err
	}

	req := WsGetTradesRequest{
		Method: "getTrades",
		Params: WsGetTradesRequestParameters{
			Symbol: fPair.String(),
			Limit:  limit,
			Sort:   sort,
			By:     by,
		},
		ID: e.MessageID(),
	}
	resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	var response WsGetTradesResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, fmt.Errorf("%v %v", e.Name, err)
	}
	if response.Error.Code > 0 || response.Error.Message != "" {
		return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
	}
	return &response, nil
}

// subToReq returns the subscription as a map to populate WsRequest
func subToReq(s *subscription.Subscription, maybePair ...currency.Pair) *WsRequest {
	name, ok := subscriptionNames[s.Channel]
	if !ok {
		panic(fmt.Errorf("%w: %s", subscription.ErrNotSupported, s.Channel))
	}
	r := &WsRequest{
		Method: name,
	}
	if len(maybePair) != 0 {
		r.Params = &WsParams{
			Symbol: maybePair[0].String(),
			Limit:  s.Levels,
		}
		if s.Interval != 0 {
			var err error
			if r.Params.Period, err = formatExchangeKlineInterval(s.Interval); err != nil {
				panic(err)
			}
		}
	} else if s.Levels != 0 {
		r.Params = &WsParams{
			Limit: s.Levels,
		}
	}
	return r
}

// isSymbolChannel returns if the channel expects receive a symbol
func isSymbolChannel(s *subscription.Subscription) bool {
	return s.Channel != subscription.MyAccountChannel
}

const subTplText = `
{{- if isSymbolChannel $.S }} 
	{{ range $asset, $pairs := $.AssetPairs }}
		{{- range $p := $pairs -}}
			{{- subToReq $.S $p | mustToJson }}
			{{ $.PairSeparator }}
		{{- end }}
		{{ $.AssetSeparator }}
	{{- end }}
{{- else }}
	{{- subToReq $.S | mustToJson }}
{{- end }}
`
