package paymentapiimp

import (
	"strconv"
	"strings"

	"github.com/tech/sendico/pkg/merrors"
	pkgmodel "github.com/tech/sendico/pkg/model"
	payecon "github.com/tech/sendico/pkg/payments/economics"
	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"
	paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
	sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
	"github.com/tech/sendico/server/interface/api/srequest"
	"go.mongodb.org/mongo-driver/v2/bson"
)

func mapQuoteIntent(intent *srequest.PaymentIntent) (*quotationv2.QuoteIntent, error) {
	if intent == nil {
		return nil, merrors.InvalidArgument("intent is required")
	}
	if err := validatePaymentKind(intent.Kind); err != nil {
		return nil, err
	}

	settlementMode, err := mapSettlementMode(intent.SettlementMode)
	if err != nil {
		return nil, err
	}
	feeTreatment, err := mapFeeTreatment(intent.FeeTreatment)
	if err != nil {
		return nil, err
	}
	resolvedSettlementMode, resolvedFeeTreatment, err := payecon.ResolveSettlementAndFee(settlementMode, feeTreatment)
	if err != nil {
		return nil, err
	}
	settlementCurrency := resolveSettlementCurrency(intent)
	if settlementCurrency == "" {
		return nil, merrors.InvalidArgument("unable to derive settlement currency from intent")
	}

	source, err := mapQuoteEndpoint(intent.Source, "intent.source")
	if err != nil {
		return nil, err
	}
	destination, err := mapQuoteEndpoint(intent.Destination, "intent.destination")
	if err != nil {
		return nil, err
	}

	quoteIntent := &quotationv2.QuoteIntent{
		Source:             source,
		Destination:        destination,
		Amount:             mapMoney(intent.Amount),
		SettlementMode:     resolvedSettlementMode,
		FeeTreatment:       resolvedFeeTreatment,
		SettlementCurrency: settlementCurrency,
		Fx:                 mapFXIntent(intent),
		Comment:            strings.TrimSpace(intent.Comment),
	}
	return quoteIntent, nil
}

func mapFXIntent(intent *srequest.PaymentIntent) *sharedv1.FXIntent {
	if intent == nil || intent.FX == nil || intent.FX.Pair == nil {
		return nil
	}
	side := fxv1.Side_SIDE_UNSPECIFIED
	switch strings.TrimSpace(string(intent.FX.Side)) {
	case string(srequest.FXSideBuyBaseSellQuote):
		side = fxv1.Side_BUY_BASE_SELL_QUOTE
	case string(srequest.FXSideSellBaseBuyQuote):
		side = fxv1.Side_SELL_BASE_BUY_QUOTE
	}
	if side == fxv1.Side_SIDE_UNSPECIFIED {
		side = fxv1.Side_SELL_BASE_BUY_QUOTE
	}
	return &sharedv1.FXIntent{
		Pair: &fxv1.CurrencyPair{
			Base:  strings.ToUpper(strings.TrimSpace(intent.FX.Pair.Base)),
			Quote: strings.ToUpper(strings.TrimSpace(intent.FX.Pair.Quote)),
		},
		Side:              side,
		Firm:              intent.FX.Firm,
		TtlMs:             intent.FX.TTLms,
		PreferredProvider: strings.TrimSpace(intent.FX.PreferredProvider),
		MaxAgeMs:          intent.FX.MaxAgeMs,
	}
}

func validatePaymentKind(kind srequest.PaymentKind) error {
	switch strings.TrimSpace(string(kind)) {
	case string(srequest.PaymentKindPayout), string(srequest.PaymentKindInternalTransfer), string(srequest.PaymentKindFxConversion):
		return nil
	default:
		return merrors.InvalidArgument("unsupported payment kind: " + string(kind))
	}
}

