package client

import (
	"context"
	"crypto/tls"
	"fmt"
	"strings"
	"time"

	chainasset "github.com/tech/sendico/pkg/chain"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/model/account_role"
	describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
	connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/protobuf/types/known/structpb"
	"google.golang.org/protobuf/types/known/wrapperspb"
)

const chainConnectorID = "chain"

// Client exposes typed helpers around the chain gateway gRPC API.
type Client interface {
	CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
	GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error)
	ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error)
	GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error)
	SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error)
	GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error)
	ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error)
	EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error)
	ComputeGasTopUp(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) (*chainv1.ComputeGasTopUpResponse, error)
	EnsureGasTopUp(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) (*chainv1.EnsureGasTopUpResponse, error)
	Close() error
}

type grpcConnectorClient interface {
	GetCapabilities(ctx context.Context, in *connectorv1.GetCapabilitiesRequest, opts ...grpc.CallOption) (*connectorv1.GetCapabilitiesResponse, error)
	OpenAccount(ctx context.Context, in *connectorv1.OpenAccountRequest, opts ...grpc.CallOption) (*connectorv1.OpenAccountResponse, error)
	GetAccount(ctx context.Context, in *connectorv1.GetAccountRequest, opts ...grpc.CallOption) (*connectorv1.GetAccountResponse, error)
	ListAccounts(ctx context.Context, in *connectorv1.ListAccountsRequest, opts ...grpc.CallOption) (*connectorv1.ListAccountsResponse, error)
	GetBalance(ctx context.Context, in *connectorv1.GetBalanceRequest, opts ...grpc.CallOption) (*connectorv1.GetBalanceResponse, error)
	SubmitOperation(ctx context.Context, in *connectorv1.SubmitOperationRequest, opts ...grpc.CallOption) (*connectorv1.SubmitOperationResponse, error)
	GetOperation(ctx context.Context, in *connectorv1.GetOperationRequest, opts ...grpc.CallOption) (*connectorv1.GetOperationResponse, error)
	ListOperations(ctx context.Context, in *connectorv1.ListOperationsRequest, opts ...grpc.CallOption) (*connectorv1.ListOperationsResponse, error)
}

type chainGatewayClient struct {
	cfg    Config
	conn   *grpc.ClientConn
	client grpcConnectorClient
}

// New dials the chain gateway endpoint and returns a ready client.
func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) {
	cfg.setDefaults()
	if strings.TrimSpace(cfg.Address) == "" {
		return nil, merrors.InvalidArgument("chain-gateway: address is required")
	}

	dialOpts := make([]grpc.DialOption, 0, len(opts)+1)
	dialOpts = append(dialOpts, opts...)

	if cfg.Insecure {
		dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
	} else {
		dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
	}

	conn, err := grpc.NewClient(cfg.Address, dialOpts...)
	if err != nil {
		return nil, merrors.Internal(fmt.Sprintf("chain-gateway: dial %s: %s", cfg.Address, err.Error()))
	}

	return &chainGatewayClient{
		cfg:    cfg,
		conn:   conn,
		client: connectorv1.NewConnectorServiceClient(conn),
	}, nil
}

// NewWithClient injects a pre-built gateway client (useful for tests).
func NewWithClient(cfg Config, gc grpcConnectorClient) Client {
	cfg.setDefaults()
	return &chainGatewayClient{
		cfg:    cfg,
		client: gc,
	}
}

func (c *chainGatewayClient) Close() error {
	if c.conn != nil {
		return c.conn.Close()
	}
	return nil
}

func (c *chainGatewayClient) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()

	params, err := walletParamsFromRequest(req)
	if err != nil {
		return nil, err
	}

	label := ""
	if desc := req.GetDescribable(); desc != nil {
		label = strings.TrimSpace(desc.GetName())
	}
	resp, err := c.client.OpenAccount(ctx, &connectorv1.OpenAccountRequest{
		IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
		Kind:           connectorv1.AccountKind_CHAIN_MANAGED_WALLET,
		Asset:          chainasset.AssetString(req.GetAsset()),
		OwnerRef:       strings.TrimSpace(req.GetOwnerRef()),
		Label:          label,
		Params:         params,
	})
	if err != nil {
		return nil, err
	}
	if resp.GetError() != nil {
		return nil, connectorError(resp.GetError())
	}
	wallet := managedWalletFromAccount(resp.GetAccount())
	return &chainv1.CreateManagedWalletResponse{Wallet: wallet}, nil
}

