Implement the following plan:

# Performance Test Tool for Combined Server

## Context

Need a standalone Go program to performance-test the combined NetBird deployment by spinning up N client peers, connecting them to a management server, and measuring tunnel communication. This tool lives in `combined/perftest/` and is compiled into a standalone binary.

## Approach

Use the **`client/embed` package** (`client/embed/embed.go`) which provides a clean API for programmatically creating NetBird clients. It handles:
- Userspace WireGuard via netstack (no root needed, no kernel interfaces)
- In-memory config generation (unique WG keys per peer)
- Auth + registration with setup key
- `Dial()`, `ListenUDP()`, `ListenTCP()` for tunnel traffic
- `Status()` for peer connectivity checks

## New File

### `combined/perftest/main.go`

Single-file Go program (~400 lines). Structure:

#### CLI Flags
```
--management-url  string   Management server URL (required)
--setup-key       string   Reusable setup key (required)
--peers           int      Number of peers to spawn (default: 5)
--force-relay     bool     Set NB_FORCE_RELAY=true (default: false)
--duration        duration Traffic test duration (default: 30s)
--packet-size     int      UDP packet size in bytes (default: 512)
--log-level       string   Log level (default: info)
```

#### Program Flow

**1. Init phase**
- Parse flags
- If `--force-relay`, set `os.Setenv("NB_FORCE_RELAY", "true")`
- Set `os.Setenv("NB_USE_NETSTACK_MODE", "true")` (embed does this but we set it early)

**2. Peer startup phase** (sequential `New()`, parallel `Start()`)
- Create N `embed.Client` instances via `embed.New(embed.Options{...})`
  - Each gets: unique `DeviceName` (e.g. `"perf-peer-0"`), same `SetupKey`, same `ManagementURL`
  - `WireguardPort: ptr(0)` for auto-assignment
- Start all clients in parallel goroutines via `client.Start(ctx)`
- Wait for all to start (with timeout)
- Print startup times

**3. Connection wait phase**
- Poll `client.Status()` on each client until every peer sees all other peers as `StatusConnected`
- Expected: each peer sees N-1 connected peers
- Timeout after a configurable wait (e.g. 120s)
- Print connection establishment time and per-peer connection type (relayed vs direct)

**4. Traffic phase**
- Each peer starts a UDP echo listener via `client.ListenUDP(":9000")`
- For each peer pair (i, j) where i < j:
  - Peer i sends UDP packets to peer j's tunnel IP via `client.Dial(ctx, "udp", "<peer_j_ip>:9000")`
  - Measure round-trip time (echo response) and throughput
- Run for `--duration` time
- Collect stats: packets sent, received, lost, avg/min/max RTT, throughput

**5. Cleanup & report**
- Stop all clients via `client.Stop(ctx)`
- Print summary: connection times, packet loss, RTT stats, throughput per pair

#### Key Code Patterns

Creating a peer (using `client/embed/embed.go`):
```go
port := 0
c, err := embed.New(embed.Options{
    DeviceName:    fmt.Sprintf("perf-peer-%d", i),
    SetupKey:      setupKey,
    ManagementURL: managementURL,
    WireguardPort: &port,
    LogLevel:      logLevel,
})
```

Getting peer tunnel IPs from status:
```go
status, _ := c.Status()
localIP := status.LocalPeerState.IP  // e.g. "100.64.0.1/16"
```

Getting connected peer IPs:
```go
for _, p := range status.Peers {
    if p.ConnStatus == peer.StatusConnected {
        peerIP := p.IP  // tunnel IP of connected peer
    }
}
```

UDP echo listener (runs on each peer):
```go
conn, _ := client.ListenUDP(":9000")
// echo loop: read packet, write it back
```

UDP sender (measures RTT):
```go
conn, _ := client.Dial(ctx, "udp", fmt.Sprintf("%s:9000", peerIP))
// send packet with timestamp, wait for echo, measure RTT
```

## Dependencies

All existing - no new Go modules needed:
- `github.com/netbirdio/netbird/client/embed` - main client API
- `github.com/netbirdio/netbird/client/internal/peer` - for `StatusConnected` constant
- Standard library: `flag`, `fmt`, `time`, `net`, `sync`, `context`, `os`, `math`, `strings`

## Key Files Referenced

