#!/usr/bin/env bash
#
#   ✻ Claustrophobic
#   multi-account harness for Claude Code. when the walls close in, switch rooms.
#
#   Self-contained: copy this file anywhere and run `bash claustrophobic setup`.
#   Requires bash 3.2+, the claude CLI, and python3 or node.
#
set -o pipefail

CLAUSTRO_VERSION="1.1.0"
CLAUSTRO_ROOT="$HOME/.claustrophobic"
CLAUSTRO_BIN="$CLAUSTRO_ROOT/bin/claustrophobic"
ACCOUNTS_DIR="$CLAUSTRO_ROOT/accounts"
USAGE_DIR="$CLAUSTRO_ROOT/usage"
PROFILES_DIR="$CLAUSTRO_ROOT/profiles"
SWAP_DIR="$CLAUSTRO_ROOT/swap"
CONFIG_FILE="$CLAUSTRO_ROOT/config"
CLAUDE_HOME="$HOME/.claude"
NOW=$(date +%s)

# ---------------------------------------------------------------- colors ----

if [ -n "${NO_COLOR:-}" ]; then
    CLAUSTRO_COLOR=0
elif [ -t 1 ] || [ "${CLAUSTRO_FORCE_COLOR:-}" = "1" ]; then
    CLAUSTRO_COLOR=1
else
    CLAUSTRO_COLOR=0
fi

set_colors() {
    if [ "$CLAUSTRO_COLOR" = "1" ]; then
        C_OR=$'\033[38;5;173m'   # Claude crail orange
        C_BOLD=$'\033[1m'
        C_DIM=$'\033[2m'
        C_GRN=$'\033[38;5;108m'
        C_YEL=$'\033[38;5;179m'
        C_RED=$'\033[38;5;167m'
        C_RST=$'\033[0m'
    else
        C_OR='' C_BOLD='' C_DIM='' C_GRN='' C_YEL='' C_RED='' C_RST=''
    fi
}
set_colors

err() { printf '%s✻ claustrophobic:%s %s\n' "$C_RED" "$C_RST" "$*" >&2; }
die() { err "$@"; exit 1; }
note() { printf '%s✻%s %s\n' "$C_OR" "$C_RST" "$*"; }

# ----------------------------------------------------------- json runtime ----
# One helper program in python3 and node flavors; whichever the machine has.

read -r -d '' PY_HELPER <<'PYEOF' || true
import json, os, sys

def g(d, path):
    for k in path.split('.'):
        if not isinstance(d, dict) or k not in d:
            return None
        d = d[k]
    return d

def q(v):
    s = '' if v is None else str(v)
    return "'" + s.replace("'", "'\\''") + "'"

def epoch(v):
    if v is None:
        return None
    s = str(v)
    try:
        return int(float(s))
    except ValueError:
        pass
    try:
        from datetime import datetime
        return int(datetime.fromisoformat(s.replace('Z', '+00:00')).timestamp())
    except Exception:
        return None

op = sys.argv[1]

if op == 'statusline-kv':
    try:
        d = json.load(sys.stdin)
    except Exception:
        d = {}
    rl = g(d, 'rate_limits') or {}
    def pct(k):
        v = g(rl, k + '.used_percentage')
        try:
            return int(round(float(v)))
        except (TypeError, ValueError):
            return None
    print('SL_MODEL=' + q(g(d, 'model.display_name')))
    print('SL_CWD=' + q(g(d, 'workspace.current_dir')))
    print('SL_SESSION_ID=' + q(g(d, 'session_id')))
    print('SL_FIVE_PCT=' + q(pct('five_hour')))
    print('SL_FIVE_RESET=' + q(epoch(g(rl, 'five_hour.resets_at'))))
    print('SL_SEVEN_PCT=' + q(pct('seven_day')))
    print('SL_SEVEN_RESET=' + q(epoch(g(rl, 'seven_day.resets_at'))))

elif op == 'get':
    try:
        with open(os.path.expanduser(sys.argv[2])) as f:
            d = json.load(f)
        v = g(d, sys.argv[3])
        if v is not None:
            print(v if isinstance(v, str) else json.dumps(v))
    except Exception:
        pass

elif op == 'merge-statusline':
    p = os.path.expanduser(sys.argv[2])
    d = {}
    if os.path.exists(p):
        with open(p) as f:
            d = json.load(f)  # corrupt settings -> non-zero exit, bash aborts
    prev = d.get('statusLine')
    if isinstance(prev, dict) and 'claustrophobic' not in str(prev.get('command', '')):
        with open(os.path.expanduser('~/.claustrophobic/prev-statusline.json'), 'w') as f:
            json.dump(prev, f)
    d['statusLine'] = {'type': 'command', 'command': sys.argv[3]}
    with open(p, 'w') as f:
        json.dump(d, f, indent=2)
        f.write('\n')

elif op == 'unmerge-statusline':
    p = os.path.expanduser(sys.argv[2])
    with open(p) as f:
        d = json.load(f)
    prevp = os.path.expanduser('~/.claustrophobic/prev-statusline.json')
    if os.path.exists(prevp):
        with open(prevp) as f:
            d['statusLine'] = json.load(f)
        os.remove(prevp)
    else:
        d.pop('statusLine', None)
    with open(p, 'w') as f:
        json.dump(d, f, indent=2)
        f.write('\n')

elif op == 'seed':
    seed = {'hasCompletedOnboarding': True}
    try:
        with open(os.path.expanduser(sys.argv[2])) as f:
            s = json.load(f)
        for k in ('theme', 'autoUpdates', 'preferredNotifChannel',
                  'bypassPermissionsModeAccepted'):
            if k in s:
                seed[k] = s[k]
    except Exception:
        pass
    with open(os.path.expanduser(sys.argv[3]), 'w') as f:
        json.dump(seed, f, indent=2)
        f.write('\n')
PYEOF

