#!/usr/bin/env sh
set -eu

base_url=${DOTFILES_SITE_URL:-"https://dotfiles.tabsp.com"}
dotman_bin=${DOTMAN_BIN:-"$HOME/.local/bin/dotman"}
dotfiles_dir=${DOTFILES_DIR:-"$HOME/.local/share/tabsp-dotfiles"}
state_home=${XDG_STATE_HOME:-"$HOME/.local/state"}
install_lock="$state_home/tabsp-dotfiles/install.lock"
yes=0
tmp_dir=""
stage="starting"

usage() {
  cat <<EOF
Usage: install [--yes]

Install or update dotman and the tabsp dotfiles bundle.

Options:
  --yes    Run bootstrap/deploy after dry-run succeeds.
  --help   Show this help.
EOF
}

while [ "$#" -gt 0 ]; do
  case "$1" in
    --yes | -y)
      yes=1
      ;;
    --help | -h)
      usage
      exit 0
      ;;
    *)
      printf 'error: unknown option: %s\n' "$1" >&2
      usage >&2
      exit 2
      ;;
  esac
  shift
done

# ── Guard: interactive mode reads confirmations from the controlling terminal.
if [ "$yes" -eq 0 ] && ! { [ -r /dev/tty ] && [ -w /dev/tty ]; }; then
  printf 'error: TTY required for interactive install.\n' >&2
  printf 'Use --yes for unattended mode.\n' >&2
  exit 1
fi

need_command() {
  if ! command -v "$1" >/dev/null 2>&1; then
    printf 'error: required command not found: %s\n' "$1" >&2
    exit 1
  fi
}

# ── ANSI helpers ──
_tty() { [ -t 1 ]; }
_c() { printf '\033[%sm' "$1"; }
if _tty; then
  BOLD="$(_c 1)"
  GREEN="$(_c '38;2;166;227;161')"
  YELLOW="$(_c '38;2;249;226;175')"
  RED="$(_c '38;2;243;139;168')"
  CYAN="$(_c '38;2;148;226;213')"
  DIM="$(_c 2)"
  NC="$(_c 0)"
else
  BOLD=''
  GREEN=''
  YELLOW=''
  RED=''
  CYAN=''
  DIM=''
  NC=''
fi

download_with_progress() {
  url=$1
  output=$2

  if ! _tty; then
    curl -fsSL "$url" -o "$output" || step_fail "download failed"
    return 0
  fi

  curl -fsSL -# -o "$output" "$url" 2>&1 || step_fail "download failed"
}

step_run() {
  title=$1; shift

  if _tty && [ "$yes" -eq 0 ]; then
    log="$tmp_dir/step.log"
    : >"$log"
    (sh -c "$*") >"$log" 2>&1 &
    pid=$!
    i=0
    while kill -0 "$pid" 2>/dev/null; do
      case $((i % 4)) in 0) c='◐' ;; 1) c='◓' ;; 2) c='◑' ;; 3) c='◒' ;; esac
      printf '\r\033[2K  %s %s%s%s' "${CYAN}${c}${NC}" "${DIM}" "$title" "${NC}"
      i=$((i + 1))
      sleep 0.12
    done
    set +e
    wait "$pid"
    rc=$?
    set -e
    if [ "$rc" -eq 0 ]; then
      printf '\r\033[2K  %s %s\n' "${GREEN}✓${NC}" "$title"
      return 0
    fi
    printf '\r\033[2K  %s %s\n' "${RED}✗${NC}" "$title" >&2
    cat "$log" >&2
    return "$rc"
  fi

  printf '  %s\n' "$title"
  sh -c "$*"
}

_spinner_pid=''
_sudo_keepalive_pid=''
_sudo_ready=0
shell_manual_action=0
shell_manual_path=''

stop_sudo_keepalive() {
  if [ -n "${_sudo_keepalive_pid:-}" ]; then
    kill "$_sudo_keepalive_pid" 2>/dev/null || true
    wait "$_sudo_keepalive_pid" 2>/dev/null || true
    _sudo_keepalive_pid=''
  fi
}

