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

# SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0

# secrets — encrypted env file sync between machines
# Uses age key-file encryption + a private git repo.

# F5: explicit HOME check. `set -u` would already error if HOME were unset,
# but the message would be cryptic ("HOME: unbound variable" pointing at
# the SECRETS_DIR default-init line). Check up front with a directed message
# so cron/sudo/CI users know what to fix.
: "${HOME:?HOME is not set; secrets needs a home directory to find or create the store}"

# Capture the user-provided SECRETS_DIR (if any) before defaulting.
# resolve_store() uses this to honor SECRETS_DIR as the legacy escape hatch
# while letting .secrets-store files take precedence per project.
_USER_SECRETS_DIR="${SECRETS_DIR:-}"
SECRETS_DIR="${SECRETS_DIR:-$HOME/.secrets}"
KEY_FILE="$SECRETS_DIR/key.txt"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Set by resolve_store(). Reports which rule chose SECRETS_DIR.
STORE_SOURCE="default"
# Path to the .secrets-store file that won resolution, if any.
_LAST_FOUND_AT=""
# --store flag value, captured by the pre-pass.
STORE_OVERRIDE=""

# ─── Helpers ───────────────────────────────────────────────────────────

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

check_cmd() {
  command -v "$1" >/dev/null 2>&1 || die "'$1' is not installed. Run: brew install $1"
}

check_initialized() {
  if [ -d "$SECRETS_DIR/.git" ]; then
    return
  fi
  if [ "$STORE_SOURCE" != "default" ]; then
    die "Store not initialized: $SECRETS_DIR
  Resolved from: $STORE_SOURCE
  This path doesn't exist on this machine yet.

  If you're joining a teammate's existing store:
    git clone <their-store-remote> $SECRETS_DIR
    # then copy their key.txt to $SECRETS_DIR/key.txt

  If you want a fresh new store at this path:
    secrets --store $SECRETS_DIR init"
  fi
  die "Not initialized. Run: secrets init"
}

check_key() {
  if [ -f "$KEY_FILE" ]; then
    return
  fi
  if [ "$STORE_SOURCE" != "default" ]; then
    die "Key file not found at $KEY_FILE
  The store at $SECRETS_DIR exists but has no key.txt.
  Resolved from: $STORE_SOURCE

  If joining a teammate's store: copy their key.txt to $KEY_FILE.
  If this is your own new store: re-run init for this store path."
  fi
  die "Key file not found at $KEY_FILE. Run: secrets init"
}

get_pubkey() {
  age-keygen -y "$KEY_FILE" 2>/dev/null || die "Failed to derive public key from $KEY_FILE"
}

derive_project_name() {
  local explicit="${1:-}"
  if [ -n "$explicit" ]; then
    echo "$explicit"
    return
  fi
  # Try git remote basename
  local remote
  remote=$(git config --get remote.origin.url 2>/dev/null || true)
  if [ -n "$remote" ]; then
    basename "$remote" .git
    return
  fi
  # Fall back to current directory name
  basename "$PWD"
}

# ─── Store resolution ──────────────────────────────────────────────────
#
# Resolution order (highest precedence first):
#   1. --store <dir>  flag (captured by main pre-pass into STORE_OVERRIDE)
#   2. .secrets-store file in cwd or any ancestor up to but NOT including $HOME
#   3. SECRETS_DIR    env var (legacy escape hatch)
#   4. $HOME/.secrets default
#
# .secrets-store file format: first non-empty non-comment line is the store
# spec. Spec is one of:
#   - absolute path (/Users/you/.secrets-work)
#   - ~/path        (expanded to $HOME/path)
#   - bare name     (e.g. "work" → $HOME/.secrets-work; "default" → $HOME/.secrets)
# Comments (#) and CRLF line endings are tolerated. NO shell expansion is
# applied to the file content — `$VAR`, `$(...)`, and backticks are read as
# literal characters to prevent code injection from a committed file.