| File | Usage |
|------|-------|
| `client/embed/embed.go` | Core API: `New()`, `Start()`, `Stop()`, `Dial()`, `ListenUDP()`, `Status()` |
| `client/internal/peer/status.go` | `FullStatus`, `State`, `ConnStatus` types |
| `client/internal/peer/env.go` | `NB_FORCE_RELAY` env var |
| `client/iface/netstack/env.go` | `NB_USE_NETSTACK_MODE` env var |
| `client/internal/profilemanager/config.go` | `CreateInMemoryConfig()` (used internally by embed) |

## Verification

1. Build: `cd combined/perftest && go build -o perftest .`
2. Start a combined server (e.g. `cd combined && go run . --config config-simple.yaml`)
3. Create a reusable setup key in the management dashboard
4. Run: `./perftest --management-url https://localhost:443 --setup-key <key> --peers 3`
5. Expected output: peers connect, traffic flows, stats printed
6. Test with relay: `./perftest --management-url https://localhost:443 --setup-key <key> --peers 3 --force-relay`

If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/misha/.REDACTED.jsonl


I'll start by reading the key source files to understand the exact APIs available, then implement the perftest tool.


     1→package embed
     2→
     3→import (
     4→	"context"
     5→	"errors"
     6→	"fmt"
     7→	"io"
     8→	"net"
     9→	"net/http"
    10→	"net/netip"
    11→	"os"
    12→	"sync"
    13→
    14→	"github.com/sirupsen/logrus"
    15→	wgnetstack "golang.zx2c4.com/wireguard/tun/netstack"
    16→
    17→	"github.com/netbirdio/netbird/client/iface/netstack"
    18→	"github.com/netbirdio/netbird/client/internal"
    19→	"github.com/netbirdio/netbird/client/internal/auth"
    20→	"github.com/netbirdio/netbird/client/internal/peer"
    21→	"github.com/netbirdio/netbird/client/internal/profilemanager"
    22→	sshcommon "github.com/netbirdio/netbird/client/ssh"
    23→	"github.com/netbirdio/netbird/client/system"
    24→	mgmProto "github.com/netbirdio/netbird/shared/management/proto"
    25→)
    26→
    27→var (
    28→	ErrClientAlreadyStarted = errors.New("client already started")
    29→	ErrClientNotStarted     = errors.New("client not started")
    30→	ErrEngineNotStarted     = errors.New("engine not started")
    31→	ErrConfigNotInitialized = errors.New("config not initialized")
    32→)
    33→
    34→// PeerConnStatus is a peer's connection status.
    35→type PeerConnStatus = peer.ConnStatus
    36→
    37→const (
    38→	// PeerStatusConnected indicates the peer is in connected state.
    39→	PeerStatusConnected = peer.StatusConnected
    40→)
    41→
    42→// Client manages a netbird embedded client instance.
    43→type Client struct {
    44→	deviceName string
    45→	config     *profilemanager.Config
    46→	mu         sync.Mutex
    47→	cancel     context.CancelFunc
    48→	setupKey   string
    49→	jwtToken   string
    50→	connect    *internal.ConnectClient
    51→	recorder   *peer.Status
    52→}
    53→
    54→// Options configures a new Client.
    55→type Options struct {
    56→	// DeviceName is this peer's name in the network
    57→	DeviceName string
    58→	// SetupKey is used for authentication
    59→	SetupKey string
    60→	// JWTToken is used for JWT-based authentication
    61→	JWTToken string
    62→	// PrivateKey is used for direct private key authentication
    63→	PrivateKey string
    64→	// ManagementURL overrides the default management server URL
    65→	ManagementURL string
    66→	// PreSharedKey is the pre-shared key for the WireGuard interface
    67→	PreSharedKey string
    68→	// LogOutput is the output destination for logs (defaults to os.Stderr if nil)
    69→	LogOutput io.Writer
    70→	// LogLevel sets the logging level (defaults to info if empty)
    71→	LogLevel string
    72→	// NoUserspace disables the userspace networking mode. Needs admin/root privileges
    73→	NoUserspace bool
    74→	// ConfigPath is the path to the netbird config file. If empty, the config will be stored in memory and not persisted.
    75→	ConfigPath string
    76→	// StatePath is the path to the netbird state file
    77→	StatePath string
    78→	// DisableClientRoutes disables the client routes
    79→	DisableClientRoutes bool
    80→	// BlockInbound blocks all inbound connections from peers
    81→	BlockInbound bool
    82→	// WireguardPort is the port for the WireGuard interface. Use 0 for a random port.
    83→	WireguardPort *int
    84→}
    85→
    86→// validateCredentials checks that exactly one credential type is provided
    87→func (opts *Options) validateCredentials() error {
    88→	credentialsProvided := 0
    89→	if opts.SetupKey != "" {
    90→		credentialsProvided++
    91→	}
    92→	if opts.JWTToken != "" {
    93→		credentialsProvided++
    94→	}
    95→	if opts.PrivateKey != "" {
    96→		credentialsProvided++
    97→	}
    98→
    99→	if credentialsProvided == 0 {
   100→		return fmt.Errorf("one of SetupKey, JWTToken, or PrivateKey must be provided")
   101→	}
   102→	if credentialsProvided > 1 {
   103→		return fmt.Errorf("only one of SetupKey, JWTToken, or PrivateKey can be specified")
   104→	}
   105→
   106→	return nil
   107→}
   108→
   109→// New creates a new netbird embedded client.
   110→func New(opts Options) (*Client, error) {
   111→	if err := opts.validateCredentials(); err != nil {
   112→		return nil, err
   113→	}
   114→
   115→	if opts.LogOutput != nil {
   116→		logrus.SetOutput(opts.LogOutput)
   117→	}
   118→
   119→	if opts.LogLevel != "" {
   120→		level, err := logrus.ParseLevel(opts.LogLevel)
   121→		if err != nil {
   122→			return nil, fmt.Errorf("parse log level: %w", err)
   123→		}
   124→		logrus.SetLevel(level)
   125→	}
   126→
   127→	if !opts.NoUserspace {
   128→		if err := os.Setenv(netstack.EnvUseNetstackMode, "true"); err != nil {
   129→			return nil, fmt.Errorf("setenv: %w", err)
   130→		}
   131→		if err := os.Setenv(netstack.EnvSkipProxy, "true"); err != nil {
   132→			return nil, fmt.Errorf("setenv: %w", err)
   133→		}
   134→	}
   135→
   136→	if opts.StatePath != "" {
   137→		// TODO: Disable state if path not provided
   138→		if err := os.Setenv("NB_DNS_STATE_FILE", opts.StatePath); err != nil {
   139→			return nil, fmt.Errorf("setenv: %w", err)
   140→		}
   141→	}
   142→
   143→	t := true
   144→	var config *profilemanager.Config
   145→	var err error
   146→	input := profilemanager.ConfigInput{
   147→		ConfigPath:          opts.ConfigPath,
   148→		ManagementURL:       opts.ManagementURL,
   149→		PreSharedKey:        &opts.PreSharedKey,
   150→		DisableServerRoutes: &t,
   151→		DisableClientRoutes: &opts.DisableClientRoutes,
   152→		BlockInbound:        &opts.BlockInbound,
   153→		WireguardPort:       opts.WireguardPort,
   154→	}
   155→	if opts.ConfigPath != "" {
   156→		config, err = profilemanager.UpdateOrCreateConfig(input)
   157→	} else {
   158→		config, err = profilemanager.CreateInMemoryConfig(input)
   159→	}
   160→	if err != nil {
   161→		return nil, fmt.Errorf("create config: %w", err)
   162→	}
   163→
   164→	if opts.PrivateKey != "" {
   165→		config.PrivateKey = opts.PrivateKey
   166→	}
   167→
   168→	return &Client{
   169→		deviceName: opts.DeviceName,
   170→		setupKey:   opts.SetupKey,
   171→		jwtToken:   opts.JWTToken,
   172→		config:     config,
   173→		recorder:   peer.NewRecorder(config.ManagementURL.String()),
   174→	}, nil
   175→}
   176→
   177→// Start begins client operation and blocks until the engine has been started successfully or a startup error occurs.
   178→// Pass a context with a deadline to limit the time spent waiting for the engine to start.
   179→func (c *Client) Start(startCtx context.Context) error {
   180→	c.mu.Lock()
   181→	defer c.mu.Unlock()
   182→	if c.connect != nil {
   183→		return ErrClientAlreadyStarted
   184→	}
   185→
   186→	ctx, cancel := context.WithCancel(internal.CtxInitState(context.Background()))
   187→	defer func() {
   188→		if c.connect == nil {
   189→			cancel()
   190→		}
   191→	}()
   192→
   193→	// nolint:staticcheck
   194→	ctx = context.WithValue(ctx, system.DeviceNameCtxKey, c.deviceName)
   195→
   196→	authClient, err := auth.NewAuth(ctx, c.config.PrivateKey, c.config.ManagementURL, c.config)
   197→	if err != nil {
   198→		return fmt.Errorf("create auth client: %w", err)
   199→	}
   200→	defer authClient.Close()
   201→
   202→	if err, _ := authClient.Login(ctx, c.setupKey, c.jwtToken); err != nil {
   203→		return fmt.Errorf("login: %w", err)
   204→	}
   205→	client := internal.NewConnectClient(ctx, c.config, c.recorder, false)
   206→	client.SetSyncResponsePersistence(true)
   207→
   208→	// either startup error (permanent backoff err) or nil err (successful engine up)
   209→	// TODO: make after-startup backoff err available
   210→	run := make(chan struct{})
   211→	clientErr := make(chan error, 1)
   212→	go func() {
   213→		if err := client.Run(run, ""); err != nil {
   214→			clientErr <- err
   215→		}
   216→	}()
   217→
   218→	select {
   219→	case <-startCtx.Done():
   220→		if stopErr := client.Stop(); stopErr != nil {
   221→			return fmt.Errorf("stop error after context done. Stop error: %w. Context done: %w", stopErr, startCtx.Err())
   222→		}
   223→		return startCtx.Err()
   224→	case err := <-clientErr:
   225→		return fmt.Errorf("startup: %w", err)
   226→	case <-run:
   227→	}
   228→
   229→	c.connect = client
   230→	c.cancel = cancel
   231→
   232→	return nil
   233→}
   234→
   235→// Stop gracefully stops the client.
   236→// Pass a context with a deadline to limit the time spent waiting for the engine to stop.
   237→func (c *Client) Stop(ctx context.Context) error {
   238→	c.mu.Lock()
   239→	defer c.mu.Unlock()
   240→
   241→	if c.connect == nil {
   242→		return ErrClientNotStarted
   243→	}
   244→
   245→	if c.cancel != nil {
   246→		c.cancel()
   247→		c.cancel = nil
   248→	}
   249→
   250→	done := make(chan error, 1)
   251→	connect := c.connect
   252→	go func() {
   253→		done <- connect.Stop()
   254→	}()
   255→
   256→	select {
   257→	case <-ctx.Done():
   258→		c.connect = nil
   259→		return ctx.Err()
   260→	case err := <-done:
   261→		c.connect = nil
   262→		if err != nil {
   263→			return fmt.Errorf("stop: %w", err)
   264→		}
   265→		return nil
   266→	}
   267→}
   268→
   269→// GetConfig returns a copy of the internal client config.
   270→func (c *Client) GetConfig() (profilemanager.Config, error) {
   271→	c.mu.Lock()
   272→	defer c.mu.Unlock()
   273→	if c.config == nil {
   274→		return profilemanager.Config{}, ErrConfigNotInitialized
   275→	}
   276→	return *c.config, nil
   277→}
   278→
   279→// Dial dials a network address in the netbird network.
   280→// Not applicable if the userspace networking mode is disabled.
   281→func (c *Client) Dial(ctx context.Context, network, address string) (net.Conn, error) {
   282→	engine, err := c.getEngine()
   283→	if err != nil {
   284→		return nil, err
   285→	}
   286→
   287→	nsnet, err := engine.GetNet()
   288→	if err != nil {
   289→		return nil, fmt.Errorf("get net: %w", err)
   290→	}
   291→
   292→	return nsnet.DialContext(ctx, network, address)
   293→}
   294→
   295→// DialContext dials a network address in the netbird network with context
   296→func (c *Client) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
   297→	return c.Dial(ctx, network, address)
   298→}
   299→
   300→// ListenTCP listens on the given address in the netbird network.
   301→// Not applicable if the userspace networking mode is disabled.
   302→func (c *Client) ListenTCP(address string) (net.Listener, error) {
   303→	nsnet, addr, err := c.getNet()
   304→	if err != nil {
   305→		return nil, err
   306→	}
   307→
   308→	_, port, err := net.SplitHostPort(address)
   309→	if err != nil {
   310→		return nil, fmt.Errorf("split host port: %w", err)
   311→	}
   312→	listenAddr := fmt.Sprintf("%s:%s", addr, port)
   313→
   314→	tcpAddr, err := net.ResolveTCPAddr("tcp", listenAddr)
   315→	if err != nil {
   316→		return nil, fmt.Errorf("resolve: %w", err)
   317→	}
   318→	return nsnet.ListenTCP(tcpAddr)
   319→}
   320→
   321→// ListenUDP listens on the given address in the netbird network.
   322→// Not applicable if the userspace networking mode is disabled.
   323→func (c *Client) ListenUDP(address string) (net.PacketConn, error) {
   324→	nsnet, addr, err := c.getNet()
   325→	if err != nil {
   326→		return nil, err
   327→	}
   328→
   329→	_, port, err := net.SplitHostPort(address)
   330→	if err != nil {
   331→		return nil, fmt.Errorf("split host port: %w", err)
   332→	}
   333→	listenAddr := fmt.Sprintf("%s:%s", addr, port)
   334→
   335→	udpAddr, err := net.ResolveUDPAddr("udp", listenAddr)
   336→	if err != nil {
   337→		return nil, fmt.Errorf("resolve: %w", err)
   338→	}
   339→
   340→	return nsnet.ListenUDP(udpAddr)
   341→}
   342→
   343→// NewHTTPClient returns a configured http.Client that uses the netbird network for requests.
   344→// Not applicable if the userspace networking mode is disabled.
   345→func (c *Client) NewHTTPClient() *http.Client {
   346→	transport := &http.Transport{
   347→		DialContext: c.Dial,
   348→	}
   349→
   350→	return &http.Client{
   351→		Transport: transport,
   352→	}
   353→}
   354→
   355→// Status returns the current status of the client.
   356→func (c *Client) Status() (peer.FullStatus, error) {
   357→	c.mu.Lock()
   358→	connect := c.connect
   359→	c.mu.Unlock()
   360→
   361→	if connect != nil {
   362→		engine := connect.Engine()
   363→		if engine != nil {
   364→			_ = engine.RunHealthProbes(false)
   365→		}
   366→	}
   367→
   368→	return c.recorder.GetFullStatus(), nil
   369→}
   370→
   371→// GetLatestSyncResponse returns the latest sync response from the management server.
   372→func (c *Client) GetLatestSyncResponse() (*mgmProto.SyncResponse, error) {
   373→	engine, err := c.getEngine()
   374→	if err != nil {
   375→		return nil, err
   376→	}
   377→
   378→	syncResp, err := engine.GetLatestSyncResponse()
   379→	if err != nil {
   380→		return nil, fmt.Errorf("get sync response: %w", err)
   381→	}
   382→
   383→	return syncResp, nil
   384→}
   385→
   386→// SetLogLevel sets the logging level for the client and its components.
   387→func (c *Client) SetLogLevel(levelStr string) error {
   388→	level, err := logrus.ParseLevel(levelStr)
   389→	if err != nil {
   390→		return fmt.Errorf("parse log level: %w", err)
   391→	}
   392→
   393→	logrus.SetLevel(level)
   394→
   395→	c.mu.Lock()
   396→	connect := c.connect
   397→	c.mu.Unlock()
   398→
   399→	if connect != nil {
   400→		connect.SetLogLevel(level)
   401→	}
   402→
   403→	return nil
   404→}
   405→
   406→// VerifySSHHostKey verifies an SSH host key against stored peer keys.
   407→// Returns nil if the key matches, ErrPeerNotFound if peer is not in network,
   408→// ErrNoStoredKey if peer has no stored key, or an error for verification failures.
   409→func (c *Client) VerifySSHHostKey(peerAddress string, key []byte) error {
   410→	engine, err := c.getEngine()
   411→	if err != nil {
   412→		return err
   413→	}
   414→
   415→	storedKey, found := engine.GetPeerSSHKey(peerAddress)
   416→	if !found {
   417→		return sshcommon.ErrPeerNotFound
   418→	}
   419→
   420→	return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
   421→}
   422→
   423→// getEngine safely retrieves the engine from the client with proper locking.
   424→// Returns ErrClientNotStarted if the client is not started.
   425→// Returns ErrEngineNotStarted if the engine is not available.
   426→func (c *Client) getEngine() (*internal.Engine, error) {
   427→	c.mu.Lock()
   428→	connect := c.connect
   429→	c.mu.Unlock()
   430→
   431→	if connect == nil {
   432→		return nil, ErrClientNotStarted
   433→	}
   434→
   435→	engine := connect.Engine()
   436→	if engine == nil {
   437→		return nil, ErrEngineNotStarted
   438→	}
   439→
   440→	return engine, nil
   441→}
   442→
   443→func (c *Client) getNet() (*wgnetstack.Net, netip.Addr, error) {
   444→	engine, err := c.getEngine()
   445→	if err != nil {
   446→		return nil, netip.Addr{}, err
   447→	}
   448→
   449→	addr, err := engine.Address()
   450→	if err != nil {
   451→		return nil, netip.Addr{}, fmt.Errorf("engine address: %w", err)
   452→	}
   453→
   454→	nsnet, err := engine.GetNet()
   455→	if err != nil {
   456→		return nil, netip.Addr{}, fmt.Errorf("get net: %w", err)
   457→	}
   458→
   459→	return nsnet, addr, nil
   460→}
   461→