authenticate_sudo_for_shell() {
  if ! command -v sudo >/dev/null 2>&1; then
    return 0
  fi

  if [ "$yes" -eq 1 ]; then
    if sudo -n -v 2>/dev/null; then
      _sudo_ready=1
    fi
    return 0
  fi

  if sudo -n -v 2>/dev/null; then
    _sudo_ready=1
    printf '  %s sudo\n' "${GREEN}✓${NC}"
    return 0
  fi

  printf 'Shell setup needs sudo. Please authenticate now.\n'
  if sudo -v </dev/tty && sudo -n -v 2>/dev/null; then
    _sudo_ready=1
    printf '  %s sudo\n' "${GREEN}✓${NC}"
    (
      while true; do
        sleep 60
        sudo -n -v 2>/dev/null || exit 0
      done
    ) &
    _sudo_keepalive_pid=$!
  else
    printf 'Could not authenticate sudo now. Manual shell commands will be shown if needed.\n' >&2
  fi
}

step_start() {
  if _tty && [ "$yes" -eq 0 ]; then
    title=$1
    i=0
    (
      while true; do
        case $((i % 4)) in 0) c='◐' ;; 1) c='◓' ;; 2) c='◑' ;; 3) c='◒' ;; esac
        printf '\r\033[2K  %s %s%s%s' "${CYAN}${c}${NC}" "${DIM}" "$title" "${NC}"
        i=$((i + 1))
        sleep 0.12
      done
    ) &
    _spinner_pid=$!
  else
    printf '  %s\n' "$1"
  fi
}

step_ok() {
  if [ -n "${_spinner_pid:-}" ]; then
    kill "$_spinner_pid" 2>/dev/null || true
    wait "$_spinner_pid" 2>/dev/null || true
    _spinner_pid=''
  fi
  if _tty && [ "$yes" -eq 0 ]; then
    printf '\r\033[2K  %s %s\n' "${GREEN}✓${NC}" "$1"
  else
    printf '  ✓ %s\n' "$1"
  fi
}

step_done() {
  step_ok "$1"
}

step_fail() {
  if [ -n "${_spinner_pid:-}" ]; then
    kill "$_spinner_pid" 2>/dev/null || true
    wait "$_spinner_pid" 2>/dev/null || true
    _spinner_pid=''
  fi
  printf '\r\033[2K  %s %s\n' "${RED}✗${NC}" "$1" >&2
  exit 1
}

prompt() {
  if [ "$yes" -eq 1 ]; then
    return 0
  fi

  if [ -n "${_spinner_pid:-}" ]; then
    kill "$_spinner_pid" 2>/dev/null || true
    sleep 0.15
    kill -0 "$_spinner_pid" 2>/dev/null && kill -9 "$_spinner_pid" 2>/dev/null || true
    wait "$_spinner_pid" 2>/dev/null || true
    _spinner_pid=''
  fi
  printf '\r\033[2K'
  if _tty; then
    printf '%s%s%s [Y/n] ' "$BOLD" "$1" "$NC"
  else
    printf '%s [Y/n] ' "$1"
  fi
  answer=$(read_input)
  case "$answer" in n | N | no | NO) return 1 ;; *) return 0 ;; esac
}

_pick_prereqs_items=''

pick_prereqs_reset() {
  _pick_prereqs_items=''
}

pick_prereqs_add() {
  if [ -z "$_pick_prereqs_items" ]; then
    _pick_prereqs_items="$1"
  else
    _pick_prereqs_items="$_pick_prereqs_items · $1"
  fi
}

pick_prereqs_show() {
  if [ "$yes" -eq 1 ]; then
    return 0
  fi
  if [ -z "$_pick_prereqs_items" ]; then
    return 1
  fi

  if [ -n "${_spinner_pid:-}" ]; then
    kill "$_spinner_pid" 2>/dev/null || true
    sleep 0.15
    kill -0 "$_spinner_pid" 2>/dev/null && kill -9 "$_spinner_pid" 2>/dev/null || true
    wait "$_spinner_pid" 2>/dev/null || true
    _spinner_pid=''
  fi
  printf '\r\033[2K'

  printf '%s%s%s\n' "$YELLOW" "Missing: $_pick_prereqs_items" "$NC"
  if ! prompt "install all?"; then
    return 1
  fi
  return 0
}

