package peerwg

import (
	"context"
	"fmt"
	"hash/fnv"
	"io"
	"log"
	"net"
	"strconv"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/tabbed/pqtype"
	"golang.org/x/xerrors"
	"inet.af/netaddr"
	"tailscale.com/ipn/ipnstate"
	"tailscale.com/net/dns"
	"tailscale.com/net/netns"
	"tailscale.com/net/tsdial"
	"tailscale.com/tailcfg"
	"tailscale.com/types/ipproto"
	"tailscale.com/types/key"
	tslogger "tailscale.com/types/logger"
	"tailscale.com/types/netmap"
	"tailscale.com/wgengine"
	"tailscale.com/wgengine/filter"
	"tailscale.com/wgengine/magicsock"
	"tailscale.com/wgengine/monitor"
	"tailscale.com/wgengine/netstack"
	"tailscale.com/wgengine/router"
	"tailscale.com/wgengine/wgcfg/nmcfg"

	"cdr.dev/slog"
)

var Logf tslogger.Logf = log.Printf

func init() {
	// Globally disable network namespacing.
	// All networking happens in userspace.
	netns.SetEnabled(false)
}

func UUIDToInet(uid uuid.UUID) pqtype.Inet {
	uid = privateUUID(uid)

	return pqtype.Inet{
		Valid: true,
		IPNet: net.IPNet{
			IP:   uid[:],
			Mask: net.CIDRMask(128, 128),
		},
	}
}

func UUIDToNetaddr(uid uuid.UUID) netaddr.IP {
	return netaddr.IPFrom16(privateUUID(uid))
}

// privateUUID sets the uid to have the tailscale private ipv6 prefix.
func privateUUID(uid uuid.UUID) uuid.UUID {
	// fd7a:115c:a1e0
	uid[0] = 0xfd
	uid[1] = 0x7a
	uid[2] = 0x11
	uid[3] = 0x5c
	uid[4] = 0xa1
	uid[5] = 0xe0
	return uid
}

type Network struct {
	mu     sync.Mutex
	logger slog.Logger

	Netstack  *netstack.Impl
	magicSock *magicsock.Conn
	netMap    *netmap.NetworkMap
	router    *router.Config
	wgEngine  wgengine.Engine

	// listeners is a map of listening sockets that will be forwarded traffic
	// from the wireguard interface.
	listeners map[listenKey]*listener

	DiscoPublicKey key.DiscoPublic
	NodePrivateKey key.NodePrivate
}