# Take a store spec (path or bare name) and return an absolute directory.
_expand_store_path() {
  local path="$1"
  case "$path" in
    /*)
      echo "$path"
      ;;
    '~')
      echo "$HOME"
      ;;
    '~/'*)
      echo "$HOME/${path#\~/}"
      ;;
    .*|*/*)
      # Relative path or path containing /. Treat as path relative to cwd.
      echo "$path"
      ;;
    default)
      # Sugar: --store default → the canonical default store
      echo "$HOME/.secrets"
      ;;
    *)
      # Bare name like "work" → $HOME/.secrets-work
      echo "$HOME/.secrets-$path"
      ;;
  esac
}

# Read a .secrets-store file. Print the first non-empty non-comment line,
# trimmed. Return 1 if no usable line is found.
_parse_secrets_store_file() {
  local file="$1"
  local line
  while IFS= read -r line || [ -n "$line" ]; do
    # Strip CRLF
    line="${line%$'\r'}"
    # Trim leading whitespace
    line="${line#"${line%%[![:space:]]*}"}"
    # Trim trailing whitespace
    line="${line%"${line##*[![:space:]]}"}"
    [ -z "$line" ] && continue
    case "$line" in '#'*) continue ;; esac
    printf '%s\n' "$line"
    return 0
  done < "$file"
  return 1
}