# ── Lock ──
lockfile=""
LOCK_DIR=""
LOCK_STALE_SECS=600

acquire_lock() {
  lockfile=$1
  mkdir -p "$(dirname "$lockfile")"

  if command -v flock >/dev/null 2>&1; then
    exec 9>"$lockfile"
    flock 9 || step_fail "failed to acquire lock"
    return 0
  fi

  LOCK_DIR="${lockfile}.d"
  while ! mkdir "$LOCK_DIR" 2>/dev/null; do
    if ! [ -d "$LOCK_DIR" ]; then
      continue
    fi
    pid=$(cat "$LOCK_DIR/pid" 2>/dev/null || true)
    started=$(cat "$LOCK_DIR/started_at" 2>/dev/null || true)
    now=$(date +%s 2>/dev/null || printf '0')
    if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
      sleep 1
      continue
    fi
    if [ "$now" -gt 0 ] && [ "$started" -gt 0 ] && [ $((now - started)) -ge "$LOCK_STALE_SECS" ]; then
      rm -rf "$LOCK_DIR"
      continue
    fi
    sleep 1
  done
  printf '%s\n' "$$" >"$LOCK_DIR/pid"
  date +%s >"$LOCK_DIR/started_at" 2>/dev/null || true
}

release_lock() {
  if [ -n "$LOCK_DIR" ] && [ -d "$LOCK_DIR" ]; then
    rm -rf "$LOCK_DIR"
  fi
  if [ -n "$lockfile" ]; then
    exec 9>&- 2>/dev/null || true
  fi
}

need_command mktemp
need_command awk

read_input() {
  if [ -r /dev/tty ]; then
    ( IFS= read -r line </dev/tty && printf '%s' "$line" ) 2>/dev/null || true
  else
    return 0
  fi
}

json_string() {
  key=$1
  sed -n "s/.*\"$key\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" "$manifest" | head -n 1
}

sha256_file() {
  file=$1
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$file" | awk '{print $1}'
  else
    shasum -a 256 "$file" | awk '{print $1}'
  fi
}

sha256_sum_file_value() {
  file=$1
  awk 'NF {print $1; exit}' "$file"
}

detect_target() {
  os=$(uname -s)
  arch=$(uname -m)

  case "$os:$arch" in
    Darwin:arm64) printf 'aarch64-apple-darwin' ;;
    Darwin:x86_64) printf 'x86_64-apple-darwin' ;;
    Linux:x86_64) printf 'x86_64-unknown-linux-gnu' ;;
    Linux:aarch64 | Linux:arm64) printf 'aarch64-unknown-linux-gnu' ;;
    *)
      printf 'error: unsupported platform: %s %s\n' "$os" "$arch" >&2
      exit 1
      ;;
  esac
}

printf '%s%s%s\n' "$BOLD" "tabsp dotfiles" "$NC"

bundle_next="$dotfiles_dir.next"
bundle_previous="$dotfiles_dir.previous"
source_checkout=0

looks_like_source_checkout() {
  path=$1

  [ -d "$path" ] || return 1
  [ -e "$path/.git" ] || return 1
  [ -f "$path/dotman.yaml" ] || return 1
  [ -f "$path/scripts/install" ] || return 1
}

detect_install_mode() {
  if looks_like_source_checkout "$dotfiles_dir"; then
    source_checkout=1
    printf 'Detected source checkout at %s; skipping published bundle download.\n' "$dotfiles_dir"
    return 0
  fi

  if looks_like_source_checkout "$bundle_previous"; then
    cat >&2 <<EOF
error: refusing to remove source checkout backup: $bundle_previous

Move or rename that directory before running the installer again.
EOF
    exit 1
  fi
}

