// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build windows

package dhcpsapi

import (
	"errors"
	"fmt"
	"net"
	"unsafe"

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

//nolint:gochecknoglobals
var (
	modDhcpServer                        = windows.NewLazySystemDLL("dhcpsapi.dll")
	procDhcpGetSubnetInfo                = modDhcpServer.NewProc("DhcpGetSubnetInfo")
	procDhcpGetSuperScopeInfoV4          = modDhcpServer.NewProc("DhcpGetSuperScopeInfoV4")
	procDhcpRpcFreeMemory                = modDhcpServer.NewProc("DhcpRpcFreeMemory")
	procDhcpV4EnumSubnetReservations     = modDhcpServer.NewProc("DhcpV4EnumSubnetReservations")
	procDhcpV4FailoverGetScopeStatistics = modDhcpServer.NewProc("DhcpV4FailoverGetScopeStatistics")
	procDhcpGetMibInfoV5                 = modDhcpServer.NewProc("DhcpGetMibInfoV5")
)

func GetDHCPV4ScopeStatistics() ([]DHCPV4Scope, error) {
	var mibInfo *DHCP_MIB_INFO_V5

	if err := dhcpGetMibInfoV5(&mibInfo); err != nil {
		return nil, fmt.Errorf("dhcpGetMibInfoV5: %w", err)
	}

	defer dhcpRpcFreeMemory(unsafe.Pointer(mibInfo))

	subnetScopeInfos := make(map[DHCP_IP_ADDRESS]DHCP_SUBNET_MIB_INFO_V5, mibInfo.Scopes)
	subnetMIBScopeInfos := unsafe.Slice(mibInfo.ScopeInfo, mibInfo.Scopes)

	for _, subnetMIBScopeInfo := range subnetMIBScopeInfos {
		subnetScopeInfos[subnetMIBScopeInfo.Subnet] = subnetMIBScopeInfo
	}

	var superScopeTable *DHCP_SUPER_SCOPE_TABLE

	if err := dhcpGetSuperScopeInfoV4(&superScopeTable); err != nil {
		return nil, fmt.Errorf("dhcpGetSuperScopeInfoV4: %w", err)
	} else if superScopeTable == nil {
		return nil, errors.New("dhcpGetSuperScopeInfoV4 returned nil")
	}

	defer dhcpRpcFreeMemory(unsafe.Pointer(superScopeTable))

	scopes := make([]DHCPV4Scope, 0, superScopeTable.Count)
	subnets := unsafe.Slice(superScopeTable.Entries, superScopeTable.Count)

	var errs []error

	for _, subnet := range subnets {
		if err := (func() error {
			var subnetInfo *DHCP_SUBNET_INFO

			err := dhcpGetSubnetInfo(subnet.SubnetAddress, &subnetInfo)
			if err != nil {
				return fmt.Errorf("failed to get subnet info: %w", err)
			}

			defer dhcpRpcFreeMemory(unsafe.Pointer(subnetInfo))

			scope := DHCPV4Scope{
				Name:             subnetInfo.SubnetName.String(),
				SuperScopeName:   subnet.SuperScopeName.String(),
				ScopeIPAddress:   net.IPNet{IP: subnetInfo.SubnetAddress.IPv4(), Mask: subnetInfo.SubnetMask.IPv4Mask()},
				SuperScopeNumber: subnet.SuperScopeNumber,
				State:            subnetInfo.SubnetState,

				AddressesFree:                 -1,
				AddressesFreeOnPartnerServer:  -1,
				AddressesFreeOnThisServer:     -1,
				AddressesInUse:                -1,
				AddressesInUseOnPartnerServer: -1,
				AddressesInUseOnThisServer:    -1,
				PendingOffers:                 -1,
				ReservedAddress:               -1,
			}

			if subnetScopeInfo, ok := subnetScopeInfos[subnetInfo.SubnetAddress]; ok {
				scope.AddressesInUse = float64(subnetScopeInfo.NumAddressesInUse)
				scope.AddressesFree = float64(subnetScopeInfo.NumAddressesFree)
				scope.PendingOffers = float64(subnetScopeInfo.NumPendingOffers)
			}

			subnetReservationCount, err := dhcpV4EnumSubnetReservations(subnet.SubnetAddress)
			if err != nil {
				return fmt.Errorf("failed to get subnet reservation count: %w", err)
			} else {
				scope.ReservedAddress = float64(subnetReservationCount)
			}

			var subnetStatistics *DHCP_FAILOVER_STATISTICS

			err = dhcpV4FailoverGetScopeStatistics(subnet.SubnetAddress, &subnetStatistics)

			defer dhcpRpcFreeMemory(unsafe.Pointer(subnetStatistics))

			if err == nil {
				scope.AddressesFree = float64(subnetStatistics.AddrFree)
				scope.AddressesInUse = float64(subnetStatistics.AddrInUse)
				scope.AddressesFreeOnPartnerServer = float64(subnetStatistics.PartnerAddrFree)
				scope.AddressesInUseOnPartnerServer = float64(subnetStatistics.PartnerAddrInUse)
				scope.AddressesFreeOnThisServer = float64(subnetStatistics.ThisAddrFree)
				scope.AddressesInUseOnThisServer = float64(subnetStatistics.ThisAddrInUse)
			} else if !errors.Is(err, ERROR_DHCP_FO_SCOPE_NOT_IN_RELATIONSHIP) {
				return fmt.Errorf("failed to get subnet statistics: %w", err)
			}

			scopes = append(scopes, scope)

			return nil
		})(); err != nil {
			errs = append(errs, err)
		}
	}

	return scopes, errors.Join(errs...)
}

// dhcpGetSubnetInfo https://learn.microsoft.com/en-us/windows/win32/api/dhcpsapi/nf-dhcpsapi-dhcpgetsubnetinfo
func dhcpGetSubnetInfo(subnetAddress DHCP_IP_ADDRESS, subnetInfo **DHCP_SUBNET_INFO) error {
	ret, _, _ := procDhcpGetSubnetInfo.Call(
		0,
		uintptr(subnetAddress),
		uintptr(unsafe.Pointer(subnetInfo)),
	)

	if ret != 0 {
		return fmt.Errorf("dhcpGetSubnetInfo failed with code %w", windows.Errno(ret))
	}

	return nil
}

// dhcpV4FailoverGetScopeStatistics https://learn.microsoft.com/en-us/windows/win32/api/dhcpsapi/nf-dhcpsapi-dhcpv4failovergetscopestatistics
func dhcpV4FailoverGetScopeStatistics(scopeId DHCP_IP_ADDRESS, stats **DHCP_FAILOVER_STATISTICS) error {
	ret, _, _ := procDhcpV4FailoverGetScopeStatistics.Call(
		0,
		uintptr(scopeId),
		uintptr(unsafe.Pointer(stats)),
	)

	if ret != 0 {
		return fmt.Errorf("dhcpV4FailoverGetScopeStatistics failed with code %w", windows.Errno(ret))
	}

	return nil
}

// dhcpGetSuperScopeInfoV4 https://learn.microsoft.com/en-us/windows/win32/api/dhcpsapi/nf-dhcpsapi-dhcpgetsuperscopeinfov4
func dhcpGetSuperScopeInfoV4(superScopeTable **DHCP_SUPER_SCOPE_TABLE) error {
	ret, _, _ := procDhcpGetSuperScopeInfoV4.Call(
		0,
		uintptr(unsafe.Pointer(superScopeTable)),
	)

	if ret != 0 {
		return fmt.Errorf("dhcpGetSuperScopeInfoV4 failed with code %w", windows.Errno(ret))
	}

	return nil
}

// dhcpGetMibInfoV5 https://learn.microsoft.com/en-us/windows/win32/api/dhcpsapi/nf-dhcpsapi-dhcpgetmibinfov5
func dhcpGetMibInfoV5(mibInfo **DHCP_MIB_INFO_V5) error {
	ret, _, _ := procDhcpGetMibInfoV5.Call(
		0,
		uintptr(unsafe.Pointer(mibInfo)),
	)

	if ret != 0 {
		return fmt.Errorf("dhcpGetMibInfoV5 failed with code %w", windows.Errno(ret))
	}

	return nil
}

// dhcpV4EnumSubnetReservations https://learn.microsoft.com/en-us/windows/win32/api/dhcpsapi/nf-dhcpsapi-dhcpv4enumsubnetreservations
func dhcpV4EnumSubnetReservations(subnetAddress DHCP_IP_ADDRESS) (uint32, error) {
	var (
		elementsRead  uint32
		elementsTotal uint32
		elementsInfo  uintptr
		resumeHandle  *windows.Handle
	)

	ret, _, _ := procDhcpV4EnumSubnetReservations.Call(
		0,
		uintptr(subnetAddress),
		uintptr(unsafe.Pointer(&resumeHandle)),
		0,
		uintptr(unsafe.Pointer(&elementsInfo)),
		uintptr(unsafe.Pointer(&elementsRead)),
		uintptr(unsafe.Pointer(&elementsTotal)),
	)

	dhcpRpcFreeMemory(unsafe.Pointer(elementsInfo))

	if !errors.Is(windows.Errno(ret), windows.ERROR_MORE_DATA) && !errors.Is(windows.Errno(ret), windows.ERROR_NO_MORE_ITEMS) {
		return 0, fmt.Errorf("dhcpV4EnumSubnetReservations failed with code %w", windows.Errno(ret))
	}

	return elementsRead + elementsTotal, nil
}

func dhcpRpcFreeMemory(pointer unsafe.Pointer) {
	if uintptr(pointer) == 0 {
		return
	}

	//nolint:dogsled
	_, _, _ = procDhcpRpcFreeMemory.Call(uintptr(pointer))
}
