#!/bin/bash
# alpha-vscode-sandbox — LXD terminal target behind the VS Code Web add-on.
# Whitelisted for www-data. The editor terminal attaches to the ACTIVE LXD
# instance; you can point it at any existing container or create a fresh one.
# Reset/Destroy are allowed ONLY on containers this add-on created (tracked in
# CREATED) — never on a panel-managed VM. Attaching a colour-prompt shell adds
# one rc file to the container and nothing else.
#   list                 name|state|os|created  for every LXD instance
#   active               the active target name
#   use <name>           point the terminal at an existing instance
#   create <name>        launch a fresh Ubuntu container, track + activate it
#   reset                destroy + recreate the ACTIVE target (add-on-created only)
#   destroy              delete the ACTIVE target (add-on-created only)
#   status               state/os/ip of the active target (+ created flag)
#   shell                interactive Ubuntu-coloured root shell in the active target
set -u
POOL=alpha
CONF=/var/www/alpha-private/vscode-sandbox.conf       # active instance name
CREATED=/var/www/alpha-private/vscode-sandbox.created  # instances we created
DEFAULT=vscode-sandbox
LXC=/snap/bin/lxc
[ -x "$LXC" ] || LXC=/usr/sbin/lxc

# lxc from a php-fpm-spawned context is blocked by the service sandbox — run in
# a transient unit (like alpha-virt). The interactive `shell` calls lxc directly.
lx() { systemd-run --quiet --wait --collect --pipe -- "$LXC" "$@"; }

valid_name() { [[ "$1" =~ ^[a-z][a-z0-9-]{0,30}$ ]]; }
exists()     { lx list "$1" -c n --format csv 2>/dev/null | grep -qx "$1"; }
state_of()   { lx list "$1" -c s --format csv 2>/dev/null | head -1; }
os_of()      { lx config get "$1" image.description 2>/dev/null | tr -d '"'; }
is_created() { [ -f "$CREATED" ] && grep -qx "$1" "$CREATED"; }

active() { local a; a=$(cat "$CONF" 2>/dev/null); [ -n "$a" ] && exists "$a" && { echo "$a"; return; }; echo "$DEFAULT"; }
set_active() { echo "$1" > "$CONF"; chown www-data:www-data "$CONF" 2>/dev/null || true; }
track()   { touch "$CREATED"; grep -qx "$1" "$CREATED" || echo "$1" >> "$CREATED"; chown www-data:www-data "$CREATED" 2>/dev/null || true; }
untrack() { [ -f "$CREATED" ] && sed -i "\|^$1\$|d" "$CREATED"; }

pick_image() { local fp; fp=$(lx image list -c f --format csv 2>/dev/null | head -1); [ -n "$fp" ] && echo "$fp" || echo "ubuntu:24.04"; }

# Claude Code INSIDE the container — pinned pure-JS build (the native 2.1.x
# binary deadlocks on this host, in containers too). Installed per-sandbox so
# it is disposable: Reset/Destroy takes it away with the box.
sandbox_claude_install() { # $1 = name
  lx exec "$1" -- bash -c '
    command -v claude >/dev/null 2>&1 && exit 0
    export DEBIAN_FRONTEND=noninteractive DISABLE_AUTOUPDATER=1
    command -v npm >/dev/null 2>&1 || { apt-get update -q; apt-get install -y -q nodejs npm; } >/dev/null 2>&1
    npm install -g @anthropic-ai/claude-code@2.1.100 >/dev/null 2>&1
    command -v claude >/dev/null 2>&1' </dev/null
}

# The panel's guided sign-in mints an OAuth token (alpha-private) rather than a
# credentials file. Stage it INSIDE the shared .claude dir so the engine and the
# terminal in the box can export it — sign-out removes it everywhere at once.
sandbox_token_sync() {
  local D="/var/www/Addons/VS Code Web/UserData/.claude" T=/var/www/alpha-private/claude-code-token
  mkdir -p "$D"
  if [ -r "$T" ]; then install -o 33 -g 33 -m 600 "$T" "$D/.alpha-oauth-token"
  else rm -f "$D/.alpha-oauth-token"; fi
}

