#!/usr/bin/env bash
# ─── Mosaic Stack Installer / Upgrader ────────────────────────────────────────
#
# Installs both components:
#   1. Mosaic framework   → ~/.config/mosaic/  (bash launcher, guides, runtime configs, tools)
#   2. @mosaicstack/mosaic (npm) → ~/.npm-global/     (CLI, TUI, gateway client, wizard)
#
# Quick:  curl -fsSL https://mosaicstack.dev/install.sh | bash
# Direct: bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
#
# Remote install (alternative — use -s -- to pass flags):
#   curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh | bash -s --
#
# Flags:
#   --check           Version check only, no install
#   --framework       Install/upgrade framework only (skip npm CLI)
#   --cli             Install/upgrade npm CLI only (skip framework)
#   --ref <branch>    Git ref for framework archive (default: main)
#   --dev             Build CLI + gateway FROM SOURCE at --ref instead of the
#                     registry @latest. Zero registry writes — packs local
#                     tarballs and installs them globally. Use to test a branch
#                     end-to-end before cutting a release.
#   --yes             Accept all defaults; headless/non-interactive install
#   --no-auto-launch  Skip automatic mosaic wizard + gateway install on first install
#   --uninstall       Reverse the install: remove framework dir, CLI package, and npmrc line
#
# Environment:
#   MOSAIC_HOME         — framework install dir    (default: ~/.config/mosaic)
#   MOSAIC_REGISTRY     — npm registry URL          (default: Gitea instance)
#   MOSAIC_SCOPE        — npm scope                 (default: @mosaicstack)
#   MOSAIC_PREFIX       — npm global prefix          (default: ~/.npm-global)
#   MOSAIC_NO_COLOR     — disable colour             (set to 1)
#   MOSAIC_REF          — git ref for framework      (default: main)
#   MOSAIC_DEV          — equivalent to --dev         (set to 1)
#   MOSAIC_ASSUME_YES   — equivalent to --yes        (set to 1)
# ──────────────────────────────────────────────────────────────────────────────
#
# Wrapped in main() for safe curl-pipe usage.

set -euo pipefail

main() {

# ─── parse flags ──────────────────────────────────────────────────────────────
FLAG_CHECK=false
FLAG_FRAMEWORK=true
FLAG_CLI=true
FLAG_NO_AUTO_LAUNCH=false
FLAG_YES=false
FLAG_UNINSTALL=false
FLAG_DEV=false
GIT_REF="${MOSAIC_REF:-main}"

# MOSAIC_ASSUME_YES env var acts the same as --yes
if [[ "${MOSAIC_ASSUME_YES:-0}" == "1" ]]; then
  FLAG_YES=true
fi

# MOSAIC_DEV env var acts the same as --dev
if [[ "${MOSAIC_DEV:-0}" == "1" ]]; then
  FLAG_DEV=true
fi

while [[ $# -gt 0 ]]; do
  case "$1" in
    --check)          FLAG_CHECK=true; shift ;;
    --framework)      FLAG_CLI=false; shift ;;
    --cli)            FLAG_FRAMEWORK=false; shift ;;
    --ref)            GIT_REF="${2:-main}"; shift 2 ;;
    --dev)            FLAG_DEV=true; shift ;;
    --yes|-y)         FLAG_YES=true; shift ;;
    --no-auto-launch) FLAG_NO_AUTO_LAUNCH=true; shift ;;
    --uninstall)      FLAG_UNINSTALL=true; shift ;;
    *)                shift ;;
  esac
done

# ─── constants ────────────────────────────────────────────────────────────────
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
REGISTRY="${MOSAIC_REGISTRY:-https://git.mosaicstack.dev/api/packages/mosaicstack/npm/}"
SCOPE="${MOSAIC_SCOPE:-@mosaicstack}"
PREFIX="${MOSAIC_PREFIX:-$HOME/.npm-global}"
CLI_PKG="${SCOPE}/mosaic"
REPO_BASE="https://git.mosaicstack.dev/mosaicstack/stack"
ARCHIVE_URL="${REPO_BASE}/archive/${GIT_REF}.tar.gz"

