// +build remoteclient

package adapter

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strconv"
	"syscall"
	"time"

	"github.com/containers/libpod/cmd/podman/cliconfig"
	"github.com/containers/libpod/cmd/podman/shared"
	"github.com/containers/libpod/libpod"
	"github.com/containers/libpod/libpod/define"
	"github.com/containers/libpod/libpod/logs"
	envLib "github.com/containers/libpod/pkg/env"
	iopodman "github.com/containers/libpod/pkg/varlink"
	"github.com/containers/libpod/pkg/varlinkapi/virtwriter"
	"github.com/cri-o/ocicni/pkg/ocicni"
	"github.com/docker/docker/pkg/term"
	"github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/varlink/go/varlink"
	"golang.org/x/crypto/ssh/terminal"
	"k8s.io/client-go/tools/remotecommand"
)

// Inspect returns an inspect struct from varlink
func (c *Container) Inspect(size bool) (*define.InspectContainerData, error) {
	reply, err := iopodman.ContainerInspectData().Call(c.Runtime.Conn, c.ID(), size)
	if err != nil {
		return nil, err
	}
	data := define.InspectContainerData{}
	if err := json.Unmarshal([]byte(reply), &data); err != nil {
		return nil, err
	}
	return &data, err
}

// ID returns the ID of the container
func (c *Container) ID() string {
	return c.config.ID
}

// Restart a single container
func (c *Container) Restart(timeout int64) error {
	_, err := iopodman.RestartContainer().Call(c.Runtime.Conn, c.ID(), timeout)
	return err
}

// Pause a container
func (c *Container) Pause() error {
	_, err := iopodman.PauseContainer().Call(c.Runtime.Conn, c.ID())
	return err
}

// Unpause a container
func (c *Container) Unpause() error {
	_, err := iopodman.UnpauseContainer().Call(c.Runtime.Conn, c.ID())
	return err
}

func (c *Container) PortMappings() ([]ocicni.PortMapping, error) {
	// First check if the container belongs to a network namespace (like a pod)
	// Taken from libpod portmappings()
	if len(c.config.NetNsCtr) > 0 {
		netNsCtr, err := c.Runtime.LookupContainer(c.config.NetNsCtr)
		if err != nil {
			return nil, errors.Wrapf(err, "unable to lookup network namespace for container %s", c.ID())
		}
		return netNsCtr.PortMappings()
	}
	return c.config.PortMappings, nil
}

// Config returns a container config
func (r *LocalRuntime) Config(name string) *libpod.ContainerConfig {
	// TODO the Spec being returned is not populated.  Matt and I could not figure out why.  Will defer
	// further looking into it for after devconf.
	// The libpod function for this has no errors so we are kind of in a tough
	// spot here.  Logging the errors for now.
	reply, err := iopodman.ContainerConfig().Call(r.Conn, name)
	if err != nil {
		logrus.Error("call to container.config failed")
	}
	data := libpod.ContainerConfig{}
	if err := json.Unmarshal([]byte(reply), &data); err != nil {
		logrus.Error("failed to unmarshal container inspect data")
	}
	return &data

}

// ContainerState returns the "state" of the container.
func (r *LocalRuntime) ContainerState(name string) (*libpod.ContainerState, error) { // no-lint
	reply, err := iopodman.ContainerStateData().Call(r.Conn, name)
	if err != nil {
		return nil, err
	}
	data := libpod.ContainerState{}
	if err := json.Unmarshal([]byte(reply), &data); err != nil {
		return nil, err
	}
	return &data, err

}

// Spec obtains the container spec.
func (r *LocalRuntime) Spec(name string) (*specs.Spec, error) {
	reply, err := iopodman.Spec().Call(r.Conn, name)
	if err != nil {
		return nil, err
	}
	data := specs.Spec{}
	if err := json.Unmarshal([]byte(reply), &data); err != nil {
		return nil, err
	}
	return &data, nil
}

// LookupContainers is a wrapper for LookupContainer
func (r *LocalRuntime) LookupContainers(idsOrNames []string) ([]*Container, error) {
	var containers []*Container
	for _, name := range idsOrNames {
		ctr, err := r.LookupContainer(name)
		if err != nil {
			return nil, err
		}
		containers = append(containers, ctr)
	}
	return containers, nil
}

// LookupContainer gets basic information about container over a varlink
// connection and then translates it to a *Container
func (r *LocalRuntime) LookupContainer(idOrName string) (*Container, error) {
	state, err := r.ContainerState(idOrName)
	if err != nil {
		return nil, err
	}
	config := r.Config(idOrName)
	return &Container{
		remoteContainer{
			r,
			config,
			state,
		},
	}, nil
}

