//go:build ios

package NetBirdSDK

import (
	"context"
	"fmt"
	"net/netip"
	"os"
	"sort"
	"strings"
	"sync"
	"time"

	log "github.com/sirupsen/logrus"
	"golang.org/x/exp/maps"

	"github.com/netbirdio/netbird/client/internal"
	"github.com/netbirdio/netbird/client/internal/auth"
	"github.com/netbirdio/netbird/client/internal/debug"
	"github.com/netbirdio/netbird/client/internal/dns"
	"github.com/netbirdio/netbird/client/internal/listener"
	"github.com/netbirdio/netbird/client/internal/peer"
	"github.com/netbirdio/netbird/client/internal/profilemanager"
	"github.com/netbirdio/netbird/client/system"
	"github.com/netbirdio/netbird/formatter"
	"github.com/netbirdio/netbird/route"
	"github.com/netbirdio/netbird/shared/management/domain"
	types "github.com/netbirdio/netbird/upload-server/types"
)

// ConnectionListener export internal Listener for mobile
type ConnectionListener interface {
	peer.Listener
}

// RouteListener export internal RouteListener for mobile
type NetworkChangeListener interface {
	listener.NetworkChangeListener
}

// DnsManager export internal dns Manager for mobile
type DnsManager interface {
	dns.IosDnsManager
}

// CustomLogger export internal CustomLogger for mobile
type CustomLogger interface {
	Debug(message string)
	Info(message string)
	Error(message string)
}

type selectRoute struct {
	NetID         string
	Network       netip.Prefix
	Domains       domain.List
	Selected      bool
	Status        string
	extraNetworks []netip.Prefix
}

func init() {
	formatter.SetLogcatFormatter(log.StandardLogger())
}

// Client struct manage the life circle of background service
type Client struct {
	cfgFile               string
	stateFile             string
	cacheDir              string
	logFilePath           string
	recorder              *peer.Status
	ctxCancel             context.CancelFunc
	ctxCancelLock         *sync.Mutex
	deviceName            string
	osName                string
	osVersion             string
	networkChangeListener listener.NetworkChangeListener
	onHostDnsFn           func([]string)
	dnsManager            dns.IosDnsManager
	loginComplete         bool
	// preloadedConfig holds config loaded from JSON (used on tvOS where file writes are blocked)
	preloadedConfig *profilemanager.Config

	stateMu       sync.RWMutex
	connectClient *internal.ConnectClient
	config        *profilemanager.Config
}

// NewClient instantiate a new Client
func NewClient(cfgFile, stateFile, cacheDir, logFilePath, deviceName string, osVersion string, osName string, networkChangeListener NetworkChangeListener, dnsManager DnsManager) *Client {
	return &Client{
		cfgFile:               cfgFile,
		stateFile:             stateFile,
		cacheDir:              cacheDir,
		logFilePath:           logFilePath,
		deviceName:            deviceName,
		osName:                osName,
		osVersion:             osVersion,
		recorder:              peer.NewRecorder(""),
		ctxCancelLock:         &sync.Mutex{},
		networkChangeListener: networkChangeListener,
		dnsManager:            dnsManager,
	}
}

// SetConfigFromJSON loads config from a JSON string into memory.
// This is used on tvOS where file writes to App Group containers are blocked.
// When set, IsLoginRequired() and Run() will use this preloaded config instead of reading from file.
func (c *Client) SetConfigFromJSON(jsonStr string) error {
	cfg, err := profilemanager.ConfigFromJSON(jsonStr)
	if err != nil {
		log.Errorf("SetConfigFromJSON: failed to parse config JSON: %v", err)
		return err
	}
	c.preloadedConfig = cfg
	log.Infof("SetConfigFromJSON: config loaded successfully from JSON")
	return nil
}

