//go:build linux
// +build linux

package runc

import (
	"encoding/json"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"

	oci "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
	"golang.org/x/sys/unix"

	"github.com/Microsoft/hcsshim/internal/guest/commonutils"
	"github.com/Microsoft/hcsshim/internal/guest/runtime"
	"github.com/Microsoft/hcsshim/internal/guest/stdio"
)

const (
	containerFilesDir = "/var/run/gcsrunc"
	initPidFilename   = "initpid"
)

func setSubreaper(i int) error {
	return unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, uintptr(i), 0, 0, 0)
}

// NewRuntime instantiates a new runcRuntime struct.
func NewRuntime(logBasePath string) (runtime.Runtime, error) {
	rtime := &runcRuntime{runcLogBasePath: logBasePath}
	if err := rtime.initialize(); err != nil {
		return nil, err
	}
	return rtime, nil
}

// runcRuntime is an implementation of the Runtime interface which uses runC as
// the container runtime.
type runcRuntime struct {
	runcLogBasePath string
}

var _ runtime.Runtime = &runcRuntime{}

// initialize sets up any state necessary for the runcRuntime to function.
func (r *runcRuntime) initialize() error {
	paths := [2]string{containerFilesDir, r.runcLogBasePath}
	for _, p := range paths {
		_, err := os.Stat(p)
		if err != nil {
			if !os.IsNotExist(err) {
				return err
			}
			if err := os.MkdirAll(p, 0700); err != nil {
				return errors.Wrapf(err, "failed making runC container files directory %s", p)
			}
		}
	}

	return nil
}

// CreateContainer creates a container with the given ID and the given
// bundlePath.
// bundlePath should be a path to an OCI bundle containing a config.json file
// and a rootfs for the container.
func (r *runcRuntime) CreateContainer(id string, bundlePath string, stdioSet *stdio.ConnectionSet) (c runtime.Container, err error) {
	c, err = r.runCreateCommand(id, bundlePath, stdioSet)
	if err != nil {
		return nil, err
	}
	return c, nil
}

// ListContainerStates returns ContainerState structs for all existing
// containers, whether they're running or not.
func (*runcRuntime) ListContainerStates() ([]runtime.ContainerState, error) {
	cmd := runcCommand("list", "-f", "json")
	out, err := cmd.CombinedOutput()
	if err != nil {
		runcErr := parseRuncError(string(out))
		return nil, errors.Wrapf(runcErr, "runc list failed with %v: %s", err, string(out))
	}
	var states []runtime.ContainerState
	if err := json.Unmarshal(out, &states); err != nil {
		return nil, errors.Wrap(err, "failed to unmarshal the states for the container list")
	}
	return states, nil
}

// getRunningPids gets the pids of all processes which runC recognizes as
// running.
func (*runcRuntime) getRunningPids(id string) ([]int, error) {
	cmd := runcCommand("ps", "-f", "json", id)
	out, err := cmd.CombinedOutput()
	if err != nil {
		runcErr := parseRuncError(string(out))
		return nil, errors.Wrapf(runcErr, "runc ps failed with %v: %s", err, string(out))
	}
	var pids []int
	if err := json.Unmarshal(out, &pids); err != nil {
		return nil, errors.Wrapf(err, "failed to unmarshal pids for container %s", id)
	}
	return pids, nil
}

// getProcessCommand gets the command line command and arguments for the process
// with the given pid.
func (*runcRuntime) getProcessCommand(pid int) ([]string, error) {
	// Get the contents of the process's cmdline file. This file is formatted
	// with a null character after every argument. e.g. "ping google.com "
	data, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "cmdline"))
	if err != nil {
		return nil, errors.Wrapf(err, "failed to read cmdline file for process %d", pid)
	}
	// Get rid of the \0 character at end.
	cmdString := strings.TrimSuffix(string(data), "\x00")
	return strings.Split(cmdString, "\x00"), nil
}

// pidMapToProcessStates is a helper function which converts a map from pid to
// ContainerProcessState to a slice of ContainerProcessStates.
func (*runcRuntime) pidMapToProcessStates(pidMap map[int]*runtime.ContainerProcessState) []runtime.ContainerProcessState {
	processStates := make([]runtime.ContainerProcessState, len(pidMap))
	i := 0
	for _, processState := range pidMap {
		processStates[i] = *processState
		i++
	}
	return processStates
}

// waitOnProcess waits for the process to exit, and returns its exit code.
func (r *runcRuntime) waitOnProcess(pid int) (int, error) {
	process, err := os.FindProcess(pid)
	if err != nil {
		return -1, errors.Wrapf(err, "failed to find process %d", pid)
	}
	state, err := process.Wait()
	if err != nil {
		return -1, errors.Wrapf(err, "failed waiting on process %d", pid)
	}

	status := state.Sys().(syscall.WaitStatus)
	if status.Signaled() {
		return 128 + int(status.Signal()), nil
	}
	return status.ExitStatus(), nil
}

// runCreateCommand sets up the arguments for calling runc create.
func (r *runcRuntime) runCreateCommand(id string, bundlePath string, stdioSet *stdio.ConnectionSet) (runtime.Container, error) {
	c := &container{r: r, id: id}
	if err := r.makeContainerDir(id); err != nil {
		return nil, err
	}
	// Create a temporary random directory to store the process's files.
	tempProcessDir, err := os.MkdirTemp(containerFilesDir, id)
	if err != nil {
		return nil, err
	}

	spec, err := ociSpecFromBundle(bundlePath)
	if err != nil {
		return nil, err
	}

	// Determine if the container owns its own pid namespace or not. Per the OCI
	// spec:
	// - If the spec has no entry for the pid namespace, the container inherits
	//   the runtime namespace (container does not own).
	// - If the spec has a pid namespace entry, but the path is empty, a new
	//   namespace will be created and used for the container (container owns).
	// - If there is a pid namespace entry with a path, the container uses the
	//   namespace at that path (container does not own).
	if spec.Linux != nil {
		for _, ns := range spec.Linux.Namespaces {
			if ns.Type == oci.PIDNamespace {
				c.ownsPidNamespace = ns.Path == ""
			}
		}
	}

	if spec.Process.Cwd != "/" {
		cwd := path.Join(bundlePath, "rootfs", spec.Process.Cwd)
		// Intentionally ignore the error.
		_ = os.MkdirAll(cwd, 0755)
	}

	args := []string{"create", "-b", bundlePath, "--no-pivot"}

	p, err := c.startProcess(tempProcessDir, spec.Process.Terminal, stdioSet, spec.Annotations, args...)
	if err != nil {
		return nil, err
	}

	// Write pid to initpid file for container.
	containerDir := r.getContainerDir(id)
	if err := os.WriteFile(filepath.Join(containerDir, initPidFilename), []byte(strconv.Itoa(p.pid)), 0777); err != nil {
		return nil, err
	}

	c.init = p
	return c, nil
}

func ociSpecFromBundle(bundlePath string) (*oci.Spec, error) {
	configPath := filepath.Join(bundlePath, "config.json")
	configFile, err := os.Open(configPath)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to open bundle config at %s", configPath)
	}
	defer configFile.Close()
	var spec *oci.Spec
	if err := commonutils.DecodeJSONWithHresult(configFile, &spec); err != nil {
		return nil, errors.Wrap(err, "failed to parse OCI spec")
	}
	return spec, nil
}
