package main

// LAN address selection.
//
// SnapBeam listens on the network, so the address it binds to is a security
// decision, not a cosmetic one. The rule is: enumerate the real interfaces,
// keep the IPv4 addresses that are up, not loopback, not point-to-point and
// inside a private range, and bind to one of those. Anything else needs the
// operator to say so out loud with --allow-public.

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

// lanCandidate is one usable IPv4 address on one interface.
type lanCandidate struct {
	IP        net.IP
	Interface string
	Index     int
	Network   string // "10.0.0.0/8", "link-local", ...
	Private   bool
}

func (c lanCandidate) String() string {
	return fmt.Sprintf("%s on %s (%s)", c.IP, c.Interface, c.Network)
}

// privateNetworks are the ranges a home or office LAN actually uses. 100.64/10
// is carrier-grade NAT, which is what Tailscale and some ISP routers hand out,
// and 169.254/16 is what two machines negotiate with no DHCP server at all —
// both are local-only, so both count as private here.
var privateNetworks = []struct {
	cidr string
	name string
}{
	{"10.0.0.0/8", "private 10.0.0.0/8"},
	{"172.16.0.0/12", "private 172.16.0.0/12"},
	{"192.168.0.0/16", "private 192.168.0.0/16"},
	{"100.64.0.0/10", "carrier-grade NAT 100.64.0.0/10"},
	{"169.254.0.0/16", "link-local 169.254.0.0/16"},
}

// classifyIPv4 names the range an address falls in and says whether it is
// local-only. Anything not listed is treated as publicly routable, which is
// the safe assumption.
func classifyIPv4(ip net.IP) (string, bool) {
	ip4 := ip.To4()
	if ip4 == nil {
		return "not IPv4", false
	}
	if ip4.IsLoopback() {
		return "loopback", false
	}
	for _, n := range privateNetworks {
		_, netw, err := net.ParseCIDR(n.cidr)
		if err != nil {
			continue
		}
		if netw.Contains(ip4) {
			return n.name, true
		}
	}
	return "public", false
}

// lanCandidatesFrom is the testable core of the address hunt: it works from an
// already-collected list of interfaces and their addresses rather than calling
// into the operating system, so the selection rules can be exercised against a
// table of made-up interfaces.
type fakeInterface struct {
	Name  string
	Index int
	Flags net.Flags
	Addrs []string // CIDR strings, e.g. "192.168.1.20/24"
}

func lanCandidatesFrom(ifaces []fakeInterface) []lanCandidate {
	var out []lanCandidate
	for _, ifc := range ifaces {
		if ifc.Flags&net.FlagUp == 0 {
			continue
		}
		if ifc.Flags&net.FlagLoopback != 0 {
			continue
		}
		if ifc.Flags&net.FlagPointToPoint != 0 {
			// A VPN or PPP link is not the Wi-Fi the phone is on.
			continue
		}
		for _, a := range ifc.Addrs {
			ip, _, err := net.ParseCIDR(a)
			if err != nil {
				// Tolerate a bare address without a prefix length.
				ip = net.ParseIP(a)
				if ip == nil {
					continue
				}
			}
			ip4 := ip.To4()
			if ip4 == nil {
				continue
			}
			name, private := classifyIPv4(ip4)
			if name == "loopback" {
				continue
			}
			out = append(out, lanCandidate{
				IP:        ip4,
				Interface: ifc.Name,
				Index:     ifc.Index,
				Network:   name,
				Private:   private,
			})
		}
	}
	sortCandidates(out)
	return out
}

// sortCandidates puts the address a phone is most likely to reach first:
// private ranges before anything else, 192.168 before 10 before 172.16 before
// CGNAT before link-local, then by interface index for a stable order.
func sortCandidates(c []lanCandidate) {
	rank := func(x lanCandidate) int {
		switch {
		case strings.HasPrefix(x.Network, "private 192.168"):
			return 0
		case strings.HasPrefix(x.Network, "private 10."):
			return 1
		case strings.HasPrefix(x.Network, "private 172.16"):
			return 2
		case strings.HasPrefix(x.Network, "carrier-grade"):
			return 3
		case strings.HasPrefix(x.Network, "link-local"):
			return 4
		default:
			return 5
		}
	}
	sort.SliceStable(c, func(i, j int) bool {
		ri, rj := rank(c[i]), rank(c[j])
		if ri != rj {
			return ri < rj
		}
		if c[i].Index != c[j].Index {
			return c[i].Index < c[j].Index
		}
		return c[i].IP.String() < c[j].IP.String()
	})
}

// systemInterfaces reads the real interface list.
func systemInterfaces() ([]fakeInterface, error) {
	ifaces, err := net.Interfaces()
	if err != nil {
		return nil, fmt.Errorf("cannot enumerate network interfaces: %w", err)
	}
	out := make([]fakeInterface, 0, len(ifaces))
	for _, ifc := range ifaces {
		addrs, err := ifc.Addrs()
		if err != nil {
			continue
		}
		strs := make([]string, 0, len(addrs))
		for _, a := range addrs {
			strs = append(strs, a.String())
		}
		out = append(out, fakeInterface{
			Name:  ifc.Name,
			Index: ifc.Index,
			Flags: ifc.Flags,
			Addrs: strs,
		})
	}
	return out, nil
}

// lanCandidates returns every usable IPv4 address on this machine, best first.
func lanCandidates() ([]lanCandidate, error) {
	ifaces, err := systemInterfaces()
	if err != nil {
		return nil, err
	}
	return lanCandidatesFrom(ifaces), nil
}

// multicastInterfaces returns the interfaces worth joining a multicast group
// on: up, not loopback, and flagged as multicast-capable by the kernel.
func multicastInterfaces() []net.Interface {
	ifaces, err := net.Interfaces()
	if err != nil {
		return nil
	}
	var out []net.Interface
	for _, ifc := range ifaces {
		if ifc.Flags&net.FlagUp == 0 {
			continue
		}
		if ifc.Flags&net.FlagLoopback != 0 {
			continue
		}
		if ifc.Flags&net.FlagMulticast == 0 {
			continue
		}
		addrs, err := ifc.Addrs()
		if err != nil {
			continue
		}
		hasV4 := false
		for _, a := range addrs {
			if ip, _, err := net.ParseCIDR(a.String()); err == nil && ip.To4() != nil {
				hasV4 = true
				break
			}
		}
		if hasV4 {
			out = append(out, ifc)
		}
	}
	return out
}
