package gateway

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/tech/sendico/gateway/chain/internal/appversion"
	"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
	chainstoragemodel "github.com/tech/sendico/gateway/chain/storage/model"
	chainasset "github.com/tech/sendico/pkg/chain"
	"github.com/tech/sendico/pkg/connector/params"
	"github.com/tech/sendico/pkg/merrors"
	describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/structpb"
	"google.golang.org/protobuf/types/known/timestamppb"
)

const chainConnectorID = "chain"

func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
	return &connectorv1.GetCapabilitiesResponse{
		Capabilities: &connectorv1.ConnectorCapabilities{
			ConnectorType:         chainConnectorID,
			Version:               appversion.Create().Short(),
			SupportedAccountKinds: []connectorv1.AccountKind{connectorv1.AccountKind_CHAIN_MANAGED_WALLET},
			SupportedOperationTypes: []connectorv1.OperationType{
				connectorv1.OperationType_TRANSFER,
				connectorv1.OperationType_FEE_ESTIMATE,
				connectorv1.OperationType_GAS_TOPUP,
			},
			OpenAccountParams: chainOpenAccountParams(),
			OperationParams:   chainOperationParams(),
		},
	}, nil
}

func (s *Service) OpenAccount(ctx context.Context, req *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) {
	if req == nil {
		return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: request is required", nil)}, nil
	}
	if req.GetKind() != connectorv1.AccountKind_CHAIN_MANAGED_WALLET {
		return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported account kind", nil)}, nil
	}
	reader := params.New(req.GetParams())
	orgRef := strings.TrimSpace(reader.String("organization_ref"))
	if orgRef == "" {
		return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: organization_ref is required", nil)}, nil
	}
	asset, err := parseChainAsset(strings.TrimSpace(req.GetAsset()), reader)
	if err != nil {
		return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), nil)}, nil
	}

	resp, err := s.CreateManagedWallet(ctx, &chainv1.CreateManagedWalletRequest{
		IdempotencyKey:  strings.TrimSpace(req.GetIdempotencyKey()),
		OrganizationRef: orgRef,
		OwnerRef:        strings.TrimSpace(req.GetOwnerRef()),
		Asset:           asset,
		Metadata:        shared.CloneMetadata(reader.StringMap("metadata")),
		Describable:     describableFromLabel(req.GetLabel(), reader.String("description")),
	})
	if err != nil {
		return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil)}, nil
	}
	return &connectorv1.OpenAccountResponse{Account: chainWalletToAccount(resp.GetWallet())}, nil
}

func (s *Service) GetAccount(ctx context.Context, req *connectorv1.GetAccountRequest) (*connectorv1.GetAccountResponse, error) {
	if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
		return nil, merrors.InvalidArgument("get_account: account_ref.account_id is required")
	}
	resp, err := s.GetManagedWallet(ctx, &chainv1.GetManagedWalletRequest{WalletRef: strings.TrimSpace(req.GetAccountRef().GetAccountId())})
	if err != nil {
		return nil, err
	}
	return &connectorv1.GetAccountResponse{Account: chainWalletToAccount(resp.GetWallet())}, nil
}

func (s *Service) ListAccounts(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error) {
	if req == nil {
		return nil, merrors.InvalidArgument("list_accounts: request is required")
	}
	asset := (*chainv1.Asset)(nil)
	if assetString := strings.TrimSpace(req.GetAsset()); assetString != "" {
		parsed, err := parseChainAsset(assetString, params.New(nil))
		if err != nil {
			s.logger.Warn("Error listing accounts", zap.Error(err))
			return nil, err
		}
		asset = parsed
	}
	resp, err := s.ListManagedWallets(ctx, &chainv1.ListManagedWalletsRequest{
		OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
		OwnerRefFilter:  req.GetOwnerRefFilter(),
		Asset:           asset,
		Page:            req.GetPage(),
	})
	if err != nil {
		return nil, err
	}
	accounts := make([]*connectorv1.Account, 0, len(resp.GetWallets()))
	for _, wallet := range resp.GetWallets() {
		accounts = append(accounts, chainWalletToAccount(wallet))
	}
	return &connectorv1.ListAccountsResponse{
		Accounts: accounts,
		Page:     resp.GetPage(),
	}, nil
}