# Mirror the host sign-in into the box (and mirror a sign-OUT too): the same
# subscription works at root@sandbox, and dies with the container.
sandbox_claude_creds() { # $1 = name
  local D="/var/www/Addons/VS Code Web/UserData"
  if [ -f "$D/.claude/.credentials.json" ]; then
    lx exec "$1" -- mkdir -p /root/.claude </dev/null >/dev/null 2>&1
    lx file push "$D/.claude/.credentials.json" "$1/root/.claude/.credentials.json" </dev/null >/dev/null 2>&1
    lx exec "$1" -- bash -c '[ -f /root/.claude.json ] || echo "{\"hasCompletedOnboarding\":true}" > /root/.claude.json' </dev/null >/dev/null 2>&1
  else
    lx exec "$1" -- rm -f /root/.claude/.credentials.json </dev/null >/dev/null 2>&1
  fi
}

# Share the editor's Projects area into the box at the same /workspaces path,
# with container-root mapped to host www-data — so the Claude agent running in
# the sandbox edits the REAL projects (and nothing else on the host).
sandbox_share_workspaces() { # $1 = name
  grep -q "^root:33:1" /etc/subuid || echo "root:33:1" >> /etc/subuid
  grep -q "^root:33:1" /etc/subgid || echo "root:33:1" >> /etc/subgid
  lx config set "$1" raw.idmap "both 33 0" </dev/null >/dev/null 2>&1
  lx config device add "$1" workspaces disk source="/var/www/Addons/VS Code Web/Projects" path=/workspaces </dev/null >/dev/null 2>&1
  # share the editor's .claude state too: the engine in the box reads the SAME
  # credentials/sessions the host extension sees — auth just works, session
  # lists stay visible, and a host sign-out applies instantly in the box.
  mkdir -p "/var/www/Addons/VS Code Web/UserData/.claude"
  chown www-data:www-data "/var/www/Addons/VS Code Web/UserData/.claude"
  lx config device add "$1" claudehome disk source="/var/www/Addons/VS Code Web/UserData/.claude" path=/root/.claude </dev/null >/dev/null 2>&1
  lx restart "$1" </dev/null >/dev/null 2>&1
  local i; for i in $(seq 1 15); do lx exec "$1" -- true >/dev/null 2>&1 && break; sleep 1; done
}

do_create() { # $1 = name
  exists "$1" && { echo "exists"; return 0; }
  lx launch "$(pick_image)" "$1" -s "$POOL" -c limits.cpu=1 -c limits.memory=1024MiB -d root,size=6GiB </dev/null \
    || { echo "launch-failed"; return 1; }
  local i; for i in $(seq 1 20); do lx exec "$1" -- true >/dev/null 2>&1 && break; sleep 1; done
  track "$1"; set_active "$1"
  sandbox_share_workspaces "$1"
  sandbox_claude_install "$1" >/dev/null 2>&1
  sandbox_claude_creds "$1" >/dev/null 2>&1
  echo ok
}

