#!/usr/bin/env bash
#
# install-claude-truenas.sh — install Claude Code on TrueNAS SCALE, entirely under /root
#
# Tailored for TrueNAS SCALE 25.10.x, where /, /usr, /usr/local and /opt are
# read-only ZFS datasets and /tmp is mounted noexec. Nothing is installed to the
# system: every byte lives under /root, which is its own writable dataset.
#
# Safe to run repeatedly. A re-run on a healthy, current install is a no-op.
#
# Source of truth: https://files.snapzfs.com/
#
set -euo pipefail

SCRIPT_NAME="install-claude-truenas.sh"
SCRIPT_VERSION="1.1.0"

# --- Upstream endpoints -------------------------------------------------------
INSTALLER_URL="https://claude.ai/install.sh"
RELEASES_URL="https://downloads.claude.ai/claude-code-releases"

# --- Everything this script owns, all under /root -----------------------------
CLAUDE_HOME="/root"
BIN_LINK="${CLAUDE_HOME}/.local/bin/claude"          # symlink -> versioned binary
SHARE_DIR="${CLAUDE_HOME}/.local/share/claude"       # versioned binaries (~126M on-disk)
STATE_DIR="${CLAUDE_HOME}/.local/state/claude"       # lock files
CACHE_DIR="${CLAUDE_HOME}/.cache/claude"             # update staging
CONFIG_DIR="${CLAUDE_HOME}/.claude"                  # settings, history, credentials
CONFIG_JSON="${CLAUDE_HOME}/.claude.json"            # per-user config
TOKEN_FILE="${CONFIG_DIR}/oauth-token"               # long-lived OAuth token, mode 0600
WORK_DIR="${CLAUDE_HOME}/.local/state/claude-truenas"  # this script's own state
BACKUP_DIR="${WORK_DIR}/backups"

# Shell startup files we manage. .zshenv is the load-bearing one: root's shell on
# SCALE is zsh, and .zshenv is sourced for interactive, login AND non-interactive
# shells, so `ssh root@nas claude -p ...` resolves too.
RC_FILES=("${CLAUDE_HOME}/.zshenv" "${CLAUDE_HOME}/.bashrc" "${CLAUDE_HOME}/.profile")

BLOCK_BEGIN="# >>> claude-code (managed by ${SCRIPT_NAME}) >>>"
BLOCK_END="# <<< claude-code (managed by ${SCRIPT_NAME}) <<<"

# Peak disk needed: the installer stages the 'latest' binary (~275M logical)
# before the binary itself fetches the requested channel. 1 GiB is comfortable.
REQUIRED_KIB=$((1024 * 1024))

# --- Options ------------------------------------------------------------------
CHANNEL="stable"
ASSUME_YES=0
DO_UNINSTALL=0
DO_PURGE=0
DO_STATUS=0
DO_REMOVE_TOKEN=0
FORCE=0
TOKEN_FILE_ARG=""
# auto = verify only when we just installed a token, which is the case where the
# answer is actionable. always/never override that.
DO_VERIFY="auto"
VERIFY_RESULT="skipped"   # skipped | passed | failed

