#!/usr/bin/env bash
# claude-sandbox — helper CLI, placed on PATH at /usr/local/bin by
# install.sh. The install clone is disposable: every command here works
# with no clone present (update fetches its own fresh one).
set -euo pipefail

REPO_URL="https://github.com/DiamondLightSource/claude-sandbox"
# Stamped by install.sh from `git describe` on the installing clone.
# Seam for tests only (version reporting is not security-critical).
VERSION_FILE="${CLAUDE_SANDBOX_VERSION_FILE:-/usr/libexec/claude-sandbox/version}"
# Written by install.sh only when the installer was the PyPI wheel.
INSTALLER_FILE="${CLAUDE_SANDBOX_INSTALLER_FILE:-/usr/libexec/claude-sandbox/installer}"

usage() {
    cat <<'EOF'
claude-sandbox — sandboxed-Claude helper commands

Usage: claude-sandbox <command> [args]

Commands:
  install               Explain that the sandbox is already installed
  gh-auth               Authenticate gh with a GitHub PAT (kept out of shell history)
  glab-auth [hostname]  Authenticate glab with a GitLab PAT (default hostname: gitlab.diamond.ac.uk)
  update                Clone and install the latest claude-sandbox release
  verify [--agent NAME] Launch a sandboxed session running the integrity battery
                        (NAME: claude, the default, codex, or pi)
  pi-local [--port PORT] Discover the running lllm2 model and context (port 1920)
  pi-local MODEL CONTEXT [PORT]
                        Configure Pi's lllm2 provider manually
  version               Show the installed claude-sandbox version
  help                  Show this help
EOF
}

# Authenticate gh CLI with a GitHub PAT (token not stored in shell history).
cmd_gh_auth() {
    local url t
    url=$'\e[4;36mhttps://github.com/settings/personal-access-tokens\e[0m'
    cat <<EOF
Create or renew a fine-grained PAT at:
  $url

Recommended settings for a sandboxed Claude Code:
  - Resource owner: your user (or org that owns this repo)
  - Repository access: Only select repositories -> just this repo
  - Expiration: short (e.g. 30 days) so a leaked token expires quickly
  - Repository permissions Read/Write:
      Issues, Pull requests
  - Repository permissions Read Only:
      Contents
    (Metadata: Read-only is added automatically)
  - Leave everything else unset / no access

EOF
    read -sp "GitHub PAT: " t && echo
    echo "$t" | gh auth login --with-token
    unset t
    gh auth setup-git
    gh auth status
}

# Authenticate glab CLI with a GitLab PAT (token not stored in shell history)
# and pin the instance to HTTPS, so glab never drives git over SSH.
cmd_glab_auth() {
    local hostname="${1:-gitlab.diamond.ac.uk}" url t
    # Derive the PAT page from the instance being authenticated. A hardcoded
    # gitlab.com URL sends self-hosted users to the wrong site to mint a token
    # that is then offered to a different instance.
    url=$'\e[4;36m'"https://$hostname/-/user_settings/personal_access_tokens"$'\e[0m'
    cat <<EOF
Create or renew a fine-grained PAT at:
  $url

Recommended scopes for a sandboxed Claude Code:
  - api, read_repository, write_repository
  - Short expiration so a leaked token expires quickly

EOF
    read -sp "GitLab PAT for $hostname: " t && echo
    echo "$t" | glab auth login --stdin --hostname "$hostname"
    unset t
    # glab has no `auth login --git-protocol` flag (it does not exist in glab
    # 1.36); passing one aborts the login. git_protocol is config, and its
    # shipped default is ssh — which a sandbox has no key for. Set it per-host
    # so only the instance just authenticated is pinned to https.
    glab config set -h "$hostname" git_protocol https
    # glab auth login records the token against the host but leaves the
    # default host alone — it ships as gitlab.com. Outside a git repo, and in
    # any repo whose remote glab cannot map, bare `glab` commands then query
    # gitlab.com and fail as unauthenticated. Point the default at the
    # instance just authenticated.
    # --global is required: without it `glab config set` writes the
    # repository-local .git/glab-cli/config.yml (leaving the real default at
    # gitlab.com), and outside a git repo it fails outright with "not a git
    # repository" — which under `set -e` would abort us right after login.
    glab config set --global host "$hostname"
    echo "Default glab host set to $hostname."
    glab auth status
}

