package sresponse

import (
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/tech/sendico/pkg/api/http/response"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mservice"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
	gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
	paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
	orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
	"github.com/tech/sendico/server/interface/api/srequest"
	"go.mongodb.org/mongo-driver/v2/bson"
	"google.golang.org/protobuf/types/known/timestamppb"
)

type FeeLine struct {
	LedgerAccountRef string              `json:"ledgerAccountRef,omitempty"`
	Amount           *paymenttypes.Money `json:"amount,omitempty"`
	LineType         string              `json:"lineType,omitempty"`
	Side             string              `json:"side,omitempty"`
	Meta             map[string]string   `json:"meta,omitempty"`
}

type FxQuote struct {
	QuoteRef        string              `json:"quoteRef,omitempty"`
	BaseCurrency    string              `json:"baseCurrency,omitempty"`
	QuoteCurrency   string              `json:"quoteCurrency,omitempty"`
	Side            string              `json:"side,omitempty"`
	Price           string              `json:"price,omitempty"`
	BaseAmount      *paymenttypes.Money `json:"baseAmount,omitempty"`
	QuoteAmount     *paymenttypes.Money `json:"quoteAmount,omitempty"`
	ExpiresAtUnixMs int64               `json:"expiresAtUnixMs,omitempty"`
	PricedAtUnixMs  int64               `json:"pricedAtUnixMs,omitempty"`
	Provider        string              `json:"provider,omitempty"`
	RateRef         string              `json:"rateRef,omitempty"`
	Firm            bool                `json:"firm,omitempty"`
}

type PaymentQuote struct {
	QuoteRef  string        `json:"quoteRef,omitempty"`
	IntentRef string        `json:"intentRef,omitempty"`
	Amounts   *QuoteAmounts `json:"amounts,omitempty"`
	Fees      *QuoteFees    `json:"fees,omitempty"`
	FxQuote   *FxQuote      `json:"fxQuote,omitempty"`
}

type QuoteAmounts struct {
	SourcePrincipal       *paymenttypes.Money `json:"sourcePrincipal,omitempty"`
	SourceDebitTotal      *paymenttypes.Money `json:"sourceDebitTotal,omitempty"`
	DestinationSettlement *paymenttypes.Money `json:"destinationSettlement,omitempty"`
}

type QuoteFees struct {
	Lines []FeeLine `json:"lines,omitempty"`
}

type PaymentQuotes struct {
	IdempotencyKey string         `json:"idempotencyKey,omitempty"`
	QuoteRef       string         `json:"quoteRef,omitempty"`
	Items          []PaymentQuote `json:"items,omitempty"`
}

type Payment struct {
	PaymentRef    string             `json:"paymentRef,omitempty"`
	State         string             `json:"state,omitempty"`
	Comment       string             `json:"comment,omitempty"`
	Source        *PaymentEndpoint   `json:"source"`
	Destination   *PaymentEndpoint   `json:"destination"`
	FailureCode   string             `json:"failureCode,omitempty"`
	FailureReason string             `json:"failureReason,omitempty"`
	Operations    []PaymentOperation `json:"operations,omitempty"`
	LastQuote     *PaymentQuote      `json:"lastQuote,omitempty"`
	CreatedAt     time.Time          `json:"createdAt,omitempty"`
	Meta          map[string]string  `json:"meta,omitempty"`
}

type PaymentEndpoint struct {
	Type             string `json:"type,omitempty"`
	Data             any    `json:"data,omitempty"`
	PaymentMethodRef string `json:"paymentMethodRef,omitempty"`
	PayeeRef         string `json:"payeeRef,omitempty"`
}

type PaymentOperation struct {
	StepRef       string                 `json:"stepRef,omitempty"`
	Code          string                 `json:"code,omitempty"`
	State         string                 `json:"state,omitempty"`
	Label         string                 `json:"label,omitempty"`
	Money         *PaymentOperationMoney `json:"money,omitempty"`
	OperationRef  string                 `json:"operationRef,omitempty"`
	Gateway       string                 `json:"gateway,omitempty"`
	FailureCode   string                 `json:"failureCode,omitempty"`
	FailureReason string                 `json:"failureReason,omitempty"`
	StartedAt     time.Time              `json:"startedAt,omitempty"`
	CompletedAt   time.Time              `json:"completedAt,omitempty"`
}