// Run start the internal client. It is a blocker function
func (c *Client) Run(fd int32, interfaceName string, envList *EnvList) error {
	exportEnvList(envList)
	log.Infof("Starting NetBird client")
	log.Debugf("Tunnel uses interface: %s", interfaceName)

	var cfg *profilemanager.Config
	var err error

	// Use preloaded config if available (tvOS where file writes are blocked)
	if c.preloadedConfig != nil {
		log.Infof("Run: using preloaded config from memory")
		cfg = c.preloadedConfig
	} else {
		log.Infof("Run: loading config from file")
		// Use DirectUpdateOrCreateConfig to avoid atomic file operations (temp file + rename)
		// which are blocked by the tvOS sandbox in App Group containers
		cfg, err = profilemanager.DirectUpdateOrCreateConfig(profilemanager.ConfigInput{
			ConfigPath:    c.cfgFile,
			StateFilePath: c.stateFile,
		})
		if err != nil {
			return err
		}
	}
	c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
	c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive)

	var ctx context.Context
	//nolint
	ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
	//nolint
	ctxWithValues = context.WithValue(ctxWithValues, system.OsNameCtxKey, c.osName)
	//nolint
	ctxWithValues = context.WithValue(ctxWithValues, system.OsVersionCtxKey, c.osVersion)
	c.ctxCancelLock.Lock()
	ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
	defer c.ctxCancel()
	c.ctxCancelLock.Unlock()

	auth := NewAuthWithConfig(ctx, cfg)
	err = auth.LoginSync()
	if err != nil {
		return err
	}

	log.Infof("Auth successful")
	// todo do not throw error in case of cancelled context
	ctx = internal.CtxInitState(ctx)
	c.onHostDnsFn = func([]string) {}
	cfg.WgIface = interfaceName

	connectClient := internal.NewConnectClient(ctx, cfg, c.recorder)
	c.setState(cfg, connectClient)
	// Persist the latest sync response so DebugBundle can include the network
	// map. On iOS this is backed by disk to keep it out of the constrained
	// process memory (see the syncstore package).
	connectClient.SetSyncResponsePersistence(true)
	return connectClient.RunOniOS(fd, c.networkChangeListener, c.dnsManager, c.stateFile, c.cacheDir, c.logFilePath)
}

// Stop the internal client and free the resources
func (c *Client) Stop() {
	c.ctxCancelLock.Lock()
	defer c.ctxCancelLock.Unlock()
	if c.ctxCancel == nil {
		return
	}

	c.ctxCancel()
	c.setState(nil, nil)
}

// DebugBundle generates a debug bundle, uploads it and returns the upload key.
// It works with or without a running engine: when the engine is up it reuses
// the live config, sync response and client metrics; otherwise it loads the
// config from disk (or the preloaded tvOS config).
func (c *Client) DebugBundle(anonymize bool) (string, error) {
	cfg, cc := c.stateSnapshot()

	// If the engine hasn't been started, load config so we can reach management.
	if cfg == nil {
		if c.preloadedConfig != nil {
			cfg = c.preloadedConfig
		} else {
			var err error
			// Use DirectUpdateOrCreateConfig to avoid atomic file operations
			// (temp file + rename) blocked by the tvOS sandbox.
			cfg, err = profilemanager.DirectUpdateOrCreateConfig(profilemanager.ConfigInput{
				ConfigPath:    c.cfgFile,
				StateFilePath: c.stateFile,
			})
			if err != nil {
				return "", fmt.Errorf("load config: %w", err)
			}
		}
	}

	deps := debug.GeneratorDependencies{
		InternalConfig: cfg,
		StatusRecorder: c.recorder,
		TempDir:        c.cacheDir,
		StatePath:      c.stateFile,
		LogPath:        c.logFilePath,
	}

	if cc != nil {
		resp, err := cc.GetLatestSyncResponse()
		if err != nil {
			log.Warnf("get latest sync response: %v", err)
		}
		deps.SyncResponse = resp

		if e := cc.Engine(); e != nil {
			if cm := e.GetClientMetrics(); cm != nil {
				deps.ClientMetrics = cm
			}
		}
	}

	bundleGenerator := debug.NewBundleGenerator(
		deps,
		debug.BundleConfig{
			Anonymize:         anonymize,
			IncludeSystemInfo: true,
		},
	)

	path, err := bundleGenerator.Generate()
	if err != nil {
		return "", fmt.Errorf("generate debug bundle: %w", err)
	}
	defer func() {
		if err := os.Remove(path); err != nil {
			log.Errorf("failed to remove debug bundle file: %v", err)
		}
	}()

	uploadCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()

	key, err := debug.UploadDebugBundle(uploadCtx, types.DefaultBundleURL, cfg.ManagementURL.String(), path)
	if err != nil {
		return "", fmt.Errorf("upload debug bundle: %w", err)
	}

	log.Infof("debug bundle uploaded with key %s", key)
	return key, nil
}

// SetTraceLogLevel configure the logger to trace level
func (c *Client) SetTraceLogLevel() {
	log.SetLevel(log.TraceLevel)
}

