package roundtrip

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/netip"
	"sync"
	"time"

	"github.com/hashicorp/go-multierror"
	log "github.com/sirupsen/logrus"
	"golang.org/x/exp/maps"
	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	grpcstatus "google.golang.org/grpc/status"

	"github.com/netbirdio/netbird/client/embed"
	nberrors "github.com/netbirdio/netbird/client/errors"
	"github.com/netbirdio/netbird/proxy/internal/types"
	"github.com/netbirdio/netbird/shared/management/proto"
	"github.com/netbirdio/netbird/util"
)

const deviceNamePrefix = "ingress-proxy-"

const clientStopTimeout = 30 * time.Second

const createProxyPeerTimeout = 30 * time.Second

// backendKey identifies a backend by its host:port from the target URL.
type backendKey string

// ServiceKey uniquely identifies a service (HTTP reverse proxy or L4 service)
// that holds a reference to an embedded NetBird client. Callers should use the
// DomainServiceKey and L4ServiceKey constructors to avoid namespace collisions.
type ServiceKey string

// DomainServiceKey returns a ServiceKey for an HTTP/TLS domain-based service.
func DomainServiceKey(domain string) ServiceKey {
	return ServiceKey("domain:" + domain)
}

// L4ServiceKey returns a ServiceKey for an L4 service (TCP/UDP).
func L4ServiceKey(id types.ServiceID) ServiceKey {
	return ServiceKey("l4:" + id)
}

var (
	// ErrNoAccountID is returned when a request context is missing the account ID.
	ErrNoAccountID = errors.New("no account ID in request context")
	// ErrNoPeerConnection is returned when no embedded client exists for the account.
	ErrNoPeerConnection = errors.New("no peer connection found")
	// ErrClientStartFailed is returned when the embedded client fails to start.
	ErrClientStartFailed = errors.New("client start failed")
	// ErrTooManyInflight is returned when the per-backend in-flight limit is reached.
	ErrTooManyInflight = errors.New("too many in-flight requests")
)

// serviceInfo holds metadata about a registered service.
type serviceInfo struct {
	serviceID types.ServiceID
}

type serviceNotification struct {
	key       ServiceKey
	serviceID types.ServiceID
}

// clientEntry holds an embedded NetBird client and tracks which services use it.
type clientEntry struct {
	client    *embed.Client
	transport *http.Transport
	// insecureTransport is a clone of transport with TLS verification disabled,
	// used when per-target skip_tls_verify is set.
	insecureTransport *http.Transport
	services          map[ServiceKey]serviceInfo
	createdAt         time.Time
	started           bool
	// inbound is opaque per-account state owned by the NetBird parent's
	// ReadyHandler. The roundtrip package never inspects this value; it
	// only stores it so RemovePeer / StopAll can hand it back to the
	// matching StopHandler. Nil when no inbound integration is active.
	inbound any
	// Per-backend in-flight limiting keyed by target host:port.
	// TODO: clean up stale entries when backend targets change.
	inflightMu  sync.Mutex
	inflightMap map[backendKey]chan struct{}
	maxInflight int
}

// IdentityForIP resolves a tunnel IP to the peer identity locally known by
// this account's embedded client. Returns (pubKey, fqdn) on success.
// ok=false means the IP is not in the account's roster — callers can use
// that as a fast deny without round-tripping management. The returned
// strings carry only what the embedded peerstore exposes; user identity
// (UserID / Email / Groups) still flows through ValidateTunnelPeer.
func (e *clientEntry) IdentityForIP(ip netip.Addr) (pubKey, fqdn string, ok bool) {
	if e == nil || e.client == nil || !ip.IsValid() {
		return "", "", false
	}
	return e.client.IdentityForIP(ip)
}

// acquireInflight attempts to acquire an in-flight slot for the given backend.
// It returns a release function that must always be called, and true on success.
func (e *clientEntry) acquireInflight(backend backendKey) (release func(), ok bool) {
	noop := func() {}
	if e.maxInflight <= 0 {
		return noop, true
	}

	e.inflightMu.Lock()
	sem, exists := e.inflightMap[backend]
	if !exists {
		sem = make(chan struct{}, e.maxInflight)
		e.inflightMap[backend] = sem
	}
	e.inflightMu.Unlock()

	select {
	case sem <- struct{}{}:
		return func() { <-sem }, true
	default:
		return noop, false
	}
}

