// Package mdm reads MDM-managed configuration from platform-native sources
// (plist on macOS, registry on Windows, UserDefaults on iOS,
// RestrictionsManager on Android). The returned Policy is consumed by
// profilemanager.Config.apply() as the highest-priority override layer.
//
// An empty Policy (no source present, or source present with zero keys)
// means no MDM enforcement is active and the client behaves as if the
// feature did not exist.
package mdm

import (
	"sort"
	"strconv"
	"strings"

	log "github.com/sirupsen/logrus"
)

// Well-known policy keys. Names mirror the corresponding ConfigInput Go field
// names (lowerCamelCase) so the daemon can map a Policy key directly to a
// configuration field.
const (
	KeyManagementURL            = "managementURL"
	KeyDisableUpdateSettings    = "disableUpdateSettings"
	KeyDisableProfiles          = "disableProfiles"
	KeyDisableNetworks          = "disableNetworks"
	KeyDisableClientRoutes      = "disableClientRoutes"
	KeyDisableServerRoutes      = "disableServerRoutes"
	KeyBlockInbound             = "blockInbound"
	KeyDisableMetricsCollection = "disableMetricsCollection"
	KeyAllowServerSSH           = "allowServerSSH"
	KeyDisableAutoConnect       = "disableAutoConnect"
	KeyPreSharedKey             = "preSharedKey"
	KeyRosenpassEnabled         = "rosenpassEnabled"
	KeyRosenpassPermissive      = "rosenpassPermissive"
	KeyWireguardPort            = "wireguardPort"

	// Split tunnel is modeled as a single conceptual policy with two
	// registry/plist values. KeySplitTunnelMode is the discriminator
	// ("allow" or "disallow"); KeySplitTunnelApps is a comma-separated
	// list of package names. The values are mutually exclusive by
	// construction — only one mode can be set at a time.
	KeySplitTunnelMode = "splitTunnelMode"
	KeySplitTunnelApps = "splitTunnelApps"

	// KeyLazyConnection forces the lazy-connection feature on or off, overriding
	// the management feature flag. Read as a bool (native bool, or on/off,
	// true/false, 1/0, yes/no); absent = defer to management.
	KeyLazyConnection = "lazyConnection"
)

// Split-tunnel mode literals (KeySplitTunnelMode values).
const (
	SplitTunnelModeAllow    = "allow"
	SplitTunnelModeDisallow = "disallow"
)

// SecretKeys lists keys whose values must be redacted in logs.
var SecretKeys = map[string]struct{}{
	KeyPreSharedKey: {},
}

// boolStringLiterals enumerates the textual boolean encodings the
// platform loaders may produce (Windows REG_SZ "true", iOS / Android
// managed-config booleans-as-strings, etc.). Lookup keeps GetBool flat
// (no nested switch on the string case).
var boolStringLiterals = map[string]bool{
	"true":  true,
	"1":     true,
	"yes":   true,
	"on":    true,
	"false": false,
	"0":     false,
	"no":    false,
	"off":   false,
}

// Policy holds MDM-managed settings read from the platform source. A nil or
// empty Policy means no enforcement is active.
type Policy struct {
	values map[string]any
}

// NewPolicy constructs a Policy from a key→value map. Pass nil or an
// empty map to construct an empty (no-enforcement) Policy. The returned
// *Policy is always non-nil.
func NewPolicy(values map[string]any) *Policy {
	if values == nil {
		values = map[string]any{}
	}
	return &Policy{values: values}
}

// LoadPolicy reads the platform-native MDM configuration. Returns an
// empty (but non-nil) Policy when no source is present, the source is
// empty, or the platform is unsupported.
//
// Diagnostic logging differentiates the three states:
//   - source absent / unsupported platform: trace log only
//   - source present, zero keys:             info "MDM enrolled (no managed keys)"
//   - source present, N keys:                info "MDM enrolled with N managed keys: [...]"
func LoadPolicy() *Policy {
	values, err := loadPlatformPolicy()
	if err != nil {
		log.Tracef("MDM policy load: %v", err)
		return &Policy{values: map[string]any{}}
	}
	if values == nil {
		return &Policy{values: map[string]any{}}
	}
	if len(values) == 0 {
		log.Info("MDM enrolled (no managed keys)")
	} else {
		log.Infof("MDM enrolled with %d managed key(s): %v", len(values), sortedKeys(values))
	}
	return &Policy{values: values}
}

