#!/bin/bash
# Officer — host dependency setup
# Run once on a fresh Ubuntu/Debian host before launching the server.
#
# Usage:
#   bash scripts/setup/setup.sh                          # full server install
#   OFFICER_PROFILE=light bash scripts/setup/setup.sh    # light install
#
# PROFILES
#   full   Everything: the self-hosted estate, the remote desktop, the music/audio stack, the shell
#          and editor tooling. What a dedicated Officer server wants.
#   light  The same process set as the macOS build — the file browser, the terminal, and
#          Claude/opencode chat — on a Linux host. Installs only what those need: node, bun, ffmpeg,
#          Postgres, pm2 and the two agent CLIs, then starts ecosystem.light.config.cjs.
#
#          Skipped by `light`: archive extras, the sudoers entry and auto-suspend disabling, and Go.
#          Of the Docker services only Postgres is brought up.
#
#          The app itself is identical — every API route stays mounted, so the features whose sidecars
#          are not running report themselves unavailable rather than disappearing. A profile changes
#          which processes start, not which code ships.
#
# NOT INSTALLED HERE — and the gaps in the section numbers are where these used to be
#   Moved to scripts/setup/setup-sidecars.sh, which nothing below invokes; run it deliberately, and
#   only after this script:  8 Rust, 9 PulseAudio, 10 cliamp, 13 yt-dlp, 17 remote desktop.
#
#   Removed outright, because the host provisioning already installs them and two installers racing
#   for the same binaries is worse than one:  11 Neovim, 12 shell extras (oh-my-zsh/eza/lazygit),
#   14 npm globals (the ~/.local npm prefix, Claude Code, pm2).
#
#   That makes node, npm, pm2 and the agent CLIs PREREQUISITES of this script rather than products of
#   it. Section 19 warns and skips rather than failing if pm2 is absent, so a host that never ran the
#   provisioning will finish "successfully" with nothing listening — check the verification block.

set -e

GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

ok()   { echo -e "  ${GREEN}✓${NC} $1"; }
warn() { echo -e "  ${YELLOW}!${NC} $1"; }
fail() { echo -e "  ${RED}✗${NC} $1"; }
skip() { echo -e "  - $1 (already installed)"; }
omit() { echo -e "  - $1 (skipped: light profile)"; }

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

OFFICER_PROFILE="${OFFICER_PROFILE:-full}"
case "$OFFICER_PROFILE" in
  full|light) ;;
  *) echo "Unknown OFFICER_PROFILE '$OFFICER_PROFILE' — expected 'full' or 'light'." >&2; exit 2 ;;
esac
is_light() { [ "$OFFICER_PROFILE" = "light" ]; }

# Which pm2 process list this install starts and verifies. ecosystem.light.config.cjs derives its apps
# from ecosystem.config.cjs, so the two cannot disagree about how a process is launched.
if is_light; then ECOSYSTEM_FILE="ecosystem.light.config.cjs"; else ECOSYSTEM_FILE="ecosystem.config.cjs"; fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ../.. — this lives in scripts/setup/, so the repo root is two levels up, not one. Nothing here fails
# loudly if that is wrong: PROJECT_DIR is where .env is written, where `bun install` and `db:push` run and
# where pm2 is pointed, so an off-by-one level silently sets up scripts/ instead of the repo.
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"

# Resolve the real user's home even when running under sudo
if [[ -n "${SUDO_USER:-}" ]]; then
  REAL_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
else
  REAL_HOME="$HOME"
fi

# ─── detect package manager ────────────────────────────────────────────────────
if has apt; then
  PM=apt
elif has pacman; then
  PM=pacman
elif has brew; then
  PM=brew
else
  fail "No supported package manager found (apt, pacman, brew)"
  exit 1
fi

install_pkg() {
  case $PM in
    apt)    sudo apt install -y "$@" ;;
    pacman) sudo pacman -S --noconfirm "$@" ;;
    brew)   brew install "$@" ;;
  esac
}

echo ""
echo "═══════════════════════════════════════════"
echo "  Officer — dependency setup ($PM)"
echo "═══════════════════════════════════════════"

# ─── 1. core system packages ───────────────────────────────────────────────────
echo ""
echo "── Core system packages ──"

CORE_PKGS=()

# git
if has git; then skip "git"; else CORE_PKGS+=(git); fi

# zip / unzip
if has zip; then skip "zip"; else CORE_PKGS+=(zip); fi
if has unzip; then skip "unzip"; else CORE_PKGS+=(unzip); fi

# curl / wget
if has curl; then skip "curl"; else CORE_PKGS+=(curl); fi
if has wget; then skip "wget"; else CORE_PKGS+=(wget); fi

# zsh
if has zsh; then skip "zsh"; else CORE_PKGS+=(zsh); fi