func (s *Service) GetBalance(ctx context.Context, req *connectorv1.GetBalanceRequest) (*connectorv1.GetBalanceResponse, error) {
	if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
		return nil, merrors.InvalidArgument("get_balance: account_ref.account_id is required")
	}
	resp, err := s.GetWalletBalance(ctx, &chainv1.GetWalletBalanceRequest{WalletRef: strings.TrimSpace(req.GetAccountRef().GetAccountId())})
	if err != nil {
		return nil, err
	}
	bal := resp.GetBalance()
	return &connectorv1.GetBalanceResponse{
		Balance: &connectorv1.Balance{
			AccountRef:      req.GetAccountRef(),
			Available:       bal.GetAvailable(),
			PendingInbound:  bal.GetPendingInbound(),
			PendingOutbound: bal.GetPendingOutbound(),
			CalculatedAt:    bal.GetCalculatedAt(),
		},
	}, nil
}

func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
	if req == nil || req.GetOperation() == nil {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil)}}, nil
	}
	op := req.GetOperation()
	if strings.TrimSpace(op.GetIdempotencyKey()) == "" {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op)}}, nil
	}
	reader := params.New(op.GetParams())
	orgRef := strings.TrimSpace(reader.String("organization_ref"))
	source := operationAccountID(op.GetFrom())
	if source == "" {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "operation: from.account is required", op)}}, nil
	}

	switch op.GetType() {
	case connectorv1.OperationType_TRANSFER:
		dest, err := transferDestinationFromOperation(op)
		if err != nil {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op)}}, nil
		}
		amount := op.GetMoney()
		if amount == nil {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: money is required", op)}}, nil
		}
		amount = normalizeMoneyForChain(amount)
		if orgRef == "" {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: organization_ref is required", op)}}, nil
		}
		resp, err := s.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{
			IdempotencyKey:  strings.TrimSpace(op.GetIdempotencyKey()),
			OrganizationRef: orgRef,
			SourceWalletRef: source,
			IntentRef:       strings.TrimSpace(op.GetIntentRef()),
			OperationRef:    op.GetOperationRef(),
			Destination:     dest,
			Amount:          amount,
			Fees:            parseChainFees(reader),
			Metadata:        shared.CloneMetadata(reader.StringMap("metadata")),
			PaymentRef:      strings.TrimSpace(reader.String("payment_ref")),
		})
		if err != nil {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil
		}
		transfer := resp.GetTransfer()
		return &connectorv1.SubmitOperationResponse{
			Receipt: &connectorv1.OperationReceipt{
				OperationId: strings.TrimSpace(transfer.GetTransferRef()),
				Status:      chainTransferStatusToOperation(transfer.GetStatus()),
				ProviderRef: strings.TrimSpace(transfer.GetTransactionHash()),
			},
		}, nil
	case connectorv1.OperationType_FEE_ESTIMATE:
		dest, err := transferDestinationFromOperation(op)
		if err != nil {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op)}}, nil
		}
		amount := op.GetMoney()
		if amount == nil {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "estimate: money is required", op)}}, nil
		}
		amount = normalizeMoneyForChain(amount)
		opID := strings.TrimSpace(op.GetOperationId())
		if opID == "" {
			opID = strings.TrimSpace(op.GetIdempotencyKey())
		}
		resp, err := s.EstimateTransferFee(ctx, &chainv1.EstimateTransferFeeRequest{
			SourceWalletRef: source,
			Destination:     dest,
			Amount:          amount,
		})
		if err != nil {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil
		}
		result := feeEstimateResult(resp)
		return &connectorv1.SubmitOperationResponse{
			Receipt: &connectorv1.OperationReceipt{
				OperationId: opID,
				Status:      connectorv1.OperationStatus_OPERATION_SUCCESS,
				Result:      result,
			},
		}, nil
	case connectorv1.OperationType_GAS_TOPUP:
		fee, err := parseMoneyFromMap(reader.Map("estimated_total_fee"))
		if err != nil {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op)}}, nil
		}
		fee = normalizeMoneyForChain(fee)
		mode := strings.ToLower(strings.TrimSpace(reader.String("mode")))
		if mode == "" {
			mode = "compute"
		}
		switch mode {
		case "compute":
			opID := strings.TrimSpace(op.GetOperationId())
			if opID == "" {
				opID = strings.TrimSpace(op.GetIdempotencyKey())
			}
			resp, err := s.ComputeGasTopUp(ctx, &chainv1.ComputeGasTopUpRequest{
				WalletRef:         source,
				EstimatedTotalFee: fee,
			})
			if err != nil {
				return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil
			}
			return &connectorv1.SubmitOperationResponse{
				Receipt: &connectorv1.OperationReceipt{
					OperationId: opID,
					Status:      connectorv1.OperationStatus_OPERATION_SUCCESS,
					Result:      gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), ""),
				},
			}, nil
		case "ensure":
			opID := strings.TrimSpace(op.GetOperationId())
			if opID == "" {
				opID = strings.TrimSpace(op.GetIdempotencyKey())
			}
			if orgRef == "" {
				return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: organization_ref is required", op)}}, nil
			}
			target := strings.TrimSpace(reader.String("target_wallet_ref"))
			if target == "" {
				return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: target_wallet_ref is required", op)}}, nil
			}
			resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
				IdempotencyKey:    strings.TrimSpace(op.GetIdempotencyKey()),
				IntentRef:         strings.TrimSpace(op.GetIntentRef()),
				OperationRef:      strings.TrimSpace(op.GetOperationRef()),
				OrganizationRef:   orgRef,
				SourceWalletRef:   source,
				TargetWalletRef:   target,
				EstimatedTotalFee: fee,
				Metadata:          shared.CloneMetadata(reader.StringMap("metadata")),
				PaymentRef:        strings.TrimSpace(reader.String("payment_ref")),
			})
			if err != nil {
				return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil
			}
			transferRef := ""
			if transfer := resp.GetTransfer(); transfer != nil {
				transferRef = strings.TrimSpace(transfer.GetTransferRef())
			}
			return &connectorv1.SubmitOperationResponse{
				Receipt: &connectorv1.OperationReceipt{
					OperationId: opID,
					Status:      shared.СhainTransferStatusToOperation(resp.GetTransfer().GetStatus()),
					Result:      gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), transferRef),
				},
			}, nil
		default:
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: invalid mode", op)}}, nil
		}
	default:
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op)}}, nil
	}
}

