package generate

import (
	"os"

	"github.com/containers/common/pkg/capabilities"
	"github.com/containers/libpod/libpod"
	"github.com/containers/libpod/libpod/image"
	"github.com/containers/libpod/pkg/specgen"
	"github.com/cri-o/ocicni/pkg/ocicni"
	spec "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/opencontainers/runtime-tools/generate"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

func GenerateNamespaceContainerOpts(s *specgen.SpecGenerator, rt *libpod.Runtime) ([]libpod.CtrCreateOption, error) {
	var portBindings []ocicni.PortMapping
	options := make([]libpod.CtrCreateOption, 0)

	// Cgroups
	switch {
	case s.CgroupNS.IsPrivate():
		ns := s.CgroupNS.Value
		if _, err := os.Stat(ns); err != nil {
			return nil, err
		}
	case s.CgroupNS.IsContainer():
		connectedCtr, err := rt.LookupContainer(s.CgroupNS.Value)
		if err != nil {
			return nil, errors.Wrapf(err, "container %q not found", s.CgroupNS.Value)
		}
		options = append(options, libpod.WithCgroupNSFrom(connectedCtr))
		//	TODO
		//default:
		//	return nil, errors.New("cgroup name only supports private and container")
	}

	if s.CgroupParent != "" {
		options = append(options, libpod.WithCgroupParent(s.CgroupParent))
	}

	if s.CgroupsMode != "" {
		options = append(options, libpod.WithCgroupsMode(s.CgroupsMode))
	}

	// ipc
	switch {
	case s.IpcNS.IsHost():
		options = append(options, libpod.WithShmDir("/dev/shm"))
	case s.IpcNS.IsContainer():
		connectedCtr, err := rt.LookupContainer(s.IpcNS.Value)
		if err != nil {
			return nil, errors.Wrapf(err, "container %q not found", s.IpcNS.Value)
		}
		options = append(options, libpod.WithIPCNSFrom(connectedCtr))
		options = append(options, libpod.WithShmDir(connectedCtr.ShmDir()))
	}

	// pid
	if s.PidNS.IsContainer() {
		connectedCtr, err := rt.LookupContainer(s.PidNS.Value)
		if err != nil {
			return nil, errors.Wrapf(err, "container %q not found", s.PidNS.Value)
		}
		options = append(options, libpod.WithPIDNSFrom(connectedCtr))
	}

	// uts
	switch {
	case s.UtsNS.IsPod():
		connectedPod, err := rt.LookupPod(s.UtsNS.Value)
		if err != nil {
			return nil, errors.Wrapf(err, "pod %q not found", s.UtsNS.Value)
		}
		options = append(options, libpod.WithUTSNSFromPod(connectedPod))
	case s.UtsNS.IsContainer():
		connectedCtr, err := rt.LookupContainer(s.UtsNS.Value)
		if err != nil {
			return nil, errors.Wrapf(err, "container %q not found", s.UtsNS.Value)
		}

		options = append(options, libpod.WithUTSNSFrom(connectedCtr))
	}

	if s.UseImageHosts {
		options = append(options, libpod.WithUseImageHosts())
	} else if len(s.HostAdd) > 0 {
		options = append(options, libpod.WithHosts(s.HostAdd))
	}

	// User

	switch {
	case s.UserNS.IsPath():
		ns := s.UserNS.Value
		if ns == "" {
			return nil, errors.Errorf("invalid empty user-defined user namespace")
		}
		_, err := os.Stat(ns)
		if err != nil {
			return nil, err
		}
		if s.IDMappings != nil {
			options = append(options, libpod.WithIDMappings(*s.IDMappings))
		}
	case s.UserNS.IsContainer():
		connectedCtr, err := rt.LookupContainer(s.UserNS.Value)
		if err != nil {
			return nil, errors.Wrapf(err, "container %q not found", s.UserNS.Value)
		}
		options = append(options, libpod.WithUserNSFrom(connectedCtr))
	default:
		if s.IDMappings != nil {
			options = append(options, libpod.WithIDMappings(*s.IDMappings))
		}
	}

	options = append(options, libpod.WithUser(s.User))
	options = append(options, libpod.WithGroups(s.Groups))

	if len(s.PortMappings) > 0 {
		portBindings = s.PortMappings
	}

	switch {
	case s.NetNS.IsPath():
		ns := s.NetNS.Value
		if ns == "" {
			return nil, errors.Errorf("invalid empty user-defined network namespace")
		}
		_, err := os.Stat(ns)
		if err != nil {
			return nil, err
		}
	case s.NetNS.IsContainer():
		connectedCtr, err := rt.LookupContainer(s.NetNS.Value)
		if err != nil {
			return nil, errors.Wrapf(err, "container %q not found", s.NetNS.Value)
		}
		options = append(options, libpod.WithNetNSFrom(connectedCtr))
	case !s.NetNS.IsHost() && s.NetNS.NSMode != specgen.NoNetwork:
		postConfigureNetNS := !s.UserNS.IsHost()
		options = append(options, libpod.WithNetNS(portBindings, postConfigureNetNS, string(s.NetNS.NSMode), s.CNINetworks))
	}

	if len(s.DNSSearch) > 0 {
		options = append(options, libpod.WithDNSSearch(s.DNSSearch))
	}
	if len(s.DNSServer) > 0 {
		// TODO I'm not sure how we are going to handle this given the input
		if len(s.DNSServer) == 1 { //&& strings.ToLower(s.DNSServer[0].) == "none" {
			options = append(options, libpod.WithUseImageResolvConf())
		} else {
			var dnsServers []string
			for _, d := range s.DNSServer {
				dnsServers = append(dnsServers, d.String())
			}
			options = append(options, libpod.WithDNS(dnsServers))
		}
	}
	if len(s.DNSOption) > 0 {
		options = append(options, libpod.WithDNSOption(s.DNSOption))
	}
	if s.StaticIP != nil {
		options = append(options, libpod.WithStaticIP(*s.StaticIP))
	}

	if s.StaticMAC != nil {
		options = append(options, libpod.WithStaticMAC(*s.StaticMAC))
	}
	return options, nil
}

func pidConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error {
	if s.PidNS.IsPath() {
		return g.AddOrReplaceLinuxNamespace(string(spec.PIDNamespace), s.PidNS.Value)
	}
	if s.PidNS.IsHost() {
		return g.RemoveLinuxNamespace(string(spec.PIDNamespace))
	}
	if s.PidNS.IsContainer() {
		logrus.Debugf("using container %s pidmode", s.PidNS.Value)
	}
	if s.PidNS.IsPod() {
		logrus.Debug("using pod pidmode")
	}
	return nil
}

func utsConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator, runtime *libpod.Runtime) error {
	hostname := s.Hostname
	var err error
	if hostname == "" {
		switch {
		case s.UtsNS.IsContainer():
			utsCtr, err := runtime.LookupContainer(s.UtsNS.Value)
			if err != nil {
				return errors.Wrapf(err, "unable to retrieve hostname from dependency container %s", s.UtsNS.Value)
			}
			hostname = utsCtr.Hostname()
		case s.NetNS.IsHost() || s.UtsNS.IsHost():
			hostname, err = os.Hostname()
			if err != nil {
				return errors.Wrap(err, "unable to retrieve hostname of the host")
			}
		default:
			logrus.Debug("No hostname set; container's hostname will default to runtime default")
		}
	}
	g.RemoveHostname()
	if s.Hostname != "" || !s.UtsNS.IsHost() {
		// Set the hostname in the OCI configuration only
		// if specified by the user or if we are creating
		// a new UTS namespace.
		g.SetHostname(hostname)
	}
	g.AddProcessEnv("HOSTNAME", hostname)

	if s.UtsNS.IsPath() {
		return g.AddOrReplaceLinuxNamespace(string(spec.UTSNamespace), s.UtsNS.Value)
	}
	if s.UtsNS.IsHost() {
		return g.RemoveLinuxNamespace(string(spec.UTSNamespace))
	}
	if s.UtsNS.IsContainer() {
		logrus.Debugf("using container %s utsmode", s.UtsNS.Value)
	}
	return nil
}

func ipcConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error {
	if s.IpcNS.IsPath() {
		return g.AddOrReplaceLinuxNamespace(string(spec.IPCNamespace), s.IpcNS.Value)
	}
	if s.IpcNS.IsHost() {
		return g.RemoveLinuxNamespace(s.IpcNS.Value)
	}
	if s.IpcNS.IsContainer() {
		logrus.Debugf("Using container %s ipcmode", s.IpcNS.Value)
	}
	return nil
}

func cgroupConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error {
	if s.CgroupNS.IsPath() {
		return g.AddOrReplaceLinuxNamespace(string(spec.CgroupNamespace), s.CgroupNS.Value)
	}
	if s.CgroupNS.IsHost() {
		return g.RemoveLinuxNamespace(s.CgroupNS.Value)
	}
	if s.CgroupNS.IsPrivate() {
		return g.AddOrReplaceLinuxNamespace(string(spec.CgroupNamespace), "")
	}
	if s.CgroupNS.IsContainer() {
		logrus.Debugf("Using container %s cgroup mode", s.CgroupNS.Value)
	}
	return nil
}

func networkConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error {
	switch {
	case s.NetNS.IsHost():
		logrus.Debug("Using host netmode")
		if err := g.RemoveLinuxNamespace(string(spec.NetworkNamespace)); err != nil {
			return err
		}

	case s.NetNS.NSMode == specgen.NoNetwork:
		logrus.Debug("Using none netmode")
	case s.NetNS.NSMode == specgen.Bridge:
		logrus.Debug("Using bridge netmode")
	case s.NetNS.IsContainer():
		logrus.Debugf("using container %s netmode", s.NetNS.Value)
	case s.NetNS.IsPath():
		logrus.Debug("Using ns netmode")
		if err := g.AddOrReplaceLinuxNamespace(string(spec.NetworkNamespace), s.NetNS.Value); err != nil {
			return err
		}
	case s.NetNS.IsPod():
		logrus.Debug("Using pod netmode, unless pod is not sharing")
	case s.NetNS.NSMode == specgen.Slirp:
		logrus.Debug("Using slirp4netns netmode")
	default:
		return errors.Errorf("unknown network mode")
	}

	if g.Config.Annotations == nil {
		g.Config.Annotations = make(map[string]string)
	}

	if s.PublishImagePorts {
		g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseTrue
	} else {
		g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseFalse
	}

	return nil
}

func userConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error {
	if s.UserNS.IsPath() {
		if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), s.UserNS.Value); err != nil {
			return err
		}
		// runc complains if no mapping is specified, even if we join another ns.  So provide a dummy mapping
		g.AddLinuxUIDMapping(uint32(0), uint32(0), uint32(1))
		g.AddLinuxGIDMapping(uint32(0), uint32(0), uint32(1))
	}

	if s.IDMappings != nil {
		if (len(s.IDMappings.UIDMap) > 0 || len(s.IDMappings.GIDMap) > 0) && !s.UserNS.IsHost() {
			if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil {
				return err
			}
		}
		for _, uidmap := range s.IDMappings.UIDMap {
			g.AddLinuxUIDMapping(uint32(uidmap.HostID), uint32(uidmap.ContainerID), uint32(uidmap.Size))
		}
		for _, gidmap := range s.IDMappings.GIDMap {
			g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size))
		}
	}
	return nil
}

func securityConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator, newImage *image.Image) error {
	// HANDLE CAPABILITIES
	// NOTE: Must happen before SECCOMP
	if s.Privileged {
		g.SetupPrivileged(true)
	}

	useNotRoot := func(user string) bool {
		if user == "" || user == "root" || user == "0" {
			return false
		}
		return true
	}
	configSpec := g.Config
	var err error
	var caplist []string
	bounding := configSpec.Process.Capabilities.Bounding
	if useNotRoot(s.User) {
		configSpec.Process.Capabilities.Bounding = caplist
	}
	caplist, err = capabilities.MergeCapabilities(configSpec.Process.Capabilities.Bounding, s.CapAdd, s.CapDrop)
	if err != nil {
		return err
	}

	configSpec.Process.Capabilities.Bounding = caplist
	configSpec.Process.Capabilities.Permitted = caplist
	configSpec.Process.Capabilities.Inheritable = caplist
	configSpec.Process.Capabilities.Effective = caplist
	configSpec.Process.Capabilities.Ambient = caplist
	if useNotRoot(s.User) {
		caplist, err = capabilities.MergeCapabilities(bounding, s.CapAdd, s.CapDrop)
		if err != nil {
			return err
		}
	}
	configSpec.Process.Capabilities.Bounding = caplist

	// HANDLE SECCOMP
	if s.SeccompProfilePath != "unconfined" {
		seccompConfig, err := getSeccompConfig(s, configSpec, newImage)
		if err != nil {
			return err
		}
		configSpec.Linux.Seccomp = seccompConfig
	}

	// Clear default Seccomp profile from Generator for privileged containers
	if s.SeccompProfilePath == "unconfined" || s.Privileged {
		configSpec.Linux.Seccomp = nil
	}

	g.SetRootReadonly(s.ReadOnlyFilesystem)
	for sysctlKey, sysctlVal := range s.Sysctl {
		g.AddLinuxSysctl(sysctlKey, sysctlVal)
	}

	return nil
}

// GetNamespaceOptions transforms a slice of kernel namespaces
// into a slice of pod create options. Currently, not all
// kernel namespaces are supported, and they will be returned in an error
func GetNamespaceOptions(ns []string) ([]libpod.PodCreateOption, error) {
	var options []libpod.PodCreateOption
	var erroredOptions []libpod.PodCreateOption
	for _, toShare := range ns {
		switch toShare {
		case "cgroup":
			options = append(options, libpod.WithPodCgroups())
		case "net":
			options = append(options, libpod.WithPodNet())
		case "mnt":
			return erroredOptions, errors.Errorf("Mount sharing functionality not supported on pod level")
		case "pid":
			options = append(options, libpod.WithPodPID())
		case "user":
			return erroredOptions, errors.Errorf("User sharing functionality not supported on pod level")
		case "ipc":
			options = append(options, libpod.WithPodIPC())
		case "uts":
			options = append(options, libpod.WithPodUTS())
		case "":
		case "none":
			return erroredOptions, nil
		default:
			return erroredOptions, errors.Errorf("Invalid kernel namespace to share: %s. Options are: net, pid, ipc, uts or none", toShare)
		}
	}
	return options, nil
}
