package bitmex

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

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

const (
	bitmexWSURL = "wss://www.bitmex.com/realtime"

	// Public Subscription Channels
	bitmexWSAnnouncement        = "announcement"
	bitmexWSChat                = "chat"
	bitmexWSConnected           = "connected"
	bitmexWSFunding             = "funding"
	bitmexWSInstrument          = "instrument"
	bitmexWSInsurance           = "insurance"
	bitmexWSLiquidation         = "liquidation"
	bitmexWSOrderbookL2         = "orderBookL2"
	bitmexWSOrderbookL225       = "orderBookL2_25"
	bitmexWSOrderbookL10        = "orderBook10"
	bitmexWSPublicNotifications = "publicNotifications"
	bitmexWSQuote               = "quote"
	bitmexWSQuote1m             = "quoteBin1m"
	bitmexWSQuote5m             = "quoteBin5m"
	bitmexWSQuote1h             = "quoteBin1h"
	bitmexWSQuote1d             = "quoteBin1d"
	bitmexWSSettlement          = "settlement"
	bitmexWSTrade               = "trade"
	bitmexWSTrade1m             = "tradeBin1m"
	bitmexWSTrade5m             = "tradeBin5m"
	bitmexWSTrade1h             = "tradeBin1h"
	bitmexWSTrade1d             = "tradeBin1d"

	// Authenticated Subscription Channels
	bitmexWSAffiliate            = "affiliate"
	bitmexWSExecution            = "execution"
	bitmexWSOrder                = "order"
	bitmexWSMargin               = "margin"
	bitmexWSPosition             = "position"
	bitmexWSPrivateNotifications = "privateNotifications"
	bitmexWSTransact             = "transact"
	bitmexWSWallet               = "wallet"

	bitmexActionInitialData = "partial"
	bitmexActionInsertData  = "insert"
	bitmexActionDeleteData  = "delete"
	bitmexActionUpdateData  = "update"
)

// WsConnect initiates a new websocket connection
func (b *Bitmex) WsConnect() error {
	if !b.Websocket.IsEnabled() || !b.IsEnabled() {
		return errors.New(stream.WebsocketNotEnabled)
	}
	var dialer websocket.Dialer
	err := b.Websocket.Conn.Dial(&dialer, http.Header{})
	if err != nil {
		return err
	}

	resp := b.Websocket.Conn.ReadMessage()
	if resp.Raw == nil {
		return errors.New("connection closed")
	}
	var welcomeResp WebsocketWelcome
	err = json.Unmarshal(resp.Raw, &welcomeResp)
	if err != nil {
		return err
	}

	if b.Verbose {
		log.Debugf(log.ExchangeSys,
			"Successfully connected to Bitmex %s at time: %s Limit: %d",
			welcomeResp.Info,
			welcomeResp.Timestamp,
			welcomeResp.Limit.Remaining)
	}

	go b.wsReadData()

	err = b.websocketSendAuth()
	if err != nil {
		log.Errorf(log.ExchangeSys,
			"%v - authentication failed: %v\n",
			b.Name,
			err)
	} else {
		authsubs, err := b.GenerateAuthenticatedSubscriptions()
		if err != nil {
			return err
		}
		return b.Websocket.SubscribeToChannels(authsubs)
	}
	return nil
}

