/* SPDX-License-Identifier: MIT
 *
 * Copyright (C) 2019-2026 WireGuard LLC. All Rights Reserved.
 */

package winipcfg

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

	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/registry"
	"golang.org/x/sys/windows/svc"
	"golang.org/x/sys/windows/svc/mgr"
)

// LUID represents a network interface.
type LUID uint64

// IPInterface method retrieves IP information for the specified interface on the local computer.
func (luid LUID) IPInterface(family AddressFamily) (*MibIPInterfaceRow, error) {
	row := &MibIPInterfaceRow{}
	row.Init()
	row.InterfaceLUID = luid
	row.Family = family
	err := row.get()
	if err != nil {
		return nil, err
	}
	return row, nil
}

// Interface method retrieves information for the specified adapter on the local computer.
// https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-getifentry2
func (luid LUID) Interface() (*MibIfRow2, error) {
	row := &MibIfRow2{}
	row.InterfaceLUID = luid
	err := row.get()
	if err != nil {
		return nil, err
	}
	return row, nil
}

// GUID method converts a locally unique identifier (LUID) for a network interface to a globally unique identifier (GUID) for the interface.
// https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-convertinterfaceluidtoguid
func (luid LUID) GUID() (*windows.GUID, error) {
	guid := &windows.GUID{}
	err := convertInterfaceLUIDToGUID(&luid, guid)
	if err != nil {
		return nil, err
	}
	return guid, nil
}

// LUIDFromGUID function converts a globally unique identifier (GUID) for a network interface to the locally unique identifier (LUID) for the interface.
// https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-convertinterfaceguidtoluid
func LUIDFromGUID(guid *windows.GUID) (LUID, error) {
	var luid LUID
	err := convertInterfaceGUIDToLUID(guid, &luid)
	if err != nil {
		return 0, err
	}
	return luid, nil
}

// LUIDFromIndex function converts a local index for a network interface to the locally unique identifier (LUID) for the interface.
// https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-convertinterfaceindextoluid
func LUIDFromIndex(index uint32) (LUID, error) {
	var luid LUID
	err := convertInterfaceIndexToLUID(index, &luid)
	if err != nil {
		return 0, err
	}
	return luid, nil
}

// IPAddress method returns MibUnicastIPAddressRow struct that matches to provided 'ip' argument. Corresponds to GetUnicastIpAddressEntry
// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-getunicastipaddressentry)
func (luid LUID) IPAddress(addr netip.Addr) (*MibUnicastIPAddressRow, error) {
	row := &MibUnicastIPAddressRow{InterfaceLUID: luid}

	err := row.Address.SetAddr(addr)
	if err != nil {
		return nil, err
	}

	err = row.get()
	if err != nil {
		return nil, err
	}

	return row, nil
}

// AddIPAddress method adds new unicast IP address to the interface. Corresponds to CreateUnicastIpAddressEntry function
// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-createunicastipaddressentry).
func (luid LUID) AddIPAddress(address netip.Prefix) error {
	row := &MibUnicastIPAddressRow{}
	row.Init()
	row.InterfaceLUID = luid
	row.DadState = DadStatePreferred
	row.ValidLifetime = 0xffffffff
	row.PreferredLifetime = 0xffffffff
	err := row.Address.SetAddr(address.Addr())
	if err != nil {
		return err
	}
	row.OnLinkPrefixLength = uint8(address.Bits())
	return row.Create()
}

// AddIPAddresses method adds multiple new unicast IP addresses to the interface. Corresponds to CreateUnicastIpAddressEntry function
// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-createunicastipaddressentry).
func (luid LUID) AddIPAddresses(addresses []netip.Prefix) error {
	for i := range addresses {
		err := luid.AddIPAddress(addresses[i])
		if err != nil {
			return err
		}
	}
	return nil
}

// SetIPAddresses method sets new unicast IP addresses to the interface.
func (luid LUID) SetIPAddresses(addresses []netip.Prefix) error {
	err := luid.FlushIPAddresses(windows.AF_UNSPEC)
	if err != nil {
		return err
	}
	return luid.AddIPAddresses(addresses)
}

// SetIPAddressesForFamily method sets new unicast IP addresses for a specific family to the interface.
func (luid LUID) SetIPAddressesForFamily(family AddressFamily, addresses []netip.Prefix) error {
	err := luid.FlushIPAddresses(family)
	if err != nil {
		return err
	}
	for i := range addresses {
		if !addresses[i].Addr().Is4() && family == windows.AF_INET {
			continue
		} else if !addresses[i].Addr().Is6() && family == windows.AF_INET6 {
			continue
		}
		err := luid.AddIPAddress(addresses[i])
		if err != nil {
			return err
		}
	}
	return nil
}