// ClientConfig holds configuration for the embedded NetBird client.
type ClientConfig struct {
	MgmtAddr     string
	WGPort       uint16
	PreSharedKey string
	Performance  embed.Performance
	// BlockInbound mirrors embed.Options.BlockInbound. Set to true on the
	// standalone proxy where the embedded client never accepts inbound;
	// set to false on the private/embedded proxy so the engine creates
	// the ACL manager and applies management's per-policy firewall rules
	// (which is what gates per-account inbound listeners on the netstack).
	BlockInbound bool
}

type statusNotifier interface {
	NotifyStatus(ctx context.Context, accountID types.AccountID, serviceID types.ServiceID, connected bool) error
}

type managementClient interface {
	CreateProxyPeer(ctx context.Context, req *proto.CreateProxyPeerRequest, opts ...grpc.CallOption) (*proto.CreateProxyPeerResponse, error)
}

// NetBird provides an http.RoundTripper implementation
// backed by underlying NetBird connections.
// Clients are keyed by AccountID, allowing multiple services to share the same connection.
type NetBird struct {
	ctx          context.Context
	proxyID      string
	proxyAddr    string
	clientCfg    ClientConfig
	logger       *log.Logger
	mgmtClient   managementClient
	transportCfg transportConfig

	clientsMux     sync.RWMutex
	clients        map[types.AccountID]*clientEntry
	lifecycleMu    sync.Map
	initLogOnce    sync.Once
	statusNotifier statusNotifier
	// readyHandler runs after the embedded client for an account reports
	// Ready. The opaque return value is stored on clientEntry and handed
	// back to stopHandler when the entry is torn down. Nil disables the
	// hook entirely (default for the standalone proxy).
	readyHandler func(ctx context.Context, accountID types.AccountID, client *embed.Client) any
	// stopHandler runs when an account's last service is removed (or the
	// transport is shutting down). Receives whatever readyHandler returned.
	stopHandler func(accountID types.AccountID, state any)

	// OnAddPeer, when set, is called after AddPeer completes for a new account
	// (i.e. when a new client was actually created, not when an existing one
	// was reused). The duration covers keygen + gRPC CreateProxyPeer + embed.New.
	OnAddPeer func(d time.Duration, err error)

	// startClient runs the post-create client startup. Nil uses runClientStartup;
	// tests override it to avoid a real embed client.Start.
	startClient func(accountID types.AccountID, client *embed.Client)
}

// ClientDebugInfo contains debug information about a client.
type ClientDebugInfo struct {
	AccountID    types.AccountID
	ServiceCount int
	ServiceKeys  []string
	HasClient    bool
	CreatedAt    time.Time
}

// accountIDContextKey is the context key for storing the account ID.
type accountIDContextKey struct{}

// skipTLSVerifyContextKey is the context key for requesting insecure TLS.
type skipTLSVerifyContextKey struct{}

// AddPeer registers a service for an account. If the account doesn't have a client yet,
// one is created by authenticating with the management server using the provided token.
// Multiple services can share the same client.
func (n *NetBird) AddPeer(ctx context.Context, accountID types.AccountID, key ServiceKey, authToken string, serviceID types.ServiceID) error {
	si := serviceInfo{serviceID: serviceID}

	if n.registerExistingClient(accountID, key, si) {
		return nil
	}

	lifecycle := n.accountLifecycle(accountID)
	lifecycle.Lock()
	transferred := false
	defer func() {
		if !transferred {
			lifecycle.Unlock()
		}
	}()

	if n.registerExistingClient(accountID, key, si) {
		return nil
	}

	createStart := time.Now()
	entry, err := n.createClientEntry(ctx, accountID, key, authToken, si)
	if n.OnAddPeer != nil {
		n.OnAddPeer(time.Since(createStart), err)
	}
	if err != nil {
		return err
	}

	n.clientsMux.Lock()
	n.clients[accountID] = entry
	n.clientsMux.Unlock()

	n.logger.WithFields(log.Fields{
		"account_id":  accountID,
		"service_key": key,
	}).Info("created new client for account")

	transferred = true
	go func() {
		defer lifecycle.Unlock()
		n.startClientStartup(accountID, entry.client)
	}()

	return nil
}

func (n *NetBird) startClientStartup(accountID types.AccountID, client *embed.Client) {
	if n.startClient != nil {
		n.startClient(accountID, client)
		return
	}
	n.runClientStartup(accountID, client)
}