# In dev (build-from-source) mode the gateway is installed globally from a
# locally-built tarball. Tell the wizard / gateway-config stage NOT to overwrite
# it with the registry @latest build (honored by gatewayConfigStage).
if [[ "$FLAG_DEV" == "true" ]]; then
  export MOSAIC_GATEWAY_SKIP_NPM_INSTALL=1
fi

# Shared monorepo checkout (populated on demand by ensure_monorepo).
WORK_DIR=""
EXTRACTED_DIR=""

# ─── uninstall path ───────────────────────────────────────────────────────────
# Shell-level uninstall for when the CLI is broken or not available.
# Handles: framework directory, npm CLI package, npmrc scope line.
# Gateway teardown: if mosaic CLI is still available, delegates to it.
# Does NOT touch gateway DB/storage — user must handle that separately.

if [[ "$FLAG_UNINSTALL" == "true" ]]; then
  echo ""
  echo "${BOLD:-}Mosaic Uninstaller (shell fallback)${RESET:-}"
  echo ""

  SCOPE_LINE="${SCOPE:-@mosaicstack}:registry=${REGISTRY:-https://git.mosaicstack.dev/api/packages/mosaicstack/npm/}"
  NPMRC_FILE="$HOME/.npmrc"

  # Gateway: try mosaic CLI first, then check pid file
  if command -v mosaic &>/dev/null; then
    echo "${B:-}ℹ${RESET:-}  Attempting gateway uninstall via mosaic CLI…"
    if mosaic gateway uninstall --yes 2>/dev/null; then
      echo "${G:-}✔${RESET:-}  Gateway uninstalled via CLI."
    else
      echo "${Y:-}⚠${RESET:-}  Gateway uninstall via CLI failed or not installed — skipping."
    fi
  else
    # Look for pid file and stop daemon if running
    GATEWAY_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}/../mosaic-gateway"
    PID_FILE="$GATEWAY_HOME/gateway.pid"
    if [[ -f "$PID_FILE" ]]; then
      PID="$(cat "$PID_FILE" 2>/dev/null || true)"
      if [[ -n "$PID" ]] && kill -0 "$PID" 2>/dev/null; then
        echo "${B:-}ℹ${RESET:-}  Stopping gateway daemon (pid $PID)…"
        kill "$PID" 2>/dev/null || true
        sleep 1
      fi
    fi
    echo "${Y:-}⚠${RESET:-}  mosaic CLI not found — skipping full gateway teardown."
    echo "     Run 'mosaic gateway uninstall' separately if the CLI is available."
  fi

  # Framework directory
  if [[ -d "$MOSAIC_HOME" ]]; then
    echo "${B:-}ℹ${RESET:-}  Removing framework: $MOSAIC_HOME"
    rm -rf "$MOSAIC_HOME"
    echo "${G:-}✔${RESET:-}  Framework removed."
  else
    echo "${Y:-}⚠${RESET:-}  Framework directory not found: $MOSAIC_HOME"
  fi

  # Runtime assets: restore backups or remove managed copies
  echo "${B:-}ℹ${RESET:-}  Reversing runtime asset copies…"
  declare -a RUNTIME_DESTS=(
    "$HOME/.claude/CLAUDE.md"
    "$HOME/.claude/settings.json"
    "$HOME/.claude/hooks-config.json"
    "$HOME/.claude/context7-integration.md"
    "$HOME/.config/opencode/AGENTS.md"
    "$HOME/.codex/instructions.md"
  )
  for dest in "${RUNTIME_DESTS[@]}"; do
    base="$(basename "$dest")"
    dir="$(dirname "$dest")"
    # Find most recent backup
    backup=""
    if [[ -d "$dir" ]]; then
      backup="$(ls -1t "$dir/${base}.mosaic-bak-"* 2>/dev/null | head -1 || true)"
    fi
    if [[ -n "$backup" ]] && [[ -f "$backup" ]]; then
      cp "$backup" "$dest"
      rm -f "$backup"
      echo "  Restored: $dest"
    elif [[ -f "$dest" ]]; then
      rm -f "$dest"
      echo "  Removed:  $dest"
    fi
  done

  # npmrc scope line
  if [[ -f "$NPMRC_FILE" ]] && grep -qF "$SCOPE_LINE" "$NPMRC_FILE" 2>/dev/null; then
    echo "${B:-}ℹ${RESET:-}  Removing $SCOPE_LINE from $NPMRC_FILE…"
    # Use sed to remove the exact line (in-place, portable)
    if sed -i.mosaic-uninstall-bak "\|^${SCOPE_LINE}\$|d" "$NPMRC_FILE" 2>/dev/null; then
      rm -f "${NPMRC_FILE}.mosaic-uninstall-bak"
      echo "${G:-}✔${RESET:-}  npmrc entry removed."
    else
      # BSD sed syntax (macOS)
      sed -i '' "\|^${SCOPE_LINE}\$|d" "$NPMRC_FILE" 2>/dev/null || \
        echo "${Y:-}⚠${RESET:-}  Could not auto-remove npmrc line — remove it manually: $SCOPE_LINE"
    fi
  fi

  # npm CLI package
  echo "${B:-}ℹ${RESET:-}  Uninstalling npm package: ${CLI_PKG}…"
  if npm uninstall -g "${CLI_PKG}" --prefix="$PREFIX" 2>&1 | sed 's/^/  /'; then
    echo "${G:-}✔${RESET:-}  CLI package removed."
  else
    echo "${Y:-}⚠${RESET:-}  npm uninstall failed — you may need to run manually:"
    echo "     npm uninstall -g ${CLI_PKG}"
  fi

  echo ""
  echo "${G:-}✔${RESET:-}  Uninstall complete."
  exit 0
