// +build linux

package cni

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net"
	"os"
	"strings"
	"time"

	"github.com/containernetworking/cni/libcni"
	"github.com/containers/podman/v3/libpod/define"
	"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/containers/storage/pkg/lockfile"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

type cniNetwork struct {
	// cniConfigDir is directory where the cni config files are stored.
	cniConfigDir string
	// cniPluginDirs is a list of directories where cni should look for the plugins.
	cniPluginDirs []string

	cniConf *libcni.CNIConfig

	// defaultNetwork is the name for the default network.
	defaultNetwork string
	// defaultSubnet is the default subnet for the default network.
	defaultSubnet types.IPNet

	// isMachine describes whenever podman runs in a podman machine environment.
	isMachine bool

	// lock is a internal lock for critical operations
	lock lockfile.Locker

	// modTime is the timestamp when the config dir was modified
	modTime time.Time

	// networks is a map with loaded networks, the key is the network name
	networks map[string]*network
}

type network struct {
	// filename is the full path to the cni config file on disk
	filename  string
	libpodNet *types.Network
	cniNet    *libcni.NetworkConfigList
}

type InitConfig struct {
	// CNIConfigDir is directory where the cni config files are stored.
	CNIConfigDir string
	// CNIPluginDirs is a list of directories where cni should look for the plugins.
	CNIPluginDirs []string

	// DefaultNetwork is the name for the default network.
	DefaultNetwork string
	// DefaultSubnet is the default subnet for the default network.
	DefaultSubnet string

	// IsMachine describes whenever podman runs in a podman machine environment.
	IsMachine bool

	// LockFile is the path to lock file.
	LockFile string
}

// NewCNINetworkInterface creates the ContainerNetwork interface for the CNI backend.
// Note: The networks are not loaded from disk until a method is called.
func NewCNINetworkInterface(conf InitConfig) (types.ContainerNetwork, error) {
	// TODO: consider using a shared memory lock
	lock, err := lockfile.GetLockfile(conf.LockFile)
	if err != nil {
		return nil, err
	}

	defaultNetworkName := conf.DefaultNetwork
	if defaultNetworkName == "" {
		defaultNetworkName = types.DefaultNetworkName
	}

	defaultSubnet := conf.DefaultSubnet
	if defaultSubnet == "" {
		defaultSubnet = types.DefaultSubnet
	}
	defaultNet, err := types.ParseCIDR(defaultSubnet)
	if err != nil {
		return nil, errors.Wrap(err, "failed to parse default subnet")
	}

	cni := libcni.NewCNIConfig(conf.CNIPluginDirs, &cniExec{})
	n := &cniNetwork{
		cniConfigDir:   conf.CNIConfigDir,
		cniPluginDirs:  conf.CNIPluginDirs,
		cniConf:        cni,
		defaultNetwork: defaultNetworkName,
		defaultSubnet:  defaultNet,
		isMachine:      conf.IsMachine,
		lock:           lock,
	}

	return n, nil
}

// Drivers will return the list of supported network drivers
// for this interface.
func (n *cniNetwork) Drivers() []string {
	return []string{types.BridgeNetworkDriver, types.MacVLANNetworkDriver, types.IPVLANNetworkDriver}
}

func (n *cniNetwork) loadNetworks() error {
	// check the mod time of the config dir
	f, err := os.Stat(n.cniConfigDir)
	if err != nil {
		return err
	}
	modTime := f.ModTime()

	// skip loading networks if they are already loaded and
	// if the config dir was not modified since the last call
	if n.networks != nil && modTime.Equal(n.modTime) {
		return nil
	}
	// make sure the remove all networks before we reload them
	n.networks = nil
	n.modTime = modTime

	// FIXME: do we have to support other file types as well, e.g. .conf?
	files, err := libcni.ConfFiles(n.cniConfigDir, []string{".conflist"})
	if err != nil {
		return err
	}
	networks := make(map[string]*network, len(files))
	for _, file := range files {
		conf, err := libcni.ConfListFromFile(file)
		if err != nil {
			// do not log ENOENT errors
			if !errors.Is(err, os.ErrNotExist) {
				logrus.Warnf("Error loading CNI config file %s: %v", file, err)
			}
			continue
		}

		if !define.NameRegex.MatchString(conf.Name) {
			logrus.Warnf("CNI config list %s has invalid name, skipping: %v", file, define.RegexError)
			continue
		}

		if _, err := n.cniConf.ValidateNetworkList(context.Background(), conf); err != nil {
			logrus.Warnf("Error validating CNI config file %s: %v", file, err)
			continue
		}

		if val, ok := networks[conf.Name]; ok {
			logrus.Warnf("CNI config list %s has the same network name as %s, skipping", file, val.filename)
			continue
		}

		net, err := createNetworkFromCNIConfigList(conf, file)
		if err != nil {
			logrus.Errorf("CNI config list %s could not be converted to a libpod config, skipping: %v", file, err)
			continue
		}
		logrus.Debugf("Successfully loaded network %s: %v", net.Name, net)
		networkInfo := network{
			filename:  file,
			cniNet:    conf,
			libpodNet: net,
		}
		networks[net.Name] = &networkInfo
	}

	// create the default network in memory if it did not exists on disk
	if networks[n.defaultNetwork] == nil {
		networkInfo, err := n.createDefaultNetwork()
		if err != nil {
			return errors.Wrapf(err, "failed to create default network %s", n.defaultNetwork)
		}
		networks[n.defaultNetwork] = networkInfo
	}

	logrus.Debugf("Successfully loaded %d networks", len(networks))
	n.networks = networks
	return nil
}