# psmisc (fuser) and procps (pgrep)
if has fuser; then skip "fuser (psmisc)"; else
  case $PM in
    apt|pacman) CORE_PKGS+=(psmisc) ;;
    brew) skip "fuser (not needed on macOS)" ;;
  esac
fi
if has pgrep; then skip "pgrep (procps)"; else
  case $PM in
    apt)    CORE_PKGS+=(procps) ;;
    pacman) CORE_PKGS+=(procps-ng) ;;
    brew)   skip "pgrep (built-in on macOS)" ;;
  esac
fi

# script (bsdutils on apt, util-linux on pacman, built-in on macOS)
if has script; then skip "script (bsdutils)"; else
  case $PM in
    apt)    CORE_PKGS+=(bsdutils) ;;
    pacman) CORE_PKGS+=(util-linux) ;;
    brew)   skip "script (built-in on macOS)" ;;
  esac
fi

# build tools (make, gcc, g++) — needed for native npm modules like node-pty
if has make && has gcc; then skip "build tools (make, gcc, g++)"; else
  case $PM in
    apt)    CORE_PKGS+=(build-essential) ;;
    pacman) CORE_PKGS+=(base-devel) ;;
    brew)   warn "Install Xcode command line tools: xcode-select --install" ;;
  esac
fi

# pkg-config — needed by cgo-based Go packages (e.g. ebitengine/oto for cliamp, in setup-sidecars.sh)
if has pkg-config; then skip "pkg-config"; else
  case $PM in
    apt)    CORE_PKGS+=(pkg-config) ;;
    pacman) CORE_PKGS+=(pkgconf) ;;
    brew)   CORE_PKGS+=(pkg-config) ;;
  esac
fi

# python3 + pip + venv
if has python3; then skip "python3"; else
  case $PM in
    apt)    CORE_PKGS+=(python3 python3-pip python3-venv) ;;
    pacman) CORE_PKGS+=(python python-pip) ;;
    brew)   CORE_PKGS+=(python3) ;;
  esac
fi
# ensure pip/venv even if python3 already exists (apt splits them)
if has python3 && [ "$PM" = "apt" ]; then
  if ! dpkg -s python3-pip &>/dev/null 2>&1; then CORE_PKGS+=(python3-pip); fi
  if ! dpkg -s python3-venv &>/dev/null 2>&1; then CORE_PKGS+=(python3-venv); fi
fi

# shell utilities
for tool in tree btop tmux jq htop lsof duf; do
  if has "$tool"; then skip "$tool"; else CORE_PKGS+=("$tool"); fi
done

# sqlite3
if has sqlite3; then skip "sqlite3"; else
  case $PM in
    apt)    CORE_PKGS+=(sqlite3) ;;
    pacman) CORE_PKGS+=(sqlite) ;;
    brew)   CORE_PKGS+=(sqlite) ;;
  esac
fi

# isync (provides mbsync for Gmail IMAP sync)
if has mbsync; then skip "isync (mbsync)"; else
  case $PM in
    apt)    CORE_PKGS+=(isync) ;;
    pacman) CORE_PKGS+=(isync) ;;
    brew)   CORE_PKGS+=(isync) ;;
  esac
fi

# ripgrep
if has rg; then skip "ripgrep"; else
  case $PM in
    apt)    CORE_PKGS+=(ripgrep) ;;
    pacman) CORE_PKGS+=(ripgrep) ;;
    brew)   CORE_PKGS+=(ripgrep) ;;
  esac
fi

# fd-find
if has fd || has fdfind; then skip "fd-find"; else
  case $PM in
    apt)    CORE_PKGS+=(fd-find) ;;
    pacman) CORE_PKGS+=(fd) ;;
    brew)   CORE_PKGS+=(fd) ;;
  esac
fi

# net-tools, less, file, man-db
case $PM in
  apt)
    for pkg in net-tools less file man-db; do
      if dpkg -s "$pkg" &>/dev/null 2>&1; then skip "$pkg"; else CORE_PKGS+=("$pkg"); fi
    done
    ;;
  pacman)
    for pkg in net-tools less file man-db; do
      if pacman -Qi "$pkg" &>/dev/null 2>&1; then skip "$pkg"; else CORE_PKGS+=("$pkg"); fi
    done
    ;;
  brew)
    skip "net-tools, less, file, man (built-in on macOS)"
    ;;
esac

# locales
case $PM in
  apt)
    if dpkg -s locales &>/dev/null 2>&1; then skip "locales"; else CORE_PKGS+=(locales); fi
    ;;
esac

# ca-certificates
case $PM in
  apt)
    if dpkg -s ca-certificates &>/dev/null 2>&1; then skip "ca-certificates"; else CORE_PKGS+=(ca-certificates); fi
    ;;
