// Copyright IBM Corp. 2015, 2025
// SPDX-License-Identifier: BUSL-1.1

package fingerprint

import (
	"fmt"
	"net"
	"sort"
	"strings"

	log "github.com/hashicorp/go-hclog"
	sockaddr "github.com/hashicorp/go-sockaddr"
	"github.com/hashicorp/go-sockaddr/template"
	"github.com/hashicorp/nomad/client/config"
	"github.com/hashicorp/nomad/nomad/structs"
)

const (
	// defaultNetworkSpeed is the speed set if the network link speed could not
	// be detected.
	defaultNetworkSpeed = 1000

	// networkDisallowLinkLocalOption/Default are used to allow the operator to
	// decide how the fingerprinter handles an interface that only contains link
	// local addresses.
	networkDisallowLinkLocalOption  = "fingerprint.network.disallow_link_local"
	networkDisallowLinkLocalDefault = false
)

// NetworkFingerprint is used to fingerprint the Network capabilities of a node
type NetworkFingerprint struct {
	StaticFingerprinter
	logger            log.Logger
	interfaceDetector NetworkInterfaceDetector
}

// Reload is a no-op but implements ReloadableFingerprint
func (f *NetworkFingerprint) Reload() {}

// An interface to isolate calls to various api in net package
// This facilitates testing where we can implement
// fake interfaces and addresses to test varios code paths
type NetworkInterfaceDetector interface {
	Interfaces() ([]net.Interface, error)
	InterfaceByName(name string) (*net.Interface, error)
	Addrs(intf *net.Interface) ([]net.Addr, error)
}

// Implements the interface detector which calls net directly
type DefaultNetworkInterfaceDetector struct {
}

func (b *DefaultNetworkInterfaceDetector) Interfaces() ([]net.Interface, error) {
	return net.Interfaces()
}

func (b *DefaultNetworkInterfaceDetector) InterfaceByName(name string) (*net.Interface, error) {
	return net.InterfaceByName(name)
}

func (b *DefaultNetworkInterfaceDetector) Addrs(intf *net.Interface) ([]net.Addr, error) {
	return intf.Addrs()
}

// NewNetworkFingerprint returns a new NetworkFingerprinter with the given
// logger
func NewNetworkFingerprint(logger log.Logger) Fingerprint {
	f := &NetworkFingerprint{logger: logger.Named("network"), interfaceDetector: &DefaultNetworkInterfaceDetector{}}
	return f
}

func (f *NetworkFingerprint) Fingerprint(req *FingerprintRequest, resp *FingerprintResponse) error {
	cfg := req.Config

	// Find the named interface
	intf, err := f.findInterface(cfg.NetworkInterface)
	switch {
	case err != nil:
		return fmt.Errorf("Error while detecting network interface %s during fingerprinting: %v",
			cfg.NetworkInterface,
			err)
	case intf == nil:
		// No interface could be found
		return nil
	}

	// Create a sub-logger with common values to help with debugging
	logger := f.logger.With("interface", intf.Name)

	// Record the throughput of the interface
	var mbits int
	throughput := f.linkSpeed(intf.Name)
	if cfg.NetworkSpeed != 0 {
		mbits = cfg.NetworkSpeed
		logger.Debug("setting link speed to user configured speed", "mbits", mbits)
	} else if throughput != 0 {
		mbits = throughput
		logger.Debug("link speed detected", "mbits", mbits)
	} else {
		mbits = defaultNetworkSpeed
		logger.Debug("link speed could not be detected and no speed specified by user, falling back to default speed", "mbits", defaultNetworkSpeed)
	}

	// Create the network resources from the interface
	disallowLinkLocal := cfg.ReadBoolDefault(networkDisallowLinkLocalOption, networkDisallowLinkLocalDefault)
	nwResources, err := f.createNetworkResources(mbits, intf, disallowLinkLocal, cfg.PreferredAddressFamily)
	if err != nil {
		return err
	}

	resp.NodeResources = &structs.NodeResources{
		Networks: nwResources,
	}

	for _, nwResource := range nwResources {
		logger.Debug("detected interface IP", "IP", nwResource.IP)
	}

	// Deprecated, setting the first IP as unique IP for the node
	if len(nwResources) > 0 {
		resp.AddAttribute("unique.network.ip-address", nwResources[0].IP)
	}

	ifaces, err := f.interfaceDetector.Interfaces()
	if err != nil {
		return err
	}
	nodeNetResources, err := f.createNodeNetworkResources(ifaces, disallowLinkLocal, req.Config)
	if err != nil {
		return err
	}
	resp.NodeResources.NodeNetworks = nodeNetResources

	resp.Detected = true

	return nil
}

