#!/bin/bash
# The one line to paste into a terminal:
#   /bin/bash -c "$(curl -fsSL https://docs.torta.app/install.sh)"
# It sets up Node, Claude Code and torta, then opens your browser.

# Add `-- --check` to the end of that line to only report what is
# already here and change nothing.

set -euo pipefail

TORTA_HOME="${HOME}/.torta"
NPM_PREFIX="${TORTA_HOME}/npm"
# The literal row appended to a profile, so it must not expand here.
# shellcheck disable=SC2016
PATH_LINE='export PATH="$HOME/.torta/npm/bin:$PATH"'
NODE_MIN=18
NODE_INDEX="https://nodejs.org/dist/index.json"
KERNEL="unknown"
OS="unknown"
ARCH="unknown"

# Colour only while a person is watching, so a saved log stays readable.
if [ -t 1 ]; then
  BOLD=$'\033[1m'
  RED=$'\033[31m'
  GREEN=$'\033[32m'
  OFF=$'\033[0m'
else
  BOLD=""
  RED=""
  GREEN=""
  OFF=""
fi

say() { printf '%s\n' "$*"; }

step() { printf '\n%s==>%s %s%s%s\n' "$GREEN" "$OFF" "$BOLD" "$*" "$OFF"; }

# Every error leaves by this door: what failed, then what to do next.
fail() {
  local line
  printf '\n%sSetup stopped.%s\n' "$RED" "$OFF" >&2
  for line in "$@"; do printf '%s\n' "$line" >&2; done
  printf '%s\n' "When that is sorted, paste the same line again." >&2
  exit 1
}

# Run one command as the administrator, or plainly when already root.
as_root() {
  if [ "$(id -u)" = "0" ]; then
    "$@"
  else
    sudo "$@"
  fi
}

detect_platform() {
  KERNEL="$(uname -s 2>/dev/null || printf 'unknown')"
  ARCH="$(uname -m 2>/dev/null || printf 'unknown')"
  case "$KERNEL" in
    Darwin) OS="mac" ;;
    Linux) OS="linux" ;;
    *) OS="other" ;;
  esac
}

platform_supported() {
  case "${OS}:${ARCH}" in
    mac:arm64|mac:x86_64) return 0 ;;
    linux:x86_64|linux:amd64|linux:aarch64|linux:arm64) return 0 ;;
    *) return 1 ;;
  esac
}

platform_name() {
  case "$OS" in
    mac) printf 'macOS on %s' "$ARCH" ;;
    linux) printf 'Linux on %s' "$ARCH" ;;
    *) printf '%s on %s' "$KERNEL" "$ARCH" ;;
  esac
}

# The first number of `node --version`, or 0 when there is no node at all.
node_major() {
  local raw major
  command -v node >/dev/null 2>&1 || { printf '0'; return 0; }
  raw="$(node --version 2>/dev/null || true)"
  major="$(printf '%s' "${raw#v}" | cut -d. -f1)"
  case "$major" in
    ''|*[!0-9]*) major=0 ;;
  esac
  printf '%s' "$major"
}

node_report() {
  local major
  major="$(node_major)"
  if [ "$major" = "0" ]; then
    printf 'not here yet'
  elif [ "$major" -ge "$NODE_MIN" ]; then
    printf 'here, version %s' "$(node --version)"
  else
    printf 'here but too old, version %s, %s or newer is needed' \
      "$(node --version)" "$NODE_MIN"
  fi
}

# Which file a new terminal window reads, by the shell this person uses.
shell_profile() {
  case "$(basename "${SHELL:-/bin/bash}")" in
    zsh) printf '%s' "${HOME}/.zshrc" ;;
    bash) printf '%s' "${HOME}/.bashrc" ;;
    *) printf '%s' "${HOME}/.profile" ;;
  esac
}

path_line_present() {
  local profile
  profile="$(shell_profile)"
  [ -f "$profile" ] || return 1
  grep -qF "$PATH_LINE" "$profile"
}

