#!/usr/bin/env bash
# TWC Automation · Claude Desktop Doctor
# -----------------------------------------
# Diagnoses the Claude desktop app when it freezes / goes to a white screen.
# It collects logs + crash info into ONE zip on your Desktop (safe, read-only),
# then offers to run a safe cache reset that fixes the most common white-screen.
#
# Run it:   bash ~/Downloads/claude-doctor.sh
#
# It only reads and copies. Nothing is changed unless you say yes to the fix at
# the end, and even that is reversible (old caches are moved, not deleted). Your
# Claude chats, settings, and logins are never touched.

set -uo pipefail

say()  { printf '\n\033[1;34m==>\033[0m %s\n' "$*"; }
ok()   { printf '   \033[1;32m✓\033[0m %s\n' "$*"; }
warn() { printf '   \033[1;33m!\033[0m %s\n' "$*"; }
step() { printf '   \033[2m%s\033[0m\n' "$*"; }

APP="/Applications/Claude.app"
APP_SUPPORT="$HOME/Library/Application Support/Claude"
LOGS="$HOME/Library/Logs/Claude"
DIAG="$HOME/Library/Logs/DiagnosticReports"
PREFS="$HOME/Library/Preferences/com.anthropic.claudefordesktop.plist"
SAVED_STATE="$HOME/Library/Saved Application State/com.anthropic.claudefordesktop.savedState"

STAMP="$(date +%Y%m%d-%H%M%S)"
OUT="$HOME/Desktop/claude-diagnostics-$STAMP"
mkdir -p "$OUT"

say "Claude Desktop Doctor"
step "Collecting logs and crash info. This takes about a minute."
step "Nothing on your computer is changed during this part."

# ── 1. System info ─────────────────────────────────────────────────────────
{
  echo "===== macOS / hardware ====="
  sw_vers 2>/dev/null
  echo ""
  system_profiler SPHardwareDataType 2>/dev/null \
    | grep -E "Model Name|Model Identifier|Chip|Processor|Total Number of Cores|Memory" 2>/dev/null
  echo ""
  echo "Uptime:    $(uptime 2>/dev/null)"
  echo ""
  echo "===== Disk space ====="
  df -h / 2>/dev/null
  echo ""
  echo "===== Memory / swap (renderer OOM shows up as heavy swap) ====="
  sysctl vm.swapusage 2>/dev/null
  echo ""
  vm_stat 2>/dev/null
  echo ""
  echo "===== Graphics (GPU) ====="
  system_profiler SPDisplaysDataType 2>/dev/null \
    | grep -E "Chipset Model|Type|VRAM|Vendor|Metal|Resolution|Display Type" 2>/dev/null
} > "$OUT/01-system.txt" 2>&1
ok "System info"

# ── 2. Claude app version + on-disk footprint ──────────────────────────────
{
  echo "===== Claude app ====="
  if [ -d "$APP" ]; then
    echo "Installed: yes ($APP)"
    echo "Version:   $(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP/Contents/Info.plist" 2>/dev/null)"
    echo "Build:     $(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion'            "$APP/Contents/Info.plist" 2>/dev/null)"
    echo "Bundle id: $(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier'         "$APP/Contents/Info.plist" 2>/dev/null)"
  else
    echo "Installed: NO  ($APP not found)"
  fi
  echo ""
  echo "===== Application Support contents ====="
  ls -laR "$APP_SUPPORT" 2>/dev/null | head -200
  echo ""
  echo "===== Cache sizes (a bloated/corrupt cache can cause freezes + white screens) ====="
  for d in "Cache" "Code Cache" "GPUCache" "DawnGraphiteCache" "DawnWebGPUCache" \
           "IndexedDB" "Local Storage" "Session Storage" "blob_storage" "Crashpad" "sentry"; do
    if [ -e "$APP_SUPPORT/$d" ]; then
      printf '%-22s %s\n' "$d" "$(du -sh "$APP_SUPPORT/$d" 2>/dev/null | cut -f1)"
    fi
  done
  echo ""
  echo "Total Application Support: $(du -sh "$APP_SUPPORT" 2>/dev/null | cut -f1)"
} > "$OUT/02-app.txt" 2>&1
ok "App version and cache footprint"