func (c *chainGatewayClient) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	if req == nil || strings.TrimSpace(req.GetWalletRef()) == "" {
		return nil, merrors.InvalidArgument("chain-gateway: wallet_ref is required")
	}
	resp, err := c.client.GetAccount(ctx, &connectorv1.GetAccountRequest{AccountRef: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetWalletRef())}})
	if err != nil {
		return nil, err
	}
	return &chainv1.GetManagedWalletResponse{Wallet: managedWalletFromAccount(resp.GetAccount())}, nil
}

func (c *chainGatewayClient) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	assetString := ""
	orgRef := ""
	var page *paginationv1.CursorPageRequest
	var ownerRefFilter *wrapperspb.StringValue
	if req != nil {
		assetString = chainasset.AssetString(req.GetAsset())
		orgRef = strings.TrimSpace(req.GetOrganizationRef())
		ownerRefFilter = req.GetOwnerRefFilter()
		page = req.GetPage()
	}
	resp, err := c.client.ListAccounts(ctx, &connectorv1.ListAccountsRequest{
		OwnerRefFilter:  ownerRefFilter,
		OrganizationRef: orgRef,
		Kind:            connectorv1.AccountKind_CHAIN_MANAGED_WALLET,
		Asset:           assetString,
		Page:            page,
	})
	if err != nil {
		return nil, err
	}
	wallets := make([]*chainv1.ManagedWallet, 0, len(resp.GetAccounts()))
	for _, account := range resp.GetAccounts() {
		wallets = append(wallets, managedWalletFromAccount(account))
	}
	return &chainv1.ListManagedWalletsResponse{Wallets: wallets, Page: resp.GetPage()}, nil
}

func (c *chainGatewayClient) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	if req == nil || strings.TrimSpace(req.GetWalletRef()) == "" {
		return nil, merrors.InvalidArgument("chain-gateway: wallet_ref is required")
	}
	resp, err := c.client.GetBalance(ctx, &connectorv1.GetBalanceRequest{AccountRef: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetWalletRef())}})
	if err != nil {
		return nil, err
	}
	balance := resp.GetBalance()
	if balance == nil {
		return nil, merrors.Internal("chain-gateway: balance response missing")
	}
	return &chainv1.GetWalletBalanceResponse{Balance: &chainv1.WalletBalance{
		Available:       balance.GetAvailable(),
		PendingInbound:  balance.GetPendingInbound(),
		PendingOutbound: balance.GetPendingOutbound(),
		CalculatedAt:    balance.GetCalculatedAt(),
	}}, nil
}

func (c *chainGatewayClient) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	if req == nil {
		return nil, merrors.InvalidArgument("chain-gateway: request is required")
	}
	operation, err := operationFromTransfer(req)
	if err != nil {
		return nil, err
	}
	resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
	if err != nil {
		return nil, err
	}
	if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
		return nil, connectorError(resp.GetReceipt().GetError())
	}
	transfer := transferFromReceipt(req, resp.GetReceipt())
	return &chainv1.SubmitTransferResponse{Transfer: transfer}, nil
}

func (c *chainGatewayClient) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	if req == nil || strings.TrimSpace(req.GetTransferRef()) == "" {
		return nil, merrors.InvalidArgument("chain-gateway: transfer_ref is required")
	}
	resp, err := c.client.GetOperation(ctx, &connectorv1.GetOperationRequest{OperationId: strings.TrimSpace(req.GetTransferRef())})
	if err != nil {
		return nil, err
	}
	return &chainv1.GetTransferResponse{Transfer: transferFromOperation(resp.GetOperation())}, nil
}