# The same order torta itself asks in: the supported answer first, then
# three quieter signs that somebody has signed in at some point.
claude_signed_in() {
  local status
  command -v claude >/dev/null 2>&1 || return 1
  status="$(claude auth status --json 2>/dev/null || true)"
  case "$status" in
    *'"loggedIn":true'*|*'"loggedIn": true'*) return 0 ;;
  esac
  if [ -n "${ANTHROPIC_API_KEY:-}" ] || [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]; then
    return 0
  fi
  if grep -qi 'token' "${HOME}/.claude/.credentials.json" 2>/dev/null; then
    return 0
  fi
  if [ "$OS" = "mac" ] && security find-generic-password \
      -s "Claude Code-credentials" >/dev/null 2>&1; then
    return 0
  fi
  return 1
}

# Where a global install lands right now, as npm itself answers.
npm_prefix_now() {
  command -v npm >/dev/null 2>&1 || { printf ''; return 0; }
  npm config get prefix 2>/dev/null || printf ''
}

# Whether this person can write where global installs land. The folder that
# actually receives a package is `lib/node_modules` under the prefix; before
# the first global install it may not exist, so the prefix itself answers.
prefix_writable() {
  local current target
  current="$(npm_prefix_now)"
  [ -n "$current" ] || return 1
  target="${current}/lib/node_modules"
  [ -d "$target" ] || target="$current"
  [ -d "$target" ] || return 1
  [ -w "$target" ]
}

# -- step 1 --

check_platform() {
  step "Step 1 of 5: checking what kind of computer this is"
  if platform_supported; then
    say "This is $(platform_name), which torta knows how to set up."
    return 0
  fi
  fail "Torta sets itself up on macOS and on common Linux computers." \
    "This one says it is $KERNEL on $ARCH, which this script cannot handle." \
    "On Windows, install the Windows Subsystem for Linux, open Ubuntu," \
    "and paste the same line there."
}

# -- step 2 --

install_node_with_brew() {
  say "Homebrew is here, so Node comes from Homebrew. This takes a few minutes."
  brew install node || fail \
    "Homebrew could not install Node." \
    "Run 'brew update' on its own, wait for it to finish, then try again."
}

# The official installer, when there is no Homebrew. Ask nodejs.org which
# release is current rather than baking a number in that goes stale.
install_node_pkg() {
  local index version url tmp pkg
  say "Downloading the official Node installer from nodejs.org."
  index="$(curl -fsSL "$NODE_INDEX")" || fail \
    "Could not reach nodejs.org to find the current version of Node." \
    "Check that this computer is online, then try again."
  version="$(printf '%s' "$index" | tr '{' '\n' | grep '"lts":"' \
    | sed -n '1s/.*"version":"\(v[0-9][0-9.]*\)".*/\1/p')"
  [ -n "$version" ] || fail \
    "nodejs.org answered, but not in a shape this script understands." \
    "Install Node by hand from nodejs.org, then paste the same line again."
  url="https://nodejs.org/dist/${version}/node-${version}.pkg"
  tmp="$(mktemp -d)"
  pkg="${tmp}/node.pkg"
  curl -fsSL -o "$pkg" "$url" || fail \
    "The download of Node ${version} did not finish." \
    "Check that this computer is online, then try again." \
    "If it keeps failing, download Node from nodejs.org and open it yourself."
  say ""
  say "Your Mac is about to ask for the password you use to log in."
  say "It is asking because installing Node touches a shared folder."
  say "Nothing appears on screen while you type it. That is normal."
  say "Type it, press Enter, and wait."
  as_root installer -pkg "$pkg" -target / || fail \
    "The Node installer did not finish." \
    "The usual reason is a mistyped password. Try the same line again." \
    "You can also download Node from nodejs.org and open the file yourself."
  rm -rf "$tmp"
  export PATH="/usr/local/bin:${PATH}"
}

# Distro package managers, in the order a machine is likely to have one.
install_node_linux() {
  say "Installing Node with this computer's own package manager."
  say "It may ask for your password so it can install for everyone."
  if command -v apt-get >/dev/null 2>&1; then
    as_root apt-get update
    as_root apt-get install -y nodejs npm
  elif command -v dnf >/dev/null 2>&1; then
    as_root dnf install -y nodejs npm
  elif command -v yum >/dev/null 2>&1; then
    as_root yum install -y nodejs npm
  elif command -v zypper >/dev/null 2>&1; then
    as_root zypper install -y nodejs npm
  elif command -v pacman >/dev/null 2>&1; then
    as_root pacman -Sy --noconfirm nodejs npm
  elif command -v apk >/dev/null 2>&1; then
    as_root apk add --no-cache nodejs npm
  else
    fail "This Linux has none of the package managers this script knows." \
      "Install Node ${NODE_MIN} or newer from nodejs.org, then try again."
  fi
}

