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

package winipcfg

import (
	"errors"
	"net"
	"strings"

	"golang.org/x/sys/windows"
)

// 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(ip net.IP) (*MibUnicastIPAddressRow, error) {
	row := &MibUnicastIPAddressRow{InterfaceLUID: luid}

	err := row.Address.SetIP(ip, 0)
	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 net.IPNet) error {
	row := &MibUnicastIPAddressRow{}
	row.Init()
	row.InterfaceLUID = luid
	err := row.Address.SetIP(address.IP, 0)
	if err != nil {
		return err
	}
	ones, _ := address.Mask.Size()
	row.OnLinkPrefixLength = uint8(ones)
	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 []net.IPNet) 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 []net.IPNet) 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 []net.IPNet) error {
	err := luid.FlushIPAddresses(family)
	if err != nil {
		return err
	}
	for i := range addresses {
		asV4 := addresses[i].IP.To4()
		if asV4 == nil && family == windows.AF_INET {
			continue
		} else if asV4 != nil && 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 net.IPNet) error {
	row := &MibUnicastIPAddressRow{}
	row.Init()
	row.InterfaceLUID = luid
	err := row.Address.SetIP(address.IP, 0)
	if err != nil {
		return err
	}
	// Note: OnLinkPrefixLength member is ignored by DeleteUnicastIpAddressEntry().
	ones, _ := address.Mask.Size()
	row.OnLinkPrefixLength = uint8(ones)
	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 net.IPNet, nextHop net.IP) (*MibIPforwardRow2, error) {
	row := &MibIPforwardRow2{}
	row.Init()
	row.InterfaceLUID = luid
	err := row.DestinationPrefix.SetIPNet(destination)
	if err != nil {
		return nil, err
	}
	err = row.NextHop.SetIP(nextHop, 0)
	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 net.IPNet, nextHop net.IP, metric uint32) error {
	row := &MibIPforwardRow2{}
	row.Init()
	row.InterfaceLUID = luid
	err := row.DestinationPrefix.SetIPNet(destination)
	if err != nil {
		return err
	}
	err = row.NextHop.SetIP(nextHop, 0)
	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 {
		asV4 := rd.Destination.IP.To4()
		if asV4 == nil && family == windows.AF_INET {
			continue
		} else if asV4 != nil && 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 net.IPNet, nextHop net.IP) error {
	row := &MibIPforwardRow2{}
	row.Init()
	row.InterfaceLUID = luid
	err := row.DestinationPrefix.SetIPNet(destination)
	if err != nil {
		return err
	}
	err = row.NextHop.SetIP(nextHop, 0)
	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() ([]net.IP, error) {
	addresses, err := GetAdaptersAddresses(windows.AF_UNSPEC, GAAFlagDefault)
	if err != nil {
		return nil, err
	}
	r := make([]net.IP, 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 {
					r = append(r, ip)
				} 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 []net.IP, 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 v4 := server.To4(); v4 != nil && family == windows.AF_INET {
			filteredServers = append(filteredServers, v4.String())
		} else if v6 := server.To16(); v4 == nil && v6 != nil && family == windows.AF_INET6 {
			filteredServers = append(filteredServers, v6.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
	}
	var maybeV6 uint64
	if family == windows.AF_INET6 {
		maybeV6 = disFlagsIPv6
	}
	// For >= Windows 10 1809
	err = setInterfaceDnsSettings(*guid, &dnsInterfaceSettings{
		Version:    disVersion1,
		Flags:      disFlagsNameServer | disFlagsSearchList | maybeV6,
		NameServer: servers16,
		SearchList: domains16,
	})
	if err == nil || !errors.Is(err, windows.ERROR_PROC_NOT_FOUND) {
		return err
	}

	// For < Windows 10 1809
	err = luid.fallbackSetDNSForFamily(family, servers)
	if err != nil {
		return err
	}
	if len(domains) > 0 {
		return luid.fallbackSetDNSDomain(domains[0])
	} else {
		return luid.fallbackSetDNSDomain("")
	}
}

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