type PaymentOperationMoney struct {
	Planned  *PaymentOperationMoneySnapshot `json:"planned,omitempty"`
	Executed *PaymentOperationMoneySnapshot `json:"executed,omitempty"`
}

type PaymentOperationMoneySnapshot struct {
	Amount          *paymenttypes.Money `json:"amount,omitempty"`
	ConvertedAmount *paymenttypes.Money `json:"convertedAmount,omitempty"`
}

type paymentQuoteResponse struct {
	authResponse   `json:",inline"`
	IdempotencyKey string        `json:"idempotencyKey,omitempty"`
	Quote          *PaymentQuote `json:"quote"`
}

type paymentQuotesResponse struct {
	authResponse `json:",inline"`
	Quote        *PaymentQuotes `json:"quote"`
}

type paymentsResponse struct {
	authResponse `json:",inline"`
	Payments     []Payment                        `json:"payments"`
	Page         *paginationv1.CursorPageResponse `json:"page,omitempty"`
}

type paymentResponse struct {
	authResponse `json:",inline"`
	Payment      *Payment `json:"payment"`
}

// PaymentQuote wraps a payment quote with refreshed access token.
func PaymentQuoteResponse(logger mlogger.Logger, idempotencyKey string, quote *quotationv2.PaymentQuote, token *TokenData) http.HandlerFunc {
	return response.Ok(logger, paymentQuoteResponse{
		Quote:          toPaymentQuote(quote),
		IdempotencyKey: idempotencyKey,
		authResponse:   authResponse{AccessToken: *token},
	})
}

// PaymentQuotes wraps batch quotes with refreshed access token.
func PaymentQuotesResponse(logger mlogger.Logger, resp *quotationv2.QuotePaymentsResponse, token *TokenData) http.HandlerFunc {
	return response.Ok(logger, paymentQuotesResponse{
		Quote:        toPaymentQuotes(resp),
		authResponse: authResponse{AccessToken: *token},
	})
}

// Payments wraps a list of payments with refreshed access token.
func PaymentsResponse(logger mlogger.Logger, payments []*orchestrationv2.Payment, token *TokenData) http.HandlerFunc {
	return response.Ok(logger, paymentsResponse{
		Payments:     toPayments(payments),
		authResponse: authResponse{AccessToken: *token},
	})
}

// PaymentsList wraps a list of payments with refreshed access token and pagination data.
func PaymentsListResponse(logger mlogger.Logger, resp *orchestrationv2.ListPaymentsResponse, token *TokenData) http.HandlerFunc {
	return response.Ok(logger, paymentsResponse{
		Payments:     toPayments(resp.GetPayments()),
		Page:         resp.GetPage(),
		authResponse: authResponse{AccessToken: *token},
	})
}

// Payment wraps a payment with refreshed access token.
func PaymentResponse(logger mlogger.Logger, payment *orchestrationv2.Payment, token *TokenData) http.HandlerFunc {
	return response.Ok(logger, paymentResponse{
		Payment:      toPayment(payment),
		authResponse: authResponse{AccessToken: *token},
	})
}

