package orchestrator

import (
	"strings"

	"github.com/shopspring/decimal"
	oracleclient "github.com/tech/sendico/fx/oracle/client"
	"github.com/tech/sendico/pkg/merrors"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"google.golang.org/protobuf/proto"

	feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
	gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
	accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
)

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

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 computeAggregates(base, fee *moneyv1.Money, network *gatewayv1.EstimateTransferFeeResponse) (*moneyv1.Money, *moneyv1.Money) {
	if base == nil {
		return nil, nil
	}
	baseDecimal, err := decimalFromMoney(base)
	if err != nil {
		return cloneMoney(base), cloneMoney(base)
	}
	debit := baseDecimal
	settlement := baseDecimal

	if feeDecimal, err := decimalFromMoneyMatching(base, fee); err == nil && feeDecimal != nil {
		debit = debit.Add(*feeDecimal)
		settlement = settlement.Sub(*feeDecimal)
	}

	if network != nil && network.GetNetworkFee() != nil {
		if networkDecimal, err := decimalFromMoneyMatching(base, network.GetNetworkFee()); err == nil && networkDecimal != nil {
			debit = debit.Add(*networkDecimal)
			settlement = settlement.Sub(*networkDecimal)
		}
	}

	return makeMoney(base.GetCurrency(), debit), makeMoney(base.GetCurrency(), settlement)
}

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

func decimalFromMoneyMatching(reference, candidate *moneyv1.Money) (*decimal.Decimal, error) {
	if reference == nil || candidate == nil {
		return nil, nil
	}
	if !strings.EqualFold(reference.GetCurrency(), candidate.GetCurrency()) {
		return nil, nil
	}
	value, err := decimal.NewFromString(candidate.GetAmount())
	if err != nil {
		return nil, err
	}
	return &value, nil
}

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

func quoteToProto(src *oracleclient.Quote) *oraclev1.Quote {
	if src == nil {
		return nil
	}
	return &oraclev1.Quote{
		QuoteRef:        src.QuoteRef,
		Pair:            src.Pair,
		Side:            src.Side,
		Price:           &moneyv1.Decimal{Value: src.Price},
		BaseAmount:      cloneMoney(src.BaseAmount),
		QuoteAmount:     cloneMoney(src.QuoteAmount),
		ExpiresAtUnixMs: src.ExpiresAt.UnixMilli(),
		Provider:        src.Provider,
		RateRef:         src.RateRef,
		Firm:            src.Firm,
	}
}

func ledgerChargesFromFeeLines(lines []*feesv1.DerivedPostingLine) []*ledgerv1.PostingLine {
	if len(lines) == 0 {
		return nil
	}
	charges := make([]*ledgerv1.PostingLine, 0, len(lines))
	for _, line := range lines {
		if line == nil || strings.TrimSpace(line.GetLedgerAccountRef()) == "" {
			continue
		}
		money := cloneMoney(line.GetMoney())
		if money == nil {
			continue
		}
		charges = append(charges, &ledgerv1.PostingLine{
			LedgerAccountRef: strings.TrimSpace(line.GetLedgerAccountRef()),
			Money:            money,
			LineType:         ledgerLineTypeFromAccounting(line.GetLineType()),
		})
	}
	if len(charges) == 0 {
		return nil
	}
	return charges
}

func ledgerLineTypeFromAccounting(lineType accountingv1.PostingLineType) ledgerv1.LineType {
	switch lineType {
	case accountingv1.PostingLineType_POSTING_LINE_SPREAD:
		return ledgerv1.LineType_LINE_SPREAD
	case accountingv1.PostingLineType_POSTING_LINE_REVERSAL:
		return ledgerv1.LineType_LINE_REVERSAL
	case accountingv1.PostingLineType_POSTING_LINE_FEE,
		accountingv1.PostingLineType_POSTING_LINE_TAX:
		return ledgerv1.LineType_LINE_FEE
	default:
		return ledgerv1.LineType_LINE_MAIN
	}
}

func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*gatewayv1.ServiceFeeBreakdown {
	if quote == nil {
		return nil
	}
	lines := quote.GetFeeLines()
	breakdown := make([]*gatewayv1.ServiceFeeBreakdown, 0, len(lines)+1)
	for _, line := range lines {
		if line == nil {
			continue
		}
		amount := cloneMoney(line.GetMoney())
		if amount == nil {
			continue
		}
		code := strings.TrimSpace(line.GetMeta()["fee_code"])
		if code == "" {
			code = strings.TrimSpace(line.GetMeta()["fee_rule_id"])
		}
		if code == "" {
			code = line.GetLineType().String()
		}
		desc := strings.TrimSpace(line.GetMeta()["description"])
		breakdown = append(breakdown, &gatewayv1.ServiceFeeBreakdown{
			FeeCode:     code,
			Amount:      amount,
			Description: desc,
		})
	}
	if quote.GetNetworkFee() != nil && quote.GetNetworkFee().GetNetworkFee() != nil {
		networkAmount := cloneMoney(quote.GetNetworkFee().GetNetworkFee())
		if networkAmount != nil {
			breakdown = append(breakdown, &gatewayv1.ServiceFeeBreakdown{
				FeeCode:     "network_fee",
				Amount:      networkAmount,
				Description: strings.TrimSpace(quote.GetNetworkFee().GetEstimationContext()),
			})
		}
	}
	if len(breakdown) == 0 {
		return nil
	}
	return breakdown
}

func moneyEquals(a, b *moneyv1.Money) bool {
	if a == nil || b == nil {
		return false
	}
	if !strings.EqualFold(a.GetCurrency(), b.GetCurrency()) {
		return false
	}
	return strings.TrimSpace(a.GetAmount()) == strings.TrimSpace(b.GetAmount())
}

func conversionAmountFromMetadata(meta map[string]string, fx *orchestratorv1.FXIntent) (*moneyv1.Money, error) {
	if meta == nil {
		meta = map[string]string{}
	}
	amount := strings.TrimSpace(meta["amount"])
	if amount == "" {
		return nil, merrors.InvalidArgument("conversion amount metadata is required")
	}
	currency := strings.TrimSpace(meta["currency"])
	if currency == "" && fx != nil && fx.GetPair() != nil {
		currency = strings.TrimSpace(fx.GetPair().GetBase())
	}
	if currency == "" {
		return nil, merrors.InvalidArgument("conversion currency metadata is required")
	}
	return &moneyv1.Money{
		Currency: currency,
		Amount:   amount,
	}, nil
}
