package bybit

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/common/crypto"
	exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
	"github.com/thrasher-corp/gocryptotrader/exchanges/order"
	"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
	"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)

// Bybit is the overarching type across this package
type Bybit struct {
	exchange.Base
}

const (
	bybitAPIURL       = "https://api.bybit.com"
	defaultRecvWindow = "5000" // 5000 milli second

	sideBuy  = "Buy"
	sideSell = "Sell"

	// Public endpoints
	bybitSpotGetSymbols   = "/spot/v1/symbols"
	bybitOrderBook        = "/spot/quote/v1/depth"
	bybitMergedOrderBook  = "/spot/quote/v1/depth/merged"
	bybitRecentTrades     = "/spot/quote/v1/trades"
	bybitCandlestickChart = "/spot/quote/v1/kline"
	bybit24HrsChange      = "/spot/quote/v1/ticker/24hr"
	bybitLastTradedPrice  = "/spot/quote/v1/ticker/price"
	bybitBestBidAskPrice  = "/spot/quote/v1/ticker/book_ticker"

	// Authenticated endpoints
	bybitSpotOrder                = "/spot/v1/order" // create, query, cancel
	bybitFastCancelSpotOrder      = "/spot/v1/order/fast"
	bybitBatchCancelSpotOrder     = "/spot/order/batch-cancel"
	bybitFastBatchCancelSpotOrder = "/spot/order/batch-fast-cancel"
	bybitOpenOrder                = "/spot/v1/open-orders"
	bybitPastOrder                = "/spot/v1/history-orders"
	bybitTradeHistory             = "/spot/v1/myTrades"
	bybitWalletBalance            = "/spot/v1/account"
	bybitServerTime               = "/spot/v1/time"

	// Account asset endpoint
	bybitGetDepositAddress = "/asset/v1/private/deposit/address"
	bybitWithdrawFund      = "/asset/v1/private/withdraw"
)

// GetAllSpotPairs gets all pairs on the exchange
func (by *Bybit) GetAllSpotPairs(ctx context.Context) ([]PairData, error) {
	resp := struct {
		Data []PairData `json:"result"`
		Error
	}{}
	return resp.Data, by.SendHTTPRequest(ctx, exchange.RestSpot, bybitSpotGetSymbols, publicSpotRate, &resp)
}

func processOB(ob [][2]string) ([]orderbook.Item, error) {
	o := make([]orderbook.Item, len(ob))
	for x := range ob {
		var price, amount float64
		amount, err := strconv.ParseFloat(ob[x][1], 64)
		if err != nil {
			return nil, err
		}
		price, err = strconv.ParseFloat(ob[x][0], 64)
		if err != nil {
			return nil, err
		}
		o[x] = orderbook.Item{
			Price:  price,
			Amount: amount,
		}
	}
	return o, nil
}

func constructOrderbook(o *orderbookResponse) (*Orderbook, error) {
	var (
		s   Orderbook
		err error
	)
	s.Bids, err = processOB(o.Data.Bids)
	if err != nil {
		return nil, err
	}
	s.Asks, err = processOB(o.Data.Asks)
	if err != nil {
		return nil, err
	}
	s.Time = o.Data.Time.Time()
	return &s, err
}

// GetOrderBook gets orderbook for a given market with a given depth (default depth 100)
func (by *Bybit) GetOrderBook(ctx context.Context, symbol string, depth int64) (*Orderbook, error) {
	var o orderbookResponse
	strDepth := "100" // default depth
	if depth > 0 && depth < 100 {
		strDepth = strconv.FormatInt(depth, 10)
	}

	params := url.Values{}
	params.Set("symbol", symbol)
	params.Set("limit", strDepth)
	path := common.EncodeURLValues(bybitOrderBook, params)
	err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &o)
	if err != nil {
		return nil, err
	}

	return constructOrderbook(&o)
}

