package bbgo

import (
	"context"
	"fmt"

	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"

	"github.com/c9s/bbgo/pkg/fixedpoint"
	"github.com/c9s/bbgo/pkg/pricesolver"
	"github.com/c9s/bbgo/pkg/risk"
	"github.com/c9s/bbgo/pkg/types"
	currency2 "github.com/c9s/bbgo/pkg/types/currency"
)

var defaultLeverage = fixedpoint.NewFromInt(3)

var maxIsolatedMarginLeverage = fixedpoint.NewFromInt(10)

var maxCrossMarginLeverage = fixedpoint.NewFromInt(3)

type AccountValueCalculator struct {
	priceSolver   *pricesolver.SimplePriceSolver
	session       *ExchangeSession
	quoteCurrency string
}

func NewAccountValueCalculator(
	session *ExchangeSession,
	priceSolver *pricesolver.SimplePriceSolver,
	quoteCurrency string,
) *AccountValueCalculator {
	return &AccountValueCalculator{
		priceSolver:   priceSolver,
		session:       session,
		quoteCurrency: quoteCurrency,
	}
}

// UpdatePrices updates the price index from the existing balances
func (c *AccountValueCalculator) UpdatePrices(ctx context.Context) error {
	account := c.session.GetAccount()
	balances := account.Balances().NotZero()
	currencies := balances.Currencies()
	markets := c.session.Markets()

	var symbols []string
	for _, currency := range currencies {
		if currency == c.quoteCurrency {
			continue
		}

		symbol := currency + c.quoteCurrency
		reversedSymbol := c.quoteCurrency + currency
		if _, ok := markets[symbol]; ok {
			symbols = append(symbols, symbol)
		} else if _, ok2 := markets[reversedSymbol]; ok2 {
			symbols = append(symbols, reversedSymbol)
		}
	}

	return c.priceSolver.UpdateFromTickers(ctx, c.session.Exchange, symbols...)
}

func (c *AccountValueCalculator) DebtValue() fixedpoint.Value {
	balances := c.session.GetAccount().Balances().Debts()
	return totalValueInQuote(balances, c.priceSolver, c.quoteCurrency, func(
		prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
	) fixedpoint.Value {
		return prev.Add(b.Debt().Mul(price))
	})
}

func (c *AccountValueCalculator) MarketValue() fixedpoint.Value {
	balances := c.session.GetAccount().Balances().NotZero()
	return totalValueInQuote(balances, c.priceSolver, c.quoteCurrency, func(
		prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
	) fixedpoint.Value {
		return prev.Add(b.Total().Mul(price))
	})
}

func (c *AccountValueCalculator) NetValue() fixedpoint.Value {
	balances := c.session.GetAccount().Balances().NotZero()
	return totalValueInQuote(balances, c.priceSolver, c.quoteCurrency, func(
		prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
	) fixedpoint.Value {
		return prev.Add(b.Net().Mul(price))
	})
}

func totalValueInQuote(
	balances types.BalanceMap,
	priceSolver *pricesolver.SimplePriceSolver,
	quoteCurrency string,
	algo func(prev fixedpoint.Value, b types.Balance, price fixedpoint.Value) fixedpoint.Value,
) (totalValue fixedpoint.Value) {
	totalValue = fixedpoint.Zero

	for _, b := range balances {
		if b.Currency == quoteCurrency {
			totalValue = algo(totalValue, b, fixedpoint.One)
		} else {
			if price, ok := priceSolver.ResolvePrice(b.Currency, quoteCurrency); ok {
				totalValue = algo(totalValue, b, price)
			} else {
				log.Warnf("unable to solve price for %s/%s", b.Currency, quoteCurrency)
			}
		}
	}

	return totalValue
}

func calculateNetValueInQuote(
	balances types.BalanceMap,
	priceSolver *pricesolver.SimplePriceSolver,
	quoteCurrency string,
) fixedpoint.Value {
	return totalValueInQuote(balances, priceSolver, quoteCurrency, func(
		prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
	) fixedpoint.Value {
		return prev.Add(b.Net().Mul(price))
	})
}