func (s *Service) GetOperation(ctx context.Context, req *connectorv1.GetOperationRequest) (*connectorv1.GetOperationResponse, error) {
	if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
		return nil, merrors.InvalidArgument("get_operation: operation_id is required")
	}

	operationRef := strings.TrimSpace(req.GetOperationId())
	if s.storage == nil || s.storage.Transfers() == nil {
		return nil, merrors.Internal("get_operation: storage is not configured")
	}

	transfer, err := s.storage.Transfers().FindByOperationRef(ctx, "", operationRef)
	if err != nil {
		return nil, err
	}
	if transfer == nil {
		return nil, merrors.NoData("transfer not found")
	}

	return &connectorv1.GetOperationResponse{Operation: chainTransferToOperation(storageTransferToProto(transfer))}, nil
}

func (s *Service) ListOperations(ctx context.Context, req *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
	if req == nil {
		return nil, merrors.InvalidArgument("list_operations: request is required")
	}
	source := ""
	if req.GetAccountRef() != nil {
		source = strings.TrimSpace(req.GetAccountRef().GetAccountId())
	}
	resp, err := s.ListTransfers(ctx, &chainv1.ListTransfersRequest{
		SourceWalletRef: source,
		Status:          chainStatusFromOperation(req.GetStatus()),
		Page:            req.GetPage(),
	})
	if err != nil {
		return nil, err
	}
	ops := make([]*connectorv1.Operation, 0, len(resp.GetTransfers()))
	for _, transfer := range resp.GetTransfers() {
		ops = append(ops, chainTransferToOperation(transfer))
	}
	return &connectorv1.ListOperationsResponse{Operations: ops, Page: resp.GetPage()}, nil
}