esac

# uidmap — newuidmap/newgidmap, needed for a member's own rootless Docker.
#
# Rootless containers map subordinate uid ranges, and those two setuid helpers are the only way to do it
# unprivileged. Without them `dockerd-rootless-setuptool.sh` fails at the first step. Core rather than a
# profile extra for the same reason acl is: the alternative to a member having their own daemon is adding
# them to the `docker` group, which is root on the host — see src/servers/os-user-docker.ts.
case $PM in
  apt)
    if dpkg -s uidmap &>/dev/null 2>&1; then skip "uidmap"; else CORE_PKGS+=(uidmap); fi
    if dpkg -s dbus-user-session &>/dev/null 2>&1; then skip "dbus-user-session"; else CORE_PKGS+=(dbus-user-session); fi
    ;;
  pacman)
    if has newuidmap; then skip "uidmap (shadow)"; else CORE_PKGS+=(shadow); fi
    ;;
esac

# acl — setfacl/getfacl, needed by per-user Linux accounts.
#
# A member's home is 700 and owned by them, which is right for a shell and locks the platform out of the
# file browser. Named ACL entries are what let both act on the same files without opening the home to every
# account on the box; mode bits cannot express it in both directions. Core rather than a profile extra
# because the alternative is an account that provisions and then cannot list its own home.
case $PM in
  apt)
    if dpkg -s acl &>/dev/null 2>&1; then skip "acl"; else CORE_PKGS+=(acl); fi
    ;;
  pacman|dnf|yum)
    if has setfacl; then skip "acl"; else CORE_PKGS+=(acl); fi
    ;;
esac

if [ ${#CORE_PKGS[@]} -gt 0 ]; then
  install_pkg "${CORE_PKGS[@]}"
  ok "Installed: ${CORE_PKGS[*]}"
fi

# locale generation (ensure en_US.UTF-8)
case $PM in
  apt)
    if ! locale -a 2>/dev/null | grep -q "en_US.utf8"; then
      sudo sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen
      sudo locale-gen
      ok "Generated en_US.UTF-8 locale"
    else
      skip "en_US.UTF-8 locale"
    fi
    ;;
esac

# symlink fdfind → fd (apt installs as fdfind)
if has fdfind && ! has fd; then
  sudo ln -sf "$(command -v fdfind)" /usr/local/bin/fd
  ok "Symlinked fdfind → fd"
fi

# ─── 2. archive extras (optional but useful) ──────────────────────────────────
echo ""
echo "── Archive utilities (optional) ──"

if is_light; then
  omit "7z, unrar"
else

ARCHIVE_PKGS=()

# p7zip
if has 7z; then skip "7z (p7zip)"; else
  case $PM in
    apt)    ARCHIVE_PKGS+=(p7zip-full) ;;
    pacman) ARCHIVE_PKGS+=(p7zip) ;;
    brew)   ARCHIVE_PKGS+=(p7zip) ;;
  esac
fi

# unrar
if has unrar; then skip "unrar"; else
  case $PM in
    apt)    ARCHIVE_PKGS+=(unrar) ;;
    pacman) ARCHIVE_PKGS+=(unrar) ;;
    brew)   ARCHIVE_PKGS+=(unrar) ;;
  esac
fi