func toFeeLines(lines []*feesv1.DerivedPostingLine) []FeeLine {
	if len(lines) == 0 {
		return nil
	}
	result := make([]FeeLine, 0, len(lines))
	for _, line := range lines {
		if line == nil {
			continue
		}
		result = append(result, FeeLine{
			LedgerAccountRef: line.GetLedgerAccountRef(),
			Amount:           toMoney(line.GetMoney()),
			LineType:         enumJSONName(line.GetLineType().String()),
			Side:             enumJSONName(line.GetSide().String()),
			Meta:             line.GetMeta(),
		})
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func toFxQuote(q *oraclev1.Quote) *FxQuote {
	if q == nil {
		return nil
	}
	pair := q.GetPair()
	pricedAtUnixMs := int64(0)
	if ts := q.GetPricedAt(); ts != nil {
		pricedAtUnixMs = ts.AsTime().UnixMilli()
	}
	base := ""
	quote := ""
	if pair != nil {
		base = pair.GetBase()
		quote = pair.GetQuote()
	}
	return &FxQuote{
		QuoteRef:        q.GetQuoteRef(),
		BaseCurrency:    base,
		QuoteCurrency:   quote,
		Side:            enumJSONName(q.GetSide().String()),
		Price:           q.GetPrice().GetValue(),
		BaseAmount:      toMoney(q.GetBaseAmount()),
		QuoteAmount:     toMoney(q.GetQuoteAmount()),
		ExpiresAtUnixMs: q.GetExpiresAtUnixMs(),
		PricedAtUnixMs:  pricedAtUnixMs,
		Provider:        q.GetProvider(),
		RateRef:         q.GetRateRef(),
		Firm:            q.GetFirm(),
	}
}

func toPaymentQuote(q *quotationv2.PaymentQuote) *PaymentQuote {
	if q == nil {
		return nil
	}
	amounts := toQuoteAmounts(q)
	fees := toQuoteFees(q.GetFeeLines())
	return &PaymentQuote{
		QuoteRef:  q.GetQuoteRef(),
		IntentRef: strings.TrimSpace(q.GetIntentRef()),
		Amounts:   amounts,
		Fees:      fees,
		FxQuote:   toFxQuote(q.GetFxQuote()),
	}
}

func toPaymentQuotes(resp *quotationv2.QuotePaymentsResponse) *PaymentQuotes {
	if resp == nil {
		return nil
	}
	items := make([]PaymentQuote, 0, len(resp.GetQuotes()))
	for _, quote := range resp.GetQuotes() {
		if dto := toPaymentQuote(quote); dto != nil {
			items = append(items, *dto)
		}
	}
	if len(items) == 0 {
		items = nil
	}
	return &PaymentQuotes{
		IdempotencyKey: resp.GetIdempotencyKey(),
		QuoteRef:       resp.GetQuoteRef(),
		Items:          items,
	}
}

func toQuoteAmounts(q *quotationv2.PaymentQuote) *QuoteAmounts {
	if q == nil {
		return nil
	}
	amounts := &QuoteAmounts{
		SourcePrincipal:       toMoney(q.GetTransferPrincipalAmount()),
		SourceDebitTotal:      toMoney(q.GetPayerTotalDebitAmount()),
		DestinationSettlement: toMoney(q.GetDestinationAmount()),
	}
	if amounts.SourcePrincipal == nil && amounts.SourceDebitTotal == nil && amounts.DestinationSettlement == nil {
		return nil
	}
	return amounts
}

func toQuoteFees(lines []*feesv1.DerivedPostingLine) *QuoteFees {
	feeLines := toFeeLines(lines)
	if len(feeLines) == 0 {
		return nil
	}
	return &QuoteFees{Lines: feeLines}
}

func toPayments(items []*orchestrationv2.Payment) []Payment {
	if len(items) == 0 {
		return nil
	}
	result := make([]Payment, 0, len(items))
	for _, item := range items {
		if p := toPayment(item); p != nil {
			result = append(result, *p)
		}
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func toPayment(p *orchestrationv2.Payment) *Payment {
	if p == nil {
		return nil
	}
	intent := p.GetIntentSnapshot()
	operations := toUserVisibleOperations(p.GetStepExecutions())
	failureCode, failureReason := firstFailure(operations)
	return &Payment{
		PaymentRef:    p.GetPaymentRef(),
		State:         enumJSONName(p.GetState().String()),
		Comment:       strings.TrimSpace(intent.GetComment()),
		Source:        toPaymentEndpoint(intent.GetSource()),
		Destination:   toPaymentEndpoint(intent.GetDestination()),
		FailureCode:   failureCode,
		FailureReason: failureReason,
		Operations:    operations,
		LastQuote:     toPaymentQuote(p.GetQuoteSnapshot()),
		CreatedAt:     timestampAsTime(p.GetCreatedAt()),
		Meta:          paymentMeta(p),
	}
}

func toPaymentEndpoint(endpoint *endpointv1.PaymentEndpoint) *PaymentEndpoint {
	if endpoint == nil {
		return nil
	}
	if paymentMethodRef := strings.TrimSpace(endpoint.GetPaymentMethodRef()); paymentMethodRef != "" {
		return &PaymentEndpoint{PaymentMethodRef: paymentMethodRef}
	}
	if payeeRef := strings.TrimSpace(endpoint.GetPayeeRef()); payeeRef != "" {
		return &PaymentEndpoint{PayeeRef: payeeRef}
	}
	method := endpoint.GetPaymentMethod()
	if method == nil {
		return nil
	}
	return &PaymentEndpoint{
		Type: paymentEndpointType(method.GetType()),
		Data: paymentEndpointData(method),
	}
}

func paymentEndpointType(methodType endpointv1.PaymentMethodType) string {
	switch methodType {
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
		return string(srequest.EndpointTypeIBAN)
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
		return string(srequest.EndpointTypeCard)
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN:
		return string(srequest.EndpointTypeCardToken)
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
		return string(srequest.EndpointTypeBankAccount)
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET:
		return string(srequest.EndpointTypeWallet)
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS:
		return string(srequest.EndpointTypeExternalChain)
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER:
		return string(srequest.EndpointTypeLedger)
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_ACCOUNT:
		return "account"
	default:
		return "unspecified"
	}
}

func paymentEndpointData(method *endpointv1.PaymentMethod) any {
	if method == nil {
		return nil
	}
	switch method.GetType() {
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER:
		type ledgerMethodData struct {
			LedgerAccountRef       string `bson:"ledgerAccountRef"`
			ContraLedgerAccountRef string `bson:"contraLedgerAccountRef,omitempty"`
		}
		var payload ledgerMethodData
		if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
			return toRawBSON(method.GetData())
		}
		return srequest.LedgerEndpoint{
			LedgerAccountRef:       strings.TrimSpace(payload.LedgerAccountRef),
			ContraLedgerAccountRef: strings.TrimSpace(payload.ContraLedgerAccountRef),
		}

	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET:
		type walletMethodData struct {
			WalletID string `bson:"walletId"`
		}
		var payload walletMethodData
		if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
			return toRawBSON(method.GetData())
		}
		return srequest.WalletEndpoint{
			WalletID: strings.TrimSpace(payload.WalletID),
		}

	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS:
		type cryptoMethodData struct {
			Currency       string  `bson:"currency"`
			Address        string  `bson:"address"`
			Network        string  `bson:"network"`
			DestinationTag *string `bson:"destinationTag,omitempty"`
		}
		var payload cryptoMethodData
		if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
			return toRawBSON(method.GetData())
		}
		endpoint := srequest.ExternalChainEndpoint{
			Asset: &srequest.Asset{
				Chain:       parseChainNetwork(payload.Network),
				TokenSymbol: strings.ToUpper(strings.TrimSpace(payload.Currency)),
			},
			Address: strings.TrimSpace(payload.Address),
		}
		if memo := strings.TrimSpace(strPtr(payload.DestinationTag)); memo != "" {
			endpoint.Memo = memo
		}
		if endpoint.Asset.Chain == srequest.ChainNetworkUnspecified && endpoint.Asset.TokenSymbol == "" {
			endpoint.Asset = nil
		}
		return endpoint

	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
		type cardMethodData struct {
			Pan       string `bson:"pan"`
			FirstName string `bson:"firstName"`
			LastName  string `bson:"lastName"`
			ExpMonth  string `bson:"expMonth"`
			ExpYear   string `bson:"expYear"`
			Country   string `bson:"country,omitempty"`
		}
		var payload cardMethodData
		if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
			return toRawBSON(method.GetData())
		}
		return srequest.CardEndpoint{
			Pan:       strings.TrimSpace(payload.Pan),
			FirstName: strings.TrimSpace(payload.FirstName),
			LastName:  strings.TrimSpace(payload.LastName),
			ExpMonth:  parseUint32(payload.ExpMonth),
			ExpYear:   parseUint32(payload.ExpYear),
			Country:   strings.TrimSpace(payload.Country),
		}

	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN:
		type cardTokenMethodData struct {
			Token string `bson:"token"`
			Last4 string `bson:"last4,omitempty"`
		}
		var payload cardTokenMethodData
		if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
			return toRawBSON(method.GetData())
		}
		return srequest.CardTokenEndpoint{
			Token:     strings.TrimSpace(payload.Token),
			MaskedPan: strings.TrimSpace(payload.Last4),
		}

	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
		type bankAccountMethodData struct {
			RecipientName        string `bson:"recipientName"`
			Inn                  string `bson:"inn"`
			Kpp                  string `bson:"kpp"`
			BankName             string `bson:"bankName"`
			Bik                  string `bson:"bik"`
			AccountNumber        string `bson:"accountNumber"`
			CorrespondentAccount string `bson:"correspondentAccount"`
		}
		var payload bankAccountMethodData
		if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
			return toRawBSON(method.GetData())
		}
		return srequest.BankAccountEndpoint{
			RecipientName:        strings.TrimSpace(payload.RecipientName),
			Inn:                  strings.TrimSpace(payload.Inn),
			Kpp:                  strings.TrimSpace(payload.Kpp),
			BankName:             strings.TrimSpace(payload.BankName),
			Bik:                  strings.TrimSpace(payload.Bik),
			AccountNumber:        strings.TrimSpace(payload.AccountNumber),
			CorrespondentAccount: strings.TrimSpace(payload.CorrespondentAccount),
		}

	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
		type ibanMethodData struct {
			IBAN          string  `bson:"iban"`
			AccountHolder string  `bson:"accountHolder"`
			BIC           *string `bson:"bic,omitempty"`
			BankName      *string `bson:"bankName,omitempty"`
		}
		var payload ibanMethodData
		if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
			return toRawBSON(method.GetData())
		}
		return srequest.IBANEndpoint{
			IBAN:          strings.TrimSpace(payload.IBAN),
			AccountHolder: strings.TrimSpace(payload.AccountHolder),
			BIC:           strings.TrimSpace(strPtr(payload.BIC)),
			BankName:      strings.TrimSpace(strPtr(payload.BankName)),
		}

	default:
		return toRawBSON(method.GetData())
	}
}