# --- Output helpers -----------------------------------------------------------
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
    C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'
    C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'; C_BLUE=$'\033[36m'
else
    C_RESET=""; C_BOLD=""; C_DIM=""; C_RED=""; C_GREEN=""; C_YELLOW=""; C_BLUE=""
fi

info()  { printf '%s\n' "$*"; }
step()  { printf '%s==>%s %s\n' "$C_BLUE"  "$C_RESET" "$*"; }
ok()    { printf '%s  ok%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
warn()  { printf '%swarn%s %s\n' "$C_YELLOW" "$C_RESET" "$*" >&2; }
die()   { printf '%s fail%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; exit 1; }

# Ask a yes/no question. Reads from /dev/tty so this still works when the script
# is piped in (curl ... | bash), where stdin is the script itself.
confirm() {
    local question="$1" answer=""
    if [ "$ASSUME_YES" -eq 1 ]; then
        return 0
    fi
    if [ -r /dev/tty ] && [ -w /dev/tty ]; then
        printf '%s [y/N] ' "$question" > /dev/tty
        read -r answer < /dev/tty || answer=""
    elif [ -t 0 ]; then
        printf '%s [y/N] ' "$question"
        read -r answer || answer=""
    else
        die "No terminal available to confirm. Re-run with --yes to proceed non-interactively."
    fi
    case "$answer" in
        [yY] | [yY][eE][sS]) return 0 ;;
        *) return 1 ;;
    esac
}

usage() {
    cat <<EOF
${SCRIPT_NAME} ${SCRIPT_VERSION}

Installs Claude Code on TrueNAS SCALE with all data confined to /root.

Usage:
  ${SCRIPT_NAME}                     Explain, then prompt before installing or updating
  ${SCRIPT_NAME} --yes               Install or update without prompting
  ${SCRIPT_NAME} --status            Report current state and change nothing
  ${SCRIPT_NAME} --uninstall         Remove the program, keep settings and login
  ${SCRIPT_NAME} --uninstall --purge Remove the program, settings and login
  ${SCRIPT_NAME} --help

Options:
  --channel <stable|latest|X.Y.Z>  Release channel or exact version (default: stable)
  --token-file <path>              Install a long-lived OAuth token from this file
                                   (generate one with 'claude setup-token' on a
                                   machine that has a browser). The token is also
                                   picked up from the CLAUDE_CODE_OAUTH_TOKEN
                                   environment variable if set.
  --remove-token                   Delete the stored OAuth token and exit
  --verify                         Make one small live API call to prove the
                                   credential actually works. Done automatically
                                   whenever a token is installed.
  --no-verify                      Skip that check
  --yes, -y                        Do not prompt
  --force                          Proceed despite host checks failing
  --status                         Read-only report
  --uninstall                      Remove what this script installed
  --purge                          With --uninstall, also delete settings and credentials
  --help, -h                       This message

There is deliberately no --token flag: a token passed on the command line would
be recorded in shell history and visible to other local users via 'ps'.
EOF
}

# --- Argument parsing ---------------------------------------------------------
while [ $# -gt 0 ]; do
    case "$1" in
        --channel)
            [ $# -ge 2 ] || die "--channel needs a value (stable, latest, or X.Y.Z)"
            CHANNEL="$2"; shift 2 ;;
        --channel=*) CHANNEL="${1#*=}"; shift ;;
        --token-file)
            [ $# -ge 2 ] || die "--token-file needs a path"
            TOKEN_FILE_ARG="$2"; shift 2 ;;
        --token-file=*) TOKEN_FILE_ARG="${1#*=}"; shift ;;
        --token | --token=*)
            die "--token is not supported: a token on the command line leaks into shell history and 'ps'. Use --token-file, or export CLAUDE_CODE_OAUTH_TOKEN." ;;
        --remove-token) DO_REMOVE_TOKEN=1; shift ;;
        --verify)    DO_VERIFY="always"; shift ;;
        --no-verify) DO_VERIFY="never"; shift ;;
        --yes | -y)  ASSUME_YES=1; shift ;;
        --force)     FORCE=1; shift ;;
        --status)    DO_STATUS=1; shift ;;
        --uninstall) DO_UNINSTALL=1; shift ;;
        --purge)     DO_PURGE=1; shift ;;
        --help | -h) usage; exit 0 ;;
        *) usage >&2; die "Unknown option: $1" ;;
    esac
done

case "$CHANNEL" in
    stable | latest | [0-9]*.[0-9]*.[0-9]*) ;;
    *) die "Invalid --channel '${CHANNEL}'. Use stable, latest, or an exact version like 2.1.220." ;;
esac

if [ "$DO_PURGE" -eq 1 ] && [ "$DO_UNINSTALL" -eq 0 ]; then
    die "--purge only applies together with --uninstall."
fi

# --- Introspection ------------------------------------------------------------

installed_version() {
    [ -x "$BIN_LINK" ] || { printf ''; return; }
    HOME="$CLAUDE_HOME" "$BIN_LINK" --version 2>/dev/null | awk '{print $1}' || printf ''
}

truenas_version() {
    [ -r /etc/version ] && tr -d '\n' < /etc/version || printf ''
}

