package network

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"sort"
	"strings"

	"github.com/containernetworking/cni/libcni"
	"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/pkg/errors"
	"github.com/sirupsen/logrus"
)

// ErrNoSuchNetworkInterface indicates that no network interface exists
var ErrNoSuchNetworkInterface = errors.New("unable to find interface name for network")

// GetCNIConfDir get CNI configuration directory
func GetCNIConfDir(configArg *config.Config) string {
	if len(configArg.Network.NetworkConfigDir) < 1 {
		dc, err := config.DefaultConfig()
		if err != nil {
			// Fallback to hard-coded dir
			return CNIConfigDir
		}
		return dc.Network.NetworkConfigDir
	}
	return configArg.Network.NetworkConfigDir
}

// LoadCNIConfsFromDir loads all the CNI configurations from a dir
func LoadCNIConfsFromDir(dir string) ([]*libcni.NetworkConfigList, error) {
	files, err := libcni.ConfFiles(dir, []string{".conflist"})
	if err != nil {
		return nil, err
	}
	sort.Strings(files)

	configs := make([]*libcni.NetworkConfigList, 0, len(files))
	for _, confFile := range files {
		conf, err := libcni.ConfListFromFile(confFile)
		if err != nil {
			return nil, errors.Wrapf(err, "in %s", confFile)
		}
		configs = append(configs, conf)
	}
	return configs, nil
}

// GetCNIConfigPathByNameOrID finds a CNI network by name and
// returns its configuration file path
func GetCNIConfigPathByNameOrID(config *config.Config, name string) (string, error) {
	files, err := libcni.ConfFiles(GetCNIConfDir(config), []string{".conflist"})
	if err != nil {
		return "", err
	}
	idMatch := 0
	file := ""
	for _, confFile := range files {
		conf, err := libcni.ConfListFromFile(confFile)
		if err != nil {
			return "", errors.Wrapf(err, "in %s", confFile)
		}
		if conf.Name == name {
			return confFile, nil
		}
		if strings.HasPrefix(GetNetworkID(conf.Name), name) {
			idMatch++
			file = confFile
		}
	}
	if idMatch == 1 {
		return file, nil
	}
	if idMatch > 1 {
		return "", errors.Errorf("more than one result for network ID %s", name)
	}
	return "", errors.Wrap(define.ErrNoSuchNetwork, fmt.Sprintf("unable to find network configuration for %s", name))
}

// ReadRawCNIConfByName reads the raw CNI configuration for a CNI
// network by name
func ReadRawCNIConfByName(config *config.Config, name string) ([]byte, error) {
	confFile, err := GetCNIConfigPathByNameOrID(config, name)
	if err != nil {
		return nil, err
	}
	b, err := ioutil.ReadFile(confFile)
	return b, err
}

// GetCNIPlugins returns a list of plugins that a given network
// has in the form of a string
func GetCNIPlugins(list *libcni.NetworkConfigList) string {
	plugins := make([]string, 0, len(list.Plugins))
	for _, plug := range list.Plugins {
		plugins = append(plugins, plug.Network.Type)
	}
	return strings.Join(plugins, ",")
}

// GetNetworkLabels returns a list of labels as a string
func GetNetworkLabels(list *libcni.NetworkConfigList) NcLabels {
	cniJSON := make(map[string]interface{})
	err := json.Unmarshal(list.Bytes, &cniJSON)
	if err != nil {
		logrus.Errorf("failed to unmarshal network config %v %v", cniJSON["name"], err)
		return nil
	}
	if args, ok := cniJSON["args"]; ok {
		if key, ok := args.(map[string]interface{}); ok {
			if labels, ok := key[PodmanLabelKey]; ok {
				if labels, ok := labels.(map[string]interface{}); ok {
					result := make(NcLabels, len(labels))
					for k, v := range labels {
						if v, ok := v.(string); ok {
							result[k] = v
						} else {
							logrus.Errorf("network config %v invalid label value type %T should be string", cniJSON["name"], labels)
						}
					}
					return result
				}
				logrus.Errorf("network config %v invalid label type %T should be map[string]string", cniJSON["name"], labels)
			}
		}
	}
	return nil
}

// GetNetworksFromFilesystem gets all the networks from the cni configuration
// files
func GetNetworksFromFilesystem(config *config.Config) ([]*allocator.Net, error) {
	var cniNetworks []*allocator.Net

	networks, err := LoadCNIConfsFromDir(GetCNIConfDir(config))
	if err != nil {
		return nil, err
	}
	for _, n := range networks {
		for _, cniplugin := range n.Plugins {
			if cniplugin.Network.Type == "bridge" {
				ipamConf := allocator.Net{}
				if err := json.Unmarshal(cniplugin.Bytes, &ipamConf); err != nil {
					return nil, err
				}
				cniNetworks = append(cniNetworks, &ipamConf)
				break
			}
		}
	}
	return cniNetworks, nil
}

// GetNetworkNamesFromFileSystem gets all the names from the cni network
// configuration files
func GetNetworkNamesFromFileSystem(config *config.Config) ([]string, error) {
	networks, err := LoadCNIConfsFromDir(GetCNIConfDir(config))
	if err != nil {
		return nil, err
	}
	networkNames := []string{}
	for _, n := range networks {
		networkNames = append(networkNames, n.Name)
	}
	return networkNames, nil
}

// GetInterfaceNameFromConfig returns the interface name for the bridge plugin
func GetInterfaceNameFromConfig(path string) (string, error) {
	var name string
	conf, err := libcni.ConfListFromFile(path)
	if err != nil {
		return "", err
	}
	for _, cniplugin := range conf.Plugins {
		if cniplugin.Network.Type == "bridge" {
			plugin := make(map[string]interface{})
			if err := json.Unmarshal(cniplugin.Bytes, &plugin); err != nil {
				return "", err
			}
			name = plugin["bridge"].(string)
			break
		}
	}
	if len(name) == 0 {
		return "", ErrNoSuchNetworkInterface
	}
	return name, nil
}

// GetBridgeNamesFromFileSystem is a convenience function to get all the bridge
// names from the configured networks
func GetBridgeNamesFromFileSystem(config *config.Config) ([]string, error) {
	networks, err := LoadCNIConfsFromDir(GetCNIConfDir(config))
	if err != nil {
		return nil, err
	}

	bridgeNames := []string{}
	for _, n := range networks {
		var name string
		// iterate network conflists
		for _, cniplugin := range n.Plugins {
			// iterate plugins
			if cniplugin.Network.Type == "bridge" {
				plugin := make(map[string]interface{})
				if err := json.Unmarshal(cniplugin.Bytes, &plugin); err != nil {
					continue
				}
				name = plugin["bridge"].(string)
			}
		}
		bridgeNames = append(bridgeNames, name)
	}
	return bridgeNames, nil
}
