#!/usr/bin/env bash
# swap-guard — shared state/guard engine for the /swap + /handoff Claude Code integration.
# Contract: section A of the binding interface contract.
# Subcommands:
#   whoami                          own session registry JSON (compact); exit 1 if not in a session
#   sessions                        JSON array of OTHER live claude sessions (always exit 0)
#   preflight <target>              switch-safety verdict JSON for slot number / alias / email (always exit 0)
#   path flag|handoff [--cwd <dir>] print per-cwd state-file path
#   flag <json>                     validate + fill + atomic-write relaunch flag; prints path (exit 1 on bad input)
#   status                          own-session status JSON (always exit 0)
#   cancel [--cwd <dir>]            remove pending handoff + flag for cwd (always exit 0)
#   artifacts [--session <id>] [--cwd <dir>]
#                                   collect this session's claude.ai artifacts: copy sources to
#                                   $STATE_ROOT/handoff-artifacts/<sid>/ + manifest.json; prints manifest
#   schedule-kill <pid>             v1 stub (exit 1)
set -u

STATE_ROOT="$HOME/.claude-swap-backup"
CTX_DIR="$STATE_ROOT/ctx"
ARCHIVE_DIR="$STATE_ROOT/handoff-archive"
ARTIFACTS_DIR="$STATE_ROOT/handoff-artifacts"
SESSIONS_DIR="$HOME/.claude/sessions"

usage() {
  cat >&2 <<'EOF'
usage: swap-guard <subcommand> [args]
  whoami                          own session registry JSON (compact)
  sessions                        JSON array of other live claude sessions
  preflight <target>              switch-safety verdict for slot / alias / email
  path flag|handoff [--cwd <dir>] print per-cwd state-file path
  flag <json>                     write relaunch flag ({"mode":"handoff"|"restart",...}); prints path
  status                          own-session status JSON
  cancel [--cwd <dir>]            remove pending handoff + flag for cwd
  artifacts [--session <id>] [--cwd <dir>]
                                  snapshot this session's artifacts (sources + manifest) for handoff
  schedule-kill <pid>             phase-2 stub
EOF
  exit 2
}

# HASH(cwd) per shared constants: first 12 hex of sha256
hash_cwd() { printf '%s' "$1" | /usr/bin/shasum -a 256 | awk '{print $1}' | cut -c1-12; }

flag_path_for()    { printf '%s/relaunch-%s.json\n'      "$STATE_ROOT" "$(hash_cwd "$1")"; }
pending_path_for() { printf '%s/handoff-pending-%s.md\n' "$STATE_ROOT" "$(hash_cwd "$1")"; }

# whoami_json [start_pid] — walk the PPID chain from start_pid (default $$) up to pid 1;
# print the first matching registry file's JSON (compact) and return 0, else return 1.
whoami_json() {
  local p="${1:-$$}"
  while :; do
    case "$p" in ''|*[!0-9]*) return 1;; esac
    [ "$p" -gt 1 ] || return 1
    if [ -f "$SESSIONS_DIR/$p.json" ]; then
      jq -c . "$SESSIONS_DIR/$p.json" 2>/dev/null || cat "$SESSIONS_DIR/$p.json"
      return 0
    fi
    p="$(ps -o ppid= -p "$p" 2>/dev/null | tr -d ' ')"
  done
}

# session_live <pid> — true iff the process exists (kill -0) AND its comm contains "claude"
session_live() {
  local pid="${1:-}" comm
  case "$pid" in ''|*[!0-9]*) return 1;; esac
  kill -0 "$pid" 2>/dev/null || return 1
  comm="$(ps -o comm= -p "$pid" 2>/dev/null)"
  case "$comm" in *claude*) return 0;; *) return 1;; esac
}

