package prmap

import (
	"strconv"
	"strings"

	"github.com/tech/sendico/payments/storage/model"
	pkgmodel "github.com/tech/sendico/pkg/model"
	paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
	endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
	"go.mongodb.org/mongo-driver/v2/bson"
)

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

func mapIntentSnapshot(src model.PaymentIntent) (*quotationv2.QuoteIntent, error) {
	source, err := mapIntentEndpoint(src.Source, "intent_snapshot.source")
	if err != nil {
		return nil, err
	}
	destination, err := mapIntentEndpoint(src.Destination, "intent_snapshot.destination")
	if err != nil {
		return nil, err
	}

	settlementMode := settlementModeToProto(src.SettlementMode)
	feeTreatment := feeTreatmentToProto(src.FeeTreatment)
	return &quotationv2.QuoteIntent{
		Source:             source,
		Destination:        destination,
		Amount:             moneyToProto(src.Amount),
		SettlementMode:     settlementMode,
		FeeTreatment:       feeTreatment,
		SettlementCurrency: strings.ToUpper(strings.TrimSpace(src.SettlementCurrency)),
		Comment:            strings.TrimSpace(src.Comment),
	}, nil
}

func mapIntentEndpoint(src model.PaymentEndpoint, field string) (*endpointv1.PaymentEndpoint, error) {
	switch src.Type {
	case model.EndpointTypeManagedWallet:
		if src.ManagedWallet == nil {
			return nil, invalidMissing(field + ".managed_wallet")
		}
		if strings.TrimSpace(src.ManagedWallet.ManagedWalletRef) == "" {
			return nil, invalidMissing(field + ".managed_wallet.managed_wallet_ref")
		}
		return endpointWithMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET, walletData(src.ManagedWallet))
	case model.EndpointTypeExternalChain:
		if src.ExternalChain == nil {
			return nil, invalidMissing(field + ".external_chain")
		}
		if strings.TrimSpace(src.ExternalChain.Address) == "" {
			return nil, invalidMissing(field + ".external_chain.address")
		}
		return endpointWithMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS, externalChainData(src.ExternalChain))
	case model.EndpointTypeCard:
		if src.Card == nil {
			return nil, invalidMissing(field + ".card")
		}
		if strings.TrimSpace(src.Card.Token) == "" && strings.TrimSpace(src.Card.Pan) == "" {
			return nil, invalidMissing(field + ".card.pan_or_token")
		}
		return endpointWithCard(src.Card)
	case model.EndpointTypeLedger:
		if src.Ledger == nil {
			return nil, invalidMissing(field + ".ledger")
		}
		if strings.TrimSpace(src.Ledger.LedgerAccountRef) == "" {
			return nil, invalidMissing(field + ".ledger.ledger_account_ref")
		}
		return endpointWithMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER, ledgerData(src.Ledger))
	default:
		return nil, invalidMissing(field)
	}
}

func endpointWithCard(card *model.CardEndpoint) (*endpointv1.PaymentEndpoint, error) {
	token := strings.TrimSpace(card.Token)
	if token != "" {
		return endpointWithMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN, tokenCardData(card))
	}
	return endpointWithMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD, rawCardData(card))
}

func endpointWithMethod(
	methodType endpointv1.PaymentMethodType,
	methodData any,
) (*endpointv1.PaymentEndpoint, error) {
	data, err := bson.Marshal(methodData)
	if err != nil {
		return nil, err
	}
	return &endpointv1.PaymentEndpoint{
		Source: &endpointv1.PaymentEndpoint_PaymentMethod{
			PaymentMethod: &endpointv1.PaymentMethod{
				Type: methodType,
				Data: data,
			},
		},
	}, nil
}

func walletData(src *model.ManagedWalletEndpoint) pkgmodel.WalletPaymentData {
	walletID := ""
	if src != nil {
		walletID = strings.TrimSpace(src.ManagedWalletRef)
	}
	return pkgmodel.WalletPaymentData{
		WalletID: walletID,
	}
}

func externalChainData(src *model.ExternalChainEndpoint) pkgmodel.CryptoAddressPaymentData {
	currency := pkgmodel.Currency("")
	network := ""
	address := ""
	if src != nil {
		if src.Asset != nil {
			currency = pkgmodel.Currency(strings.ToUpper(strings.TrimSpace(src.Asset.TokenSymbol)))
			network = strings.TrimSpace(src.Asset.Chain)
		}
		address = strings.TrimSpace(src.Address)
	}
	data := pkgmodel.CryptoAddressPaymentData{
		Currency: currency,
		Network:  network,
		Address:  address,
	}
	if src != nil && strings.TrimSpace(src.Memo) != "" {
		memo := strings.TrimSpace(src.Memo)
		data.DestinationTag = &memo
	}
	return data
}

func rawCardData(src *model.CardEndpoint) pkgmodel.CardPaymentData {
	if src == nil {
		return pkgmodel.CardPaymentData{}
	}
	return pkgmodel.CardPaymentData{
		Pan:       strings.TrimSpace(src.Pan),
		FirstName: strings.TrimSpace(src.Cardholder),
		LastName:  strings.TrimSpace(src.CardholderSurname),
		ExpMonth:  uintToString(src.ExpMonth),
		ExpYear:   uintToString(src.ExpYear),
		Country:   strings.TrimSpace(src.Country),
	}
}

func tokenCardData(src *model.CardEndpoint) pkgmodel.TokenPaymentData {
	if src == nil {
		return pkgmodel.TokenPaymentData{}
	}
	return pkgmodel.TokenPaymentData{
		Token:          strings.TrimSpace(src.Token),
		Last4:          strings.TrimSpace(src.MaskedPan),
		ExpMonth:       uintToString(src.ExpMonth),
		ExpYear:        uintToString(src.ExpYear),
		CardholderName: strings.TrimSpace(src.Cardholder),
		Country:        strings.TrimSpace(src.Country),
	}
}

func ledgerData(src *model.LedgerEndpoint) ledgerMethodData {
	if src == nil {
		return ledgerMethodData{}
	}
	return ledgerMethodData{
		LedgerAccountRef:       strings.TrimSpace(src.LedgerAccountRef),
		ContraLedgerAccountRef: strings.TrimSpace(src.ContraLedgerAccountRef),
	}
}

func settlementModeToProto(mode model.SettlementMode) paymentv1.SettlementMode {
	switch mode {
	case model.SettlementModeFixReceived:
		return paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED
	case model.SettlementModeFixSource:
		return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
	default:
		return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
	}
}

func feeTreatmentToProto(value model.FeeTreatment) quotationv2.FeeTreatment {
	switch value {
	case model.FeeTreatmentDeductFromDestination:
		return quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION
	case model.FeeTreatmentAddToSource:
		return quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE
	default:
		return quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE
	}
}

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