install_dotman_from_source() {
  if ! command -v cargo >/dev/null 2>&1; then
    cat >&2 <<EOF
error: source checkout install requires cargo

Install Rust/Cargo, then run the installer again.
EOF
    exit 1
  fi

  stage="building dotman from source"
  mkdir -p "$(dirname -- "$dotman_bin")"
  build_log="$tmp_dir/dotman-build.log"
  if ! (
    cd "$dotfiles_dir"
    cargo build --release --locked
  ) >"$build_log" 2>&1; then
    cat "$build_log" >&2
    exit 1
  fi
  cp "$dotfiles_dir/target/release/dotman" "$dotman_bin"
  chmod 755 "$dotman_bin"
}

detect_install_mode

cleanup() {
  status=$?
  if [ -n "${_spinner_pid:-}" ]; then
    kill "$_spinner_pid" 2>/dev/null || true
    _spinner_pid=''
  fi
  stop_sudo_keepalive
  release_lock

  if [ -n "$tmp_dir" ]; then
    rm -rf "$tmp_dir"
  fi

  if [ "$status" -eq 130 ] || [ "$status" -eq 143 ]; then
    rm -rf "$bundle_next"
    if [ ! -d "$dotfiles_dir" ] && [ -d "$bundle_previous" ]; then
      mv "$bundle_previous" "$dotfiles_dir"
    fi
    printf '\nInstall interrupted during: %s\n' "$stage" >&2
    printf 'Cleaned temporary files. Existing dotfiles were left in place when possible.\n' >&2
    if [ -d "$bundle_previous" ]; then
      printf 'Previous bundle backup is available at: %s\n' "$bundle_previous" >&2
    fi
    printf 'Run the installer again to resume.\n' >&2
  fi
}

interrupt() {
  trap - INT TERM
  exit 130
}

trap cleanup EXIT
trap interrupt INT TERM

acquire_lock "$install_lock"

ensure_brew() {
  step_run "Installing Homebrew..." \
    'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'

  os=$(uname -s)
  if [ "$os" = "Darwin" ]; then
    if [ -x /opt/homebrew/bin/brew ]; then
      eval "$(/opt/homebrew/bin/brew shellenv)"
    elif [ -x /usr/local/bin/brew ]; then
      eval "$(/usr/local/bin/brew shellenv)"
    fi
  elif [ "$os" = "Linux" ]; then
    if [ -x /home/linuxbrew/.linuxbrew/bin/brew ]; then
      eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
    elif [ -x "$HOME/.linuxbrew/bin/brew" ]; then
      eval "$("$HOME/.linuxbrew/bin/brew" shellenv)"
    fi
  fi

  if ! command -v brew >/dev/null 2>&1; then
    printf 'error: Homebrew installation completed but brew is still not in PATH\n' >&2
    exit 1
  fi
}

ensure_fish() {
  if ! command -v brew >/dev/null 2>&1; then
    printf 'Skipping fish: Homebrew is not available.\n'
    return 0
  fi

  step_run "Installing fish..." \
    "brew install fish"

  if ! command -v fish >/dev/null 2>&1; then
    printf 'error: fish installation completed but fish is still not in PATH\n' >&2
    exit 1
  fi
}

ensure_shell_registered() {
  shell_path=$1

  if [ -r /etc/shells ] && grep -Fx "$shell_path" /etc/shells >/dev/null 2>&1; then
    return 0
  fi

  if [ -w /etc/shells ]; then
    printf '%s\n' "$shell_path" >>/etc/shells
  elif command -v sudo >/dev/null 2>&1 && [ "$_sudo_ready" -eq 1 ]; then
    sudo -n sh -c 'grep -Fx "$1" /etc/shells >/dev/null 2>&1 || printf "%s\n" "$1" >>/etc/shells' sh "$shell_path" 2>/dev/null
  else
    printf 'Could not update /etc/shells. Run this later:\n' >&2
    printf '  grep -Fx %s /etc/shells || printf "%%s\\n" %s | sudo tee -a /etc/shells\n' "$shell_path" "$shell_path" >&2
    return 1
  fi
}