func (f *NetworkFingerprint) createNodeNetworkResources(ifaces []net.Interface, disallowLinkLocal bool, conf *config.Config) ([]*structs.NodeNetworkResource, error) {
	nets := make([]*structs.NodeNetworkResource, 0)
	for _, iface := range ifaces {
		speed := f.linkSpeed(iface.Name)
		if speed == 0 {
			speed = defaultNetworkSpeed
			f.logger.Debug("link speed could not be detected, falling back to default speed", "interface", iface.Name, "mbits", defaultNetworkSpeed)
		}

		newNetwork := &structs.NodeNetworkResource{
			Mode:       "host",
			Device:     iface.Name,
			MacAddress: iface.HardwareAddr.String(),
			Speed:      speed,
		}
		addrs, err := f.interfaceDetector.Addrs(&iface)
		if err != nil {
			return nil, err
		}
		var networkAddrs, linkLocalAddrs []structs.NodeNetworkAddress
		for _, addr := range addrs {
			// Find the IP Addr and the CIDR from the Address
			var ip net.IP
			var family structs.NodeNetworkAF
			switch v := (addr).(type) {
			case *net.IPNet:
				ip = v.IP
			case *net.IPAddr:
				ip = v.IP
			}

			if ip.To4() != nil {
				family = structs.NodeNetworkAF_IPv4
			} else {
				family = structs.NodeNetworkAF_IPv6
			}

			for _, alias := range deriveAddressAliases(iface, ip, conf) {
				newAddr := structs.NodeNetworkAddress{
					Address: ip.String(),
					Family:  family,
					Alias:   alias,
				}

				if hostNetwork, ok := conf.HostNetworks[alias]; ok {
					newAddr.ReservedPorts = hostNetwork.ReservedPorts
				}

				if newAddr.Alias != "" {
					if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
						linkLocalAddrs = append(linkLocalAddrs, newAddr)
					} else {
						networkAddrs = append(networkAddrs, newAddr)
					}
				}
			}
		}

		sortNodeNetworkAddresses(networkAddrs, conf.PreferredAddressFamily)
		sortNodeNetworkAddresses(linkLocalAddrs, conf.PreferredAddressFamily)

		if len(networkAddrs) == 0 && len(linkLocalAddrs) > 0 {
			if disallowLinkLocal {
				f.logger.Debug("ignoring detected link-local address on interface", "interface", iface.Name)
			} else {
				newNetwork.Addresses = linkLocalAddrs
			}
		} else {
			newNetwork.Addresses = networkAddrs
		}

		if len(newNetwork.Addresses) > 0 {
			nets = append(nets, newNetwork)
		}
	}

	sortNodeNetworkResources(nets, conf.PreferredAddressFamily)

	return nets, nil
}

func deriveAddressAliases(iface net.Interface, addr net.IP, config *config.Config) (aliases []string) {
	for name, conf := range config.HostNetworks {
		var cidrMatch, ifaceMatch bool
		if conf.CIDR != "" {
			for _, cidr := range strings.Split(conf.CIDR, ",") {
				_, ipnet, err := net.ParseCIDR(cidr)
				if err != nil {
					continue
				}

				if ipnet.Contains(addr) {
					cidrMatch = true
					break
				}
			}
		} else {
			cidrMatch = true
		}
		if conf.Interface != "" {
			ifaceName, err := template.Parse(conf.Interface)
			if err != nil {
				continue
			}

			if ifaceName == iface.Name {
				ifaceMatch = true
			}
		} else {
			ifaceMatch = true
		}
		if cidrMatch && ifaceMatch {
			aliases = append(aliases, name)
		}
	}

	if config.NetworkInterface != "" {
		if config.NetworkInterface == iface.Name {
			aliases = append(aliases, "default")
		}
	} else if ri, err := sockaddr.NewRouteInfo(); err == nil {
		defaultIface, err := ri.GetDefaultInterfaceName()
		if err == nil && iface.Name == defaultIface {
			aliases = append(aliases, "default")
		}
	}

	return
}

// createNetworkResources creates network resources for every IP
func (f *NetworkFingerprint) createNetworkResources(throughput int, intf *net.Interface, disallowLinkLocal bool, preferredAF structs.NodeNetworkAF) ([]*structs.NetworkResource, error) {
	// Find the interface with the name
	addrs, err := f.interfaceDetector.Addrs(intf)
	if err != nil {
		return nil, err
	}

	nwResources := make([]*structs.NetworkResource, 0)
	linkLocals := make([]*structs.NetworkResource, 0)

	for _, addr := range addrs {
		// Create a new network resource
		newNetwork := &structs.NetworkResource{
			Mode:   "host",
			Device: intf.Name,
			MBits:  throughput,
		}

		// Find the IP Addr and the CIDR from the Address
		var ip net.IP
		switch v := (addr).(type) {
		case *net.IPNet:
			ip = v.IP
		case *net.IPAddr:
			ip = v.IP
		}

		newNetwork.IP = ip.String()
		if ip.To4() != nil {
			newNetwork.CIDR = newNetwork.IP + "/32"
		} else {
			newNetwork.CIDR = newNetwork.IP + "/128"
		}

		// If the ip is link-local then we ignore it unless the user allows it
		// and we detect nothing else
		if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
			linkLocals = append(linkLocals, newNetwork)
			continue
		}

		nwResources = append(nwResources, newNetwork)
	}

	sortNetworkResources(nwResources, preferredAF)
	sortNetworkResources(linkLocals, preferredAF)

	if len(nwResources) == 0 && len(linkLocals) != 0 {
		if disallowLinkLocal {
			f.logger.Debug("ignoring detected link-local address on interface", "interface", intf.Name)
			return nwResources, nil
		}

		return linkLocals, nil
	}

	return nwResources, nil
}

