package profilemanager

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"net"
	"net/url"
	"os"
	"os/user"
	"path/filepath"
	"reflect"
	"runtime"
	"slices"
	"strings"
	"time"

	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"

	log "github.com/sirupsen/logrus"

	"github.com/netbirdio/netbird/client/iface"
	"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
	"github.com/netbirdio/netbird/client/mdm"
	"github.com/netbirdio/netbird/client/ssh"
	mgm "github.com/netbirdio/netbird/shared/management/client"
	"github.com/netbirdio/netbird/shared/management/domain"
	"github.com/netbirdio/netbird/util"
)

const (
	// managementLegacyPortString is the port that was used before by the Management gRPC server.
	// It is used for backward compatibility now.
	managementLegacyPortString = "33073"
	// DefaultManagementURL points to the NetBird's cloud management endpoint
	DefaultManagementURL = "https://api.netbird.io:443"
	// oldDefaultManagementURL points to the NetBird's old cloud management endpoint
	oldDefaultManagementURL = "https://api.wiretrustee.com:443"
	// DefaultAdminURL points to NetBird's cloud management console
	DefaultAdminURL = "https://app.netbird.io:443"
)

// mgmProber is the subset of management client needed for URL migration probes.
type mgmProber interface {
	HealthCheck() error
	Close() error
}

// newMgmProber creates a management client for probing URL reachability.
// Overridden in tests to avoid real network calls.
var newMgmProber = func(ctx context.Context, addr string, key wgtypes.Key, tlsEnabled bool) (mgmProber, error) {
	return mgm.NewClient(ctx, addr, key, tlsEnabled)
}

var DefaultInterfaceBlacklist = []string{
	iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts",
	"Tailscale", "tailscale", "docker", "veth", "br-", "lo",
}

// loadMDMPolicy is the package-level indirection used by apply() to read the
// active MDM policy. Tests override this to inject a fake policy.
var loadMDMPolicy = mdm.LoadPolicy

// ConfigInput carries configuration changes to the client
type ConfigInput struct {
	ManagementURL                 string
	AdminURL                      string
	ConfigPath                    string
	StateFilePath                 string
	PreSharedKey                  *string
	ServerSSHAllowed              *bool
	EnableSSHRoot                 *bool
	EnableSSHSFTP                 *bool
	EnableSSHLocalPortForwarding  *bool
	EnableSSHRemotePortForwarding *bool
	DisableSSHAuth                *bool
	SSHJWTCacheTTL                *int
	NATExternalIPs                []string
	CustomDNSAddress              []byte
	RosenpassEnabled              *bool
	RosenpassPermissive           *bool
	InterfaceName                 *string
	WireguardPort                 *int
	NetworkMonitor                *bool
	DisableAutoConnect            *bool
	ExtraIFaceBlackList           []string
	DNSRouteInterval              *time.Duration
	ClientCertPath                string
	ClientCertKeyPath             string

	DisableClientRoutes *bool
	DisableServerRoutes *bool
	DisableDNS          *bool
	DisableFirewall     *bool
	BlockLANAccess      *bool
	BlockInbound        *bool
	DisableIPv6         *bool
	SyncMessageVersion  *int

	DisableNotifications *bool

	DNSLabels domain.List

	MTU *uint16
}