read -r -d '' JS_HELPER <<'JSEOF' || true
const fs = require('fs');
const os = require('os');
const ex = p => p.replace(/^~(?=$|\/)/, os.homedir());
const g = (d, path) => {
    for (const k of path.split('.')) {
        if (d === null || typeof d !== 'object' || !(k in d)) return null;
        d = d[k];
    }
    return d;
};
const q = v => "'" + String(v === null || v === undefined ? '' : v).replace(/'/g, "'\\''") + "'";
const epoch = v => {
    if (v === null || v === undefined) return null;
    const n = Number(v);
    if (!Number.isNaN(n)) return Math.floor(n);
    const t = Date.parse(String(v));
    return Number.isNaN(t) ? null : Math.floor(t / 1000);
};
const readJson = p => JSON.parse(fs.readFileSync(ex(p), 'utf8'));
const writeJson = (p, d) => fs.writeFileSync(ex(p), JSON.stringify(d, null, 2) + '\n');
const [op, a, b] = process.argv.slice(1);

if (op === 'statusline-kv') {
    let d = {};
    try { d = JSON.parse(fs.readFileSync(0, 'utf8')); } catch (e) {}
    const rl = g(d, 'rate_limits') || {};
    const pct = k => {
        const v = g(rl, k + '.used_percentage');
        return v === null ? null : Math.round(Number(v));
    };
    console.log('SL_MODEL=' + q(g(d, 'model.display_name')));
    console.log('SL_CWD=' + q(g(d, 'workspace.current_dir')));
    console.log('SL_SESSION_ID=' + q(g(d, 'session_id')));
    console.log('SL_FIVE_PCT=' + q(pct('five_hour')));
    console.log('SL_FIVE_RESET=' + q(epoch(g(rl, 'five_hour.resets_at'))));
    console.log('SL_SEVEN_PCT=' + q(pct('seven_day')));
    console.log('SL_SEVEN_RESET=' + q(epoch(g(rl, 'seven_day.resets_at'))));
} else if (op === 'get') {
    try {
        const v = g(readJson(a), b);
        if (v !== null) console.log(typeof v === 'string' ? v : JSON.stringify(v));
    } catch (e) {}
} else if (op === 'merge-statusline') {
    let d = {};
    if (fs.existsSync(ex(a))) d = readJson(a);
    const prev = d.statusLine;
    if (prev && typeof prev === 'object' && String(prev.command || '').indexOf('claustrophobic') < 0) {
        writeJson('~/.claustrophobic/prev-statusline.json', prev);
    }
    d.statusLine = { type: 'command', command: b };
    writeJson(a, d);
} else if (op === 'unmerge-statusline') {
    const d = readJson(a);
    const prevp = ex('~/.claustrophobic/prev-statusline.json');
    if (fs.existsSync(prevp)) {
        d.statusLine = JSON.parse(fs.readFileSync(prevp, 'utf8'));
        fs.unlinkSync(prevp);
    } else {
        delete d.statusLine;
    }
    writeJson(a, d);
} else if (op === 'seed') {
    const seed = { hasCompletedOnboarding: true };
    try {
        const s = readJson(a);
        for (const k of ['theme', 'autoUpdates', 'preferredNotifChannel',
                         'bypassPermissionsModeAccepted']) {
            if (k in s) seed[k] = s[k];
        }
    } catch (e) {}
    writeJson(b, seed);
}
JSEOF

CLAUSTRO_RT=""
json_rt() {
    [ -n "$CLAUSTRO_RT" ] && return 0
    # detection forks a python3 health probe; cache it on disk so the
    # statusline hot path stays fork-free
    if [ -r "$CLAUSTRO_ROOT/.runtime" ]; then
        read -r CLAUSTRO_RT < "$CLAUSTRO_ROOT/.runtime" || CLAUSTRO_RT=''
        case "$CLAUSTRO_RT" in
            py) command -v python3 >/dev/null 2>&1 || CLAUSTRO_RT='' ;;
            js) command -v node >/dev/null 2>&1 || CLAUSTRO_RT='' ;;
            *) CLAUSTRO_RT='' ;;
        esac
        [ -n "$CLAUSTRO_RT" ] && return 0
    fi
    if command -v python3 >/dev/null 2>&1 && python3 -c '' >/dev/null 2>&1; then
        CLAUSTRO_RT=py
    elif command -v node >/dev/null 2>&1; then
        CLAUSTRO_RT=js
    else
        die "needs python3 (xcode-select --install) or node for JSON parsing"
    fi
    mkdir -p "$CLAUSTRO_ROOT"
    printf '%s\n' "$CLAUSTRO_RT" > "$CLAUSTRO_ROOT/.runtime"
}

claustro_json() {
    json_rt
    if [ "$CLAUSTRO_RT" = "py" ]; then
        python3 -c "$PY_HELPER" "$@"
    else
        node -e "$JS_HELPER" "$@"
    fi
}

# ---------------------------------------------------------------- helpers ----

fmt_clock() { # epoch -> HH:MM (BSD date first, GNU fallback)
    date -r "$1" +%H:%M 2>/dev/null || date -d "@$1" +%H:%M 2>/dev/null || printf '?'
}

expired() { # reset-epoch -> true if that usage window has passed
    [ -n "$1" ] && [ "$NOW" -ge "$1" ]
}

sq() { # value -> value safe to embed in single quotes in a sourced file
    # replacement via variable: bash 3.2 keeps quote chars in ${//} literal
    local q="'\\''"
    printf '%s' "${1//\'/$q}"
}

rel_time() {
    local d=$(( NOW - $1 ))
    if   [ "$d" -lt 60 ];     then printf 'now'
    elif [ "$d" -lt 3600 ];   then printf '%dm ago' $(( d / 60 ))
    elif [ "$d" -lt 86400 ];  then printf '%dh ago' $(( d / 3600 ))
    else                           printf '%dd ago' $(( d / 86400 ))
    fi
}

# the three below set globals instead of printing: the statusline hot path
# calls them per render, and $(...) would fork a subshell each time
usage_bar() { # pct -> BAR
    local pct=$1 filled i
    BAR=''
    filled=$(( (pct + 10) / 20 ))
    [ "$filled" -gt 5 ] && filled=5
    [ "$filled" -lt 0 ] && filled=0
    i=0
    while [ "$i" -lt 5 ]; do
        # ${BAR} braced: bash 3.2 corrupts a multibyte char right after $var
        if [ "$i" -lt "$filled" ]; then BAR="${BAR}✦"; else BAR="${BAR}✧"; fi
        i=$(( i + 1 ))
    done
}

heat_color() { # pct -> HC
    if   [ "$1" -ge 85 ]; then HC=$C_RED
    elif [ "$1" -ge 60 ]; then HC=$C_YEL
    else                       HC=$C_GRN
    fi
}