func toRawBSON(raw []byte) map[string]any {
	if len(raw) == 0 {
		return nil
	}
	var data map[string]any
	if err := bson.Unmarshal(raw, &data); err != nil {
		return nil
	}
	if len(data) == 0 {
		return nil
	}
	return data
}

func parseChainNetwork(value string) srequest.ChainNetwork {
	switch strings.ToUpper(strings.TrimSpace(value)) {
	case "ETHEREUM_MAINNET":
		return srequest.ChainNetworkEthereumMainnet
	case "ARBITRUM_ONE":
		return srequest.ChainNetworkArbitrumOne
	case "TRON_MAINNET":
		return srequest.ChainNetworkTronMainnet
	case "TRON_NILE":
		return srequest.ChainNetworkTronNile
	case "", "UNSPECIFIED":
		return srequest.ChainNetworkUnspecified
	default:
		return srequest.ChainNetwork(strings.ToLower(strings.TrimSpace(value)))
	}
}

func parseUint32(value string) uint32 {
	clean := strings.TrimSpace(value)
	if clean == "" {
		return 0
	}
	parsed, err := strconv.ParseUint(clean, 10, 32)
	if err != nil {
		return 0
	}
	return uint32(parsed)
}

func strPtr(v *string) string {
	if v == nil {
		return ""
	}
	return *v
}