// Config Configuration type
type Config struct {
	// Name is the human-readable profile name shown in CLI/UI listings.
	// It is independent of the profile's on-disk filename (which is the ID).
	Name string

	// Wireguard private key of local peer
	PrivateKey                    string
	PreSharedKey                  string
	ManagementURL                 *url.URL
	AdminURL                      *url.URL
	WgIface                       string
	WgPort                        int
	NetworkMonitor                *bool
	IFaceBlackList                []string
	DisableIPv6Discovery          bool
	RosenpassEnabled              bool
	RosenpassPermissive           bool
	ServerSSHAllowed              *bool
	EnableSSHRoot                 *bool
	EnableSSHSFTP                 *bool
	EnableSSHLocalPortForwarding  *bool
	EnableSSHRemotePortForwarding *bool
	DisableSSHAuth                *bool
	SSHJWTCacheTTL                *int

	DisableClientRoutes bool
	DisableServerRoutes bool
	DisableDNS          bool
	DisableFirewall     bool
	BlockLANAccess      bool
	BlockInbound        bool
	DisableIPv6         bool
	SyncMessageVersion  *int

	DisableNotifications *bool

	DNSLabels domain.List

	// SSHKey is a private SSH key in a PEM format
	SSHKey string

	// ExternalIP mappings, if different from the host interface IP
	//
	//   External IP must not be behind a CGNAT and port-forwarding for incoming UDP packets from WgPort on ExternalIP
	//   to WgPort on host interface IP must be present. This can take form of single port-forwarding rule, 1:1 DNAT
	//   mapping ExternalIP to host interface IP, or a NAT DMZ to host interface IP.
	//
	//   A single mapping will take the form of: external[/internal]
	//    external (required): either the external IP address or "stun" to use STUN to determine the external IP address
	//    internal (optional): either the internal/interface IP address or an interface name
	//
	//   examples:
	//      "12.34.56.78"          => all interfaces IPs will be mapped to external IP of 12.34.56.78
	//      "12.34.56.78/eth0"     => IPv4 assigned to interface eth0 will be mapped to external IP of 12.34.56.78
	//      "12.34.56.78/10.1.2.3" => interface IP 10.1.2.3 will be mapped to external IP of 12.34.56.78

	NATExternalIPs []string
	// CustomDNSAddress sets the DNS resolver listening address in format ip:port
	CustomDNSAddress string

	// DisableAutoConnect determines whether the client should not start with the service
	// it's set to false by default due to backwards compatibility
	DisableAutoConnect bool

	// DNSRouteInterval is the interval in which the DNS routes are updated
	DNSRouteInterval time.Duration
	// Path to a certificate used for mTLS authentication
	ClientCertPath string

	// Path to corresponding private key of ClientCertPath
	ClientCertKeyPath string

	ClientCertKeyPair *tls.Certificate `json:"-"`

	// LazyConnection is the MDM-managed lazy-connection override ("on"/"off"/"").
	// Runtime-only: re-derived from MDM policy on each load, never persisted.
	LazyConnection string `json:"-"`

	MTU uint16

	// policy is the MDM policy that produced the currently-set values for
	// any MDM-enforced fields. Set by applyMDMPolicy at the tail of apply()
	// and reset on every apply() invocation. Never persisted to disk.
	// Callers query enforcement state via Policy() and the mdm.Policy API
	// (HasKey, ManagedKeys, IsEmpty).
	policy *mdm.Policy `json:"-"`
}

// Policy returns the MDM policy applied to this Config. Returns a non-nil
// empty Policy when MDM enforcement is inactive; callers can always invoke
// HasKey / ManagedKeys / IsEmpty without a nil check.
func (config *Config) Policy() *mdm.Policy {
	if config == nil || config.policy == nil {
		return mdm.NewPolicy(nil)
	}
	return config.policy
}

var ConfigDirOverride string

func getConfigDir() (string, error) {
	if ConfigDirOverride != "" {
		return ConfigDirOverride, nil
	}

	base, err := baseConfigDir()
	if err != nil {
		return "", err
	}

	configDir := filepath.Join(base, "netbird")
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		return "", err
	}
	return configDir, nil
}

func baseConfigDir() (string, error) {
	if runtime.GOOS == "darwin" {
		if u, err := user.Current(); err == nil && u.HomeDir != "" {
			return filepath.Join(u.HomeDir, "Library", "Application Support"), nil
		}
	}
	return os.UserConfigDir()
}

func getConfigDirForUser(username string) (string, error) {
	if ConfigDirOverride != "" {
		return ConfigDirOverride, nil
	}

	username = sanitizeProfileName(username)

	configDir := filepath.Join(DefaultConfigPathDir, username)
	if _, err := os.Stat(configDir); os.IsNotExist(err) {
		if err := os.MkdirAll(configDir, 0700); err != nil {
			return "", err
		}
	}

	return configDir, nil
}

func fileExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

// createNewConfig creates a new config generating a new Wireguard key and saving to file
func createNewConfig(input ConfigInput) (*Config, error) {
	config := &Config{
		// defaults to false only for new (post 0.26) configurations
		ServerSSHAllowed: util.False(),
		WgPort:           iface.DefaultWgPort,
	}

	if _, err := config.apply(input); err != nil {
		return nil, err
	}

	return config, nil
}

