package bybit

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

	"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/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/ticker"
	"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
	"github.com/thrasher-corp/gocryptotrader/log"
)

const (
	bybitWSBaseURL      = "wss://stream.bybit.com/"
	wsSpotPublicTopicV2 = "spot/quote/ws/v2"
	wsSpotPrivate       = "spot/ws"
	bybitWebsocketTimer = 20 * time.Second
	wsOrderbook         = "depth"
	wsTicker            = "bookTicker"
	wsTrades            = "trade"
	wsKlines            = "kline"

	wsAccountInfo    = "outboundAccountInfo"
	wsOrderExecution = "executionReport"
	wsTickerInfo     = "ticketInfo"

	sub    = "sub"    // event for subscribe
	cancel = "cancel" // event for unsubscribe
)

var comms = make(chan stream.Response)

// WsConnect connects to a websocket feed
func (by *Bybit) WsConnect() error {
	if !by.Websocket.IsEnabled() || !by.IsEnabled() || !by.IsAssetWebsocketSupported(asset.Spot) {
		return errors.New(stream.WebsocketNotEnabled)
	}
	var dialer websocket.Dialer
	err := by.Websocket.Conn.Dial(&dialer, http.Header{})
	if err != nil {
		return err
	}
	by.Websocket.Conn.SetupPingHandler(stream.PingHandler{
		MessageType: websocket.TextMessage,
		Message:     []byte(`{"op":"ping"}`),
		Delay:       bybitWebsocketTimer,
	})

	by.Websocket.Wg.Add(1)
	go by.wsReadData(by.Websocket.Conn)
	if by.IsWebsocketAuthenticationSupported() {
		err = by.WsAuth(context.TODO())
		if err != nil {
			by.Websocket.DataHandler <- err
			by.Websocket.SetCanUseAuthenticatedEndpoints(false)
		}
	}

	by.Websocket.Wg.Add(1)
	go by.WsDataHandler()
	return nil
}

// WsAuth sends an authentication message to receive auth data
func (by *Bybit) WsAuth(ctx context.Context) error {
	var dialer websocket.Dialer
	err := by.Websocket.AuthConn.Dial(&dialer, http.Header{})
	if err != nil {
		return err
	}

	by.Websocket.AuthConn.SetupPingHandler(stream.PingHandler{
		MessageType: websocket.TextMessage,
		Message:     []byte(`{"op":"ping"}`),
		Delay:       bybitWebsocketTimer,
	})

	by.Websocket.Wg.Add(1)
	go by.wsReadData(by.Websocket.AuthConn)

	creds, err := by.GetCredentials(ctx)
	if err != nil {
		return err
	}
	intNonce := (time.Now().Unix() + 1) * 1000
	strNonce := strconv.FormatInt(intNonce, 10)
	hmac, err := crypto.GetHMAC(
		crypto.HashSHA256,
		[]byte("GET/realtime"+strNonce),
		[]byte(creds.Secret),
	)
	if err != nil {
		return err
	}
	sign := crypto.HexEncodeToString(hmac)
	req := Authenticate{
		Operation: "auth",
		Args:      []interface{}{creds.Key, intNonce, sign},
	}
	return by.Websocket.AuthConn.SendJSONMessage(req)
}

// Subscribe sends a websocket message to receive data from the channel
func (by *Bybit) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
	var errs error
	for i := range channelsToSubscribe {
		var subReq WsReq
		subReq.Topic = channelsToSubscribe[i].Channel
		subReq.Event = sub

		formattedPair, err := by.FormatExchangeCurrency(channelsToSubscribe[i].Currency, asset.Spot)
		if err != nil {
			errs = common.AppendError(errs, err)
			continue
		}
		if channelsToSubscribe[i].Channel == wsKlines {
			subReq.Parameters = WsParams{
				Symbol:    formattedPair.String(),
				IsBinary:  true,
				KlineType: "1m",
			}
		} else {
			subReq.Parameters = WsParams{
				Symbol:   formattedPair.String(),
				IsBinary: true,
			}
		}
		err = by.Websocket.Conn.SendJSONMessage(subReq)
		if err != nil {
			errs = common.AppendError(errs, err)
			continue
		}
		by.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
	}
	return errs
}

