#!/usr/bin/env bash
# =============================================================================
# hub-agent ワンライナーインストーラ (Sprint L)
#
# Hub の `GET /api/cockpit/agents/install-script` が、このスクリプトに
# enrollment token + hub url を埋め込んだ personalized 版を返す。
#
# 単独使用 (token なし):
#   curl -fsSL https://raw.githubusercontent.com/cocorograph/D00000_hub-agent/main/scripts/install.sh | bash
#
# 内容:
#   1. macOS なら Homebrew (なければ install)
#      - 管理者権限が無い環境では自動で $HOME/homebrew にユーザーローカル
#        インストールに切り替わる (HUB_AGENT_USER_BREW=1 で強制も可)
#   2. tmux + node (なければ install via brew / apt)
#   3. npm i -g @cocorograph/hub-agent
#   4. npm i -g @anthropic-ai/claude-code (既存があれば upgrade のみ)
#   5. HUB_AGENT_TOKEN が設定されていれば hub-agent enroll を自動実行
#   6. hub-agent install-service で OS サービス化
# =============================================================================

set -euo pipefail

# brew の auto-update / hint 出力は初回 install で長時間化する原因の最大要因。
# 環境変数で抑止して 1 発完走率を上げる。最新 Formula が欲しいときは
# ユーザーが `brew update` を別途実行する前提。
export HOMEBREW_NO_AUTO_UPDATE=1
export HOMEBREW_NO_ENV_HINTS=1
export HOMEBREW_NO_INSTALL_CLEANUP=1

# Node.js のサポート範囲ポリシー（Active LTS のみ）。
# - 既存 node のメジャーが [MIN, MAX] に収まっていれば現状維持
# - 範囲外なら NODE_DEFAULT_BREW_FORMULA (最新 Active LTS) にアップ / ダウングレード
# 2026-05 時点の LTS スケジュール:
#   - node 22: Active LTS → 2027/04 EoL
#   - node 24: Active LTS → 2028/04 EoL（最新、サポート期間最長）
#   - node 26: current（LTS ではない、SSL/CA 周りの問題報告あり）
NODE_MIN_MAJOR=22
NODE_MAX_MAJOR=24
NODE_DEFAULT_BREW_FORMULA="node@24"

PACKAGE_NAME="@cocorograph/hub-agent"
CLAUDE_CODE_PACKAGE="@anthropic-ai/claude-code"

color_step() { printf "\033[1;34m==> %s\033[0m\n" "$1"; }
color_ok()   { printf "\033[1;32m✓ %s\033[0m\n" "$1"; }
color_warn() { printf "\033[1;33m! %s\033[0m\n" "$1"; }
color_err()  { printf "\033[1;31m✗ %s\033[0m\n" "$1" >&2; }

present() { command -v "$1" >/dev/null 2>&1; }

# step counter (main() で使う)。色は出すが詳細メッセージは関数内に任せる。
# STEP_TOTAL は main の冒頭で再設定する想定。
STEP_TOTAL=10
STEP_NUM=0
step_header() {
  STEP_NUM=$((STEP_NUM + 1))
  printf "\033[1;36m\n━━━ [%d/%d] %s ━━━\033[0m\n" "$STEP_NUM" "$STEP_TOTAL" "$1"
}

# 指定コマンドを最大 N 回まで指数 backoff で retry する。
# transient な network / brew / npm 失敗を耐える用途。
# 使い方: retry 3 brew install foo
#         retry 3 npm install -g bar
#
# 重要: bash の `set -e` 下では `if cmd` パターンは cmd の戻り値で分岐するため、
#       cmd 失敗時に script が exit しない (`set -e` の標準仕様)。
#       retry 全体が失敗した時のみ呼び出し元が exit する。
retry() {
  local max="$1"; shift
  local i=1
  local delay=2
  while true; do
    if "$@"; then
      return 0
    fi
    if (( i >= max )); then
      color_err "コマンドが ${max} 回連続失敗: $*"
      return 1
    fi
    color_warn "失敗 (${i}/${max}) → ${delay}s 後に再試行: $*"
    sleep "$delay"
    i=$((i + 1))
    delay=$((delay * 2))
  done
}

# 現在 PATH にある brew が、このユーザーで書き込み可能か判定する。
# 「brew はあるが Cellar が他ユーザー所有で書き込み不可」というケースを検知して
# user-local Homebrew にフォールバックするための判定関数。
_existing_brew_writable() {
  local prefix
  prefix="$(brew --prefix 2>/dev/null || echo '')"
  [[ -z "$prefix" ]] && return 1
  # Cellar / opt が書き込み可能か（または未作成でも prefix 自体が書き込み可能か）
  if [[ -w "$prefix/Cellar" ]] || ( [[ ! -e "$prefix/Cellar" ]] && [[ -w "$prefix" ]] ); then
    return 0
  fi
  return 1
}