// registerExistingClient registers the service against an already-present
// client for the account and returns true when it did. It notifies management
// of the new service when the client is already started.
func (n *NetBird) registerExistingClient(accountID types.AccountID, key ServiceKey, si serviceInfo) bool {
	n.clientsMux.Lock()
	entry, exists := n.clients[accountID]
	if !exists {
		n.clientsMux.Unlock()
		return false
	}
	entry.services[key] = si
	started := entry.started
	n.clientsMux.Unlock()

	n.logger.WithFields(log.Fields{
		"account_id":  accountID,
		"service_key": key,
	}).Debug("registered service with existing client")

	if started && n.statusNotifier != nil {
		if err := n.statusNotifier.NotifyStatus(context.Background(), accountID, si.serviceID, true); err != nil {
			n.logger.WithFields(log.Fields{
				"account_id":  accountID,
				"service_key": key,
			}).WithError(err).Warn("failed to notify status for existing client")
		}
	}
	return true
}

// accountLifecycle returns the per-account lifecycle mutex, serialising client
// creation against teardown so a slow client.Stop cannot race a new
// client.Start for the same account, without blocking clientsMux.
func (n *NetBird) accountLifecycle(accountID types.AccountID) *sync.Mutex {
	mu, _ := n.lifecycleMu.LoadOrStore(accountID, &sync.Mutex{})
	return mu.(*sync.Mutex)
}

// createClientEntry generates a WireGuard keypair, authenticates with management,
// and creates an embedded NetBird client. Must be called with the account's
// lifecycle mutex held.
func (n *NetBird) createClientEntry(ctx context.Context, accountID types.AccountID, key ServiceKey, authToken string, si serviceInfo) (*clientEntry, error) {
	serviceID := si.serviceID
	n.logger.WithFields(log.Fields{
		"account_id": accountID,
		"service_id": serviceID,
	}).Debug("generating WireGuard keypair for new peer")

	privateKey, err := wgtypes.GeneratePrivateKey()
	if err != nil {
		return nil, fmt.Errorf("generate wireguard private key: %w", err)
	}
	publicKey := privateKey.PublicKey()

	n.logger.WithFields(log.Fields{
		"account_id": accountID,
		"service_id": serviceID,
		"public_key": publicKey.String(),
	}).Debug("authenticating new proxy peer with management")

	createCtx, cancel := context.WithTimeout(ctx, createProxyPeerTimeout)
	defer cancel()
	resp, err := n.mgmtClient.CreateProxyPeer(createCtx, &proto.CreateProxyPeerRequest{
		ServiceId:          string(serviceID),
		AccountId:          string(accountID),
		Token:              authToken,
		WireguardPublicKey: publicKey.String(),
		Cluster:            n.proxyAddr,
	})
	if err != nil {
		return nil, fmt.Errorf("authenticate proxy peer with management: %w", err)
	}
	if resp != nil && !resp.GetSuccess() {
		errMsg := "unknown error"
		if resp.ErrorMessage != nil {
			errMsg = *resp.ErrorMessage
		}
		return nil, fmt.Errorf("proxy peer authentication failed: %s", errMsg)
	}

	n.logger.WithFields(log.Fields{
		"account_id": accountID,
		"service_id": serviceID,
		"public_key": publicKey.String(),
	}).Info("proxy peer authenticated successfully with management")

	n.initLogOnce.Do(func() {
		if err := util.InitLog(log.WarnLevel.String(), util.LogConsole); err != nil {
			n.logger.WithField("account_id", accountID).Warnf("failed to initialize embedded client logging: %v", err)
		}
	})

	// Create embedded NetBird client with the generated private key.
	// The peer has already been created via CreateProxyPeer RPC with the public key.
	wgPort := int(n.clientCfg.WGPort)
	client, err := embed.New(embed.Options{
		DeviceName:    deviceNamePrefix + n.proxyID,
		ManagementURL: n.clientCfg.MgmtAddr,
		PrivateKey:    privateKey.String(),
		LogLevel:      log.WarnLevel.String(),
		BlockInbound:  n.clientCfg.BlockInbound,
		// The embedded proxy peer must never be a stepping stone into
		// the proxy host's LAN: it only exists to reach NetBird mesh
		// targets or, when direct_upstream is set, the host network
		// stack via the MultiTransport's direct branch (which bypasses
		// the engine routing entirely).
		BlockLANAccess: true,
		WireguardPort:  &wgPort,
		PreSharedKey:   n.clientCfg.PreSharedKey,
		Performance:    n.clientCfg.Performance,
	})
	if err != nil {
		return nil, fmt.Errorf("create netbird client: %w", err)
	}

	// Create a transport using the client dialer. We do this instead of using
	// the client's HTTPClient to avoid issues with request validation that do
	// not work with reverse proxied requests.
	transport := &http.Transport{
		DialContext:           dialWithTimeout(client.DialContext),
		ForceAttemptHTTP2:     true,
		MaxIdleConns:          n.transportCfg.maxIdleConns,
		MaxIdleConnsPerHost:   n.transportCfg.maxIdleConnsPerHost,
		MaxConnsPerHost:       n.transportCfg.maxConnsPerHost,
		IdleConnTimeout:       n.transportCfg.idleConnTimeout,
		TLSHandshakeTimeout:   n.transportCfg.tlsHandshakeTimeout,
		ExpectContinueTimeout: n.transportCfg.expectContinueTimeout,
		ResponseHeaderTimeout: n.transportCfg.responseHeaderTimeout,
		WriteBufferSize:       n.transportCfg.writeBufferSize,
		ReadBufferSize:        n.transportCfg.readBufferSize,
		DisableCompression:    n.transportCfg.disableCompression,
	}

	insecureTransport := transport.Clone()
	insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec

	return &clientEntry{
		client:            client,
		services:          map[ServiceKey]serviceInfo{key: si},
		transport:         transport,
		insecureTransport: insecureTransport,
		createdAt:         time.Now(),
		started:           false,
		inflightMap:       make(map[backendKey]chan struct{}),
		maxInflight:       n.transportCfg.maxInflight,
	}, nil
}

