//go:build darwin
// +build darwin

package platform

import (
	"crypto/rand"
	"fmt"
	"net/url"
	"path/filepath"
	"strings"

	"github.com/safedep/dry/log"
	"github.com/safedep/dry/utils"
	"github.com/safedep/pmg/sandbox"
	"github.com/safedep/pmg/sandbox/util"
)

type seatbeltPolicyTranslator struct {
	logTag                       string
	enableMoveBlockingMitigation bool
	enableDangerousFileBlocking  bool
}

// generateLogTag generates a unique log tag for tracking sandbox violations
func generateLogTag() string {
	// Go rand.Text() returns 26 bytes of random string
	randomStr := rand.Text()
	return fmt.Sprintf("PMG_SBX_%s", randomStr[:12])
}

// Shared vocabulary between rule emission (translator) and violation
// parsing (diagnostics); a drift between the two breaks message rendering.
const (
	seatbeltKindNetworkOutbound  = "network-outbound"
	seatbeltLockdownTargetDirect = "direct"
)

func seatbeltLogMessage(runID, kind, target string) string {
	return fmt.Sprintf("PMG_SBX|run=%s|kind=%s|target=%s", runID, kind, url.QueryEscape(target))
}

func newSeatbeltPolicyTranslator() *seatbeltPolicyTranslator {
	return &seatbeltPolicyTranslator{
		logTag:                      generateLogTag(),
		enableDangerousFileBlocking: true,

		// We will keep this disabled for now. There are some issues with npm
		// that needs investigation.
		enableMoveBlockingMitigation: false,
	}
}

// extractBaseDir extracts the base directory from a path that may contain glob patterns.
// For glob patterns, it returns the deepest directory path before any glob characters appear.
// Examples:
//   - "/path/to/**" -> "/path/to"
//   - "/path/to/*.txt" -> "/path/to"
//   - "/path/*/subdir" -> "/path"
//   - "/tmp/test[123].txt" -> "/tmp"
//   - "/path/to/file" -> "/path/to/file" (no glob, return as-is)
//   - "/*.txt" -> "/" (root directory)
func extractBaseDir(pattern string) string {
	if !util.ContainsGlob(pattern) {
		return pattern
	}

	// Split the path into components
	components := strings.Split(pattern, string(filepath.Separator))

	// Find the first component that contains a glob character
	baseComponents := []string{}
	for _, component := range components {
		if util.ContainsGlob(component) {
			// Stop before the component with glob
			break
		}
		baseComponents = append(baseComponents, component)
	}

	// Join the base components back together
	basePath := strings.Join(baseComponents, string(filepath.Separator))

	// Handle edge cases
	if basePath == "" {
		// Pattern started with glob (e.g., "*.txt" or if it's an absolute path like "/*.txt", basePath would be "")
		if strings.HasPrefix(pattern, string(filepath.Separator)) {
			// Absolute path starting with glob at root (e.g., "/*.txt")
			return string(filepath.Separator)
		}

		// Relative path starting with glob (e.g., "*.txt")
		return "."
	}

	return basePath
}

// getAncestorDirectories returns all ancestor directories for a path, up to (but not including) root.
// Example: /private/tmp/test/file.txt -> ["/private/tmp/test", "/private/tmp", "/private"]
func getAncestorDirectories(pathStr string) []string {
	ancestors := []string{}
	currentPath := filepath.Dir(pathStr)

	// Walk up the directory tree until we reach root
	for currentPath != string(filepath.Separator) && currentPath != "." {
		ancestors = append(ancestors, currentPath)
		parentPath := filepath.Dir(currentPath)
		// Break if we've reached the top (filepath.Dir returns the same path for root)
		if parentPath == currentPath {
			break
		}
		currentPath = parentPath
	}

	return ancestors
}