func (c *chainGatewayClient) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	source := ""
	status := chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
	var page *paginationv1.CursorPageRequest
	if req != nil {
		source = strings.TrimSpace(req.GetSourceWalletRef())
		status = req.GetStatus()
		page = req.GetPage()
	}
	resp, err := c.client.ListOperations(ctx, &connectorv1.ListOperationsRequest{
		AccountRef: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: source},
		Status:     operationStatusFromTransfer(status),
		Page:       page,
	})
	if err != nil {
		return nil, err
	}
	transfers := make([]*chainv1.Transfer, 0, len(resp.GetOperations()))
	for _, op := range resp.GetOperations() {
		transfers = append(transfers, transferFromOperation(op))
	}
	return &chainv1.ListTransfersResponse{Transfers: transfers, Page: resp.GetPage()}, nil
}

func (c *chainGatewayClient) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	if req == nil {
		return nil, merrors.InvalidArgument("chain-gateway: request is required")
	}
	operation, err := feeEstimateOperation(req)
	if err != nil {
		return nil, err
	}
	resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
	if err != nil {
		return nil, err
	}
	if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
		return nil, connectorError(resp.GetReceipt().GetError())
	}
	return estimateFromReceipt(resp.GetReceipt()), nil
}

func (c *chainGatewayClient) ComputeGasTopUp(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) (*chainv1.ComputeGasTopUpResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	if req == nil || strings.TrimSpace(req.GetWalletRef()) == "" {
		return nil, merrors.InvalidArgument("chain-gateway: wallet_ref is required")
	}
	operation, err := gasTopUpComputeOperation(req)
	if err != nil {
		return nil, err
	}
	resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
	if err != nil {
		return nil, err
	}
	if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
		return nil, connectorError(resp.GetReceipt().GetError())
	}
	return computeGasTopUpFromReceipt(resp.GetReceipt()), nil
}

func (c *chainGatewayClient) EnsureGasTopUp(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) (*chainv1.EnsureGasTopUpResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	if req == nil {
		return nil, merrors.InvalidArgument("chain-gateway: request is required")
	}
	operation, err := gasTopUpEnsureOperation(req)
	if err != nil {
		return nil, err
	}
	resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
	if err != nil {
		return nil, err
	}
	if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
		return nil, connectorError(resp.GetReceipt().GetError())
	}
	return ensureGasTopUpFromReceipt(resp.GetReceipt()), nil
}

func (c *chainGatewayClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
	timeout := c.cfg.CallTimeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}
	//nolint:gosec // Caller receives cancel func and defers it in every call path.
	return context.WithTimeout(ctx, timeout)
}

func walletParamsFromRequest(req *chainv1.CreateManagedWalletRequest) (*structpb.Struct, error) {
	if req == nil {
		//nolint:nilnil // Nil request means optional params are absent.
		return nil, nil
	}
	params := map[string]interface{}{
		"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
	}
	if asset := req.GetAsset(); asset != nil {
		params["network"] = chainasset.NetworkName(asset.GetChain())
		params["token_symbol"] = strings.TrimSpace(asset.GetTokenSymbol())
		params["contract_address"] = strings.TrimSpace(asset.GetContractAddress())
	}
	desc := ""
	if describable := req.GetDescribable(); describable != nil {
		desc = strings.TrimSpace(describable.GetDescription())
	}
	if desc != "" {
		params["description"] = desc
	}
	if len(req.GetMetadata()) > 0 {
		params["metadata"] = mapStringToInterface(req.GetMetadata())
	}
	return structpb.NewStruct(params)
}