# 'claude auth status' emits JSON, e.g.
#   {"loggedIn": false, "authMethod": "none", "apiProvider": "firstParty"}
# The stored token is a plain env var, so feed it in when one is configured.
auth_status_json() {
    [ -x "$BIN_LINK" ] || return 1
    local token=""
    [ -r "$TOKEN_FILE" ] && token="$(head -n1 "$TOKEN_FILE")"
    if [ -n "$token" ]; then
        HOME="$CLAUDE_HOME" CLAUDE_CODE_OAUTH_TOKEN="$token" "$BIN_LINK" auth status 2>/dev/null
    else
        HOME="$CLAUDE_HOME" "$BIN_LINK" auth status 2>/dev/null
    fi
}

is_authenticated() {
    auth_status_json | grep -q '"loggedIn"[[:space:]]*:[[:space:]]*true'
}

auth_method() {
    auth_status_json \
        | sed -n 's/.*"authMethod"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
        | head -n1
}

# --- Host checks --------------------------------------------------------------

check_host() {
    local problems=0

    [ "$(id -u)" -eq 0 ] || die "Must run as root. Claude Code will live in /root."

    if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then
        die "Run this as root directly, not via sudo from ${SUDO_USER}. Under sudo the install would land in /root but '${SUDO_USER}' could not use it."
    fi

    local tnver; tnver="$(truenas_version)"
    if [ -z "$tnver" ]; then
        warn "This does not look like TrueNAS (no /etc/version)."
        problems=$((problems + 1))
    else
        case "$tnver" in
            25.10.*) ok "TrueNAS SCALE ${tnver}" ;;
            *)
                warn "Built and tested against TrueNAS SCALE 25.10.x; this host reports ${tnver}."
                problems=$((problems + 1)) ;;
        esac
    fi

    if [ ! -d "$CLAUDE_HOME" ] || [ ! -w "$CLAUDE_HOME" ]; then
        die "/root is not writable. Cannot continue."
    fi

    # /root must allow execution. On SCALE it does (exec=on), but a hardened host
    # could differ, and a noexec /root would break the launcher in a confusing way.
    local probe="${CLAUDE_HOME}/.claude-exec-probe.$$"
    printf '#!/bin/sh\nexit 0\n' > "$probe"
    chmod +x "$probe"
    if ! "$probe" 2>/dev/null; then
        rm -f "$probe"
        die "/root is mounted noexec. Claude Code cannot run from there."
    fi
    rm -f "$probe"
    ok "/root is writable and allows execution"

    local avail_kib; avail_kib="$(df -Pk "$CLAUDE_HOME" | awk 'NR==2 {print $4}')"
    if [ "${avail_kib:-0}" -lt "$REQUIRED_KIB" ]; then
        warn "Only $((avail_kib / 1024)) MiB free on /root; $((REQUIRED_KIB / 1024)) MiB recommended."
        problems=$((problems + 1))
    else
        ok "Disk space: $((avail_kib / 1024)) MiB free on /root"
    fi

    command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 \
        || die "Neither curl nor wget is available."

    if curl -fsS --max-time 15 "${RELEASES_URL}/stable" >/dev/null 2>&1; then
        ok "Reached downloads.claude.ai"
    else
        warn "Cannot reach ${RELEASES_URL} — check DNS and outbound HTTPS."
        problems=$((problems + 1))
    fi

    if [ "$problems" -gt 0 ] && [ "$FORCE" -eq 0 ]; then
        info ""
        confirm "${problems} check(s) raised concerns. Continue anyway?" \
            || die "Stopped at your request."
    fi
}

# --- Shell integration --------------------------------------------------------
# The upstream installer deliberately does not edit shell startup files; it only
# prints advice. This is the one thing it leaves for us.
#
# The block is identical whether or not a token is configured: the token clause
# is a no-op when the file is absent. That keeps the block byte-stable, so
# re-runs can detect drift by simple comparison.

managed_block() {
    cat <<EOF
${BLOCK_BEGIN}
# Puts the Claude Code launcher on PATH, and loads a long-lived OAuth token if
# one has been installed. Remove with: ${SCRIPT_NAME} --uninstall
case ":\${PATH}:" in
    *":\${HOME}/.local/bin:"*) ;;
    *) PATH="\${HOME}/.local/bin:\${PATH}" ;;