func (config *Config) apply(input ConfigInput) (updated bool, err error) {
	if config.Name != "" {
		sanitized, err := sanitizeDisplayName(config.Name)
		if err != nil {
			return false, fmt.Errorf("invalid profile name: %w", err)
		}
		if sanitized != config.Name {
			config.Name = sanitized
			updated = true
		}
	}
	if config.ManagementURL == nil {
		log.Infof("using default Management URL %s", DefaultManagementURL)
		config.ManagementURL, err = parseURL("Management URL", DefaultManagementURL)
		if err != nil {
			return false, err
		}
	}
	if input.ManagementURL != "" && input.ManagementURL != config.ManagementURL.String() {
		log.Infof("new Management URL provided, updated to %#v (old value %#v)",
			input.ManagementURL, config.ManagementURL.String())
		URL, err := parseURL("Management URL", input.ManagementURL)
		if err != nil {
			return false, err
		}
		config.ManagementURL = URL
		updated = true
	} else if config.ManagementURL == nil {
		log.Infof("using default Management URL %s", DefaultManagementURL)
		config.ManagementURL, err = parseURL("Management URL", DefaultManagementURL)
		if err != nil {
			return false, err
		}
	}

	if config.AdminURL == nil {
		log.Infof("using default Admin URL %s", DefaultAdminURL)
		config.AdminURL, err = parseURL("Admin URL", DefaultAdminURL)
		if err != nil {
			return false, err
		}
	}
	if input.AdminURL != "" && input.AdminURL != config.AdminURL.String() {
		log.Infof("new Admin Panel URL provided, updated to %#v (old value %#v)",
			input.AdminURL, config.AdminURL.String())
		newURL, err := parseURL("Admin Panel URL", input.AdminURL)
		if err != nil {
			return updated, err
		}
		config.AdminURL = newURL
		updated = true
	}

	if config.PrivateKey == "" {
		log.Infof("generated new Wireguard key")
		config.PrivateKey = generateKey()
		updated = true
	}

	if config.SSHKey == "" {
		log.Infof("generated new SSH key")
		pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
		if err != nil {
			return false, err
		}
		config.SSHKey = string(pem)
		updated = true
	}

	if input.WireguardPort != nil && *input.WireguardPort != config.WgPort {
		log.Infof("updating Wireguard port %d (old value %d)",
			*input.WireguardPort, config.WgPort)
		config.WgPort = *input.WireguardPort
		updated = true
	}

	if input.InterfaceName != nil && *input.InterfaceName != config.WgIface {
		log.Infof("updating Wireguard interface %#v (old value %#v)",
			*input.InterfaceName, config.WgIface)
		config.WgIface = *input.InterfaceName
		updated = true
	} else if config.WgIface == "" {
		config.WgIface = iface.WgInterfaceDefault
		log.Infof("using default Wireguard interface %s", config.WgIface)
		updated = true
	}

	if input.NATExternalIPs != nil && !reflect.DeepEqual(config.NATExternalIPs, input.NATExternalIPs) {
		log.Infof("updating NAT External IP [ %s ] (old value: [ %s ])",
			strings.Join(input.NATExternalIPs, " "),
			strings.Join(config.NATExternalIPs, " "))
		config.NATExternalIPs = input.NATExternalIPs
		updated = true
	}

	if input.PreSharedKey != nil && *input.PreSharedKey != config.PreSharedKey {
		log.Infof("new pre-shared key provided, replacing old key")
		config.PreSharedKey = *input.PreSharedKey
		updated = true
	}

	if input.RosenpassEnabled != nil && *input.RosenpassEnabled != config.RosenpassEnabled {
		log.Infof("switching Rosenpass to %t", *input.RosenpassEnabled)
		config.RosenpassEnabled = *input.RosenpassEnabled
		updated = true
	}

	if input.RosenpassPermissive != nil && *input.RosenpassPermissive != config.RosenpassPermissive {
		log.Infof("switching Rosenpass permissive to %t", *input.RosenpassPermissive)
		config.RosenpassPermissive = *input.RosenpassPermissive
		updated = true
	}

	if input.NetworkMonitor != nil && (config.NetworkMonitor == nil || *input.NetworkMonitor != *config.NetworkMonitor) {
		log.Infof("switching Network Monitor to %t", *input.NetworkMonitor)
		config.NetworkMonitor = input.NetworkMonitor
		updated = true
	}

	if config.NetworkMonitor == nil {
		// enable network monitoring by default on windows and darwin clients
		if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
			enabled := true
			config.NetworkMonitor = &enabled
			updated = true
		}
	}

	if input.CustomDNSAddress != nil && string(input.CustomDNSAddress) != config.CustomDNSAddress {
		log.Infof("updating custom DNS address %#v (old value %#v)",
			string(input.CustomDNSAddress), config.CustomDNSAddress)
		config.CustomDNSAddress = string(input.CustomDNSAddress)
		updated = true
	}

	if len(config.IFaceBlackList) == 0 {
		log.Infof("filling in interface blacklist with defaults: [ %s ]",
			strings.Join(DefaultInterfaceBlacklist, " "))
		config.IFaceBlackList = append(config.IFaceBlackList, DefaultInterfaceBlacklist...)
		updated = true
	}

	if len(input.ExtraIFaceBlackList) > 0 {
		for _, iFace := range util.SliceDiff(input.ExtraIFaceBlackList, config.IFaceBlackList) {
			log.Infof("adding new entry to interface blacklist: %s", iFace)
			config.IFaceBlackList = append(config.IFaceBlackList, iFace)
			updated = true
		}
	}

	if input.DisableAutoConnect != nil && *input.DisableAutoConnect != config.DisableAutoConnect {
		if *input.DisableAutoConnect {
			log.Infof("turning off automatic connection on startup")
		} else {
			log.Infof("enabling automatic connection on startup")
		}
		config.DisableAutoConnect = *input.DisableAutoConnect
		updated = true
	}

	if input.ServerSSHAllowed != nil && (config.ServerSSHAllowed == nil || *input.ServerSSHAllowed != *config.ServerSSHAllowed) {
		if *input.ServerSSHAllowed {
			log.Infof("enabling SSH server")
		} else {
			log.Infof("disabling SSH server")
		}
		config.ServerSSHAllowed = input.ServerSSHAllowed
		updated = true
	} else if config.ServerSSHAllowed == nil {
		if runtime.GOOS == "android" {
			// default to disabled SSH on Android for security
			log.Infof("setting SSH server to false by default on Android")
			config.ServerSSHAllowed = util.False()
		} else {
			// enables SSH for configs from old versions to preserve backwards compatibility
			log.Infof("falling back to enabled SSH server for pre-existing configuration")
			config.ServerSSHAllowed = util.True()
		}
		updated = true
	}

	if input.EnableSSHRoot != nil && (config.EnableSSHRoot == nil || *input.EnableSSHRoot != *config.EnableSSHRoot) {
		if *input.EnableSSHRoot {
			log.Infof("enabling SSH root login")
		} else {
			log.Infof("disabling SSH root login")
		}
		config.EnableSSHRoot = input.EnableSSHRoot
		updated = true
	}

	if input.EnableSSHSFTP != nil && (config.EnableSSHSFTP == nil || *input.EnableSSHSFTP != *config.EnableSSHSFTP) {
		if *input.EnableSSHSFTP {
			log.Infof("enabling SSH SFTP subsystem")
		} else {
			log.Infof("disabling SSH SFTP subsystem")
		}
		config.EnableSSHSFTP = input.EnableSSHSFTP
		updated = true
	}

	if input.EnableSSHLocalPortForwarding != nil && (config.EnableSSHLocalPortForwarding == nil || *input.EnableSSHLocalPortForwarding != *config.EnableSSHLocalPortForwarding) {
		if *input.EnableSSHLocalPortForwarding {
			log.Infof("enabling SSH local port forwarding")
		} else {
			log.Infof("disabling SSH local port forwarding")
		}
		config.EnableSSHLocalPortForwarding = input.EnableSSHLocalPortForwarding
		updated = true
	}

	if input.EnableSSHRemotePortForwarding != nil && (config.EnableSSHRemotePortForwarding == nil || *input.EnableSSHRemotePortForwarding != *config.EnableSSHRemotePortForwarding) {
		if *input.EnableSSHRemotePortForwarding {
			log.Infof("enabling SSH remote port forwarding")
		} else {
			log.Infof("disabling SSH remote port forwarding")
		}
		config.EnableSSHRemotePortForwarding = input.EnableSSHRemotePortForwarding
		updated = true
	}

	if input.DisableSSHAuth != nil && (config.DisableSSHAuth == nil || *input.DisableSSHAuth != *config.DisableSSHAuth) {
		if *input.DisableSSHAuth {
			log.Infof("disabling SSH authentication")
		} else {
			log.Infof("enabling SSH authentication")
		}
		config.DisableSSHAuth = input.DisableSSHAuth
		updated = true
	}

	if input.SSHJWTCacheTTL != nil && (config.SSHJWTCacheTTL == nil || *input.SSHJWTCacheTTL != *config.SSHJWTCacheTTL) {
		log.Infof("updating SSH JWT cache TTL to %d seconds", *input.SSHJWTCacheTTL)
		config.SSHJWTCacheTTL = input.SSHJWTCacheTTL
		updated = true
	}

	if input.DNSRouteInterval != nil && *input.DNSRouteInterval != config.DNSRouteInterval {
		log.Infof("updating DNS route interval to %s (old value %s)",
			input.DNSRouteInterval.String(), config.DNSRouteInterval.String())
		config.DNSRouteInterval = *input.DNSRouteInterval
		updated = true
	} else if config.DNSRouteInterval == 0 {
		config.DNSRouteInterval = dynamic.DefaultInterval
		log.Infof("using default DNS route interval %s", config.DNSRouteInterval)
		updated = true
	}

	if input.DisableClientRoutes != nil && *input.DisableClientRoutes != config.DisableClientRoutes {
		if *input.DisableClientRoutes {
			log.Infof("disabling client routes")
		} else {
			log.Infof("enabling client routes")
		}
		config.DisableClientRoutes = *input.DisableClientRoutes
		updated = true
	}

	if input.DisableServerRoutes != nil && *input.DisableServerRoutes != config.DisableServerRoutes {
		if *input.DisableServerRoutes {
			log.Infof("disabling server routes")
		} else {
			log.Infof("enabling server routes")
		}
		config.DisableServerRoutes = *input.DisableServerRoutes
		updated = true
	}

	if input.DisableDNS != nil && *input.DisableDNS != config.DisableDNS {
		if *input.DisableDNS {
			log.Infof("disabling DNS configuration")
		} else {
			log.Infof("enabling DNS configuration")
		}
		config.DisableDNS = *input.DisableDNS
		updated = true
	}

	if input.DisableFirewall != nil && *input.DisableFirewall != config.DisableFirewall {
		if *input.DisableFirewall {
			log.Infof("disabling firewall configuration")
		} else {
			log.Infof("enabling firewall configuration")
		}
		config.DisableFirewall = *input.DisableFirewall
		updated = true
	}

	if input.BlockLANAccess != nil && *input.BlockLANAccess != config.BlockLANAccess {
		if *input.BlockLANAccess {
			log.Infof("blocking LAN access")
		} else {
			log.Infof("allowing LAN access")
		}
		config.BlockLANAccess = *input.BlockLANAccess
		updated = true
	}

	if input.BlockInbound != nil && *input.BlockInbound != config.BlockInbound {
		if *input.BlockInbound {
			log.Infof("blocking inbound connections")
		} else {
			log.Infof("allowing inbound connections")
		}
		config.BlockInbound = *input.BlockInbound
		updated = true
	}

	if input.DisableIPv6 != nil && *input.DisableIPv6 != config.DisableIPv6 {
		log.Infof("setting IPv6 overlay disabled=%v", *input.DisableIPv6)
		config.DisableIPv6 = *input.DisableIPv6
		updated = true
	}

	if input.SyncMessageVersion != nil && *input.SyncMessageVersion != *config.SyncMessageVersion {
		log.Infof("setting SyncMessageVersion to %v", *input.SyncMessageVersion)
		*config.SyncMessageVersion = *input.SyncMessageVersion
		updated = true
	}

	if input.DisableNotifications != nil && (config.DisableNotifications == nil || *input.DisableNotifications != *config.DisableNotifications) {
		if *input.DisableNotifications {
			log.Infof("disabling notifications")
		} else {
			log.Infof("enabling notifications")
		}
		config.DisableNotifications = input.DisableNotifications
		updated = true
	}

	if config.DisableNotifications == nil {
		disabled := true
		config.DisableNotifications = &disabled
		log.Infof("setting notifications to disabled by default")
		updated = true
	}

	if input.ClientCertKeyPath != "" {
		config.ClientCertKeyPath = input.ClientCertKeyPath
		updated = true
	}

	if input.ClientCertPath != "" {
		config.ClientCertPath = input.ClientCertPath
		updated = true
	}

	if config.ClientCertPath != "" && config.ClientCertKeyPath != "" {
		cert, err := tls.LoadX509KeyPair(config.ClientCertPath, config.ClientCertKeyPath)
		if err != nil {
			log.Error("Failed to load mTLS cert/key pair: ", err)
		} else {
			config.ClientCertKeyPair = &cert
			log.Info("Loaded client mTLS cert/key pair")
		}
	}

	if input.DNSLabels != nil && !slices.Equal(config.DNSLabels, input.DNSLabels) {
		log.Infof("updating DNS labels [ %s ] (old value: [ %s ])",
			input.DNSLabels.SafeString(),
			config.DNSLabels.SafeString())
		config.DNSLabels = input.DNSLabels
		updated = true
	}

	if input.MTU != nil && *input.MTU != config.MTU {
		log.Infof("updating MTU to %d (old value %d)", *input.MTU, config.MTU)
		config.MTU = *input.MTU
		updated = true
	} else if config.MTU == 0 {
		config.MTU = iface.DefaultMTU
		log.Infof("using default MTU %d", config.MTU)
		updated = true
	}

	// MDM is the last override layer: any key present in the policy
	// supersedes defaults, on-disk config, env vars and CLI input.
	config.applyMDMPolicy(loadMDMPolicy())

	return updated, nil
}