func managedWalletFromAccount(account *connectorv1.Account) *chainv1.ManagedWallet {
	if account == nil {
		return nil
	}
	details := map[string]interface{}{}
	if account.GetProviderDetails() != nil {
		details = account.GetProviderDetails().AsMap()
	}
	walletRef := ""
	if ref := account.GetRef(); ref != nil {
		walletRef = strings.TrimSpace(ref.GetAccountId())
	}
	if v := stringFromDetails(details, "wallet_ref"); v != "" {
		walletRef = v
	}
	organizationRef := stringFromDetails(details, "organization_ref")
	ownerRef := stringFromDetails(details, "owner_ref")
	if ownerRef == "" {
		ownerRef = strings.TrimSpace(account.GetOwnerRef())
	}
	asset := &chainv1.Asset{
		Chain:           chainasset.NetworkFromString(stringFromDetails(details, "network")),
		TokenSymbol:     strings.TrimSpace(stringFromDetails(details, "token_symbol")),
		ContractAddress: strings.TrimSpace(stringFromDetails(details, "contract_address")),
	}
	if asset.GetTokenSymbol() == "" {
		asset.TokenSymbol = strings.TrimSpace(chainasset.TokenFromAssetString(account.GetAsset()))
	}
	describable := account.GetDescribable()
	label := strings.TrimSpace(account.GetLabel())
	if describable == nil {
		if label != "" {
			describable = &describablev1.Describable{Name: label}
		}
	} else if strings.TrimSpace(describable.GetName()) == "" && label != "" {
		desc := strings.TrimSpace(describable.GetDescription())
		if desc == "" {
			describable = &describablev1.Describable{Name: label}
		} else {
			describable = &describablev1.Describable{Name: label, Description: &desc}
		}
	}
	return &chainv1.ManagedWallet{
		WalletRef:       walletRef,
		OrganizationRef: organizationRef,
		OwnerRef:        ownerRef,
		Asset:           asset,
		DepositAddress:  stringFromDetails(details, "deposit_address"),
		Status:          managedWalletStatusFromAccount(account.GetState()),
		CreatedAt:       account.GetCreatedAt(),
		UpdatedAt:       account.GetUpdatedAt(),
		Describable:     describable,
	}
}

func operationFromTransfer(req *chainv1.SubmitTransferRequest) (*connectorv1.Operation, error) {
	if req == nil {
		return nil, merrors.InvalidArgument("chain-gateway: request is required")
	}
	if strings.TrimSpace(req.GetIdempotencyKey()) == "" {
		return nil, merrors.InvalidArgument("chain-gateway: idempotency_key is required")
	}
	if strings.TrimSpace(req.GetSourceWalletRef()) == "" {
		return nil, merrors.InvalidArgument("chain-gateway: source_wallet_ref is required")
	}
	if req.GetDestination() == nil {
		return nil, merrors.InvalidArgument("chain-gateway: destination is required")
	}
	if req.GetAmount() == nil {
		return nil, merrors.InvalidArgument("chain-gateway: amount is required")
	}

	params := map[string]interface{}{
		"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
		"payment_ref":      strings.TrimSpace(req.GetPaymentRef()),
	}
	if memo := strings.TrimSpace(req.GetDestination().GetMemo()); memo != "" {
		params["destination_memo"] = memo
	}
	if len(req.GetMetadata()) > 0 {
		params["metadata"] = mapStringToInterface(req.GetMetadata())
	}
	if len(req.GetFees()) > 0 {
		params["fees"] = feesToInterface(req.GetFees())
	}

	op := &connectorv1.Operation{
		Type:           connectorv1.OperationType_TRANSFER,
		IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
		From:           &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetSourceWalletRef())}}},
		Money:          req.GetAmount(),
		Params:         structFromMap(params),
		IntentRef:      strings.TrimSpace(req.GetIntentRef()),
		OperationRef:   strings.TrimSpace(req.GetOperationRef()),
	}
	to, err := destinationToParty(req.GetDestination())
	if err != nil {
		return nil, err
	}
	op.To = to
	setOperationRolesFromMetadata(op, req.GetMetadata())
	return op, nil
}

func destinationToParty(dest *chainv1.TransferDestination) (*connectorv1.OperationParty, error) {
	if dest == nil {
		return nil, merrors.InvalidArgument("chain-gateway: destination is required")
	}
	switch d := dest.GetDestination().(type) {
	case *chainv1.TransferDestination_ManagedWalletRef:
		return &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(d.ManagedWalletRef)}}}, nil
	case *chainv1.TransferDestination_ExternalAddress:
		return &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_External{External: &connectorv1.ExternalRef{ExternalRef: strings.TrimSpace(d.ExternalAddress)}}}, nil
	default:
		return nil, merrors.InvalidArgument("chain-gateway: destination is required")
	}
}

