package process

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

	"github.com/Dreamacro/clash/common/pool"

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

var (
	modIphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")

	procGetExtendedTcpTable = modIphlpapi.NewProc("GetExtendedTcpTable")
	procGetExtendedUdpTable = modIphlpapi.NewProc("GetExtendedUdpTable")
)

func findProcessPath(network string, from netip.AddrPort, to netip.AddrPort) (string, error) {
	family := uint32(windows.AF_INET)
	if from.Addr().Is6() {
		family = windows.AF_INET6
	}

	var protocol uint32
	switch network {
	case TCP:
		protocol = windows.IPPROTO_TCP
	case UDP:
		protocol = windows.IPPROTO_UDP
	default:
		return "", ErrInvalidNetwork
	}

	pid, err := findPidByConnectionEndpoint(family, protocol, from, to)
	if err != nil {
		return "", err
	}

	return getExecPathFromPID(pid)
}

func findPidByConnectionEndpoint(family uint32, protocol uint32, from netip.AddrPort, to netip.AddrPort) (uint32, error) {
	buf := pool.Get(0)
	defer pool.Put(buf)

	bufSize := uint32(len(buf))

loop:
	for {
		var ret uintptr

		switch protocol {
		case windows.IPPROTO_TCP:
			ret, _, _ = procGetExtendedTcpTable.Call(
				uintptr(unsafe.Pointer(unsafe.SliceData(buf))),
				uintptr(unsafe.Pointer(&bufSize)),
				0,
				uintptr(family),
				4, // TCP_TABLE_OWNER_PID_CONNECTIONS
				0,
			)
		case windows.IPPROTO_UDP:
			ret, _, _ = procGetExtendedUdpTable.Call(
				uintptr(unsafe.Pointer(unsafe.SliceData(buf))),
				uintptr(unsafe.Pointer(&bufSize)),
				0,
				uintptr(family),
				1, // UDP_TABLE_OWNER_PID
				0,
			)
		default:
			return 0, errors.New("unsupported network")
		}

		switch ret {
		case 0:
			buf = buf[:bufSize]

			break loop
		case uintptr(windows.ERROR_INSUFFICIENT_BUFFER):
			pool.Put(buf)
			buf = pool.Get(int(bufSize))

			continue loop
		default:
			return 0, fmt.Errorf("syscall error: %d", ret)
		}
	}

	if len(buf) < int(unsafe.Sizeof(uint32(0))) {
		return 0, fmt.Errorf("invalid table size: %d", len(buf))
	}

	entriesSize := *(*uint32)(unsafe.Pointer(&buf[0]))

	switch protocol {
	case windows.IPPROTO_TCP:
		if family == windows.AF_INET {
			type MibTcpRowOwnerPid struct {
				State      uint32
				LocalAddr  [4]byte
				LocalPort  uint32
				RemoteAddr [4]byte
				RemotePort uint32
				OwningPid  uint32
			}

			if uint32(len(buf))-4 < entriesSize*uint32(unsafe.Sizeof(MibTcpRowOwnerPid{})) {
				return 0, fmt.Errorf("invalid tables size: %d", len(buf))
			}

			entries := unsafe.Slice((*MibTcpRowOwnerPid)(unsafe.Pointer(&buf[4])), entriesSize)
			for _, entry := range entries {
				localAddr := netip.AddrFrom4(entry.LocalAddr)
				localPort := windows.Ntohs(uint16(entry.LocalPort))
				remoteAddr := netip.AddrFrom4(entry.RemoteAddr)
				remotePort := windows.Ntohs(uint16(entry.RemotePort))

				if localAddr == from.Addr() && remoteAddr == to.Addr() && localPort == from.Port() && remotePort == to.Port() {
					return entry.OwningPid, nil
				}
			}
		} else {
			type MibTcp6RowOwnerPid struct {
				LocalAddr     [16]byte
				LocalScopeID  uint32
				LocalPort     uint32
				RemoteAddr    [16]byte
				RemoteScopeID uint32
				RemotePort    uint32
				State         uint32
				OwningPid     uint32
			}

			if uint32(len(buf))-4 < entriesSize*uint32(unsafe.Sizeof(MibTcp6RowOwnerPid{})) {
				return 0, fmt.Errorf("invalid tables size: %d", len(buf))
			}

			entries := unsafe.Slice((*MibTcp6RowOwnerPid)(unsafe.Pointer(&buf[4])), entriesSize)
			for _, entry := range entries {
				localAddr := netip.AddrFrom16(entry.LocalAddr)
				localPort := windows.Ntohs(uint16(entry.LocalPort))
				remoteAddr := netip.AddrFrom16(entry.RemoteAddr)
				remotePort := windows.Ntohs(uint16(entry.RemotePort))

				if localAddr == from.Addr() && remoteAddr == to.Addr() && localPort == from.Port() && remotePort == to.Port() {
					return entry.OwningPid, nil
				}
			}
		}
	case windows.IPPROTO_UDP:
		if family == windows.AF_INET {
			type MibUdpRowOwnerPid struct {
				LocalAddr [4]byte
				LocalPort uint32
				OwningPid uint32
			}

			if uint32(len(buf))-4 < entriesSize*uint32(unsafe.Sizeof(MibUdpRowOwnerPid{})) {
				return 0, fmt.Errorf("invalid tables size: %d", len(buf))
			}

			entries := unsafe.Slice((*MibUdpRowOwnerPid)(unsafe.Pointer(&buf[4])), entriesSize)
			for _, entry := range entries {
				localAddr := netip.AddrFrom4(entry.LocalAddr)
				localPort := windows.Ntohs(uint16(entry.LocalPort))

				if (localAddr == from.Addr() || localAddr.IsUnspecified()) && localPort == from.Port() {
					return entry.OwningPid, nil
				}
			}
		} else {
			type MibUdp6RowOwnerPid struct {
				LocalAddr    [16]byte
				LocalScopeId uint32
				LocalPort    uint32
				OwningPid    uint32
			}

			if uint32(len(buf))-4 < entriesSize*uint32(unsafe.Sizeof(MibUdp6RowOwnerPid{})) {
				return 0, fmt.Errorf("invalid tables size: %d", len(buf))
			}

			entries := unsafe.Slice((*MibUdp6RowOwnerPid)(unsafe.Pointer(&buf[4])), entriesSize)
			for _, entry := range entries {
				localAddr := netip.AddrFrom16(entry.LocalAddr)
				localPort := windows.Ntohs(uint16(entry.LocalPort))

				if (localAddr == from.Addr() || localAddr.IsUnspecified()) && localPort == from.Port() {
					return entry.OwningPid, nil
				}
			}
		}
	default:
		return 0, ErrInvalidNetwork
	}

	return 0, ErrNotFound
}

func getExecPathFromPID(pid uint32) (string, error) {
	// kernel process starts with a colon in order to distinguish with normal processes
	switch pid {
	case 0:
		// reserved pid for system idle process
		return ":System Idle Process", nil
	case 4:
		// reserved pid for windows kernel image
		return ":System", nil
	}
	h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
	if err != nil {
		return "", err
	}
	defer windows.CloseHandle(h)

	buf := make([]uint16, windows.MAX_LONG_PATH)
	size := uint32(len(buf))

	err = windows.QueryFullProcessImageName(h, 0, &buf[0], &size)
	if err != nil {
		return "", err
	}

	return windows.UTF16ToString(buf[:size]), nil
}