// applyMDMPolicy overlays MDM-supplied values on top of the resolved Config.
// The provided Policy is also stored on the Config so callers can later query
// which fields are enforced. Invalid values (e.g. malformed URLs) are logged
// and skipped to avoid bricking the client; the field keeps its previous
// resolved value but is still marked as managed (Policy.HasKey returns true
// for the key, so per-field rejection of user writes still applies).
func (config *Config) applyMDMPolicy(policy *mdm.Policy) {
	config.policy = policy
	if policy.IsEmpty() {
		return
	}

	// Helper: log the application of a single MDM-managed key. Values for
	// keys in mdm.SecretKeys are redacted.
	logApplied := func(key string, displayValue any) {
		if _, secret := mdm.SecretKeys[key]; secret {
			log.Infof("MDM override %s = ********** (secret)", key)
			return
		}
		log.Infof("MDM override %s = %v", key, displayValue)
	}

	if v, ok := policy.GetString(mdm.KeyManagementURL); ok {
		if u, err := parseURL("Management URL", v); err != nil {
			log.Warnf("MDM management URL %q invalid: %v; keeping previous value", v, err)
		} else {
			config.ManagementURL = u
			logApplied(mdm.KeyManagementURL, u.String())
		}
	}

	if v, ok := policy.GetString(mdm.KeyPreSharedKey); ok {
		// Defensive: refuse the redaction mask in case it round-tripped
		// through a manifest by mistake.
		if !isPreSharedKeyHidden(&v) {
			config.PreSharedKey = v
			logApplied(mdm.KeyPreSharedKey, "")
		}
	}

	// applyBool collapses the per-key "read + set + log" boilerplate
	// for every plain bool MDM key into a single helper. Keeps the
	// outer function's cognitive complexity below SonarCube's
	// threshold; functional behaviour is identical to the inlined
	// branches it replaces.
	applyBool := func(key string, setter func(bool)) {
		v, ok := policy.GetBool(key)
		if !ok {
			return
		}
		setter(v)
		logApplied(key, v)
	}

	applyBool(mdm.KeyAllowServerSSH, func(v bool) { bv := v; config.ServerSSHAllowed = &bv })
	applyBool(mdm.KeyDisableClientRoutes, func(v bool) { config.DisableClientRoutes = v })
	applyBool(mdm.KeyDisableServerRoutes, func(v bool) { config.DisableServerRoutes = v })
	applyBool(mdm.KeyBlockInbound, func(v bool) { config.BlockInbound = v })
	applyBool(mdm.KeyDisableAutoConnect, func(v bool) { config.DisableAutoConnect = v })
	applyBool(mdm.KeyRosenpassEnabled, func(v bool) { config.RosenpassEnabled = v })
	applyBool(mdm.KeyRosenpassPermissive, func(v bool) { config.RosenpassPermissive = v })

	if v, ok := policy.GetInt(mdm.KeyWireguardPort); ok {
		// REG_DWORD is 32-bit; UDP port range is 1-65535. Clamp at the
		// upper bound and reject obviously-invalid values to avoid the
		// engine binding to an unusable port if the admin pushes garbage.
		if v >= 1 && v <= 65535 {
			config.WgPort = int(v)
			logApplied(mdm.KeyWireguardPort, v)
		} else {
			log.Warnf("MDM wireguard port %d out of range [1,65535]; keeping previous value", v)
		}
	}

	if v, ok := policy.GetBool(mdm.KeyLazyConnection); ok {
		state := "off"
		if v {
			state = "on"
		}
		config.LazyConnection = state
		logApplied(mdm.KeyLazyConnection, state)
	}
}

