package generate

import (
	"context"
	"os"
	"strings"

	"github.com/containers/image/v5/manifest"
	"github.com/containers/podman/v3/libpod"
	"github.com/containers/podman/v3/libpod/image"
	ann "github.com/containers/podman/v3/pkg/annotations"
	envLib "github.com/containers/podman/v3/pkg/env"
	"github.com/containers/podman/v3/pkg/signal"
	"github.com/containers/podman/v3/pkg/specgen"
	spec "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"golang.org/x/sys/unix"
)

// Fill any missing parts of the spec generator (e.g. from the image).
// Returns a set of warnings or any fatal error that occurred.
func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerator) ([]string, error) {
	var (
		newImage *image.Image
		err      error
	)

	// Only add image configuration if we have an image
	if s.Image != "" {
		newImage, err = r.ImageRuntime().NewFromLocal(s.Image)
		if err != nil {
			return nil, err
		}

		_, mediaType, err := newImage.Manifest(ctx)
		if err != nil {
			if errors.Cause(err) != image.ErrImageIsBareList {
				return nil, err
			}
			// if err is not runnable image
			// use the local store image with repo@digest matches with the list, if exists
			manifestByte, manifestType, err := newImage.GetManifest(ctx, nil)
			if err != nil {
				return nil, err
			}
			list, err := manifest.ListFromBlob(manifestByte, manifestType)
			if err != nil {
				return nil, err
			}
			images, err := r.ImageRuntime().GetImages()
			if err != nil {
				return nil, err
			}
			findLocal := false
			listDigest, err := list.ChooseInstance(r.SystemContext())
			if err != nil {
				return nil, err
			}
			for _, img := range images {
				for _, imageDigest := range img.Digests() {
					if imageDigest == listDigest {
						newImage = img
						s.Image = img.ID()
						mediaType = manifestType
						findLocal = true
						logrus.Debug("image contains manifest list, using image from local storage")
						break
					}
				}
			}
			if !findLocal {
				return nil, image.ErrImageIsBareList
			}
		}

		if s.HealthConfig == nil && mediaType == manifest.DockerV2Schema2MediaType {
			s.HealthConfig, err = newImage.GetHealthCheck(ctx)
			if err != nil {
				return nil, err
			}
		}

		// Image stop signal
		if s.StopSignal == nil {
			stopSignal, err := newImage.StopSignal(ctx)
			if err != nil {
				return nil, err
			}
			if stopSignal != "" {
				sig, err := signal.ParseSignalNameOrNumber(stopSignal)
				if err != nil {
					return nil, err
				}
				s.StopSignal = &sig
			}
		}
	}

	rtc, err := r.GetConfig()
	if err != nil {
		return nil, err
	}

	// Get Default Environment from containers.conf
	defaultEnvs, err := envLib.ParseSlice(rtc.GetDefaultEnvEx(s.EnvHost, s.HTTPProxy))
	if err != nil {
		return nil, errors.Wrap(err, "error parsing fields in containers.conf")
	}
	if defaultEnvs["container"] == "" {
		defaultEnvs["container"] = "podman"
	}
	var envs map[string]string

	// Image Environment defaults
	if newImage != nil {
		// Image envs from the image if they don't exist
		// already, overriding the default environments
		imageEnvs, err := newImage.Env(ctx)
		if err != nil {
			return nil, err
		}

		envs, err = envLib.ParseSlice(imageEnvs)
		if err != nil {
			return nil, errors.Wrap(err, "Env fields from image failed to parse")
		}
		defaultEnvs = envLib.Join(defaultEnvs, envs)
	}

	// First transform the os env into a map. We need it for the labels later in
	// any case.
	osEnv, err := envLib.ParseSlice(os.Environ())
	if err != nil {
		return nil, errors.Wrap(err, "error parsing host environment variables")
	}
	// Caller Specified defaults
	if s.EnvHost {
		defaultEnvs = envLib.Join(defaultEnvs, osEnv)
	} else if s.HTTPProxy {
		for _, envSpec := range []string{
			"http_proxy",
			"HTTP_PROXY",
			"https_proxy",
			"HTTPS_PROXY",
			"ftp_proxy",
			"FTP_PROXY",
			"no_proxy",
			"NO_PROXY",
		} {
			if v, ok := osEnv[envSpec]; ok {
				defaultEnvs[envSpec] = v
			}
		}
	}

	s.Env = envLib.Join(defaultEnvs, s.Env)

	// Labels and Annotations
	annotations := make(map[string]string)
	if newImage != nil {
		labels, err := newImage.Labels(ctx)
		if err != nil {
			return nil, err
		}

		// labels from the image that don't exist already
		if len(labels) > 0 && s.Labels == nil {
			s.Labels = make(map[string]string)
		}
		for k, v := range labels {
			if _, exists := s.Labels[k]; !exists {
				s.Labels[k] = v
			}
		}

		// Add annotations from the image
		imgAnnotations, err := newImage.Annotations(ctx)
		if err != nil {
			return nil, err
		}
		for k, v := range imgAnnotations {
			annotations[k] = v
		}
	}

	// in the event this container is in a pod, and the pod has an infra container
	// we will want to configure it as a type "container" instead defaulting to
	// the behavior of a "sandbox" container
	// In Kata containers:
	// - "sandbox" is the annotation that denotes the container should use its own
	//   VM, which is the default behavior
	// - "container" denotes the container should join the VM of the SandboxID
	//   (the infra container)

	if len(s.Pod) > 0 {
		annotations[ann.SandboxID] = s.Pod
		annotations[ann.ContainerType] = ann.ContainerTypeContainer
	}

	for _, v := range rtc.Containers.Annotations {
		split := strings.SplitN(v, "=", 2)
		k := split[0]
		v := ""
		if len(split) == 2 {
			v = split[1]
		}
		annotations[k] = v
	}
	// now pass in the values from client
	for k, v := range s.Annotations {
		annotations[k] = v
	}
	s.Annotations = annotations

	if len(s.SeccompProfilePath) < 1 {
		p, err := libpod.DefaultSeccompPath()
		if err != nil {
			return nil, err
		}
		s.SeccompProfilePath = p
	}

	if len(s.User) == 0 && newImage != nil {
		s.User, err = newImage.User(ctx)
		if err != nil {
			return nil, err
		}
	}
	if err := finishThrottleDevices(s); err != nil {
		return nil, err
	}
	// Unless already set via the CLI, check if we need to disable process
	// labels or set the defaults.
	if len(s.SelinuxOpts) == 0 {
		if err := setLabelOpts(s, r, s.PidNS, s.IpcNS); err != nil {
			return nil, err
		}
	}

	// If caller did not specify Pids Limits load default
	if s.ResourceLimits == nil || s.ResourceLimits.Pids == nil {
		if s.CgroupsMode != "disabled" {
			limit := rtc.PidsLimit()
			if limit != 0 {
				if s.ResourceLimits == nil {
					s.ResourceLimits = &spec.LinuxResources{}
				}
				s.ResourceLimits.Pids = &spec.LinuxPids{
					Limit: limit,
				}
			}
		}
	}

	if s.LogConfiguration == nil {
		s.LogConfiguration = &specgen.LogConfig{}
	}
	// set log-driver from common if not already set
	if len(s.LogConfiguration.Driver) < 1 {
		s.LogConfiguration.Driver = rtc.Containers.LogDriver
	}

	warnings, err := verifyContainerResources(s)
	if err != nil {
		return warnings, err
	}

	// Warn on net=host/container/pod/none and port mappings.
	if (s.NetNS.NSMode == specgen.Host || s.NetNS.NSMode == specgen.FromContainer ||
		s.NetNS.NSMode == specgen.FromPod || s.NetNS.NSMode == specgen.NoNetwork) &&
		len(s.PortMappings) > 0 {
		warnings = append(warnings, "Port mappings have been discarded as one of the Host, Container, Pod, and None network modes are in use")
	}

	return warnings, nil
}