func resolveSettlementCurrency(intent *srequest.PaymentIntent) string {
	if intent == nil {
		return ""
	}

	fx := intent.FX
	if fx != nil && fx.Pair != nil {
		base := strings.TrimSpace(fx.Pair.Base)
		quote := strings.TrimSpace(fx.Pair.Quote)
		switch strings.TrimSpace(string(fx.Side)) {
		case string(srequest.FXSideBuyBaseSellQuote):
			if base != "" {
				return base
			}
		case string(srequest.FXSideSellBaseBuyQuote):
			if quote != "" {
				return quote
			}
		}
	}

	if intent.Amount != nil {
		return strings.TrimSpace(intent.Amount.Currency)
	}
	return ""
}

func mapQuoteEndpoint(endpoint *srequest.Endpoint, field string) (*endpointv1.PaymentEndpoint, error) {
	if endpoint == nil {
		return nil, merrors.InvalidArgument(field + " is required")
	}

	switch endpoint.Type {
	case srequest.EndpointTypeLedger:
		payload, err := endpoint.DecodeLedger()
		if err != nil {
			return nil, merrors.InvalidArgument(field + ": " + err.Error())
		}
		method := &ledgerMethodData{
			LedgerAccountRef:       strings.TrimSpace(payload.LedgerAccountRef),
			ContraLedgerAccountRef: strings.TrimSpace(payload.ContraLedgerAccountRef),
		}
		if method.LedgerAccountRef == "" {
			return nil, merrors.InvalidArgument(field + ".ledger_account_ref is required")
		}
		return endpointFromMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER, method)

	case srequest.EndpointTypeManagedWallet:
		payload, err := endpoint.DecodeManagedWallet()
		if err != nil {
			return nil, merrors.InvalidArgument(field + ": " + err.Error())
		}
		method := &pkgmodel.WalletPaymentData{WalletID: strings.TrimSpace(payload.ManagedWalletRef)}
		if method.WalletID == "" {
			return nil, merrors.InvalidArgument(field + ".managed_wallet_ref is required")
		}
		return endpointFromMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET, method)

	case srequest.EndpointTypeWallet:
		payload, err := endpoint.DecodeWallet()
		if err != nil {
			return nil, merrors.InvalidArgument(field + ": " + err.Error())
		}
		method := &pkgmodel.WalletPaymentData{WalletID: strings.TrimSpace(payload.WalletID)}
		if method.WalletID == "" {
			return nil, merrors.InvalidArgument(field + ".walletId is required")
		}
		return endpointFromMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET, method)

	case srequest.EndpointTypeExternalChain:
		payload, err := endpoint.DecodeExternalChain()
		if err != nil {
			return nil, merrors.InvalidArgument(field + ": " + err.Error())
		}
		method, mapErr := mapExternalChainMethod(payload, field)
		if mapErr != nil {
			return nil, mapErr
		}
		return endpointFromMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS, method)

	case srequest.EndpointTypeCard:
		payload, err := endpoint.DecodeCard()
		if err != nil {
			return nil, merrors.InvalidArgument(field + ": " + err.Error())
		}
		method := &pkgmodel.CardPaymentData{
			Pan:       strings.TrimSpace(payload.Pan),
			FirstName: strings.TrimSpace(payload.FirstName),
			LastName:  strings.TrimSpace(payload.LastName),
			ExpMonth:  uint32ToString(payload.ExpMonth),
			ExpYear:   uint32ToString(payload.ExpYear),
			Country:   strings.TrimSpace(payload.Country),
		}
		if method.Pan == "" {
			return nil, merrors.InvalidArgument(field + ".pan is required")
		}
		return endpointFromMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD, method)

	case srequest.EndpointTypeCardToken:
		payload, err := endpoint.DecodeCardToken()
		if err != nil {
			return nil, merrors.InvalidArgument(field + ": " + err.Error())
		}
		method := &pkgmodel.TokenPaymentData{
			Token: strings.TrimSpace(payload.Token),
			Last4: strings.TrimSpace(payload.MaskedPan),
		}
		if method.Token == "" {
			return nil, merrors.InvalidArgument(field + ".token is required")
		}
		return endpointFromMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN, method)

	case "":
		return nil, merrors.InvalidArgument(field + " endpoint type is required")

	default:
		return nil, merrors.InvalidArgument(field + " endpoint type is unsupported in v2: " + string(endpoint.Type))
	}
}