// DeleteIPAddress method deletes interface's unicast IP address. Corresponds to DeleteUnicastIpAddressEntry function
// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-deleteunicastipaddressentry).
func (luid LUID) DeleteIPAddress(address netip.Prefix) error {
	row := &MibUnicastIPAddressRow{}
	row.Init()
	row.InterfaceLUID = luid
	err := row.Address.SetAddr(address.Addr())
	if err != nil {
		return err
	}
	// Note: OnLinkPrefixLength member is ignored by DeleteUnicastIpAddressEntry().
	row.OnLinkPrefixLength = uint8(address.Bits())
	return row.Delete()
}

// FlushIPAddresses method deletes all interface's unicast IP addresses.
func (luid LUID) FlushIPAddresses(family AddressFamily) error {
	var tab *mibUnicastIPAddressTable
	err := getUnicastIPAddressTable(family, &tab)
	if err != nil {
		return err
	}
	t := tab.get()
	for i := range t {
		if t[i].InterfaceLUID == luid {
			t[i].Delete()
		}
	}
	tab.free()
	return nil
}

// Route method returns route determined with the input arguments. Corresponds to GetIpForwardEntry2 function
// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-getipforwardentry2).
// NOTE: If the corresponding route isn't found, the method will return error.
func (luid LUID) Route(destination netip.Prefix, nextHop netip.Addr) (*MibIPforwardRow2, error) {
	row := &MibIPforwardRow2{}
	row.Init()
	row.InterfaceLUID = luid
	row.ValidLifetime = 0xffffffff
	row.PreferredLifetime = 0xffffffff
	err := row.DestinationPrefix.SetPrefix(destination)
	if err != nil {
		return nil, err
	}
	err = row.NextHop.SetAddr(nextHop)
	if err != nil {
		return nil, err
	}

	err = row.get()
	if err != nil {
		return nil, err
	}
	return row, nil
}

// AddRoute method adds a route to the interface. Corresponds to CreateIpForwardEntry2 function, with added splitDefault feature.
// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-createipforwardentry2)
func (luid LUID) AddRoute(destination netip.Prefix, nextHop netip.Addr, metric uint32) error {
	row := &MibIPforwardRow2{}
	row.Init()
	row.InterfaceLUID = luid
	err := row.DestinationPrefix.SetPrefix(destination)
	if err != nil {
		return err
	}
	err = row.NextHop.SetAddr(nextHop)
	if err != nil {
		return err
	}
	row.Metric = metric
	return row.Create()
}

// AddRoutes method adds multiple routes to the interface.
func (luid LUID) AddRoutes(routesData []*RouteData) error {
	for _, rd := range routesData {
		err := luid.AddRoute(rd.Destination, rd.NextHop, rd.Metric)
		if err != nil {
			return err
		}
	}
	return nil
}

// SetRoutes method sets (flush than add) multiple routes to the interface.
func (luid LUID) SetRoutes(routesData []*RouteData) error {
	err := luid.FlushRoutes(windows.AF_UNSPEC)
	if err != nil {
		return err
	}
	return luid.AddRoutes(routesData)
}

// SetRoutesForFamily method sets (flush than add) multiple routes for a specific family to the interface.
func (luid LUID) SetRoutesForFamily(family AddressFamily, routesData []*RouteData) error {
	err := luid.FlushRoutes(family)
	if err != nil {
		return err
	}
	for _, rd := range routesData {
		if !rd.Destination.Addr().Is4() && family == windows.AF_INET {
			continue
		} else if !rd.Destination.Addr().Is6() && family == windows.AF_INET6 {
			continue
		}
		err := luid.AddRoute(rd.Destination, rd.NextHop, rd.Metric)
		if err != nil {
			return err
		}
	}
	return nil
}

// DeleteRoute method deletes a route that matches the criteria. Corresponds to DeleteIpForwardEntry2 function
// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-deleteipforwardentry2).
func (luid LUID) DeleteRoute(destination netip.Prefix, nextHop netip.Addr) error {
	row := &MibIPforwardRow2{}
	row.Init()
	row.InterfaceLUID = luid
	err := row.DestinationPrefix.SetPrefix(destination)
	if err != nil {
		return err
	}
	err = row.NextHop.SetAddr(nextHop)
	if err != nil {
		return err
	}
	err = row.get()
	if err != nil {
		return err
	}
	return row.Delete()
}

