package bybit

import (
	"fmt"
	"strconv"
	"time"

	"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
	v3 "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi/v3"
	"github.com/c9s/bbgo/pkg/fixedpoint"
	"github.com/c9s/bbgo/pkg/types"
)

func toGlobalMarket(m bybitapi.Instrument) types.Market {
	return types.Market{
		Exchange:        types.ExchangeBybit,
		Symbol:          m.Symbol,
		LocalSymbol:     m.Symbol,
		PricePrecision:  m.LotSizeFilter.QuotePrecision.NumFractionalDigits(),
		VolumePrecision: m.LotSizeFilter.BasePrecision.NumFractionalDigits(),
		QuoteCurrency:   m.QuoteCoin,
		BaseCurrency:    m.BaseCoin,
		MinNotional:     m.LotSizeFilter.MinOrderAmt,
		MinAmount:       m.LotSizeFilter.MinOrderAmt,

		// quantity
		MinQuantity: m.LotSizeFilter.MinOrderQty,
		MaxQuantity: m.LotSizeFilter.MaxOrderQty,
		StepSize:    m.LotSizeFilter.BasePrecision,

		// price
		MinPrice: m.LotSizeFilter.MinOrderAmt,
		MaxPrice: m.LotSizeFilter.MaxOrderAmt,
		TickSize: m.PriceFilter.TickSize,
	}
}

func toGlobalTicker(stats bybitapi.Ticker, time time.Time) types.Ticker {
	return types.Ticker{
		Volume: stats.Volume24H,
		Last:   stats.LastPrice,
		Open:   stats.PrevPrice24H, // Market price 24 hours ago
		High:   stats.HighPrice24H,
		Low:    stats.LowPrice24H,
		Buy:    stats.Bid1Price,
		Sell:   stats.Ask1Price,
		Time:   time,
	}
}

func toGlobalOrder(order bybitapi.Order) (*types.Order, error) {
	side, err := toGlobalSideType(order.Side)
	if err != nil {
		return nil, err
	}

	orderType, err := toGlobalOrderType(order.OrderType)
	if err != nil {
		return nil, err
	}

	timeInForce, err := toGlobalTimeInForce(order.TimeInForce)
	if err != nil {
		return nil, err
	}

	status, err := toGlobalOrderStatus(order.OrderStatus, order.Side, order.OrderType)
	if err != nil {
		return nil, err
	}

	// linear and inverse : 42f4f364-82e1-49d3-ad1d-cd8cf9aa308d (UUID format)
	// spot : 1468264727470772736 (only numbers)
	// Now we only use spot trading.
	orderIdNum, err := strconv.ParseUint(order.OrderId, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("unexpected order id: %s, err: %w", order.OrderId, err)
	}

	price := order.Price
	if orderType == types.OrderTypeMarket {
		price = order.AvgPrice
	}

	return &types.Order{
		SubmitOrder: types.SubmitOrder{
			ClientOrderID: order.OrderLinkId,
			Symbol:        order.Symbol,
			Side:          side,
			Type:          orderType,
			// We specified the quantity for the market buy order as the base coin when we submitted the order,
			// so we can just use the Qty field.
			Quantity:    order.Qty,
			Price:       price,
			TimeInForce: timeInForce,
		},
		Exchange:         types.ExchangeBybit,
		OrderID:          orderIdNum,
		Status:           status,
		ExecutedQuantity: order.CumExecQty,
		IsWorking:        status == types.OrderStatusNew || status == types.OrderStatusPartiallyFilled,
		CreationTime:     types.Time(order.CreatedTime.Time()),
		UpdateTime:       types.Time(order.UpdatedTime.Time()),
	}, nil
}

func toGlobalSideType(side bybitapi.Side) (types.SideType, error) {
	switch side {
	case bybitapi.SideBuy:
		return types.SideTypeBuy, nil

	case bybitapi.SideSell:
		return types.SideTypeSell, nil

	default:
		return types.SideType(side), fmt.Errorf("unexpected side: %s", side)
	}
}

func toGlobalOrderType(s bybitapi.OrderType) (types.OrderType, error) {
	switch s {
	case bybitapi.OrderTypeMarket:
		return types.OrderTypeMarket, nil

	case bybitapi.OrderTypeLimit:
		return types.OrderTypeLimit, nil

	default:
		return types.OrderType(s), fmt.Errorf("unexpected order type: %s", s)
	}
}

func toGlobalTimeInForce(force bybitapi.TimeInForce) (types.TimeInForce, error) {
	switch force {
	case bybitapi.TimeInForceGTC:
		return types.TimeInForceGTC, nil

	case bybitapi.TimeInForceIOC:
		return types.TimeInForceIOC, nil

	case bybitapi.TimeInForceFOK:
		return types.TimeInForceFOK, nil

	default:
		return types.TimeInForce(force), fmt.Errorf("unexpected timeInForce type: %s", force)
	}
}