vibe() { # pct -> VIBE
    if   [ "$1" -lt 0 ];  then VIBE='fresh air'
    elif [ "$1" -lt 40 ]; then VIBE='plenty of air'
    elif [ "$1" -lt 70 ]; then VIBE='getting stuffy'
    elif [ "$1" -lt 90 ]; then VIBE='thin air'
    else                       VIBE='barely breathing'
    fi
}

load_config() {
    CLAUDE_BASE_ARGS='--dangerously-skip-permissions'
    CLAUSTRO_SWAP_HINT_PCT=90
    [ -f "$CONFIG_FILE" ] && . "$CONFIG_FILE"
}

# --------------------------------------------------------------- accounts ----

list_ids() {
    ls "$ACCOUNTS_DIR" 2>/dev/null | sed -n 's/^\([0-9][0-9]*\)\.account$/\1/p' | sort -n
}

read_account() { # id -> A_EMAIL A_CONFIG_DIR A_ADDED ; 1 if missing
    local f="$ACCOUNTS_DIR/$1.account"
    [ -f "$f" ] || return 1
    EMAIL='' LABEL='' CONFIG_DIR='' ADDED=''
    . "$f"
    A_EMAIL=$EMAIL A_CONFIG_DIR=$CONFIG_DIR A_ADDED=$ADDED
}

write_account() { # id email config_dir
    mkdir -p "$ACCOUNTS_DIR"
    {
        printf "EMAIL='%s'\n" "$(sq "$2")"
        printf "CONFIG_DIR='%s'\n" "$(sq "$3")"
        printf "ADDED='%s'\n" "$(date +%Y-%m-%d)"
    } > "$ACCOUNTS_DIR/$1.account"
}

plan_name() { # config_dir ('' = the ~/.claude login) -> PLAN
    local cj="${1:-$HOME}/.claude.json" tier
    PLAN='-'
    [ -f "$cj" ] || return 0
    tier=$(claustro_json get "$cj" oauthAccount.organizationRateLimitTier 2>/dev/null)
    [ -n "$tier" ] || tier=$(claustro_json get "$cj" oauthAccount.organizationType 2>/dev/null)
    [ -n "$tier" ] || return 0
    tier=${tier#default_}
    tier=${tier#claude_}
    case "$tier" in
        max_20x)     PLAN='Max 20x' ;;
        max_5x)      PLAN='Max 5x' ;;
        max)         PLAN='Max' ;;
        pro)         PLAN='Pro' ;;
        free)        PLAN='Free' ;;
        team*)       PLAN='Team' ;;
        enterprise*) PLAN='Enterprise' ;;
        *)           PLAN=${tier//_/ } ;;
    esac
}

next_id() {
    local max=0 id
    for id in $(list_ids); do
        [ "$id" -gt "$max" ] && max=$id
    done
    printf '%d' $(( max + 1 ))
}

read_snapshot() { # id -> S_FIVE_PCT S_FIVE_RESET S_SEVEN_PCT S_SEVEN_RESET S_TS ; 1 if none
    local f="$USAGE_DIR/$1.snapshot"
    S_FIVE_PCT='' S_FIVE_RESET='' S_SEVEN_PCT='' S_SEVEN_RESET='' S_TS=''
    [ -f "$f" ] || return 1
    FIVE_PCT='' FIVE_RESET='' SEVEN_PCT='' SEVEN_RESET='' TS=''
    . "$f" >/dev/null 2>&1 || true
    # everything here feeds integer comparisons; drop non-numeric values
    case "$FIVE_PCT" in *[!0-9]*) FIVE_PCT='' ;; esac
    case "$FIVE_RESET" in *[!0-9]*) FIVE_RESET='' ;; esac
    case "$SEVEN_PCT" in *[!0-9]*) SEVEN_PCT='' ;; esac
    case "$SEVEN_RESET" in *[!0-9]*) SEVEN_RESET='' ;; esac
    case "$TS" in *[!0-9]*) TS='' ;; esac
    S_FIVE_PCT=$FIVE_PCT S_FIVE_RESET=$FIVE_RESET
    S_SEVEN_PCT=$SEVEN_PCT S_SEVEN_RESET=$SEVEN_RESET S_TS=$TS
}

snapshot_is_fresher() { # S_* vs payload SL_*: newer reset wins, then higher pct in the same window
    [ -n "$S_FIVE_PCT" ] || return 1
    [ -n "$SL_FIVE_PCT" ] || return 0
    if [ -z "$S_FIVE_RESET" ] || [ -z "$SL_FIVE_RESET" ]; then
        return 1
    fi
    [ "$S_FIVE_RESET" -gt "$SL_FIVE_RESET" ] && return 0
    [ "$S_FIVE_RESET" -eq "$SL_FIVE_RESET" ] && [ "$S_FIVE_PCT" -gt "$SL_FIVE_PCT" ] && return 0
    return 1
}

compute_eff() { # id -> sets EFF / EFF_WK (effective 5h/weekly pct, -1 = never seen) + S_* vars
    EFF=-1 EFF_WK=-1
    if ! read_snapshot "$1" || [ -z "$S_FIVE_PCT" ]; then
        return
    fi
    if expired "$S_FIVE_RESET"; then
        EFF=0
    else
        EFF=$S_FIVE_PCT
    fi
    if [ -z "$S_SEVEN_PCT" ] || expired "$S_SEVEN_RESET"; then
        EFF_WK=0
    else
        EFF_WK=$S_SEVEN_PCT
    fi
}

autopick() { # -> best account id on stdout; 1 if no accounts
    # a room is as cramped as its fullest meter: the 5h window resets within
    # hours, but a burnt weekly meter locks the room for days — so a 100%-wk
    # room must not win just because its 5h count rolled back to 0. Score by
    # the hotter of the two, break ties with the combined load.
    local best='' best_score=999 best_total=999 id score total
    for id in $(list_ids); do
        compute_eff "$id"
        if [ "$EFF" -lt 0 ]; then
            # never-used beats everything, including 0%-after-reset
            score=-1 total=-1
        else
            score=$EFF
            [ "$EFF_WK" -gt "$score" ] && score=$EFF_WK
            total=$(( EFF + EFF_WK ))
        fi
        if [ "$score" -lt "$best_score" ] || { [ "$score" -eq "$best_score" ] && [ "$total" -lt "$best_total" ]; }; then
            best=$id best_score=$score best_total=$total
        fi
    done
    [ -n "$best" ] || return 1
    printf '%s' "$best"
}