esac
export PATH
if [ -r "\${HOME}/.claude/oauth-token" ]; then
    CLAUDE_CODE_OAUTH_TOKEN="\$(head -n1 "\${HOME}/.claude/oauth-token")"
    export CLAUDE_CODE_OAUTH_TOKEN
fi
${BLOCK_END}
EOF
}

extract_block() {
    local rc="$1"
    [ -f "$rc" ] || return 0
    sed -n "/^${BLOCK_BEGIN}\$/,/^${BLOCK_END}\$/p" "$rc"
}

backup_file() {
    local file="$1"
    [ -f "$file" ] || return 0
    mkdir -p "$BACKUP_DIR"
    local stamp; stamp="$(date +%Y%m%d-%H%M%S)"
    cp -a "$file" "${BACKUP_DIR}/$(basename "$file").${stamp}"
}

# Delete our block from a file, using the two markers as anchors. Never a broad
# regex: a greedy match here would eat whatever the user keeps next to it.
#
# Also drops trailing blank lines. We insert a blank line ahead of the block when
# appending; without this, every install/uninstall cycle would leave one behind
# and the file would slowly accrete whitespace.
strip_block() {
    local rc="$1" out="$2"
    sed "/^${BLOCK_BEGIN}\$/,/^${BLOCK_END}\$/d" "$rc" > "${out}.raw"
    if grep -qF "$BLOCK_BEGIN" "${out}.raw" || grep -qF "$BLOCK_END" "${out}.raw"; then
        rm -f "${out}.raw"
        return 1
    fi
    awk '{ line[NR] = $0 }
         END { last = 0
               for (i = 1; i <= NR; i++) if (line[i] ~ /[^[:space:]]/) last = i
               for (i = 1; i <= last; i++) print line[i] }' "${out}.raw" > "$out"
    rm -f "${out}.raw"
    return 0
}

# Append the block, separated by one blank line when the file has content.
append_block() {
    local rc="$1" want="$2"
    if [ -s "$rc" ]; then
        printf '\n%s\n' "$want" >> "$rc"
    else
        printf '%s\n' "$want" > "$rc"
    fi
}

wire_shell() {
    local changed=0 rc want
    want="$(managed_block)"

    for rc in "${RC_FILES[@]}"; do
        if [ -f "$rc" ] && grep -qF "$BLOCK_BEGIN" "$rc"; then
            # Present. Replace only if this script version wants different content.
            if [ "$(extract_block "$rc")" = "$want" ]; then
                continue
            fi
            backup_file "$rc"
            local tmp="${rc}.claude-tmp.$$"
            if ! strip_block "$rc" "$tmp"; then
                rm -f "$tmp"
                warn "Could not cleanly update the block in ${rc}; left it alone."
                continue
            fi
            mv "$tmp" "$rc"
            append_block "$rc" "$want"
            changed=1
            ok "Updated the managed block in ${rc}"
            continue
        fi

        backup_file "$rc"
        if [ ! -e "$rc" ]; then
            # Record that we created it, so --uninstall can remove it cleanly.
            mkdir -p "$WORK_DIR"
            printf '%s\n' "$rc" >> "${WORK_DIR}/created-rc-files"
            : > "$rc"
        fi
        append_block "$rc" "$want"
        changed=1
        ok "Added PATH entry to ${rc}"
    done

    [ "$changed" -eq 1 ] || ok "Shell startup files already configured"
}

unwire_shell() {
    local rc created=""
    [ -f "${WORK_DIR}/created-rc-files" ] && created="$(cat "${WORK_DIR}/created-rc-files")"

    for rc in "${RC_FILES[@]}"; do
        [ -f "$rc" ] || continue
        grep -qF "$BLOCK_BEGIN" "$rc" || continue
        backup_file "$rc"

        local tmp="${rc}.claude-tmp.$$"
        if ! strip_block "$rc" "$tmp"; then
            rm -f "$tmp"
            warn "Could not cleanly remove the block from ${rc}; left it alone. Edit it by hand."
            continue
        fi

        # If we created this file and it is now effectively empty, remove it.
        if printf '%s\n' "$created" | grep -qxF "$rc" && [ ! -s "$tmp" ]; then
            rm -f "$tmp" "$rc"
            ok "Removed ${rc} (created by this script, now empty)"
        else
            mv "$tmp" "$rc"
            ok "Removed the managed block from ${rc}"
        fi
    done
}

