package okx

import (
	"context"
	"errors"
	"fmt"
	"reflect"

	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/encoding/json"
	"github.com/thrasher-corp/gocryptotrader/exchanges/order"
	"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)

var (
	errInvalidWebsocketRequest     = errors.New("invalid websocket request")
	errOperationFailed             = errors.New("operation failed")
	errPartialSuccess              = errors.New("bulk operation partially succeeded")
	errMassCancelFailed            = errors.New("mass cancel failed")
	errCancelAllSpreadOrdersFailed = errors.New("cancel all spread orders failed")
	errMultipleItemsReturned       = errors.New("multiple items returned")
)

// WSPlaceOrder submits an order
func (e *Exchange) WSPlaceOrder(ctx context.Context, arg *PlaceOrderRequestParam) (*OrderData, error) {
	if err := arg.Validate(); err != nil {
		return nil, err
	}

	var resp []*OrderData
	if err := e.SendAuthenticatedWebsocketRequest(ctx, placeOrderEPL, e.MessageID(), "order", []PlaceOrderRequestParam{*arg}, &resp); err != nil {
		return nil, err
	}
	return singleItem(resp)
}

// WSPlaceMultipleOrders submits multiple orders
func (e *Exchange) WSPlaceMultipleOrders(ctx context.Context, args []PlaceOrderRequestParam) ([]*OrderData, error) {
	if len(args) == 0 {
		return nil, fmt.Errorf("%T: %w", args, order.ErrSubmissionIsNil)
	}

	for i := range args {
		if err := args[i].Validate(); err != nil {
			return nil, err
		}
	}

	var resp []*OrderData
	return resp, e.SendAuthenticatedWebsocketRequest(ctx, placeMultipleOrdersEPL, e.MessageID(), "batch-orders", args, &resp)
}

// WSCancelOrder cancels an order
func (e *Exchange) WSCancelOrder(ctx context.Context, arg *CancelOrderRequestParam) (*OrderData, error) {
	if arg == nil {
		return nil, fmt.Errorf("%T: %w", arg, common.ErrNilPointer)
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	if arg.OrderID == "" && arg.ClientOrderID == "" {
		return nil, order.ErrOrderIDNotSet
	}

	var resp []*OrderData
	if err := e.SendAuthenticatedWebsocketRequest(ctx, cancelOrderEPL, e.MessageID(), "cancel-order", []CancelOrderRequestParam{*arg}, &resp); err != nil {
		return nil, err
	}

	return singleItem(resp)
}

// WSCancelMultipleOrders cancels multiple orders
func (e *Exchange) WSCancelMultipleOrders(ctx context.Context, args []CancelOrderRequestParam) ([]*OrderData, error) {
	if len(args) == 0 {
		return nil, fmt.Errorf("%T: %w", args, order.ErrSubmissionIsNil)
	}

	for i := range args {
		if args[i].InstrumentID == "" {
			return nil, errMissingInstrumentID
		}
		if args[i].OrderID == "" && args[i].ClientOrderID == "" {
			return nil, order.ErrOrderIDNotSet
		}
	}

	var resp []*OrderData
	return resp, e.SendAuthenticatedWebsocketRequest(ctx, cancelMultipleOrdersEPL, e.MessageID(), "batch-cancel-orders", args, &resp)
}

// WSAmendOrder amends an order
func (e *Exchange) WSAmendOrder(ctx context.Context, arg *AmendOrderRequestParams) (*OrderData, error) {
	if arg == nil {
		return nil, fmt.Errorf("%T: %w", arg, common.ErrNilPointer)
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	if arg.ClientOrderID == "" && arg.OrderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	if arg.NewQuantity <= 0 && arg.NewPrice <= 0 {
		return nil, errInvalidNewSizeOrPriceInformation
	}

	var resp []*OrderData
	if err := e.SendAuthenticatedWebsocketRequest(ctx, amendOrderEPL, e.MessageID(), "amend-order", []AmendOrderRequestParams{*arg}, &resp); err != nil {
		return nil, err
	}
	return singleItem(resp)
}

// WSAmendMultipleOrders amends multiple orders
func (e *Exchange) WSAmendMultipleOrders(ctx context.Context, args []AmendOrderRequestParams) ([]*OrderData, error) {
	if len(args) == 0 {
		return nil, fmt.Errorf("%T: %w", args, order.ErrSubmissionIsNil)
	}

	for x := range args {
		if args[x].InstrumentID == "" {
			return nil, errMissingInstrumentID
		}
		if args[x].ClientOrderID == "" && args[x].OrderID == "" {
			return nil, order.ErrOrderIDNotSet
		}
		if args[x].NewQuantity <= 0 && args[x].NewPrice <= 0 {
			return nil, errInvalidNewSizeOrPriceInformation
		}
	}

	var resp []*OrderData
	return resp, e.SendAuthenticatedWebsocketRequest(ctx, amendMultipleOrdersEPL, e.MessageID(), "batch-amend-orders", args, &resp)
}

// WSMassCancelOrders cancels all MMP pending orders of an instrument family. Only applicable to Option in Portfolio Margin mode, and MMP privilege is required.
func (e *Exchange) WSMassCancelOrders(ctx context.Context, args []CancelMassReqParam) error {
	if len(args) == 0 {
		return fmt.Errorf("%T: %w", args, order.ErrSubmissionIsNil)
	}

	for x := range args {
		if args[x].InstrumentType == "" {
			return fmt.Errorf("%w, instrument type can not be empty", errInvalidInstrumentType)
		}
		if args[x].InstrumentFamily == "" {
			return errInstrumentFamilyRequired
		}
	}

	var resps []*struct {
		Result bool `json:"result"`
	}
	if err := e.SendAuthenticatedWebsocketRequest(ctx, amendOrderEPL, e.MessageID(), "mass-cancel", args, &resps); err != nil {
		return err
	}

	resp, err := singleItem(resps)
	if err != nil {
		return err
	}

	if !resp.Result {
		return errMassCancelFailed
	}

	return nil
}

// WSPlaceSpreadOrder submits a spread order
func (e *Exchange) WSPlaceSpreadOrder(ctx context.Context, arg *SpreadOrderParam) (*SpreadOrderResponse, error) {
	if err := arg.Validate(); err != nil {
		return nil, err
	}

	var resp []*SpreadOrderResponse
	if err := e.SendAuthenticatedWebsocketRequest(ctx, placeSpreadOrderEPL, e.MessageID(), "sprd-order", []SpreadOrderParam{*arg}, &resp); err != nil {
		return nil, err
	}

	return singleItem(resp)
}

// WSAmendSpreadOrder amends a spread order
func (e *Exchange) WSAmendSpreadOrder(ctx context.Context, arg *AmendSpreadOrderParam) (*SpreadOrderResponse, error) {
	if arg == nil {
		return nil, fmt.Errorf("%T: %w", arg, common.ErrNilPointer)
	}
	if arg.OrderID == "" && arg.ClientOrderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	if arg.NewPrice == 0 && arg.NewSize == 0 {
		return nil, errSizeOrPriceIsRequired
	}

	var resp []*SpreadOrderResponse
	if err := e.SendAuthenticatedWebsocketRequest(ctx, amendSpreadOrderEPL, e.MessageID(), "sprd-amend-order", []AmendSpreadOrderParam{*arg}, &resp); err != nil {
		return nil, err
	}

	return singleItem(resp)
}

// WSCancelSpreadOrder cancels an incomplete spread order through the websocket connection.
func (e *Exchange) WSCancelSpreadOrder(ctx context.Context, orderID, clientOrderID string) (*SpreadOrderResponse, error) {
	if orderID == "" && clientOrderID == "" {
		return nil, order.ErrOrderIDNotSet
	}

	arg := make(map[string]string)
	if orderID != "" {
		arg["ordId"] = orderID
	}
	if clientOrderID != "" {
		arg["clOrdId"] = clientOrderID
	}

	var resp []*SpreadOrderResponse
	if err := e.SendAuthenticatedWebsocketRequest(ctx, cancelSpreadOrderEPL, e.MessageID(), "sprd-cancel-order", []map[string]string{arg}, &resp); err != nil {
		return nil, err
	}

	return singleItem(resp)
}

// WSCancelAllSpreadOrders cancels all spread orders and return success message through the websocket channel.
func (e *Exchange) WSCancelAllSpreadOrders(ctx context.Context, spreadID string) error {
	arg := make(map[string]string, 1)
	if spreadID != "" {
		arg["sprdId"] = spreadID
	}

	var resps []*ResponseResult
	if err := e.SendAuthenticatedWebsocketRequest(ctx, cancelAllSpreadOrderEPL, e.MessageID(), "sprd-mass-cancel", []map[string]string{arg}, &resps); err != nil {
		return err
	}

	resp, err := singleItem(resps)
	if err != nil {
		return err
	}

	if !resp.Result {
		return errCancelAllSpreadOrdersFailed
	}

	return nil
}

// SendAuthenticatedWebsocketRequest sends a websocket request to the server
func (e *Exchange) SendAuthenticatedWebsocketRequest(ctx context.Context, epl request.EndpointLimit, id, operation string, payload, result any) error {
	if operation == "" || payload == nil {
		return errInvalidWebsocketRequest
	}

	outbound := &struct {
		ID        string `json:"id"`
		Operation string `json:"op"`
		Arguments any    `json:"args"`
		// TODO: Add ExpTime to the struct, the struct should look like this:
		// ExpTime  string `json:"expTime,omitempty"` so a deadline can be set
		// Request effective deadline. Unix timestamp format in milliseconds, e.g. 1597026383085
	}{
		ID:        id,
		Operation: operation,
		Arguments: payload,
	}

	incoming, err := e.Websocket.AuthConn.SendMessageReturnResponse(ctx, epl, id, outbound)
	if err != nil {
		return err
	}

	intermediary := struct {
		ID        string `json:"id"`
		Operation string `json:"op"`
		Code      int64  `json:"code,string"`
		Message   string `json:"msg"`
		Data      any    `json:"data"`
		InTime    string `json:"inTime"`
		OutTime   string `json:"outTime"`
	}{
		Data: result,
	}

	if err := json.Unmarshal(incoming, &intermediary); err != nil {
		return err
	}

	switch intermediary.Code {
	case 0:
		return nil
	case 1:
		return parseWSResponseErrors(result, errOperationFailed)
	case 2:
		return parseWSResponseErrors(result, errPartialSuccess)
	default:
		return getStatusError(intermediary.Code, intermediary.Message)
	}
}

func parseWSResponseErrors(result any, err error) error {
	s := reflect.ValueOf(result).Elem()
	for i := range s.Len() {
		v := s.Index(i)
		if subErr, ok := v.Interface().(interface{ Error() error }); ok && subErr.Error() != nil {
			err = common.AppendError(err, fmt.Errorf("%s[%d]: %w", v.Type(), i+1, subErr.Error()))
		}
	}
	return err
}

func singleItem[T any](resp []*T) (*T, error) {
	if len(resp) == 0 {
		return nil, common.ErrNoResponse
	}
	if len(resp) > 1 {
		return nil, fmt.Errorf("%w, received %d", errMultipleItemsReturned, len(resp))
	}
	return resp[0], nil
}