cmd_update() {
    if [ "$(id -u)" -ne 0 ]; then
        echo "claude-sandbox: update must run as root (install requires it)." >&2
        exit 1
    fi
    if [ "${IS_SANDBOX:-0}" = "1" ]; then
        echo "claude-sandbox: refusing to update from inside a sandboxed claude session." >&2
        exit 1
    fi
    if [ -d /opt/claude-sandbox ]; then
        echo "claude-sandbox: this is the published container image — update by pulling a newer image and recreating the container (uvx claude-sandbox@latest --recreate)." >&2
        exit 1
    fi
    if [ "$(cat "$INSTALLER_FILE" 2>/dev/null)" = uvx ]; then
        # The wheel is the pin (ADR 23): a git clone here would step past it.
        echo "claude-sandbox: this sandbox was installed from the PyPI wheel — update with: uvx claude-sandbox@latest install" >&2
        echo "  (or bump the pinned version in your devcontainer's postCreate)" >&2
        exit 1
    fi
    local tmp
    tmp="$(mktemp -d)"
    git clone --quiet "$REPO_URL" "$tmp/claude-sandbox"
    # Release selection lives in `install` (which defaults to the newest
    # stable tag) so there is ONE implementation of "what is current" —
    # shared with a first install from the documented clone one-liner.
    # This clone is pristine and on the default branch, so the default
    # applies and no flag is needed.
    # Safe self-replacement: install overwrites the very file bash is
    # reading incrementally. exec into a fresh `bash -c` — the command
    # string lives in argv, not in this file — so nothing reads this
    # script again after the overwrite; the temp clone is removed after.
    exec bash -c 'bash "$1/claude-sandbox/install" && rm -rf "$1"' claude-sandbox-update "$tmp"
}

# Prompt used when driving an agent to run the phase-1 battery for us.
VERIFY_PROMPT="Run: bash /usr/libexec/claude-sandbox/verify-sandbox-battery.sh — show its output verbatim and report the Summary line and exit code. Do not attempt to fix failures."

cmd_verify() {
    local agent=claude
    while [ "$#" -gt 0 ]; do
        case "$1" in
            --agent)
                case "${2:-}" in
                    claude|codex|pi) agent="$2"; shift 2 ;;
                    *) echo "claude-sandbox: verify --agent needs claude, codex, or pi" >&2; exit 2 ;;
                esac
                ;;
            *) echo "claude-sandbox: unknown verify option '$1'" >&2; exit 2 ;;
        esac
    done

    if [ "${IS_SANDBOX:-0}" = "1" ]; then
        # Don't point at /verify-sandbox unconditionally: that project
        # command ships with a claude-sandbox clone, so it is absent in
        # most workspaces. The battery is installed globally.
        echo "claude-sandbox: already inside a sandboxed session — verification launches its own session." >&2
        echo "  from a terminal outside this session: claude-sandbox verify [--agent codex]" >&2
        echo "  here and now:                         bash /usr/libexec/claude-sandbox/verify-sandbox-battery.sh" >&2
        exit 1
    fi

    if [ "$agent" = pi ]; then
        if [ ! -x /usr/libexec/claude-sandbox/pi-dist/pi ]; then
            echo 'claude-sandbox: Pi is not installed; re-run ./install with WITH_PI=1.' >&2
            exit 1
        fi
        exec pi -p "$VERIFY_PROMPT"
    fi

    if [ "$agent" = codex ]; then
        # `codex exec` is Codex's headless one-shot mode: prompt in, result
        # out, no TUI. Phase 1 only — the phase-2 adversarial probes are a
        # Claude slash-command flow (.claude/commands/verify-sandbox.md) and
        # have no Codex equivalent yet.
        # Codex ships as a PACKAGE, so its binary sits inside the relocated
        # release dir. /usr/libexec/claude-sandbox/codex is never created by
        # the installer — testing it made this command always report "not
        # installed" and exit 1.
        if [ ! -x /usr/libexec/claude-sandbox/codex-dist/bin/codex ]; then
            echo "claude-sandbox: no codex binary installed — re-run ./install (or it was installed with WITH_CODEX=0)." >&2
            exit 1
        fi
        exec codex exec "$VERIFY_PROMPT"
    fi

    if [ -f .claude/commands/verify-sandbox.md ]; then
        # In the claude-sandbox clone: the project-scoped /verify-sandbox
        # command runs the full two-phase flow (battery + adversarial probes).
        exec claude "/verify-sandbox"
    fi
    # Anywhere else: the phase-1 battery is installed globally at an
    # absolute path; start a sandboxed session that runs it.
    exec claude "$VERIFY_PROMPT"
}

