// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Package client provides Talos API client.
package client

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"context"
	"errors"
	"fmt"
	"io"
	"time"

	cosiv1alpha1 "github.com/cosi-project/runtime/api/v1alpha1"
	"github.com/cosi-project/runtime/pkg/state"
	"github.com/cosi-project/runtime/pkg/state/protobuf/client"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/durationpb"
	"google.golang.org/protobuf/types/known/emptypb"

	clusterapi "github.com/siderolabs/talos/pkg/machinery/api/cluster"
	"github.com/siderolabs/talos/pkg/machinery/api/common"
	inspectapi "github.com/siderolabs/talos/pkg/machinery/api/inspect"
	machineapi "github.com/siderolabs/talos/pkg/machinery/api/machine"
	storageapi "github.com/siderolabs/talos/pkg/machinery/api/storage"
	timeapi "github.com/siderolabs/talos/pkg/machinery/api/time"
	clientconfig "github.com/siderolabs/talos/pkg/machinery/client/config"
)

// Client implements the proto.MachineServiceClient interface. It serves as the
// concrete type with the required methods.
type Client struct {
	options *Options
	conn    *grpcConnectionWrapper

	MachineClient   machineapi.MachineServiceClient
	TimeClient      timeapi.TimeServiceClient
	ClusterClient   clusterapi.ClusterServiceClient
	StorageClient   storageapi.StorageServiceClient
	InspectClient   inspectapi.InspectServiceClient
	ImageClient     machineapi.ImageServiceClient
	DebugClient     machineapi.DebugServiceClient
	LifecycleClient machineapi.LifecycleServiceClient

	COSI state.State

	Inspect *InspectClient
}

func (c *Client) resolveConfigContext() error {
	var ok bool

	if c.options.unixSocketPath != "" {
		return nil
	}

	if c.options.configContext != nil {
		return nil
	}

	if c.options.config == nil {
		if err := WithDefaultConfig()(c.options); err != nil {
			return fmt.Errorf("failed to load default config: %w", err)
		}
	}

	if c.options.contextOverrideSet {
		c.options.configContext, ok = c.options.config.Contexts[c.options.contextOverride]
		if !ok {
			return fmt.Errorf("context %q not found in config", c.options.contextOverride)
		}

		return nil
	}

	c.options.configContext, ok = c.options.config.Contexts[c.options.config.Context]
	if !ok {
		if c.options.config.Context == "" && len(c.options.config.Contexts) == 0 {
			return errors.New("talos config file is empty")
		}

		return fmt.Errorf("default context %q not found in config", c.options.config.Context)
	}

	return nil
}

// GetConfigContext returns resolved config context.
func (c *Client) GetConfigContext() *clientconfig.Context {
	if err := c.resolveConfigContext(); err != nil {
		return nil
	}

	return c.options.configContext
}

// GetEndpoints returns the client's endpoints from the override set with WithEndpoints
// or from the configuration.
func (c *Client) GetEndpoints() []string {
	if c.options.unixSocketPath != "" {
		return []string{c.options.unixSocketPath}
	}

	if len(c.options.endpointsOverride) > 0 {
		return c.options.endpointsOverride
	}

	if c.options.configContext != nil {
		return c.options.configContext.Endpoints
	}

	if c.options.config != nil {
		if err := c.resolveConfigContext(); err != nil {
			return nil
		}

		return c.options.configContext.Endpoints
	}

	return nil
}

// GetClusterName returns the client's cluster name from the override set with WithClustername
// or from the configuration.
func (c *Client) GetClusterName() string {
	if c.options.clusterNameOverride != "" {
		return c.options.clusterNameOverride
	}

	if c.options.config != nil {
		if err := c.resolveConfigContext(); err != nil {
			return ""
		}

		if c.options.configContext != nil {
			return c.options.configContext.Cluster
		}
	}

	return ""
}

