package bittrex

import (
	"bytes"
	"compress/flate"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strconv"
	"sync"
	"time"

	"github.com/gofrs/uuid"
	"github.com/gorilla/websocket"
	"github.com/thrasher-corp/gocryptotrader/common"
	"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/request"
	"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
	"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
	"github.com/thrasher-corp/gocryptotrader/log"
)

const (
	bittrexAPIWSURL             = "wss://socket-v3.bittrex.com/signalr"
	bittrexAPIWSNegotiationsURL = "https://socket-v3.bittrex.com/signalr"

	bittrexWebsocketTimer = 13 * time.Second
	wsTicker              = "ticker"
	wsOrderbook           = "orderbook"
	wsMarketSummary       = "market_summary"
	wsOrders              = "order"
	wsHeartbeat           = "heartbeat"
	authenticate          = "Authenticate"
	subscribe             = "subscribe"
	unsubscribe           = "unsubscribe"
	wsRateLimit           = 50
	wsMessageRateLimit    = 60
)

var defaultSpotSubscribedChannels = []string{
	// wsHeartbeat,
	wsOrderbook,
	wsTicker,
	wsMarketSummary,
}

var defaultSpotSubscribedChannelsAuth = []string{
	wsOrders,
}

type TickerCache struct {
	MarketSummaries map[string]*MarketSummaryData
	Tickers         map[string]*TickerData
	sync.RWMutex
}

// WsConnect connects to a websocket feed
func (b *Bittrex) WsConnect() error {
	if !b.Websocket.IsEnabled() || !b.IsEnabled() {
		return errors.New(stream.WebsocketNotEnabled)
	}

	var wsHandshakeData WsSignalRHandshakeData
	err := b.WsSignalRHandshake(&wsHandshakeData)
	if err != nil {
		return err
	}

	var dialer websocket.Dialer
	endpoint, err := b.API.Endpoints.GetURL(exchange.WebsocketSpot)
	if err != nil {
		return err
	}

	params := url.Values{}
	params.Set("clientProtocol", "1.5")
	params.Set("transport", "webSockets")
	params.Set("connectionToken", wsHandshakeData.ConnectionToken)
	params.Set("connectionData", "[{name:\"c3\"}]")
	params.Set("tid", "10")

	path := common.EncodeURLValues("/connect", params)

	err = b.Websocket.SetWebsocketURL(endpoint+path, false, false)
	if err != nil {
		return err
	}

	err = b.Websocket.Conn.Dial(&dialer, http.Header{})
	if err != nil {
		return err
	}
	// Can set up custom ping handler per websocket connection.
	b.Websocket.Conn.SetupPingHandler(stream.PingHandler{
		MessageType: websocket.PingMessage,
		Delay:       bittrexWebsocketTimer,
	})

	// This reader routine is called prior to initiating a subscription for
	// efficient processing.
	go b.wsReadData()
	b.setupOrderbookManager()
	b.tickerCache = &TickerCache{
		MarketSummaries: make(map[string]*MarketSummaryData),
		Tickers:         make(map[string]*TickerData),
	}

	if b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		err = b.WsAuth()
		if err != nil {
			b.Websocket.DataHandler <- err
			b.Websocket.SetCanUseAuthenticatedEndpoints(false)
		}
	}
	return nil
}

// WsSignalRHandshake requests the SignalR connection token over https
func (b *Bittrex) WsSignalRHandshake(result interface{}) error {
	endpoint, err := b.API.Endpoints.GetURL(exchange.WebsocketSpotSupplementary)
	if err != nil {
		return err
	}
	path := "/negotiate?connectionData=[{name:\"c3\"}]&clientProtocol=1.5"
	return b.SendPayload(context.Background(), &request.Item{
		Method:        http.MethodGet,
		Path:          endpoint + path,
		Result:        result,
		Verbose:       b.Verbose,
		HTTPDebugging: b.HTTPDebugging,
		HTTPRecording: b.HTTPRecording,
	})
}

