package quotation

import (
	"strings"

	"github.com/tech/sendico/payments/storage/model"
	chainasset "github.com/tech/sendico/pkg/chain"
	payecon "github.com/tech/sendico/pkg/payments/economics"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
	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"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
	sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
)

func intentFromProto(src *sharedv1.PaymentIntent) model.PaymentIntent {
	if src == nil {
		return model.PaymentIntent{}
	}
	attrs := cloneMetadata(src.GetAttributes())
	feeTreatment := feeTreatmentFromAttributes(attrs)
	delete(attrs, "fee_treatment")
	delete(attrs, "feeTreatment")
	delete(attrs, "settlement_mode")
	delete(attrs, "settlementMode")

	settlementCurrency := derivedSettlementCurrencyFromProtoIntent(src)
	requiresFX := derivedRequiresFXFromProtoIntent(src, settlementCurrency)

	intent := model.PaymentIntent{
		Ref:                src.GetRef(),
		Kind:               modelKindFromProto(src.GetKind()),
		Source:             endpointFromProto(src.GetSource()),
		Destination:        endpointFromProto(src.GetDestination()),
		Amount:             moneyFromProto(src.GetAmount()),
		RequiresFX:         requiresFX,
		FeePolicy:          feePolicyFromProto(src.GetFeePolicy()),
		SettlementMode:     settlementModeFromProto(src.GetSettlementMode()),
		FeeTreatment:       feeTreatment,
		SettlementCurrency: settlementCurrency,
		Attributes:         attrs,
		Customer:           customerFromProto(src.GetCustomer()),
	}
	if src.GetFx() != nil {
		intent.FX = fxIntentFromProto(src.GetFx())
	}
	return intent
}

func endpointFromProto(src *sharedv1.PaymentEndpoint) model.PaymentEndpoint {
	if src == nil {
		return model.PaymentEndpoint{Type: model.EndpointTypeUnspecified}
	}
	result := model.PaymentEndpoint{
		Type:       model.EndpointTypeUnspecified,
		InstanceID: strings.TrimSpace(src.GetInstanceId()),
		Metadata:   cloneMetadata(src.GetMetadata()),
	}
	if ledger := src.GetLedger(); ledger != nil {
		result.Type = model.EndpointTypeLedger
		result.Ledger = &model.LedgerEndpoint{
			LedgerAccountRef:       strings.TrimSpace(ledger.GetLedgerAccountRef()),
			ContraLedgerAccountRef: strings.TrimSpace(ledger.GetContraLedgerAccountRef()),
		}
		return result
	}
	if managed := src.GetManagedWallet(); managed != nil {
		result.Type = model.EndpointTypeManagedWallet
		result.ManagedWallet = &model.ManagedWalletEndpoint{
			ManagedWalletRef: strings.TrimSpace(managed.GetManagedWalletRef()),
			Asset:            assetFromProto(managed.GetAsset()),
		}
		return result
	}
	if external := src.GetExternalChain(); external != nil {
		result.Type = model.EndpointTypeExternalChain
		result.ExternalChain = &model.ExternalChainEndpoint{
			Asset:   assetFromProto(external.GetAsset()),
			Address: strings.TrimSpace(external.GetAddress()),
			Memo:    strings.TrimSpace(external.GetMemo()),
		}
		return result
	}
	if card := src.GetCard(); card != nil {
		result.Type = model.EndpointTypeCard
		result.Card = &model.CardEndpoint{
			Pan:               strings.TrimSpace(card.GetPan()),
			Token:             strings.TrimSpace(card.GetToken()),
			Cardholder:        strings.TrimSpace(card.GetCardholderName()),
			CardholderSurname: strings.TrimSpace(card.GetCardholderSurname()),
			ExpMonth:          card.GetExpMonth(),
			ExpYear:           card.GetExpYear(),
			Country:           strings.TrimSpace(card.GetCountry()),
			MaskedPan:         strings.TrimSpace(card.GetMaskedPan()),
		}
		return result
	}
	return result
}

func fxIntentFromProto(src *sharedv1.FXIntent) *model.FXIntent {
	if src == nil {
		return nil
	}
	return &model.FXIntent{
		Pair:              pairFromProto(src.GetPair()),
		Side:              fxSideFromProto(src.GetSide()),
		Firm:              src.GetFirm(),
		TTLMillis:         src.GetTtlMs(),
		PreferredProvider: strings.TrimSpace(src.GetPreferredProvider()),
		MaxAgeMillis:      src.GetMaxAgeMs(),
	}
}

