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

package vnet

import (
	"cmp"
	"context"
	"fmt"
	"iter"
	"net/netip"
	"os"
	"slices"
	"time"

	"github.com/google/gopacket/layers"
	"github.com/google/gopacket/pcapgo"
	"tailscale.com/tailcfg"
	"tailscale.com/types/logger"
	"tailscale.com/util/must"
	"tailscale.com/util/set"
)

// Note: the exported Node and Network are the configuration types;
// the unexported node and network are the runtime types that are actually
// used once the server is created.

// Config is the requested state of the natlab virtual network.
//
// The zero value is a valid empty configuration. Call AddNode
// and AddNetwork to methods on the returned Node and Network
// values to modify the config before calling NewServer.
// Once the NewServer is called, Config is no longer used.
type Config struct {
	nodes        []*Node
	networks     []*Network
	pcapFile     string
	blendReality bool
}

// SetPCAPFile sets the filename to write a pcap file to,
// or empty to disable pcap file writing.
func (c *Config) SetPCAPFile(file string) {
	c.pcapFile = file
}

// NumNodes returns the number of nodes in the configuration.
func (c *Config) NumNodes() int {
	return len(c.nodes)
}

// SetBlendReality sets whether to blend the real controlplane.tailscale.com and
// DERP servers into the virtual network. This is mostly useful for interactive
// testing when working on natlab.
func (c *Config) SetBlendReality(v bool) {
	c.blendReality = v
}

// FirstNetwork returns the first network in the config, or nil if none.
func (c *Config) FirstNetwork() *Network {
	if len(c.networks) == 0 {
		return nil
	}
	return c.networks[0]
}

func (c *Config) Nodes() iter.Seq2[int, *Node] {
	return slices.All(c.nodes)
}

func nodeMac(n int) MAC {
	// 52=TS then 0xcc for cccclient
	return MAC{0x52, 0xcc, 0xcc, 0xcc, 0xcc, byte(n)}
}

// nodeNICMac returns the MAC for the nicIdx-th secondary NIC (1-indexed) of node n.
// Primary NICs (index 0) use nodeMac. Secondary NICs use a different scheme:
// 52:cc:cc:cc:KK:NN where KK is the NIC index and NN is the node number.
func nodeNICMac(nodeNum, nicIdx int) MAC {
	return MAC{0x52, 0xcc, 0xcc, 0xcc, byte(nicIdx), byte(nodeNum)}
}

func routerMac(n int) MAC {
	// 52=TS then 0xee for 'etwork
	return MAC{0x52, 0xee, 0xee, 0xee, 0xee, byte(n)}
}

var lanSLAACBase = netip.MustParseAddr("fe80::50cc:ccff:fecc:cc01")

// nodeLANIP6 returns a node number's Link Local SLAAC IPv6 address,
// such as fe80::50cc:ccff:fecc:cc03 for node 3.
func nodeLANIP6(n int) netip.Addr {
	a := lanSLAACBase.As16()
	a[15] = byte(n)
	return netip.AddrFrom16(a)
}

// AddNode creates a new node in the world.
//
// The opts may be of the following types:
//   - *Network: zero, one, or more networks to add this node to
//   - TODO: more
//
// On an error or unknown opt type, AddNode returns a
// node with a carried error that gets returned later.
func (c *Config) AddNode(opts ...any) *Node {
	num := len(c.nodes) + 1
	n := &Node{
		num: num,
		mac: nodeMac(num),
	}
	c.nodes = append(c.nodes, n)
	for _, o := range opts {
		switch o := o.(type) {
		case *Network:
			if !slices.Contains(o.nodes, n) {
				o.nodes = append(o.nodes, n)
			}
			n.nets = append(n.nets, o)
		case TailscaledEnv:
			n.env = append(n.env, o)
		case NodeOption:
			switch o {
			case HostFirewall:
				n.hostFW = true
			case RotateDisco:
				n.rotateDisco = true
			case PreICMPPing:
				n.preICMPPing = true
			case DontJoinTailnet:
				n.dontJoinTailnet = true
			case VerboseSyslog:
				n.verboseSyslog = true
			default:
				if n.err == nil {
					n.err = fmt.Errorf("unknown NodeOption %q", o)
				}
			}
		case MAC:
			n.mac = o
		case tailcfg.NodeCapMap:
			n.capMap = o
		default:
			if n.err == nil {
				n.err = fmt.Errorf("unknown AddNode option type %T", o)
			}
		}
	}
	return n
}