// finishThrottleDevices takes the temporary representation of the throttle
// devices in the specgen and looks up the major and major minors. it then
// sets the throttle devices proper in the specgen
func finishThrottleDevices(s *specgen.SpecGenerator) error {
	if bps := s.ThrottleReadBpsDevice; len(bps) > 0 {
		for k, v := range bps {
			statT := unix.Stat_t{}
			if err := unix.Stat(k, &statT); err != nil {
				return err
			}
			v.Major = (int64(unix.Major(uint64(statT.Rdev))))
			v.Minor = (int64(unix.Minor(uint64(statT.Rdev))))
			s.ResourceLimits.BlockIO.ThrottleReadBpsDevice = append(s.ResourceLimits.BlockIO.ThrottleReadBpsDevice, v)
		}
	}
	if bps := s.ThrottleWriteBpsDevice; len(bps) > 0 {
		for k, v := range bps {
			statT := unix.Stat_t{}
			if err := unix.Stat(k, &statT); err != nil {
				return err
			}
			v.Major = (int64(unix.Major(uint64(statT.Rdev))))
			v.Minor = (int64(unix.Minor(uint64(statT.Rdev))))
			s.ResourceLimits.BlockIO.ThrottleWriteBpsDevice = append(s.ResourceLimits.BlockIO.ThrottleWriteBpsDevice, v)
		}
	}
	if iops := s.ThrottleReadIOPSDevice; len(iops) > 0 {
		for k, v := range iops {
			statT := unix.Stat_t{}
			if err := unix.Stat(k, &statT); err != nil {
				return err
			}
			v.Major = (int64(unix.Major(uint64(statT.Rdev))))
			v.Minor = (int64(unix.Minor(uint64(statT.Rdev))))
			s.ResourceLimits.BlockIO.ThrottleReadIOPSDevice = append(s.ResourceLimits.BlockIO.ThrottleReadIOPSDevice, v)
		}
	}
	if iops := s.ThrottleWriteIOPSDevice; len(iops) > 0 {
		for k, v := range iops {
			statT := unix.Stat_t{}
			if err := unix.Stat(k, &statT); err != nil {
				return err
			}
			v.Major = (int64(unix.Major(uint64(statT.Rdev))))
			v.Minor = (int64(unix.Minor(uint64(statT.Rdev))))
			s.ResourceLimits.BlockIO.ThrottleWriteIOPSDevice = append(s.ResourceLimits.BlockIO.ThrottleWriteIOPSDevice, v)
		}
	}
	return nil
}