// hazmat-launch is the narrow privileged helper that bridges the dr→agent
// privilege gap for Hazmat-owned agent operations.
//
// It is the sole binary covered by the NOPASSWD sudoers rule:
//
//	dr ALL = (agent) NOPASSWD: /usr/local/libexec/hazmat-launch
//	dr ALL = (agent) NOPASSWD: sha256:<digest> /Users/dr/.local/libexec/hazmat-launch
//
// Unlike granting NOPASSWD to /usr/bin/sandbox-exec directly, this helper:
//   - Accepts only a policy file path (no inline policies)
//   - Validates the policy file is at /private/tmp/hazmat-<pid>.sb
//   - Validates the policy file is owned by the invoking user (SUDO_UID),
//     not by root or agent, to prevent pre-planted policy substitution
//   - Validates the policy file has mode 0644
//   - Validates the policy contains (deny default)
//
// Instead of exec'ing sandbox-exec, the Darwin sandbox backend calls
// sandbox_init() directly via cgo and then execs the target command. This
// eliminates the sandbox-exec process from the chain, giving:
//   - Correct signal forwarding (the process IS the target)
//   - Proper PTY handling for interactive TUI applications
//   - One fewer process in the sudo → sandbox → target chain
//
// Usage:
//
//	sudo -u agent <hazmat-launch> <policy-file> <cmd> [args...]
//	sudo -u agent <hazmat-launch> exec <cmd> [args...]

package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"regexp"
	"strconv"
	"syscall"

	"golang.org/x/sys/unix"
)

// policyFilePattern enforces that the policy file lives under /private/tmp
// and follows the hazmat-<pid>.sb naming convention.  The numeric PID
// component prevents traversal attacks (/private/tmp/../../etc/...).
var policyFilePattern = regexp.MustCompile(`^/private/tmp/hazmat-\d+\.sb$`)

// denyDefaultMarker is the literal string that every legitimate session policy
// generated by hazmat's generateSBPL starts with.  Policies that lack it
// (e.g. hand-written "(version 1)(allow default)" files) are rejected.
const denyDefaultMarker = "(deny default)"

const metadataJSONArg = "--hazmat-metadata-json"

func main() {
	if err := closeInheritedFDs(); err != nil {
		die("hazmat-launch: close inherited fds: %v", err)
	}

	if len(os.Args) < 2 {
		dieUsage()
	}

	if os.Args[1] == "exec" {
		if len(os.Args) < 3 {
			dieUsage()
		}
		execCommand(os.Args[2:])
		return
	}

	if len(os.Args) < 3 {
		dieUsage()
	}

	runLaunchMode(os.Args[1], os.Args[2:])
}

func runLaunchMode(policyFile string, cmdArgs []string) {
	metadataJSON, cmdArgs, err := parseLaunchModeArgs(cmdArgs)
	if err != nil {
		die("hazmat-launch: %v", err)
	}
	policy, err := validateAndReadPolicy(policyFile)
	if err != nil {
		die("hazmat-launch: %v", err)
	}

	// Apply the seatbelt sandbox to this process. After sandbox_init(),
	// the sandbox is active and all subsequent operations (including exec)
	// are subject to the policy.
	if err := sandboxInit(policy); err != nil {
		die("hazmat-launch: %v", err)
	}
	if metadataJSON != "" {
		fmt.Fprintln(os.Stderr, metadataJSON)
	}

	execCommand(cmdArgs)
}

func parseLaunchModeArgs(args []string) (metadataJSON string, cmdArgs []string, err error) {
	if len(args) == 0 {
		return "", nil, fmt.Errorf("missing command")
	}
	if args[0] != metadataJSONArg {
		return "", args, nil
	}
	if len(args) < 3 {
		return "", nil, fmt.Errorf("%s requires a JSON payload and command", metadataJSONArg)
	}
	if args[1] == "" {
		return "", nil, fmt.Errorf("%s payload is empty", metadataJSONArg)
	}
	return args[1], args[2:], nil
}

func execCommand(cmdArgs []string) {
	// Exec the target command. Since sandbox_init() was called in this
	// process, the sandbox is inherited by the exec'd program.
	// This is a direct exec (no fork) — signals, PTY, and exit codes
	// all work correctly.
	bin, err := resolveExecPath(cmdArgs[0])
	if err != nil {
		die("hazmat-launch: %v", err)
	}

	if err := syscall.Exec(bin, cmdArgs, os.Environ()); err != nil {
		die("hazmat-launch: exec %s: %v", bin, err)
	}
}