// GetStatusDetails return with the list of the PeerInfos
func (c *Client) GetStatusDetails() *StatusDetails {

	fullStatus := c.recorder.GetFullStatus()

	peerInfos := make([]PeerInfo, len(fullStatus.Peers))
	for n, p := range fullStatus.Peers {
		var routes = RoutesDetails{}
		for r := range p.GetRoutes() {
			routeInfo := RoutesInfo{r}
			routes.items = append(routes.items, routeInfo)
		}
		pi := PeerInfo{
			IP:                         p.IP,
			IPv6:                       p.IPv6,
			FQDN:                       p.FQDN,
			LocalIceCandidateEndpoint:  p.LocalIceCandidateEndpoint,
			RemoteIceCandidateEndpoint: p.RemoteIceCandidateEndpoint,
			LocalIceCandidateType:      p.LocalIceCandidateType,
			RemoteIceCandidateType:     p.RemoteIceCandidateType,
			PubKey:                     p.PubKey,
			Latency:                    formatDuration(p.Latency),
			BytesRx:                    p.BytesRx,
			BytesTx:                    p.BytesTx,
			ConnStatus:                 p.ConnStatus.String(),
			ConnStatusUpdate:           p.ConnStatusUpdate.Format("2006-01-02 15:04:05"),
			LastWireguardHandshake:     p.LastWireguardHandshake.String(),
			Relayed:                    p.Relayed,
			RosenpassEnabled:           p.RosenpassEnabled,
			Routes:                     routes,
		}
		peerInfos[n] = pi
	}
	return &StatusDetails{items: peerInfos, fqdn: fullStatus.LocalPeerState.FQDN, ip: fullStatus.LocalPeerState.IP, ipv6: fullStatus.LocalPeerState.IPv6}
}

// SetConnectionListener set the network connection listener
func (c *Client) SetConnectionListener(listener ConnectionListener) {
	c.recorder.SetConnectionListener(listener)
}

// RemoveConnectionListener remove connection listener
func (c *Client) RemoveConnectionListener() {
	c.recorder.RemoveConnectionListener()
}

// IsLoginRequiredCached reports whether the LAST observed management error was an
// auth failure (PermissionDenied/InvalidArgument), using the in-memory status
// recorder. Unlike IsLoginRequired() it performs NO network call, so it is safe to
// call from the connection listener during teardown (e.g. onDisconnected) without
// blocking on a slow or unavailable network. Returns false while connected to
// management or when the last error was not auth-related.
func (c *Client) IsLoginRequiredCached() bool {
	return c.recorder.IsLoginRequired()
}

func (c *Client) IsLoginRequired() bool {
	var ctx context.Context
	//nolint
	ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
	//nolint
	ctxWithValues = context.WithValue(ctxWithValues, system.OsNameCtxKey, c.osName)
	//nolint
	ctxWithValues = context.WithValue(ctxWithValues, system.OsVersionCtxKey, c.osVersion)
	c.ctxCancelLock.Lock()
	defer c.ctxCancelLock.Unlock()
	ctx, c.ctxCancel = context.WithCancel(ctxWithValues)

	var cfg *profilemanager.Config
	var err error

	// Use preloaded config if available (tvOS where file writes are blocked)
	if c.preloadedConfig != nil {
		log.Infof("IsLoginRequired: using preloaded config from memory")
		cfg = c.preloadedConfig
	} else {
		log.Infof("IsLoginRequired: loading config from file")
		// Use DirectUpdateOrCreateConfig to avoid atomic file operations (temp file + rename)
		// which are blocked by the tvOS sandbox in App Group containers
		cfg, err = profilemanager.DirectUpdateOrCreateConfig(profilemanager.ConfigInput{
			ConfigPath: c.cfgFile,
		})
		if err != nil {
			log.Errorf("IsLoginRequired: failed to load config: %v", err)
			// If we can't load config, assume login is required
			return true
		}
	}

	if cfg == nil {
		log.Errorf("IsLoginRequired: config is nil")
		return true
	}

	authClient, err := auth.NewAuth(ctx, cfg.PrivateKey, cfg.ManagementURL, cfg)
	if err != nil {
		log.Errorf("IsLoginRequired: failed to create auth client: %v", err)
		return true // Assume login is required if we can't create auth client
	}
	defer authClient.Close()

	needsLogin, err := authClient.IsLoginRequired(ctx)
	if err != nil {
		log.Errorf("IsLoginRequired: check failed: %v", err)
		// If the check fails, assume login is required to be safe
		return true
	}
	log.Infof("IsLoginRequired: needsLogin=%v", needsLogin)
	return needsLogin
}