current_login_shell() {
  current_shell=$(getent passwd "$(id -un)" 2>/dev/null | cut -d: -f7)
  if [ -z "$current_shell" ]; then
    current_shell=$(dscl . -read ~/ UserShell 2>/dev/null | awk '{print $NF}' || printf '')
  fi
  if [ -z "$current_shell" ]; then
    current_shell=${SHELL:-}
  fi

  printf '%s' "$current_shell"
}

login_shell_is() {
  [ "$(current_login_shell)" = "$1" ]
}

change_login_shell() {
  shell_path=$1
  user_name=$(id -un)

  if [ "$yes" -eq 1 ]; then
    if chsh -s "$shell_path" </dev/null 2>/dev/null && login_shell_is "$shell_path"; then
      return 0
    fi

    if command -v sudo >/dev/null 2>&1; then
      sudo -n chsh -s "$shell_path" "$user_name" 2>/dev/null || true
      login_shell_is "$shell_path"
      return $?
    fi

    return 1
  fi

  if login_shell_is "$shell_path"; then
    return 0
  fi

  case "$(uname -s)" in
    Darwin)
      chsh -s "$shell_path" </dev/null 2>/dev/null || true
      ;;
    *)
      if command -v sudo >/dev/null 2>&1 && [ "$_sudo_ready" -eq 1 ]; then
        sudo -n chsh -s "$shell_path" "$user_name" 2>/dev/null || true
      else
        chsh -s "$shell_path" </dev/null 2>/dev/null || true
      fi
      ;;
  esac

  if login_shell_is "$shell_path"; then
    return 0
  fi

  if [ "$(uname -s)" = "Darwin" ] && command -v sudo >/dev/null 2>&1 && [ "$_sudo_ready" -eq 1 ]; then
    sudo -n chsh -s "$shell_path" "$user_name" 2>/dev/null || true
    login_shell_is "$shell_path"
    return $?
  fi

  return 1
}

ensure_fish_login() {
  if ! command -v fish >/dev/null 2>&1; then
    return 0
  fi

  fish_path=$(command -v fish)

  if login_shell_is "$fish_path"; then
    return 0
  fi

  ensure_shell_registered "$fish_path" || return 0
  change_login_shell "$fish_path"
}