// GetMergedOrderBook gets orderbook for a given market with a given depth (default depth 100)
func (by *Bybit) GetMergedOrderBook(ctx context.Context, symbol string, scale, depth int64) (*Orderbook, error) {
	var o orderbookResponse
	params := url.Values{}
	if scale > 0 {
		params.Set("scale", strconv.FormatInt(scale, 10))
	}

	strDepth := "100" // default depth
	if depth > 0 && depth <= 200 {
		strDepth = strconv.FormatInt(depth, 10)
	}

	params.Set("symbol", symbol)
	params.Set("limit", strDepth)
	path := common.EncodeURLValues(bybitMergedOrderBook, params)
	err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &o)
	if err != nil {
		return nil, err
	}

	return constructOrderbook(&o)
}

// GetTrades gets recent trades from the exchange
func (by *Bybit) GetTrades(ctx context.Context, symbol string, limit int64) ([]TradeItem, error) {
	resp := struct {
		Data []struct {
			Price        float64           `json:"price,string"`
			Time         bybitTimeMilliSec `json:"time"`
			Quantity     float64           `json:"qty,string"`
			IsBuyerMaker bool              `json:"isBuyerMaker"`
		} `json:"result"`
		Error
	}{}

	params := url.Values{}
	params.Set("symbol", symbol)

	strLimit := "60" // default limit
	if limit > 0 && limit < 60 {
		strLimit = strconv.FormatInt(limit, 10)
	}
	params.Set("limit", strLimit)
	path := common.EncodeURLValues(bybitRecentTrades, params)
	err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &resp)
	if err != nil {
		return nil, err
	}

	trades := make([]TradeItem, len(resp.Data))
	for x := range resp.Data {
		var tradeSide string
		if resp.Data[x].IsBuyerMaker {
			tradeSide = order.Buy.String()
		} else {
			tradeSide = order.Sell.String()
		}

		trades[x] = TradeItem{
			CurrencyPair: symbol,
			Price:        resp.Data[x].Price,
			Side:         tradeSide,
			Volume:       resp.Data[x].Quantity,
			Time:         resp.Data[x].Time.Time(),
		}
	}
	return trades, nil
}