ensure_brew() {
  if [[ "$(uname)" != "Darwin" ]]; then return 0; fi

  # 既に brew がインストールされている場合の判定:
  # - ユーザーが書き込み可能 → そのまま使用（system / user-local どちらでも OK）
  # - 書き込み不可（他ユーザー所有の brew が居る）→ $HOME/homebrew に並列で user-local 化
  if present brew; then
    if _existing_brew_writable; then
      color_ok "brew already installed and writable ($(brew --prefix))"
      persist_brew_shellenv
      return 0
    fi
    color_warn "既存 brew ($(brew --prefix 2>/dev/null)) は書き込み不可 → \$HOME/homebrew にユーザーローカル Homebrew を並列インストール"
    _install_user_local_brew
    return 0
  fi

  # brew コマンドが PATH にない場合の判定:
  # - 環境変数 HUB_AGENT_USER_BREW=1 で明示強制
  # - sudo がパスワードなしで通らない = 管理者ではない可能性が高いと判定
  local use_user_mode=0
  if [[ "${HUB_AGENT_USER_BREW:-0}" == "1" ]]; then
    use_user_mode=1
    color_step "HUB_AGENT_USER_BREW=1 → ユーザーローカル Homebrew を使用"
  elif ! sudo -n true 2>/dev/null; then
    color_warn "管理者権限なしと判定 → \$HOME/homebrew にユーザーローカル Homebrew をインストール"
    use_user_mode=1
  fi

  if (( use_user_mode == 1 )); then
    _install_user_local_brew
  else
    color_step "Homebrew をシステムインストール"
    # Homebrew 公式 install.sh をダウンロードして実行。
    # curl は GitHub raw のレート制限や transient 502 を retry でカバーする。
    retry 3 bash -c '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
    # Apple Silicon の brew はデフォルト PATH に入らないので追加
    if [[ -d /opt/homebrew/bin ]]; then
      export PATH="/opt/homebrew/bin:$PATH"
    fi
    persist_brew_shellenv
  fi
  present brew || { color_err "brew install failed"; exit 1; }
}

# user-local Homebrew のインストール本体（重複排除のため関数化）。
# 既に $HOME/homebrew が展開済みなら再展開せず PATH/shellenv だけ整える。
_install_user_local_brew() {
  if [[ -x "$HOME/homebrew/bin/brew" ]]; then
    color_ok "\$HOME/homebrew は既に展開済み"
  else
    color_step "Homebrew をユーザーローカルインストール (\$HOME/homebrew)"
    mkdir -p "$HOME/homebrew"
    # GitHub tarball ダウンロード + tar 展開を 1 つの bash -c に包んで retry にかける。
    # pipe 途中の curl 失敗を retry のスコープに収めるため。
    retry 3 bash -c 'curl -fsSL https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C "$HOME/homebrew"'
  fi
  # PATH を user-local 優先で並べる（既存 /usr/local/bin/brew より先）
  export PATH="$HOME/homebrew/bin:$PATH"
  hash -r 2>/dev/null || true  # bash のコマンドキャッシュをクリア
  persist_user_brew_shellenv
}

# Apple Silicon Mac で brew のパスを zsh / bash 永続化する。既に追記済みなら no-op。
# `eval "$(/opt/homebrew/bin/brew shellenv)"` を書くことで、Homebrew が用意する
# HOMEBREW_PREFIX / PATH / MANPATH / INFOPATH すべてを新規シェルで自動セットする。
persist_brew_shellenv() {
  local brew_bin=""
  # $HOME/homebrew が PATH 上で優先されているなら、user-local 側で永続化する
  if [[ -x "$HOME/homebrew/bin/brew" ]] && [[ "$(command -v brew)" == "$HOME/homebrew/bin/brew" ]]; then
    persist_user_brew_shellenv
    return 0
  fi
  if [[ -x /opt/homebrew/bin/brew ]]; then
    brew_bin="/opt/homebrew/bin/brew"
  elif [[ -x /usr/local/bin/brew ]]; then
    # Intel Mac は `/usr/local/bin` がデフォルト PATH に入るので追記不要
    return 0
  else
    return 0
  fi
  local snippet="eval \"\$(${brew_bin} shellenv)\""
  local marker="# >>> hub-agent: brew shellenv (Apple Silicon PATH) >>>"
  local end_marker="# <<< hub-agent: brew shellenv <<<"
  local target
  for target in "$HOME/.zprofile" "$HOME/.bash_profile"; do
    if [[ -f "$target" ]] && grep -Fq "$snippet" "$target"; then
      color_ok "brew shellenv は既に $target にあります"
      continue
    fi
    color_step "$target に brew shellenv を追記"
    {
      printf '\n%s\n' "$marker"
      printf '%s\n' "$snippet"
      printf '%s\n' "$end_marker"
    } >> "$target"
    color_ok "$target に追記しました (新規シェルから有効)"
  done
}