// New returns a new Client.
func New(_ context.Context, opts ...OptionFunc) (c *Client, err error) {
	c = new(Client)

	c.options = new(Options)

	for _, opt := range opts {
		if err = opt(c.options); err != nil {
			return nil, err
		}
	}

	if len(c.GetEndpoints()) < 1 {
		return nil, errors.New("failed to determine endpoints")
	}

	c.conn, err = c.getConn()
	if err != nil {
		return nil, fmt.Errorf("failed to create client connection: %w", err)
	}

	c.MachineClient = machineapi.NewMachineServiceClient(c.conn)
	c.TimeClient = timeapi.NewTimeServiceClient(c.conn)
	c.ClusterClient = clusterapi.NewClusterServiceClient(c.conn)
	c.StorageClient = storageapi.NewStorageServiceClient(c.conn)
	c.InspectClient = inspectapi.NewInspectServiceClient(c.conn)
	c.ImageClient = machineapi.NewImageServiceClient(c.conn)
	c.DebugClient = machineapi.NewDebugServiceClient(c.conn)
	c.LifecycleClient = machineapi.NewLifecycleServiceClient(c.conn)

	c.Inspect = &InspectClient{c.InspectClient}
	c.COSI = state.WrapCore(client.NewAdapter(cosiv1alpha1.NewStateClient(c.conn)))

	return c, nil
}

// Close shuts down client protocol.
func (c *Client) Close() error {
	return c.conn.Close()
}

// KubeconfigRaw returns K8s client config (kubeconfig).
//
// This method doesn't support multiplexing of the result:
// * either client.WithNodes is not used, or it contains a single node in the list.
func (c *Client) KubeconfigRaw(ctx context.Context) (io.ReadCloser, error) {
	stream, err := c.MachineClient.Kubeconfig(ctx, &emptypb.Empty{})
	if err != nil {
		return nil, err
	}

	return ReadStream(stream)
}

func (c *Client) extractKubeconfig(r io.ReadCloser) ([]byte, error) {
	defer r.Close() //nolint:errcheck

	gzR, err := gzip.NewReader(r)
	if err != nil {
		return nil, err
	}

	// returned .tar.gz should contain only single file (kubeconfig)
	var kubeconfigBuf bytes.Buffer

	tar := tar.NewReader(gzR)

	for {
		_, err = tar.Next()
		if err == io.EOF {
			break
		}

		if err != nil {
			return nil, err
		}

		_, err = io.Copy(&kubeconfigBuf, tar)
		if err != nil {
			return nil, err
		}
	}

	if err = gzR.Close(); err != nil {
		return nil, err
	}

	return kubeconfigBuf.Bytes(), nil
}

// Kubeconfig returns K8s client config (kubeconfig).
func (c *Client) Kubeconfig(ctx context.Context) ([]byte, error) {
	r, err := c.KubeconfigRaw(ctx)
	if err != nil {
		return nil, err
	}

	return c.extractKubeconfig(r)
}

// ApplyConfiguration implements proto.MachineServiceClient interface.
func (c *Client) ApplyConfiguration(ctx context.Context, req *machineapi.ApplyConfigurationRequest, callOptions ...grpc.CallOption) (resp *machineapi.ApplyConfigurationResponse, err error) {
	resp, err = c.MachineClient.ApplyConfiguration(ctx, req, callOptions...)

	return FilterMessages(resp, err)
}

// Disks returns the list of block devices.
func (c *Client) Disks(ctx context.Context, callOptions ...grpc.CallOption) (resp *storageapi.DisksResponse, err error) {
	resp, err = c.StorageClient.Disks(ctx, &emptypb.Empty{}, callOptions...)

	return FilterMessages(resp, err)
}

