//go:build linux
// +build linux

package platform

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

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

// bubblewrapPolicyTranslator translates PMG SandboxPolicy to Bubblewrap (bwrap) CLI arguments.
//
// Bubblewrap uses command-line arguments instead of profile files (like Seatbelt).
// The translator generates arguments for:
// - Filesystem bind mounts (--bind, --ro-bind, --dev-bind)
// - Network isolation (--unshare-net)
// - Process isolation (--unshare-pid, --unshare-ipc)
// - Device access (--dev-bind /dev/null, etc.)
// - Essential system permissions
type bubblewrapPolicyTranslator struct {
	config *bubblewrapConfig
}

// newBubblewrapPolicyTranslator creates a new translator with the given config.
func newBubblewrapPolicyTranslator(config *bubblewrapConfig) *bubblewrapPolicyTranslator {
	return &bubblewrapPolicyTranslator{
		config: config,
	}
}

// translate converts a PMG SandboxPolicy to bwrap CLI arguments.
// Returns a slice of arguments to pass to the bwrap command.
func (t *bubblewrapPolicyTranslator) translate(policy *sandbox.SandboxPolicy) ([]string, error) {
	args := []string{}

	// 1. Add essential system permissions (filesystem, devices, proc)
	systemArgs, err := t.addEssentialSystemPermissions()
	if err != nil {
		return nil, fmt.Errorf("failed to add essential system permissions: %w", err)
	}

	args = append(args, systemArgs...)

	// 2. Add isolation namespaces
	isolationArgs := t.addIsolationNamespaces(policy)
	args = append(args, isolationArgs...)

	// 3. Add filesystem rules (allow read, allow write, deny patterns)
	filesystemArgs, err := t.translateFilesystem(policy)
	if err != nil {
		return nil, fmt.Errorf("failed to translate filesystem rules: %w", err)
	}

	args = append(args, filesystemArgs...)

	// 4. Add PTY support if needed
	if utils.SafelyGetValue(policy.AllowPTY) {
		ptyArgs := t.addPTYSupport()
		args = append(args, ptyArgs...)
	}

	// 5. Add tmpdir support (package managers need writable temp directory)
	tmpdirArgs := t.addTmpdirSupport()
	args = append(args, tmpdirArgs...)

	// 6. Check total argument limit and log warning if exceeded
	// Do not fail, let bwrap fail naturally if it does.
	if len(args) > t.config.totalArgsLimit {
		log.Warnf("Total bwrap arguments (%d) exceeds safety limit (%d), sandbox may fail with 'Argument list too long' error",
			len(args), t.config.totalArgsLimit)
	}

	log.Debugf("Translated policy '%s' to %d bwrap arguments (limit: %d)", policy.Name, len(args), t.config.totalArgsLimit)

	return args, nil
}

// addEssentialSystemPermissions adds bind mounts for essential system paths and devices
// that package managers need to function properly.
func (t *bubblewrapPolicyTranslator) addEssentialSystemPermissions() ([]string, error) {
	args := []string{}

	// Add essential system paths (read-only)
	for _, path := range t.config.getEssentialSystemPaths() {
		args = append(args, "--ro-bind-try", path, path)
	}

	// Add essential device files
	for _, device := range t.config.getEssentialDevices() {
		args = append(args, "--dev-bind-try", device, device)
	}

	// Add proc filesystem (read-only for safety)
	for _, procPath := range t.config.procPaths {
		args = append(args, "--proc", procPath)
	}

	return args, nil
}

// addIsolationNamespaces adds namespace isolation arguments based on policy and config.
func (t *bubblewrapPolicyTranslator) addIsolationNamespaces(policy *sandbox.SandboxPolicy) []string {
	args := []string{}

	// Network isolation
	hasAllowRules := len(policy.Network.AllowOutbound) > 0
	hasDenyAll := false
	for _, pattern := range policy.Network.DenyOutbound {
		if pattern == "*:*" {
			hasDenyAll = true
			break
		}
	}

	if t.config.shouldUnshareNetwork(hasAllowRules, hasDenyAll) {
		args = append(args, "--unshare-net")
		log.Debugf("Network isolated (--unshare-net)")
	} else {
		log.Debugf("Network allowed (no --unshare-net)")
	}

	// Note: AllowNetworkBind and Network.AllowBind are not handled here because
	// bwrap's --unshare-net creates a namespace with loopback available, so
	// localhost binding already works. Non-localhost binding requires full host
	// network (no --unshare-net), which is controlled by AllowOutbound rules.

	// PID namespace isolation
	if t.config.unsharePID {
		args = append(args, "--unshare-pid")
	}

	// IPC namespace isolation
	if t.config.unshareIPC {
		args = append(args, "--unshare-ipc")
	}

	// New session
	if t.config.newSession {
		args = append(args, "--new-session")
	}

	// Die with parent
	if t.config.dieWithParent {
		args = append(args, "--die-with-parent")
	}

	return args
}