package embed

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/netip"
	"os"
	"sync"

	"github.com/sirupsen/logrus"
	wgnetstack "golang.zx2c4.com/wireguard/tun/netstack"

	"github.com/netbirdio/netbird/client/iface/netstack"
	"github.com/netbirdio/netbird/client/internal"
	"github.com/netbirdio/netbird/client/internal/auth"
	"github.com/netbirdio/netbird/client/internal/peer"
	"github.com/netbirdio/netbird/client/internal/profilemanager"
	sshcommon "github.com/netbirdio/netbird/client/ssh"
	"github.com/netbirdio/netbird/client/system"
	mgmProto "github.com/netbirdio/netbird/shared/management/proto"
)

var (
	ErrClientAlreadyStarted = errors.New("client already started")
	ErrClientNotStarted     = errors.New("client not started")
	ErrEngineNotStarted     = errors.New("engine not started")
	ErrConfigNotInitialized = errors.New("config not initialized")
)

// PeerConnStatus is a peer's connection status.
type PeerConnStatus = peer.ConnStatus

const (
	// PeerStatusConnected indicates the peer is in connected state.
	PeerStatusConnected = peer.StatusConnected
)

// Client manages a netbird embedded client instance.
type Client struct {
	deviceName string
	config     *profilemanager.Config
	mu         sync.Mutex
	cancel     context.CancelFunc
	setupKey   string
	jwtToken   string
	connect    *internal.ConnectClient
	recorder   *peer.Status
}

