// +build linux

package cni

import (
	"encoding/json"
	"io/ioutil"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/containernetworking/cni/libcni"
	"github.com/containers/podman/v3/libpod/network/types"
	"github.com/containers/podman/v3/libpod/network/util"
	pkgutil "github.com/containers/podman/v3/pkg/util"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

func createNetworkFromCNIConfigList(conf *libcni.NetworkConfigList, confPath string) (*types.Network, error) {
	network := types.Network{
		Name:        conf.Name,
		ID:          getNetworkIDFromName(conf.Name),
		Labels:      map[string]string{},
		Options:     map[string]string{},
		IPAMOptions: map[string]string{},
	}

	cniJSON := make(map[string]interface{})
	err := json.Unmarshal(conf.Bytes, &cniJSON)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to unmarshal network config %s", conf.Name)
	}
	if args, ok := cniJSON["args"]; ok {
		if key, ok := args.(map[string]interface{}); ok {
			// read network labels and options from the conf file
			network.Labels = getNetworkArgsFromConfList(key, podmanLabelKey)
			network.Options = getNetworkArgsFromConfList(key, podmanOptionsKey)
		}
	}

	f, err := os.Stat(confPath)
	if err != nil {
		return nil, err
	}
	stat := f.Sys().(*syscall.Stat_t)
	network.Created = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))

	firstPlugin := conf.Plugins[0]
	network.Driver = firstPlugin.Network.Type

	switch firstPlugin.Network.Type {
	case types.BridgeNetworkDriver:
		var bridge hostLocalBridge
		err := json.Unmarshal(firstPlugin.Bytes, &bridge)
		if err != nil {
			return nil, errors.Wrapf(err, "failed to unmarshal the bridge plugin config in %s", confPath)
		}
		network.NetworkInterface = bridge.BrName

		// if isGateway is false we have an internal network
		if !bridge.IsGW {
			network.Internal = true
		}

		// set network options
		if bridge.MTU != 0 {
			network.Options["mtu"] = strconv.Itoa(bridge.MTU)
		}
		if bridge.Vlan != 0 {
			network.Options["vlan"] = strconv.Itoa(bridge.Vlan)
		}

		err = convertIPAMConfToNetwork(&network, bridge.IPAM, confPath)
		if err != nil {
			return nil, err
		}

	case types.MacVLANNetworkDriver, types.IPVLANNetworkDriver:
		var vlan VLANConfig
		err := json.Unmarshal(firstPlugin.Bytes, &vlan)
		if err != nil {
			return nil, errors.Wrapf(err, "failed to unmarshal the macvlan plugin config in %s", confPath)
		}
		network.NetworkInterface = vlan.Master

		// set network options
		if vlan.MTU != 0 {
			network.Options["mtu"] = strconv.Itoa(vlan.MTU)
		}

		if vlan.Mode != "" {
			network.Options["mode"] = vlan.Mode
		}

		err = convertIPAMConfToNetwork(&network, vlan.IPAM, confPath)
		if err != nil {
			return nil, err
		}

	default:
		// A warning would be good but users would get this warning everytime so keep this at info level.
		logrus.Infof("Unsupported CNI config type %s in %s, this network can still be used but inspect or list cannot show all information",
			firstPlugin.Network.Type, confPath)
	}

	// check if the dnsname plugin is configured
	network.DNSEnabled = findPluginByName(conf.Plugins, "dnsname")

	return &network, nil
}

func findPluginByName(plugins []*libcni.NetworkConfig, name string) bool {
	for _, plugin := range plugins {
		if plugin.Network.Type == name {
			return true
		}
	}
	return false
}