// IsEmpty reports whether the Policy has no managed keys.
func (p *Policy) IsEmpty() bool {
	return p == nil || len(p.values) == 0
}

// HasKey reports whether the given key is MDM-managed.
func (p *Policy) HasKey(key string) bool {
	if p == nil {
		return false
	}
	_, ok := p.values[key]
	return ok
}

// ManagedKeys returns the sorted list of managed key names. Returns an empty
// slice (not nil) on an empty Policy.
func (p *Policy) ManagedKeys() []string {
	if p == nil {
		return []string{}
	}
	return sortedKeys(p.values)
}

// GetString returns the managed value for key coerced to string, and whether
// the key was set. A non-string value returns ("", false).
func (p *Policy) GetString(key string) (string, bool) {
	if p == nil {
		return "", false
	}
	v, ok := p.values[key]
	if !ok {
		return "", false
	}
	s, ok := v.(string)
	if !ok || s == "" {
		return "", false
	}
	return s, true
}

// GetBool returns the managed value for key coerced to bool, and whether the
// key was set. Accepts native bool and string literals (true/false, 1/0,
// yes/no, on/off), case-insensitively and trimmed of surrounding whitespace.
func (p *Policy) GetBool(key string) (bool, bool) {
	if p == nil {
		return false, false
	}
	v, ok := p.values[key]
	if !ok {
		return false, false
	}
	switch t := v.(type) {
	case bool:
		return t, true
	case string:
		b, known := boolStringLiterals[strings.ToLower(strings.TrimSpace(t))]
		return b, known
	case int:
		return t != 0, true
	case int64:
		return t != 0, true
	}
	return false, false
}

// GetInt returns the managed value for key as int64, and whether the key
// was set. Accepts native int / int64 (as produced by the Windows registry
// loader for REG_DWORD/REG_QWORD) and numeric strings (decimal).
func (p *Policy) GetInt(key string) (int64, bool) {
	if p == nil {
		return 0, false
	}
	v, ok := p.values[key]
	if !ok {
		return 0, false
	}
	switch t := v.(type) {
	case int64:
		return t, true
	case int:
		return int64(t), true
	case int32:
		return int64(t), true
	case uint64:
		return int64(t), true
	case float64:
		return int64(t), true
	case string:
		if n, err := strconv.ParseInt(t, 10, 64); err == nil {
			return n, true
		}
	}
	return 0, false
}

// GetStringSlice returns the managed value for key as []string, and whether
// the key was set. Accepts []string, []any (of strings), and a single string
// (treated as a one-element list).
func (p *Policy) GetStringSlice(key string) ([]string, bool) {
	if p == nil {
		return nil, false
	}
	v, ok := p.values[key]
	if !ok {
		return nil, false
	}
	switch t := v.(type) {
	case []string:
		return append([]string(nil), t...), true
	case []any:
		out := make([]string, 0, len(t))
		for _, item := range t {
			s, ok := item.(string)
			if !ok {
				return nil, false
			}
			out = append(out, s)
		}
		return out, true
	case string:
		return []string{t}, true
	}
	return nil, false
}

// sortedKeys returns the keys of m as a deterministic, lexicographically
// sorted slice. Used internally by Policy.ManagedKeys and LoadPolicy's
// diagnostic log line so callers see a stable key order across runs
// regardless of Go's randomised map iteration.
func sortedKeys(m map[string]any) []string {
	out := make([]string, 0, len(m))
	for k := range m {
		out = append(out, k)
	}
	sort.Strings(out)
	return out
}