func protoIntentFromModel(src model.PaymentIntent) *sharedv1.PaymentIntent {
	attrs := cloneMetadata(src.Attributes)
	if attrs == nil {
		attrs = map[string]string{}
	}
	if feeTreatment := strings.TrimSpace(string(src.FeeTreatment)); feeTreatment != "" && feeTreatment != string(model.FeeTreatmentUnspecified) {
		attrs["fee_treatment"] = feeTreatment
	}
	if len(attrs) == 0 {
		attrs = nil
	}

	settlementCurrency := strings.TrimSpace(src.SettlementCurrency)
	if settlementCurrency == "" {
		settlementCurrency = derivedSettlementCurrencyFromModelIntent(src)
	}

	intent := &sharedv1.PaymentIntent{
		Ref:                src.Ref,
		Kind:               protoKindFromModel(src.Kind),
		Source:             protoEndpointFromModel(src.Source),
		Destination:        protoEndpointFromModel(src.Destination),
		Amount:             protoMoney(src.Amount),
		RequiresFx:         derivedRequiresFXFromModelIntent(src, settlementCurrency),
		FeePolicy:          feePolicyToProto(src.FeePolicy),
		SettlementMode:     settlementModeToProto(src.SettlementMode),
		SettlementCurrency: settlementCurrency,
		Attributes:         attrs,
		Customer:           protoCustomerFromModel(src.Customer),
	}
	if src.FX != nil {
		intent.Fx = protoFXIntentFromModel(src.FX)
	}
	return intent
}

func feeTreatmentFromAttributes(attrs map[string]string) model.FeeTreatment {
	if len(attrs) == 0 {
		return model.FeeTreatmentAddToSource
	}
	keys := []string{"fee_treatment", "feeTreatment"}
	for _, key := range keys {
		if value := strings.TrimSpace(attrs[key]); value != "" {
			switch payecon.ResolveFeeTreatmentFromStringOrDefault(value) {
			case quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION:
				return model.FeeTreatmentDeductFromDestination
			default:
				return model.FeeTreatmentAddToSource
			}
		}
	}
	return model.FeeTreatmentAddToSource
}

func derivedSettlementCurrencyFromProtoIntent(src *sharedv1.PaymentIntent) string {
	if src == nil {
		return ""
	}
	if fx := src.GetFx(); fx != nil && fx.GetPair() != nil {
		if currency := settlementCurrencyFromPair(fx.GetPair(), fx.GetSide()); currency != "" {
			return currency
		}
	}
	if amount := src.GetAmount(); amount != nil {
		if currency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency())); currency != "" {
			return currency
		}
	}
	return strings.ToUpper(strings.TrimSpace(src.GetSettlementCurrency()))
}

func derivedSettlementCurrencyFromModelIntent(src model.PaymentIntent) string {
	if src.FX != nil && src.FX.Pair != nil {
		pair := &fxv1.CurrencyPair{
			Base:  strings.TrimSpace(src.FX.Pair.Base),
			Quote: strings.TrimSpace(src.FX.Pair.Quote),
		}
		if currency := settlementCurrencyFromPair(pair, fxSideToProto(src.FX.Side)); currency != "" {
			return currency
		}
	}
	if src.Amount != nil {
		if currency := strings.ToUpper(strings.TrimSpace(src.Amount.Currency)); currency != "" {
			return currency
		}
	}
	return ""
}

func settlementCurrencyFromPair(pair *fxv1.CurrencyPair, side fxv1.Side) string {
	if pair == nil {
		return ""
	}
	base := strings.ToUpper(strings.TrimSpace(pair.GetBase()))
	quote := strings.ToUpper(strings.TrimSpace(pair.GetQuote()))
	switch side {
	case fxv1.Side_BUY_BASE_SELL_QUOTE:
		if base != "" {
			return base
		}
	case fxv1.Side_SELL_BASE_BUY_QUOTE, fxv1.Side_SIDE_UNSPECIFIED:
		if quote != "" {
			return quote
		}
	}
	if quote != "" {
		return quote
	}
	return base
}

