package dhcpv6

import (
	"fmt"
	"net"

	"github.com/insomniacslk/dhcp/iana"
)

// InterfaceAddresses is used to fetch addresses of an interface with given name
var InterfaceAddresses func(string) ([]net.Addr, error) = interfaceAddresses

func interfaceAddresses(ifname string) ([]net.Addr, error) {
	iface, err := net.InterfaceByName(ifname)
	if err != nil {
		return nil, err
	}
	return iface.Addrs()
}

func getMatchingAddr(ifname string, matches func(net.IP) bool) (net.IP, error) {
	ifaddrs, err := InterfaceAddresses(ifname)
	if err != nil {
		return nil, err
	}
	for _, ifaddr := range ifaddrs {
		if ifaddr, ok := ifaddr.(*net.IPNet); ok && matches(ifaddr.IP) {
			return ifaddr.IP, nil
		}
	}
	return nil, fmt.Errorf("no matching address found for interface %s", ifname)
}

// GetLinkLocalAddr returns a link-local address for the interface
func GetLinkLocalAddr(ifname string) (net.IP, error) {
	return getMatchingAddr(ifname, func(ip net.IP) bool {
		return ip.To4() == nil && ip.IsLinkLocalUnicast()
	})
}

// GetGlobalAddr returns a global address for the interface
func GetGlobalAddr(ifname string) (net.IP, error) {
	return getMatchingAddr(ifname, func(ip net.IP) bool {
		return ip.To4() == nil && ip.IsGlobalUnicast()
	})
}

// GetMacAddressFromEUI64 will return a valid MAC address ONLY if it's a EUI-48
func GetMacAddressFromEUI64(ip net.IP) (net.HardwareAddr, error) {
	if ip.To16() == nil {
		return nil, fmt.Errorf("IP address shorter than 16 bytes")
	}

	if isEUI48 := ip[11] == 0xff && ip[12] == 0xfe; !isEUI48 {
		return nil, fmt.Errorf("IP address is not an EUI48 address")
	}

	mac := make(net.HardwareAddr, 6)
	copy(mac[0:3], ip[8:11])
	copy(mac[3:6], ip[13:16])
	mac[0] ^= 0x02

	return mac, nil
}

// ExtractMAC looks into the inner most PeerAddr field in the RelayInfo header
// which contains the EUI-64 address of the client making the request, populated
// by the dhcp relay, it is possible to extract the mac address from that IP.
// If that fails, it looks for the MAC addressed embededded in the DUID.
// Note that this only works with type DuidLL and DuidLLT.
// If a mac address cannot be found an error will be returned.
func ExtractMAC(packet DHCPv6) (net.HardwareAddr, error) {
	msg := packet
	if packet.IsRelay() {
		inner, err := DecapsulateRelayIndex(packet, -1)
		if err != nil {
			return nil, err
		}
		relay := inner.(*RelayMessage)
		if _, mac := relay.Options.ClientLinkLayerAddress(); mac != nil {
			return mac, nil
		}
		if mac, err := GetMacAddressFromEUI64(relay.PeerAddr); err == nil {
			return mac, nil
		}
		msg, err = msg.(*RelayMessage).GetInnerMessage()
		if err != nil {
			return nil, err
		}
	}
	duid := msg.(*Message).Options.ClientID()
	if duid == nil {
		return nil, fmt.Errorf("client ID not found in packet")
	}
	switch d := duid.(type) {
	case *DUIDLL:
		if d.LinkLayerAddr != nil {
			return d.LinkLayerAddr, nil
		}
	case *DUIDLLT:
		if d.LinkLayerAddr != nil {
			return d.LinkLayerAddr, nil
		}
	}
	return nil, fmt.Errorf("failed to extract MAC")
}

// GetDUIDLL generates a DUID-LL based on the MAC address of the first
// available, up, non-loopback network interface. This provides a stable,
// predictable DUID for the server.
func GetDUIDLL() (*DUIDLL, error) {
	ifaces, err := net.Interfaces()
	if err != nil {
		return nil, err
	}

	for _, iface := range ifaces {
		// Skip loopback and down interfaces
		if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
			continue
		}
		// Skip interfaces without a MAC address
		if len(iface.HardwareAddr) == 0 {
			continue
		}

		// Found a suitable interface
		return &DUIDLL{
			HWType:        iana.HWTypeEthernet,
			LinkLayerAddr: iface.HardwareAddr,
		}, nil
	}

	return nil, fmt.Errorf("no suitable network interface found to generate a DUID")
}
