package zb

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"regexp"
	"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"
	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/ticker"
	"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
	"github.com/thrasher-corp/gocryptotrader/log"
)

const (
	zbWebsocketAPI       = "wss://api.zb.live/websocket"
	zWebsocketAddChannel = "addChannel"
	zbWebsocketRateLimit = 20
)

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

	go z.wsReadData()
	return nil
}

// wsReadData handles all the websocket data coming from the websocket
// connection
func (z *ZB) wsReadData() {
	z.Websocket.Wg.Add(1)
	defer z.Websocket.Wg.Done()

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

func (z *ZB) wsHandleData(respRaw []byte) error {
	fixedJSON := z.wsFixInvalidJSON(respRaw)
	var result Generic
	err := json.Unmarshal(fixedJSON, &result)
	if err != nil {
		return err
	}
	if result.No > 0 {
		if z.Websocket.Match.IncomingWithData(result.No, fixedJSON) {
			return nil
		}
	}
	if result.Code > 0 && result.Code != 1000 {
		return fmt.Errorf("%v request failed, message: %v, error code: %v",
			z.Name,
			result.Message,
			wsErrCodes[result.Code])
	}

	switch {
	case strings.Contains(result.Channel, "markets"):
		var markets Markets
		err := json.Unmarshal(result.Data, &markets)
		if err != nil {
			return err
		}
	case strings.Contains(result.Channel, "ticker"):
		cPair := strings.Split(result.Channel, currency.UnderscoreDelimiter)
		var wsTicker WsTicker
		err := json.Unmarshal(fixedJSON, &wsTicker)
		if err != nil {
			return err
		}

		p, err := currency.NewPairFromString(cPair[0])
		if err != nil {
			return err
		}

		z.Websocket.DataHandler <- &ticker.Price{
			ExchangeName: z.Name,
			Close:        wsTicker.Data.Last,
			Volume:       wsTicker.Data.Volume24Hr,
			High:         wsTicker.Data.High,
			Low:          wsTicker.Data.Low,
			Last:         wsTicker.Data.Last,
			Bid:          wsTicker.Data.Buy,
			Ask:          wsTicker.Data.Sell,
			LastUpdated:  time.Unix(0, wsTicker.Date*int64(time.Millisecond)),
			AssetType:    asset.Spot,
			Pair:         p,
		}
	case strings.Contains(result.Channel, "depth"):
		var depth WsDepth
		err := json.Unmarshal(fixedJSON, &depth)
		if err != nil {
			return err
		}

		var book orderbook.Base
		for i := range depth.Asks {
			book.Asks = append(book.Asks, orderbook.Item{
				Amount: depth.Asks[i][1].(float64),
				Price:  depth.Asks[i][0].(float64),
			})
		}

		for i := range depth.Bids {
			book.Bids = append(book.Bids, orderbook.Item{
				Amount: depth.Bids[i][1].(float64),
				Price:  depth.Bids[i][0].(float64),
			})
		}

		channelInfo := strings.Split(result.Channel, currency.UnderscoreDelimiter)
		cPair, err := currency.NewPairFromString(channelInfo[0])
		if err != nil {
			return err
		}

		book.Asks.Reverse() // Reverse asks for correct alignment
		book.Asset = asset.Spot
		book.Pair = cPair
		book.Exchange = z.Name
		book.VerifyOrderbook = z.CanVerifyOrderbook

		err = z.Websocket.Orderbook.LoadSnapshot(&book)
		if err != nil {
			return err
		}
	case strings.Contains(result.Channel, "_order"):
		cPair := strings.Split(result.Channel, currency.UnderscoreDelimiter)
		var o WsSubmitOrderResponse
		err := json.Unmarshal(fixedJSON, &o)
		if err != nil {
			return err
		}
		if !o.Success {
			return fmt.Errorf("%s - Order %v failed to be placed. %s",
				z.Name,
				o.Data.EntrustID,
				respRaw)
		}

		p, err := currency.NewPairFromString(cPair[0])
		if err != nil {
			return err
		}

		var a asset.Item
		a, err = z.GetPairAssetType(p)
		if err != nil {
			return err
		}
		z.Websocket.DataHandler <- &order.Detail{
			Exchange:  z.Name,
			ID:        strconv.FormatInt(o.Data.EntrustID, 10),
			Pair:      p,
			AssetType: a,
		}
	case strings.Contains(result.Channel, "_cancelorder"):
		cPair := strings.Split(result.Channel, currency.UnderscoreDelimiter)
		var o WsSubmitOrderResponse
		err := json.Unmarshal(fixedJSON, &o)
		if err != nil {
			return err
		}
		if !o.Success {
			return fmt.Errorf("%s - Order %v failed to be cancelled. %s",
				z.Name,
				o.Data.EntrustID,
				respRaw)
		}

		p, err := currency.NewPairFromString(cPair[0])
		if err != nil {
			return err
		}

		z.Websocket.DataHandler <- &order.Modify{
			Exchange: z.Name,
			ID:       strconv.FormatInt(o.Data.EntrustID, 10),
			Pair:     p,
			Status:   order.Cancelled,
		}
	case strings.Contains(result.Channel, "trades"):
		if !z.IsSaveTradeDataEnabled() {
			return nil
		}
		var tradeData WsTrades
		err := json.Unmarshal(fixedJSON, &tradeData)
		if err != nil {
			return err
		}
		var trades []trade.Data
		for i := range tradeData.Data {
			channelInfo := strings.Split(result.Channel, currency.UnderscoreDelimiter)
			cPair, err := currency.NewPairFromString(channelInfo[0])
			if err != nil {
				return err
			}
			var tSide order.Side
			tSide, err = order.StringToOrderSide(tradeData.Data[i].Type)
			if err != nil {
				return &order.ClassificationError{
					Exchange: z.Name,
					Err:      err,
				}
			}

			trades = append(trades, trade.Data{
				Timestamp:    time.Unix(tradeData.Data[i].Date, 0),
				CurrencyPair: cPair,
				AssetType:    asset.Spot,
				Exchange:     z.Name,
				Price:        tradeData.Data[i].Price,
				Amount:       tradeData.Data[i].Amount,
				Side:         tSide,
				TID:          strconv.FormatInt(tradeData.Data[i].TID, 10),
			})
		}
		return trade.AddTradesToBuffer(z.Name, trades...)
	default:
		z.Websocket.DataHandler <- stream.UnhandledMessageWarning{
			Message: z.Name +
				stream.UnhandledMessage +
				string(respRaw)}
	}
	return nil
}

// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (z *ZB) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
	var subscriptions []stream.ChannelSubscription
	// market configuration is its own channel
	subscriptions = append(subscriptions, stream.ChannelSubscription{
		Channel: "markets",
	})
	channels := []string{"%s_ticker", "%s_depth", "%s_trades"}
	enabledCurrencies, err := z.GetEnabledPairs(asset.Spot)
	if err != nil {
		return nil, err
	}

	for i := range channels {
		for j := range enabledCurrencies {
			enabledCurrencies[j].Delimiter = ""
			subscriptions = append(subscriptions, stream.ChannelSubscription{
				Channel:  fmt.Sprintf(channels[i], enabledCurrencies[j].Lower().String()),
				Currency: enabledCurrencies[j].Lower(),
				Asset:    asset.Spot,
			})
		}
	}
	return subscriptions, nil
}

