// Copyright (c) 2016 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//

package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strings"
	"text/tabwriter"

	"github.com/kata-containers/runtime/virtcontainers/pkg/uuid"
	"github.com/kata-containers/runtime/virtcontainers/types"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"

	vc "github.com/kata-containers/runtime/virtcontainers"
)

var virtcLog *logrus.Entry

var listFormat = "%s\t%s\t%s\t%s\n"
var statusFormat = "%s\t%s\n"

var ctx = context.Background()

var (
	errNeedContainerID = errors.New("Container ID cannot be empty")
	errNeedSandboxID   = errors.New("Sandbox ID cannot be empty")
)

var sandboxConfigFlags = []cli.Flag{
	cli.GenericFlag{
		Name:  "agent",
		Value: new(vc.AgentType),
		Usage: "the guest agent",
	},

	cli.StringFlag{
		Name:  "id",
		Value: "",
		Usage: "the sandbox identifier (default: auto-generated)",
	},

	cli.StringFlag{
		Name:  "machine-type",
		Value: vc.QemuPC,
		Usage: "hypervisor machine type",
	},

	cli.GenericFlag{
		Name:  "network",
		Value: new(vc.NetworkModel),
		Usage: "the network model",
	},

	cli.GenericFlag{
		Name:  "proxy",
		Value: new(vc.ProxyType),
		Usage: "the agent's proxy",
	},

	cli.StringFlag{
		Name:  "proxy-path",
		Value: "",
		Usage: "path to proxy binary",
	},

	cli.GenericFlag{
		Name:  "shim",
		Value: new(vc.ShimType),
		Usage: "the shim type",
	},

	cli.StringFlag{
		Name:  "shim-path",
		Value: "",
		Usage: "the shim binary path",
	},

	cli.StringFlag{
		Name:  "hyper-ctl-sock-name",
		Value: "",
		Usage: "the hyperstart control socket name",
	},

	cli.StringFlag{
		Name:  "hyper-tty-sock-name",
		Value: "",
		Usage: "the hyperstart tty socket name",
	},

	cli.UintFlag{
		Name:  "cpus",
		Value: 0,
		Usage: "the number of virtual cpus available for this sandbox",
	},

	cli.UintFlag{
		Name:  "memory",
		Value: 0,
		Usage: "the amount of memory available for this sandbox in MiB",
	},
}

var ccKernelParams = []vc.Param{
	{
		Key:   "init",
		Value: "/usr/lib/systemd/systemd",
	},
	{
		Key:   "systemd.unit",
		Value: "clear-containers.target",
	},
	{
		Key:   "systemd.mask",
		Value: "systemd-networkd.service",
	},
	{
		Key:   "systemd.mask",
		Value: "systemd-networkd.socket",
	},
}

func buildKernelParams(config *vc.HypervisorConfig) error {
	for _, p := range ccKernelParams {
		if err := config.AddKernelParam(p); err != nil {
			return err
		}
	}

	return nil
}