fi

# ─── colours ──────────────────────────────────────────────────────────────────
if [[ "${MOSAIC_NO_COLOR:-0}" == "1" ]] || ! [[ -t 1 ]]; then
  R="" G="" Y="" B="" C="" DIM="" BOLD="" RESET=""
else
  R=$'\033[0;31m' G=$'\033[0;32m' Y=$'\033[0;33m'
  B=$'\033[0;34m' C=$'\033[0;36m' DIM=$'\033[2m'
  BOLD=$'\033[1m' RESET=$'\033[0m'
fi

info()  { echo "${B}ℹ${RESET}  $*"; }
ok()    { echo "${G}✔${RESET}  $*"; }
warn()  { echo "${Y}⚠${RESET}  $*"; }
fail()  { echo "${R}✖${RESET}  $*" >&2; }
dim()   { echo "${DIM}$*${RESET}"; }
step()  { echo ""; echo "${BOLD}$*${RESET}"; }

# ─── helpers ──────────────────────────────────────────────────────────────────

require_cmd() {
  if ! command -v "$1" &>/dev/null; then
    fail "Required command not found: $1"
    echo "  Install it and re-run this script."
    exit 1
  fi
}

installed_cli_version() {
  local json
  json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true
  if [[ -n "$json" ]]; then
    node -e "
      const d = JSON.parse(process.argv[1]);
      const v = d?.dependencies?.['${CLI_PKG}']?.version ?? '';
      process.stdout.write(v);
    " "$json" 2>/dev/null || true
  fi
}

latest_cli_version() {
  npm view "${CLI_PKG}" version --registry="$REGISTRY" 2>/dev/null || true
}

version_lt() {
  node -e "
    const a=process.argv[1], b=process.argv[2];
    const sp = v => { const i=v.indexOf('-'); return i===-1 ? [v,null] : [v.slice(0,i),v.slice(i+1)]; };
    const [cA,pA]=sp(a.replace(/^v/,'')), [cB,pB]=sp(b.replace(/^v/,''));
    const nA=cA.split('.').map(Number), nB=cB.split('.').map(Number);
    for(let i=0;i<3;i++){if((nA[i]||0)<(nB[i]||0))process.exit(0);if((nA[i]||0)>(nB[i]||0))process.exit(1);}
    if(pA!==null&&pB===null)process.exit(0);
    if(pA===null)process.exit(1);
    if(pA<pB)process.exit(0);
    process.exit(1);
  " "$1" "$2" 2>/dev/null
}

framework_version() {
  # Read framework schema version stamp
  local vf="$MOSAIC_HOME/.framework-version"
  if [[ -f "$vf" ]]; then
    cat "$vf" 2>/dev/null || true
  fi
}

