package quotation

import (
	"strings"
	"time"

	"github.com/shopspring/decimal"
	oracleclient "github.com/tech/sendico/fx/oracle/client"
	qshared "github.com/tech/sendico/payments/quotation/internal/shared"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/timestamppb"

	feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
	accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
	fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
)

type moneyGetter interface {
	GetAmount() string
	GetCurrency() string
}

const (
	feeLineMetaTarget     = "fee_target"
	feeLineTargetWallet   = "wallet"
	feeLineMetaWalletRef  = "fee_wallet_ref"
	feeLineMetaWalletType = "fee_wallet_type"
)

func cloneProtoMoney(input *moneyv1.Money) *moneyv1.Money {
	return qshared.CloneProtoMoneyRaw(input)
}

func cloneMetadata(input map[string]string) map[string]string {
	if len(input) == 0 {
		return nil
	}
	clone := make(map[string]string, len(input))
	for k, v := range input {
		clone[k] = v
	}
	return clone
}

func cloneFeeLines(lines []*feesv1.DerivedPostingLine) []*feesv1.DerivedPostingLine {
	if len(lines) == 0 {
		return nil
	}
	out := make([]*feesv1.DerivedPostingLine, 0, len(lines))
	for _, line := range lines {
		if line == nil {
			continue
		}
		if cloned, ok := proto.Clone(line).(*feesv1.DerivedPostingLine); ok {
			out = append(out, cloned)
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func cloneFeeRules(rules []*feesv1.AppliedRule) []*feesv1.AppliedRule {
	if len(rules) == 0 {
		return nil
	}
	out := make([]*feesv1.AppliedRule, 0, len(rules))
	for _, rule := range rules {
		if rule == nil {
			continue
		}
		if cloned, ok := proto.Clone(rule).(*feesv1.AppliedRule); ok {
			out = append(out, cloned)
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func extractFeeTotal(lines []*feesv1.DerivedPostingLine, currency string) *moneyv1.Money {
	if len(lines) == 0 || currency == "" {
		return nil
	}
	total := decimal.Zero
	for _, line := range lines {
		if line == nil || line.GetMoney() == nil {
			continue
		}
		if !strings.EqualFold(line.GetMoney().GetCurrency(), currency) {
			continue
		}
		amount, err := decimal.NewFromString(line.GetMoney().GetAmount())
		if err != nil {
			continue
		}
		switch line.GetSide() {
		case accountingv1.EntrySide_ENTRY_SIDE_CREDIT:
			total = total.Sub(amount.Abs())
		default:
			total = total.Add(amount.Abs())
		}
	}
	if total.IsZero() {
		return nil
	}
	return &moneyv1.Money{
		Currency: currency,
		Amount:   total.String(),
	}
}

func resolveTradeAmounts(intentAmount *moneyv1.Money, fxQuote *oraclev1.Quote, side fxv1.Side) (*moneyv1.Money, *moneyv1.Money) {
	if fxQuote == nil {
		return cloneProtoMoney(intentAmount), cloneProtoMoney(intentAmount)
	}
	qSide := fxQuote.GetSide()
	if qSide == fxv1.Side_SIDE_UNSPECIFIED {
		qSide = side
	}

	switch qSide {
	case fxv1.Side_BUY_BASE_SELL_QUOTE:
		pay := cloneProtoMoney(fxQuote.GetQuoteAmount())
		settle := cloneProtoMoney(fxQuote.GetBaseAmount())
		if pay == nil {
			pay = cloneProtoMoney(intentAmount)
		}
		if settle == nil {
			settle = cloneProtoMoney(intentAmount)
		}
		return pay, settle
	case fxv1.Side_SELL_BASE_BUY_QUOTE:
		pay := cloneProtoMoney(fxQuote.GetBaseAmount())
		settle := cloneProtoMoney(fxQuote.GetQuoteAmount())
		if pay == nil {
			pay = cloneProtoMoney(intentAmount)
		}
		if settle == nil {
			settle = cloneProtoMoney(intentAmount)
		}
		return pay, settle
	default:
		return cloneProtoMoney(intentAmount), cloneProtoMoney(intentAmount)
	}
}

func computeAggregates(
	pay,
	settlement,
	fee *moneyv1.Money,
	network *chainv1.EstimateTransferFeeResponse,
	fxQuote *oraclev1.Quote,
	feeTreatment quotationv2.FeeTreatment,
) (*moneyv1.Money, *moneyv1.Money) {
	if pay == nil {
		return nil, nil
	}
	debitDecimal, err := decimalFromMoney(pay)
	if err != nil {
		return cloneProtoMoney(pay), cloneProtoMoney(settlement)
	}

	settlementCurrency := pay.GetCurrency()
	if settlement != nil && strings.TrimSpace(settlement.GetCurrency()) != "" {
		settlementCurrency = settlement.GetCurrency()
	}

	settlementDecimal := debitDecimal
	if settlement != nil {
		if val, err := decimalFromMoney(settlement); err == nil {
			settlementDecimal = val
		}
	}

	applyChargeToDebit := func(m *moneyv1.Money) {
		converted, err := ensureCurrency(m, pay.GetCurrency(), fxQuote)
		if err != nil || converted == nil {
			return
		}
		if val, err := decimalFromMoney(converted); err == nil {
			debitDecimal = debitDecimal.Add(val)
		}
	}

	applyChargeToSettlement := func(m *moneyv1.Money) {
		converted, err := ensureCurrency(m, settlementCurrency, fxQuote)
		if err != nil || converted == nil {
			return
		}
		if val, err := decimalFromMoney(converted); err == nil {
			settlementDecimal = settlementDecimal.Sub(val)
		}
	}

	switch feeTreatment {
	case quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION:
		applyChargeToSettlement(fee)
	default:
		// Default to payer-covers-fee when fee_treatment is omitted.
		applyChargeToDebit(fee)
	}

	if network != nil && network.GetNetworkFee() != nil {
		switch feeTreatment {
		case quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION:
			applyChargeToSettlement(network.GetNetworkFee())
		default:
			applyChargeToDebit(network.GetNetworkFee())
		}
	}

	return makeMoney(pay.GetCurrency(), debitDecimal), makeMoney(settlementCurrency, settlementDecimal)
}

func decimalFromMoney(m moneyGetter) (decimal.Decimal, error) {
	if m == nil {
		return decimal.Zero, nil
	}
	return decimal.NewFromString(m.GetAmount())
}

func makeMoney(currency string, value decimal.Decimal) *moneyv1.Money {
	return &moneyv1.Money{
		Currency: currency,
		Amount:   value.String(),
	}
}

func ensureCurrency(m *moneyv1.Money, targetCurrency string, quote *oraclev1.Quote) (*moneyv1.Money, error) {
	if m == nil || strings.TrimSpace(targetCurrency) == "" {
		return nil, nil //nolint:nilnil // nil means no amount available for conversion
	}
	if strings.EqualFold(m.GetCurrency(), targetCurrency) {
		return cloneProtoMoney(m), nil
	}
	return convertWithQuote(m, quote, targetCurrency)
}

func convertWithQuote(m *moneyv1.Money, quote *oraclev1.Quote, targetCurrency string) (*moneyv1.Money, error) {
	if m == nil || quote == nil || quote.GetPair() == nil || quote.GetPrice() == nil {
		return nil, nil //nolint:nilnil // nil means conversion cannot be performed with available quote
	}

	base := strings.TrimSpace(quote.GetPair().GetBase())
	qt := strings.TrimSpace(quote.GetPair().GetQuote())
	if base == "" || qt == "" || strings.TrimSpace(targetCurrency) == "" {
		return nil, nil //nolint:nilnil // nil means conversion cannot be performed with incomplete pair
	}

	price, err := decimal.NewFromString(quote.GetPrice().GetValue())
	if err != nil || price.IsZero() {
		return nil, err
	}
	value, err := decimalFromMoney(m)
	if err != nil {
		return nil, err
	}

	switch {
	case strings.EqualFold(m.GetCurrency(), base) && strings.EqualFold(targetCurrency, qt):
		return makeMoney(targetCurrency, value.Mul(price)), nil
	case strings.EqualFold(m.GetCurrency(), qt) && strings.EqualFold(targetCurrency, base):
		return makeMoney(targetCurrency, value.Div(price)), nil
	default:
		return nil, nil //nolint:nilnil // nil means quote pair does not match requested conversion
	}
}

func quoteToProto(src *oracleclient.Quote) *oraclev1.Quote {
	if src == nil {
		return nil
	}
	var pricedAt *timestamppb.Timestamp
	if !src.PricedAt.IsZero() {
		pricedAt = timestamppb.New(src.PricedAt.UTC())
	}
	return &oraclev1.Quote{
		QuoteRef:        src.QuoteRef,
		Pair:            src.Pair,
		Side:            src.Side,
		Price:           &moneyv1.Decimal{Value: src.Price},
		BaseAmount:      cloneProtoMoney(src.BaseAmount),
		QuoteAmount:     cloneProtoMoney(src.QuoteAmount),
		ExpiresAtUnixMs: src.ExpiresAt.UnixMilli(),
		PricedAt:        pricedAt,
		Provider:        src.Provider,
		RateRef:         src.RateRef,
		Firm:            src.Firm,
	}
}

func setFeeLineTarget(lines []*feesv1.DerivedPostingLine, target string) {
	target = strings.TrimSpace(target)
	if target == "" || len(lines) == 0 {
		return
	}
	for _, line := range lines {
		if line == nil {
			continue
		}
		if line.Meta == nil {
			line.Meta = map[string]string{}
		}
		line.Meta[feeLineMetaTarget] = target
		if strings.EqualFold(target, feeLineTargetWallet) {
			line.LedgerAccountRef = ""
		}
	}
}

func feeLineTarget(line *feesv1.DerivedPostingLine) string {
	if line == nil {
		return ""
	}
	return strings.TrimSpace(line.GetMeta()[feeLineMetaTarget])
}

func isWalletTargetFeeLine(line *feesv1.DerivedPostingLine) bool {
	return strings.EqualFold(feeLineTarget(line), feeLineTargetWallet)
}

func setFeeLineWalletRef(lines []*feesv1.DerivedPostingLine, walletRef, walletType string) {
	walletRef = strings.TrimSpace(walletRef)
	walletType = strings.TrimSpace(walletType)
	if walletRef == "" || len(lines) == 0 {
		return
	}
	for _, line := range lines {
		if line == nil {
			continue
		}
		if line.Meta == nil {
			line.Meta = map[string]string{}
		}
		line.Meta[feeLineMetaWalletRef] = walletRef
		if walletType != "" {
			line.Meta[feeLineMetaWalletType] = walletType
		}
	}
}

func quoteExpiry(now time.Time, feeQuote *feesv1.PrecomputeFeesResponse, fxQuote *oraclev1.Quote) time.Time {
	expiry := time.Time{}
	if feeQuote != nil && feeQuote.GetExpiresAt() != nil {
		expiry = feeQuote.GetExpiresAt().AsTime()
	}
	if expiry.IsZero() {
		expiry = now.Add(time.Duration(defaultFeeQuoteTTLMillis) * time.Millisecond)
	}
	if fxQuote != nil && fxQuote.GetExpiresAtUnixMs() > 0 {
		fxExpiry := time.UnixMilli(fxQuote.GetExpiresAtUnixMs()).UTC()
		if fxExpiry.Before(expiry) {
			expiry = fxExpiry
		}
	}
	return expiry
}

func assignLedgerAccounts(lines []*feesv1.DerivedPostingLine, account string) []*feesv1.DerivedPostingLine {
	if account == "" || len(lines) == 0 {
		return lines
	}
	for _, line := range lines {
		if line == nil || isWalletTargetFeeLine(line) {
			continue
		}
		if strings.TrimSpace(line.GetLedgerAccountRef()) != "" {
			continue
		}
		line.LedgerAccountRef = account
	}
	return lines
}