default_account_id() { # account whose CONFIG_DIR is empty (the ~/.claude login)
    local id
    for id in $(list_ids); do
        read_account "$id"
        if [ -z "$A_CONFIG_DIR" ]; then printf '%s' "$id"; return 0; fi
    done
    return 1
}

resolve_account_from_env() {
    if [ -n "${CLAUSTROPHOBIC_ACCOUNT:-}" ]; then
        printf '%s' "$CLAUSTROPHOBIC_ACCOUNT"
        return 0
    fi
    if [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
        local base
        base=$(basename "$CLAUDE_CONFIG_DIR")
        case "$CLAUDE_CONFIG_DIR" in
            "$PROFILES_DIR"/*) printf '%s' "$base"; return 0 ;;
        esac
    fi
    default_account_id
}

bootstrap_default_account() {
    [ -n "$(list_ids)" ] && return 0
    local email
    email=$(claustro_json get "$HOME/.claude.json" oauthAccount.emailAddress 2>/dev/null)
    [ -z "$email" ] && return 1
    write_account 1 "$email" ''
    note "registered your current login as room 1 ${C_DIM}($email)${C_RST}"
}

# ----------------------------------------------------------------- launch ----

wear_room() { # id -> export the env that makes claude run as that room
    read_account "$1" || return 1
    export CLAUSTROPHOBIC_ACCOUNT=$1
    if [ -n "$A_CONFIG_DIR" ]; then
        export CLAUDE_CONFIG_DIR=$A_CONFIG_DIR
    else
        unset CLAUDE_CONFIG_DIR
    fi
}

prune_swap_files() { # drop swap state left behind by wrappers that are gone
    local f pid
    for f in "$SWAP_DIR"/*.launch; do
        [ -e "$f" ] || continue
        WRAPPER_PID=''
        . "$f" 2>/dev/null || WRAPPER_PID=''
        pid=$WRAPPER_PID
        case "$pid" in '' | *[!0-9]*) pid='' ;; esac
        if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
            rm -f "${f%.launch}.launch" "${f%.launch}.swap" "${f%.launch}.session"
        fi
    done
    for f in "$SWAP_DIR"/*.session "$SWAP_DIR"/*.swap; do
        [ -e "$f" ] || continue
        [ -f "${f%.*}.launch" ] || rm -f "$f"
    done
}

cmd_launch() {
    local extra_args=() acct='' a
    while [ $# -gt 0 ] && [ "$1" != '--' ]; do
        extra_args[${#extra_args[@]}]=$1
        shift
    done
    [ "${1:-}" = '--' ] && shift

    if [ $# -gt 0 ] && [[ $1 =~ ^[0-9]+$ ]] && [ -f "$ACCOUNTS_DIR/$1.account" ]; then
        acct=$1
        shift
    fi

    if [ -z "$acct" ]; then
        acct=$(autopick) || die "no accounts yet. run: ${C_BOLD}cl setup${C_RST}"
        read_account "$acct"
        local eff
        compute_eff "$acct"
        eff=$EFF
        if [ -t 2 ]; then
            local detail score=$eff
            if [ "$eff" -lt 0 ]; then
                detail='unused'
            else
                detail="5h ${eff}% · wk ${EFF_WK}%"
                [ "$EFF_WK" -gt "$score" ] && score=$EFF_WK
            fi
            vibe "$score"
            printf '%s✻ room %s%s · %s · %s · %s\n' \
                "$C_OR" "$acct" "$C_RST" "$A_EMAIL" "$detail" "$VIBE" >&2
        fi
    else
        read_account "$acct" || die "no such room: $acct"
    fi

    wear_room "$acct" || die "no such room: $acct"

    load_config
    local args=()
    for a in $CLAUDE_BASE_ARGS; do args[${#args[@]}]=$a; done
    for a in "${extra_args[@]}"; do args[${#args[@]}]=$a; done

    if [ -n "${CLAUSTROPHOBIC_DRY_RUN:-}" ]; then
        printf 'CLAUSTROPHOBIC_ACCOUNT=%s\n' "$CLAUSTROPHOBIC_ACCOUNT"
        printf 'CLAUDE_CONFIG_DIR=%s\n' "${CLAUDE_CONFIG_DIR:-}"
        printf 'exec claude'
        for a in "${args[@]}" "$@"; do printf ' %s' "$a"; done
        printf '\n'
        exit 0
    fi

    command -v claude >/dev/null 2>&1 || die "claude CLI not found on PATH"

    # swap loop: claude runs as a child so /swap (claustrophobic swap-request)
    # can end it and have us resume the same session as the airiest room
    mkdir -p "$SWAP_DIR"
    prune_swap_files
    export CLAUSTRO_LAUNCH_ID=$$
    local lbase="$SWAP_DIR/$$" rc=0 sid pick
    while :; do
        rm -f "$lbase.swap"
        printf "WRAPPER_PID='%s'\n" "$$" > "$lbase.launch"
        claude "${args[@]}" "$@"
        rc=$?
        [ -f "$lbase.swap" ] || break
        SESSION_ID=''
        . "$lbase.swap"
        sid=$SESSION_ID
        case "$sid" in *[!0-9a-fA-F-]*) sid='' ;; esac
        stty sane 2>/dev/null # claude may not get to restore the tty
        NOW=$(date +%s)
        pick=$(autopick) || pick=$CLAUSTROPHOBIC_ACCOUNT
        wear_room "$pick" || break
        set --
        args=()
        for a in $CLAUDE_BASE_ARGS; do args[${#args[@]}]=$a; done
        if [ -n "$sid" ]; then
            args[${#args[@]}]='--resume'
            args[${#args[@]}]=$sid
        else
            args[${#args[@]}]='--continue'
        fi
        compute_eff "$pick"
        local score=$EFF
        [ "$EFF_WK" -gt "$score" ] && score=$EFF_WK
        vibe "$score"
        printf '%s✻ swapped to room %s%s · %s · %s\n' \
            "$C_OR" "$pick" "$C_RST" "$A_EMAIL" "$VIBE" >&2
    done
    rm -f "$lbase.launch" "$lbase.swap" "$lbase.session"
    exit "$rc"
}

cmd_swap_request() { # invoked inside claude by /swap; flags the wrapper, ends claude
    local lid=${CLAUSTRO_LAUNCH_ID:-} lbase cpid
    if [ -z "$lid" ]; then
        printf 'Not launched through claustrophobic (c/cr/cw), so there is no wrapper to swap rooms.\n'
        return 0
    fi
    lbase="$SWAP_DIR/$lid"
    if [ ! -f "$lbase.launch" ]; then
        printf 'No launch record for this session; cannot swap.\n'
        return 0
    fi
    WRAPPER_PID=''
    . "$lbase.launch"
    case "$WRAPPER_PID" in '' | *[!0-9]*)
        printf 'Corrupt launch record; cannot swap.\n'
        return 0
        ;;
    esac
    SESSION_ID=''
    [ -f "$lbase.session" ] && . "$lbase.session"
    printf "SESSION_ID='%s'\n" "$(sq "$SESSION_ID")" > "$lbase.swap"
    cpid=$(pgrep -P "$WRAPPER_PID" 2>/dev/null | head -n 1)
    [ -n "$cpid" ] || cpid=$(ps -axo pid=,ppid= 2>/dev/null | awk -v p="$WRAPPER_PID" '$2 == p { print $1; exit }')
    printf 'Swap requested: this session restarts on the airiest room momentarily.\n'
    if [ -n "$cpid" ]; then
        # escalator first: claude's shutdown may reap us right after the TERM
        (
            sleep 2
            kill -0 "$cpid" 2>/dev/null || exit 0
            kill -INT "$cpid" 2>/dev/null
            sleep 0.4
            kill -INT "$cpid" 2>/dev/null
            sleep 2
            kill -0 "$cpid" 2>/dev/null && kill -KILL "$cpid" 2>/dev/null
        ) >/dev/null 2>&1 &
        kill -TERM "$cpid" 2>/dev/null
    fi
}

# ------------------------------------------------------------- statusline ----

cmd_statusline_render() {
    CLAUSTRO_COLOR=1
    set_colors
    local input
    input=$(cat)
    eval "$(printf '%s' "$input" | claustro_json statusline-kv)"

    local id name
    id=$(resolve_account_from_env) || id=''
    if [ -n "$id" ] && read_account "$id"; then
        name=$A_EMAIL
    else
        id='?' name='claude'
    fi

    # The payload only reflects this session's last API call, so it goes
    # stale in idle sessions; the per-room snapshot is shared by all of
    # them. Render and store whichever is fresher; ~ marks adopted data.
    local cached='' tmp=''
    if [[ $id =~ ^[0-9]+$ ]]; then
        read_snapshot "$id" || true
        if [ -n "$SL_FIVE_PCT" ] && ! snapshot_is_fresher; then
            mkdir -p "$USAGE_DIR"
            # write-then-rename: concurrent sessions render simultaneously,
            # and a plain > truncates under a reader's feet
            tmp="$USAGE_DIR/.$id.$$"
            {
                printf "FIVE_PCT='%s'\n" "$SL_FIVE_PCT"
                printf "FIVE_RESET='%s'\n" "$SL_FIVE_RESET"
                printf "SEVEN_PCT='%s'\n" "$SL_SEVEN_PCT"
                printf "SEVEN_RESET='%s'\n" "$SL_SEVEN_RESET"
                printf "TS='%s'\n" "$NOW"
            } > "$tmp" && mv -f "$tmp" "$USAGE_DIR/$id.snapshot"
        elif [ -n "$S_FIVE_PCT" ]; then
            cached='~'
            SL_FIVE_PCT=$S_FIVE_PCT SL_FIVE_RESET=$S_FIVE_RESET
            SL_SEVEN_PCT=$S_SEVEN_PCT SL_SEVEN_RESET=$S_SEVEN_RESET
        fi
    fi
    if [ -n "$SL_FIVE_PCT" ] && expired "$SL_FIVE_RESET"; then
        cached='~' SL_FIVE_PCT=0 SL_FIVE_RESET=''
    fi
    if [ -n "$SL_SEVEN_PCT" ] && expired "$SL_SEVEN_RESET"; then
        cached='~' SL_SEVEN_PCT=0 SL_SEVEN_RESET=''
    fi

    # /swap plumbing: stash the session id where the wrapper can find it,
    # and offer /swap once this room's hotter meter crosses the threshold
    local swap_part=''
    if [[ $id =~ ^[0-9]+$ ]] && [ -n "${CLAUSTRO_LAUNCH_ID:-}" ]; then
        if [ -n "$SL_SESSION_ID" ]; then
            local sfile="$SWAP_DIR/$CLAUSTRO_LAUNCH_ID.session" sline scur=''
            sline="SESSION_ID='$(sq "$SL_SESSION_ID")'"
            [ -r "$sfile" ] && IFS= read -r scur < "$sfile"
            if [ "$scur" != "$sline" ]; then
                mkdir -p "$SWAP_DIR"
                printf '%s\n' "$sline" > "$sfile"
            fi
        fi
        load_config
        local hot=''
        case "$SL_FIVE_PCT" in '' | *[!0-9]*) ;; *) hot=$SL_FIVE_PCT ;; esac
        case "$SL_SEVEN_PCT" in
            '' | *[!0-9]*) ;;
            *) if [ -z "$hot" ] || [ "$SL_SEVEN_PCT" -gt "$hot" ]; then hot=$SL_SEVEN_PCT; fi ;;
        esac
        case "$CLAUSTRO_SWAP_HINT_PCT" in '' | *[!0-9]*) hot='' ;; esac
        if [ -n "$hot" ] && [ "$hot" -ge "$CLAUSTRO_SWAP_HINT_PCT" ]; then
            local pick pscore
            pick=$(autopick 2>/dev/null) || pick=''
            if [ -n "$pick" ] && [ "$pick" != "$id" ]; then
                compute_eff "$pick"
                pscore=$EFF
                [ "$EFF_WK" -gt "$pscore" ] && pscore=$EFF_WK
                if [ "$pscore" -lt "$hot" ]; then
                    swap_part="${C_DIM} │ ${C_RST}${C_OR}/swap${C_RST}${C_DIM} → room ${pick}${C_RST}"
                fi
            fi
        fi
    fi

    local sep="${C_DIM} │ ${C_RST}" five wk
    if [ -n "$SL_FIVE_PCT" ]; then
        local reset_part=''
        heat_color "$SL_FIVE_PCT"
        usage_bar "$SL_FIVE_PCT"
        [ -n "$SL_FIVE_RESET" ] && reset_part="${C_DIM} → $(fmt_clock "$SL_FIVE_RESET")${C_RST}"
        five="${C_DIM}5h${C_RST} ${HC}${BAR} ${cached}${SL_FIVE_PCT}%${C_RST}${reset_part}"
    else
        five="${C_DIM}5h -${C_RST}"
    fi
    if [ -n "$SL_SEVEN_PCT" ]; then
        heat_color "$SL_SEVEN_PCT"
        wk="${C_DIM}wk${C_RST} ${HC}${cached}${SL_SEVEN_PCT}%${C_RST}"
    else
        wk="${C_DIM}wk -${C_RST}"
    fi

    local tail=''
    [ -n "$SL_MODEL" ] && tail="${sep}${C_DIM}${SL_MODEL}${C_RST}"
    [ -n "$SL_CWD" ] && tail="${tail}${sep}${C_DIM}${SL_CWD##*/}${C_RST}"

    printf '%s✻ %s%s %s%s%s%s%s%s%s\n' \
        "$C_OR$C_BOLD" "$id" "$C_RST" "$C_OR" "$name" "$C_RST" \
        "$sep$five" "$sep$wk" "$swap_part" "$tail"
}