// translateFilesystem converts filesystem policy rules to bwrap bind mount arguments.
//
// Bubblewrap filesystem isolation works via bind mounts:
// - --ro-bind: Read-only bind mount
// - --bind: Read-write bind mount
// - --dev-bind: Device file bind mount
// - --tmpfs: Temporary file system mount (used to hide specific files/directories)
// - Paths not mounted are inaccessible (deny-by-default)
//
// Strategy:
//  1. Start with essential system paths (added separately)
//  2. Add user-specified allow_read paths FIRST (read-only bind mounts)
//     This establishes the base filesystem view (e.g., "/" for full access)
//  3. Add user-specified allow_write paths SECOND (read-write bind mounts)
//     These OVERRIDE earlier read-only binds (bwrap: later mounts win)
//  4. Handle deny patterns by mounting /dev/null or read-only for directories
//  5. Add mandatory deny patterns
func (t *bubblewrapPolicyTranslator) translateFilesystem(policy *sandbox.SandboxPolicy) ([]string, error) {
	args := []string{}

	// Track paths we've already bound for read and write to avoid duplicates
	// Bubblewrap later mounts win, so we need to track both read and write bound paths.
	readBoundPaths := make(map[string]bool)
	writeBoundPaths := make(map[string]bool)

	// Add essential system paths to bound paths (already handled separately)
	for _, path := range t.config.getEssentialSystemPaths() {
		readBoundPaths[path] = true
	}

	// Mark tmpdir as already bound (will be handled by addTmpdirSupport())
	// This prevents conflicts from policy patterns like /tmp/**
	tmpDir := os.TempDir()
	writeBoundPaths[tmpDir] = true

	// 1. Process allow_read rules FIRST (read-only bind mounts)
	// This establishes the base read-only filesystem view (including "/" if specified)
	for _, pattern := range policy.Filesystem.AllowRead {
		expanded, err := util.ExpandVariables(pattern)
		if err != nil {
			log.Warnf("Failed to expand variables in allow_read pattern '%s': %v", pattern, err)
			continue
		}

		// Glob chars are handled by the processReadRule function.
		readArgs, err := t.processReadRule(expanded, readBoundPaths)
		if err != nil {
			log.Warnf("Failed to process allow_read rule '%s': %v", expanded, err)
			continue
		}

		args = append(args, readArgs...)
	}

	// 2. Process allow_write rules SECOND (read-write bind mounts)
	// These OVERRIDE earlier read-only binds (bwrap: later mounts win)
	// Use a separate map so we don't skip paths that need write access
	writeBoundPaths[tmpDir] = true // tmpdir handled by addTmpdirSupport
	for _, pattern := range policy.Filesystem.AllowWrite {
		expanded, err := util.ExpandVariables(pattern)
		if err != nil {
			log.Warnf("Failed to expand variables in allow_write pattern '%s': %v", pattern, err)
			continue
		}

		// Glob chars are handled by the processWriteRule function.
		writeArgs, err := t.processWriteRule(expanded, writeBoundPaths)
		if err != nil {
			log.Warnf("Failed to process allow_write rule '%s': %v", expanded, err)
			continue
		}

		args = append(args, writeArgs...)
	}

	// 3. Process deny_write rules after allow_write so read-only binds override writable parents.
	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)
	}

	// bwrap has no primitive that denies reads while allowing writes — --bind
	// exposes both, and read-blocking mounts (--tmpfs, --ro-bind /dev/null)
	// also block writes. When the user opts out of write but not read for a
	// mandatory path, the read-side deny is unenforceable; warn so it's not
	// silent.
	suppressedWriteSet := make(map[string]bool, len(mandatoryResult.SuppressedWrite))
	for _, p := range mandatoryResult.SuppressedWrite {
		suppressedWriteSet[p] = true
	}
	for _, p := range mandatoryResult.DenyRead {
		if suppressedWriteSet[p] {
			log.Warnf("sandbox: read-side mandatory deny %q cannot be enforced on linux because allow_write for the same path exposes both read and write; consider also listing the path in allow_read if read access is intended, or remove from allow_write if not", p)
		}
	}

	for _, pattern := range policy.Filesystem.DenyRead {
		expanded, err := util.ExpandVariables(pattern)
		if err != nil {
			log.Warnf("Failed to expand variables in deny_read pattern '%s': %v", pattern, err)
			continue
		}

		denyArgs, err := t.processDenyReadRule(expanded)
		if err != nil {
			log.Debugf("Deny read rule '%s' skipped: %v", expanded, err)
			continue
		}

		args = append(args, denyArgs...)
	}

	for _, pattern := range policy.Filesystem.DenyWrite {
		expanded, err := util.ExpandVariables(pattern)
		if err != nil {
			log.Warnf("Failed to expand variables in deny pattern '%s': %v", pattern, err)
			continue
		}

		denyArgs, err := t.processDenyWriteRule(expanded)
		if err != nil {
			log.Debugf("Deny rule '%s' skipped: %v", expanded, err)
			continue
		}

		args = append(args, denyArgs...)
	}

	// Skip mandatory write denies for paths the user listed in allow_read: the
	// allow_read --ro-bind already denies writes (EROFS), and overlaying
	// /dev/null on top would also mask reads, breaking the read-side opt-out.
	// User-listed deny_write entries above are unaffected — "deny wins" still
	// applies to explicit user rules.
	allowReadSet := make(map[string]bool, len(expandedAllowRead))
	for _, p := range expandedAllowRead {
		allowReadSet[filepath.Clean(p)] = true
	}
	for _, pattern := range mandatoryResult.DenyWrite {
		if allowReadSet[filepath.Clean(pattern)] {
			continue
		}

		expanded, err := util.ExpandVariables(pattern)
		if err != nil {
			log.Warnf("Failed to expand variables in deny pattern '%s': %v", pattern, err)
			continue
		}

		denyArgs, err := t.processDenyRule(expanded)
		if err != nil {
			log.Debugf("Deny rule '%s' skipped: %v", expanded, err)
			continue
		}

		args = append(args, denyArgs...)
	}

	// 4. Tmpfs-hide credential directories. Tmpfs blocks both directions, so
	// only paths denied on both sides qualify.
	tmpfsCandidates := intersectStrings(mandatoryResult.DenyRead, mandatoryResult.DenyWrite)
	hiddenDirs := make(map[string]bool)
	for _, pattern := range tmpfsCandidates {
		expanded, err := util.ExpandVariables(pattern)
		if err != nil {
			continue
		}

		var dirsToHide []string
		if util.ContainsGlob(expanded) {
			matches, err := filepath.Glob(expanded)
			if err != nil {
				continue
			}

			dirsToHide = matches
		} else {
			dirsToHide = []string{expanded}
		}

		for _, dir := range dirsToHide {
			if hiddenDirs[dir] {
				continue
			}

			if info, err := os.Stat(dir); err == nil && info.IsDir() {
				args = append(args, "--tmpfs", dir)
				hiddenDirs[dir] = true

				log.Debugf("Hiding credential directory '%s' with tmpfs", dir)
			}
		}
	}

	// 5. Process deny_exec rules (mount /dev/null over executables)
	for _, exePath := range policy.Process.DenyExec {
		expanded, err := util.ExpandVariables(exePath)
		if err != nil {
			log.Warnf("Failed to expand variables in deny_exec pattern '%s': %v", exePath, err)
			continue
		}

		// Handle glob patterns (e.g., /usr/bin/python*)
		if util.ContainsGlob(expanded) {
			matches, err := filepath.Glob(expanded)
			if err != nil {
				log.Warnf("Failed to expand deny_exec glob '%s': %v", expanded, err)
				continue
			}
			for _, match := range matches {
				if info, err := os.Stat(match); err == nil && !info.IsDir() {
					args = append(args, "--ro-bind", "/dev/null", match)
					log.Debugf("Blocked execution of '%s'", match)
				}
			}
		} else {
			if info, err := os.Stat(expanded); err == nil && !info.IsDir() {
				args = append(args, "--ro-bind", "/dev/null", expanded)
				log.Debugf("Blocked execution of '%s'", expanded)
			}
		}
	}

	return args, nil
}

