#!/usr/bin/env bash
set -euo pipefail

CODEX_WORKSPACE_VERSION="0.1.0"
CODEX_WORKSPACE_CAPABILITIES=(
  "capabilities-command"
  "supports-flag"
  "version"
  "create-alias"
  "output-json"
  "tunnel-name-v1"
  "tunnel-json"
  "secrets-explicit-dir"
  "rm-default-volumes"
  "rm-keep-volumes-flag"
)

DEFAULT_IMAGE="${CODEX_ENV_IMAGE:-graysurf/agent-env:linuxbrew}"
WORKSPACE_PREFIX="${CODEX_WORKSPACE_PREFIX:-agent-ws}"
DEFAULT_GITHUB_HOST="${GITHUB_HOST:-github.com}"
DEFAULT_SECRETS_DIR=""
DEFAULT_SECRETS_MOUNT="${DEFAULT_SECRETS_MOUNT:-/home/agent/codex_secrets}"

usage() {
  cat <<'EOF'
agent-workspace: start isolated Codex workspaces from a git repo input.

Usage:
  agent-workspace --version
  agent-workspace capabilities
  agent-workspace --supports <capability>
  agent-workspace up|create [<repo>] [--name <name>] [--no-clone] [--host <host>] [--image <image>] [--no-pull]
                    [--ref <git-ref>] [--dir <path>]
                    [--setup-git]
                    [--secrets-dir <host-path>] [--secrets-mount <container-path>] [--no-secrets]
                    [--codex-profile <profile>]
                    [--tunnel] [--tunnel-detach]
                    [--output json]
  agent-workspace shell <name>
  agent-workspace tunnel <name> [--name <tunnel_name>] [--detach] [--output json]
  agent-workspace ls
  agent-workspace start <name>
  agent-workspace stop <name>
  agent-workspace rm <name> [--keep-volumes]

Defaults:
  --image:       graysurf/agent-env:linuxbrew  (or $CODEX_ENV_IMAGE)
  workspace name: derived from <repo> + timestamp
  secrets mount: /home/agent/codex_secrets (override via --secrets-mount; env: DEFAULT_SECRETS_MOUNT)

Notes:
  - Repo is cloned into a Docker named volume mounted at /work (no host workspace bind mount).
  - Use `--no-clone` to create a workspace without cloning a repo into /work.
    When `--no-clone` is set, `<repo>` is optional but `--name` is required if no repo is provided.
  - For private repos, export GH_TOKEN (or GITHUB_TOKEN) on the host before running `up`.
  - Secrets are opt-in: pass `--secrets-dir <host-path>` to mount secrets.
    - Recommended: `--secrets-dir $HOME/.config/codex_secrets`
    - When secrets are mounted, agent-workspace sets CODEX_SECRET_DIR in-container to the mount path.
  - `--codex-profile` requires secrets (`--secrets-dir ...`) and runs `codex-use <profile>` in the container.
  - JSON output: when `--output json` is used, all human output goes to stderr and stdout is pure JSON.
EOF
}

die() {
  echo "error: $*" >&2
  exit 1
}

warn() {
  echo "warn: $*" >&2
}

json_escape() {
  local s="${1-}"
  s="${s//\\/\\\\}"
  s="${s//\"/\\\"}"
  s="${s//$'\n'/\\n}"
  s="${s//$'\r'/\\r}"
  s="${s//$'\t'/\\t}"
  printf '%s' "$s"
}

json_bool() {
  if [[ "${1-}" == "1" ]]; then
    printf 'true'
  else
    printf 'false'
  fi
}

json_string_array() {
  local -a items=("$@")
  local first="1"
  printf '['
  local item=""
  for item in "${items[@]}"; do
    if [[ "$first" != "1" ]]; then
      printf ','
    fi
    first="0"
    printf '"%s"' "$(json_escape "$item")"
  done
  printf ']'
}

need_cmd() {
  command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
}

supports_capability() {
  local needle="${1:-}"
  local cap=""
  for cap in "${CODEX_WORKSPACE_CAPABILITIES[@]}"; do
    if [[ "$cap" == "$needle" ]]; then
      return 0
    fi
  done
  return 1
}

emit_capabilities_json() {
  printf '{'
  printf '"version":"%s",' "$(json_escape "$CODEX_WORKSPACE_VERSION")"
  printf '"capabilities":%s' "$(json_string_array "${CODEX_WORKSPACE_CAPABILITIES[@]}")"
  printf '}\n'
}