// runClientStartup starts the client and notifies registered services on
// success. This function runs in a goroutine launched from AddPeer, so it
// must never inherit the caller's request-scoped context — a canceled
// request must not abort the inbound listener bring-up or the management
// status notification. The embedded client.Start gets its own bounded
// startCtx; once Start succeeds, notifyClientReady takes over with a
// fresh context.Background() (see that function for the contract).
func (n *NetBird) runClientStartup(accountID types.AccountID, client *embed.Client) {
	startCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	if err := client.Start(startCtx); err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			n.logger.WithField("account_id", accountID).Warn("netbird client start timed out, will retry on first request")
		} else {
			n.logger.WithField("account_id", accountID).WithError(err).Error("failed to start netbird client")
		}
		return
	}

	n.notifyClientReady(accountID, client)
}

// notifyClientReady marks the account's client as started, fires the
// readyHandler hook, and notifies management of the new tunnel
// connection for every registered service. It is split out of
// runClientStartup so a regression test can drive the post-Start tail
// without needing a live embedded client. The contract that the
// hooks/notifier see context.Background() — never the AddPeer caller's
// ctx — lives here.
func (n *NetBird) notifyClientReady(accountID types.AccountID, client *embed.Client) {
	n.clientsMux.Lock()
	entry, exists := n.clients[accountID]
	if exists {
		entry.started = true
	}
	var toNotify []serviceNotification
	if exists {
		for key, info := range entry.services {
			toNotify = append(toNotify, serviceNotification{key: key, serviceID: info.serviceID})
		}
	}
	readyHandler := n.readyHandler
	n.clientsMux.Unlock()

	if readyHandler != nil {
		state := readyHandler(n.ctx, accountID, client)
		n.clientsMux.Lock()
		if e, ok := n.clients[accountID]; ok {
			e.inbound = state
		} else if state != nil && n.stopHandler != nil {
			// Account was removed while readyHandler ran; tear down the
			// resources it just brought up.
			stop := n.stopHandler
			n.clientsMux.Unlock()
			stop(accountID, state)
			n.clientsMux.Lock()
		}
		n.clientsMux.Unlock()
	}

	if n.statusNotifier == nil {
		return
	}
	for _, sn := range toNotify {
		if err := n.statusNotifier.NotifyStatus(n.ctx, accountID, sn.serviceID, true); err != nil {
			n.logger.WithFields(log.Fields{
				"account_id":  accountID,
				"service_key": sn.key,
			}).WithError(err).Warn("failed to notify tunnel connection status")
		} else {
			n.logger.WithFields(log.Fields{
				"account_id":  accountID,
				"service_key": sn.key,
			}).Info("notified management about tunnel connection")
		}
	}
}

