package oracle

import (
	"context"
	"errors"
	"fmt"
	"math/big"
	"strings"
	"time"

	"github.com/tech/sendico/fx/storage/model"
	"github.com/tech/sendico/pkg/merrors"
)

type priceSet struct {
	bid *big.Rat
	ask *big.Rat
	mid *big.Rat
}

func (s *Service) computeCrossRate(ctx context.Context, pair *model.Pair, provider string) (*model.RateSnapshot, error) {
	if pair == nil || pair.Cross == nil || !pair.Cross.Enabled {
		return nil, merrors.ErrNoData
	}

	baseSnap, err := s.fetchCrossLegSnapshot(ctx, pair.Cross.BaseLeg, provider)
	if err != nil {
		return nil, err
	}
	quoteSnap, err := s.fetchCrossLegSnapshot(ctx, pair.Cross.QuoteLeg, provider)
	if err != nil {
		return nil, err
	}

	basePrices, err := buildPriceSet(baseSnap)
	if err != nil {
		return nil, err
	}
	quotePrices, err := buildPriceSet(quoteSnap)
	if err != nil {
		return nil, err
	}

	if pair.Cross.BaseLeg.Invert {
		basePrices, err = invertPriceSet(basePrices)
		if err != nil {
			return nil, err
		}
	}
	if pair.Cross.QuoteLeg.Invert {
		quotePrices, err = invertPriceSet(quotePrices)
		if err != nil {
			return nil, err
		}
	}

	result := multiplyPriceSets(basePrices, quotePrices)
	if result.ask.Cmp(result.bid) < 0 {
		result.ask, result.bid = result.bid, result.ask
	}

	spread := calcSpreadBps(result)

	asOfMs := minNonZero(baseSnap.AsOfUnixMs, quoteSnap.AsOfUnixMs)
	if asOfMs == 0 {
		asOfMs = time.Now().UnixMilli()
	}
	asOf := time.UnixMilli(asOfMs)

	rateRef := fmt.Sprintf("cross|%s/%s|%s|%s+%s", pair.Pair.Base, pair.Pair.Quote, provider, baseSnap.RateRef, quoteSnap.RateRef)

	return &model.RateSnapshot{
		RateRef:     rateRef,
		Pair:        pair.Pair,
		Provider:    provider,
		Mid:         formatPrice(result.mid),
		Bid:         formatPrice(result.bid),
		Ask:         formatPrice(result.ask),
		SpreadBps:   formatPrice(spread),
		AsOfUnixMs:  asOfMs,
		AsOf:        &asOf,
		Source:      "cross_rate",
		ProviderRef: rateRef,
	}, nil
}

func (s *Service) fetchCrossLegSnapshot(ctx context.Context, leg model.CrossRateLeg, fallbackProvider string) (*model.RateSnapshot, error) {
	provider := fallbackProvider
	if strings.TrimSpace(leg.Provider) != "" {
		provider = leg.Provider
	}
	if provider == "" {
		return nil, merrors.InvalidArgument("oracle: cross leg provider missing")
	}
	return s.storage.Rates().LatestSnapshot(ctx, leg.Pair, provider)
}

func buildPriceSet(rate *model.RateSnapshot) (priceSet, error) {
	if rate == nil {
		return priceSet{}, merrors.InvalidArgument("oracle: cross rate requires underlying snapshot")
	}
	ask, err := parsePrice(rate.Ask)
	if err != nil && !errors.Is(err, merrors.ErrNoData) {
		return priceSet{}, err
	}
	if errors.Is(err, merrors.ErrNoData) {
		ask = nil
	}

	bid, err := parsePrice(rate.Bid)
	if err != nil && !errors.Is(err, merrors.ErrNoData) {
		return priceSet{}, err
	}
	if errors.Is(err, merrors.ErrNoData) {
		bid = nil
	}

	mid, err := parsePrice(rate.Mid)
	if err != nil && !errors.Is(err, merrors.ErrNoData) {
		return priceSet{}, err
	}
	if errors.Is(err, merrors.ErrNoData) {
		mid = nil
	}

	if ask == nil && bid == nil {
		if mid == nil {
			return priceSet{}, merrors.InvalidArgument("oracle: cross rate snapshot missing price data")
		}
		ask = new(big.Rat).Set(mid)
		bid = new(big.Rat).Set(mid)
	}
	if ask == nil && mid != nil {
		ask = new(big.Rat).Set(mid)
	}
	if bid == nil && mid != nil {
		bid = new(big.Rat).Set(mid)
	}
	if ask == nil || bid == nil {
		return priceSet{}, merrors.InvalidArgument("oracle: cross rate snapshot missing bid/ask data")
	}

	ps := priceSet{
		bid: new(big.Rat).Set(bid),
		ask: new(big.Rat).Set(ask),
		mid: averageOrMid(bid, ask, mid),
	}
	if ps.ask.Cmp(ps.bid) < 0 {
		ps.ask, ps.bid = ps.bid, ps.ask
	}
	return ps, nil
}

func parsePrice(value string) (*big.Rat, error) {
	if strings.TrimSpace(value) == "" {
		return nil, merrors.ErrNoData
	}
	return ratFromString(value)
}

func averageOrMid(bid, ask, mid *big.Rat) *big.Rat {
	if mid != nil {
		return new(big.Rat).Set(mid)
	}
	sum := new(big.Rat).Add(bid, ask)
	return sum.Quo(sum, big.NewRat(2, 1))
}

func invertPriceSet(ps priceSet) (priceSet, error) {
	if ps.ask.Sign() == 0 || ps.bid.Sign() == 0 {
		return priceSet{}, merrors.InvalidArgument("oracle: cannot invert zero price")
	}
	one := big.NewRat(1, 1)
	invBid := new(big.Rat).Quo(one, ps.ask)
	invAsk := new(big.Rat).Quo(one, ps.bid)
	var invMid *big.Rat
	if ps.mid != nil && ps.mid.Sign() != 0 {
		invMid = new(big.Rat).Quo(one, ps.mid)
	} else {
		invMid = averageOrMid(invBid, invAsk, nil)
	}
	result := priceSet{
		bid: invBid,
		ask: invAsk,
		mid: invMid,
	}
	if result.ask.Cmp(result.bid) < 0 {
		result.ask, result.bid = result.bid, result.ask
	}
	return result, nil
}

func multiplyPriceSets(a, b priceSet) priceSet {
	result := priceSet{
		bid: mulRat(a.bid, b.bid),
		ask: mulRat(a.ask, b.ask),
	}
	result.mid = averageOrMid(result.bid, result.ask, nil)
	return result
}

func calcSpreadBps(ps priceSet) *big.Rat {
	if ps.mid == nil || ps.mid.Sign() == 0 {
		return nil
	}
	spread := new(big.Rat).Sub(ps.ask, ps.bid)
	if spread.Sign() < 0 {
		spread.Neg(spread)
	}
	spread.Quo(spread, ps.mid)
	spread.Mul(spread, big.NewRat(10000, 1))
	return spread
}

func minNonZero(values ...int64) int64 {
	var result int64
	for _, v := range values {
		if v <= 0 {
			continue
		}
		if result == 0 || v < result {
			result = v
		}
	}
	return result
}

func formatPrice(r *big.Rat) string {
	if r == nil {
		return ""
	}
	return r.FloatString(8)
}