// Subscribe sends a websocket message to receive data from the channel
func (z *ZB) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
	var errs common.Errors
	for i := range channelsToSubscribe {
		subscriptionRequest := Subscription{
			Event:   zWebsocketAddChannel,
			Channel: channelsToSubscribe[i].Channel,
		}
		err := z.Websocket.Conn.SendJSONMessage(subscriptionRequest)
		if err != nil {
			errs = append(errs, err)
			continue
		}
		z.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
	}
	if errs != nil {
		return errs
	}
	return nil
}

func (z *ZB) wsGenerateSignature(request interface{}) string {
	jsonResponse, err := json.Marshal(request)
	if err != nil {
		log.Error(log.ExchangeSys, err)
		return ""
	}
	hmac := crypto.GetHMAC(crypto.HashMD5,
		jsonResponse,
		[]byte(crypto.Sha1ToHex(z.API.Credentials.Secret)))
	return fmt.Sprintf("%x", hmac)
}

func (z *ZB) wsFixInvalidJSON(json []byte) []byte {
	invalidZbJSONRegex := `(\"\[|\"\{)(.*)(\]\"|\}\")`
	regexChecker := regexp.MustCompile(invalidZbJSONRegex)
	matchingResults := regexChecker.Find(json)
	if matchingResults == nil {
		return json
	}
	// Remove first quote character
	capturedInvalidZBJSON := strings.Replace(string(matchingResults), "\"", "", 1)
	// Remove last quote character
	fixedJSON := capturedInvalidZBJSON[:len(capturedInvalidZBJSON)-1]
	return []byte(strings.Replace(string(json), string(matchingResults), fixedJSON, 1))
}

func (z *ZB) wsAddSubUser(username, password string) (*WsGetSubUserListResponse, error) {
	if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
	}
	request := WsAddSubUserRequest{
		Memo:        "memo",
		Password:    password,
		SubUserName: username,
	}
	request.Channel = "addSubUser"
	request.Event = zWebsocketAddChannel
	request.Accesskey = z.API.Credentials.Key
	request.No = z.Websocket.Conn.GenerateMessageID(true)
	request.Sign = z.wsGenerateSignature(request)
	resp, err := z.Websocket.Conn.SendMessageReturnResponse(request.No, request)
	if err != nil {
		return nil, err
	}
	var genericResponse Generic
	err = json.Unmarshal(resp, &genericResponse)
	if err != nil {
		return nil, err
	}
	if genericResponse.Code > 0 && genericResponse.Code != 1000 {
		return nil,
			fmt.Errorf("%v request failed, message: %v, error code: %v",
				z.Name,
				genericResponse.Message,
				wsErrCodes[genericResponse.Code])
	}
	var response WsGetSubUserListResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, err
	}
	return &response, nil
}