# Walk up from cwd looking for .secrets-store. Bounded by $HOME — never
# walks INTO or PAST $HOME. If cwd is outside $HOME entirely (e.g. /tmp),
# the walk does not run. Symlinks are resolved with `cd -P`.
# On success: prints expanded store dir, sets _LAST_FOUND_AT, returns 0.
_find_secrets_store_file() {
  local dir
  dir=$(pwd -P 2>/dev/null) || dir="$PWD"
  local home_resolved
  home_resolved=$(cd -P "$HOME" 2>/dev/null && pwd -P) || home_resolved="$HOME"

  while [ -n "$dir" ] && [ "$dir" != "/" ] && [ "$dir" != "$home_resolved" ]; do
    # Bound: only walk while we're strictly below $HOME.
    case "$dir" in
      "$home_resolved"/*) ;;
      *) return 1 ;;
    esac

    # F2: never follow a symlinked .secrets-store. A committed symlink
    # could point at any user-readable file (~/.aws/credentials, /etc/passwd)
    # and trick the resolver into reading attacker-chosen content.
    if [ -L "$dir/.secrets-store" ]; then
      :
    elif [ -f "$dir/.secrets-store" ]; then
      local content
      if content=$(_parse_secrets_store_file "$dir/.secrets-store"); then
        local expanded
        expanded=$(_expand_store_path "$content")
        _LAST_FOUND_AT="$dir/.secrets-store"
        printf '%s\n' "$expanded"
        return 0
      fi
    fi
    dir=$(dirname "$dir")
  done
  return 1
}

# Resolve the active store directory and update SECRETS_DIR + KEY_FILE.
# Sets STORE_SOURCE to one of:
#   "--store flag" | ".secrets-store file (<path>)" | "SECRETS_DIR env var" | "default"
resolve_store() {
  local resolved=""
  local source=""

  if [ -n "${STORE_OVERRIDE:-}" ]; then
    resolved=$(_expand_store_path "$STORE_OVERRIDE")
    source="--store flag"
    _LAST_FOUND_AT=""
  elif resolved=$(_find_secrets_store_file); then
    source=".secrets-store file ($_LAST_FOUND_AT)"
  elif [ -n "${_USER_SECRETS_DIR:-}" ]; then
    resolved="$_USER_SECRETS_DIR"
    source="SECRETS_DIR env var"
    _LAST_FOUND_AT=""
  else
    resolved="$HOME/.secrets"
    source="default"
    _LAST_FOUND_AT=""
  fi

  SECRETS_DIR="$resolved"
  KEY_FILE="$SECRETS_DIR/key.txt"
  STORE_SOURCE="$source"
}

# Echo "==> Store: ..." when SECRETS_DIR is not the canonical default.
# Called from cmd_push/cmd_pull (and their workspace variants) after resolution.
echo_store_if_non_default() {
  if [ "$SECRETS_DIR" != "$HOME/.secrets" ]; then
    info "Store: $SECRETS_DIR (from $STORE_SOURCE)"
  fi
}

# ─── End store resolution ──────────────────────────────────────────────

# Collect secret files from a directory:
#   .env, .env.*, .dev.vars (excluding .envrc, .environment-*)
# Sets the COLLECTED_FILES array. Returns 1 if no files found.
collect_env_files() {
  local dir="$1"
  COLLECTED_FILES=()
  for f in "$dir"/.env "$dir"/.env.* "$dir"/.dev.vars; do
    [ -f "$f" ] || continue
    local basename_f
    basename_f=$(basename "$f")
    case "$basename_f" in
      .envrc|.environment*) continue ;;
    esac
    COLLECTED_FILES+=("$f")
  done
  [ ${#COLLECTED_FILES[@]} -gt 0 ]
}

# Read package.json workspaces and expand globs to actual directories.
# Prints one workspace path per line (relative to the monorepo root).
get_workspaces() {
  local root="$1"
  local pkg="$root/package.json"
  [ -f "$pkg" ] || die "No package.json found in $root"
  check_cmd jq

  local patterns
  patterns=$(jq -r '.workspaces // .workspaces.packages // empty | .[]' "$pkg" 2>/dev/null)
  [ -n "$patterns" ] || die "No workspaces field in $pkg"

  # Expand each glob pattern relative to root
  local old_dir="$PWD"
  cd "$root"
  for pattern in $patterns; do
    # Use bash glob expansion
    for dir in $pattern; do
      [ -d "$dir" ] && echo "$dir"
    done
  done
  cd "$old_dir"
}

install_hook() {
  local hook_src="$SCRIPT_DIR/hooks/pre-commit"
  local hook_dst="$SECRETS_DIR/.git/hooks/pre-commit"
  if [ -f "$hook_src" ]; then
    cp "$hook_src" "$hook_dst"
    chmod +x "$hook_dst"
  else
    # Inline hook if template not found (e.g. secrets installed standalone)
    cat > "$hook_dst" << 'HOOKEOF'
#!/usr/bin/env bash
BLOCKED=$(git diff --cached --name-only | grep -E '\.(env|dev\.vars)' | grep -v '\.age$' || true)
if [ -n "$BLOCKED" ]; then
  echo "ERROR: Plaintext secret files staged for commit:"
  echo "$BLOCKED"
  echo "Only .age (encrypted) files should be committed."
  exit 1
fi
HOOKEOF
    chmod +x "$hook_dst"
  fi
}

# ─── Subcommands ───────────────────────────────────────────────────────

cmd_init() {
  check_cmd age
  check_cmd git
  resolve_store

  if [ -d "$SECRETS_DIR/.git" ]; then
    die "Already initialized at $SECRETS_DIR. Key file preserved."
  fi

  info "Initializing secrets repo at $SECRETS_DIR"
  mkdir -p "$SECRETS_DIR"
  git init "$SECRETS_DIR" >/dev/null

  # Generate age key pair
  info "Generating age key pair"
  age-keygen -o "$KEY_FILE" 2>&1

  # Write .gitignore
  cat > "$SECRETS_DIR/.gitignore" << 'EOF'
# Never commit the private key
key.txt

# Block plaintext secret files
**/.env
**/.env.*
**/.dev.vars

# Allow encrypted files
!**/.env.age
!**/.env.*.age
!**/.dev.vars.age
EOF

  # Install pre-commit hook
  mkdir -p "$SECRETS_DIR/.git/hooks"
  install_hook

  local pubkey
  pubkey=$(get_pubkey)

  info "Done! Your public key is:"
  echo "  $pubkey"
  echo ""
  echo "Next steps:"
  echo "  1. Add a remote:  cd $SECRETS_DIR && git remote add origin <url>"
  echo "  2. Copy $KEY_FILE to your other machine (AirDrop, scp, USB)"
  echo "  3. Run 'secrets push <project>' from a project directory"
}