func (c *AccountValueCalculator) AvailableQuote() (fixedpoint.Value, error) {
	accountValue := fixedpoint.Zero

	balances := c.session.Account.Balances()
	for _, b := range balances {
		if b.Currency == c.quoteCurrency {
			accountValue = accountValue.Add(b.Net())
			continue
		}

		if price, ok := c.priceSolver.ResolvePrice(b.Currency, c.quoteCurrency); ok {
			accountValue = accountValue.Add(b.Net().Mul(price))
		}
	}

	return accountValue, nil
}

// MarginLevel calculates the margin level from the asset market value and the debt value
// See https://www.binance.com/en/support/faq/360030493931
func (c *AccountValueCalculator) MarginLevel() (fixedpoint.Value, error) {
	marketValue := c.MarketValue()
	debtValue := c.DebtValue()

	if marketValue.IsZero() || debtValue.IsZero() {
		return fixedpoint.NewFromFloat(999.0), nil
	}

	return marketValue.Div(debtValue), nil
}

func aggregateUsdNetValue(balances types.BalanceMap) fixedpoint.Value {
	totalUsdValue := fixedpoint.Zero
	// get all usd value if any
	for currency, balance := range balances {
		if currency2.IsUSDFiatCurrency(currency) {
			totalUsdValue = totalUsdValue.Add(balance.Net())
		}
	}

	return totalUsdValue
}

func usdFiatBalances(balances types.BalanceMap) (fiats types.BalanceMap, rest types.BalanceMap) {
	rest = make(types.BalanceMap)
	fiats = make(types.BalanceMap)
	for currency, balance := range balances {
		if currency2.IsUSDFiatCurrency(currency) {
			fiats[currency] = balance
		} else {
			rest[currency] = balance
		}
	}

	return fiats, rest
}

