// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package netcheck

import (
	"context"
	"errors"
	"net/netip"

	"github.com/metacubex/tailscale/net/netaddr"
	"github.com/metacubex/tailscale/net/netns"
	"github.com/metacubex/tailscale/net/stun"
	"github.com/metacubex/tailscale/types/logger"
	"github.com/metacubex/tailscale/types/nettype"
)

// Standalone creates the necessary UDP sockets on the given bindAddr and starts
// an IO loop so that the Client can perform active probes with no further need
// for external driving of IO (no need to set/implement SendPacket, or call
// ReceiveSTUNPacket). It must be called prior to starting any reports and is
// shut down by cancellation of the provided context. If both IPv4 and IPv6 fail
// to bind, errors will be returned, if one or both protocols can bind no error
// is returned.
func (c *Client) Standalone(ctx context.Context, bindAddr string) error {
	if c.NetMon == nil {
		panic("netcheck.Client.NetMon must be set")
	}
	if bindAddr == "" {
		bindAddr = ":0"
	}
	var errs []error

	u4, err := nettype.MakePacketListenerWithNetIP(netns.Listener(c.logf, c.NetMon)).ListenPacket(ctx, "udp4", bindAddr)
	if err != nil {
		c.logf("udp4: %v", err)
		errs = append(errs, err)
	} else {
		go readPackets(ctx, c.logf, u4, c.ReceiveSTUNPacket)
	}

	u6, err := nettype.MakePacketListenerWithNetIP(netns.Listener(c.logf, c.NetMon)).ListenPacket(ctx, "udp6", bindAddr)
	if err != nil {
		c.logf("udp6: %v", err)
		errs = append(errs, err)
	} else {
		go readPackets(ctx, c.logf, u6, c.ReceiveSTUNPacket)
	}

	c.SendPacket = func(pkt []byte, dst netip.AddrPort) (int, error) {
		pc := u4
		if dst.Addr().Is6() {
			pc = u6
		}
		if pc == nil {
			return 0, errors.New("no UDP socket")
		}

		return pc.WriteToUDPAddrPort(pkt, dst)
	}

	// If both v4 and v6 failed, report an error, otherwise let one succeed.
	if len(errs) == 2 {
		return errors.Join(errs...)
	}
	return nil
}

// readPackets reads STUN packets from pc until there's an error or ctx is done.
// In either case, it closes pc.
func readPackets(ctx context.Context, logf logger.Logf, pc nettype.PacketConn, recv func([]byte, netip.AddrPort)) {
	done := make(chan struct{})
	defer close(done)

	go func() {
		select {
		case <-ctx.Done():
		case <-done:
		}
		pc.Close()
	}()

	var buf [64 << 10]byte
	for {
		n, addr, err := pc.ReadFromUDPAddrPort(buf[:])
		if err != nil {
			if ctx.Err() != nil {
				return
			}
			logf("ReadFrom: %v", err)
			return
		}
		pkt := buf[:n]
		if !stun.Is(pkt) {
			continue
		}
		if ap := netaddr.Unmap(addr); ap.IsValid() {
			recv(pkt, ap)
		}
	}
}
