package generate

import (
	"context"
	"encoding/json"
	"path/filepath"
	"strings"

	cdi "github.com/container-orchestrated-devices/container-device-interface/pkg/cdi"
	"github.com/containers/common/libimage"
	"github.com/containers/podman/v4/libpod"
	"github.com/containers/podman/v4/libpod/define"
	"github.com/containers/podman/v4/pkg/namespaces"
	"github.com/containers/podman/v4/pkg/specgen"
	"github.com/containers/podman/v4/pkg/util"
	spec "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/opencontainers/selinux/go-selinux/label"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// MakeContainer creates a container based on the SpecGenerator.
// Returns the created, container and any warnings resulting from creating the
// container, or an error.
func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator) (*spec.Spec, *specgen.SpecGenerator, []libpod.CtrCreateOption, error) {
	rtc, err := rt.GetConfigNoCopy()
	if err != nil {
		return nil, nil, nil, err
	}

	// If joining a pod, retrieve the pod for use, and its infra container
	var pod *libpod.Pod
	var infra *libpod.Container
	if s.Pod != "" {
		pod, err = rt.LookupPod(s.Pod)
		if err != nil {
			return nil, nil, nil, errors.Wrapf(err, "error retrieving pod %s", s.Pod)
		}
		if pod.HasInfraContainer() {
			infra, err = pod.InfraContainer()
			if err != nil {
				return nil, nil, nil, err
			}
		}
	}

	options := []libpod.CtrCreateOption{}
	compatibleOptions := &libpod.InfraInherit{}
	var infraSpec *spec.Spec
	if infra != nil {
		options, infraSpec, compatibleOptions, err = Inherit(*infra)
		if err != nil {
			return nil, nil, nil, err
		}
	}

	if err := FinishThrottleDevices(s); err != nil {
		return nil, nil, nil, err
	}
	// Set defaults for unset namespaces
	if s.PidNS.IsDefault() {
		defaultNS, err := GetDefaultNamespaceMode("pid", rtc, pod)
		if err != nil {
			return nil, nil, nil, err
		}
		s.PidNS = defaultNS
	}
	if s.IpcNS.IsDefault() {
		defaultNS, err := GetDefaultNamespaceMode("ipc", rtc, pod)
		if err != nil {
			return nil, nil, nil, err
		}
		s.IpcNS = defaultNS
	}
	if s.UtsNS.IsDefault() {
		defaultNS, err := GetDefaultNamespaceMode("uts", rtc, pod)
		if err != nil {
			return nil, nil, nil, err
		}
		s.UtsNS = defaultNS
	}
	if s.UserNS.IsDefault() {
		defaultNS, err := GetDefaultNamespaceMode("user", rtc, pod)
		if err != nil {
			return nil, nil, nil, err
		}
		s.UserNS = defaultNS

		mappings, err := util.ParseIDMapping(namespaces.UsernsMode(s.UserNS.NSMode), nil, nil, "", "")
		if err != nil {
			return nil, nil, nil, err
		}
		s.IDMappings = mappings
	}
	if s.NetNS.IsDefault() {
		defaultNS, err := GetDefaultNamespaceMode("net", rtc, pod)
		if err != nil {
			return nil, nil, nil, err
		}
		s.NetNS = defaultNS
	}
	if s.CgroupNS.IsDefault() {
		defaultNS, err := GetDefaultNamespaceMode("cgroup", rtc, pod)
		if err != nil {
			return nil, nil, nil, err
		}
		s.CgroupNS = defaultNS
	}

	if s.ContainerCreateCommand != nil {
		options = append(options, libpod.WithCreateCommand(s.ContainerCreateCommand))
	}

	if s.Rootfs != "" {
		options = append(options, libpod.WithRootFS(s.Rootfs, s.RootfsOverlay))
	}

	newImage, resolvedImageName, imageData, err := getImageFromSpec(ctx, rt, s)
	if err != nil {
		return nil, nil, nil, err
	}
	if newImage != nil {
		// If the input name changed, we could properly resolve the
		// image. Otherwise, it must have been an ID where we're
		// defaulting to the first name or an empty one if no names are
		// present.
		if strings.HasPrefix(newImage.ID(), resolvedImageName) {
			names := newImage.Names()
			if len(names) > 0 {
				resolvedImageName = names[0]
			}
		}

		options = append(options, libpod.WithRootFSFromImage(newImage.ID(), resolvedImageName, s.RawImageName))
	}
	if err := s.Validate(); err != nil {
		return nil, nil, nil, errors.Wrap(err, "invalid config provided")
	}

	finalMounts, finalVolumes, finalOverlays, err := finalizeMounts(ctx, s, rt, rtc, newImage)
	if err != nil {
		return nil, nil, nil, err
	}

	if len(s.HostUsers) > 0 {
		options = append(options, libpod.WithHostUsers(s.HostUsers))
	}

	command, err := makeCommand(ctx, s, imageData, rtc)
	if err != nil {
		return nil, nil, nil, err
	}

	infraVolumes := (len(compatibleOptions.InfraVolumes) > 0 || len(compatibleOptions.InfraUserVolumes) > 0 || len(compatibleOptions.InfraImageVolumes) > 0)
	opts, err := createContainerOptions(ctx, rt, s, pod, finalVolumes, finalOverlays, imageData, command, infraVolumes, *compatibleOptions)
	if err != nil {
		return nil, nil, nil, err
	}
	options = append(options, opts...)

	if containerType := s.InitContainerType; len(containerType) > 0 {
		options = append(options, libpod.WithInitCtrType(containerType))
	}
	if len(s.Name) > 0 {
		logrus.Debugf("setting container name %s", s.Name)
		options = append(options, libpod.WithName(s.Name))
	}
	if len(s.Devices) > 0 {
		opts = ExtractCDIDevices(s)
		options = append(options, opts...)
	}
	runtimeSpec, err := SpecGenToOCI(ctx, s, rt, rtc, newImage, finalMounts, pod, command, compatibleOptions)
	if err != nil {
		return nil, nil, nil, err
	}
	if len(s.HostDeviceList) > 0 {
		options = append(options, libpod.WithHostDevice(s.HostDeviceList))
	}
	if infraSpec != nil && infraSpec.Linux != nil { // if we are inheriting Linux info from a pod...
		// Pass Security annotations
		if len(infraSpec.Annotations[define.InspectAnnotationLabel]) > 0 && len(runtimeSpec.Annotations[define.InspectAnnotationLabel]) == 0 {
			runtimeSpec.Annotations[define.InspectAnnotationLabel] = infraSpec.Annotations[define.InspectAnnotationLabel]
		}
		if len(infraSpec.Annotations[define.InspectAnnotationSeccomp]) > 0 && len(runtimeSpec.Annotations[define.InspectAnnotationSeccomp]) == 0 {
			runtimeSpec.Annotations[define.InspectAnnotationSeccomp] = infraSpec.Annotations[define.InspectAnnotationSeccomp]
		}
		if len(infraSpec.Annotations[define.InspectAnnotationApparmor]) > 0 && len(runtimeSpec.Annotations[define.InspectAnnotationApparmor]) == 0 {
			runtimeSpec.Annotations[define.InspectAnnotationApparmor] = infraSpec.Annotations[define.InspectAnnotationApparmor]
		}
	}
	return runtimeSpec, s, options, err
}
func ExecuteCreate(ctx context.Context, rt *libpod.Runtime, runtimeSpec *spec.Spec, s *specgen.SpecGenerator, infra bool, options ...libpod.CtrCreateOption) (*libpod.Container, error) {
	ctr, err := rt.NewContainer(ctx, runtimeSpec, s, infra, options...)
	if err != nil {
		return ctr, err
	}

	return ctr, rt.PrepareVolumeOnCreateContainer(ctx, ctr)
}

