//go:build with_gvisor

package tailscale

import (
	"net/netip"

	"github.com/sagernet/sing-box/adapter"
	C "github.com/sagernet/sing-box/constant"
	"github.com/sagernet/sing-box/log"
	"github.com/sagernet/sing-tun"
	"github.com/sagernet/sing-tun/gtcpip/header"
	E "github.com/sagernet/sing/common/exceptions"
	M "github.com/sagernet/sing/common/metadata"
	N "github.com/sagernet/sing/common/network"
	tsTUN "github.com/sagernet/tailscale/net/tstun"
	"github.com/sagernet/tailscale/types/ipproto"
	"github.com/sagernet/tailscale/wgengine/filter"
)

func (t *Endpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction {
	return adapter.PreMatchFlow
}

func (t *Endpoint) PortAddresses() (netip.Addr, netip.Addr) {
	if !t.started.Load() {
		return netip.Addr{}, netip.Addr{}
	}
	return t.server.TailscaleIPs()
}

func (t *Endpoint) PortMTU() uint32 {
	if t.systemInterface {
		return t.systemInterfaceMTU
	}
	return uint32(tsTUN.DefaultTUNMTU())
}

func (t *Endpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
	inet4Address, inet6Address := t.PortAddresses()
	if destination.Addr() == inet4Address || destination.Addr() == inet6Address {
		return tun.FlowVerdict{Action: tun.ActionAccept}
	}
	if t.started.Load() {
		tsFilter := t.wgEngine.GetFilter()
		if tsFilter != nil {
			var (
				ipProto         ipproto.Proto
				destinationPort uint16
			)
			switch network {
			case uint8(header.TCPProtocolNumber):
				ipProto = ipproto.TCP
				destinationPort = destination.Port()
			case uint8(header.UDPProtocolNumber):
				ipProto = ipproto.UDP
				destinationPort = destination.Port()
			case uint8(header.ICMPv4ProtocolNumber):
				ipProto = ipproto.ICMPv4
			case uint8(header.ICMPv6ProtocolNumber):
				ipProto = ipproto.ICMPv6
			}
			switch tsFilter.Check(source.Addr(), destination.Addr(), destinationPort, ipProto) {
			case filter.Drop:
				return tun.FlowVerdict{Action: tun.ActionReject}
			case filter.DropSilently:
				return tun.FlowVerdict{Action: tun.ActionDrop}
			}
		}
	}
	return adapter.JudgeFlow(t.router, t.Tag(), t.Type(), network, source, destination, firstPacket)
}

func (t *Endpoint) NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
	ctx := log.ContextWithNewID(t.ctx)
	var metadata adapter.InboundContext
	metadata.Inbound = t.Tag()
	metadata.InboundType = t.Type()
	metadata.Network = N.NetworkUDP
	metadata.Source = source
	metadata.Destination = destination
	metadata.Protocol = C.ProtocolDNS
	t.logger.InfoContext(ctx, "inbound DNS packet from ", source)
	t.router.HijackDNSPacket(ctx, payload, writer, metadata)
}

func (t *Endpoint) AttachReturn(returnPath tun.Return) error {
	t.returnAccess.Lock()
	defer t.returnAccess.Unlock()
	if t.returnPath == returnPath {
		return nil
	}
	if t.returnPath != nil {
		return E.New("return path already attached")
	}
	err := t.wgEngine.SetReturnPath(returnPath)
	if err != nil {
		return err
	}
	t.returnPath = returnPath
	return nil
}

func (t *Endpoint) DetachReturn(returnPath tun.Return) error {
	t.returnAccess.Lock()
	defer t.returnAccess.Unlock()
	if t.returnPath == returnPath {
		t.returnPath = nil
	}
	return nil
}

func (t *Endpoint) WritePackets(packets [][]byte) error {
	if !t.started.Load() {
		return E.New("Tailscale is not ready yet")
	}
	unmatched, err := t.wgEngine.InputPackets(packets)
	if err != nil || len(unmatched) == 0 {
		return err
	}
	t.returnAccess.Lock()
	returnPath := t.returnPath
	t.returnAccess.Unlock()
	if returnPath == nil {
		return nil
	}
	headroom := returnPath.ReturnHeadroom()
	inet4Address, inet6Address := t.PortAddresses()
	var replies [][]byte
	for _, packet := range unmatched {
		source := inet4Address
		if header.IPVersion(packet) == header.IPv6Version {
			source = inet6Address
		}
		reply, replyOk := tun.BuildUnreachable(packet, source, headroom)
		if replyOk {
			replies = append(replies, reply)
		}
	}
	if len(replies) > 0 {
		returnPath.ReturnPackets(replies)
	}
	return nil
}
