package util

import (
	"errors"
	"fmt"
	"net"
	"os"
	"strings"

	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"
	apinet "k8s.io/apimachinery/pkg/util/net"
	netutils "k8s.io/utils/net"
)

// JoinIPs stringifies and joins a list of IP addresses with commas.
func JoinIPs(elems []net.IP) string {
	var strs []string
	for _, elem := range elems {
		strs = append(strs, elem.String())
	}
	return strings.Join(strs, ",")
}

// JoinIPNets stringifies and joins a list of IP networks with commas.
func JoinIPNets(elems []*net.IPNet) string {
	var strs []string
	for _, elem := range elems {
		strs = append(strs, elem.String())
	}
	return strings.Join(strs, ",")
}

// getFirst4Net returns the first IPv4 network from the list of IP networks.
// If no IPv4 addresses are found, an error is raised.
func getFirst4Net(elems []*net.IPNet) (*net.IPNet, error) {
	for _, elem := range elems {
		if elem == nil || elem.IP.To4() == nil {
			continue
		}
		return elem, nil
	}
	return nil, errors.New("no IPv4 CIDRs found")
}

// getFirst4 returns the first IPv4 address from the list of IP addresses.
// If no IPv4 addresses are found, an error is raised.
func getFirst4(elems []net.IP) (net.IP, error) {
	for _, elem := range elems {
		if elem == nil || elem.To4() == nil {
			continue
		}
		return elem, nil
	}
	return nil, errors.New("no IPv4 address found")
}

// GetFirst4String returns the first IPv4 address from a list of IP address strings.
// If no IPv4 addresses are found, an error is raised.
func GetFirst4String(elems []string) (string, error) {
	ips := []net.IP{}
	for _, elem := range elems {
		for _, v := range strings.Split(elem, ",") {
			ips = append(ips, net.ParseIP(v))
		}
	}
	ip, err := getFirst4(ips)
	if err != nil {
		return "", err
	}
	return ip.String(), nil
}

// JoinIP4Nets stringifies and joins a list of IPv4 networks with commas.
func JoinIP4Nets(elems []*net.IPNet) string {
	var strs []string
	for _, elem := range elems {
		if elem != nil && elem.IP.To4() != nil {
			strs = append(strs, elem.String())
		}
	}
	return strings.Join(strs, ",")
}

// getFirst6 returns the first IPv6 address from the list of IP addresses.
// If no IPv6 addresses are found, an error is raised.
func getFirst6(elems []net.IP) (net.IP, error) {
	for _, elem := range elems {
		if elem != nil && netutils.IsIPv6(elem) {
			return elem, nil
		}
	}
	return nil, errors.New("no IPv6 address found")
}

// getFirst6Net returns the first IPv4 network from the list of IP networks.
// If no IPv6 addresses are found, an error is raised.
func getFirst6Net(elems []*net.IPNet) (*net.IPNet, error) {
	for _, elem := range elems {
		if elem != nil && netutils.IsIPv6(elem.IP) {
			return elem, nil
		}
	}
	return nil, errors.New("no IPv6 CIDRs found")
}

// GetFirst6String returns the first IPv6 address from a list of IP address strings.
// If no IPv6 addresses are found, an error is raised.
func GetFirst6String(elems []string) (string, error) {
	ips := []net.IP{}
	for _, elem := range elems {
		for _, v := range strings.Split(elem, ",") {
			ips = append(ips, net.ParseIP(v))
		}
	}
	ip, err := getFirst6(ips)
	if err != nil {
		return "", err
	}
	return ip.String(), nil
}

// JoinIP6Nets stringifies and joins a list of IPv6 networks with commas.
func JoinIP6Nets(elems []*net.IPNet) string {
	var strs []string
	for _, elem := range elems {
		if elem != nil && netutils.IsIPv6(elem.IP) {
			strs = append(strs, elem.String())
		}
	}
	return strings.Join(strs, ",")
}

// GetHostnameAndIPs takes a node name and list of IPs, usually from CLI args.
// If set, these are used to return the node's name and addresses. If not set,
// the system hostname and primary interface addresses are returned instead.
func GetHostnameAndIPs(name string, nodeIPs cli.StringSlice) (string, []net.IP, error) {
	ips := []net.IP{}
	if len(nodeIPs) == 0 {
		hostIP, err := apinet.ChooseHostInterface()
		if err != nil {
			return "", nil, err
		}
		ips = append(ips, hostIP)
		// If IPv6 it's an IPv6 only node
		if hostIP.To4() != nil {
			hostIPv6, err := apinet.ResolveBindAddress(net.IPv6loopback)
			if err == nil && !hostIPv6.Equal(hostIP) {
				ips = append(ips, hostIPv6)
			}
		}
	} else {
		var err error
		ips, err = ParseStringSliceToIPs(nodeIPs)
		if err != nil {
			return "", nil, fmt.Errorf("invalid node-ip: %w", err)
		}
	}

	if name == "" {
		hostname, err := os.Hostname()
		if err != nil {
			return "", nil, err
		}
		name = hostname
	}

	// Use lower case hostname to comply with kubernetes constraint:
	// https://github.com/kubernetes/kubernetes/issues/71140
	name = strings.ToLower(name)

	return name, ips, nil
}