# ── 3. Claude logs (recent tail of each) ───────────────────────────────────
# Includes the Cowork micro-VM logs (cowork_vm_*.log / coworkd.log), which is
# where a renderer/VM crash during a long agentic task leaves a trace.
mkdir -p "$OUT/logs"
if [ -d "$LOGS" ]; then
  for f in "$LOGS"/*.log; do
    [ -e "$f" ] || continue
    tail -n 6000 "$f" > "$OUT/logs/$(basename "$f")" 2>/dev/null
  done
fi
ok "Claude app logs"

# ── 4. Crash reports ───────────────────────────────────────────────────────
mkdir -p "$OUT/crashes"
{
  echo "===== Crashpad minidumps ====="
  ls -lat "$APP_SUPPORT/Crashpad/completed" 2>/dev/null | head -40
  echo ""
  echo "Pending:"
  ls -lat "$APP_SUPPORT/Crashpad/pending" 2>/dev/null | head -40
} > "$OUT/crashes/_crashpad-listing.txt" 2>&1
find "$APP_SUPPORT/Crashpad" -name '*.dmp' -type f 2>/dev/null \
  | head -15 | while read -r dmp; do cp "$dmp" "$OUT/crashes/" 2>/dev/null; done
# macOS crash/hang/spin reports for Claude (matches "Claude Helper (Renderer)" too).
find "$DIAG" -maxdepth 1 -type f -iname '*claude*' -mtime -14 2>/dev/null \
  | while read -r rpt; do cp "$rpt" "$OUT/crashes/" 2>/dev/null; done
if [ -d "$APP_SUPPORT/sentry" ]; then
  mkdir -p "$OUT/crashes/sentry"
  find "$APP_SUPPORT/sentry" -type f -size -2M 2>/dev/null | head -40 \
    | while read -r s; do cp "$s" "$OUT/crashes/sentry/" 2>/dev/null; done
fi
ok "Crash reports"

# ── 5. Unified system log for the Claude processes ─────────────────────────
log show --last 1d --style compact \
  --predicate 'process CONTAINS "Claude"' 2>/dev/null \
  | tail -n 4000 > "$OUT/03-system-log-claude.txt" 2>/dev/null
ok "System log for Claude"

# ── 6. Preferences + MCP config (secrets redacted) ─────────────────────────
if [ -f "$PREFS" ]; then
  plutil -convert xml1 -o "$OUT/04-preferences.plist.xml" "$PREFS" 2>/dev/null \
    || cp "$PREFS" "$OUT/04-preferences.plist" 2>/dev/null
fi
CFG="$APP_SUPPORT/claude_desktop_config.json"
if [ -f "$CFG" ]; then
  if command -v python3 >/dev/null 2>&1; then
    python3 - "$CFG" > "$OUT/05-claude_desktop_config.redacted.json" 2>/dev/null <<'PY'
import json, sys, re
SECRET = re.compile(r'(key|token|secret|password|auth|bearer|cred|client_id|client_secret|api)', re.I)
def red(o):
    if isinstance(o, dict):
        return {k: ("***REDACTED***" if SECRET.search(k) else red(v)) for k, v in o.items()}
    if isinstance(o, list):
        return [red(x) for x in o]
    return o
def env_red(o):
    if isinstance(o, dict):
        for k, v in list(o.items()):
            if k == "env" and isinstance(v, dict):
                o[k] = {ek: "***REDACTED***" for ek in v}
            else:
                env_red(v)
    elif isinstance(o, list):
        for x in o:
            env_red(x)
try:
    d = json.load(open(sys.argv[1]))
    d = red(d); env_red(d)
    print(json.dumps(d, indent=2))
except Exception as e:
    print("could not parse config: %s" % e)
PY
  else
    {
      echo "python3 not available; raw config not copied to avoid leaking secrets."
      echo "config size: $(wc -c < "$CFG" 2>/dev/null) bytes"
    } > "$OUT/05-claude_desktop_config.redacted.json"
  fi
fi
ok "Preferences and config (secrets redacted)"

# ── 7. At-a-glance summary ─────────────────────────────────────────────────
CRASH_CT=$(find "$DIAG" -maxdepth 1 -type f -iname '*claude*' -mtime -7 2>/dev/null | wc -l | tr -d ' ')
DMP_CT=$(find "$APP_SUPPORT/Crashpad" -name '*.dmp' -type f 2>/dev/null | wc -l | tr -d ' ')
{
  echo "Claude Desktop Diagnostics  -  $(date)"
  echo "Collected on: $(scutil --get ComputerName 2>/dev/null) / $(whoami)"
  echo ""
  echo "macOS:        $(sw_vers -productVersion 2>/dev/null)"
  echo "Chip:         $(system_profiler SPHardwareDataType 2>/dev/null | grep -E 'Chip|Processor' | head -1 | sed 's/^ *//')"
  echo "Memory:       $(system_profiler SPHardwareDataType 2>/dev/null | grep -E 'Memory' | head -1 | sed 's/^ *//')"
  echo "Claude ver:   $(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP/Contents/Info.plist" 2>/dev/null)"
  echo "Free disk:    $(df -h / 2>/dev/null | awk 'NR==2{print $4" free of "$2}')"
  echo "Swap in use:  $(sysctl -n vm.swapusage 2>/dev/null)"
  echo ""
  echo "macOS crash/hang reports for Claude (last 7 days): $CRASH_CT"
  echo "Crashpad minidumps on disk:                        $DMP_CT"
  echo ""
  echo "Cache sizes:"
  for d in "Cache" "Code Cache" "GPUCache" "DawnGraphiteCache" "DawnWebGPUCache"; do
    [ -e "$APP_SUPPORT/$d" ] && printf '  %-20s %s\n' "$d" "$(du -sh "$APP_SUPPORT/$d" 2>/dev/null | cut -f1)"
  done
  echo ""
  echo "Last 60 lines of main.log:"
  echo "----------------------------------------------------------------"
  tail -n 60 "$LOGS/main.log" 2>/dev/null
} > "$OUT/00-SUMMARY.txt" 2>&1
ok "Summary"

