#!/usr/bin/env bash
# cc-remote — run interactive Claude Code on the shared worker over Cloudflare Tunnel.
# No Tailscale required: SSH rides a websocket through cloudflared.
#
# Each cwd maps to a persistent remote workspace. Your files sync up before
# the session, sync back when you exit. Behaves like local `claude` for most tasks.
#
# Usage:
#   cc-remote [args passed to claude]
#   cc-remote -p "one-shot prompt"
#
# Requires: ssh, rsync, cloudflared (the installer sets everything up).

set -e
set -o pipefail

# ─── config ──────────────────────────────────────────────────────
CC_REMOTE_VERSION="2026.07.05.1"
# Where `cc-remote update` re-fetches itself from. Override with env.
UPDATE_URL="${CC_REMOTE_UPDATE_URL:-https://cc.quake0day.com/cc-remote}"
SSH_HOSTNAME="${CC_SSH_HOSTNAME:-cc-ssh.quake0day.com}"
WORKER_USER="claude"
SSH_ALIAS="cc-worker"          # managed Host block in ~/.ssh/config
# Relative path → resolved against the per-user HOME set by sshd ForceCommand
# (each registered identity gets its own isolated home + workspaces).
WORKER_BASE="workspaces"
SSH_KEY="${CC_REMOTE_KEY:-$HOME/.ssh/id_cc_remote}"

# ─── colors ──────────────────────────────────────────────────────
if [[ -t 1 ]]; then
  C_DIM='\033[2m'; C_BLU='\033[36m'; C_YEL='\033[33m'; C_GRN='\033[32m'; C_RED='\033[31m'; C_RST='\033[0m'
else
  C_DIM=''; C_BLU=''; C_YEL=''; C_GRN=''; C_RED=''; C_RST=''
fi
log()  { printf "${C_BLU}[cc-remote]${C_RST} %s\n" "$*"; }
warn() { printf "${C_YEL}[cc-remote]${C_RST} %s\n" "$*"; }
err()  { printf "${C_RED}[cc-remote]${C_RST} %s\n" "$*" >&2; }
ok()   { printf "${C_GRN}[cc-remote]${C_RST} %s\n" "$*"; }