# ── install_dotman_and_bundle ──
install_dotman_and_bundle() {
  need_command curl
  need_command tar
  need_command sed

  stage="reading manifest"
  target=$(detect_target)
  bundle_url=$(json_string bundle_url)
  bundle_sha256=$(json_string bundle_sha256)
  dotman_version=$(json_string dotman_version)
  release_base_url=$(json_string dotman_release_base_url)
  asset_template=$(json_string dotman_asset_template)
  asset_sha256_template=$(json_string dotman_asset_sha256_template)

  if [ -z "$bundle_url" ] || [ -z "$release_base_url" ] || [ -z "$asset_template" ]; then
    printf 'error: manifest is missing required fields\n' >&2
    exit 1
  fi

  asset_name=$(printf '%s' "$asset_template" | sed "s/{target}/$target/g")
  dotman_url="$release_base_url/$asset_name"
  dotman_sha256_url=""
  if [ -n "$asset_sha256_template" ]; then
    asset_sha256_name=$(printf '%s' "$asset_sha256_template" | sed "s/{target}/$target/g")
    dotman_sha256_url="$release_base_url/$asset_sha256_name"
  fi

  installed_version=""
  if [ -x "$dotman_bin" ]; then
    installed_version=$("$dotman_bin" --version 2>/dev/null | awk '{print $2}' || true)
  fi

  mkdir -p "$(dirname -- "$dotman_bin")"

  if [ -z "$installed_version" ] || [ "$installed_version" != "$dotman_version" ]; then
    stage="installing dotman"
    dotman_archive="$tmp_dir/$asset_name"
    dotman_extract_dir="$tmp_dir/dotman"
    mkdir -p "$dotman_extract_dir"
    download_with_progress "$dotman_url" "$dotman_archive"
    if [ -n "$dotman_sha256_url" ]; then
      stage="verifying dotman"
      dotman_sha256_file="$tmp_dir/$asset_name.sha256"
      download_with_progress "$dotman_sha256_url" "$dotman_sha256_file"
      expected_dotman_sha256=$(sha256_sum_file_value "$dotman_sha256_file")
      actual_dotman_sha256=$(sha256_file "$dotman_archive")
      if [ "$actual_dotman_sha256" != "$expected_dotman_sha256" ]; then
        printf 'error: dotman checksum mismatch\n' >&2
        printf 'expected: %s\nactual:   %s\n' "$expected_dotman_sha256" "$actual_dotman_sha256" >&2
        printf 'asset:    %s\n' "$dotman_url" >&2
        exit 1
      fi
    fi
    tar -xzf "$dotman_archive" -C "$dotman_extract_dir"
    cp "$dotman_extract_dir/dotman" "$dotman_bin"
    chmod 755 "$dotman_bin"
    installed_version=$("$dotman_bin" --version 2>/dev/null | awk '{print $2}' || true)
    if [ "$installed_version" != "$dotman_version" ]; then
      printf 'error: installed dotman version mismatch\n' >&2
      printf 'expected: %s\nactual:   %s\n' "$dotman_version" "${installed_version:-unknown}" >&2
      printf 'asset:    %s\n' "$dotman_url" >&2
      exit 1
    fi
  fi

  active_dotman=$(command -v dotman 2>/dev/null || true)
  if [ -n "$active_dotman" ] && [ "$active_dotman" != "$dotman_bin" ]; then
    active_version=$("$active_dotman" --version 2>/dev/null | awk '{print $2}' || true)
    printf '%s%s%s\n' "$YELLOW" "dotman on PATH resolves to $active_dotman${active_version:+ ($active_version)}, not $dotman_bin." "$NC"
    printf '%s%s%s\n' "$YELLOW" "Put $HOME/.local/bin before other dotman installs in PATH, or remove the older dotman." "$NC"
  fi

  bundle_archive="$tmp_dir/dotfiles-bundle.tar.gz"

  stage="downloading dotfiles bundle"
  download_with_progress "$bundle_url" "$bundle_archive"
  if [ -n "$bundle_sha256" ]; then
    stage="verifying dotfiles bundle"
    actual_sha256=$(sha256_file "$bundle_archive")
    if [ "$actual_sha256" != "$bundle_sha256" ]; then
      printf 'error: bundle checksum mismatch\n' >&2
      printf 'expected: %s\nactual:   %s\n' "$bundle_sha256" "$actual_sha256" >&2
      exit 1
    fi
  fi

  stage="extracting dotfiles bundle"
  rm -rf "$bundle_next" && mkdir -p "$bundle_next" && tar -xzf "$bundle_archive" -C "$bundle_next"

  stage="installing dotfiles bundle"
  rm -rf "$bundle_previous"
  if [ -d "$dotfiles_dir" ]; then
    mv "$dotfiles_dir" "$bundle_previous"
  fi
  mv "$bundle_next" "$dotfiles_dir"
}

# ── install_prerequisites ──
install_prerequisites() {
  pick_prereqs_reset
  [ "$need_brew" -eq 1 ] && pick_prereqs_add "brew"
  [ "$need_fish" -eq 1 ] && pick_prereqs_add "fish"
  # shell setup is implicit when fish is newly installed
  need_shell=0
  if [ "$need_fish" -eq 1 ] || [ "$need_shell_reg" -eq 1 ] || [ "$need_chsh" -eq 1 ]; then
    need_shell=1
    pick_prereqs_add "shell → fish"
  fi

  pick_prereqs_show || return 0

  [ "$need_brew" -eq 1 ] && ensure_brew
  [ "$need_fish" -eq 1 ] && ensure_fish

  # re-detect after fish install
  if [ "$need_fish" -eq 1 ] && command -v fish >/dev/null 2>&1; then
    fish_path=$(command -v fish)
    need_shell_reg=0 need_chsh=0 need_shell=0
    if [ -r /etc/shells ] && ! grep -Fx "$fish_path" /etc/shells >/dev/null 2>&1; then need_shell_reg=1; fi
    if ! login_shell_is "$fish_path"; then need_chsh=1; fi
    if [ "$need_shell_reg" -eq 1 ] || [ "$need_chsh" -eq 1 ]; then need_shell=1; fi
  fi

  if [ "$need_shell" -eq 1 ]; then
    if ensure_shell_registered "$fish_path" && ensure_fish_login; then
      printf '  %s shell → fish\n' "${GREEN}✓${NC}"
    else
      shell_manual_action=1
      shell_manual_path=$fish_path
      printf '  %s shell → fish deferred\n' "${YELLOW}!${NC}" >&2
    fi
  fi
}

