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

package vnet

import (
	"fmt"
	"net/netip"
)

var vips = map[string]virtualIP{} // DNS name => details

var (
	fakeDNS               = newVIP("dns", "4.11.4.11", "2411::411")
	fakeProxyControlplane = newVIP("controlplane.tailscale.com", 1)
	fakeTestAgent         = newVIP("test-driver.tailscale", 2)
	fakeControl           = newVIP("control.tailscale", 3)
	fakeDERP1             = newVIP("derp1.tailscale", "33.4.0.1") // 3340=DERP; 1=derp 1
	fakeDERP2             = newVIP("derp2.tailscale", "33.4.0.2") // 3340=DERP; 2=derp 2
	fakeLogCatcher        = newVIP("log.tailscale.com", 4)
	fakeSyslog            = newVIP("syslog.tailscale", 9)
	fakeCloudInit         = newVIP("cloud-init.tailscale", 5) // serves cloud-init metadata/userdata per node
	fakeFiles             = newVIP("files.tailscale", 6)      // serves binary files (tta, tailscale, tailscaled) to VMs

	// FakeDualStackWeb is a dual-stack webserver VIP used by
	// TestExitNodeV4Only to verify that traffic works through an
	// IPv4-only exit node even when DNS returns both A and AAAA.
	FakeDualStackWeb = newVIP("dualstack-web.example.com", "5.0.0.100", "2052::5:100")
)

type virtualIP struct {
	name string // for DNS
	v4   netip.Addr
	v6   netip.Addr
}

func (v virtualIP) Match(a netip.Addr) bool {
	return v.v4 == a.Unmap() || v.v6 == a
}

// TestDriverIPv4 returns the IPv4 address of the test driver VIP (52.52.0.2).
// TTA agents dial this IP on port TestDriverPort to connect to the test harness.
func TestDriverIPv4() netip.Addr { return fakeTestAgent.v4 }

// TestDriverPort is the port the test driver listens on.
const TestDriverPort = 8008

// FakeDNSIPv4 returns the fake DNS IPv4 address.
func FakeDNSIPv4() netip.Addr { return fakeDNS.v4 }

// FakeDNSIPv6 returns the fake DNS IPv6 address.
func FakeDNSIPv6() netip.Addr { return fakeDNS.v6 }

// FakeSyslogIPv4 returns the fake syslog IPv4 address.
func FakeSyslogIPv4() netip.Addr { return fakeSyslog.v4 }

// FakeSyslogIPv6 returns the fake syslog IPv6 address.
func FakeSyslogIPv6() netip.Addr { return fakeSyslog.v6 }

// newVIP returns a new virtual IP.
//
// opts may be an IPv4 an IPv6 (in string form) or an int (bounded by uint8) to
// use IPv4 of 52.52.0.x.
//
// If the IPv6 is omitted, one is derived from the IPv4.
//
// If an opt is invalid or the DNS name is already used, it panics.
func newVIP(name string, opts ...any) (v virtualIP) {
	if _, ok := vips[name]; ok {
		panic(fmt.Sprintf("duplicate VIP %q", name))
	}
	v.name = name
	for _, o := range opts {
		switch o := o.(type) {
		case string:
			if ip, err := netip.ParseAddr(o); err == nil {
				if ip.Is4() {
					v.v4 = ip
				} else if ip.Is6() {
					v.v6 = ip
				}
			} else {
				panic(fmt.Sprintf("unsupported string option %q", o))
			}
		case int:
			if o <= 0 || o > 255 {
				panic(fmt.Sprintf("bad octet %d", o))
			}
			v.v4 = netip.AddrFrom4([4]byte{52, 52, 0, byte(o)})
		default:
			panic(fmt.Sprintf("unknown option type %T", o))
		}
	}
	if !v.v6.IsValid() && v.v4.IsValid() {
		// Map 1.2.3.4 to 2052::0102:0304
		// But make 52.52.0.x map to 2052::x
		a := [16]byte{0: 0x20, 1: 0x52} // 2052::
		v4 := v.v4.As4()
		if v4[0] == 52 && v4[1] == 52 && v4[2] == 0 {
			a[15] = v4[3]
		} else {
			copy(a[12:], v.v4.AsSlice())
		}
		v.v6 = netip.AddrFrom16(a)
	}
	for _, b := range vips {
		if b.Match(v.v4) || b.Match(v.v6) {
			panic(fmt.Sprintf("VIP %q collides with %q", name, v.name))
		}
	}
	vips[name] = v
	return v
}