func chainOpenAccountParams() []*connectorv1.ParamSpec {
	return []*connectorv1.ParamSpec{
		{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference for the wallet."},
		{Key: "network", Type: connectorv1.ParamType_STRING, Required: true, Description: "Blockchain network name."},
		{Key: "token_symbol", Type: connectorv1.ParamType_STRING, Required: true, Description: "Token symbol (e.g., USDT)."},
		{Key: "contract_address", Type: connectorv1.ParamType_STRING, Required: false, Description: "Token contract address override."},
		{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Additional metadata map."},
		{Key: "description", Type: connectorv1.ParamType_STRING, Required: false, Description: "Wallet description."},
	}
}

func chainOperationParams() []*connectorv1.OperationParamSpec {
	return []*connectorv1.OperationParamSpec{
		{OperationType: connectorv1.OperationType_TRANSFER, Params: []*connectorv1.ParamSpec{
			{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference."},
			{Key: "destination_memo", Type: connectorv1.ParamType_STRING, Required: false, Description: "Destination memo/tag."},
			{Key: "client_reference", Type: connectorv1.ParamType_STRING, Required: false, Description: "Client reference id."},
			{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Transfer metadata."},
			{Key: "fees", Type: connectorv1.ParamType_JSON, Required: false, Description: "Service fee breakdowns."},
		}},
		{OperationType: connectorv1.OperationType_FEE_ESTIMATE, Params: []*connectorv1.ParamSpec{
			{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: false, Description: "Organization reference."},
			{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Estimate metadata."},
		}},
		{OperationType: connectorv1.OperationType_GAS_TOPUP, Params: []*connectorv1.ParamSpec{
			{Key: "mode", Type: connectorv1.ParamType_STRING, Required: false, Description: "compute | ensure."},
			{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: false, Description: "Organization reference (required for ensure)."},
			{Key: "target_wallet_ref", Type: connectorv1.ParamType_STRING, Required: false, Description: "Target wallet ref (ensure)."},
			{Key: "estimated_total_fee", Type: connectorv1.ParamType_JSON, Required: true, Description: "Estimated total fee {amount,currency}."},
			{Key: "client_reference", Type: connectorv1.ParamType_STRING, Required: false, Description: "Client reference."},
			{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Top-up metadata."},
		}},
	}
}

func chainWalletToAccount(wallet *chainv1.ManagedWallet) *connectorv1.Account {
	if wallet == nil {
		return nil
	}
	details, _ := structpb.NewStruct(map[string]interface{}{
		"deposit_address":  wallet.GetDepositAddress(),
		"organization_ref": wallet.GetOrganizationRef(),
		"owner_ref":        wallet.GetOwnerRef(),
		"network":          wallet.GetAsset().GetChain().String(),
		"token_symbol":     wallet.GetAsset().GetTokenSymbol(),
		"contract_address": wallet.GetAsset().GetContractAddress(),
		"wallet_ref":       wallet.GetWalletRef(),
	})
	return &connectorv1.Account{
		Ref: &connectorv1.AccountRef{
			ConnectorId: chainConnectorID,
			AccountId:   strings.TrimSpace(wallet.GetWalletRef()),
		},
		Kind:            connectorv1.AccountKind_CHAIN_MANAGED_WALLET,
		Asset:           chainasset.AssetString(wallet.GetAsset()),
		State:           chainWalletState(wallet.GetStatus()),
		Label:           strings.TrimSpace(wallet.GetDescribable().GetName()),
		OwnerRef:        strings.TrimSpace(wallet.GetOwnerRef()),
		ProviderDetails: details,
		CreatedAt:       wallet.GetCreatedAt(),
		UpdatedAt:       wallet.GetUpdatedAt(),
		Describable:     wallet.GetDescribable(),
	}
}

func chainWalletState(status chainv1.ManagedWalletStatus) connectorv1.AccountState {
	switch status {
	case chainv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE:
		return connectorv1.AccountState_ACCOUNT_ACTIVE
	case chainv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED:
		return connectorv1.AccountState_ACCOUNT_SUSPENDED
	case chainv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED:
		return connectorv1.AccountState_ACCOUNT_CLOSED
	default:
		return connectorv1.AccountState_ACCOUNT_STATE_UNSPECIFIED
	}
}

func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
	if op == nil {
		return nil, merrors.InvalidArgument("transfer: operation is required")
	}
	if to := op.GetTo(); to != nil {
		if account := to.GetAccount(); account != nil {
			return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(account.GetAccountId())}}, nil
		}
		if ext := to.GetExternal(); ext != nil {
			return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(ext.GetExternalRef())}}, nil
		}
	}
	return nil, merrors.InvalidArgument("transfer: to.account or to.external is required")
}

func normalizeMoneyForChain(m *moneyv1.Money) *moneyv1.Money {
	if m == nil {
		return nil
	}
	currency := strings.TrimSpace(m.GetCurrency())
	if idx := strings.Index(currency, "-"); idx > 0 {
		currency = currency[:idx]
	}
	return &moneyv1.Money{
		Amount:   strings.TrimSpace(m.GetAmount()),
		Currency: currency,
	}
}

func parseChainFees(reader params.Reader) []*chainv1.ServiceFeeBreakdown {
	rawFees := reader.List("fees")
	if len(rawFees) == 0 {
		return nil
	}
	result := make([]*chainv1.ServiceFeeBreakdown, 0, len(rawFees))
	for _, item := range rawFees {
		raw, ok := item.(map[string]interface{})
		if !ok {
			continue
		}
		amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
		currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
		if amount == "" || currency == "" {
			continue
		}
		result = append(result, &chainv1.ServiceFeeBreakdown{
			FeeCode:     strings.TrimSpace(fmt.Sprint(raw["fee_code"])),
			Description: strings.TrimSpace(fmt.Sprint(raw["description"])),
			Amount:      &moneyv1.Money{Amount: amount, Currency: currency},
		})
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
	if raw == nil {
		return nil, merrors.InvalidArgument("money is required")
	}
	amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
	currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
	if amount == "" || currency == "" {
		return nil, merrors.InvalidArgument("money is required")
	}
	return &moneyv1.Money{
		Amount:   amount,
		Currency: currency,
	}, nil
}

func feeEstimateResult(resp *chainv1.EstimateTransferFeeResponse) *structpb.Struct {
	if resp == nil {
		return nil
	}
	payload := map[string]interface{}{
		"estimation_context": strings.TrimSpace(resp.GetEstimationContext()),
	}
	if fee := resp.GetNetworkFee(); fee != nil {
		payload["network_fee"] = map[string]interface{}{
			"amount":   strings.TrimSpace(fee.GetAmount()),
			"currency": strings.TrimSpace(fee.GetCurrency()),
		}
	}
	result, err := structpb.NewStruct(payload)
	if err != nil {
		return nil
	}
	return result
}

func storageTransferToProto(transfer *chainstoragemodel.Transfer) *chainv1.Transfer {
	if transfer == nil {
		return nil
	}

	destination := &chainv1.TransferDestination{Memo: strings.TrimSpace(transfer.Destination.Memo)}
	if managedWalletRef := strings.TrimSpace(transfer.Destination.ManagedWalletRef); managedWalletRef != "" {
		destination.Destination = &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: managedWalletRef}
	} else if externalAddress := strings.TrimSpace(transfer.Destination.ExternalAddress); externalAddress != "" {
		destination.Destination = &chainv1.TransferDestination_ExternalAddress{ExternalAddress: externalAddress}
	}

	fees := make([]*chainv1.ServiceFeeBreakdown, 0, len(transfer.Fees))
	for _, fee := range transfer.Fees {
		fees = append(fees, &chainv1.ServiceFeeBreakdown{
			FeeCode:     strings.TrimSpace(fee.FeeCode),
			Amount:      fee.Amount,
			Description: strings.TrimSpace(fee.Description),
		})
	}

	asset := &chainv1.Asset{
		Chain:           shared.ChainEnumFromName(transfer.Network),
		TokenSymbol:     strings.TrimSpace(transfer.TokenSymbol),
		ContractAddress: strings.TrimSpace(transfer.ContractAddress),
	}

	protoTransfer := &chainv1.Transfer{
		TransferRef:     strings.TrimSpace(transfer.TransferRef),
		IdempotencyKey:  strings.TrimSpace(transfer.IdempotencyKey),
		IntentRef:       strings.TrimSpace(transfer.IntentRef),
		OperationRef:    strings.TrimSpace(transfer.OperationRef),
		OrganizationRef: strings.TrimSpace(transfer.OrganizationRef),
		SourceWalletRef: strings.TrimSpace(transfer.SourceWalletRef),
		Destination:     destination,
		Asset:           asset,
		RequestedAmount: shared.MonenyToProto(transfer.RequestedAmount),
		NetAmount:       shared.MonenyToProto(transfer.NetAmount),
		Fees:            fees,
		Status:          shared.TransferStatusToProto(transfer.Status),
		TransactionHash: strings.TrimSpace(transfer.TxHash),
		FailureReason:   strings.TrimSpace(transfer.FailureReason),
		PaymentRef:      strings.TrimSpace(transfer.PaymentRef),
	}

	if !transfer.CreatedAt.IsZero() {
		protoTransfer.CreatedAt = timestamppb.New(transfer.CreatedAt.UTC())
	}
	if !transfer.UpdatedAt.IsZero() {
		protoTransfer.UpdatedAt = timestamppb.New(transfer.UpdatedAt.UTC())
	}

	return protoTransfer
}

func gasTopUpResult(amount *moneyv1.Money, capHit bool, transferRef string) *structpb.Struct {
	payload := map[string]interface{}{
		"cap_hit": capHit,
	}
	if amount != nil {
		payload["topup_amount"] = map[string]interface{}{
			"amount":   strings.TrimSpace(amount.GetAmount()),
			"currency": strings.TrimSpace(amount.GetCurrency()),
		}
	}
	if strings.TrimSpace(transferRef) != "" {
		payload["transfer_ref"] = strings.TrimSpace(transferRef)
	}
	result, err := structpb.NewStruct(payload)
	if err != nil {
		return nil
	}
	return result
}

func chainTransferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation {
	if transfer == nil {
		return nil
	}
	op := &connectorv1.Operation{
		OperationId:  strings.TrimSpace(transfer.GetTransferRef()),
		Type:         connectorv1.OperationType_TRANSFER,
		Status:       chainTransferStatusToOperation(transfer.GetStatus()),
		Money:        transfer.GetRequestedAmount(),
		ProviderRef:  strings.TrimSpace(transfer.GetTransactionHash()),
		IntentRef:    strings.TrimSpace(transfer.GetIntentRef()),
		OperationRef: strings.TrimSpace(transfer.GetOperationRef()),
		CreatedAt:    transfer.GetCreatedAt(),
		UpdatedAt:    transfer.GetUpdatedAt(),
		From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
			ConnectorId: chainConnectorID,
			AccountId:   strings.TrimSpace(transfer.GetSourceWalletRef()),
		}}},
	}
	params := map[string]interface{}{}
	if paymentRef := strings.TrimSpace(transfer.GetPaymentRef()); paymentRef != "" {
		params["payment_ref"] = paymentRef
	}
	if organizationRef := strings.TrimSpace(transfer.GetOrganizationRef()); organizationRef != "" {
		params["organization_ref"] = organizationRef
	}
	if failureReason := strings.TrimSpace(transfer.GetFailureReason()); failureReason != "" {
		params["failure_reason"] = failureReason
	}
	if len(params) > 0 {
		op.Params = structFromMap(params)
	}
	if dest := transfer.GetDestination(); dest != nil {
		switch d := dest.GetDestination().(type) {
		case *chainv1.TransferDestination_ManagedWalletRef:
			op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
				ConnectorId: chainConnectorID,
				AccountId:   strings.TrimSpace(d.ManagedWalletRef),
			}}}
		case *chainv1.TransferDestination_ExternalAddress:
			op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_External{External: &connectorv1.ExternalRef{
				ExternalRef: strings.TrimSpace(d.ExternalAddress),
			}}}
		}
	}
	return op
}

func chainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
	switch status {

	case chainv1.TransferStatus_TRANSFER_CREATED:
		return connectorv1.OperationStatus_OPERATION_CREATED

	case chainv1.TransferStatus_TRANSFER_PROCESSING:
		return connectorv1.OperationStatus_OPERATION_PROCESSING

	case chainv1.TransferStatus_TRANSFER_WAITING:
		return connectorv1.OperationStatus_OPERATION_WAITING

	case chainv1.TransferStatus_TRANSFER_SUCCESS:
		return connectorv1.OperationStatus_OPERATION_SUCCESS

	case chainv1.TransferStatus_TRANSFER_FAILED:
		return connectorv1.OperationStatus_OPERATION_FAILED

	case chainv1.TransferStatus_TRANSFER_CANCELLED:
		return connectorv1.OperationStatus_OPERATION_CANCELLED

	default:
		return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
	}
}

func chainStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
	switch status {

	case connectorv1.OperationStatus_OPERATION_CREATED:
		return chainv1.TransferStatus_TRANSFER_CREATED

	case connectorv1.OperationStatus_OPERATION_PROCESSING:
		return chainv1.TransferStatus_TRANSFER_PROCESSING

	case connectorv1.OperationStatus_OPERATION_WAITING:
		return chainv1.TransferStatus_TRANSFER_WAITING

	case connectorv1.OperationStatus_OPERATION_SUCCESS:
		return chainv1.TransferStatus_TRANSFER_SUCCESS

	case connectorv1.OperationStatus_OPERATION_FAILED:
		return chainv1.TransferStatus_TRANSFER_FAILED

	case connectorv1.OperationStatus_OPERATION_CANCELLED:
		return chainv1.TransferStatus_TRANSFER_CANCELLED

	default:
		return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
	}
}

