package abi

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"path/filepath"
	"strings"

	"github.com/containernetworking/cni/libcni"
	cniversion "github.com/containernetworking/cni/pkg/version"
	"github.com/containers/libpod/libpod"
	"github.com/containers/libpod/pkg/domain/entities"
	"github.com/containers/libpod/pkg/network"
	"github.com/containers/libpod/pkg/util"
	"github.com/pkg/errors"
)

func (ic *ContainerEngine) NetworkList(ctx context.Context, options entities.NetworkListOptions) ([]*entities.NetworkListReport, error) {
	var reports []*entities.NetworkListReport

	config, err := ic.Libpod.GetConfig()
	if err != nil {
		return nil, err
	}

	networks, err := network.LoadCNIConfsFromDir(network.GetCNIConfDir(config))
	if err != nil {
		return nil, err
	}

	var tokens []string
	// tokenize the networkListOptions.Filter in key=value.
	if len(options.Filter) > 0 {
		tokens = strings.Split(options.Filter, "=")
		if len(tokens) != 2 {
			return nil, fmt.Errorf("invalid filter syntax : %s", options.Filter)
		}
	}

	for _, n := range networks {
		if ifPassesFilterTest(n, tokens) {
			reports = append(reports, &entities.NetworkListReport{NetworkConfigList: n})
		}
	}
	return reports, nil
}

func (ic *ContainerEngine) NetworkInspect(ctx context.Context, namesOrIds []string, options entities.NetworkInspectOptions) ([]entities.NetworkInspectReport, error) {
	config, err := ic.Libpod.GetConfig()
	if err != nil {
		return nil, err
	}

	rawCNINetworks := make([]entities.NetworkInspectReport, 0, len(namesOrIds))
	for _, name := range namesOrIds {
		rawList, err := network.InspectNetwork(config, name)
		if err != nil {
			return nil, err
		}
		rawCNINetworks = append(rawCNINetworks, rawList)
	}
	return rawCNINetworks, nil
}

func (ic *ContainerEngine) NetworkRm(ctx context.Context, namesOrIds []string, options entities.NetworkRmOptions) ([]*entities.NetworkRmReport, error) {
	reports := []*entities.NetworkRmReport{}

	config, err := ic.Libpod.GetConfig()
	if err != nil {
		return nil, err
	}

	for _, name := range namesOrIds {
		report := entities.NetworkRmReport{Name: name}
		containers, err := ic.Libpod.GetAllContainers()
		if err != nil {
			return reports, err
		}
		// We need to iterate containers looking to see if they belong to the given network
		for _, c := range containers {
			if util.StringInSlice(name, c.Config().Networks) {
				// if user passes force, we nuke containers
				if !options.Force {
					// Without the force option, we return an error
					return reports, errors.Errorf("%q has associated containers with it. Use -f to forcibly delete containers", name)
				}
				if err := ic.Libpod.RemoveContainer(ctx, c, true, true); err != nil {
					return reports, err
				}
			}
		}
		if err := network.RemoveNetwork(config, name); err != nil {
			report.Err = err
		}
		reports = append(reports, &report)
	}
	return reports, nil
}

func (ic *ContainerEngine) NetworkCreate(ctx context.Context, name string, options entities.NetworkCreateOptions) (*entities.NetworkCreateReport, error) {
	var (
		err      error
		fileName string
	)
	if len(options.MacVLAN) > 0 {
		fileName, err = createMacVLAN(ic.Libpod, name, options)
	} else {
		fileName, err = createBridge(ic.Libpod, name, options)
	}
	if err != nil {
		return nil, err
	}
	return &entities.NetworkCreateReport{Filename: fileName}, nil
}

