package order

import (
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/currency"
	"github.com/thrasher-corp/gocryptotrader/exchanges/validate"
)

// Validate checks the supplied data and returns whether or not it's valid
func (s *Submit) Validate(opt ...validate.Checker) error {
	if s == nil {
		return ErrSubmissionIsNil
	}

	if s.Pair.IsEmpty() {
		return ErrPairIsEmpty
	}

	if s.AssetType == "" {
		return ErrAssetNotSet
	}

	if s.Side != Buy &&
		s.Side != Sell &&
		s.Side != Bid &&
		s.Side != Ask {
		return ErrSideIsInvalid
	}

	if s.Type != Market && s.Type != Limit {
		return ErrTypeIsInvalid
	}

	if s.Amount <= 0 {
		return fmt.Errorf("submit validation error %w, suppled: %.8f", ErrAmountIsInvalid, s.Amount)
	}

	if s.Type == Limit && s.Price <= 0 {
		return ErrPriceMustBeSetIfLimitOrder
	}

	for _, o := range opt {
		err := o.Check()
		if err != nil {
			return err
		}
	}

	return nil
}

// UpdateOrderFromDetail Will update an order detail (used in order management)
// by comparing passed in and existing values
func (d *Detail) UpdateOrderFromDetail(m *Detail) {
	var updated bool
	if d.ImmediateOrCancel != m.ImmediateOrCancel {
		d.ImmediateOrCancel = m.ImmediateOrCancel
		updated = true
	}
	if d.HiddenOrder != m.HiddenOrder {
		d.HiddenOrder = m.HiddenOrder
		updated = true
	}
	if d.FillOrKill != m.FillOrKill {
		d.FillOrKill = m.FillOrKill
		updated = true
	}
	if m.Price > 0 && m.Price != d.Price {
		d.Price = m.Price
		updated = true
	}
	if m.Amount > 0 && m.Amount != d.Amount {
		d.Amount = m.Amount
		updated = true
	}
	if m.LimitPriceUpper > 0 && m.LimitPriceUpper != d.LimitPriceUpper {
		d.LimitPriceUpper = m.LimitPriceUpper
		updated = true
	}
	if m.LimitPriceLower > 0 && m.LimitPriceLower != d.LimitPriceLower {
		d.LimitPriceLower = m.LimitPriceLower
		updated = true
	}
	if m.TriggerPrice > 0 && m.TriggerPrice != d.TriggerPrice {
		d.TriggerPrice = m.TriggerPrice
		updated = true
	}
	if m.TargetAmount > 0 && m.TargetAmount != d.TargetAmount {
		d.TargetAmount = m.TargetAmount
		updated = true
	}
	if m.ExecutedAmount > 0 && m.ExecutedAmount != d.ExecutedAmount {
		d.ExecutedAmount = m.ExecutedAmount
		updated = true
	}
	if m.Fee > 0 && m.Fee != d.Fee {
		d.Fee = m.Fee
		updated = true
	}
	if m.AccountID != "" && m.AccountID != d.AccountID {
		d.AccountID = m.AccountID
		updated = true
	}
	if m.PostOnly != d.PostOnly {
		d.PostOnly = m.PostOnly
		updated = true
	}
	if !m.Pair.IsEmpty() && m.Pair != d.Pair {
		d.Pair = m.Pair
		updated = true
	}
	if m.Leverage != 0 && m.Leverage != d.Leverage {
		d.Leverage = m.Leverage
		updated = true
	}
	if m.ClientID != "" && m.ClientID != d.ClientID {
		d.ClientID = m.ClientID
		updated = true
	}
	if m.WalletAddress != "" && m.WalletAddress != d.WalletAddress {
		d.WalletAddress = m.WalletAddress
		updated = true
	}
	if m.Type != "" && m.Type != d.Type {
		d.Type = m.Type
		updated = true
	}
	if m.Side != "" && m.Side != d.Side {
		d.Side = m.Side
		updated = true
	}
	if m.Status != "" && m.Status != d.Status {
		d.Status = m.Status
		updated = true
	}
	if m.AssetType != "" && m.AssetType != d.AssetType {
		d.AssetType = m.AssetType
		updated = true
	}
	if m.Trades != nil {
		for x := range m.Trades {
			var found bool
			for y := range d.Trades {
				if d.Trades[y].TID != m.Trades[x].TID {
					continue
				}
				found = true
				if d.Trades[y].Fee != m.Trades[x].Fee {
					d.Trades[y].Fee = m.Trades[x].Fee
					updated = true
				}
				if m.Trades[x].Price != 0 && d.Trades[y].Price != m.Trades[x].Price {
					d.Trades[y].Price = m.Trades[x].Price
					updated = true
				}
				if d.Trades[y].Side != m.Trades[x].Side {
					d.Trades[y].Side = m.Trades[x].Side
					updated = true
				}
				if d.Trades[y].Type != m.Trades[x].Type {
					d.Trades[y].Type = m.Trades[x].Type
					updated = true
				}
				if d.Trades[y].Description != m.Trades[x].Description {
					d.Trades[y].Description = m.Trades[x].Description
					updated = true
				}
				if m.Trades[x].Amount != 0 && d.Trades[y].Amount != m.Trades[x].Amount {
					d.Trades[y].Amount = m.Trades[x].Amount
					updated = true
				}
				if d.Trades[y].Timestamp != m.Trades[x].Timestamp {
					d.Trades[y].Timestamp = m.Trades[x].Timestamp
					updated = true
				}
				if d.Trades[y].IsMaker != m.Trades[x].IsMaker {
					d.Trades[y].IsMaker = m.Trades[x].IsMaker
					updated = true
				}
			}
			if !found {
				d.Trades = append(d.Trades, m.Trades[x])
				updated = true
			}
			m.RemainingAmount -= m.Trades[x].Amount
		}
	}
	if m.RemainingAmount > 0 && m.RemainingAmount != d.RemainingAmount {
		d.RemainingAmount = m.RemainingAmount
		updated = true
	}
	if updated {
		if d.LastUpdated == m.LastUpdated {
			d.LastUpdated = time.Now()
		} else {
			d.LastUpdated = m.LastUpdated
		}
	}
	if d.Exchange == "" {
		d.Exchange = m.Exchange
	}
	if d.ID == "" {
		d.ID = m.ID
	}
}