// Returns the interface with the name passed by user. If the name is blank, we
// use the interface attached to the default route.
func (f *NetworkFingerprint) findInterface(deviceName string) (*net.Interface, error) {
	// If we aren't given a device, look it up by using the interface with the default route
	if deviceName == "" {
		ri, err := sockaddr.NewRouteInfo()
		if err != nil {
			return nil, err
		}

		defaultIfName, err := ri.GetDefaultInterfaceName()
		if err != nil {
			return nil, err
		}
		if defaultIfName == "" {
			return nil, fmt.Errorf("no network_interface given and failed to determine interface attached to default route")
		}
		deviceName = defaultIfName
	}

	return f.interfaceDetector.InterfaceByName(deviceName)
}

// Define a type for the comparison function
type LessFunc[T any] func(a, b T) bool

// Generic sort function
func sortResources[T any](res []T, less LessFunc[T]) {
	sort.Slice(res, func(i, j int) bool {
		return less(res[i], res[j])
	})
}

// Less functions for each resource type and address family
func lessNetworkResourceIPv4(a, b *structs.NetworkResource) bool {
	return net.ParseIP(a.IP).To4() != nil && net.ParseIP(b.IP).To4() == nil
}

func lessNetworkResourceIPv6(a, b *structs.NetworkResource) bool {
	return net.ParseIP(a.IP).To4() == nil && net.ParseIP(b.IP).To4() != nil
}

func lessNodeNetworkResourceIPv4(a, b *structs.NodeNetworkResource) bool {
	if len(a.Addresses) == 0 && len(b.Addresses) == 0 {
		return false
	} else if len(a.Addresses) == 0 {
		return false
	} else if len(b.Addresses) == 0 {
		return true
	} else if a.Addresses[0].Family == structs.NodeNetworkAF_IPv4 && b.Addresses[0].Family == structs.NodeNetworkAF_IPv6 {
		return true
	}
	return false
}

func lessNodeNetworkResourceIPv6(a, b *structs.NodeNetworkResource) bool {
	if len(a.Addresses) == 0 {
		return false
	} else if len(b.Addresses) == 0 {
		return true
	}
	return a.Addresses[0].Family == structs.NodeNetworkAF_IPv6 && b.Addresses[0].Family == structs.NodeNetworkAF_IPv4
}

func lessNodeNetworkAddressIPv4(a, b structs.NodeNetworkAddress) bool {
	return a.Family == structs.NodeNetworkAF_IPv4 && b.Family == structs.NodeNetworkAF_IPv6
}

func lessNodeNetworkAddressIPv6(a, b structs.NodeNetworkAddress) bool {
	return a.Family == structs.NodeNetworkAF_IPv6 && b.Family == structs.NodeNetworkAF_IPv4
}

// Sorting functions for different resource types and address families
func sortNetworkResources(res []*structs.NetworkResource, preferredAF structs.NodeNetworkAF) {
	if preferredAF == structs.NodeNetworkAF_IPv4 {
		sortResources(res, lessNetworkResourceIPv4)
	} else if preferredAF == structs.NodeNetworkAF_IPv6 {
		sortResources(res, lessNetworkResourceIPv6)
	}
}

func sortNodeNetworkResources(res []*structs.NodeNetworkResource, preferredAF structs.NodeNetworkAF) {
	if preferredAF == structs.NodeNetworkAF_IPv4 {
		sortResources(res, lessNodeNetworkResourceIPv4)
	} else if preferredAF == structs.NodeNetworkAF_IPv6 {
		sortResources(res, lessNodeNetworkResourceIPv6)
	}
}

func sortNodeNetworkAddresses(res []structs.NodeNetworkAddress, preferredAF structs.NodeNetworkAF) {
	if preferredAF == structs.NodeNetworkAF_IPv4 {
		sortResources(res, lessNodeNetworkAddressIPv4)
	} else if preferredAF == structs.NodeNetworkAF_IPv6 {
		sortResources(res, lessNodeNetworkAddressIPv6)
	}
}
