// Package sandbox provides a pluggable per-session execution sandbox that sits
// between acp.session_service.Create and acp.Supervisor.Spawn. It confines an
// agent child process to a low-privilege UID, bind-mounts only that session's
// worktree + the per-user agent home, applies OS resource limits, and routes
// egress through an allowlist proxy — all WITHOUT touching proc.Group's
// Setpgid-based tree-kill (Apply only ADDS to cmd.SysProcAttr, never replaces).
//
// Three modes, config-gated via acp.sandbox.mode:
//   - none      : no-op (default; Windows dev + CI + any non-Linux box)
//   - uid       : drop to a low-priv UID + setrlimit (+ optional namespace/bind
//     mounts) — Linux only
//   - container : run the agent inside a rootless runc/bwrap or docker container
//     on an egress-restricted network — Linux only
//
// The Linux-specific mechanics live in build-tagged files; this file holds the
// cross-platform contract and the factory so app.go and supervisor.go compile
// on every platform.
package sandbox

import "os/exec"

// Mode selects the sandbox strategy.
type Mode string

const (
	// ModeNone disables sandboxing (no-op Apply). Default everywhere except
	// production Linux.
	ModeNone Mode = "none"
	// ModeUID drops to a low-priv UID + setrlimit (+ optional bind mounts).
	ModeUID Mode = "uid"
	// ModeContainer runs the child inside a rootless container.
	ModeContainer Mode = "container"
)

// Limits captures the OS resource limits applied to the child (and its tree).
// A zero field means "do not set this limit" (inherit the parent's).
type Limits struct {
	AddressSpaceBytes uint64 // RLIMIT_AS — caps total virtual memory (anti big-alloc)
	NProc             uint64 // RLIMIT_NPROC — caps process/thread count (anti fork-bomb)
	CPUSeconds        uint64 // RLIMIT_CPU — caps CPU seconds
	PIDs              uint64 // cgroup pids.max (container mode); mirrors NProc for uid
	DiskBytes         uint64 // RLIMIT_FSIZE — caps max file size the child can write
}

// Spec is the per-spawn sandbox request, derived from the session + agent kind.
type Spec struct {
	Mode        Mode
	UID         int    // target low-priv uid (uid mode); base+offset computed by caller
	GID         int    // target low-priv gid
	HomeDir     string // per-user agent home (writable, bind-mounted in)
	WorktreeDir string // this session's worktree (writable, bind-mounted in)
	DataRoot    string // /data root to mask everything else under
	Rlimits     Limits
	ProxyURL    string // HTTP(S)_PROXY value injected into the child env (egress allowlist)
	NoProxy     string // NO_PROXY value (e.g. localhost,127.0.0.1)
}

// Sandbox applies a Spec to an exec.Cmd before it is started.
type Sandbox interface {
	// Apply mutates cmd.SysProcAttr (Credential, namespace/chroot flags) and
	// cmd.Env (HTTP(S)_PROXY/NO_PROXY) BEFORE proc.Group.Prepare and cmd.Start.
	// It returns a cleanup closure (unmount binds, remove ephemeral dirs, stop
	// container) that the supervisor calls from monitorProcess AFTER
	// group.Close(). cleanup is always non-nil (a no-op when there is nothing to
	// clean) and must be safe to call exactly once. Apply MUST NOT set Setpgid —
	// that is proc.Group's job; on Linux it ADDS to the existing SysProcAttr.
	Apply(cmd *exec.Cmd, spec Spec) (cleanup func(), err error)
}

// New returns the Sandbox implementation for the given mode. On non-Linux
// platforms (or mode=none) it always returns the no-op sandbox. The actual
// uid/container constructors are provided by build-tagged files; newPlatform
// returns nil when the mode is unsupported on this OS, in which case we fall
// back to no-op so dev/CI never break.
func New(mode Mode) Sandbox {
	switch mode {
	case ModeUID, ModeContainer:
		if sb := newPlatform(mode); sb != nil {
			return sb
		}
		// Unsupported on this platform → safe no-op fallback.
		return noopSandbox{}
	default:
		return noopSandbox{}
	}
}

// noopSandbox makes no changes and returns a no-op cleanup. It is the default
// and the fallback for unsupported modes/platforms.
type noopSandbox struct{}

func (noopSandbox) Apply(_ *exec.Cmd, _ Spec) (func(), error) {
	return func() {}, nil
}

// injectProxyEnv appends egress-proxy env vars to cmd.Env when a proxy URL is
// configured. Shared by uid + container modes (both honor HTTP(S)_PROXY). Kept
// here so the cross-platform contract is testable without build tags.
func injectProxyEnv(cmd *exec.Cmd, spec Spec) {
	if spec.ProxyURL == "" {
		return
	}
	noProxy := spec.NoProxy
	if noProxy == "" {
		noProxy = "localhost,127.0.0.1,::1"
	}
	cmd.Env = append(cmd.Env,
		"HTTP_PROXY="+spec.ProxyURL,
		"HTTPS_PROXY="+spec.ProxyURL,
		"http_proxy="+spec.ProxyURL,
		"https_proxy="+spec.ProxyURL,
		"NO_PROXY="+noProxy,
		"no_proxy="+noProxy,
	)
}