# Encrypt env files from a source dir into a project path in the secrets repo.
# Does NOT commit or push — caller handles that.
push_dir_to_project() {
  local source_dir="$1"
  local project="$2"
  local pubkey="$3"

  if ! collect_env_files "$source_dir"; then
    return 1
  fi

  info "$project: ${#COLLECTED_FILES[@]} file(s)"
  for f in "${COLLECTED_FILES[@]}"; do
    echo "  $(basename "$f")"
  done

  mkdir -p "$SECRETS_DIR/$project"
  for f in "${COLLECTED_FILES[@]}"; do
    local name
    name=$(basename "$f")
    age -r "$pubkey" -o "$SECRETS_DIR/$project/${name}.age" "$f"
  done
  return 0
}

# Git commit + push for the secrets repo. Shared by push and push --workspaces.
commit_and_push_secrets() {
  local message="$1"

  if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
    if ! git -C "$SECRETS_DIR" pull --ff-only 2>/dev/null; then
      die "Fast-forward pull failed. Run 'secrets pull' first, then retry push."
    fi
  fi

  git -C "$SECRETS_DIR" add -A
  if git -C "$SECRETS_DIR" diff --cached --quiet 2>/dev/null; then
    info "No changes to push (secrets unchanged)"
    return
  fi
  git -C "$SECRETS_DIR" commit -m "$message" >/dev/null
  if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
    git -C "$SECRETS_DIR" push >/dev/null 2>&1
    info "Pushed to remote"
  else
    info "Committed locally (no remote configured)"
  fi
}

cmd_push() {
  check_cmd age
  check_cmd git
  resolve_store
  check_initialized
  check_key

  local project
  project=$(derive_project_name "${1:-}")
  info "Pushing secrets for project: $project"
  echo_store_if_non_default

  local pubkey
  pubkey=$(get_pubkey)

  if ! push_dir_to_project "$PWD" "$project" "$pubkey"; then
    die "No secret files (.env, .env.*, .dev.vars) found in $PWD"
  fi

  commit_and_push_secrets "update $project"
}