if [ ${#ARCHIVE_PKGS[@]} -gt 0 ]; then
  install_pkg "${ARCHIVE_PKGS[@]}" || warn "Some archive packages may need non-free repos"
  ok "Installed: ${ARCHIVE_PKGS[*]}"
fi
fi

# ─── 3. ffmpeg ─────────────────────────────────────────────────────────────────
echo ""
echo "── FFmpeg ──"

if has ffmpeg; then
  skip "ffmpeg ($(ffmpeg -version 2>&1 | head -1 | awk '{print $3}'))"
else
  install_pkg ffmpeg
  ok "ffmpeg installed"
fi

# ─── 4. sudoers for officer service user ──────────────────────────────────────
echo ""
echo "── Sudoers (Linux user isolation) ──"

# Both of these are server decisions. A passwordless sudoers entry is a security posture a small
# install should opt into deliberately, and a laptop-shaped host wants to keep suspending — the macOS
# build does neither, so `light` does neither.
if is_light; then
  omit "sudoers entry, auto-suspend disabling"
else

SERVICE_USER="$(whoami)"
SUDOERS_FILE="/etc/sudoers.d/officer-service"

# Match the actual rule, not just the username appearing somewhere in the file — a comment mentioning
# the user would otherwise read as "configured".
if [ -f "$SUDOERS_FILE" ] && grep -qE "^${SERVICE_USER}[[:space:]]+ALL=" "$SUDOERS_FILE" 2>/dev/null; then
  skip "sudoers entry for $SERVICE_USER"
else
  case $PM in
    apt|pacman)
      # Validate BEFORE this lands in /etc/sudoers.d. A malformed file there breaks sudo COMPLETELY,
      # and you cannot sudo to repair it — on a remote machine that is unrecoverable short of physical
      # access or a rescue boot. `visudo -c` is the standard gate and costs nothing.
      #
      # install(1) rather than tee+chmod: it writes the content and the 0440 mode in one step. tee
      # creates the file at the default umask first, and sudo refuses to read a sudoers file with
      # loose permissions, so that ordering leaves a window where sudo can reject its own config.
      SUDOERS_TMP="$(mktemp)"
      echo "$SERVICE_USER ALL=(ALL) NOPASSWD: ALL" > "$SUDOERS_TMP"
      if sudo visudo -c -f "$SUDOERS_TMP" >/dev/null 2>&1; then
        sudo install -m 0440 -o root -g root "$SUDOERS_TMP" "$SUDOERS_FILE"
        ok "Created sudoers entry for $SERVICE_USER at $SUDOERS_FILE"
      else
        fail "visudo rejected the sudoers entry for '$SERVICE_USER' — not installing it"
      fi
      rm -f "$SUDOERS_TMP"
      ;;
    brew)
      warn "Sudoers setup is Linux-only — skipping on macOS"
      ;;
  esac
fi

# ─── 4b. Disable auto-suspend (server doesn't need to sleep) ──────────────────
echo ""
echo "── Auto-suspend (disable for server) ──"

# Disable system suspend/hibernate
if systemctl is-enabled sleep.target 2>/dev/null | grep -q "masked"; then
  skip "sleep targets already masked"
else
  sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target 2>/dev/null
  ok "Masked sleep/suspend targets"
fi

# Configure logind to ignore idle — patch individual keys, don't overwrite the file
LOGIND_CHANGED=0
set_logind_key() {
  local key="$1" val="$2" file="/etc/systemd/logind.conf"
  # Already set (uncommented) to the desired value → nothing to do.
  if grep -qE "^${key}=${val}$" "$file" 2>/dev/null; then
    return
  fi
  if grep -qE "^${key}=" "$file" 2>/dev/null; then
    sudo sed -i "s|^${key}=.*|${key}=${val}|" "$file"
  elif grep -qE "^#${key}=" "$file" 2>/dev/null; then
    sudo sed -i "s|^#${key}=.*|${key}=${val}|" "$file"
  else
    echo "${key}=${val}" | sudo tee -a "$file" > /dev/null
  fi
  LOGIND_CHANGED=1
}

set_logind_key HandleLidSwitch ignore
set_logind_key HandleLidSwitchExternalPower ignore
set_logind_key HandlePowerKey ignore
set_logind_key IdleAction none
set_logind_key RuntimeDirectorySize 10%

# Only restart logind when something actually changed — a needless restart can disrupt live sessions.
if [ "$LOGIND_CHANGED" = "1" ]; then
  sudo systemctl restart systemd-logind
  ok "Configured logind to disable auto-suspend"
else
  skip "logind auto-suspend settings"
fi
fi

# ─── 5. Node.js 22 (system-wide) ─────────────────────────────────────────────
echo ""
echo "── Node.js 22 (system-wide) ──"

# Always check the canonical system path, not `which node` (which may resolve nvm).
SYSTEM_NODE="/usr/bin/node"

install_node22_apt() {
  echo "  Setting up NodeSource repository..."
  curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
  echo "  Installing nodejs..."
  sudo apt-get install -y nodejs
}

