#!/usr/bin/env bash
#
# spawn-agent — bootstrap an isolated environment and launch an autonomous
# agent in it. Generic adaptation of the reservamos/platform-purchase pattern.
#
# Two isolation levels (your "hybrid"):
#   worktree   git worktree on the host, shared tools   (fast, lightweight)
#   container  worktree + its own Docker stack          (full OS/dep isolation)
#
# Container stacks get a unique COMPOSE_PROJECT_NAME and an APP_HOST_PORT of
# 8000+PORT_OFFSET, so many run in parallel without colliding.
#
# Container mode needs your Claude subscription token so the in-container agent
# authenticates as you (macOS keeps the normal credential in the Keychain, which
# a mount can't carry). Store it once — this script reads it automatically:
#
#   claude setup-token                 # prints the token
#   security add-generic-password -a "$USER" -s claude-code-oauth-token -w '<token>' -U
#
# Usage:
#   spawn-agent "Implement retry backoff in llm.py"
#   spawn-agent "Refactor config" --isolation container --port-offset 1100
#   spawn-agent "Fix bug" --agent agy --model smart
#   spawn-agent "Add tests" --branch tests/llm --setup-only
#
# Options:
#   --isolation worktree|container   default: worktree
#   --branch NAME                    branch name (default: agent/<slug>)
#   --base REF                       base ref for the branch (default: main)
#   --agent claude|agy               which agent to launch (default: claude)
#   --model PRESET                   agy model preset when --agent agy (default: smart)
#   --port-offset N                  container host-port offset (default: 1100)
#   --dangerous                      launch claude with --dangerously-skip-permissions
#                                    (default in container mode: the container is the sandbox)
#   --setup-only                     create the env, don't launch the agent
#   --dry-run                        print what would happen, do nothing
#   -h, --help
#
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
REPO_NAME="$(basename "$REPO_ROOT")"
KEYCHAIN_SERVICE="claude-code-oauth-token"

ISOLATION="worktree"
BRANCH=""
BASE="main"
AGENT="claude"
MODEL="smart"
PORT_OFFSET="1100"
DANGEROUS=0
DANGEROUS_SET=0
SETUP_ONLY=0
DRY_RUN=0
TASK=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --isolation)   ISOLATION="$2"; shift 2 ;;
    --branch)      BRANCH="$2"; shift 2 ;;
    --base)        BASE="$2"; shift 2 ;;
    --agent)       AGENT="$2"; shift 2 ;;
    --model)       MODEL="$2"; shift 2 ;;
    --port-offset) PORT_OFFSET="$2"; shift 2 ;;
    --dangerous)   DANGEROUS=1; DANGEROUS_SET=1; shift ;;
    --setup-only)  SETUP_ONLY=1; shift ;;
    --dry-run)     DRY_RUN=1; shift ;;
    -h|--help)     grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
    -*)            echo "spawn-agent: unknown option $1" >&2; exit 2 ;;
    *)             TASK="${TASK:+$TASK }$1"; shift ;;
  esac
done

[[ -z "$TASK" ]] && { echo "spawn-agent: provide a task description" >&2; exit 2; }
[[ "$ISOLATION" != "worktree" && "$ISOLATION" != "container" ]] && {
  echo "spawn-agent: --isolation must be worktree|container" >&2; exit 2; }