# Discover lllm2's single loaded model and actual per-slot context. The manual
# form remains available for servers without llama.cpp's /props endpoint.
cmd_pi_local() (
    local model="" context="" port="${CLAUDE_SANDBOX_LOCAL_MODEL_PORT:-1920}" discover=false dir file tmp
    case "$#" in
        0) discover=true ;;
        2) if [ "$1" = --port ]; then port="$2"; discover=true; else model="$1"; context="$2"; fi ;;
        3) model="$1"; context="$2"; port="$3" ;;
        *) echo 'Usage: claude-sandbox pi-local [--port PORT] or pi-local MODEL CONTEXT [PORT]' >&2; exit 2 ;;
    esac
    if ! [[ "$port" =~ ^[1-9][0-9]{0,4}$ ]] || (( port > 65535 )); then
        echo 'claude-sandbox: local model port must be 1–65535.' >&2
        exit 2
    fi
    if "$discover"; then
        if ! model="$(curl --noproxy '*' -fs --connect-timeout 1 --max-time 3 "http://127.0.0.1:$port/v1/models" \
            | jq -er '.data | select(length == 1) | .[0].id | select(type == "string" and length > 0)')" \
            || ! context="$(curl --noproxy '*' -fs --connect-timeout 1 --max-time 3 "http://127.0.0.1:$port/props" \
            | jq -er '.default_generation_settings.n_ctx | select(type == "number" and . == floor)')"; then
            echo "claude-sandbox: could not discover one loaded model and its context on port $port; existing Pi configuration kept. Start a model in lllm2, or use pi-local MODEL CONTEXT [PORT]." >&2
            exit 1
        fi
    fi
    if [ -z "$model" ] || ! [[ "$context" =~ ^[1-9][0-9]{2,6}$ ]] || (( context < 512 )); then
        echo 'claude-sandbox: supply a model ID and context of 512–9999999 tokens.' >&2
        exit 2
    fi
    umask 077
    dir="$HOME/.pi/agent"
    file="$dir/models.json"
    mkdir -p "$dir"
    tmp="$(mktemp "$dir/.models.XXXXXX")"
    trap 'rm -f "$tmp"' EXIT
    if [ -e "$file" ]; then
        jq -e 'type == "object" and ((.providers // {}) | type == "object")' "$file" >/dev/null
    else
        printf '{}\n' > "$tmp"
        file="$tmp"
    fi
    local result
    result="$(jq --arg model "$model" --argjson context "$context" --arg port "$port" '
        .providers.lllm2 = ((.providers.lllm2 // {}) + {
            baseUrl: ("http://127.0.0.1:" + $port + "/v1"),
            api: "openai-completions", apiKey: (.providers.lllm2.apiKey // "local"),
            compat: ({supportsDeveloperRole: false, supportsReasoningEffort: false} + (.providers.lllm2.compat // {})),
            models: [((.providers.lllm2.models // [] | map(select(.id == $model)) | first) // {}) +
                     {id: $model, name: ("Local (lllm2): " + $model),
                      contextWindow: $context, maxTokens: ([$context / 4 | floor, 32000] | min)}]
        })' "$file")"
    printf '%s\n' "$result" > "$tmp"
    mv "$tmp" "$dir/models.json"
    echo "Configured Pi's lllm2 provider at http://127.0.0.1:$port/v1."
    echo "Select lllm2 in Pi's /model picker, or launch pi --provider lllm2."
    echo "The relay setting in /etc/claude-sandbox.conf must match port $port (shipped default: 1920)."
)

cmd_version() {
    if [ -r "$VERSION_FILE" ]; then
        echo "claude-sandbox $(cat "$VERSION_FILE")"
    else
        echo "claude-sandbox: version unknown ($VERSION_FILE missing — re-run install)" >&2
        exit 1
    fi
}

case "${1:-help}" in
    install)
        echo "claude-sandbox is already installed in this container. No installation is needed."
        echo "Run claude, codex or pi to start a sandboxed agent session."
        ;;
    gh-auth)              shift; cmd_gh_auth "$@" ;;
    glab-auth)            shift; cmd_glab_auth "$@" ;;
    update)               shift; cmd_update "$@" ;;
    verify)               shift; cmd_verify "$@" ;;
    pi-local)             shift; cmd_pi_local "$@" ;;
    version|--version|-v) cmd_version ;;
    help|-h|--help)       usage ;;
    *)
        echo "claude-sandbox: unknown command '$1'" >&2
        usage >&2
        exit 2
        ;;
esac