// parseURL parses and validates the URL for the named service. The URL
// must use the http or https scheme; if no port is present, ":443" is
// appended for https or ":80" for http. The serviceName parameter is
// used to contextualise error messages. On success returns the parsed
// *url.URL; on failure returns a non-nil error.
func parseURL(serviceName, serviceURL string) (*url.URL, error) {
	parsedMgmtURL, err := url.ParseRequestURI(serviceURL)
	if err != nil {
		log.Errorf("failed parsing %s URL %s: [%s]", serviceName, serviceURL, err.Error())
		return nil, err
	}

	if parsedMgmtURL.Scheme != "https" && parsedMgmtURL.Scheme != "http" {
		return nil, fmt.Errorf(
			"invalid %s URL provided %s. Supported format [http|https]://[host]:[port]",
			serviceName, serviceURL)
	}

	if parsedMgmtURL.Port() == "" {
		switch parsedMgmtURL.Scheme {
		case "https":
			parsedMgmtURL.Host += ":443"
		case "http":
			parsedMgmtURL.Host += ":80"
		default:
			log.Infof("unable to determine a default port for schema %s in URL %s", parsedMgmtURL.Scheme, serviceURL)
		}
	}

	return parsedMgmtURL, err
}

// generateKey generates a new Wireguard private key
func generateKey() string {
	key, err := wgtypes.GeneratePrivateKey()
	if err != nil {
		panic(err)
	}
	return key.String()
}