// processDenyReadRule hides readable content. Files are masked with /dev/null;
// directories are overlaid with tmpfs so their host contents are not visible.
func (t *bubblewrapPolicyTranslator) processDenyReadRule(path string) ([]string, error) {
	args := []string{}

	if util.ContainsGlob(path) {
		if strings.Contains(path, "**") {
			parentDir := t.extractParentDir(path)
			if parentDir == "" || parentDir == "." {
				return args, nil
			}
			log.Warnf("Deny read glob '%s' uses **; hiding parent directory '%s' to avoid expanding many bubblewrap arguments", path, parentDir)
			return t.processDenyReadRule(parentDir)
		}

		paths, _, err := t.expandGlobPattern(path, t.config.mandatoryDenyScanDepth, t.config.maxGlobPaths)
		if err != nil {
			return args, nil
		}

		for _, p := range paths {
			if info, err := os.Stat(p); err == nil {
				if info.IsDir() {
					args = append(args, "--tmpfs", p)
				} else {
					args = append(args, "--ro-bind", "/dev/null", p)
				}
			}
		}

		return args, nil
	}

	if info, err := os.Stat(path); err == nil {
		if info.IsDir() {
			args = append(args, "--tmpfs", path)
		} else {
			args = append(args, "--ro-bind", "/dev/null", path)
		}
	} else if os.IsNotExist(err) {
		log.Debugf("Deny read rule: skipping non-existent path '%s'", path)
	}

	return args, nil
}