// RemovePeer unregisters a service from an account. The client is only stopped
// when no services are using it anymore.
func (n *NetBird) RemovePeer(ctx context.Context, accountID types.AccountID, key ServiceKey) error {
	lifecycle := n.accountLifecycle(accountID)
	lifecycle.Lock()
	transferred := false
	defer func() {
		if !transferred {
			lifecycle.Unlock()
		}
	}()

	n.clientsMux.Lock()

	entry, exists := n.clients[accountID]
	if !exists {
		n.clientsMux.Unlock()
		n.logger.WithField("account_id", accountID).Debug("remove peer: account not found")
		return nil
	}

	si, svcExists := entry.services[key]
	if !svcExists {
		n.clientsMux.Unlock()
		n.logger.WithFields(log.Fields{
			"account_id":  accountID,
			"service_key": key,
		}).Debug("remove peer: service not registered")
		return nil
	}

	delete(entry.services, key)

	stopClient := len(entry.services) == 0
	if stopClient {
		n.logger.WithField("account_id", accountID).Info("stopping client, no more services")
		delete(n.clients, accountID)
	} else {
		n.logger.WithFields(log.Fields{
			"account_id":         accountID,
			"service_key":        key,
			"remaining_services": len(entry.services),
		}).Debug("unregistered service, client still in use")
	}
	n.clientsMux.Unlock()

	n.notifyDisconnect(ctx, accountID, key, si.serviceID)

	if stopClient {
		transferred = true
		go n.stopClientLocked(accountID, lifecycle, entry)
	}

	return nil
}

// stopClientLocked releases a client's resources off the caller's goroutine so a
// slow client.Stop cannot wedge the mapping receive loop (which calls RemovePeer
// synchronously). It unlocks lifecycle when done so a new client.Start for the
// same account waits for this teardown.
func (n *NetBird) stopClientLocked(accountID types.AccountID, lifecycle *sync.Mutex, entry *clientEntry) {
	defer lifecycle.Unlock()

	if entry.inbound != nil && n.stopHandler != nil {
		n.stopHandler(accountID, entry.inbound)
	}
	if entry.transport != nil {
		entry.transport.CloseIdleConnections()
	}
	if entry.insecureTransport != nil {
		entry.insecureTransport.CloseIdleConnections()
	}
	if entry.client == nil {
		return
	}

	ctx, cancel := context.WithTimeout(context.Background(), clientStopTimeout)
	defer cancel()
	if err := entry.client.Stop(ctx); err != nil {
		n.logger.WithField("account_id", accountID).WithError(err).Warn("failed to stop netbird client")
	}
}

func (n *NetBird) notifyDisconnect(ctx context.Context, accountID types.AccountID, key ServiceKey, serviceID types.ServiceID) {
	if n.statusNotifier == nil {
		return
	}
	if err := n.statusNotifier.NotifyStatus(ctx, accountID, serviceID, false); err != nil {
		if s, ok := grpcstatus.FromError(err); ok && s.Code() == codes.NotFound {
			n.logger.WithField("service_key", key).Debug("service already removed, skipping disconnect notification")
		} else {
			n.logger.WithFields(log.Fields{
				"account_id":  accountID,
				"service_key": key,
			}).WithError(err).Warn("failed to notify tunnel disconnection status")
		}
	}
}