func derivedRequiresFXFromProtoIntent(src *sharedv1.PaymentIntent, settlementCurrency string) bool {
	if src == nil {
		return false
	}
	if fx := src.GetFx(); fx != nil && fx.GetPair() != nil {
		return true
	}
	amount := src.GetAmount()
	if amount == nil {
		return false
	}
	amountCurrency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency()))
	settlementCurrency = strings.ToUpper(strings.TrimSpace(settlementCurrency))
	return amountCurrency != "" && settlementCurrency != "" && !strings.EqualFold(amountCurrency, settlementCurrency)
}

func derivedRequiresFXFromModelIntent(src model.PaymentIntent, settlementCurrency string) bool {
	if src.FX != nil && src.FX.Pair != nil {
		return true
	}
	if src.Amount == nil {
		return false
	}
	amountCurrency := strings.ToUpper(strings.TrimSpace(src.Amount.Currency))
	settlementCurrency = strings.ToUpper(strings.TrimSpace(settlementCurrency))
	return amountCurrency != "" && settlementCurrency != "" && !strings.EqualFold(amountCurrency, settlementCurrency)
}

func customerFromProto(src *sharedv1.Customer) *model.Customer {
	if src == nil {
		return nil
	}
	return &model.Customer{
		ID:         strings.TrimSpace(src.GetId()),
		FirstName:  strings.TrimSpace(src.GetFirstName()),
		MiddleName: strings.TrimSpace(src.GetMiddleName()),
		LastName:   strings.TrimSpace(src.GetLastName()),
		IP:         strings.TrimSpace(src.GetIp()),
		Zip:        strings.TrimSpace(src.GetZip()),
		Country:    strings.TrimSpace(src.GetCountry()),
		State:      strings.TrimSpace(src.GetState()),
		City:       strings.TrimSpace(src.GetCity()),
		Address:    strings.TrimSpace(src.GetAddress()),
	}
}

func protoCustomerFromModel(src *model.Customer) *sharedv1.Customer {
	if src == nil {
		return nil
	}
	return &sharedv1.Customer{
		Id:         strings.TrimSpace(src.ID),
		FirstName:  strings.TrimSpace(src.FirstName),
		MiddleName: strings.TrimSpace(src.MiddleName),
		LastName:   strings.TrimSpace(src.LastName),
		Ip:         strings.TrimSpace(src.IP),
		Zip:        strings.TrimSpace(src.Zip),
		Country:    strings.TrimSpace(src.Country),
		State:      strings.TrimSpace(src.State),
		City:       strings.TrimSpace(src.City),
		Address:    strings.TrimSpace(src.Address),
	}
}

func protoEndpointFromModel(src model.PaymentEndpoint) *sharedv1.PaymentEndpoint {
	endpoint := &sharedv1.PaymentEndpoint{
		Metadata:   cloneMetadata(src.Metadata),
		InstanceId: strings.TrimSpace(src.InstanceID),
	}
	switch src.Type {
	case model.EndpointTypeLedger:
		if src.Ledger != nil {
			endpoint.Endpoint = &sharedv1.PaymentEndpoint_Ledger{
				Ledger: &sharedv1.LedgerEndpoint{
					LedgerAccountRef:       src.Ledger.LedgerAccountRef,
					ContraLedgerAccountRef: src.Ledger.ContraLedgerAccountRef,
				},
			}
		}
	case model.EndpointTypeManagedWallet:
		if src.ManagedWallet != nil {
			endpoint.Endpoint = &sharedv1.PaymentEndpoint_ManagedWallet{
				ManagedWallet: &sharedv1.ManagedWalletEndpoint{
					ManagedWalletRef: src.ManagedWallet.ManagedWalletRef,
					Asset:            assetToProto(src.ManagedWallet.Asset),
				},
			}
		}
	case model.EndpointTypeExternalChain:
		if src.ExternalChain != nil {
			endpoint.Endpoint = &sharedv1.PaymentEndpoint_ExternalChain{
				ExternalChain: &sharedv1.ExternalChainEndpoint{
					Asset:   assetToProto(src.ExternalChain.Asset),
					Address: src.ExternalChain.Address,
					Memo:    src.ExternalChain.Memo,
				},
			}
		}
	case model.EndpointTypeCard:
		if src.Card != nil {
			card := &sharedv1.CardEndpoint{
				CardholderName:    src.Card.Cardholder,
				CardholderSurname: src.Card.CardholderSurname,
				ExpMonth:          src.Card.ExpMonth,
				ExpYear:           src.Card.ExpYear,
				Country:           src.Card.Country,
				MaskedPan:         src.Card.MaskedPan,
			}
			if pan := strings.TrimSpace(src.Card.Pan); pan != "" {
				card.Card = &sharedv1.CardEndpoint_Pan{Pan: pan}
			}
			if token := strings.TrimSpace(src.Card.Token); token != "" {
				card.Card = &sharedv1.CardEndpoint_Token{Token: token}
			}
			endpoint.Endpoint = &sharedv1.PaymentEndpoint_Card{Card: card}
		}
	default:
		// leave unspecified
	}
	return endpoint
}