// NodeOption is an option that can be passed to Config.AddNode.
type NodeOption string

const (
	HostFirewall    NodeOption = "HostFirewall"
	RotateDisco     NodeOption = "RotateDisco"
	PreICMPPing     NodeOption = "PreICMPPing"
	DontJoinTailnet NodeOption = "DontJoinTailnet"
	VerboseSyslog   NodeOption = "VerboseSyslog"
)

// TailscaledEnv is а option that can be passed to Config.AddNode
// to set an environment variable for tailscaled.
type TailscaledEnv struct {
	Key, Value string
}

// AddNetwork add a new network.
//
// The opts may be of the following types:
//   - string IP address, for the network's WAN IP (if any)
//   - string netip.Prefix, for the network's LAN IP (defaults to 192.168.0.0/24)
//     if IPv4, or its WAN IPv6 + CIDR (e.g. "2000:52::1/64")
//   - NAT, the type of NAT to use
//   - NetworkService, a service to add to the network
//
// On an error or unknown opt type, AddNetwork returns a
// network with a carried error that gets returned later.
func (c *Config) AddNetwork(opts ...any) *Network {
	num := len(c.networks) + 1
	n := &Network{
		num: num,
		mac: routerMac(num),
	}
	c.networks = append(c.networks, n)
	for _, o := range opts {
		switch o := o.(type) {
		case string:
			if ip, err := netip.ParseAddr(o); err == nil {
				n.wanIP4 = ip
			} else if ip, err := netip.ParsePrefix(o); err == nil {
				// If the prefix is IPv4, treat it as the router's internal IPv4 address + CIDR.
				// If the prefix is IPv6, treat it as the router's WAN IPv6 + CIDR (typically a /64).
				if ip.Addr().Is4() {
					n.lanIP4 = ip
				} else if ip.Addr().Is6() {
					n.wanIP6 = ip
				}
			} else {
				if n.err == nil {
					n.err = fmt.Errorf("unknown string option %q", o)
				}
			}
		case NAT:
			n.natType = o
		case NetworkService:
			n.AddService(o)
		default:
			if n.err == nil {
				n.err = fmt.Errorf("unknown AddNetwork option type %T", o)
			}
		}
	}
	return n
}

// Node is the configuration of a node in the virtual network.
type Node struct {
	err    error
	num    int   // 1-based node number
	n      *node // nil until NewServer called
	client *NodeAgentClient

	env             []TailscaledEnv
	hostFW          bool
	rotateDisco     bool
	preICMPPing     bool
	verboseSyslog   bool
	dontJoinTailnet bool
	capMap          tailcfg.NodeCapMap

	// TODO(bradfitz): this is halfway converted to supporting multiple NICs
	// but not done. We need a MAC-per-Network.

	mac  MAC
	nets []*Network
}

// Num returns the 1-based node number.
func (n *Node) Num() int {
	return n.num
}

// String returns the string "nodeN" where N is the 1-based node number.
func (n *Node) String() string {
	return fmt.Sprintf("node%d", n.num)
}

// MAC returns the MAC address of the node's primary NIC.
func (n *Node) MAC() MAC {
	return n.mac
}

// NumNICs returns the number of network interfaces on the node
// (one per network the node is on).
func (n *Node) NumNICs() int {
	return len(n.nets)
}

// NICMac returns the MAC address for the i-th NIC (0-indexed).
// NIC 0 is the primary NIC (same as MAC()). NIC 1+ are extra NICs.
func (n *Node) NICMac(i int) MAC {
	if i == 0 {
		return n.mac
	}
	return nodeNICMac(n.num, i)
}