# ─── self-update / version ───────────────────────────────────────
self_update() {
  local url="$UPDATE_URL" tmp target newver
  target="$0"
  [[ "$target" == */* ]] || target="$(command -v cc-remote 2>/dev/null || echo "$0")"
  log "checking for updates · current v${CC_REMOTE_VERSION} · source ${url}"
  tmp="$(mktemp)" || { err "mktemp failed"; return 1; }
  if ! curl -fsSL "$url" -o "$tmp"; then err "download failed: $url"; rm -f "$tmp"; return 1; fi
  # safety: must look like our script AND parse cleanly before we replace anything
  if ! head -1 "$tmp" | grep -q 'bash' || ! grep -q 'CC_REMOTE_VERSION=' "$tmp"; then
    err "downloaded file doesn't look like cc-remote — aborting"; rm -f "$tmp"; return 1
  fi
  if ! bash -n "$tmp" 2>/dev/null; then
    err "downloaded script failed syntax check — aborting (not installing)"; rm -f "$tmp"; return 1
  fi
  newver="$(grep -m1 '^CC_REMOTE_VERSION=' "$tmp" | cut -d'"' -f2)"
  if cmp -s "$tmp" "$target"; then ok "already up to date (v${CC_REMOTE_VERSION})"; rm -f "$tmp"; return 0; fi
  chmod +x "$tmp"
  if [[ -w "$target" ]]; then
    cp "$tmp" "$target"
  elif command -v sudo >/dev/null 2>&1; then
    warn "writing $target needs sudo…"; sudo cp "$tmp" "$target"
  else
    err "cannot write $target. Manual: curl -fsSL $url -o \"$target\" && chmod +x \"$target\""
    rm -f "$tmp"; return 1
  fi
  rm -f "$tmp"
  ok "updated: v${CC_REMOTE_VERSION} → v${newver:-?}  ($target)"
}

case "${1:-}" in
  update|--update|upgrade|--upgrade|self-update|--self-update)
    self_update; exit $? ;;
  version|--version|-V)
    echo "cc-remote v${CC_REMOTE_VERSION}"; exit 0 ;;
esac

# ─── preflight ───────────────────────────────────────────────────
if ! command -v rsync >/dev/null; then
  err "rsync not found. Install: brew install rsync (macOS) / apt install rsync (Linux)"
  exit 1
fi
if ! command -v cloudflared >/dev/null; then
  err "cloudflared not found — cc-remote tunnels SSH through it."
  err "Install: brew install cloudflared (macOS) / https://developers.cloudflare.com/cloudflared/"
  err "Or re-run the installer: curl -fsSL https://cc.quake0day.com/install.sh | bash"
  exit 1
fi
if [[ ! -f "$SSH_KEY" ]]; then
  err "SSH key not found: $SSH_KEY"
  err "Run the installer first: curl -fsSL https://cc.quake0day.com/install.sh | bash"
  exit 1
fi

# ─── managed ssh config block (self-healing) ─────────────────────
# All connection details (ProxyCommand via cloudflared, key, user) live in a
# managed Host block so ssh AND rsync-over-ssh pick them up without any
# quoting gymnastics around the space-containing ProxyCommand.
ensure_ssh_config() {
  local cfg="$HOME/.ssh/config" begin="# >>> cc-remote managed >>>" end="# <<< cc-remote managed <<<"
  local block
  block="$(cat <<EOF
$begin
Host $SSH_ALIAS
  HostName $SSH_HOSTNAME
  User $WORKER_USER
  IdentityFile $SSH_KEY
  IdentitiesOnly yes
  ProxyCommand cloudflared access ssh --hostname $SSH_HOSTNAME
  StrictHostKeyChecking accept-new
  ServerAliveInterval 30
  ConnectTimeout 15
$end
EOF
)"
  mkdir -p "$HOME/.ssh"; chmod 700 "$HOME/.ssh"
  touch "$cfg"; chmod 600 "$cfg"
  if grep -qF "$begin" "$cfg"; then
    # replace existing managed block if it drifted
    local current
    current="$(sed -n "/^$begin\$/,/^$end\$/p" "$cfg")"
    if [[ "$current" != "$block" ]]; then
      local tmp; tmp="$(mktemp)"
      sed "/^$begin\$/,/^$end\$/d" "$cfg" > "$tmp"
      printf '%s\n' "$block" >> "$tmp"
      cat "$tmp" > "$cfg"; rm -f "$tmp"
      log "refreshed managed ssh config block ($SSH_ALIAS)"
    fi
  else
    printf '\n%s\n' "$block" >> "$cfg"
    log "added managed ssh config block ($SSH_ALIAS)"
  fi
}
ensure_ssh_config
ssh_target="$SSH_ALIAS"

# Refuse $HOME / root / common giant trees. Override with --force-cwd.
force=0
for a in "$@"; do [[ "$a" == "--force-cwd" ]] && force=1; done
if [[ $force -eq 0 ]]; then
  case "$(pwd)" in
    "$HOME"|"$HOME/"|/|/Users|/home)
      err "cwd is \$HOME or a root dir — would try to sync your entire user folder (likely tens of GB)."
      err "Run cc-remote from inside a specific project directory:"
      err "    cd ~/my-project && cc-remote"
      err "If you really mean to sync the current dir, re-run with: cc-remote --force-cwd ..."
      exit 1
      ;;
  esac
  case "$(pwd)" in
    "$HOME"/Library*|"$HOME"/Downloads*|"$HOME"/Documents|"$HOME"/Pictures*|"$HOME"/Movies*|"$HOME"/Music*)
      err "cwd looks like a system / media folder ($(pwd))."
      err "cc-remote is for project directories. Refusing to sync — pass --force-cwd to override."
      exit 1
      ;;
  esac
  # Soft file-count check (portable across macOS/Linux)
  fcount=$(find . -type f \
    -not -path '*/.git/*' -not -path '*/node_modules/*' \
    -not -path '*/.venv/*' -not -path '*/venv/*' \
    -not -path '*/__pycache__/*' -not -path '*/.cache/*' \
    -not -path '*/dist/*' -not -path '*/build/*' \
    2>/dev/null | head -10001 | wc -l | tr -d ' ')
  if [[ "$fcount" -gt 10000 ]]; then
    warn "Current dir has >10k files (after standard excludes). rsync may take a while."
    warn "If unintentional, Ctrl+C in 3s. Override this check with --force-cwd."
    sleep 3
  fi
fi

# ─── compute remote workspace path ───────────────────────────────
local_dir="$(pwd)"
host_tag="$(hostname -s 2>/dev/null || echo host)"
me="${USER:-$(whoami)}"
# hash of (host:user:cwd) -> stable 10-char ID
key_str="${host_tag}:${me}:${local_dir}"
if command -v shasum >/dev/null; then
  hash_id="$(printf '%s' "$key_str" | shasum -a 256 | cut -c1-10)"
else
  hash_id="$(printf '%s' "$key_str" | sha256sum | cut -c1-10)"
fi
safe_name="$(basename "$local_dir" | tr -c 'a-zA-Z0-9._-' '_')"
# Relative to remote $HOME (resolved per-identity by claude-session wrapper)
remote_dir="${WORKER_BASE}/sync-${safe_name}-${hash_id}"

# ─── ensure remote dir + ssh works ───────────────────────────────
log "preparing remote workspace…"
if ! ssh "$ssh_target" "mkdir -p \"\$HOME/$remote_dir\" && echo ok" >/dev/null 2>&1; then
  err "cannot reach the worker through the tunnel ($SSH_HOSTNAME)"
  err "checks:"
  err "  cloudflared access ssh --hostname $SSH_HOSTNAME   (should connect, Ctrl+C to quit)"
  err "  ssh $SSH_ALIAS whoami                              (should print 'claude')"
  err "If your key was never registered: curl -fsSL https://cc.quake0day.com/install.sh | bash"
  exit 1
fi

# ─── rsync excludes ──────────────────────────────────────────────
rsync_excludes=(
  --exclude='.git/'         --exclude='.svn/'        --exclude='.hg/'
  --exclude='node_modules/' --exclude='.venv/'       --exclude='venv/'
  --exclude='__pycache__/'  --exclude='*.pyc'        --exclude='.mypy_cache/'
  --exclude='.pytest_cache/' --exclude='.ruff_cache/'
  --exclude='dist/'         --exclude='build/'       --exclude='target/'
  --exclude='.next/'        --exclude='.nuxt/'       --exclude='.cache/'
  --exclude='*.log'         --exclude='.DS_Store'
)
# honor .claudeignore if it exists
[[ -f "$local_dir/.claudeignore" ]] && rsync_excludes+=(--exclude-from="$local_dir/.claudeignore")

# ─── sync up ─────────────────────────────────────────────────────
log "↑ syncing $(pwd) → ${SSH_HOSTNAME}:${remote_dir##*/}"
rsync_args=(-az --delete "${rsync_excludes[@]}" \
            "$local_dir/" "${ssh_target}:${remote_dir}/")
rsync "${rsync_args[@]}" 2>&1 | tail -3 || { err "upload failed"; exit 1; }

# ─── sync back on exit ───────────────────────────────────────────
sync_back() {
  ec=$?
  # stop the live mid-session sync loop first, to avoid racing the final pull
  if [[ -n "${LIVE_SYNC_PID:-}" ]]; then
    kill "$LIVE_SYNC_PID" 2>/dev/null || true
    wait "$LIVE_SYNC_PID" 2>/dev/null || true
  fi
  log "↓ syncing changes back"
  # -u: never clobber local files you edited more recently than the remote copy
  rsync -auz "${rsync_excludes[@]}" \
    "${ssh_target}:${remote_dir}/" "$local_dir/" 2>&1 | tail -3 || warn "download had issues"
  if [[ $ec -eq 0 ]]; then ok "session ended cleanly"; fi
  exit $ec
}
trap sync_back EXIT
# Route fatal signals through EXIT so sync_back runs and the live-sync loop is
# reaped (untrapped TERM/HUP would skip the EXIT trap → orphaned rsync loop).
trap 'exit 129' HUP
trap 'exit 143' TERM
LIVE_SYNC_PID=""

# ─── run claude interactively ────────────────────────────────────
ok "launching claude (cwd: ${remote_dir##*/})"
echo

# Detect:
#   -p / --print            → headless one-shot, REQUIRES skip-perms (no TTY for Y/N)
#   -dsp / --dsp            → user explicitly asks for --dangerously-skip-permissions
#   $CC_REMOTE_DSP=1        → env var sets it permanently per shell
is_headless=0
skip_perms=0
case "${CC_REMOTE_DSP:-0}" in 1|true|yes|on) skip_perms=1 ;; esac
for arg in "$@"; do
  case "$arg" in
    -p|--print)   is_headless=1 ;;
    -dsp|--dsp)   skip_perms=1 ;;
  esac