case $PM in
  apt)
    NEED_INSTALL=0
    if [ -f "$SYSTEM_NODE" ]; then
      SYS_MAJOR=$("$SYSTEM_NODE" -v 2>/dev/null | cut -d. -f1 | sed 's/^v//')
      if [ "$SYS_MAJOR" = "22" ]; then
        skip "node v$("$SYSTEM_NODE" -v) (system-wide at $SYSTEM_NODE)"
      else
        warn "System node v$("$SYSTEM_NODE" -v) at $SYSTEM_NODE is not v22 — upgrading..."
        NEED_INSTALL=1
      fi
    else
      if has node; then
        warn "node found at $(which node) (not system-wide, likely nvm) — installing Node 22 system-wide..."
      else
        echo "  Node.js not found — installing v22..."
      fi
      NEED_INSTALL=1
    fi
    if [ "$NEED_INSTALL" = "1" ]; then
      install_node22_apt
      if [ -f "$SYSTEM_NODE" ] && [ "$("$SYSTEM_NODE" -v 2>/dev/null | cut -d. -f1 | sed 's/^v//')" = "22" ]; then
        ok "node v$("$SYSTEM_NODE" -v) installed at $SYSTEM_NODE"
        if has node && [ "$(command -v node)" != "$SYSTEM_NODE" ]; then
          warn "Shell resolves 'node' to $(command -v node) — system node is at $SYSTEM_NODE"
          warn "nvm may shadow it in interactive shells; systemd services will use $SYSTEM_NODE"
        fi
      else
        fail "Node.js 22 install failed — $SYSTEM_NODE not found or wrong version"
        exit 1
      fi
    fi
    ;;
  pacman)
    if has node && [ "$(node -v 2>/dev/null | cut -d. -f1 | sed 's/^v//')" = "22" ]; then
      skip "node v$(node -v)"
    else
      install_pkg nodejs npm
      if has node; then ok "node v$(node -v) installed"; else fail "node install failed"; exit 1; fi
    fi
    ;;
  brew)
    if has node && [ "$(node -v 2>/dev/null | cut -d. -f1 | sed 's/^v//')" = "22" ]; then
      skip "node v$(node -v)"
    else
      install_pkg node
      if has node; then ok "node v$(node -v) installed"; else fail "node install failed"; exit 1; fi
    fi
    ;;
esac

# ─── 6. Bun ───────────────────────────────────────────────────────────────────
echo ""
echo "── Bun ──"

export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"

if [ -f "$BUN_INSTALL/bin/bun" ]; then
  skip "bun ($(bun --version 2>/dev/null))"
else
  curl -fsSL https://bun.sh/install | bash
  if [ ! -f "$BUN_INSTALL/bin/bun" ]; then fail "bun install failed"; exit 1; fi
  ok "bun $(bun --version) installed"
fi

# Symlink to system-wide path so all users and systemd services can access it
if [ ! -L /usr/local/bin/bun ] || [ "$(readlink /usr/local/bin/bun)" != "$BUN_INSTALL/bin/bun" ]; then
  sudo ln -sf "$BUN_INSTALL/bin/bun" /usr/local/bin/bun
  ok "bun symlinked to /usr/local/bin/bun"
else
  skip "bun symlink at /usr/local/bin/bun"
fi

# ─── 6b. Starship prompt ──────────────────────────────────────────────────────
#
# Outside the light-profile skip below, unlike the rest of the terminal tooling. The light profile exists to
# serve a file browser, a terminal and chat — the terminal is one of its three reasons to be, and it is also
# what every member gets when per-user Linux accounts are on. `src/servers/shell-skel/zshrc` deploys this same
# prompt to every account, so leaving starship out of light meant every member's shell fell back to the plain
# one on exactly the installs most likely to have members.
#
# One static binary and one config file, which is why it survived the cull that removed the rest of the
# terminal tooling: oh-my-zsh, eza and lazygit are host comforts the provisioning installs, and the shell
# template treats each as optional. Starship it does not — the prompt would visibly degrade.
echo ""
echo "── Prompt (starship) ──"

# Starship prompt
if has starship; then
  skip "starship"
else
  curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b /usr/local/bin
  if has starship; then ok "starship installed"; else warn "starship install failed"; fi
fi

# Deploy starship config. Unconditionally cp'ing here overwrote a customised ~/.config/starship.toml on
# every run, silently. Converge when there is nothing to lose, keep what the user wrote when there is.
mkdir -p "$HOME/.config"
STARSHIP_DEST="$HOME/.config/starship.toml"
if [ ! -f "$STARSHIP_DEST" ]; then
  cp "$SCRIPT_DIR/starship.toml" "$STARSHIP_DEST"
  ok "starship config deployed"
elif cmp -s "$SCRIPT_DIR/starship.toml" "$STARSHIP_DEST"; then
  skip "starship config"
else
  warn "starship config kept — yours differs (cp scripts/setup/starship.toml ~/.config/ to take this one)"
fi


# Go is a host comfort rather than anything the app needs to serve a file browser, a terminal and a
# chat, so `light` skips it. It is the only section left in this block — 8-13 were removed or moved.
if is_light; then
  echo ""
  omit "Go"
else

# ─── 7. Go ─────────────────────────────────────────────────────────────────────
echo ""
echo "── Go ──"

echo "  Fetching latest Go version..."
GOLANG_VERSION=$(curl -fsSL "https://go.dev/dl/?mode=json" | jq -r '.[0].version' | sed 's/^go//')
if [ -z "$GOLANG_VERSION" ]; then
  warn "Could not fetch latest Go version — falling back to 1.23.6"
  GOLANG_VERSION=1.23.6
fi
echo "  Latest Go: $GOLANG_VERSION"