// Options configures a new Client.
type Options struct {
	// DeviceName is this peer's name in the network
	DeviceName string
	// SetupKey is used for authentication
	SetupKey string
	// JWTToken is used for JWT-based authentication
	JWTToken string
	// PrivateKey is used for direct private key authentication
	PrivateKey string
	// ManagementURL overrides the default management server URL
	ManagementURL string
	// PreSharedKey is the pre-shared key for the WireGuard interface
	PreSharedKey string
	// LogOutput is the output destination for logs (defaults to os.Stderr if nil)
	LogOutput io.Writer
	// LogLevel sets the logging level (defaults to info if empty)
	LogLevel string
	// NoUserspace disables the userspace networking mode. Needs admin/root privileges
	NoUserspace bool
	// ConfigPath is the path to the netbird config file. If empty, the config will be stored in memory and not persisted.
	ConfigPath string
	// StatePath is the path to the netbird state file
	StatePath string
	// DisableClientRoutes disables the client routes
	DisableClientRoutes bool
	// BlockInbound blocks all inbound connections from peers
	BlockInbound bool
	// WireguardPort is the port for the WireGuard interface. Use 0 for a random port.
	WireguardPort *int
}

// validateCredentials checks that exactly one credential type is provided
func (opts *Options) validateCredentials() error {
	credentialsProvided := 0
	if opts.SetupKey != "" {
		credentialsProvided++
	}
	if opts.JWTToken != "" {
		credentialsProvided++
	}
	if opts.PrivateKey != "" {
		credentialsProvided++
	}

	if credentialsProvided == 0 {
		return fmt.Errorf("one of SetupKey, JWTToken, or PrivateKey must be provided")
	}
	if credentialsProvided > 1 {
		return fmt.Errorf("only one of SetupKey, JWTToken, or PrivateKey can be specified")
	}

	return nil
}

