package ctrld

import (
	"bufio"
	"bytes"
	"encoding/hex"
	"net"
	"net/netip"
	"os"
	"runtime"
	"strings"

	"tailscale.com/net/netmon"

	"github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile"
)

const (
	v4RouteFile = "/proc/net/route"
	v6RouteFile = "/proc/net/ipv6_route"
)

func dnsFns() []dnsFn {
	return []dnsFn{dnsFromResolvConf, dns4, dns6, dnsFromSystemdResolver}
}

func dns4() []string {
	// Skip route-based DNS discovery on Android
	if runtime.GOOS == "android" {
		return nil
	}

	f, err := os.Open(v4RouteFile)
	if err != nil {
		return nil
	}
	defer f.Close()

	var dns []string
	seen := make(map[string]bool)
	vis := virtualInterfaces()
	s := bufio.NewScanner(f)
	first := true
	for s.Scan() {
		if first {
			first = false
			continue
		}
		fields := bytes.Fields(s.Bytes())
		if len(fields) < 2 {
			continue
		}
		// Skip virtual interfaces.
		if vis.contains(string(bytes.TrimSpace(fields[0]))) {
			continue
		}
		gw := make([]byte, net.IPv4len)
		// Third fields is gateway.
		if _, err := hex.Decode(gw, fields[2]); err != nil {
			continue
		}
		ip := net.IPv4(gw[3], gw[2], gw[1], gw[0])
		if ip.Equal(net.IPv4zero) || seen[ip.String()] {
			continue
		}
		seen[ip.String()] = true
		dns = append(dns, ip.String())
	}
	return dns
}

func dns6() []string {
	// Skip route-based DNS discovery on Android
	if runtime.GOOS == "android" {
		return nil
	}

	f, err := os.Open(v6RouteFile)
	if err != nil {
		return nil
	}
	defer f.Close()

	var dns []string
	vis := virtualInterfaces()
	s := bufio.NewScanner(f)
	for s.Scan() {
		fields := bytes.Fields(s.Bytes())
		if len(fields) < 4 {
			continue
		}
		// Skip virtual interfaces.
		if vis.contains(string(bytes.TrimSpace(fields[len(fields)-1]))) {
			continue
		}

		gw := make([]byte, net.IPv6len)
		// Fifth fields is gateway.
		if _, err := hex.Decode(gw, fields[4]); err != nil {
			continue
		}
		ip := net.IP(gw)
		if ip.Equal(net.IPv6zero) {
			continue
		}
		dns = append(dns, ip.String())
	}
	return dns
}

func dnsFromSystemdResolver() []string {
	// Skip systemd resolver on Android
	if runtime.GOOS == "android" {
		return nil
	}

	c, err := resolvconffile.ParseFile("/run/systemd/resolve/resolv.conf")
	if err != nil {
		return nil
	}
	ns := make([]string, 0, len(c.Nameservers))
	for _, nameserver := range c.Nameservers {
		ns = append(ns, nameserver.String())
	}
	return ns
}

type set map[string]struct{}

func (s *set) add(e string) {
	(*s)[e] = struct{}{}
}

func (s *set) contains(e string) bool {
	_, ok := (*s)[e]
	return ok
}

// virtualInterfaces returns a set of virtual interfaces on current machine.
func virtualInterfaces() set {
	s := make(set)
	entries, _ := os.ReadDir("/sys/devices/virtual/net")
	for _, entry := range entries {
		if entry.IsDir() {
			s.add(strings.TrimSpace(entry.Name()))
		}
	}
	return s
}

// validInterfacesMap returns a set containing non virtual interfaces.
// TODO: deduplicated with cmd/cli/net_linux.go in v2.
func validInterfaces() set {
	m := make(map[string]struct{})
	vis := virtualInterfaces()
	netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
		if _, existed := vis[i.Name]; existed {
			return
		}
		m[i.Name] = struct{}{}
	})
	// Fallback to default route interface if found nothing.
	if len(m) == 0 {
		defaultRoute, err := netmon.DefaultRoute()
		if err != nil {
			return m
		}
		m[defaultRoute.InterfaceName] = struct{}{}
	}
	return m
}