// FlushRoutes method deletes all interface's routes.
// It continues on failures, and returns the last error afterwards.
func (luid LUID) FlushRoutes(family AddressFamily) error {
	var tab *mibIPforwardTable2
	err := getIPForwardTable2(family, &tab)
	if err != nil {
		return err
	}
	t := tab.get()
	for i := range t {
		if t[i].InterfaceLUID == luid {
			err2 := t[i].Delete()
			if err2 != nil {
				err = err2
			}
		}
	}
	tab.free()
	return err
}

// DNS method returns all DNS server addresses associated with the adapter.
func (luid LUID) DNS() ([]netip.Addr, error) {
	addresses, err := GetAdaptersAddresses(windows.AF_UNSPEC, GAAFlagDefault)
	if err != nil {
		return nil, err
	}
	r := make([]netip.Addr, 0, len(addresses))
	for _, addr := range addresses {
		if addr.LUID == luid {
			for dns := addr.FirstDNSServerAddress; dns != nil; dns = dns.Next {
				if ip := dns.Address.IP(); ip != nil {
					if a, ok := netip.AddrFromSlice(ip); ok {
						r = append(r, a)
					}
				} else {
					return nil, windows.ERROR_INVALID_PARAMETER
				}
			}
		}
	}
	return r, nil
}

// SetDNS method clears previous and associates new DNS servers and search domains with the adapter for a specific family.
func (luid LUID) SetDNS(family AddressFamily, servers []netip.Addr, domains []string) error {
	if family != windows.AF_INET && family != windows.AF_INET6 {
		return windows.ERROR_PROTOCOL_UNREACHABLE
	}

	var filteredServers []string
	for _, server := range servers {
		if (server.Is4() && family == windows.AF_INET) || (server.Is6() && family == windows.AF_INET6) {
			filteredServers = append(filteredServers, server.String())
		}
	}
	servers16, err := windows.UTF16PtrFromString(strings.Join(filteredServers, ","))
	if err != nil {
		return err
	}
	domains16, err := windows.UTF16PtrFromString(strings.Join(domains, ","))
	if err != nil {
		return err
	}
	guid, err := luid.GUID()
	if err != nil {
		return err
	}
	dnsInterfaceSettings := &DnsInterfaceSettings{
		Version:    DnsInterfaceSettingsVersion1,
		Flags:      DnsInterfaceSettingsFlagNameserver | DnsInterfaceSettingsFlagSearchList,
		NameServer: servers16,
		SearchList: domains16,
	}
	if family == windows.AF_INET6 {
		dnsInterfaceSettings.Flags |= DnsInterfaceSettingsFlagIPv6
	}
	// For >= Windows 10 1809
	err = SetInterfaceDnsSettings(*guid, dnsInterfaceSettings)
	if err == nil || !errors.Is(err, windows.ERROR_PROC_NOT_FOUND) {
		return err
	}

	// For < Windows 10 1809
	var regPath string
	switch family {
	case windows.AF_INET:
		regPath = fmt.Sprintf("SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\%v", guid)
	case windows.AF_INET6:
		regPath = fmt.Sprintf("SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters\\Interfaces\\%v", guid)
	}
	key, err := registry.OpenKey(registry.LOCAL_MACHINE, regPath, registry.SET_VALUE)
	if err != nil {
		return fmt.Errorf("opening interface registry key: %w", err)
	}
	defer key.Close()
	if err := key.SetStringValue("NameServer", strings.Join(filteredServers, ",")); err != nil {
		return fmt.Errorf("setting NameServer registry value: %w", err)
	}
	if err := key.SetStringValue("SearchList", strings.Join(domains, ",")); err != nil {
		return fmt.Errorf("setting SearchList registry value: %w", err)
	}
	scm, err := mgr.Connect()
	if err != nil {
		return nil
	}
	defer scm.Disconnect()
	s := mgr.Service{Name: "dnscache"}
	s.Handle, err = windows.OpenService(scm.Handle, windows.StringToUTF16Ptr(s.Name), windows.SERVICE_PAUSE_CONTINUE)
	if err != nil {
		return nil
	}
	defer s.Close()
	s.Control(svc.ParamChange)
	return nil
}

// FlushDNS method clears all DNS servers associated with the adapter.
func (luid LUID) FlushDNS(family AddressFamily) error {
	return luid.SetDNS(family, nil, nil)
}