// GetAllContainers returns all containers in a slice
func (r *LocalRuntime) GetAllContainers() ([]*Container, error) {
	var containers []*Container
	ctrs, err := iopodman.GetContainersByContext().Call(r.Conn, true, false, []string{})
	if err != nil {
		return nil, err
	}
	for _, ctr := range ctrs {
		container, err := r.LookupContainer(ctr)
		if err != nil {
			return nil, err
		}
		containers = append(containers, container)
	}
	return containers, nil
}

func (r *LocalRuntime) LookupContainersWithStatus(filters []string) ([]*Container, error) {
	var containers []*Container
	ctrs, err := iopodman.GetContainersByStatus().Call(r.Conn, filters)
	if err != nil {
		return nil, err
	}
	// This is not performance savvy; if this turns out to be a problematic series of lookups, we need to
	// create a new endpoint to speed things up
	for _, ctr := range ctrs {
		container, err := r.LookupContainer(ctr.Id)
		if err != nil {
			return nil, err
		}
		containers = append(containers, container)
	}
	return containers, nil
}

func (r *LocalRuntime) GetLatestContainer() (*Container, error) {
	reply, err := iopodman.GetContainersByContext().Call(r.Conn, false, true, nil)
	if err != nil {
		return nil, err
	}
	if len(reply) > 0 {
		return r.LookupContainer(reply[0])
	}
	return nil, errors.New("no containers exist")
}

// GetArtifact returns a container's artifacts
func (c *Container) GetArtifact(name string) ([]byte, error) {
	var data []byte
	reply, err := iopodman.ContainerArtifacts().Call(c.Runtime.Conn, c.ID(), name)
	if err != nil {
		return nil, err
	}
	if err := json.Unmarshal([]byte(reply), &data); err != nil {
		return nil, err
	}
	return data, err
}

// Config returns a container's Config ... same as ctr.Config()
func (c *Container) Config() *libpod.ContainerConfig {
	if c.config != nil {
		return c.config
	}
	return c.Runtime.Config(c.ID())
}

// Name returns the name of the container
func (c *Container) Name() string {
	return c.config.Name
}

// StopContainers stops requested containers using varlink.
// Returns the list of stopped container ids, map of failed to stop container ids + errors, or any non-container error
func (r *LocalRuntime) StopContainers(ctx context.Context, cli *cliconfig.StopValues) ([]string, map[string]error, error) {
	var (
		ok       = []string{}
		failures = map[string]error{}
	)

	ids, err := iopodman.GetContainersByContext().Call(r.Conn, cli.All, cli.Latest, cli.InputArgs)
	if err != nil {
		return ok, failures, TranslateError(err)
	}

	for _, id := range ids {
		if _, err := iopodman.StopContainer().Call(r.Conn, id, int64(cli.Timeout)); err != nil {
			transError := TranslateError(err)
			if errors.Cause(transError) == define.ErrCtrStopped {
				ok = append(ok, id)
				continue
			}
			if errors.Cause(transError) == define.ErrCtrStateInvalid && cli.All {
				ok = append(ok, id)
				continue
			}
			failures[id] = err
		} else {
			// We should be using ID here because in varlink, only successful returns
			// include the string id
			ok = append(ok, id)
		}
	}
	return ok, failures, nil
}

// InitContainers initializes container(s) based on Varlink.
// It returns a list of successful ID(s), a map of failed container ID to error,
// or an error if a more general error occurred.
func (r *LocalRuntime) InitContainers(ctx context.Context, cli *cliconfig.InitValues) ([]string, map[string]error, error) {
	var (
		ok       = []string{}
		failures = map[string]error{}
	)

	ids, err := iopodman.GetContainersByContext().Call(r.Conn, cli.All, cli.Latest, cli.InputArgs)
	if err != nil {
		return nil, nil, err
	}

	for _, id := range ids {
		initialized, err := iopodman.InitContainer().Call(r.Conn, id)
		if err != nil {
			if cli.All {
				switch err.(type) {
				case *iopodman.InvalidState:
					ok = append(ok, initialized)
				default:
					failures[id] = err
				}
			} else {
				failures[id] = err
			}
		} else {
			ok = append(ok, initialized)
		}
	}
	return ok, failures, nil
}