case "${1:-}" in
  list)
    lx list -c ns --format csv 2>/dev/null | while IFS=, read -r n s; do
      [ -n "$n" ] || continue
      c=0; is_created "$n" && c=1
      printf '%s|%s|%s|%s\n' "$n" "$(echo "$s" | tr 'A-Z' 'a-z')" "$(os_of "$n")" "$c"
    done ;;
  active) active ;;
  use)
    n="${2:-}"; valid_name "$n" || { echo "bad-name"; exit 1; }
    exists "$n" || { echo "no-such-instance"; exit 1; }
    set_active "$n"; echo ok ;;
  create)
    n="${2:-$DEFAULT}"; valid_name "$n" || { echo "bad-name"; exit 1; }
    do_create "$n" ;;
  reset)
    a=$(active); is_created "$a" || { echo "not-ours"; exit 1; }
    [ -n "$(state_of "$a")" ] && lx delete "$a" --force >/dev/null 2>&1
    untrack "$a"; do_create "$a" ;;
  destroy)
    a=$(active); is_created "$a" || { echo "not-ours"; exit 1; }
    [ -z "$(state_of "$a")" ] && { untrack "$a"; echo "already-gone"; exit 0; }
    lx delete "$a" --force >/dev/null 2>&1 || { echo "delete-failed"; exit 1; }
    untrack "$a"
    # fall back to the default sandbox if it exists, else clear
    exists "$DEFAULT" && set_active "$DEFAULT" || set_active "$DEFAULT"
    echo ok ;;
  status)
    a=$(active); st=$(state_of "$a")
    if [ -z "$st" ]; then printf 'name=%s\nstate=missing\ncreated=%s\n' "$a" "$(is_created "$a" && echo 1 || echo 0)"; exit 0; fi
    ip=$(lx list "$a" -c 4 --format csv 2>/dev/null | head -1 | awk '{print $1}')
    printf 'name=%s\nstate=%s\nip=%s\nos=%s\ncreated=%s\n' "$a" "$(echo "$st" | tr 'A-Z' 'a-z')" "$ip" "$(os_of "$a")" "$(is_created "$a" && echo 1 || echo 0)" ;;
  shell)
    a=$(active); st=$(state_of "$a")
    if [ -z "$st" ]; then echo "No sandbox '$a' — create it from the VS Code Web panel (Sandbox terminal card)."; exit 0; fi
    [ "$st" != "RUNNING" ] && "$LXC" start "$a" >/dev/null 2>&1
    for i in $(seq 1 15); do "$LXC" exec "$a" -- true >/dev/null 2>&1 && break; sleep 1; done
    # push an Ubuntu-style colour-prompt rc file (refresh every open), then exec a shell that uses it
    tmp=$(mktemp)
    cat > "$tmp" << 'RCEOF'