install_go() {
  case $PM in
    apt|pacman)
      ARCH=$(uname -m)
      case $ARCH in
        x86_64)  GO_ARCH=amd64 ;;
        aarch64) GO_ARCH=arm64 ;;
        *)       GO_ARCH=amd64 ;;
      esac
      echo "  Installing Go ${GOLANG_VERSION} from official tarball..."
      curl -fsSL "https://go.dev/dl/go${GOLANG_VERSION}.linux-${GO_ARCH}.tar.gz" -o /tmp/go.tar.gz
      mkdir -p "$HOME/.local"
      # Only remove old installation after successful download
      rm -rf "$HOME/.local/go"
      tar -C "$HOME/.local" -xzf /tmp/go.tar.gz
      rm /tmp/go.tar.gz
      export GOPATH="$HOME/.local/go-path"
      export PATH="$HOME/.local/go/bin:$GOPATH/bin:$PATH"
      ;;
    brew)
      brew install go
      ;;
  esac
}

if has go; then
  INSTALLED_GO=$(go version 2>/dev/null | awk '{print $3}' | sed 's/^go//')
  if [ "$INSTALLED_GO" = "$GOLANG_VERSION" ]; then
    skip "go $INSTALLED_GO"
  else
    warn "go $INSTALLED_GO installed but latest is $GOLANG_VERSION — upgrading..."
    install_go
    if has go; then ok "go $(go version | awk '{print $3}') installed"; else warn "go upgrade failed"; fi
  fi
else
  install_go
  if has go; then ok "go $(go version | awk '{print $3}') installed"; else warn "go not found — install manually from https://go.dev/dl/"; fi
fi

# 8 Rust, 9 PulseAudio, 10 cliamp and 13 yt-dlp are in setup-sidecars.sh.
# 11 Neovim, 12 shell extras (oh-my-zsh/eza/lazygit) and 14 npm globals are gone entirely — the host
# provisioning owns node, npm, pm2, Claude Code, Neovim and the shell, and this script duplicating
# them meant two installers racing for the same binaries.

fi  # end of the light-profile skip, which is now section 7 alone

# Kept from the removed section 14: nothing here installs into ~/.local/bin any more, but section 19
# still asks `has pm2` and the agent still resolves `claude` off PATH. A host that installed either
# user-locally would otherwise look like it has neither.
export PATH="$HOME/.local/bin:$PATH"

# ─── 15. bun install (project dependencies) ──────────────────────────────────
echo ""
echo "── Project dependencies ──"

if has bun && [ -f "$PROJECT_DIR/package.json" ]; then
  echo "  Running bun install..."
  (cd "$PROJECT_DIR" && bun install)
  ok "Project dependencies installed"
else
  warn "Skipping bun install (bun not found or not in project dir)"
fi

# ─── 16. environment (.env) ──────────────────────────────────────────────────
echo ""
echo "── Environment (.env) ──"

GENERATE_ENV=true

if [ ! -t 0 ]; then
  # Non-interactive shell: the prompts below would hit EOF and abort the whole script under `set -e`.
  GENERATE_ENV=false
  if [ -f "$PROJECT_DIR/.env" ]; then
    skip ".env (kept existing — non-interactive shell)"
  else
    warn "No .env and not a terminal — re-run setup.sh interactively to generate it"
  fi
elif [ -f "$PROJECT_DIR/.env" ]; then
  echo -n "  .env already exists. Regenerate? (y/n) [n]: "
  read -r REGEN
  if [[ "$REGEN" != "y" && "$REGEN" != "Y" ]]; then
    GENERATE_ENV=false
    skip ".env (kept existing)"
  fi
fi