// globDoubleStarAutoAllowParentDirIfNeeded checks if a pattern ends with /** and emits a rule for the parent directory.
// This ensures operations like mkdir('dir') or stat('dir') succeed before accessing dir/** contents.
//
// When the parent itself still contains glob characters (e.g. ${CWD}/**/node_modules),
// a literal rule can never match a real path, so a regex rule is emitted instead.
// This grants access only to directories matching the parent pattern — strictly
// narrower than the Linux drivers, which bind the prefix before the first /**
// (Bubblewrap) or the glob's expansion/parent (Landlock) read-write. Deny rules
// are emitted after allows and override them, so mandatory credential denies
// are unaffected.
func globDoubleStarAutoAllowParentDirIfNeeded(sb *strings.Builder, pattern string, expanded string, operation string) {
	if !strings.HasSuffix(expanded, "/**") {
		return
	}

	parentDir := strings.TrimSuffix(expanded, "/**")
	// Handle edge case where /** results in empty string (use "/" instead)
	if parentDir == "" {
		parentDir = "/"
	}

	sb.WriteString(";; Auto-allow parent directory for ")
	sb.WriteString(pattern)
	sb.WriteString("\n")
	sb.WriteString("(allow ")
	sb.WriteString(operation)
	if util.ContainsGlob(parentDir) {
		sb.WriteString(" (regex #\"")
		sb.WriteString(util.GlobToRegex(parentDir))
		sb.WriteString("\"))\n")
		return
	}
	sb.WriteString(" (literal \"")
	sb.WriteString(parentDir)
	sb.WriteString("\"))\n")
}

func writeSeatbeltDenyRule(sb *strings.Builder, operation, matcher, value, message string) {
	sb.WriteString("(deny ")
	sb.WriteString(operation)
	sb.WriteString("* (")
	sb.WriteString(matcher)
	if matcher == "regex" {
		sb.WriteString(" #\"")
	} else {
		sb.WriteString(" \"")
	}
	sb.WriteString(value)
	sb.WriteString("\") (with message \"")
	sb.WriteString(message)
	sb.WriteString("\"))\n")
}

// generateMoveBlockingRules generates deny rules for file movement (file-write-unlink) to protect paths.
// This prevents bypassing read or write restrictions by moving files/directories.
//
// For each protected path pattern:
// - Blocks moving/renaming the path itself (via subpath or regex)
// - Blocks moving ancestor directories to prevent bypass
//
// Attack scenario this prevents:
//
//	Policy denies write to /sensitive/file
//	Attacker tries: mv /sensitive /tmp/renamed && echo "data" > /tmp/renamed/file && mv /tmp/renamed /sensitive
//	This blocks the initial "mv /sensitive" operation
func generateMoveBlockingRules(pathPatterns []string, logTag string) []string {
	rules := []string{}

	for _, pathPattern := range pathPatterns {
		if util.ContainsGlob(pathPattern) {
			// For glob patterns, use regex matching for precise pattern enforcement
			regexPattern := util.GlobToRegex(pathPattern)
			rules = append(rules, fmt.Sprintf("(deny file-write-unlink (regex #\"%s\") (with message \"%s\"))", regexPattern, seatbeltLogMessage(logTag, "file-write-unlink", pathPattern)))

			// Also block moving the base directory to prevent bypass
			baseDir := extractBaseDir(pathPattern)
			rules = append(rules, fmt.Sprintf("(deny file-write-unlink (subpath \"%s\") (with message \"%s\"))", baseDir, seatbeltLogMessage(logTag, "file-write-unlink", baseDir)))

			// Block moving ancestor directories
			for _, ancestorDir := range getAncestorDirectories(baseDir) {
				rules = append(rules, fmt.Sprintf("(deny file-write-unlink (literal \"%s\") (with message \"%s\"))", ancestorDir, seatbeltLogMessage(logTag, "file-write-unlink", ancestorDir)))
			}
		} else {
			// For literal paths, use subpath matching
			rules = append(rules, fmt.Sprintf("(deny file-write-unlink (subpath \"%s\") (with message \"%s\"))", pathPattern, seatbeltLogMessage(logTag, "file-write-unlink", pathPattern)))

			// Block moving ancestor directories
			for _, ancestorDir := range getAncestorDirectories(pathPattern) {
				rules = append(rules, fmt.Sprintf("(deny file-write-unlink (literal \"%s\") (with message \"%s\"))", ancestorDir, seatbeltLogMessage(logTag, "file-write-unlink", ancestorDir)))
			}
		}
	}

	return rules
}

func (t *seatbeltPolicyTranslator) LogTag() string {
	return t.logTag
}