// convertIPAMConfToNetwork converts A cni IPAMConfig to libpod network subnets.
// It returns an array of subnets and an extra bool if dhcp is configured.
func convertIPAMConfToNetwork(network *types.Network, ipam ipamConfig, confPath string) error {
	if ipam.PluginType == types.DHCPIPAMDriver {
		network.IPAMOptions["driver"] = types.DHCPIPAMDriver
		return nil
	}

	if ipam.PluginType != types.HostLocalIPAMDriver {
		return errors.Errorf("unsupported ipam plugin %s in %s", ipam.PluginType, confPath)
	}

	network.IPAMOptions["driver"] = types.HostLocalIPAMDriver
	for _, r := range ipam.Ranges {
		for _, ipam := range r {
			s := types.Subnet{}

			// Do not use types.ParseCIDR() because we want the ip to be
			// the network address and not a random ip in the sub.
			_, sub, err := net.ParseCIDR(ipam.Subnet)
			if err != nil {
				return err
			}
			s.Subnet = types.IPNet{IPNet: *sub}

			// gateway
			var gateway net.IP
			if ipam.Gateway != "" {
				gateway = net.ParseIP(ipam.Gateway)
				if gateway == nil {
					return errors.Errorf("failed to parse gateway ip %s", ipam.Gateway)
				}
				// convert to 4 byte if ipv4
				ipv4 := gateway.To4()
				if ipv4 != nil {
					gateway = ipv4
				}
			} else if !network.Internal {
				// only add a gateway address if the network is not internal
				gateway, err = util.FirstIPInSubnet(sub)
				if err != nil {
					return errors.Errorf("failed to get first ip in subnet %s", sub.String())
				}
			}
			s.Gateway = gateway

			var rangeStart net.IP
			var rangeEnd net.IP
			if ipam.RangeStart != "" {
				rangeStart = net.ParseIP(ipam.RangeStart)
				if rangeStart == nil {
					return errors.Errorf("failed to parse range start ip %s", ipam.RangeStart)
				}
			}
			if ipam.RangeEnd != "" {
				rangeEnd = net.ParseIP(ipam.RangeEnd)
				if rangeEnd == nil {
					return errors.Errorf("failed to parse range end ip %s", ipam.RangeEnd)
				}
			}
			if rangeStart != nil || rangeEnd != nil {
				s.LeaseRange = &types.LeaseRange{}
				s.LeaseRange.StartIP = rangeStart
				s.LeaseRange.EndIP = rangeEnd
			}
			if util.IsIPv6(s.Subnet.IP) {
				network.IPv6Enabled = true
			}
			network.Subnets = append(network.Subnets, s)
		}
	}
	return nil
}

// getNetworkArgsFromConfList returns the map of args in a conflist, argType should be labels or options
func getNetworkArgsFromConfList(args map[string]interface{}, argType string) map[string]string {
	if args, ok := args[argType]; ok {
		if labels, ok := args.(map[string]interface{}); ok {
			result := make(map[string]string, len(labels))
			for k, v := range labels {
				if v, ok := v.(string); ok {
					result[k] = v
				}
			}
			return result
		}
	}
	return map[string]string{}
}

