#!/bin/sh
# 0dai native installer.
#
#   curl -fsSL https://0dai.dev/install.sh | sh
#
# Clones the 0dai git repository and stops. The clone is the install.
# This script does not install an npm package and it does not download a
# release binary. After a successful clone it hands off to the existing
# scripts/bootstrap/install.sh in that clone. It is not a third installer
# and it does not call ops/docker/install.sh.
#
# Options:
#   --prefix DIR
#       Directory the repo is cloned into. This is not an npm prefix.
#       Default, when the flag is omitted: $ODAI_TMP_DIR if that variable
#       is set (the clone directory scripts/bootstrap/install.sh already
#       honors), otherwise $HOME/.0dai/src.
#       ~/.0dai is the state home scripts/bootstrap/first_run.py already
#       uses for auth and telemetry. The checkout lives in ~/.0dai/src so
#       a clone does not overwrite those files.
#       A directory selected with --prefix is not removed on exit.
#       The default directory is not removed either: this script's result
#       is the checkout. The temp-dir cleanup inside
#       scripts/bootstrap/install.sh applies only when that script is run
#       on its own, with no --prefix and without --checkout-only.
#   --version REF
#       Git ref or tag to check out, for example v4.8.0. This is not an
#       npm package version. When the flag is omitted, the ref is main.
#   --help
#
# sudo apt:
#   scripts/bootstrap/first_run.py installs missing OS packages with
#   `sudo apt-get` on Linux. It asks first, unless ODAI_INSTALL_AUTOCONFIRM=1,
#   in which case it runs sudo immediately. That script has no flag to skip
#   the package step. This public script never calls it and never runs sudo.
#   When the cloned bootstrap accepts --checkout-only, this script passes
#   that flag so the clone is kept and apt is not started. When the ref is
#   older and has no such flag, the clone is left in place and first_run.py
#   is not started. The script prints the command to run it yourself.
#
# Audit: this file is served verbatim from web/public/install.sh. Read it
# before piping to a shell. It performs no privileged actions.
set -eu

# The repository is private, so its address is not baked into this public
# script: the clone path is opt-in through ODAI_REPO_URL. Without it the
# installer uses the published bundle, which needs no credentials.
REPO_URL="${ODAI_REPO_URL:-}"
# Public artifact source. The repository is private, so its GitHub Release
# assets are not fetchable without credentials, and an anonymous clone of the
# repository cannot work either. The bundle is published on the product's own
# domain and needs nothing but this domain to be reachable.
BUNDLE_BASE="${ODAI_BUNDLE_BASE_URL:-https://0dai.dev/downloads}"
VERSION_API="${ODAI_VERSION_API_URL:-https://api.0dai.dev/v1/version}"
VERSION="main"
PREFIX_SET=0
PREFIX=""
SOURCE="auto"
INSTALL_DIR="${HOME}/.local"

if [ -t 2 ]; then C='\033[36m'; R='\033[31m'; Z='\033[0m'; else C=''; R=''; Z=''; fi
say() { printf "${C}0dai${Z} %s\n" "$1" >&2; }
err() { printf "${R}0dai error:${Z} %s\n" "$1" >&2; }
have() { command -v "$1" >/dev/null 2>&1; }

usage() {
  cat >&2 <<EOF
0dai installer

Usage: sh install.sh [options]
   or: curl -fsSL https://0dai.dev/install.sh | sh -s -- [options]

Options:
  --version REF   git ref or tag to clone, for example v4.8.0
                  (default: main). Not an npm package version.
  --prefix DIR    directory to clone into (default: \$ODAI_TMP_DIR if set,
                  otherwise \$HOME/.0dai/src). Not an npm prefix.
                  A directory you pass here is not removed on exit.
  --source MODE   where the CLI comes from:
                    clone  - clone the repository
                    bundle - download the published CLI bundle
                    auto   - try the clone, fall back to the bundle
                  (default: auto)
                  The clone path needs ODAI_REPO_URL to point at a
                  repository you have access to. Without that variable
                  the installer uses the published bundle.
  --install-dir DIR
                  where the bundle installs (default \$HOME/.local);
                  the launcher lands in <DIR>/bin/0dai
  --help          show this help and exit

The clone is the install. This script does not run sudo. OS packages are
a separate step: python3 <prefix>/scripts/bootstrap/first_run.py --interactive
That step can run sudo apt-get; this script does not start it.

With --source bundle (and automatically in "auto" when the clone is not
possible), the script downloads the published CLI bundle instead, verifies
its SHA-256, and installs a launcher into <install-dir>/bin. That path
needs Node.js 22.12+ and no credentials.
EOF
}

