//go:build darwin
// +build darwin

package dev

import (
	"bytes"
	"fmt"
	"github.com/Dreamacro/clash/log"
	"net"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"sync"
	"syscall"
	"unsafe"

	"golang.org/x/net/ipv6"
	"golang.org/x/sys/unix"

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

const (
	utunControlName = "com.apple.net.utun_control"
	iocOut          = 0x40000000
	iocIn           = 0x80000000
	iocInout        = iocIn | iocOut
)

// _CTLIOCGINFO value derived from /usr/include/sys/{kern_control,ioccom}.h
// https://github.com/apple/darwin-xnu/blob/master/bsd/sys/ioccom.h

// #define CTLIOCGINFO     _IOWR('N', 3, struct ctl_info)	/* get id from name */ = 0xc0644e03
const _CTLIOCGINFO = iocInout | ((100 & 0x1fff) << 16) | uint32(byte('N'))<<8 | 3

// #define	SIOCPROTOATTACH_IN6	_IOWR('i', 110, struct in6_aliasreq_64)
const siocprotoattachIn6 = iocInout | ((128 & 0x1fff) << 16) | uint32(byte('i'))<<8 | 110

// #define	SIOCLL_START		_IOWR('i', 130, struct in6Aliasreq)
const siocllStart = iocInout | ((128 & 0x1fff) << 16) | uint32(byte('i'))<<8 | 130

// Following the wireguard-go solution:
// These unix.SYS_* constants were removed from golang.org/x/sys/unix
// so copy them here for now.
// See https://github.com/golang/go/issues/41868
const (
	sysIoctl      = 54
	sysConnect    = 98
	sysGetsockopt = 118
)

type tunDarwin struct {
	name       string
	tunAddress string
	autoRoute  bool
	tunFile    *os.File
	errors     chan error

	closed   bool
	stopOnce sync.Once
}

// sockaddr_ctl specifeid in /usr/include/sys/kern_control.h
type sockaddrCtl struct {
	scLen      uint8
	scFamily   uint8
	ssSysaddr  uint16
	scID       uint32
	scUnit     uint32
	scReserved [5]uint32
}

type ctlInfo struct {
	ctlID   uint32
	ctlName [96]byte
}

// https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/bsd/sys/sockio.h#L107
// https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/bsd/net/if.h#L570-L575
// https://man.openbsd.org/netintro.4#SIOCAIFADDR
type aliasreq struct {
	ifraName    [unix.IFNAMSIZ]byte
	ifraAddr    unix.RawSockaddrInet4
	ifraDstaddr unix.RawSockaddrInet4
	ifraMask    unix.RawSockaddrInet4
}

// SIOCAIFADDR_IN6
// https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/bsd/netinet6/in6_var.h#L114-L119
// https://opensource.apple.com/source/network_cmds/network_cmds-543.260.3/
type in6Addrlifetime struct{}

// https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/bsd/netinet6/in6_var.h#L336-L343
// https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/bsd/netinet6/in6.h#L174-L181
type in6Aliasreq struct {
	ifraName       [unix.IFNAMSIZ]byte
	ifraAddr       unix.RawSockaddrInet6
	ifraDstaddr    unix.RawSockaddrInet6
	ifraPrefixmask unix.RawSockaddrInet6
	ifraFlags      int32
	ifraLifetime   in6Addrlifetime
}

// https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/bsd/net/if.h#L402-L563

//type ifreqAddr struct {
//	Name [unix.IFNAMSIZ]byte
//	Addr unix.RawSockaddrInet4
//	Pad  [8]byte
//}

var sockaddrCtlSize uintptr = 32

// OpenTunDevice return a TunDevice according a URL
func OpenTunDevice(tunAddress string, autoRoute bool) (TunDevice, error) {
	name := "utun"
	mtu := 9000

	ifIndex := -1
	if name != "utun" {
		_, err := fmt.Sscanf(name, "utun%d", &ifIndex)
		if err != nil || ifIndex < 0 {
			return nil, fmt.Errorf("interface name must be utun[0-9]*")
		}
	}

	fd, err := unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, 2)
	if err != nil {
		return nil, err
	}

	ctlInfo1 := &ctlInfo{}

	copy(ctlInfo1.ctlName[:], []byte(utunControlName))

	_, _, errno := unix.Syscall(
		sysIoctl,
		uintptr(fd),
		uintptr(_CTLIOCGINFO),
		uintptr(unsafe.Pointer(ctlInfo1)),
	)

	if errno != 0 {
		return nil, fmt.Errorf("_CTLIOCGINFO: %v", errno)
	}

	sc := sockaddrCtl{
		scLen:     uint8(sockaddrCtlSize),
		scFamily:  unix.AF_SYSTEM,
		ssSysaddr: 2,
		scID:      ctlInfo1.ctlID,
		scUnit:    uint32(ifIndex) + 1,
	}

	scPointer := unsafe.Pointer(&sc)

	_, _, errno = unix.RawSyscall(
		sysConnect,
		uintptr(fd),
		uintptr(scPointer),
		uintptr(sockaddrCtlSize),
	)

	if errno != 0 {
		return nil, fmt.Errorf("SYS_CONNECT: %v", errno)
	}

	err = syscall.SetNonblock(fd, true)
	if err != nil {
		return nil, err
	}

	tun, err := CreateTUNFromFile(os.NewFile(uintptr(fd), ""), mtu, tunAddress, autoRoute)
	if err != nil {
		return nil, err
	}

	if autoRoute {
		setAutoRoute(tunAddress)
	}

	return tun, nil
}