// KillContainers sends signal to container(s) based on varlink.
// Returns list of successful id(s), map of failed id(s) + error, or error not from container
func (r *LocalRuntime) KillContainers(ctx context.Context, cli *cliconfig.KillValues, signal syscall.Signal) ([]string, map[string]error, error) {
	var (
		ok       = []string{}
		failures = map[string]error{}
	)

	ids, err := iopodman.GetContainersByContext().Call(r.Conn, cli.All, cli.Latest, cli.InputArgs)
	if err != nil {
		return ok, failures, err
	}

	for _, id := range ids {
		killed, err := iopodman.KillContainer().Call(r.Conn, id, int64(signal))
		if err != nil {
			failures[id] = err
		} else {
			ok = append(ok, killed)
		}
	}
	return ok, failures, nil
}

// RemoveContainer removes container(s) based on varlink inputs.
func (r *LocalRuntime) RemoveContainers(ctx context.Context, cli *cliconfig.RmValues) ([]string, map[string]error, error) {
	var (
		ok       = []string{}
		failures = map[string]error{}
	)

	ids, err := iopodman.GetContainersByContext().Call(r.Conn, cli.All, cli.Latest, cli.InputArgs)
	if err != nil {
		// Failed to get containers. If force is specified, get the containers ID
		// and evict them
		if !cli.Force {
			return nil, nil, TranslateError(err)
		}

		for _, ctr := range cli.InputArgs {
			logrus.Debugf("Evicting container %q", ctr)
			id, err := iopodman.EvictContainer().Call(r.Conn, ctr, cli.Volumes)
			if err != nil {
				failures[ctr] = errors.Wrapf(err, "Failed to evict container: %q", id)
				continue
			}
			ok = append(ok, id)
		}
		return ok, failures, nil
	}

	for _, id := range ids {
		_, err := iopodman.RemoveContainer().Call(r.Conn, id, cli.Force, cli.Volumes)
		if err != nil {
			failures[id] = err
		} else {
			ok = append(ok, id)
		}
	}
	return ok, failures, nil
}

// UmountRootFilesystems umounts container(s) root filesystems based on varlink inputs
func (r *LocalRuntime) UmountRootFilesystems(ctx context.Context, cli *cliconfig.UmountValues) ([]string, map[string]error, error) {
	ids, err := iopodman.GetContainersByContext().Call(r.Conn, cli.All, cli.Latest, cli.InputArgs)
	if err != nil {
		return nil, nil, err
	}

	var (
		ok       = []string{}
		failures = map[string]error{}
	)

	for _, id := range ids {
		err := iopodman.UnmountContainer().Call(r.Conn, id, cli.Force)
		if err != nil {
			failures[id] = err
		} else {
			ok = append(ok, id)
		}
	}
	return ok, failures, nil
}

// WaitOnContainers waits for all given container(s) to stop.
// interval is currently ignored.
func (r *LocalRuntime) WaitOnContainers(ctx context.Context, cli *cliconfig.WaitValues, interval time.Duration) ([]string, map[string]error, error) {
	var (
		ok       = []string{}
		failures = map[string]error{}
	)

	ids, err := iopodman.GetContainersByContext().Call(r.Conn, false, cli.Latest, cli.InputArgs)
	if err != nil {
		return ok, failures, err
	}

	for _, id := range ids {
		stopped, err := iopodman.WaitContainer().Call(r.Conn, id, int64(interval))
		if err != nil {
			failures[id] = err
		} else {
			ok = append(ok, strconv.FormatInt(stopped, 10))
		}
	}
	return ok, failures, nil
}

// BatchContainerOp is wrapper func to mimic shared's function with a similar name meant for libpod
func BatchContainerOp(ctr *Container, opts shared.PsOptions) (shared.BatchContainerStruct, error) {
	// TODO If pod ps ever shows container's sizes, re-enable this code; otherwise it isn't needed
	// and would be a perf hit
	// data, err := ctr.Inspect(true)
	// if err != nil {
	// 	return shared.BatchContainerStruct{}, err
	// }
	//
	// size := new(shared.ContainerSize)
	// size.RootFsSize = data.SizeRootFs
	// size.RwSize = data.SizeRw

	bcs := shared.BatchContainerStruct{
		ConConfig:   ctr.config,
		ConState:    ctr.state.State,
		ExitCode:    ctr.state.ExitCode,
		Pid:         ctr.state.PID,
		StartedTime: ctr.state.StartedTime,
		ExitedTime:  ctr.state.FinishedTime,
		// Size: size,
	}
	return bcs, nil
}