while [ $# -gt 0 ]; do
  case "$1" in
    --version)
      [ $# -ge 2 ] || { err "--version requires a git ref or tag"; exit 2; }
      VERSION="$2"
      shift 2
      ;;
    --version=*)
      VERSION="${1#--version=}"
      shift
      ;;
    --prefix)
      [ $# -ge 2 ] || { err "--prefix requires a directory"; exit 2; }
      PREFIX="$2"
      PREFIX_SET=1
      shift 2
      ;;
    --prefix=*)
      PREFIX="${1#--prefix=}"
      PREFIX_SET=1
      shift
      ;;
    --source)
      [ $# -ge 2 ] || { err "--source requires a mode: clone, bundle or auto"; exit 2; }
      SOURCE="$2"
      shift 2
      ;;
    --source=*)
      SOURCE="${1#--source=}"
      shift
      ;;
    --install-dir)
      [ $# -ge 2 ] || { err "--install-dir requires a directory"; exit 2; }
      INSTALL_DIR="$2"
      shift 2
      ;;
    --install-dir=*)
      INSTALL_DIR="${1#--install-dir=}"
      shift
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    *)
      err "unknown option: $1"
      usage
      exit 2
      ;;
  esac
done

case "$VERSION" in
  ""|-*)
    err "--version must be a git ref or tag, for example v4.8.0"
    exit 2
    ;;
  *[[:space:]]*|*..*)
    err "git ref must not contain whitespace or .."
    exit 2
    ;;
esac

if [ "$PREFIX_SET" = 1 ]; then
  DEST="$PREFIX"
elif [ -n "${ODAI_TMP_DIR:-}" ]; then
  DEST="$ODAI_TMP_DIR"
else
  DEST="${HOME}/.0dai/src"
fi

case "$SOURCE" in
  clone|bundle|auto) ;;
  *)
    err "--source must be one of: clone, bundle, auto"
    exit 2
    ;;
esac

if [ -z "$DEST" ] || [ "$DEST" = "/" ]; then
  err "refusing to clone into '$DEST'"
  exit 2
fi

# Fresh clone. DEST must not exist yet. On failure, remove only the
# directory this function created.
clone_fresh() {
  mkdir -p "$(dirname "$DEST")"
  if [ -e "$DEST" ]; then
    err "$DEST already exists and is not a git checkout."
    exit 1
  fi
  if git -c advice.detachedHead=false clone --quiet --depth 1 --branch "$VERSION" "$REPO_URL" "$DEST"; then
    return 0
  fi
  rm -rf "$DEST"
  if git -c advice.detachedHead=false clone --quiet "$REPO_URL" "$DEST" \
    && git -c advice.detachedHead=false -C "$DEST" checkout --quiet --detach "$VERSION"; then
    return 0
  fi
  rm -rf "$DEST"
  err "could not clone $REPO_URL at ref $VERSION"
  return 1
}

# Existing checkout from a previous run. Fetch the requested ref and
# leave the directory in place if the fetch fails.
update_existing() {
  if git -C "$DEST" fetch --quiet --depth 1 origin "refs/heads/$VERSION"; then
    git -c advice.detachedHead=false -C "$DEST" checkout --quiet --detach FETCH_HEAD
    return 0
  fi
  if git -C "$DEST" fetch --quiet --depth 1 origin tag "$VERSION"; then
    git -c advice.detachedHead=false -C "$DEST" checkout --quiet --detach FETCH_HEAD
    return 0
  fi
  if git -C "$DEST" fetch --quiet origin "$VERSION" \
    && git -c advice.detachedHead=false -C "$DEST" checkout --quiet --detach FETCH_HEAD; then
    return 0
  fi
  err "could not check out $VERSION in $DEST. The existing checkout was left unchanged."
  exit 1
}

# --- published bundle source -------------------------------------------
#
# The bundle is the same CLI, published as a flat archive on the product's
# own domain. It is downloaded, its SHA-256 is verified against the
# checksum published next to it, and it is unpacked into <install-dir>.
# Nothing is written outside that directory, no privileged action is
# taken, and no credentials are needed.