func setOperationRolesFromMetadata(op *connectorv1.Operation, metadata map[string]string) {
	if op == nil || len(metadata) == 0 {
		return
	}
	if raw := strings.TrimSpace(metadata[account_role.MetadataKeyFromRole]); raw != "" {
		if role, ok := account_role.Parse(raw); ok && role != "" {
			op.FromRole = account_role.ToProto(role)
		}
	}
	if raw := strings.TrimSpace(metadata[account_role.MetadataKeyToRole]); raw != "" {
		if role, ok := account_role.Parse(raw); ok && role != "" {
			op.ToRole = account_role.ToProto(role)
		}
	}
}

func transferFromReceipt(req *chainv1.SubmitTransferRequest, receipt *connectorv1.OperationReceipt) *chainv1.Transfer {
	transfer := &chainv1.Transfer{}
	if req != nil {
		transfer.IdempotencyKey = strings.TrimSpace(req.GetIdempotencyKey())
		transfer.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
		transfer.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
		transfer.Destination = req.GetDestination()
		transfer.RequestedAmount = req.GetAmount()
		transfer.NetAmount = req.GetAmount()
		transfer.IntentRef = strings.TrimSpace(req.GetIntentRef())
		transfer.OperationRef = strings.TrimSpace(req.GetOperationRef())
		transfer.PaymentRef = strings.TrimSpace(req.GetPaymentRef())
	}
	if receipt != nil {
		transfer.TransferRef = strings.TrimSpace(receipt.GetOperationId())
		transfer.Status = transferStatusFromOperation(receipt.GetStatus())
		transfer.TransactionHash = strings.TrimSpace(receipt.GetProviderRef())
	}
	return transfer
}

func transferFromOperation(op *connectorv1.Operation) *chainv1.Transfer {
	if op == nil {
		return nil
	}
	transfer := &chainv1.Transfer{
		TransferRef:     strings.TrimSpace(op.GetOperationId()),
		IdempotencyKey:  strings.TrimSpace(op.GetOperationId()),
		RequestedAmount: op.GetMoney(),
		NetAmount:       op.GetMoney(),
		Status:          transferStatusFromOperation(op.GetStatus()),
		TransactionHash: strings.TrimSpace(op.GetProviderRef()),
		CreatedAt:       op.GetCreatedAt(),
		UpdatedAt:       op.GetUpdatedAt(),
	}
	if from := op.GetFrom(); from != nil && from.GetAccount() != nil {
		transfer.SourceWalletRef = strings.TrimSpace(from.GetAccount().GetAccountId())
	}
	if to := op.GetTo(); to != nil {
		if account := to.GetAccount(); account != nil {
			transfer.Destination = &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(account.GetAccountId())}}
		}
		if external := to.GetExternal(); external != nil {
			transfer.Destination = &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(external.GetExternalRef())}}
		}
	}
	return transfer
}

func feeEstimateOperation(req *chainv1.EstimateTransferFeeRequest) (*connectorv1.Operation, error) {
	if req == nil {
		return nil, merrors.InvalidArgument("chain-gateway: request is required")
	}
	if strings.TrimSpace(req.GetSourceWalletRef()) == "" {
		return nil, merrors.InvalidArgument("chain-gateway: source_wallet_ref is required")
	}
	if req.GetDestination() == nil {
		return nil, merrors.InvalidArgument("chain-gateway: destination is required")
	}
	if req.GetAmount() == nil {
		return nil, merrors.InvalidArgument("chain-gateway: amount is required")
	}
	params := map[string]interface{}{}
	op := &connectorv1.Operation{
		Type:           connectorv1.OperationType_FEE_ESTIMATE,
		IdempotencyKey: feeEstimateKey(req),
		From:           &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetSourceWalletRef())}}},
		Money:          req.GetAmount(),
		Params:         structFromMap(params),
	}
	to, err := destinationToParty(req.GetDestination())
	if err != nil {
		return nil, err
	}
	op.To = to
	return op, nil
}