func buildSandboxConfig(context *cli.Context) (vc.SandboxConfig, error) {
	var agConfig interface{}

	hyperCtlSockName := context.String("hyper-ctl-sock-name")
	hyperTtySockName := context.String("hyper-tty-sock-name")
	proxyPath := context.String("proxy-path")
	shimPath := context.String("shim-path")
	machineType := context.String("machine-type")
	vmMemory := context.Uint("vm-memory")
	agentType, ok := context.Generic("agent").(*vc.AgentType)
	if ok != true {
		return vc.SandboxConfig{}, fmt.Errorf("Could not convert agent type")
	}

	networkModel, ok := context.Generic("network").(*vc.NetworkModel)
	if ok != true {
		return vc.SandboxConfig{}, fmt.Errorf("Could not convert network model")
	}

	proxyType, ok := context.Generic("proxy").(*vc.ProxyType)
	if ok != true {
		return vc.SandboxConfig{}, fmt.Errorf("Could not convert proxy type")
	}

	shimType, ok := context.Generic("shim").(*vc.ShimType)
	if ok != true {
		return vc.SandboxConfig{}, fmt.Errorf("Could not convert shim type")
	}

	kernelPath := "/usr/share/clear-containers/vmlinuz.container"
	if machineType == vc.QemuPCLite {
		kernelPath = "/usr/share/clear-containers/vmlinux.container"
	}

	hypervisorConfig := vc.HypervisorConfig{
		KernelPath:            kernelPath,
		ImagePath:             "/usr/share/clear-containers/clear-containers.img",
		HypervisorMachineType: machineType,
		MemorySize:            uint32(vmMemory),
	}

	if err := buildKernelParams(&hypervisorConfig); err != nil {
		return vc.SandboxConfig{}, err
	}

	netConfig := vc.NetworkConfig{}

	switch *agentType {
	case vc.HyperstartAgent:
		agConfig = vc.HyperConfig{
			SockCtlName: hyperCtlSockName,
			SockTtyName: hyperTtySockName,
		}
	default:
		agConfig = nil
	}

	proxyConfig := getProxyConfig(*proxyType, proxyPath)

	shimConfig := getShimConfig(*shimType, shimPath)

	id := context.String("id")
	if id == "" {
		// auto-generate sandbox name
		id = uuid.Generate().String()
	}

	sandboxConfig := vc.SandboxConfig{
		ID: id,

		HypervisorType:   vc.QemuHypervisor,
		HypervisorConfig: hypervisorConfig,

		AgentType:   *agentType,
		AgentConfig: agConfig,

		NetworkModel:  *networkModel,
		NetworkConfig: netConfig,

		ProxyType:   *proxyType,
		ProxyConfig: proxyConfig,

		ShimType:   *shimType,
		ShimConfig: shimConfig,

		Containers: []vc.ContainerConfig{},
	}

	return sandboxConfig, nil
}

func getProxyConfig(proxyType vc.ProxyType, path string) vc.ProxyConfig {
	var proxyConfig vc.ProxyConfig

	switch proxyType {
	case vc.KataProxyType:
		fallthrough
	case vc.CCProxyType:
		proxyConfig = vc.ProxyConfig{
			Path: path,
		}
	}

	return proxyConfig
}

func getShimConfig(shimType vc.ShimType, path string) interface{} {
	var shimConfig interface{}

	switch shimType {
	case vc.CCShimType, vc.KataShimType:
		shimConfig = vc.ShimConfig{
			Path: path,
		}

	default:
		shimConfig = nil
	}

	return shimConfig
}

// checkRequiredSandboxArgs checks to ensure the required command-line
// arguments have been specified for the sandbox sub-command specified by
// the context argument.
func checkRequiredSandboxArgs(context *cli.Context) error {
	if context == nil {
		return fmt.Errorf("BUG: need Context")
	}

	// sub-sub-command name
	name := context.Command.Name

	switch name {
	case "create":
		fallthrough
	case "list":
		fallthrough
	case "run":
		// these commands don't require any arguments
		return nil
	}

	id := context.String("id")
	if id == "" {
		return errNeedSandboxID
	}

	return nil
}

// checkRequiredContainerArgs checks to ensure the required command-line
// arguments have been specified for the container sub-command specified
// by the context argument.
func checkRequiredContainerArgs(context *cli.Context) error {
	if context == nil {
		return fmt.Errorf("BUG: need Context")
	}

	// sub-sub-command name
	name := context.Command.Name

	sandboxID := context.String("sandbox-id")
	if sandboxID == "" {
		return errNeedSandboxID
	}

	rootfs := context.String("rootfs")
	if name == "create" && rootfs == "" {
		return fmt.Errorf("%s: need rootfs", name)
	}

	id := context.String("id")
	if id == "" {
		return errNeedContainerID
	}

	return nil
}