// createCNIConfigListFromNetwork will create a cni config file from the given network.
// It returns the cni config and the path to the file where the config was written.
// Set writeToDisk to false to only add this network into memory.
func (n *cniNetwork) createCNIConfigListFromNetwork(network *types.Network, writeToDisk bool) (*libcni.NetworkConfigList, string, error) {
	var (
		routes     []ipamRoute
		ipamRanges [][]ipamLocalHostRangeConf
		ipamConf   ipamConfig
		err        error
	)
	if len(network.Subnets) > 0 {
		for _, subnet := range network.Subnets {
			route, err := newIPAMDefaultRoute(util.IsIPv6(subnet.Subnet.IP))
			if err != nil {
				return nil, "", err
			}
			routes = append(routes, route)
			ipam := newIPAMLocalHostRange(subnet.Subnet, subnet.LeaseRange, subnet.Gateway)
			ipamRanges = append(ipamRanges, []ipamLocalHostRangeConf{*ipam})
		}
		ipamConf = newIPAMHostLocalConf(routes, ipamRanges)
	} else {
		ipamConf = ipamConfig{PluginType: "dhcp"}
	}

	vlan := 0
	mtu := 0
	vlanPluginMode := ""
	for k, v := range network.Options {
		switch k {
		case "mtu":
			mtu, err = parseMTU(v)
			if err != nil {
				return nil, "", err
			}

		case "vlan":
			vlan, err = parseVlan(v)
			if err != nil {
				return nil, "", err
			}

		case "mode":
			switch network.Driver {
			case types.MacVLANNetworkDriver:
				if !pkgutil.StringInSlice(v, []string{"", "bridge", "private", "vepa", "passthru"}) {
					return nil, "", errors.Errorf("unknown macvlan mode %q", v)
				}
			case types.IPVLANNetworkDriver:
				if !pkgutil.StringInSlice(v, []string{"", "l2", "l3", "l3s"}) {
					return nil, "", errors.Errorf("unknown ipvlan mode %q", v)
				}
			default:
				return nil, "", errors.Errorf("cannot set option \"mode\" with driver %q", network.Driver)
			}
			vlanPluginMode = v

		default:
			return nil, "", errors.Errorf("unsupported network option %s", k)
		}
	}

	isGateway := true
	ipMasq := true
	if network.Internal {
		isGateway = false
		ipMasq = false
	}
	// create CNI plugin configuration
	// explicitly use CNI version 0.4.0 here, to use v1.0.0 at least containernetwork-plugins-1.0.1 has to be installed
	// the dnsname plugin also needs to be updated for 1.0.0
	// TODO change to 1.0.0 when most distros support it
	ncList := newNcList(network.Name, "0.4.0", network.Labels, network.Options)
	var plugins []interface{}

	switch network.Driver {
	case types.BridgeNetworkDriver:
		bridge := newHostLocalBridge(network.NetworkInterface, isGateway, ipMasq, mtu, vlan, ipamConf)
		plugins = append(plugins, bridge, newPortMapPlugin(), newFirewallPlugin(), newTuningPlugin())
		// if we find the dnsname plugin we add configuration for it
		if hasDNSNamePlugin(n.cniPluginDirs) && network.DNSEnabled {
			// Note: in the future we might like to allow for dynamic domain names
			plugins = append(plugins, newDNSNamePlugin(defaultPodmanDomainName))
		}
		// Add the podman-machine CNI plugin if we are in a machine
		if n.isMachine {
			plugins = append(plugins, newPodmanMachinePlugin())
		}

	case types.MacVLANNetworkDriver:
		plugins = append(plugins, newVLANPlugin(types.MacVLANNetworkDriver, network.NetworkInterface, vlanPluginMode, mtu, ipamConf))

	case types.IPVLANNetworkDriver:
		plugins = append(plugins, newVLANPlugin(types.IPVLANNetworkDriver, network.NetworkInterface, vlanPluginMode, mtu, ipamConf))

	default:
		return nil, "", errors.Errorf("driver %q is not supported by cni", network.Driver)
	}
	ncList["plugins"] = plugins
	b, err := json.MarshalIndent(ncList, "", "   ")
	if err != nil {
		return nil, "", err
	}
	cniPathName := ""
	if writeToDisk {
		cniPathName = filepath.Join(n.cniConfigDir, network.Name+".conflist")
		err = ioutil.WriteFile(cniPathName, b, 0644)
		if err != nil {
			return nil, "", err
		}
		f, err := os.Stat(cniPathName)
		if err != nil {
			return nil, "", err
		}
		stat := f.Sys().(*syscall.Stat_t)
		network.Created = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
	} else {
		network.Created = time.Now()
	}
	config, err := libcni.ConfListFromBytes(b)
	if err != nil {
		return nil, "", err
	}
	return config, cniPathName, nil
}

// parseMTU parses the mtu option
func parseMTU(mtu string) (int, error) {
	if mtu == "" {
		return 0, nil // default
	}
	m, err := strconv.Atoi(mtu)
	if err != nil {
		return 0, err
	}
	if m < 0 {
		return 0, errors.Errorf("mtu %d is less than zero", m)
	}
	return m, nil
}

// parseVlan parses the vlan option
func parseVlan(vlan string) (int, error) {
	if vlan == "" {
		return 0, nil // default
	}
	v, err := strconv.Atoi(vlan)
	if err != nil {
		return 0, err
	}
	if v < 0 || v > 4094 {
		return 0, errors.Errorf("vlan ID %d must be between 0 and 4094", v)
	}
	return v, nil
}

func convertSpecgenPortsToCNIPorts(ports []types.PortMapping) ([]cniPortMapEntry, error) {
	cniPorts := make([]cniPortMapEntry, 0, len(ports))
	for _, port := range ports {
		if port.Protocol == "" {
			return nil, errors.New("port protocol should not be empty")
		}
		protocols := strings.Split(port.Protocol, ",")

		for _, protocol := range protocols {
			if !pkgutil.StringInSlice(protocol, []string{"tcp", "udp", "sctp"}) {
				return nil, errors.Errorf("unknown port protocol %s", protocol)
			}
			cniPort := cniPortMapEntry{
				HostPort:      int(port.HostPort),
				ContainerPort: int(port.ContainerPort),
				HostIP:        port.HostIP,
				Protocol:      protocol,
			}
			cniPorts = append(cniPorts, cniPort)
			for i := 1; i < int(port.Range); i++ {
				cniPort := cniPortMapEntry{
					HostPort:      int(port.HostPort) + i,
					ContainerPort: int(port.ContainerPort) + i,
					HostIP:        port.HostIP,
					Protocol:      protocol,
				}
				cniPorts = append(cniPorts, cniPort)
			}
		}
	}
	return cniPorts, nil
}