# --- OAuth token --------------------------------------------------------------

# Resolve a token from --token-file or the environment into RESOLVED_TOKEN.
# Sets a global rather than printing: 'die' inside a command substitution would
# only kill the subshell, so a bad --token-file path would be reported and then
# silently ignored. Never echoes the token itself.
RESOLVED_TOKEN=""
resolve_token() {
    if [ -n "$TOKEN_FILE_ARG" ]; then
        [ -r "$TOKEN_FILE_ARG" ] || die "Cannot read token file: ${TOKEN_FILE_ARG}"
        RESOLVED_TOKEN="$(head -n1 "$TOKEN_FILE_ARG" | tr -d '[:space:]')"
        [ -n "$RESOLVED_TOKEN" ] || die "Token file ${TOKEN_FILE_ARG} is empty."
    elif [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]; then
        RESOLVED_TOKEN="$(printf '%s' "$CLAUDE_CODE_OAUTH_TOKEN" | tr -d '[:space:]')"
    fi
}

install_token() {
    local token="$1"
    [ -n "$token" ] || return 0

    # Soft sanity check. Do not hard-fail on format: the token format is
    # Anthropic's to change, and rejecting a valid token is worse than storing
    # a bad one, which simply fails at first use with a clear error.
    case "$token" in
        sk-ant-*) ;;
        *) warn "Token does not start with 'sk-ant-'. Storing it anyway; verify with 'claude auth status'." ;;
    esac

    mkdir -p "$CONFIG_DIR"
    # umask in a subshell so the file is never briefly world-readable.
    ( umask 077; printf '%s\n' "$token" > "$TOKEN_FILE" )
    chmod 600 "$TOKEN_FILE"
    ok "Stored OAuth token at ${TOKEN_FILE} (mode 0600)"
}

# 'claude auth status' reports whether a credential is *present*, not whether it
# works: a syntactically valid but bogus token still reports loggedIn=true. The
# only honest check is to spend one small request and see what comes back.
verify_credential() {
    step "Verifying the credential with one live API call"
    local token="" out="" rc=0
    [ -r "$TOKEN_FILE" ] && token="$(head -n1 "$TOKEN_FILE")"

    if [ -n "$token" ]; then
        out="$(cd "$CLAUDE_HOME" && HOME="$CLAUDE_HOME" CLAUDE_CODE_OAUTH_TOKEN="$token" \
            timeout 120 "$BIN_LINK" -p 'Reply with exactly: OK' 2>&1)" || rc=$?
    else
        out="$(cd "$CLAUDE_HOME" && HOME="$CLAUDE_HOME" \
            timeout 120 "$BIN_LINK" -p 'Reply with exactly: OK' 2>&1)" || rc=$?
    fi

    if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -qi 'ok'; then
        VERIFY_RESULT="passed"
        ok "Credential verified — the API answered"
        return 0
    fi

    VERIFY_RESULT="failed"
    warn "The credential did not work. Claude Code said:"
    printf '%s\n' "$out" | head -n4 | sed 's/^/       /' >&2
    return 1
}

remove_token() {
    if [ -f "$TOKEN_FILE" ]; then
        rm -f "$TOKEN_FILE"
        ok "Removed ${TOKEN_FILE}"
        info "Open a new shell to clear CLAUDE_CODE_OAUTH_TOKEN from the environment."
    else
        ok "No stored token at ${TOKEN_FILE}"
    fi
}

# --- Install / update ---------------------------------------------------------