// wsReadData receives and passes on websocket messages for processing
func (b *Bitmex) wsReadData() {
	b.Websocket.Wg.Add(1)
	defer b.Websocket.Wg.Done()

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

func (b *Bitmex) wsHandleData(respRaw []byte) error {
	quickCapture := make(map[string]interface{})
	err := json.Unmarshal(respRaw, &quickCapture)
	if err != nil {
		return err
	}

	var respError WebsocketErrorResponse
	if _, ok := quickCapture["status"]; ok {
		err = json.Unmarshal(respRaw, &respError)
		if err != nil {
			return err
		}
	}

	if _, ok := quickCapture["success"]; ok {
		var decodedResp WebsocketSubscribeResp
		err = json.Unmarshal(respRaw, &decodedResp)
		if err != nil {
			return err
		}

		if decodedResp.Success {
			if len(quickCapture) == 3 {
				if b.Verbose {
					log.Debugf(log.ExchangeSys, "%s websocket: Successfully subscribed to %s",
						b.Name, decodedResp.Subscribe)
				}
			} else {
				b.Websocket.SetCanUseAuthenticatedEndpoints(true)
				if b.Verbose {
					log.Debugf(log.ExchangeSys, "%s websocket: Successfully authenticated websocket connection",
						b.Name)
				}
			}
			return nil
		}

		b.Websocket.DataHandler <- fmt.Errorf("%s websocket error: Unable to subscribe %s",
			b.Name, decodedResp.Subscribe)
	} else if _, ok := quickCapture["table"]; ok {
		var decodedResp WebsocketMainResponse
		err = json.Unmarshal(respRaw, &decodedResp)
		if err != nil {
			return err
		}
		switch decodedResp.Table {
		case bitmexWSOrderbookL2, bitmexWSOrderbookL225, bitmexWSOrderbookL10:
			var orderbooks OrderBookData
			err = json.Unmarshal(respRaw, &orderbooks)
			if err != nil {
				return err
			}
			if len(orderbooks.Data) == 0 {
				return fmt.Errorf("%s - Empty orderbook data received: %s", b.Name, respRaw)
			}
			var p currency.Pair
			p, err = currency.NewPairFromString(orderbooks.Data[0].Symbol)
			if err != nil {
				return err
			}

			var a asset.Item
			a, err = b.GetPairAssetType(p)
			if err != nil {
				return err
			}

			err = b.processOrderbook(orderbooks.Data,
				orderbooks.Action,
				p,
				a)
			if err != nil {
				return err
			}

		case bitmexWSTrade:
			if !b.IsSaveTradeDataEnabled() {
				return nil
			}
			var tradeHolder TradeData
			err = json.Unmarshal(respRaw, &tradeHolder)
			if err != nil {
				return err
			}
			var trades []trade.Data
			for i := range tradeHolder.Data {
				if tradeHolder.Data[i].Price == 0 {
					// Please note that indices (symbols starting with .) post trades at intervals to the trade feed.
					// These have a size of 0 and are used only to indicate a changing price.
					continue
				}
				var p currency.Pair
				p, err = currency.NewPairFromString(tradeHolder.Data[i].Symbol)
				if err != nil {
					return err
				}

				var a asset.Item
				a, err = b.GetPairAssetType(p)
				if err != nil {
					return err
				}
				var oSide order.Side
				oSide, err = order.StringToOrderSide(tradeHolder.Data[i].Side)
				if err != nil {
					b.Websocket.DataHandler <- order.ClassificationError{
						Exchange: b.Name,
						Err:      err,
					}
				}

				trades = append(trades, trade.Data{
					TID:          tradeHolder.Data[i].TrdMatchID,
					Exchange:     b.Name,
					CurrencyPair: p,
					AssetType:    a,
					Side:         oSide,
					Price:        tradeHolder.Data[i].Price,
					Amount:       float64(tradeHolder.Data[i].Size),
					Timestamp:    tradeHolder.Data[i].Timestamp,
				})
			}
			return b.AddTradesToBuffer(trades...)
		case bitmexWSAnnouncement:
			var announcement AnnouncementData
			err = json.Unmarshal(respRaw, &announcement)
			if err != nil {
				return err
			}

			if announcement.Action == bitmexActionInitialData {
				return nil
			}

			b.Websocket.DataHandler <- announcement.Data
		case bitmexWSAffiliate:
			var response WsAffiliateResponse
			err = json.Unmarshal(respRaw, &response)
			if err != nil {
				return err
			}
			b.Websocket.DataHandler <- response
		case bitmexWSInstrument:
			// ticker
		case bitmexWSExecution:
			// trades of an order
			var response WsExecutionResponse
			err = json.Unmarshal(respRaw, &response)
			if err != nil {
				return err
			}

			for i := range response.Data {
				var p currency.Pair
				p, err = currency.NewPairFromString(response.Data[i].Symbol)
				if err != nil {
					return err
				}

				var a asset.Item
				a, err = b.GetPairAssetType(p)
				if err != nil {
					return err
				}
				var oStatus order.Status
				oStatus, err = order.StringToOrderStatus(response.Data[i].OrdStatus)
				if err != nil {
					b.Websocket.DataHandler <- order.ClassificationError{
						Exchange: b.Name,
						OrderID:  response.Data[i].OrderID,
						Err:      err,
					}
				}
				var oSide order.Side
				oSide, err = order.StringToOrderSide(response.Data[i].Side)
				if err != nil {
					b.Websocket.DataHandler <- order.ClassificationError{
						Exchange: b.Name,
						OrderID:  response.Data[i].OrderID,
						Err:      err,
					}
				}
				b.Websocket.DataHandler <- &order.Modify{
					Exchange:  b.Name,
					ID:        response.Data[i].OrderID,
					AccountID: strconv.FormatInt(response.Data[i].Account, 10),
					AssetType: a,
					Pair:      p,
					Status:    oStatus,
					Trades: []order.TradeHistory{
						{
							Price:     response.Data[i].Price,
							Amount:    response.Data[i].OrderQuantity,
							Exchange:  b.Name,
							TID:       response.Data[i].ExecID,
							Side:      oSide,
							Timestamp: response.Data[i].Timestamp,
							IsMaker:   false,
						},
					},
				}
			}
		case bitmexWSOrder:
			var response WsOrderResponse
			err = json.Unmarshal(respRaw, &response)
			if err != nil {
				return err
			}
			switch response.Action {
			case "update", "insert":
				for x := range response.Data {
					var p currency.Pair
					var a asset.Item
					p, a, err = b.GetRequestFormattedPairAndAssetType(response.Data[x].Symbol)
					if err != nil {
						return err
					}
					var oSide order.Side
					oSide, err = order.StringToOrderSide(response.Data[x].Side)
					if err != nil {
						b.Websocket.DataHandler <- order.ClassificationError{
							Exchange: b.Name,
							OrderID:  response.Data[x].OrderID,
							Err:      err,
						}
					}
					var oType order.Type
					oType, err = order.StringToOrderType(response.Data[x].OrderType)
					if err != nil {
						b.Websocket.DataHandler <- order.ClassificationError{
							Exchange: b.Name,
							OrderID:  response.Data[x].OrderID,
							Err:      err,
						}
					}
					var oStatus order.Status
					oStatus, err = order.StringToOrderStatus(response.Data[x].OrderStatus)
					if err != nil {
						b.Websocket.DataHandler <- order.ClassificationError{
							Exchange: b.Name,
							OrderID:  response.Data[x].OrderID,
							Err:      err,
						}
					}
					b.Websocket.DataHandler <- &order.Detail{
						Price:     response.Data[x].Price,
						Amount:    response.Data[x].OrderQuantity,
						Exchange:  b.Name,
						ID:        response.Data[x].OrderID,
						AccountID: strconv.FormatInt(response.Data[x].Account, 10),
						Type:      oType,
						Side:      oSide,
						Status:    oStatus,
						AssetType: a,
						Date:      response.Data[x].TransactTime,
						Pair:      p,
					}
				}
			case "delete":
				for x := range response.Data {
					var p currency.Pair
					var a asset.Item
					p, a, err = b.GetRequestFormattedPairAndAssetType(response.Data[x].Symbol)
					if err != nil {
						return err
					}
					var oSide order.Side
					oSide, err = order.StringToOrderSide(response.Data[x].Side)
					if err != nil {
						b.Websocket.DataHandler <- order.ClassificationError{
							Exchange: b.Name,
							OrderID:  response.Data[x].OrderID,
							Err:      err,
						}
					}
					var oType order.Type
					oType, err = order.StringToOrderType(response.Data[x].OrderType)
					if err != nil {
						b.Websocket.DataHandler <- order.ClassificationError{
							Exchange: b.Name,
							OrderID:  response.Data[x].OrderID,
							Err:      err,
						}
					}
					var oStatus order.Status
					oStatus, err = order.StringToOrderStatus(response.Data[x].OrderStatus)
					if err != nil {
						b.Websocket.DataHandler <- order.ClassificationError{
							Exchange: b.Name,
							OrderID:  response.Data[x].OrderID,
							Err:      err,
						}
					}
					b.Websocket.DataHandler <- &order.Modify{
						Price:     response.Data[x].Price,
						Amount:    response.Data[x].OrderQuantity,
						Exchange:  b.Name,
						ID:        response.Data[x].OrderID,
						AccountID: strconv.FormatInt(response.Data[x].Account, 10),
						Type:      oType,
						Side:      oSide,
						Status:    oStatus,
						AssetType: a,
						Date:      response.Data[x].TransactTime,
						Pair:      p,
					}
				}
			default:
				b.Websocket.DataHandler <- fmt.Errorf("%s - Unsupported order update %+v", b.Name, response)
			}
		case bitmexWSMargin:
			var response WsMarginResponse
			err = json.Unmarshal(respRaw, &response)
			if err != nil {
				return err
			}
			b.Websocket.DataHandler <- response
		case bitmexWSPosition:
			var response WsPositionResponse
			err = json.Unmarshal(respRaw, &response)
			if err != nil {
				return err
			}

		case bitmexWSPrivateNotifications:
			var response WsPrivateNotificationsResponse
			err = json.Unmarshal(respRaw, &response)
			if err != nil {
				return err
			}
			b.Websocket.DataHandler <- response
		case bitmexWSTransact:
			var response WsTransactResponse
			err = json.Unmarshal(respRaw, &response)
			if err != nil {
				return err
			}
			b.Websocket.DataHandler <- response
		case bitmexWSWallet:
			var response WsWalletResponse
			err = json.Unmarshal(respRaw, &response)
			if err != nil {
				return err
			}
			b.Websocket.DataHandler <- response
		default:
			b.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: b.Name + stream.UnhandledMessage + string(respRaw)}
			return nil
		}
	}
	return nil
}