# transcript_status <cwd> <sessionId> — activity fallback for sessions whose registry
# file has no status field (clients < ~2.1.211 never publish one). An active session
# appends to ~/.claude/projects/<flattened-cwd>/<sessionId>.jsonl on every turn, so:
# mtime within TRANSCRIPT_ACTIVE_SECS -> "busy"; older -> "idle"; no transcript -> "unknown".
TRANSCRIPT_ACTIVE_SECS=120
transcript_status() {
  local cwd="${1:-}" sid="${2:-}" t mtime now
  [ -n "$cwd" ] && [ -n "$sid" ] || { printf 'unknown\n'; return 0; }
  t="$HOME/.claude/projects/$(printf '%s' "$cwd" | sed 's/[^A-Za-z0-9]/-/g')/$sid.jsonl"
  [ -f "$t" ] || { printf 'unknown\n'; return 0; }
  mtime="$(stat -f %m "$t" 2>/dev/null)"
  case "$mtime" in ''|*[!0-9]*) printf 'unknown\n'; return 0;; esac
  now="$(date +%s)"
  if [ $(( now - mtime )) -le "$TRANSCRIPT_ACTIVE_SECS" ]; then
    printf 'busy\n'
  else
    printf 'idle\n'
  fi
}

# sessions_json — JSON array of OTHER live sessions. status precedence: the value the
# session itself reports (statusSource "reported") > transcript-mtime fallback
# (statusSource "transcript-mtime") > "unknown" (statusSource "none").
sessions_json() {
  local own_pid="" out="[]" new_out f pid entry st
  own_pid="$(whoami_json 2>/dev/null | jq -r '.pid // empty' 2>/dev/null)" || own_pid=""
  for f in "$SESSIONS_DIR"/*.json; do
    [ -f "$f" ] || continue
    pid="$(jq -r '.pid // empty' "$f" 2>/dev/null)" || continue
    [ -n "$pid" ] || continue
    [ "$pid" = "$own_pid" ] && continue
    session_live "$pid" || continue
    entry="$(jq -c '{pid, sessionId, cwd, kind, entrypoint, status: (.status // "unknown"),
      statusSource: (if .status then "reported" else "none" end)}' "$f" 2>/dev/null)" || continue
    [ -n "$entry" ] || continue
    if [ "$(printf '%s' "$entry" | jq -r '.statusSource' 2>/dev/null)" = "none" ]; then
      st="$(transcript_status "$(printf '%s' "$entry" | jq -r '.cwd // empty')" \
                              "$(printf '%s' "$entry" | jq -r '.sessionId // empty')")"
      case "$st" in
        busy|idle) entry="$(printf '%s' "$entry" | jq -c --arg s "$st" '.status = $s | .statusSource = "transcript-mtime"' 2>/dev/null)" || continue;;
      esac
    fi
    new_out="$(printf '%s' "$out" | jq -c --argjson e "$entry" '. + [$e]' 2>/dev/null)" && [ -n "$new_out" ] && out="$new_out"
  done
  printf '%s\n' "$out"
}

# resolve_cwd [explicit] — precedence: explicit --cwd > whoami .cwd > $PWD
resolve_cwd() {
  local c="${1:-}"
  if [ -z "$c" ]; then
    c="$(whoami_json 2>/dev/null | jq -r '.cwd // empty' 2>/dev/null)" || c=""
  fi
  [ -n "$c" ] || c="$PWD"
  printf '%s\n' "$c"
}

# parse_cwd_opt "$@" — echo value of --cwd/--cwd= if present; return 1 if absent
parse_cwd_opt() {
  while [ $# -gt 0 ]; do
    case "$1" in
      --cwd) shift; printf '%s\n' "${1:-}"; return 0;;
      --cwd=*) printf '%s\n' "${1#--cwd=}"; return 0;;
    esac
    shift
  done
  return 1
}

cmd_whoami() {
  local out
  if out="$(whoami_json)"; then
    printf '%s\n' "$out"
    exit 0
  fi
  printf '%s\n' '{"error":"not-in-session"}'
  exit 1
}

cmd_sessions() {
  sessions_json
  exit 0
}

cmd_preflight() {
  local raw="${1:-}" busy busy_n list tgt slot="" verdict detail target_json number email us
  busy="$(sessions_json | jq -c '[.[] | select(.status == "busy" or .status == "unknown")]' 2>/dev/null)" || busy="[]"
  [ -n "$busy" ] || busy="[]"
  busy_n="$(printf '%s' "$busy" | jq 'length' 2>/dev/null)" || busy_n=0

  if [ -z "$raw" ]; then
    jq -cn --argjson busy "$busy" \
      '{verdict:"unknown-target",target:{number:null,email:null},busy:$busy,detail:"no target given"}'
    exit 0
  fi

  list="$(cswap list --json 2>/dev/null)"
  if [ -z "$list" ] || ! printf '%s' "$list" | jq -e . >/dev/null 2>&1; then
    jq -cn --argjson busy "$busy" --arg t "$raw" \
      '{verdict:"unknown-target",target:{number:null,email:null},busy:$busy,detail:("cswap list --json failed - cannot verify target " + ($t|tojson))}'
    exit 0
  fi

  # Resolve target: digits -> slot number; else alias in sequence.json; else email
  case "$raw" in
    *[!0-9]*)
      slot="$(jq -r --arg a "$raw" '.accounts | to_entries[] | select((.value.alias // "") == $a) | .key' \
        "$STATE_ROOT/sequence.json" 2>/dev/null | head -n1)" || slot=""
      ;;
    *) slot="$raw";;
  esac
  if [ -n "$slot" ]; then
    tgt="$(printf '%s' "$list" | jq -c --arg n "$slot" '[.accounts[]? | select((.number|tostring) == $n)] | first // empty' 2>/dev/null)"
  else
    tgt="$(printf '%s' "$list" | jq -c --arg e "$raw" '[.accounts[]? | select(((.email // "") | ascii_downcase) == ($e | ascii_downcase))] | first // empty' 2>/dev/null)"
  fi

  if [ -z "$tgt" ]; then
    jq -cn --argjson busy "$busy" --arg t "$raw" \
      '{verdict:"unknown-target",target:{number:null,email:null},busy:$busy,detail:("no account matches " + ($t|tojson) + " (not a slot number, alias, or email)")}'
    exit 0
  fi

  target_json="$(printf '%s' "$tgt" | jq -c '{number, email}')"
  number="$(printf '%s' "$tgt" | jq -r '.number')"
  email="$(printf '%s' "$tgt" | jq -r '.email // ""')"
  us="$(printf '%s' "$tgt" | jq -r '.usageStatus // ""' | tr '[:upper:]' '[:lower:]')"

  case "$us" in
    *login*|*quarantin*|*expired*|*invalid*|*revoked*)
      verdict="relogin-required"
      detail="account $number ($email) requires re-login (usageStatus: $us) - recover with /login then: cswap add --slot $number"
      ;;
    *)
      if [ "$busy_n" -gt 0 ] 2>/dev/null; then
        verdict="busy"
        detail="account $number ($email) is healthy, but $busy_n other session(s) are busy or possibly-busy - a switch flips ALL sessions within ~30s"
      else
        verdict="ok"
        detail="account $number ($email) is healthy and no other session is busy"
      fi
      ;;
  esac

  jq -cn --arg v "$verdict" --argjson t "$target_json" --argjson busy "$busy" --arg d "$detail" \
    '{verdict:$v,target:$t,busy:$busy,detail:$d}'
  exit 0
}

cmd_path() {
  local kind="${1:-}"
  [ $# -gt 0 ] && shift
  local cwd_opt="" cwd
  cwd_opt="$(parse_cwd_opt "$@")" || cwd_opt=""
  cwd="$(resolve_cwd "$cwd_opt")"
  case "$kind" in
    flag)    flag_path_for "$cwd";;
    handoff) pending_path_for "$cwd";;
    *) usage;;
  esac
  exit 0
}

cmd_flag() {
  local input="${1:-}"
  if [ -z "$input" ] || ! printf '%s' "$input" | jq -e . >/dev/null 2>&1; then
    echo "swap-guard flag: input is not valid JSON" >&2
    exit 1
  fi
  local mode
  mode="$(printf '%s' "$input" | jq -r '.mode // empty')"
  case "$mode" in
    handoff|restart) ;;
    *) echo 'swap-guard flag: .mode must be "handoff" or "restart"' >&2; exit 1;;
  esac
  local sid cwd created out p tmp
  sid="$(printf '%s' "$input" | jq -r '.sessionId // ""')"
  cwd="$(printf '%s' "$input" | jq -r '.cwd // empty')"
  cwd="$(resolve_cwd "$cwd")"
  created="$(printf '%s' "$input" | jq -r '.created // empty' 2>/dev/null)"
  case "$created" in ''|*[!0-9]*) created="$(date +%s)";; esac
  out="$(jq -cn --arg m "$mode" --arg s "$sid" --arg c "$cwd" --argjson t "$created" \
    '{mode:$m,sessionId:$s,cwd:$c,created:$t}')"
  p="$(flag_path_for "$cwd")"
  mkdir -p "$STATE_ROOT" 2>/dev/null || { echo "swap-guard flag: cannot create $STATE_ROOT" >&2; exit 1; }
  tmp="$p.tmp.$$"
  if printf '%s\n' "$out" > "$tmp" 2>/dev/null && mv "$tmp" "$p" 2>/dev/null; then
    printf '%s\n' "$p"
    exit 0
  fi
  rm -f "$tmp" 2>/dev/null
  echo "swap-guard flag: write failed" >&2
  exit 1
}

# _status_json <sessionId-or-empty> <cwd> — build the status JSON (factored for testability)
_status_json() {
  local sid="${1:-}" cwd="${2:-$PWD}"
  local pct="" fired=0 bannered=0
  if [ -n "$sid" ] && [ -f "$CTX_DIR/$sid.json" ]; then
    pct="$(jq -r 'if (.pct|type) == "number" then (.pct|tostring) else "" end' "$CTX_DIR/$sid.json" 2>/dev/null)" || pct=""
  fi
  if [ -n "$sid" ] && [ -f "$CTX_DIR/$sid.state" ]; then
    fired="$(jq -r '.fired // 0' "$CTX_DIR/$sid.state" 2>/dev/null)" || fired=0
    bannered="$(jq -r '.bannered // 0' "$CTX_DIR/$sid.state" 2>/dev/null)" || bannered=0
  fi
  case "$fired" in ''|*[!0-9]*) fired=0;; esac
  case "$bannered" in ''|*[!0-9]*) bannered=0;; esac

  local h pending flagf pending_out="" page="" flag_out="" arch="" created
  h="$(hash_cwd "$cwd")"
  pending="$STATE_ROOT/handoff-pending-$h.md"
  flagf="$STATE_ROOT/relaunch-$h.json"
  if [ -f "$pending" ]; then
    pending_out="$pending"
    created="$(sed -n '1s/.*created="\([0-9][0-9]*\)".*/\1/p' "$pending" 2>/dev/null)"
    [ -n "$created" ] || created="$(stat -f %m "$pending" 2>/dev/null)"
    case "$created" in
      ''|*[!0-9]*) page="";;
      *) page="$(( $(date +%s) - created ))";;
    esac
  fi
  [ -f "$flagf" ] && flag_out="$flagf"
  if [ -d "$ARCHIVE_DIR" ]; then
    arch="$(ls "$ARCHIVE_DIR" 2>/dev/null | grep -E -- "-$h(-expired)?\.md$" | sort | tail -n1)"
    [ -n "$arch" ] && arch="$ARCHIVE_DIR/$arch"
  fi

  jq -cn --arg sid "$sid" --arg pct "$pct" --arg fired "$fired" --arg bannered "$bannered" \
    --arg pending "$pending_out" --arg page "$page" --arg flagp "$flag_out" --arg arch "$arch" '
    {sessionId: (if $sid == "" then null else $sid end),
     pct: (if $pct == "" then null else ($pct|tonumber) end),
     fired: ($fired|tonumber),
     bannered: ($bannered|tonumber),
     pendingHandoff: (if $pending == "" then null else $pending end),
     pendingAgeSec: (if $page == "" then null else ($page|tonumber) end),
     flag: (if $flagp == "" then null else $flagp end),
     latestArchive: (if $arch == "" then null else $arch end)}'
}