// GetKlines data returns the kline data for a specific symbol. Limitation: It only returns latest 3500 candles irrespective of interval passed
func (by *Bybit) GetKlines(ctx context.Context, symbol, period string, limit int64, start, end time.Time) ([]KlineItem, error) {
	resp := struct {
		Data [][]interface{} `json:"result"`
		Error
	}{}

	v := url.Values{}
	v.Add("symbol", symbol)
	v.Add("interval", period)
	if !start.IsZero() {
		v.Add("startTime", strconv.FormatInt(start.UnixMilli(), 10))
	}
	if !end.IsZero() {
		v.Add("endTime", strconv.FormatInt(end.UnixMilli(), 10))
	}
	if limit <= 0 || limit > 1000 {
		limit = 1000
	}
	v.Add("limit", strconv.FormatInt(limit, 10))

	path := common.EncodeURLValues(bybitCandlestickChart, v)
	if err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &resp); err != nil {
		return nil, err
	}

	klines := make([]KlineItem, len(resp.Data))
	for x := range resp.Data {
		if len(resp.Data[x]) != 11 {
			return nil, fmt.Errorf("%v GetKlines: invalid response, array length not as expected, check api docs for updates", by.Name)
		}
		var err error
		startTime, ok := resp.Data[x][0].(float64)
		if !ok {
			return nil, fmt.Errorf("%v GetKlines: %w for StartTime", by.Name, errTypeAssert)
		}
		klines[x].StartTime = time.UnixMilli(int64(startTime))

		open, ok := resp.Data[x][1].(string)
		if !ok {
			return nil, fmt.Errorf("%v GetKlines: %w for Open", by.Name, errTypeAssert)
		}
		klines[x].Open, err = strconv.ParseFloat(open, 64)
		if err != nil {
			return nil, fmt.Errorf("%v GetKlines: %w for Open", by.Name, errStrParsing)
		}

		high, ok := resp.Data[x][2].(string)
		if !ok {
			return nil, fmt.Errorf("%v GetKlines: %w for High", by.Name, errTypeAssert)
		}
		klines[x].High, err = strconv.ParseFloat(high, 64)
		if err != nil {
			return nil, fmt.Errorf("%v GetKlines: %w for High", by.Name, errStrParsing)
		}

		low, ok := resp.Data[x][3].(string)
		if !ok {
			return nil, fmt.Errorf("%v GetKlines: %w for Low", by.Name, errTypeAssert)
		}
		klines[x].Low, err = strconv.ParseFloat(low, 64)
		if err != nil {
			return nil, fmt.Errorf("%v GetKlines: %w for Low", by.Name, errStrParsing)
		}

		c, ok := resp.Data[x][4].(string)
		if !ok {
			return nil, fmt.Errorf("%v GetKlines: %w for Close", by.Name, errTypeAssert)
		}
		klines[x].Close, err = strconv.ParseFloat(c, 64)
		if err != nil {
			return nil, fmt.Errorf("%v GetKlines: %w for Close", by.Name, errStrParsing)
		}

		volume, ok := resp.Data[x][5].(string)
		if !ok {
			return nil, fmt.Errorf("%v GetKlines: %w for Volume", by.Name, errTypeAssert)
		}
		klines[x].Volume, err = strconv.ParseFloat(volume, 64)
		if err != nil {
			return nil, fmt.Errorf("%v GetKlines: %w for Volume", by.Name, errStrParsing)
		}

		endTime, ok := resp.Data[x][6].(float64)
		if !ok {
			return nil, fmt.Errorf("%v GetKlines: %w for EndTime", by.Name, errTypeAssert)
		}
		klines[x].EndTime = time.UnixMilli(int64(endTime))
		quoteAssetVolume, ok := resp.Data[x][7].(string)
		if !ok {
			return nil, fmt.Errorf("%v GetKlines: %w for QuoteAssetVolume", by.Name, errTypeAssert)
		}
		klines[x].QuoteAssetVolume, err = strconv.ParseFloat(quoteAssetVolume, 64)
		if err != nil {
			return nil, fmt.Errorf("%v GetKlines: %w for QuoteAssetVolume", by.Name, errStrParsing)
		}

		tradesCount, ok := resp.Data[x][8].(float64)
		if !ok {
			return nil, fmt.Errorf("%v GetKlines: %w for TradesCount", by.Name, errTypeAssert)
		}
		klines[x].TradesCount = int64(tradesCount)

		klines[x].TakerBaseVolume, ok = resp.Data[x][9].(float64)
		if !ok {
			return nil, fmt.Errorf("%v GetKlines: %w for TakerBaseVolume", by.Name, errTypeAssert)
		}

		klines[x].TakerQuoteVolume, ok = resp.Data[x][10].(float64)
		if !ok {
			return nil, fmt.Errorf("%v GetKlines: %w for TakerQuoteVolume", by.Name, errTypeAssert)
		}
	}
	return klines, nil
}