ensure_node() {
  step "Step 2 of 5: making sure Node is on this computer"
  if [ "$(node_major)" -ge "$NODE_MIN" ]; then
    say "Node $(node --version) is already here, so there is nothing to do."
    return 0
  fi
  say "Node is what torta runs on, and it is not here yet, so it goes in now."
  if command -v brew >/dev/null 2>&1; then
    install_node_with_brew
  elif [ "$OS" = "mac" ]; then
    install_node_pkg
  else
    install_node_linux
  fi
  hash -r 2>/dev/null || true
  if [ "$(node_major)" -lt "$NODE_MIN" ]; then
    fail "Node still is not here, or the version installed is too old." \
      "Open nodejs.org, take the big green download button, open the file" \
      "that arrives, and click through it. Then paste the same line again."
  fi
  command -v npm >/dev/null 2>&1 || fail \
    "Node arrived but npm, the part that fetches packages, did not." \
    "Install Node again from nodejs.org, which always brings npm with it."
  say "Node $(node --version) is ready."
}

# -- step 3 --

# A prefix this person already owns is not a problem, so it is left alone.
# The rescue below is only for the root owned prefix that makes a global
# install fail with EACCES, and it is the whole reason this step exists.
setup_prefix() {
  local profile current
  step "Step 3 of 5: checking where the tools are allowed to install"
  current="$(npm_prefix_now)"
  if prefix_writable; then
    say "Tools already install into ${current}, which you can write to."
    say "That works, so nothing here is changed and nothing is added anywhere."
    export PATH="${current}/bin:${PATH}"
    hash -r 2>/dev/null || true
    return 0
  fi
  say "Right now tools would install into ${current:-a folder npm did not name},"
  say "which this account cannot write to. That is the folder that makes an"
  say "install stop with a permission error, so torta uses one you own instead."
  mkdir -p "${NPM_PREFIX}/bin"
  npm config set prefix "$NPM_PREFIX" >/dev/null || fail \
    "Could not point npm at ${NPM_PREFIX}." \
    "Run this yourself, then try again:" \
    "  npm config set prefix ${NPM_PREFIX}"
  say "Tools will now install into ${NPM_PREFIX}, which you own."
  export PATH="${NPM_PREFIX}/bin:${PATH}"
  hash -r 2>/dev/null || true
  profile="$(shell_profile)"
  if path_line_present; then
    say "Your ${profile} already points at that folder, so it is left alone."
    return 0
  fi
  printf '\n%s\n' "$PATH_LINE" >> "$profile" || fail \
    "Could not write to ${profile}." \
    "Add this one line to that file yourself, then try again:" \
    "$PATH_LINE"
  say "One line was added to the end of ${profile}:"
  say "  ${PATH_LINE}"
  say "That is how a brand new terminal window will find these tools."
}

# -- step 4 --

ensure_claude() {
  step "Step 4 of 5: setting up the AI that does the work"
  if claude_signed_in; then
    say "Claude Code is here and already signed in, so this step is skipped."
    return 0
  fi
  if ! command -v claude >/dev/null 2>&1; then
    say "Installing Claude Code. A lot of text will scroll past. Let it."
    npm install -g @anthropic-ai/claude-code || fail \
      "Claude Code did not install." \
      "The usual reason is a dropped connection. Try the same line again."
    hash -r 2>/dev/null || true
  fi
  command -v claude >/dev/null 2>&1 || fail \
    "Claude Code installed but this terminal cannot find it." \
    "Close this window, open a new one, and paste the same line again."
  say ""
  say "Claude Code now asks you to sign in."
  say "A browser window opens by itself. Sign in with your Anthropic account."
  say "Then come back to this terminal window, where the setup carries on."
  say "If it asks anything else, press Enter to take the answer it suggests."
  claude || true
  if ! claude_signed_in; then
    say ""
    say "This computer cannot tell whether the sign in worked."
    say "The setup carries on. If an agent later fails to start, type"
    say "  claude"
    say "on its own, sign in, then type /exit."
  fi
}