// UpdateOrderFromModify Will update an order detail (used in order management)
// by comparing passed in and existing values
func (d *Detail) UpdateOrderFromModify(m *Modify) {
	var updated bool
	if d.ImmediateOrCancel != m.ImmediateOrCancel {
		d.ImmediateOrCancel = m.ImmediateOrCancel
		updated = true
	}
	if d.HiddenOrder != m.HiddenOrder {
		d.HiddenOrder = m.HiddenOrder
		updated = true
	}
	if d.FillOrKill != m.FillOrKill {
		d.FillOrKill = m.FillOrKill
		updated = true
	}
	if m.Price > 0 && m.Price != d.Price {
		d.Price = m.Price
		updated = true
	}
	if m.Amount > 0 && m.Amount != d.Amount {
		d.Amount = m.Amount
		updated = true
	}
	if m.LimitPriceUpper > 0 && m.LimitPriceUpper != d.LimitPriceUpper {
		d.LimitPriceUpper = m.LimitPriceUpper
		updated = true
	}
	if m.LimitPriceLower > 0 && m.LimitPriceLower != d.LimitPriceLower {
		d.LimitPriceLower = m.LimitPriceLower
		updated = true
	}
	if m.TriggerPrice > 0 && m.TriggerPrice != d.TriggerPrice {
		d.TriggerPrice = m.TriggerPrice
		updated = true
	}
	if m.TargetAmount > 0 && m.TargetAmount != d.TargetAmount {
		d.TargetAmount = m.TargetAmount
		updated = true
	}
	if m.ExecutedAmount > 0 && m.ExecutedAmount != d.ExecutedAmount {
		d.ExecutedAmount = m.ExecutedAmount
		updated = true
	}
	if m.Fee > 0 && m.Fee != d.Fee {
		d.Fee = m.Fee
		updated = true
	}
	if m.AccountID != "" && m.AccountID != d.AccountID {
		d.AccountID = m.AccountID
		updated = true
	}
	if m.PostOnly != d.PostOnly {
		d.PostOnly = m.PostOnly
		updated = true
	}
	if !m.Pair.IsEmpty() && m.Pair != d.Pair {
		d.Pair = m.Pair
		updated = true
	}
	if m.Leverage != 0 && m.Leverage != d.Leverage {
		d.Leverage = m.Leverage
		updated = true
	}
	if m.ClientID != "" && m.ClientID != d.ClientID {
		d.ClientID = m.ClientID
		updated = true
	}
	if m.WalletAddress != "" && m.WalletAddress != d.WalletAddress {
		d.WalletAddress = m.WalletAddress
		updated = true
	}
	if m.Type != "" && m.Type != d.Type {
		d.Type = m.Type
		updated = true
	}
	if m.Side != "" && m.Side != d.Side {
		d.Side = m.Side
		updated = true
	}
	if m.Status != "" && m.Status != d.Status {
		d.Status = m.Status
		updated = true
	}
	if m.AssetType != "" && m.AssetType != d.AssetType {
		d.AssetType = m.AssetType
		updated = true
	}
	if m.Trades != nil {
		for x := range m.Trades {
			var found bool
			for y := range d.Trades {
				if d.Trades[y].TID != m.Trades[x].TID {
					continue
				}
				found = true
				if d.Trades[y].Fee != m.Trades[x].Fee {
					d.Trades[y].Fee = m.Trades[x].Fee
					updated = true
				}
				if m.Trades[x].Price != 0 && d.Trades[y].Price != m.Trades[x].Price {
					d.Trades[y].Price = m.Trades[x].Price
					updated = true
				}
				if d.Trades[y].Side != m.Trades[x].Side {
					d.Trades[y].Side = m.Trades[x].Side
					updated = true
				}
				if d.Trades[y].Type != m.Trades[x].Type {
					d.Trades[y].Type = m.Trades[x].Type
					updated = true
				}
				if d.Trades[y].Description != m.Trades[x].Description {
					d.Trades[y].Description = m.Trades[x].Description
					updated = true
				}
				if m.Trades[x].Amount != 0 && d.Trades[y].Amount != m.Trades[x].Amount {
					d.Trades[y].Amount = m.Trades[x].Amount
					updated = true
				}
				if d.Trades[y].Timestamp != m.Trades[x].Timestamp {
					d.Trades[y].Timestamp = m.Trades[x].Timestamp
					updated = true
				}
				if d.Trades[y].IsMaker != m.Trades[x].IsMaker {
					d.Trades[y].IsMaker = m.Trades[x].IsMaker
					updated = true
				}
			}
			if !found {
				d.Trades = append(d.Trades, m.Trades[x])
				updated = true
			}
			m.RemainingAmount -= m.Trades[x].Amount
		}
	}
	if m.RemainingAmount > 0 && m.RemainingAmount != d.RemainingAmount {
		d.RemainingAmount = m.RemainingAmount
		updated = true
	}
	if updated {
		if d.LastUpdated == m.LastUpdated {
			d.LastUpdated = time.Now()
		} else {
			d.LastUpdated = m.LastUpdated
		}
	}
}