# user-mode（$HOME/homebrew）用の shellenv 永続化。
# system 版 persist_brew_shellenv と同じ仕組みだが対象 brew が $HOME 配下。
persist_user_brew_shellenv() {
  local brew_bin="$HOME/homebrew/bin/brew"
  [[ -x "$brew_bin" ]] || return 0
  local snippet="eval \"\$(${brew_bin} shellenv)\""
  local marker="# >>> hub-agent: brew shellenv (user-local) >>>"
  local end_marker="# <<< hub-agent: brew shellenv <<<"
  local target
  for target in "$HOME/.zprofile" "$HOME/.bash_profile"; do
    if [[ -f "$target" ]] && grep -Fq "$snippet" "$target"; then
      color_ok "user-local brew shellenv は既に $target にあります"
      continue
    fi
    color_step "$target に user-local brew shellenv を追記"
    {
      printf '\n%s\n' "$marker"
      printf '%s\n' "$snippet"
      printf '%s\n' "$end_marker"
    } >> "$target"
    color_ok "$target に追記しました (新規シェルから有効)"
  done
}

# user-mode の場合、npm の global prefix を $HOME/.npm-global に切り替える。
# brew が $HOME/homebrew にいるとき、その prefix への書き込み権限はあるものの、
# 後で system 版 node / npm に切り替わる可能性も考えて user 領域に逃がしておく。
# system mode（/opt/homebrew 等）では何もしない（既存挙動を温存）。
ensure_npm_user_prefix() {
  # macOS は user-mode (brew が $HOME/homebrew) のときだけ切替（既存挙動を温存）。
  # Linux は apt 等の global prefix が sudo 必須 / global bin が PATH に通らない問題を
  # 起こしやすい（npm i -g は成功しても hub-agent / claude が command not found になる）。
  # そのため Linux では常に $HOME/.npm-global へ寄せ、sudo 不要・bin パス確定・PATH 反映を
  # 一貫させる（WSL クリーン Ubuntu で発覚）。
  if [[ "$(uname)" == "Darwin" ]]; then
    [[ -d "$HOME/homebrew" ]] || return 0  # user-mode 検知
  fi

  # 既に $HOME/.npm-global が prefix なら何もしない
  local cur_prefix
  cur_prefix="$(npm config get prefix 2>/dev/null || echo '')"
  if [[ "$cur_prefix" == "$HOME/.npm-global" ]]; then
    # ワンライナー (bash -c) は非ログイン非対話で profile が読まれないため、
    # 既存 prefix の再セットアップ時に $HOME/.npm-global/bin が PATH に乗らず
    # 後段の `hub-agent` / `claude` 呼び出しが command not found になる事故があった。
    # 実行中シェルの PATH を毎回明示的に整える。
    export PATH="$HOME/.npm-global/bin:$PATH"
    color_ok "npm global prefix は既に $HOME/.npm-global"
    return 0
  fi

  color_step "npm global prefix を \$HOME/.npm-global に切替 (user-mode)"
  mkdir -p "$HOME/.npm-global"
  npm config set prefix "$HOME/.npm-global"
  export PATH="$HOME/.npm-global/bin:$PATH"

  local snippet='export PATH="$HOME/.npm-global/bin:$PATH"'
  local marker="# >>> hub-agent: npm-global PATH (user-mode) >>>"
  local end_marker="# <<< hub-agent: npm-global PATH <<<"
  # 永続先は OS で出し分け。Linux の hub-agent は systemd --user 起動時や
  # `$SHELL -lic` 経由の env 注入で PATH を解決するため、login(.profile) と
  # interactive(.bashrc) の双方に通す。macOS は従来どおり zsh/bash の profile。
  local targets
  if [[ "$(uname)" == "Darwin" ]]; then
    targets=("$HOME/.zprofile" "$HOME/.bash_profile")
  else
    targets=("$HOME/.bashrc" "$HOME/.profile")
  fi
  local target
  for target in "${targets[@]}"; do
    if [[ -f "$target" ]] && grep -Fq "$snippet" "$target"; then
      continue
    fi
    {
      printf '\n%s\n' "$marker"
      printf '%s\n' "$snippet"
      printf '%s\n' "$end_marker"
    } >> "$target"
    color_ok "$target に npm-global PATH を追記"
  done
}

# =============================================================================
# Node TLS 環境の自動修復
#
# 背景: npm install で `UNABLE_TO_GET_ISSUER_CERT_LOCALLY` が出る環境がある。
# 原因は「node は OS の信頼ストアを使わず、自分にコンパイル時に焼き込まれた
# CA リストだけで TLS 検証する」設計にある:
#
#   - curl は macOS Keychain (or /etc/ssl/certs) を使う → 通る
#   - node はバンドル CA のみ → 通らない
#
# テナント環境では以下のいずれかで MITM 的な証明書差し替えが起きうる:
#   - ウィルス対策ソフト (Sophos / Trend Micro / Norton / Symantec / Kaspersky 等)
#   - 企業の SSL インスペクション proxy (ZScaler / Cloudflare WARP for Teams 等)
#   - VPN クライアントによるトラフィック検査
#   - 広告ブロッカー (NextDNS / AdGuard / 1.1.1.1 等)
#   - 親会社配布のセキュリティアプリ
#
# どれもユーザー操作なしには検出しにくく、ユーザー自身も気づいていないことが多い。
#
# 対処: ユーザーが既に OS で信頼している CA バンドルを node にも渡せば、curl と
# node の信頼ストアの乖離が解消する。`NODE_EXTRA_CA_CERTS` 環境変数を使えば
# node のバンドル CA に「追加で」信頼する証明書を渡せる (バンドル CA を置き換える
# わけではないので、通常環境への副作用はない)。
#
# 1. pre-flight check で node の TLS が通るかテスト
# 2. 失敗時のみ、OS の信頼ストアを PEM に書き出して NODE_EXTRA_CA_CERTS にセット
# 3. シェル profile (.zprofile / .bash_profile) にも追記して永続化
# 4. 再テスト → ダメなら明確な日本語エラーガイドで終了
# =============================================================================