// WsAuth sends an authentication message to receive auth data
// Authentications expire after 10 minutes
func (b *Bittrex) WsAuth() error {
	// [apiKey, timestamp in ms, random uuid, signed payload]
	apiKey := b.API.Credentials.Key
	randomContent, err := uuid.NewV4()
	if err != nil {
		return err
	}
	timestamp := strconv.FormatInt(time.Now().UnixNano()/1000000, 10)
	hmac := crypto.GetHMAC(
		crypto.HashSHA512,
		[]byte(timestamp+randomContent.String()),
		[]byte(b.API.Credentials.Secret),
	)
	signature := crypto.HexEncodeToString(hmac)

	req := WsEventRequest{
		Hub:          "c3",
		Method:       authenticate,
		InvocationID: b.Websocket.Conn.GenerateMessageID(false),
	}

	arguments := make([]string, 0)
	arguments = append(arguments, apiKey, timestamp, randomContent.String(), signature)
	req.Arguments = arguments

	requestString, err := json.Marshal(req)
	if err != nil {
		return err
	}
	if b.Verbose {
		log.Debugf(log.WebsocketMgr, "%s Sending JSON message - %s\n", b.Name, requestString)
	}

	respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req)
	if err != nil {
		return err
	}
	var response WsAuthResponse
	err = json.Unmarshal(respRaw, &response)
	if err != nil {
		log.Warnf(log.WebsocketMgr, "%s - Cannot unmarshal into WsAuthResponse (%s)\n", b.Name, string(respRaw))
		return err
	}
	if !response.Response.Success {
		log.Warnf(log.WebsocketMgr, "%s - Unable to authenticate (%s)", b.Name, response.Response.ErrorCode)
		b.Websocket.SetCanUseAuthenticatedEndpoints(false)
	}
	return nil
}

// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be
// handled by ManageSubscriptions()
func (b *Bittrex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
	var subscriptions []stream.ChannelSubscription
	pairs, err := b.GetEnabledPairs(asset.Spot)
	if err != nil {
		return nil, err
	}

	channels := defaultSpotSubscribedChannels
	if b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		channels = append(channels, defaultSpotSubscribedChannelsAuth...)
	}

	for i := range pairs {
		pair, err := b.FormatExchangeCurrency(pairs[i], asset.Spot)
		if err != nil {
			return nil, err
		}
		for y := range channels {
			var channel string
			switch channels[y] {
			case wsOrderbook:
				channel = channels[y] + "_" + pair.String() + "_" + strconv.FormatInt(orderbookDepth, 10)
			case wsTicker:
				channel = channels[y] + "_" + pair.String()
			case wsMarketSummary:
				channel = channels[y] + "_" + pair.String()
			default:
				channel = channels[y]
			}
			subscriptions = append(subscriptions,
				stream.ChannelSubscription{
					Channel:  channel,
					Currency: pair,
					Asset:    asset.Spot,
				})
		}
	}

	return subscriptions, nil
}

// Subscribe sends a websocket message to receive data from the channel
func (b *Bittrex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
	var x int
	var errs common.Errors
	for x = 0; x+wsMessageRateLimit < len(channelsToSubscribe); x += wsMessageRateLimit {
		err := b.subscribeSlice(channelsToSubscribe[x : x+wsMessageRateLimit])
		if err != nil {
			errs = append(errs, err)
		}
	}
	err := b.subscribeSlice(channelsToSubscribe[x:])
	if err != nil {
		errs = append(errs, err)
	}
	if errs != nil {
		return errs
	}
	return nil
}

func (b *Bittrex) subscribeSlice(channelsToSubscribe []stream.ChannelSubscription) error {
	req := WsEventRequest{
		Hub:          "c3",
		Method:       subscribe,
		InvocationID: b.Websocket.Conn.GenerateMessageID(false),
	}

	var channels []string
	for i := range channelsToSubscribe {
		channels = append(channels, channelsToSubscribe[i].Channel)
	}
	arguments := make([][]string, 0)
	arguments = append(arguments, channels)
	req.Arguments = arguments

	requestString, err := json.Marshal(req)
	if err != nil {
		return err
	}
	if b.Verbose {
		log.Debugf(log.WebsocketMgr, "%s - Sending JSON message - %s\n", b.Name, requestString)
	}
	respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req)
	if err != nil {
		return err
	}
	var response WsSubscriptionResponse
	err = json.Unmarshal(respRaw, &response)
	if err != nil {
		return err
	}
	var errs common.Errors
	for i := range response.Response {
		if !response.Response[i].Success {
			errs = append(errs, errors.New("unable to subscribe to "+channels[i]+" - error code "+response.Response[i].ErrorCode))
			continue
		}
		b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
	}
	if errs != nil {
		return errs
	}
	return nil
}

// Unsubscribe sends a websocket message to receive data from the channel
func (b *Bittrex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
	var x int
	var errs common.Errors
	for x = 0; x+wsMessageRateLimit < len(channelsToUnsubscribe); x += wsMessageRateLimit {
		err := b.unsubscribeSlice(channelsToUnsubscribe[x : x+wsMessageRateLimit])
		if err != nil {
			errs = append(errs, err)
		}
	}
	err := b.unsubscribeSlice(channelsToUnsubscribe[x:])
	if err != nil {
		errs = append(errs, err)
	}
	if errs != nil {
		return errs
	}
	return nil
}