// String implements the stringer interface
func (t Type) String() string {
	return string(t)
}

// Lower returns the type lower case string
func (t Type) Lower() string {
	return strings.ToLower(string(t))
}

// Title returns the type titleized, eg "Limit"
func (t Type) Title() string {
	return strings.Title(strings.ToLower(string(t)))
}

// String implements the stringer interface
func (s Side) String() string {
	return string(s)
}

// Lower returns the side lower case string
func (s Side) Lower() string {
	return strings.ToLower(string(s))
}

// Title returns the side titleized, eg "Buy"
func (s Side) Title() string {
	return strings.Title(strings.ToLower(string(s)))
}

// String implements the stringer interface
func (s Status) String() string {
	return string(s)
}

// FilterOrdersBySide removes any order details that don't match the
// order status provided
func FilterOrdersBySide(orders *[]Detail, side Side) {
	if side == "" || side == AnySide {
		return
	}

	var filteredOrders []Detail
	for i := range *orders {
		if strings.EqualFold(string((*orders)[i].Side), string(side)) {
			filteredOrders = append(filteredOrders, (*orders)[i])
		}
	}

	*orders = filteredOrders
}

// FilterOrdersByType removes any order details that don't match the order type
// provided
func FilterOrdersByType(orders *[]Detail, orderType Type) {
	if orderType == "" || orderType == AnyType {
		return
	}

	var filteredOrders []Detail
	for i := range *orders {
		if strings.EqualFold(string((*orders)[i].Type), string(orderType)) {
			filteredOrders = append(filteredOrders, (*orders)[i])
		}
	}

	*orders = filteredOrders
}