done
[[ $is_headless -eq 1 ]] && skip_perms=1

# Build a properly-quoted remote command. We want: cd <dir> && claude <args...>
remote_args=""
for arg in "$@"; do
  # Strip our own wrapper-only flags before passing to claude
  case "$arg" in
    --force-cwd|-dsp|--dsp) continue ;;
  esac
  remote_args+="$(printf '%q ' "$arg")"
done
[[ $skip_perms -eq 1 ]] && remote_args+="--dangerously-skip-permissions "
# Worker custom MCP tools (whoami / dashboard_status / r2_publish / archive_session)
remote_args+="--mcp-config /etc/claude-worker/mcp.json "
if [[ $is_headless -eq 1 ]]; then ssh_tty=(); else ssh_tty=(-t); fi

# ─── live mid-session sync (worker → local, every N seconds) ─────
# Mirrors Claude's changes back to your local dir *during* the session, not
# just on exit — inspect/run files live, and don't lose work if the link drops.
# Pull-only with -u: never clobbers local files you've edited more recently.
# Interactive sessions only. Tune via CC_REMOTE_SYNC_SECS (0 = disable).
LIVE_SYNC_SECS="${CC_REMOTE_SYNC_SECS:-5}"
if ! [[ "$LIVE_SYNC_SECS" =~ ^[0-9]+$ ]]; then
  warn "CC_REMOTE_SYNC_SECS='$LIVE_SYNC_SECS' is not a number — using 5"
  LIVE_SYNC_SECS=5
fi
if [[ $is_headless -eq 0 && "$LIVE_SYNC_SECS" -gt 0 ]]; then
  (
    while sleep "$LIVE_SYNC_SECS"; do
      # bail out if the parent script died without reaping us (e.g. SIGKILL)
      kill -0 "$$" 2>/dev/null || exit 0
      rsync -auz "${rsync_excludes[@]}" \
        "${ssh_target}:${remote_dir}/" "$local_dir/" >/dev/null 2>&1 || true
    done
  ) &
  LIVE_SYNC_PID=$!
  ok "live sync on · pulling worker → local every ${LIVE_SYNC_SECS}s (CC_REMOTE_SYNC_SECS=0 to disable)"
fi

# shellcheck disable=SC2029
ssh "${ssh_tty[@]}" "$ssh_target" "cd \"\$HOME/${remote_dir}\" && claude ${remote_args}"