// loginForMobileAuthTimeout is the timeout for requesting auth info from the server
const loginForMobileAuthTimeout = 30 * time.Second

func (c *Client) LoginForMobile() string {
	var ctx context.Context
	//nolint
	ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
	//nolint
	ctxWithValues = context.WithValue(ctxWithValues, system.OsNameCtxKey, c.osName)
	//nolint
	ctxWithValues = context.WithValue(ctxWithValues, system.OsVersionCtxKey, c.osVersion)
	c.ctxCancelLock.Lock()
	defer c.ctxCancelLock.Unlock()
	ctx, c.ctxCancel = context.WithCancel(ctxWithValues)

	// Use DirectUpdateOrCreateConfig to avoid atomic file operations (temp file + rename)
	// which are blocked by the tvOS sandbox in App Group containers
	cfg, err := profilemanager.DirectUpdateOrCreateConfig(profilemanager.ConfigInput{
		ConfigPath: c.cfgFile,
	})
	if err != nil {
		log.Errorf("LoginForMobile: failed to load config: %v", err)
		return fmt.Sprintf("failed to load config: %v", err)
	}

	oAuthFlow, err := auth.NewOAuthFlow(ctx, cfg, false, false, "")
	if err != nil {
		return err.Error()
	}

	// Use a bounded timeout for the auth info request to prevent indefinite hangs
	authInfoCtx, authInfoCancel := context.WithTimeout(ctx, loginForMobileAuthTimeout)
	defer authInfoCancel()

	flowInfo, err := oAuthFlow.RequestAuthInfo(authInfoCtx)
	if err != nil {
		return err.Error()
	}

	// This could cause a potential race condition with loading the extension which need to be handled on swift side
	go func() {
		tokenInfo, err := oAuthFlow.WaitToken(ctx, flowInfo)
		if err != nil {
			log.Errorf("LoginForMobile: WaitToken failed: %v", err)
			return
		}
		jwtToken := tokenInfo.GetTokenToUse()
		authClient, err := auth.NewAuth(ctx, cfg.PrivateKey, cfg.ManagementURL, cfg)
		if err != nil {
			log.Errorf("LoginForMobile: failed to create auth client: %v", err)
			return
		}
		defer authClient.Close()
		if err, _ := authClient.Login(ctx, "", jwtToken); err != nil {
			log.Errorf("LoginForMobile: Login failed: %v", err)
			return
		}
		c.loginComplete = true
	}()

	return flowInfo.VerificationURIComplete
}

func (c *Client) IsLoginComplete() bool {
	return c.loginComplete
}

func (c *Client) ClearLoginComplete() {
	c.loginComplete = false
}

func (c *Client) GetRoutesSelectionDetails() (*RoutesSelectionDetails, error) {
	_, connectClient := c.stateSnapshot()
	if connectClient == nil {
		return nil, fmt.Errorf("not connected")
	}

	engine := connectClient.Engine()
	if engine == nil {
		return nil, fmt.Errorf("not connected")
	}

	routeManager := engine.GetRouteManager()
	if routeManager == nil {
		return nil, fmt.Errorf("could not get route manager")
	}
	routesMap := routeManager.GetClientRoutesWithNetID()
	routeSelector := routeManager.GetRouteSelector()
	if routeSelector == nil {
		return nil, fmt.Errorf("could not get route selector")
	}

	v6ExitMerged := route.V6ExitMergeSet(routesMap)
	routes := buildSelectRoutes(routesMap, routeSelector.IsSelected, v6ExitMerged)
	resolvedDomains := c.recorder.GetResolvedDomainsStates()

	// Compute each route's connection status in the core (mirroring the Android
	// bridge), so the UI doesn't have to infer it by string-matching the joined
	// Network value against peer routes. For a merged exit node the status reflects
	// whichever of the v4/v6 prefixes is served by a connected peer; for dynamic
	// (DNS) routes the peer route key is the domain pattern (see dynamic.Route.String).
	connectedRoutes := c.connectedRouteSet()
	for _, r := range routes {
		r.Status = routeStatus(r, connectedRoutes)
	}

	return prepareRouteSelectionDetails(routes, resolvedDomains), nil
}