func runSandbox(context *cli.Context) error {
	sandboxConfig, err := buildSandboxConfig(context)
	if err != nil {
		return fmt.Errorf("Could not build sandbox config: %s", err)
	}

	_, err = vc.RunSandbox(ctx, sandboxConfig, nil)
	if err != nil {
		return fmt.Errorf("Could not run sandbox: %s", err)
	}

	return nil
}

func createSandbox(context *cli.Context) error {
	sandboxConfig, err := buildSandboxConfig(context)
	if err != nil {
		return fmt.Errorf("Could not build sandbox config: %s", err)
	}

	p, err := vc.CreateSandbox(ctx, sandboxConfig, nil)
	if err != nil {
		return fmt.Errorf("Could not create sandbox: %s", err)
	}

	fmt.Printf("Sandbox %s created\n", p.ID())

	return nil
}

func checkSandboxArgs(context *cli.Context, f func(context *cli.Context) error) error {
	if err := checkRequiredSandboxArgs(context); err != nil {
		return err
	}

	return f(context)
}

func checkContainerArgs(context *cli.Context, f func(context *cli.Context) error) error {
	if err := checkRequiredContainerArgs(context); err != nil {
		return err
	}

	return f(context)
}

func deleteSandbox(context *cli.Context) error {
	p, err := vc.DeleteSandbox(ctx, context.String("id"))
	if err != nil {
		return fmt.Errorf("Could not delete sandbox: %s", err)
	}

	fmt.Printf("Sandbox %s deleted\n", p.ID())

	return nil
}

func startSandbox(context *cli.Context) error {
	p, err := vc.StartSandbox(ctx, context.String("id"))
	if err != nil {
		return fmt.Errorf("Could not start sandbox: %s", err)
	}

	fmt.Printf("Sandbox %s started\n", p.ID())

	return nil
}

func stopSandbox(context *cli.Context) error {
	p, err := vc.StopSandbox(ctx, context.String("id"))
	if err != nil {
		return fmt.Errorf("Could not stop sandbox: %s", err)
	}

	fmt.Printf("Sandbox %s stopped\n", p.ID())

	return nil
}

func pauseSandbox(context *cli.Context) error {
	p, err := vc.PauseSandbox(ctx, context.String("id"))
	if err != nil {
		return fmt.Errorf("Could not pause sandbox: %s", err)
	}

	fmt.Printf("Sandbox %s paused\n", p.ID())

	return nil
}

func resumeSandbox(context *cli.Context) error {
	p, err := vc.ResumeSandbox(ctx, context.String("id"))
	if err != nil {
		return fmt.Errorf("Could not resume sandbox: %s", err)
	}

	fmt.Printf("Sandbox %s resumed\n", p.ID())

	return nil
}

func listSandboxes(context *cli.Context) error {
	sandboxStatusList, err := vc.ListSandbox(ctx)
	if err != nil {
		return fmt.Errorf("Could not list sandbox: %s", err)
	}

	w := tabwriter.NewWriter(os.Stdout, 2, 8, 1, '\t', 0)
	fmt.Fprintf(w, listFormat, "SB ID", "STATE", "HYPERVISOR", "AGENT")

	for _, sandboxStatus := range sandboxStatusList {
		fmt.Fprintf(w, listFormat,
			sandboxStatus.ID, sandboxStatus.State.State, sandboxStatus.Hypervisor, sandboxStatus.Agent)
	}

	w.Flush()

	return nil
}