// Unsubscribe sends a websocket message to stop receiving data from the channel
func (by *Bybit) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
	var errs error

	for i := range channelsToUnsubscribe {
		var unSub WsReq
		unSub.Event = cancel
		unSub.Topic = channelsToUnsubscribe[i].Channel

		formattedPair, err := by.FormatExchangeCurrency(channelsToUnsubscribe[i].Currency, asset.Spot)
		if err != nil {
			errs = common.AppendError(errs, err)
			continue
		}
		unSub.Parameters = WsParams{
			Symbol: formattedPair.String(),
		}
		err = by.Websocket.Conn.SendJSONMessage(unSub)
		if err != nil {
			errs = common.AppendError(errs, err)
			continue
		}
		by.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i])
	}
	return errs
}

// wsReadData receives and passes on websocket messages for processing
func (by *Bybit) wsReadData(ws stream.Connection) {
	defer by.Websocket.Wg.Done()
	for {
		resp := ws.ReadMessage()
		if resp.Raw == nil {
			return
		}
		comms <- resp
	}
}

// GenerateDefaultSubscriptions generates default subscription
func (by *Bybit) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
	var subscriptions []stream.ChannelSubscription
	var channels = []string{wsTicker, wsTrades, wsOrderbook, wsKlines}
	pairs, err := by.GetEnabledPairs(asset.Spot)
	if err != nil {
		return nil, err
	}
	for z := range pairs {
		for x := range channels {
			subscriptions = append(subscriptions,
				stream.ChannelSubscription{
					Channel:  channels[x],
					Currency: pairs[z],
					Asset:    asset.Spot,
				})
		}
	}
	return subscriptions, nil
}

func stringToOrderStatus(status string) (order.Status, error) {
	switch status {
	case "NEW":
		return order.New, nil
	case "CANCELED":
		return order.Cancelled, nil
	case "REJECTED":
		return order.Rejected, nil
	case "TRADE":
		return order.PartiallyFilled, nil
	case "EXPIRED":
		return order.Expired, nil
	default:
		return order.UnknownStatus, errors.New(status + " not recognised as order status")
	}
}

// WsDataHandler handles data from wsReadData
func (by *Bybit) WsDataHandler() {
	defer by.Websocket.Wg.Done()
	for {
		select {
		case <-by.Websocket.ShutdownC:
			return
		case resp := <-comms:
			err := by.wsHandleData(resp.Raw)
			if err != nil {
				by.Websocket.DataHandler <- err
			}
		}
	}
}

