// +build !windows

package shim

import (
	"fmt"
	"io/ioutil"
	"net"

	gocontext "context"

	"github.com/containerd/console"
	"github.com/containerd/containerd/cmd/ctr/commands"
	shim "github.com/containerd/containerd/linux/shim/v1"
	"github.com/containerd/typeurl"
	ptypes "github.com/gogo/protobuf/types"
	"github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/stevvooe/ttrpc"
	"github.com/urfave/cli"
)

var empty = &ptypes.Empty{}

var fifoFlags = []cli.Flag{
	cli.StringFlag{
		Name:  "stdin",
		Usage: "specify the path to the stdin fifo",
	},
	cli.StringFlag{
		Name:  "stdout",
		Usage: "specify the path to the stdout fifo",
	},
	cli.StringFlag{
		Name:  "stderr",
		Usage: "specify the path to the stderr fifo",
	},
	cli.BoolFlag{
		Name:  "tty,t",
		Usage: "enable tty support",
	},
}

// Command is the cli command for interacting with a shim
var Command = cli.Command{
	Name:  "shim",
	Usage: "interact with a shim directly",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "socket",
			Usage: "socket on which to connect to the shim",
		},
	},
	Subcommands: []cli.Command{
		deleteCommand,
		execCommand,
		startCommand,
		stateCommand,
	},
}

var startCommand = cli.Command{
	Name:  "start",
	Usage: "start a container with a shim",
	Action: func(context *cli.Context) error {
		service, err := getShimService(context)
		if err != nil {
			return err
		}
		_, err = service.Start(gocontext.Background(), &shim.StartRequest{
			ID: context.Args().First(),
		})
		return err
	},
}

var deleteCommand = cli.Command{
	Name:  "delete",
	Usage: "delete a container with a shim",
	Action: func(context *cli.Context) error {
		service, err := getShimService(context)
		if err != nil {
			return err
		}
		r, err := service.Delete(gocontext.Background(), empty)
		if err != nil {
			return err
		}
		fmt.Printf("container deleted and returned exit status %d\n", r.ExitStatus)
		return nil
	},
}

var stateCommand = cli.Command{
	Name:  "state",
	Usage: "get the state of all the processes of the shim",
	Action: func(context *cli.Context) error {
		service, err := getShimService(context)
		if err != nil {
			return err
		}
		r, err := service.State(gocontext.Background(), &shim.StateRequest{
			ID: context.Args().First(),
		})
		if err != nil {
			return err
		}
		commands.PrintAsJSON(r)
		return nil
	},
}

var execCommand = cli.Command{
	Name:  "exec",
	Usage: "exec a new process in the shim's container",
	Flags: append(fifoFlags,
		cli.BoolFlag{
			Name:  "attach,a",
			Usage: "stay attached to the container and open the fifos",
		},
		cli.StringSliceFlag{
			Name:  "env,e",
			Usage: "add environment vars",
			Value: &cli.StringSlice{},
		},
		cli.StringFlag{
			Name:  "cwd",
			Usage: "current working directory",
		},
		cli.StringFlag{
			Name:  "spec",
			Usage: "runtime spec",
		},
	),
	Action: func(context *cli.Context) error {
		service, err := getShimService(context)
		if err != nil {
			return err
		}
		var (
			id  = context.Args().First()
			ctx = gocontext.Background()
		)

		if id == "" {
			return errors.New("exec id must be provided")
		}

		tty := context.Bool("tty")
		wg, err := prepareStdio(context.String("stdin"), context.String("stdout"), context.String("stderr"), tty)
		if err != nil {
			return err
		}

		// read spec file and extract Any object
		spec, err := ioutil.ReadFile(context.String("spec"))
		if err != nil {
			return err
		}
		url, err := typeurl.TypeURL(specs.Process{})
		if err != nil {
			return err
		}

		rq := &shim.ExecProcessRequest{
			ID: id,
			Spec: &ptypes.Any{
				TypeUrl: url,
				Value:   spec,
			},
			Stdin:    context.String("stdin"),
			Stdout:   context.String("stdout"),
			Stderr:   context.String("stderr"),
			Terminal: tty,
		}
		if _, err := service.Exec(ctx, rq); err != nil {
			return err
		}
		r, err := service.Start(ctx, &shim.StartRequest{
			ID: id,
		})
		if err != nil {
			return err
		}
		fmt.Printf("exec running with pid %d\n", r.Pid)
		if context.Bool("attach") {
			logrus.Info("attaching")
			if tty {
				current := console.Current()
				defer current.Reset()
				if err := current.SetRaw(); err != nil {
					return err
				}
				size, err := current.Size()
				if err != nil {
					return err
				}
				if _, err := service.ResizePty(ctx, &shim.ResizePtyRequest{
					ID:     id,
					Width:  uint32(size.Width),
					Height: uint32(size.Height),
				}); err != nil {
					return err
				}
			}
			wg.Wait()
		}
		return nil
	},
}

func getShimService(context *cli.Context) (shim.ShimService, error) {
	bindSocket := context.GlobalString("socket")
	if bindSocket == "" {
		return nil, errors.New("socket path must be specified")
	}

	conn, err := net.Dial("unix", "\x00"+bindSocket)
	if err != nil {
		return nil, err
	}

	client := ttrpc.NewClient(conn)

	// TODO(stevvooe): This actually leaks the connection. We were leaking it
	// before, so may not be a huge deal.

	return shim.NewShimClient(client), nil
}