// Stats implements the proto.MachineServiceClient interface.
func (c *Client) Stats(ctx context.Context, namespace string, driver common.ContainerDriver, callOptions ...grpc.CallOption) (resp *machineapi.StatsResponse, err error) {
	resp, err = c.MachineClient.Stats(
		ctx, &machineapi.StatsRequest{
			Namespace: namespace,
			Driver:    driver,
		},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// Containers implements the proto.MachineServiceClient interface.
func (c *Client) Containers(ctx context.Context, namespace string, driver common.ContainerDriver, callOptions ...grpc.CallOption) (resp *machineapi.ContainersResponse, err error) {
	resp, err = c.MachineClient.Containers(
		ctx,
		&machineapi.ContainersRequest{
			Namespace: namespace,
			Driver:    driver,
		},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// Restart implements the proto.MachineServiceClient interface.
func (c *Client) Restart(ctx context.Context, namespace string, driver common.ContainerDriver, id string, callOptions ...grpc.CallOption) (err error) {
	resp, err := c.MachineClient.Restart(ctx, &machineapi.RestartRequest{
		Id:        id,
		Namespace: namespace,
		Driver:    driver,
	})
	if err == nil {
		_, err = FilterMessages(resp, err)
	}

	return err
}

// Reset implements the proto.MachineServiceClient interface.
func (c *Client) Reset(ctx context.Context, graceful, reboot bool) (err error) {
	resp, err := c.MachineClient.Reset(ctx, &machineapi.ResetRequest{Graceful: graceful, Reboot: reboot})
	if err == nil {
		_, err = FilterMessages(resp, err)
	}

	return err
}

// ResetGeneric implements the proto.MachineServiceClient interface.
func (c *Client) ResetGeneric(ctx context.Context, req *machineapi.ResetRequest) error {
	_, err := c.ResetGenericWithResponse(ctx, req)

	return err
}

// ResetGenericWithResponse resets the machine and returns the response.
func (c *Client) ResetGenericWithResponse(ctx context.Context, req *machineapi.ResetRequest) (*machineapi.ResetResponse, error) {
	resp, err := c.MachineClient.Reset(ctx, req)
	if err == nil {
		_, err = FilterMessages(resp, err)
	}

	return resp, err
}

// RebootMode provides various mode through which the reboot process can be done.
type RebootMode func(*machineapi.RebootRequest)

// WithRebootMode sets the reboot mode for the Reboot API call.
func WithRebootMode(mode machineapi.RebootRequest_Mode) func(req *machineapi.RebootRequest) {
	return func(req *machineapi.RebootRequest) {
		req.Mode = mode
	}
}

// WithPowerCycle option runs the Reboot fun in powercycle mode.
func WithPowerCycle(req *machineapi.RebootRequest) {
	req.Mode = machineapi.RebootRequest_POWERCYCLE
}

// WithForce option runs the Reboot fun in force mode.
func WithForce(req *machineapi.RebootRequest) {
	req.Mode = machineapi.RebootRequest_FORCE
}

// Reboot implements the proto.MachineServiceClient interface.
func (c *Client) Reboot(ctx context.Context, opts ...RebootMode) error {
	_, err := c.RebootWithResponse(ctx, opts...)

	return err
}

// RebootWithResponse reboots the machine and returns the response.
func (c *Client) RebootWithResponse(ctx context.Context, opts ...RebootMode) (*machineapi.RebootResponse, error) {
	var req machineapi.RebootRequest
	for _, opt := range opts {
		opt(&req)
	}

	resp, err := c.MachineClient.Reboot(ctx, &req)
	if err == nil {
		_, err = FilterMessages(resp, err)
	}

	return resp, err
}

// Rollback implements the proto.MachineServiceClient interface.
func (c *Client) Rollback(ctx context.Context) (err error) {
	resp, err := c.MachineClient.Rollback(ctx, &machineapi.RollbackRequest{})
	if err == nil {
		_, err = FilterMessages(resp, err)
	}

	return err
}

// Bootstrap implements the proto.MachineServiceClient interface.
func (c *Client) Bootstrap(ctx context.Context, req *machineapi.BootstrapRequest) (err error) {
	resp, err := c.MachineClient.Bootstrap(ctx, req)
	if err == nil {
		_, err = FilterMessages(resp, err)
	}

	return err
}

// ShutdownOption provides shutdown API options.
type ShutdownOption func(*machineapi.ShutdownRequest)

// WithShutdownForce forces the shutdown even if the Kubernetes API is down.
func WithShutdownForce(force bool) ShutdownOption {
	return func(req *machineapi.ShutdownRequest) {
		req.Force = force
	}
}

// Shutdown implements the proto.MachineServiceClient interface.
func (c *Client) Shutdown(ctx context.Context, opts ...ShutdownOption) error {
	_, err := c.ShutdownWithResponse(ctx, opts...)

	return err
}

// ShutdownWithResponse shuts down the machine and returns the response.
func (c *Client) ShutdownWithResponse(ctx context.Context, opts ...ShutdownOption) (*machineapi.ShutdownResponse, error) {
	var req machineapi.ShutdownRequest

	for _, opt := range opts {
		opt(&req)
	}

	resp, err := c.MachineClient.Shutdown(ctx, &req)
	if err == nil {
		_, err = FilterMessages(resp, err)
	}

	return resp, err
}

// Dmesg implements the proto.MachineServiceClient interface.
func (c *Client) Dmesg(ctx context.Context, follow, tail bool) (machineapi.MachineService_DmesgClient, error) {
	return c.MachineClient.Dmesg(ctx, &machineapi.DmesgRequest{
		Follow: follow,
		Tail:   tail,
	})
}

// Logs implements the proto.MachineServiceClient interface.
func (c *Client) Logs(ctx context.Context, namespace string, driver common.ContainerDriver, id string, follow bool, tailLines int32) (stream machineapi.MachineService_LogsClient, err error) {
	stream, err = c.MachineClient.Logs(ctx, &machineapi.LogsRequest{
		Namespace: namespace,
		Driver:    driver,
		Id:        id,
		Follow:    follow,
		TailLines: tailLines,
	})

	return stream, err
}

// LogsContainers implements the proto.MachineServiceClient interface.
func (c *Client) LogsContainers(ctx context.Context, callOptions ...grpc.CallOption) (resp *machineapi.LogsContainersResponse, err error) {
	resp, err = c.MachineClient.LogsContainers(
		ctx,
		&emptypb.Empty{},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// Version implements the proto.MachineServiceClient interface.
func (c *Client) Version(ctx context.Context, callOptions ...grpc.CallOption) (resp *machineapi.VersionResponse, err error) {
	resp, err = c.MachineClient.Version(
		ctx,
		&emptypb.Empty{},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// Processes implements the proto.MachineServiceClient interface.
func (c *Client) Processes(ctx context.Context, callOptions ...grpc.CallOption) (resp *machineapi.ProcessesResponse, err error) {
	resp, err = c.MachineClient.Processes(
		ctx,
		&emptypb.Empty{},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// Memory implements the proto.MachineServiceClient interface.
func (c *Client) Memory(ctx context.Context, callOptions ...grpc.CallOption) (resp *machineapi.MemoryResponse, err error) {
	resp, err = c.MachineClient.Memory(
		ctx,
		&emptypb.Empty{},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// Mounts implements the proto.MachineServiceClient interface.
func (c *Client) Mounts(ctx context.Context, callOptions ...grpc.CallOption) (resp *machineapi.MountsResponse, err error) {
	resp, err = c.MachineClient.Mounts(
		ctx,
		&emptypb.Empty{},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// LS implements the proto.MachineServiceClient interface.
func (c *Client) LS(ctx context.Context, req *machineapi.ListRequest) (stream machineapi.MachineService_ListClient, err error) {
	return c.MachineClient.List(ctx, req)
}

// DiskUsage implements the proto.MachineServiceClient interface.
func (c *Client) DiskUsage(ctx context.Context, req *machineapi.DiskUsageRequest) (stream machineapi.MachineService_DiskUsageClient, err error) {
	return c.MachineClient.DiskUsage(ctx, req)
}

// Copy implements the proto.MachineServiceClient interface.
//
// This method doesn't support multiplexing of the result:
// * either client.WithNodes is not used, or it contains a single node in the list.
func (c *Client) Copy(ctx context.Context, rootPath string) (io.ReadCloser, error) {
	stream, err := c.MachineClient.Copy(ctx, &machineapi.CopyRequest{
		RootPath: rootPath,
	})
	if err != nil {
		return nil, err
	}

	return ReadStream(stream)
}

// UpgradeOptions provides upgrade API options.
type UpgradeOptions struct {
	Request         machineapi.UpgradeRequest
	GRPCCallOptions []grpc.CallOption
}

// UpgradeOption provides upgrade API options.
type UpgradeOption func(*UpgradeOptions)

// WithUpgradeImage sets the image for the upgrade.
func WithUpgradeImage(image string) UpgradeOption {
	return func(req *UpgradeOptions) {
		req.Request.Image = image
	}
}

// WithUpgradeRebootMode sets the reboot mode for the upgrade.
func WithUpgradeRebootMode(mode machineapi.UpgradeRequest_RebootMode) UpgradeOption {
	return func(req *UpgradeOptions) {
		req.Request.RebootMode = mode
	}
}

// WithUpgradePreserve sets the preserve flag for the upgrade.
func WithUpgradePreserve(preserve bool) UpgradeOption {
	return func(req *UpgradeOptions) {
		req.Request.Preserve = preserve
	}
}

// WithUpgradeStage sets the stage flag for the upgrade.
func WithUpgradeStage(stage bool) UpgradeOption {
	return func(req *UpgradeOptions) {
		req.Request.Stage = stage
	}
}

// WithUpgradeForce sets the force flag for the upgrade.
func WithUpgradeForce(force bool) UpgradeOption {
	return func(req *UpgradeOptions) {
		req.Request.Force = force
	}
}

// WithUpgradeGRPCCallOptions sets the gRPC call options for the upgrade.
func WithUpgradeGRPCCallOptions(opts ...grpc.CallOption) UpgradeOption {
	return func(req *UpgradeOptions) {
		req.GRPCCallOptions = opts
	}
}

// Upgrade initiates a Talos upgrade and implements the proto.MachineServiceClient interface.
//
// Deprecated: use LifecycleClient instead.
func (c *Client) Upgrade(ctx context.Context, image string, stage, force bool, callOptions ...grpc.CallOption) (*machineapi.UpgradeResponse, error) {
	return c.UpgradeWithOptions(
		ctx,
		WithUpgradeImage(image),
		WithUpgradeRebootMode(machineapi.UpgradeRequest_DEFAULT),
		WithUpgradeStage(stage),
		WithUpgradeForce(force),
		WithUpgradeGRPCCallOptions(callOptions...),
	)
}

// UpgradeWithOptions initiates a Talos upgrade with the given options.
//
// Deprecated: use LifecycleClient instead.
func (c *Client) UpgradeWithOptions(ctx context.Context, opts ...UpgradeOption) (*machineapi.UpgradeResponse, error) {
	var options UpgradeOptions

	for _, opt := range opts {
		opt(&options)
	}

	resp, err := c.MachineClient.Upgrade(ctx, &options.Request, options.GRPCCallOptions...)

	return FilterMessages(resp, err)
}

// ServiceList returns list of services with their state.
func (c *Client) ServiceList(ctx context.Context, callOptions ...grpc.CallOption) (resp *machineapi.ServiceListResponse, err error) {
	resp, err = c.MachineClient.ServiceList(
		ctx,
		&emptypb.Empty{},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// ServiceInfo provides info about a service and node metadata.
type ServiceInfo struct {
	Metadata *common.Metadata
	Service  *machineapi.ServiceInfo
}

// ServiceInfo returns info about a single service
//
// This is implemented via service list API, as we don't have many services
// If service with given id is not registered, function returns nil.
func (c *Client) ServiceInfo(ctx context.Context, id string, callOptions ...grpc.CallOption) (services []ServiceInfo, err error) {
	var resp *machineapi.ServiceListResponse

	resp, err = c.MachineClient.ServiceList(
		ctx,
		&emptypb.Empty{},
		callOptions...,
	)
	if err != nil {
		return services, err
	}

	resp, err = FilterMessages(resp, err)

	// FilterMessages might remove responses if they actually contain errors,
	// errors will be merged into `resp`.
	if resp == nil {
		return services, err
	}

	for _, resp := range resp.Messages {
		for _, svc := range resp.Services {
			if svc.Id == id {
				services = append(services, ServiceInfo{
					Metadata: resp.Metadata,
					Service:  svc,
				})
			}
		}
	}

	return services, err
}

// ServiceStart starts a service.
func (c *Client) ServiceStart(ctx context.Context, id string, callOptions ...grpc.CallOption) (resp *machineapi.ServiceStartResponse, err error) {
	resp, err = c.MachineClient.ServiceStart(
		ctx,
		&machineapi.ServiceStartRequest{Id: id},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// ServiceStop stops a service.
func (c *Client) ServiceStop(ctx context.Context, id string, callOptions ...grpc.CallOption) (resp *machineapi.ServiceStopResponse, err error) {
	resp, err = c.MachineClient.ServiceStop(
		ctx,
		&machineapi.ServiceStopRequest{Id: id},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// ServiceRestart restarts a service.
func (c *Client) ServiceRestart(ctx context.Context, id string, callOptions ...grpc.CallOption) (resp *machineapi.ServiceRestartResponse, err error) {
	resp, err = c.MachineClient.ServiceRestart(
		ctx,
		&machineapi.ServiceRestartRequest{Id: id},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// Time returns the time.
func (c *Client) Time(ctx context.Context, callOptions ...grpc.CallOption) (resp *timeapi.TimeResponse, err error) {
	resp, err = c.TimeClient.Time(
		ctx,
		&emptypb.Empty{},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// TimeCheck returns the time compared to the specified ntp server.
func (c *Client) TimeCheck(ctx context.Context, server string, callOptions ...grpc.CallOption) (resp *timeapi.TimeResponse, err error) {
	resp, err = c.TimeClient.TimeCheck(
		ctx,
		&timeapi.TimeRequest{Server: server},
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// Read reads a file.
//
// This method doesn't support multiplexing of the result:
// * either client.WithNodes is not used, or it contains a single node in the list.
func (c *Client) Read(ctx context.Context, path string) (io.ReadCloser, error) {
	stream, err := c.MachineClient.Read(ctx, &machineapi.ReadRequest{Path: path})
	if err != nil {
		return nil, err
	}

	return ReadStream(stream)
}

// ClusterHealthCheck runs a Talos cluster health check.
func (c *Client) ClusterHealthCheck(ctx context.Context, waitTimeout time.Duration, clusterInfo *clusterapi.ClusterInfo) (clusterapi.ClusterService_HealthCheckClient, error) {
	return c.ClusterClient.HealthCheck(ctx, &clusterapi.HealthCheckRequest{
		WaitTimeout: durationpb.New(waitTimeout),
		ClusterInfo: clusterInfo,
	})
}

// EtcdRemoveMemberByID removes a node from etcd cluster by etcd member ID.
func (c *Client) EtcdRemoveMemberByID(ctx context.Context, req *machineapi.EtcdRemoveMemberByIDRequest, callOptions ...grpc.CallOption) error {
	resp, err := c.MachineClient.EtcdRemoveMemberByID(ctx, req, callOptions...)
	if err == nil {
		_, err = FilterMessages(resp, err)
	}

	return err
}

// EtcdLeaveCluster makes node leave etcd cluster.
func (c *Client) EtcdLeaveCluster(ctx context.Context, req *machineapi.EtcdLeaveClusterRequest, callOptions ...grpc.CallOption) error {
	resp, err := c.MachineClient.EtcdLeaveCluster(ctx, req, callOptions...)
	if err == nil {
		_, err = FilterMessages(resp, err)
	}

	return err
}

// EtcdForfeitLeadership makes node forfeit leadership in the etcd cluster.
func (c *Client) EtcdForfeitLeadership(ctx context.Context, req *machineapi.EtcdForfeitLeadershipRequest, callOptions ...grpc.CallOption) (*machineapi.EtcdForfeitLeadershipResponse, error) {
	resp, err := c.MachineClient.EtcdForfeitLeadership(ctx, req, callOptions...)

	return FilterMessages(resp, err)
}

// EtcdMemberList lists etcd members of the cluster.
func (c *Client) EtcdMemberList(ctx context.Context, req *machineapi.EtcdMemberListRequest, callOptions ...grpc.CallOption) (*machineapi.EtcdMemberListResponse, error) {
	resp, err := c.MachineClient.EtcdMemberList(ctx, req, callOptions...)

	return FilterMessages(resp, err)
}

// EtcdSnapshot receives a snapshot of the etcd from the node.
//
// This method doesn't support multiplexing of the result:
// * either client.WithNodes is not used, or it contains a single node in the list.
func (c *Client) EtcdSnapshot(ctx context.Context, req *machineapi.EtcdSnapshotRequest, callOptions ...grpc.CallOption) (io.ReadCloser, error) {
	stream, err := c.MachineClient.EtcdSnapshot(ctx, req, callOptions...)
	if err != nil {
		return nil, err
	}

	return ReadStream(stream)
}

// EtcdRecover uploads etcd snapshot created with EtcdSnapshot to the node.
func (c *Client) EtcdRecover(ctx context.Context, snapshot io.Reader, callOptions ...grpc.CallOption) (*machineapi.EtcdRecoverResponse, error) {
	cli, err := c.MachineClient.EtcdRecover(ctx, callOptions...)
	if err != nil {
		return nil, err
	}

	buf := make([]byte, 4096)

	for {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		default:
		}

		var n int

		n, err = snapshot.Read(buf)
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}

			return nil, fmt.Errorf("error reading snapshot: %w", err)
		}

		if err = cli.Send(&common.Data{
			Bytes: buf[:n],
		}); err != nil {
			if errors.Is(err, io.EOF) {
				break
			}

			return nil, err
		}
	}

	resp, err := cli.CloseAndRecv()

	return FilterMessages(resp, err)
}

// EtcdAlarmList lists etcd alarms for the current node.
//
// This method is available only on control plane nodes (which run etcd).
func (c *Client) EtcdAlarmList(ctx context.Context, opts ...grpc.CallOption) (*machineapi.EtcdAlarmListResponse, error) {
	resp, err := c.MachineClient.EtcdAlarmList(ctx, &emptypb.Empty{}, opts...)

	return FilterMessages(resp, err)
}

// EtcdAlarmDisarm disarms etcd alarms for the current node.
//
// This method is available only on control plane nodes (which run etcd).
func (c *Client) EtcdAlarmDisarm(ctx context.Context, opts ...grpc.CallOption) (*machineapi.EtcdAlarmDisarmResponse, error) {
	resp, err := c.MachineClient.EtcdAlarmDisarm(ctx, &emptypb.Empty{}, opts...)

	return FilterMessages(resp, err)
}

// EtcdDefragment defragments etcd data directory for the current node.
//
// Defragmentation is a resource-heavy operation, so it should only run on a specific
// node.
//
// This method is available only on control plane nodes (which run etcd).
func (c *Client) EtcdDefragment(ctx context.Context, opts ...grpc.CallOption) (*machineapi.EtcdDefragmentResponse, error) {
	resp, err := c.MachineClient.EtcdDefragment(ctx, &emptypb.Empty{}, opts...)

	return FilterMessages(resp, err)
}

// EtcdStatus returns etcd status for the current member.
//
// This method is available only on control plane nodes (which run etcd).
func (c *Client) EtcdStatus(ctx context.Context, opts ...grpc.CallOption) (*machineapi.EtcdStatusResponse, error) {
	resp, err := c.MachineClient.EtcdStatus(ctx, &emptypb.Empty{}, opts...)

	return FilterMessages(resp, err)
}

// EtcdDowngradeCancel cancels etcd cluster downgrade that is in progress.
//
// This method is available only on control plane nodes (which run etcd).
func (c *Client) EtcdDowngradeCancel(ctx context.Context, opts ...grpc.CallOption) (*machineapi.EtcdDowngradeCancelResponse, error) {
	resp, err := c.MachineClient.EtcdDowngradeCancel(ctx, &emptypb.Empty{}, opts...)

	return FilterMessages(resp, err)
}

// EtcdDowngradeEnable enables etcd cluster downgrade to a specific version.
//
// This method is available only on control plane nodes (which run etcd).
func (c *Client) EtcdDowngradeEnable(ctx context.Context, req *machineapi.EtcdDowngradeEnableRequest, opts ...grpc.CallOption) (*machineapi.EtcdDowngradeEnableResponse, error) {
	resp, err := c.MachineClient.EtcdDowngradeEnable(ctx, req, opts...)

	return FilterMessages(resp, err)
}

// EtcdDowngradeValidate validates etcd cluster for downgrade to a specific version.
//
// This method is available only on control plane nodes (which run etcd).
func (c *Client) EtcdDowngradeValidate(ctx context.Context, req *machineapi.EtcdDowngradeValidateRequest, opts ...grpc.CallOption) (*machineapi.EtcdDowngradeValidateResponse, error) {
	resp, err := c.MachineClient.EtcdDowngradeValidate(ctx, req, opts...)

	return FilterMessages(resp, err)
}

// GenerateClientConfiguration implements proto.MachineServiceClient interface.
func (c *Client) GenerateClientConfiguration(ctx context.Context, req *machineapi.GenerateClientConfigurationRequest, callOptions ...grpc.CallOption) (resp *machineapi.GenerateClientConfigurationResponse, err error) { //nolint:lll
	resp, err = c.MachineClient.GenerateClientConfiguration(ctx, req, callOptions...)

	return FilterMessages(resp, err)
}

// PacketCapture implements the proto.MachineServiceClient interface.
//
// This method doesn't support multiplexing of the result:
// * either client.WithNodes is not used, or it contains a single node in the list.
func (c *Client) PacketCapture(ctx context.Context, req *machineapi.PacketCaptureRequest) (io.ReadCloser, error) {
	stream, err := c.MachineClient.PacketCapture(ctx, req)
	if err != nil {
		return nil, err
	}

	return ReadStream(stream)
}

// MachineStream is a common interface for streams returned by streaming APIs.
type MachineStream interface {
	Recv() (*common.Data, error)
	grpc.ClientStream
}

// ReadStream converts grpc stream into io.Reader.
func ReadStream(stream MachineStream) (io.ReadCloser, error) {
	pr, pw := io.Pipe()

	go func() {
		//nolint:errcheck
		defer pw.Close()

		for {
			data, err := stream.Recv()
			if err != nil {
				if errors.Is(err, io.EOF) || StatusCode(err) == codes.Canceled || StatusCode(err) == codes.DeadlineExceeded {
					return
				}

				pw.CloseWithError(err)

				return
			}

			if data.Bytes != nil {
				_, err = pw.Write(data.Bytes)
				if err != nil {
					return
				}
			}

			if data.Metadata != nil && data.Metadata.Error != "" {
				pw.CloseWithError(metaToErr(data.Metadata))

				return
			}
		}
	}()

	return pr, stream.CloseSend()
}

func metaToErr(md *common.Metadata) error {
	if md.Status == nil {
		return errors.New(md.Error)
	}

	code := codes.Code(md.Status.Code)
	if code == codes.Canceled || code == codes.DeadlineExceeded {
		return nil
	}

	return status.FromProto(md.Status).Err()
}

// Netstat lists the network sockets on the current node.
func (c *Client) Netstat(ctx context.Context, req *machineapi.NetstatRequest, callOptions ...grpc.CallOption) (*machineapi.NetstatResponse, error) {
	resp, err := c.MachineClient.Netstat(
		ctx,
		req,
		callOptions...,
	)

	return FilterMessages(resp, err)
}

// MetaWrite writes a key to META storage.
func (c *Client) MetaWrite(ctx context.Context, key uint8, value []byte, callOptions ...grpc.CallOption) error {
	resp, err := c.MachineClient.MetaWrite(
		ctx,
		&machineapi.MetaWriteRequest{
			Key:   uint32(key),
			Value: value,
		},
		callOptions...,
	)

	_, err = FilterMessages(resp, err)

	return err
}

// MetaDelete deletes a key from META storage.
func (c *Client) MetaDelete(ctx context.Context, key uint8, callOptions ...grpc.CallOption) error {
	resp, err := c.MachineClient.MetaDelete(
		ctx,
		&machineapi.MetaDeleteRequest{
			Key: uint32(key),
		},
		callOptions...,
	)

	_, err = FilterMessages(resp, err)

	return err
}

// ImageList lists images in the CRI.
//
// Deprecated: use ImageServiceClient instead.
func (c *Client) ImageList(ctx context.Context, namespace common.ContainerdNamespace, callOptions ...grpc.CallOption) (machineapi.MachineService_ImageListClient, error) {
	return c.MachineClient.ImageList(ctx,
		&machineapi.ImageListRequest{
			Namespace: namespace,
		},
		callOptions...,
	)
}

// ImagePull pre-pulls an image to the CRI.
//
// Deprecated: use ImageServiceClient instead.
func (c *Client) ImagePull(ctx context.Context, namespace common.ContainerdNamespace, imageRef string, callOptions ...grpc.CallOption) error {
	resp, err := c.MachineClient.ImagePull(ctx,
		&machineapi.ImagePullRequest{
			Namespace: namespace,
			Reference: imageRef,
		},
		callOptions...,
	)

	_, err = FilterMessages(resp, err)

	return err
}

// BlockDeviceWipe wipes a block device which is not used as a volume.
func (c *Client) BlockDeviceWipe(ctx context.Context, req *storageapi.BlockDeviceWipeRequest, callOptions ...grpc.CallOption) error {
	resp, err := c.StorageClient.BlockDeviceWipe(ctx, req, callOptions...)

	_, err = FilterMessages(resp, err)

	return err
}
