//go:build linux

package platform

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"os/exec"
	"strings"
)

// LinuxNetworkDiscoverer discovers network configuration using ip(8) and
// /etc/resolv.conf. On systemd-resolved systems it falls back to resolvectl(1)
// when all nameservers are stub addresses.
type LinuxNetworkDiscoverer struct{}

// stubAddrs are local stub resolver addresses used by systemd-resolved.
var stubAddrs = map[string]bool{
	"127.0.0.53": true,
	"127.0.0.1":  true,
	"127.0.1.1":  true,
}

// Discover returns the default gateway and DNS server list for this host.
func (d *LinuxNetworkDiscoverer) Discover() (NetworkInfo, error) {
	info := NetworkInfo{}

	// Step 1: Determine default gateway and interface via "ip route show default".
	gateway, iface, err := linuxDefaultRoute()
	if err != nil {
		return info, fmt.Errorf("reading default route: %w", err)
	}
	info.Gateway = gateway
	info.Interface = iface

	// Step 2: Parse /etc/resolv.conf for nameserver lines.
	servers, err := linuxResolvConf()
	if err != nil {
		// A missing resolv.conf is not fatal — return empty list.
		return info, nil
	}

	// Step 3: If all servers are stub addresses, try resolvectl for upstream IPs.
	if len(servers) > 0 && allStubs(servers) && iface != "" {
		if upstream, err := linuxResolvectl(iface); err == nil && len(upstream) > 0 {
			servers = upstream
		}
	}

	info.DNSServers = servers
	return info, nil
}

// linuxDefaultRoute parses "ip route show default" and returns the gateway IP
// and interface name for the best (lowest metric) default route.
func linuxDefaultRoute() (gateway, iface string, err error) {
	out, err := exec.Command("ip", "route", "show", "default").Output()
	if err != nil {
		return "", "", fmt.Errorf("ip route show default: %w", err)
	}

	bestMetric := -1
	bestGW := ""
	bestIface := ""

	scanner := bufio.NewScanner(strings.NewReader(string(out)))
	for scanner.Scan() {
		line := scanner.Text()
		fields := strings.Fields(line)
		if len(fields) == 0 || fields[0] != "default" {
			continue
		}
		// Parse by keyword: "via <ip>" and "dev <iface>" and optional "metric <N>".
		var gw, dev string
		metric := 0
		for i := 1; i < len(fields)-1; i++ {
			switch fields[i] {
			case "via":
				gw = fields[i+1]
			case "dev":
				dev = fields[i+1]
			case "metric":
				if n, err := parseInt(fields[i+1]); err == nil {
					metric = n
				}
			}
		}
		if gw == "" || net.ParseIP(gw) == nil {
			continue
		}
		if bestMetric < 0 || metric < bestMetric {
			bestMetric = metric
			bestGW = gw
			bestIface = dev
		}
	}

	if bestGW == "" {
		return "", "", fmt.Errorf("no default route found")
	}
	return bestGW, bestIface, nil
}

// linuxResolvConf reads /etc/resolv.conf and returns the nameserver IPs.
func linuxResolvConf() ([]string, error) {
	f, err := os.Open("/etc/resolv.conf")
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var servers []string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
			continue
		}
		if !strings.HasPrefix(line, "nameserver") {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) < 2 {
			continue
		}
		ip := fields[1]
		if net.ParseIP(ip) != nil {
			servers = append(servers, ip)
		}
	}
	return servers, nil
}

// linuxResolvectl runs "resolvectl status <iface>" and returns the upstream DNS
// servers. Returns an error if resolvectl is not available.
func linuxResolvectl(iface string) ([]string, error) {
	path, err := exec.LookPath("resolvectl")
	if err != nil {
		return nil, fmt.Errorf("resolvectl not found: %w", err)
	}
	out, err := exec.Command(path, "status", iface).Output()
	if err != nil {
		return nil, fmt.Errorf("resolvectl status %s: %w", iface, err)
	}

	// Parse "DNS Servers: <ip> <ip>..." and continuation lines.
	var servers []string
	inDNS := false
	scanner := bufio.NewScanner(strings.NewReader(string(out)))
	for scanner.Scan() {
		line := scanner.Text()
		stripped := strings.TrimSpace(line)
		if strings.HasPrefix(stripped, "DNS Servers:") {
			inDNS = true
			// Extract IPs from the same line after the label.
			rest := strings.TrimPrefix(stripped, "DNS Servers:")
			for _, f := range strings.Fields(rest) {
				if net.ParseIP(f) != nil {
					servers = append(servers, f)
				}
			}
			continue
		}
		if inDNS {
			// Continuation lines are indented; stop at a non-indented line.
			if len(line) > 0 && line[0] != ' ' && line[0] != '\t' {
				break
			}
			for _, f := range strings.Fields(stripped) {
				if net.ParseIP(f) != nil {
					servers = append(servers, f)
				}
			}
		}
	}
	return servers, nil
}

// allStubs reports whether every address in servers is a known stub resolver.
func allStubs(servers []string) bool {
	for _, s := range servers {
		if !stubAddrs[s] {
			return false
		}
	}
	return true
}

func parseInt(s string) (int, error) {
	n := 0
	for _, c := range s {
		if c < '0' || c > '9' {
			return 0, fmt.Errorf("not an int: %q", s)
		}
		n = n*10 + int(c-'0')
	}
	return n, nil
}