// New creates a new netbird embedded client.
func New(opts Options) (*Client, error) {
	if err := opts.validateCredentials(); err != nil {
		return nil, err
	}

	if opts.LogOutput != nil {
		logrus.SetOutput(opts.LogOutput)
	}

	if opts.LogLevel != "" {
		level, err := logrus.ParseLevel(opts.LogLevel)
		if err != nil {
			return nil, fmt.Errorf("parse log level: %w", err)
		}
		logrus.SetLevel(level)
	}

	if !opts.NoUserspace {
		if err := os.Setenv(netstack.EnvUseNetstackMode, "true"); err != nil {
			return nil, fmt.Errorf("setenv: %w", err)
		}
		if err := os.Setenv(netstack.EnvSkipProxy, "true"); err != nil {
			return nil, fmt.Errorf("setenv: %w", err)
		}
	}

	if opts.StatePath != "" {
		// TODO: Disable state if path not provided
		if err := os.Setenv("NB_DNS_STATE_FILE", opts.StatePath); err != nil {
			return nil, fmt.Errorf("setenv: %w", err)
		}
	}

	t := true
	var config *profilemanager.Config
	var err error
	input := profilemanager.ConfigInput{
		ConfigPath:          opts.ConfigPath,
		ManagementURL:       opts.ManagementURL,
		PreSharedKey:        &opts.PreSharedKey,
		DisableServerRoutes: &t,
		DisableClientRoutes: &opts.DisableClientRoutes,
		BlockInbound:        &opts.BlockInbound,
		WireguardPort:       opts.WireguardPort,
	}
	if opts.ConfigPath != "" {
		config, err = profilemanager.UpdateOrCreateConfig(input)
	} else {
		config, err = profilemanager.CreateInMemoryConfig(input)
	}
	if err != nil {
		return nil, fmt.Errorf("create config: %w", err)
	}

	if opts.PrivateKey != "" {
		config.PrivateKey = opts.PrivateKey
	}

	return &Client{
		deviceName: opts.DeviceName,
		setupKey:   opts.SetupKey,
		jwtToken:   opts.JWTToken,
		config:     config,
		recorder:   peer.NewRecorder(config.ManagementURL.String()),
	}, nil
}

