package plan

import (
	"strings"
	"time"

	"github.com/shopspring/decimal"
	"github.com/tech/sendico/payments/quotation/internal/shared"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	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"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
	"google.golang.org/protobuf/types/known/timestamppb"
)

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

func cloneMoney(input *paymenttypes.Money) *paymenttypes.Money {
	return shared.CloneModelMoneyRaw(input)
}

func cloneStringList(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	result := make([]string, 0, len(values))
	for _, value := range values {
		clean := strings.TrimSpace(value)
		if clean == "" {
			continue
		}
		result = append(result, clean)
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

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] = strings.TrimSpace(v)
	}
	return clone
}

func attributeLookup(attrs map[string]string, keys ...string) string {
	if len(attrs) == 0 || len(keys) == 0 {
		return ""
	}
	for _, key := range keys {
		needle := strings.ToLower(strings.TrimSpace(key))
		if needle == "" {
			continue
		}
		for attrKey, value := range attrs {
			if strings.EqualFold(strings.TrimSpace(attrKey), needle) {
				if val := strings.TrimSpace(value); val != "" {
					return val
				}
			}
		}
	}
	return ""
}

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

func moneyFromProto(m *moneyv1.Money) *paymenttypes.Money {
	if m == nil {
		return nil
	}
	return &paymenttypes.Money{Currency: m.GetCurrency(), Amount: m.GetAmount()}
}

func protoMoney(m *paymenttypes.Money) *moneyv1.Money {
	if m == nil {
		return nil
	}
	return &moneyv1.Money{Currency: m.GetCurrency(), Amount: m.GetAmount()}
}

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

func decimalFromProto(value *moneyv1.Decimal) *paymenttypes.Decimal {
	if value == nil {
		return nil
	}
	return &paymenttypes.Decimal{Value: value.GetValue()}
}

func decimalToProto(value *paymenttypes.Decimal) *moneyv1.Decimal {
	if value == nil {
		return nil
	}
	return &moneyv1.Decimal{Value: value.GetValue()}
}

func pairFromProto(pair *fxv1.CurrencyPair) *paymenttypes.CurrencyPair {
	if pair == nil {
		return nil
	}
	return &paymenttypes.CurrencyPair{Base: pair.GetBase(), Quote: pair.GetQuote()}
}

func pairToProto(pair *paymenttypes.CurrencyPair) *fxv1.CurrencyPair {
	if pair == nil {
		return nil
	}
	return &fxv1.CurrencyPair{Base: pair.GetBase(), Quote: pair.GetQuote()}
}

func fxSideFromProto(side fxv1.Side) paymenttypes.FXSide {
	switch side {
	case fxv1.Side_BUY_BASE_SELL_QUOTE:
		return paymenttypes.FXSideBuyBaseSellQuote
	case fxv1.Side_SELL_BASE_BUY_QUOTE:
		return paymenttypes.FXSideSellBaseBuyQuote
	default:
		return paymenttypes.FXSideUnspecified
	}
}

func fxSideToProto(side paymenttypes.FXSide) fxv1.Side {
	switch side {
	case paymenttypes.FXSideBuyBaseSellQuote:
		return fxv1.Side_BUY_BASE_SELL_QUOTE
	case paymenttypes.FXSideSellBaseBuyQuote:
		return fxv1.Side_SELL_BASE_BUY_QUOTE
	default:
		return fxv1.Side_SIDE_UNSPECIFIED
	}
}

func fxQuoteFromProto(quote *oraclev1.Quote) *paymenttypes.FXQuote {
	if quote == nil {
		return nil
	}
	pricedAtUnixMs := int64(0)
	if ts := quote.GetPricedAt(); ts != nil {
		pricedAtUnixMs = ts.AsTime().UnixMilli()
	}
	return &paymenttypes.FXQuote{
		QuoteRef:        strings.TrimSpace(quote.GetQuoteRef()),
		Pair:            pairFromProto(quote.GetPair()),
		Side:            fxSideFromProto(quote.GetSide()),
		Price:           decimalFromProto(quote.GetPrice()),
		BaseAmount:      moneyFromProto(quote.GetBaseAmount()),
		QuoteAmount:     moneyFromProto(quote.GetQuoteAmount()),
		ExpiresAtUnixMs: quote.GetExpiresAtUnixMs(),
		PricedAtUnixMs:  pricedAtUnixMs,
		Provider:        strings.TrimSpace(quote.GetProvider()),
		RateRef:         strings.TrimSpace(quote.GetRateRef()),
		Firm:            quote.GetFirm(),
	}
}

func fxQuoteToProto(quote *paymenttypes.FXQuote) *oraclev1.Quote {
	if quote == nil {
		return nil
	}
	var pricedAt *timestamppb.Timestamp
	if quote.PricedAtUnixMs > 0 {
		pricedAt = timestamppb.New(time.UnixMilli(quote.PricedAtUnixMs).UTC())
	}
	return &oraclev1.Quote{
		QuoteRef:        strings.TrimSpace(quote.QuoteRef),
		Pair:            pairToProto(quote.Pair),
		Side:            fxSideToProto(quote.Side),
		Price:           decimalToProto(quote.Price),
		BaseAmount:      protoMoney(quote.BaseAmount),
		QuoteAmount:     protoMoney(quote.QuoteAmount),
		ExpiresAtUnixMs: quote.ExpiresAtUnixMs,
		PricedAt:        pricedAt,
		Provider:        strings.TrimSpace(quote.Provider),
		RateRef:         strings.TrimSpace(quote.RateRef),
		Firm:            quote.Firm,
	}
}