func (by *Bybit) wsHandleData(respRaw []byte) error {
	var result interface{}
	err := json.Unmarshal(respRaw, &result)
	if err != nil {
		return err
	}
	switch d := result.(type) {
	case map[string]interface{}:
		if method, ok := d["event"].(string); ok {
			if strings.EqualFold(method, sub) {
				return nil
			}
			if strings.EqualFold(method, cancel) {
				return nil
			}
		}

		if t, ok := d["topic"].(string); ok {
			switch t {
			case wsOrderbook:
				var data WsOrderbook
				err := json.Unmarshal(respRaw, &data)
				if err != nil {
					return err
				}
				p, err := by.extractCurrencyPair(data.OBData.Symbol, asset.Spot)
				if err != nil {
					return err
				}

				err = by.wsUpdateOrderbook(&data.OBData, p, asset.Spot)
				if err != nil {
					return err
				}
				return nil
			case wsTrades:
				if !by.IsSaveTradeDataEnabled() {
					return nil
				}
				var data WsTrade
				err := json.Unmarshal(respRaw, &data)
				if err != nil {
					return err
				}

				p, err := by.extractCurrencyPair(data.Parameters.Symbol, asset.Spot)
				if err != nil {
					return err
				}

				side := order.Sell
				if data.TradeData.Side {
					side = order.Buy
				}

				return trade.AddTradesToBuffer(by.Name, trade.Data{
					Timestamp:    data.TradeData.Time.Time(),
					CurrencyPair: p,
					AssetType:    asset.Spot,
					Exchange:     by.Name,
					Price:        data.TradeData.Price,
					Amount:       data.TradeData.Size,
					Side:         side,
					TID:          data.TradeData.ID,
				})
			case wsTicker:
				var data WsSpotTicker
				err := json.Unmarshal(respRaw, &data)
				if err != nil {
					return err
				}

				p, err := by.extractCurrencyPair(data.Ticker.Symbol, asset.Spot)
				if err != nil {
					return err
				}

				by.Websocket.DataHandler <- &ticker.Price{
					ExchangeName: by.Name,
					Bid:          data.Ticker.Bid,
					Ask:          data.Ticker.Ask,
					LastUpdated:  data.Ticker.Time.Time(),
					AssetType:    asset.Spot,
					Pair:         p,
				}
				return nil
			case wsKlines:
				var data KlineStream
				err := json.Unmarshal(respRaw, &data)
				if err != nil {
					return err
				}

				p, err := by.extractCurrencyPair(data.Kline.Symbol, asset.Spot)
				if err != nil {
					return err
				}

				by.Websocket.DataHandler <- stream.KlineData{
					Pair:       p,
					AssetType:  asset.Spot,
					Exchange:   by.Name,
					StartTime:  data.Kline.StartTime.Time(),
					Interval:   data.Parameters.KlineType,
					OpenPrice:  data.Kline.OpenPrice,
					ClosePrice: data.Kline.ClosePrice,
					HighPrice:  data.Kline.HighPrice,
					LowPrice:   data.Kline.LowPrice,
					Volume:     data.Kline.Volume,
				}
				return nil
			default:
				by.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: by.Name + stream.UnhandledMessage + string(respRaw)}
			}
		}

		if m, ok := d["auth"]; ok {
			log.Infof(log.WebsocketMgr, "%v received auth response: %v", by.Name, m)
			return nil
		}

		if m, ok := d["pong"]; ok {
			log.Infof(log.WebsocketMgr, "%v received pong: %v", by.Name, m)
			return nil
		}
	case []interface{}:
		for i := range d {
			obj, ok := d[i].(map[string]interface{})
			if !ok {
				return common.GetAssertError("map[string]interface{}", d[i])
			}
			e, ok := obj["e"].(string)
			if !ok {
				return common.GetAssertError("string", obj["e"])
			}

			switch e {
			case wsAccountInfo:
				var data []wsAccount
				err := json.Unmarshal(respRaw, &data)
				if err != nil {
					return fmt.Errorf("%v - Could not convert to outboundAccountInfo structure %w",
						by.Name,
						err)
				}
				by.Websocket.DataHandler <- data
				return nil
			case wsOrderExecution:
				var data []wsOrderUpdate
				err := json.Unmarshal(respRaw, &data)
				if err != nil {
					return fmt.Errorf("%v - Could not convert to executionReport structure %w",
						by.Name,
						err)
				}

				for j := range data {
					oType, err := order.StringToOrderType(data[j].OrderType)
					if err != nil {
						by.Websocket.DataHandler <- order.ClassificationError{
							Exchange: by.Name,
							OrderID:  data[j].OrderID,
							Err:      err,
						}
					}
					var oSide order.Side
					oSide, err = order.StringToOrderSide(data[j].Side)
					if err != nil {
						by.Websocket.DataHandler <- order.ClassificationError{
							Exchange: by.Name,
							OrderID:  data[j].OrderID,
							Err:      err,
						}
					}
					var oStatus order.Status
					oStatus, err = stringToOrderStatus(data[j].OrderStatus)
					if err != nil {
						by.Websocket.DataHandler <- order.ClassificationError{
							Exchange: by.Name,
							OrderID:  data[j].OrderID,
							Err:      err,
						}
					}

					p, err := by.extractCurrencyPair(data[j].Symbol, asset.Spot)
					if err != nil {
						return err
					}

					by.Websocket.DataHandler <- order.Detail{
						Price:           data[j].Price,
						Amount:          data[j].Quantity,
						ExecutedAmount:  data[j].CumulativeFilledQuantity,
						RemainingAmount: data[j].Quantity - data[j].CumulativeFilledQuantity,
						Exchange:        by.Name,
						OrderID:         data[j].OrderID,
						Type:            oType,
						Side:            oSide,
						Status:          oStatus,
						AssetType:       asset.Spot,
						Date:            data[j].OrderCreationTime.Time(),
						Pair:            p,
						ClientOrderID:   data[j].ClientOrderID,
						Trades: []order.TradeHistory{
							{
								Price:     data[j].Price,
								Amount:    data[j].Quantity,
								Exchange:  by.Name,
								Timestamp: data[j].OrderCreationTime.Time(),
							},
						},
					}
				}
				return nil
			case wsTickerInfo:
				var data []wsOrderFilled
				err := json.Unmarshal(respRaw, &data)
				if err != nil {
					return fmt.Errorf("%v - Could not convert to ticketInfo structure %w",
						by.Name,
						err)
				}

				for j := range data {
					var oSide order.Side
					oSide, err = order.StringToOrderSide(data[j].Side)
					if err != nil {
						by.Websocket.DataHandler <- order.ClassificationError{
							Exchange: by.Name,
							OrderID:  data[j].OrderID,
							Err:      err,
						}
					}

					p, err := by.extractCurrencyPair(data[j].Symbol, asset.Spot)
					if err != nil {
						return err
					}

					by.Websocket.DataHandler <- &order.Detail{
						Exchange:  by.Name,
						OrderID:   data[j].OrderID,
						Side:      oSide,
						AssetType: asset.Spot,
						Pair:      p,
						Price:     data[j].Price,
						Amount:    data[j].Quantity,
						Date:      data[j].Timestamp.Time(),
						Trades: []order.TradeHistory{
							{
								Price:     data[j].Price,
								Amount:    data[j].Quantity,
								Exchange:  by.Name,
								Timestamp: data[j].Timestamp.Time(),
								TID:       data[j].TradeID,
								IsMaker:   data[j].IsMaker,
							},
						},
					}
				}
				return nil
			}
		}
	}

	return fmt.Errorf("unhandled stream data %s", string(respRaw))
}

