#!/usr/bin/env bash
#
# Lotics CLI installer — macOS, Linux, WSL.
#
#   curl -fsSL https://lotics.ai/install.sh | bash
#
# Downloads one compiled executable. No Node.js, no npm, no package manager:
# the person this is written for does not have any of them, and telling them to
# go and get one is the first wall in a flow whose promise is five minutes from
# nothing.
#
# Everything is defined before `main` runs at the very end, because this script
# is piped into a shell — a connection that drops mid-transfer would otherwise
# execute half an installer. Truncated here, bash defines some functions, calls
# nothing, and exits.

set -euo pipefail

DOWNLOADS="${LOTICS_DOWNLOADS_URL:-https://downloads.lotics.ai/cli}"
BIN_DIR="${LOTICS_BIN_DIR:-$HOME/.local/bin}"
TARGET="$BIN_DIR/lotics"

say() { printf '%s\n' "$*" >&2; }
die() { printf 'lotics install: %s\n' "$*" >&2; exit 1; }

need() {
  command -v "$1" >/dev/null 2>&1 || die "\`$1\` is required to install and was not found."
}

is_musl() {
  # `ldd --version` writes to stderr on glibc and exits non-zero on musl, so both
  # streams are read and the status ignored — the string is the signal.
  (ldd --version 2>&1 || true) | head -1 | grep -qi musl
}

# ── Which build. The name matches a key in the release manifest, so an
#    unsupported machine is refused by NAME rather than by a 404 on a URL the
#    user never saw.
#
#    musl is a separate build, not a variant: a glibc executable does not start
#    at all on Alpine — the dynamic loader fails before any of our code — so the
#    distinction has to be drawn here, where it can still be acted on.
detect_platform() {
  local os arch
  case "$(uname -s)" in
    Darwin) os=darwin ;;
    Linux) os=linux ;;
    *) die "unsupported OS \`$(uname -s)\`. macOS, Linux and WSL are supported. On native Windows, run this inside WSL." ;;
  esac
  case "$(uname -m)" in
    x86_64 | amd64) arch=x64 ;;
    arm64 | aarch64) arch=arm64 ;;
    *) die "unsupported architecture \`$(uname -m)\`. x86_64 and arm64 are supported." ;;
  esac
  if [ "$os" = linux ] && [ "$arch" = x64 ] && is_musl; then
    printf 'linux-x64-musl'
    return
  fi
  printf '%s-%s' "$os" "$arch"
}

sha256_of() {
  if command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | awk '{print $1}'
  elif command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{print $1}'
  else
    die "neither \`shasum\` nor \`sha256sum\` is available — cannot verify the download."
  fi
}

# ── Read one platform's entry out of the manifest without a JSON parser, since
#    the machine may have none. Anchored to the platform key so a value can never
#    be picked up from a neighbouring block.
manifest_field() {
  local manifest="$1" platform="$2" field="$3"
  printf '%s' "$manifest" | tr -d ' \n' \
    | grep -o "\"${platform}\":{[^}]*}" \
    | grep -o "\"${field}\":\"[^\"]*\"" \
    | head -1 | cut -d'"' -f4
}