if [ "$GENERATE_ENV" = true ]; then
  # Run setup-dockers.sh and capture its stdout output. Postgres is the only one of the five the light
  # profile needs — it is the platform's only database. NPM, Mailhog, Redis and SearXNG all serve parts
  # of the estate a light install is not running.
  echo "  Setting up Docker Compose services..."
  if is_light; then
    DOCKER_OUTPUT=$(SETUP_DOCKER_SERVICES=2 bash "$SCRIPT_DIR/setup-dockers.sh")
  else
    DOCKER_OUTPUT=$(bash "$SCRIPT_DIR/setup-dockers.sh")
  fi

  # Parse output from setup-dockers.sh
  COMPOSE_DIR=$(echo "$DOCKER_OUTPUT" | grep '^COMPOSE_DIR=' | cut -d= -f2-)
  POSTGRES_URL=$(echo "$DOCKER_OUTPUT" | grep '^POSTGRES_URL=' | cut -d= -f2-)
  DOCKER_MAIL_TRANSPORT=$(echo "$DOCKER_OUTPUT" | grep '^MAIL_TRANSPORT=' | cut -d= -f2-)

  # Prompt for remaining values
  echo ""
  echo -n "  PORT [9010]: "
  read -r ENV_PORT
  ENV_PORT="${ENV_PORT:-9010}"

  # A light install is reached at localhost on the machine running it, so there is exactly one right
  # answer and no reason to make someone produce it. Plain HTTP is fine there: browsers treat
  # http://localhost as a secure context, so passkeys, microphone capture and the clipboard all work
  # without TLS. That stops being true over the LAN — http://192.168.x.x is NOT a secure context and
  # those APIs fail in browser-specific ways — so reaching a light install from another device means
  # putting an HTTPS proxy in front of it.
  if is_light; then
    echo -n "  PUBLIC_URL [http://localhost:$ENV_PORT]: "
    read -r ENV_PUBLIC_URL
    ENV_PUBLIC_URL="${ENV_PUBLIC_URL:-http://localhost:$ENV_PORT}"
  else
    echo -n "  PUBLIC_URL (required): "
    read -r ENV_PUBLIC_URL
    while [ -z "$ENV_PUBLIC_URL" ]; do
      warn "PUBLIC_URL is required"
      echo -n "  PUBLIC_URL: "
      read -r ENV_PUBLIC_URL
    done
  fi

  echo -n "  DATA_PATH [$REAL_HOME/.local/data]: "
  read -r ENV_DATA_PATH
  ENV_DATA_PATH="${ENV_DATA_PATH:-$REAL_HOME/.local/data}"

  # The file-based item store (skills/tools/tasks/…). Defaults to a sibling of the repo; without it the
  # server falls back to <repo>/officer-items and boots with an empty store.
  ITEMS_DEFAULT="$(dirname "$PROJECT_DIR")/officer-items"
  echo -n "  OFFICER_ITEMS_DIR [$ITEMS_DEFAULT]: "
  read -r ENV_OFFICER_ITEMS_DIR
  ENV_OFFICER_ITEMS_DIR="${ENV_OFFICER_ITEMS_DIR:-$ITEMS_DEFAULT}"

  MAIL_DEFAULT="${DOCKER_MAIL_TRANSPORT:-smtp://127.0.0.1:1025}"
  echo -n "  MAIL_TRANSPORT [$MAIL_DEFAULT]: "
  read -r ENV_MAIL_TRANSPORT
  ENV_MAIL_TRANSPORT="${ENV_MAIL_TRANSPORT:-$MAIL_DEFAULT}"

  echo -n "  DISCORD_BUG_REPORT_WEBHOOK []: "
  read -r ENV_DISCORD_WEBHOOK

  if [ -z "$POSTGRES_URL" ]; then
    echo -n "  POSTGRES_URL: "
    read -r POSTGRES_URL
  fi

  # Auto-generate values
  JWT_SECRET=$(openssl rand -base64 48 | tr -d '/+=' | head -c 48)

  # Write .env
  cat > "$PROJECT_DIR/.env" <<ENVFILE
PORT="$ENV_PORT"
JWT_SECRET="$JWT_SECRET"
MAIL_TRANSPORT="$ENV_MAIL_TRANSPORT"
PUBLIC_URL="$ENV_PUBLIC_URL"
PUBLIC_BUILD_ENV="production"
DATA_PATH="$ENV_DATA_PATH"
OFFICER_ITEMS_DIR="$ENV_OFFICER_ITEMS_DIR"
HOME_DIR="$HOME"
POSTGRES_URL="$POSTGRES_URL"
DISCORD_BUG_REPORT_WEBHOOK="$ENV_DISCORD_WEBHOOK"
ENVFILE

  ok ".env written to $PROJECT_DIR/.env"
fi

# 17 remote desktop is in setup-sidecars.sh.

# ─── 18. project initialization ──────────────────────────────────────────────
echo ""
echo "── Project initialization ──"

if ! has bun || [ ! -f "$PROJECT_DIR/.env" ]; then
  warn "Skipping project initialization (bun or .env missing)"
else
  # index.gen.html is gitignored and built from .env, so it does not exist on a fresh clone.
  echo "  Generating index.gen.html from PUBLIC_URL..."
  if (cd "$PROJECT_DIR" && bun run gen:index); then
    ok "index.gen.html generated"
  else
    fail "gen:index failed — the app will not serve until this succeeds"
  fi

  # Officer applies its schema with push; there are no migrations to run.
  echo "  Applying database schema..."
  if (cd "$PROJECT_DIR" && bun db:push); then
    ok "database schema applied"
  else
    fail "db:push failed — check POSTGRES_URL in .env and that Postgres is reachable"
  fi
fi

# ─── 19. start the services ──────────────────────────────────────────────────
echo ""
echo "── Services (pm2) ──"