// connectedRouteSet returns the set of route keys (as strings) currently served by a
// connected peer, gathered across all connected peers' route tables. The keys match
// what the route manager records: a prefix string for static routes (e.g. "0.0.0.0/0")
// and the domain pattern for dynamic routes (e.g. "*.example.com").
func (c *Client) connectedRouteSet() map[string]struct{} {
	connected := map[string]struct{}{}
	for _, p := range c.recorder.GetFullStatus().Peers {
		if p.ConnStatus != peer.StatusConnected {
			continue
		}
		for r := range p.GetRoutes() {
			connected[r] = struct{}{}
		}
	}
	return connected
}

// routeStatus reports "Connected" if any of the route's keys is served by a connected
// peer: the primary Network prefix, an extra v6 network of a merged exit node, or the
// domain pattern for a dynamic DNS route. Otherwise "Idle".
func routeStatus(r *selectRoute, connectedRoutes map[string]struct{}) string {
	keys := make([]string, 0, 1+len(r.extraNetworks))
	if len(r.Domains) > 0 {
		keys = append(keys, r.Domains.SafeString())
	} else {
		keys = append(keys, r.Network.String())
	}
	for _, extra := range r.extraNetworks {
		keys = append(keys, extra.String())
	}
	for _, k := range keys {
		if _, ok := connectedRoutes[k]; ok {
			return peer.StatusConnected.String()
		}
	}
	return peer.StatusIdle.String()
}

func buildSelectRoutes(routesMap map[route.NetID][]*route.Route, isSelected func(route.NetID) bool, v6Merged map[route.NetID]struct{}) []*selectRoute {
	var routes []*selectRoute
	for id, rt := range routesMap {
		if len(rt) == 0 {
			continue
		}
		if _, ok := v6Merged[id]; ok {
			continue
		}

		r := &selectRoute{
			NetID:    string(id),
			Network:  rt[0].Network,
			Domains:  rt[0].Domains,
			Selected: isSelected(id),
		}

		v6ID := route.NetID(string(id) + route.V6ExitSuffix)
		if _, ok := v6Merged[v6ID]; ok {
			r.extraNetworks = []netip.Prefix{routesMap[v6ID][0].Network}
		}

		routes = append(routes, r)
	}

	sort.Slice(routes, func(i, j int) bool {
		iBits, jBits := routes[i].Network.Bits(), routes[j].Network.Bits()
		if iBits != jBits {
			return iBits < jBits
		}
		iAddr, jAddr := routes[i].Network.Addr(), routes[j].Network.Addr()
		if iAddr != jAddr {
			return iAddr.Less(jAddr)
		}
		return routes[i].NetID < routes[j].NetID
	})

	return routes
}

func prepareRouteSelectionDetails(routes []*selectRoute, resolvedDomains map[domain.Domain]peer.ResolvedDomainInfo) *RoutesSelectionDetails {
	var routeSelection []RoutesSelectionInfo
	for _, r := range routes {
		// resolvedDomains is keyed by the resolved domain (e.g. api.ipify.org),
		// not the configured pattern (e.g. *.ipify.org). Group entries whose
		// ParentDomain belongs to this route, mirroring the daemon logic in
		// client/server/network.go.
		domainList := make([]DomainInfo, 0, len(r.Domains))
		domainIndex := make(map[domain.Domain]int, len(r.Domains))
		for _, d := range r.Domains {
			domainIndex[d] = len(domainList)
			domainList = append(domainList, DomainInfo{Domain: d.SafeString()})
		}

		for _, info := range resolvedDomains {
			idx, ok := domainIndex[info.ParentDomain]
			if !ok {
				continue
			}
			for _, prefix := range info.Prefixes {
				domainList[idx].AddResolvedIP(prefix.Addr().String())
			}
		}

		domainDetails := DomainDetails{items: domainList}

		// For dynamic (DNS) routes, expose the joined domain pattern as the
		// Network value so it matches the peer.routes entries on the Swift
		// side (mirroring the Android bridge in client/android/client.go).
		netStr := r.Network.String()
		if len(r.Domains) > 0 {
			netStr = r.Domains.SafeString()
		}
		for _, extra := range r.extraNetworks {
			netStr += ", " + extra.String()
		}

		routeSelection = append(routeSelection, RoutesSelectionInfo{
			ID:       r.NetID,
			Network:  netStr,
			Domains:  &domainDetails,
			Selected: r.Selected,
			Status:   r.Status,
		})
	}

	routeSelectionDetails := RoutesSelectionDetails{items: routeSelection}
	return &routeSelectionDetails
}