bundle_release_tag() {
  case "$VERSION" in
    v[0-9]*.[0-9]*.[0-9]*)
      printf '%s\n' "$VERSION"
      return 0
      ;;
  esac
  have curl || { err "curl is required to download the CLI bundle."; return 1; }
  latest=$(curl -fsSL "$VERSION_API" \
    | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)
  [ -n "$latest" ] || { err "could not read the published version from $VERSION_API"; return 1; }
  printf 'v%s\n' "$latest"
}

sha256_digest() {
  if have sha256sum; then
    sha256sum "$1" | awk '{print $1}'
  elif have shasum; then
    shasum -a 256 "$1" | awk '{print $1}'
  else
    err "neither sha256sum nor shasum is available."
    return 1
  fi
}

bundle_cleanup() {
  rm -rf "$work"
  [ -z "$staging" ] || rm -rf "$staging"
  [ -z "$launcher_tmp" ] || rm -f "$launcher_tmp"
  if [ -n "$previous" ] && [ -e "$previous" ]; then
    if [ -e "$target" ]; then rm -rf "$previous"; else mv "$previous" "$target"; fi
  fi
}

install_from_bundle() {
  tag=$(bundle_release_tag) || exit 1
  have curl || { err "curl is required to download the CLI bundle."; exit 1; }
  have tar || { err "tar is required to unpack the CLI bundle."; exit 1; }
  if ! have node; then
    err "Node.js 22.12+ is required to run the CLI. Install Node.js and run this again."
    exit 1
  fi
  node_version=$(node -p "process.versions.node" 2>/dev/null || echo 0.0.0)
  node_major=$(printf '%s' "$node_version" | cut -d. -f1)
  node_minor=$(printf '%s' "$node_version" | cut -d. -f2)
  case "$node_major" in ''|*[!0-9]*) node_major=0 ;; esac
  case "$node_minor" in ''|*[!0-9]*) node_minor=0 ;; esac
  # 22.12, not 22: the bundle loads @clack/prompts, which is ESM-only, with
  # require(). require(esm) is unflagged from Node 22.12 — on 22.0-22.11 the
  # CLI would die with ERR_REQUIRE_ESM instead of starting.
  if [ "$node_major" -lt 22 ] || { [ "$node_major" -eq 22 ] && [ "$node_minor" -lt 12 ]; }; then
    err "Node.js 22.12+ is required to run the CLI (found $node_version)."
    exit 1
  fi

  work=$(mktemp -d) || { err "could not create a temporary directory."; exit 1; }
  # The download scratch is removed on every exit path, including a checksum
  # mismatch or a failed unpack, not only after a successful install (#8032).
  # Scratch and swap state are tracked so an exit at any point, including a
  # signal between the two renames, removes the scratch and puts the previous
  # version back if the target is missing.
  staging=""
  previous=""
  launcher_tmp=""
  trap 'bundle_cleanup' EXIT
  trap 'exit 1' HUP INT TERM
  archive="$work/0dai-cli-$tag.tar.gz"
  say "downloading the CLI bundle $tag from $BUNDLE_BASE"
  if ! curl -fsSL -o "$archive" "$BUNDLE_BASE/0dai-cli-$tag.tar.gz"; then
    err "could not download $BUNDLE_BASE/0dai-cli-$tag.tar.gz"
    exit 1
  fi
  if ! curl -fsSL -o "$archive.sha256" "$BUNDLE_BASE/0dai-cli-$tag.tar.gz.sha256"; then
    err "could not download the published checksum"
    exit 1
  fi
  expected=$(awk '{print $1; exit}' "$archive.sha256")
  actual=$(sha256_digest "$archive") || exit 1
  if [ -z "$expected" ] || [ "$expected" != "$actual" ]; then
    err "checksum mismatch for 0dai-cli-$tag.tar.gz (expected ${expected:-none}, got $actual)"
    exit 1
  fi
  say "checksum ok"

  target="$INSTALL_DIR/lib/0dai-$tag"
  mkdir -p "$INSTALL_DIR/lib" "$INSTALL_DIR/bin" || { err "could not create $INSTALL_DIR"; exit 1; }
  # Unpack beside the target, never into it. A truncated archive with a valid
  # checksum used to overwrite part of a working install before tar failed
  # (#8032). The staging directory sits in the same lib/ directory, so the
  # swap below is a rename on one filesystem.
  staging="$INSTALL_DIR/lib/.0dai-$tag.staging.$$"
  rm -rf "$staging"
  mkdir "$staging" || { err "could not create $staging"; exit 1; }
  if ! tar -xzf "$archive" -C "$staging" || [ ! -f "$staging/bin/0dai.js" ]; then
    err "could not unpack the CLI bundle; the existing installation was not changed"
    exit 1
  fi
  if [ -e "$target" ]; then
    previous="$INSTALL_DIR/lib/.0dai-$tag.previous.$$"
    rm -rf "$previous"
    if ! mv "$target" "$previous"; then
      err "could not move the existing $target aside; it was not changed"
      exit 1
    fi
  fi
  if ! mv "$staging" "$target"; then
    if [ -n "$previous" ]; then mv "$previous" "$target" || true; fi
    err "could not install $target; the previous version was restored"
    exit 1
  fi
  staging=""
  # The launcher is written to a temporary name and renamed into place, so a
  # failed write never leaves a truncated bin/0dai.
  launcher_tmp="$INSTALL_DIR/bin/.0dai.tmp.$$"
  if ! {
    printf '#!/bin/sh\n'
    printf 'exec node "%s/bin/0dai.js" "$@"\n' "$target"
  } > "$launcher_tmp" || ! chmod 0755 "$launcher_tmp" || ! mv -f "$launcher_tmp" "$INSTALL_DIR/bin/0dai"; then
    err "could not write the launcher $INSTALL_DIR/bin/0dai"
    exit 1
  fi
  # Cleanup last: a failing delete must not abort an install that already
  # produced a working launcher.
  launcher_tmp=""
  if [ -n "$previous" ]; then rm -rf "$previous" || true; fi
  previous=""
  rm -rf "$work" || true
  trap - EXIT HUP INT TERM
  say "$tag installed: $INSTALL_DIR/bin/0dai"
  case ":$PATH:" in
    *":$INSTALL_DIR/bin:"*) ;;
    *) say "add it to your PATH: export PATH=\"$INSTALL_DIR/bin:\$PATH\"" ;;
  esac
}

