package cli

import (
	"errors"
	"fmt"
	"net"
	"net/netip"
	"slices"
	"strings"

	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/registry"
	"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"

	"github.com/Control-D-Inc/ctrld"
	ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
)

const (
	v4InterfaceKeyPathFormat = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\`
	v6InterfaceKeyPathFormat = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\`
)

// setDnsIgnoreUnusableInterface likes setDNS, but return a nil error if the interface is not usable.
func setDnsIgnoreUnusableInterface(iface *net.Interface, nameservers []string) error {
	return setDNS(iface, nameservers)
}

// setDNS sets the dns server for the provided network interface
func setDNS(iface *net.Interface, nameservers []string) error {
	if len(nameservers) == 0 {
		return errors.New("empty DNS nameservers")
	}

	luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
	if err != nil {
		return fmt.Errorf("setDNS: %w", err)
	}
	var (
		serversV4 []netip.Addr
		serversV6 []netip.Addr
	)
	for _, ns := range nameservers {
		if addr, err := netip.ParseAddr(ns); err == nil {
			if addr.Is4() {
				serversV4 = append(serversV4, addr)
			} else {
				serversV6 = append(serversV6, addr)
			}
		}
	}

	// Note that Windows won't modify the current search domains if passing nil to luid.SetDNS function.
	// searchDomains is still implemented for Windows just in case Windows API changes in future versions.
	_ = searchDomains

	if len(serversV4) == 0 && len(serversV6) == 0 {
		return errors.New("invalid DNS nameservers")
	}
	if len(serversV4) > 0 {
		if err := luid.SetDNS(windows.AF_INET, serversV4, nil); err != nil {
			return fmt.Errorf("could not set DNS ipv4: %w", err)
		}
	}
	if len(serversV6) > 0 {
		if err := luid.SetDNS(windows.AF_INET6, serversV6, nil); err != nil {
			return fmt.Errorf("could not set DNS ipv6: %w", err)
		}
	}
	return nil
}

// resetDnsIgnoreUnusableInterface likes resetDNS, but return a nil error if the interface is not usable.
func resetDnsIgnoreUnusableInterface(iface *net.Interface) error {
	return resetDNS(iface)
}

// resetDNS resets DNS servers for the specified interface
func resetDNS(iface *net.Interface) error {
	luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
	if err != nil {
		return fmt.Errorf("resetDNS: %w", err)
	}
	// Restoring DHCP settings.
	if err := luid.SetDNS(windows.AF_INET, nil, nil); err != nil {
		return fmt.Errorf("could not reset DNS ipv4: %w", err)
	}
	if err := luid.SetDNS(windows.AF_INET6, nil, nil); err != nil {
		return fmt.Errorf("could not reset DNS ipv6: %w", err)
	}
	return nil
}

// restoreDNS restores the DNS settings of the given interface.
// this should only be executed upon turning off the ctrld service.
func restoreDNS(iface *net.Interface) (err error) {
	if nss := ctrld.SavedStaticNameservers(iface); len(nss) > 0 {
		v4ns := make([]string, 0, 2)
		v6ns := make([]string, 0, 2)
		for _, ns := range nss {
			if ctrldnet.IsIPv6(ns) {
				v6ns = append(v6ns, ns)
			} else {
				v4ns = append(v4ns, ns)
			}
		}

		luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
		if err != nil {
			return fmt.Errorf("restoreDNS: %w", err)
		}

		if len(v4ns) > 0 {
			mainLog.Load().Debug().Msgf("Restoring IPv4 static DNS for interface %q: %v", iface.Name, v4ns)
			if err := setDNS(iface, v4ns); err != nil {
				return fmt.Errorf("restoreDNS (IPv4): %w", err)
			}
		} else {
			mainLog.Load().Debug().Msgf("Restoring IPv4 DHCP for interface %q", iface.Name)
			if err := luid.SetDNS(windows.AF_INET, nil, nil); err != nil {
				return fmt.Errorf("restoreDNS (IPv4 clear): %w", err)
			}
		}

		if len(v6ns) > 0 {
			mainLog.Load().Debug().Msgf("Restoring IPv6 static DNS for interface %q: %v", iface.Name, v6ns)
			if err := setDNS(iface, v6ns); err != nil {
				return fmt.Errorf("restoreDNS (IPv6): %w", err)
			}
		} else {
			mainLog.Load().Debug().Msgf("Restoring IPv6 DHCP for interface %q", iface.Name)
			if err := luid.SetDNS(windows.AF_INET6, nil, nil); err != nil {
				return fmt.Errorf("restoreDNS (IPv6 clear): %w", err)
			}
		}
	}
	return err
}

// currentDNS returns the current DNS servers for the specified interface
func currentDNS(iface *net.Interface) []string {
	luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
	if err != nil {
		mainLog.Load().Error().Err(err).Msg("Failed to get interface LUID")
		return nil
	}
	nameservers, err := luid.DNS()
	if err != nil {
		mainLog.Load().Error().Err(err).Msg("Failed to get interface DNS")
		return nil
	}
	ns := make([]string, 0, len(nameservers))
	for _, nameserver := range nameservers {
		ns = append(ns, nameserver.String())
	}
	return ns
}

// currentStaticDNS checks both the IPv4 and IPv6 paths for static DNS values using keys
// like "NameServer" and "ProfileNameServer".
func currentStaticDNS(iface *net.Interface) ([]string, error) {
	luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
	if err != nil {
		return nil, fmt.Errorf("fallback winipcfg.LUIDFromIndex: %w", err)
	}
	guid, err := luid.GUID()
	if err != nil {
		return nil, fmt.Errorf("fallback luid.GUID: %w", err)
	}

	var ns []string
	keyPaths := []string{v4InterfaceKeyPathFormat, v6InterfaceKeyPathFormat}
	for _, path := range keyPaths {
		interfaceKeyPath := path + guid.String()
		k, err := registry.OpenKey(registry.LOCAL_MACHINE, interfaceKeyPath, registry.QUERY_VALUE)
		if err != nil {
			mainLog.Load().Debug().Err(err).Msgf("Failed to open registry key %q for interface %q; trying next key", interfaceKeyPath, iface.Name)
			continue
		}
		func() {
			defer k.Close()
			for _, keyName := range []string{"NameServer", "ProfileNameServer"} {
				value, _, err := k.GetStringValue(keyName)
				if err != nil && !errors.Is(err, registry.ErrNotExist) {
					mainLog.Load().Debug().Err(err).Msgf("Error reading %s registry key", keyName)
					continue
				}
				if len(value) > 0 {
					mainLog.Load().Debug().Msgf("Found static DNS for interface %q: %s", iface.Name, value)
					parsed := parseDNSServers(value)
					for _, pns := range parsed {
						if !slices.Contains(ns, pns) {
							ns = append(ns, pns)
						}
					}
				}
			}
		}()
	}
	if len(ns) == 0 {
		mainLog.Load().Debug().Msgf("No static DNS values found for interface %q", iface.Name)
	}
	return ns, nil
}

// parseDNSServers splits a DNS server string that may be comma- or space-separated,
// and trims any extraneous whitespace or null characters.
func parseDNSServers(val string) []string {
	fields := strings.FieldsFunc(val, func(r rune) bool {
		return r == ' ' || r == ','
	})
	var servers []string
	for _, f := range fields {
		trimmed := strings.TrimSpace(f)
		if len(trimmed) > 0 {
			servers = append(servers, trimmed)
		}
	}
	return servers
}