run_upstream_installer() {
    local tmp_installer="${WORK_DIR}/install.sh"
    mkdir -p "$WORK_DIR"

    step "Fetching the official installer"
    if command -v curl >/dev/null 2>&1; then
        curl -fsSL --max-time 60 "$INSTALLER_URL" -o "$tmp_installer" \
            || die "Could not download ${INSTALLER_URL}"
    else
        wget -q --timeout=60 -O "$tmp_installer" "$INSTALLER_URL" \
            || die "Could not download ${INSTALLER_URL}"
    fi

    # Sanity-check we got a shell script and not a captive portal or error page.
    head -n1 "$tmp_installer" | grep -q '^#!' \
        || die "Downloaded installer does not look like a shell script. Refusing to run it."
    ok "Installer downloaded ($(wc -c < "$tmp_installer") bytes)"

    # The upstream installer verifies the binary's SHA256 against the signed
    # release manifest, then hands off to '<binary> install <channel>'.
    step "Installing Claude Code (${CHANNEL})"
    HOME="$CLAUDE_HOME" bash "$tmp_installer" "$CHANNEL" \
        || die "The official installer failed. Nothing outside /root was touched."
    rm -f "$tmp_installer"
}

do_install_or_update() {
    local before; before="$(installed_version)"

    if [ -n "$before" ]; then
        step "Claude Code ${before} is already installed — checking for updates"
        if HOME="$CLAUDE_HOME" "$BIN_LINK" update; then
            local after; after="$(installed_version)"
            if [ "$after" = "$before" ]; then
                ok "Already current (${after}) — nothing to do"
            else
                ok "Updated ${before} -> ${after}"
            fi
        else
            warn "Update check failed; repairing the installation instead"
            run_upstream_installer
        fi
    else
        if [ -e "$BIN_LINK" ] || [ -d "$SHARE_DIR" ]; then
            warn "Found a broken or partial installation — repairing"
        fi
        run_upstream_installer
    fi

    [ -n "$(installed_version)" ] || die "Installation finished but 'claude --version' does not work."
}

# --- Uninstall ----------------------------------------------------------------

do_uninstall() {
    info ""
    info "${C_BOLD}This will remove Claude Code from /root.${C_RESET}"
    info ""
    info "Removed:"
    info "  ${BIN_LINK}"
    info "  ${SHARE_DIR}       (the binary, ~126 MiB on disk)"
    info "  ${STATE_DIR}"
    info "  ${CACHE_DIR}"
    info "  the managed block in: ${RC_FILES[*]}"
    if [ "$DO_PURGE" -eq 1 ]; then
        info ""
        info "${C_YELLOW}--purge also removes your settings, history and saved login:${C_RESET}"
        info "  ${CONFIG_DIR}   (including the stored OAuth token)"
        info "  ${CONFIG_JSON}"
        info "  ${WORK_DIR}   (including startup-file backups)"
        info "  You will have to sign in again next time."
    else
        info ""
        info "Kept (settings, history and saved login):"
        info "  ${CONFIG_DIR}"
        info "  ${CONFIG_JSON}"
        [ -f "$TOKEN_FILE" ] && info "  ${TOKEN_FILE} — the stored OAuth token is kept"
        info "  Add --purge to remove these too."
    fi
    info ""
    info "Nothing outside /root is touched. Your own files in /root are untouched."
    info ""

    confirm "Proceed with removal?" || die "Stopped at your request."

    unwire_shell

    rm -f  "$BIN_LINK"      && ok "Removed ${BIN_LINK}"
    rm -rf "$SHARE_DIR"     && ok "Removed ${SHARE_DIR}"
    rm -rf "$STATE_DIR"     && ok "Removed ${STATE_DIR}"
    rm -rf "$CACHE_DIR"     && ok "Removed ${CACHE_DIR}"

    if [ "$DO_PURGE" -eq 1 ]; then
        rm -rf "$CONFIG_DIR"  && ok "Removed ${CONFIG_DIR}"
        rm -f  "$CONFIG_JSON" && ok "Removed ${CONFIG_JSON}"
        # Done last: unwire_shell above reads created-rc-files from here.
        rm -rf "$WORK_DIR"    && ok "Removed ${WORK_DIR} (including startup-file backups)"
    fi

    # Prune directories we may have created, but only while empty, so anything
    # else you keep in /root/.local or /root/.cache survives.
    rmdir --ignore-fail-on-non-empty \
        "${CLAUDE_HOME}/.local/bin" "${CLAUDE_HOME}/.local/share" \
        "${CLAUDE_HOME}/.local/state" "${CLAUDE_HOME}/.local" \
        "${CLAUDE_HOME}/.cache" 2>/dev/null || true

    info ""
    ok "Claude Code removed."
    info "Open a new shell (or run: ${C_BOLD}exec zsh${C_RESET}) to drop it from PATH."
    if [ -d "$BACKUP_DIR" ]; then
        info "Backups of edited startup files are in ${BACKUP_DIR}"
    fi
}