func (by *Bybit) wsUpdateOrderbook(update *WsOrderbookData, p currency.Pair, assetType asset.Item) error {
	if update == nil || (len(update.Asks) == 0 && len(update.Bids) == 0) {
		return errors.New("no orderbook data")
	}
	asks := make([]orderbook.Item, len(update.Asks))
	for i := range update.Asks {
		target, err := strconv.ParseFloat(update.Asks[i][0], 64)
		if err != nil {
			return err
		}
		amount, err := strconv.ParseFloat(update.Asks[i][1], 64)
		if err != nil {
			return err
		}
		asks[i] = orderbook.Item{Price: target, Amount: amount}
	}
	bids := make([]orderbook.Item, len(update.Bids))
	for i := range update.Bids {
		target, err := strconv.ParseFloat(update.Bids[i][0], 64)
		if err != nil {
			return err
		}
		amount, err := strconv.ParseFloat(update.Bids[i][1], 64)
		if err != nil {
			return err
		}
		bids[i] = orderbook.Item{Price: target, Amount: amount}
	}
	return by.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{
		Bids:            bids,
		Asks:            asks,
		Pair:            p,
		LastUpdated:     update.Time.Time(),
		Asset:           assetType,
		Exchange:        by.Name,
		VerifyOrderbook: by.CanVerifyOrderbook,
	})
}