// Get24HrsChange returns price change statistics for the last 24 hours
// If symbol not passed then it will return price change statistics for all pairs
func (by *Bybit) Get24HrsChange(ctx context.Context, symbol string) ([]PriceChangeStats, error) {
	type priceChangeStats struct {
		Time         bybitTimeMilliSec `json:"time"`
		Symbol       string            `json:"symbol"`
		BestBidPrice float64           `json:"bestBidPrice,string"`
		BestAskPrice float64           `json:"bestAskPrice,string"`
		LastPrice    float64           `json:"lastPrice,string"`
		OpenPrice    float64           `json:"openPrice,string"`
		HighPrice    float64           `json:"highPrice,string"`
		LowPrice     float64           `json:"lowPrice,string"`
		Volume       float64           `json:"volume,string"`
		QuoteVolume  float64           `json:"quoteVolume,string"`
	}

	var stats []PriceChangeStats
	if symbol != "" {
		resp := struct {
			Data priceChangeStats `json:"result"`
			Error
		}{}

		params := url.Values{}
		params.Set("symbol", symbol)
		path := common.EncodeURLValues(bybit24HrsChange, params)
		err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &resp)
		if err != nil {
			return nil, err
		}

		stats = append(stats, PriceChangeStats{
			resp.Data.Time.Time(),
			resp.Data.Symbol,
			resp.Data.BestAskPrice,
			resp.Data.BestAskPrice,
			resp.Data.LastPrice,
			resp.Data.OpenPrice,
			resp.Data.HighPrice,
			resp.Data.LowPrice,
			resp.Data.Volume,
			resp.Data.QuoteVolume,
		})
	} else {
		resp := struct {
			Data []priceChangeStats `json:"result"`
			Error
		}{}

		err := by.SendHTTPRequest(ctx, exchange.RestSpot, bybit24HrsChange, publicSpotRate, &resp)
		if err != nil {
			return nil, err
		}

		for x := range resp.Data {
			stats = append(stats, PriceChangeStats{
				resp.Data[x].Time.Time(),
				resp.Data[x].Symbol,
				resp.Data[x].BestAskPrice,
				resp.Data[x].BestAskPrice,
				resp.Data[x].LastPrice,
				resp.Data[x].OpenPrice,
				resp.Data[x].HighPrice,
				resp.Data[x].LowPrice,
				resp.Data[x].Volume,
				resp.Data[x].QuoteVolume,
			})
		}
	}
	return stats, nil
}

// GetLastTradedPrice returns last trading price
// If symbol not passed then it will return last trading price for all pairs
func (by *Bybit) GetLastTradedPrice(ctx context.Context, symbol string) ([]LastTradePrice, error) {
	var lastTradePrices []LastTradePrice
	if symbol != "" {
		resp := struct {
			Data LastTradePrice `json:"result"`
			Error
		}{}

		params := url.Values{}
		params.Set("symbol", symbol)
		path := common.EncodeURLValues(bybitLastTradedPrice, params)
		err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &resp)
		if err != nil {
			return nil, err
		}
		lastTradePrices = append(lastTradePrices, LastTradePrice{
			resp.Data.Symbol,
			resp.Data.Price,
		})
	} else {
		resp := struct {
			Data []LastTradePrice `json:"result"`
			Error
		}{}

		err := by.SendHTTPRequest(ctx, exchange.RestSpot, bybitLastTradedPrice, publicSpotRate, &resp)
		if err != nil {
			return nil, err
		}
		for x := range resp.Data {
			lastTradePrices = append(lastTradePrices, LastTradePrice{
				resp.Data[x].Symbol,
				resp.Data[x].Price,
			})
		}
	}
	return lastTradePrices, nil
}

// GetBestBidAskPrice returns best BID and ASK price
// If symbol not passed then it will return best BID and ASK price for all pairs
func (by *Bybit) GetBestBidAskPrice(ctx context.Context, symbol string) ([]TickerData, error) {
	type bestTicker struct {
		Symbol      string            `json:"symbol"`
		BidPrice    float64           `json:"bidPrice,string"`
		BidQuantity float64           `json:"bidQty,string"`
		AskPrice    float64           `json:"askPrice,string"`
		AskQuantity float64           `json:"askQty,string"`
		Time        bybitTimeMilliSec `json:"time"`
	}

	var tickers []TickerData
	if symbol != "" {
		resp := struct {
			Data bestTicker `json:"result"`
			Error
		}{}

		params := url.Values{}
		params.Set("symbol", symbol)
		path := common.EncodeURLValues(bybitBestBidAskPrice, params)
		err := by.SendHTTPRequest(ctx, exchange.RestSpot, path, publicSpotRate, &resp)
		if err != nil {
			return nil, err
		}
		tickers = append(tickers, TickerData{
			resp.Data.Symbol,
			resp.Data.BidPrice,
			resp.Data.BidQuantity,
			resp.Data.AskPrice,
			resp.Data.AskQuantity,
			resp.Data.Time.Time(),
		})
	} else {
		resp := struct {
			Data []bestTicker `json:"result"`
			Error
		}{}

		err := by.SendHTTPRequest(ctx, exchange.RestSpot, bybitBestBidAskPrice, publicSpotRate, &resp)
		if err != nil {
			return nil, err
		}
		for x := range resp.Data {
			tickers = append(tickers, TickerData{
				resp.Data[x].Symbol,
				resp.Data[x].BidPrice,
				resp.Data[x].BidQuantity,
				resp.Data[x].AskPrice,
				resp.Data[x].AskQuantity,
				resp.Data[x].Time.Time(),
			})
		}
	}
	return tickers, nil
}

