package orchestrator

import (
	"strings"
	"time"

	"github.com/tech/sendico/payments/orchestrator/storage/model"
	chainasset "github.com/tech/sendico/pkg/chain"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	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"
	gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func intentFromProto(src *orchestratorv1.PaymentIntent) model.PaymentIntent {
	if src == nil {
		return model.PaymentIntent{}
	}
	intent := model.PaymentIntent{
		Ref:                src.GetRef(),
		Kind:               modelKindFromProto(src.GetKind()),
		Source:             endpointFromProto(src.GetSource()),
		Destination:        endpointFromProto(src.GetDestination()),
		Amount:             moneyFromProto(src.GetAmount()),
		RequiresFX:         src.GetRequiresFx(),
		FeePolicy:          feePolicyFromProto(src.GetFeePolicy()),
		SettlementMode:     settlementModeFromProto(src.GetSettlementMode()),
		SettlementCurrency: strings.TrimSpace(src.GetSettlementCurrency()),
		Attributes:         cloneMetadata(src.GetAttributes()),
		Customer:           customerFromProto(src.GetCustomer()),
	}
	if src.GetFx() != nil {
		intent.FX = fxIntentFromProto(src.GetFx())
	}
	return intent
}

func endpointFromProto(src *orchestratorv1.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 *orchestratorv1.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 quoteSnapshotToModel(src *orchestratorv1.PaymentQuote) *model.PaymentQuoteSnapshot {
	if src == nil {
		return nil
	}
	return &model.PaymentQuoteSnapshot{
		DebitAmount:              moneyFromProto(src.GetDebitAmount()),
		ExpectedSettlementAmount: moneyFromProto(src.GetExpectedSettlementAmount()),
		ExpectedFeeTotal:         moneyFromProto(src.GetExpectedFeeTotal()),
		FeeLines:                 feeLinesFromProto(src.GetFeeLines()),
		FeeRules:                 feeRulesFromProto(src.GetFeeRules()),
		FXQuote:                  fxQuoteFromProto(src.GetFxQuote()),
		NetworkFee:               networkFeeFromProto(src.GetNetworkFee()),
		QuoteRef:                 strings.TrimSpace(src.GetQuoteRef()),
	}
}

func toProtoPayment(src *model.Payment) *orchestratorv1.Payment {
	if src == nil {
		return nil
	}
	payment := &orchestratorv1.Payment{
		PaymentRef:     src.PaymentRef,
		IdempotencyKey: src.IdempotencyKey,
		Intent:         protoIntentFromModel(src.Intent),
		State:          protoStateFromModel(src.State),
		FailureCode:    protoFailureFromModel(src.FailureCode),
		FailureReason:  src.FailureReason,
		LastQuote:      modelQuoteToProto(src.LastQuote),
		Execution:      protoExecutionFromModel(src.Execution),
		ExecutionPlan:  protoExecutionPlanFromModel(src.ExecutionPlan),
		PaymentPlan:    protoPaymentPlanFromModel(src.PaymentPlan),
		Metadata:       cloneMetadata(src.Metadata),
	}
	if src.CardPayout != nil {
		payment.CardPayout = &orchestratorv1.CardPayout{
			PayoutRef:         src.CardPayout.PayoutRef,
			ProviderPaymentId: src.CardPayout.ProviderPaymentID,
			Status:            src.CardPayout.Status,
			FailureReason:     src.CardPayout.FailureReason,
			CardCountry:       src.CardPayout.CardCountry,
			MaskedPan:         src.CardPayout.MaskedPan,
			ProviderCode:      src.CardPayout.ProviderCode,
			GatewayReference:  src.CardPayout.GatewayReference,
		}
	}
	if src.CreatedAt.IsZero() {
		payment.CreatedAt = timestamppb.New(time.Now().UTC())
	} else {
		payment.CreatedAt = timestamppb.New(src.CreatedAt.UTC())
	}
	if src.UpdatedAt != (time.Time{}) {
		payment.UpdatedAt = timestamppb.New(src.UpdatedAt.UTC())
	}
	return payment
}

func protoIntentFromModel(src model.PaymentIntent) *orchestratorv1.PaymentIntent {
	intent := &orchestratorv1.PaymentIntent{
		Ref:                src.Ref,
		Kind:               protoKindFromModel(src.Kind),
		Source:             protoEndpointFromModel(src.Source),
		Destination:        protoEndpointFromModel(src.Destination),
		Amount:             protoMoney(src.Amount),
		RequiresFx:         src.RequiresFX,
		FeePolicy:          feePolicyToProto(src.FeePolicy),
		SettlementMode:     settlementModeToProto(src.SettlementMode),
		SettlementCurrency: strings.TrimSpace(src.SettlementCurrency),
		Attributes:         cloneMetadata(src.Attributes),
		Customer:           protoCustomerFromModel(src.Customer),
	}
	if src.FX != nil {
		intent.Fx = protoFXIntentFromModel(src.FX)
	}
	return intent
}

func customerFromProto(src *orchestratorv1.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) *orchestratorv1.Customer {
	if src == nil {
		return nil
	}
	return &orchestratorv1.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) *orchestratorv1.PaymentEndpoint {
	endpoint := &orchestratorv1.PaymentEndpoint{
		Metadata:   cloneMetadata(src.Metadata),
		InstanceId: strings.TrimSpace(src.InstanceID),
	}
	switch src.Type {
	case model.EndpointTypeLedger:
		if src.Ledger != nil {
			endpoint.Endpoint = &orchestratorv1.PaymentEndpoint_Ledger{
				Ledger: &orchestratorv1.LedgerEndpoint{
					LedgerAccountRef:       src.Ledger.LedgerAccountRef,
					ContraLedgerAccountRef: src.Ledger.ContraLedgerAccountRef,
				},
			}
		}
	case model.EndpointTypeManagedWallet:
		if src.ManagedWallet != nil {
			endpoint.Endpoint = &orchestratorv1.PaymentEndpoint_ManagedWallet{
				ManagedWallet: &orchestratorv1.ManagedWalletEndpoint{
					ManagedWalletRef: src.ManagedWallet.ManagedWalletRef,
					Asset:            assetToProto(src.ManagedWallet.Asset),
				},
			}
		}
	case model.EndpointTypeExternalChain:
		if src.ExternalChain != nil {
			endpoint.Endpoint = &orchestratorv1.PaymentEndpoint_ExternalChain{
				ExternalChain: &orchestratorv1.ExternalChainEndpoint{
					Asset:   assetToProto(src.ExternalChain.Asset),
					Address: src.ExternalChain.Address,
					Memo:    src.ExternalChain.Memo,
				},
			}
		}
	case model.EndpointTypeCard:
		if src.Card != nil {
			card := &orchestratorv1.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 = &orchestratorv1.CardEndpoint_Pan{Pan: pan}
			}
			if token := strings.TrimSpace(src.Card.Token); token != "" {
				card.Card = &orchestratorv1.CardEndpoint_Token{Token: token}
			}
			endpoint.Endpoint = &orchestratorv1.PaymentEndpoint_Card{Card: card}
		}
	default:
		// leave unspecified
	}
	return endpoint
}

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

func protoExecutionFromModel(src *model.ExecutionRefs) *orchestratorv1.ExecutionRefs {
	if src == nil {
		return nil
	}
	return &orchestratorv1.ExecutionRefs{
		DebitEntryRef:    src.DebitEntryRef,
		CreditEntryRef:   src.CreditEntryRef,
		FxEntryRef:       src.FXEntryRef,
		ChainTransferRef: src.ChainTransferRef,
		CardPayoutRef:    src.CardPayoutRef,
		FeeTransferRef:   src.FeeTransferRef,
	}
}

func protoExecutionStepFromModel(src *model.ExecutionStep) *orchestratorv1.ExecutionStep {
	if src == nil {
		return nil
	}
	return &orchestratorv1.ExecutionStep{
		Code:            src.Code,
		Description:     src.Description,
		Amount:          protoMoney(src.Amount),
		NetworkFee:      protoMoney(src.NetworkFee),
		SourceWalletRef: src.SourceWalletRef,
		DestinationRef:  src.DestinationRef,
		TransferRef:     src.TransferRef,
		Metadata:        cloneMetadata(src.Metadata),
		OperationRef:    src.OperationRef,
	}
}

func protoExecutionPlanFromModel(src *model.ExecutionPlan) *orchestratorv1.ExecutionPlan {
	if src == nil {
		return nil
	}
	steps := make([]*orchestratorv1.ExecutionStep, 0, len(src.Steps))
	for _, step := range src.Steps {
		if protoStep := protoExecutionStepFromModel(step); protoStep != nil {
			steps = append(steps, protoStep)
		}
	}
	if len(steps) == 0 {
		steps = nil
	}
	return &orchestratorv1.ExecutionPlan{
		Steps:           steps,
		TotalNetworkFee: protoMoney(src.TotalNetworkFee),
	}
}

func protoPaymentStepFromModel(src *model.PaymentStep) *orchestratorv1.PaymentStep {
	if src == nil {
		return nil
	}
	return &orchestratorv1.PaymentStep{
		Rail:         protoRailFromModel(src.Rail),
		GatewayId:    strings.TrimSpace(src.GatewayID),
		Action:       protoRailOperationFromModel(src.Action),
		Amount:       protoMoney(src.Amount),
		StepId:       strings.TrimSpace(src.StepID),
		InstanceId:   strings.TrimSpace(src.InstanceID),
		DependsOn:    cloneStringList(src.DependsOn),
		CommitPolicy: strings.TrimSpace(string(src.CommitPolicy)),
		CommitAfter:  cloneStringList(src.CommitAfter),
	}
}

func protoPaymentPlanFromModel(src *model.PaymentPlan) *orchestratorv1.PaymentPlan {
	if src == nil {
		return nil
	}
	steps := make([]*orchestratorv1.PaymentStep, 0, len(src.Steps))
	for _, step := range src.Steps {
		if protoStep := protoPaymentStepFromModel(step); protoStep != nil {
			steps = append(steps, protoStep)
		}
	}
	if len(steps) == 0 {
		steps = nil
	}
	plan := &orchestratorv1.PaymentPlan{
		Id:             strings.TrimSpace(src.ID),
		Steps:          steps,
		IdempotencyKey: strings.TrimSpace(src.IdempotencyKey),
		FxQuote:        fxQuoteToProto(src.FXQuote),
		Fees:           feeLinesToProto(src.Fees),
	}
	if !src.CreatedAt.IsZero() {
		plan.CreatedAt = timestamppb.New(src.CreatedAt.UTC())
	}
	return plan
}

func modelQuoteToProto(src *model.PaymentQuoteSnapshot) *orchestratorv1.PaymentQuote {
	if src == nil {
		return nil
	}
	return &orchestratorv1.PaymentQuote{
		DebitAmount:              protoMoney(src.DebitAmount),
		ExpectedSettlementAmount: protoMoney(src.ExpectedSettlementAmount),
		ExpectedFeeTotal:         protoMoney(src.ExpectedFeeTotal),
		FeeLines:                 feeLinesToProto(src.FeeLines),
		FeeRules:                 feeRulesToProto(src.FeeRules),
		FxQuote:                  fxQuoteToProto(src.FXQuote),
		NetworkFee:               networkFeeToProto(src.NetworkFee),
		QuoteRef:                 strings.TrimSpace(src.QuoteRef),
	}
}

func filterFromProto(req *orchestratorv1.ListPaymentsRequest) *model.PaymentFilter {
	if req == nil {
		return &model.PaymentFilter{}
	}
	filter := &model.PaymentFilter{
		SourceRef:       strings.TrimSpace(req.GetSourceRef()),
		DestinationRef:  strings.TrimSpace(req.GetDestinationRef()),
		OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
	}
	if req.GetPage() != nil {
		filter.Cursor = strings.TrimSpace(req.GetPage().GetCursor())
		filter.Limit = req.GetPage().GetLimit()
	}
	if len(req.GetFilterStates()) > 0 {
		filter.States = make([]model.PaymentState, 0, len(req.GetFilterStates()))
		for _, st := range req.GetFilterStates() {
			filter.States = append(filter.States, modelStateFromProto(st))
		}
	}
	return filter
}

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

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

func protoRailFromModel(rail model.Rail) gatewayv1.Rail {
	switch strings.ToUpper(strings.TrimSpace(string(rail))) {
	case string(model.RailCrypto):
		return gatewayv1.Rail_RAIL_CRYPTO
	case string(model.RailProviderSettlement):
		return gatewayv1.Rail_RAIL_PROVIDER_SETTLEMENT
	case string(model.RailLedger):
		return gatewayv1.Rail_RAIL_LEDGER
	case string(model.RailCardPayout):
		return gatewayv1.Rail_RAIL_CARD_PAYOUT
	case string(model.RailFiatOnRamp):
		return gatewayv1.Rail_RAIL_FIAT_ONRAMP
	default:
		return gatewayv1.Rail_RAIL_UNSPECIFIED
	}
}

func protoRailOperationFromModel(action model.RailOperation) gatewayv1.RailOperation {
	switch strings.ToUpper(strings.TrimSpace(string(action))) {
	case string(model.RailOperationDebit):
		return gatewayv1.RailOperation_RAIL_OPERATION_DEBIT
	case string(model.RailOperationCredit):
		return gatewayv1.RailOperation_RAIL_OPERATION_CREDIT
	case string(model.RailOperationExternalDebit):
		return gatewayv1.RailOperation_RAIL_OPERATION_DEBIT
	case string(model.RailOperationExternalCredit):
		return gatewayv1.RailOperation_RAIL_OPERATION_CREDIT
	case string(model.RailOperationMove):
		return gatewayv1.RailOperation_RAIL_OPERATION_MOVE
	case string(model.RailOperationSend):
		return gatewayv1.RailOperation_RAIL_OPERATION_SEND
	case string(model.RailOperationFee):
		return gatewayv1.RailOperation_RAIL_OPERATION_FEE
	case string(model.RailOperationObserveConfirm):
		return gatewayv1.RailOperation_RAIL_OPERATION_OBSERVE_CONFIRM
	case string(model.RailOperationFXConvert):
		return gatewayv1.RailOperation_RAIL_OPERATION_FX_CONVERT
	case string(model.RailOperationBlock):
		return gatewayv1.RailOperation_RAIL_OPERATION_BLOCK
	case string(model.RailOperationRelease):
		return gatewayv1.RailOperation_RAIL_OPERATION_RELEASE
	default:
		return gatewayv1.RailOperation_RAIL_OPERATION_UNSPECIFIED
	}
}

func protoStateFromModel(state model.PaymentState) orchestratorv1.PaymentState {
	switch state {
	case model.PaymentStateAccepted:
		return orchestratorv1.PaymentState_PAYMENT_STATE_ACCEPTED
	case model.PaymentStateFundsReserved:
		return orchestratorv1.PaymentState_PAYMENT_STATE_FUNDS_RESERVED
	case model.PaymentStateSubmitted:
		return orchestratorv1.PaymentState_PAYMENT_STATE_SUBMITTED
	case model.PaymentStateSettled:
		return orchestratorv1.PaymentState_PAYMENT_STATE_SETTLED
	case model.PaymentStateFailed:
		return orchestratorv1.PaymentState_PAYMENT_STATE_FAILED
	case model.PaymentStateCancelled:
		return orchestratorv1.PaymentState_PAYMENT_STATE_CANCELLED
	default:
		return orchestratorv1.PaymentState_PAYMENT_STATE_UNSPECIFIED
	}
}

func modelStateFromProto(state orchestratorv1.PaymentState) model.PaymentState {
	switch state {
	case orchestratorv1.PaymentState_PAYMENT_STATE_ACCEPTED:
		return model.PaymentStateAccepted
	case orchestratorv1.PaymentState_PAYMENT_STATE_FUNDS_RESERVED:
		return model.PaymentStateFundsReserved
	case orchestratorv1.PaymentState_PAYMENT_STATE_SUBMITTED:
		return model.PaymentStateSubmitted
	case orchestratorv1.PaymentState_PAYMENT_STATE_SETTLED:
		return model.PaymentStateSettled
	case orchestratorv1.PaymentState_PAYMENT_STATE_FAILED:
		return model.PaymentStateFailed
	case orchestratorv1.PaymentState_PAYMENT_STATE_CANCELLED:
		return model.PaymentStateCancelled
	default:
		return model.PaymentStateUnspecified
	}
}

func protoFailureFromModel(code model.PaymentFailureCode) orchestratorv1.PaymentFailureCode {
	switch code {
	case model.PaymentFailureCodeBalance:
		return orchestratorv1.PaymentFailureCode_FAILURE_BALANCE
	case model.PaymentFailureCodeLedger:
		return orchestratorv1.PaymentFailureCode_FAILURE_LEDGER
	case model.PaymentFailureCodeFX:
		return orchestratorv1.PaymentFailureCode_FAILURE_FX
	case model.PaymentFailureCodeChain:
		return orchestratorv1.PaymentFailureCode_FAILURE_CHAIN
	case model.PaymentFailureCodeFees:
		return orchestratorv1.PaymentFailureCode_FAILURE_FEES
	case model.PaymentFailureCodePolicy:
		return orchestratorv1.PaymentFailureCode_FAILURE_POLICY
	default:
		return orchestratorv1.PaymentFailureCode_FAILURE_UNSPECIFIED
	}
}

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

func settlementModeToProto(mode model.SettlementMode) orchestratorv1.SettlementMode {
	switch mode {
	case model.SettlementModeFixSource:
		return orchestratorv1.SettlementMode_SETTLEMENT_FIX_SOURCE
	case model.SettlementModeFixReceived:
		return orchestratorv1.SettlementMode_SETTLEMENT_FIX_RECEIVED
	default:
		return orchestratorv1.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 fxQuoteFromProto(quote *oraclev1.Quote) *paymenttypes.FXQuote {
	if quote == nil {
		return nil
	}
	return &paymenttypes.FXQuote{
		QuoteRef:        strings.TrimSpace(quote.GetQuoteRef()),
		Pair:            pairFromProto(quote.GetPair()),
		Side:            fxSideFromProto(quote.GetSide()),
		Price:           decimalFromProto(quote.GetPrice()),
		BaseAmount:      moneyFromProto(quote.GetBaseAmount()),
		QuoteAmount:     moneyFromProto(quote.GetQuoteAmount()),
		ExpiresAtUnixMs: quote.GetExpiresAtUnixMs(),
		Provider:        strings.TrimSpace(quote.GetProvider()),
		RateRef:         strings.TrimSpace(quote.GetRateRef()),
		Firm:            quote.GetFirm(),
	}
}

func fxQuoteToProto(quote *paymenttypes.FXQuote) *oraclev1.Quote {
	if quote == nil {
		return nil
	}
	return &oraclev1.Quote{
		QuoteRef:        strings.TrimSpace(quote.QuoteRef),
		Pair:            pairToProto(quote.Pair),
		Side:            fxSideToProto(quote.Side),
		Price:           decimalToProto(quote.Price),
		BaseAmount:      protoMoney(quote.BaseAmount),
		QuoteAmount:     protoMoney(quote.QuoteAmount),
		ExpiresAtUnixMs: quote.ExpiresAtUnixMs,
		Provider:        strings.TrimSpace(quote.Provider),
		RateRef:         strings.TrimSpace(quote.RateRef),
		Firm:            quote.Firm,
	}
}

func decimalFromProto(value *moneyv1.Decimal) *paymenttypes.Decimal {
	if value == nil {
		return nil
	}
	return &paymenttypes.Decimal{Value: value.GetValue()}
}

func decimalToProto(value *paymenttypes.Decimal) *moneyv1.Decimal {
	if value == nil {
		return nil
	}
	return &moneyv1.Decimal{Value: value.GetValue()}
}

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

func networkFeeFromProto(resp *chainv1.EstimateTransferFeeResponse) *paymenttypes.NetworkFeeEstimate {
	if resp == nil {
		return nil
	}
	return &paymenttypes.NetworkFeeEstimate{
		NetworkFee:        moneyFromProto(resp.GetNetworkFee()),
		EstimationContext: strings.TrimSpace(resp.GetEstimationContext()),
	}
}

func networkFeeToProto(resp *paymenttypes.NetworkFeeEstimate) *chainv1.EstimateTransferFeeResponse {
	if resp == nil {
		return nil
	}
	return &chainv1.EstimateTransferFeeResponse{
		NetworkFee:        protoMoney(resp.NetworkFee),
		EstimationContext: strings.TrimSpace(resp.EstimationContext),
	}
}

func feeLinesFromProto(lines []*feesv1.DerivedPostingLine) []*paymenttypes.FeeLine {
	if len(lines) == 0 {
		return nil
	}
	result := make([]*paymenttypes.FeeLine, 0, len(lines))
	for _, line := range lines {
		if line == nil {
			continue
		}
		result = append(result, &paymenttypes.FeeLine{
			LedgerAccountRef: strings.TrimSpace(line.GetLedgerAccountRef()),
			Money:            moneyFromProto(line.GetMoney()),
			LineType:         postingLineTypeFromProto(line.GetLineType()),
			Side:             entrySideFromProto(line.GetSide()),
			Meta:             cloneMetadata(line.GetMeta()),
		})
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func feeLinesToProto(lines []*paymenttypes.FeeLine) []*feesv1.DerivedPostingLine {
	if len(lines) == 0 {
		return nil
	}
	result := make([]*feesv1.DerivedPostingLine, 0, len(lines))
	for _, line := range lines {
		if line == nil {
			continue
		}
		result = append(result, &feesv1.DerivedPostingLine{
			LedgerAccountRef: strings.TrimSpace(line.LedgerAccountRef),
			Money:            protoMoney(line.Money),
			LineType:         postingLineTypeToProto(line.LineType),
			Side:             entrySideToProto(line.Side),
			Meta:             cloneMetadata(line.Meta),
		})
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func feeRulesFromProto(rules []*feesv1.AppliedRule) []*paymenttypes.AppliedRule {
	if len(rules) == 0 {
		return nil
	}
	result := make([]*paymenttypes.AppliedRule, 0, len(rules))
	for _, rule := range rules {
		if rule == nil {
			continue
		}
		result = append(result, &paymenttypes.AppliedRule{
			RuleID:      strings.TrimSpace(rule.GetRuleId()),
			RuleVersion: strings.TrimSpace(rule.GetRuleVersion()),
			Formula:     strings.TrimSpace(rule.GetFormula()),
			Rounding:    roundingModeFromProto(rule.GetRounding()),
			TaxCode:     strings.TrimSpace(rule.GetTaxCode()),
			TaxRate:     strings.TrimSpace(rule.GetTaxRate()),
			Parameters:  cloneMetadata(rule.GetParameters()),
		})
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func feeRulesToProto(rules []*paymenttypes.AppliedRule) []*feesv1.AppliedRule {
	if len(rules) == 0 {
		return nil
	}
	result := make([]*feesv1.AppliedRule, 0, len(rules))
	for _, rule := range rules {
		if rule == nil {
			continue
		}
		result = append(result, &feesv1.AppliedRule{
			RuleId:      strings.TrimSpace(rule.RuleID),
			RuleVersion: strings.TrimSpace(rule.RuleVersion),
			Formula:     strings.TrimSpace(rule.Formula),
			Rounding:    roundingModeToProto(rule.Rounding),
			TaxCode:     strings.TrimSpace(rule.TaxCode),
			TaxRate:     strings.TrimSpace(rule.TaxRate),
			Parameters:  cloneMetadata(rule.Parameters),
		})
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func entrySideFromProto(side accountingv1.EntrySide) paymenttypes.EntrySide {
	switch side {
	case accountingv1.EntrySide_ENTRY_SIDE_DEBIT:
		return paymenttypes.EntrySideDebit
	case accountingv1.EntrySide_ENTRY_SIDE_CREDIT:
		return paymenttypes.EntrySideCredit
	default:
		return paymenttypes.EntrySideUnspecified
	}
}

func entrySideToProto(side paymenttypes.EntrySide) accountingv1.EntrySide {
	switch side {
	case paymenttypes.EntrySideDebit:
		return accountingv1.EntrySide_ENTRY_SIDE_DEBIT
	case paymenttypes.EntrySideCredit:
		return accountingv1.EntrySide_ENTRY_SIDE_CREDIT
	default:
		return accountingv1.EntrySide_ENTRY_SIDE_UNSPECIFIED
	}
}

func postingLineTypeFromProto(lineType accountingv1.PostingLineType) paymenttypes.PostingLineType {
	switch lineType {
	case accountingv1.PostingLineType_POSTING_LINE_FEE:
		return paymenttypes.PostingLineTypeFee
	case accountingv1.PostingLineType_POSTING_LINE_TAX:
		return paymenttypes.PostingLineTypeTax
	case accountingv1.PostingLineType_POSTING_LINE_SPREAD:
		return paymenttypes.PostingLineTypeSpread
	case accountingv1.PostingLineType_POSTING_LINE_REVERSAL:
		return paymenttypes.PostingLineTypeReversal
	default:
		return paymenttypes.PostingLineTypeUnspecified
	}
}

func postingLineTypeToProto(lineType paymenttypes.PostingLineType) accountingv1.PostingLineType {
	switch lineType {
	case paymenttypes.PostingLineTypeFee:
		return accountingv1.PostingLineType_POSTING_LINE_FEE
	case paymenttypes.PostingLineTypeTax:
		return accountingv1.PostingLineType_POSTING_LINE_TAX
	case paymenttypes.PostingLineTypeSpread:
		return accountingv1.PostingLineType_POSTING_LINE_SPREAD
	case paymenttypes.PostingLineTypeReversal:
		return accountingv1.PostingLineType_POSTING_LINE_REVERSAL
	default:
		return accountingv1.PostingLineType_POSTING_LINE_TYPE_UNSPECIFIED
	}
}

func roundingModeFromProto(mode moneyv1.RoundingMode) paymenttypes.RoundingMode {
	switch mode {
	case moneyv1.RoundingMode_ROUND_HALF_EVEN:
		return paymenttypes.RoundingModeHalfEven
	case moneyv1.RoundingMode_ROUND_HALF_UP:
		return paymenttypes.RoundingModeHalfUp
	case moneyv1.RoundingMode_ROUND_DOWN:
		return paymenttypes.RoundingModeDown
	default:
		return paymenttypes.RoundingModeUnspecified
	}
}

func roundingModeToProto(mode paymenttypes.RoundingMode) moneyv1.RoundingMode {
	switch mode {
	case paymenttypes.RoundingModeHalfEven:
		return moneyv1.RoundingMode_ROUND_HALF_EVEN
	case paymenttypes.RoundingModeHalfUp:
		return moneyv1.RoundingMode_ROUND_HALF_UP
	case paymenttypes.RoundingModeDown:
		return moneyv1.RoundingMode_ROUND_DOWN
	default:
		return moneyv1.RoundingMode_ROUNDING_MODE_UNSPECIFIED
	}
}