// Log one or more containers over a varlink connection
func (r *LocalRuntime) Log(c *cliconfig.LogsValues, options *logs.LogOptions) error {
	// GetContainersLogs
	reply, err := iopodman.GetContainersLogs().Send(r.Conn, uint64(varlink.More), c.InputArgs, c.Follow, c.Latest, options.Since.Format(time.RFC3339Nano), c.Tail, c.Timestamps)
	if err != nil {
		return errors.Wrapf(err, "failed to get container logs")
	}
	if len(c.InputArgs) > 1 {
		options.Multi = true
	}
	for {
		log, flags, err := reply()
		if err != nil {
			return err
		}
		if log.Time == "" && log.Msg == "" {
			// We got a blank log line which can signal end of stream
			break
		}
		lTime, err := time.Parse(time.RFC3339Nano, log.Time)
		if err != nil {
			return errors.Wrapf(err, "unable to parse time of log %s", log.Time)
		}
		logLine := logs.LogLine{
			Device:       log.Device,
			ParseLogType: log.ParseLogType,
			Time:         lTime,
			Msg:          log.Msg,
			CID:          log.Cid,
		}
		fmt.Println(logLine.String(options))
		if flags&varlink.Continues == 0 {
			break
		}
	}
	return nil
}

// CreateContainer creates a container from the cli over varlink
func (r *LocalRuntime) CreateContainer(ctx context.Context, c *cliconfig.CreateValues) (string, error) {
	results := shared.NewIntermediateLayer(&c.PodmanCommand, true)
	return iopodman.CreateContainer().Call(r.Conn, results.MakeVarlink())
}

// Run creates a container overvarlink and then starts it
func (r *LocalRuntime) Run(ctx context.Context, c *cliconfig.RunValues, exitCode int) (int, error) {
	// TODO the exit codes for run need to be figured out for remote connections
	results := shared.NewIntermediateLayer(&c.PodmanCommand, true)
	cid, err := iopodman.CreateContainer().Call(r.Conn, results.MakeVarlink())
	if err != nil {
		return exitCode, err
	}
	if c.Bool("detach") {
		if _, err := iopodman.StartContainer().Call(r.Conn, cid); err != nil {
			return exitCode, err
		}
		fmt.Println(cid)
		return 0, nil
	}
	inputStream := os.Stdin
	// If -i is not set, clear stdin
	if !c.Bool("interactive") {
		inputStream = nil
	}
	exitChan, errChan, err := r.attach(ctx, inputStream, os.Stdout, cid, true, c.String("detach-keys"))
	if err != nil {
		return exitCode, err
	}
	exitCode = <-exitChan
	finalError := <-errChan
	return exitCode, finalError
}

func ReadExitFile(runtimeTmp, ctrID string) (int, error) {
	return 0, define.ErrNotImplemented
}

// Ps lists containers based on criteria from user
func (r *LocalRuntime) Ps(c *cliconfig.PsValues, opts shared.PsOptions) ([]shared.PsContainerOutput, error) {
	var psContainers []shared.PsContainerOutput
	last := int64(c.Last)
	PsOpts := iopodman.PsOpts{
		All:     c.All,
		Filters: &c.Filter,
		Last:    &last,
		Latest:  &c.Latest,
		NoTrunc: &c.NoTrunct,
		Pod:     &c.Pod,
		Quiet:   &c.Quiet,
		Size:    &c.Size,
		Sort:    &c.Sort,
		Sync:    &c.Sync,
	}
	containers, err := iopodman.Ps().Call(r.Conn, PsOpts)
	if err != nil {
		return nil, err
	}
	for _, ctr := range containers {
		createdAt, err := time.Parse(time.RFC3339Nano, ctr.CreatedAt)
		if err != nil {
			return nil, err
		}
		exitedAt, err := time.Parse(time.RFC3339Nano, ctr.ExitedAt)
		if err != nil {
			return nil, err
		}
		startedAt, err := time.Parse(time.RFC3339Nano, ctr.StartedAt)
		if err != nil {
			return nil, err
		}
		containerSize := shared.ContainerSize{
			RootFsSize: ctr.RootFsSize,
			RwSize:     ctr.RwSize,
		}
		state, err := define.StringToContainerStatus(ctr.State)
		if err != nil {
			return nil, err
		}
		psc := shared.PsContainerOutput{
			ID:        ctr.Id,
			Image:     ctr.Image,
			Command:   ctr.Command,
			Created:   ctr.Created,
			Ports:     ctr.Ports,
			Names:     ctr.Names,
			IsInfra:   ctr.IsInfra,
			Status:    ctr.Status,
			State:     state,
			Pid:       int(ctr.PidNum),
			Size:      &containerSize,
			Pod:       ctr.Pod,
			CreatedAt: createdAt,
			ExitedAt:  exitedAt,
			StartedAt: startedAt,
			Labels:    ctr.Labels,
			PID:       ctr.NsPid,
			Cgroup:    ctr.Cgroup,
			IPC:       ctr.Ipc,
			MNT:       ctr.Mnt,
			NET:       ctr.Net,
			PIDNS:     ctr.PidNs,
			User:      ctr.User,
			UTS:       ctr.Uts,
			Mounts:    ctr.Mounts,
		}
		psContainers = append(psContainers, psc)
	}
	return psContainers, nil
}