// Networks returns the list of networks this node is on.
func (n *Node) Networks() []*Network {
	return n.nets
}

func (n *Node) Env() []TailscaledEnv {
	return n.env
}

func (n *Node) HostFirewall() bool {
	return n.hostFW
}

func (n *Node) VerboseSyslog() bool {
	return n.verboseSyslog
}

func (n *Node) SetVerboseSyslog(v bool) {
	n.verboseSyslog = v
}

func (n *Node) SetClient(c *NodeAgentClient) {
	n.client = c
}

// PostConnectedToControl should be called after the clients have connected to
// control to modify the client behaviour after getting the network maps.
// Currently, the only implemented behavior is rotating disco keys.
func (n *Node) PostConnectedToControl(ctx context.Context) error {
	if n.rotateDisco {
		if err := n.client.DebugAction(ctx, "rotate-disco-key"); err != nil {
			return err
		}
	}
	return nil
}

// PreICMPPing reports whether node should send an ICMP Ping sent before
// the disco ping. This is important for the nodes having rotated their
// disco keys while control is down. Disco pings deliberately does not
// trigger a TSMPDiscoKeyAdvertisement, making the need for other traffic (here
// simlulated as an ICMP ping) needed first. Any traffic could trigger this key
// exchange, the ICMP Ping is used as a handy existing way of sending some
// non-disco traffic.
func (n *Node) PreICMPPing() bool {
	return n.preICMPPing
}

// ShouldJoinTailnet reports whether node should join the test tailnet. Machines in
// the virtual universe that aren't on the tailnet are useful for testing that
// Tailscale does not break connectivity to resources outside the tailnet.
func (n *Node) ShouldJoinTailnet() bool {
	return !n.dontJoinTailnet
}

// WantCapMap returns the [tailcfg.NodeCapMap] that control should send down to
// this node, if any.
func (n *Node) WantCapMap() tailcfg.NodeCapMap {
	return n.capMap
}

// IsV6Only reports whether this node is only connected to IPv6 networks.
func (n *Node) IsV6Only() bool {
	for _, net := range n.nets {
		if net.CanV4() {
			return false
		}
	}
	for _, net := range n.nets {
		if net.CanV6() {
			return true
		}
	}
	return false
}

// LanIP returns the node's LAN IPv4 address on the given network.
// It requires the [Server] to have been initialized (i.e., [New] was called).
// Returns an invalid addr if the node has no IP on that network.
func (n *Node) LanIP(net *Network) netip.Addr {
	if n.n == nil {
		return netip.Addr{}
	}
	for i, nn := range n.nets {
		if nn == net {
			if i == 0 {
				return n.n.lanIP
			}
			if i-1 < len(n.n.extraNICs) {
				return n.n.extraNICs[i-1].lanIP
			}
		}
	}
	return netip.Addr{}
}

// Network returns the first network this node is connected to,
// or nil if none.
func (n *Node) Network() *Network {
	if len(n.nets) == 0 {
		return nil
	}
	return n.nets[0]
}

// Network is the configuration of a network in the virtual network.
type Network struct {
	num     int // 1-based
	mac     MAC // MAC address of the router/gateway
	natType NAT

	wanIP6 netip.Prefix // global unicast router in host bits; CIDR is /64 delegated to LAN

	wanIP4                      netip.Addr // IPv4 WAN IP, if any
	lanIP4                      netip.Prefix
	nodes                       []*Node
	breakWAN4                   bool // whether to break WAN IPv4 connectivity
	postConnectBlackholeControl bool // whether to break control connectivity after nodes have connected
	network                     *network

	svcs set.Set[NetworkService]

	latency  time.Duration // latency applied to interface writes
	lossRate float64       // chance of packet loss (0.0 to 1.0)

	// ...
	err error // carried error
}

// SetLatency sets the simulated network latency for this network.
func (n *Network) SetLatency(d time.Duration) {
	n.latency = d
}