// New constructs a Wireguard network that filters traffic
// to destinations matching the addresses provided.
func New(logger slog.Logger, addresses []netaddr.IPPrefix) (*Network, error) {
	nodePrivateKey := key.NewNode()
	nodePublicKey := nodePrivateKey.Public()
	id, stableID := nodeIDs(nodePublicKey)

	netMap := &netmap.NetworkMap{
		NodeKey:    nodePublicKey,
		PrivateKey: nodePrivateKey,
		Addresses:  addresses,
		PacketFilter: []filter.Match{{
			// Allow any protocol!
			IPProto: []ipproto.Proto{ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6, ipproto.SCTP},
			// Allow traffic sourced from anywhere.
			Srcs: []netaddr.IPPrefix{
				netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
				netaddr.IPPrefixFrom(netaddr.IPv6Unspecified(), 0),
			},
			// Allow traffic to route anywhere.
			Dsts: []filter.NetPortRange{
				{
					Net: netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
					Ports: filter.PortRange{
						First: 0,
						Last:  65535,
					},
				},
				{
					Net: netaddr.IPPrefixFrom(netaddr.IPv6Unspecified(), 0),
					Ports: filter.PortRange{
						First: 0,
						Last:  65535,
					},
				},
			},
			Caps: []filter.CapMatch{},
		}},
	}
	// Identify itself as a node on the network with the addresses provided.
	netMap.SelfNode = &tailcfg.Node{
		ID:         id,
		StableID:   stableID,
		Key:        nodePublicKey,
		Addresses:  netMap.Addresses,
		AllowedIPs: append(netMap.Addresses, netaddr.MustParseIPPrefix("::/0")),
		Endpoints:  []string{},
		DERP:       DefaultDerpHome,
	}

	wgMonitor, err := monitor.New(Logf)
	if err != nil {
		return nil, xerrors.Errorf("create link monitor: %w", err)
	}

	dialer := new(tsdial.Dialer)
	dialer.Logf = Logf
	// Create a wireguard engine in userspace.
	engine, err := wgengine.NewUserspaceEngine(Logf, wgengine.Config{
		LinkMonitor: wgMonitor,
		Dialer:      dialer,
	})
	if err != nil {
		return nil, xerrors.Errorf("create wgengine: %w", err)
	}

	// This is taken from Tailscale:
	// https://github.com/tailscale/tailscale/blob/0f05b2c13ff0c305aa7a1655fa9c17ed969d65be/tsnet/tsnet.go#L247-L255
	// nolint
	tunDev, magicConn, dnsManager, ok := engine.(wgengine.InternalsGetter).GetInternals()
	if !ok {
		return nil, xerrors.New("could not get wgengine internals")
	}

	// Update the keys for the magic connection!
	err = magicConn.SetPrivateKey(nodePrivateKey)
	if err != nil {
		return nil, xerrors.Errorf("set node private key: %w", err)
	}
	netMap.SelfNode.DiscoKey = magicConn.DiscoPublicKey()

	// Create the networking stack.
	// This is called to route connections.
	netStack, err := netstack.Create(Logf, tunDev, engine, magicConn, dialer, dnsManager)
	if err != nil {
		return nil, xerrors.Errorf("create netstack: %w", err)
	}
	netStack.ProcessLocalIPs = true
	netStack.ProcessSubnets = true
	dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
		_, ok := engine.PeerForIP(ip)
		return ok
	}
	dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
		return netStack.DialContextTCP(ctx, dst)
	}
	err = netStack.Start()
	if err != nil {
		return nil, xerrors.Errorf("start netstack: %w", err)
	}
	engine = wgengine.NewWatchdog(engine)

	// Update the wireguard configuration to allow traffic to flow.
	cfg, err := nmcfg.WGCfg(netMap, Logf, netmap.AllowSingleHosts|netmap.AllowSubnetRoutes, netMap.SelfNode.StableID)
	if err != nil {
		return nil, xerrors.Errorf("create wgcfg: %w", err)
	}

	rtr := &router.Config{
		LocalAddrs: cfg.Addresses,
	}
	err = engine.Reconfig(cfg, rtr, &dns.Config{}, &tailcfg.Debug{})
	if err != nil {
		return nil, xerrors.Errorf("reconfig: %w", err)
	}

	engine.SetDERPMap(DerpMap)
	engine.SetNetworkMap(copyNetMap(netMap))

	ipb := netaddr.IPSetBuilder{}
	for _, addr := range netMap.Addresses {
		ipb.AddPrefix(addr)
	}
	ips, _ := ipb.IPSet()

	iplb := netaddr.IPSetBuilder{}
	ipl, _ := iplb.IPSet()
	engine.SetFilter(filter.New(netMap.PacketFilter, ips, ipl, nil, Logf))

	wn := &Network{
		logger:         logger,
		NodePrivateKey: nodePrivateKey,
		DiscoPublicKey: magicConn.DiscoPublicKey(),
		wgEngine:       engine,
		Netstack:       netStack,
		magicSock:      magicConn,
		netMap:         netMap,
		router:         rtr,
		listeners:      map[listenKey]*listener{},
	}
	netStack.ForwardTCPIn = wn.forwardTCP

	return wn, nil
}

// forwardTCP handles incoming connections from Wireguard in userspace.
func (n *Network) forwardTCP(conn net.Conn, port uint16) {
	n.mu.Lock()
	listener, ok := n.listeners[listenKey{"tcp", "", fmt.Sprint(port)}]
	n.mu.Unlock()
	if !ok {
		// No in-memory listener exists, forward to host.
		n.forwardTCPToLocalHandler(conn, port)
		return
	}

	timer := time.NewTimer(time.Second)
	defer timer.Stop()
	select {
	case listener.conn <- conn:
	case <-timer.C:
		_ = conn.Close()
	}
}

// forwardTCPToLocalHandler forwards the provided net.Conn to the
// matching port bound to localhost.
func (n *Network) forwardTCPToLocalHandler(c net.Conn, port uint16) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	defer c.Close()

	dialAddrStr := net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port)))
	var stdDialer net.Dialer
	server, err := stdDialer.DialContext(ctx, "tcp", dialAddrStr)
	if err != nil {
		n.logger.Debug(ctx, "dial local port", slog.F("port", port), slog.Error(err))
		return
	}
	defer server.Close()

	connClosed := make(chan error, 2)
	go func() {
		_, err := io.Copy(server, c)
		connClosed <- err
	}()
	go func() {
		_, err := io.Copy(c, server)
		connClosed <- err
	}()
	err = <-connClosed
	if err != nil {
		n.logger.Debug(ctx, "proxy connection closed with error", slog.Error(err))
	}
	n.logger.Debug(ctx, "forwarded connection closed", slog.F("local_addr", dialAddrStr))
}