func statusSandbox(context *cli.Context) error {
	sandboxStatus, err := vc.StatusSandbox(ctx, context.String("id"))
	if err != nil {
		return fmt.Errorf("Could not get sandbox status: %s", err)
	}

	w := tabwriter.NewWriter(os.Stdout, 2, 8, 1, '\t', 0)
	fmt.Fprintf(w, listFormat, "SB ID", "STATE", "HYPERVISOR", "AGENT")

	fmt.Fprintf(w, listFormat+"\n",
		sandboxStatus.ID, sandboxStatus.State.State, sandboxStatus.Hypervisor, sandboxStatus.Agent)

	fmt.Fprintf(w, statusFormat, "CONTAINER ID", "STATE")

	for _, contStatus := range sandboxStatus.ContainersStatus {
		fmt.Fprintf(w, statusFormat, contStatus.ID, contStatus.State.State)
	}

	w.Flush()

	return nil
}

var runSandboxCommand = cli.Command{
	Name:  "run",
	Usage: "run a sandbox",
	Flags: sandboxConfigFlags,
	Action: func(context *cli.Context) error {
		return checkSandboxArgs(context, runSandbox)
	},
}

var createSandboxCommand = cli.Command{
	Name:  "create",
	Usage: "create a sandbox",
	Flags: sandboxConfigFlags,
	Action: func(context *cli.Context) error {
		return checkSandboxArgs(context, createSandbox)
	},
}

var deleteSandboxCommand = cli.Command{
	Name:  "delete",
	Usage: "delete an existing sandbox",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "id",
			Value: "",
			Usage: "the sandbox identifier",
		},
	},
	Action: func(context *cli.Context) error {
		return checkSandboxArgs(context, deleteSandbox)
	},
}

var startSandboxCommand = cli.Command{
	Name:  "start",
	Usage: "start an existing sandbox",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "id",
			Value: "",
			Usage: "the sandbox identifier",
		},
	},
	Action: func(context *cli.Context) error {
		return checkSandboxArgs(context, startSandbox)
	},
}

var stopSandboxCommand = cli.Command{
	Name:  "stop",
	Usage: "stop an existing sandbox",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "id",
			Value: "",
			Usage: "the sandbox identifier",
		},
	},
	Action: func(context *cli.Context) error {
		return checkSandboxArgs(context, stopSandbox)
	},
}

var listSandboxesCommand = cli.Command{
	Name:  "list",
	Usage: "list all existing sandboxes",
	Action: func(context *cli.Context) error {
		return checkSandboxArgs(context, listSandboxes)
	},
}

var statusSandboxCommand = cli.Command{
	Name:  "status",
	Usage: "returns a detailed sandbox status",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "id",
			Value: "",
			Usage: "the sandbox identifier",
		},
	},
	Action: func(context *cli.Context) error {
		return checkSandboxArgs(context, statusSandbox)
	},
}

var pauseSandboxCommand = cli.Command{
	Name:  "pause",
	Usage: "pause an existing sandbox",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "id",
			Value: "",
			Usage: "the sandbox identifier",
		},
	},
	Action: func(context *cli.Context) error {
		return checkSandboxArgs(context, pauseSandbox)
	},
}

var resumeSandboxCommand = cli.Command{
	Name:  "resume",
	Usage: "unpause a paused sandbox",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "id",
			Value: "",
			Usage: "the sandbox identifier",
		},
	},
	Action: func(context *cli.Context) error {
		return checkSandboxArgs(context, resumeSandbox)
	},
}

func createContainer(context *cli.Context) error {
	console := context.String("console")

	interactive := false
	if console != "" {
		interactive = true
	}

	envs := []types.EnvVar{
		{
			Var:   "PATH",
			Value: "/bin:/usr/bin:/sbin:/usr/sbin",
		},
	}

	cmd := types.Cmd{
		Args:        strings.Split(context.String("cmd"), " "),
		Envs:        envs,
		WorkDir:     "/",
		Interactive: interactive,
		Console:     console,
	}

	id := context.String("id")
	if id == "" {
		// auto-generate container name
		id = uuid.Generate().String()
	}

	containerConfig := vc.ContainerConfig{
		ID:     id,
		RootFs: context.String("rootfs"),
		Cmd:    cmd,
	}

	_, c, err := vc.CreateContainer(ctx, context.String("sandbox-id"), containerConfig)
	if err != nil {
		return fmt.Errorf("Could not create container: %s", err)
	}

	fmt.Printf("Container %s created\n", c.ID())

	return nil
}