// Start begins client operation and blocks until the engine has been started successfully or a startup error occurs.
// Pass a context with a deadline to limit the time spent waiting for the engine to start.
func (c *Client) Start(startCtx context.Context) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.connect != nil {
		return ErrClientAlreadyStarted
	}

	ctx, cancel := context.WithCancel(internal.CtxInitState(context.Background()))
	defer func() {
		if c.connect == nil {
			cancel()
		}
	}()

	// nolint:staticcheck
	ctx = context.WithValue(ctx, system.DeviceNameCtxKey, c.deviceName)

	authClient, err := auth.NewAuth(ctx, c.config.PrivateKey, c.config.ManagementURL, c.config)
	if err != nil {
		return fmt.Errorf("create auth client: %w", err)
	}
	defer authClient.Close()

	if err, _ := authClient.Login(ctx, c.setupKey, c.jwtToken); err != nil {
		return fmt.Errorf("login: %w", err)
	}
	client := internal.NewConnectClient(ctx, c.config, c.recorder, false)
	client.SetSyncResponsePersistence(true)

	// either startup error (permanent backoff err) or nil err (successful engine up)
	// TODO: make after-startup backoff err available
	run := make(chan struct{})
	clientErr := make(chan error, 1)
	go func() {
		if err := client.Run(run, ""); err != nil {
			clientErr <- err
		}
	}()

	select {
	case <-startCtx.Done():
		if stopErr := client.Stop(); stopErr != nil {
			return fmt.Errorf("stop error after context done. Stop error: %w. Context done: %w", stopErr, startCtx.Err())
		}
		return startCtx.Err()
	case err := <-clientErr:
		return fmt.Errorf("startup: %w", err)
	case <-run:
	}

	c.connect = client
	c.cancel = cancel

	return nil
}