print_complete() {
  echo
  printf '%s\n' "tabsp dotfiles installed"
  if [ "$shell_manual_action" -eq 1 ]; then
    printf '  Shell: %smanual setup required%s\n' "$YELLOW" "$NC"
    printf '    grep -Fx %s /etc/shells || printf "%%s\\n" %s | sudo tee -a /etc/shells\n' "$shell_manual_path" "$shell_manual_path"
    printf '    sudo chsh -s %s %s\n' "$shell_manual_path" "$(id -un)"
  fi
  if command -v fish >/dev/null 2>&1; then
    printf '  Run: %sexec fish -l%s\n' "$DIM" "$NC"
  fi
  printf '  Manage: %sdotman%s deploy | bootstrap\n' "$DIM" "$NC"
}

# ── Main ──
need_brew=0 need_fish=0 need_shell_reg=0 need_chsh=0
if ! command -v brew >/dev/null 2>&1; then need_brew=1; fi
if ! command -v fish >/dev/null 2>&1; then need_fish=1; fi
if command -v fish >/dev/null 2>&1; then
  fish_path=$(command -v fish)
  if [ -r /etc/shells ] && ! grep -Fx "$fish_path" /etc/shells >/dev/null 2>&1; then need_shell_reg=1; fi
  if ! login_shell_is "$fish_path"; then need_chsh=1; fi
fi

if [ "$need_fish" -eq 1 ] || [ "$need_shell_reg" -eq 1 ] || [ "$need_chsh" -eq 1 ]; then
  authenticate_sudo_for_shell
fi

stage="creating temporary workspace"
tmp_dir=$(mktemp -d)

manifest="$tmp_dir/manifest.json"
if [ "$source_checkout" -eq 0 ]; then
  stage="downloading manifest"
  download_with_progress "$base_url/manifest.json" "$manifest"
fi

# ── Step 1: Install dotman & dotfiles ──
step_start "Installing dotman & dotfiles..."
if [ "$source_checkout" -eq 1 ]; then
  install_dotman_from_source
  step_done "dotman (source) · dotfiles (checkout)"
else
  install_dotman_and_bundle
  step_done "dotman $installed_version · bundle"
fi

# ── Prerequisites (silent when satisfied) ──
if [ "$need_brew" -eq 1 ] || [ "$need_fish" -eq 1 ] || [ "$need_shell_reg" -eq 1 ] || [ "$need_chsh" -eq 1 ]; then
  install_prerequisites
fi

# ── Step 2: Apply dotfiles ──
if ! prompt "Apply dotfiles?"; then
  print_complete
  exit 0
fi

step_start "Applying..."
output=$(
  cd "$dotfiles_dir"
  "$dotman_bin" bootstrap --summary </dev/null
  "$dotman_bin" deploy --summary </dev/null
) || step_fail "dotman bootstrap/deploy failed"
links=$(echo "$output" | awk -F': ' '/^link:/ {sum += $2+0} END {print sum}')
dirs=$(echo "$output" | awk -F': ' '/^create:/ {sum += $2+0} END {print sum}')
shell=$(echo "$output" | awk -F': ' '/^shell:/ {sum += $2+0} END {print sum}')
step_done "${links} links · ${dirs} dirs · ${shell} commands deployed"

# ── Step 3: Complete ──
print_complete