// Attach to a remote terminal
func (r *LocalRuntime) Attach(ctx context.Context, c *cliconfig.AttachValues) error {
	ctr, err := r.LookupContainer(c.InputArgs[0])
	if err != nil {
		return nil
	}
	if ctr.state.State != define.ContainerStateRunning {
		return errors.New("you can only attach to running containers")
	}
	inputStream := os.Stdin
	if c.NoStdin {
		inputStream, err = os.Open(os.DevNull)
		if err != nil {
			return err
		}
	}
	_, errChan, err := r.attach(ctx, inputStream, os.Stdout, c.InputArgs[0], false, c.DetachKeys)
	if err != nil {
		return err
	}
	return <-errChan
}

// Checkpoint one or more containers
func (r *LocalRuntime) Checkpoint(c *cliconfig.CheckpointValues) error {
	if c.Export != "" {
		return errors.New("the remote client does not support exporting checkpoints")
	}
	if c.IgnoreRootfs {
		return errors.New("the remote client does not support --ignore-rootfs")
	}

	var lastError error
	ids, err := iopodman.GetContainersByContext().Call(r.Conn, c.All, c.Latest, c.InputArgs)
	if err != nil {
		return err
	}
	if c.All {
		// We don't have a great way to get all the running containers, so need to get all and then
		// check status on them bc checkpoint considers checkpointing a stopped container an error
		var runningIds []string
		for _, id := range ids {
			ctr, err := r.LookupContainer(id)
			if err != nil {
				return err
			}
			if ctr.state.State == define.ContainerStateRunning {
				runningIds = append(runningIds, id)
			}
		}
		ids = runningIds
	}

	for _, id := range ids {
		if _, err := iopodman.ContainerCheckpoint().Call(r.Conn, id, c.Keep, c.Keep, c.TcpEstablished); err != nil {
			if lastError != nil {
				fmt.Fprintln(os.Stderr, lastError)
			}
			lastError = errors.Wrapf(err, "failed to checkpoint container %v", id)
		} else {
			fmt.Println(id)
		}
	}
	return lastError
}

// Restore one or more containers
func (r *LocalRuntime) Restore(ctx context.Context, c *cliconfig.RestoreValues) error {
	if c.Import != "" {
		return errors.New("the remote client does not support importing checkpoints")
	}
	if c.IgnoreRootfs {
		return errors.New("the remote client does not support --ignore-rootfs")
	}

	var lastError error
	ids, err := iopodman.GetContainersByContext().Call(r.Conn, c.All, c.Latest, c.InputArgs)
	if err != nil {
		return err
	}
	if c.All {
		// We don't have a great way to get all the exited containers, so need to get all and then
		// check status on them bc checkpoint considers restoring a running container an error
		var exitedIDs []string
		for _, id := range ids {
			ctr, err := r.LookupContainer(id)
			if err != nil {
				return err
			}
			if ctr.state.State != define.ContainerStateRunning {
				exitedIDs = append(exitedIDs, id)
			}
		}
		ids = exitedIDs
	}

	for _, id := range ids {
		if _, err := iopodman.ContainerRestore().Call(r.Conn, id, c.Keep, c.TcpEstablished); err != nil {
			if lastError != nil {
				fmt.Fprintln(os.Stderr, lastError)
			}
			lastError = errors.Wrapf(err, "failed to restore container %v", id)
		} else {
			fmt.Println(id)
		}
	}
	return lastError
}

// Start starts an already created container
func (r *LocalRuntime) Start(ctx context.Context, c *cliconfig.StartValues, sigProxy bool) (int, error) {
	var (
		finalErr error
		exitCode = define.ExecErrorCodeGeneric
	)
	// TODO Figure out how to deal with exit codes
	inputStream := os.Stdin
	if !c.Interactive {
		inputStream = nil
	}

	containerIDs, err := iopodman.GetContainersByContext().Call(r.Conn, false, c.Latest, c.InputArgs)
	if err != nil {
		return exitCode, err
	}
	if len(containerIDs) < 1 {
		return exitCode, errors.New("failed to find containers to start")
	}
	// start.go makes sure that if attach, there can be only one ctr
	if c.Attach {
		exitChan, errChan, err := r.attach(ctx, inputStream, os.Stdout, containerIDs[0], true, c.DetachKeys)
		if err != nil {
			return exitCode, nil
		}
		exitCode := <-exitChan
		err = <-errChan
		return exitCode, err
	}

	// TODO the notion of starting a pod container and its deps still needs to be worked through
	//	Everything else is detached
	for _, cid := range containerIDs {
		reply, err := iopodman.StartContainer().Call(r.Conn, cid)
		if err != nil {
			if finalErr != nil {
				fmt.Println(err)
			}
			finalErr = err
		} else {
			fmt.Println(reply)
		}
	}
	return exitCode, finalErr
}

