package pricesolver

import (
	"context"
	"slices"
	"sync"

	log "github.com/sirupsen/logrus"

	"github.com/c9s/bbgo/pkg/fixedpoint"
	"github.com/c9s/bbgo/pkg/types"
)

// SimplePriceSolver implements a map-structure-based price index
type SimplePriceSolver struct {
	// symbolPrices stores the latest trade price by mapping symbol to price
	symbolPrices map[string]fixedpoint.Value
	markets      types.MarketMap

	// pricesByBase stores the prices by currency names as a 2-level map
	// BTC -> USDT -> 48000.0
	// BTC -> TWD -> 1536000
	pricesByBase map[string]map[string]float64

	// pricesByQuote is for reversed pairs, like USDT/TWD or BNB/BTC
	// the reason that we don't store the reverse pricing in the same map is:
	// expression like (1/price) could produce precision issue since the data type is fixed-point, only 8 fraction numbers are supported.
	pricesByQuote map[string]map[string]float64

	mu sync.Mutex
}

func NewSimplePriceResolver(markets types.MarketMap) *SimplePriceSolver {
	if markets == nil {
		markets = make(types.MarketMap)
	}

	return &SimplePriceSolver{
		markets:       markets,
		symbolPrices:  make(map[string]fixedpoint.Value),
		pricesByBase:  make(map[string]map[string]float64),
		pricesByQuote: make(map[string]map[string]float64),
	}
}
func (m *SimplePriceSolver) SetMarkets(markets types.MarketMap) {
	m.mu.Lock()
	defer m.mu.Unlock()

	if markets == nil {
		markets = make(types.MarketMap)
	}

	m.markets = markets
}

func (m *SimplePriceSolver) Update(symbol string, price fixedpoint.Value) {
	m.mu.Lock()
	defer m.mu.Unlock()

	m.symbolPrices[symbol] = price
	market, ok := m.markets[symbol]
	if !ok {
		log.Warnf("market info %s not found, unable to update price (%s)", symbol, price.String())
		return
	}

	quoteMap, ok2 := m.pricesByBase[market.BaseCurrency]
	if !ok2 {
		quoteMap = make(map[string]float64)
		m.pricesByBase[market.BaseCurrency] = quoteMap
	}

	quoteMap[market.QuoteCurrency] = price.Float64()

	baseMap, ok3 := m.pricesByQuote[market.QuoteCurrency]
	if !ok3 {
		baseMap = make(map[string]float64)
		m.pricesByQuote[market.QuoteCurrency] = baseMap
	}

	baseMap[market.BaseCurrency] = price.Float64()
}

func (m *SimplePriceSolver) UpdateFromTrade(trade types.Trade) {
	m.Update(trade.Symbol, trade.Price)
}

func (m *SimplePriceSolver) BindStream(stream types.Stream) {
	if stream.GetPublicOnly() {
		stream.OnKLineClosed(func(k types.KLine) {
			m.Update(k.Symbol, k.Close)
		})
	} else {
		stream.OnTradeUpdate(m.UpdateFromTrade)
	}
}

func (m *SimplePriceSolver) UpdateFromTickers(ctx context.Context, ex types.Exchange, symbols ...string) error {
	for _, symbol := range symbols {
		// only query the ticker for the symbol that is in the market map
		_, ok := m.markets[symbol]
		if !ok {
			continue
		}

		ticker, err := ex.QueryTicker(ctx, symbol)
		if err != nil {
			return err
		}

		price := ticker.GetValidPrice()
		if !price.IsZero() {
			m.Update(symbol, price)
		}
	}

	return nil
}

func (m *SimplePriceSolver) inferencePrice(asset string, assetPrice float64, preferredFiats ...string) (float64, bool) {
	quotePrices, ok := m.pricesByBase[asset]
	if ok {
		for quote, price := range quotePrices {
			if slices.Contains(preferredFiats, quote) {
				return price * assetPrice, true
			}
		}

		for quote, price := range quotePrices {
			if infPrice, ok := m.inferencePrice(quote, price*assetPrice, preferredFiats...); ok {
				return infPrice, true
			}
		}
	}

	// for example, quote = TWD here, we can get a price map with:
	// USDT: 32.0 (for USDT/TWD at 32.0)
	basePrices, ok := m.pricesByQuote[asset]
	if ok {
		for base, basePrice := range basePrices {
			if slices.Contains(preferredFiats, base) {
				return assetPrice / basePrice, true
			}
		}

		for base, basePrice := range basePrices {
			if infPrice, ok2 := m.inferencePrice(base, assetPrice/basePrice, preferredFiats...); ok2 {
				return infPrice, true
			}
		}
	}

	return 0.0, false
}

func (m *SimplePriceSolver) ResolvePrice(asset string, preferredFiats ...string) (fixedpoint.Value, bool) {
	if len(preferredFiats) == 0 {
		return fixedpoint.Zero, false
	} else if asset == preferredFiats[0] {
		return fixedpoint.One, true
	}

	m.mu.Lock()
	defer m.mu.Unlock()
	fn, ok := m.inferencePrice(asset, 1.0, preferredFiats...)
	if ok {
		return fixedpoint.NewFromFloat(fn), ok
	}

	return fixedpoint.Zero, false
}