func parseChainAsset(assetString string, reader params.Reader) (*chainv1.Asset, error) {
	return chainasset.ParseAsset(
		assetString,
		reader.String("network"),
		reader.String("token_symbol"),
		reader.String("contract_address"),
	)
}

func describableFromLabel(label, desc string) *describablev1.Describable {
	label = strings.TrimSpace(label)
	desc = strings.TrimSpace(desc)
	if label == "" && desc == "" {
		return nil
	}
	return &describablev1.Describable{
		Name:        label,
		Description: &desc,
	}
}

func operationAccountID(party *connectorv1.OperationParty) string {
	if party == nil {
		return ""
	}
	if account := party.GetAccount(); account != nil {
		return strings.TrimSpace(account.GetAccountId())
	}
	return ""
}

func structFromMap(values map[string]interface{}) *structpb.Struct {
	if len(values) == 0 {
		return nil
	}
	result, err := structpb.NewStruct(values)
	if err != nil {
		return nil
	}
	return result
}

func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation) *connectorv1.ConnectorError {
	err := &connectorv1.ConnectorError{
		Code:    code,
		Message: strings.TrimSpace(message),
	}
	if op != nil {
		err.CorrelationId = strings.TrimSpace(op.GetCorrelationId())
		err.ParentIntentId = strings.TrimSpace(op.GetParentIntentId())
		err.OperationId = strings.TrimSpace(op.GetOperationId())
	}
	return err
}

func mapErrorCode(err error) connectorv1.ErrorCode {
	switch {
	case errors.Is(err, merrors.ErrInvalidArg):
		return connectorv1.ErrorCode_INVALID_PARAMS
	case errors.Is(err, merrors.ErrNoData):
		return connectorv1.ErrorCode_NOT_FOUND
	case errors.Is(err, merrors.ErrNotImplemented):
		return connectorv1.ErrorCode_UNSUPPORTED_OPERATION
	case errors.Is(err, merrors.ErrInternal):
		return connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE
	default:
		return connectorv1.ErrorCode_PROVIDER_ERROR
	}
}