cmd_statusline_install() {
    [ -f "$CLAUDE_HOME/settings.json" ] && cp "$CLAUDE_HOME/settings.json" "$CLAUDE_HOME/settings.json.claustrophobic-bak"
    mkdir -p "$CLAUDE_HOME"
    claustro_json merge-statusline "$CLAUDE_HOME/settings.json" '~/.claustrophobic/bin/claustrophobic statusline' \
        || die "could not update $CLAUDE_HOME/settings.json (is it valid JSON?)"
    note "statusline installed ${C_DIM}(backup: settings.json.claustrophobic-bak)${C_RST}"
}

cmd_statusline_uninstall() {
    claustro_json unmerge-statusline "$CLAUDE_HOME/settings.json" \
        || die "could not update $CLAUDE_HOME/settings.json"
    note "statusline removed"
}

# ------------------------------------------------------------------ status ----

cmd_status() {
    NOW=$(date +%s)
    local ids
    ids=$(list_ids)
    if [ -z "$ids" ]; then
        note "no rooms yet. run ${C_BOLD}cl setup${C_RST} to get started"
        exit 0
    fi

    # ACCOUNT column hugs the longest email
    local namew=7 id
    for id in $ids; do
        read_account "$id"
        [ ${#A_EMAIL} -gt "$namew" ] && namew=${#A_EMAIL}
    done
    [ "$namew" -gt 28 ] && namew=28

    printf '\n  %s✻ Claustrophobic%s %s· when the walls close in, switch rooms%s\n\n' \
        "$C_OR$C_BOLD" "$C_RST" "$C_DIM" "$C_RST"
    printf '    %s%-3s %-*s %-10s %-12s %-5s %-7s %s%s\n' \
        "$C_DIM" 'ID' "$namew" 'ACCOUNT' 'PLAN' '5H' 'WK' 'RESETS' 'SEEN' "$C_RST"

    local pick eff mark name plan fivec wkc resets seen
    pick=$(autopick 2>/dev/null) || pick=''

    for id in $ids; do
        read_account "$id"
        compute_eff "$id"
        eff=$EFF

        mark=' '
        [ "$id" = "$pick" ] && mark="${C_OR}→${C_RST}"
        [ "$id" = "${CLAUSTROPHOBIC_ACCOUNT:-}" ] && mark="${C_OR}✻${C_RST}"

        name=$A_EMAIL
        [ ${#name} -gt "$namew" ] && name="${name:0:namew-1}…"
        plan_name "$A_CONFIG_DIR"
        plan=$PLAN
        [ ${#plan} -gt 10 ] && plan="${plan:0:9}…"

        # bar glyphs are mixed-byte-width; pad fields by hand, not printf %-N
        if [ "$eff" -lt 0 ]; then
            usage_bar 0
            fivec="${C_DIM}${BAR} fresh ${C_RST}"
            wkc="${C_DIM}-    ${C_RST}"
            resets='-      '
            seen="${C_DIM}never${C_RST}"
        else
            heat_color "$eff"
            usage_bar "$eff"
            fivec="${HC}${BAR} $(printf '%-6s' "${eff}%")${C_RST}"
            if [ -n "$S_SEVEN_PCT" ] && ! expired "$S_SEVEN_RESET"; then
                heat_color "$S_SEVEN_PCT"
                wkc="${HC}$(printf '%-5s' "${S_SEVEN_PCT}%")${C_RST}"
            else
                wkc="${C_DIM}-    ${C_RST}"
            fi
            if [ -n "$S_FIVE_RESET" ] && ! expired "$S_FIVE_RESET"; then
                resets=$(printf '%-7s' "$(fmt_clock "$S_FIVE_RESET")")
            else
                resets='-      '
            fi
            seen="${C_DIM}$(rel_time "${S_TS:-$NOW}")${C_RST}"
        fi

        printf '  %s %s%-3s%s %-*s %s%-10s%s %s %s %s %s\n' \
            "$mark" "$C_BOLD" "$id" "$C_RST" "$namew" "$name" "$C_DIM" "$plan" "$C_RST" \
            "$fivec" "$wkc" "$resets" "$seen"
    done

    printf '\n  %sc [id]%s launch %s· omit the id for the airiest room%s' "$C_BOLD" "$C_RST" "$C_DIM" "$C_RST"
    printf '   %scl add%s new room   %scl help%s more\n\n' "$C_BOLD" "$C_RST" "$C_BOLD" "$C_RST"
}

# -------------------------------------------------------------------- add ----

cmd_add() {
    bootstrap_default_account >/dev/null 2>&1 || true
    local id dir d f email dup
    id=$(next_id)
    dir="$PROFILES_DIR/$id"
    mkdir -p "$dir" "$CLAUDE_HOME/projects" "$CLAUDE_HOME/todos"

    # share sessions, settings, skills & friends with the main ~/.claude
    for d in projects todos shell-snapshots agents commands skills hooks plugins; do
        [ -e "$CLAUDE_HOME/$d" ] && ln -sfn "$CLAUDE_HOME/$d" "$dir/$d"
    done
    for f in settings.json settings.local.json keybindings.json CLAUDE.md history.jsonl; do
        [ -e "$CLAUDE_HOME/$f" ] && ln -sf "$CLAUDE_HOME/$f" "$dir/$f"
    done
    claustro_json seed "$HOME/.claude.json" "$dir/.claude.json"

    printf '\n%s✻ opening a fresh room (%s)%s\n' "$C_OR$C_BOLD" "$id" "$C_RST"
    printf '  Claude will launch with no login. Sign in with the account you want\n'
    printf '  to add %s(type /login if you are not prompted)%s, then exit Claude\n' "$C_DIM" "$C_RST"
    printf '  %s(/exit or Ctrl+C twice)%s to come back here.\n\n' "$C_DIM" "$C_RST"
    printf '  press enter to launch... '
    read -r _ </dev/tty

    ( cd "$HOME" && CLAUDE_CONFIG_DIR="$dir" claude ) || true

    email=$(claustro_json get "$dir/.claude.json" oauthAccount.emailAddress 2>/dev/null)
    if [ -z "$email" ]; then
        write_account "$id" '(not logged in)' "$dir"
        err "no login detected. room $id is reserved; finish with: ${C_BOLD}cl relogin $id${C_RST}"
        return 1
    fi

    for dup in $(list_ids); do
        read_account "$dup"
        if [ "$A_EMAIL" = "$email" ]; then
            err "heads-up: $email is already room $dup; two rooms on one account share limits and fight over the Keychain"
        fi
    done

    write_account "$id" "$email" "$dir"
    plan_name "$dir"
    printf '\n'
    note "room $id ready: ${C_BOLD}$email${C_RST} ${C_DIM}($PLAN)${C_RST}  ·  launch with: ${C_BOLD}c $id${C_RST}"
}

cmd_relogin() {
    local id=${1:-}
    [ -n "$id" ] || die "usage: cl relogin <id>"
    read_account "$id" || die "no such room: $id"
    if [ -z "$A_CONFIG_DIR" ]; then
        note "room $id is your default ~/.claude login. just run ${C_BOLD}claude${C_RST} and ${C_BOLD}/login${C_RST}"
        return 0
    fi
    note "log in, then exit Claude to come back"
    ( cd "$HOME" && CLAUDE_CONFIG_DIR="$A_CONFIG_DIR" claude ) || true
    local email
    email=$(claustro_json get "$A_CONFIG_DIR/.claude.json" oauthAccount.emailAddress 2>/dev/null)
    if [ -n "$email" ]; then
        write_account "$id" "$email" "$A_CONFIG_DIR"
        note "room $id → ${C_BOLD}$email${C_RST}"
    fi
}

cmd_rm() {
    local id=${1:-}
    [ -n "$id" ] || die "usage: cl rm <id>"
    read_account "$id" || die "no such room: $id"
    [ -z "$A_CONFIG_DIR" ] && die "room $id is your default ~/.claude login. claustrophobic won't demolish it"
    printf 'demolish room %s (%s)? [y/N] ' "$id" "$A_EMAIL"
    read -r ans </dev/tty
    case "$ans" in
        y|Y|yes) ;;
        *) note "kept"; return 0 ;;
    esac
    rm -rf "$A_CONFIG_DIR"
    rm -f "$ACCOUNTS_DIR/$id.account" "$USAGE_DIR/$id.snapshot"
    note "room $id demolished ${C_DIM}(the login itself may live on in your Keychain)${C_RST}"
}

# ---------------------------------------------------------------- install ----

cmd_shell_init() {
    cat <<EOF
case ":\$PATH:" in *":\$HOME/.claustrophobic/bin:"*) ;; *) export PATH="\$HOME/.claustrophobic/bin:\$PATH" ;; esac
unalias c cr cw cl 2>/dev/null
# 'function' keyword form: zsh would reject 'c() {...}' while an alias c exists
function c  { "\$HOME/.claustrophobic/bin/claustrophobic" launch -- "\$@"; }
function cr { "\$HOME/.claustrophobic/bin/claustrophobic" launch --resume -- "\$@"; }
function cw { "\$HOME/.claustrophobic/bin/claustrophobic" launch --worktree -- "\$@"; }
function cl { "\$HOME/.claustrophobic/bin/claustrophobic" "\$@"; }
EOF
}

self_path() {
    local src=$0 dir
    dir=$(cd "$(dirname "$src")" >/dev/null 2>&1 && pwd) || return 1
    printf '%s/%s' "$dir" "$(basename "$src")"
}

install_rc_block() { # rcfile
    local rc=$1
    if grep -E -q '^alias (c|cr|cw|cl)=' "$rc" 2>/dev/null; then
        # write through a temp file + cat so rc files that are symlinks
        # (dotfile repos) keep pointing at their target
        local tmp
        tmp=$(mktemp) || die "mktemp failed"
        cp "$rc" "$rc.claustrophobic-bak" \
            && sed -E 's/^alias (c|cr|cw|cl)=/# claustrophobic took over this binding: alias \1=/' "$rc" > "$tmp" \
            && cat "$tmp" > "$rc" \
            && note "commented out old c/cr/cw aliases in $rc ${C_DIM}(backup: $rc.claustrophobic-bak)${C_RST}"
        rm -f "$tmp"
    fi
    grep -q 'claustrophobic.*shell-init' "$rc" 2>/dev/null && return 0
    {
        printf '\n# >>> claustrophobic: when the walls close in, switch rooms >>>\n'
        printf 'eval "$("$HOME/.claustrophobic/bin/claustrophobic" shell-init)"\n'
        printf '# <<< claustrophobic <<<\n'
    } >> "$rc"
    note "shell bindings installed in $rc"
}

install_swap_command() { # /swap slash command; rooms see it via the commands symlink
    local cmdfile="$CLAUDE_HOME/commands/swap.md"
    if [ -f "$cmdfile" ] && ! grep -q claustrophobic "$cmdfile" 2>/dev/null; then
        err "skipped $cmdfile: it exists and isn't ours"
        return 0
    fi
    mkdir -p "$CLAUDE_HOME/commands"
    cat > "$cmdfile" <<'EOF'
---
description: Hop this session to the airiest claustrophobic room
allowed-tools: Bash(~/.claustrophobic/bin/claustrophobic swap-request)
disable-model-invocation: true
---

!`~/.claustrophobic/bin/claustrophobic swap-request`

claustrophobic is restarting this session under another account. Acknowledge in one short line.
EOF
}

cmd_install() {
    # check prerequisites before touching anything, especially rc files
    command -v claude >/dev/null 2>&1 || die "install the claude CLI first: https://code.claude.com"
    json_rt

    mkdir -p "$CLAUSTRO_ROOT/bin" "$ACCOUNTS_DIR" "$USAGE_DIR" "$PROFILES_DIR"

    local me
    me=$(self_path)
    if [ -n "$me" ] && [ "$me" != "$CLAUSTRO_BIN" ] && ! cmp -s "$me" "$CLAUSTRO_BIN" 2>/dev/null; then
        cp "$me" "$CLAUSTRO_BIN"
    fi
    chmod +x "$CLAUSTRO_BIN" 2>/dev/null

    if [ ! -f "$CONFIG_FILE" ]; then
        {
            printf '# claustrophobic config\n'
            printf '# flags prepended to every claude launch (word-split)\n'
            printf "CLAUDE_BASE_ARGS='--dangerously-skip-permissions'\n"
            printf '# statusline offers /swap when the hotter meter hits this %% (empty = never)\n'
            printf "CLAUSTRO_SWAP_HINT_PCT='90'\n"
        } > "$CONFIG_FILE"
    fi

    install_swap_command

    [ -f "$HOME/.zshrc" ] || touch "$HOME/.zshrc"
    install_rc_block "$HOME/.zshrc"
    [ -f "$HOME/.bashrc" ] && install_rc_block "$HOME/.bashrc"

    bootstrap_default_account || true
}

cmd_setup() {
    printf '\n  %s✻ Claustrophobic %s%s\n' "$C_OR$C_BOLD" "$CLAUSTRO_VERSION" "$C_RST"
    printf '  %sfeeling the walls close in? let'\''s get you some extra rooms.%s\n\n' "$C_DIM" "$C_RST"

    cmd_install

    local ans
    printf '\n  run claude with --dangerously-skip-permissions by default? [Y/n] '
    read -r ans </dev/tty
    case "$ans" in
        n|N|no)
            sed -i.bak "s/^CLAUDE_BASE_ARGS=.*/CLAUDE_BASE_ARGS=''/" "$CONFIG_FILE" && rm -f "$CONFIG_FILE.bak"
            ;;
    esac

    printf '  install the statusline (shows room + air supply inside Claude)? [Y/n] '
    read -r ans </dev/tty
    case "$ans" in
        n|N|no) ;;
        *) cmd_statusline_install ;;
    esac

    while :; do
        printf '\n  add another account now? [y/N] '
        read -r ans </dev/tty
        case "$ans" in
            y|Y|yes) cmd_add || true ;;
            *) break ;;
        esac
    done

    cmd_status
    note "restart your shell (or: source ~/.zshrc) and you're in."
}