// don't overwrite pre-shared key if we receive asterisks from UI
func isPreSharedKeyHidden(preSharedKey *string) bool {
	if preSharedKey != nil && *preSharedKey == "**********" {
		return true
	}
	return false
}

// UpdateConfig update existing configuration according to input configuration and return with the configuration
func UpdateConfig(input ConfigInput) (*Config, error) {
	configExists, err := fileExists(input.ConfigPath)
	if err != nil {
		return nil, fmt.Errorf("failed to check if config file exists: %w", err)
	}
	if !configExists {
		return nil, fmt.Errorf("config file %s does not exist", input.ConfigPath)
	}

	return update(input)
}

// UpdateOrCreateConfig reads existing config or generates a new one
func UpdateOrCreateConfig(input ConfigInput) (*Config, error) {
	configExists, err := fileExists(input.ConfigPath)
	if err != nil {
		return nil, fmt.Errorf("failed to check if config file exists: %w", err)
	}
	if !configExists {
		log.Infof("generating new config %s", input.ConfigPath)
		cfg, err := createNewConfig(input)
		if err != nil {
			return nil, err
		}
		err = util.WriteJsonWithRestrictedPermission(context.Background(), input.ConfigPath, cfg)
		return cfg, err
	}

	if isPreSharedKeyHidden(input.PreSharedKey) {
		input.PreSharedKey = nil
	}
	err = util.EnforcePermission(input.ConfigPath)
	if err != nil {
		log.Errorf("failed to enforce permission on config dir: %v", err)
	}
	return update(input)
}

