package orchestrator

import (
	"strings"
	"time"

	"github.com/shopspring/decimal"
	oracleclient "github.com/tech/sendico/fx/oracle/client"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/payments/rail"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	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"
	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"
)

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

func cloneProtoMoney(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 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 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, mode orchestratorv1.SettlementMode) (*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 mode {
	case orchestratorv1.SettlementMode_SETTLEMENT_FIX_RECEIVED:
		// Sender pays the fee: keep settlement fixed, increase debit.
		applyChargeToDebit(fee)
	default:
		// Recipient pays the fee (default): reduce settlement, keep debit fixed.
		applyChargeToSettlement(fee)
	}

	if network != nil && network.GetNetworkFee() != nil {
		switch mode {
		case orchestratorv1.SettlementMode_SETTLEMENT_FIX_RECEIVED:
			applyChargeToDebit(network.GetNetworkFee())
		default:
			applyChargeToSettlement(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
	}
	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
	}

	base := strings.TrimSpace(quote.GetPair().GetBase())
	qt := strings.TrimSpace(quote.GetPair().GetQuote())
	if base == "" || qt == "" || strings.TrimSpace(targetCurrency) == "" {
		return nil, nil
	}

	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
	}
}

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:      cloneProtoMoney(src.BaseAmount),
		QuoteAmount:     cloneProtoMoney(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 := cloneProtoMoney(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 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 feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []rail.FeeBreakdown {
	if quote == nil {
		return nil
	}
	lines := quote.GetFeeLines()
	breakdown := make([]rail.FeeBreakdown, 0, len(lines)+1)
	for _, line := range lines {
		if line == nil {
			continue
		}
		amount := moneyFromProto(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, rail.FeeBreakdown{
			FeeCode:     code,
			Amount:      amount,
			Description: desc,
		})
	}
	if quote.GetNetworkFee() != nil && quote.GetNetworkFee().GetNetworkFee() != nil {
		networkAmount := moneyFromProto(quote.GetNetworkFee().GetNetworkFee())
		if networkAmount != nil {
			breakdown = append(breakdown, rail.FeeBreakdown{
				FeeCode:     "network_fee",
				Amount:      networkAmount,
				Description: strings.TrimSpace(quote.GetNetworkFee().GetEstimationContext()),
			})
		}
	}
	if len(breakdown) == 0 {
		return nil
	}
	return breakdown
}

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

func moneyEquals(a, b moneyGetter) 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
}