// SetPacketLoss sets the packet loss rate for this network 0.0 (no loss) to 1.0 (total loss).
func (n *Network) SetPacketLoss(rate float64) {
	if rate < 0 {
		rate = 0
	} else if rate > 1 {
		rate = 1
	}
	n.lossRate = rate
}

// SetBlackholedIPv4 sets whether the network should blackhole all IPv4 traffic
// out to the Internet. (DHCP etc continues to work on the LAN.)
func (n *Network) SetBlackholedIPv4(v bool) {
	n.breakWAN4 = v
}

// SetPostConnectControlBlackhole sets whether the network should blackhole all
// traffic to the control server after the clients have connected.
func (n *Network) SetPostConnectControlBlackhole(v bool) {
	n.postConnectBlackholeControl = v
}

func (n *Network) CanV4() bool {
	return n.lanIP4.IsValid() || n.wanIP4.IsValid()
}

func (n *Network) CanV6() bool {
	return n.wanIP6.IsValid()
}

func (n *Network) CanTakeMoreNodes() bool {
	if n.natType == One2OneNAT {
		return len(n.nodes) == 0
	}
	return len(n.nodes) < 150
}

// PostConnectedToControl should be called after the clients have connected to
// the control server to modify network behaviors. Currently the only
// implemented behavior is to conditionally blackhole traffic to control.
func (n *Network) PostConnectedToControl() {
	n.network.SetControlBlackholed(n.postConnectBlackholeControl)
}

// NetworkService is a service that can be added to a network.
type NetworkService string

const (
	NATPMP NetworkService = "NAT-PMP"
	PCP    NetworkService = "PCP"
	UPnP   NetworkService = "UPnP"
)

// AddService adds a network service (such as port mapping protocols) to a
// network.
func (n *Network) AddService(s NetworkService) {
	if n.svcs == nil {
		n.svcs = set.Of(s)
	} else {
		n.svcs.Add(s)
	}
}