func firstFailure(operations []PaymentOperation) (string, string) {
	for _, op := range operations {
		if strings.TrimSpace(op.FailureCode) == "" && strings.TrimSpace(op.FailureReason) == "" {
			continue
		}
		return strings.TrimSpace(op.FailureCode), strings.TrimSpace(op.FailureReason)
	}
	return "", ""
}

func toUserVisibleOperations(steps []*orchestrationv2.StepExecution) []PaymentOperation {
	if len(steps) == 0 {
		return nil
	}
	ops := make([]PaymentOperation, 0, len(steps))
	for _, step := range steps {
		if step == nil || !isUserVisibleStep(step.GetReportVisibility()) {
			continue
		}
		ops = append(ops, toPaymentOperation(step))
	}
	if len(ops) == 0 {
		return nil
	}
	return ops
}

func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation {
	operationRef, gateway := operationRefAndGateway(step.GetStepCode(), step.GetRefs())
	plannedAmount := stepPlannedAmount(step)
	plannedConvertedAmount := stepPlannedConvertedAmount(step)
	executedAmount := stepExecutedAmount(step)
	executedConvertedAmount := stepExecutedConvertedAmount(step)
	op := PaymentOperation{
		StepRef:      step.GetStepRef(),
		Code:         step.GetStepCode(),
		State:        enumJSONName(step.GetState().String()),
		Label:        strings.TrimSpace(step.GetUserLabel()),
		Money:        toOperationMoney(plannedAmount, plannedConvertedAmount, executedAmount, executedConvertedAmount),
		OperationRef: operationRef,
		Gateway:      gateway,
		StartedAt:    timestampAsTime(step.GetStartedAt()),
		CompletedAt:  timestampAsTime(step.GetCompletedAt()),
	}
	failure := step.GetFailure()
	if failure == nil {
		return op
	}
	op.FailureCode = enumJSONName(failure.GetCategory().String())
	op.FailureReason = strings.TrimSpace(failure.GetMessage())
	if op.FailureReason == "" {
		op.FailureReason = strings.TrimSpace(failure.GetCode())
	}
	return op
}

func stepPlannedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money {
	if step == nil {
		return nil
	}
	if money := step.GetMoney(); money != nil {
		if planned := money.GetPlanned(); planned != nil {
			if normalized := normalizeOperationMoney(toMoney(planned.GetAmount())); normalized != nil {
				return normalized
			}
		}
	}
	return nil
}

func stepPlannedConvertedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money {
	if step == nil {
		return nil
	}
	if money := step.GetMoney(); money != nil {
		if planned := money.GetPlanned(); planned != nil {
			if normalized := normalizeOperationMoney(toMoney(planned.GetConvertedAmount())); normalized != nil {
				return normalized
			}
		}
	}
	return nil
}

func stepExecutedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money {
	if step == nil {
		return nil
	}
	if money := step.GetMoney(); money != nil {
		if executed := money.GetExecuted(); executed != nil {
			if normalized := normalizeOperationMoney(toMoney(executed.GetAmount())); normalized != nil {
				return normalized
			}
		}
	}
	return nil
}

func stepExecutedConvertedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money {
	if step == nil {
		return nil
	}
	if money := step.GetMoney(); money != nil {
		if executed := money.GetExecuted(); executed != nil {
			if normalized := normalizeOperationMoney(toMoney(executed.GetConvertedAmount())); normalized != nil {
				return normalized
			}
		}
	}
	return nil
}

func toOperationMoney(
	plannedAmount *paymenttypes.Money,
	plannedConvertedAmount *paymenttypes.Money,
	executedAmount *paymenttypes.Money,
	executedConvertedAmount *paymenttypes.Money,
) *PaymentOperationMoney {
	planned := toOperationMoneySnapshot(plannedAmount, plannedConvertedAmount)
	executed := toOperationMoneySnapshot(executedAmount, executedConvertedAmount)
	if planned == nil && executed == nil {
		return nil
	}
	return &PaymentOperationMoney{
		Planned:  planned,
		Executed: executed,
	}
}

func toOperationMoneySnapshot(amount *paymenttypes.Money, convertedAmount *paymenttypes.Money) *PaymentOperationMoneySnapshot {
	if amount == nil && convertedAmount == nil {
		return nil
	}
	return &PaymentOperationMoneySnapshot{
		Amount:          amount,
		ConvertedAmount: convertedAmount,
	}
}

func normalizeOperationMoney(value *paymenttypes.Money) *paymenttypes.Money {
	if value == nil {
		return nil
	}
	amount := strings.TrimSpace(value.GetAmount())
	currency := strings.TrimSpace(value.GetCurrency())
	if amount == "" || currency == "" {
		return nil
	}
	return &paymenttypes.Money{
		Amount:   amount,
		Currency: currency,
	}
}

const (
	externalRefKindOperation = "operation_ref"
)

func operationRefAndGateway(stepCode string, refs []*orchestrationv2.ExternalReference) (string, mservice.Type) {
	var (
		operationRef string
		gateway      mservice.Type
	)

	for _, ref := range refs {
		if ref == nil {
			continue
		}

		kind := strings.ToLower(strings.TrimSpace(ref.GetKind()))
		value := strings.TrimSpace(ref.GetRef())
		candidateGateway := inferGatewayType(ref.GetGatewayInstanceId(), ref.GetRail(), stepCode)

		if kind == externalRefKindOperation && operationRef == "" && value != "" {
			operationRef = value
		}
		if gateway == "" && candidateGateway != "" {
			gateway = candidateGateway
		}
	}
	if gateway == "" {
		gateway = inferGatewayType("", gatewayv1.Rail_RAIL_UNSPECIFIED, stepCode)
	}
	return operationRef, gateway
}

