package hub

import (
	"net"
	"net/netip"
	"sort"
)

// Host addresses (v0.119.0) — "which addresses does this box actually hold?"
//
// The hub could not answer that at all: HostMetrics carried node/cpu/mem/disk/load/uptime/temp and
// no address of any kind, so the LAN IP of a managed host was invisible in every operator surface.
// Two sources looked like answers and are not: `lan_resolver.host_ip` is an OPTIONAL config value
// (absent unless that feature is configured), and DeriveHostIP(local_api.listen_addr) yields the
// R-50 island literal 169.254.253.1 — a link-local address that is the same on every box. Reporting
// either would have produced a confident wrong answer, which is worse than the blank it replaces.
//
// This reads the kernel's own view instead, and it issues NO block I/O (the CLAUDE.md health-check
// rule): net.Interfaces() is a netlink/procfs read, needs no privilege, and touches no filesystem.

// HostAddress is one routable address the host holds, tagged with the interface carrying it.
//
// Deliberately iface+cidr rather than a single `lan_ip`: a Proxmox host legitimately holds several
// (a management bridge, a tailnet, the WG tunnel), and picking one of them to call "the" LAN IP is a
// guess the agent is not entitled to make — on a box whose management bridge is not vmbr0 that guess
// is silently wrong. The agent reports what exists; the hub does the labelling.
type HostAddress struct {
	Iface string `json:"iface"` // e.g. "vmbr0", "wg-felhom", "tailscale0"
	CIDR  string `json:"cidr"`  // e.g. "192.168.0.162/24" — prefix length kept, it is operator-relevant
}

// ifaceAddrs is one enumerated interface: the ONLY facts the filter needs. Keeping the seam this
// narrow is what lets the filter be tested against real measured shapes without a network stack.
type ifaceAddrs struct {
	Name     string
	Up       bool
	Loopback bool
	CIDRs    []string
}

// AddressEnumerator returns the host's interfaces. Injectable so the filter can be driven with the
// shapes measured on real hardware (see hostaddr_test.go) instead of whatever the test box happens
// to have.
type AddressEnumerator func() ([]ifaceAddrs, error)

// systemInterfaces is the production enumerator: the kernel's interface table.
func systemInterfaces() ([]ifaceAddrs, error) {
	ifaces, err := net.Interfaces()
	if err != nil {
		return nil, err
	}
	out := make([]ifaceAddrs, 0, len(ifaces))
	for _, i := range ifaces {
		e := ifaceAddrs{
			Name:     i.Name,
			Up:       i.Flags&net.FlagUp != 0,
			Loopback: i.Flags&net.FlagLoopback != 0,
		}
		// A per-interface error is not fatal: one unreadable interface must not cost the report
		// every other address (serve-degraded, as everywhere else in the collector).
		addrs, aerr := i.Addrs()
		if aerr != nil {
			out = append(out, e)
			continue
		}
		for _, a := range addrs {
			e.CIDRs = append(e.CIDRs, a.String())
		}
		out = append(out, e)
	}
	return out, nil
}

// filterHostAddresses keeps every GLOBAL UNICAST address on an up, non-loopback interface.
//
// IsGlobalUnicast() is the whole rule, and it was chosen by measuring both demo hosts rather than by
// listing interface names to exclude. It drops, in one predicate:
//   - loopback (127.0.0.1, ::1)
//   - IPv6 link-local (fe80::/10) — every bridge carries one, pure noise
//   - IPv4 link-local (169.254.0.0/16) — which is exactly the R-50 island address on vmbr9, an
//     identical constant on every box and therefore actively misleading if surfaced
//
// It needs NO veth/fwbr/tap denylist: on a Proxmox host that per-guest plumbing carries no IP at
// all, so it self-excludes by having nothing to report. Verified on demo-felhom and demo-hp —
// veth9201i0/i1, fwbr*, and the unused NICs all appear in `ip link` and in no `ip addr` output.
//
// What survives on a real box: vmbr0's LAN address, wg-felhom's tunnel address, and tailscale0's
// tailnet addresses. All three are true and useful; none is labelled here.
func filterHostAddresses(in []ifaceAddrs) []HostAddress {
	out := []HostAddress{}
	for _, i := range in {
		if i.Loopback || !i.Up {
			continue
		}
		for _, c := range i.CIDRs {
			p, err := netip.ParsePrefix(c)
			if err != nil {
				continue // not a CIDR we understand — skip it, never fail the report
			}
			if !p.Addr().IsGlobalUnicast() {
				continue
			}
			out = append(out, HostAddress{Iface: i.Name, CIDR: p.String()})
		}
	}
	// Deterministic order so a report diff reflects a real change, not interface-table ordering.
	sort.Slice(out, func(a, b int) bool {
		if out[a].Iface != out[b].Iface {
			return out[a].Iface < out[b].Iface
		}
		return out[a].CIDR < out[b].CIDR
	})
	return out
}

// collectAddresses is the collector's entry point. It returns a non-nil slice so the field always
// marshals as [] — an absent key and "this box has no routable address" must not look alike to the
// hub, and [] is the honest encoding of the latter.
func (c *Collector) collectAddresses() []HostAddress {
	enum := c.addrEnum
	if enum == nil {
		// Default to the REAL enumerator, deliberately inverting the nil-reporter-means-off
		// convention used by the optional stanzas above. Those gate on a config feature; this has
		// no dependency and no feature flag, so a forgotten wiring call in main.go would produce a
		// silently empty field — the inert-seam failure this repo has shipped four times.
		enum = systemInterfaces
	}
	ifaces, err := enum()
	if err != nil {
		c.logger.Warn("host addresses: interface enumeration failed", "err", err)
		return []HostAddress{}
	}
	return filterHostAddresses(ifaces)
}
