package tlsspoof

import (
	"encoding/binary"
	"net"
	"net/netip"
	"strconv"
	"strings"
	"sync"
	"syscall"

	"github.com/sagernet/sing-tun/gtcpip/header"
	E "github.com/sagernet/sing/common/exceptions"

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

const PlatformSupported = true

// Offsets into xinpcb_n within each net.inet.tcp.pcblist_n record, identical
// to the values used by common/process/searcher_darwin_shared.go.
const (
	darwinXinpgenSize       = 24
	darwinXsocketOffset     = 104
	darwinXinpcbForeignPort = 16
	darwinXinpcbLocalPort   = 18
	darwinXinpcbVFlag       = 44
	darwinXinpcbForeignAddr = 48
	darwinXinpcbLocalAddr   = 64
	darwinXinpcbIPv4Offset  = 12

	darwinTCPExtraSize = 208

	darwinXtcpcbSndNxtOffset = 56
	darwinXtcpcbRcvNxtOffset = 80
)

// darwinStructSize returns the size of xinpcb_n for the running Darwin kernel.
// Darwin 22 (macOS 13 Ventura) grew the struct from 384 to 408 bytes; there is
// no ABI-stable way to read it, so we key off the kernel version.
var darwinStructSize = sync.OnceValues(func() (int, error) {
	value, err := syscall.Sysctl("kern.osrelease")
	if err != nil {
		return 0, E.Cause(err, "sysctl kern.osrelease")
	}
	major, _, ok := strings.Cut(value, ".")
	if !ok {
		return 0, E.New("unexpected kern.osrelease format: ", value)
	}
	n, err := strconv.ParseInt(major, 10, 64)
	if err != nil {
		return 0, E.Cause(err, "parse kern.osrelease major version: ", value)
	}
	if n >= 22 {
		return 408, nil
	}
	return 384, nil
})

type darwinSpoofer struct {
	method      Method
	src         netip.AddrPort
	dst         netip.AddrPort
	rawFD       int
	rawSockAddr unix.Sockaddr
	sendNext    uint32
	receiveNext uint32
}

func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) {
	_, src, dst, err := tcpEndpoints(conn)
	if err != nil {
		return nil, err
	}
	fd, sockaddr, err := openDarwinRawSocket(src, dst)
	if err != nil {
		return nil, err
	}
	sendNext, receiveNext, err := readDarwinTCPSequence(src, dst)
	if err != nil {
		unix.Close(fd)
		return nil, err
	}
	return &darwinSpoofer{
		method:      method,
		src:         src,
		dst:         dst,
		rawFD:       fd,
		rawSockAddr: sockaddr,
		sendNext:    sendNext,
		receiveNext: receiveNext,
	}, nil
}