// RoundTrip implements http.RoundTripper. It looks up the client for the account
// specified in the request context and uses it to dial the backend.
func (n *NetBird) RoundTrip(req *http.Request) (*http.Response, error) {
	accountID := AccountIDFromContext(req.Context())
	if accountID == "" {
		return nil, ErrNoAccountID
	}

	// Copy references while holding lock, then unlock early to avoid blocking
	// other requests during the potentially slow RoundTrip.
	n.clientsMux.RLock()
	entry, exists := n.clients[accountID]
	if !exists {
		n.clientsMux.RUnlock()
		return nil, fmt.Errorf("%w for account: %s", ErrNoPeerConnection, accountID)
	}
	client := entry.client
	transport := entry.transport
	if skipTLSVerifyFromContext(req.Context()) {
		transport = entry.insecureTransport
	}
	n.clientsMux.RUnlock()

	release, ok := entry.acquireInflight(backendKey(req.URL.Host))
	defer release()
	if !ok {
		return nil, ErrTooManyInflight
	}

	// Attempt to start the client, if the client is already running then
	// it will return an error that we ignore, if this hits a timeout then
	// this request is unprocessable.
	startCtx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
	defer cancel()
	if err := client.Start(startCtx); err != nil {
		if !errors.Is(err, embed.ErrClientAlreadyStarted) {
			return nil, fmt.Errorf("%w: %w", ErrClientStartFailed, err)
		}
	}

	start := time.Now()
	resp, err := transport.RoundTrip(req)
	duration := time.Since(start)

	if err != nil {
		n.logger.Debugf("roundtrip: method=%s host=%s url=%s account=%s duration=%s err=%v",
			req.Method, req.Host, req.URL.String(), accountID, duration.Truncate(time.Millisecond), err)
		return nil, err
	}

	n.logger.Debugf("roundtrip: method=%s host=%s url=%s account=%s status=%d duration=%s",
		req.Method, req.Host, req.URL.String(), accountID, resp.StatusCode, duration.Truncate(time.Millisecond))
	return resp, nil
}

// StopAll stops all clients.
func (n *NetBird) StopAll(ctx context.Context) error {
	n.clientsMux.Lock()
	defer n.clientsMux.Unlock()

	stopHandler := n.stopHandler
	var merr *multierror.Error
	for accountID, entry := range n.clients {
		if entry.inbound != nil && stopHandler != nil {
			stopHandler(accountID, entry.inbound)
		}
		entry.transport.CloseIdleConnections()
		entry.insecureTransport.CloseIdleConnections()
		if err := entry.client.Stop(ctx); err != nil {
			n.logger.WithFields(log.Fields{
				"account_id": accountID,
			}).WithError(err).Warn("failed to stop netbird client during shutdown")
			merr = multierror.Append(merr, err)
		}
	}
	maps.Clear(n.clients)

	return nberrors.FormatErrorOrNil(merr)
}

// HasClient returns true if there is a client for the given account.
func (n *NetBird) HasClient(accountID types.AccountID) bool {
	n.clientsMux.RLock()
	defer n.clientsMux.RUnlock()
	_, exists := n.clients[accountID]
	return exists
}

// ServiceCount returns the number of services registered for the given account.
// Returns 0 if the account has no client.
func (n *NetBird) ServiceCount(accountID types.AccountID) int {
	n.clientsMux.RLock()
	defer n.clientsMux.RUnlock()
	entry, exists := n.clients[accountID]
	if !exists {
		return 0
	}
	return len(entry.services)
}

// ClientCount returns the total number of active clients.
func (n *NetBird) ClientCount() int {
	n.clientsMux.RLock()
	defer n.clientsMux.RUnlock()
	return len(n.clients)
}

// GetClient returns the embed.Client for the given account ID.
func (n *NetBird) GetClient(accountID types.AccountID) (*embed.Client, bool) {
	n.clientsMux.RLock()
	defer n.clientsMux.RUnlock()
	entry, exists := n.clients[accountID]
	if !exists {
		return nil, false
	}
	return entry.client, true
}

// IdentityForIP resolves a tunnel IP to a peer identity local to the given
// account. Delegates to clientEntry.IdentityForIP. Returns ok=false when
// the account has no client or the IP is not in its peerstore.
func (n *NetBird) IdentityForIP(accountID types.AccountID, ip netip.Addr) (pubKey, fqdn string, ok bool) {
	n.clientsMux.RLock()
	entry, exists := n.clients[accountID]
	n.clientsMux.RUnlock()
	if !exists {
		return "", "", false
	}
	return entry.IdentityForIP(ip)
}

// ListClientsForDebug returns information about all clients for debug purposes.
func (n *NetBird) ListClientsForDebug() map[types.AccountID]ClientDebugInfo {
	n.clientsMux.RLock()
	defer n.clientsMux.RUnlock()

	result := make(map[types.AccountID]ClientDebugInfo)
	for accountID, entry := range n.clients {
		keys := make([]string, 0, len(entry.services))
		for k := range entry.services {
			keys = append(keys, string(k))
		}
		result[accountID] = ClientDebugInfo{
			AccountID:    accountID,
			ServiceCount: len(entry.services),
			ServiceKeys:  keys,
			HasClient:    entry.client != nil,
			CreatedAt:    entry.createdAt,
		}
	}
	return result
}