func (c *Client) SelectRoute(id string) error {
	_, connectClient := c.stateSnapshot()
	if connectClient == nil {
		return fmt.Errorf("not connected")
	}

	engine := connectClient.Engine()
	if engine == nil {
		return fmt.Errorf("not connected")
	}

	routeManager := engine.GetRouteManager()
	routeSelector := routeManager.GetRouteSelector()
	if id == "All" {
		log.Debugf("select all routes")
		routeSelector.SelectAllRoutes()
	} else {
		log.Debugf("select route with id: %s", id)
		routes := toNetIDs([]string{id})
		routesMap := routeManager.GetClientRoutesWithNetID()
		routes = route.ExpandV6ExitPairs(routes, routesMap)
		if err := routeSelector.SelectRoutes(routes, true, maps.Keys(routesMap)); err != nil {
			log.Debugf("error when selecting routes: %s", err)
			return fmt.Errorf("select routes: %w", err)
		}
	}
	routeManager.TriggerSelection(routeManager.GetClientRoutes())
	return nil

}

func (c *Client) DeselectRoute(id string) error {
	_, connectClient := c.stateSnapshot()
	if connectClient == nil {
		return fmt.Errorf("not connected")
	}
	engine := connectClient.Engine()
	if engine == nil {
		return fmt.Errorf("not connected")
	}

	routeManager := engine.GetRouteManager()
	routeSelector := routeManager.GetRouteSelector()
	if id == "All" {
		log.Debugf("deselect all routes")
		routeSelector.DeselectAllRoutes()
	} else {
		log.Debugf("deselect route with id: %s", id)
		routes := toNetIDs([]string{id})
		routesMap := routeManager.GetClientRoutesWithNetID()
		routes = route.ExpandV6ExitPairs(routes, routesMap)
		if err := routeSelector.DeselectRoutes(routes, maps.Keys(routesMap)); err != nil {
			log.Debugf("error when deselecting routes: %s", err)
			return fmt.Errorf("deselect routes: %w", err)
		}
	}
	routeManager.TriggerSelection(routeManager.GetClientRoutes())
	return nil
}

// setState stores the running engine state so DebugBundle can reuse the live
// config and ConnectClient. It is cleared on Stop.
func (c *Client) setState(cfg *profilemanager.Config, cc *internal.ConnectClient) {
	c.stateMu.Lock()
	defer c.stateMu.Unlock()
	c.config = cfg
	c.connectClient = cc
}

// stateSnapshot returns the current config and ConnectClient under the lock.
func (c *Client) stateSnapshot() (*profilemanager.Config, *internal.ConnectClient) {
	c.stateMu.RLock()
	defer c.stateMu.RUnlock()
	return c.config, c.connectClient
}

func formatDuration(d time.Duration) string {
	ds := d.String()
	dotIndex := strings.Index(ds, ".")
	if dotIndex != -1 {
		// Determine end of numeric part, ensuring we stop at two decimal places or the actual end if fewer
		endIndex := dotIndex + 3
		if endIndex > len(ds) {
			endIndex = len(ds)
		}
		// Find where the numeric part ends by finding the first non-digit character after the dot
		unitStart := endIndex
		for unitStart < len(ds) && (ds[unitStart] >= '0' && ds[unitStart] <= '9') {
			unitStart++
		}
		// Ensures that we only take the unit characters after the numerical part
		if unitStart < len(ds) {
			return ds[:endIndex] + ds[unitStart:]
		}
		return ds[:endIndex] // In case no units are found after the digits
	}
	return ds
}

func toNetIDs(routes []string) []route.NetID {
	var netIDs []route.NetID
	for _, rt := range routes {
		netIDs = append(netIDs, route.NetID(rt))
	}
	return netIDs
}

func exportEnvList(list *EnvList) {
	if list == nil {
		return
	}
	for k, v := range list.AllItems() {
		log.Debugf("Env variable %s's value is currently: %s", k, os.Getenv(k))
		log.Debugf("Setting env variable %s: %s", k, v)

		if err := os.Setenv(k, v); err != nil {
			log.Errorf("could not set env variable %s: %v", k, err)
		} else {
			log.Debugf("Env variable %s was set successfully", k)
		}
	}
}