// processDenyWriteRule handles deny_write rules without masking reads. Files
// and directories are mounted read-only over any earlier writable parent bind.
func (t *bubblewrapPolicyTranslator) processDenyWriteRule(path string) ([]string, error) {
	args := []string{}

	if util.ContainsGlob(path) {
		paths, _, err := t.expandGlobPattern(path, t.config.mandatoryDenyScanDepth, t.config.maxGlobPaths)
		if err != nil {
			return args, nil
		}

		for _, p := range paths {
			if _, err := os.Stat(p); err == nil {
				args = append(args, "--ro-bind-try", p, p)
				log.Debugf("Deny write rule: mounted '%s' as read-only", p)
			}
		}

		return args, nil
	}

	if _, err := os.Stat(path); err == nil {
		args = append(args, "--ro-bind-try", path, path)
		log.Debugf("Deny write rule: mounted '%s' as read-only", path)
	} else if os.IsNotExist(err) {
		log.Debugf("Deny write rule: skipping non-existent path '%s'", path)
	}

	return args, nil
}

// processReadRule handles a single allow_read rule, expanding globs and creating ro-bind mounts.
func (t *bubblewrapPolicyTranslator) processReadRule(path string, boundPaths map[string]bool) ([]string, error) {
	args := []string{}

	// Check if path contains glob pattern
	if util.ContainsGlob(path) {
		// Check if the base directory is already bound
		baseDir := t.extractParentDir(path)
		if boundPaths[baseDir] {
			log.Debugf("Skipping pattern '%s' - base directory '%s' already bound", path, baseDir)
			return args, nil
		}

		// Expand glob pattern to concrete paths with fallback detection
		paths, useFallback, err := t.expandGlobPattern(path, t.config.maxGlobDepth, t.config.maxGlobPaths)
		if err != nil {
			return nil, fmt.Errorf("failed to expand glob pattern: %w", err)
		}

		if useFallback {
			// Coarse-grained: bind parent directory
			for _, parentDir := range paths {
				if !boundPaths[parentDir] {
					args = append(args, "--ro-bind-try", parentDir, parentDir)
					boundPaths[parentDir] = true
					log.Debugf("Coarse-grained fallback: bound parent directory '%s' (read-only)", parentDir)
				}
			}
		} else {
			// Fine-grained: bind individual paths
			for _, p := range paths {
				if !boundPaths[p] {
					args = append(args, "--ro-bind-try", p, p)
					boundPaths[p] = true
				}
			}
		}
	} else {
		// Literal path - create read-only bind
		if !boundPaths[path] {
			args = append(args, "--ro-bind-try", path, path)
			boundPaths[path] = true
		}
	}

	return args, nil
}