func protoFXIntentFromModel(src *model.FXIntent) *sharedv1.FXIntent {
	if src == nil {
		return nil
	}
	return &sharedv1.FXIntent{
		Pair:              pairToProto(src.Pair),
		Side:              fxSideToProto(src.Side),
		Firm:              src.Firm,
		TtlMs:             src.TTLMillis,
		PreferredProvider: src.PreferredProvider,
		MaxAgeMs:          src.MaxAgeMillis,
	}
}

func protoKindFromModel(kind model.PaymentKind) sharedv1.PaymentKind {
	switch kind {
	case model.PaymentKindPayout:
		return sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT
	case model.PaymentKindInternalTransfer:
		return sharedv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER
	case model.PaymentKindFXConversion:
		return sharedv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION
	default:
		return sharedv1.PaymentKind_PAYMENT_KIND_UNSPECIFIED
	}
}

func modelKindFromProto(kind sharedv1.PaymentKind) model.PaymentKind {
	switch kind {
	case sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT:
		return model.PaymentKindPayout
	case sharedv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER:
		return model.PaymentKindInternalTransfer
	case sharedv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION:
		return model.PaymentKindFXConversion
	default:
		return model.PaymentKindUnspecified
	}
}

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

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

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 feePolicyFromProto(src *feesv1.PolicyOverrides) *paymenttypes.FeePolicy {
	if src == nil {
		return nil
	}
	return &paymenttypes.FeePolicy{
		InsufficientNet: insufficientPolicyFromProto(src.GetInsufficientNet()),
	}
}

func feePolicyToProto(src *paymenttypes.FeePolicy) *feesv1.PolicyOverrides {
	if src == nil {
		return nil
	}
	return &feesv1.PolicyOverrides{
		InsufficientNet: insufficientPolicyToProto(src.InsufficientNet),
	}
}

func insufficientPolicyFromProto(policy feesv1.InsufficientNetPolicy) paymenttypes.InsufficientNetPolicy {
	switch policy {
	case feesv1.InsufficientNetPolicy_BLOCK_POSTING:
		return paymenttypes.InsufficientNetBlockPosting
	case feesv1.InsufficientNetPolicy_SWEEP_ORG_CASH:
		return paymenttypes.InsufficientNetSweepOrgCash
	case feesv1.InsufficientNetPolicy_INVOICE_LATER:
		return paymenttypes.InsufficientNetInvoiceLater
	default:
		return paymenttypes.InsufficientNetUnspecified
	}
}

func insufficientPolicyToProto(policy paymenttypes.InsufficientNetPolicy) feesv1.InsufficientNetPolicy {
	switch policy {
	case paymenttypes.InsufficientNetBlockPosting:
		return feesv1.InsufficientNetPolicy_BLOCK_POSTING
	case paymenttypes.InsufficientNetSweepOrgCash:
		return feesv1.InsufficientNetPolicy_SWEEP_ORG_CASH
	case paymenttypes.InsufficientNetInvoiceLater:
		return feesv1.InsufficientNetPolicy_INVOICE_LATER
	default:
		return feesv1.InsufficientNetPolicy_INSUFFICIENT_NET_UNSPECIFIED
	}
}

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 assetFromProto(asset *chainv1.Asset) *paymenttypes.Asset {
	if asset == nil {
		return nil
	}
	return &paymenttypes.Asset{
		Chain:           chainasset.NetworkAlias(asset.GetChain()),
		TokenSymbol:     asset.GetTokenSymbol(),
		ContractAddress: asset.GetContractAddress(),
	}
}

func assetToProto(asset *paymenttypes.Asset) *chainv1.Asset {
	if asset == nil {
		return nil
	}
	return &chainv1.Asset{
		Chain:           chainasset.NetworkFromString(asset.Chain),
		TokenSymbol:     asset.TokenSymbol,
		ContractAddress: asset.ContractAddress,
	}
}