func update(input ConfigInput) (*Config, error) {
	config := &Config{}

	if _, err := util.ReadJson(input.ConfigPath, config); err != nil {
		return nil, err
	}

	updated, err := config.apply(input)
	if err != nil {
		return nil, err
	}

	if updated {
		if err := util.WriteJson(context.Background(), input.ConfigPath, config); err != nil {
			return nil, err
		}
	}

	return config, nil
}

// GetConfig read config file and return with Config and if it was created. Errors out if it does not exist
func GetConfig(configPath string) (*Config, error) {
	return readConfig(configPath, false)
}

// UpdateOldManagementURL checks whether client can switch to the new Management URL with port 443 and the management domain.
// If it can switch, then it updates the config and returns a new one. Otherwise, it returns the provided config.
// The check is performed only for the NetBird's managed version.
func UpdateOldManagementURL(ctx context.Context, config *Config, configPath string) (*Config, error) {
	defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
	if err != nil {
		return nil, err
	}

	parsedOldDefaultManagementURL, err := parseURL("Management URL", oldDefaultManagementURL)
	if err != nil {
		return nil, err
	}

	if config.ManagementURL.Hostname() != defaultManagementURL.Hostname() &&
		config.ManagementURL.Hostname() != parsedOldDefaultManagementURL.Hostname() {
		// only do the check for the NetBird's managed version
		return config, nil
	}

	var mgmTlsEnabled bool
	if config.ManagementURL.Scheme == "https" {
		mgmTlsEnabled = true
	}

	if !mgmTlsEnabled {
		// only do the check for HTTPs scheme (the hosted version of the Management service is always HTTPs)
		return config, nil
	}

	if config.ManagementURL.Port() != managementLegacyPortString &&
		config.ManagementURL.Hostname() == defaultManagementURL.Hostname() {
		return config, nil
	}

	newURL, err := parseURL("Management URL", fmt.Sprintf("%s://%s", config.ManagementURL.Scheme, net.JoinHostPort(defaultManagementURL.Hostname(), "443")))
	if err != nil {
		return nil, err
	}
	// here we check whether we could switch from the legacy 33073 port to the new 443
	log.Infof("attempting to switch from the legacy Management URL %s to the new one %s",
		config.ManagementURL.String(), newURL.String())
	key, err := wgtypes.ParseKey(config.PrivateKey)
	if err != nil {
		log.Infof("couldn't switch to the new Management %s", newURL.String())
		return config, err
	}

	client, err := newMgmProber(ctx, newURL.Host, key, mgmTlsEnabled)
	if err != nil {
		log.Infof("couldn't switch to the new Management %s", newURL.String())
		return config, err
	}
	defer func() {
		if err := client.Close(); err != nil {
			log.Warnf("failed to close the Management service client %v", err)
		}
	}()

	// gRPC check
	if err = client.HealthCheck(); err != nil {
		log.Infof("couldn't switch to the new Management %s", newURL.String())
		return nil, err
	}

	// everything is alright => update the config
	newConfig, err := UpdateConfig(ConfigInput{
		ManagementURL: newURL.String(),
		ConfigPath:    configPath,
	})
	if err != nil {
		log.Infof("couldn't switch to the new Management %s", newURL.String())
		return config, fmt.Errorf("failed updating config file: %v", err)
	}
	log.Infof("successfully switched to the new Management URL: %s", newURL.String())

	return newConfig, nil
}

