//go:build windows

package sysproxy

import (
	"log/slog"
	"net/url"
	"strings"
	"unsafe"

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

var (
	winhttp              = windows.NewLazySystemDLL("winhttp.dll")
	procGetIEProxyConfig = winhttp.NewProc("WinHttpGetIEProxyConfigForCurrentUser")
	procOpen             = winhttp.NewProc("WinHttpOpen")
	procGetProxyForURL   = winhttp.NewProc("WinHttpGetProxyForUrl")
	procCloseHandle      = winhttp.NewProc("WinHttpCloseHandle")

	kernel32       = windows.NewLazySystemDLL("kernel32.dll")
	procGlobalFree = kernel32.NewProc("GlobalFree")
)

type ieProxyConfig struct {
	fAutoDetect       int32
	lpszAutoConfigURL *uint16
	lpszProxy         *uint16
	lpszProxyBypass   *uint16
}

type autoproxyOptions struct {
	dwFlags                uint32
	dwAutoDetectFlags      uint32
	lpszAutoConfigURL      *uint16
	lpvReserved            uintptr
	dwReserved             uint32
	fAutoLogonIfChallenged int32
}

type proxyInfo struct {
	dwAccessType    uint32
	lpszProxy       *uint16
	lpszProxyBypass *uint16
}

const (
	fAutoDetect       = 0x00000001
	fConfigURL        = 0x00000002
	detectTypeDHCP    = 0x00000001
	detectTypeDNSA    = 0x00000002
	accessTypeNoProxy = 1
)

// ForURL resolves the Windows system proxy (static IE proxy, PAC autoconfig URL,
// or WPAD auto-detect) for target. Returns nil when the system is set to direct
// or no proxy applies; callers then fall back to env/direct.
func ForURL(target *url.URL) (*url.URL, error) {
	if target == nil {
		return nil, nil
	}
	var ie ieProxyConfig
	if r, _, callErr := procGetIEProxyConfig.Call(uintptr(unsafe.Pointer(&ie))); r == 0 {
		// A service account or an RDP session with no per-user IE config fails
		// here, which is indistinguishable from "system is set to direct"
		// unless the errno is recorded (#4798).
		slog.Debug("sysproxy: WinHttpGetIEProxyConfigForCurrentUser failed",
			"err", callErr, "host", target.Hostname())
		return nil, nil
	}
	defer globalFree(ie.lpszProxy)
	defer globalFree(ie.lpszProxyBypass)
	defer globalFree(ie.lpszAutoConfigURL)

	scheme := strings.ToLower(target.Scheme)
	if scheme == "" {
		scheme = "http"
	}

	if ie.lpszProxy != nil {
		bypass := ptrToString(ie.lpszProxyBypass)
		if !bypassed(target.Hostname(), bypass) {
			if u := parseProxyList(ptrToString(ie.lpszProxy), scheme); u != nil {
				return u, nil
			}
		}
	}
	if ie.fAutoDetect != 0 || ie.lpszAutoConfigURL != nil {
		if u := pacProxy(target, ie.lpszAutoConfigURL, scheme); u != nil {
			return u, nil
		}
	}
	slog.Debug("sysproxy: no system proxy applies; using a direct connection",
		"host", target.Hostname(),
		"static_proxy", ie.lpszProxy != nil,
		"auto_detect", ie.fAutoDetect != 0,
		"pac_url", ie.lpszAutoConfigURL != nil)
	return nil, nil
}

func pacProxy(target *url.URL, autoConfigURL *uint16, scheme string) *url.URL {
	session, _, _ := procOpen.Call(0, accessTypeNoProxy, 0, 0, 0)
	if session == 0 {
		return nil
	}
	defer func() { _, _, _ = procCloseHandle.Call(session) }()

	opts := autoproxyOptions{fAutoLogonIfChallenged: 1}
	if autoConfigURL != nil {
		opts.dwFlags = fConfigURL
		opts.lpszAutoConfigURL = autoConfigURL
	} else {
		opts.dwFlags = fAutoDetect
		opts.dwAutoDetectFlags = detectTypeDHCP | detectTypeDNSA
	}

	urlPtr, err := windows.UTF16PtrFromString(target.String())
	if err != nil {
		return nil
	}
	var info proxyInfo
	r, _, _ := procGetProxyForURL.Call(session, uintptr(unsafe.Pointer(urlPtr)), uintptr(unsafe.Pointer(&opts)), uintptr(unsafe.Pointer(&info)))
	if r == 0 {
		return nil
	}
	defer globalFree(info.lpszProxy)
	defer globalFree(info.lpszProxyBypass)
	if info.lpszProxy == nil {
		return nil
	}
	return parseProxyList(ptrToString(info.lpszProxy), scheme)
}

func ptrToString(p *uint16) string {
	if p == nil {
		return ""
	}
	return windows.UTF16PtrToString(p)
}

func globalFree(p *uint16) {
	if p != nil {
		_, _, _ = procGlobalFree.Call(uintptr(unsafe.Pointer(p)))
	}
}