// CreatePostOrder create and post order
func (by *Bybit) CreatePostOrder(ctx context.Context, o *PlaceOrderRequest) (*PlaceOrderResponse, error) {
	if o == nil {
		return nil, errInvalidOrderRequest
	}

	params := url.Values{}
	params.Set("symbol", o.Symbol)
	params.Set("qty", strconv.FormatFloat(o.Quantity, 'f', -1, 64))
	params.Set("side", o.Side)
	params.Set("type", o.TradeType)

	if o.TimeInForce != "" {
		params.Set("timeInForce", o.TimeInForce)
	}
	if (o.TradeType == BybitRequestParamsOrderLimit || o.TradeType == BybitRequestParamsOrderLimitMaker) && o.Price == 0 {
		return nil, errMissingPrice
	}
	if o.Price != 0 {
		params.Set("price", strconv.FormatFloat(o.Price, 'f', -1, 64))
	}
	if o.OrderLinkID != "" {
		params.Set("orderLinkId", o.OrderLinkID)
	}

	resp := struct {
		Data PlaceOrderResponse `json:"result"`
		Error
	}{}
	return &resp.Data, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, bybitSpotOrder, params, nil, &resp, privateSpotRate)
}

// QueryOrder returns order data based upon orderID or orderLinkID
func (by *Bybit) QueryOrder(ctx context.Context, orderID, orderLinkID string) (*QueryOrderResponse, error) {
	if orderID == "" && orderLinkID == "" {
		return nil, errOrderOrOrderLinkIDMissing
	}

	params := url.Values{}
	if orderID != "" {
		params.Set("orderId", orderID)
	}
	if orderLinkID != "" {
		params.Set("orderLinkId", orderLinkID)
	}

	resp := struct {
		Data QueryOrderResponse `json:"result"`
		Error
	}{}
	return &resp.Data, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, bybitSpotOrder, params, nil, &resp, privateSpotRate)
}

// CancelExistingOrder cancels existing order based upon orderID or orderLinkID
func (by *Bybit) CancelExistingOrder(ctx context.Context, orderID, orderLinkID string) (*CancelOrderResponse, error) {
	if orderID == "" && orderLinkID == "" {
		return nil, errOrderOrOrderLinkIDMissing
	}

	params := url.Values{}
	if orderID != "" {
		params.Set("orderId", orderID)
	}
	if orderLinkID != "" {
		params.Set("orderLinkId", orderLinkID)
	}

	resp := struct {
		Data CancelOrderResponse `json:"result"`
		Error
	}{}
	err := by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, bybitSpotOrder, params, nil, &resp, privateSpotRate)
	if err != nil {
		return nil, err
	}

	// In case open order is cancelled, this endpoint return status as NEW whereas if we try to cancel a already cancelled order then it's status is returned as CANCELED without any error. So this check is added to prevent this obscurity.
	if resp.Data.Status == "CANCELED" {
		return nil, fmt.Errorf("%s order already cancelled", resp.Data.OrderID)
	}
	return &resp.Data, nil
}

// FastCancelExistingOrder cancels existing order based upon orderID or orderLinkID
func (by *Bybit) FastCancelExistingOrder(ctx context.Context, symbol, orderID, orderLinkID string) (bool, error) {
	resp := struct {
		Data struct {
			IsCancelled bool `json:"isCancelled"`
		} `json:"result"`
		Error
	}{}

	if orderID == "" && orderLinkID == "" {
		return resp.Data.IsCancelled, errOrderOrOrderLinkIDMissing
	}

	params := url.Values{}
	if symbol == "" {
		return resp.Data.IsCancelled, errSymbolMissing
	}
	params.Set("symbolId", symbol)

	if orderID != "" {
		params.Set("orderId", orderID)
	}
	if orderLinkID != "" {
		params.Set("orderLinkId", orderLinkID)
	}

	return resp.Data.IsCancelled, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, bybitFastCancelSpotOrder, params, nil, &resp, privateSpotRate)
}

