package generate

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"text/template"
	"time"

	"github.com/containers/libpod/version"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// EnvVariable "PODMAN_SYSTEMD_UNIT" is set in all generated systemd units and
// is set to the unit's (unique) name.
const EnvVariable = "PODMAN_SYSTEMD_UNIT"

// 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.
	ContainerName string
	// InfraContainer of the pod.
	InfraContainer 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
	// PIDFile of the service. Required for forking services. Must point to the
	// PID of the associated conmon process.
	PIDFile 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
	// RequiredServices are services this service requires. Note that this
	// service runs before them.
	RequiredServices []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
	// TimeStamp at the time of creating the unit file. Will be set internally.
	TimeStamp string
	// New controls if a new container is created or if an existing one is started.
	New bool
	// CreateCommand is the full command plus arguments of the process the
	// container has been created with.
	CreateCommand []string
	// RunCommand is a post-processed variant of CreateCommand and used for
	// the ExecStart field in generic unit files.
	RunCommand string
	// EnvVariable is generate.EnvVariable and must not be set.
	EnvVariable string
}

var restartPolicies = []string{"no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", "always"}

// validateRestartPolicy checks that the user-provided policy is valid.
func validateRestartPolicy(restart string) error {
	for _, i := range restartPolicies {
		if i == restart {
			return nil
		}
	}
	return errors.Errorf("%s is not a valid restart policy", restart)
}

const containerTemplate = `# {{.ServiceName}}.service
# autogenerated by Podman {{.PodmanVersion}}
{{- if .TimeStamp}}
# {{.TimeStamp}}
{{- end}}

[Unit]
Description=Podman {{.ServiceName}}.service
Documentation=man:podman-generate-systemd(1)
Wants=network.target
After=network-online.target
{{- if .BoundToServices}}
RefuseManualStart=yes
RefuseManualStop=yes
BindsTo={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
After={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
{{- end}}
{{- if .RequiredServices}}
Requires={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
Before={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
{{- end}}

[Service]
Environment={{.EnvVariable}}=%n
Restart={{.RestartPolicy}}
{{- if .New}}
ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-cid
ExecStart={{.RunCommand}}
ExecStop={{.Executable}} stop --ignore --cidfile %t/%n-cid {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}}
ExecStopPost={{.Executable}} rm --ignore -f --cidfile %t/%n-cid
PIDFile=%t/%n-pid
{{- else}}
ExecStart={{.Executable}} start {{.ContainerName}}
ExecStop={{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.ContainerName}}
PIDFile={{.PIDFile}}
{{- end}}
KillMode=none
Type=forking

[Install]
WantedBy=multi-user.target default.target`

// Options include different options to control the unit file generation.
type Options struct {
	// When set, generate service files in the current working directory and
	// return the paths to these files instead of returning all contents in one
	// big string.
	Files bool
	// New controls if a new container is created or if an existing one is started.
	New bool
}

// CreateContainerSystemdUnit creates a systemd unit file for a container.
func CreateContainerSystemdUnit(info *ContainerInfo, opts Options) (string, error) {
	if err := validateRestartPolicy(info.RestartPolicy); err != nil {
		return "", err
	}

	// 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.EnvVariable = EnvVariable

	// 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 opts.New {
		// The create command must at least have three arguments:
		// 	/usr/bin/podman run $IMAGE
		index := 2
		if info.CreateCommand[1] == "container" {
			index = 3
		}
		if len(info.CreateCommand) < index+1 {
			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 subcomand.
		command := []string{
			info.Executable,
			"run",
			"--conmon-pidfile", "%t/%n-pid",
			"--cidfile", "%t/%n-cid",
			"--cgroups=no-conmon",
		}

		// 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.
		hasDetachParam := false
		for _, p := range info.CreateCommand[index:] {
			if p == "--detach" || p == "-d" {
				hasDetachParam = true
			}
		}
		if !hasDetachParam {
			command = append(command, "-d")
		}

		command = append(command, info.CreateCommand[index:]...)
		info.RunCommand = strings.Join(command, " ")
		info.New = true
	}

	if info.PodmanVersion == "" {
		info.PodmanVersion = version.Version
	}
	if info.GenerateTimestamp {
		info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
	}

	// Sort the slices to assure a deterministic output.
	sort.Strings(info.RequiredServices)
	sort.Strings(info.BoundToServices)

	// Generate the template and compile it.
	templ, err := template.New("systemd_service_file").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
	}

	if !opts.Files {
		return buf.String(), nil
	}

	buf.WriteByte('\n')
	cwd, err := os.Getwd()
	if err != nil {
		return "", errors.Wrap(err, "error getting current working directory")
	}
	path := filepath.Join(cwd, fmt.Sprintf("%s.service", info.ServiceName))
	if err := ioutil.WriteFile(path, buf.Bytes(), 0644); err != nil {
		return "", errors.Wrap(err, "error generating systemd unit")
	}
	return path, nil
}