# -------------------------------------------------------------------- help ----

cmd_help() {
    cat <<EOF

  ${C_OR}${C_BOLD}✻ Claustrophobic${C_RST} ${CLAUSTRO_VERSION} ${C_DIM}· multi-account harness for Claude Code${C_RST}
  ${C_DIM}because the context window isn't the only thing closing in${C_RST}

  ${C_BOLD}launching${C_RST}
    c  [id] [args...]     claude  ${C_DIM}(omit the id to auto-pick the airiest room)${C_RST}
    cr [id] [args...]     claude --resume
    cw [id] [args...]     claude --worktree
    cl <id>               same as c <id>

  ${C_BOLD}rooms${C_RST}
    cl                    show all rooms, plans + usage
    cl add                add an account ${C_DIM}(official claude /login flow)${C_RST}
    cl rm <id>            remove a room
    cl relogin <id>       re-authenticate a room

  ${C_BOLD}swapping${C_RST} ${C_DIM}(inside a c/cr/cw session)${C_RST}
    /swap                 exit + resume this session on the airiest room
    ! cl swap-request     the same, without the /swap command file

  ${C_BOLD}plumbing${C_RST}
    cl setup              interactive first-time setup ${C_DIM}(safe to re-run)${C_RST}
    cl install            non-interactive: bindings + dirs + register login
    cl statusline install|uninstall
    cl help · cl version

  ${C_DIM}sessions are shared across rooms, so any account can resume any session
  (/swap automates exit + cr). the statusline offers /swap once usage hits
  CLAUSTRO_SWAP_HINT_PCT (default 90, set in ~/.claustrophobic/config).
  usage data refreshes whenever a room's statusline is on screen.${C_RST}

EOF
}

# ---------------------------------------------------------------- dispatch ----

case "${1:-status}" in
    status|ls|list)        cmd_status ;;
    launch)                shift; cmd_launch "$@" ;;
    add)                   cmd_add ;;
    rm|remove)             shift; cmd_rm "$@" ;;
    relogin)               shift; cmd_relogin "$@" ;;
    swap-request)          cmd_swap_request ;;
    statusline)
        case "${2:-}" in
            install)       cmd_statusline_install ;;
            uninstall)     cmd_statusline_uninstall ;;
            *)             cmd_statusline_render ;;
        esac ;;
    shell-init)            cmd_shell_init ;;
    install)               cmd_install; note "installed. run ${C_BOLD}cl setup${C_RST} to add accounts" ;;
    setup)                 cmd_setup ;;
    help|-h|--help)        cmd_help ;;
    version|--version)     printf 'claustrophobic %s\n' "$CLAUSTRO_VERSION" ;;
    *)
        if [[ $1 =~ ^[0-9]+$ ]]; then
            shift_id=$1; shift
            cmd_launch -- "$shift_id" "$@"
        else
            err "unknown command: $1"
            cmd_help
            exit 1
        fi ;;
esac