func toGlobalOrderStatus(status bybitapi.OrderStatus, side bybitapi.Side, orderType bybitapi.OrderType) (types.OrderStatus, error) {
	switch status {

	case bybitapi.OrderStatusPartiallyFilledCanceled:
		// market buy order	-> PartiallyFilled -> PartiallyFilledCanceled
		if orderType == bybitapi.OrderTypeMarket && side == bybitapi.SideBuy {
			return types.OrderStatusFilled, nil
		}
		// limit buy/sell order -> PartiallyFilled -> PartiallyFilledCanceled(Canceled)
		return types.OrderStatusCanceled, nil

	default:
		return processOtherOrderStatus(status)
	}
}

func processOtherOrderStatus(status bybitapi.OrderStatus) (types.OrderStatus, error) {
	switch status {
	case bybitapi.OrderStatusCreated,
		bybitapi.OrderStatusNew,
		bybitapi.OrderStatusActive:
		return types.OrderStatusNew, nil

	case bybitapi.OrderStatusFilled:
		return types.OrderStatusFilled, nil

	case bybitapi.OrderStatusPartiallyFilled:
		return types.OrderStatusPartiallyFilled, nil

	case bybitapi.OrderStatusCancelled,
		bybitapi.OrderStatusDeactivated:
		return types.OrderStatusCanceled, nil

	case bybitapi.OrderStatusRejected:
		return types.OrderStatusRejected, nil

	default:
		// following not supported
		// bybitapi.OrderStatusUntriggered
		// bybitapi.OrderStatusTriggered
		return types.OrderStatus(status), fmt.Errorf("unexpected order status: %s", status)
	}
}

// processMarketBuyQuantity converts the quantity unit from quote coin to base coin if the order is a **MARKET BUY**.
//
// If the status is OrderStatusPartiallyFilled, it returns the estimated quantity based on the base coin.
//
// If the order status is OrderStatusPartiallyFilledCanceled, it indicates that the order is not fully filled,
// and the system has automatically canceled it. In this scenario, CumExecQty is considered equal to Qty.
func processMarketBuyQuantity(o bybitapi.Order) (fixedpoint.Value, error) {
	if o.Side != bybitapi.SideBuy || o.OrderType != bybitapi.OrderTypeMarket {
		return o.Qty, nil
	}

	var qty fixedpoint.Value
	switch o.OrderStatus {
	case bybitapi.OrderStatusPartiallyFilled:
		// if CumExecValue is zero, it indicates the caller is from the RESTFUL API.
		// we can use AvgPrice to estimate quantity.
		if o.CumExecValue.IsZero() {
			if o.AvgPrice.IsZero() {
				return fixedpoint.Zero, fmt.Errorf("AvgPrice shouldn't be zero")
			}

			qty = o.Qty.Div(o.AvgPrice)
		} else {
			if o.CumExecQty.IsZero() {
				return fixedpoint.Zero, fmt.Errorf("CumExecQty shouldn't be zero")
			}

			// from web socket event
			qty = o.Qty.Div(o.CumExecValue.Div(o.CumExecQty))
		}

	case bybitapi.OrderStatusPartiallyFilledCanceled,
		// Considering extreme scenarios, there's a possibility that 'OrderStatusFilled' could occur.
		bybitapi.OrderStatusFilled:
		qty = o.CumExecQty

	case bybitapi.OrderStatusCreated,
		bybitapi.OrderStatusNew,
		bybitapi.OrderStatusRejected:
		qty = fixedpoint.Zero

	case bybitapi.OrderStatusCancelled:
		qty = o.Qty

	default:
		return fixedpoint.Zero, fmt.Errorf("unexpected order status: %s", o.OrderStatus)
	}

	return qty, nil
}

func toLocalOrderType(orderType types.OrderType) (bybitapi.OrderType, error) {
	switch orderType {
	case types.OrderTypeLimit:
		return bybitapi.OrderTypeLimit, nil

	case types.OrderTypeMarket:
		return bybitapi.OrderTypeMarket, nil

	default:
		return "", fmt.Errorf("order type %s not supported", orderType)
	}
}

func toLocalSide(side types.SideType) (bybitapi.Side, error) {
	switch side {
	case types.SideTypeSell:
		return bybitapi.SideSell, nil

	case types.SideTypeBuy:
		return bybitapi.SideBuy, nil

	default:
		return "", fmt.Errorf("side type %s not supported", side)
	}
}

func toV3Buyer(isBuyer v3.Side) (types.SideType, error) {
	switch isBuyer {
	case v3.SideBuy:
		return types.SideTypeBuy, nil
	case v3.SideSell:
		return types.SideTypeSell, nil
	default:
		return "", fmt.Errorf("unexpected side type: %s", isBuyer)
	}
}
func toV3Maker(isMaker v3.OrderType) (bool, error) {
	switch isMaker {
	case v3.OrderTypeMaker:
		return true, nil
	case v3.OrderTypeTaker:
		return false, nil
	default:
		return false, fmt.Errorf("unexpected order type: %s", isMaker)
	}
}