// CreateInMemoryConfig generate a new config but do not write out it to the store
func CreateInMemoryConfig(input ConfigInput) (*Config, error) {
	return createNewConfig(input)
}

// ReadConfig read config file and return with Config. If it is not exists create a new with default values
func ReadConfig(configPath string) (*Config, error) {
	return readConfig(configPath, true)
}

// ReadConfig read config file and return with Config. If it is not exists create a new with default values
func readConfig(configPath string, createIfMissing bool) (*Config, error) {
	configExists, err := fileExists(configPath)
	if err != nil {
		return nil, fmt.Errorf("failed to check if config file exists: %w", err)
	}

	if configExists {
		err := util.EnforcePermission(configPath)
		if err != nil {
			log.Errorf("failed to enforce permission on config dir: %v", err)
		}

		config := &Config{}
		if _, err := util.ReadJson(configPath, config); err != nil {
			return nil, err
		}
		// initialize through apply() without changes
		if changed, err := config.apply(ConfigInput{}); err != nil {
			return nil, err
		} else if changed {
			if err = WriteOutConfig(configPath, config); err != nil {
				return nil, err
			}
		}

		return config, nil
	} else if !createIfMissing {
		return nil, fmt.Errorf("config file %s does not exist", configPath)
	}

	cfg, err := createNewConfig(ConfigInput{ConfigPath: configPath})
	if err != nil {
		return nil, err
	}

	err = WriteOutConfig(configPath, cfg)
	return cfg, err
}

// WriteOutConfig write put the prepared config to the given path
func WriteOutConfig(path string, config *Config) error {
	return util.WriteJson(context.Background(), path, config)
}

// DirectWriteOutConfig writes config directly without atomic temp file operations.
// Use this on platforms where atomic writes are blocked (e.g., tvOS sandbox).
func DirectWriteOutConfig(path string, config *Config) error {
	return util.DirectWriteJson(context.Background(), path, config)
}

// DirectUpdateOrCreateConfig is like UpdateOrCreateConfig but uses direct (non-atomic) writes.
// Use this on platforms where atomic writes are blocked (e.g., tvOS sandbox).
func DirectUpdateOrCreateConfig(input ConfigInput) (*Config, error) {
	configExists, err := fileExists(input.ConfigPath)
	if err != nil {
		return nil, fmt.Errorf("failed to check if config file exists: %w", err)
	}
	if !configExists {
		log.Infof("generating new config %s", input.ConfigPath)
		cfg, err := createNewConfig(input)
		if err != nil {
			return nil, err
		}
		err = util.DirectWriteJson(context.Background(), input.ConfigPath, cfg)
		return cfg, err
	}

	if isPreSharedKeyHidden(input.PreSharedKey) {
		input.PreSharedKey = nil
	}

	// Enforce permissions on existing config files (same as UpdateOrCreateConfig)
	if err := util.EnforcePermission(input.ConfigPath); err != nil {
		log.Errorf("failed to enforce permission on config file: %v", err)
	}

	return directUpdate(input)
}

func directUpdate(input ConfigInput) (*Config, error) {
	config := &Config{}

	if _, err := util.ReadJson(input.ConfigPath, config); err != nil {
		return nil, err
	}

	updated, err := config.apply(input)
	if err != nil {
		return nil, err
	}

	if updated {
		if err := util.DirectWriteJson(context.Background(), input.ConfigPath, config); err != nil {
			return nil, err
		}
	}

	return config, nil
}

// ConfigToJSON serializes a Config struct to a JSON string.
// This is useful for exporting config to alternative storage mechanisms
// (e.g., UserDefaults on tvOS where file writes are blocked).
func ConfigToJSON(config *Config) (string, error) {
	bs, err := json.MarshalIndent(config, "", "    ")
	if err != nil {
		return "", err
	}
	return string(bs), nil
}

// ConfigFromJSON deserializes a JSON string to a Config struct.
// This is useful for restoring config from alternative storage mechanisms.
// After unmarshaling, defaults are applied to ensure the config is fully initialized.
func ConfigFromJSON(jsonStr string) (*Config, error) {
	config := &Config{}
	err := json.Unmarshal([]byte(jsonStr), config)
	if err != nil {
		return nil, err
	}

	// Apply defaults to ensure required fields are initialized.
	// This mirrors what readConfig does after loading from file.
	if _, err := config.apply(ConfigInput{}); err != nil {
		return nil, fmt.Errorf("failed to apply defaults to config: %w", err)
	}

	return config, nil
}