// ProcessOrderbook processes orderbook updates
func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency.Pair, a asset.Item) error {
	if len(data) < 1 {
		return errors.New("no orderbook data")
	}

	switch action {
	case bitmexActionInitialData:
		var book orderbook.Base
		for i := range data {
			item := orderbook.Item{
				Price:  data[i].Price,
				Amount: float64(data[i].Size),
				ID:     data[i].ID,
			}
			switch {
			case strings.EqualFold(data[i].Side, order.Sell.String()):
				book.Asks = append(book.Asks, item)
			case strings.EqualFold(data[i].Side, order.Buy.String()):
				book.Bids = append(book.Bids, item)
			default:
				return fmt.Errorf("could not process websocket orderbook update, order side could not be matched for %s",
					data[i].Side)
			}
		}
		book.Asks.Reverse() // Reverse asks for correct alignment
		book.Asset = a
		book.Pair = p
		book.Exchange = b.Name
		book.VerifyOrderbook = b.CanVerifyOrderbook

		err := b.Websocket.Orderbook.LoadSnapshot(&book)
		if err != nil {
			return fmt.Errorf("process orderbook error -  %s",
				err)
		}
	default:
		var asks, bids []orderbook.Item
		for i := range data {
			nItem := orderbook.Item{
				Price:  data[i].Price,
				Amount: float64(data[i].Size),
				ID:     data[i].ID,
			}
			if strings.EqualFold(data[i].Side, "Sell") {
				asks = append(asks, nItem)
				continue
			}
			bids = append(bids, nItem)
		}

		err := b.Websocket.Orderbook.Update(&buffer.Update{
			Bids:   bids,
			Asks:   asks,
			Pair:   p,
			Asset:  a,
			Action: buffer.Action(action),
		})
		if err != nil {
			return err
		}
	}
	return nil
}

// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitmex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
	channels := []string{bitmexWSOrderbookL2, bitmexWSTrade}
	subscriptions := []stream.ChannelSubscription{
		{
			Channel: bitmexWSAnnouncement,
		},
	}

	assets := b.GetAssetTypes()
	for x := range assets {
		contracts, err := b.GetEnabledPairs(assets[x])
		if err != nil {
			return nil, err
		}
		for y := range contracts {
			for z := range channels {
				if assets[x] == asset.Index && channels[z] == bitmexWSOrderbookL2 {
					// There are no L2 orderbook for index assets
					continue
				}
				subscriptions = append(subscriptions, stream.ChannelSubscription{
					Channel:  channels[z] + ":" + contracts[y].String(),
					Currency: contracts[y],
					Asset:    assets[x],
				})
			}
		}
	}
	return subscriptions, nil
}

// GenerateAuthenticatedSubscriptions Adds authenticated subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitmex) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscription, error) {
	if !b.Websocket.CanUseAuthenticatedEndpoints() {
		return nil, nil
	}
	contracts, err := b.GetEnabledPairs(asset.PerpetualContract)
	if err != nil {
		return nil, err
	}
	channels := []string{bitmexWSExecution,
		bitmexWSPosition,
	}
	subscriptions := []stream.ChannelSubscription{
		{
			Channel: bitmexWSAffiliate,
		},
		{
			Channel: bitmexWSOrder,
		},
		{
			Channel: bitmexWSMargin,
		},
		{
			Channel: bitmexWSPrivateNotifications,
		},
		{
			Channel: bitmexWSTransact,
		},
		{
			Channel: bitmexWSWallet,
		},
	}
	for i := range channels {
		for j := range contracts {
			subscriptions = append(subscriptions, stream.ChannelSubscription{
				Channel:  channels[i] + ":" + contracts[j].String(),
				Currency: contracts[j],
				Asset:    asset.PerpetualContract,
			})
		}
	}
	return subscriptions, nil
}