// Stop gracefully stops the client.
// Pass a context with a deadline to limit the time spent waiting for the engine to stop.
func (c *Client) Stop(ctx context.Context) error {
	c.mu.Lock()
	defer c.mu.Unlock()

	if c.connect == nil {
		return ErrClientNotStarted
	}

	if c.cancel != nil {
		c.cancel()
		c.cancel = nil
	}

	done := make(chan error, 1)
	connect := c.connect
	go func() {
		done <- connect.Stop()
	}()

	select {
	case <-ctx.Done():
		c.connect = nil
		return ctx.Err()
	case err := <-done:
		c.connect = nil
		if err != nil {
			return fmt.Errorf("stop: %w", err)
		}
		return nil
	}
}

// GetConfig returns a copy of the internal client config.
func (c *Client) GetConfig() (profilemanager.Config, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.config == nil {
		return profilemanager.Config{}, ErrConfigNotInitialized
	}
	return *c.config, nil
}

// Dial dials a network address in the netbird network.
// Not applicable if the userspace networking mode is disabled.
func (c *Client) Dial(ctx context.Context, network, address string) (net.Conn, error) {
	engine, err := c.getEngine()
	if err != nil {
		return nil, err
	}

	nsnet, err := engine.GetNet()
	if err != nil {
		return nil, fmt.Errorf("get net: %w", err)
	}

	return nsnet.DialContext(ctx, network, address)
}

// DialContext dials a network address in the netbird network with context
func (c *Client) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	return c.Dial(ctx, network, address)
}