// BatchCancelOrder cancels orders in batch based upon symbol, side or orderType
func (by *Bybit) BatchCancelOrder(ctx context.Context, symbol, side, orderTypes string) (bool, error) {
	params := url.Values{}
	if symbol != "" {
		params.Set("symbol", symbol)
	}
	if side != "" {
		params.Set("side", side)
	}
	if orderTypes != "" {
		params.Set("orderTypes", orderTypes)
	}

	resp := struct {
		Result struct {
			Success bool `json:"success"`
		} `json:"result"`
		Error
	}{}
	return resp.Result.Success, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, bybitBatchCancelSpotOrder, params, nil, &resp, privateSpotRate)
}

// BatchFastCancelOrder cancels orders in batch based upon symbol, side or orderType
func (by *Bybit) BatchFastCancelOrder(ctx context.Context, symbol, side, orderTypes string) (bool, error) {
	params := url.Values{}
	if symbol != "" {
		params.Set("symbol", symbol)
	}
	if side != "" {
		params.Set("side", side)
	}
	if orderTypes != "" {
		params.Set("orderTypes", orderTypes)
	}

	resp := struct {
		Result struct {
			Success bool `json:"success"`
		} `json:"result"`
		Error
	}{}
	return resp.Result.Success, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, bybitFastBatchCancelSpotOrder, params, nil, &resp, privateSpotRate)
}

// BatchCancelOrderByIDs cancels orders in batch based on comma separated order id's
func (by *Bybit) BatchCancelOrderByIDs(ctx context.Context, orderIDs []string) (bool, error) {
	params := url.Values{}
	if len(orderIDs) == 0 {
		return false, errEmptyOrderIDs
	}
	params.Set("orderIds", strings.Join(orderIDs, ","))

	resp := struct {
		Result struct {
			Success bool `json:"success"`
		} `json:"result"`
		Error
	}{}
	return resp.Result.Success, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, bybitFastBatchCancelSpotOrder, params, nil, &resp, privateSpotRate)
}

// ListOpenOrders returns all open orders
func (by *Bybit) ListOpenOrders(ctx context.Context, symbol, orderID string, limit int64) ([]QueryOrderResponse, error) {
	params := url.Values{}
	if symbol != "" {
		params.Set("symbol", symbol)
	}
	if orderID != "" {
		params.Set("orderId", orderID)
	}
	if limit != 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}

	resp := struct {
		Data []QueryOrderResponse `json:"result"`
		Error
	}{}
	return resp.Data, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, bybitOpenOrder, params, nil, &resp, privateSpotRate)
}

// GetPastOrders returns all past orders from history
func (by *Bybit) GetPastOrders(ctx context.Context, symbol, orderID string, limit int64, startTime, endTime time.Time) ([]QueryOrderResponse, error) {
	params := url.Values{}
	if symbol != "" {
		params.Set("symbol", symbol)
	}
	if orderID != "" {
		params.Set("orderId", orderID)
	}
	if limit != 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	if !startTime.IsZero() {
		params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
	}
	if !endTime.IsZero() {
		params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
	}
	resp := struct {
		Data []QueryOrderResponse `json:"result"`
		Error
	}{}
	return resp.Data, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, bybitPastOrder, params, nil, &resp, privateSpotRate)
}

// GetTradeHistory returns user trades
func (by *Bybit) GetTradeHistory(ctx context.Context, limit int64, symbol, fromID, toID, orderID string, startTime, endTime time.Time) ([]HistoricalTrade, error) {
	params := url.Values{}
	if symbol != "" {
		params.Set("symbol", symbol)
	}
	if limit != 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	if fromID != "" {
		params.Set("fromTicketId", fromID)
	}
	if toID != "" {
		params.Set("toTicketId", toID)
	}
	if orderID != "" {
		params.Set("orderId", orderID)
	}
	if !startTime.IsZero() {
		params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
	}
	if !endTime.IsZero() {
		params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
	}
	resp := struct {
		Data []HistoricalTrade `json:"result"`
		Error
	}{}
	return resp.Data, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, bybitTradeHistory, params, nil, &resp, privateSpotRate)
}