func (r *LocalRuntime) attach(ctx context.Context, stdin, stdout *os.File, cid string, start bool, detachKeys string) (chan int, chan error, error) {
	var (
		oldTermState *term.State
	)
	spec, err := r.Spec(cid)
	if err != nil {
		return nil, nil, err
	}
	resize := make(chan remotecommand.TerminalSize, 5)
	haveTerminal := terminal.IsTerminal(int(os.Stdin.Fd()))

	// Check if we are attached to a terminal. If we are, generate resize
	// events, and set the terminal to raw mode
	if haveTerminal && spec.Process.Terminal {
		cancel, oldTermState, err := handleTerminalAttach(ctx, resize)
		if err != nil {
			return nil, nil, err
		}
		defer cancel()
		defer restoreTerminal(oldTermState) // nolint: errcheck

		logrus.SetFormatter(&RawTtyFormatter{})
		term.SetRawTerminal(os.Stdin.Fd()) // nolint: errcheck
	}

	reply, err := iopodman.Attach().Send(r.Conn, varlink.Upgrade, cid, detachKeys, start)
	if err != nil {
		restoreTerminal(oldTermState) // nolint: errcheck
		return nil, nil, err
	}

	// See if the server accepts the upgraded connection or returns an error
	_, err = reply()

	if err != nil {
		restoreTerminal(oldTermState) // nolint: errcheck
		return nil, nil, err
	}

	ecChan := make(chan int, 1)
	errChan := configureVarlinkAttachStdio(r.Conn.Reader, r.Conn.Writer, stdin, stdout, oldTermState, resize, ecChan)
	return ecChan, errChan, nil
}

// PauseContainers pauses container(s) based on CLI inputs.
func (r *LocalRuntime) PauseContainers(ctx context.Context, cli *cliconfig.PauseValues) ([]string, map[string]error, error) {
	var (
		ok       []string
		failures = map[string]error{}
		ctrs     []*Container
		err      error
	)

	if cli.All {
		filters := []string{define.ContainerStateRunning.String()}
		ctrs, err = r.LookupContainersWithStatus(filters)
	} else {
		ctrs, err = r.LookupContainers(cli.InputArgs)
	}
	if err != nil {
		return ok, failures, err
	}

	for _, c := range ctrs {
		c := c
		err := c.Pause()
		if err != nil {
			failures[c.ID()] = err
		} else {
			ok = append(ok, c.ID())
		}
	}
	return ok, failures, nil
}

// UnpauseContainers unpauses containers based on input
func (r *LocalRuntime) UnpauseContainers(ctx context.Context, cli *cliconfig.UnpauseValues) ([]string, map[string]error, error) {
	var (
		ok       = []string{}
		failures = map[string]error{}
		ctrs     []*Container
		err      error
	)

	maxWorkers := shared.DefaultPoolSize("unpause")
	if cli.GlobalIsSet("max-workers") {
		maxWorkers = cli.GlobalFlags.MaxWorks
	}
	logrus.Debugf("Setting maximum rm workers to %d", maxWorkers)

	if cli.All {
		filters := []string{define.ContainerStatePaused.String()}
		ctrs, err = r.LookupContainersWithStatus(filters)
	} else {
		ctrs, err = r.LookupContainers(cli.InputArgs)
	}
	if err != nil {
		return ok, failures, err
	}
	for _, c := range ctrs {
		c := c
		err := c.Unpause()
		if err != nil {
			failures[c.ID()] = err
		} else {
			ok = append(ok, c.ID())
		}
	}
	return ok, failures, nil
}