func CreateTUNFromFile(file *os.File, mtu int, tunAddress string, autoRoute bool) (TunDevice, error) {
	tun := &tunDarwin{
		tunFile:    file,
		tunAddress: tunAddress,
		autoRoute:  autoRoute,
		errors:     make(chan error, 5),
	}

	name, err := tun.getName()
	if err != nil {
		tun.tunFile.Close()
		return nil, err
	}
	tun.name = name

	if err != nil {
		tun.tunFile.Close()
		return nil, err
	}

	if mtu > 0 {
		err = tun.setMTU(mtu)
		if err != nil {
			tun.Close()
			return nil, err
		}
	}

	// This address doesn't mean anything here. NIC just net an IP address to set route upon.
	p2pAddress := net.ParseIP(tunAddress)
	err = tun.setTunAddress(p2pAddress)
	if err != nil {
		tun.Close()
		return nil, err
	}
	err = tun.attachLinkLocal()
	if err != nil {
		tun.Close()
		return nil, err
	}

	return tun, nil
}

func (t *tunDarwin) Name() string {
	return t.name
}

func (t *tunDarwin) URL() string {
	return fmt.Sprintf("dev://%s", t.Name())
}

func (t *tunDarwin) MTU() (int, error) {
	return t.getInterfaceMtu()
}

func (t *tunDarwin) Read(buff []byte) (int, error) {
	select {
	case err := <-t.errors:
		return 0, err
	default:
		n, err := t.tunFile.Read(buff)
		if n < 4 {
			return 0, err
		}

		copy(buff[:], buff[4:])
		return n - 4, err
	}
}

func (t *tunDarwin) Write(buff []byte) (int, error) {
	// reserve space for header
	buf := pool.Get(pool.RelayBufferSize)
	defer pool.Put(buf[:cap(buf)])

	buf[0] = 0x00
	buf[1] = 0x00
	buf[2] = 0x00

	copy(buf[4:], buff)
	if buf[4]>>4 == ipv6.Version {
		buf[3] = unix.AF_INET6
	} else {
		buf[3] = unix.AF_INET
	}

	// write
	return t.tunFile.Write(buf[:4+len(buff)])
}

func (t *tunDarwin) IsClose() bool {
	return t.closed
}