// Subscribe subscribes to a websocket channel
func (b *Bitmex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
	var subscriber WebsocketRequest
	subscriber.Command = "subscribe"

	for i := range channelsToSubscribe {
		subscriber.Arguments = append(subscriber.Arguments,
			channelsToSubscribe[i].Channel)
	}
	err := b.Websocket.Conn.SendJSONMessage(subscriber)
	if err != nil {
		return err
	}
	b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe...)
	return nil
}

// Unsubscribe sends a websocket message to stop receiving data from the channel
func (b *Bitmex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
	var unsubscriber WebsocketRequest
	unsubscriber.Command = "unsubscribe"

	for i := range channelsToUnsubscribe {
		unsubscriber.Arguments = append(unsubscriber.Arguments,
			channelsToUnsubscribe[i].Channel)
	}
	err := b.Websocket.Conn.SendJSONMessage(unsubscriber)
	if err != nil {
		return err
	}
	b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...)
	return nil
}

// WebsocketSendAuth sends an authenticated subscription
func (b *Bitmex) websocketSendAuth() error {
	if !b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", b.Name)
	}
	b.Websocket.SetCanUseAuthenticatedEndpoints(true)
	timestamp := time.Now().Add(time.Hour * 1).Unix()
	newTimestamp := strconv.FormatInt(timestamp, 10)
	hmac := crypto.GetHMAC(crypto.HashSHA256,
		[]byte("GET/realtime"+newTimestamp),
		[]byte(b.API.Credentials.Secret))
	signature := crypto.HexEncodeToString(hmac)

	var sendAuth WebsocketRequest
	sendAuth.Command = "authKeyExpires"
	sendAuth.Arguments = append(sendAuth.Arguments, b.API.Credentials.Key, timestamp,
		signature)
	err := b.Websocket.Conn.SendJSONMessage(sendAuth)
	if err != nil {
		b.Websocket.SetCanUseAuthenticatedEndpoints(false)
		return err
	}
	return nil
}