func dieUsage() {
	die("usage: hazmat-launch <policy-file> <cmd> [args...]\n       hazmat-launch exec <cmd> [args...]")
}

// closeInheritedFDs drops every non-stdio descriptor before any helper logic
// runs. sandbox_init() cannot revoke access granted by an already-open handle.
//
// Enumerates actually-open fds via /dev/fd/ (always present on Darwin) instead
// of iterating from 3 to RLIMIT_NOFILE. RLIMIT_NOFILE can be RLIM_INFINITY
// (~9 quintillion on macOS when the parent shell has 'ulimit -n unlimited'),
// which would hang the loop forever.
func closeInheritedFDs() error {
	entries, err := os.ReadDir("/dev/fd")
	if err != nil {
		return fmt.Errorf("read /dev/fd: %w", err)
	}

	for _, entry := range entries {
		fd, parseErr := strconv.Atoi(entry.Name())
		if parseErr != nil || fd < 3 {
			continue
		}
		if err := unix.Close(fd); err != nil && err != unix.EBADF {
			return fmt.Errorf("close fd %d: %w", fd, err)
		}
	}
	return nil
}

// resolveExecPath finds the absolute path for a command. syscall.Exec
// requires an absolute path — it doesn't search PATH.
func resolveExecPath(cmd string) (string, error) {
	if cmd[0] == '/' {
		return cmd, nil
	}
	// Search PATH
	for _, dir := range splitPath(os.Getenv("PATH")) {
		full := dir + "/" + cmd
		if info, err := os.Stat(full); err == nil && info.Mode()&0o111 != 0 {
			return full, nil
		}
	}
	return "", fmt.Errorf("command not found: %s", cmd)
}

func splitPath(path string) []string {
	if path == "" {
		return nil
	}
	var result []string
	start := 0
	for i := 0; i < len(path); i++ {
		if path[i] == ':' {
			if i > start {
				result = append(result, path[start:i])
			}
			start = i + 1
		}
	}
	if start < len(path) {
		result = append(result, path[start:])
	}
	return result
}

// validateAndReadPolicy checks the policy file and returns its contents.
func validateAndReadPolicy(path string) (string, error) {
	// ── Path ──────────────────────────────────────────────────────────────────
	if !policyFilePattern.MatchString(path) {
		return "", fmt.Errorf("policy file must match /private/tmp/hazmat-<pid>.sb, got %q", path)
	}

	// ── Existence and metadata ────────────────────────────────────────────────
	info, err := os.Stat(path)
	if err != nil {
		return "", fmt.Errorf("stat %q: %w", path, err)
	}

	// ── Ownership: must be SUDO_UID (the invoking dr user), not root/agent ─────
	sudoUID, err := strconv.Atoi(os.Getenv("SUDO_UID"))
	if err != nil || sudoUID == 0 {
		return "", fmt.Errorf("SUDO_UID not set or root — not a valid sudo invocation")
	}
	stat, ok := info.Sys().(*syscall.Stat_t)
	if !ok {
		return "", fmt.Errorf("cannot inspect policy file ownership")
	}
	if int(stat.Uid) != sudoUID {
		return "", fmt.Errorf("policy file owner uid %d does not match invoking user uid %d",
			stat.Uid, sudoUID)
	}

	// ── Mode: must be exactly 0644 ────────────────────────────────────────────
	if perm := info.Mode().Perm(); perm != 0o644 {
		return "", fmt.Errorf("policy file has mode %04o, expected 0644", perm)
	}

	// ── Content ──────────────────────────────────────────────────────────────
	data, err := readFileCloexec(path)
	if err != nil {
		return "", fmt.Errorf("read %q: %w", path, err)
	}
	if !bytes.Contains(data, []byte(denyDefaultMarker)) {
		return "", fmt.Errorf("policy file does not contain %q — permissive policies are not allowed", denyDefaultMarker)
	}

	return string(data), nil
}

func readFileCloexec(path string) ([]byte, error) {
	fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC, 0)
	if err != nil {
		return nil, err
	}

	f := os.NewFile(uintptr(fd), path)
	if f == nil {
		_ = unix.Close(fd)
		return nil, fmt.Errorf("wrap fd %d for %q", fd, path)
	}
	defer f.Close()

	return io.ReadAll(f)
}

// validatePolicyFile checks the policy file without reading its full contents.
// Kept for backward compatibility with tests.
func validatePolicyFile(path string) error {
	_, err := validateAndReadPolicy(path)
	return err
}

func die(format string, args ...any) {
	fmt.Fprintf(os.Stderr, format+"\n", args...)
	os.Exit(1)
}