# ── Zip it up ──────────────────────────────────────────────────────────────
ZIP="$HOME/Desktop/claude-diagnostics-$STAMP.zip"
( cd "$HOME/Desktop" && zip -r -q "$(basename "$ZIP")" "$(basename "$OUT")" ) 2>/dev/null
rm -rf "$OUT" 2>/dev/null

say "Done collecting"
if [ -f "$ZIP" ]; then
  ok "Created on your Desktop:  $(basename "$ZIP")"
  step "Send that .zip to the Automation team on Slack so we can read it."
  open -R "$ZIP" 2>/dev/null
else
  warn "Could not create the zip. The folder is on your Desktop: $(basename "$OUT")"
fi

# ── Optional safe fix ──────────────────────────────────────────────────────
say "Optional fix"
step "The usual cause of the freeze/white-screen is a stale graphics cache."
step "I can clear it now. Safe + reversible: your chats, settings, and logins"
step "are NOT touched, and the old caches are moved to a Desktop backup folder."
printf '   \033[1mClear the cache and restart Claude now? [y/N]\033[0m '
ANSWER=""
read -r ANSWER </dev/tty 2>/dev/null || read -r ANSWER || true
case "$ANSWER" in
  y|Y|yes|YES)
    BACKUP="$HOME/Desktop/claude-cache-backup-$STAMP"
    mkdir -p "$BACKUP"
    say "Quitting Claude"
    osascript -e 'tell application "Claude" to quit' 2>/dev/null || true
    sleep 3
    pkill -f "Claude.app/Contents/MacOS/Claude" 2>/dev/null || true
    sleep 2
    MOVED=0
    for d in "GPUCache" "DawnGraphiteCache" "DawnWebGPUCache" "Code Cache" "Cache"; do
      if [ -e "$APP_SUPPORT/$d" ]; then
        mv "$APP_SUPPORT/$d" "$BACKUP/" 2>/dev/null && { ok "Cleared $d"; MOVED=$((MOVED+1)); }
      fi
    done
    if [ -e "$SAVED_STATE" ]; then
      mv "$SAVED_STATE" "$BACKUP/" 2>/dev/null && { ok "Cleared saved window state"; MOVED=$((MOVED+1)); }
    fi
    if [ "$MOVED" -eq 0 ]; then
      warn "Nothing needed clearing (caches were already empty)."
      rmdir "$BACKUP" 2>/dev/null || true
    else
      ok "Old caches backed up to: $(basename "$BACKUP")"
      step "Drag that folder to the Trash once Claude is working."
    fi
    say "Reopening Claude"
    open -a "$APP" 2>/dev/null || open -a "Claude" 2>/dev/null || true
    step "First launch may be a few seconds slower while it rebuilds. That's normal."
    ;;
  *)
    step "Skipped the fix. You can re-run this anytime to apply it."
    step "Quick tip: when it goes white, press Cmd+R (or View > Reload) to recover"
    step "faster than a full quit, and run heavy daily jobs in Chat rather than Cowork."
    ;;
esac

say "All set. Questions? Ping the Automation team on Slack."