# Download + extract the monorepo archive at $GIT_REF exactly once per run.
# Sets the script-level EXTRACTED_DIR to the repo root. Reused by both the
# framework install (Part 1) and the dev build-from-source path (Part 2).
ensure_monorepo() {
  if [[ -n "$EXTRACTED_DIR" ]] && [[ -d "$EXTRACTED_DIR" ]]; then
    return 0
  fi

  require_cmd tar

  WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-install-XXXXXX")"
  # shellcheck disable=SC2317
  cleanup_work() { [[ -n "$WORK_DIR" ]] && rm -rf "$WORK_DIR"; }
  trap cleanup_work EXIT

  info "Downloading source from ${GIT_REF}…"
  if command -v curl &>/dev/null; then
    curl -fsSL "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
  elif command -v wget &>/dev/null; then
    wget -qO- "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
  else
    fail "curl or wget required to download source."
    exit 1
  fi

  # Gitea archives extract to <repo-name>/ inside the work dir
  EXTRACTED_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)"
  if [[ -z "$EXTRACTED_DIR" ]] || [[ ! -d "$EXTRACTED_DIR" ]]; then
    fail "Could not locate extracted source in archive."
    ls -la "$WORK_DIR" >&2
    exit 1
  fi
}

# Build @mosaicstack/mosaic + @mosaicstack/gateway from source and install both
# globally from locally-packed tarballs. ZERO registry writes. Workspace deps
# (brain/config/db/…) are pulled from the registry at the versions pinned in
# each package.json — `pnpm pack` rewrites `workspace:*` to those versions.
install_cli_from_source() {
  local src="$EXTRACTED_DIR"
  local out_dir="$WORK_DIR/dist-tarballs"
  mkdir -p "$out_dir"

  # pnpm via corepack (ships with Node >= 16.9; required by Node >= 20 preflight).
  # Pin to the repo's packageManager version so the build matches CI. Surface
  # corepack failures so the fresh-machine case gives an actionable error
  # instead of a bare "command not found".
  if ! command -v pnpm &>/dev/null; then
    info "Activating pnpm via corepack…"
    corepack enable 2>&1 | sed 's/^/  /' || warn "corepack enable failed — pnpm may need manual install."
    corepack prepare pnpm@10.6.2 --activate 2>&1 | sed 's/^/  /' \
      || warn "corepack prepare failed — pnpm may need manual install."
  fi
  if ! command -v pnpm &>/dev/null; then
    fail "pnpm not available after corepack activation."
    echo "  Install pnpm manually (https://pnpm.io/installation) and re-run with --dev."
    exit 1
  fi

  info "Installing workspace dependencies (pnpm install)…"
  ( cd "$src" && pnpm install ) 2>&1 | sed 's/^/  /'

  info "Building CLI + gateway from source…"
  ( cd "$src" && pnpm --filter "@mosaicstack/mosaic..." --filter "@mosaicstack/gateway..." run build ) 2>&1 | sed 's/^/  /'

  info "Packing local tarballs…"
  ( cd "$src/packages/mosaic" && pnpm pack --pack-destination "$out_dir" ) 2>&1 | sed 's/^/  /'
  ( cd "$src/apps/gateway"    && pnpm pack --pack-destination "$out_dir" ) 2>&1 | sed 's/^/  /'

  local cli_tgz gw_tgz
  cli_tgz="$(ls -1t "$out_dir"/mosaicstack-mosaic-*.tgz 2>/dev/null | head -1)"
  gw_tgz="$(ls -1t "$out_dir"/mosaicstack-gateway-*.tgz 2>/dev/null | head -1)"

  if [[ ! -f "$cli_tgz" ]]; then
    fail "CLI tarball was not produced by pnpm pack."
    exit 1
  fi
  if [[ ! -f "$gw_tgz" ]]; then
    fail "Gateway tarball was not produced by pnpm pack."
    exit 1
  fi

  # Gateway first so it is present globally before the CLI's wizard runs (which
  # skips its own gateway install via MOSAIC_GATEWAY_SKIP_NPM_INSTALL=1).
  info "Installing gateway from source tarball (global)…"
  npm install -g "$gw_tgz" --prefix="$PREFIX" 2>&1 | sed 's/^/  /'

  info "Installing CLI from source tarball (global)…"
  npm install -g "$cli_tgz" --prefix="$PREFIX" 2>&1 | sed 's/^/  /'

  ok "Installed from source: CLI $(installed_cli_version)"
}