func (t *seatbeltPolicyTranslator) translate(policy *sandbox.SandboxPolicy, rt *sandbox.ExecutionContext) (string, error) {
	var sb strings.Builder

	// Header
	sb.WriteString("(version 1)\n")
	sb.WriteString(fmt.Sprintf(";; PMG Sandbox Policy: %s\n", policy.Name))
	sb.WriteString(fmt.Sprintf(";; %s\n", policy.Description))
	sb.WriteString(";; Generated by PMG sandbox system\n\n")

	// Default policy: deny by default for maximum security
	// Add log tag to track what gets denied by the default rule
	sb.WriteString(fmt.Sprintf("(deny default (with message \"%s\"))\n\n", seatbeltLogMessage(t.logTag, "default", "")))

	// Essential system permissions - based on Chrome/Chromium sandbox policy
	// These are the minimum permissions needed for stable process execution
	sb.WriteString(";; Essential system permissions\n")
	sb.WriteString(";; Based on Chrome/Chromium sandbox for stable process execution\n\n")

	// Process permissions
	sb.WriteString(";; Process permissions\n")
	sb.WriteString("(allow process-exec)\n")
	sb.WriteString("(allow process-fork)\n")
	sb.WriteString("(allow process-info* (target same-sandbox))\n")
	sb.WriteString("(allow signal (target same-sandbox))\n")
	sb.WriteString("(allow mach-priv-task-port (target same-sandbox))\n\n")

	// User preferences
	sb.WriteString(";; User preferences\n")
	sb.WriteString("(allow user-preference-read)\n\n")

	// Mach IPC - specific services only (no wildcard for security)
	sb.WriteString(";; Mach IPC - specific services only\n")
	sb.WriteString("(allow mach-lookup\n")
	sb.WriteString("  (global-name \"com.apple.audio.systemsoundserver\")\n")
	sb.WriteString("  (global-name \"com.apple.distributed_notifications@Uv3\")\n")
	sb.WriteString("  (global-name \"com.apple.FSEvents\")\n")
	sb.WriteString("  (global-name \"com.apple.FontObjectsServer\")\n")
	sb.WriteString("  (global-name \"com.apple.fonts\")\n")
	sb.WriteString("  (global-name \"com.apple.logd\")\n")
	sb.WriteString("  (global-name \"com.apple.lsd.mapdb\")\n")
	sb.WriteString("  (global-name \"com.apple.PowerManagement.control\")\n")
	sb.WriteString("  (global-name \"com.apple.system.logger\")\n")
	sb.WriteString("  (global-name \"com.apple.system.notification_center\")\n")
	sb.WriteString("  (global-name \"com.apple.trustd.agent\")\n")
	sb.WriteString("  (global-name \"com.apple.system.opendirectoryd.libinfo\")\n")
	sb.WriteString("  (global-name \"com.apple.system.opendirectoryd.membership\")\n")
	sb.WriteString("  (global-name \"com.apple.bsd.dirhelper\")\n")
	sb.WriteString("  (global-name \"com.apple.securityd.xpc\")\n")
	sb.WriteString("  (global-name \"com.apple.coreservices.launchservicesd\")\n")
	sb.WriteString(")\n\n")

	// POSIX IPC
	sb.WriteString(";; POSIX IPC\n")
	sb.WriteString("(allow ipc-posix-shm)  ; Shared memory\n")
	sb.WriteString("(allow ipc-posix-sem)  ; Semaphores for Python multiprocessing\n\n")

	// IOKit operations
	sb.WriteString(";; IOKit operations\n")
	sb.WriteString("(allow iokit-open\n")
	sb.WriteString("  (iokit-registry-entry-class \"IOSurfaceRootUserClient\")\n")
	sb.WriteString("  (iokit-registry-entry-class \"RootDomainUserClient\")\n")
	sb.WriteString("  (iokit-user-client-class \"IOSurfaceSendRight\")\n")
	sb.WriteString(")\n")
	sb.WriteString("(allow iokit-get-properties)\n\n")

	// Specific safe system socket
	sb.WriteString(";; Specific safe system socket\n")
	sb.WriteString("(allow system-socket (require-all (socket-domain AF_SYSTEM) (socket-protocol 2)))\n\n")

	// sysctl - specific sysctls only
	sb.WriteString(";; sysctl - specific sysctls only\n")
	sb.WriteString("(allow sysctl-read\n")
	// Hardware info
	sb.WriteString("  (sysctl-name \"hw.activecpu\")\n")
	sb.WriteString("  (sysctl-name \"hw.busfrequency_compat\")\n")
	sb.WriteString("  (sysctl-name \"hw.byteorder\")\n")
	sb.WriteString("  (sysctl-name \"hw.cacheconfig\")\n")
	sb.WriteString("  (sysctl-name \"hw.cachelinesize_compat\")\n")
	sb.WriteString("  (sysctl-name \"hw.cpufamily\")\n")
	sb.WriteString("  (sysctl-name \"hw.cpufrequency\")\n")
	sb.WriteString("  (sysctl-name \"hw.cpufrequency_compat\")\n")
	sb.WriteString("  (sysctl-name \"hw.cputype\")\n")
	sb.WriteString("  (sysctl-name \"hw.l1dcachesize_compat\")\n")
	sb.WriteString("  (sysctl-name \"hw.l1icachesize_compat\")\n")
	sb.WriteString("  (sysctl-name \"hw.l2cachesize_compat\")\n")
	sb.WriteString("  (sysctl-name \"hw.l3cachesize_compat\")\n")
	sb.WriteString("  (sysctl-name \"hw.logicalcpu\")\n")
	sb.WriteString("  (sysctl-name \"hw.logicalcpu_max\")\n")
	sb.WriteString("  (sysctl-name \"hw.machine\")\n")
	sb.WriteString("  (sysctl-name \"hw.memsize\")\n")
	sb.WriteString("  (sysctl-name \"hw.ncpu\")\n")
	sb.WriteString("  (sysctl-name \"hw.nperflevels\")\n")
	sb.WriteString("  (sysctl-name \"hw.packages\")\n")
	sb.WriteString("  (sysctl-name \"hw.pagesize_compat\")\n")
	sb.WriteString("  (sysctl-name \"hw.pagesize\")\n")
	sb.WriteString("  (sysctl-name \"hw.physicalcpu\")\n")
	sb.WriteString("  (sysctl-name \"hw.physicalcpu_max\")\n")
	sb.WriteString("  (sysctl-name \"hw.tbfrequency_compat\")\n")
	sb.WriteString("  (sysctl-name \"hw.vectorunit\")\n")
	// Kernel info
	sb.WriteString("  (sysctl-name \"kern.argmax\")\n")
	sb.WriteString("  (sysctl-name \"kern.bootargs\")\n")
	sb.WriteString("  (sysctl-name \"kern.hostname\")\n")
	sb.WriteString("  (sysctl-name \"kern.maxfiles\")\n")
	sb.WriteString("  (sysctl-name \"kern.maxfilesperproc\")\n")
	sb.WriteString("  (sysctl-name \"kern.maxproc\")\n")
	sb.WriteString("  (sysctl-name \"kern.ngroups\")\n")
	sb.WriteString("  (sysctl-name \"kern.osproductversion\")\n")
	sb.WriteString("  (sysctl-name \"kern.osrelease\")\n")
	sb.WriteString("  (sysctl-name \"kern.ostype\")\n")
	sb.WriteString("  (sysctl-name \"kern.osvariant_status\")\n")
	sb.WriteString("  (sysctl-name \"kern.osversion\")\n")
	sb.WriteString("  (sysctl-name \"kern.secure_kernel\")\n")
	sb.WriteString("  (sysctl-name \"kern.tcsm_available\")\n")
	sb.WriteString("  (sysctl-name \"kern.tcsm_enable\")\n")
	sb.WriteString("  (sysctl-name \"kern.usrstack64\")\n")
	sb.WriteString("  (sysctl-name \"kern.version\")\n")
	sb.WriteString("  (sysctl-name \"kern.willshutdown\")\n")
	// machdep info
	sb.WriteString("  (sysctl-name \"machdep.cpu.brand_string\")\n")
	sb.WriteString("  (sysctl-name \"machdep.ptrauth_enabled\")\n")
	// Security info
	sb.WriteString("  (sysctl-name \"security.mac.lockdown_mode_state\")\n")
	// Other
	sb.WriteString("  (sysctl-name \"sysctl.proc_cputype\")\n")
	sb.WriteString("  (sysctl-name \"vm.loadavg\")\n")
	// Prefixes for more sysctls
	sb.WriteString("  (sysctl-name-prefix \"hw.optional.arm\")\n")
	sb.WriteString("  (sysctl-name-prefix \"hw.optional.arm.\")\n")
	sb.WriteString("  (sysctl-name-prefix \"hw.optional.armv8_\")\n")
	sb.WriteString("  (sysctl-name-prefix \"hw.perflevel\")\n")
	sb.WriteString("  (sysctl-name-prefix \"kern.proc.all\")\n")
	sb.WriteString("  (sysctl-name-prefix \"kern.proc.pgrp.\")\n")
	sb.WriteString("  (sysctl-name-prefix \"kern.proc.pid.\")\n")
	sb.WriteString("  (sysctl-name-prefix \"machdep.cpu.\")\n")
	sb.WriteString("  (sysctl-name-prefix \"net.routetable.\")\n")
	sb.WriteString(")\n\n")

	// V8 thread calculations
	sb.WriteString(";; V8 thread calculations\n")
	sb.WriteString("(allow sysctl-write\n")
	sb.WriteString("  (sysctl-name \"kern.tcsm_enable\")\n")
	sb.WriteString(")\n\n")

	// Distributed notifications
	sb.WriteString(";; Distributed notifications\n")
	sb.WriteString("(allow distributed-notification-post)\n\n")

	// Specific mach-lookup for security
	sb.WriteString(";; Specific mach-lookup for security\n")
	sb.WriteString("(allow mach-lookup (global-name \"com.apple.SecurityServer\"))\n\n")

	// Device file I/O
	sb.WriteString(";; Device file I/O\n")
	sb.WriteString("(allow file-ioctl (literal \"/dev/null\"))\n")
	sb.WriteString("(allow file-ioctl (literal \"/dev/zero\"))\n")
	sb.WriteString("(allow file-ioctl (literal \"/dev/random\"))\n")
	sb.WriteString("(allow file-ioctl (literal \"/dev/urandom\"))\n")
	sb.WriteString("(allow file-ioctl (literal \"/dev/dtracehelper\"))\n")
	sb.WriteString("(allow file-ioctl (literal \"/dev/tty\"))\n\n")

	sb.WriteString("(allow file-ioctl file-read-data file-write-data\n")
	sb.WriteString("  (require-all\n")
	sb.WriteString("    (literal \"/dev/null\")\n")
	sb.WriteString("    (vnode-type CHARACTER-DEVICE)\n")
	sb.WriteString("  )\n")
	sb.WriteString(")\n\n")

	// File metadata
	sb.WriteString(";; File metadata for getcwd() and similar\n")
	sb.WriteString("(allow file-read-metadata)\n\n")

	// System configuration and libraries
	sb.WriteString(";; System configuration and libraries\n")
	sb.WriteString("(allow file-read* (subpath \"/dev\"))\n")
	sb.WriteString("(allow file-read* (subpath \"/etc\"))\n\n")

	// Filesystem rules
	if err := t.translateFilesystem(policy, &sb); err != nil {
		return "", fmt.Errorf("failed to translate filesystem rules: %w", err)
	}

	// Network rules
	if err := t.translateNetwork(policy, rt, &sb); err != nil {
		return "", fmt.Errorf("failed to translate network rules: %w", err)
	}

	// Process execution rules
	if err := t.translateProcess(policy, &sb); err != nil {
		return "", fmt.Errorf("failed to translate process rules: %w", err)
	}

	// PTY support (optional)
	if utils.SafelyGetValue(policy.AllowPTY) {
		sb.WriteString(";; Pseudo-terminal (PTY) support\n")
		sb.WriteString("(allow pseudo-tty)\n")
		sb.WriteString("(allow file-ioctl\n")
		sb.WriteString("  (literal \"/dev/ptmx\")\n")
		sb.WriteString("  (regex #\"^/dev/ttys\")\n")
		sb.WriteString(")\n")
		sb.WriteString("(allow file-read* file-write*\n")
		sb.WriteString("  (literal \"/dev/ptmx\")\n")
		sb.WriteString("  (regex #\"^/dev/ttys\")\n")
		sb.WriteString(")\n\n")
	}

	return sb.String(), nil
}