// Restart restarts a container over varlink
func (r *LocalRuntime) Restart(ctx context.Context, c *cliconfig.RestartValues) ([]string, map[string]error, error) {
	var (
		containers        []*Container
		restartContainers []*Container
		err               error
		ok                = []string{}
		failures          = map[string]error{}
	)
	useTimeout := c.Flag("timeout").Changed || c.Flag("time").Changed
	inputTimeout := c.Timeout

	if c.Latest { // nolint: gocritic
		lastCtr, err := r.GetLatestContainer()
		if err != nil {
			return nil, nil, errors.Wrapf(err, "unable to get latest container")
		}
		restartContainers = append(restartContainers, lastCtr)
	} else if c.Running {
		containers, err = r.LookupContainersWithStatus([]string{define.ContainerStateRunning.String()})
		if err != nil {
			return nil, nil, err
		}
		restartContainers = append(restartContainers, containers...)
	} else if c.All {
		containers, err = r.GetAllContainers()
		if err != nil {
			return nil, nil, err
		}
		restartContainers = append(restartContainers, containers...)
	} else {
		for _, id := range c.InputArgs {
			ctr, err := r.LookupContainer(id)
			if err != nil {
				return nil, nil, err
			}
			restartContainers = append(restartContainers, ctr)
		}
	}

	for _, c := range restartContainers {
		c := c
		timeout := c.config.StopTimeout
		if useTimeout {
			timeout = inputTimeout
		}
		err := c.Restart(int64(timeout))
		if err != nil {
			failures[c.ID()] = err
		} else {
			ok = append(ok, c.ID())
		}
	}
	return ok, failures, nil
}

// Top display the running processes of a container
func (r *LocalRuntime) Top(cli *cliconfig.TopValues) ([]string, error) {
	var (
		ctr         *Container
		err         error
		descriptors []string
	)
	if cli.Latest {
		ctr, err = r.GetLatestContainer()
		descriptors = cli.InputArgs
	} else {
		ctr, err = r.LookupContainer(cli.InputArgs[0])
		descriptors = cli.InputArgs[1:]
	}
	if err != nil {
		return nil, err
	}
	return iopodman.Top().Call(r.Conn, ctr.ID(), descriptors)
}

// Prune removes stopped containers
func (r *LocalRuntime) Prune(ctx context.Context, maxWorkers int, filter []string) ([]string, map[string]error, error) {

	var (
		ok       = []string{}
		failures = map[string]error{}
		ctrs     []*Container
		err      error
	)
	logrus.Debugf("Setting maximum rm workers to %d", maxWorkers)

	filters := []string{define.ContainerStateExited.String()}
	ctrs, err = r.LookupContainersWithStatus(filters)
	if err != nil {
		return ok, failures, err
	}
	for _, c := range ctrs {
		c := c
		_, err := iopodman.RemoveContainer().Call(r.Conn, c.ID(), false, false)
		if err != nil {
			failures[c.ID()] = err
		} else {
			ok = append(ok, c.ID())
		}
	}
	return ok, failures, nil
}

// Cleanup any leftovers bits of stopped containers
func (r *LocalRuntime) CleanupContainers(ctx context.Context, cli *cliconfig.CleanupValues) ([]string, map[string]error, error) {
	return nil, nil, errors.New("container cleanup not supported for remote clients")
}

// Port displays port information about existing containers
func (r *LocalRuntime) Port(c *cliconfig.PortValues) ([]*Container, error) {
	var (
		containers []*Container
		err        error
	)
	// This one is a bit odd because when all is used, we only use running containers.
	if !c.All {
		containers, err = r.GetContainersByContext(false, c.Latest, c.InputArgs)
	} else {
		//	we need to only use running containers if all
		filters := []string{define.ContainerStateRunning.String()}
		containers, err = r.LookupContainersWithStatus(filters)
	}
	if err != nil {
		return nil, err
	}
	return containers, nil
}

// GenerateSystemd creates a systemd until for a container
func (r *LocalRuntime) GenerateSystemd(c *cliconfig.GenerateSystemdValues) (string, error) {
	return "", errors.New("systemd generation not supported for remote clients")
}

// GetNamespaces returns namespace information about a container for PS
func (r *LocalRuntime) GetNamespaces(container shared.PsContainerOutput) *shared.Namespace {
	ns := shared.Namespace{
		PID:    container.PID,
		Cgroup: container.Cgroup,
		IPC:    container.IPC,
		MNT:    container.MNT,
		NET:    container.NET,
		PIDNS:  container.PIDNS,
		User:   container.User,
		UTS:    container.UTS,
	}
	return &ns
}

// Commit creates a local image from a container
func (r *LocalRuntime) Commit(ctx context.Context, c *cliconfig.CommitValues, container, imageName string) (string, error) {
	var iid string
	reply, err := iopodman.Commit().Send(r.Conn, varlink.More, container, imageName, c.Change, c.Author, c.Message, c.Pause, c.Format)
	if err != nil {
		return "", err
	}
	for {
		responses, flags, err := reply()
		if err != nil {
			return "", err
		}
		for _, line := range responses.Logs {
			fmt.Fprintln(os.Stderr, line)
		}
		iid = responses.Id
		if flags&varlink.Continues == 0 {
			break
		}
	}
	return iid, nil
}