func mapExternalChainMethod(payload srequest.ExternalChainEndpoint, field string) (*pkgmodel.CryptoAddressPaymentData, error) {
	address := strings.TrimSpace(payload.Address)
	if address == "" {
		return nil, merrors.InvalidArgument(field + ".address is required")
	}
	if payload.Asset == nil {
		return nil, merrors.InvalidArgument(field + ".asset is required")
	}
	token := strings.ToUpper(strings.TrimSpace(payload.Asset.TokenSymbol))
	if token == "" {
		return nil, merrors.InvalidArgument(field + ".asset.token_symbol is required")
	}
	if _, err := mapChainNetwork(payload.Asset.Chain); err != nil {
		return nil, merrors.InvalidArgument(field + ".asset.chain: " + err.Error())
	}

	result := &pkgmodel.CryptoAddressPaymentData{
		Currency: pkgmodel.Currency(token),
		Address:  address,
		Network:  strings.ToUpper(strings.TrimSpace(string(payload.Asset.Chain))),
	}
	if memo := strings.TrimSpace(payload.Memo); memo != "" {
		result.DestinationTag = &memo
	}
	return result, nil
}

func endpointFromMethod(methodType endpointv1.PaymentMethodType, data any) (*endpointv1.PaymentEndpoint, error) {
	raw, err := bson.Marshal(data)
	if err != nil {
		return nil, merrors.InternalWrap(err, "failed to encode payment method data")
	}
	method := &endpointv1.PaymentMethod{
		Type: methodType,
		Data: raw,
	}
	return &endpointv1.PaymentEndpoint{
		Source: &endpointv1.PaymentEndpoint_PaymentMethod{
			PaymentMethod: method,
		},
	}, nil
}

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

func mapSettlementMode(mode srequest.SettlementMode) (paymentv1.SettlementMode, error) {
	switch strings.TrimSpace(string(mode)) {
	case "", string(srequest.SettlementModeUnspecified):
		return paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED, nil
	case string(srequest.SettlementModeFixSource):
		return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE, nil
	case string(srequest.SettlementModeFixReceived):
		return paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED, nil
	default:
		return paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED, merrors.InvalidArgument("unsupported settlement mode: " + string(mode))
	}
}

func mapFeeTreatment(treatment srequest.FeeTreatment) (quotationv2.FeeTreatment, error) {
	switch strings.TrimSpace(string(treatment)) {
	case "", string(srequest.FeeTreatmentUnspecified):
		return quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED, nil
	case string(srequest.FeeTreatmentAddToSource):
		return quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE, nil
	case string(srequest.FeeTreatmentDeductFromDestination):
		return quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION, nil
	default:
		return quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED, merrors.InvalidArgument("unsupported fee treatment: " + string(treatment))
	}
}

func mapChainNetwork(chain srequest.ChainNetwork) (chainv1.ChainNetwork, error) {
	switch strings.TrimSpace(string(chain)) {
	case "", string(srequest.ChainNetworkUnspecified):
		return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, nil
	case string(srequest.ChainNetworkEthereumMainnet):
		return chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, nil
	case string(srequest.ChainNetworkArbitrumOne):
		return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE, nil
	case string(srequest.ChainNetworkTronMainnet):
		return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET, nil
	case string(srequest.ChainNetworkTronNile):
		return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_NILE, nil
	default:
		return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument("unsupported chain network: " + string(chain))
	}
}

func uint32ToString(v uint32) string {
	if v == 0 {
		return ""
	}
	return strconv.FormatUint(uint64(v), 10)
}

type ledgerMethodData struct {
	LedgerAccountRef       string `bson:"ledgerAccountRef"`
	ContraLedgerAccountRef string `bson:"contraLedgerAccountRef,omitempty"`
}