// FilterOrdersByTimeRange removes any OrderDetails outside of the time range
func FilterOrdersByTimeRange(orders *[]Detail, startTime, endTime time.Time) {
	if startTime.IsZero() ||
		endTime.IsZero() ||
		startTime.Unix() == 0 ||
		endTime.Unix() == 0 ||
		endTime.Before(startTime) {
		return
	}

	var filteredOrders []Detail
	for i := range *orders {
		if ((*orders)[i].Date.Unix() >= startTime.Unix() && (*orders)[i].Date.Unix() <= endTime.Unix()) ||
			(*orders)[i].Date.IsZero() {
			filteredOrders = append(filteredOrders, (*orders)[i])
		}
	}

	*orders = filteredOrders
}

// FilterOrdersByCurrencies removes any order details that do not match the
// provided currency list. It is forgiving in that the provided currencies can
// match quote or base currencies
func FilterOrdersByCurrencies(orders *[]Detail, currencies []currency.Pair) {
	if len(currencies) == 0 {
		return
	}
	if len(currencies) == 1 && currencies[0].IsEmpty() {
		return
	}

	var filteredOrders []Detail
	for i := range *orders {
		matchFound := false
		for _, c := range currencies {
			if !matchFound && (*orders)[i].Pair.EqualIncludeReciprocal(c) {
				matchFound = true
			}
		}

		if matchFound {
			filteredOrders = append(filteredOrders, (*orders)[i])
		}
	}

	*orders = filteredOrders
}

func (b ByPrice) Len() int {
	return len(b)
}

func (b ByPrice) Less(i, j int) bool {
	return b[i].Price < b[j].Price
}

func (b ByPrice) Swap(i, j int) {
	b[i], b[j] = b[j], b[i]
}

// SortOrdersByPrice the caller function to sort orders
func SortOrdersByPrice(orders *[]Detail, reverse bool) {
	if reverse {
		sort.Sort(sort.Reverse(ByPrice(*orders)))
	} else {
		sort.Sort(ByPrice(*orders))
	}
}

func (b ByOrderType) Len() int {
	return len(b)
}

func (b ByOrderType) Less(i, j int) bool {
	return b[i].Type.String() < b[j].Type.String()
}

func (b ByOrderType) Swap(i, j int) {
	b[i], b[j] = b[j], b[i]
}

// SortOrdersByType the caller function to sort orders
func SortOrdersByType(orders *[]Detail, reverse bool) {
	if reverse {
		sort.Sort(sort.Reverse(ByOrderType(*orders)))
	} else {
		sort.Sort(ByOrderType(*orders))
	}
}

func (b ByCurrency) Len() int {
	return len(b)
}

func (b ByCurrency) Less(i, j int) bool {
	return b[i].Pair.String() < b[j].Pair.String()
}

func (b ByCurrency) Swap(i, j int) {
	b[i], b[j] = b[j], b[i]
}

// SortOrdersByCurrency the caller function to sort orders
func SortOrdersByCurrency(orders *[]Detail, reverse bool) {
	if reverse {
		sort.Sort(sort.Reverse(ByCurrency(*orders)))
	} else {
		sort.Sort(ByCurrency(*orders))
	}
}

func (b ByDate) Len() int {
	return len(b)
}