main() {
  need curl
  # The artifact is gzipped — an executable is ~83 MB of runtime and halves over
  # the wire. Named here so a machine without it is told that, rather than
  # failing after a 36 MB download.
  need gzip

  local platform version manifest file sha actual tmp
  platform="$(detect_platform)"

  version="${LOTICS_VERSION:-}"
  if [ -z "$version" ]; then
    version="$(curl -fsSL "$DOWNLOADS/latest" 2>/dev/null | tr -d '[:space:]')" \
      || die "could not reach $DOWNLOADS/latest.

  If this is an agent sandbox or a locked-down network, these hosts have to be
  reachable — allow all three, since each is a different step:
    lotics.ai            this installer
    downloads.lotics.ai  the executable it downloads
    api.lotics.ai        everything the CLI does afterwards
  Otherwise check your connection."
  fi
  [ -n "$version" ] || die "the release channel named no version. Try again shortly, or see https://lotics.ai/docs/cli."

  # Already current? Then this is a no-op, and saying so beats re-downloading
  # tens of megabytes to arrive at the same bytes. That is what makes "run this
  # even if it is installed" honest advice rather than a toll — an agent
  # following a pasted prompt should never have to decide whether to skip.
  #
  # A binary that cannot report a version does not match anything here, so a
  # corrupt or half-written one reinstalls rather than being mistaken for current.
  if [ -x "$TARGET" ] && [ "$("$TARGET" --version 2>/dev/null | tr -d '[:space:]')" = "$version" ]; then
    say "Lotics CLI $version is already installed at $TARGET."
    # The version match proves the FILE is current. It says nothing about whether
    # `lotics` RESOLVES, and those are different questions: a shell opened before
    # the first install carries a stale PATH forever. Skipping straight out left
    # the caller reading "already installed" and then "command not found" — on the
    # re-run this script now tells every agent to make.
    report_path
    return 0
  fi

  manifest="$(curl -fsSL "$DOWNLOADS/$version/manifest.json")" \
    || die "no release manifest for version $version at $DOWNLOADS."

  file="$(manifest_field "$manifest" "$platform" file)"
  sha="$(manifest_field "$manifest" "$platform" sha256)"
  [ -n "$file" ] && [ -n "$sha" ] \
    || die "release $version publishes no build for \`$platform\`. See https://lotics.ai/docs/cli."

  say "Downloading Lotics CLI $version for $platform…"
  mkdir -p "$BIN_DIR"
  tmp="$(mktemp "$BIN_DIR/.lotics.XXXXXX")"
  # Any exit from here removes the partial files, so a failed install never
  # leaves something on PATH that is not a working CLI.
  trap 'rm -f "$tmp" "$tmp.bin"' EXIT

  curl -fsSL "$DOWNLOADS/$version/$file" -o "$tmp" \
    || die "could not download $DOWNLOADS/$version/$file."

  actual="$(sha256_of "$tmp")"
  # The executable is about to run as this user on every later `lotics`, so one
  # that arrived wrong is not installed at all.
  [ "$actual" = "$sha" ] \
    || die "checksum mismatch for $file (expected $sha, got $actual). Nothing was installed."

  # The checksum above covers the compressed bytes; gzip's own CRC32 covers the
  # executable that comes out of them, so a stream that passes both is verified
  # end to end.
  gzip -dc "$tmp" > "$tmp.bin" \
    || die "the download for $platform did not decompress. Nothing was installed."
  # The archive is done with, and it is the larger of the two: leaving it parks
  # tens of megabytes in the bin directory on every successful install, where the
  # EXIT trap no longer reaches it once the install commits.
  rm -f "$tmp"

  chmod +x "$tmp.bin"
  # Replaced by rename, which is atomic on one filesystem: a concurrent `lotics`
  # runs either the old binary or the new one, never a half-written file.
  mv -f "$tmp.bin" "$TARGET"
  trap - EXIT

  local reported
  reported="$("$TARGET" --version 2>/dev/null || true)"
  [ -n "$reported" ] \
    || die "installed to $TARGET, but it did not run. Report this at https://lotics.ai/docs/cli."

  say ""
  say "Lotics CLI $reported installed to $TARGET"
  report_path
}

# Whether `lotics` will actually resolve, and what to do when it will not.
#
# Called from BOTH exits — the fresh install and the already-current one —
# because the question it answers is about the SHELL, not about the download.
report_path() {
  case ":$PATH:" in
    *":$BIN_DIR:"*)
      say ""
      say "Next:  lotics model apply model.json   # example models: https://lotics.ai/presets/index.json"
      ;;
    *)
      say ""
      say "$BIN_DIR is not on your PATH — this shell will not find \`lotics\`."
      say "For this shell:  export PATH=\"$BIN_DIR:\$PATH\""
      say "For new ones:    echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.profile"
      say ""
      say "Or run it directly:  $TARGET model apply model.json"
      ;;
  esac
}

main "$@"