cmd_push_workspaces() {
  check_cmd age
  check_cmd git
  check_cmd jq
  resolve_store
  check_initialized
  check_key

  local root="$PWD"
  local monorepo_name
  monorepo_name=$(derive_project_name "")
  info "Pushing workspaces for monorepo: $monorepo_name"
  echo_store_if_non_default

  local pubkey
  pubkey=$(get_pubkey)
  local total=0

  # Push root env files (if any)
  if push_dir_to_project "$root" "$monorepo_name" "$pubkey"; then
    total=$((total + ${#COLLECTED_FILES[@]}))
  fi

  # Push each workspace
  local workspaces
  workspaces=$(get_workspaces "$root")
  while IFS= read -r ws; do
    [ -n "$ws" ] || continue
    local ws_dir="$root/$ws"
    local ws_project="$monorepo_name/$ws"
    if push_dir_to_project "$ws_dir" "$ws_project" "$pubkey"; then
      total=$((total + ${#COLLECTED_FILES[@]}))
    fi
  done <<< "$workspaces"

  if [ "$total" -eq 0 ]; then
    die "No secret files found in any workspace"
  fi

  commit_and_push_secrets "update $monorepo_name workspaces"
}

cmd_pull() {
  check_cmd age
  check_cmd git
  resolve_store
  check_initialized
  check_key

  local project
  project=$(derive_project_name "${1:-}")
  local target_dir="$PWD"
  info "Pulling secrets for project: $project"
  echo_store_if_non_default

  # Pull latest
  if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
    git -C "$SECRETS_DIR" pull >/dev/null 2>&1
  fi

  # Check project exists
  if [ ! -d "$SECRETS_DIR/$project" ]; then
    die "Project '$project' not found. Run: secrets list"
  fi

  # Decrypt each .age file into target dir (including dotfiles)
  local count=0
  for f in "$SECRETS_DIR/$project"/*.age "$SECRETS_DIR/$project"/.*.age; do
    [ -f "$f" ] || continue
    local name
    name=$(basename "$f" .age)
    local outfile="$target_dir/$name"
    age -d -i "$KEY_FILE" -o "$outfile" "$f"
    # Integrity check: verify non-empty
    if [ ! -s "$outfile" ]; then
      echo "WARNING: Decrypted file '$name' is empty (possibly truncated .age blob)"
    fi
    count=$((count + 1))
  done

  info "Decrypted $count file(s) into $target_dir"

  # Reinstall hook if missing
  if [ ! -x "$SECRETS_DIR/.git/hooks/pre-commit" ]; then
    install_hook
    info "Reinstalled pre-commit hook"
  fi
}

# Pull and decrypt .age files from a project path into a target directory.
# Does NOT do git pull — caller handles that.
pull_project_to_dir() {
  local project="$1"
  local target_dir="$2"
  local project_dir="$SECRETS_DIR/$project"

  [ -d "$project_dir" ] || return 1

  local count=0
  for f in "$project_dir"/*.age "$project_dir"/.*.age; do
    [ -f "$f" ] || continue
    local name
    name=$(basename "$f" .age)
    local outfile="$target_dir/$name"
    age -d -i "$KEY_FILE" -o "$outfile" "$f"
    if [ ! -s "$outfile" ]; then
      echo "WARNING: Decrypted file '$name' is empty (possibly truncated .age blob)"
    fi
    count=$((count + 1))
  done
  echo "$count"
}

cmd_pull_workspaces() {
  check_cmd age
  check_cmd git
  check_cmd jq
  resolve_store
  check_initialized
  check_key

  local root="$PWD"
  local monorepo_name
  monorepo_name=$(derive_project_name "")
  info "Pulling workspaces for monorepo: $monorepo_name"
  echo_store_if_non_default

  # Pull latest from remote
  if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
    git -C "$SECRETS_DIR" pull >/dev/null 2>&1
  fi

  local total=0

  # Pull root secrets (if any)
  if [ -d "$SECRETS_DIR/$monorepo_name" ]; then
    local count
    count=$(pull_project_to_dir "$monorepo_name" "$root")
    total=$((total + count))
    info "$monorepo_name (root): $count file(s)"
  fi

  # Pull each workspace
  local workspaces
  workspaces=$(get_workspaces "$root")
  while IFS= read -r ws; do
    [ -n "$ws" ] || continue
    local ws_dir="$root/$ws"
    local ws_project="$monorepo_name/$ws"
    if [ -d "$SECRETS_DIR/$ws_project" ]; then
      local count
      count=$(pull_project_to_dir "$ws_project" "$ws_dir")
      total=$((total + count))
      info "$ws_project: $count file(s)"
    fi
  done <<< "$workspaces"

  if [ "$total" -eq 0 ]; then
    die "No secrets found for any workspace in $monorepo_name"
  fi

  info "Decrypted $total file(s) total"

  # Reinstall hook if missing
  if [ ! -x "$SECRETS_DIR/.git/hooks/pre-commit" ]; then
    install_hook
    info "Reinstalled pre-commit hook"
  fi
}

cmd_list() {
  resolve_store
  check_initialized

  local found=0
  for dir in "$SECRETS_DIR"/*/; do
    [ -d "$dir" ] || continue
    local project
    project=$(basename "$dir")
    # Skip hidden dirs
    [[ "$project" == .* ]] && continue
    echo "$project:"
    for f in "$dir"*.age "$dir".*.age; do
      [ -f "$f" ] || continue
      echo "  $(basename "$f" .age)"
      found=1
    done
  done

  if [ "$found" -eq 0 ]; then
    echo "No projects found. Run 'secrets push <project>' to add one."
  fi

  # DX-7 hint: when a non-default store is active, point users at `secrets which`.
  if [ "$SECRETS_DIR" != "$HOME/.secrets" ]; then
    echo ""
    info "Showing projects in $SECRETS_DIR. Run 'secrets which' for details."
  fi
}

cmd_rm() {
  check_cmd git
  resolve_store
  check_initialized

  local project="${1:-}"
  [ -n "$project" ] || die "Usage: secrets rm <project>"

  if [ ! -d "$SECRETS_DIR/$project" ]; then
    die "Project '$project' not found. Run: secrets list"
  fi

  info "Removing project: $project"
  git -C "$SECRETS_DIR" rm -r "$project/" >/dev/null
  git -C "$SECRETS_DIR" commit -m "remove $project" >/dev/null
  if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
    git -C "$SECRETS_DIR" push >/dev/null 2>&1
    info "Removed $project from remote"
  else
    info "Removed $project locally (no remote configured)"
  fi
}

cmd_rekey() {
  check_cmd age
  check_cmd git
  resolve_store
  check_initialized
  check_key

  # Create temp dir with cleanup trap
  local tmpdir
  tmpdir=$(mktemp -d)
  trap 'rm -rf "$tmpdir"' EXIT INT TERM

  info "Decrypting all files with current key..."

  # Decrypt all .age files into temp dir
  local file_count=0
  for dir in "$SECRETS_DIR"/*/; do
    [ -d "$dir" ] || continue
    local project
    project=$(basename "$dir")
    [[ "$project" == .* ]] && continue
    mkdir -p "$tmpdir/$project"
    for f in "$dir"*.age "$dir".*.age; do
      [ -f "$f" ] || continue
      local name
      name=$(basename "$f" .age)
      if ! age -d -i "$KEY_FILE" -o "$tmpdir/$project/$name" "$f"; then
        die "Decryption failed for $project/$name. Rekey aborted. Old key preserved."
      fi
      file_count=$((file_count + 1))
    done
  done

  if [ "$file_count" -eq 0 ]; then
    die "No encrypted files found. Nothing to rekey."
  fi

  info "Decrypted $file_count file(s). Generating new key pair..."

  # Generate new key (overwrites old)
  age-keygen -o "$KEY_FILE" 2>&1
  local pubkey
  pubkey=$(get_pubkey)

  info "Re-encrypting all files with new key..."

  # Re-encrypt all files
  for dir in "$tmpdir"/*/; do
    [ -d "$dir" ] || continue
    local project
    project=$(basename "$dir")
    mkdir -p "$SECRETS_DIR/$project"
    for f in "$dir"*; do
      [ -f "$f" ] || continue
      local name
      name=$(basename "$f")
      age -r "$pubkey" -o "$SECRETS_DIR/$project/${name}.age" "$f"
    done
  done

  # Commit and push
  git -C "$SECRETS_DIR" add -A
  git -C "$SECRETS_DIR" commit -m "rekey all secrets" >/dev/null
  if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
    git -C "$SECRETS_DIR" push >/dev/null 2>&1
    info "Pushed rekeyed secrets to remote"
  else
    info "Committed rekeyed secrets locally (no remote configured)"
  fi

  info "Rekey complete!"
  echo ""
  echo "IMPORTANT: Copy new key to your other machine:"
  echo "  scp $KEY_FILE <other-machine>:$KEY_FILE"
  echo ""
  echo "WARNING: Old ciphertext remains in git history."
  echo "For full rotation, create a fresh repo."
}

cmd_clear() {
  local dir="$PWD"
  if ! collect_env_files "$dir"; then
    info "No secret files to clear in $dir"
    return
  fi

  local count=0
  for f in "${COLLECTED_FILES[@]}"; do
    rm "$f"
    count=$((count + 1))
  done
  info "Cleared $count secret file(s) from $dir"
}

cmd_clear_workspaces() {
  check_cmd jq

  local root="$PWD"
  local total=0

  # Clear root
  if collect_env_files "$root"; then
    for f in "${COLLECTED_FILES[@]}"; do
      rm "$f"
      total=$((total + 1))
    done
  fi

  # Clear each workspace
  local workspaces
  workspaces=$(get_workspaces "$root")
  while IFS= read -r ws; do
    [ -n "$ws" ] || continue
    local ws_dir="$root/$ws"
    if collect_env_files "$ws_dir"; then
      for f in "${COLLECTED_FILES[@]}"; do
        rm "$f"
        total=$((total + 1))
      done
    fi
  done <<< "$workspaces"

  info "Cleared $total secret file(s) from workspace"
}

# EXIT-trap helpers for cmd_run. Defined as functions (not inline string
# traps) so $_RUN_PWD is dereferenced safely regardless of special chars
# in the path. F1: trap "cd '$X'" breaks when X contains a single quote.
_run_cleanup() {
  cd -- "${_RUN_PWD:-.}" 2>/dev/null && cmd_clear
}
_run_cleanup_workspaces() {
  cd -- "${_RUN_PWD:-.}" 2>/dev/null && cmd_clear_workspaces
}

cmd_run() {
  local workspace_mode=false
  local project=""

  # Parse flags before the command
  while [ $# -gt 0 ]; do
    case "$1" in
      -w|--workspaces) workspace_mode=true; shift ;;
      --) shift; break ;;
      -*) die "Unknown flag: $1. Usage: secrets run [-w] [--] <command...>" ;;
      *) break ;;
    esac
  done

  [ $# -gt 0 ] || die "Usage: secrets run [-w] [--] <command...>"

  # F8: pin the project directory now so the EXIT trap clears the right
  # plaintext files even if the user's command does `cd` into another dir.
  # Use a global + named function (NOT string-interpolated trap) so paths
  # with special characters — apostrophes, dollar signs, spaces — work.
  # Single-quoting `$_RUN_PWD` into a string trap would break on any path
  # with a single quote (e.g. /Users/bri/it's-app), and the trap would
  # silently fail to clean up plaintext secrets. EGB-281 F1.
  _RUN_PWD="$PWD"

  # Pull secrets (cmd_pull/cmd_pull_workspaces call resolve_store internally)
  if [ "$workspace_mode" = true ]; then
    cmd_pull_workspaces
  else
    cmd_pull "$project"
  fi

  if [ "$workspace_mode" = true ]; then
    trap _run_cleanup_workspaces EXIT
  else
    trap _run_cleanup EXIT
  fi

  # Execute the command, capturing exit code (don't let set -e kill us)
  local rc=0
  "$@" || rc=$?
  exit "$rc"
}

cmd_which() {
  resolve_store
  echo "store:  $SECRETS_DIR"
  echo "source: $STORE_SOURCE"
}

cmd_help() {
  cat << 'EOF'
secrets — encrypted secret file sync between machines

Usage:
  secrets init                  Initialize the secrets repo and generate an age key
  secrets push [project]        Encrypt secret files and push to the secrets repo
  secrets push -w|--workspaces  Push secrets from all workspaces in package.json
  secrets pull [project]        Pull and decrypt secret files into current directory
  secrets pull -w|--workspaces  Pull secrets into all workspaces from package.json
  secrets clear                 Remove plaintext secret files from current directory
  secrets clear -w|--workspaces Clear secrets from all workspaces in package.json
  secrets run [-w] <command>    Pull secrets, run command, clear secrets on exit
  secrets list                  List all projects and their secret files
  secrets rm <project>          Remove a project's secrets from the repo
  secrets rekey                 Re-encrypt all secrets with a new key
  secrets which                 Show the active store path and which rule chose it
  secrets where                 Alias for `which`
  secrets status                Alias for `which`

Tracked files: .env, .env.*, .dev.vars

If [project] is omitted, it is derived from the current directory's
git remote (if available) or the directory name.

Stores:
  Most users have one ~/.secrets/ store. To use a separate store (e.g.
  for work secrets vs personal), use any of these resolution rules
  (highest precedence first):

    1. --store <dir>  flag             secrets --store ~/.secrets-work pull
    2. .secrets-store file in project  echo work > .secrets-store && git add ...
    3. SECRETS_DIR env var (legacy)    SECRETS_DIR=~/.secrets-work secrets pull
    4. ~/.secrets default

  Bare names ("work") expand to ~/.secrets-work. The name "default"
  resolves to ~/.secrets. Run `secrets which` to inspect the active store.

Workspaces:
  With -w/--workspaces, reads package.json "workspaces" field to find
  workspace directories. Each workspace's secret files are stored under
  <monorepo>/<workspace-path>/ in the secrets repo. Root secret files
  are stored under <monorepo>/ directly. Requires jq.

Environment:
  SECRETS_DIR    Path to secrets repo (default: ~/.secrets). See also
                 the .secrets-store file and --store flag above.
EOF
}

# ─── Main ──────────────────────────────────────────────────────────────

# Pre-pass: extract --store flag from anywhere in the args (before `--` only).
# Lets `secrets --store work push`, `secrets push --store work`, and
# `secrets run --store work cmd ...` all work consistently. After `--`,
# args belong to the user's command and are passed through untouched.
ARGS=()
while [ $# -gt 0 ]; do
  case "$1" in
    --)
      # Stop pre-pass; pass `--` and everything after through untouched
      # so the user's command (e.g. `secrets run -- cmd --store foo`) is
      # not mangled.
      ARGS+=("$@")
      break
      ;;
    --store)
      [ $# -ge 2 ] || die "--store requires a directory or store name"
      # F3: reject values that look like another flag — almost always a typo
      # (`secrets --store push` → user dropped the value, would silently use
      # ~/.secrets-push and surface a confusing "not initialized" error).
      case "$2" in
        --|-*) die "--store value looks like a flag: $2 (did you forget the value?)" ;;
      esac
      STORE_OVERRIDE="$2"
      shift 2
      ;;
    --store=*)
      # F4: --store= with empty value used to silently fall through to the
      # next rule. Treat it as a typo too.
      [ -n "${1#--store=}" ] || die "--store= requires a value"
      STORE_OVERRIDE="${1#--store=}"
      shift
      ;;
    *)
      ARGS+=("$1")
      shift
      ;;
  esac
done
# Re-set positional params. Bulletproof against empty array under `set -u`.
if [ ${#ARGS[@]} -gt 0 ]; then
  set -- "${ARGS[@]}"
else
  set --
fi

case "${1:-help}" in
  init)   cmd_init ;;
  push)
    if [ "${2:-}" = "-w" ] || [ "${2:-}" = "--workspaces" ]; then
      cmd_push_workspaces
    else
      cmd_push "${2:-}"
    fi
    ;;
  pull)
    if [ "${2:-}" = "-w" ] || [ "${2:-}" = "--workspaces" ]; then
      cmd_pull_workspaces
    else
      cmd_pull "${2:-}"
    fi
    ;;
  clear)
    if [ "${2:-}" = "-w" ] || [ "${2:-}" = "--workspaces" ]; then
      cmd_clear_workspaces
    else
      cmd_clear
    fi
    ;;
  run)
    shift
    cmd_run "$@"
    ;;
  list)   cmd_list ;;
  rm)     cmd_rm "${2:-}" ;;
  rekey)  cmd_rekey ;;
  which|where|status) cmd_which ;;
  help|--help|-h) cmd_help ;;
  *)      die "Unknown command: $1. Run 'secrets help' for usage." ;;
esac
