package libcontainer

import (
	"errors"
	"io"
	"math"
	"os"

	"github.com/opencontainers/runc/libcontainer/configs"
)

var errInvalidProcess = errors.New("invalid process")

type processOperations interface {
	wait() (*os.ProcessState, error)
	signal(sig os.Signal) error
	pid() int
}

// Process defines the configuration and IO for a process inside a container.
//
// Note that some Process properties are also present in container configuration
// ([configs.Config]). In all such cases, Process properties take precedence
// over container configuration ones.
type Process struct {
	// The command to be run followed by any arguments.
	Args []string

	// Env specifies the environment variables for the process.
	Env []string

	// UID and GID of the executing process running inside the container
	// local to the container's user and group configuration.
	UID, GID int

	// AdditionalGroups specifies the gids that should be added to supplementary groups
	// in addition to those that the user belongs to.
	AdditionalGroups []int

	// Cwd will change the process's current working directory inside the container's rootfs.
	Cwd string

	// Stdin is a reader which provides the standard input stream.
	Stdin io.Reader

	// Stdout is a writer which receives the standard output stream.
	Stdout io.Writer

	// Stderr is a writer which receives the standard error stream.
	Stderr io.Writer

	// ExtraFiles specifies additional open files to be inherited by the process.
	ExtraFiles []*os.File

	// Open handles to cloned binaries -- see exeseal.CloneSelfExe for more details.
	clonedExes []*os.File

	// Initial size for the console.
	ConsoleWidth  uint16
	ConsoleHeight uint16

	// Capabilities specify the capabilities to keep when executing the process.
	// All capabilities not specified will be dropped from the processes capability mask.
	//
	// If not nil, takes precedence over container's [configs.Config.Capabilities].
	Capabilities *configs.Capabilities

	// AppArmorProfile specifies the profile to apply to the process and is
	// changed at the time the process is executed.
	//
	// If not empty, takes precedence over container's [configs.Config.AppArmorProfile].
	AppArmorProfile string

	// Label specifies the label to apply to the process. It is commonly used by selinux.
	//
	// If not empty, takes precedence over container's [configs.Config.ProcessLabel].
	Label string

	// NoNewPrivileges controls whether processes can gain additional privileges.
	//
	// If not nil, takes precedence over container's [configs.Config.NoNewPrivileges].
	NoNewPrivileges *bool

	// Rlimits specifies the resource limits, such as max open files, to set for the process.
	// If unset, the process will inherit rlimits from the parent process.
	//
	// If not empty, takes precedence over container's [configs.Config.Rlimit].
	Rlimits []configs.Rlimit

	// ConsoleSocket provides the masterfd console.
	ConsoleSocket *os.File

	// PidfdSocket provides process file descriptor of it own.
	PidfdSocket *os.File

	// Init specifies whether the process is the first process in the container.
	Init bool

	ops processOperations

	// LogLevel is a string containing a numeric representation of the current
	// log level (i.e. "4", but never "info"). It is passed on to runc init as
	// _LIBCONTAINER_LOGLEVEL environment variable.
	LogLevel string

	// SubCgroupPaths specifies sub-cgroups to run the process in.
	// Map keys are controller names, map values are paths (relative to
	// container's top-level cgroup).
	//
	// If empty, the default top-level container's cgroup is used.
	//
	// For cgroup v2, the only key allowed is "".
	SubCgroupPaths map[string]string

	// Scheduler represents the scheduling attributes for a process.
	//
	// If not empty, takes precedence over container's [configs.Config.Scheduler].
	Scheduler *configs.Scheduler

	// IOPriority is a process I/O priority.
	//
	// If not empty, takes precedence over container's [configs.Config.IOPriority].
	IOPriority *configs.IOPriority

	CPUAffinity *configs.CPUAffinity
}

// Wait waits for the process to exit.
// Wait releases any resources associated with the Process
func (p Process) Wait() (*os.ProcessState, error) {
	if p.ops == nil {
		return nil, errInvalidProcess
	}
	return p.ops.wait()
}

// Pid returns the process ID
func (p Process) Pid() (int, error) {
	// math.MinInt32 is returned here, because it's invalid value
	// for the kill() system call.
	if p.ops == nil {
		return math.MinInt32, errInvalidProcess
	}
	return p.ops.pid(), nil
}

// Signal sends a signal to the Process.
func (p Process) Signal(sig os.Signal) error {
	if p.ops == nil {
		return errInvalidProcess
	}
	return p.ops.signal(sig)
}

// closeClonedExes cleans up any existing cloned binaries associated with the
// Process.
func (p *Process) closeClonedExes() {
	for _, exe := range p.clonedExes {
		_ = exe.Close()
	}
	p.clonedExes = nil
}

// IO holds the process's STDIO
type IO struct {
	Stdin  io.WriteCloser
	Stdout io.ReadCloser
	Stderr io.ReadCloser
}
