package server

import (
	"net"

	"go.uber.org/zap"

	"github.com/status-im/status-go/common"
	"github.com/status-im/status-go/logutils"
)

var (
	LocalHostIP = net.IP{127, 0, 0, 1}
	Localhost   = "Localhost"
)

func GetOutboundIP() (net.IP, error) {
	conn, err := net.Dial("udp", "255.255.255.255:8080")
	if err != nil {
		return nil, err
	}
	defer conn.Close()

	localAddr := conn.LocalAddr().(*net.UDPAddr)

	return localAddr.IP, nil
}

// addrToIPNet casts addr to IPNet.
// Returns nil if addr is not of IPNet type.
func addrToIPNet(addr net.Addr) *net.IPNet {
	switch v := addr.(type) {
	case *net.IPNet:
		return v
	default:
		return nil
	}
}

// filterAddressesForPairingServer filters private unicast addresses.
// ips is a 2-dimensional array, where each sub-array is a list of IP
// addresses for a single network interface.
func filterAddressesForPairingServer(ips [][]net.IP) []net.IP {
	var result = map[string]net.IP{}

	for _, niIps := range ips {
		var ipv4, ipv6 []net.IP

		for _, ip := range niIps {

			// Only take private global unicast addrs
			if !ip.IsGlobalUnicast() || !ip.IsPrivate() {
				continue
			}

			if v := ip.To4(); v != nil {
				ipv4 = append(ipv4, ip)
			} else {
				ipv6 = append(ipv6, ip)
			}
		}

		// Prefer IPv4 over IPv6 for shorter connection string
		if len(ipv4) == 0 {
			for _, ip := range ipv6 {
				result[ip.String()] = ip
			}
		} else {
			for _, ip := range ipv4 {
				result[ip.String()] = ip
			}
		}
	}

	var out []net.IP
	for _, v := range result {
		out = append(out, v)
	}

	return out
}

// getAndroidLocalIP uses the net dial default ip as the standard Android IP address
// patches https://github.com/status-im/status-mobile/issues/17156
// more work required for a more robust implementation, see https://github.com/wlynxg/anet
func getAndroidLocalIP() ([][]net.IP, error) {
	ip, err := GetOutboundIP()
	if err != nil {
		return nil, err
	}

	return [][]net.IP{{ip}}, nil
}

// getLocalAddresses returns an array of all addresses
// of all available network interfaces.
func getLocalAddresses() ([][]net.IP, error) {
	// TODO until we can resolve Android errors when calling net.Interfaces() just return the outbound local address.
	//  Sorry Android
	if common.OperatingSystemIs(common.AndroidPlatform) {
		return getAndroidLocalIP()
	}

	nis, err := net.Interfaces()
	if err != nil {
		return nil, err
	}

	var ips [][]net.IP

	for _, ni := range nis {
		var niIps []net.IP

		addrs, err := ni.Addrs()
		if err != nil {
			logutils.ZapLogger().Warn("failed to get addresses of network interface",
				zap.String("networkInterface", ni.Name),
				zap.Error(err))
			continue
		}

		for _, addr := range addrs {
			var ip net.IP
			if ipNet := addrToIPNet(addr); ipNet == nil {
				continue
			} else {
				ip = ipNet.IP
			}
			niIps = append(niIps, ip)
		}

		if len(niIps) > 0 {
			ips = append(ips, niIps)
		}
	}

	return ips, nil
}

// GetLocalAddressesForPairingServer is a high-level func
// that returns a list of addresses to be used by local pairing server.
func GetLocalAddressesForPairingServer() ([]net.IP, error) {
	ips, err := getLocalAddresses()
	if err != nil {
		return nil, err
	}
	return filterAddressesForPairingServer(ips), nil
}

// findReachableAddresses returns a filtered remoteIps array,
// in which each IP matches one or more of given localNets.
func findReachableAddresses(remoteIPs []net.IP, localNets []net.IPNet) []net.IP {
	var result []net.IP
	for _, localNet := range localNets {
		for _, remoteIP := range remoteIPs {
			if localNet.Contains(remoteIP) {
				result = append(result, remoteIP)
			}
		}
	}
	return result
}

// getAllAvailableNetworks collects all networks
// from available network interfaces.
func getAllAvailableNetworks() ([]net.IPNet, error) {
	var localNets []net.IPNet

	nis, err := net.Interfaces()
	if err != nil {
		return nil, err
	}

	for _, ni := range nis {
		addrs, err := ni.Addrs()
		if err != nil {
			logutils.ZapLogger().Warn("failed to get addresses of network interface",
				zap.String("networkInterface", ni.Name),
				zap.Error(err))
			continue
		}

		for _, localAddr := range addrs {
			localNets = append(localNets, *addrToIPNet(localAddr))
		}
	}
	return localNets, nil
}

// FindReachableAddressesForPairingClient is a high-level func
// that returns a reachable server's address to be used by local pairing client.
func FindReachableAddressesForPairingClient(serverIps []net.IP) ([]net.IP, error) {
	// TODO until we can resolve Android errors when calling net.Interfaces() just noop. Sorry Android
	if common.OperatingSystemIs(common.AndroidPlatform) {
		return serverIps, nil
	}

	nets, err := getAllAvailableNetworks()
	if err != nil {
		return nil, err
	}
	return findReachableAddresses(serverIps, nets), nil
}