func inferGatewayType(gatewayInstanceID string, rail gatewayv1.Rail, stepCode string) mservice.Type {
	if gateway := gatewayTypeFromInstanceID(gatewayInstanceID); gateway != "" {
		return gateway
	}
	if gateway := gatewayTypeFromRail(rail); gateway != "" {
		return gateway
	}
	return gatewayTypeFromStepCode(stepCode)
}

func gatewayTypeFromInstanceID(raw string) mservice.Type {
	value := strings.ToLower(strings.TrimSpace(raw))
	if value == "" {
		return ""
	}

	switch value {
	case mservice.ChainGateway, mservice.TronGateway, mservice.MntxGateway, mservice.PaymentGateway, mservice.TgSettle, mservice.Ledger:
		return value
	}

	switch {
	case strings.Contains(value, "ledger"):
		return mservice.Ledger
	case strings.Contains(value, "tgsettle"):
		return mservice.TgSettle
	case strings.Contains(value, "payment_gateway"),
		strings.Contains(value, "settlement"),
		strings.Contains(value, "onramp"),
		strings.Contains(value, "offramp"):
		return mservice.PaymentGateway
	case strings.Contains(value, "mntx"), strings.Contains(value, "mcards"):
		return mservice.MntxGateway
	case strings.Contains(value, "tron"):
		return mservice.TronGateway
	case strings.Contains(value, "chain"), strings.Contains(value, "crypto"):
		return mservice.ChainGateway
	case strings.Contains(value, "card"):
		return mservice.MntxGateway
	default:
		return ""
	}
}

func gatewayTypeFromRail(rail gatewayv1.Rail) mservice.Type {
	switch rail {
	case gatewayv1.Rail_RAIL_LEDGER:
		return mservice.Ledger
	case gatewayv1.Rail_RAIL_CARD:
		return mservice.MntxGateway
	case gatewayv1.Rail_RAIL_SETTLEMENT, gatewayv1.Rail_RAIL_ONRAMP, gatewayv1.Rail_RAIL_OFFRAMP:
		return mservice.PaymentGateway
	case gatewayv1.Rail_RAIL_CRYPTO:
		return mservice.ChainGateway
	default:
		return ""
	}
}

func gatewayTypeFromStepCode(stepCode string) mservice.Type {
	code := strings.ToLower(strings.TrimSpace(stepCode))
	switch {
	case strings.Contains(code, "ledger"):
		return mservice.Ledger
	case strings.Contains(code, "card_payout"), strings.Contains(code, ".card."):
		return mservice.MntxGateway
	case strings.Contains(code, "provider_settlement"),
		strings.Contains(code, "settlement"),
		strings.Contains(code, "fx_convert"),
		strings.Contains(code, "onramp"),
		strings.Contains(code, "offramp"):
		return mservice.PaymentGateway
	case strings.Contains(code, "crypto"), strings.Contains(code, "chain"):
		return mservice.ChainGateway
	default:
		return ""
	}
}

func isUserVisibleStep(visibility orchestrationv2.ReportVisibility) bool {
	switch visibility {
	case orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN,
		orchestrationv2.ReportVisibility_REPORT_VISIBILITY_BACKOFFICE,
		orchestrationv2.ReportVisibility_REPORT_VISIBILITY_AUDIT:
		return false
	default:
		return true
	}
}

func paymentMeta(p *orchestrationv2.Payment) map[string]string {
	if p == nil {
		return nil
	}
	meta := make(map[string]string)
	if quotationRef := strings.TrimSpace(p.GetQuotationRef()); quotationRef != "" {
		meta["quotationRef"] = quotationRef
	}
	if clientPaymentRef := strings.TrimSpace(p.GetClientPaymentRef()); clientPaymentRef != "" {
		meta["clientPaymentRef"] = clientPaymentRef
	}
	if version := p.GetVersion(); version > 0 {
		meta["version"] = strconv.FormatUint(version, 10)
	}
	if len(meta) == 0 {
		return nil
	}
	return meta
}

func timestampAsTime(ts *timestamppb.Timestamp) time.Time {
	if ts == nil {
		return time.Time{}
	}
	return ts.AsTime()
}

func enumJSONName(value string) string {
	return strings.ToLower(strings.TrimSpace(value))
}