func (b *Bittrex) unsubscribeSlice(channelsToUnsubscribe []stream.ChannelSubscription) error {
	req := WsEventRequest{
		Hub:          "c3",
		Method:       unsubscribe,
		InvocationID: b.Websocket.Conn.GenerateMessageID(false),
	}

	var channels []string
	for i := range channelsToUnsubscribe {
		channels = append(channels, channelsToUnsubscribe[i].Channel)
	}
	arguments := make([][]string, 0)
	arguments = append(arguments, channels)
	req.Arguments = arguments

	requestString, err := json.Marshal(req)
	if err != nil {
		return err
	}
	if b.Verbose {
		log.Debugf(log.WebsocketMgr, "%s - Sending JSON message - %s\n", b.Name, requestString)
	}
	respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req)
	if err != nil {
		return err
	}
	var response WsSubscriptionResponse
	err = json.Unmarshal(respRaw, &response)
	if err != nil {
		return err
	}
	var errs common.Errors
	for i := range response.Response {
		if !response.Response[i].Success {
			errs = append(errs, errors.New("unable to unsubscribe from "+channels[i]+" - error code "+response.Response[i].ErrorCode))
			continue
		}
		b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i])
	}
	if errs != nil {
		return errs
	}
	return nil
}

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

	for {
		select {
		case <-b.Websocket.ShutdownC:
			return
		default:
			resp := b.Websocket.Conn.ReadMessage()
			if resp.Raw == nil {
				log.Warnf(log.WebsocketMgr, "%s Received empty message\n", b.Name)
				return
			}

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

func (b *Bittrex) wsDecodeMessage(encodedMessage string, v interface{}) error {
	raw, err := crypto.Base64Decode(encodedMessage)
	if err != nil {
		return err
	}
	reader := flate.NewReader(bytes.NewBuffer(raw))
	message, err := ioutil.ReadAll(reader)
	if err != nil {
		return err
	}
	return json.Unmarshal(message, v)
}

func (b *Bittrex) wsHandleData(respRaw []byte) error {
	var response WsEventResponse
	err := json.Unmarshal(respRaw, &response)
	if err != nil {
		log.Warnf(log.WebsocketMgr, "%s Cannot unmarshal into eventResponse (%s)\n", b.Name, string(respRaw))
		return err
	}
	if response.Response != nil && response.InvocationID > 0 {
		if b.Websocket.Match.IncomingWithData(response.InvocationID, respRaw) {
			return nil
		}
		return errors.New("received response to unknown request")
	}

	if response.Response == nil && len(response.Message) == 0 && response.C == "" {
		if b.Verbose {
			log.Warnf(log.WebsocketMgr, "%s Received keep-alive (%s)\n", b.Name, string(respRaw))
		}
		return nil
	}
	for i := range response.Message {
		switch response.Message[i].Method {
		case "orderBook":
			for j := range response.Message[i].Arguments {
				var orderbookUpdate OrderbookUpdateMessage
				err = b.wsDecodeMessage(response.Message[i].Arguments[j], &orderbookUpdate)
				if err != nil {
					return err
				}
				var init bool
				init, err = b.UpdateLocalOBBuffer(&orderbookUpdate)
				if err != nil {
					if init {
						return nil
					}
					return fmt.Errorf("%v - UpdateLocalCache error: %s",
						b.Name,
						err)
				}
			}
		case "ticker":
			for j := range response.Message[i].Arguments {
				var tickerUpdate TickerData
				err = b.wsDecodeMessage(response.Message[i].Arguments[j], &tickerUpdate)
				if err != nil {
					return err
				}
				err = b.WsProcessUpdateTicker(tickerUpdate)
				if err != nil {
					return err
				}
			}
		case "marketSummary":
			for j := range response.Message[i].Arguments {
				var marketSummaryUpdate MarketSummaryData
				err = b.wsDecodeMessage(response.Message[i].Arguments[j], &marketSummaryUpdate)
				if err != nil {
					return err
				}

				err = b.WsProcessUpdateMarketSummary(&marketSummaryUpdate)
				if err != nil {
					return err
				}
			}
		case "heartbeat":
			if b.Verbose {
				log.Warnf(log.WebsocketMgr, "%s Received heartbeat\n", b.Name)
			}
		case "authenticationExpiring":
			if b.Verbose {
				log.Debugf(log.WebsocketMgr, "%s - Re-authenticating.\n", b.Name)
			}
			err = b.WsAuth()
			if err != nil {
				b.Websocket.DataHandler <- err
				b.Websocket.SetCanUseAuthenticatedEndpoints(false)
			}
		case "order":
			for j := range response.Message[i].Arguments {
				var orderUpdate OrderUpdateMessage
				err = b.wsDecodeMessage(response.Message[i].Arguments[j], &orderUpdate)
				if err != nil {
					return err
				}
				err = b.WsProcessUpdateOrder(&orderUpdate)
				if err != nil {
					return err
				}
			}
		}
	}
	return nil
}

// WsProcessUpdateTicker processes an update on the ticker
func (b *Bittrex) WsProcessUpdateTicker(tickerData TickerData) error {
	pair, err := currency.NewPairFromString(tickerData.Symbol)
	if err != nil {
		return err
	}

	tickerPrice, err := ticker.GetTicker(b.Name, pair, asset.Spot)
	if err != nil {
		b.tickerCache.Lock()
		defer b.tickerCache.Unlock()
		if b.tickerCache.MarketSummaries[tickerData.Symbol] != nil {
			marketSummaryData := b.tickerCache.MarketSummaries[tickerData.Symbol]
			tickerPrice = b.constructTicker(tickerData, marketSummaryData, pair, asset.Spot)
			b.Websocket.DataHandler <- tickerPrice
			return nil
		}
		b.tickerCache.Tickers[tickerData.Symbol] = &tickerData
		return nil
	}

	tickerPrice.Last = tickerData.LastTradeRate
	tickerPrice.Bid = tickerData.BidRate
	tickerPrice.Ask = tickerData.AskRate

	b.Websocket.DataHandler <- tickerPrice

	return nil
}

// WsProcessUpdateMarketSummary processes an update on the ticker
func (b *Bittrex) WsProcessUpdateMarketSummary(marketSummaryData *MarketSummaryData) error {
	pair, err := currency.NewPairFromString(marketSummaryData.Symbol)
	if err != nil {
		return err
	}

	tickerPrice, err := ticker.GetTicker(b.Name, pair, asset.Spot)
	if err != nil {
		b.tickerCache.Lock()
		defer b.tickerCache.Unlock()
		if b.tickerCache.Tickers[marketSummaryData.Symbol] != nil {
			tickerData := b.tickerCache.Tickers[marketSummaryData.Symbol]
			tickerPrice = b.constructTicker(*tickerData, marketSummaryData, pair, asset.Spot)
			b.Websocket.DataHandler <- tickerPrice
			return nil
		}
		b.tickerCache.MarketSummaries[marketSummaryData.Symbol] = marketSummaryData
		return nil
	}

	tickerPrice.High = marketSummaryData.High
	tickerPrice.Low = marketSummaryData.Low
	tickerPrice.Volume = marketSummaryData.Volume
	tickerPrice.QuoteVolume = marketSummaryData.QuoteVolume
	tickerPrice.LastUpdated = marketSummaryData.UpdatedAt

	b.Websocket.DataHandler <- tickerPrice

	return nil
}

// WsProcessUpdateOrder processes an update on the open orders
func (b *Bittrex) WsProcessUpdateOrder(data *OrderUpdateMessage) error {
	orderType, err := order.StringToOrderType(data.Delta.Type)
	if err != nil {
		b.Websocket.DataHandler <- order.ClassificationError{
			Exchange: b.Name,
			OrderID:  data.Delta.ID,
			Err:      err,
		}
	}
	orderSide, err := order.StringToOrderSide(data.Delta.Direction)
	if err != nil {
		b.Websocket.DataHandler <- order.ClassificationError{
			Exchange: b.Name,
			OrderID:  data.Delta.ID,
			Err:      err,
		}
	}
	orderStatus, err := order.StringToOrderStatus(data.Delta.Status)
	if err != nil {
		b.Websocket.DataHandler <- order.ClassificationError{
			Exchange: b.Name,
			OrderID:  data.Delta.ID,
			Err:      err,
		}
	}

	pair, err := currency.NewPairFromString(data.Delta.MarketSymbol)
	if err != nil {
		b.Websocket.DataHandler <- order.ClassificationError{
			Exchange: b.Name,
			OrderID:  data.Delta.ID,
			Err:      err,
		}
	}

	b.Websocket.DataHandler <- &order.Modify{
		ImmediateOrCancel: data.Delta.TimeInForce == string(ImmediateOrCancel),
		FillOrKill:        data.Delta.TimeInForce == string(GoodTilCancelled),
		PostOnly:          data.Delta.TimeInForce == string(PostOnlyGoodTilCancelled),
		Price:             data.Delta.Limit,
		Amount:            data.Delta.Quantity,
		RemainingAmount:   data.Delta.Quantity - data.Delta.FillQuantity,
		ExecutedAmount:    data.Delta.FillQuantity,
		Exchange:          b.Name,
		ID:                data.Delta.ID,
		Type:              orderType,
		Side:              orderSide,
		Status:            orderStatus,
		AssetType:         asset.Spot,
		Date:              data.Delta.CreatedAt,
		Pair:              pair,
	}
	return nil
}