func CalculateBaseQuantity(
	session *ExchangeSession, market types.Market, price, quantity, leverage fixedpoint.Value,
) (fixedpoint.Value, error) {
	// default leverage guard
	if leverage.IsZero() {
		leverage = defaultLeverage
	}

	baseBalance, hasBaseBalance := session.Account.Balance(market.BaseCurrency)
	balances := session.Account.Balances()

	usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures
	if !usingLeverage {
		// For spot, we simply sell the base quoteCurrency
		if hasBaseBalance {
			if quantity.IsZero() {
				log.Warnf("sell quantity is not set, using all available base balance: %v", baseBalance)
				if !baseBalance.Available.IsZero() {
					return baseBalance.Available, nil
				}
			} else {
				return fixedpoint.Min(quantity, baseBalance.Available), nil
			}
		}

		return quantity, types.NewZeroAssetError(
			fmt.Errorf("quantity is zero, can not submit sell order, please check your quantity settings, your account balances: %+v", balances))
	}

	usdBalances, restBalances := usdFiatBalances(balances)

	// for isolated margin, we can calculate from these two pair
	totalUsdValue := fixedpoint.Zero
	if len(restBalances) == 1 && currency2.IsUSDFiatCurrency(market.QuoteCurrency) {
		totalUsdValue = aggregateUsdNetValue(balances)
	} else if len(restBalances) > 1 {
		priceSolver := pricesolver.NewSimplePriceResolver(session.Markets())
		accountValue := NewAccountValueCalculator(session, priceSolver, "USDT")
		if err := accountValue.UpdatePrices(context.Background()); err != nil {
			return fixedpoint.Zero, err
		}

		netValue := accountValue.NetValue()
		totalUsdValue = netValue
	} else {
		// TODO: translate quote currency like BTC of ETH/BTC to usd value
		totalUsdValue = aggregateUsdNetValue(usdBalances)
	}

	if !quantity.IsZero() {
		return quantity, nil
	}

	if price.IsZero() {
		return quantity, fmt.Errorf("%s price can not be zero", market.Symbol)
	}

	// using leverage -- starts from here
	log.Infof("calculating available leveraged base quantity: base balance = %+v, total usd value %f", baseBalance, totalUsdValue.Float64())

	// calculate the quantity automatically
	if session.Margin || session.IsolatedMargin {
		baseBalanceValue := baseBalance.Net().Mul(price)
		accountUsdValue := baseBalanceValue.Add(totalUsdValue)

		// avoid using all account value since there will be some trade loss for interests and the fee
		accountUsdValue = accountUsdValue.Mul(one.Sub(fixedpoint.NewFromFloat(0.01)))

		log.Infof("calculated account usd value %f %s", accountUsdValue.Float64(), market.QuoteCurrency)

		originLeverage := leverage
		if session.IsolatedMargin {
			leverage = fixedpoint.Min(leverage, maxIsolatedMarginLeverage)
			log.Infof("using isolated margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f",
				maxIsolatedMarginLeverage.Float64(),
				originLeverage.Float64(),
				leverage.Float64())
		} else {
			leverage = fixedpoint.Min(leverage, maxCrossMarginLeverage)
			log.Infof("using cross margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f",
				maxCrossMarginLeverage.Float64(),
				originLeverage.Float64(),
				leverage.Float64())
		}

		// spot margin use the equity value, so we use the total quote balance here
		maxPosition := risk.CalculateMaxPosition(price, accountUsdValue, leverage)
		debt := baseBalance.Debt()
		maxQuantity := maxPosition.Sub(debt)

		log.Infof("margin leverage: calculated maxQuantity=%f maxPosition=%f debt=%f price=%f accountValue=%f %s leverage=%f",
			maxQuantity.Float64(),
			maxPosition.Float64(),
			debt.Float64(),
			price.Float64(),
			accountUsdValue.Float64(),
			market.QuoteCurrency,
			leverage.Float64())

		return maxQuantity, nil
	}

	if session.Futures || session.IsolatedFutures {
		maxPositionQuantity := risk.CalculateMaxPosition(price, totalUsdValue, leverage)

		return maxPositionQuantity, nil
	}

	return quantity, types.NewZeroAssetError(
		errors.New("quantity is zero, can not submit sell order, please check your settings"))
}

func CalculateQuoteQuantity(
	ctx context.Context, session *ExchangeSession, quoteCurrency string, leverage fixedpoint.Value,
) (fixedpoint.Value, error) {
	// default leverage guard
	if leverage.IsZero() {
		leverage = defaultLeverage
	}

	quoteBalance, _ := session.Account.Balance(quoteCurrency)

	usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures
	if !usingLeverage {
		// For spot, we simply return the quote balance
		return quoteBalance.Available.Mul(fixedpoint.Min(leverage, fixedpoint.One)), nil
	}

	originLeverage := leverage
	if session.IsolatedMargin {
		leverage = fixedpoint.Min(leverage, maxIsolatedMarginLeverage)
		log.Infof("using isolated margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f",
			maxIsolatedMarginLeverage.Float64(),
			originLeverage.Float64(),
			leverage.Float64())
	} else {
		leverage = fixedpoint.Min(leverage, maxCrossMarginLeverage)
		log.Infof("using cross margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f",
			maxCrossMarginLeverage.Float64(),
			originLeverage.Float64(),
			leverage.Float64())
	}

	// using leverage -- starts from here
	priceSolver := pricesolver.NewSimplePriceResolver(session.Markets())

	accountValue := NewAccountValueCalculator(session, priceSolver, quoteCurrency)
	if err := accountValue.UpdatePrices(ctx); err != nil {
		return fixedpoint.Zero, err
	}

	availableQuote, err := accountValue.AvailableQuote()
	if err != nil {
		log.WithError(err).Errorf("can not update available quote")
		return fixedpoint.Zero, err
	}

	log.Infof("calculating available leveraged quote quantity: account available quote = %+v", availableQuote)

	return availableQuote.Mul(leverage), nil
}