// ExtractCDIDevices process the list of Devices in the spec and determines if any of these are CDI devices.
// The CDI devices are added to the list of CtrCreateOptions.
// Note that this may modify the device list associated with the spec, which should then only contain non-CDI devices.
func ExtractCDIDevices(s *specgen.SpecGenerator) []libpod.CtrCreateOption {
	devs := make([]spec.LinuxDevice, 0, len(s.Devices))
	var cdiDevs []string
	var options []libpod.CtrCreateOption

	for _, device := range s.Devices {
		if isCDIDevice(device.Path) {
			logrus.Debugf("Identified CDI device %v", device.Path)
			cdiDevs = append(cdiDevs, device.Path)
			continue
		}
		logrus.Debugf("Non-CDI device %v; assuming standard device", device.Path)
		devs = append(devs, device)
	}
	s.Devices = devs
	if len(cdiDevs) > 0 {
		options = append(options, libpod.WithCDI(cdiDevs))
	}
	return options
}

// isCDIDevice checks whether the specified device is a CDI device.
func isCDIDevice(device string) bool {
	return cdi.IsQualifiedName(device)
}

func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod, volumes []*specgen.NamedVolume, overlays []*specgen.OverlayVolume, imageData *libimage.ImageData, command []string, infraVolumes bool, compatibleOptions libpod.InfraInherit) ([]libpod.CtrCreateOption, error) {
	var options []libpod.CtrCreateOption
	var err error

	if s.PreserveFDs > 0 {
		options = append(options, libpod.WithPreserveFDs(s.PreserveFDs))
	}

	if s.Stdin {
		options = append(options, libpod.WithStdin())
	}

	if s.Timezone != "" {
		options = append(options, libpod.WithTimezone(s.Timezone))
	}
	if s.Umask != "" {
		options = append(options, libpod.WithUmask(s.Umask))
	}
	if s.Volatile {
		options = append(options, libpod.WithVolatile())
	}

	useSystemd := false
	switch s.Systemd {
	case "always":
		useSystemd = true
	case "false":
		break
	case "", "true":
		if len(command) == 0 && imageData != nil {
			command = imageData.Config.Cmd
		}

		if len(command) > 0 {
			useSystemdCommands := map[string]bool{
				"/sbin/init":           true,
				"/usr/sbin/init":       true,
				"/usr/local/sbin/init": true,
			}
			if useSystemdCommands[command[0]] || (filepath.Base(command[0]) == "systemd") {
				useSystemd = true
			}
		}
	default:
		return nil, errors.Wrapf(err, "invalid value %q systemd option requires 'true, false, always'", s.Systemd)
	}
	logrus.Debugf("using systemd mode: %t", useSystemd)
	if useSystemd {
		// is StopSignal was not set by the user then set it to systemd
		// expected StopSigal
		if s.StopSignal == nil {
			stopSignal, err := util.ParseSignal("RTMIN+3")
			if err != nil {
				return nil, errors.Wrapf(err, "error parsing systemd signal")
			}
			s.StopSignal = &stopSignal
		}

		options = append(options, libpod.WithSystemd())
	}
	if len(s.SdNotifyMode) > 0 {
		options = append(options, libpod.WithSdNotifyMode(s.SdNotifyMode))
	}
	if pod != nil {
		logrus.Debugf("adding container to pod %s", pod.Name())
		options = append(options, rt.WithPod(pod))
	}
	destinations := []string{}
	// Take all mount and named volume destinations.
	for _, mount := range s.Mounts {
		destinations = append(destinations, mount.Destination)
	}
	for _, volume := range volumes {
		destinations = append(destinations, volume.Dest)
	}
	for _, overlayVolume := range overlays {
		destinations = append(destinations, overlayVolume.Destination)
	}
	for _, imageVolume := range s.ImageVolumes {
		destinations = append(destinations, imageVolume.Destination)
	}

	if len(destinations) > 0 || !infraVolumes {
		options = append(options, libpod.WithUserVolumes(destinations))
	}

	if len(volumes) != 0 {
		var vols []*libpod.ContainerNamedVolume
		for _, v := range volumes {
			vols = append(vols, &libpod.ContainerNamedVolume{
				Name:    v.Name,
				Dest:    v.Dest,
				Options: v.Options,
			})
		}
		options = append(options, libpod.WithNamedVolumes(vols))
	}

	if len(overlays) != 0 {
		var vols []*libpod.ContainerOverlayVolume
		for _, v := range overlays {
			vols = append(vols, &libpod.ContainerOverlayVolume{
				Dest:    v.Destination,
				Source:  v.Source,
				Options: v.Options,
			})
		}
		options = append(options, libpod.WithOverlayVolumes(vols))
	}

	if len(s.ImageVolumes) != 0 {
		var vols []*libpod.ContainerImageVolume
		for _, v := range s.ImageVolumes {
			vols = append(vols, &libpod.ContainerImageVolume{
				Dest:      v.Destination,
				Source:    v.Source,
				ReadWrite: v.ReadWrite,
			})
		}
		options = append(options, libpod.WithImageVolumes(vols))
	}

	if s.Command != nil {
		options = append(options, libpod.WithCommand(s.Command))
	}
	if s.Entrypoint != nil {
		options = append(options, libpod.WithEntrypoint(s.Entrypoint))
	}
	if len(s.ContainerStorageConfig.StorageOpts) > 0 {
		options = append(options, libpod.WithStorageOpts(s.StorageOpts))
	}
	// If the user did not specify a workdir on the CLI, let's extract it
	// from the image.
	if s.WorkDir == "" && imageData != nil {
		options = append(options, libpod.WithCreateWorkingDir())
		s.WorkDir = imageData.Config.WorkingDir
	}
	if s.WorkDir == "" {
		s.WorkDir = "/"
	}
	if s.CreateWorkingDir {
		options = append(options, libpod.WithCreateWorkingDir())
	}
	if s.StopSignal != nil {
		options = append(options, libpod.WithStopSignal(*s.StopSignal))
	}
	if s.StopTimeout != nil {
		options = append(options, libpod.WithStopTimeout(*s.StopTimeout))
	}
	if s.Timeout != 0 {
		options = append(options, libpod.WithTimeout(s.Timeout))
	}
	if s.LogConfiguration != nil {
		if len(s.LogConfiguration.Path) > 0 {
			options = append(options, libpod.WithLogPath(s.LogConfiguration.Path))
		}
		if s.LogConfiguration.Size > 0 {
			options = append(options, libpod.WithMaxLogSize(s.LogConfiguration.Size))
		}
		if len(s.LogConfiguration.Options) > 0 && s.LogConfiguration.Options["tag"] != "" {
			// Note: I'm really guessing here.
			options = append(options, libpod.WithLogTag(s.LogConfiguration.Options["tag"]))
		}

		if len(s.LogConfiguration.Driver) > 0 {
			options = append(options, libpod.WithLogDriver(s.LogConfiguration.Driver))
		}
	}
	// Security options
	if len(s.SelinuxOpts) > 0 {
		options = append(options, libpod.WithSecLabels(s.SelinuxOpts))
	} else {
		if pod != nil && len(compatibleOptions.InfraLabels) == 0 {
			// duplicate the security options from the pod
			processLabel, err := pod.ProcessLabel()
			if err != nil {
				return nil, err
			}
			if processLabel != "" {
				selinuxOpts, err := label.DupSecOpt(processLabel)
				if err != nil {
					return nil, err
				}
				options = append(options, libpod.WithSecLabels(selinuxOpts))
			}
		}
	}
	options = append(options, libpod.WithPrivileged(s.Privileged))

	// Get namespace related options
	namespaceOpts, err := namespaceOptions(ctx, s, rt, pod, imageData)
	if err != nil {
		return nil, err
	}
	options = append(options, namespaceOpts...)

	if len(s.ConmonPidFile) > 0 {
		options = append(options, libpod.WithConmonPidFile(s.ConmonPidFile))
	}
	options = append(options, libpod.WithLabels(s.Labels))
	if s.ShmSize != nil {
		options = append(options, libpod.WithShmSize(*s.ShmSize))
	}
	if s.Rootfs != "" {
		options = append(options, libpod.WithRootFS(s.Rootfs, s.RootfsOverlay))
	}
	// Default used if not overridden on command line

	if s.RestartPolicy != "" {
		if s.RestartRetries != nil {
			options = append(options, libpod.WithRestartRetries(*s.RestartRetries))
		}
		options = append(options, libpod.WithRestartPolicy(s.RestartPolicy))
	}

	if s.ContainerHealthCheckConfig.HealthConfig != nil {
		options = append(options, libpod.WithHealthCheck(s.ContainerHealthCheckConfig.HealthConfig))
		logrus.Debugf("New container has a health check")
	}

	if len(s.Secrets) != 0 {
		manager, err := rt.SecretsManager()
		if err != nil {
			return nil, err
		}
		var secrs []*libpod.ContainerSecret
		for _, s := range s.Secrets {
			secr, err := manager.Lookup(s.Source)
			if err != nil {
				return nil, err
			}
			secrs = append(secrs, &libpod.ContainerSecret{
				Secret: secr,
				UID:    s.UID,
				GID:    s.GID,
				Mode:   s.Mode,
				Target: s.Target,
			})
		}
		options = append(options, libpod.WithSecrets(secrs))
	}

	if len(s.EnvSecrets) != 0 {
		options = append(options, libpod.WithEnvSecrets(s.EnvSecrets))
	}

	if len(s.DependencyContainers) > 0 {
		deps := make([]*libpod.Container, 0, len(s.DependencyContainers))
		for _, ctr := range s.DependencyContainers {
			depCtr, err := rt.LookupContainer(ctr)
			if err != nil {
				return nil, errors.Wrapf(err, "%q is not a valid container, cannot be used as a dependency", ctr)
			}
			deps = append(deps, depCtr)
		}
		options = append(options, libpod.WithDependencyCtrs(deps))
	}
	if s.PidFile != "" {
		options = append(options, libpod.WithPidFile(s.PidFile))
	}

	options = append(options, libpod.WithSelectedPasswordManagement(s.Passwd))

	return options, nil
}

func Inherit(infra libpod.Container) (opts []libpod.CtrCreateOption, infraS *spec.Spec, compat *libpod.InfraInherit, err error) {
	options := []libpod.CtrCreateOption{}
	compatibleOptions := &libpod.InfraInherit{}
	infraConf := infra.Config()
	infraSpec := infraConf.Spec

	config, err := json.Marshal(infraConf)
	if err != nil {
		return nil, nil, nil, err
	}
	err = json.Unmarshal(config, compatibleOptions)
	if err != nil {
		return nil, nil, nil, err
	}
	if infraSpec.Linux != nil && infraSpec.Linux.Resources != nil {
		resources, err := json.Marshal(infraSpec.Linux.Resources)
		if err != nil {
			return nil, nil, nil, err
		}
		err = json.Unmarshal(resources, &compatibleOptions.InfraResources)
		if err != nil {
			return nil, nil, nil, err
		}
	}
	if compatibleOptions != nil {
		options = append(options, libpod.WithInfraConfig(*compatibleOptions))
	}
	return options, infraSpec, compatibleOptions, nil
}