# node 側 TLS で npm registry に到達できるかテスト。成功で 0、失敗で 1。
# stderr の最後の行を grep 用に echo するので、呼び出し側で原因種別を判定できる。
_test_node_tls() {
  if ! present node; then
    return 1
  fi
  # node -e で出る ERR_TLS_CERT_ALTNAME_INVALID 等の他種別エラーは別途扱う必要が
  # あるため戻り値だけで判定。stdout/stderr は呼び出し側で破棄してよい。
  node -e "require('https').get('https://registry.npmjs.org/', r => { process.exit(r.statusCode >= 400 ? 1 : 0); }).on('error', e => { console.error(e.code || e.message); process.exit(1); });" 2>&1
}

# OS の信頼ストア (system / login keychain or /etc/ssl/certs) を PEM に書き出す。
# 返り値: 成功なら 0 + ファイルパスを stdout に echo / 失敗なら 1。
_export_os_ca_bundle() {
  local out_pem="$HOME/.hub-agent-ca.pem"
  case "$(uname -s)" in
    Darwin)
      # System.keychain には企業配布 CA や手動追加 CA、SystemRootCertificates.keychain
      # には Apple 配布の標準 root CA が入っている。両方をマージ。
      # `-p` で PEM 形式、`-a` で全件出力。pemcat に近い操作。
      {
        security find-certificate -a -p /Library/Keychains/System.keychain 2>/dev/null || true
        security find-certificate -a -p /System/Library/Keychains/SystemRootCertificates.keychain 2>/dev/null || true
      } > "$out_pem"
      ;;
    Linux)
      # Debian/Ubuntu 系
      if [[ -r /etc/ssl/certs/ca-certificates.crt ]]; then
        cp /etc/ssl/certs/ca-certificates.crt "$out_pem"
      # RHEL/CentOS/Fedora 系
      elif [[ -r /etc/pki/tls/certs/ca-bundle.crt ]]; then
        cp /etc/pki/tls/certs/ca-bundle.crt "$out_pem"
      # SUSE 系
      elif [[ -r /var/lib/ca-certificates/ca-bundle.pem ]]; then
        cp /var/lib/ca-certificates/ca-bundle.pem "$out_pem"
      else
        return 1
      fi
      ;;
    *)
      return 1
      ;;
  esac
  if [[ ! -s "$out_pem" ]]; then
    rm -f "$out_pem"
    return 1
  fi
  echo "$out_pem"
}

# シェル profile に `export NODE_EXTRA_CA_CERTS=...` を追記する。
# 既に同じパス指定の export 行があればスキップ (idempotent)。
_persist_node_extra_ca_certs() {
  local ca_path="$1"
  local snippet="export NODE_EXTRA_CA_CERTS=\"$ca_path\""
  local marker="# >>> hub-agent: NODE_EXTRA_CA_CERTS (TLS fallback) >>>"
  local end_marker="# <<< hub-agent: NODE_EXTRA_CA_CERTS <<<"
  local target
  for target in "$HOME/.zprofile" "$HOME/.bash_profile"; do
    [[ -e "$target" ]] || touch "$target"
    if grep -Fq "$snippet" "$target" 2>/dev/null; then
      continue
    fi
    {
      printf '\n%s\n' "$marker"
      printf '%s\n' "$snippet"
      printf '%s\n' "$end_marker"
    } >> "$target"
    color_ok "$target に NODE_EXTRA_CA_CERTS を追記"
  done
}

