#!/usr/bin/env bash
# safeclaude — run Claude in a locked-down container, one per project.
#
# Like nvm/pyenv, but for whole environments: each project keeps its setup in a
# .safeclaude/ folder. We find it, build the container once, then reuse it.
set -euo pipefail

SAFECLAUDE_VERSION="0.1.0"
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
SKELETON_DIR="$SCRIPT_DIR/skeleton"
BASE_IMAGE="safeclaude-base:latest"

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

usage() {
  cat >&2 <<'EOF'
safeclaude — run Claude in a locked-down container, one per project

Usage:
  safeclaude [claude-args...]   Launch Claude for the project here (args pass to claude)
  safeclaude build              Rebuild the project's container from scratch
  safeclaude init               Create a starter .safeclaude/ in the current directory
  safeclaude envs               List the containers and saved data safeclaude made
  safeclaude version            Print the safeclaude version
  safeclaude help               Show this help

Run from inside a project — the current directory, or the nearest parent that
has a .safeclaude/ folder.
EOF
}

# Turn a folder name into something Docker is happy to use as a name.
proj_name() { basename "$1" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9_.-' '-'; }

# Look in this folder, then its parents, for a project (one that has a
# .safeclaude/) — so you can launch from a subdirectory and still find it.
find_project_root() {
  local dir; dir="$(cd "$1" 2>/dev/null && pwd)" || return 1
  while [ -n "$dir" ] && [ "$dir" != "/" ]; do
    [ -f "$dir/.safeclaude/Dockerfile" ] && { echo "$dir"; return 0; }
    dir="$(dirname "$dir")"
  done
  [ -f "/.safeclaude/Dockerfile" ] && { echo "/"; return 0; }
  return 1
}

# Fingerprint the project's setup so we can tell when it changed. If the
# fingerprint matches what we already built, we skip rebuilding. We skip .env
# (changing a secret shouldn't force a rebuild) and cache/ (it's scratch space
# the hooks write to constantly — hashing it would rebuild on every run).
context_hash() {
  local sc="$1/.safeclaude"
  {
    find "$sc" -type f -not -name '.env' -not -name '.env.*' -not -path "$sc/cache/*" -print0 \
      | sort -z | xargs -0 sha256sum
    docker image inspect --format '{{.Id}}' "$BASE_IMAGE" 2>/dev/null || true
  } | sha256sum | cut -c1-12
}

ensure_base() {
  docker image inspect "$BASE_IMAGE" &>/dev/null && return 0
  echo "[safeclaude] building base image $BASE_IMAGE ..." >&2
  docker build -t "$BASE_IMAGE" -f "$SCRIPT_DIR/Dockerfile.base" "$SCRIPT_DIR" >&2
}

# Build the project's container if we haven't already (force=1 rebuilds it
# regardless). Prints the name of the image so the caller can run it.
ensure_project_image() {
  local proj="$1" force="${2:-0}" image
  image="safeclaude-$(proj_name "$proj"):$(context_hash "$proj")"
  if [ "$force" = "1" ] || ! docker image inspect "$image" &>/dev/null; then
    echo "[safeclaude] building $image ..." >&2
    docker build -t "$image" "$proj/.safeclaude" >&2
  fi
  echo "$image"
}

cmd_run() {
  # Always operate on the current directory's project; every argument is for claude.
  local proj; proj="$(find_project_root ".")" \
    || die "no .safeclaude/ found here or in any parent — run: safeclaude init"

  ensure_base
  local image; image="$(ensure_project_image "$proj")"

  local home_vol="safeclaude-home-$(proj_name "$proj")"
  local env_args=() tty_args=()
  [ -f "$proj/.safeclaude/.env" ] && env_args=(--env-file "$proj/.safeclaude/.env")
  [ -t 0 ] && tty_args=(-it)

  # Tell the inner Claude where it is and what it can/can't do. The host path is
  # included so it can give the developer exact instructions. Built here (not in
  # the container) because only the launcher knows the real host path.
  local sys_prompt
  sys_prompt="You are running inside safeclaude, a sandboxed Docker container — not directly on the host machine.

- This project is mounted at /code. On the host it lives at: ${proj}
- You run as the non-root user 'coder'. You cannot use sudo or install system packages.
- The .safeclaude/ directory configures this container. NEVER edit anything under .safeclaude/ yourself — instead, tell the developer exactly what to change and have them do it on the host.
- To add a system package: ask the developer to add it to .safeclaude/Dockerfile and run 'safeclaude build' on the host.
- To add setup that runs at container startup: ask the developer to add a script to .safeclaude/hooks/.
- /home/coder persists between runs. Edits under /code are real and shared with the host. Everything else is discarded when the container exits."

  exec docker run --rm "${tty_args[@]}" \
    --user 1001:1001 \
    --cap-drop ALL \
    --security-opt no-new-privileges:true \
    --add-host host.docker.internal:host-gateway \
    -v "${home_vol}:/home/coder" \
    -v "${proj}:/code" \
    -w /code \
    "${env_args[@]}" \
    "$image" claude --append-system-prompt "$sys_prompt" "$@"
}

cmd_build() {
  local proj; proj="$(find_project_root ".")" \
    || die "no .safeclaude/ found here or in any parent — run: safeclaude init"
  ensure_base
  ensure_project_image "$proj" 1 >/dev/null
  echo "[safeclaude] env ready for $(proj_name "$proj")"
}

cmd_envs() {
  echo "Built env images:"
  docker images --filter 'reference=safeclaude-*' \
    --format '  {{.Repository}}:{{.Tag}}  ({{.Size}}, {{.CreatedSince}})' || true
  echo
  echo "Home volumes:"
  docker volume ls --filter 'name=safeclaude-home-' --format '  {{.Name}}' || true
}

cmd_init() {
  local sc; sc="$(pwd)/.safeclaude"
  [ -e "$sc" ] && die ".safeclaude/ already exists at $sc"
  [ -d "$SKELETON_DIR" ] || die "skeleton not found at $SKELETON_DIR"

  # Copy the template files (see skeleton/), then add the bits that are generated
  # rather than templated: the cache dir and the version stamp.
  mkdir -p "$sc"
  cp -R "$SKELETON_DIR/." "$sc/"
  mkdir -p "$sc/cache"
  echo "$SAFECLAUDE_VERSION" > "$sc/version"

  echo "[safeclaude] created $sc"
  echo "  - edit Dockerfile to add the packages and language versions you need"
  echo "  - edit hooks/10-setup.sh for any startup setup"
  echo "  - then run:        safeclaude"
}

main() {
  local cmd="${1:-run}"
  case "$cmd" in
    build)        cmd_build ;;
    init)         cmd_init ;;
    envs|ls|list) cmd_envs ;;
    version|-v|--version) echo "safeclaude $SAFECLAUDE_VERSION" ;;
    help|-h|--help) usage ;;
    run)          shift; cmd_run "$@" ;;
    *)            cmd_run "$@" ;;   # default: all args go to claude
  esac
}

main "$@"