# Slug from the branch or the task text.
slug() { echo "$1" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-//;s/-$//' | cut -c1-32; }
SLUG="$(slug "${BRANCH:-$TASK}")"
[[ -z "$BRANCH" ]] && BRANCH="agent/$SLUG"
INSTANCE="$SLUG"
WORKTREE_DIR="$(dirname "$REPO_ROOT")/${REPO_NAME}-${SLUG}"
# Portable: prefix stacks by repo name, base the app port on APP_PORT (the
# app's in-container port; override per project, e.g. APP_PORT=3000 for Rails).
APP_PORT="${APP_PORT:-8000}"
APP_HOST_PORT=$((APP_PORT + PORT_OFFSET))
PROJECT="${REPO_NAME}-${INSTANCE}"
LOG="/tmp/agent-${SLUG}.log"

# Container mode is a sandbox, so default to skip-permissions there unless the
# user overrode it. Worktree mode shares the host, so stay safe by default.
if [[ "$ISOLATION" == "container" && "$DANGEROUS_SET" -eq 0 ]]; then
  DANGEROUS=1
fi

run() {
  if [[ "$DRY_RUN" -eq 1 ]]; then echo "  + $*"; else eval "$@"; fi
}

echo "spawn-agent"
echo "  task:       $TASK"
echo "  isolation:  $ISOLATION"
echo "  branch:     $BRANCH  (base $BASE)"
echo "  worktree:   $WORKTREE_DIR"
echo "  agent:      $AGENT$( [[ $AGENT == agy ]] && echo " ($MODEL)")"
[[ "$ISOLATION" == "container" ]] && echo "  stack:      $PROJECT  (host port $APP_HOST_PORT)"
echo ""

# 1. Worktree (both modes).
if git -C "$REPO_ROOT" show-ref --verify --quiet "refs/heads/$BRANCH"; then
  echo "branch $BRANCH exists; reusing"
fi
if [[ -d "$WORKTREE_DIR" ]]; then
  echo "worktree already exists at $WORKTREE_DIR"
else
  run "git -C '$REPO_ROOT' worktree add '$WORKTREE_DIR' -b '$BRANCH' '$BASE'"
fi

# 2. Container stack (container mode only).
if [[ "$ISOLATION" == "container" ]]; then
  export COMPOSE_PROJECT_NAME="$PROJECT"
  export STACK_IMAGE="${REPO_NAME}-dev:latest"
  export APP_HOST_PORT APP_PORT WORKTREE_DIR
  # gh token for private deps — read live from the host, nothing stored.
  export GH_TOKEN="$(gh auth token 2>/dev/null || echo '')"
  # Claude subscription token — use the env var if set, else read it from the
  # macOS Keychain (store once: bin/spawn-agent --help, or see docs). Never
  # printed or written to disk.
  if [[ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
    export CLAUDE_CODE_OAUTH_TOKEN="$(security find-generic-password -a "$USER" -s "$KEYCHAIN_SERVICE" -w 2>/dev/null || echo '')"
  fi
  # Fallback: a gitignored .env.local (portable backup across machines/projects).
  if [[ -z "$CLAUDE_CODE_OAUTH_TOKEN" && -f "$REPO_ROOT/.env.local" ]]; then
    line="$(grep -E '^[[:space:]]*CLAUDE_CODE_OAUTH_TOKEN=' "$REPO_ROOT/.env.local" | tail -1)"
    line="${line#*=}"; line="${line%\"}"; line="${line#\"}"
    export CLAUDE_CODE_OAUTH_TOKEN="$line"
  fi
  if [[ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]]; then
    echo "  warning: no CLAUDE_CODE_OAUTH_TOKEN — Claude inside the container" >&2
    echo "           won't authenticate. Store it once with:" >&2
    echo "           security add-generic-password -a \"\$USER\" -s $KEYCHAIN_SERVICE -w '<token>' -U" >&2
  fi
  COMPOSE="docker compose -f '$REPO_ROOT/.devcontainer/docker-compose.yml'"
  run "$COMPOSE up -d --build"
fi

if [[ "$SETUP_ONLY" -eq 1 ]]; then
  echo ""
  echo "setup complete (--setup-only). Launch the agent yourself:"
  if [[ "$ISOLATION" == "container" ]]; then
    echo "  COMPOSE_PROJECT_NAME=$PROJECT docker compose -f .devcontainer/docker-compose.yml exec app claude"
  else
    echo "  cd '$WORKTREE_DIR' && claude"
  fi
  exit 0
fi

# 3. Launch the agent, detached, with a logfile.
echo ""
echo "launching $AGENT → $LOG"
if [[ "$AGENT" == "agy" ]]; then
  run "cd '$WORKTREE_DIR' && nohup agy -p '$TASK' --model '$MODEL' --print-timeout 10m > '$LOG' 2>&1 &"
elif [[ "$ISOLATION" == "container" ]]; then
  FLAGS=""; [[ "$DANGEROUS" -eq 1 ]] && FLAGS="--dangerously-skip-permissions"
  run "COMPOSE_PROJECT_NAME=$PROJECT docker compose -f '$REPO_ROOT/.devcontainer/docker-compose.yml' exec -d app bash -lc \"claude -p '$TASK' $FLAGS > /workspace/.agent.log 2>&1\""
  echo "  (log inside container: $WORKTREE_DIR/.agent.log)"
else
  FLAGS=""; [[ "$DANGEROUS" -eq 1 ]] && FLAGS="--dangerously-skip-permissions"
  run "cd '$WORKTREE_DIR' && nohup claude -p '$TASK' $FLAGS > '$LOG' 2>&1 &"
fi

echo ""
echo "agent launched. Watch it:   tail -f $LOG"
echo "tear down when done:"
if [[ "$ISOLATION" == "container" ]]; then
  echo "  COMPOSE_PROJECT_NAME=$PROJECT docker compose -f .devcontainer/docker-compose.yml down -v"
fi
echo "  git worktree remove '$WORKTREE_DIR'"