func (n *cniNetwork) createDefaultNetwork() (*network, error) {
	net := types.Network{
		Name:             n.defaultNetwork,
		NetworkInterface: "cni-podman0",
		Driver:           types.BridgeNetworkDriver,
		Subnets: []types.Subnet{
			{Subnet: n.defaultSubnet},
		},
	}
	return n.networkCreate(net, true)
}

// getNetwork will lookup a network by name or ID. It returns an
// error when no network was found or when more than one network
// with the given (partial) ID exists.
// getNetwork will read from the networks map, therefore the caller
// must ensure that n.lock is locked before using it.
func (n *cniNetwork) getNetwork(nameOrID string) (*network, error) {
	// fast path check the map key, this will only work for names
	if val, ok := n.networks[nameOrID]; ok {
		return val, nil
	}
	// If there was no match we might got a full or partial ID.
	var net *network
	for _, val := range n.networks {
		// This should not happen because we already looked up the map by name but check anyway.
		if val.libpodNet.Name == nameOrID {
			return val, nil
		}

		if strings.HasPrefix(val.libpodNet.ID, nameOrID) {
			if net != nil {
				return nil, errors.Errorf("more than one result for network ID %s", nameOrID)
			}
			net = val
		}
	}
	if net != nil {
		return net, nil
	}
	return nil, errors.Wrapf(define.ErrNoSuchNetwork, "unable to find network with name or ID %s", nameOrID)
}

// getNetworkIDFromName creates a network ID from the name. It is just the
// sha256 hash so it is not safe but it should be safe enough for our use case.
func getNetworkIDFromName(name string) string {
	hash := sha256.Sum256([]byte(name))
	return hex.EncodeToString(hash[:])
}

// getFreeIPv6NetworkSubnet returns a unused ipv4 subnet
func (n *cniNetwork) getFreeIPv4NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) {
	// the default podman network is 10.88.0.0/16
	// start locking for free /24 networks
	network := &net.IPNet{
		IP:   net.IP{10, 89, 0, 0},
		Mask: net.IPMask{255, 255, 255, 0},
	}

	// TODO: make sure to not use public subnets
	for {
		if intersectsConfig := util.NetworkIntersectsWithNetworks(network, usedNetworks); !intersectsConfig {
			logrus.Debugf("found free ipv4 network subnet %s", network.String())
			return &types.Subnet{
				Subnet: types.IPNet{IPNet: *network},
			}, nil
		}
		var err error
		network, err = util.NextSubnet(network)
		if err != nil {
			return nil, err
		}
	}
}

// getFreeIPv6NetworkSubnet returns a unused ipv6 subnet
func (n *cniNetwork) getFreeIPv6NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) {
	// FIXME: Is 10000 fine as limit? We should prevent an endless loop.
	for i := 0; i < 10000; i++ {
		// RFC4193: Choose the ipv6 subnet random and NOT sequentially.
		network, err := util.GetRandomIPv6Subnet()
		if err != nil {
			return nil, err
		}
		if intersectsConfig := util.NetworkIntersectsWithNetworks(&network, usedNetworks); !intersectsConfig {
			logrus.Debugf("found free ipv6 network subnet %s", network.String())
			return &types.Subnet{
				Subnet: types.IPNet{IPNet: network},
			}, nil
		}
	}
	return nil, errors.New("failed to get random ipv6 subnet")
}

// getUsedSubnets returns a list of all used subnets by network
// configs and interfaces on the host.
func (n *cniNetwork) getUsedSubnets() ([]*net.IPNet, error) {
	// first, load all used subnets from network configs
	subnets := make([]*net.IPNet, 0, len(n.networks))
	for _, val := range n.networks {
		for i := range val.libpodNet.Subnets {
			subnets = append(subnets, &val.libpodNet.Subnets[i].Subnet.IPNet)
		}
	}
	// second, load networks from the current system
	liveSubnets, err := util.GetLiveNetworkSubnets()
	if err != nil {
		return nil, err
	}
	return append(subnets, liveSubnets...), nil
}

// getFreeDeviceName returns a free device name which can
// be used for new configs as name and bridge interface name
func (n *cniNetwork) getFreeDeviceName() (string, error) {
	bridgeNames := n.getBridgeInterfaceNames()
	netNames := n.getUsedNetworkNames()
	liveInterfaces, err := util.GetLiveNetworkNames()
	if err != nil {
		return "", nil
	}
	names := make([]string, 0, len(bridgeNames)+len(netNames)+len(liveInterfaces))
	names = append(names, bridgeNames...)
	names = append(names, netNames...)
	names = append(names, liveInterfaces...)
	// FIXME: Is a limit fine?
	// Start by 1, 0 is reserved for the default network
	for i := 1; i < 1000000; i++ {
		deviceName := fmt.Sprintf("%s%d", cniDeviceName, i)
		if !pkgutil.StringInSlice(deviceName, names) {
			logrus.Debugf("found free device name %s", deviceName)
			return deviceName, nil
		}
	}
	return "", errors.New("could not find free device name, to many iterations")
}

// getUsedNetworkNames returns all network names already used
// by network configs
func (n *cniNetwork) getUsedNetworkNames() []string {
	names := make([]string, 0, len(n.networks))
	for _, val := range n.networks {
		names = append(names, val.libpodNet.Name)
	}
	return names
}

// getUsedNetworkNames returns all bridge device names already used
// by network configs
func (n *cniNetwork) getBridgeInterfaceNames() []string {
	names := make([]string, 0, len(n.networks))
	for _, val := range n.networks {
		if val.libpodNet.Driver == types.BridgeNetworkDriver {
			names = append(names, val.libpodNet.NetworkInterface)
		}
	}
	return names
}