func estimateFromReceipt(receipt *connectorv1.OperationReceipt) *chainv1.EstimateTransferFeeResponse {
	resp := &chainv1.EstimateTransferFeeResponse{}
	if receipt == nil || receipt.GetResult() == nil {
		return resp
	}
	data := receipt.GetResult().AsMap()
	if networkFee, ok := data["network_fee"].(map[string]interface{}); ok {
		amount := strings.TrimSpace(fmt.Sprint(networkFee["amount"]))
		currency := strings.TrimSpace(fmt.Sprint(networkFee["currency"]))
		if amount != "" && currency != "" {
			resp.NetworkFee = &moneyv1.Money{Amount: amount, Currency: currency}
		}
	}
	if ctx, ok := data["estimation_context"].(string); ok {
		resp.EstimationContext = strings.TrimSpace(ctx)
	}
	return resp
}

func gasTopUpComputeOperation(req *chainv1.ComputeGasTopUpRequest) (*connectorv1.Operation, error) {
	if req == nil || strings.TrimSpace(req.GetWalletRef()) == "" {
		return nil, merrors.InvalidArgument("chain-gateway: wallet_ref is required")
	}
	fee := req.GetEstimatedTotalFee()
	if fee == nil {
		return nil, merrors.InvalidArgument("chain-gateway: estimated_total_fee is required")
	}
	params := map[string]interface{}{
		"mode":                "compute",
		"estimated_total_fee": map[string]interface{}{"amount": fee.GetAmount(), "currency": fee.GetCurrency()},
	}
	return &connectorv1.Operation{
		Type:           connectorv1.OperationType_GAS_TOPUP,
		IdempotencyKey: fmt.Sprintf("gas_topup_compute:%s:%s", strings.TrimSpace(req.GetWalletRef()), strings.TrimSpace(fee.GetAmount())),
		From:           &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetWalletRef())}}},
		Params:         structFromMap(params),
	}, nil
}

func gasTopUpEnsureOperation(req *chainv1.EnsureGasTopUpRequest) (*connectorv1.Operation, error) {
	if req == nil {
		return nil, merrors.InvalidArgument("chain-gateway: request is required")
	}
	if strings.TrimSpace(req.GetIdempotencyKey()) == "" {
		return nil, merrors.InvalidArgument("chain-gateway: idempotency_key is required")
	}
	if strings.TrimSpace(req.GetSourceWalletRef()) == "" {
		return nil, merrors.InvalidArgument("chain-gateway: source_wallet_ref is required")
	}
	if strings.TrimSpace(req.GetTargetWalletRef()) == "" {
		return nil, merrors.InvalidArgument("chain-gateway: target_wallet_ref is required")
	}
	fee := req.GetEstimatedTotalFee()
	if fee == nil {
		return nil, merrors.InvalidArgument("chain-gateway: estimated_total_fee is required")
	}
	params := map[string]interface{}{
		"mode":                "ensure",
		"organization_ref":    strings.TrimSpace(req.GetOrganizationRef()),
		"target_wallet_ref":   strings.TrimSpace(req.GetTargetWalletRef()),
		"payment_ref":         strings.TrimSpace(req.GetPaymentRef()),
		"estimated_total_fee": map[string]interface{}{"amount": fee.GetAmount(), "currency": fee.GetCurrency()},
	}
	if len(req.GetMetadata()) > 0 {
		params["metadata"] = mapStringToInterface(req.GetMetadata())
	}
	return &connectorv1.Operation{
		Type:           connectorv1.OperationType_GAS_TOPUP,
		IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
		From:           &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetSourceWalletRef())}}},
		Params:         structFromMap(params),
	}, nil
}

func computeGasTopUpFromReceipt(receipt *connectorv1.OperationReceipt) *chainv1.ComputeGasTopUpResponse {
	resp := &chainv1.ComputeGasTopUpResponse{}
	if receipt == nil || receipt.GetResult() == nil {
		return resp
	}
	data := receipt.GetResult().AsMap()
	if amount, ok := data["topup_amount"].(map[string]interface{}); ok {
		resp.TopupAmount = &moneyv1.Money{
			Amount:   strings.TrimSpace(fmt.Sprint(amount["amount"])),
			Currency: strings.TrimSpace(fmt.Sprint(amount["currency"])),
		}
	}
	if capHit, ok := data["cap_hit"].(bool); ok {
		resp.CapHit = capHit
	}
	return resp
}