// translateFilesystem translates filesystem access rules.
func (t *seatbeltPolicyTranslator) translateFilesystem(policy *sandbox.SandboxPolicy, sb *strings.Builder) error {
	sb.WriteString(";; Filesystem access\n")

	// Expand and add allow read rules
	for _, pattern := range policy.Filesystem.AllowRead {
		expanded, err := util.ExpandVariables(pattern)
		if err != nil {
			return fmt.Errorf("failed to expand pattern %s: %w", pattern, err)
		}

		// Use regex matching for glob patterns, subpath for literals
		if util.ContainsGlob(expanded) {
			globDoubleStarAutoAllowParentDirIfNeeded(sb, pattern, expanded, "file-read*")

			regexPattern := util.GlobToRegex(expanded)
			sb.WriteString("(allow file-read* (regex #\"")
			sb.WriteString(regexPattern)
			sb.WriteString("\"))\n")
		} else {
			sb.WriteString("(allow file-read* (subpath \"")
			sb.WriteString(expanded)
			sb.WriteString("\"))\n")
		}
	}

	sb.WriteString("\n")

	// Auto-allow TMPDIR parent on macOS when write restrictions are enabled
	// This is necessary because package managers need temp file access
	hasWriteRestrictions := len(policy.Filesystem.AllowWrite) > 0
	if hasWriteRestrictions {
		tmpdirParents := util.GetTmpdirParent()
		if len(tmpdirParents) > 0 {
			sb.WriteString(";; Auto-allow TMPDIR parent on macOS\n")
			for _, parent := range tmpdirParents {
				sb.WriteString("(allow file-write* (subpath \"")
				sb.WriteString(parent)
				sb.WriteString("\"))\n")
			}
			sb.WriteString("\n")
		}
	}

	// Expand and add allow write rules
	for _, pattern := range policy.Filesystem.AllowWrite {
		expanded, err := util.ExpandVariables(pattern)
		if err != nil {
			return fmt.Errorf("failed to expand pattern %s: %w", pattern, err)
		}

		// Use regex matching for glob patterns, subpath for literals
		if util.ContainsGlob(expanded) {
			globDoubleStarAutoAllowParentDirIfNeeded(sb, pattern, expanded, "file-write*")

			regexPattern := util.GlobToRegex(expanded)
			sb.WriteString("(allow file-write* (regex #\"")
			sb.WriteString(regexPattern)
			sb.WriteString("\"))\n")
		} else {
			sb.WriteString("(allow file-write* (subpath \"")
			sb.WriteString(expanded)
			sb.WriteString("\"))\n")
		}
	}

	sb.WriteString("\n")

	// Deny rules have higher priority (applied after allow)
	// Note: Seatbelt evaluates rules in order, so denies after allows will override
	expandedDenyRead := []string{}
	for _, pattern := range policy.Filesystem.DenyRead {
		expanded, err := util.ExpandVariables(pattern)
		if err != nil {
			return fmt.Errorf("failed to expand pattern %s: %w", pattern, err)
		}

		// Use regex matching for glob patterns, subpath for literals
		if util.ContainsGlob(expanded) {
			regexPattern := util.GlobToRegex(expanded)
			writeSeatbeltDenyRule(sb, "file-read", "regex", regexPattern, seatbeltLogMessage(t.logTag, "file-read", expanded))
		} else {
			writeSeatbeltDenyRule(sb, "file-read", "subpath", expanded, seatbeltLogMessage(t.logTag, "file-read", expanded))
		}
		expandedDenyRead = append(expandedDenyRead, expanded)
	}

	// Add file movement protection for deny read paths
	if t.enableMoveBlockingMitigation && (len(expandedDenyRead) > 0) {
		sb.WriteString("\n;; Prevent bypassing read restrictions via file movement\n")
		for _, rule := range generateMoveBlockingRules(expandedDenyRead, t.logTag) {
			sb.WriteString(rule + "\n")
		}
	}

	sb.WriteString("\n")

	expandedDenyWrite := []string{}
	for _, pattern := range policy.Filesystem.DenyWrite {
		expanded, err := util.ExpandVariables(pattern)
		if err != nil {
			return fmt.Errorf("failed to expand pattern %s: %w", pattern, err)
		}

		// Use regex matching for glob patterns, subpath for literals
		if util.ContainsGlob(expanded) {
			regexPattern := util.GlobToRegex(expanded)
			writeSeatbeltDenyRule(sb, "file-write", "regex", regexPattern, seatbeltLogMessage(t.logTag, "file-write", expanded))
		} else {
			writeSeatbeltDenyRule(sb, "file-write", "subpath", expanded, seatbeltLogMessage(t.logTag, "file-write", expanded))
		}
		expandedDenyWrite = append(expandedDenyWrite, expanded)
	}

	sb.WriteString("\n")

	if t.enableDangerousFileBlocking {
		sb.WriteString(";; Mandatory security denies (credentials, git hooks, etc.)\n")
		expandedAllowRead, err := expandAll(policy.Filesystem.AllowRead)
		if err != nil {
			log.Warnf("sandbox: failed to expand allow_read for mandatory deny suppression, all mandatory denies preserved: %v", err)
			expandedAllowRead = nil
		}
		expandedAllowWrite, err := expandAll(policy.Filesystem.AllowWrite)
		if err != nil {
			log.Warnf("sandbox: failed to expand allow_write for mandatory deny suppression, all mandatory denies preserved: %v", err)
			expandedAllowWrite = nil
		}

		mandatoryResult := util.GetMandatoryDenyPatterns(util.MandatoryDenyOptions{
			AllowGitConfig: utils.SafelyGetValue(policy.AllowGitConfig),
			AllowRead:      expandedAllowRead,
			AllowWrite:     expandedAllowWrite,
		})

		for _, p := range mandatoryResult.SuppressedRead {
			log.Warnf("sandbox: mandatory deny %q suppressed for read by explicit allow rule in policy %q", p, policy.Name)
		}
		for _, p := range mandatoryResult.SuppressedWrite {
			log.Warnf("sandbox: mandatory deny %q suppressed for write by explicit allow rule in policy %q", p, policy.Name)
		}

		for _, pattern := range mandatoryResult.DenyWrite {
			expanded, err := util.ExpandVariables(pattern)
			if err != nil {
				return fmt.Errorf("failed to expand mandatory deny pattern %s: %w", pattern, err)
			}

			if util.ContainsGlob(expanded) {
				regexPattern := util.GlobToRegex(expanded)
				writeSeatbeltDenyRule(sb, "file-write", "regex", regexPattern, seatbeltLogMessage(t.logTag, "file-write", expanded))
			} else {
				writeSeatbeltDenyRule(sb, "file-write", "subpath", expanded, seatbeltLogMessage(t.logTag, "file-write", expanded))
			}
			expandedDenyWrite = append(expandedDenyWrite, expanded)
		}

		for _, pattern := range mandatoryResult.DenyRead {
			expanded, err := util.ExpandVariables(pattern)
			if err != nil {
				return fmt.Errorf("failed to expand mandatory deny pattern %s: %w", pattern, err)
			}

			if util.ContainsGlob(expanded) {
				regexPattern := util.GlobToRegex(expanded)
				writeSeatbeltDenyRule(sb, "file-read", "regex", regexPattern, seatbeltLogMessage(t.logTag, "file-read", expanded))
			} else {
				writeSeatbeltDenyRule(sb, "file-read", "subpath", expanded, seatbeltLogMessage(t.logTag, "file-read", expanded))
			}
		}

		sb.WriteString("\n")
	}

	// Add file movement protection for all deny write paths (user + mandatory)
	if t.enableMoveBlockingMitigation && (len(expandedDenyWrite) > 0) {
		sb.WriteString(";; Prevent bypassing write restrictions via file movement\n")
		for _, rule := range generateMoveBlockingRules(expandedDenyWrite, t.logTag) {
			sb.WriteString(rule + "\n")
		}
	}

	sb.WriteString("\n")

	return nil
}