func (t *tunDarwin) Close() error {
	t.stopOnce.Do(func() {
		if t.autoRoute {
			resetAutoRoute(t.tunAddress)
		}
		t.closed = true
		t.tunFile.Close()
	})
	return nil
}

func (t *tunDarwin) getInterfaceMtu() (int, error) {
	// open datagram socket

	fd, err := unix.Socket(
		unix.AF_INET,
		unix.SOCK_DGRAM,
		0,
	)
	if err != nil {
		return 0, err
	}

	defer unix.Close(fd)

	// do ioctl call

	var ifr [64]byte
	copy(ifr[:], t.name)
	_, _, errno := unix.Syscall(
		sysIoctl,
		uintptr(fd),
		uintptr(unix.SIOCGIFMTU),
		uintptr(unsafe.Pointer(&ifr[0])),
	)
	if errno != 0 {
		return 0, fmt.Errorf("failed to get MTU on %s", t.name)
	}

	return int(*(*int32)(unsafe.Pointer(&ifr[16]))), nil
}

func (t *tunDarwin) getName() (string, error) {
	var ifName struct {
		name [16]byte
	}
	ifNameSize := uintptr(16)

	var errno syscall.Errno
	t.operateOnFd(func(fd uintptr) {
		_, _, errno = unix.Syscall6(
			sysGetsockopt,
			fd,
			2, /* #define SYSPROTO_CONTROL 2 */
			2, /* #define UTUN_OPT_IFNAME 2 */
			uintptr(unsafe.Pointer(&ifName)),
			uintptr(unsafe.Pointer(&ifNameSize)), 0)
	})

	if errno != 0 {
		return "", fmt.Errorf("SYS_GETSOCKOPT: %v", errno)
	}

	t.name = string(ifName.name[:ifNameSize-1])
	return t.name, nil
}

func (t *tunDarwin) setMTU(n int) error {
	// open datagram socket
	fd, err := unix.Socket(
		unix.AF_INET,
		unix.SOCK_DGRAM,
		0,
	)
	if err != nil {
		return err
	}

	defer unix.Close(fd)

	// do ioctl call

	var ifr [32]byte
	copy(ifr[:], t.name)
	*(*uint32)(unsafe.Pointer(&ifr[unix.IFNAMSIZ])) = uint32(n)
	_, _, errno := unix.Syscall(
		sysIoctl,
		uintptr(fd),
		uintptr(unix.SIOCSIFMTU),
		uintptr(unsafe.Pointer(&ifr[0])),
	)

	if errno != 0 {
		return fmt.Errorf("failed to set MTU on %s", t.name)
	}

	return nil
}

func (t *tunDarwin) operateOnFd(fn func(fd uintptr)) {
	sysconn, err := t.tunFile.SyscallConn()
	// TODO: consume the errors
	if err != nil {
		t.errors <- fmt.Errorf("unable to find sysconn for tunfile: %s", err.Error())
		return
	}
	err = sysconn.Control(fn)
	if err != nil {
		t.errors <- fmt.Errorf("unable to control sysconn for tunfile: %s", err.Error())
	}
}

func (t *tunDarwin) setTunAddress(addr net.IP) error {
	var ifr [unix.IFNAMSIZ]byte
	copy(ifr[:], t.name)

	// set IPv4 address
	fd4, err := unix.Socket(
		unix.AF_INET,
		unix.SOCK_DGRAM,
		0,
	)
	if err != nil {
		return err
	}
	defer syscall.Close(fd4)

	var ip4 [4]byte
	copy(ip4[:], addr.To4())
	ip4mask := [4]byte{255, 255, 0, 0}
	ifra4 := aliasreq{
		ifraName: ifr,
		ifraAddr: unix.RawSockaddrInet4{
			Len:    unix.SizeofSockaddrInet4,
			Family: unix.AF_INET,
			Addr:   ip4,
		},
		ifraDstaddr: unix.RawSockaddrInet4{
			Len:    unix.SizeofSockaddrInet4,
			Family: unix.AF_INET,
			Addr:   ip4,
		},
		ifraMask: unix.RawSockaddrInet4{
			Len:    unix.SizeofSockaddrInet4,
			Family: unix.AF_INET,
			Addr:   ip4mask,
		},
	}

	if _, _, errno := unix.Syscall(
		sysIoctl,
		uintptr(fd4),
		uintptr(unix.SIOCAIFADDR),
		uintptr(unsafe.Pointer(&ifra4)),
	); errno != 0 {
		return fmt.Errorf("failed to set ip address on %s: %v", t.name, errno)
	}

	return nil
}

