package dialer

import (
	"net"
	"net/netip"
	"strconv"
	"strings"

	"github.com/Dreamacro/clash/component/iface"
)

func LookupLocalAddrFromIfaceName(ifaceName string, network string, destination netip.Addr, port int) (net.Addr, error) {
	ifaceObj, err := iface.ResolveInterface(ifaceName)
	if err != nil {
		return nil, err
	}

	var addr netip.Prefix
	switch network {
	case "udp4", "tcp4":
		addr, err = ifaceObj.PickIPv4Addr(destination)
	case "tcp6", "udp6":
		addr, err = ifaceObj.PickIPv6Addr(destination)
	default:
		if destination.IsValid() {
			if destination.Is4() || destination.Is4In6() {
				addr, err = ifaceObj.PickIPv4Addr(destination)
			} else {
				addr, err = ifaceObj.PickIPv6Addr(destination)
			}
		} else {
			addr, err = ifaceObj.PickIPv4Addr(destination)
		}
	}
	if err != nil {
		return nil, err
	}

	if strings.HasPrefix(network, "tcp") {
		return &net.TCPAddr{
			IP:   addr.Addr().AsSlice(),
			Port: port,
		}, nil
	} else if strings.HasPrefix(network, "udp") {
		return &net.UDPAddr{
			IP:   addr.Addr().AsSlice(),
			Port: port,
		}, nil
	}

	return nil, iface.ErrAddrNotFound
}

func fallbackBindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error {
	if !destination.IsGlobalUnicast() {
		return nil
	}

	local := uint64(0)
	if dialer.LocalAddr != nil {
		_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
		if err == nil {
			local, _ = strconv.ParseUint(port, 10, 16)
		}
	}

	addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, destination, int(local))
	if err != nil {
		return err
	}

	dialer.LocalAddr = addr

	return nil
}

func fallbackBindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) {
	_, port, err := net.SplitHostPort(address)
	if err != nil {
		port = "0"
	}

	local, _ := strconv.ParseUint(port, 10, 16)

	addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, netip.Addr{}, int(local))
	if err != nil {
		return "", err
	}

	return addr.String(), nil
}

func fallbackParseNetwork(network string, addr netip.Addr) string {
	// fix fallbackBindIfaceToListenConfig() force bind to an ipv4 address
	if !strings.HasSuffix(network, "4") &&
		!strings.HasSuffix(network, "6") &&
		addr.Unmap().Is6() {
		network += "6"
	}
	return network
}
