package oracle

import (
	"math/big"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/tech/sendico/fx/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
)

type quoteComputation struct {
	pair         *model.Pair
	rate         *model.RateSnapshot
	sideProto    fxv1.Side
	sideModel    model.QuoteSide
	price        *big.Rat
	baseInput    *big.Rat
	quoteInput   *big.Rat
	amountType   model.QuoteAmountType
	baseRounded  *big.Rat
	quoteRounded *big.Rat
	priceRounded *big.Rat
	baseScale    uint32
	quoteScale   uint32
	priceScale   uint32
	provider     string
}

func newQuoteComputation(pair *model.Pair, rate *model.RateSnapshot, side fxv1.Side, provider string) (*quoteComputation, error) {
	if pair == nil || rate == nil {
		return nil, merrors.InvalidArgument("oracle: missing pair or rate")
	}
	sideModel := protoSideToModel(side)
	if sideModel == "" {
		return nil, merrors.InvalidArgument("oracle: unsupported side")
	}
	price, err := priceFromRate(rate, side)
	if err != nil {
		return nil, err
	}
	if strings.TrimSpace(provider) == "" {
		provider = rate.Provider
	}
	return &quoteComputation{
		pair:       pair,
		rate:       rate,
		sideProto:  side,
		sideModel:  sideModel,
		price:      price,
		baseScale:  pair.BaseMeta.Decimals,
		quoteScale: pair.QuoteMeta.Decimals,
		priceScale: pair.QuoteMeta.Decimals,
		provider:   provider,
	}, nil
}

func (qc *quoteComputation) withBaseInput(m *moneyv1.Money) error {
	if m == nil {
		return merrors.InvalidArgument("oracle: base amount missing")
	}
	if !strings.EqualFold(m.GetCurrency(), qc.pair.Pair.Base) {
		return merrors.InvalidArgument("oracle: base amount currency mismatch")
	}
	val, err := ratFromString(m.GetAmount())
	if err != nil {
		return err
	}
	qc.baseInput = val
	qc.amountType = model.QuoteAmountTypeBase
	return nil
}

func (qc *quoteComputation) withQuoteInput(m *moneyv1.Money) error {
	if m == nil {
		return merrors.InvalidArgument("oracle: quote amount missing")
	}
	if !strings.EqualFold(m.GetCurrency(), qc.pair.Pair.Quote) {
		return merrors.InvalidArgument("oracle: quote amount currency mismatch")
	}
	val, err := ratFromString(m.GetAmount())
	if err != nil {
		return err
	}
	qc.quoteInput = val
	qc.amountType = model.QuoteAmountTypeQuote
	return nil
}

func (qc *quoteComputation) compute() error {
	var baseRaw, quoteRaw *big.Rat
	switch qc.amountType {
	case model.QuoteAmountTypeBase:
		baseRaw = qc.baseInput
		quoteRaw = mulRat(qc.baseInput, qc.price)
	case model.QuoteAmountTypeQuote:
		quoteRaw = qc.quoteInput
		base, err := divRat(qc.quoteInput, qc.price)
		if err != nil {
			return err
		}
		baseRaw = base
	default:
		return merrors.InvalidArgument("oracle: amount type not set")
	}

	var err error
	qc.baseRounded, err = roundRatToScale(baseRaw, qc.baseScale, qc.pair.BaseMeta.Rounding)
	if err != nil {
		return err
	}
	qc.quoteRounded, err = roundRatToScale(quoteRaw, qc.quoteScale, qc.pair.QuoteMeta.Rounding)
	if err != nil {
		return err
	}
	qc.priceRounded, err = roundRatToScale(qc.price, qc.priceScale, qc.pair.QuoteMeta.Rounding)
	if err != nil {
		return err
	}
	return nil
}

func (qc *quoteComputation) buildModelQuote(firm bool, expiryMillis int64, req *oraclev1.GetQuoteRequest) (*model.Quote, error) {
	if qc.baseRounded == nil || qc.quoteRounded == nil || qc.priceRounded == nil {
		return nil, merrors.Internal("oracle: computation not executed")
	}
	pricedAtUnixMs := qc.rate.AsOfUnixMs
	if pricedAtUnixMs <= 0 {
		pricedAtUnixMs = time.Now().UnixMilli()
	}

	quote := &model.Quote{
		QuoteRef: uuid.NewString(),
		Firm:     firm,
		Status:   model.QuoteStatusIssued,
		Pair:     qc.pair.Pair,
		Side:     qc.sideModel,
		Price:    formatRat(qc.priceRounded, qc.priceScale),
		BaseAmount: paymenttypes.Money{
			Currency: qc.pair.Pair.Base,
			Amount:   formatRat(qc.baseRounded, qc.baseScale),
		},
		QuoteAmount: paymenttypes.Money{
			Currency: qc.pair.Pair.Quote,
			Amount:   formatRat(qc.quoteRounded, qc.quoteScale),
		},
		AmountType:        qc.amountType,
		PricedAtUnixMs:    pricedAtUnixMs,
		RateRef:           qc.rate.RateRef,
		Provider:          qc.provider,
		PreferredProvider: req.GetPreferredProvider(),
		RequestedTTLMs:    req.GetTtlMs(),
		MaxAgeToleranceMs: int64(req.GetMaxAgeMs()),
		Meta:              buildQuoteMeta(req.GetMeta()),
	}

	if firm {
		quote.ExpiresAtUnixMs = expiryMillis
		expiry := time.UnixMilli(expiryMillis)
		quote.ExpiresAt = &expiry
	}

	return quote, nil
}

func buildQuoteMeta(meta *oraclev1.RequestMeta) *model.QuoteMeta {
	if meta == nil {
		return nil
	}
	trace := meta.GetTrace()
	qm := &model.QuoteMeta{
		TenantRef: meta.GetTenantRef(),
	}

	if trace != nil {
		qm.RequestRef = trace.GetRequestRef()
		qm.TraceRef = trace.GetTraceRef()
		qm.IdempotencyKey = strings.TrimSpace(trace.GetIdempotencyKey())
	}
	if org := strings.TrimSpace(meta.GetOrganizationRef()); org != "" {
		if objID, err := bson.ObjectIDFromHex(org); err == nil {
			qm.SetOrganizationRef(objID)
		}
	}
	return qm
}

func protoSideToModel(side fxv1.Side) model.QuoteSide {
	switch side {
	case fxv1.Side_BUY_BASE_SELL_QUOTE:
		return model.QuoteSideBuyBaseSellQuote
	case fxv1.Side_SELL_BASE_BUY_QUOTE:
		return model.QuoteSideSellBaseBuyQuote
	default:
		return ""
	}
}

func computeExpiry(now time.Time, ttlMs int64) (int64, error) {
	if ttlMs <= 0 {
		return 0, merrors.InvalidArgument("oracle: ttl must be positive")
	}
	return now.Add(time.Duration(ttlMs) * time.Millisecond).UnixMilli(), nil
}