func deleteContainer(context *cli.Context) error {
	c, err := vc.DeleteContainer(ctx, context.String("sandbox-id"), context.String("id"))
	if err != nil {
		return fmt.Errorf("Could not delete container: %s", err)
	}

	fmt.Printf("Container %s deleted\n", c.ID())

	return nil
}

func startContainer(context *cli.Context) error {
	c, err := vc.StartContainer(ctx, context.String("sandbox-id"), context.String("id"))
	if err != nil {
		return fmt.Errorf("Could not start container: %s", err)
	}

	fmt.Printf("Container %s started\n", c.ID())

	return nil
}

func stopContainer(context *cli.Context) error {
	c, err := vc.StopContainer(ctx, context.String("sandbox-id"), context.String("id"))
	if err != nil {
		return fmt.Errorf("Could not stop container: %s", err)
	}

	fmt.Printf("Container %s stopped\n", c.ID())

	return nil
}

func enterContainer(context *cli.Context) error {
	console := context.String("console")

	interactive := false
	if console != "" {
		interactive = true
	}

	envs := []types.EnvVar{
		{
			Var:   "PATH",
			Value: "/bin:/usr/bin:/sbin:/usr/sbin",
		},
	}

	cmd := types.Cmd{
		Args:        strings.Split(context.String("cmd"), " "),
		Envs:        envs,
		WorkDir:     "/",
		Interactive: interactive,
		Console:     console,
	}

	_, c, _, err := vc.EnterContainer(ctx, context.String("sandbox-id"), context.String("id"), cmd)
	if err != nil {
		return fmt.Errorf("Could not enter container: %s", err)
	}

	fmt.Printf("Container %s entered\n", c.ID())

	return nil
}

func statusContainer(context *cli.Context) error {
	contStatus, err := vc.StatusContainer(ctx, context.String("sandbox-id"), context.String("id"))
	if err != nil {
		return fmt.Errorf("Could not get container status: %s", err)
	}

	w := tabwriter.NewWriter(os.Stdout, 2, 8, 1, '\t', 0)
	fmt.Fprintf(w, statusFormat, "CONTAINER ID", "STATE")
	fmt.Fprintf(w, statusFormat, contStatus.ID, contStatus.State.State)

	w.Flush()

	return nil
}

var createContainerCommand = cli.Command{
	Name:  "create",
	Usage: "create a container",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "id",
			Value: "",
			Usage: "the container identifier (default: auto-generated)",
		},
		cli.StringFlag{
			Name:  "sandbox-id",
			Value: "",
			Usage: "the sandbox identifier",
		},
		cli.StringFlag{
			Name:  "rootfs",
			Value: "",
			Usage: "the container rootfs directory",
		},
		cli.StringFlag{
			Name:  "cmd",
			Value: "",
			Usage: "the command executed inside the container",
		},
		cli.StringFlag{
			Name:  "console",
			Value: "",
			Usage: "the container console",
		},
	},
	Action: func(context *cli.Context) error {
		return checkContainerArgs(context, createContainer)
	},
}

var deleteContainerCommand = cli.Command{
	Name:  "delete",
	Usage: "delete an existing container",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "id",
			Value: "",
			Usage: "the container identifier",
		},
		cli.StringFlag{
			Name:  "sandbox-id",
			Value: "",
			Usage: "the sandbox identifier",
		},
	},
	Action: func(context *cli.Context) error {
		return checkContainerArgs(context, deleteContainer)
	},
}

var startContainerCommand = cli.Command{
	Name:  "start",
	Usage: "start an existing container",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "id",
			Value: "",
			Usage: "the container identifier",
		},
		cli.StringFlag{
			Name:  "sandbox-id",
			Value: "",
			Usage: "the sandbox identifier",
		},
	},
	Action: func(context *cli.Context) error {
		return checkContainerArgs(context, startContainer)
	},
}

