#!/bin/sh
# rclaude — durable Claude Code sessions, local or remote.
#
# Two layers of resilience:
#   1. tmux on <host> survives terminal/transport drops.
#   2. `claude --continue` resumes the per-directory session from disk after
#      the host itself dies (reboot, crash, OOM).
#
# Each invocation starts a fresh Claude session in a new named tmux window.
# To reattach an existing session: `rclaude resume [pattern]`
# To resume a Claude conversation from disk after host loss: `rclaude resume` picks
# up the on-disk session via `claude --continue`.
#
# Permission mode: --dangerously-skip-permissions is on by default. Override
# with RCLAUDE_PERMS=default (or any --permission-mode value).
#
# Hosts scanned by `list`/`resume` default to: local + apricot. Override with
# RCLAUDE_HOSTS="apricot black quinn-vps".
#
# Usage:
#   rclaude                                  # local, $PWD
#   rclaude .                                # local, $PWD
#   rclaude <host>                           # remote $HOME on <host>
#   rclaude <host> <dir>                     # remote (or local) at <dir>
#   rclaude list                             # show active sessions across hosts
#   rclaude resume [pattern]                 # reattach (interactive if pattern matches >1)

set -eu

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

# Resolve $0 to its real path, correctly handling relative symlinks at each hop.
resolve_self() {
    _rs=$0
    while [ -L "$_rs" ]; do
        _link=$(readlink "$_rs")
        case "$_link" in
            /*) _rs="$_link" ;;
            *)  _rs="$(dirname "$_rs")/$_link" ;;
        esac
    done
    printf '%s' "$_rs"
}

is_local() {
    case $1 in
        local|localhost|127.0.0.1|::1) return 0 ;;
    esac
    [ "$1" = "$(hostname)" ] && return 0
    [ "$1" = "$(hostname -s 2>/dev/null)" ] && return 0
    return 1
}

# List claude-* tmux sessions on a host. Output one row per session:
#   <host>\ttmux\t<session_name>\t<detail_from_tmux_ls>
list_tmux_on() {
    _host=$1
    if is_local "$_host"; then
        command -v tmux >/dev/null 2>&1 || return 0
        _raw=$(tmux ls 2>/dev/null || true)
    else
        _raw=$(ssh -o BatchMode=yes -o ConnectTimeout=3 "$_host" 'tmux ls 2>/dev/null' || true)
    fi
    # tmux ls lines look like:  claude-foo: 1 windows (created ...) [80x24]
    printf %s "$_raw" | awk -v host="$_host" '
        /^claude-/ {
            name=$1; sub(/:$/, "", name);
            $1="";
            sub(/^[[:space:]]+/, "");
            printf "%s\ttmux\t%s\t%s\n", host, name, $0
        }
    '
}

# List on-disk Claude project sessions on a host (via _claude-projects helper).
# Output one row per project:
#   <host>\tdisk\t<cwd>\t<sessions=N, last used <relative-time>>
list_disk_on() {
    _host=$1
    _helper_dir=$(dirname "$(resolve_self)")
    if is_local "$_host"; then
        _raw=$("$_helper_dir/_claude-projects" 2>/dev/null || true)
    else
        # Send the helper over stdin so we don't depend on a pre-installed copy
        # on the remote (and to dodge quoting issues).
        _raw=$(ssh -o BatchMode=yes -o ConnectTimeout=3 "$_host" 'python3 -' < "$_helper_dir/_claude-projects" 2>/dev/null || true)
    fi
    _now=$(date +%s)
    printf %s "$_raw" | awk -F'\t' -v host="$_host" -v now="$_now" '
        function rel(secs,   abs, s) {
            abs = (secs < 0) ? -secs : secs
            if (abs < 60)         s = abs " seconds"
            else if (abs < 3600)  s = int(abs/60) " min"
            else if (abs < 86400) s = int(abs/3600) " hours"
            else                  s = int(abs/86400) " days"
            return s " ago"
        }
        NF >= 3 {
            printf "%s\tdisk\t%s\tsessions=%s, last used %s\n", host, $2, $3, rel(now - $1)
        }
    '
}

# Combined enumeration: tmux first (live), then on-disk.
list_all_on() {
    list_tmux_on "$1"
    list_disk_on "$1"
}

# All hosts to scan for list/resume.
scan_hosts() {
    printf "local\n"
    for h in ${RCLAUDE_HOSTS:-apricot}; do
        printf "%s\n" "$h"
    done
}

# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------

cmd_list() {
    _mode=${1:-all}   # all | tmux | disk
    printf "%-10s  %-6s  %-60s  %s\n" "HOST" "KIND" "SESSION/CWD" "DETAIL"
    scan_hosts | while IFS= read -r h; do
        case $_mode in
            tmux) list_tmux_on "$h" ;;
            disk) list_disk_on "$h" ;;
            *)    list_all_on  "$h" ;;
        esac | awk -F'\t' '{printf "%-10s  %-6s  %-60s  %s\n", $1, $2, $3, $4}'
    done
}

# Resume strategy:
#   - matches a tmux row → ssh+tmux attach (preserves the live conversation)
#   - matches a disk row → re-exec self with `<host> <cwd>` so the normal
#     launch path spins up a fresh tmux + claude --continue in that dir
cmd_resume() {
    _pattern=${1:-}
    _matches=$(scan_hosts | while IFS= read -r h; do list_all_on "$h"; done)
    if [ -n "$_pattern" ]; then
        _matches=$(printf '%s\n' "$_matches" | grep -F -- "$_pattern" || true)
    fi
    _count=0
    [ -n "$_matches" ] && _count=$(printf '%s\n' "$_matches" | wc -l | tr -d ' ')
    if [ "$_count" -eq 0 ]; then
        echo "no matching sessions${_pattern:+ for pattern '$_pattern'}" >&2
        exit 1
    fi
    if [ "$_count" -gt 1 ]; then
        echo "multiple matches; refine pattern:" >&2
        printf '%s\n' "$_matches" | awk -F'\t' '{printf "  %-10s  %-6s  %s\n", $1, $2, $3}' >&2
        exit 1
    fi
    _host=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $1}')
    _kind=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $2}')
    _target=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $3}')
    case $_kind in
        tmux)
            if is_local "$_host"; then
                exec tmux attach -t "$_target"
            else
                exec ssh -t "$_host" tmux attach -t "$_target"
            fi
            ;;
        disk)
            # Spawn tmux + claude --continue at the recorded cwd. The env var
            # tells the launch path below to add --continue (it doesn't by
            # default, since --continue is the explicit-resume behavior).
            RCLAUDE_RESUME=1 exec "$0" "$_host" "$_target"
            ;;
    esac
}

# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------

cmd_version() {
    _self=$(resolve_self)
    _repo=$(cd "$(dirname "$_self")/.." 2>/dev/null && pwd)
    if [ -d "$_repo/.git" ] && command -v git >/dev/null 2>&1; then
        _sha=$(git -C "$_repo" rev-parse --short HEAD 2>/dev/null)
        _dirty=""
        [ -n "$(git -C "$_repo" status --porcelain 2>/dev/null)" ] && _dirty="-dirty"
        _date=$(git -C "$_repo" log -1 --format=%cd --date=short HEAD 2>/dev/null)
        printf 'rclaude (session-tools) %s%s  %s  %s\n' "$_sha" "$_dirty" "$_date" "$_repo"
    else
        printf 'rclaude (session-tools)  %s\n' "$_repo"
    fi
}

case ${1:-} in
    list)            shift; cmd_list    "$@"; exit ;;
    resume)          shift; cmd_resume  "$@"; exit ;;
    -v|--version)    cmd_version; exit ;;
esac

# ---------------------------------------------------------------------------
# Default behavior: launch (or reattach to) a session.
# ---------------------------------------------------------------------------

# Argument resolution:
#   `rclaude`        → local, $PWD
#   `rclaude .`      → local, $PWD
#   `rclaude <host>` → host, default dir (~ remote, $PWD local)
#   `rclaude <host> <dir>` → host, dir (with `.` resolving to $PWD)
if [ $# -eq 0 ] || [ "${1:-}" = "." ]; then
    host=local
    dir=$PWD
else
    host=$1
    dir=${2:-}
fi

# Defaults + `.` expansion now that we know whether we're local or remote.
if is_local "$host"; then
    case ${dir:-.} in
        .|"") dir=$PWD ;;
    esac
else
    if [ "$dir" = "." ]; then
        echo "error: '.' as dir requires a local target; pass an explicit remote path" >&2
        exit 2
    fi
    [ -z "$dir" ] && dir=\~
fi

slug=$(printf %s "$dir" | sed -e 's|^[~/]*||' -e 's|[^A-Za-z0-9]|-|g')
[ -z "$slug" ] && slug=home
session="claude-$(whoami)-${slug}-$(date +%s)"

perms=${RCLAUDE_PERMS:-bypass}
case $perms in
    bypass) flag="--dangerously-skip-permissions" ;;
    *)      flag="--permission-mode $perms" ;;
esac

# Inner command for the tmux pane. If claude exits nonzero OR ends in under
# 2 seconds (usually a misconfig: missing dir, locked session, crashed
# claude), the pane stays open with the exit code visible instead of
# silently dying and dragging the whole tmux session + ssh transport down
# with it. A real interactive session lasts much longer than 2s, so a clean
# /exit closes the pane normally.
build_inner() {
    # Single-line, single-quote-safe. Variables prefixed with rc_ to avoid
    # collision with anything in the user's shell.
    #
    # Note: launch path uses plain `claude` (fresh session). Each invocation
    # creates a new uniquely-named tmux session. Reattach to a live session
    # via `rclaude resume <pattern>`; disk-resume after host death likewise.
    _resume_flag=""
    [ "${RCLAUDE_RESUME:-0}" = "1" ] && _resume_flag="--continue"
    printf '%s' \
        "cd ${1} && rc_t=\$(date +%s); claude ${_resume_flag} ${flag}; rc_ec=\$?; " \
        "rc_e=\$(date +%s); rc_d=\$((rc_e - rc_t)); " \
        "if [ \$rc_ec -ne 0 ] || [ \$rc_d -lt 2 ]; then " \
        "printf '\\n[rclaude] claude exited in %ds with code %d\\n' \$rc_d \$rc_ec; " \
        "printf '[rclaude] press enter to close pane (or Ctrl-b d to detach)... '; " \
        "read rc_; fi"
}

if is_local "$host"; then
    if ! command -v tmux >/dev/null 2>&1; then
        echo "rclaude: tmux not installed locally — install via 'brew install tmux' (macOS) or your package manager" >&2
        exit 1
    fi
    if ! cd "$dir" 2>/dev/null; then
        echo "rclaude: local directory not found: $dir" >&2
        exit 1
    fi
    exec tmux new-session -s "$session" "$(build_inner "$dir")"
fi

# Remote: pre-flight the directory so a typo or missing path fails loudly
# here instead of silently killing the tmux pane and closing the ssh
# transport (which looks like a generic 'Connection closed' to the user).
if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$host" "test -d ${dir}" 2>/dev/null; then
    echo "rclaude: directory not found on $host: $dir" >&2
    case $dir in
        *@proj/*|*@apps/*|*@pkg/*)
            echo "  hint: '@proj/@apps/@pkg' are Claude-instruction aliases, not real shell paths." >&2
            echo "  See ~/.claude/instructions/project-paths.md for the real ~/Code/<bucket>/<project> mapping." >&2
            ;;
    esac
    exit 1
fi

inner=$(build_inner "$dir")
exec ssh -t "$host" "tmux new-session -s '${session}' \"${inner}\""