// processWriteRule handles a single allow_write rule, expanding globs and creating rw-bind mounts.
func (t *bubblewrapPolicyTranslator) processWriteRule(path string, boundPaths map[string]bool) ([]string, error) {
	args := []string{}

	// Check if path contains glob pattern
	if util.ContainsGlob(path) {
		baseDir := t.extractParentDir(path)

		// Globstar write rules always bind the parent directory read-write. Per-path
		// binds interact badly with earlier read-only parent mounts (e.g. ${CWD}/**)
		// and miss files beyond maxGlobDepth — see https://github.com/safedep/pmg/issues/315.
		// Base dir is the path prefix before the first "/**" (see extractGlobstarWriteBaseDir).
		if strings.Contains(path, "**") {
			baseDir = extractGlobstarWriteBaseDir(path)
			args = append(args, "--bind-try", baseDir, baseDir)
			boundPaths[baseDir] = true
			log.Debugf("Globstar allow_write: bound parent directory '%s' (read-write)", baseDir)
			return args, nil
		}

		// Check if the base directory is already bound (e.g., /tmp already bound, skip /tmp/**)
		if boundPaths[baseDir] {
			log.Debugf("Skipping pattern '%s' - base directory '%s' already bound", path, baseDir)
			return args, nil
		}

		// Expand glob pattern to concrete paths with fallback detection
		paths, useFallback, err := t.expandGlobPattern(path, t.config.maxGlobDepth, t.config.maxGlobPaths)
		if err != nil {
			return nil, fmt.Errorf("failed to expand glob pattern: %w", err)
		}

		if useFallback {
			// Coarse-grained: bind parent directory
			for _, parentDir := range paths {
				if !boundPaths[parentDir] {
					args = append(args, "--bind-try", parentDir, parentDir)
					boundPaths[parentDir] = true
					log.Debugf("Coarse-grained fallback: bound parent directory '%s' (read-write)", parentDir)
				} else {
					log.Debugf("Parent directory '%s' already bound for write, skipping duplicate bind", parentDir)
				}
			}
		} else {
			// Fine-grained: bind individual paths
			for _, p := range paths {
				// Check if path exists - if not, bind parent directory instead
				// This allows creating new directories (e.g., node_modules/** when node_modules doesn't exist)
				pathToBind := p
				if _, err := os.Stat(p); os.IsNotExist(err) {
					parentDir := filepath.Dir(p)
					if parentDir != "" && parentDir != "." && parentDir != "/" {
						pathToBind = parentDir
						log.Debugf("Path '%s' doesn't exist, binding parent '%s' as writable to allow creation", p, parentDir)
					}
				}

				if !boundPaths[pathToBind] {
					args = append(args, "--bind-try", pathToBind, pathToBind)
					boundPaths[pathToBind] = true
				} else {
					// Path already bound, add another bind to upgrade to read-write
					// bwrap: later mounts override earlier ones
					args = append(args, "--bind-try", pathToBind, pathToBind)
					log.Debugf("Path '%s' already bound, adding write bind to override", pathToBind)
				}
			}
		}
	} else {
		// Literal path - create read-write bind
		if !boundPaths[path] {
			args = append(args, "--bind-try", path, path)
			boundPaths[path] = true
		}
	}

	return args, nil
}

// processDenyRule handles deny rules by mounting /dev/null to prevent file access.
// This technique is borrowed from Anthropic's sandbox-runtime.
func (t *bubblewrapPolicyTranslator) processDenyRule(path string) ([]string, error) {
	args := []string{}

	// For glob patterns, expand and deny each path
	if util.ContainsGlob(path) {
		// For deny rules, we scan for existing files matching the pattern
		// Note: For deny rules, we ignore the fallback indicator since we want to
		// deny all matched paths individually for maximum security
		paths, _, err := t.expandGlobPattern(path, t.config.mandatoryDenyScanDepth, t.config.maxGlobPaths)
		if err != nil {
			// If glob expansion fails, it's not critical for deny rules
			return args, nil
		}

		for _, p := range paths {
			info, err := os.Stat(p)
			if err == nil {
				if info.IsDir() {
					// For directories, mount as read-only to prevent writes
					// This overrides any previous writable bind of parent directories
					args = append(args, "--ro-bind-try", p, p)
					log.Debugf("Deny rule: mounted directory '%s' as read-only", p)
				} else {
					// For files, mount /dev/null to prevent access
					args = append(args, "--ro-bind", "/dev/null", p)
				}
			}
		}
	} else {
		// For literal paths, check if they exist
		if info, err := os.Stat(path); err == nil {
			if info.IsDir() {
				// For directories, mount as read-only to prevent writes
				// This overrides any previous writable bind of parent directories
				args = append(args, "--ro-bind-try", path, path)
				log.Debugf("Deny rule: mounted directory '%s' as read-only", path)
			} else {
				// File exists - mount /dev/null over it
				args = append(args, "--ro-bind", "/dev/null", path)
			}
		} else if os.IsNotExist(err) {
			// File doesn't exist - skip it
			// IMPORTANT: We cannot use --ro-bind /dev/null for non-existent paths because
			// bwrap creates the file on the host filesystem as a mount point, which leaves
			// empty files (.env, .aws, etc.) in the user's directory after sandbox exits.
			// Non-existent files are harmless (no secrets to leak), and blocking creation
			// in writable directories isn't critical since an attacker creating an empty
			// .env is not a security threat.
			log.Debugf("Deny rule: skipping non-existent path '%s' (bwrap would create empty file as mount point)", path)
		}
	}

	return args, nil
}