cmd_status() {
  local who sid="" cwd=""
  if who="$(whoami_json 2>/dev/null)"; then
    sid="$(printf '%s' "$who" | jq -r '.sessionId // empty' 2>/dev/null)" || sid=""
    cwd="$(printf '%s' "$who" | jq -r '.cwd // empty' 2>/dev/null)" || cwd=""
  fi
  [ -n "$cwd" ] || cwd="$PWD"
  _status_json "$sid" "$cwd"
  exit 0
}

cmd_cancel() {
  local cwd_opt="" cwd removed=0 f
  cwd_opt="$(parse_cwd_opt "$@")" || cwd_opt=""
  cwd="$(resolve_cwd "$cwd_opt")"
  for f in "$(pending_path_for "$cwd")" "$(flag_path_for "$cwd")"; do
    if [ -f "$f" ]; then
      rm -f "$f" 2>/dev/null && { printf 'removed: %s\n' "$f"; removed=1; }
    fi
  done
  [ "$removed" -eq 1 ] || printf 'nothing to remove for cwd %s\n' "$cwd"
  exit 0
}

# cmd_artifacts — snapshot every claude.ai artifact this session (and its subagents /
# workflows) published or touched, so a handoff can carry them across sessions and
# accounts. Source of truth is the transcript's structured toolUseResult
# ({url, artifact_id, title, path, ...}) — never the model's recollection. Sources
# live in the session-scoped /tmp scratchpad, so they are COPIED to a durable dir.
# Artifacts are owned server-side by the account that published them; the manifest
# records the account active at collection time as `account`.
cmd_artifacts() {
  local sid="" cwd="" who flat tdir out_dir manifest tmp acct now
  while [ $# -gt 0 ]; do
    case "$1" in
      --session) sid="${2:-}"; shift 2 || shift;;
      --cwd)     cwd="${2:-}"; shift 2 || shift;;
      *) shift;;
    esac
  done
  if [ -z "$sid" ] || [ -z "$cwd" ]; then
    who="$(whoami_json 2>/dev/null)" || who=""
    [ -n "$sid" ] || sid="$(printf '%s' "$who" | jq -r '.sessionId // empty' 2>/dev/null)"
    [ -n "$cwd" ] || cwd="$(printf '%s' "$who" | jq -r '.cwd // empty' 2>/dev/null)"
  fi
  [ -n "$cwd" ] || cwd="$PWD"
  case "$sid" in ''|*/*|*..*) printf '%s\n' '{"error":"no-session-id"}'; exit 1;; esac
  flat="$(printf '%s' "$cwd" | sed 's/[^A-Za-z0-9]/-/g')"
  tdir="$HOME/.claude/projects/$flat"
  [ -f "$tdir/$sid.jsonl" ] || { printf '{"error":"no-transcript","path":"%s"}\n' "$tdir/$sid.jsonl"; exit 1; }

  out_dir="$ARTIFACTS_DIR/$sid"
  mkdir -p "$out_dir" || exit 1
  acct="$(jq -r '.oauthAccount.emailAddress // empty' "$HOME/.claude.json" 2>/dev/null)"
  now="$(date +%s)"

  # One record per artifact_id (last touch wins). Join each result with its tool_use
  # input to recover supporting `files` (map form: pub -> src | {from}).
  tmp="$(mktemp)"
  { printf '%s\n' "$tdir/$sid.jsonl"
    [ -d "$tdir/$sid" ] && find "$tdir/$sid" -name '*.jsonl' -type f
  } | while IFS= read -r f; do
    jq -c '
      if .type == "assistant" then
        (.message.content[]? | select(.type == "tool_use" and .name == "Artifact")
         | {k: "use", id, files: (.input.files // null), file_path: (.input.file_path // null)})
      elif .type == "user" and (.toolUseResult | type) == "object"
           and (.toolUseResult.artifact_id // .toolUseResult.url // null) != null then
        {k: "res", id: (.message.content[0].tool_use_id // null), ts: (.timestamp // ""),
         r: (.toolUseResult | {url, artifact_id, title, path, audience, capabilities})}
      else empty end' "$f" 2>/dev/null
  done > "$tmp"

  jq -s '
    (map(select(.k == "use")) | map({key: (.id // ""), value: .}) | from_entries) as $uses
    | map(select(.k == "res" and (.r.url // "") != ""))
    | sort_by(.ts)
    | map(. as $x | $x.r + {ts: $x.ts,
          path: ($x.r.path // $uses[$x.id // ""].file_path // null),
          files: ($uses[$x.id // ""].files // null)})
    | map(.artifact_id = (.artifact_id // (.url | sub(".*/artifact/"; ""))))
    | group_by(.artifact_id)
    | map(reduce .[] as $e ({}; . + ($e | with_entries(select(.value != null)))))
    | sort_by(.ts)' "$tmp" > "$tmp.recs" 2>/dev/null || printf '[]' > "$tmp.recs"
  rm -f "$tmp"

  # Copy sources: page -> <id>/<basename>; supporting files -> <id>/<published path>.
  local n i rec aid src dst saved files_saved pub fsrc
  n="$(jq 'length' "$tmp.recs")"
  printf '[]' > "$tmp.out"
  i=0
  while [ "$i" -lt "$n" ]; do
    rec="$(jq -c ".[$i]" "$tmp.recs")"
    aid="$(printf '%s' "$rec" | jq -r '.artifact_id' | tr -cd 'A-Za-z0-9_-')"
    src="$(printf '%s' "$rec" | jq -r '.path // empty')"
    saved="null"; files_saved="{}"
    if [ -n "$aid" ] && [ -n "$src" ] && [ -f "$src" ]; then
      mkdir -p "$out_dir/$aid"
      dst="$out_dir/$aid/${src##*/}"
      cp -p "$src" "$dst" 2>/dev/null && saved="$(jq -Rn --arg d "$dst" '$d')"
      while IFS=$'\t' read -r pub fsrc; do
        [ -n "$pub" ] && [ -n "$fsrc" ] || continue
        case "$pub" in /*|*..*) continue;; esac
        case "$fsrc" in /*) :;; *) fsrc="$cwd/$fsrc";; esac
        [ -f "$fsrc" ] || continue
        mkdir -p "$(dirname "$out_dir/$aid/$pub")" &&
          cp -p "$fsrc" "$out_dir/$aid/$pub" 2>/dev/null &&
          files_saved="$(printf '%s' "$files_saved" | jq -c --arg k "$pub" --arg v "$out_dir/$aid/$pub" '. + {($k): $v}')"
      done < <(printf '%s' "$rec" | jq -r '
        (.files // {}) | if type == "object" then to_entries[]
          | [.key, (if (.value | type) == "string" then .value
                    elif (.value | type) == "object" then (.value.from // empty)
                    else empty end)] | @tsv else empty end' 2>/dev/null)
    fi
    jq --argjson r "$rec" --argjson s "$saved" --argjson fs "$files_saved" \
      '. + [$r + {saved: $s, savedFiles: $fs} | del(.files)]' "$tmp.out" > "$tmp.out2" && mv "$tmp.out2" "$tmp.out"
    i=$((i + 1))
  done

  manifest="$out_dir/manifest.json"
  jq -n --arg sid "$sid" --arg cwd "$cwd" --arg acct "$acct" --argjson now "$now" \
     --slurpfile a "$tmp.out" \
     '{sessionId: $sid, cwd: $cwd, account: $acct, collected: $now, artifacts: $a[0]}' > "$manifest.tmp" &&
    mv "$manifest.tmp" "$manifest"
  rm -f "$tmp.recs" "$tmp.out"
  # Keep the store bounded: drop snapshots untouched for 30+ days.
  find "$ARTIFACTS_DIR" -mindepth 1 -maxdepth 1 -type d -mtime +30 -exec rm -rf {} + 2>/dev/null
  jq -c --arg m "$manifest" '. + {manifest: $m}' "$manifest"
  exit 0
}

cmd_schedule_kill() {
  echo "phase-2 feature not enabled" >&2
  exit 1
}

main() {
  local sub="${1:-}"
  [ $# -gt 0 ] && shift
  case "$sub" in
    whoami)        cmd_whoami "$@";;
    sessions)      cmd_sessions "$@";;
    preflight)     cmd_preflight "$@";;
    path)          cmd_path "$@";;
    flag)          cmd_flag "$@";;
    status)        cmd_status "$@";;
    cancel)        cmd_cancel "$@";;
    artifacts)     cmd_artifacts "$@";;
    schedule-kill) cmd_schedule_kill "$@";;
    *) usage;;
  esac
}

if [ "${BASH_SOURCE[0]:-}" = "$0" ]; then
  main "$@"
fi