# Installing pm2 is not the same as running anything with it. Without this the setup finishes with
# every dependency in place and nothing actually listening — and the sidecars matter beyond the web
# app: /desktop returns 503 until officer-vnc is connected, and chat needs officer-agent.
if ! has pm2 || [ ! -f "$PROJECT_DIR/$ECOSYSTEM_FILE" ]; then
  warn "Skipping service start (pm2 or $ECOSYSTEM_FILE missing)"
else
  # startOrRestart also picks up apps added to the ecosystem since the last run. These are all
  # fork-mode apps, so reload would buy nothing over restart.
  echo "  Starting Officer and its sidecars ($ECOSYSTEM_FILE)..."
  if (cd "$PROJECT_DIR" && pm2 startOrRestart "$ECOSYSTEM_FILE"); then
    ok "services started"
  else
    fail "pm2 could not start the services — check 'pm2 logs'"
  fi

  # Persist the process list so the boot unit has something to resurrect.
  pm2 save >/dev/null 2>&1 && ok "process list saved" || warn "pm2 save failed"

  # Boot persistence. pm2 startup writes a systemd unit; it needs root, and re-running it when the
  # unit already exists is harmless.
  if systemctl list-unit-files 2>/dev/null | grep -q "^pm2-$(whoami)\.service"; then
    skip "pm2 boot service (pm2-$(whoami).service)"
  else
    echo "  Enabling start on boot..."
    if sudo env PATH="$PATH" pm2 startup systemd -u "$(whoami)" --hp "$HOME" >/dev/null 2>&1; then
      pm2 save >/dev/null 2>&1
      ok "services will start on boot"
    else
      warn "Could not enable boot startup — run: pm2 startup  (and follow its instructions)"
    fi
  fi
fi

# ─── verification ─────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════"
echo "  Verification"
echo "═══════════════════════════════════════════"
echo ""

check() {
  if has "$1"; then ok "$1"; else fail "$1 — NOT FOUND"; fi
}

echo "Required:"
check git
check node
check bun
check npm
check ffmpeg
check zip
check script
check python3
check make
check gcc

echo ""
echo "Dev tools:"
# Only what this script still installs is checked. Reporting Go as NOT FOUND on a light install that
# deliberately skipped it makes a clean run look broken; so does checking for nvim, lazygit and eza,
# which this script no longer owns at all.
if ! is_light; then
  check go
fi
check starship
check zsh
check rg
check fd
check jq
check htop
check tmux
check tree
check btop
check sqlite3

# Neither of these is installed here any more — they come from the host provisioning. They are still
# checked because section 19 and every chat turn depend on them, and "NOT FOUND" here is the only
# warning you get before the services silently do not start.
echo ""
echo "AI agents (from host provisioning):"
check claude

echo ""
echo "Process manager (from host provisioning):"
check pm2

echo ""
echo "Optional:"
check unzip
check 7z
check unrar
check pgrep
check fuser

echo ""
echo "═══════════════════════════════════════════"
echo "  Setup complete!"
echo "═══════════════════════════════════════════"
# Services actually running is a better signal than the binaries being present. The list comes from the
# ecosystem this install started, so it cannot drift as sidecars are added.
#
# Read with node rather than grepped: ecosystem.light.config.cjs derives its apps from the full file
# and has no literal `name:` keys to match, so a grep would silently verify nothing. Loading it also
# exercises its own consistency checks, which is worth doing here.
if has pm2 && [ -f "$PROJECT_DIR/$ECOSYSTEM_FILE" ]; then
  echo ""
  echo "Services:"
  ECOSYSTEM_APPS=$(node -e "require('$PROJECT_DIR/$ECOSYSTEM_FILE').apps.forEach(a=>console.log(a.name))" 2>/dev/null) \
    || fail "$ECOSYSTEM_FILE could not be loaded — run: node -e \"require('./$ECOSYSTEM_FILE')\" to see why"
  for app in $ECOSYSTEM_APPS; do
    if pm2 pid "$app" >/dev/null 2>&1 && [ -n "$(pm2 pid "$app" 2>/dev/null | tr -d '[:space:]')" ]; then
      ok "$app"
    else
      fail "$app — not running (pm2 logs $app)"
    fi
  done
fi

echo ""
echo "Notes:"
echo "  • Make sure ~/.local/go/bin and ~/.local/go-path/bin are in your PATH for Go tools"
echo "  • sharp, whisper-cpp, mlx-audio can be installed from Settings > Applications"
echo "  • node, npm, pm2 and the agent CLIs come from the host provisioning, not from here"
echo "  • Rust, PulseAudio, cliamp, yt-dlp and the remote desktop are NOT installed by this script:"
echo "    run 'bash scripts/setup/setup-sidecars.sh' if you want them"
echo ""
