package report

import (
	"encoding/binary"
	"net"
	"strings"

	"github.com/k-sone/critbitgo"
)

// Networks represent a set of subnets
type Networks struct{ *critbitgo.Net }

// LocalNetworks helps in determining which addresses a probe reports
// as being host-scoped.
//
// TODO this design is broken, make it consistent with probe networks.
var LocalNetworks = MakeNetworks()

// MakeNetworks creates a datastructure representing a set of networks.
func MakeNetworks() Networks {
	return Networks{critbitgo.NewNet()}
}

// Add adds a network.
func (n Networks) Add(ipnet *net.IPNet) error {
	return n.Net.Add(ipnet, struct{}{})
}

// AddCIDR adds a network, represented as CIDR.
func (n Networks) AddCIDR(cidr string) error {
	return n.Net.AddCIDR(cidr, struct{}{})
}

// Contains returns true if IP is in Networks.
func (n Networks) Contains(ip net.IP) bool {
	contained, _ := n.ContainedIP(ip)
	return contained
}

// LocalAddresses returns a list of the local IP addresses.
func LocalAddresses() ([]net.IP, error) {
	result := []net.IP{}

	infs, err := net.Interfaces()
	if err != nil {
		return []net.IP{}, err
	}

	for _, inf := range infs {
		if strings.HasPrefix(inf.Name, "veth") ||
			strings.HasPrefix(inf.Name, "docker") ||
			strings.HasPrefix(inf.Name, "lo") {
			continue
		}

		addrs, err := inf.Addrs()
		if err != nil {
			return []net.IP{}, err
		}

		for _, ipnet := range ipv4Nets(addrs) {
			result = append(result, ipnet.IP)
		}
	}

	return result, nil
}

// AddLocalBridge records the subnet address associated with the bridge name
// supplied, such that MakeAddressNodeID will scope addresses in this subnet
// as local.
func AddLocalBridge(name string) error {
	inf, err := net.InterfaceByName(name)
	if err != nil {
		return err
	}

	addrs, err := inf.Addrs()
	if err != nil {
		return err
	}

	for _, ipnet := range ipv4Nets(addrs) {
		LocalNetworks.Add(ipnet)
	}

	return nil
}

// GetLocalNetworks returns all the local networks.
func GetLocalNetworks() ([]*net.IPNet, error) {
	addrs, err := net.InterfaceAddrs()
	if err != nil {
		return nil, err
	}
	return ipv4Nets(addrs), nil
}

func ipv4Nets(addrs []net.Addr) []*net.IPNet {
	nets := []*net.IPNet{}
	for _, addr := range addrs {
		if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil {
			nets = append(nets, ipnet)
		}
	}
	return nets
}

// ContainingIPv4Network determines the smallest network containing
// the given IPv4 addresses. When no addresses are specified, nil is
// returned.
func ContainingIPv4Network(ips []net.IP) *net.IPNet {
	if len(ips) == 0 {
		return nil
	}
	cpl := net.IPv4len * 8
	network := networkFromPrefix(ips[0], cpl)
	for _, ip := range ips[1:] {
		if ncpl := commonIPv4PrefixLen(network.IP, ip); ncpl < cpl {
			cpl = ncpl
			network = networkFromPrefix(network.IP, cpl)
		}
	}
	return network
}

func networkFromPrefix(ip net.IP, prefixLen int) *net.IPNet {
	mask := net.CIDRMask(prefixLen, net.IPv4len*8)
	return &net.IPNet{IP: ip.Mask(mask), Mask: mask}
}

func commonIPv4PrefixLen(a, b net.IP) int {
	x := binary.BigEndian.Uint32(a)
	y := binary.BigEndian.Uint32(b)
	cpl := 32
	for ; x != y; cpl-- {
		x >>= 1
		y >>= 1
	}
	return cpl
}

// ParseIP parses s as an IP address into a byte slice if supplied, returning the result.
// (mostly copied from net.ParseIP, modified to save memory allocations)
func ParseIP(s []byte, into []byte) net.IP {
	for i := 0; i < len(s); i++ {
		switch s[i] {
		case '.':
			return parseIPv4(s, into)
		case ':':
			return net.ParseIP(string(s)) // leave IPv6 to the original code since we don't see many of those
		}
	}
	return nil
}

// Parse IPv4 address (d.d.d.d).
// (mostly copied from net.parseIPv4, modified to save memory allocations)
func parseIPv4(s []byte, into []byte) net.IP {
	var p []byte
	if len(into) >= net.IPv4len { // check if we can use the supplied slice
		p = into[:net.IPv4len]
	} else {
		p = make([]byte, net.IPv4len)
	}
	for i := 0; i < net.IPv4len; i++ {
		if len(s) == 0 {
			// Missing octets.
			return nil
		}
		if i > 0 {
			if s[0] != '.' {
				return nil
			}
			s = s[1:]
		}
		n, c, ok := dtoi(s)
		if !ok || n > 0xFF {
			return nil
		}
		s = s[c:]
		p[i] = byte(n)
	}
	if len(s) != 0 {
		return nil
	}
	return p
}

// Bigger than we need, not too big to worry about overflow
const big = 0xFFFFFF

// Decimal to integer.
// Returns number, characters consumed, success.
// (completely copied from net.dtoi, just because it wasn't exported)
func dtoi(s []byte) (n int, i int, ok bool) {
	n = 0
	for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
		n = n*10 + int(s[i]-'0')
		if n >= big {
			return big, i, false
		}
	}
	if i == 0 {
		return 0, 0, false
	}
	return n, i, true
}