func (z *ZB) wsGetSubUserList() (*WsGetSubUserListResponse, error) {
	if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		return nil,
			fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
	}
	request := WsAuthenticatedRequest{}
	request.Channel = "getSubUserList"
	request.Event = zWebsocketAddChannel
	request.Accesskey = z.API.Credentials.Key
	request.No = z.Websocket.Conn.GenerateMessageID(true)
	request.Sign = z.wsGenerateSignature(request)

	resp, err := z.Websocket.Conn.SendMessageReturnResponse(request.No, request)
	if err != nil {
		return nil, err
	}
	var response WsGetSubUserListResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, err
	}
	if response.Code > 0 && response.Code != 1000 {
		return &response,
			fmt.Errorf("%v request failed, message: %v, error code: %v",
				z.Name,
				response.Message,
				wsErrCodes[response.Code])
	}
	return &response, nil
}

func (z *ZB) wsDoTransferFunds(pair currency.Code, amount float64, fromUserName, toUserName string) (*WsRequestResponse, error) {
	if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		return nil,
			fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
	}
	request := WsDoTransferFundsRequest{
		Amount:       amount,
		Currency:     pair,
		FromUserName: fromUserName,
		ToUserName:   toUserName,
		No:           z.Websocket.Conn.GenerateMessageID(true),
	}
	request.Channel = "doTransferFunds"
	request.Event = zWebsocketAddChannel
	request.Accesskey = z.API.Credentials.Key
	request.Sign = z.wsGenerateSignature(request)

	resp, err := z.Websocket.Conn.SendMessageReturnResponse(request.No, request)
	if err != nil {
		return nil, err
	}
	var response WsRequestResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, err
	}
	if response.Code > 0 && response.Code != 1000 {
		return &response,
			fmt.Errorf("%v request failed, message: %v, error code: %v",
				z.Name,
				response.Message,
				wsErrCodes[response.Code])
	}
	return &response, nil
}

func (z *ZB) wsCreateSubUserKey(assetPerm, entrustPerm, leverPerm, moneyPerm bool, keyName, toUserID string) (*WsRequestResponse, error) {
	if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		return nil,
			fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
	}
	request := WsCreateSubUserKeyRequest{
		AssetPerm:   assetPerm,
		EntrustPerm: entrustPerm,
		KeyName:     keyName,
		LeverPerm:   leverPerm,
		MoneyPerm:   moneyPerm,
		No:          z.Websocket.Conn.GenerateMessageID(true),
		ToUserID:    toUserID,
	}
	request.Channel = "createSubUserKey"
	request.Event = zWebsocketAddChannel
	request.Accesskey = z.API.Credentials.Key
	request.Sign = z.wsGenerateSignature(request)

	resp, err := z.Websocket.Conn.SendMessageReturnResponse(request.No, request)
	if err != nil {
		return nil, err
	}
	var response WsRequestResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, err
	}
	if response.Code > 0 && response.Code != 1000 {
		return &response,
			fmt.Errorf("%v request failed, message: %v, error code: %v",
				z.Name,
				response.Message,
				wsErrCodes[response.Code])
	}
	return &response, nil
}

func (z *ZB) wsSubmitOrder(pair currency.Pair, amount, price float64, tradeType int64) (*WsSubmitOrderResponse, error) {
	if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		return nil,
			fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
	}
	request := WsSubmitOrderRequest{
		Amount:    amount,
		Price:     price,
		TradeType: tradeType,
		No:        z.Websocket.Conn.GenerateMessageID(true),
	}
	request.Channel = pair.String() + "_order"
	request.Event = zWebsocketAddChannel
	request.Accesskey = z.API.Credentials.Key
	request.Sign = z.wsGenerateSignature(request)

	resp, err := z.Websocket.Conn.SendMessageReturnResponse(request.No, request)
	if err != nil {
		return nil, err
	}
	var response WsSubmitOrderResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, err
	}
	if response.Code > 0 && response.Code != 1000 {
		return &response,
			fmt.Errorf("%v request failed, message: %v, error code: %v",
				z.Name,
				response.Message,
				wsErrCodes[response.Code])
	}
	return &response, nil
}