# ─── preflight ────────────────────────────────────────────────────────────────

require_cmd node
require_cmd npm

NODE_MAJOR="$(node -e 'process.stdout.write(String(process.versions.node.split(".")[0]))')"
if [[ "$NODE_MAJOR" -lt 20 ]]; then
  fail "Node.js >= 20 required (found v$(node --version))"
  exit 1
fi

echo ""
echo "${BOLD}Mosaic Stack Installer${RESET}"
echo ""

# ═══════════════════════════════════════════════════════════════════════════════
# PART 1: Framework (bash launcher + guides + runtime configs + tools)
# ═══════════════════════════════════════════════════════════════════════════════

if [[ "$FLAG_FRAMEWORK" == "true" ]]; then
  step "Framework (~/.config/mosaic)"

  FRAMEWORK_CURRENT="$(framework_version)"
  HAS_FRAMEWORK=false
  [[ -f "$MOSAIC_HOME/AGENTS.md" ]] || [[ -f "$MOSAIC_HOME/.framework-version" ]] && HAS_FRAMEWORK=true

  if [[ -n "$FRAMEWORK_CURRENT" ]]; then
    dim "  Installed: framework v${FRAMEWORK_CURRENT}"
  elif [[ "$HAS_FRAMEWORK" == "true" ]]; then
    dim "  Installed: framework (version unknown)"
  else
    dim "  Installed: (none)"
  fi
  dim "  Source:    ${REPO_BASE} (ref: ${GIT_REF})"
  echo ""

  if [[ "$FLAG_CHECK" == "true" ]]; then
    if [[ "$HAS_FRAMEWORK" == "true" ]]; then
      ok "Framework is installed."
    else
      warn "Framework not installed."
    fi
  else
    # Download repo archive and extract framework (shared with the dev build)
    ensure_monorepo
    FRAMEWORK_SRC="$EXTRACTED_DIR/packages/mosaic/framework"

    if [[ ! -d "$FRAMEWORK_SRC" ]]; then
      fail "Framework not found in archive at packages/mosaic/framework/"
      fail "Archive contents:"
      ls -la "$WORK_DIR" >&2
      exit 1
    fi

    # Run the framework's own install.sh (handles keep/overwrite for SOUL.md etc.)
    info "Installing framework to ${MOSAIC_HOME}…"
    MOSAIC_INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" \
      MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1 \
      MOSAIC_SKIP_SKILLS_SYNC="${MOSAIC_SKIP_SKILLS_SYNC:-0}" \
      bash "$FRAMEWORK_SRC/install.sh"

    ok "Framework installed"
    echo ""

    # Framework bin is no longer needed on PATH — the npm CLI delegates
    # to mosaic-launch directly via its absolute path.
  fi
fi

# ═══════════════════════════════════════════════════════════════════════════════
# PART 2: @mosaicstack/mosaic (npm — TUI, gateway client, wizard, CLI)
# ═══════════════════════════════════════════════════════════════════════════════