# --- Status -------------------------------------------------------------------

do_status() {
    local version; version="$(installed_version)"

    info "${C_BOLD}Claude Code on TrueNAS SCALE $(truenas_version)${C_RESET}"
    info ""
    if [ -n "$version" ]; then
        info "  Installed        ${C_GREEN}yes${C_RESET} (${version})"
        info "  Launcher         ${BIN_LINK} -> $(readlink -f "$BIN_LINK" 2>/dev/null || echo '?')"
        info "  Disk used        $(du -sh "$SHARE_DIR" 2>/dev/null | awk '{print $1}')"
    else
        info "  Installed        ${C_RED}no${C_RESET}"
    fi

    local wired="no" rc
    for rc in "${RC_FILES[@]}"; do
        if [ -f "$rc" ] && grep -qF "$BLOCK_BEGIN" "$rc"; then
            wired="yes"; break
        fi
    done
    info "  Shell configured ${wired}"

    if [ -f "$TOKEN_FILE" ]; then
        info "  OAuth token      present ($(stat -c '%a' "$TOKEN_FILE" 2>/dev/null))"
    else
        info "  OAuth token      none"
    fi

    local latest; latest="$(curl -fsS --max-time 15 "${RELEASES_URL}/${CHANNEL}" 2>/dev/null || echo '?')"
    info "  Channel          ${CHANNEL} (upstream: ${latest})"

    if [ -n "$version" ]; then
        # Presence, not validity: a bogus token still reports loggedIn=true.
        if is_authenticated; then
            info "  Credential       ${C_GREEN}configured${C_RESET} ($(auth_method))"
        else
            info "  Credential       ${C_YELLOW}none${C_RESET} — run ${C_BOLD}claude${C_RESET} from /root to log in"
        fi

        if [ "$DO_VERIFY" = "always" ]; then
            info ""
            verify_credential || true
        elif is_authenticated; then
            info "  ${C_DIM}(configured means present, not proven — check with --verify)${C_RESET}"
        fi
    fi
    info ""
}

# --- The explanation shown when run with no arguments -------------------------

explain() {
    cat <<EOF

${C_BOLD}Claude Code for TrueNAS SCALE${C_RESET}  (${SCRIPT_NAME} ${SCRIPT_VERSION})

${C_BOLD}What this does${C_RESET}
  Installs Anthropic's Claude Code CLI on this TrueNAS host so that you can
  type ${C_BOLD}claude${C_RESET} as root and get a session.

${C_BOLD}Why it is safe on an appliance${C_RESET}
  TrueNAS SCALE keeps /, /usr, /usr/local and /opt as read-only ZFS datasets,
  and nothing here tries to change that. No packages are installed, apt is
  never called, and no system file is modified. Everything lives under /root,
  which is its own writable dataset:

      /root/.local/bin/claude          launcher (a symlink)
      /root/.local/share/claude/       the binary, about 126 MiB on disk
      /root/.local/state/claude/       lock files
      /root/.cache/claude/             update staging
      /root/.claude/  and  .claude.json   settings, history, saved login

  The only other change is a small, clearly marked block appended to your
  shell startup files (/root/.zshenv, .bashrc, .profile) that puts
  ~/.local/bin on PATH. Root's shell here is zsh, and the upstream installer
  does not do this step for you. ${C_BOLD}--uninstall${C_RESET} removes the block again.

${C_BOLD}What it downloads${C_RESET}
  Anthropic's official installer from claude.ai, which verifies the binary's
  SHA256 against the signed release manifest before installing it.
  Channel: ${C_BOLD}${CHANNEL}${C_RESET}

${C_BOLD}Running it again${C_RESET}
  Re-running is safe. If Claude Code is current, nothing happens. If a newer
  release exists on the ${CHANNEL} channel it is updated in place. If the
  install is broken or half-finished, it is repaired.

${C_BOLD}Note on TrueNAS upgrades${C_RESET}
  /root lives in the boot environment. A TrueNAS upgrade or a boot-environment
  rollback can take /root with it. If Claude disappears after one, just run
  this script again.

${C_BOLD}To remove it later${C_RESET}
  ${SCRIPT_NAME} --uninstall            keeps your settings and login
  ${SCRIPT_NAME} --uninstall --purge    removes those too

EOF
}