// readDarwinTCPSequence scans net.inet.tcp.pcblist_n for the PCB that matches
// src -> dst and returns (snd_nxt, rcv_nxt). These live in xtcpcb_n at the end
// of each record; see darwin-xnu bsd/netinet/in_pcblist.c:get_pcblist_n.
func readDarwinTCPSequence(src, dst netip.AddrPort) (uint32, uint32, error) {
	buffer, err := unix.SysctlRaw("net.inet.tcp.pcblist_n")
	if err != nil {
		return 0, 0, E.Cause(err, "sysctl net.inet.tcp.pcblist_n")
	}
	structSize, err := darwinStructSize()
	if err != nil {
		return 0, 0, err
	}
	itemSize := structSize + darwinTCPExtraSize
	for i := darwinXinpgenSize; i+itemSize <= len(buffer); i += itemSize {
		inpcb := buffer[i : i+darwinXsocketOffset]
		xtcpcb := buffer[i+structSize : i+itemSize]
		localPort := binary.BigEndian.Uint16(inpcb[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2])
		remotePort := binary.BigEndian.Uint16(inpcb[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2])
		if localPort != src.Port() || remotePort != dst.Port() {
			continue
		}
		versionFlag := inpcb[darwinXinpcbVFlag]
		var localAddr, remoteAddr netip.Addr
		switch {
		case versionFlag&0x1 != 0:
			localAddr = netip.AddrFrom4([4]byte(inpcb[darwinXinpcbLocalAddr+darwinXinpcbIPv4Offset : darwinXinpcbLocalAddr+darwinXinpcbIPv4Offset+4]))
			remoteAddr = netip.AddrFrom4([4]byte(inpcb[darwinXinpcbForeignAddr+darwinXinpcbIPv4Offset : darwinXinpcbForeignAddr+darwinXinpcbIPv4Offset+4]))
		case versionFlag&0x2 != 0:
			localAddr = netip.AddrFrom16([16]byte(inpcb[darwinXinpcbLocalAddr : darwinXinpcbLocalAddr+16]))
			remoteAddr = netip.AddrFrom16([16]byte(inpcb[darwinXinpcbForeignAddr : darwinXinpcbForeignAddr+16]))
		default:
			continue
		}
		if localAddr.Unmap() != src.Addr() || remoteAddr.Unmap() != dst.Addr() {
			continue
		}
		sendNext := binary.NativeEndian.Uint32(xtcpcb[darwinXtcpcbSndNxtOffset : darwinXtcpcbSndNxtOffset+4])
		receiveNext := binary.NativeEndian.Uint32(xtcpcb[darwinXtcpcbRcvNxtOffset : darwinXtcpcbRcvNxtOffset+4])
		return sendNext, receiveNext, nil
	}
	return 0, 0, E.New("tls_spoof: connection ", src, "->", dst, " not found in pcblist_n")
}

func openDarwinRawSocket(src, dst netip.AddrPort) (int, unix.Sockaddr, error) {
	if dst.Addr().Is4() {
		return openIPv4RawSocket(dst)
	}
	// macOS does not accept IPV6_HDRINCL on AF_INET6 SOCK_RAW IPPROTO_TCP
	// sockets, so the kernel builds the IPv6 header itself. Bind to the real
	// connection's source address so in6_selectsrc returns it, and rely on
	// in6p_cksum defaulting to -1 so the user-supplied TCP checksum is
	// preserved (including deliberately corrupted ones).
	fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_RAW, unix.IPPROTO_TCP)
	if err != nil {
		return -1, nil, E.Cause(err, "open AF_INET6 SOCK_RAW")
	}
	err = unix.Bind(fd, &unix.SockaddrInet6{Addr: src.Addr().As16()})
	if err != nil {
		unix.Close(fd)
		return -1, nil, E.Cause(err, "bind AF_INET6 SOCK_RAW")
	}
	sockaddr := &unix.SockaddrInet6{Port: int(dst.Port()), Addr: dst.Addr().As16()}
	return fd, sockaddr, nil
}

func (s *darwinSpoofer) Inject(payload []byte) error {
	if !s.src.Addr().Is4() {
		segment, err := buildSpoofTCPSegment(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload)
		if err != nil {
			return err
		}
		err = unix.Sendto(s.rawFD, segment, 0, s.rawSockAddr)
		if err != nil {
			return E.Cause(err, "sendto raw socket")
		}
		return nil
	}
	frame, err := buildSpoofFrame(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload)
	if err != nil {
		return err
	}
	// Darwin inherits the historical BSD quirk: with IP_HDRINCL the kernel
	// expects ip_len and ip_off in host byte order, not network byte order.
	// Apple's rip_output swaps them back before transmission.
	ip := header.IPv4(frame)
	ip.SetTotalLengthDarwinRaw(ip.TotalLength())
	ip.SetFlagsFragmentOffsetDarwinRaw(ip.Flags(), ip.FragmentOffset())
	err = unix.Sendto(s.rawFD, frame, 0, s.rawSockAddr)
	if err != nil {
		return E.Cause(err, "sendto raw socket")
	}
	return nil
}

func (s *darwinSpoofer) Close() error {
	if s.rawFD < 0 {
		return nil
	}
	err := unix.Close(s.rawFD)
	s.rawFD = -1
	return err
}