func entrySideFromProto(side accountingv1.EntrySide) paymenttypes.EntrySide {
	switch side {
	case accountingv1.EntrySide_ENTRY_SIDE_DEBIT:
		return paymenttypes.EntrySideDebit
	case accountingv1.EntrySide_ENTRY_SIDE_CREDIT:
		return paymenttypes.EntrySideCredit
	default:
		return paymenttypes.EntrySideUnspecified
	}
}

func entrySideToProto(side paymenttypes.EntrySide) accountingv1.EntrySide {
	switch side {
	case paymenttypes.EntrySideDebit:
		return accountingv1.EntrySide_ENTRY_SIDE_DEBIT
	case paymenttypes.EntrySideCredit:
		return accountingv1.EntrySide_ENTRY_SIDE_CREDIT
	default:
		return accountingv1.EntrySide_ENTRY_SIDE_UNSPECIFIED
	}
}

func postingLineTypeFromProto(lineType accountingv1.PostingLineType) paymenttypes.PostingLineType {
	switch lineType {
	case accountingv1.PostingLineType_POSTING_LINE_FEE:
		return paymenttypes.PostingLineTypeFee
	case accountingv1.PostingLineType_POSTING_LINE_TAX:
		return paymenttypes.PostingLineTypeTax
	case accountingv1.PostingLineType_POSTING_LINE_SPREAD:
		return paymenttypes.PostingLineTypeSpread
	case accountingv1.PostingLineType_POSTING_LINE_REVERSAL:
		return paymenttypes.PostingLineTypeReversal
	default:
		return paymenttypes.PostingLineTypeUnspecified
	}
}

func postingLineTypeToProto(lineType paymenttypes.PostingLineType) accountingv1.PostingLineType {
	switch lineType {
	case paymenttypes.PostingLineTypeFee:
		return accountingv1.PostingLineType_POSTING_LINE_FEE
	case paymenttypes.PostingLineTypeTax:
		return accountingv1.PostingLineType_POSTING_LINE_TAX
	case paymenttypes.PostingLineTypeSpread:
		return accountingv1.PostingLineType_POSTING_LINE_SPREAD
	case paymenttypes.PostingLineTypeReversal:
		return accountingv1.PostingLineType_POSTING_LINE_REVERSAL
	default:
		return accountingv1.PostingLineType_POSTING_LINE_TYPE_UNSPECIFIED
	}
}

func feeLinesFromProto(lines []*feesv1.DerivedPostingLine) []*paymenttypes.FeeLine {
	if len(lines) == 0 {
		return nil
	}
	result := make([]*paymenttypes.FeeLine, 0, len(lines))
	for _, line := range lines {
		if line == nil {
			continue
		}
		result = append(result, &paymenttypes.FeeLine{
			LedgerAccountRef: strings.TrimSpace(line.GetLedgerAccountRef()),
			Money:            moneyFromProto(line.GetMoney()),
			LineType:         postingLineTypeFromProto(line.GetLineType()),
			Side:             entrySideFromProto(line.GetSide()),
			Meta:             cloneMetadata(line.GetMeta()),
		})
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func feeLinesToProto(lines []*paymenttypes.FeeLine) []*feesv1.DerivedPostingLine {
	if len(lines) == 0 {
		return nil
	}
	result := make([]*feesv1.DerivedPostingLine, 0, len(lines))
	for _, line := range lines {
		if line == nil {
			continue
		}
		result = append(result, &feesv1.DerivedPostingLine{
			LedgerAccountRef: strings.TrimSpace(line.LedgerAccountRef),
			Money:            protoMoney(line.Money),
			LineType:         postingLineTypeToProto(line.LineType),
			Side:             entrySideToProto(line.Side),
			Meta:             cloneMetadata(line.Meta),
		})
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func executionQuote(payment *model.Payment, quote *sharedv1.PaymentQuote) *sharedv1.PaymentQuote {
	if quote != nil {
		return quote
	}
	if payment != nil && payment.LastQuote != nil {
		return &sharedv1.PaymentQuote{
			DebitAmount:              protoMoney(payment.LastQuote.DebitAmount),
			DebitSettlementAmount:    protoMoney(payment.LastQuote.DebitSettlementAmount),
			ExpectedSettlementAmount: protoMoney(payment.LastQuote.ExpectedSettlementAmount),
			ExpectedFeeTotal:         protoMoney(payment.LastQuote.ExpectedFeeTotal),
			FeeLines:                 feeLinesToProto(payment.LastQuote.FeeLines),
			FxQuote:                  fxQuoteToProto(payment.LastQuote.FXQuote),
			QuoteRef:                 strings.TrimSpace(payment.LastQuote.QuoteRef),
		}
	}
	return &sharedv1.PaymentQuote{}
}

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 cardPayoutAmount(payment *model.Payment) (*paymenttypes.Money, error) {
	if payment == nil {
		return nil, merrors.InvalidArgument("payment is required")
	}
	amount := cloneMoney(payment.Intent.Amount)
	if payment.LastQuote != nil {
		settlement := payment.LastQuote.ExpectedSettlementAmount
		if settlement != nil && strings.TrimSpace(settlement.GetAmount()) != "" && strings.TrimSpace(settlement.GetCurrency()) != "" {
			amount = cloneMoney(settlement)
		}
	}
	if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
		return nil, merrors.InvalidArgument("card payout: amount is required")
	}
	return amount, nil
}