func v3ToGlobalTrade(trade v3.Trade) (*types.Trade, error) {
	side, err := toV3Buyer(trade.IsBuyer)
	if err != nil {
		return nil, err
	}
	isMaker, err := toV3Maker(trade.IsMaker)
	if err != nil {
		return nil, err
	}

	orderIdNum, err := strconv.ParseUint(trade.OrderId, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("unexpected order id: %s, err: %w", trade.OrderId, err)
	}
	tradeIdNum, err := strconv.ParseUint(trade.TradeId, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("unexpected trade id: %s, err: %w", trade.TradeId, err)
	}

	return &types.Trade{
		ID:            tradeIdNum,
		OrderID:       orderIdNum,
		Exchange:      types.ExchangeBybit,
		Price:         trade.OrderPrice,
		Quantity:      trade.OrderQty,
		QuoteQuantity: trade.OrderPrice.Mul(trade.OrderQty),
		Symbol:        trade.Symbol,
		Side:          side,
		IsBuyer:       side == types.SideTypeBuy,
		IsMaker:       isMaker,
		Time:          types.Time(trade.ExecutionTime),
		Fee:           trade.ExecFee,
		FeeCurrency:   trade.FeeTokenId,
		IsMargin:      false,
		IsFutures:     false,
		IsIsolated:    false,
	}, nil
}

func toGlobalTrade(trade bybitapi.Trade) (*types.Trade, error) {
	side, err := toGlobalSideType(trade.Side)
	if err != nil {
		return nil, fmt.Errorf("unexpected side: %s, err: %w", trade.Side, err)
	}
	orderIdNum, err := strconv.ParseUint(trade.OrderId, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("unexpected order id: %s, err: %w", trade.OrderId, err)
	}
	tradeIdNum, err := strconv.ParseUint(trade.ExecId, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("unexpected trade id: %s, err: %w", trade.ExecId, err)
	}

	return &types.Trade{
		ID:            tradeIdNum,
		OrderID:       orderIdNum,
		Exchange:      types.ExchangeBybit,
		Price:         trade.ExecPrice,
		Quantity:      trade.ExecQty,
		QuoteQuantity: trade.ExecPrice.Mul(trade.ExecQty),
		Symbol:        trade.Symbol,
		Side:          side,
		IsBuyer:       side == types.SideTypeBuy,
		IsMaker:       trade.IsMaker,
		Time:          types.Time(trade.ExecTime),
		Fee:           trade.ExecFee,
		FeeCurrency:   trade.FeeCurrency,
		IsMargin:      false,
		IsFutures:     false,
		IsIsolated:    false,
	}, nil
}

func toGlobalBalanceMap(events []bybitapi.WalletBalances) types.BalanceMap {
	bm := types.BalanceMap{}
	for _, event := range events {
		if event.AccountType != bybitapi.AccountTypeUnified {
			continue
		}

		for _, obj := range event.Coins {
			bm[obj.Coin] = types.Balance{
				Currency:  obj.Coin,
				Available: obj.WalletBalance.Sub(obj.Locked),
				Locked:    obj.Locked,
			}
		}
	}
	return bm
}

func toLocalInterval(interval types.Interval) (string, error) {
	if _, found := bybitapi.SupportedIntervals[interval]; !found {
		return "", fmt.Errorf("interval not supported: %s", interval)
	}

	switch interval {

	case types.Interval1d:
		return string(bybitapi.IntervalSignDay), nil

	case types.Interval1w:
		return string(bybitapi.IntervalSignWeek), nil

	case types.Interval1mo:
		return string(bybitapi.IntervalSignMonth), nil

	default:
		return fmt.Sprintf("%d", interval.Minutes()), nil

	}
}

func toGlobalKLines(symbol string, interval types.Interval, klines []bybitapi.KLine) []types.KLine {
	gKLines := make([]types.KLine, len(klines))
	for i, kline := range klines {
		endTime := types.Time(kline.StartTime.Time().Add(interval.Duration() - time.Millisecond))
		gKLines[i] = types.KLine{
			Exchange:    types.ExchangeBybit,
			Symbol:      symbol,
			StartTime:   types.Time(kline.StartTime),
			EndTime:     endTime,
			Interval:    interval,
			Open:        kline.Open,
			Close:       kline.Close,
			High:        kline.High,
			Low:         kline.Low,
			Volume:      kline.Volume,
			QuoteVolume: kline.TurnOver,
			// Bybit doesn't support close flag in REST API
			Closed: false,
		}
	}
	return gKLines
}
