package generate

import (
	"bytes"
	"fmt"
	"os"
	"sort"
	"strings"
	"text/template"
	"time"

	"github.com/containers/podman/v4/libpod"
	libpodDefine "github.com/containers/podman/v4/libpod/define"
	"github.com/containers/podman/v4/pkg/domain/entities"
	"github.com/containers/podman/v4/pkg/systemd/define"
	"github.com/containers/podman/v4/version"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/spf13/pflag"
)

// containerInfo contains data required for generating a container's systemd
// unit file.
type containerInfo struct {
	// ServiceName of the systemd service.
	ServiceName string
	// Name or ID of the container.
	ContainerNameOrID string
	// Type of the unit.
	Type string
	// NotifyAccess of the unit.
	NotifyAccess string
	// StopTimeout sets the timeout Podman waits before killing the container
	// during service stop.
	StopTimeout uint
	// RestartPolicy of the systemd unit (e.g., no, on-failure, always).
	RestartPolicy string
	// Custom number of restart attempts.
	StartLimitBurst string
	// PIDFile of the service. Required for forking services. Must point to the
	// PID of the associated conmon process.
	PIDFile string
	// ContainerIDFile to be used in the unit.
	ContainerIDFile string
	// GenerateTimestamp, if set the generated unit file has a time stamp.
	GenerateTimestamp bool
	// BoundToServices are the services this service binds to.  Note that this
	// service runs after them.
	BoundToServices []string
	// PodmanVersion for the header. Will be set internally. Will be auto-filled
	// if left empty.
	PodmanVersion string
	// Executable is the path to the podman executable. Will be auto-filled if
	// left empty.
	Executable string
	// RootFlags contains the root flags which were used to create the container
	// Only used with --new
	RootFlags string
	// TimeStamp at the time of creating the unit file. Will be set internally.
	TimeStamp string
	// CreateCommand is the full command plus arguments of the process the
	// container has been created with.
	CreateCommand []string
	// containerEnv stores the container environment variables
	containerEnv []string
	// ExtraEnvs contains the container environment variables referenced
	// by only the key in the container create command, e.g. --env FOO.
	// This is only used with --new
	ExtraEnvs []string
	// EnvVariable is generate.EnvVariable and must not be set.
	EnvVariable string
	// ExecStartPre of the unit.
	ExecStartPre string
	// ExecStart of the unit.
	ExecStart string
	// TimeoutStartSec of the unit.
	TimeoutStartSec uint
	// TimeoutStopSec of the unit.
	TimeoutStopSec uint
	// ExecStop of the unit.
	ExecStop string
	// ExecStopPost of the unit.
	ExecStopPost string
	// Removes autogenerated by Podman and timestamp if set to true
	GenerateNoHeader bool
	// If not nil, the container is part of the pod.  We can use the
	// podInfo to extract the relevant data.
	Pod *podInfo
	// Location of the GraphRoot for the container.  Required for ensuring the
	// volume has finished mounting when coming online at boot.
	GraphRoot string
	// Location of the RunRoot for the container.  Required for ensuring the tmpfs
	// or volume exists and is mounted when coming online at boot.
	RunRoot string
	// Add %i and %I to description and execute parts
	IdentifySpecifier bool
	// Wants are the list of services that this service is (weak) dependent on. This
	// option does not influence the order in which services are started or stopped.
	Wants []string
	// After ordering dependencies between the list of services and this service.
	After []string
	// Similar to Wants, but declares a stronger requirement dependency.
	Requires []string
}

const containerTemplate = headerTemplate + `
{{{{- if .BoundToServices}}}}
BindsTo={{{{- range $index, $value := .BoundToServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}}
After={{{{- range $index, $value := .BoundToServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}}
{{{{- end}}}}
{{{{- if or .Wants .After .Requires }}}}

# User-defined dependencies
{{{{- end}}}}
{{{{- if .Wants}}}}
Wants={{{{- range $index, $value := .Wants }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
{{{{- end}}}}
{{{{- if .After}}}}
After={{{{- range $index, $value := .After }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
{{{{- end}}}}
{{{{- if .Requires}}}}
Requires={{{{- range $index, $value := .Requires }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
{{{{- end}}}}

[Service]
Environment={{{{.EnvVariable}}}}=%n{{{{- if (eq .IdentifySpecifier true) }}}}-%i{{{{- end}}}}
{{{{- if .ExtraEnvs}}}}
Environment={{{{- range $index, $value := .ExtraEnvs -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
{{{{- end}}}}
Restart={{{{.RestartPolicy}}}}
{{{{- if .StartLimitBurst}}}}
StartLimitBurst={{{{.StartLimitBurst}}}}
{{{{- end}}}}
{{{{- if ne .TimeoutStartSec 0}}}}
TimeoutStartSec={{{{.TimeoutStartSec}}}}
{{{{- end}}}}
TimeoutStopSec={{{{.TimeoutStopSec}}}}
{{{{- if .ExecStartPre}}}}
ExecStartPre={{{{.ExecStartPre}}}}
{{{{- end}}}}
ExecStart={{{{.ExecStart}}}}
{{{{- if .ExecStop}}}}
ExecStop={{{{.ExecStop}}}}
{{{{- end}}}}
{{{{- if .ExecStopPost}}}}
ExecStopPost={{{{.ExecStopPost}}}}
{{{{- end}}}}
{{{{- if .PIDFile}}}}
PIDFile={{{{.PIDFile}}}}
{{{{- end}}}}
Type={{{{.Type}}}}
{{{{- if .NotifyAccess}}}}
NotifyAccess={{{{.NotifyAccess}}}}
{{{{- end}}}}

[Install]
WantedBy=default.target
`