// expandGlobPattern expands a glob pattern to a list of concrete paths.
// Implements depth limiting and path count limiting to prevent DoS.
// Returns (paths, useFallback, error) where useFallback indicates if
// coarse-grained parent directory fallback should be used.
func (t *bubblewrapPolicyTranslator) expandGlobPattern(pattern string, maxDepth int, maxPaths int) ([]string, bool, error) {
	// Handle ** globstar patterns specially
	if strings.Contains(pattern, "**") {
		paths, err := t.expandGlobstarPattern(pattern, maxDepth, maxPaths)
		if err != nil {
			return nil, false, err
		}

		// Check if we should use fallback
		if len(paths) > t.config.globFallbackThreshold {
			log.Warnf("Glob pattern '%s' matched %d paths (threshold: %d), using coarse-grained parent directory fallback for scalability",
				pattern, len(paths), t.config.globFallbackThreshold)

			parentDir := t.extractParentDir(pattern)
			return []string{parentDir}, true, nil
		}

		return paths, false, nil
	}

	// Use filepath.Glob for simple patterns (*, ?, [])
	matches, err := filepath.Glob(pattern)
	if err != nil {
		return nil, false, fmt.Errorf("glob expansion failed: %w", err)
	}

	// Check fallback threshold before applying maxPaths limit
	if len(matches) > t.config.globFallbackThreshold {
		log.Warnf("Glob pattern '%s' matched %d paths (threshold: %d), using coarse-grained parent directory fallback for scalability",
			pattern, len(matches), t.config.globFallbackThreshold)
		parentDir := t.extractParentDir(pattern)
		return []string{parentDir}, true, nil
	}

	// Limit number of matches (shouldn't happen if fallback threshold < maxPaths)
	if len(matches) > maxPaths {
		log.Warnf("Glob pattern '%s' matched %d paths, limiting to %d", pattern, len(matches), maxPaths)
		matches = matches[:maxPaths]
	}

	return matches, false, nil
}

func (t *bubblewrapPolicyTranslator) expandGlobstarPattern(pattern string, maxDepth, maxPaths int) ([]string, error) {
	return expandGlobstarPattern(pattern, maxDepth, maxPaths)
}

func (t *bubblewrapPolicyTranslator) extractParentDir(pattern string) string {
	return extractGlobParentDir(pattern)
}

// addPTYSupport adds arguments for pseudo-terminal support.
// Required for interactive package manager commands.
func (t *bubblewrapPolicyTranslator) addPTYSupport() []string {
	args := []string{}

	// Bind /dev/pts for PTY allocation
	args = append(args, "--dev-bind-try", "/dev/pts", "/dev/pts")

	// Bind /dev/ptmx for PTY master
	args = append(args, "--dev-bind-try", "/dev/ptmx", "/dev/ptmx")

	return args
}

// addTmpdirSupport adds arguments for temporary directory access.
// Package managers need writable temp space for downloads, extraction, etc.
func (t *bubblewrapPolicyTranslator) addTmpdirSupport() []string {
	args := []string{}
	tmpDir := os.TempDir()

	// Bind tmp directory as writable
	// Use --bind instead of --bind-try to ensure it's available
	args = append(args, "--bind", tmpDir, tmpDir)

	return args
}

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
}

// intersectStrings returns the order-preserving intersection of a and b.
func intersectStrings(a, b []string) []string {
	bset := make(map[string]bool, len(b))
	for _, x := range b {
		bset[x] = true
	}
	out := []string{}
	for _, x := range a {
		if bset[x] {
			out = append(out, x)
		}
	}
	return out
}
