#!/usr/bin/env bash
# vibe — CLI for vibe-coded.ai
set -euo pipefail

CREDS="$HOME/.vibe-coded/credentials.json"
API_BASE="https://api.vibe-coded.ai"

# Clean up sensitive variables on exit
trap 'unset TOKEN 2>/dev/null' EXIT

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

_load_creds() {
  if [ ! -f "$CREDS" ]; then
    echo "Error: No credentials. Run the login flow in Claude Code first." >&2
    exit 1
  fi
  TOKEN=$(jq -r .token "$CREDS")
  USER_SLUG=$(jq -r .userSlug "$CREDS")
  _validate_slug userSlug "$USER_SLUG"
  API_URL=$(jq -r .apiUrl "$CREDS")
  EXPIRES=$(jq -r .expiresAt "$CREDS")
  # Strip fractional seconds and Z for clean lexicographic comparison
  local expires_clean="${EXPIRES%%.*}"
  expires_clean="${expires_clean%%Z}"
  if [[ "$(date -u +%Y-%m-%dT%H:%M:%S)" > "$expires_clean" ]]; then
    echo "Error: Token expired ($EXPIRES). Re-run login in Claude Code." >&2
    exit 1
  fi
}

_load_manifest() {
  if [ ! -f ".vibe-coded.json" ]; then
    echo "Error: No .vibe-coded.json in current directory. Run 'vibe init' first." >&2
    exit 1
  fi
  VIBE_SLUG=$(jq -r .vibeSlug .vibe-coded.json)
  _validate_slug vibeSlug "$VIBE_SLUG"
}

_print_urls() {
  local env="${1:-preview}"
  local prefix=""
  [ "$env" = "preview" ] && prefix="p--"

  local host="${prefix}${USER_SLUG}--${VIBE_SLUG}.vibe-coded.ai"
  echo ""
  echo "  App:        https://${host}"
  echo "  MCP:        https://${host}/mcp"
  if [ "$env" = "production" ]; then
    echo ""
    echo "  Connect (Streamable HTTP — claude.ai, Claude Desktop, Claude Code all speak it):"
    echo "    claude.ai / Desktop:  Settings → Connectors → Add custom connector → https://${host}/mcp"
    echo "    Claude Code:          claude mcp add --transport http ${VIBE_SLUG} https://${host}/mcp"
  fi
  echo ""
}

# Print the vibe's MCP tool count from a vibe-detail JSON blob, warning on zero —
# a green build that ships no tools is the failure that matters most for an
# MCP server. Shared by 'vibe preview' and 'vibe status'.
_print_tool_count() {
  local info="$1" count
  count=$(echo "$info" | jq -r '.vibe.mcpToolCount // .mcpToolCount // 0')
  echo "  MCP Tools:  $count"
  if [ "$count" = "0" ]; then
    echo "  Warning: no MCP tools were extracted. @mcp-expose is only picked up on" >&2
    echo "  'async function name(...)' declarations — arrow functions and object" >&2
    echo "  methods are skipped. Check the build logs: vibe logs" >&2
  fi
}

_api() {
  local method="$1" path="$2"
  shift 2
  # Pass Authorization header via curl --config stdin so the Bearer token
  # never appears in argv and is not visible to other users via `ps`.
  curl -s -K - -X "$method" "${API_URL}${path}" \
    -H "Content-Type: application/json" \
    -H "User-Agent: vibe-cli/1.0" \
    "$@" \
    <<<"header = \"Authorization: Bearer ${TOKEN}\""
}

# Check API response success, exit with error message on failure
_check_success() {
  local result="$1" msg="${2:-Operation}"
  if [ "$(echo "$result" | jq -r '.success // false')" != "true" ]; then
    echo "$msg failed: $(echo "$result" | jq -r '.error // "unknown"')" >&2
    exit 1
  fi
}

# Validate a secret name against the platform contract before splicing into URL paths.
# Blocks path-confusion names like FOO/../_audit.
_validate_secret_name() {
  local name="$1"
  if [[ ! "$name" =~ ^[A-Z][A-Z0-9_]{0,63}$ ]]; then
    echo "Error: Secret name must match ^[A-Z][A-Z0-9_]{0,63}\$ (got: '$name')" >&2
    exit 1
  fi
}

# Validate a slug is URL-path-safe before splicing into API paths. Guards against a
# tampered credentials.json / .vibe-coded.json injecting traversal sequences.
_validate_slug() {
  local kind="$1" slug="$2"
  if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]*$ ]] || [ "${#slug}" -gt 63 ]; then
    echo "Error: invalid $kind '$slug' (expected lowercase alphanumeric + hyphens, max 63 chars)" >&2
    exit 1
  fi
}