func (b ByDate) Less(i, j int) bool {
	return b[i].Date.Unix() < b[j].Date.Unix()
}

func (b ByDate) Swap(i, j int) {
	b[i], b[j] = b[j], b[i]
}

// SortOrdersByDate the caller function to sort orders
func SortOrdersByDate(orders *[]Detail, reverse bool) {
	if reverse {
		sort.Sort(sort.Reverse(ByDate(*orders)))
	} else {
		sort.Sort(ByDate(*orders))
	}
}

func (b ByOrderSide) Len() int {
	return len(b)
}

func (b ByOrderSide) Less(i, j int) bool {
	return b[i].Side.String() < b[j].Side.String()
}

func (b ByOrderSide) Swap(i, j int) {
	b[i], b[j] = b[j], b[i]
}

// SortOrdersBySide the caller function to sort orders
func SortOrdersBySide(orders *[]Detail, reverse bool) {
	if reverse {
		sort.Sort(sort.Reverse(ByOrderSide(*orders)))
	} else {
		sort.Sort(ByOrderSide(*orders))
	}
}

// StringToOrderSide for converting case insensitive order side
// and returning a real Side
func StringToOrderSide(side string) (Side, error) {
	switch {
	case strings.EqualFold(side, Buy.String()):
		return Buy, nil
	case strings.EqualFold(side, Sell.String()):
		return Sell, nil
	case strings.EqualFold(side, Bid.String()):
		return Bid, nil
	case strings.EqualFold(side, Ask.String()):
		return Ask, nil
	case strings.EqualFold(side, AnySide.String()):
		return AnySide, nil
	default:
		return UnknownSide, errors.New(side + " not recognised as order side")
	}
}

// StringToOrderType for converting case insensitive order type
// and returning a real Type
func StringToOrderType(oType string) (Type, error) {
	switch {
	case strings.EqualFold(oType, Limit.String()),
		strings.EqualFold(oType, "EXCHANGE LIMIT"):
		return Limit, nil
	case strings.EqualFold(oType, Market.String()),
		strings.EqualFold(oType, "EXCHANGE MARKET"):
		return Market, nil
	case strings.EqualFold(oType, ImmediateOrCancel.String()),
		strings.EqualFold(oType, "immediate or cancel"),
		strings.EqualFold(oType, "IOC"),
		strings.EqualFold(oType, "EXCHANGE IOC"):
		return ImmediateOrCancel, nil
	case strings.EqualFold(oType, Stop.String()),
		strings.EqualFold(oType, "stop loss"),
		strings.EqualFold(oType, "stop_loss"),
		strings.EqualFold(oType, "EXCHANGE STOP"):
		return Stop, nil
	case strings.EqualFold(oType, StopLimit.String()),
		strings.EqualFold(oType, "EXCHANGE STOP LIMIT"):
		return StopLimit, nil
	case strings.EqualFold(oType, TrailingStop.String()),
		strings.EqualFold(oType, "trailing stop"),
		strings.EqualFold(oType, "EXCHANGE TRAILING STOP"):
		return TrailingStop, nil
	case strings.EqualFold(oType, FillOrKill.String()),
		strings.EqualFold(oType, "EXCHANGE FOK"):
		return FillOrKill, nil
	case strings.EqualFold(oType, IOS.String()):
		return IOS, nil
	case strings.EqualFold(oType, PostOnly.String()):
		return PostOnly, nil
	case strings.EqualFold(oType, AnyType.String()):
		return AnyType, nil
	case strings.EqualFold(oType, Trigger.String()):
		return Trigger, nil
	default:
		return UnknownType, errors.New(oType + " not recognised as order type")
	}
}

