package network

import (
	"encoding/json"
	"net"
	"os"

	"github.com/containernetworking/cni/libcni"
	"github.com/containernetworking/cni/pkg/types"
	"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator"
	"github.com/containers/common/pkg/config"
	"github.com/containers/podman/v3/libpod/define"
	"github.com/containers/podman/v3/pkg/domain/entities"
	"github.com/containers/podman/v3/pkg/rootless"
	"github.com/containers/podman/v3/pkg/util"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

var (
	// BridgeNetworkDriver defines the bridge cni driver
	BridgeNetworkDriver = "bridge"
	// DefaultNetworkDriver is the default network type used
	DefaultNetworkDriver = BridgeNetworkDriver
	// MacVLANNetworkDriver defines the macvlan cni driver
	MacVLANNetworkDriver = "macvlan"
)

// SupportedNetworkDrivers describes the list of supported drivers
var SupportedNetworkDrivers = []string{BridgeNetworkDriver, MacVLANNetworkDriver}

// isSupportedDriver checks if the user provided driver is supported
func isSupportedDriver(driver string) error {
	if util.StringInSlice(driver, SupportedNetworkDrivers) {
		return nil
	}
	return errors.Errorf("driver '%s' is not supported", driver)
}

// GetLiveNetworks returns a slice of networks representing what the system
// has defined as network interfaces
func GetLiveNetworks() ([]*net.IPNet, error) {
	addrs, err := net.InterfaceAddrs()
	if err != nil {
		return nil, err
	}
	nets := make([]*net.IPNet, 0, len(addrs))
	for _, address := range addrs {
		_, n, err := net.ParseCIDR(address.String())
		if err != nil {
			return nil, err
		}
		nets = append(nets, n)
	}
	return nets, nil
}

// GetLiveNetworkNames returns a list of network interfaces on the system
func GetLiveNetworkNames() ([]string, error) {
	liveInterfaces, err := net.Interfaces()
	if err != nil {
		return nil, err
	}
	interfaceNames := make([]string, 0, len(liveInterfaces))
	for _, i := range liveInterfaces {
		interfaceNames = append(interfaceNames, i.Name)
	}
	return interfaceNames, nil
}

// GetFreeNetwork looks for a free network according to existing cni configuration
// files and network interfaces.
func GetFreeNetwork(config *config.Config) (*net.IPNet, error) {
	networks, err := GetNetworksFromFilesystem(config)
	if err != nil {
		return nil, err
	}
	liveNetworks, err := GetLiveNetworks()
	if err != nil {
		return nil, err
	}
	nextNetwork, err := GetDefaultPodmanNetwork()
	if err != nil {
		return nil, err
	}
	logrus.Debugf("default network is %s", nextNetwork.String())
	for {
		newNetwork, err := NextSubnet(nextNetwork)
		if err != nil {
			return nil, err
		}
		logrus.Debugf("checking if network %s intersects with other cni networks", nextNetwork.String())
		if intersectsConfig, _ := networkIntersectsWithNetworks(newNetwork, allocatorToIPNets(networks)); intersectsConfig {
			logrus.Debugf("network %s is already being used by a cni configuration", nextNetwork.String())
			nextNetwork = newNetwork
			continue
		}
		logrus.Debugf("checking if network %s intersects with any network interfaces", nextNetwork.String())
		if intersectsLive, _ := networkIntersectsWithNetworks(newNetwork, liveNetworks); !intersectsLive {
			break
		}
		logrus.Debugf("network %s is being used by a network interface", nextNetwork.String())
		nextNetwork = newNetwork
	}
	return nextNetwork, nil
}

func allocatorToIPNets(networks []*allocator.Net) []*net.IPNet {
	var nets []*net.IPNet
	for _, network := range networks {
		if len(network.IPAM.Ranges) > 0 {
			// this is the new IPAM range style
			// append each subnet from ipam the rangeset
			for _, r := range network.IPAM.Ranges[0] {
				nets = append(nets, newIPNetFromSubnet(r.Subnet))
			}
		} else {
			//	 looks like the old, deprecated style
			nets = append(nets, newIPNetFromSubnet(network.IPAM.Subnet))
		}
	}
	return nets
}

func newIPNetFromSubnet(subnet types.IPNet) *net.IPNet {
	n := net.IPNet{
		IP:   subnet.IP,
		Mask: subnet.Mask,
	}
	return &n
}

func networkIntersectsWithNetworks(n *net.IPNet, networklist []*net.IPNet) (bool, *net.IPNet) {
	for _, nw := range networklist {
		if networkIntersect(n, nw) {
			return true, nw
		}
	}
	return false, nil
}

func networkIntersect(n1, n2 *net.IPNet) bool {
	return n2.Contains(n1.IP) || n1.Contains(n2.IP)
}

// ValidateUserNetworkIsAvailable returns via an error if a network is available
// to be used
func ValidateUserNetworkIsAvailable(config *config.Config, userNet *net.IPNet) error {
	if len(userNet.IP) == 0 || len(userNet.Mask) == 0 {
		return errors.Errorf("network %s's ip or mask cannot be empty", userNet.String())
	}

	ones, bit := userNet.Mask.Size()
	if ones == 0 || bit == 0 {
		return errors.Errorf("network %s's mask is invalid", userNet.String())
	}

	networks, err := GetNetworksFromFilesystem(config)
	if err != nil {
		return err
	}
	liveNetworks, err := GetLiveNetworks()
	if err != nil {
		return err
	}
	logrus.Debugf("checking if network %s exists in cni networks", userNet.String())
	if intersectsConfig, _ := networkIntersectsWithNetworks(userNet, allocatorToIPNets(networks)); intersectsConfig {
		return errors.Errorf("network %s is already being used by a cni configuration", userNet.String())
	}
	logrus.Debugf("checking if network %s exists in any network interfaces", userNet.String())
	if intersectsLive, _ := networkIntersectsWithNetworks(userNet, liveNetworks); intersectsLive {
		return errors.Errorf("network %s is being used by a network interface", userNet.String())
	}
	return nil
}

// removeNetwork is removes a cni network without a lock and should only be called
// when a lock was otherwise acquired.
func removeNetwork(config *config.Config, name string) error {
	cniPath, err := GetCNIConfigPathByNameOrID(config, name)
	if err != nil {
		return err
	}
	// Before we delete the configuration file, we need to make sure we can read and parse
	// it to get the network interface name so we can remove that too
	interfaceName, err := GetInterfaceNameFromConfig(cniPath)
	if err == nil {
		// Don't try to remove the network interface if we are not root
		if !rootless.IsRootless() {
			liveNetworkNames, err := GetLiveNetworkNames()
			if err != nil {
				return errors.Wrapf(err, "failed to get live network names")
			}
			if util.StringInSlice(interfaceName, liveNetworkNames) {
				if err := RemoveInterface(interfaceName); err != nil {
					return errors.Wrapf(err, "failed to delete the network interface %q", interfaceName)
				}
			}
		}
	} else if err != ErrNoSuchNetworkInterface {
		// Don't error if we couldn't find the network interface name
		return err
	}
	// Remove the configuration file
	if err := os.Remove(cniPath); err != nil {
		return errors.Wrap(err, "failed to remove network configuration")
	}
	return nil
}

// RemoveNetwork removes a given network by name.  If the network has container associated with it, that
// must be handled outside the context of this.
func RemoveNetwork(config *config.Config, name string) error {
	l, err := acquireCNILock(config)
	if err != nil {
		return err
	}
	defer l.releaseCNILock()
	return removeNetwork(config, name)
}

// InspectNetwork reads a CNI config and returns its configuration
func InspectNetwork(config *config.Config, name string) (map[string]interface{}, error) {
	b, err := ReadRawCNIConfByNameOrID(config, name)
	if err != nil {
		return nil, err
	}
	rawList := make(map[string]interface{})
	err = json.Unmarshal(b, &rawList)
	return rawList, err
}

// Exists says whether a given network exists or not; it meant
// specifically for restful responses so 404s can be used
func Exists(config *config.Config, name string) (bool, error) {
	_, err := ReadRawCNIConfByNameOrID(config, name)
	if err != nil {
		if errors.Cause(err) == define.ErrNoSuchNetwork {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

// PruneNetworks removes networks that are not being used and that is not the default
// network.  To keep proper fencing for imports, you must provide the used networks
// to this function as a map.  the key is meaningful in the map, the book is a no-op
func PruneNetworks(rtc *config.Config, usedNetworks map[string]bool) ([]*entities.NetworkPruneReport, error) {
	var reports []*entities.NetworkPruneReport
	lock, err := acquireCNILock(rtc)
	if err != nil {
		return nil, err
	}
	defer lock.releaseCNILock()
	nets, err := GetNetworkNamesFromFileSystem(rtc)
	if err != nil {
		return nil, err
	}
	for _, n := range nets {
		_, found := usedNetworks[n]
		// Remove is not default network and not found in the used list
		if n != rtc.Network.DefaultNetwork && !found {
			reports = append(reports, &entities.NetworkPruneReport{
				Name:  n,
				Error: removeNetwork(rtc, n),
			})
		}
	}
	return reports, nil
}

// NormalizeName translates a network ID into a name.
// If the input is a name the name is returned.
func NormalizeName(config *config.Config, nameOrID string) (string, error) {
	path, err := GetCNIConfigPathByNameOrID(config, nameOrID)
	if err != nil {
		return "", err
	}
	conf, err := libcni.ConfListFromFile(path)
	if err != nil {
		return "", err
	}
	return conf.Name, nil
}