normalize_secrets_mount() {
  local path="${1:-}"
  [[ -n "$path" ]] || die "missing secrets mount path"
  if [[ "$path" == "~"* ]]; then
    path="/home/agent${path#~}"
  fi
  [[ "$path" == /* ]] || die "secrets mount must be an absolute path (or \\$HOME/<path>): $path"
  path="${path%/}"
  [[ -n "$path" ]] || die "invalid secrets mount path"
  echo "$path"
}

slugify() {
  printf "%s" "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g'
}

timestamp() {
  date +"%Y%m%d-%H%M%S"
}

tunnel_name_hash4() {
  local input="${1:-}"
  [[ -n "$input" ]] || {
    echo "0000"
    return 0
  }

  local hash4=""
  if command -v shasum >/dev/null 2>&1; then
    hash4="$(printf "%s" "$input" | shasum -a 1 2>/dev/null | awk '{print $1}' | cut -c1-4)"
  elif command -v sha1sum >/dev/null 2>&1; then
    hash4="$(printf "%s" "$input" | sha1sum 2>/dev/null | awk '{print $1}' | cut -c1-4)"
  elif command -v openssl >/dev/null 2>&1; then
    hash4="$(printf "%s" "$input" | openssl sha1 2>/dev/null | awk '{print $2}' | cut -c1-4)"
  fi

  [[ -n "$hash4" ]] || hash4="0000"
  echo "$hash4"
}

tunnel_name_sanitize() {
  local name="${1:-}"
  name="$(printf "%s" "$name" | tr '[:upper:]' '[:lower:]')"
  name="$(printf "%s" "$name" | sed -E 's/[^a-z0-9-]+/-/g; s/--+/-/g; s/^-+//; s/-+$//')"
  [[ -n "$name" ]] || name="ws"
  echo "$name"
}

tunnel_default_name() {
  local container="${1:-}"
  [[ -n "$container" ]] || die "missing container for tunnel name"

  local base="${container#"${WORKSPACE_PREFIX}"-}"
  local candidate="$base"

  # Common pattern: <owner>-<repo>-YYYYMMDD-HHMMSS
  if [[ "$candidate" == *-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9] ]]; then
    candidate="${candidate%-[0-9][0-9][0-9][0-9][0-9][0-9]}"
    candidate="${candidate%-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]}"
  fi

  candidate="$(tunnel_name_sanitize "$candidate")"

  local -i max_len=20
  if (( ${#candidate} <= max_len )); then
    echo "$candidate"
    return 0
  fi

  local hash4
  hash4="$(tunnel_name_hash4 "$container")"
  local -i prefix_len=$(( max_len - 5 )) # "<prefix>-<hash4>"
  local short_prefix="${candidate:0:prefix_len}"
  while [[ "$short_prefix" == *- ]]; do
    short_prefix="${short_prefix%-}"
  done
  [[ -n "$short_prefix" ]] || short_prefix="ws"
  short_prefix="$(tunnel_name_sanitize "$short_prefix")"

  local shortened="${short_prefix}-${hash4}"
  echo "${shortened:0:max_len}"
}

normalize_container_name() {
  local name="$1"
  if [[ "$name" == "${WORKSPACE_PREFIX}-"* ]]; then
    echo "$name"
    return 0
  fi

  # Avoid duplicate "...-ws-ws-..." when prefix already ends with "-ws".
  local normalized="$name"
  if [[ "$WORKSPACE_PREFIX" == *"-ws" && "$normalized" == ws-* ]]; then
    local stripped="${normalized#ws-}"
    if [[ -n "$stripped" ]]; then
      normalized="$stripped"
    fi
  fi

  local canonical="${WORKSPACE_PREFIX}-${normalized}"
  local legacy="${WORKSPACE_PREFIX}-${name}"
  if [[ "$canonical" != "$legacy" ]] && container_exists "$legacy" && ! container_exists "$canonical"; then
    echo "$legacy"
    return 0
  fi

  echo "$canonical"
}

parse_repo() {
  local input="$1"
  local host="" owner="" repo=""

  if [[ "$input" =~ ^git@([^:]+):([^/]+)/([^/]+)(\.git)?$ ]]; then
    host="${BASH_REMATCH[1]}"
    owner="${BASH_REMATCH[2]}"
    repo="${BASH_REMATCH[3]}"
  elif [[ "$input" =~ ^ssh://git@([^/]+)/([^/]+)/([^/]+)(\.git)?$ ]]; then
    host="${BASH_REMATCH[1]}"
    owner="${BASH_REMATCH[2]}"
    repo="${BASH_REMATCH[3]}"
  elif [[ "$input" =~ ^https?://([^/]+)/([^/]+)/([^/]+)(\.git)?/?$ ]]; then
    host="${BASH_REMATCH[1]}"
    owner="${BASH_REMATCH[2]}"
    repo="${BASH_REMATCH[3]}"
  elif [[ "$input" =~ ^([^/]+)/([^/]+)$ ]]; then
    host="$DEFAULT_GITHUB_HOST"
    owner="${BASH_REMATCH[1]}"
    repo="${BASH_REMATCH[2]}"
  else
    die "unsupported repo input: $input (expected: OWNER/REPO, https://..., or git@...:OWNER/REPO.git)"
  fi

  repo="${repo%.git}"
  echo "$host" "$owner" "$repo"
}

ensure_image() {
  local image="$1"
  local pull="$2"

  if docker image inspect "$image" >/dev/null 2>&1; then
    return 0
  fi

  if [[ "$pull" != "1" ]]; then
    die "image not found locally: $image (re-run without --no-pull)"
  fi

  echo "+ docker pull $image"
  docker pull "$image"
}

container_exists() {
  docker inspect "$1" >/dev/null 2>&1
}

container_running() {
  [[ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null || true)" == "true" ]]
}

ensure_container_running() {
  local container="$1"
  if ! container_exists "$container"; then
    die "workspace not found: $container"
  fi
  if ! container_running "$container"; then
    echo "+ docker start $container"
    docker start "$container" >/dev/null
  fi
}

volume_names() {
  local container="$1"
  echo "${container}-work" "${container}-home" "${container}-agent-home"
}

setup_git_auth() {
  local container="$1"
  local host="$2"
  local token="${3:-}"

  docker exec "$container" bash -lc 'command -v git >/dev/null 2>&1' || return 0

  if [[ -n "$token" ]]; then
    # Prefer gh config-based auth (token stored in container config; not container env).
    if docker exec "$container" bash -lc 'command -v gh >/dev/null 2>&1'; then
      printf '%s\n' "$token" | docker exec -i "$container" bash -lc '
        set -euo pipefail
        host="${1:-github.com}"
        IFS= read -r token || exit 0
        [[ -n "$token" ]] || exit 0

        printf "%s\n" "$token" | gh auth login --hostname "$host" --with-token >/dev/null 2>&1 || true
        gh auth setup-git --hostname "$host" --force >/dev/null 2>&1 || gh auth setup-git --hostname "$host" >/dev/null 2>&1 || true
        gh config set git_protocol https -h "$host" 2>/dev/null || gh config set git_protocol https 2>/dev/null || true
      ' -- "$host" >/dev/null 2>&1 || true
      return 0
    fi

    # Fallback: git-only credential helper using a token file in the container.
    printf '%s\n' "$token" | docker exec -i "$container" bash -lc '
      set -euo pipefail
      host="${1:-github.com}"
      IFS= read -r token || exit 0
      [[ -n "$token" ]] || exit 0

      token_file="$HOME/.agents-env/gh.token"
      mkdir -p "${token_file%/*}"
      printf "%s\n" "$token" >| "$token_file"
      chmod 600 "$token_file" 2>/dev/null || true

      url="https://${host}"
      git config --global "credential.${url}.helper" \
        "!f() { echo username=x-access-token; echo password=\$(cat \"$token_file\"); }; f"
    ' -- "$host" >/dev/null 2>&1 || true
    return 0
  fi

  return 0
}

workspace_up() {
  local output="text"
  local repo_input=""
  local image="$DEFAULT_IMAGE"
  local ws_name=""
  local pull="1"
  local repo_ref=""
  local repo_dir=""
  local no_clone="0"
  local host_override=""
  local use_secrets="1"
  local secrets_dir="$DEFAULT_SECRETS_DIR"
  local secrets_dir_explicit="0"
  local secrets_mount=""
  secrets_mount="$(normalize_secrets_mount "$DEFAULT_SECRETS_MOUNT")"
  local secrets_mount_explicit="0"
  if [[ "$DEFAULT_SECRETS_MOUNT" != "/home/agent/codex_secrets" ]]; then
    secrets_mount_explicit="1"
  fi
  local no_secrets_explicit="0"
  local codex_profile=""
  local start_tunnel="0"
  local tunnel_detach="0"
  local setup_git="0"

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --no-clone)
        no_clone="1"; shift ;;
      --host)
        host_override="${2:-}"; shift 2 ;;
      --name)
        ws_name="${2:-}"; shift 2 ;;
      --image)
        image="${2:-}"; shift 2 ;;
      --no-pull)
        pull="0"; shift ;;
      --ref)
        repo_ref="${2:-}"; shift 2 ;;
      --dir)
        repo_dir="${2:-}"; shift 2 ;;
      --setup-git)
        setup_git="1"; shift ;;
      --secrets-dir)
        secrets_dir="${2:-}"; secrets_dir_explicit="1"; shift 2 ;;
      --secrets-mount)
        secrets_mount="$(normalize_secrets_mount "${2:-}")"; secrets_mount_explicit="1"; shift 2 ;;
      --no-secrets)
        use_secrets="0"; no_secrets_explicit="1"; shift ;;
      --codex-profile)
        codex_profile="${2:-}"; shift 2 ;;
      --output)
        output="${2:-}"; shift 2 ;;
      --output=*)
        output="${1#*=}"; shift ;;
      --tunnel)
        start_tunnel="1"; shift ;;
      --tunnel-detach)
        start_tunnel="1"; tunnel_detach="1"; shift ;;
      -h|--help)
        usage; exit 0 ;;
      --)
        shift
        while [[ $# -gt 0 ]]; do
          if [[ -z "$repo_input" ]]; then
            repo_input="$1"
            shift
          else
            die "unexpected arg: $1"
          fi
        done
        ;;
      *)
        if [[ "$1" == -* ]]; then
          die "unknown flag: $1"
        fi
        if [[ -z "$repo_input" ]]; then
          repo_input="$1"
          shift
        else
          die "unexpected arg: $1"
        fi
        ;;
    esac
  done

  case "$output" in
    ""|text)
      output="text"
      ;;
    json)
      ;;
    *)
      die "unsupported --output: $output (expected: text|json)"
      ;;
  esac

  if [[ "$output" == "json" && "$start_tunnel" == "1" && "$tunnel_detach" != "1" ]]; then
    die "--output json cannot be combined with interactive --tunnel; use --tunnel-detach or run 'agent-workspace tunnel ...' separately"
  fi

  if [[ "$use_secrets" == "1" && "$secrets_mount_explicit" == "1" && "$secrets_dir_explicit" != "1" ]]; then
    die "--secrets-mount requires --secrets-dir (secrets are opt-in)"
  fi

  if [[ -n "$codex_profile" ]]; then
    if [[ "$use_secrets" != "1" ]]; then
      die "--codex-profile cannot be combined with --no-secrets"
    fi
    if [[ "$secrets_dir_explicit" != "1" ]]; then
      die "--codex-profile requires --secrets-dir (secrets are opt-in)"
    fi
  fi

  if [[ "$no_clone" != "1" && -z "$repo_input" ]]; then
    die "usage: agent-workspace up|create <repo> [flags]"
  fi

  if [[ "$output" == "json" ]]; then
    exec 3>&1
    exec 1>&2
  fi

  local host owner repo owner_repo clone_url
  host=""
  owner=""
  repo=""
  owner_repo=""
  clone_url=""
  if [[ -n "$repo_input" ]]; then
    read -r host owner repo < <(parse_repo "$repo_input")
    owner_repo="${owner}/${repo}"
    clone_url="https://${host}/${owner}/${repo}.git"
  else
    host="${host_override:-$DEFAULT_GITHUB_HOST}"
  fi
  local ts
  ts="$(timestamp)"

  if [[ -z "$ws_name" ]]; then
    if [[ -n "$owner_repo" ]]; then
      ws_name="$(slugify "${owner}-${repo}-${ts}")"
    else
      die "--no-clone requires --name when no repo is provided"
    fi
  else
    ws_name="$(slugify "$ws_name")"
  fi

  [[ -n "$ws_name" ]] || die "invalid workspace name"

  local container
  container="$(normalize_container_name "$ws_name")"
  local hostname="$container"
  local -i max_hostname_len=63
  if (( ${#hostname} > max_hostname_len )); then
    local -i tail_len=12
    local tail="${hostname: -tail_len}"
    local -i head_len=$(( max_hostname_len - 1 - tail_len ))
    hostname="${hostname:0:head_len}-${tail}"
    warn "container name too long for hostname; using: $hostname"
  fi

  local vol_work vol_home _
  read -r vol_work vol_home _ < <(volume_names "$container")

  local gh_token="${GH_TOKEN:-}"
  local github_token="${GITHUB_TOKEN:-}"

  ensure_image "$image" "$pull"

  if container_exists "$container"; then
    echo "workspace exists: $container"
    if [[ "$secrets_dir_explicit" == "1" || "$secrets_mount_explicit" == "1" || "$no_secrets_explicit" == "1" ]]; then
      warn "workspace already exists; mounts/env cannot be changed after creation"
      warn "if you need new mounts/env, run: $(basename "$0") rm ${container} && re-run up|create"
    fi
    ensure_container_running "$container"
  else
    local created="1"
	    local run_args=(
	      -d
	      --name "$container"
	      --hostname "$hostname"
	      --label "agent-kit.workspace=1"
	      --label "agent-kit.created-at=$ts"
	      -e "HOME=/home/agent"
	      -e "AGENT_HOME=/home/agent/.agents"
	      -e "AGENT_KIT_DIR=/opt/agent-kit"
	      -e "CODEX_AUTH_FILE=/home/agent/.codex/auth.json"
      -v "${vol_work}:/work"
      -v "${vol_home}:/home/agent"
	      -w /work
	    )
		    if [[ -n "$owner_repo" ]]; then
		      run_args+=( --label "agent-kit.repo=$owner_repo" )
		    fi

	    if [[ "$use_secrets" == "1" && "$secrets_dir_explicit" == "1" ]]; then
	      [[ -n "$secrets_dir" ]] || die "--secrets-dir requires a value"
	      [[ -d "$secrets_dir" ]] || die "secrets dir not found: $secrets_dir"
      run_args+=( -v "${secrets_dir}:${secrets_mount}:rw" )
      run_args+=( -e "CODEX_SECRET_DIR=${secrets_mount}" )
    elif [[ -n "$codex_profile" ]]; then
      die "--codex-profile requires --secrets-dir (secrets are opt-in)"
    fi

    echo "+ docker run ${container}"
    docker run "${run_args[@]}" "$image" sleep infinity >/dev/null
  fi

  # Ensure /work is writable for the default user even when backed by a fresh named volume.
  docker exec -u root "$container" bash -lc 'mkdir -p /work && chown -R agent:agent /work' >/dev/null

  if [[ -z "$repo_dir" ]]; then
    if [[ -n "$owner_repo" ]]; then
      repo_dir="/work/${owner}/${repo}"
    else
      repo_dir="/work"
    fi
  fi

  if [[ "$no_clone" == "1" ]]; then
    :
  else
    if docker exec "$container" test -d "${repo_dir%/}/.git"; then
      echo "repo already present: $repo_dir"
    else
      echo "+ clone $owner_repo -> $repo_dir"
      docker exec "$container" mkdir -p "$(dirname "$repo_dir")"

      local token_env=()
      [[ -n "$gh_token" ]] && token_env+=( -e "GH_TOKEN=$gh_token" )
      [[ -n "$github_token" ]] && token_env+=( -e "GITHUB_TOKEN=$github_token" )

      if ! docker exec "${token_env[@]}" "$container" bash -lc '
        set -euo pipefail
        repo_url="$1"
        dest="$2"
        ref="${3-}"

        mkdir -p "$(dirname "$dest")"

        if [[ -n "${GH_TOKEN:-${GITHUB_TOKEN:-}}" ]]; then
          askpass="/tmp/agent-workspace-git-askpass"
          cat >"$askpass" <<EOF
#!/usr/bin/env bash
case "\${1-}" in
  *Username*) echo "x-access-token" ;;
  *Password*) echo "\${GH_TOKEN:-\${GITHUB_TOKEN:-}}" ;;
  *) echo "" ;;
esac
EOF
          chmod 700 "$askpass"
          GIT_TERMINAL_PROMPT=0 GIT_ASKPASS="$askpass" git clone "$repo_url" "$dest"
          rm -f "$askpass"
        else
          GIT_TERMINAL_PROMPT=0 git clone "$repo_url" "$dest"
        fi

        if [[ -n "$ref" ]]; then
          git -C "$dest" checkout "$ref"
        fi
      ' -- "$clone_url" "$repo_dir" "$repo_ref"; then
        if [[ -z "$gh_token" && -z "$github_token" ]]; then
          die "git clone failed; if this repo is private, export GH_TOKEN or GITHUB_TOKEN on the host"
        fi
        die "git clone failed; verify GH_TOKEN/GITHUB_TOKEN scopes and repo access"
      fi
    fi
  fi

  if [[ "$setup_git" == "1" ]]; then
    local setup_host="${host_override:-$host}"
    [[ -n "$setup_host" ]] || setup_host="$DEFAULT_GITHUB_HOST"
    local token="${gh_token:-$github_token}"
    if setup_git_auth "$container" "$setup_host" "$token"; then
      :
    else
      warn "failed to configure git auth; git may prompt for credentials"
    fi
  fi

  if [[ -n "$codex_profile" ]]; then
    echo "+ codex-use $codex_profile"
    docker exec "$container" zsh -lic "codex-use ${codex_profile}" >/dev/null
  fi

  if [[ "$no_clone" != "1" ]]; then
    echo
    echo "git remote -v:"
    docker exec "$container" git -C "$repo_dir" remote -v
  fi

  echo
  echo "workspace:  $container"
  if [[ -n "$owner_repo" ]]; then
    echo "repo:       $owner_repo"
  else
    echo "repo:       (none)"
  fi
  echo "path:       $repo_dir"
  echo
  echo "Next:"
  echo "  - Shell:   docker exec -it -w /work ${container} zsh -l"
  echo "  - Tunnel:  $(basename "$0") tunnel ${container}"

  if [[ "$start_tunnel" == "1" ]]; then
    echo
    if [[ "$tunnel_detach" == "1" ]]; then
      workspace_tunnel "$container" --detach
    else
      workspace_tunnel "$container"
    fi
  fi

  if [[ "$output" == "json" ]]; then
    local created_flag="0"
    if container_exists "$container" && [[ "${created-0}" == "1" ]]; then
      created_flag="1"
    fi

    local repo_json="null"
    if [[ -n "$owner_repo" ]]; then
      repo_json="\"$(json_escape "$owner_repo")\""
    fi

    local secrets_enabled="0"
    if [[ "$use_secrets" == "1" && "$secrets_dir_explicit" == "1" ]]; then
      secrets_enabled="1"
    fi

    local secrets_dir_json="null"
    local secrets_mount_json="null"
    if [[ "$secrets_enabled" == "1" ]]; then
      secrets_dir_json="\"$(json_escape "$secrets_dir")\""
      secrets_mount_json="\"$(json_escape "$secrets_mount")\""
    fi

    local codex_profile_json="null"
    if [[ -n "$codex_profile" ]]; then
      codex_profile_json="\"$(json_escape "$codex_profile")\""
    fi

    local cmd_name="${CODEX_WORKSPACE_SUBCOMMAND:-up}"
    printf '{' >&3
    printf '"version":"%s",' "$(json_escape "$CODEX_WORKSPACE_VERSION")" >&3
    printf '"capabilities":%s,' "$(json_string_array "${CODEX_WORKSPACE_CAPABILITIES[@]}")" >&3
    printf '"command":"%s",' "$(json_escape "$cmd_name")" >&3
    printf '"workspace":"%s",' "$(json_escape "$container")" >&3
    printf '"created":%s,' "$(json_bool "$created_flag")" >&3
    printf '"repo":%s,' "$repo_json" >&3
    printf '"path":"%s",' "$(json_escape "$repo_dir")" >&3
    printf '"image":"%s",' "$(json_escape "$image")" >&3
    printf '"secrets":{"enabled":%s,"dir":%s,"mount":%s,"codex_profile":%s}' \
      "$(json_bool "$secrets_enabled")" \
      "$secrets_dir_json" \
      "$secrets_mount_json" \
      "$codex_profile_json" >&3
    printf '}\n' >&3

    exec 1>&3 3>&-
  fi
}

workspace_shell() {
  local name="$1"
  local container
  container="$(normalize_container_name "$name")"
  ensure_container_running "$container"
  docker exec -it -w /work "$container" zsh -l
}

workspace_tunnel() {
  local name="$1"
  shift

  local container
  container="$(normalize_container_name "$name")"

  local output="text"
  local tunnel_name_arg=""
  local detach="0"
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --name)
        tunnel_name_arg="${2:-}"; shift 2 ;;
      --name=*)
        tunnel_name_arg="${1#*=}"; shift ;;
      --detach)
        detach="1"; shift ;;
      --output)
        output="${2:-}"; shift 2 ;;
      --output=*)
        output="${1#*=}"; shift ;;
      -h|--help)
        usage; exit 0 ;;
      *)
        die "unknown flag: $1" ;;
    esac
  done

  case "$output" in
    ""|text)
      output="text"
      ;;
    json)
      ;;
    *)
      die "unsupported --output: $output (expected: text|json)"
      ;;
  esac

  if [[ "$output" == "json" ]]; then
    exec 3>&1
    exec 1>&2
  fi

  ensure_container_running "$container"
  docker exec "$container" bash -lc 'command -v code >/dev/null 2>&1' || die "missing 'code' in container (build with INSTALL_VSCODE=1)"

  local log_path="/home/agent/.agents-env/logs/code-tunnel.log"

  local tunnel_name=""
  if [[ -n "$tunnel_name_arg" ]]; then
    tunnel_name="$(tunnel_name_sanitize "$tunnel_name_arg")"
  else
    tunnel_name="$(tunnel_default_name "$container")"
  fi

  local -i max_len=20
  if (( ${#tunnel_name} > max_len )); then
    die "VS Code tunnel name too long (${#tunnel_name} > ${max_len}): $tunnel_name (use --name <short>)"
  fi

  local already_running="0"
  if docker exec "$container" bash -lc "pgrep -fa \"[c]ode-tunnel tunnel|[c]ode tunnel\" >/dev/null 2>&1"; then
    already_running="1"
    warn "code tunnel already running in $container"
    if [[ "$output" != "json" ]]; then
      echo "status:"
      docker exec "$container" bash -lc "code tunnel status 2>/dev/null || true"
      echo "log: $log_path"
      echo "tail: docker exec -it $container bash -lc 'tail -f $log_path'"
    fi
    if [[ "$output" == "json" ]]; then
      printf '{' >&3
      printf '"version":"%s",' "$(json_escape "$CODEX_WORKSPACE_VERSION")" >&3
      printf '"capabilities":%s,' "$(json_string_array "${CODEX_WORKSPACE_CAPABILITIES[@]}")" >&3
      printf '"command":"tunnel",' >&3
      printf '"workspace":"%s",' "$(json_escape "$container")" >&3
      printf '"tunnel_name":"%s",' "$(json_escape "$tunnel_name")" >&3
      printf '"detach":%s,' "$(json_bool "$detach")" >&3
      printf '"already_running":%s,' "$(json_bool "$already_running")" >&3
      printf '"log_path":"%s"' "$(json_escape "$log_path")" >&3
      printf '}\n' >&3
      exec 1>&3 3>&-
    fi
    return 0
  fi

  if [[ "$detach" == "1" ]]; then
    docker exec "$container" bash -lc "mkdir -p \"\$(dirname \"$log_path\")\" && : >\"$log_path\""
    docker exec -d "$container" bash -lc "code tunnel --accept-server-license-terms --name \"$tunnel_name\" >\"$log_path\" 2>&1"
    if [[ "$output" != "json" ]]; then
      echo "started: code tunnel ($tunnel_name) in $container"
      echo "status:"
      sleep 1
      docker exec "$container" bash -lc "code tunnel status 2>/dev/null || true"
      echo "log: $log_path"
      echo "tail: docker exec -it $container bash -lc 'tail -f $log_path'"
    fi
    if [[ "$output" == "json" ]]; then
      printf '{' >&3
      printf '"version":"%s",' "$(json_escape "$CODEX_WORKSPACE_VERSION")" >&3
      printf '"capabilities":%s,' "$(json_string_array "${CODEX_WORKSPACE_CAPABILITIES[@]}")" >&3
      printf '"command":"tunnel",' >&3
      printf '"workspace":"%s",' "$(json_escape "$container")" >&3
      printf '"tunnel_name":"%s",' "$(json_escape "$tunnel_name")" >&3
      printf '"detach":%s,' "$(json_bool "$detach")" >&3
      printf '"already_running":%s,' "$(json_bool "$already_running")" >&3
      printf '"log_path":"%s"' "$(json_escape "$log_path")" >&3
      printf '}\n' >&3
      exec 1>&3 3>&-
    fi
    return 0
  fi

  if [[ "$output" == "json" ]]; then
    die "--output json requires --detach for tunnel (non-interactive)"
  fi

  echo "Starting VS Code tunnel (name: $tunnel_name)."
  echo "If this is the first run, follow the device-code login prompts."
  docker exec -it "$container" code tunnel --accept-server-license-terms --name "$tunnel_name"
}

workspace_ls() {
  need_cmd docker
  docker ps -a --filter "label=agent-kit.workspace=1" --format '{{.Names}}' \
    | while IFS= read -r c; do
      [[ -n "$c" ]] || continue
      repo="$(docker inspect -f '{{ index .Config.Labels "agent-kit.repo" }}' "$c" 2>/dev/null || true)"
      created="$(docker inspect -f '{{ index .Config.Labels "agent-kit.created-at" }}' "$c" 2>/dev/null || true)"
      status="$(docker inspect -f '{{.State.Status}}' "$c" 2>/dev/null || true)"
      printf "%s\t%s\t%s\t%s\n" "$c" "$status" "$repo" "$created"
    done
}

workspace_start() {
  local name="$1"
  local container
  container="$(normalize_container_name "$name")"
  ensure_container_running "$container"
  echo "started: $container"
}

workspace_stop() {
  local name="$1"
  local container
  container="$(normalize_container_name "$name")"
  ensure_container_running "$container"
  echo "+ docker stop $container"
  docker stop "$container" >/dev/null
  echo "stopped: $container"
}

workspace_rm() {
  local name="$1"
  shift

  local container
  container="$(normalize_container_name "$name")"

  local rm_volumes="1"
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --keep-volumes)
        rm_volumes="0"; shift ;;
      --volumes)
        rm_volumes="1"; shift ;;
      -h|--help)
        usage; exit 0 ;;
      *)
        die "unknown flag: $1" ;;
    esac
  done

  if container_exists "$container"; then
    echo "+ docker rm -f $container"
    docker rm -f "$container" >/dev/null
  else
    warn "workspace not found: $container"
  fi

  if [[ "$rm_volumes" == "1" ]]; then
    read -r vol_work vol_home vol_codex < <(volume_names "$container")
    docker volume rm "$vol_work" "$vol_home" "$vol_codex" >/dev/null 2>&1 || true
    echo "volumes removed: $container"
  fi
}

main() {
  local cmd="${1:-}"
  shift || true

  case "$cmd" in
    --version)
      echo "$CODEX_WORKSPACE_VERSION"
      return 0
      ;;
    capabilities)
      emit_capabilities_json
      return 0
      ;;
    --supports)
      [[ $# -ge 1 ]] || die "usage: agent-workspace --supports <capability>"
      if supports_capability "$1"; then
        return 0
      fi
      return 1
      ;;
    -h|--help|help|"")
      usage
      return 0
      ;;
  esac

  need_cmd docker
  case "$cmd" in
    up)
      CODEX_WORKSPACE_SUBCOMMAND="up" workspace_up "$@" ;;
    create)
      CODEX_WORKSPACE_SUBCOMMAND="create" workspace_up "$@" ;;
    shell)
      [[ $# -ge 1 ]] || die "usage: agent-workspace shell <name>"
      workspace_shell "$1" ;;
    tunnel)
      [[ $# -ge 1 ]] || die "usage: agent-workspace tunnel <name> [--name <tunnel_name>] [--detach] [--output json]"
      workspace_tunnel "$1" "${@:2}" ;;
    ls)
      workspace_ls ;;
    start)
      [[ $# -ge 1 ]] || die "usage: agent-workspace start <name>"
      workspace_start "$1" ;;
    stop)
      [[ $# -ge 1 ]] || die "usage: agent-workspace stop <name>"
      workspace_stop "$1" ;;
    rm)
      [[ $# -ge 1 ]] || die "usage: agent-workspace rm <name> [--keep-volumes]"
      workspace_rm "$1" "${@:2}" ;;
    *)
      die "unknown command: $cmd" ;;
  esac
}

main "$@"