// AddPeer allows connections from another Wireguard instance with the
// handshake credentials.
func (n *Network) AddPeer(handshake Handshake) error {
	n.mu.Lock()
	defer n.mu.Unlock()

	// If the peer already exists in the network map, do nothing.
	for _, p := range n.netMap.Peers {
		if p.Key == handshake.NodePublicKey {
			n.logger.Debug(context.Background(), "peer already in netmap", slog.F("peer", handshake.NodePublicKey.ShortString()))
			return nil
		}
	}

	// The Tailscale engine owns this slice, so we need to copy to make
	// modifications.
	peers := append(([]*tailcfg.Node)(nil), n.netMap.Peers...)

	id, stableID := nodeIDs(handshake.NodePublicKey)
	peers = append(peers, &tailcfg.Node{
		ID:         id,
		StableID:   stableID,
		Name:       handshake.NodePublicKey.String() + ".com",
		Key:        handshake.NodePublicKey,
		DiscoKey:   handshake.DiscoPublicKey,
		Addresses:  []netaddr.IPPrefix{netaddr.IPPrefixFrom(handshake.IPv6, 128)},
		AllowedIPs: []netaddr.IPPrefix{netaddr.IPPrefixFrom(handshake.IPv6, 128)},
		DERP:       DefaultDerpHome,
		Endpoints:  []string{DefaultDerpHome},
	})

	n.netMap.Peers = peers

	cfg, err := nmcfg.WGCfg(n.netMap, Logf, netmap.AllowSingleHosts|netmap.AllowSubnetRoutes, tailcfg.StableNodeID("nBBoJZ5CNTRL"))
	if err != nil {
		return xerrors.Errorf("create wgcfg: %w", err)
	}

	err = n.wgEngine.Reconfig(cfg, n.router, &dns.Config{}, &tailcfg.Debug{})
	if err != nil {
		return xerrors.Errorf("reconfig: %w", err)
	}

	// Always give the Tailscale engine a copy of our network map.
	n.wgEngine.SetNetworkMap(copyNetMap(n.netMap))
	return nil
}

// Ping sends a discovery ping to the provided peer.
// The peer address must be connected before a successful ping will work.
func (n *Network) Ping(ip netaddr.IP) *ipnstate.PingResult {
	ch := make(chan *ipnstate.PingResult)
	n.wgEngine.Ping(ip, tailcfg.PingDisco, func(pr *ipnstate.PingResult) {
		ch <- pr
	})
	return <-ch
}

// Listener returns a net.Listener in userspace that can be used to accept
// connections from the Wireguard network to the specified address. If a
// listener exists for a given address, all connections will be forwarded to the
// listener instead of being routed to the host.
func (n *Network) Listen(network, addr string) (net.Listener, error) {
	host, port, err := net.SplitHostPort(addr)
	if err != nil {
		return nil, xerrors.Errorf("split addr host port: %w", err)
	}

	lkey := listenKey{network, host, port}
	ln := &listener{
		wn:   n,
		key:  lkey,
		addr: addr,

		conn: make(chan net.Conn, 1),
	}

	n.mu.Lock()
	defer n.mu.Unlock()

	if _, ok := n.listeners[lkey]; ok {
		return nil, xerrors.Errorf("listener already open for %s, %s", network, addr)
	}
	n.listeners[lkey] = ln

	return ln, nil
}

func (n *Network) Close() error {
	// Close all listeners.
	for _, l := range n.listeners {
		_ = l.Close()
	}

	// Close the Wireguard netstack and engine.
	_ = n.Netstack.Close()
	n.wgEngine.Close()

	return nil
}

type listenKey struct {
	network string
	host    string
	port    string
}

type listener struct {
	wn   *Network
	key  listenKey
	addr string
	conn chan net.Conn
}

func (ln *listener) Accept() (net.Conn, error) {
	c, ok := <-ln.conn
	if !ok {
		return nil, xerrors.Errorf("tsnet: %w", net.ErrClosed)
	}
	return c, nil
}

func (ln *listener) Addr() net.Addr { return addr{ln} }
func (ln *listener) Close() error {
	ln.wn.mu.Lock()
	defer ln.wn.mu.Unlock()

	if v, ok := ln.wn.listeners[ln.key]; ok && v == ln {
		delete(ln.wn.listeners, ln.key)
		close(ln.conn)
	}

	return nil
}

type addr struct{ ln *listener }

func (a addr) Network() string { return a.ln.key.network }
func (a addr) String() string  { return a.ln.addr }

// nodeIDs generates Tailscale node IDs for the provided public key.
func nodeIDs(public key.NodePublic) (tailcfg.NodeID, tailcfg.StableNodeID) {
	idhash := fnv.New64()
	pub, _ := public.MarshalText()
	_, _ = idhash.Write(pub)

	return tailcfg.NodeID(idhash.Sum64()), tailcfg.StableNodeID(pub)
}

func copyNetMap(nm *netmap.NetworkMap) *netmap.NetworkMap {
	nmCopy := *nm
	return &nmCopy
}