// ExecContainer executes a command in the container
func (r *LocalRuntime) ExecContainer(ctx context.Context, cli *cliconfig.ExecValues) (int, error) {
	var (
		oldTermState *term.State
		ec           = define.ExecErrorCodeGeneric
	)
	// default invalid command exit code
	// Validate given environment variables
	cliEnv, err := envLib.ParseSlice(cli.Env)
	if err != nil {
		return 0, errors.Wrap(err, "error parsing environment variables")
	}
	envs := envLib.Slice(cliEnv)

	resize := make(chan remotecommand.TerminalSize, 5)
	haveTerminal := terminal.IsTerminal(int(os.Stdin.Fd()))

	// Check if we are attached to a terminal. If we are, generate resize
	// events, and set the terminal to raw mode
	if haveTerminal && cli.Tty {
		cancel, oldTermState, err := handleTerminalAttach(ctx, resize)
		if err != nil {
			return ec, err
		}
		defer cancel()
		defer restoreTerminal(oldTermState) // nolint: errcheck

		logrus.SetFormatter(&RawTtyFormatter{})
		term.SetRawTerminal(os.Stdin.Fd()) // nolint: errcheck
	}

	opts := iopodman.ExecOpts{
		Name:       cli.InputArgs[0],
		Tty:        cli.Tty,
		Privileged: cli.Privileged,
		Cmd:        cli.InputArgs[1:],
		User:       &cli.User,
		Workdir:    &cli.Workdir,
		Env:        &envs,
		DetachKeys: &cli.DetachKeys,
	}

	inputStream := os.Stdin
	if !cli.Interactive {
		inputStream = nil
	}

	reply, err := iopodman.ExecContainer().Send(r.Conn, varlink.Upgrade, opts)
	if err != nil {
		return ec, errors.Wrapf(err, "Exec failed to contact service for %s", cli.InputArgs)
	}

	_, err = reply()
	if err != nil {
		return ec, errors.Wrapf(err, "Exec operation failed for %s", cli.InputArgs)
	}
	ecChan := make(chan int, 1)
	errChan := configureVarlinkAttachStdio(r.Conn.Reader, r.Conn.Writer, inputStream, os.Stdout, oldTermState, resize, ecChan)

	ec = <-ecChan
	err = <-errChan

	return ec, err
}

func configureVarlinkAttachStdio(reader *bufio.Reader, writer *bufio.Writer, stdin *os.File, stdout *os.File, oldTermState *term.State, resize chan remotecommand.TerminalSize, ecChan chan int) chan error { // nolint: interfacer
	errChan := make(chan error, 1)
	// These are the special writers that encode input from the client.
	varlinkStdinWriter := virtwriter.NewVirtWriteCloser(writer, virtwriter.ToStdin)
	varlinkResizeWriter := virtwriter.NewVirtWriteCloser(writer, virtwriter.TerminalResize)
	varlinkHangupWriter := virtwriter.NewVirtWriteCloser(writer, virtwriter.HangUpFromClient)

	go func() {
		// Read from the wire and direct to stdout or stderr
		err := virtwriter.Reader(reader, stdout, os.Stderr, nil, nil, ecChan)
		defer restoreTerminal(oldTermState) // nolint: errcheck
		sendGenericError(ecChan)
		errChan <- err
	}()

	go func() {
		for termResize := range resize {
			b, err := json.Marshal(termResize)
			if err != nil {
				defer restoreTerminal(oldTermState) // nolint: errcheck,staticcheck
				sendGenericError(ecChan)
				errChan <- err
			}
			_, err = varlinkResizeWriter.Write(b)
			if err != nil {
				defer restoreTerminal(oldTermState) // nolint: errcheck,staticcheck
				sendGenericError(ecChan)
				errChan <- err
			}
		}
	}()
	if stdin != nil {
		// Takes stdinput and sends it over the wire after being encoded
		go func() {
			if _, err := io.Copy(varlinkStdinWriter, stdin); err != nil {
				defer restoreTerminal(oldTermState) // nolint: errcheck
				sendGenericError(ecChan)
				errChan <- err
			}
			_, err := varlinkHangupWriter.Write([]byte("EOF"))
			if err != nil {
				logrus.Errorf("unable to notify server to hangup: %q", err)
			}
			err = varlinkStdinWriter.Close()
			errChan <- err
		}()
	}
	return errChan
}

func sendGenericError(ecChan chan int) {
	if ecChan != nil {
		ecChan <- define.ExecErrorCodeGeneric
	}
}