// ContainerUnit generates a systemd unit for the specified container.  Based
// on the options, the return value might be the entire unit or a file it has
// been written to.
func ContainerUnit(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string, error) {
	info, err := generateContainerInfo(ctr, options)
	if err != nil {
		return "", "", err
	}
	content, err := executeContainerTemplate(info, options)
	if err != nil {
		return "", "", err
	}
	return info.ServiceName, content, nil
}

func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSystemdOptions) (*containerInfo, error) {
	stopTimeout := ctr.StopTimeout()
	if options.StopTimeout != nil {
		stopTimeout = *options.StopTimeout
	}

	startTimeout := uint(0)
	if options.StartTimeout != nil {
		startTimeout = *options.StartTimeout
	}

	config := ctr.Config()
	conmonPidFile := config.ConmonPidFile
	if conmonPidFile == "" {
		return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag")
	}

	createCommand := []string{}
	if config.CreateCommand != nil {
		createCommand = config.CreateCommand
	} else if options.New {
		return nil, errors.Errorf("cannot use --new on container %q: no create command found: only works on containers created directly with podman but not via REST API", ctr.ID())
	}

	nameOrID, serviceName := containerServiceName(ctr, options)

	var runRoot string
	if options.New {
		runRoot = "%t/containers"
	} else {
		runRoot = ctr.Runtime().RunRoot()
		if runRoot == "" {
			return nil, errors.Errorf("could not lookup container's runroot: got empty string")
		}
	}

	envs := config.Spec.Process.Env

	info := containerInfo{
		ServiceName:       serviceName,
		ContainerNameOrID: nameOrID,
		RestartPolicy:     define.DefaultRestartPolicy,
		PIDFile:           conmonPidFile,
		TimeoutStartSec:   startTimeout,
		StopTimeout:       stopTimeout,
		GenerateTimestamp: true,
		CreateCommand:     createCommand,
		RunRoot:           runRoot,
		containerEnv:      envs,
		Wants:             options.Wants,
		After:             options.After,
		Requires:          options.Requires,
	}

	return &info, nil
}

// containerServiceName returns the nameOrID and the service name of the
// container.
func containerServiceName(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string) {
	nameOrID := ctr.ID()
	if options.Name {
		nameOrID = ctr.Name()
	}

	serviceName := getServiceName(options.ContainerPrefix, options.Separator, nameOrID)

	return nameOrID, serviceName
}

// setContainerNameForTemplate updates startCommand to contain the name argument with
// a value that includes the identify specifier.
// In case startCommand doesn't contain that argument it's added after "run" and its
// value will be set to info.ServiceName concated with the identify specifier %i.
func setContainerNameForTemplate(startCommand []string, info *containerInfo) ([]string, error) {
	// find the index of "--name" in the command slice
	nameIx := -1
	for argIx, arg := range startCommand {
		if arg == "--name" {
			nameIx = argIx + 1
			break
		}
		if strings.HasPrefix(arg, "--name=") {
			nameIx = argIx
			break
		}
	}
	switch {
	case nameIx == -1:
		// if not found, add --name argument in the command slice before the "run" argument.
		// it's assumed that the command slice contains this argument.
		runIx := -1
		for argIx, arg := range startCommand {
			if arg == "run" {
				runIx = argIx
				break
			}
		}
		if runIx == -1 {
			return startCommand, fmt.Errorf("\"run\" is missing in the command arguments")
		}
		startCommand = append(startCommand[:runIx+1], startCommand[runIx:]...)
		startCommand[runIx+1] = fmt.Sprintf("--name=%s-%%i", info.ServiceName)
	default:
		// append the identity specifier (%i) to the end of the --name value
		startCommand[nameIx] = fmt.Sprintf("%s-%%i", startCommand[nameIx])
	}
	return startCommand, nil
}