# Alpha VS Code sandbox — Ubuntu-style coloured shell
[ -f /etc/bash.bashrc ] && . /etc/bash.bashrc
[ -f ~/.bashrc ] && . ~/.bashrc
force_color_prompt=yes
PS1='\[\e[01;32m\]\u@\h\[\e[00m\]:\[\e[01;34m\]\w\[\e[00m\]\$ '
alias ls='ls --color=auto'
alias ll='ls -alF --color=auto'
alias la='ls -A --color=auto'
alias grep='grep --color=auto'
export DISABLE_AUTOUPDATER=1   # claude stays on the pinned build (native one hangs)
[ -r ~/.claude/.alpha-oauth-token ] && export CLAUDE_CODE_OAUTH_TOKEN=$(cat ~/.claude/.alpha-oauth-token)
RCEOF
    "$LXC" file push "$tmp" "$a/root/.alpha-vscode-rc" >/dev/null 2>&1
    rm -f "$tmp"
    # claude in the box: sync the host sign-in/out, install in the background if missing
    sandbox_token_sync
    sandbox_claude_creds "$a" >/dev/null 2>&1
    "$LXC" exec "$a" -- bash -lc 'command -v claude' >/dev/null 2>&1 || ( sandbox_claude_install "$a" >/dev/null 2>&1 & )
    exec "$LXC" exec "$a" --env HOME=/root --env "TERM=${TERM:-xterm-256color}" --cwd /root -- bash --rcfile /root/.alpha-vscode-rc -i
    ;;
  run)
    # Run the Claude Code ENGINE inside the sandbox — this is what the VS Code
    # extension launches (via /usr/local/bin/claude-sandbox), so the AI agent's
    # shell commands execute at root@sandbox while its file edits land on the
    # real projects through the shared /workspaces mount. stdio passes straight
    # through, the extension can't tell the difference.
    #   run [--fwd VAR=VAL]... -- <claude args>
    shift
    ENVARGS=()
    while [ "${1:-}" = "--fwd" ]; do ENVARGS+=(--env "${2:-}"); shift 2; done
    [ "${1:-}" = "--" ] && shift
    a=$(active); st=$(state_of "$a")
    if [ -n "$st" ]; then
      # </dev/null on EVERY pre-flight exec — they would otherwise swallow the
      # protocol stream the extension pipes to the engine over stdin
      [ "$st" != "RUNNING" ] && "$LXC" start "$a" </dev/null >/dev/null 2>&1
      for i in $(seq 1 15); do "$LXC" exec "$a" -- true </dev/null >/dev/null 2>&1 && break; sleep 1; done
      sandbox_token_sync
      sandbox_claude_creds "$a" >/dev/null 2>&1
      "$LXC" exec "$a" -- bash -lc 'command -v claude' </dev/null >/dev/null 2>&1 || sandbox_claude_install "$a" >/dev/null 2>&1
      CWD="${PWD:-/workspaces}"
      "$LXC" exec "$a" -- test -d "$CWD" </dev/null >/dev/null 2>&1 || CWD=/root
      exec "$LXC" exec "$a" ${ENVARGS[@]+"${ENVARGS[@]}"} --env HOME=/root --env "TERM=${TERM:-xterm-256color}" --env DISABLE_AUTOUPDATER=1 --cwd "$CWD" -- bash -c 'TOK=/root/.claude/.alpha-oauth-token; [ -r "$TOK" ] && export CLAUDE_CODE_OAUTH_TOKEN=$(cat "$TOK"); exec claude "$@"' _ "$@"
    fi
    # sandbox destroyed → fall back to the host CLI so the editor's AI never dies
    exec /usr/local/bin/claude "$@"
    ;;
  # ---- background AI jobs: run Claude headless on a COPY of a project inside
  # the sandbox, emit a reviewable diff; apply = sync the copy back. The real
  # project is never touched until the human clicks Apply in the panel. ----
  ai-job)  # $2 = project name, $3 = job id (10 hex)
    P="${2:-}"; J="${3:-}"
    AIDIR=/var/www/alpha-private/vscode-ai
    [[ "$J" =~ ^[a-f0-9]{10}$ ]] || { echo "bad-id"; exit 1; }
    [ -f "$AIDIR/$J.prompt" ] || { echo "no-prompt"; exit 1; }
    a=$(active); st=$(state_of "$a"); [ -n "$st" ] || { echo "no-sandbox" > "$AIDIR/$J.status"; echo "no-sandbox"; exit 1; }
    if [ "$st" != "RUNNING" ]; then
      "$LXC" start "$a" </dev/null >/dev/null 2>&1
      for i in $(seq 1 15); do "$LXC" exec "$a" -- true </dev/null >/dev/null 2>&1 && break; sleep 1; done
    fi
    sandbox_token_sync
    sandbox_claude_creds "$a" >/dev/null 2>&1
    "$LXC" exec "$a" -- bash -lc 'command -v claude' </dev/null >/dev/null 2>&1 || sandbox_claude_install "$a" >/dev/null 2>&1
    "$LXC" exec "$a" -- test -d "/workspaces/$P" </dev/null >/dev/null 2>&1 || { echo "no-proj" > "$AIDIR/$J.status"; echo "no-proj"; exit 1; }
    "$LXC" file push "$AIDIR/$J.prompt" "$a/root/ai-$J.prompt" </dev/null >/dev/null 2>&1
    if ! "$LXC" exec "$a" --env DISABLE_AUTOUPDATER=1 --env HOME=/root --env IS_SANDBOX=1 -- bash -c '
        set -e
        P="$1"; J="$2"
        rm -rf "/root/ai-work/$J"; mkdir -p "/root/ai-work/$J"
        cd "/workspaces/$P"
        tar cf - --exclude=node_modules --exclude=vendor --exclude=.git . | tar xf - -C "/root/ai-work/$J"
        cd "/root/ai-work/$J"
        TOK=/root/.claude/.alpha-oauth-token; [ -r "$TOK" ] && export CLAUDE_CODE_OAUTH_TOKEN=$(cat "$TOK")
        timeout 600 claude -p "$(cat "/root/ai-$J.prompt")" --dangerously-skip-permissions
      ' _ "$P" "$J" </dev/null > "$AIDIR/$J.log" 2>&1; then
      echo "run-fail" > "$AIDIR/$J.status"; chown www-data:www-data "$AIDIR/$J.status" "$AIDIR/$J.log" 2>/dev/null
      echo "run-fail"; exit 1
    fi
    "$LXC" exec "$a" -- bash -c 'diff -ruN --exclude=node_modules --exclude=vendor --exclude=.git "/workspaces/$1" "/root/ai-work/$2"' _ "$P" "$J" </dev/null > "$AIDIR/$J.diff" 2>/dev/null || true
    echo "done" > "$AIDIR/$J.status"
    chown www-data:www-data "$AIDIR/$J.status" "$AIDIR/$J.log" "$AIDIR/$J.diff" 2>/dev/null
    echo ok ;;

  ai-apply)  # $2 = project, $3 = job id — sync the reviewed copy back onto the project
    P="${2:-}"; J="${3:-}"
    [[ "$J" =~ ^[a-f0-9]{10}$ ]] || { echo "bad-id"; exit 1; }
    a=$(active); st=$(state_of "$a"); [ "$st" = "RUNNING" ] || { echo "not-running"; exit 1; }
    "$LXC" exec "$a" -- test -d "/root/ai-work/$J" </dev/null >/dev/null 2>&1 || { echo "no-work"; exit 1; }
    "$LXC" exec "$a" -- bash -lc 'command -v rsync' </dev/null >/dev/null 2>&1 || \
      "$LXC" exec "$a" -- bash -lc 'export DEBIAN_FRONTEND=noninteractive; apt-get install -y -q rsync >/dev/null' </dev/null >/dev/null 2>&1
    "$LXC" exec "$a" -- bash -c 'rsync -a --delete --exclude=node_modules --exclude=vendor --exclude=.git "/root/ai-work/$2/" "/workspaces/$1/"' _ "$P" "$J" </dev/null || { echo "sync-fail"; exit 1; }
    "$LXC" exec "$a" -- rm -rf "/root/ai-work/$J" "/root/ai-$J.prompt" </dev/null >/dev/null 2>&1
    echo ok ;;

  ai-discard)  # $2 = job id — throw the work copy away
    J="${2:-}"
    [[ "$J" =~ ^[a-f0-9]{10}$ ]] || { echo "bad-id"; exit 1; }
    a=$(active); st=$(state_of "$a")
    [ "$st" = "RUNNING" ] && "$LXC" exec "$a" -- rm -rf "/root/ai-work/$J" "/root/ai-$J.prompt" </dev/null >/dev/null 2>&1
    echo ok ;;

  provision)
    # install a toolchain inside the ACTIVE sandbox (project stacks — the panel
    # calls this detached right after scaffolding a project).
    STACK="${2:-}"
    a=$(active); st=$(state_of "$a")
    [ -n "$st" ] || { echo "no-sandbox"; exit 1; }
    if [ "$st" != "RUNNING" ]; then
      "$LXC" start "$a" </dev/null >/dev/null 2>&1
      for i in $(seq 1 15); do "$LXC" exec "$a" -- true </dev/null >/dev/null 2>&1 && break; sleep 1; done
    fi
    case "$STACK" in
      php)    PKGS="php-cli php-mbstring php-xml php-curl php-sqlite3 composer" ;;
      node)   PKGS="nodejs npm" ;;
      python) PKGS="python3 python3-pip python3-venv" ;;
      *) echo "bad-stack"; exit 1 ;;
    esac
    "$LXC" exec "$a" -- bash -lc "export DEBIAN_FRONTEND=noninteractive; apt-get update -q >/dev/null && apt-get install -y -q $PKGS >/dev/null" </dev/null || { echo "apt-fail"; exit 1; }
    echo ok ;;

  *)
    echo "usage: alpha-vscode-sandbox list|active|use <n>|create <n>|reset|destroy|status|shell|run [--fwd V=X]... -- <args>|provision <php|node|python>"; exit 1 ;;
esac