# Resolve the skill's root dir (parent of bin/), following symlinks. The CLI is
# typically symlinked to ~/.local/bin/vibe, so BASH_SOURCE points at the symlink.
_resolve_skill_dir() {
  local src="${BASH_SOURCE[0]}" dir
  while [ -h "$src" ]; do
    dir="$(cd -P "$(dirname "$src")" && pwd)"
    src="$(readlink "$src")"
    [[ "$src" != /* ]] && src="$dir/$src"
  done
  dir="$(cd -P "$(dirname "$src")" && pwd)"
  dirname "$dir"
}

_templates_dir() {
  local skill_dir
  skill_dir="$(_resolve_skill_dir)"
  printf '%s/templates\n' "$skill_dir"
}

_print_templates() {
  local templates_dir template_dir name description found=false
  templates_dir="$(_templates_dir)"

  echo "Available templates:"
  if [ -d "$templates_dir" ]; then
    for template_dir in "$templates_dir"/*; do
      [ -d "$template_dir" ] || continue
      [ -f "$template_dir/template.json" ] || continue
      name=$(jq -r '.name // empty' "$template_dir/template.json")
      description=$(jq -r '.description // empty' "$template_dir/template.json")
      [ -n "$name" ] || continue
      printf "  %-12s %s\n" "$name" "$description"
      found=true
    done
  fi

  if [ "$found" = false ]; then
    echo "  (none found)"
  fi
}

_resolve_template_dir() {
  local template_name="$1" templates_dir template_dir
  templates_dir="$(_templates_dir)"

  [ -d "$templates_dir" ] || return 1
  for template_dir in "$templates_dir"/*; do
    [ -d "$template_dir" ] || continue
    if [ "$(basename "$template_dir")" = "$template_name" ] && [ -f "$template_dir/template.json" ]; then
      printf '%s\n' "$template_dir"
      return 0
    fi
  done
  return 1
}

_check_template_targets() {
  local template_dir="$1" source target
  while IFS= read -r -d '' source; do
    target="./$(basename "$source")"
    if [ -e "$target" ] || [ -L "$target" ]; then
      echo "Error: Cannot apply template: $target already exists; refusing to overwrite it." >&2
      return 1
    fi
  done < <(find "$template_dir" -mindepth 1 -maxdepth 1 ! -name template.json -print0)
}

_copy_template_files() {
  local template_dir="$1" source
  while IFS= read -r -d '' source; do
    cp -R "$source" .
  done < <(find "$template_dir" -mindepth 1 -maxdepth 1 ! -name template.json -print0)
}

# ── Commands ─────────────────────────────────────────────────────────────────

cmd_init() {
  local slug=""
  local title=""
  local storage="kv"
  local visibility="private"
  local interaction="invite_only"
  local template=""
  local template_dir=""

  while [ $# -gt 0 ]; do
    case "$1" in
      --title|--storage|--visibility|--template)
        if [ $# -lt 2 ]; then
          echo "Error: $1 requires a value." >&2
          exit 1
        fi
        case "$1" in
          --title) title="$2" ;;
          --storage) storage="$2"; [ "$storage" = "sql" ] && storage="d1" ;;
          --visibility)
            visibility="$2"
            case "$visibility" in
              private) interaction="invite_only" ;;
              unlisted|public) interaction="public" ;;
            esac
            ;;
          --template) template="$2" ;;
        esac
        shift 2
        ;;
      *)
        if [ -n "$slug" ]; then
          echo "Error: Unexpected argument '$1'." >&2
          exit 1
        fi
        slug="$1"
        shift
        ;;
    esac
  done

  if [ -z "$slug" ]; then
    echo "Usage: vibe init [--template <name>] <slug> [--title \"My App\"] [--storage kv|sql] [--visibility private|unlisted|public]"
    exit 1
  fi
  # Validate slug format (lowercase alphanumeric + hyphens, must start with letter/number)
  if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
    echo "Error: Slug must be lowercase alphanumeric with hyphens (e.g., my-app)" >&2
    exit 1
  fi

  [ -n "$title" ] || title="$slug"

  if [ -n "$template" ]; then
    if ! template_dir="$(_resolve_template_dir "$template")"; then
      echo "Error: Unknown template '$template'." >&2
      _print_templates >&2
      exit 1
    fi
    if ! storage=$(jq -er '.storage | select(. == "kv" or . == "d1")' "$template_dir/template.json"); then
      echo "Error: Template '$template' has invalid storage metadata." >&2
      exit 1
    fi
    _check_template_targets "$template_dir"
  fi

  _load_creds

  echo "Creating vibe: $slug (storage: $storage, visibility: $visibility)"

  # Create on platform
  RESULT=$(_api POST "/api/v1/vibes" \
    -d "$(jq -n --arg s "$slug" --arg t "$title" --arg st "$storage" \
      '{slug: $s, title: $t, storageType: $st}')")

  if [ "$(echo "$RESULT" | jq -r '.success')" != "true" ]; then
    local err
    err=$(echo "$RESULT" | jq -r '.error // "unknown"')
    # Slug-exists recovery: exit 2 with existing config as JSON on stdout
    if echo "$err" | grep -qi "already exists\|slug.*taken\|duplicate"; then
      echo "Slug '$slug' already exists. Fetching existing config..." >&2
      _api GET "/api/v1/vibes/$USER_SLUG/$slug" \
        | jq '.vibe // . | {title, storageType, visibility, interactionPolicy}'
      exit 2
    fi
    echo "Error: $err" >&2
    exit 1
  fi

  VIBE_SLUG="$slug"
  echo "Created on platform."

  # Set visibility
  _api PATCH "/api/v1/vibes/$USER_SLUG/$slug" \
    -d "$(jq -n --arg v "$visibility" --arg i "$interaction" \
      '{visibility: $v, interactionPolicy: $i}')" > /dev/null

  echo "Visibility: $visibility + $interaction"

  # Write manifest
  jq -n --arg u "$USER_SLUG" --arg v "$slug" --arg st "$storage" \
    '{userSlug: $u, vibeSlug: $v, type: "fullstack", storage: $st}' > .vibe-coded.json

  echo "Wrote .vibe-coded.json"
  if [ -n "$template" ]; then
    _copy_template_files "$template_dir"
    echo "Copied template: $template"
  fi
  echo ""
  echo "Next steps:"
  if [ -n "$template" ]; then
    echo "  1. Run: vibe preview"
  else
    echo "  1. Add your source files (worker.ts, Vue components, etc.)"
    echo "  2. Run: vibe deploy"
  fi
  _print_urls preview
}

cmd_templates() {
  _print_templates
}

cmd_preview() {
  _load_creds
  _load_manifest

  echo "Uploading $USER_SLUG/$VIBE_SLUG to preview..."

  # Build JSON payload with base64-encoded files
  PAYLOAD='{"files":{'
  FIRST=true
  local count=0
  while IFS= read -r -d '' FILE; do
    REL="${FILE#./}"
    case "$REL" in
      *.html) CT="text/html" ;; *.ts) CT="text/typescript" ;; *.js) CT="application/javascript" ;;
      *.json) CT="application/json" ;; *.vue) CT="text/x-vue" ;; *.css) CT="text/css" ;;
      *.svg) CT="image/svg+xml" ;; *.png) CT="image/png" ;; *.lock) CT="application/octet-stream" ;;
      *) CT="application/octet-stream" ;;
    esac
    B64=$(base64 < "$FILE" | tr -d '\n')
    if [ "$FIRST" = true ]; then FIRST=false; else PAYLOAD+=','; fi
    KEY=$(printf '%s' "$REL" | jq -Rs '.')
    PAYLOAD+="$KEY:{\"content\":\"$B64\",\"contentType\":\"$CT\"}"
    count=$((count + 1))
  done < <(find . -type f \
    -not -path './node_modules/*' -not -path './dist/*' -not -path './.git/*' \
    -not -name '.vibe-coded.json' -not -name '.DS_Store' -not -name '*.map' \
    -not -name '.env' -not -name '.env.*' \
    -print0)
  PAYLOAD+='}}'

  # Client-side size check (50MB decoded limit)
  PAYLOAD_SIZE=${#PAYLOAD}
  MAX_SIZE=$((70 * 1024 * 1024))  # ~70MB with base64 overhead ≈ 50MB decoded
  if [ "$PAYLOAD_SIZE" -gt "$MAX_SIZE" ]; then
    echo "Error: Payload too large ($(( PAYLOAD_SIZE / 1024 / 1024 ))MB). Max 50MB decoded." >&2
    exit 1
  fi

  echo "Uploading $count files ($(( PAYLOAD_SIZE / 1024 ))KB)..."
  # Use tempfile + --data-binary @file so payload bypasses ARG_MAX (macOS: 1MB)
  TMP_PAYLOAD=$(mktemp -t vibe-upload)
  printf '%s' "$PAYLOAD" > "$TMP_PAYLOAD"
  # Remember the latest build BEFORE uploading so the poll below can tell a
  # new build from the previous one.
  PREV_BUILD_ID=$(_api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/builds?limit=1" | jq -r '.builds[0].id // empty')
  UPLOAD=$(_api POST "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/source" --data-binary "@$TMP_PAYLOAD")
  rm -f "$TMP_PAYLOAD"
  _check_success "$UPLOAD" "Upload"

  # Source unchanged → the scheduler skips the build; nothing to wait for.
  if [ "$(echo "$UPLOAD" | jq -r '.buildSkipped // false')" = "true" ]; then
    echo "Source unchanged — no new build needed."
    _print_urls preview
    _print_tool_count "$(_api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG")"
    exit 0
  fi

  # Poll for build. The platform caps a build at 10 minutes
  # (build-runner TOTAL_BUILD_TIMEOUT_MS), so wait up to that, not 120s.
  # Only trust a build that is NEWER than the one that existed before this
  # upload — a stale prior build can otherwise read as an instant success.
  echo "Waiting for build (up to 10 minutes)..."
  local elapsed=0 interval=5 max_wait=600
  while [ "$elapsed" -lt "$max_wait" ]; do
    sleep "$interval"
    elapsed=$((elapsed + interval))
    [ "$elapsed" -ge 60 ] && interval=10
    BUILD=$(_api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/builds?limit=1")
    IFS=$'\t' read -r BUILD_ID STATUS < <(echo "$BUILD" | jq -r '[(.builds[0].id // "" | tostring), (.builds[0].status // "pending")] | @tsv')
    if [ -n "$PREV_BUILD_ID" ] && [ "$BUILD_ID" = "$PREV_BUILD_ID" ]; then
      printf "  [%3ds] queued\n" "$elapsed"
      continue
    fi
    printf "  [%3ds] %s\n" "$elapsed" "$STATUS"
    if [ "$STATUS" = "succeeded" ]; then
      echo ""
      echo "Build succeeded!"
      _print_urls preview
      # Surface the tool count here, not only in 'vibe status'.
      _print_tool_count "$(_api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG")"
      echo "  Run 'vibe publish' to go live."
      exit 0
    elif [ "$STATUS" = "failed" ]; then
      echo ""
      echo "Build failed. Logs:" >&2
      _api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/builds/$BUILD_ID/logs" >&2
      exit 1
    fi
  done
  echo "Build still in progress after ${max_wait}s (platform limit is 10 minutes). Run 'vibe status' to check." >&2
  exit 1
}

cmd_publish() {
  _load_creds
  _load_manifest

  echo "Publishing $USER_SLUG/$VIBE_SLUG to production..."
  RESULT=$(_api POST "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/publish")

  _check_success "$RESULT" "Publish"

  # Verify (200 = public, 401 = private/auth-required — both mean it's live)
  HTTP=$(curl -s -o /dev/null -w "%{http_code}" -H "User-Agent: vibe-cli/1.0" \
    "https://${USER_SLUG}--${VIBE_SLUG}.vibe-coded.ai")

  if [ "$HTTP" = "200" ] || [ "$HTTP" = "401" ]; then
    echo "Published!"
    [ "$HTTP" = "401" ] && echo "(Private — authentication required)"
  else
    echo "Warning: got HTTP $HTTP — may not be live yet"
  fi
  _print_urls production
}

cmd_status() {
  _load_creds

  # --plan flag: print user's plan and exit (no manifest needed)
  if [ "${1:-}" = "--plan" ]; then
    _api GET "/api/v1/vibes" | jq -r '.plan // "free"'
    return
  fi

  _load_manifest

  echo "Vibe: $USER_SLUG/$VIBE_SLUG"
  echo ""

  # Vibe info (fields nested under .vibe in detail response)
  INFO=$(_api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG")
  echo "  Title:      $(echo "$INFO" | jq -r '.vibe.title // .title // "unknown"')"
  echo "  Visibility: $(echo "$INFO" | jq -r '.vibe.visibility // .visibility // "unknown"')"
  echo "  Policy:     $(echo "$INFO" | jq -r '.vibe.interactionPolicy // .interactionPolicy // "unknown"')"
  local storage_display
  storage_display=$(echo "$INFO" | jq -r '.vibe.storageType // .storageType // "kv"')
  [ "$storage_display" = "d1" ] && storage_display="sql"
  echo "  Storage:    $storage_display"
  _print_tool_count "$INFO"
  echo ""

  # Recent builds
  echo "Recent builds:"
  BUILDS=$(_api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/builds?limit=5")
  local build_count
  build_count=$(echo "$BUILDS" | jq '.builds | length')
  if [ "$build_count" -gt 0 ] 2>/dev/null; then
    echo "$BUILDS" | jq -r '.builds[] | "  \(.id) \(.status) \(.environment) \(.createdAt)"'
    echo ""
    # Only show URLs if there have been builds
    local has_preview has_prod
    has_preview=$(echo "$INFO" | jq -r '.vibe.previewBuildId // .previewBuildId // empty')
    has_prod=$(echo "$INFO" | jq -r '.vibe.productionBuildId // .productionBuildId // empty')
    if [ -n "$has_preview" ]; then echo "Preview:" && _print_urls preview; fi
    if [ -n "$has_prod" ]; then echo "Production:" && _print_urls production; fi
    if [ -z "$has_preview" ] && [ -z "$has_prod" ]; then echo "  (no deployments yet — run 'vibe preview' to deploy)"; fi
  else
    echo "  (none — run 'vibe preview' to deploy)"
  fi
}

cmd_urls() {
  _load_creds
  _load_manifest

  echo "Preview:"
  _print_urls preview
  echo "Production:"
  _print_urls production
}

cmd_logs() {
  _load_creds
  _load_manifest

  local build_id="${1:-}"
  if [ -z "$build_id" ]; then
    build_id=$(_api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/builds?limit=1" \
      | jq -r '.builds[0].id // empty')
    if [ -z "$build_id" ]; then
      echo "No builds found." >&2
      exit 1
    fi
    echo "Latest build: $build_id"
  fi
  _api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/builds/$build_id/logs" \
    | jq -r '.lines[]? | "\(.ts) [\(.step)] \(.msg)"' 2>/dev/null \
    || _api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/builds/$build_id/logs" | jq '.'
}

cmd_settings() {
  _load_creds
  _load_manifest

  if [ $# -eq 0 ]; then
    _api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG" \
      | jq '.vibe // . | {title, visibility, interactionPolicy}'
    return
  fi

  local title="" visibility="" interaction=""
  while [ $# -gt 0 ]; do
    case "$1" in
      --title) title="$2"; shift 2 ;;
      --visibility) visibility="$2"; shift 2 ;;
      --interaction) interaction="$2"; shift 2 ;;
      *) echo "Unknown flag: $1" >&2; exit 1 ;;
    esac
  done

  # Visibility / interaction changes are web-only (vibe-coded-1a4d.20): they
  # expose the vibe, so an API token can't perform them — the platform would
  # 403 anyway. Hand the user a deep link instead.
  if [ -n "$visibility" ] || [ -n "$interaction" ]; then
    _web_only "Changing visibility or interaction policy" "settings"
    return
  fi

  # Build JSON body with only provided fields (benign fields stay token-editable)
  local body
  body=$(jq -n \
    ${title:+--arg title "$title"} \
    '{} + (if $ARGS.named | has("title") then {title: $ARGS.named.title} else {} end)')

  RESULT=$(_api PATCH "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG" -d "$body")
  _check_success "$RESULT" "Settings update"
  echo "Settings updated."
  # PATCH returns {success, updated} — fetch current settings to confirm
  _api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG" \
    | jq '.vibe // . | {title, visibility, interactionPolicy}'
}

# Web-only operation: print a dashboard deep link instead of calling the API.
# These operations create durable state that outlives the token (access grants,
# exposure, deletion), so they require an interactive login — the platform
# enforces this server-side (403 for Bearer); this is the friendly path.
_web_only() {
  local operation="$1" panel="$2"
  echo "$operation requires an interactive web login (not available with an API token)."
  echo ""
  echo "Complete it here: https://vibe-coded.ai/account?vibe=$VIBE_SLUG&panel=$panel"
}

cmd_delete() {
  _load_creds
  _load_manifest

  # Deleting a vibe is web-only (vibe-coded-1a4d.20): irreversible, so it
  # requires an interactive login — an API token gets 403 from the platform.
  _web_only "Deleting $USER_SLUG/$VIBE_SLUG" "delete"
}

cmd_password() {
  _load_creds
  _load_manifest

  local sub="${1:-check}"
  shift 2>/dev/null || true

  case "$sub" in
    check)
      _api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/password" | jq '.'
      ;;
    set)
      local req_view=true req_write=true req_admin=true
      while [ $# -gt 0 ]; do
        case "$1" in
          --no-view) req_view=false; shift ;;
          --no-write) req_write=false; shift ;;
          --no-admin) req_admin=false; shift ;;
          *) shift ;;
        esac
      done
      echo -n "Vibe password: " && read -s VIBE_PW && echo ""
      local body
      # Export so jq reads via env.VIBE_PW — value never appears in argv/ps
      export VIBE_PW
      body=$(jq -n \
        --argjson v "$req_view" --argjson w "$req_write" --argjson a "$req_admin" \
        '{password: env.VIBE_PW, requireForView: $v, requireForWrite: $w, requireForAdmin: $a}')
      unset VIBE_PW
      RESULT=$(_api POST "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/password/set" -d "$body")
      _check_success "$RESULT" "Password set"
      echo "Password set."
      ;;
    remove)
      RESULT=$(_api POST "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/password/remove")
      _check_success "$RESULT" "Password remove"
      echo "Password removed."
      ;;
    *)
      echo "Usage: vibe password check|set|remove" >&2
      exit 1
      ;;
  esac
}

cmd_login() {
  # Disable strict mode for login — API responses can contain edge cases
  set +eu

  local email="${1:-}"
  if [ -z "$email" ]; then
    echo "Usage: vibe login <email>" >&2
    set -eu; return 1
  fi

  echo -n "Password: "
  read -s PASSWORD
  echo ""

  if [ -z "$PASSWORD" ]; then
    echo "Error: No password entered." >&2
    set -eu; return 1
  fi

  # Temp files for the login flow
  local cookie_jar body resp
  cookie_jar=$(mktemp)
  body=$(mktemp)
  resp=$(mktemp)

  # Build login body
  export PASSWORD
  jq -n --arg e "$email" '{email: $e, password: env.PASSWORD}' > "$body"
  unset PASSWORD

  # Step 1: Login
  curl -s -o "$resp" -c "$cookie_jar" -X POST "$API_BASE/auth/login" \
    -H "Content-Type: application/json" --data @"$body"
  rm -f "$body"

  local login_ok
  login_ok=$(jq -r '.success' "$resp" 2>/dev/null)
  if [ "$login_ok" != "true" ]; then
    echo "Login failed: $(jq -r '.error // "unknown"' "$resp" 2>/dev/null)" >&2
    rm -f "$cookie_jar" "$resp"
    set -eu; return 1
  fi

  # Step 2: CSRF
  curl -s -o "$resp" -b "$cookie_jar" "$API_BASE/auth/csrf"
  local csrf
  csrf=$(jq -r '.token' "$resp" 2>/dev/null)

  # Step 3: Mint API token
  curl -s -o "$resp" -b "$cookie_jar" -X POST "$API_BASE/auth/api-token" \
    -H "X-CSRF-Token: $csrf" -H "Content-Type: application/json"
  rm -f "$cookie_jar"

  local token
  token=$(jq -r '.token' "$resp" 2>/dev/null)
  if [ -z "$token" ] || [ "$token" = "null" ]; then
    echo "Token generation failed: $(jq -r '.error // "unknown"' "$resp" 2>/dev/null)" >&2
    rm -f "$resp"
    set -eu; return 1
  fi

  # Extract userSlug + expiresAt — try API response first, then JWT payload
  local user_slug expires_at
  user_slug=$(jq -r '.userSlug // empty' "$resp" 2>/dev/null)
  expires_at=$(jq -r '.expiresAt // empty' "$resp" 2>/dev/null)
  rm -f "$resp"

  if [ -z "$user_slug" ] || [ -z "$expires_at" ]; then
    # Decode JWT payload: base64url → base64 (with padding) → json
    local b64part payload
    b64part=$(printf '%s' "$token" | cut -d. -f2 | tr '_-' '/+')
    # Add base64 padding (macOS base64 requires it)
    while [ $((${#b64part} % 4)) -ne 0 ]; do b64part="${b64part}="; done
    payload=$(printf '%s' "$b64part" | base64 -d 2>/dev/null || printf '%s' "$b64part" | base64 -D 2>/dev/null || true)
    if [ -n "$payload" ]; then
      [ -z "$user_slug" ] && user_slug=$(printf '%s' "$payload" | jq -r '.slug // empty' 2>/dev/null)
      if [ -z "$expires_at" ]; then
        local exp_unix
        exp_unix=$(printf '%s' "$payload" | jq -r '.exp // empty' 2>/dev/null)
        if [ -n "$exp_unix" ]; then
          expires_at=$(date -u -r "$exp_unix" +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || echo "")
        fi
      fi
    fi
  fi

  if [ -z "$user_slug" ]; then
    echo "Error: Could not determine userSlug from API or token." >&2
    set -eu; return 1
  fi

  mkdir -p ~/.vibe-coded
  chmod 700 ~/.vibe-coded
  jq -n --arg t "$token" --arg u "$user_slug" --arg e "${expires_at:-}" \
    --arg a "$API_BASE" '{apiUrl: $a, token: $t, userSlug: $u, expiresAt: $e}' \
    > ~/.vibe-coded/credentials.json
  chmod 600 ~/.vibe-coded/credentials.json

  echo "Credentials saved for $user_slug (expires ${expires_at:-unknown})"

  # Validate
  local validate
  validate=$(curl -s -K - "$API_BASE/api/v1/vibes" \
    <<<"header = \"Authorization: Bearer ${token}\"" \
    | jq -r '.success' 2>/dev/null)
  if [ "$validate" = "true" ]; then
    echo "Validated — API access working."
  else
    echo "Warning: validation call failed." >&2
  fi

  set -eu
}

cmd_logout() {
  # Remove local credentials. Server-side revocation is best-effort: an API
  # (Bearer) token cannot revoke itself — full revocation needs a web session
  # cookie (DELETE /auth/api-token). We always clear the local creds.
  if [ ! -f "$CREDS" ]; then
    echo "Already logged out — no credentials at $CREDS."
    return
  fi
  local token
  token=$(jq -r '.token // empty' "$CREDS" 2>/dev/null)
  if [ -n "$token" ]; then
    curl -s -K - -X DELETE "$API_BASE/auth/api-token" \
      -H "Content-Type: application/json" \
      <<<"header = \"Authorization: Bearer ${token}\"" >/dev/null 2>&1 || true
  fi
  rm -f "$CREDS"
  echo "Logged out — local credentials removed ($CREDS)."
  echo "To fully revoke the token server-side: DELETE /auth/api-token from a signed-in browser session."
}

cmd_secrets() {
  _load_creds
  _load_manifest

  local sub="${1:-list}"
  shift 2>/dev/null || true

  case "$sub" in
    list)
      _api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/secrets" \
        | jq '.secrets[] | {name, usageType, createdAt}'
      ;;
    create)
      local name="${1:-}"
      if [ -z "$name" ]; then
        echo "Usage: vibe secrets create <NAME> [--type custom]" >&2
        exit 1
      fi
      _validate_secret_name "$name"
      shift
      local usage_type="custom"
      [ "${1:-}" = "--type" ] && usage_type="${2:-custom}"

      echo -n "Secret value: " && read -s SECRET_VAL && echo ""
      local body
      # Export so jq reads via env.SECRET_VAL — value never appears in argv/ps
      export SECRET_VAL
      body=$(jq -n --arg n "$name" --arg t "$usage_type" \
        '{name: $n, value: env.SECRET_VAL, usageType: $t}')
      unset SECRET_VAL
      RESULT=$(_api POST "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/secrets" -d "$body")
      _check_success "$RESULT" "Secret create"
      echo "Secret $name created."
      ;;
    update)
      local name="${1:-}"
      if [ -z "$name" ]; then echo "Usage: vibe secrets update <NAME>" >&2; exit 1; fi
      _validate_secret_name "$name"
      echo -n "New value: " && read -s SECRET_VAL && echo ""
      local body
      # Export so jq reads via env.SECRET_VAL — value never appears in argv/ps
      export SECRET_VAL
      body=$(jq -n '{value: env.SECRET_VAL}')
      unset SECRET_VAL
      RESULT=$(_api PUT "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/secrets/$name" -d "$body")
      _check_success "$RESULT" "Secret update"
      echo "Secret $name updated."
      ;;
    delete)
      local name="${1:-}"
      if [ -z "$name" ]; then echo "Usage: vibe secrets delete <NAME>" >&2; exit 1; fi
      _validate_secret_name "$name"
      RESULT=$(_api DELETE "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/secrets/$name")
      _check_success "$RESULT" "Secret delete"
      echo "Secret $name deleted."
      ;;
    audit)
      _api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/secrets/_audit" | jq '.audit'
      ;;
    *)
      echo "Usage: vibe secrets list|create|update|delete|audit [NAME]" >&2
      echo "(secrets are write-only — values cannot be read back; they reach your worker as env.<NAME>)" >&2
      exit 1
      ;;
  esac
}

cmd_access() {
  _load_creds
  _load_manifest

  local sub="${1:-list}"
  shift 2>/dev/null || true

  case "$sub" in
    list)
      _api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/access" \
        | jq '.access[] | {userId, role, user: .user.email}'
      ;;
    grant|update|revoke|revoke-all)
      # Access mutations are web-only (vibe-coded-1a4d.20): grants create
      # durable ACLs that outlive the token, so they require an interactive
      # login — an API token gets 403 from the platform.
      _web_only "Managing collaborator access" "access"
      ;;
    *)
      echo "Usage: vibe access list|grant|update|revoke|revoke-all [args]" >&2
      echo "(grant/update/revoke open in the web dashboard — access changes need an interactive login)" >&2
      exit 1
      ;;
  esac
}

cmd_r2() {
  local sub="${1:-}"
  shift 2>/dev/null || true

  # Help works without a vibe directory or creds.
  case "$sub" in
    ""|help|--help|-h)
      cat <<'USAGE' >&2
Usage: vibe r2 <subcommand> [args]

Subcommands:
  upload <path> --key <r2-key> [--content-type ct] [--ttl 300]
      Upload a local file to R2 (browser-direct via signed URL).

  download <r2-key> [--to <local-path>] [--ttl 3600]
      Download an R2 object to a local path (defaults to basename).

  url <r2-key> [--ttl 3600]
      Print a signed download URL (no fetch — useful for embedding).

  ls [--prefix <p>]
      List objects under your vibe's prefix.

  rm <r2-key>
      Delete an object.

  usage
      Show storage used / plan limit / remaining.

Notes:
  - All keys are auto-namespaced to your vibe (vibes/<id>/<key>).
  - R2 access requires a paid plan (BYOV or Pro).
USAGE
      exit 1
      ;;
  esac

  _load_creds
  _load_manifest

  case "$sub" in
    upload)
      # vibe r2 upload <local-path> --key <r2-key> [--content-type <ct>] [--max-size <bytes>] [--ttl <secs>]
      local local_path="${1:-}" key="" content_type="" max_size="" ttl=""
      [ -z "$local_path" ] && { echo "Usage: vibe r2 upload <local-path> --key <r2-key>" >&2; exit 1; }
      shift
      while [ $# -gt 0 ]; do
        case "$1" in
          --key)          key="$2"; shift 2 ;;
          --content-type) content_type="$2"; shift 2 ;;
          --max-size)     max_size="$2"; shift 2 ;;
          --ttl)          ttl="$2"; shift 2 ;;
          *) echo "Unknown flag: $1" >&2; exit 1 ;;
        esac
      done
      [ -z "$key" ] && { echo "--key is required" >&2; exit 1; }
      [ ! -f "$local_path" ] && { echo "File not found: $local_path" >&2; exit 1; }

      # Default content-type by file extension if not given
      if [ -z "$content_type" ]; then
        case "$local_path" in
          *.png) content_type="image/png" ;; *.jpg|*.jpeg) content_type="image/jpeg" ;;
          *.gif) content_type="image/gif" ;; *.svg) content_type="image/svg+xml" ;;
          *.webp) content_type="image/webp" ;; *.pdf) content_type="application/pdf" ;;
          *.json) content_type="application/json" ;; *.txt) content_type="text/plain" ;;
          *.html) content_type="text/html" ;; *.css) content_type="text/css" ;;
          *.js) content_type="application/javascript" ;;
          *) content_type="application/octet-stream" ;;
        esac
      fi
      local size; size=$(wc -c < "$local_path" | tr -d ' ')
      [ -z "$max_size" ] && max_size="$size"

      # Mint signed URL
      local body; body=$(jq -n \
        --arg k "$key" --arg ct "$content_type" \
        --argjson ms "$max_size" \
        --argjson ttl "${ttl:-300}" \
        '{key: $k, contentType: $ct, maxSize: $ms, ttl: $ttl}')
      local mint; mint=$(_api POST "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/r2/upload-url" -d "$body")
      _check_success "$mint" "R2 upload-url mint"
      local url; url=$(echo "$mint" | jq -r '.url')

      # PUT file directly to R2 via signed URL (no tempfile games needed — curl reads from disk)
      local http_code; http_code=$(curl -s -o /tmp/vibe-r2-put.log -w "%{http_code}" \
        -X PUT -H "Content-Type: $content_type" --data-binary "@$local_path" "$url")
      if [ "$http_code" != "200" ] && [ "$http_code" != "204" ]; then
        echo "Upload failed: HTTP $http_code" >&2
        cat /tmp/vibe-r2-put.log >&2
        rm -f /tmp/vibe-r2-put.log
        exit 1
      fi
      rm -f /tmp/vibe-r2-put.log
      echo "Uploaded $local_path → $key ($size bytes)"
      ;;

    download)
      # vibe r2 download <r2-key> [--to <local-path>] [--ttl <secs>]
      local key="${1:-}" to="" ttl=""
      [ -z "$key" ] && { echo "Usage: vibe r2 download <r2-key> [--to <path>]" >&2; exit 1; }
      shift
      while [ $# -gt 0 ]; do
        case "$1" in
          --to)  to="$2"; shift 2 ;;
          --ttl) ttl="$2"; shift 2 ;;
          *) echo "Unknown flag: $1" >&2; exit 1 ;;
        esac
      done
      [ -z "$to" ] && to=$(basename "$key")
      local body; body=$(jq -n --arg k "$key" --argjson ttl "${ttl:-3600}" '{key: $k, ttl: $ttl}')
      local mint; mint=$(_api POST "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/r2/download-url" -d "$body")
      _check_success "$mint" "R2 download-url mint"
      local url; url=$(echo "$mint" | jq -r '.url')
      local http_code; http_code=$(curl -s -o "$to" -w "%{http_code}" "$url")
      if [ "$http_code" != "200" ]; then
        echo "Download failed: HTTP $http_code" >&2
        exit 1
      fi
      echo "Downloaded $key → $to"
      ;;

    url)
      # vibe r2 url <r2-key> [--ttl <secs>]   — print signed download URL only
      local key="${1:-}" ttl=""
      [ -z "$key" ] && { echo "Usage: vibe r2 url <r2-key> [--ttl <secs>]" >&2; exit 1; }
      shift
      [ "${1:-}" = "--ttl" ] && ttl="$2"
      local body; body=$(jq -n --arg k "$key" --argjson ttl "${ttl:-3600}" '{key: $k, ttl: $ttl}')
      local mint; mint=$(_api POST "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/r2/download-url" -d "$body")
      _check_success "$mint" "R2 url mint"
      echo "$mint" | jq -r '.url'
      ;;

    ls|list)
      # vibe r2 ls [--prefix <p>] [--limit <n>]  — paginates via cursor to list ALL objects
      local prefix="" limit=""
      while [ $# -gt 0 ]; do
        case "$1" in
          --prefix) prefix="$2"; shift 2 ;;
          --limit)  limit="$2"; shift 2 ;;
          *) echo "Unknown flag: $1" >&2; exit 1 ;;
        esac
      done
      if [ -n "$limit" ] && ! printf '%s' "$limit" | grep -Eq '^[1-9][0-9]*$'; then
        echo "Error: --limit must be a positive integer" >&2; exit 1
      fi
      local nl=$'\n' cursor="" count=0 all_lines=""
      while :; do
        # Server caps page size at 1000; request only what's still needed under --limit.
        local page_req=1000
        if [ -n "$limit" ]; then
          local remaining=$((limit - count))
          [ "$remaining" -lt "$page_req" ] && page_req="$remaining"
        fi
        local query="?limit=${page_req}"
        [ -n "$prefix" ] && query="${query}&prefix=$(printf '%s' "$prefix" | jq -sRr @uri)"
        [ -n "$cursor" ] && query="${query}&cursor=$(printf '%s' "$cursor" | jq -sRr @uri)"

        local resp; resp=$(_api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/r2/list${query}")
        _check_success "$resp" "R2 list"

        local page; page=$(echo "$resp" | jq -r '.objects[] | "\(.size)\t\(.key)"')
        [ -n "$page" ] && all_lines="${all_lines}${all_lines:+$nl}${page}"
        count=$((count + $(echo "$resp" | jq '.objects | length')))

        cursor=$(echo "$resp" | jq -r '.cursor // empty')
        [ "$(echo "$resp" | jq -r '.truncated // false')" != "true" ] && break
        [ -z "$cursor" ] && break
        [ -n "$limit" ] && [ "$count" -ge "$limit" ] && break
      done

      # --limit is authoritative client-side (defensive against a server page overshoot).
      if [ -n "$limit" ] && [ "$count" -gt "$limit" ]; then
        all_lines=$(printf '%s\n' "$all_lines" | head -n "$limit")
        count="$limit"
      fi

      [ -n "$all_lines" ] && printf '%s\n' "$all_lines" | column -t -s $'\t'
      echo "Listed ${count} object(s)" >&2
      ;;

    rm|delete)
      local key="${1:-}"
      [ -z "$key" ] && { echo "Usage: vibe r2 rm <r2-key>" >&2; exit 1; }
      RESULT=$(_api DELETE "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/r2/$(printf '%s' "$key" | jq -sRr @uri)")
      _check_success "$RESULT" "R2 delete"
      echo "Deleted $key"
      ;;

    usage)
      _api GET "/api/v1/vibes/$USER_SLUG/$VIBE_SLUG/r2/usage" \
        | jq -r '"Used: \(.bytesUsed / 1024 / 1024 | floor)MB  Limit: \(.limitBytes / 1024 / 1024 | floor)MB  Remaining: \(.remainingBytes / 1024 / 1024 | floor)MB"'
      ;;

    *) echo "Unknown r2 subcommand: $sub. Run 'vibe r2 help' for usage." >&2; exit 1 ;;
  esac
}

cmd_dev() {
  # Local dev — fully offline, no credentials needed.
  _load_manifest
  local type storage
  type=$(jq -r '.type // "fullstack"' .vibe-coded.json)
  storage=$(jq -r '.storage // "kv"' .vibe-coded.json)

  if [ "$type" = "static" ]; then
    echo "Static vibe — open directly in your browser (no server needed):"
    echo "  file://$(pwd)/index.html"
    return
  fi

  if ! command -v node >/dev/null 2>&1; then
    echo "Error: node not found. Install Node 18+ to use 'vibe dev'." >&2
    exit 1
  fi

  local skill_dir; skill_dir="$(_resolve_skill_dir)"
  if [ ! -d "$skill_dir/node_modules/miniflare" ]; then
    echo "First run: installing local dev dependencies in $skill_dir …"
    ( cd "$skill_dir" && npm install --silent ) \
      || { echo "Error: npm install failed in $skill_dir." >&2; exit 1; }
  fi

  exec node "$skill_dir/runtime/dev-server.mjs" "$@"
}

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

case "${1:-help}" in
  init)            shift; cmd_init "$@" ;;
  templates)       cmd_templates ;;
  dev)             shift 2>/dev/null || true; cmd_dev "$@" ;;
  preview|deploy)  cmd_preview ;;
  publish)         cmd_publish ;;
  status)          shift 2>/dev/null || true; cmd_status "$@" ;;
  urls)            cmd_urls ;;
  logs)            shift 2>/dev/null || true; cmd_logs "$@" ;;
  settings)        shift 2>/dev/null || true; cmd_settings "$@" ;;
  delete)          shift 2>/dev/null || true; cmd_delete "$@" ;;
  password)        shift 2>/dev/null || true; cmd_password "$@" ;;
  login)           shift 2>/dev/null || true; cmd_login "$@" ;;
  logout)          cmd_logout ;;
  secrets)         shift 2>/dev/null || true; cmd_secrets "$@" ;;
  access)          shift 2>/dev/null || true; cmd_access "$@" ;;
  r2)              shift 2>/dev/null || true; cmd_r2 "$@" ;;
  help|--help|-h)
    echo "vibe — CLI for vibe-coded.ai"
    echo ""
    echo "Workflow: init → dev (local) → preview (platform) → publish (production)"
    echo ""
    echo "Commands:"
    echo "  login <email>                Authenticate and save credentials"
    echo "  logout                       Remove local credentials"
    echo "  init <slug> [opts]           Create vibe (--template name, --storage kv|sql)"
    echo "  templates                    List runnable starter templates"
    echo "  dev [--port N] [--reset]     Run locally (Miniflare: D1+KV+R2, hot-reload)"
    echo "  dev seed <file.sql>          Apply SQL to local D1"
    echo "  preview                      Upload source + build (alias: deploy)"
    echo "  publish                      Promote preview to production"
    echo "  status [--plan]              Show vibe info + recent builds"
    echo "  urls                         Show all endpoints (app, MCP)"
    echo "  logs [build-id]              Show build logs (latest if omitted)"
    echo "  settings [--title T] ...     View or update vibe settings"
    echo "  access list|grant|...        Manage collaborators"
    echo "  password check|set|remove    Manage vibe password"
    echo "  secrets list|create|...      Manage secrets (Pro)"
    echo "  r2 upload|download|ls|...    Manage R2 file storage (Pro/BYOV)"
    echo "  delete --confirm             Permanently delete vibe"
    echo ""
    echo "Develop locally with 'vibe dev' (local worker + D1/KV/R2) — preview/publish when ready."
    echo ""
    echo "Credentials: ~/.vibe-coded/credentials.json (run 'vibe login')"
    echo "Manifest:    .vibe-coded.json (created by init)"
    ;;
  *)
    echo "Unknown command: $1. Run 'vibe help' for usage." >&2
    exit 1 ;;
esac