// StringToOrderStatus for converting case insensitive order status
// and returning a real Status
func StringToOrderStatus(status string) (Status, error) {
	switch {
	case strings.EqualFold(status, AnyStatus.String()):
		return AnyStatus, nil
	case strings.EqualFold(status, New.String()),
		strings.EqualFold(status, "placed"):
		return New, nil
	case strings.EqualFold(status, Active.String()):
		return Active, nil
	case strings.EqualFold(status, PartiallyFilled.String()),
		strings.EqualFold(status, "partially matched"),
		strings.EqualFold(status, "partially filled"):
		return PartiallyFilled, nil
	case strings.EqualFold(status, Filled.String()),
		strings.EqualFold(status, "fully matched"),
		strings.EqualFold(status, "fully filled"):
		return Filled, nil
	case strings.EqualFold(status, PartiallyCancelled.String()),
		strings.EqualFold(status, "partially cancelled"):
		return PartiallyCancelled, nil
	case strings.EqualFold(status, Open.String()):
		return Open, nil
	case strings.EqualFold(status, Closed.String()):
		return Closed, nil
	case strings.EqualFold(status, Cancelled.String()):
		return Cancelled, nil
	case strings.EqualFold(status, "CANCELED"): // Kraken case
		return Cancelled, nil
	case strings.EqualFold(status, PendingCancel.String()),
		strings.EqualFold(status, "pending cancel"),
		strings.EqualFold(status, "pending cancellation"):
		return PendingCancel, nil
	case strings.EqualFold(status, Rejected.String()):
		return Rejected, nil
	case strings.EqualFold(status, Expired.String()):
		return Expired, nil
	case strings.EqualFold(status, Hidden.String()):
		return Hidden, nil
	case strings.EqualFold(status, InsufficientBalance.String()):
		return InsufficientBalance, nil
	case strings.EqualFold(status, MarketUnavailable.String()):
		return MarketUnavailable, nil
	default:
		return UnknownStatus, errors.New(status + " not recognised as order status")
	}
}

func (o *ClassificationError) Error() string {
	if o.OrderID != "" {
		return fmt.Sprintf("%s - OrderID: %s classification error: %v",
			o.Exchange,
			o.OrderID,
			o.Err)
	}
	return fmt.Sprintf("%s - classification error: %v",
		o.Exchange,
		o.Err)
}

// StandardCancel defines an option in the validator to make sure an ID is set
// for a standard cancel
func (c *Cancel) StandardCancel() validate.Checker {
	return validate.Check(func() error {
		if c.ID == "" {
			return errors.New("ID not set")
		}
		return nil
	})
}

// PairAssetRequired is a validation check for when a cancel request
// requires an asset type and currency pair to be present
func (c *Cancel) PairAssetRequired() validate.Checker {
	return validate.Check(func() error {
		if c.Pair.IsEmpty() {
			return ErrPairIsEmpty
		}

		if c.AssetType == "" {
			return ErrAssetNotSet
		}
		return nil
	})
}

// Validate checks internal struct requirements
func (c *Cancel) Validate(opt ...validate.Checker) error {
	if c == nil {
		return ErrCancelOrderIsNil
	}

	var errs common.Errors
	for _, o := range opt {
		err := o.Check()
		if err != nil {
			errs = append(errs, err)
		}
	}

	if errs != nil {
		return errs
	}
	return nil
}

// Validate checks internal struct requirements
func (g *GetOrdersRequest) Validate(opt ...validate.Checker) error {
	if g == nil {
		return ErrGetOrdersRequestIsNil
	}
	if !g.AssetType.IsValid() {
		return fmt.Errorf("assetType %v not supported", g.AssetType)
	}
	var errs common.Errors
	for _, o := range opt {
		err := o.Check()
		if err != nil {
			errs = append(errs, err)
		}
	}

	if errs != nil {
		return errs
	}
	return nil
}

// Validate checks internal struct requirements
func (m *Modify) Validate(opt ...validate.Checker) error {
	if m == nil {
		return ErrModifyOrderIsNil
	}

	if m.Pair.IsEmpty() {
		return ErrPairIsEmpty
	}

	if m.AssetType.String() == "" {
		return ErrAssetNotSet
	}

	var errs common.Errors
	for _, o := range opt {
		err := o.Check()
		if err != nil {
			errs = append(errs, err)
		}
	}

	if errs != nil {
		return errs
	}
	if m.ClientOrderID == "" && m.ID == "" {
		return ErrOrderIDNotSet
	}
	return nil
}