func formatOptions(options []string) string {
	var formatted strings.Builder
	if len(options) == 0 {
		return ""
	}
	formatted.WriteString(options[0])
	for _, o := range options[1:] {
		if strings.HasPrefix(o, "-") {
			formatted.WriteString(" \\\n\t" + o)
			continue
		}
		formatted.WriteString(" " + o)
	}
	return formatted.String()
}

// executeContainerTemplate executes the container template on the specified
// containerInfo.  Note that the containerInfo is also post processed and
// completed, which allows for an easier unit testing.
func executeContainerTemplate(info *containerInfo, options entities.GenerateSystemdOptions) (string, error) {
	if options.RestartPolicy != nil {
		if err := validateRestartPolicy(*options.RestartPolicy); err != nil {
			return "", err
		}
		info.RestartPolicy = *options.RestartPolicy
	}

	// Make sure the executable is set.
	if info.Executable == "" {
		executable, err := os.Executable()
		if err != nil {
			executable = "/usr/bin/podman"
			logrus.Warnf("Could not obtain podman executable location, using default %s", executable)
		}
		info.Executable = executable
	}

	info.Type = "forking"
	info.EnvVariable = define.EnvVariable
	info.ExecStart = "{{{{.Executable}}}} start {{{{.ContainerNameOrID}}}}"
	info.ExecStop = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.ContainerNameOrID}}}}"
	info.ExecStopPost = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.ContainerNameOrID}}}}"

	// Assemble the ExecStart command when creating a new container.
	//
	// Note that we cannot catch all corner cases here such that users
	// *must* manually check the generated files.  A container might have
	// been created via a Python script, which would certainly yield an
	// invalid `info.CreateCommand`.  Hence, we're doing a best effort unit
	// generation and don't try aiming at completeness.
	if options.New {
		info.Type = "notify"
		info.NotifyAccess = "all"
		info.PIDFile = ""
		info.ContainerIDFile = "%t/%n.ctr-id"
		info.ExecStartPre = "/bin/rm -f {{{{.ContainerIDFile}}}}"
		info.ExecStop = "{{{{.Executable}}}} stop --ignore --cidfile={{{{.ContainerIDFile}}}}"
		info.ExecStopPost = "{{{{.Executable}}}} rm -f --ignore --cidfile={{{{.ContainerIDFile}}}}"
		// The create command must at least have three arguments:
		// 	/usr/bin/podman run $IMAGE
		index := 0
		for i, arg := range info.CreateCommand {
			if arg == "run" || arg == "create" {
				index = i + 1
				break
			}
		}
		if index == 0 {
			return "", errors.Errorf("container's create command is too short or invalid: %v", info.CreateCommand)
		}
		// We're hard-coding the first five arguments and append the
		// CreateCommand with a stripped command and subcommand.
		startCommand := []string{info.Executable}
		if index > 2 {
			// include root flags
			info.RootFlags = strings.Join(escapeSystemdArguments(info.CreateCommand[1:index-1]), " ")
			startCommand = append(startCommand, info.CreateCommand[1:index-1]...)
		}
		startCommand = append(startCommand,
			"run",
			"--cidfile={{{{.ContainerIDFile}}}}",
			"--cgroups=no-conmon",
			"--rm",
		)
		remainingCmd := info.CreateCommand[index:]
		// Presence check for certain flags/options.
		fs := pflag.NewFlagSet("args", pflag.ContinueOnError)
		fs.ParseErrorsWhitelist.UnknownFlags = true
		fs.Usage = func() {}
		fs.SetInterspersed(false)
		fs.BoolP("detach", "d", false, "")
		fs.String("name", "", "")
		fs.Bool("replace", false, "")
		fs.StringArrayP("env", "e", nil, "")
		fs.String("sdnotify", "", "")
		fs.String("restart", "", "")
		if err := fs.Parse(remainingCmd); err != nil {
			return "", fmt.Errorf("parsing remaining command-line arguments: %w", err)
		}

		remainingCmd = filterCommonContainerFlags(remainingCmd, fs.NArg())
		// If the container is in a pod, make sure that the
		// --pod-id-file is set correctly.
		if info.Pod != nil {
			podFlags := []string{"--pod-id-file", "{{{{.Pod.PodIDFile}}}}"}
			startCommand = append(startCommand, podFlags...)
			remainingCmd = filterPodFlags(remainingCmd, fs.NArg())
		}

		hasDetachParam, err := fs.GetBool("detach")
		if err != nil {
			return "", err
		}
		hasNameParam := fs.Lookup("name").Changed
		hasReplaceParam, err := fs.GetBool("replace")
		if err != nil {
			return "", err
		}

		// Default to --sdnotify=conmon unless already set by the
		// container.
		hasSdnotifyParam := fs.Lookup("sdnotify").Changed
		if !hasSdnotifyParam {
			startCommand = append(startCommand, "--sdnotify=conmon")
		}

		if !hasDetachParam {
			// Enforce detaching
			//
			// since we use systemd `Type=forking` service @see
			// https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=
			// when we generated systemd service file with the
			// --new param, `ExecStart` will have `/usr/bin/podman
			// run ...` if `info.CreateCommand` has no `-d` or
			// `--detach` param, podman will run the container in
			// default attached mode, as a result, `systemd start`
			// will wait the `podman run` command exit until failed
			// with timeout error.
			startCommand = append(startCommand, "-d")

			if fs.Changed("detach") {
				// this can only happen if --detach=false is set
				// in that case we need to remove it otherwise we
				// would overwrite the previous detach arg to false
				remainingCmd = removeDetachArg(remainingCmd, fs.NArg())
			}
		}
		if hasNameParam && !hasReplaceParam {
			// Enforce --replace for named containers.  This will
			// make systemd units more robust as it allows them to
			// start after system crashes (see
			// github.com/containers/podman/issues/5485).
			startCommand = append(startCommand, "--replace")

			if fs.Changed("replace") {
				// this can only happen if --replace=false is set
				// in that case we need to remove it otherwise we
				// would overwrite the previous replace arg to false
				remainingCmd = removeReplaceArg(remainingCmd, fs.NArg())
			}
		}

		// Unless the user explicitly set a restart policy, check
		// whether the container was created with a custom one and use
		// it instead of the default.
		if options.RestartPolicy == nil {
			restartPolicy, err := fs.GetString("restart")
			if err != nil {
				return "", err
			}
			if restartPolicy != "" {
				if strings.HasPrefix(restartPolicy, "on-failure:") {
					// Special case --restart=on-failure:5
					spl := strings.Split(restartPolicy, ":")
					restartPolicy = spl[0]
					info.StartLimitBurst = spl[1]
				} else if restartPolicy == libpodDefine.RestartPolicyUnlessStopped {
					restartPolicy = libpodDefine.RestartPolicyAlways
				}
				info.RestartPolicy = restartPolicy
			}
		}

		envs, err := fs.GetStringArray("env")
		if err != nil {
			return "", err
		}
		for _, env := range envs {
			// if env arg does not contain a equal sign we have to add the envar to the unit
			// because it does try to red the value from the environment
			if !strings.Contains(env, "=") {
				for _, containerEnv := range info.containerEnv {
					split := strings.SplitN(containerEnv, "=", 2)
					if split[0] == env {
						info.ExtraEnvs = append(info.ExtraEnvs, escapeSystemdArg(containerEnv))
					}
				}
			}
		}

		startCommand = append(startCommand, remainingCmd...)
		startCommand = escapeSystemdArguments(startCommand)
		if options.TemplateUnitFile {
			info.IdentifySpecifier = true
			startCommand, err = setContainerNameForTemplate(startCommand, info)
			if err != nil {
				return "", err
			}
		}
		info.ExecStart = formatOptions(startCommand)
	}
	info.TimeoutStopSec = minTimeoutStopSec + info.StopTimeout

	if info.PodmanVersion == "" {
		info.PodmanVersion = version.Version.String()
	}

	if options.NoHeader {
		info.GenerateNoHeader = true
		info.GenerateTimestamp = false
	}

	if info.GenerateTimestamp {
		info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
	}
	// Sort the slices to assure a deterministic output.
	sort.Strings(info.BoundToServices)

	// Generate the template and compile it.
	//
	// Note that we need a two-step generation process to allow for fields
	// embedding other fields.  This way we can replace `A -> B -> C` and
	// make the code easier to maintain at the cost of a slightly slower
	// generation.  That's especially needed for embedding the PID and ID
	// files in other fields which will eventually get replaced in the 2nd
	// template execution.
	templ, err := template.New("container_template").Delims("{{{{", "}}}}").Parse(containerTemplate)
	if err != nil {
		return "", errors.Wrap(err, "error parsing systemd service template")
	}

	var buf bytes.Buffer
	if err := templ.Execute(&buf, info); err != nil {
		return "", err
	}

	// Now parse the generated template (i.e., buf) and execute it.
	templ, err = template.New("container_template").Delims("{{{{", "}}}}").Parse(buf.String())
	if err != nil {
		return "", errors.Wrap(err, "error parsing systemd service template")
	}

	buf = bytes.Buffer{}
	if err := templ.Execute(&buf, info); err != nil {
		return "", err
	}

	return buf.String(), nil
}