if [[ "$FLAG_CLI" == "true" ]]; then
  step "@mosaicstack/mosaic (npm package)"

  # Ensure prefix dir
  if [[ ! -d "$PREFIX" ]]; then
    info "Creating global prefix directory: $PREFIX"
    mkdir -p "$PREFIX"/{bin,lib}
  fi

  # Ensure npmrc scope mapping
  NPMRC="$HOME/.npmrc"
  SCOPE_LINE="${SCOPE}:registry=${REGISTRY}"

  if ! grep -qF "$SCOPE_LINE" "$NPMRC" 2>/dev/null; then
    info "Adding ${SCOPE} registry to $NPMRC"
    echo "$SCOPE_LINE" >> "$NPMRC"
    ok "Registry configured"
  fi

  if ! grep -qF "prefix=$PREFIX" "$NPMRC" 2>/dev/null; then
    if ! grep -q '^prefix=' "$NPMRC" 2>/dev/null; then
      echo "prefix=$PREFIX" >> "$NPMRC"
      info "Set npm global prefix to $PREFIX"
    fi
  fi

  CURRENT="$(installed_cli_version)"
  if [[ "$FLAG_DEV" == "true" ]]; then
    LATEST=""
  else
    LATEST="$(latest_cli_version)"
  fi

  if [[ -n "$CURRENT" ]]; then
    dim "  Installed: ${CLI_PKG}@${CURRENT}"
  else
    dim "  Installed: (none)"
  fi

  if [[ "$FLAG_DEV" == "true" ]]; then
    dim "  Source:    ${REPO_BASE} (ref: ${GIT_REF}, build-from-source)"
  elif [[ -n "$LATEST" ]]; then
    dim "  Latest:    ${CLI_PKG}@${LATEST}"
  else
    dim "  Latest:    (registry unreachable)"
  fi
  echo ""

  if [[ "$FLAG_CHECK" == "true" ]]; then
    if [[ "$FLAG_DEV" == "true" ]]; then
      info "Dev mode: installed version is ${CURRENT:-(none)} (no registry comparison)."
    elif [[ -z "$LATEST" ]]; then
      warn "Could not reach registry."
    elif [[ -z "$CURRENT" ]]; then
      warn "Not installed."
    elif [[ "$CURRENT" == "$LATEST" ]]; then
      ok "Up to date."
    elif version_lt "$CURRENT" "$LATEST"; then
      warn "Update available: $CURRENT → $LATEST"
    else
      ok "Up to date (or ahead of registry)."
    fi
  elif [[ "$FLAG_DEV" == "true" ]]; then
    info "Dev mode — building CLI + gateway from source at ref ${GIT_REF}…"
    ensure_monorepo
    install_cli_from_source

    # PATH check for npm prefix
    if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then
      warn "$PREFIX/bin is not on your PATH"
      dim "  Add to your shell rc:  export PATH=\"$PREFIX/bin:\$PATH\""
    fi
  else
    if [[ -z "$LATEST" ]]; then
      warn "Could not reach registry at $REGISTRY — skipping npm CLI."
    elif [[ -z "$CURRENT" ]]; then
      info "Installing ${CLI_PKG}@${LATEST}…"
      npm install -g "${CLI_PKG}@${LATEST}" --prefix="$PREFIX" 2>&1 | sed 's/^/  /'
      ok "CLI installed: $(installed_cli_version)"
    elif [[ "$CURRENT" == "$LATEST" ]]; then
      ok "Already at latest version ($LATEST)."
    elif version_lt "$CURRENT" "$LATEST"; then
      info "Upgrading ${CLI_PKG}: $CURRENT → $LATEST…"
      npm install -g "${CLI_PKG}@${LATEST}" --prefix="$PREFIX" 2>&1 | sed 's/^/  /'
      ok "CLI upgraded: $(installed_cli_version)"
    else
      ok "CLI is at or ahead of registry ($CURRENT ≥ $LATEST)."
    fi

    # PATH check for npm prefix
    if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then
      warn "$PREFIX/bin is not on your PATH"
      dim "  Add to your shell rc:  export PATH=\"$PREFIX/bin:\$PATH\""
    fi
  fi
fi

# ═══════════════════════════════════════════════════════════════════════════════
# Summary
# ═══════════════════════════════════════════════════════════════════════════════