// GetWalletBalance returns user wallet balance
func (by *Bybit) GetWalletBalance(ctx context.Context) ([]Balance, error) {
	resp := struct {
		Data struct {
			Balances []Balance `json:"balances"`
		} `json:"result"`
		Error
	}{}
	return resp.Data.Balances, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, bybitWalletBalance, url.Values{}, nil, &resp, privateSpotRate)
}

// GetSpotServerTime returns server time
func (by *Bybit) GetSpotServerTime(ctx context.Context) (time.Time, error) {
	resp := struct {
		Result struct {
			ServerTime int64 `json:"serverTime"`
		} `json:"result"`
		Error
	}{}
	err := by.SendHTTPRequest(ctx, exchange.RestSpot, bybitServerTime, publicSpotRate, &resp)
	return time.UnixMilli(resp.Result.ServerTime), err
}

// GetDepositAddressForCurrency returns deposit wallet address based upon the coin.
func (by *Bybit) GetDepositAddressForCurrency(ctx context.Context, coin string) (DepositWalletInfo, error) {
	resp := struct {
		Result DepositWalletInfo `json:"result"`
		Error
	}{}

	params := url.Values{}
	if coin == "" {
		return resp.Result, errInvalidCoin
	}
	params.Set("coin", strings.ToUpper(coin))
	return resp.Result, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, bybitGetDepositAddress, params, nil, &resp, publicSpotRate)
}

// WithdrawFund creates request for fund withdrawal.
func (by *Bybit) WithdrawFund(ctx context.Context, coin, chain, address, tag, amount string) (string, error) {
	resp := struct {
		Data struct {
			ID string `json:"id"`
		} `json:"result"`
		Error
	}{}

	params := make(map[string]interface{})
	params["coin"] = coin
	params["chain"] = chain
	params["address"] = address
	params["amount"] = amount
	if tag != "" {
		params["tag"] = tag
	}
	return resp.Data.ID, by.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, bybitWithdrawFund, nil, params, &resp, privateSpotRate)
}

// SendHTTPRequest sends an unauthenticated request
func (by *Bybit) SendHTTPRequest(ctx context.Context, ePath exchange.URL, path string, f request.EndpointLimit, result UnmarshalTo) error {
	endpointPath, err := by.API.Endpoints.GetURL(ePath)
	if err != nil {
		return err
	}

	err = by.SendPayload(ctx, f, func() (*request.Item, error) {
		return &request.Item{
			Method:        http.MethodGet,
			Path:          endpointPath + path,
			Result:        result,
			Verbose:       by.Verbose,
			HTTPDebugging: by.HTTPDebugging,
			HTTPRecording: by.HTTPRecording}, nil
	})
	if err != nil {
		return err
	}
	return result.GetError()
}