func ensureGasTopUpFromReceipt(receipt *connectorv1.OperationReceipt) *chainv1.EnsureGasTopUpResponse {
	resp := &chainv1.EnsureGasTopUpResponse{}
	if receipt == nil || receipt.GetResult() == nil {
		return resp
	}
	data := receipt.GetResult().AsMap()
	if amount, ok := data["topup_amount"].(map[string]interface{}); ok {
		resp.TopupAmount = &moneyv1.Money{
			Amount:   strings.TrimSpace(fmt.Sprint(amount["amount"])),
			Currency: strings.TrimSpace(fmt.Sprint(amount["currency"])),
		}
	}
	if capHit, ok := data["cap_hit"].(bool); ok {
		resp.CapHit = capHit
	}
	if transferRef, ok := data["transfer_ref"].(string); ok {
		resp.Transfer = &chainv1.Transfer{TransferRef: strings.TrimSpace(transferRef)}
	}
	return resp
}

func feeEstimateKey(req *chainv1.EstimateTransferFeeRequest) string {
	if req == nil || req.GetAmount() == nil {
		return "fee_estimate"
	}
	return fmt.Sprintf("fee_estimate:%s:%s:%s", strings.TrimSpace(req.GetSourceWalletRef()), strings.TrimSpace(req.GetAmount().GetCurrency()), strings.TrimSpace(req.GetAmount().GetAmount()))
}

func connectorError(err *connectorv1.ConnectorError) error {
	if err == nil {
		return nil
	}
	msg := strings.TrimSpace(err.GetMessage())
	switch err.GetCode() {
	case connectorv1.ErrorCode_INVALID_PARAMS:
		return merrors.InvalidArgument(msg)
	case connectorv1.ErrorCode_NOT_FOUND:
		return merrors.NoData(msg)
	case connectorv1.ErrorCode_UNSUPPORTED_OPERATION, connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND:
		return merrors.NotImplemented(msg)
	case connectorv1.ErrorCode_RATE_LIMITED, connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE:
		return merrors.Internal(msg)
	default:
		return merrors.Internal(msg)
	}
}

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

func mapStringToInterface(input map[string]string) map[string]interface{} {
	if len(input) == 0 {
		return nil
	}
	out := make(map[string]interface{}, len(input))
	for k, v := range input {
		out[k] = v
	}
	return out
}

func feesToInterface(fees []*chainv1.ServiceFeeBreakdown) []interface{} {
	if len(fees) == 0 {
		return nil
	}
	result := make([]interface{}, 0, len(fees))
	for _, fee := range fees {
		if fee == nil || fee.GetAmount() == nil {
			continue
		}
		result = append(result, map[string]interface{}{
			"fee_code":    strings.TrimSpace(fee.GetFeeCode()),
			"description": strings.TrimSpace(fee.GetDescription()),
			"amount":      strings.TrimSpace(fee.GetAmount().GetAmount()),
			"currency":    strings.TrimSpace(fee.GetAmount().GetCurrency()),
		})
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func stringFromDetails(details map[string]interface{}, key string) string {
	if details == nil {
		return ""
	}
	if value, ok := details[key]; ok {
		return strings.TrimSpace(fmt.Sprint(value))
	}
	return ""
}

func managedWalletStatusFromAccount(state connectorv1.AccountState) chainv1.ManagedWalletStatus {
	switch state {
	case connectorv1.AccountState_ACCOUNT_ACTIVE:
		return chainv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE
	case connectorv1.AccountState_ACCOUNT_SUSPENDED:
		return chainv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED
	case connectorv1.AccountState_ACCOUNT_CLOSED:
		return chainv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED
	default:
		return chainv1.ManagedWalletStatus_MANAGED_WALLET_STATUS_UNSPECIFIED
	}
}

func operationStatusFromTransfer(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 transferStatusFromOperation(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
	}
}