// translateNetwork translates network access rules. SBPL is last-match-wins,
// so rule ordering is normative: the lockdown broad deny first, specific
// allows after, and the AllowNetworkBind rules last so loopback traffic
// keeps working under lockdown.
func (t *seatbeltPolicyTranslator) translateNetwork(policy *sandbox.SandboxPolicy, rt *sandbox.ExecutionContext, sb *strings.Builder) error {
	sb.WriteString(";; Network access\n")

	if utils.SafelyGetValue(policy.NetworkViaProxyOnly) {
		sb.WriteString(";; network_via_proxy_only: all outbound confined to the PMG proxy\n")
		sb.WriteString("(deny network-outbound (with message \"")
		sb.WriteString(seatbeltLogMessage(t.logTag, seatbeltKindNetworkOutbound, seatbeltLockdownTargetDirect))
		sb.WriteString("\"))\n")

		// Without a running proxy (render/inspection, e.g. `pmg sandbox
		// profile show`) the profile stays fail-closed: the deny stands and
		// the runtime-only allow is documented instead of emitted with a
		// fabricated port.
		if rt == nil || rt.ProxyAddr == "" {
			sb.WriteString(";; Rendered without a running PMG proxy. At runtime an\n")
			sb.WriteString(";; (allow network-outbound (remote ip \"localhost:<pmg-proxy-port>\"))\n")
			sb.WriteString(";; rule permits traffic to the PMG proxy only.\n")
		} else {
			port, err := sandbox.ValidateNetworkLockdown(policy, rt)
			if err != nil {
				return err
			}

			sb.WriteString("(allow network-outbound (remote ip \"localhost:")
			sb.WriteString(port)
			sb.WriteString("\"))\n")
		}

		// Bind rules cannot cover outbound connects (the local address is
		// unbound at connect() time), so loopback outbound is re-opened
		// explicitly for bind-enabled profiles.
		if utils.SafelyGetValue(policy.AllowNetworkBind) {
			sb.WriteString("(allow network-outbound (remote ip \"localhost:*\"))\n")
		}

		// getaddrinfo resolves via the com.apple.dnssd.service XPC endpoint,
		// legacy clients use the mDNSResponder socket (both /var and resolved
		// /private/var paths — Seatbelt matches resolved paths), and
		// self-resolving clients need direct port 53.
		if utils.SafelyGetValue(policy.AllowDirectDNS) {
			sb.WriteString("(allow mach-lookup (global-name \"com.apple.dnssd.service\"))\n")
			sb.WriteString("(allow network-outbound (remote unix-socket (path-literal \"/var/run/mDNSResponder\")))\n")
			sb.WriteString("(allow network-outbound (remote unix-socket (path-literal \"/private/var/run/mDNSResponder\")))\n")
			sb.WriteString("(allow network-outbound (remote ip \"*:53\"))\n")
		}

		sb.WriteString("\n")
	} else {
		// Check if deny all is present
		denyAll := false
		for _, pattern := range policy.Network.DenyOutbound {
			if pattern == "*:*" {
				denyAll = true
				break
			}
		}

		// If there are allow outbound rules, allow network-outbound generally
		// (Seatbelt doesn't support fine-grained host:port filtering in all cases)
		// Note: This is a limitation of Seatbelt - for more fine-grained control,
		// consider using a network filtering solution or firewall rules
		if len(policy.Network.AllowOutbound) > 0 {
			sb.WriteString(";; Network outbound allowed to specific hosts\n")
			sb.WriteString(";; Note: Seatbelt has limited host-based filtering, consider using firewall rules for strict control\n")
			sb.WriteString("(allow network-outbound)\n")
		} else if denyAll {
			// If there are no allow rules but deny all is set, explicitly deny network
			// This handles the case where user wants to completely block network access
			sb.WriteString(";; Network outbound denied (no allowed hosts specified)\n")
			sb.WriteString("(deny network-outbound)\n")
		}

		// Note: We don't add an explicit deny rule when both allow and deny_all are present
		// because the default (deny default) at the top of the profile handles blocking
		// everything that isn't explicitly allowed. Adding an explicit deny here would
		// override the allow rule above, breaking network access entirely.

		sb.WriteString("\n")
	}

	// Network bind rules for local listening
	if utils.SafelyGetValue(policy.AllowNetworkBind) {
		sb.WriteString(";; Local network bind (localhost only)\n")
		sb.WriteString("(allow network-bind (local ip \"localhost:*\"))\n")
		sb.WriteString("(allow network* (local ip \"localhost:*\"))\n")
	}

	for _, addr := range policy.Network.AllowBind {
		sb.WriteString("(allow network-bind (local ip \"")
		sb.WriteString(addr)
		sb.WriteString("\"))\n")
		sb.WriteString("(allow network* (local ip \"")
		sb.WriteString(addr)
		sb.WriteString("\"))\n")
	}

	if utils.SafelyGetValue(policy.AllowNetworkBind) || len(policy.Network.AllowBind) > 0 {
		sb.WriteString("\n")
	}

	return nil
}