// ListClientsForStartup returns all embed.Client instances for health checks.
func (n *NetBird) ListClientsForStartup() map[types.AccountID]*embed.Client {
	n.clientsMux.RLock()
	defer n.clientsMux.RUnlock()

	result := make(map[types.AccountID]*embed.Client)
	for accountID, entry := range n.clients {
		if entry.client != nil {
			result[accountID] = entry.client
		}
	}
	return result
}

// NewNetBird creates a new NetBird transport. Set clientCfg.WGPort to 0 for a random
// OS-assigned port. A fixed port only works with single-account deployments;
// multiple accounts will fail to bind the same port.
func NewNetBird(ctx context.Context, proxyID, proxyAddr string, clientCfg ClientConfig, logger *log.Logger, notifier statusNotifier, mgmtClient managementClient) *NetBird {
	if logger == nil {
		logger = log.StandardLogger()
	}
	return &NetBird{
		ctx:            ctx,
		proxyID:        proxyID,
		proxyAddr:      proxyAddr,
		clientCfg:      clientCfg,
		logger:         logger,
		clients:        make(map[types.AccountID]*clientEntry),
		statusNotifier: notifier,
		mgmtClient:     mgmtClient,
		transportCfg:   loadTransportConfig(logger),
	}
}

// SetClientLifecycle registers callbacks that run when an embedded
// client becomes ready and when its entry is torn down. The opaque value
// returned by ready is stored on the entry and handed back to stop on
// cleanup. Must be called before AddPeer. A nil pair leaves the
// outbound-only behaviour intact.
func (n *NetBird) SetClientLifecycle(ready func(ctx context.Context, accountID types.AccountID, client *embed.Client) any, stop func(accountID types.AccountID, state any)) {
	n.clientsMux.Lock()
	defer n.clientsMux.Unlock()
	n.readyHandler = ready
	n.stopHandler = stop
}

// dialWithTimeout wraps a DialContext function so that any dial timeout
// stored in the context (via types.WithDialTimeout) is applied only to
// the connection establishment phase, not the full request lifetime.
func dialWithTimeout(dial func(ctx context.Context, network, addr string) (net.Conn, error)) func(ctx context.Context, network, addr string) (net.Conn, error) {
	return func(ctx context.Context, network, addr string) (net.Conn, error) {
		if d, ok := types.DialTimeoutFromContext(ctx); ok {
			var cancel context.CancelFunc
			ctx, cancel = context.WithTimeout(ctx, d)
			defer cancel()
		}
		return dial(ctx, network, addr)
	}
}

// WithAccountID adds the account ID to the context.
func WithAccountID(ctx context.Context, accountID types.AccountID) context.Context {
	return context.WithValue(ctx, accountIDContextKey{}, accountID)
}

// AccountIDFromContext retrieves the account ID from the context.
func AccountIDFromContext(ctx context.Context) types.AccountID {
	v := ctx.Value(accountIDContextKey{})
	if v == nil {
		return ""
	}
	accountID, ok := v.(types.AccountID)
	if !ok {
		return ""
	}
	return accountID
}

// WithSkipTLSVerify marks the context to use an insecure transport that skips
// TLS certificate verification for the backend connection.
func WithSkipTLSVerify(ctx context.Context) context.Context {
	return context.WithValue(ctx, skipTLSVerifyContextKey{}, true)
}

func skipTLSVerifyFromContext(ctx context.Context) bool {
	v, _ := ctx.Value(skipTLSVerifyContextKey{}).(bool)
	return v
}

// directUpstreamContextKey signals that the request should bypass the embedded
// NetBird WireGuard client and dial via the host's network stack instead.
// Set by the reverse-proxy rewrite step when the matched target carries
// PathTarget.DirectUpstream; consumed by MultiTransport.
type directUpstreamContextKey struct{}

// WithDirectUpstream marks the context so MultiTransport routes the request
// through its stdlib transport instead of the embedded NetBird roundtripper.
func WithDirectUpstream(ctx context.Context) context.Context {
	return context.WithValue(ctx, directUpstreamContextKey{}, true)
}

// DirectUpstreamFromContext reports whether the context has been marked to
// bypass the embedded NetBird client.
func DirectUpstreamFromContext(ctx context.Context) bool {
	v, _ := ctx.Value(directUpstreamContextKey{}).(bool)
	return v
}