# --- Post-install report ------------------------------------------------------

report_ready() {
    local version; version="$(installed_version)"

    info ""
    ok "Claude Code ${version} is installed at ${BIN_LINK}"
    info ""
    if [ "$VERIFY_RESULT" = "passed" ]; then
        info "${C_BOLD}Verified and ready to go.${C_RESET} ($(auth_method))"
        info ""
        info "  Start a new shell so PATH picks it up:   ${C_BOLD}exec zsh${C_RESET}"
        info "  Then, from /root:                        ${C_BOLD}claude${C_RESET}"
    elif [ "$VERIFY_RESULT" = "failed" ]; then
        info "${C_YELLOW}Claude Code is installed, but the credential was rejected.${C_RESET}"
        info ""
        info "  Tokens from 'claude setup-token' are valid for one year, so an"
        info "  older one may have expired. Mint a fresh token on a machine with"
        info "  a browser and install it here:"
        info ""
        info "    ${C_BOLD}${SCRIPT_NAME} --token-file <path>${C_RESET}"
    elif [ -f "$TOKEN_FILE" ]; then
        info "${C_BOLD}A token is installed.${C_RESET} (not verified this run)"
        info ""
        info "  Confirm it works with: ${C_BOLD}${SCRIPT_NAME} --verify${C_RESET}"
        info "  Then, from /root:      ${C_BOLD}claude${C_RESET}"
    else
        info "${C_BOLD}One step left: sign in.${C_RESET}"
        info ""
        info "  1. Start a new shell so PATH picks it up: ${C_BOLD}exec zsh${C_RESET}"
        info "  2. From /root, run:                       ${C_BOLD}claude${C_RESET}"
        info ""
        info "     It will print a URL. This host has no browser, so open that"
        info "     URL on your workstation, sign in, then paste the code back"
        info "     into the terminal. The login is saved to /root/.claude and"
        info "     persists across reboots."
        info ""
        info "  ${C_DIM}To skip this on future hosts, run 'claude setup-token' on a machine${C_RESET}"
        info "  ${C_DIM}with a browser, then install here with --token-file <path>.${C_RESET}"
    fi
    info ""
    info "${C_DIM}Health check: claude doctor    Remove: ${SCRIPT_NAME} --uninstall${C_RESET}"
    info ""
}

# --- Main ---------------------------------------------------------------------

main() {
    if [ "$DO_STATUS" -eq 1 ]; then
        do_status
        exit 0
    fi

    if [ "$DO_REMOVE_TOKEN" -eq 1 ]; then
        [ "$(id -u)" -eq 0 ] || die "Must run as root."
        remove_token
        exit 0
    fi

    if [ "$DO_UNINSTALL" -eq 1 ]; then
        [ "$(id -u)" -eq 0 ] || die "Must run as root."
        do_uninstall
        exit 0
    fi

    # Resolve the token before prompting, so a bad path fails fast.
    resolve_token

    # No arguments at all: explain first, then ask. This is the requested default.
    if [ "$ASSUME_YES" -eq 0 ]; then
        explain
        confirm "Install Claude Code into /root now?" || { info "Nothing was changed."; exit 0; }
        info ""
    fi

    step "Checking this host"
    check_host

    do_install_or_update
    install_token "$RESOLVED_TOKEN"
    wire_shell

    if [ "$DO_VERIFY" = "always" ] \
       || { [ "$DO_VERIFY" = "auto" ] && [ -n "$RESOLVED_TOKEN" ]; }; then
        verify_credential || true
    fi

    report_ready
}

main "$@"
