#!/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

SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
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 [PATH] [claude-args...]   Launch Claude for a project (default: current dir)
  safeclaude build [PATH]              Rebuild the project's container from scratch
  safeclaude init [PATH]               Create a starter .safeclaude/ folder
  safeclaude envs                      List the containers and saved data safeclaude made
  safeclaude help                      Show this help

A project is any folder (or a parent of it) 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 leave .env
# out on purpose — changing a secret shouldn't force a rebuild.)
context_hash() {
  local sc="$1/.safeclaude"
  {
    find "$sc" -type f -not -name '.env' -not -name '.env.*' -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() {
  local target="${1:-.}"; [ $# -ge 1 ] && shift || true
  # If the first argument isn't a folder, it was meant for claude — so hand it
  # back to claude and use the current directory as the project.
  if [ ! -d "$target" ]; then set -- "$target" "$@"; target="."; fi

  local proj; proj="$(find_project_root "$target")" \
    || die "no .safeclaude/ found in '$target' or 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)

  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 "$@"
}

cmd_build() {
  local proj; proj="$(find_project_root "${1:-.}")" \
    || die "no .safeclaude/ found in '${1:-.}' or 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 target="${1:-.}"
  [ -d "$target" ] || die "not a directory: $target"
  local sc; sc="$(cd "$target" && pwd)/.safeclaude"
  [ -e "$sc" ] && die ".safeclaude/ already exists at $sc"
  mkdir -p "$sc/hooks"

  cat > "$sc/Dockerfile" <<'EOF'
# safeclaude builds this on top of its shared base. Everything here happens once
# and is cached, so it won't slow down your day-to-day launches.
FROM safeclaude-base:latest

# Add the system packages and language versions your project needs below.
# (You're root during the build, so apt just works.)
#
# Example — Ruby + Postgres client + headless Chrome (see the repo's example/):
#
# RUN apt-get update && apt-get install -y --no-install-recommends \
#       build-essential libssl-dev libreadline-dev zlib1g-dev \
#       libffi-dev libyaml-dev libpq-dev socat chromium chromium-driver \
#     && rm -rf /var/lib/apt/lists/*
#
# Tip: tools that need to stick around between runs (rbenv, nvm, installed gems)
# should be set up in hooks/, not here — the home folder is swapped in fresh each
# run, so anything installed into it during the build would just disappear.
EOF

  cat > "$sc/hooks/README.md" <<'EOF'
# Hooks

Each `*.sh` file here runs when the container starts, with your project available
at `/code`. They run in name order, so prefix them with numbers (`10-`, `20-`)
to control the sequence.

Because they run every launch, keep them quick and make them safe to re-run —
check whether the work is already done before doing it. If one fails, startup
stops rather than continuing half-configured.

Edits take effect on the next launch (no rebuild needed). Rename a
`*.sh.example` file to `*.sh` to turn it on.
EOF

  cat > "$sc/hooks/10-deps.sh.example" <<'EOF'
#!/bin/bash
# Installs your dependencies, but only when they've actually changed: it
# remembers the last lockfile it saw (saved between runs) and skips if it matches.
set -euo pipefail

install_if_changed() {
  local lockfile="$1"; shift
  [ -f "$lockfile" ] || return 0
  local marker="$HOME/.safeclaude-deps/$(echo "$lockfile" | tr / _).sha"
  mkdir -p "$(dirname "$marker")"
  local cur; cur="$(sha256sum "$lockfile" | cut -d' ' -f1)"
  if [ "$(cat "$marker" 2>/dev/null || true)" != "$cur" ]; then
    echo "[deps] $lockfile changed — installing..."
    "$@" && echo "$cur" > "$marker"
  fi
}

# Uncomment the ones your project uses:
# install_if_changed /code/Gemfile.lock        bash -lc 'cd /code && bundle install'
# install_if_changed /code/package-lock.json   bash -lc 'cd /code && npm ci'
# install_if_changed /code/requirements.txt    bash -lc 'pip install -r /code/requirements.txt'
EOF

  cat > "$sc/.env.example" <<'EOF'
# Put secrets here, then copy this file to .env (which stays out of git).
# Anything in .env is handed to the container when it starts — for example, a
# token for a private gem source:
# BUNDLE_GEMS__GRAPHQL__PRO=user:token
EOF

  cat > "$sc/.gitignore" <<'EOF'
.env
EOF

  echo "[safeclaude] created $sc"
  echo "  - edit Dockerfile to add the packages your project needs"
  echo "  - turn on a hook:  mv hooks/10-deps.sh.example hooks/10-deps.sh"
  echo "  - then run:        safeclaude $target"
}

main() {
  local cmd="${1:-run}"
  case "$cmd" in
    build)        shift; cmd_build "${1:-.}" ;;
    init)         shift; cmd_init "${1:-.}" ;;
    envs|ls|list) cmd_envs ;;
    help|-h|--help) usage ;;
    run)          shift; cmd_run "$@" ;;
    *)            cmd_run "$@" ;;   # default: PATH and/or claude args
  esac
}

main "$@"