func (t *tunDarwin) attachLinkLocal() error {
	var ifr [unix.IFNAMSIZ]byte
	copy(ifr[:], t.name)

	// attach link-local address
	fd6, err := unix.Socket(
		unix.AF_INET6,
		unix.SOCK_DGRAM,
		0,
	)
	if err != nil {
		return err
	}
	defer syscall.Close(fd6)

	// Attach link-local address
	ifra6 := in6Aliasreq{
		ifraName: ifr,
	}
	if _, _, errno := unix.Syscall(
		sysIoctl,
		uintptr(fd6),
		uintptr(siocprotoattachIn6),
		uintptr(unsafe.Pointer(&ifra6)),
	); errno != 0 {
		return fmt.Errorf("failed to attach link-local address on %s: SIOCPROTOATTACH_IN6 %v", t.name, errno)
	}

	if _, _, errno := unix.Syscall(
		sysIoctl,
		uintptr(fd6),
		uintptr(siocllStart),
		uintptr(unsafe.Pointer(&ifra6)),
	); errno != 0 {
		return fmt.Errorf("failed to set ipv6 address on %s: SIOCLL_START %v", t.name, errno)
	}

	return nil
}

// GetAutoDetectInterface get ethernet interface
func GetAutoDetectInterface() (string, error) {
	cmd := exec.Command("route", "-n", "get", "default")
	if result, err := cmd.Output(); err != nil {
		return "", err
	} else {
		resultString := string(result)
		reg, err := regexp.Compile("(interface:)(.*)")
		if err != nil {
			return "", err
		}
		matchResult := reg.FindStringSubmatch(resultString)
		interfaceName := strings.TrimSpace(matchResult[len(matchResult)-1])
		return interfaceName, nil
	}

}

func setAutoRoute(tunGateway string) {
	addRoute("1", tunGateway)
	addRoute("2/7", tunGateway)
	addRoute("4/6", tunGateway)
	addRoute("8/5", tunGateway)
	addRoute("16/4", tunGateway)
	addRoute("32/3", tunGateway)
	addRoute("64/2", tunGateway)
	addRoute("128.0/1", tunGateway)
	addRoute("198.18.0/16", tunGateway)
}

func resetAutoRoute(tunGateway string) {
	delRoute("1", tunGateway)
	delRoute("2/7", tunGateway)
	delRoute("4/6", tunGateway)
	delRoute("8/5", tunGateway)
	delRoute("16/4", tunGateway)
	delRoute("32/3", tunGateway)
	delRoute("64/2", tunGateway)
	delRoute("128.0/1", tunGateway)
	delRoute("198.18.0/16", tunGateway)
}

func addRoute(net, name string) {
	cmd := exec.Command("route", "add", "-net", net, name)
	var stderr bytes.Buffer
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		log.Errorln("[auto route] Failed to add system route: %s: %s , cmd: %s", err.Error(), stderr.String(), cmd.String())
	}
}

func delRoute(net, name string) {
	cmd := exec.Command("route", "delete", "-net", net, name)
	var stderr bytes.Buffer
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		log.Errorln("[auto route] Failed to delete system route: %s: %s , cmd: %s", err.Error(), stderr.String(), cmd.String())
	}
}