# -- step 5 --

start_torta() {
  step "Step 5 of 5: starting torta"
  say "Your browser is about to open on the torta setup questions."
  say "Everything from here is clicking. You are done with typing."
  say "To stop torta later, come back to this window and press Control and C."
  say ""
  npx -y torta-ai start
}

# -- the --check report --

report() {
  local profile current
  profile="$(shell_profile)"
  printf '\n%storta setup check%s\n\n' "$BOLD" "$OFF"
  printf '  %-18s %s\n' "Computer" \
    "$(platform_name)$(platform_supported && printf ', supported' \
      || printf ', NOT supported by this script')"
  printf '  %-18s %s\n' "Node" "$(node_report)"
  if command -v npm >/dev/null 2>&1; then
    printf '  %-18s %s\n' "npm" "here, version $(npm --version)"
  else
    printf '  %-18s %s\n' "npm" "not here yet"
  fi
  current="$(npm_prefix_now)"
  if [ -z "$current" ]; then
    printf '  %-18s %s\n' "Tools install into" \
      "nowhere yet, because npm is not here to say"
    printf '  %-18s %s\n' "Your own folder" "${NPM_PREFIX}, not needed so far"
    printf '  %-18s %s\n' "PATH line" "not in ${profile}, and not needed so far"
  elif prefix_writable; then
    printf '  %-18s %s\n' "Tools install into" \
      "${current}, which you can write to, so nothing needs changing"
    if [ -d "$NPM_PREFIX" ]; then
      printf '  %-18s %s\n' "Your own folder" \
        "${NPM_PREFIX} exists, and is not being used"
    else
      printf '  %-18s %s\n' "Your own folder" \
        "${NPM_PREFIX} was never needed, so it was never made"
    fi
    if path_line_present; then
      printf '  %-18s %s\n' "PATH line" "in ${profile}, and doing no harm"
    else
      printf '  %-18s %s\n' "PATH line" "not in ${profile}, and not needed"
    fi
  else
    printf '  %-18s %s\n' "Tools install into" \
      "${current}, which you cannot write to, so the setup switches"
    if [ -d "$NPM_PREFIX" ]; then
      printf '  %-18s %s\n' "Your own folder" "${NPM_PREFIX}, made already"
    else
      printf '  %-18s %s\n' "Your own folder" \
        "${NPM_PREFIX}, which the setup makes and installs into"
    fi
    if path_line_present; then
      printf '  %-18s %s\n' "PATH line" "already in ${profile}"
    else
      printf '  %-18s %s\n' "PATH line" "the setup adds it to ${profile}"
    fi
  fi
  if command -v claude >/dev/null 2>&1; then
    if claude_signed_in; then
      printf '  %-18s %s\n' "Claude Code" "here and signed in"
    else
      printf '  %-18s %s\n' "Claude Code" "here, but not signed in"
    fi
  else
    printf '  %-18s %s\n' "Claude Code" "not here yet"
  fi
  if command -v torta >/dev/null 2>&1; then
    printf '  %-18s %s\n' "torta" "here, at $(command -v torta)"
  else
    printf '  %-18s %s\n' "torta" "not installed, npx fetches it when it runs"
  fi
  printf '\n%s\n' "Nothing was changed by this check."
}

main() {
  detect_platform
  case "${1:-}" in
    --check|-c)
      report
      return 0
      ;;
    --help|-h)
      say "Sets up Node, Claude Code and torta, then opens your browser."
      say ""
      say "  install.sh           do the setup"
      say "  install.sh --check   say what is here and change nothing"
      return 0
      ;;
    "") ;;
    *)
      fail "This script does not know the option '${1}'." \
        "Use no option at all to set everything up, or --check to look only."
      ;;
  esac

  printf '%sSetting up torta.%s\n' "$BOLD" "$OFF"
  say "Five short steps. Each one says what it is doing before it does it."
  say "Anything already on this computer is left exactly as it is."
  check_platform
  ensure_node
  setup_prefix
  ensure_claude
  start_torta
}

main "$@"