// initFromConfig initializes the server from the previous calls
// to NewNode and NewNetwork and returns an error if
// there were any configuration issues.
func (s *Server) initFromConfig(c *Config) error {
	netOfConf := map[*Network]*network{}
	if c.pcapFile != "" {
		pcf, err := os.OpenFile(c.pcapFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
		if err != nil {
			return err
		}
		nw, err := pcapgo.NewNgWriter(pcf, layers.LinkTypeEthernet)
		if err != nil {
			return err
		}
		pw := &pcapWriter{
			f: pcf,
			w: nw,
		}
		s.pcapWriter = pw
	}
	for i, conf := range c.networks {
		if conf.err != nil {
			return conf.err
		}
		if !conf.lanIP4.IsValid() && !conf.wanIP6.IsValid() {
			conf.lanIP4 = netip.MustParsePrefix("192.168.0.0/24")
		}
		n := &network{
			num:        conf.num,
			s:          s,
			mac:        conf.mac,
			portmap:    conf.svcs.Contains(NATPMP), // TODO: expand network.portmap
			wanIP6:     conf.wanIP6,
			v4:         conf.lanIP4.IsValid(),
			v6:         conf.wanIP6.IsValid(),
			wanIP4:     conf.wanIP4,
			lanIP4:     conf.lanIP4,
			breakWAN4:  conf.breakWAN4,
			latency:    conf.latency,
			lossRate:   conf.lossRate,
			nodesByIP4: map[netip.Addr]*node{},
			nodesByMAC: map[MAC]*node{},
			logf:       logger.WithPrefix(s.logf, fmt.Sprintf("[net-%v] ", conf.mac)),
		}
		netOfConf[conf] = n
		s.networks.Add(n)

		conf.network = n
		if conf.wanIP4.IsValid() {
			if conf.wanIP4.Is6() {
				return fmt.Errorf("invalid IPv6 address in wanIP")
			}
			if _, ok := s.networkByWAN.Lookup(conf.wanIP4); ok {
				return fmt.Errorf("two networks have the same WAN IP %v; Anycast not (yet?) supported", conf.wanIP4)
			}
			s.networkByWAN.Insert(netip.PrefixFrom(conf.wanIP4, 32), n)
		}
		if conf.wanIP6.IsValid() {
			if conf.wanIP6.Addr().Is4() {
				return fmt.Errorf("invalid IPv4 address in wanIP6")
			}
			if _, ok := s.networkByWAN.LookupPrefix(conf.wanIP6); ok {
				return fmt.Errorf("two networks have the same WAN IPv6 %v; Anycast not (yet?) supported", conf.wanIP6)
			}
			s.networkByWAN.Insert(conf.wanIP6, n)
		}
		n.lanInterfaceID = must.Get(s.pcapWriter.AddInterface(pcapgo.NgInterface{
			Name:     fmt.Sprintf("network%d-lan", i+1),
			LinkType: layers.LinkTypeIPv4,
		}))
		n.wanInterfaceID = must.Get(s.pcapWriter.AddInterface(pcapgo.NgInterface{
			Name:     fmt.Sprintf("network%d-wan", i+1),
			LinkType: layers.LinkTypeIPv4,
		}))
	}
	for _, conf := range c.nodes {
		if conf.err != nil {
			return conf.err
		}
		primaryNet := netOfConf[conf.Network()]
		n := &node{
			num:           conf.num,
			mac:           conf.mac,
			net:           primaryNet,
			verboseSyslog: conf.VerboseSyslog(),
		}
		n.interfaceID = must.Get(s.pcapWriter.AddInterface(pcapgo.NgInterface{
			Name:     n.String(),
			LinkType: layers.LinkTypeEthernet,
		}))
		conf.n = n
		if _, ok := s.nodeByMAC[n.mac]; ok {
			return fmt.Errorf("two nodes have the same MAC %v", n.mac)
		}
		s.nodes = append(s.nodes, n)
		s.nodeByMAC[n.mac] = n

		if n.net != nil && n.net.v4 {
			// Allocate a lanIP for the node. Use the network's CIDR and use final
			// octet 101 (for first node), 102, etc. The node number comes from the
			// last octet of the MAC address (0-based)
			ip4 := n.net.lanIP4.Addr().As4()
			ip4[3] = 100 + n.mac[5]
			n.lanIP = netip.AddrFrom4(ip4)
			n.net.nodesByIP4[n.lanIP] = n
		}
		if n.net != nil {
			n.net.nodesByMAC[n.mac] = n
		}

		// Set up extra NICs for multi-homed nodes (nodes on more than one network).
		for nicIdx, confNet := range conf.nets[1:] {
			extraNet := netOfConf[confNet]
			if extraNet == nil {
				continue
			}
			mac := nodeNICMac(conf.num, nicIdx+1)
			nic := nodeNIC{
				mac: mac,
				net: extraNet,
			}
			nic.interfaceID = must.Get(s.pcapWriter.AddInterface(pcapgo.NgInterface{
				Name:     fmt.Sprintf("%s-nic%d", n.String(), nicIdx+1),
				LinkType: layers.LinkTypeEthernet,
			}))
			// Allocate a lanIP for the node. Use the network's CIDR and use final
			// octet 101 (for first node), 102, etc. The node number comes from the
			// last octet of the MAC address (0-based)
			if extraNet.v4 {
				ip4 := extraNet.lanIP4.Addr().As4()
				ip4[3] = 100 + mac[5]
				nic.lanIP = netip.AddrFrom4(ip4)
				extraNet.nodesByIP4[nic.lanIP] = n
			}
			extraNet.nodesByMAC[mac] = n
			if _, ok := s.nodeByMAC[mac]; ok {
				return fmt.Errorf("two nodes have the same MAC %v", mac)
			}
			s.nodeByMAC[mac] = n
			n.extraNICs = append(n.extraNICs, nic)
		}
	}

	// Now that nodes are populated, set up NAT:
	for _, conf := range c.networks {
		n := netOfConf[conf]
		natType := cmp.Or(conf.natType, EasyNAT)
		if err := n.InitNAT(natType); err != nil {
			return err
		}
	}

	return nil
}