// ListenTCP listens on the given address in the netbird network.
// Not applicable if the userspace networking mode is disabled.
func (c *Client) ListenTCP(address string) (net.Listener, error) {
	nsnet, addr, err := c.getNet()
	if err != nil {
		return nil, err
	}

	_, port, err := net.SplitHostPort(address)
	if err != nil {
		return nil, fmt.Errorf("split host port: %w", err)
	}
	listenAddr := fmt.Sprintf("%s:%s", addr, port)

	tcpAddr, err := net.ResolveTCPAddr("tcp", listenAddr)
	if err != nil {
		return nil, fmt.Errorf("resolve: %w", err)
	}
	return nsnet.ListenTCP(tcpAddr)
}

// ListenUDP listens on the given address in the netbird network.
// Not applicable if the userspace networking mode is disabled.
func (c *Client) ListenUDP(address string) (net.PacketConn, error) {
	nsnet, addr, err := c.getNet()
	if err != nil {
		return nil, err
	}

	_, port, err := net.SplitHostPort(address)
	if err != nil {
		return nil, fmt.Errorf("split host port: %w", err)
	}
	listenAddr := fmt.Sprintf("%s:%s", addr, port)

	udpAddr, err := net.ResolveUDPAddr("udp", listenAddr)
	if err != nil {
		return nil, fmt.Errorf("resolve: %w", err)
	}

	return nsnet.ListenUDP(udpAddr)
}

// NewHTTPClient returns a configured http.Client that uses the netbird network for requests.
// Not applicable if the userspace networking mode is disabled.
func (c *Client) NewHTTPClient() *http.Client {
	transport := &http.Transport{
		DialContext: c.Dial,
	}

	return &http.Client{
		Transport: transport,
	}
}

// Status returns the current status of the client.
func (c *Client) Status() (peer.FullStatus, error) {
	c.mu.Lock()
	connect := c.connect
	c.mu.Unlock()

	if connect != nil {
		engine := connect.Engine()
		if engine != nil {
			_ = engine.RunHealthProbes(false)
		}
	}

	return c.recorder.GetFullStatus(), nil
}

// GetLatestSyncResponse returns the latest sync response from the management server.
func (c *Client) GetLatestSyncResponse() (*mgmProto.SyncResponse, error) {
	engine, err := c.getEngine()
	if err != nil {
		return nil, err
	}

	syncResp, err := engine.GetLatestSyncResponse()
	if err != nil {
		return nil, fmt.Errorf("get sync response: %w", err)
	}

	return syncResp, nil
}

// SetLogLevel sets the logging level for the client and its components.
func (c *Client) SetLogLevel(levelStr string) error {
	level, err := logrus.ParseLevel(levelStr)
	if err != nil {
		return fmt.Errorf("parse log level: %w", err)
	}

	logrus.SetLevel(level)

	c.mu.Lock()
	connect := c.connect
	c.mu.Unlock()

	if connect != nil {
		connect.SetLogLevel(level)
	}

	return nil
}

// VerifySSHHostKey verifies an SSH host key against stored peer keys.
// Returns nil if the key matches, ErrPeerNotFound if peer is not in network,
// ErrNoStoredKey if peer has no stored key, or an error for verification failures.
func (c *Client) VerifySSHHostKey(peerAddress string, key []byte) error {
	engine, err := c.getEngine()
	if err != nil {
		return err
	}

	storedKey, found := engine.GetPeerSSHKey(peerAddress)
	if !found {
		return sshcommon.ErrPeerNotFound
	}

	return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
}

// getEngine safely retrieves the engine from the client with proper locking.
// Returns ErrClientNotStarted if the client is not started.
// Returns ErrEngineNotStarted if the engine is not available.
func (c *Client) getEngine() (*internal.Engine, error) {
	c.mu.Lock()
	connect := c.connect
	c.mu.Unlock()

	if connect == nil {
		return nil, ErrClientNotStarted
	}

	engine := connect.Engine()
	if engine == nil {
		return nil, ErrEngineNotStarted
	}

	return engine, nil
}

func (c *Client) getNet() (*wgnetstack.Net, netip.Addr, error) {
	engine, err := c.getEngine()
	if err != nil {
		return nil, netip.Addr{}, err
	}

	addr, err := engine.Address()
	if err != nil {
		return nil, netip.Addr{}, fmt.Errorf("engine address: %w", err)
	}

	nsnet, err := engine.GetNet()
	if err != nil {
		return nil, netip.Addr{}, fmt.Errorf("get net: %w", err)
	}

	return nsnet, addr, nil
}