if [ "$SOURCE" = "bundle" ]; then
  install_from_bundle
  exit 0
fi

if [ -z "$REPO_URL" ]; then
  if [ "$SOURCE" = "auto" ]; then
    say "no repository URL is configured (ODAI_REPO_URL); using the published bundle instead"
    install_from_bundle
    exit 0
  fi
  err "the clone path needs ODAI_REPO_URL to point at a repository you have access to."
  err "use --source bundle for the published bundle."
  exit 1
fi

# git is needed only for the clone path. The published bundle needs no
# repository and no git at all.
require_git() {
  if ! command -v git >/dev/null 2>&1; then
    err "git is required to clone the repository."
    exit 1
  fi
}

require_git

if [ -d "$DEST/.git" ]; then
  say "updating existing checkout at $DEST (ref $VERSION)"
  update_existing
elif [ -e "$DEST" ]; then
  err "$DEST exists and is not a git checkout."
  exit 1
else
  say "cloning $REPO_URL at $VERSION into $DEST"
  if ! clone_fresh; then
    if [ "$SOURCE" = "auto" ]; then
      say "the repository is not reachable from here; using the published bundle instead"
      install_from_bundle
      exit 0
    fi
    exit 1
  fi
fi

bootstrap="$DEST/scripts/bootstrap/install.sh"
if [ -f "$bootstrap" ] && grep -q -- '--checkout-only' "$bootstrap"; then
  if ! command -v bash >/dev/null 2>&1; then
    err "bash is required to run scripts/bootstrap/install.sh. The clone is at $DEST."
    exit 1
  fi
  if bash "$bootstrap" \
    --prefix "$DEST" \
    --version "$VERSION" \
    --already-cloned \
    --checkout-only; then
    exit 0
  fi
  if [ "$SOURCE" = "auto" ]; then
    say "the bootstrap failed; using the published bundle instead"
    install_from_bundle
    exit 0
  fi
  exit 1
fi

say "Checkout is at $DEST (git ref $VERSION)."
say "OS packages were not installed."
say "This ref's scripts/bootstrap/install.sh has no flag to skip sudo apt-get, so first_run.py was not started."
say "first_run.py installs missing dependencies with sudo apt-get on Linux after a prompt (or immediately if ODAI_INSTALL_AUTOCONFIRM=1)."
say "Run that yourself if you want OS packages:"
say "  python3 $DEST/scripts/bootstrap/first_run.py --interactive"