if [[ "$FLAG_CHECK" == "false" ]]; then
  step "Summary"

  echo "  ${BOLD}mosaic:${RESET}          $PREFIX/bin/mosaic"
  dim "  Framework data:  $MOSAIC_HOME/"
  echo ""

  # First install guidance / auto-launch
  if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then
    echo ""
    if [[ "$FLAG_NO_AUTO_LAUNCH" == "false" ]] && [[ -t 0 ]] && [[ -t 1 ]]; then
      # Interactive TTY and auto-launch not suppressed: run the unified wizard.
      # `mosaic wizard` now runs the full first-run flow end-to-end: identity
      # setup → runtimes → hooks preview → skills → finalize → gateway
      # config → admin bootstrap. No second call needed.
      info "First install detected — launching unified setup wizard…"
      echo ""

      MOSAIC_BIN="$PREFIX/bin/mosaic"

      if ! command -v "$MOSAIC_BIN" &>/dev/null && ! command -v mosaic &>/dev/null; then
        warn "mosaic binary not found on PATH — skipping auto-launch."
        warn "Add $PREFIX/bin to PATH and run: mosaic wizard"
      else
        # Prefer the absolute path from the prefix we just installed to
        MOSAIC_CMD="mosaic"
        if [[ -x "$MOSAIC_BIN" ]]; then
          MOSAIC_CMD="$MOSAIC_BIN"
        fi

        if "$MOSAIC_CMD" wizard; then
          ok "Wizard complete."
        else
          warn "Wizard exited non-zero."
          echo "  You can retry with: ${C}mosaic wizard${RESET}"
          echo "  Or run gateway install alone: ${C}mosaic gateway install${RESET}"
        fi
      fi
    else
      # Non-interactive or --no-auto-launch: print guidance only
      info "First install detected. Set up your agent identity:"
      echo "  ${C}mosaic wizard${RESET}   (unified first-run wizard — identity + gateway + admin)"
      echo "  ${C}mosaic gateway install${RESET}   (standalone gateway (re)configure)"
    fi
  fi

  # ── Write install manifest ──────────────────────────────────────────────────
  # Records what was mutated so that `mosaic uninstall` can precisely reverse it.
  # Written last (after all mutations) so an incomplete install leaves no manifest.
  MANIFEST_PATH="$MOSAIC_HOME/.install-manifest.json"
  MANIFEST_CLI_VERSION="$(installed_cli_version)"
  MANIFEST_FW_VERSION="$(framework_version)"
  MANIFEST_SCOPE_LINE="${SCOPE}:registry=${REGISTRY}"
  MANIFEST_TS="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")"

  # Build runtimeAssetCopies array by scanning known destinations for backups
  collect_runtime_copies() {
    local home_dir="$HOME"
    local copies="[]"
    local dests=(
      "$home_dir/.claude/CLAUDE.md"
      "$home_dir/.claude/settings.json"
      "$home_dir/.claude/hooks-config.json"
      "$home_dir/.claude/context7-integration.md"
      "$home_dir/.config/opencode/AGENTS.md"
      "$home_dir/.codex/instructions.md"
    )
    copies="["
    local first=true
    for dest in "${dests[@]}"; do
      [[ -f "$dest" ]] || continue
      local base dir backup_path backup_val
      base="$(basename "$dest")"
      dir="$(dirname "$dest")"
      backup_path="$(ls -1t "$dir/${base}.mosaic-bak-"* 2>/dev/null | head -1 || true)"
      if [[ -n "$backup_path" ]]; then
        backup_val="\"$backup_path\""
      else
        backup_val="null"
      fi
      if [[ "$first" == "true" ]]; then
        first=false
      else
        copies="$copies,"
      fi
      copies="$copies{\"source\":\"\",\"dest\":\"$dest\",\"backup\":$backup_val}"
    done
    copies="$copies]"
    echo "$copies"
  }

  RUNTIME_COPIES="$(collect_runtime_copies)"

  # Check whether the npmrc line was present (we may have added it above)
  NPMRC_LINES_JSON="[]"
  if grep -qF "$MANIFEST_SCOPE_LINE" "$HOME/.npmrc" 2>/dev/null; then
    NPMRC_LINES_JSON="[\"$MANIFEST_SCOPE_LINE\"]"
  fi

  node -e "
    const fs = require('fs');
    const path = require('path');
    const p = process.argv[1];
    const m = {
      version: 1,
      installedAt: process.argv[2],
      cliVersion: process.argv[3] || '(unknown)',
      frameworkVersion: parseInt(process.argv[4] || '0', 10),
      mutations: {
        directories: [path.dirname(p)],
        npmGlobalPackages: ['@mosaicstack/mosaic'],
        npmrcLines: JSON.parse(process.argv[5]),
        shellProfileEdits: [],
        runtimeAssetCopies: JSON.parse(process.argv[6]),
      }
    };
    fs.mkdirSync(path.dirname(p), { recursive: true });
    fs.writeFileSync(p, JSON.stringify(m, null, 2) + '\\n', { mode: 0o600 });
  " \
    "$MANIFEST_PATH" \
    "$MANIFEST_TS" \
    "$MANIFEST_CLI_VERSION" \
    "$MANIFEST_FW_VERSION" \
    "$NPMRC_LINES_JSON" \
    "$RUNTIME_COPIES" 2>/dev/null \
    && ok "Install manifest written: $MANIFEST_PATH" \
    || warn "Could not write install manifest (non-fatal)"

  echo ""
  ok "Done."
fi

} # end main

main "$@"