// ParseStringSliceToIPs converts slice of strings that in turn can be lists of comma separated unparsed IP addresses
// into a single slice of net.IP, it returns error if at any point parsing failed
func ParseStringSliceToIPs(s cli.StringSlice) ([]net.IP, error) {
	var ips []net.IP
	for _, unparsedIP := range s {
		for _, v := range strings.Split(unparsedIP, ",") {
			ip := net.ParseIP(v)
			if ip == nil {
				return nil, fmt.Errorf("invalid ip format '%s'", v)
			}
			ips = append(ips, ip)
		}
	}

	return ips, nil
}

// GetFirstValidIPString returns the first valid address from a list of IP address strings,
// without preference for IP family. If no address are found, an empty string is returned.
func GetFirstValidIPString(s cli.StringSlice) string {
	for _, unparsedIP := range s {
		for _, v := range strings.Split(unparsedIP, ",") {
			if ip := net.ParseIP(v); ip != nil {
				return v
			}
		}
	}
	return ""
}

// GetFirstIP checks what is the IPFamily of the first item. Based on that, returns a set of values
func GetDefaultAddresses(nodeIP net.IP) (string, string, string, error) {

	if netutils.IsIPv4(nodeIP) {
		ListenAddress := "0.0.0.0"
		clusterCIDR := "10.42.0.0/16"
		serviceCIDR := "10.43.0.0/16"

		return ListenAddress, clusterCIDR, serviceCIDR, nil
	}

	if netutils.IsIPv6(nodeIP) {
		ListenAddress := "::"
		clusterCIDR := "fd00:42::/56"
		serviceCIDR := "fd00:43::/112"

		return ListenAddress, clusterCIDR, serviceCIDR, nil
	}

	return "", "", "", fmt.Errorf("ip: %v is not ipv4 or ipv6", nodeIP)
}

// GetFirstString returns the first IP4 address from a list of IP address strings.
// If no IPv4 addresses are found, returns the first IPv6 address
// if neither of IPv4 or IPv6 are found an error is raised.
func GetFirstString(elems []string) (string, bool, error) {
	ip, err := GetFirst4String(elems)
	IPv6only := false
	if err != nil {
		ip, err = GetFirst6String(elems)
		if err != nil {
			return "", false, err
		}
		IPv6only = true
	}
	return ip, IPv6only, nil
}

// IPToIPNet converts an IP to an IPNet, using a fully filled mask appropriate for the address family.
func IPToIPNet(ip net.IP) (*net.IPNet, error) {
	address := ip.String()
	if strings.Contains(address, ":") {
		address += "/128"
	} else {
		address += "/32"
	}
	_, cidr, err := net.ParseCIDR(address)
	return cidr, err
}

// IPStringToIPNet converts an IP string to an IPNet, using a fully filled mask appropriate for the address family.
func IPStringToIPNet(address string) (*net.IPNet, error) {
	if strings.Contains(address, ":") {
		address += "/128"
	} else {
		address += "/32"
	}
	_, cidr, err := net.ParseCIDR(address)
	return cidr, err
}

// GetIPFromInterface is the public function that returns the IP of an interface
func GetIPFromInterface(ifaceName string) (string, error) {
	ip, err := getIPFromInterface(ifaceName)
	if err != nil {
		return "", fmt.Errorf("interface %s does not have a correct global unicast ip: %w", ifaceName, err)
	}
	logrus.Infof("Found ip %s from iface %s", ip, ifaceName)
	return ip, nil
}

// getIPFromInterface is the private function that returns de IP of an interface
func getIPFromInterface(ifaceName string) (string, error) {
	iface, err := net.InterfaceByName(ifaceName)
	if err != nil {
		return "", err
	}
	addrs, err := iface.Addrs()
	if err != nil {
		return "", err
	}
	if iface.Flags&net.FlagUp == 0 {
		return "", fmt.Errorf("the interface %s is not up", ifaceName)
	}

	globalUnicasts := []string{}
	globalUnicastsIPv6 := []string{}
	for _, addr := range addrs {
		ip, _, err := net.ParseCIDR(addr.String())
		if err != nil {
			return "", fmt.Errorf("unable to parse CIDR for interface %s: %w", iface.Name, err)
		}
		// if not IPv4 adding it on IPv6 list
		if ip.To4() == nil {
			if ip.IsGlobalUnicast() {
				globalUnicastsIPv6 = append(globalUnicastsIPv6, ip.String())
			}
			continue
		}
		if ip.IsGlobalUnicast() {
			globalUnicasts = append(globalUnicasts, ip.String())
		}
	}

	if len(globalUnicasts) > 1 {
		return "", fmt.Errorf("multiple global unicast addresses defined for %s, please set ip from one of %v", ifaceName, globalUnicasts)
	}
	if len(globalUnicasts) == 1 && len(globalUnicastsIPv6) == 0 {
		return globalUnicasts[0], nil
	} else if len(globalUnicastsIPv6) > 0 && len(globalUnicasts) == 1 {
		return globalUnicasts[0] + "," + globalUnicastsIPv6[0], nil
	} else if len(globalUnicastsIPv6) > 0 {
		return globalUnicastsIPv6[0], nil
	}

	return "", fmt.Errorf("can't find ip for interface %s", ifaceName)
}