var stopContainerCommand = cli.Command{
	Name:  "stop",
	Usage: "stop an existing container",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "id",
			Value: "",
			Usage: "the container identifier",
		},
		cli.StringFlag{
			Name:  "sandbox-id",
			Value: "",
			Usage: "the sandbox identifier",
		},
	},
	Action: func(context *cli.Context) error {
		return checkContainerArgs(context, stopContainer)
	},
}

var enterContainerCommand = cli.Command{
	Name:  "enter",
	Usage: "enter an existing container",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "id",
			Value: "",
			Usage: "the container identifier",
		},
		cli.StringFlag{
			Name:  "sandbox-id",
			Value: "",
			Usage: "the sandbox identifier",
		},
		cli.StringFlag{
			Name:  "cmd",
			Value: "echo",
			Usage: "the command executed inside the container",
		},
		cli.StringFlag{
			Name:  "console",
			Value: "",
			Usage: "the process console",
		},
	},
	Action: func(context *cli.Context) error {
		return checkContainerArgs(context, enterContainer)
	},
}

var statusContainerCommand = cli.Command{
	Name:  "status",
	Usage: "returns detailed container status",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "id",
			Value: "",
			Usage: "the container identifier",
		},
		cli.StringFlag{
			Name:  "sandbox-id",
			Value: "",
			Usage: "the sandbox identifier",
		},
	},
	Action: func(context *cli.Context) error {
		return checkContainerArgs(context, statusContainer)
	},
}

func main() {
	cli.VersionFlag = cli.BoolFlag{
		Name:  "version",
		Usage: "print the version",
	}

	virtc := cli.NewApp()
	virtc.Name = "VirtContainers CLI"
	virtc.Version = "0.0.1"

	virtc.Flags = []cli.Flag{
		cli.BoolFlag{
			Name:  "debug",
			Usage: "enable debug output for logging",
		},
		cli.StringFlag{
			Name:  "log",
			Value: "",
			Usage: "set the log file path where internal debug information is written",
		},
		cli.StringFlag{
			Name:  "log-format",
			Value: "text",
			Usage: "set the format used by logs ('text' (default), or 'json')",
		},
	}

	virtc.Commands = []cli.Command{
		{
			Name:  "sandbox",
			Usage: "sandbox commands",
			Subcommands: []cli.Command{
				createSandboxCommand,
				deleteSandboxCommand,
				listSandboxesCommand,
				pauseSandboxCommand,
				resumeSandboxCommand,
				runSandboxCommand,
				startSandboxCommand,
				stopSandboxCommand,
				statusSandboxCommand,
			},
		},
		{
			Name:  "container",
			Usage: "container commands",
			Subcommands: []cli.Command{
				createContainerCommand,
				deleteContainerCommand,
				startContainerCommand,
				stopContainerCommand,
				enterContainerCommand,
				statusContainerCommand,
			},
		},
	}

	virtc.Before = func(context *cli.Context) error {
		if context.GlobalBool("debug") {
			virtcLog.Level = logrus.DebugLevel
		}

		if path := context.GlobalString("log"); path != "" {
			f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0640)
			if err != nil {
				return err
			}
			virtcLog.Logger.Out = f
		}

		switch context.GlobalString("log-format") {
		case "text":
			// retain logrus's default.
		case "json":
			virtcLog.Logger.Formatter = new(logrus.JSONFormatter)
		default:
			return fmt.Errorf("unknown log-format %q", context.GlobalString("log-format"))
		}

		// Set virtcontainers logger.
		vc.SetLogger(ctx, virtcLog)

		return nil
	}

	err := virtc.Run(os.Args)
	if err != nil {
		virtcLog.Fatal(err)
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