// SendAuthHTTPRequest sends an authenticated HTTP request
// If payload is non-nil then request is considered to be JSON
func (by *Bybit) SendAuthHTTPRequest(ctx context.Context, ePath exchange.URL, method, path string, params url.Values, jsonPayload map[string]interface{}, result UnmarshalTo, f request.EndpointLimit) error {
	creds, err := by.GetCredentials(ctx)
	if err != nil {
		return err
	}

	if result == nil {
		result = &Error{}
	}

	endpointPath, err := by.API.Endpoints.GetURL(ePath)
	if err != nil {
		return err
	}

	if params == nil && jsonPayload == nil {
		params = url.Values{}
	}

	if jsonPayload != nil {
		jsonPayload["recvWindow"] = defaultRecvWindow
	} else if params.Get("recvWindow") == "" {
		params.Set("recvWindow", defaultRecvWindow)
	}

	err = by.SendPayload(ctx, f, func() (*request.Item, error) {
		var (
			payload       []byte
			hmacSignedStr string
		)
		headers := make(map[string]string)

		if jsonPayload != nil {
			headers["Content-Type"] = "application/json"
			jsonPayload["timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10)
			jsonPayload["api_key"] = creds.Key
			hmacSignedStr, err = getJSONRequestSignature(jsonPayload, creds.Secret)
			if err != nil {
				return nil, err
			}
			jsonPayload["sign"] = hmacSignedStr
			payload, err = json.Marshal(jsonPayload)
			if err != nil {
				return nil, err
			}
		} else {
			params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10))
			params.Set("api_key", creds.Key)
			hmacSignedStr, err = getSign(params.Encode(), creds.Secret)
			if err != nil {
				return nil, err
			}
			headers["Content-Type"] = "application/x-www-form-urlencoded"
			switch method {
			case http.MethodPost:
				params.Set("sign", hmacSignedStr)
				payload = []byte(params.Encode())
			default:
				path = common.EncodeURLValues(path, params)
				path += "&sign=" + hmacSignedStr
			}
		}

		return &request.Item{
			Method:        method,
			Path:          endpointPath + path,
			Headers:       headers,
			Body:          bytes.NewBuffer(payload),
			Result:        &result,
			AuthRequest:   true,
			Verbose:       by.Verbose,
			HTTPDebugging: by.HTTPDebugging,
			HTTPRecording: by.HTTPRecording}, nil
	})
	if err != nil {
		return err
	}
	return result.GetError()
}

// Error defines all error information for each request
type Error struct {
	ReturnCode int64  `json:"ret_code"`
	ReturnMsg  string `json:"ret_msg"`
	ExtCode    string `json:"ext_code"`
	ExtMsg     string `json:"ext_info"`
}

// GetError checks and returns an error if it is supplied.
func (e Error) GetError() error {
	if e.ReturnCode != 0 && e.ReturnMsg != "" {
		return errors.New(e.ReturnMsg)
	}
	if e.ExtCode != "" && e.ExtMsg != "" {
		return errors.New(e.ExtMsg)
	}
	return nil
}

func getSide(side string) order.Side {
	switch side {
	case sideBuy:
		return order.Buy
	case sideSell:
		return order.Sell
	default:
		return order.UnknownSide
	}
}

func getTradeType(tradeType string) order.Type {
	switch tradeType {
	case BybitRequestParamsOrderLimit:
		return order.Limit
	case BybitRequestParamsOrderMarket:
		return order.Market
	case BybitRequestParamsOrderLimitMaker:
		return order.Limit
	default:
		return order.UnknownType
	}
}

func getOrderStatus(status string) order.Status {
	switch status {
	case "NEW":
		return order.New
	case "PARTIALLY_FILLED":
		return order.PartiallyFilled
	case "FILLED":
		return order.Filled
	case "CANCELED":
		return order.Cancelled
	case "PENDING_CANCEL":
		return order.PendingCancel
	case "PENDING_NEW":
		return order.Pending
	case "REJECTED":
		return order.Rejected
	default:
		return order.UnknownStatus
	}
}

func getJSONRequestSignature(payload map[string]interface{}, secret string) (string, error) {
	payloadArr := make([]string, len(payload))
	var i int
	for p := range payload {
		payloadArr[i] = p
		i++
	}
	sort.Strings(payloadArr)
	var signStr string
	for _, key := range payloadArr {
		if value, found := payload[key]; found {
			if v, ok := value.(string); ok {
				signStr += key + "=" + v + "&"
			}
		} else {
			return "", errors.New("non-string payload parameter not expected")
		}
	}
	return getSign(signStr[:len(signStr)-1], secret)
}

func getSign(sign, secret string) (string, error) {
	hmacSigned, err := crypto.GetHMAC(crypto.HashSHA256, []byte(sign), []byte(secret))
	if err != nil {
		return "", err
	}
	return crypto.HexEncodeToString(hmacSigned), nil
}