// createBridge creates a CNI network
func createBridge(r *libpod.Runtime, name string, options entities.NetworkCreateOptions) (string, error) {
	isGateway := true
	ipMasq := true
	subnet := &options.Subnet
	ipRange := options.Range
	runtimeConfig, err := r.GetConfig()
	if err != nil {
		return "", err
	}
	// if range is provided, make sure it is "in" network
	if subnet.IP != nil {
		// if network is provided, does it conflict with existing CNI or live networks
		err = network.ValidateUserNetworkIsAvailable(runtimeConfig, subnet)
	} else {
		// if no network is provided, figure out network
		subnet, err = network.GetFreeNetwork(runtimeConfig)
	}
	if err != nil {
		return "", err
	}
	gateway := options.Gateway
	if gateway == nil {
		// if no gateway is provided, provide it as first ip of network
		gateway = network.CalcGatewayIP(subnet)
	}
	// if network is provided and if gateway is provided, make sure it is "in" network
	if options.Subnet.IP != nil && options.Gateway != nil {
		if !subnet.Contains(gateway) {
			return "", errors.Errorf("gateway %s is not in valid for subnet %s", gateway.String(), subnet.String())
		}
	}
	if options.Internal {
		isGateway = false
		ipMasq = false
	}

	// if a range is given, we need to ensure it is "in" the network range.
	if options.Range.IP != nil {
		if options.Subnet.IP == nil {
			return "", errors.New("you must define a subnet range to define an ip-range")
		}
		firstIP, err := network.FirstIPInSubnet(&options.Range)
		if err != nil {
			return "", err
		}
		lastIP, err := network.LastIPInSubnet(&options.Range)
		if err != nil {
			return "", err
		}
		if !subnet.Contains(firstIP) || !subnet.Contains(lastIP) {
			return "", errors.Errorf("the ip range %s does not fall within the subnet range %s", options.Range.String(), subnet.String())
		}
	}
	bridgeDeviceName, err := network.GetFreeDeviceName(runtimeConfig)
	if err != nil {
		return "", err
	}

	if len(name) > 0 {
		netNames, err := network.GetNetworkNamesFromFileSystem(runtimeConfig)
		if err != nil {
			return "", err
		}
		if util.StringInSlice(name, netNames) {
			return "", errors.Errorf("the network name %s is already used", name)
		}
	} else {
		// If no name is given, we give the name of the bridge device
		name = bridgeDeviceName
	}

	ncList := network.NewNcList(name, cniversion.Current())
	var plugins []network.CNIPlugins
	var routes []network.IPAMRoute

	defaultRoute, err := network.NewIPAMDefaultRoute()
	if err != nil {
		return "", err
	}
	routes = append(routes, defaultRoute)
	ipamConfig, err := network.NewIPAMHostLocalConf(subnet, routes, ipRange, gateway)
	if err != nil {
		return "", err
	}

	// TODO need to iron out the role of isDefaultGW and IPMasq
	bridge := network.NewHostLocalBridge(bridgeDeviceName, isGateway, false, ipMasq, ipamConfig)
	plugins = append(plugins, bridge)
	plugins = append(plugins, network.NewPortMapPlugin())
	plugins = append(plugins, network.NewFirewallPlugin())
	// if we find the dnsname plugin, we add configuration for it
	if network.HasDNSNamePlugin(runtimeConfig.Network.CNIPluginDirs) && !options.DisableDNS {
		// Note: in the future we might like to allow for dynamic domain names
		plugins = append(plugins, network.NewDNSNamePlugin(network.DefaultPodmanDomainName))
	}
	ncList["plugins"] = plugins
	b, err := json.MarshalIndent(ncList, "", "   ")
	if err != nil {
		return "", err
	}
	cniPathName := filepath.Join(network.GetCNIConfDir(runtimeConfig), fmt.Sprintf("%s.conflist", name))
	err = ioutil.WriteFile(cniPathName, b, 0644)
	return cniPathName, err
}

func createMacVLAN(r *libpod.Runtime, name string, options entities.NetworkCreateOptions) (string, error) {
	var (
		plugins []network.CNIPlugins
	)
	liveNetNames, err := network.GetLiveNetworkNames()
	if err != nil {
		return "", err
	}

	config, err := r.GetConfig()
	if err != nil {
		return "", err
	}

	// Make sure the host-device exists
	if !util.StringInSlice(options.MacVLAN, liveNetNames) {
		return "", errors.Errorf("failed to find network interface %q", options.MacVLAN)
	}
	if len(name) > 0 {
		netNames, err := network.GetNetworkNamesFromFileSystem(config)
		if err != nil {
			return "", err
		}
		if util.StringInSlice(name, netNames) {
			return "", errors.Errorf("the network name %s is already used", name)
		}
	} else {
		name, err = network.GetFreeDeviceName(config)
		if err != nil {
			return "", err
		}
	}
	ncList := network.NewNcList(name, cniversion.Current())
	macvlan := network.NewMacVLANPlugin(options.MacVLAN)
	plugins = append(plugins, macvlan)
	ncList["plugins"] = plugins
	b, err := json.MarshalIndent(ncList, "", "   ")
	if err != nil {
		return "", err
	}
	cniPathName := filepath.Join(network.GetCNIConfDir(config), fmt.Sprintf("%s.conflist", name))
	err = ioutil.WriteFile(cniPathName, b, 0644)
	return cniPathName, err
}

func ifPassesFilterTest(netconf *libcni.NetworkConfigList, filter []string) bool {
	result := false
	if len(filter) == 0 {
		// No filter, so pass
		return true
	}
	switch strings.ToLower(filter[0]) {
	case "name":
		if filter[1] == netconf.Name {
			result = true
		}
	case "plugin":
		plugins := network.GetCNIPlugins(netconf)
		if strings.Contains(plugins, filter[1]) {
			result = true
		}
	default:
		result = false
	}
	return result
}
