//go:build linux

package sandbox

import (
	"fmt"
	"os/exec"
	"strconv"
	"time"

	"github.com/google/uuid"
)

// containerSandbox runs the agent binary inside a rootless container with an
// egress-restricted network and the two writable paths bind-mounted in. This is
// the hard isolation boundary (UID + filesystem mask + network policy + cgroup
// limits) — unlike uid mode, a non-cooperating binary cannot bypass the egress
// allowlist because the container network only routes through the proxy/network
// policy.
//
// It rewrites cmd to `docker run --rm --name <n> --user uid:gid --read-only
// --network <egress-net> --pids-limit --memory --cpus --mount <home> --mount
// <worktree> -w <worktree> <image> <binary> <args>` where the image is the same
// runtime image (so the agent CLI is present). The cleanup closure runs
// `docker stop <n>` so proc.Group's tree-kill (which signals the `docker run`
// client's pgid) is reinforced by stopping the actual container — otherwise
// killing the client could orphan the container.
//
// Tree-kill invariant: Apply does NOT set Setpgid (proc.Group owns it). docker
// run becomes the group leader; killing -pgid signals it, and cleanup() issues
// docker stop for the container it spawned. The mapping client->container is via
// the deterministic --name we assign.
//
// The container runtime + egress network are provisioned by the deployment
// (Dockerfile installs the runtime; docker-compose defines the egress-net + the
// forward-proxy sidecar). Image/network/runtime are read from the spec's
// DataRoot convention or sensible defaults; here we keep them parameterized via
// package-level defaults so app.go can override without a code change later.
type containerSandbox struct{}

// container runtime defaults; overridable by deployment via env in a later pass.
const (
	defaultRuntime    = "docker"
	defaultImage      = "agent-coding-workflow-sandbox:latest"
	defaultEgressNet  = "egress-net"
	containerStopWait = 5 * time.Second
)

func (s *containerSandbox) Apply(cmd *exec.Cmd, spec Spec) (func(), error) {
	if spec.WorktreeDir == "" {
		return func() {}, fmt.Errorf("sandbox container: worktree dir required")
	}
	runtime, err := exec.LookPath(defaultRuntime)
	if err != nil {
		// Runtime missing: fall back to no-op cleanup but surface the error so
		// the supervisor can decide (it currently logs and proceeds unsandboxed
		// only if configured to; otherwise spawn fails).
		return func() {}, fmt.Errorf("sandbox container: %s not found: %w", defaultRuntime, err)
	}

	name := "acw-sbx-" + uuid.NewString()
	orig := append([]string{cmd.Path}, cmd.Args[1:]...)

	runArgs := []string{
		"run", "--rm", "--name", name,
		"--read-only",
		"--network", defaultEgressNet,
		"-w", spec.WorktreeDir,
		"--mount", "type=bind,source=" + spec.WorktreeDir + ",target=" + spec.WorktreeDir,
	}
	if spec.HomeDir != "" {
		runArgs = append(runArgs,
			"--mount", "type=bind,source="+spec.HomeDir+",target="+spec.HomeDir,
			"-e", "HOME="+spec.HomeDir)
	}
	if spec.UID > 0 {
		gid := spec.GID
		if gid <= 0 {
			gid = spec.UID
		}
		runArgs = append(runArgs, "--user", strconv.Itoa(spec.UID)+":"+strconv.Itoa(gid))
	}
	if spec.Rlimits.AddressSpaceBytes > 0 {
		runArgs = append(runArgs, "--memory", strconv.FormatUint(spec.Rlimits.AddressSpaceBytes, 10))
	}
	if spec.Rlimits.PIDs > 0 {
		runArgs = append(runArgs, "--pids-limit", strconv.FormatUint(spec.Rlimits.PIDs, 10))
	} else if spec.Rlimits.NProc > 0 {
		runArgs = append(runArgs, "--pids-limit", strconv.FormatUint(spec.Rlimits.NProc, 10))
	}
	if spec.Rlimits.CPUSeconds > 0 {
		// docker has no direct CPU-seconds cap; approximate with --cpus=1 and
		// rely on the wall-clock/budget reaper for hard CPU-time bounds.
		runArgs = append(runArgs, "--cpus", "1")
	}
	// Egress proxy env (the proxy itself enforces the allowlist on egress-net).
	if spec.ProxyURL != "" {
		noProxy := spec.NoProxy
		if noProxy == "" {
			noProxy = "localhost,127.0.0.1,::1"
		}
		runArgs = append(runArgs,
			"-e", "HTTP_PROXY="+spec.ProxyURL,
			"-e", "HTTPS_PROXY="+spec.ProxyURL,
			"-e", "NO_PROXY="+noProxy)
	}
	runArgs = append(runArgs, defaultImage)
	runArgs = append(runArgs, orig...)

	cmd.Path = runtime
	cmd.Args = append([]string{runtime}, runArgs...)

	cleanup := func() {
		stop := exec.Command(runtime, "stop", "-t", strconv.Itoa(int(containerStopWait.Seconds())), name)
		_ = stop.Run() // best-effort; --rm removes it after stop
	}
	return cleanup, nil
}