// translateProcess translates process execution rules.
func (t *seatbeltPolicyTranslator) translateProcess(policy *sandbox.SandboxPolicy, sb *strings.Builder) error {
	sb.WriteString(";; Process execution\n")

	// Add allow exec rules
	for _, exePath := range policy.Process.AllowExec {
		expanded, err := util.ExpandVariables(exePath)
		if err != nil {
			return fmt.Errorf("failed to expand exec path %s: %w", exePath, err)
		}

		if util.ContainsGlob(expanded) {
			// For glob patterns, use regex matching for precise control
			regexPattern := util.GlobToRegex(expanded)
			sb.WriteString("(allow process-exec* (regex #\"")
			sb.WriteString(regexPattern)
			sb.WriteString("\"))\n")
		} else {
			sb.WriteString("(allow process-exec* (literal \"")
			sb.WriteString(expanded)
			sb.WriteString("\"))\n")
		}
	}

	sb.WriteString("\n")

	// Add deny exec rules
	for _, exePath := range policy.Process.DenyExec {
		expanded, err := util.ExpandVariables(exePath)
		if err != nil {
			return fmt.Errorf("failed to expand exec path %s: %w", exePath, err)
		}

		if util.ContainsGlob(expanded) {
			// For glob patterns, use regex matching for precise control
			regexPattern := util.GlobToRegex(expanded)
			writeSeatbeltDenyRule(sb, "process-exec", "regex", regexPattern, seatbeltLogMessage(t.logTag, "process-exec", expanded))
		} else {
			writeSeatbeltDenyRule(sb, "process-exec", "literal", expanded, seatbeltLogMessage(t.logTag, "process-exec", expanded))
		}
	}

	sb.WriteString("\n")

	return nil
}

func expandAll(patterns []string) ([]string, error) {
	out := make([]string, 0, len(patterns))
	for _, p := range patterns {
		expanded, err := util.ExpandVariables(p)
		if err != nil {
			return nil, fmt.Errorf("failed to expand pattern %q: %w", p, err)
		}
		out = append(out, expanded)
	}
	return out, nil
}