# 「UNABLE_TO_GET_ISSUER_CERT_LOCALLY が出た時に何を確認すべきか」のガイド表示。
# フォールバックも効かなかった最終手段ケース用。
_print_tls_failure_guidance() {
  # ヘッダーだけ printf で色付け。本文は cat <<EOF で複数行を読みやすく出す。
  printf '\n\033[1;31m✗ node の TLS 検証が修復できませんでした。\033[0m\n'
  cat <<EOF

  curl は通るのに node だけ落ちる場合、お使いの Mac/PC に
  HTTPS 通信を検査しているソフトが入っている可能性が高いです。
  典型例:
    - ウィルス対策ソフト (Sophos / Norton / Trend Micro / Symantec 等)
    - 親会社配布のセキュリティアプリ
    - SSL インスペクション機能つきの VPN クライアント
    - 広告ブロッカー (NextDNS / 1.1.1.1 for Families / AdGuard 等)

  以下のコマンドで「実際の証明書発行者」が見えます。
  ここに表示される \`issuer\` が \`Let's Encrypt\` や \`DigiCert\` ではなく、
  特定のソフト名 (ZScaler / Cocorograph / 製品名 等) なら、それが原因です:

    node -e 'const t=require("tls");const s=t.connect(443,"registry.npmjs.org",{servername:"registry.npmjs.org",rejectUnauthorized:false},()=>{let c=s.getPeerCertificate(true);while(c&&Object.keys(c).length){console.log("issuer:",JSON.stringify(c.issuer));if(!c.issuerCertificate||c.issuerCertificate===c)break;c=c.issuerCertificate;}s.end();});'

  暫定的に install を進めたい場合（自分の環境を信頼している前提）:

    npm install -g $PACKAGE_NAME --strict-ssl=false
    npm install -g $CLAUDE_CODE_PACKAGE --strict-ssl=false
    hub-agent enroll <token> --hub-url <hub-url>
    hub-agent install-service

  詳細サポートは Hub の cockpit チャネルへ。

EOF
}

# pre-flight TLS 検査 + 自動 fallback の本体。
# main() から ensure_npm_user_prefix の直後で呼ぶ想定。
ensure_node_tls_works() {
  # Linux の中には node を持たない経路 (本スクリプトより前に node が入る) もあるので
  # node が無ければスキップ (後段の ensure_global_install で別エラーになる)。
  present node || return 0

  color_step "node の TLS 検証を pre-flight check"

  # ステップ 0: brew link 直後はシェル command hash に古い node/npm パスが残る
  # ことがあるので、念のためクリアしてから検査する。
  hash -r 2>/dev/null || true

  if _test_node_tls >/dev/null 2>&1; then
    color_ok "node TLS 検証 OK (registry.npmjs.org に到達可能)"
    return 0
  fi

  color_warn "node TLS 検証失敗 → OS 信頼ストアから CA をエクスポートして再試行"

  local ca_pem
  if ! ca_pem=$(_export_os_ca_bundle); then
    color_err "OS の信頼ストア (macOS Keychain / Linux ca-certificates) からの CA エクスポートに失敗"
    _print_tls_failure_guidance
    exit 1
  fi
  color_ok "$ca_pem に OS 信頼ストアの CA を書き出し ($(wc -l < "$ca_pem" | tr -d ' ') 行)"

  export NODE_EXTRA_CA_CERTS="$ca_pem"
  _persist_node_extra_ca_certs "$ca_pem"

  # 再テスト
  if _test_node_tls >/dev/null 2>&1; then
    color_ok "node TLS 検証 OK (NODE_EXTRA_CA_CERTS=$ca_pem 経由)"
    return 0
  fi

  color_err "OS 信頼ストアを渡しても node TLS 検証が通りません"
  _print_tls_failure_guidance
  exit 1
}

ensure_pkg() {
  local cmd="$1"
  local brew_pkg="$2"
  local apt_pkg="${3:-$brew_pkg}"
  if present "$cmd"; then color_ok "$cmd already installed"; return 0; fi
  color_step "$cmd をインストール"
  if [[ "$(uname)" == "Darwin" ]]; then
    # brew install は transient (network / hash mismatch / pour 失敗) を retry で吸収
    retry 3 brew install "$brew_pkg"
  elif present apt-get; then
    retry 3 sudo apt-get update -y
    retry 3 sudo apt-get install -y "$apt_pkg"
  elif present dnf; then
    retry 3 sudo dnf install -y "$apt_pkg"
  elif present pacman; then
    retry 3 sudo pacman -S --noconfirm "$apt_pkg"
  else
    color_err "対応するパッケージマネージャ (brew/apt/dnf/pacman) が見つかりません。$cmd を手動で install してください"
    exit 1
  fi
}

# macOS で brew 経由で Active LTS の node (NODE_DEFAULT_BREW_FORMULA) を導入/切替する。
# 既存の無印 node が link されていれば unlink してから新 formula を link --overwrite --force。
_install_node_lts_brew() {
  local formula="$NODE_DEFAULT_BREW_FORMULA"
  color_step "$formula (Active LTS) を install"
  retry 3 brew install "$formula"
  # 既存無印 node が link されていれば外す（v26 current 等を退かす）
  if brew list node >/dev/null 2>&1; then
    color_step "既存 'node' formula を unlink ($formula を優先するため)"
    brew unlink node 2>/dev/null || true
  fi
  color_step "$formula を link --overwrite --force"
  brew link --overwrite --force "$formula"
  hash -r 2>/dev/null || true
}

# Linux で Active LTS の node を導入する。
# Ubuntu の apt 素 nodejs は 24.04 でも v18 でポリシー (>=22) を満たせないため、
# NodeSource (setup_<major>.x) を使ってシステムワイドに最新 LTS を導入する。既存の
# apt 版 nodejs があっても apt-get install -y nodejs が NodeSource 版へ置き換える。
_install_node_lts_linux() {
  local major="${NODE_DEFAULT_BREW_FORMULA##*@}"   # "node@24" -> "24"
  color_step "NodeSource 経由で Node ${major} (Active LTS) を install"
  if present apt-get; then
    retry 3 bash -c "curl -fsSL https://deb.nodesource.com/setup_${major}.x | sudo -E bash -"
    retry 3 sudo apt-get install -y nodejs
  elif present dnf; then
    retry 3 bash -c "curl -fsSL https://rpm.nodesource.com/setup_${major}.x | sudo -E bash -"
    retry 3 sudo dnf install -y nodejs
  else
    color_err "NodeSource 非対応の環境です。Node ${NODE_MIN_MAJOR}+ を手動で install してください (nvm 推奨)"
    exit 1
  fi
  hash -r 2>/dev/null || true
  color_ok "node $(node --version 2>/dev/null || echo '?') を導入"
}

# ネイティブアドオンのビルドに必要な make / g++ / python3 を導入する。
# PTY backend は @lydell/node-pty に移行済みで全 OS の prebuild を同梱するため通常は
# native ビルド不要だが、prebuild が当たらない環境では node-gyp rebuild に
# フォールバックする。クリーンな Ubuntu には build-essential が無く
# `gyp ERR! not found: make` で失敗する (WSL クリーン Ubuntu で発覚) ため保険で導入する。
# macOS は Xcode CLT 前提なので何もしない。
ensure_build_tools() {
  [[ "$(uname)" == "Darwin" ]] && return 0
  if present make && present g++; then
    color_ok "build tools (make / g++) already installed"
    return 0
  fi
  color_step "ネイティブビルド依存 (build-essential / python3) を install"
  if present apt-get; then
    retry 3 sudo apt-get update -y
    retry 3 sudo apt-get install -y build-essential python3
  elif present dnf; then
    retry 3 sudo dnf groupinstall -y "Development Tools"
    retry 3 sudo dnf install -y python3
  elif present pacman; then
    retry 3 sudo pacman -S --noconfirm base-devel python
  else
    color_err "対応するパッケージマネージャが見つかりません。make / g++ / python3 を手動で install してください"
    exit 1
  fi
}

# Node.js のサポート範囲ポリシー判定。
# - 未インストール → LTS を install
# - [MIN, MAX] 範囲内 → 現状維持（ユーザーの環境を尊重）
# - 範囲未満 → LTS にアップグレード
# - 範囲超過（current 系等） → LTS にダウングレード
ensure_node_version() {
  local v
  # node 皆無のクリーン環境では `node --version` が exit 127。set -euo pipefail 下
  # では pipefail がこれを拾ってスクリプトごと死ぬため、`|| v=""` で必ず吸収する
  # (macOS テスト機には既に node があり露見しなかった潜在バグ。WSL クリーン Ubuntu で発覚)。
  v=$(node --version 2>/dev/null | sed 's/^v//' | cut -d. -f1) || v=""
  if [[ -z "$v" ]]; then
    color_step "node が未インストール → $NODE_DEFAULT_BREW_FORMULA (Active LTS) を install"
    if [[ "$(uname)" == "Darwin" ]]; then
      _install_node_lts_brew
    else
      _install_node_lts_linux
    fi
    return
  fi

  if (( v >= NODE_MIN_MAJOR && v <= NODE_MAX_MAJOR )); then
    color_ok "node v$v は動作保証範囲 (${NODE_MIN_MAJOR} ≤ v ≤ ${NODE_MAX_MAJOR}) 内。現状維持"
    return
  fi

  if (( v < NODE_MIN_MAJOR )); then
    color_warn "node v$v は古い (>=${NODE_MIN_MAJOR} 必須) → $NODE_DEFAULT_BREW_FORMULA にアップグレード"
  else
    color_warn "node v$v は新しすぎ (current 系) → 安定動作のため $NODE_DEFAULT_BREW_FORMULA (LTS) にダウングレード"
  fi
  if [[ "$(uname)" == "Darwin" ]]; then
    _install_node_lts_brew
  else
    _install_node_lts_linux
  fi
}

ensure_global_install() {
  # @latest 明示 + --force で npm cache stale 起因の「同 version 判定 skip」を回避。
  # 過去事例: 0.5.24 が既に入っているマシンで `npm i -g xxx` が 0.5.26 に更新
  # しない事象があった (npm の "no changes needed" 誤発火)。--force でこの判定を
  # 飛ばし、@latest で確実に最新版を解決する。
  if present hub-agent; then
    local cur
    cur=$(hub-agent --version 2>/dev/null || echo "unknown")
    color_step "hub-agent (現在 $cur) を最新版にアップデート"
  else
    color_step "$PACKAGE_NAME を install"
  fi
  retry 3 npm install -g "${PACKAGE_NAME}@latest" --force
  hash -r 2>/dev/null || true
  color_ok "hub-agent $(hub-agent --version)"
}

# Claude Code CLI を同梱でセットアップ。Cockpit 側から claude を呼び出すので、
# 同じ npm-global prefix に入れておくとパス解決が一貫する。
# 既存があれば upgrade のみ（破壊しない）。
ensure_claude_code() {
  if present claude; then
    local cur
    cur=$(claude --version 2>/dev/null || echo "unknown")
    color_step "Claude Code (現在 $cur) を最新版にアップデート"
    # claude は Anthropic 配信。失敗しても既存版で継続 (warning のみ)。
    retry 2 npm install -g "${CLAUDE_CODE_PACKAGE}@latest" --force \
      || color_warn "Claude Code upgrade に失敗 (既存版で継続)"
  else
    color_step "$CLAUDE_CODE_PACKAGE を install"
    retry 3 npm install -g "${CLAUDE_CODE_PACKAGE}@latest" --force
  fi
  hash -r 2>/dev/null || true
  if present claude; then
    color_ok "claude $(claude --version 2>/dev/null || echo 'installed')"
  else
    color_warn "claude コマンドが PATH に見つかりません。新規シェルで再確認してください"
  fi
}

do_enroll() {
  if [[ -z "${HUB_AGENT_TOKEN:-}" ]]; then
    color_warn "HUB_AGENT_TOKEN が未設定。enroll は skip します"
    color_warn "  手動で実行: hub-agent enroll <token> --hub-url ${HUB_AGENT_URL:-https://api.hub.cocorograph.com}"
    return 0
  fi
  local hub_url="${HUB_AGENT_URL:-https://api.hub.cocorograph.com}"
  # enroll は token が短命の可能性があるため retry は 1 回のみ。
  # (transient network なら retry が効くが、token 期限切れだと回数を増やしても無駄)
  if [[ -f "$HOME/.hub/agent.json" ]]; then
    color_warn "~/.hub/agent.json が既にあります。--force で上書きします"
    retry 2 hub-agent enroll "$HUB_AGENT_TOKEN" --hub-url "$hub_url" --force
  else
    retry 2 hub-agent enroll "$HUB_AGENT_TOKEN" --hub-url "$hub_url"
  fi

  # enroll 内部の syncBundle 失敗は warning だけで握りつぶされる (enroll 自体は
  # success 扱い) ため、ここで bundle 展開状況を verify する。
  # 2 段チェック:
  #   - CLAUDE.md の HUB-AI-RULES マーカー (運用ルール本体が入っているか)
  #   - scripts/manifest.json の存在 (バンドル本体のファイル群が落ちているか)
  # マーカーは通常 syncBundle の最終ステップで書かれるが、scripts/manifest.json
  # が無いと SessionStart hook / hub-helper.py が動かないため両方必須。どちらかが
  # 欠けていれば半完了状態とみなして sync-bundle を retry。
  if _hub_bundle_incomplete; then
    color_warn "Hub AI bundle が ~/.claude に完全展開されていない可能性。sync-bundle を再試行"
    if ! retry 2 hub-agent sync-bundle; then
      color_warn "hub-agent sync-bundle が継続失敗。あとで手動実行してください:"
      color_warn "  hub-agent sync-bundle"
    fi
  fi
}

# Hub AI bundle の展開状況を判定する。完全に展開されていれば 0、欠けていれば 1。
# do_enroll の事後 verify と verify_setup の最終チェックで共通利用する。
_hub_bundle_incomplete() {
  if [[ ! -f "$HOME/.claude/CLAUDE.md" ]]; then
    return 0
  fi
  if ! grep -q "BEGIN HUB-AI-RULES" "$HOME/.claude/CLAUDE.md" 2>/dev/null; then
    return 0
  fi
  if [[ ! -f "$HOME/.claude/scripts/manifest.json" ]]; then
    return 0
  fi
  return 1
}

do_install_service() {
  color_step "OS サービスとして自動起動を登録"
  # install-service は launchctl bootout/bootstrap の transient で初回失敗する
  # ことがある (既存 unit の残骸 + 同名 bootstrap 競合 等)。retry でカバー。
  retry 2 hub-agent install-service
  color_ok "install-service 完了。ログ: ~/.hub/agent.log"
}

# enroll で bundle 配信された ~/.claude/scripts/setup_hub_ai.py を同期実行して
# ~/.claude/CLAUDE.md と ~/.claude/settings.json を初期化する。
#
# enroll.mjs 内でも kickSetupHubAi で best-effort spawn しているが、それは
# detached + stdio ignore で完了を待たないため、続く verify_setup が
# CLAUDE.md マーカー欠落を warn として拾ってしまう。install.sh では同期実行で
# verify を成功させる。setup_hub_ai 不在 (bundle 同期失敗) のときは色付き
# warn を出して継続。再実行時は step_* が冪等処理を返すだけで害なし。
do_bootstrap_hub_ai() {
  local setup_script="$HOME/.claude/scripts/setup_hub_ai.py"
  if [[ ! -f "$setup_script" ]]; then
    color_warn "setup_hub_ai.py 不在 (bundle 同期が失敗した可能性)。スキップ"
    return 0
  fi
  color_step "setup_hub_ai.py --silent を実行 (~/.claude/CLAUDE.md / settings.json 初期化)"
  if python3 "$setup_script" --silent; then
    color_ok "Hub AI bootstrap 完了"
  else
    color_warn "setup_hub_ai.py --silent が exit≠0 で終了 (継続)。手動で再実行: python3 $setup_script"
  fi
}

# セットアップ最終検証。
# 「online には見えるがバンドル未配信」「サービス起動失敗」等の半完了状態を
# 検知してユーザーに次の手を案内する。返り値は 0 (errors > 0 でも継続)。
verify_setup() {
  local errors=0

  # 1. hub-agent CLI
  if present hub-agent; then
    color_ok "hub-agent CLI: $(hub-agent --version 2>/dev/null || echo 'installed')"
  else
    color_err "hub-agent コマンドが PATH に見つかりません"
    errors=$((errors + 1))
  fi

  # 2. Claude Code (任意なので warn のみ)
  if present claude; then
    color_ok "Claude Code: $(claude --version 2>/dev/null || echo 'installed')"
  else
    color_warn "Claude Code (claude) コマンドが見つかりません (cockpit から claude を呼ぶ場合は必要)"
  fi

  # 3. Hub AI bundle 展開 (do_enroll と同じ 2 段チェック: CLAUDE.md マーカー +
  #    scripts/manifest.json)。CLAUDE.md は書き込まれたが manifest.json が落ちて
  #    いない半完了状態も検知する。
  if ! _hub_bundle_incomplete; then
    local mver
    mver="$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$HOME/.claude/scripts/manifest.json" 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)"$/\1/')"
    color_ok "Hub AI bundle 展開済み (~/.claude/CLAUDE.md + scripts/manifest.json ${mver:+v$mver})"
  else
    if [[ ! -f "$HOME/.claude/CLAUDE.md" ]] || ! grep -q "BEGIN HUB-AI-RULES" "$HOME/.claude/CLAUDE.md" 2>/dev/null; then
      color_warn "Hub AI bundle が ~/.claude に展開されていません (CLAUDE.md マーカー欠落)"
    else
      color_warn "Hub AI bundle が半完了状態 (~/.claude/scripts/manifest.json 欠落)"
    fi
    color_warn "  手動再同期: hub-agent sync-bundle"
    errors=$((errors + 1))
  fi

  # 4. OS サービス起動 (Darwin 限定の確認。Linux は systemctl --user で別途)
  if [[ "$(uname)" == "Darwin" ]]; then
    if launchctl list 2>/dev/null | grep -q "co.cocorograph.hub-agent"; then
      color_ok "launchd service 起動中 (co.cocorograph.hub-agent)"
    else
      color_warn "launchd service が見つかりません"
      color_warn "  手動起動: hub-agent install-service"
      errors=$((errors + 1))
    fi
  fi

  if (( errors > 0 )); then
    color_warn ""
    color_warn "${errors} 件の不整合を検出。Hub UI で agent が online でない場合は以下を試してください:"
    color_warn "  hub-agent restart           # サービス再起動"
    color_warn "  hub-agent sync-bundle       # bundle 再配信"
    color_warn "  tail -20 ~/.hub/agent.log   # 詳細ログ"
  fi
}

macos_perm_guidance() {
  [[ "$(uname)" != "Darwin" ]] && return 0
  # 現在 PATH 上の node 実体パスを案内に埋め込む（フルディスクアクセス登録時に役立つ）
  local node_path
  node_path="$(command -v node 2>/dev/null || echo '/path/to/node')"
  cat <<EOF

📣 macOS の権限ダイアログについて
  hub-agent は tmux/pty を中継するため、初回起動時に macOS から
  「node がローカルネットワーク上の機器を検出することを求めています」
  「node がアクセシビリティを制御することを求めています」
  「node がほかのアプリからのデータへのアクセス権を求めています」
  などのダイアログが出る場合があります。すべて「許可」を選んでください。
  許可は「システム設定 > プライバシーとセキュリティ」から後で変更できます。

  ▶ ダイアログが頻繁に出る場合（特に Claude Code 経由で様々な cwd を使う場合）:
    「フルディスクアクセス」に node 本体を追加すると静かになります。
      システム設定 > プライバシーとセキュリティ > フルディスクアクセス
      → ＋ ボタンで以下のパスを追加:
        ${node_path}
    （node バイナリパスが変わったら再登録が必要です）

EOF
}

main() {
  color_step "hub-agent ワンライナーセットアップを開始"
  STEP_TOTAL=11
  STEP_NUM=0

  step_header "Homebrew"
  ensure_brew
  step_header "tmux"
  ensure_pkg tmux tmux tmux
  step_header "Node.js (Active LTS)"
  ensure_node_version
  step_header "Build tools (native addons)"
  ensure_build_tools
  step_header "npm global prefix"
  ensure_npm_user_prefix
  # ensure_node_version 直後だと brew link でシェルの command hash がズレている
  # 場合があるため、prefix 設定後にまとめて TLS pre-flight check + 自動 fallback
  # を行う。失敗時はガイダンス付きで exit するので、後段の npm install で TLS
  # エラーを再度浴びる経路はカットされる。
  step_header "Node TLS pre-flight"
  ensure_node_tls_works
  step_header "hub-agent install"
  ensure_global_install
  step_header "Claude Code install"
  ensure_claude_code
  step_header "enroll + bundle 同期"
  do_enroll
  step_header "Hub AI bootstrap (CLAUDE.md / hooks 初期化)"
  do_bootstrap_hub_ai
  step_header "OS サービス化"
  do_install_service
  step_header "最終検証"
  verify_setup

  echo ""
  color_ok "セットアップ完了。Hub UI で online 表示を確認してください"
  echo "    https://hub.cocorograph.com/user/cockpit/agents"
  macos_perm_guidance
}

main "$@"