func (z *ZB) wsCancelOrder(pair currency.Pair, orderID int64) (*WsCancelOrderResponse, error) {
	if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		return nil,
			fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
	}
	request := WsCancelOrderRequest{
		ID: orderID,
		No: z.Websocket.Conn.GenerateMessageID(true),
	}
	request.Channel = pair.String() + "_cancelorder"
	request.Event = zWebsocketAddChannel
	request.Accesskey = z.API.Credentials.Key
	request.Sign = z.wsGenerateSignature(request)

	resp, err := z.Websocket.Conn.SendMessageReturnResponse(request.No, request)
	if err != nil {
		return nil, err
	}
	var response WsCancelOrderResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, err
	}
	if response.Code > 0 && response.Code != 1000 {
		return &response,
			fmt.Errorf("%v request failed, message: %v, error code: %v",
				z.Name,
				response.Message,
				wsErrCodes[response.Code])
	}
	return &response, nil
}

func (z *ZB) wsGetOrder(pair currency.Pair, orderID int64) (*WsGetOrderResponse, error) {
	if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		return nil,
			fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
	}
	request := WsGetOrderRequest{
		ID: orderID,
		No: z.Websocket.Conn.GenerateMessageID(true),
	}
	request.Channel = pair.String() + "_getorder"
	request.Event = zWebsocketAddChannel
	request.Accesskey = z.API.Credentials.Key
	request.Sign = z.wsGenerateSignature(request)

	resp, err := z.Websocket.Conn.SendMessageReturnResponse(request.No, request)
	if err != nil {
		return nil, err
	}
	var response WsGetOrderResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, err
	}
	if response.Code > 0 && response.Code != 1000 {
		return &response,
			fmt.Errorf("%v request failed, message: %v, error code: %v",
				z.Name,
				response.Message,
				wsErrCodes[response.Code])
	}
	return &response, nil
}

func (z *ZB) wsGetOrders(pair currency.Pair, pageIndex, tradeType int64) (*WsGetOrdersResponse, error) {
	if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		return nil,
			fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
	}
	request := WsGetOrdersRequest{
		PageIndex: pageIndex,
		TradeType: tradeType,
		No:        z.Websocket.Conn.GenerateMessageID(true),
	}
	request.Channel = pair.String() + "_getorders"
	request.Event = zWebsocketAddChannel
	request.Accesskey = z.API.Credentials.Key
	request.Sign = z.wsGenerateSignature(request)
	resp, err := z.Websocket.Conn.SendMessageReturnResponse(request.No, request)
	if err != nil {
		return nil, err
	}
	var response WsGetOrdersResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, err
	}
	if response.Code > 0 && response.Code != 1000 {
		return &response,
			fmt.Errorf("%v request failed, message: %v, error code: %v",
				z.Name,
				response.Message,
				wsErrCodes[response.Code])
	}
	return &response, nil
}

func (z *ZB) wsGetOrdersIgnoreTradeType(pair currency.Pair, pageIndex, pageSize int64) (*WsGetOrdersIgnoreTradeTypeResponse, error) {
	if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		return nil,
			fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
	}
	request := WsGetOrdersIgnoreTradeTypeRequest{
		PageIndex: pageIndex,
		PageSize:  pageSize,
		No:        z.Websocket.Conn.GenerateMessageID(true),
	}
	request.Channel = pair.String() + "_getordersignoretradetype"
	request.Event = zWebsocketAddChannel
	request.Accesskey = z.API.Credentials.Key
	request.Sign = z.wsGenerateSignature(request)

	resp, err := z.Websocket.Conn.SendMessageReturnResponse(request.No, request)
	if err != nil {
		return nil, err
	}
	var response WsGetOrdersIgnoreTradeTypeResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, err
	}
	if response.Code > 0 && response.Code != 1000 {
		return &response,
			fmt.Errorf("%v request failed, message: %v, error code: %v",
				z.Name,
				response.Message,
				wsErrCodes[response.Code])
	}
	return &response, nil
}

func (z *ZB) wsGetAccountInfoRequest() (*WsGetAccountInfoResponse, error) {
	if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
		return nil,
			fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
	}
	request := WsAuthenticatedRequest{
		Channel:   "getaccountinfo",
		Event:     zWebsocketAddChannel,
		Accesskey: z.API.Credentials.Key,
		No:        z.Websocket.Conn.GenerateMessageID(true),
	}
	request.Sign = z.wsGenerateSignature(request)

	resp, err := z.Websocket.Conn.SendMessageReturnResponse(request.No, request)
	if err != nil {
		return nil, err
	}
	var response WsGetAccountInfoResponse
	err = json.Unmarshal(resp, &response)
	if err != nil {
		return nil, err
	}
	if response.Code > 0 && response.Code != 1000 {
		return &response,
			fmt.Errorf("%v request failed, message: %v, error code: %v",
				z.Name,
				response.Message,
				wsErrCodes[response.Code])
	}
	return &response, nil
}
