#!/bin/sh
# Alkera CLI installer.
#   curl -fsSL https://releases.alkera.ai/install.sh | sh

set -eu

BASE_URL="${ALKERA_RELEASE_BASE_URL:-${ALKERA_INSTALL_BASE_URL:-https://releases.alkera.ai}}"
BASE_URL="${BASE_URL%/}"

# Transfers stay on https through any redirect. The one exception is an
# explicit loopback override (the test harness serves its fixture CDN on
# 127.0.0.1): loopback traffic never leaves the host, so plain http there
# defeats no transport protection.
case "$BASE_URL" in
  http://127.0.0.1:* | http://127.0.0.1/* | http://localhost:* | http://localhost/*)
    CURL_PROTO="=http,https" ;;
  *)
    CURL_PROTO="=https" ;;
esac
CHANNEL="${ALKERA_CHANNEL:-stable}"
ALKERA_HOME="${ALKERA_HOME:-$HOME/.alkera}"
INSTALL_DIR="${ALKERA_INSTALL_DIR:-$ALKERA_HOME/bin}"

PATH_BEGIN="# >>> alkera installer (PATH) >>>"
PATH_END="# <<< alkera installer (PATH) <<<"

workdir=""
stage_dir=""

info() { printf '%s\n' "alkera: $*"; }
err() { printf '%s\n' "alkera: error: $*" >&2; }
die() { err "$*"; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }

# Network waits are bounded without ever capping a slow-but-flowing transfer:
# connect gets 15s, and the transfer only dies after 120s of NO progress
# (curl: <1 byte/s for 120s; wget: 120s read timeout). No total deadline — a
# large artifact on a slow link takes as long as it takes.
download_once() {
  if have curl; then
    # -q ignores $HOME/.curlrc / $CURL_HOME (a prior CI step could plant one to
    # alter TLS or routing).
    curl -q -fsSL --proto "$CURL_PROTO" --proto-redir "$CURL_PROTO" --connect-timeout 15 --speed-limit 1 --speed-time 120 "$1" -o "$2"
  elif have wget; then
    wget -q --tries=1 --connect-timeout=15 --read-timeout=120 "$1" -O "$2"
  else
    die "curl or wget is required"
  fi
}

download() {
  # Shell-level retries cover the classes curl's own --retry does not
  # (mid-body resets, DNS blips) and work on any curl/wget version.
  for attempt in 1 2 3; do
    download_once "$1" "$2" && return 0
    if [ "$attempt" -lt 3 ]; then
      info "download failed; retrying ($attempt/2)"
      sleep "$attempt"
    fi
  done
  return 1
}

# The big-artifact variant: identical timeout/retry policy, but renders a live
# progress bar when stderr is a terminal. Progress goes to stderr, so a
# `curl -fsSL ... | sh` pipeline keeps a clean stdout; CI logs and pipes see
# nothing extra. wget only gets --show-progress where it exists (busybox lacks it).
download_progress_once() {
  if [ -t 2 ]; then
    if have curl; then
      curl -q -fSL --progress-bar --proto "$CURL_PROTO" --proto-redir "$CURL_PROTO" --connect-timeout 15 --speed-limit 1 --speed-time 120 "$1" -o "$2"
      return
    fi
    if have wget && wget --help 2>&1 | grep -q -- --show-progress; then
      wget -q --show-progress --tries=1 --connect-timeout=15 --read-timeout=120 "$1" -O "$2"
      return
    fi
  fi
  download_once "$1" "$2"
}

download_progress() {
  for attempt in 1 2 3; do
    download_progress_once "$1" "$2" && return 0
    if [ "$attempt" -lt 3 ]; then
      info "download failed; retrying ($attempt/2)"
      sleep "$attempt"
    fi
  done
  return 1
}

sha256_of() {
  if have sha256sum; then
    sha256sum "$1" | awk '{print $1}'
  elif have shasum; then
    shasum -a 256 "$1" | awk '{print $1}'
  else
    die "sha256sum or shasum is required"
  fi
}

# Prove a binary runs, without letting a broken one hang the installer: no
# stdin, and a watchdog kills it after ALKERA_SMOKE_TIMEOUT (default 60s).
# The watchdog is fully detached from our stdio — its sleep child survives the
# subshell kill, and if it inherited our pipes it would hold a `curl | sh`
# caller's capture open until the sleep expired.
smoke_run() {
  "$1" --version >/dev/null 2>&1 </dev/null &
  smoke_pid=$!
  (
    sleep "${ALKERA_SMOKE_TIMEOUT:-60}"
    kill -9 "$smoke_pid" 2>/dev/null
  ) >/dev/null 2>&1 </dev/null &
  smoke_watchdog=$!
  smoke_rc=0
  wait "$smoke_pid" || smoke_rc=$?
  kill "$smoke_watchdog" 2>/dev/null || true
  wait "$smoke_watchdog" 2>/dev/null || true
  return "$smoke_rc"
}

detect_target() {
  os="$(uname -s | tr '[:upper:]' '[:lower:]')"
  arch="$(uname -m)"
  case "$os" in
    darwin) os="darwin" ;;
    linux) os="linux" ;;
    mingw* | msys* | cygwin*) die "on Windows, run: irm ${BASE_URL}/install.ps1 | iex" ;;
    *) die "unsupported OS: $os" ;;
  esac
  case "$arch" in
    arm64 | aarch64) arch="arm64" ;;
    x86_64 | amd64) arch="x64" ;;
    *) die "unsupported architecture: $arch" ;;
  esac
  printf '%s-%s\n' "$os" "$arch"
}

json_top_string() {
  if have python3; then
    python3 -c 'import json,sys; v=json.load(open(sys.argv[1])).get(sys.argv[2],""); print(v if v is not None else "")' "$1" "$2"
  else
    sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\{0,1\}\([^\",}]*\)\"\{0,1\}.*/\1/p" "$1" | head -n 1
  fi
}

json_target_field() {
  if have python3; then
    python3 -c 'import json,sys; t=json.load(open(sys.argv[1])).get("targets",{}).get(sys.argv[2]) or {}; print(t.get(sys.argv[3],"") or "")' "$1" "$2" "$3"
  else
    awk -v target="\"$2\"" -v field="\"$3\"" '
      index($0, target) { cap = 1 }
      cap { buf = buf $0 " "; if (index($0, "}")) { cap = 0; done = 1 } }
      done { exit }
      END {
        n = index(buf, field); if (n == 0) exit
        rest = substr(buf, n + length(field))
        sub(/^[[:space:]]*:[[:space:]]*"/, "", rest)
        q = index(rest, "\""); if (q > 0) print substr(rest, 1, q - 1)
      }' "$1"
  fi
}

resolve_url() {
  case "$1" in
    http://* | https://*) printf '%s\n' "$1" ;;
    /*) printf '%s\n' "$BASE_URL$1" ;;
    *) printf '%s\n' "$BASE_URL/$1" ;;
  esac
}

profile_for_shell() {
  case "${SHELL:-}" in
    */zsh) printf '%s\n' "$HOME/.zshrc" ;;
    */fish) printf '%s\n' "$HOME/.config/fish/config.fish" ;;
    */bash)
      if [ -f "$HOME/.bash_profile" ] && [ ! -f "$HOME/.bashrc" ]; then
        printf '%s\n' "$HOME/.bash_profile"
      else
        printf '%s\n' "$HOME/.bashrc"
      fi
      ;;
    *) printf '%s\n' "$HOME/.profile" ;;
  esac
}

on_path() {
  case ":$PATH:" in
    *":$INSTALL_DIR:"*) return 0 ;;
    *) return 1 ;;
  esac
}

add_to_path() {
  profile="$(profile_for_shell)"
  mkdir -p "$(dirname "$profile")"
  [ -f "$profile" ] || : >"$profile"
  if grep -qF "$PATH_BEGIN" "$profile" 2>/dev/null; then
    # A stanza from a previous install with a different dir must not shadow
    # this one; rewrite it rather than skipping.
    grep -qF "$INSTALL_DIR" "$profile" 2>/dev/null && return 0
    remove_from_path
  fi
  case "$profile" in
    */config.fish) line="set -gx PATH \"$INSTALL_DIR\" \$PATH" ;;
    *) line="export PATH=\"$INSTALL_DIR:\$PATH\"" ;;
  esac
  printf '%s\n%s\n%s\n' "$PATH_BEGIN" "$line" "$PATH_END" >>"$profile"
  info "added $INSTALL_DIR to PATH in $profile"
}

remove_from_path() {
  # rp_profile, not profile: sh has no locals, and add_to_path's $profile must
  # survive this loop when it calls in for the stale-stanza rewrite.
  for rp_profile in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile" "$HOME/.config/fish/config.fish"; do
    [ -f "$rp_profile" ] || continue
    grep -qF "$PATH_BEGIN" "$rp_profile" 2>/dev/null || continue
    tmp="$(mktemp)"
    awk -v b="$PATH_BEGIN" -v e="$PATH_END" '
      $0 == b { skip = 1; next } $0 == e { skip = 0; next } skip != 1 { print }' "$rp_profile" >"$tmp"
    mv "$tmp" "$rp_profile"
  done
}

write_marker() {
  mkdir -p "$ALKERA_HOME"
  cat >"$ALKERA_HOME/install.json" <<JSON
{
  "method": "installer",
  "version": "$1",
  "channel": "$CHANNEL",
  "bin": "$INSTALL_DIR/alkera",
  "base_url": "$BASE_URL"
}
JSON
}

# The bin dir to remove on uninstall: explicit env wins, then the location the
# marker recorded at install time, then the default.
uninstall_dir() {
  if [ -n "${ALKERA_INSTALL_DIR:-}" ]; then
    printf '%s\n' "$ALKERA_INSTALL_DIR"
    return
  fi
  if [ -f "$ALKERA_HOME/install.json" ]; then
    bin="$(json_top_string "$ALKERA_HOME/install.json" bin)"
    if [ -n "$bin" ]; then
      printf '%s\n' "${bin%/*}"
      return
    fi
  fi
  printf '%s\n' "$ALKERA_HOME/bin"
}

do_uninstall() {
  dir="$(uninstall_dir)"
  rm -rf "$dir" "$ALKERA_HOME/install.json"
  remove_from_path
  info "uninstalled ($dir removed). ~/.alkera/ (auth, preferences) left intact."
}

# Copy the (possibly external) tarball into the private workdir BEFORE hashing, so
# verify-and-extract only ever touch a path under mktemp -d (mode 0700, unpredictable
# name), never the shared, well-known ALKERA_ARTIFACT path. A swap on that cache path
# during the copy just yields bytes whose sha won't match, caught by the verify below.
# This does NOT make the copy immutable: a process running as the SAME runner UID can
# still discover the workdir path and race the reopen between hash and tar. That
# same-UID case is out of the threat model (see the gate action README) -- it requires
# a hostile step already in the customer's own job, a job-level compromise no composite
# action can undo. A tarball already inside the workdir is hashed in place.
stage_for_verification() {
  case "$tarball" in
    "$workdir"/*) verified="$tarball" ;;
    *)
      verified="$workdir/verified.tar.gz"
      cp "$tarball" "$verified" || die "could not stage the artifact for verification"
      ;;
  esac
}

do_install() {
  target="$(detect_target)"

  if [ -n "${ALKERA_VERSION:-}" ]; then
    version="$ALKERA_VERSION"
    manifest_url="$BASE_URL/cli/v$version/manifest.json"
  else
    manifest_url="$BASE_URL/cli/$CHANNEL.json"
  fi

  workdir="$(mktemp -d)"
  trap 'rm -rf "$workdir" ${stage_dir:+"$stage_dir"}' EXIT INT TERM
  # The GC's age cutoff: 60s BEFORE this install started (stamped once; the
  # workdir's own mtime moves as downloads land in it). The margin closes the
  # sub-second window where a concurrent installer's just-created dir could
  # still compare as "older" and get deleted. Falls back to "now" if neither
  # date dialect works — strictly safer (GC keeps more).
  age_ref="$workdir/.age-ref"
  : >"$age_ref"
  # Local time on purpose: touch -t interprets its stamp in local time.
  cutoff="$(date -d '60 seconds ago' +%Y%m%d%H%M.%S 2>/dev/null \
    || date -v-60S +%Y%m%d%H%M.%S 2>/dev/null || true)"
  [ -n "$cutoff" ] && touch -t "$cutoff" "$age_ref" 2>/dev/null || true

  if [ -n "${ALKERA_VERSION:-}" ]; then
    info "fetching the v$ALKERA_VERSION release manifest"
  else
    info "resolving the latest $CHANNEL version"
  fi
  manifest="$workdir/manifest.json"
  if [ -n "${ALKERA_VERSION:-}" ]; then
    # A pinned version either exists or it doesn't — don't retry a 404.
    download_once "$manifest_url" "$manifest" \
      || die "version $ALKERA_VERSION not found (no manifest at $manifest_url)"
  else
    download "$manifest_url" "$manifest" || die "could not fetch $manifest_url"
  fi

  schema="$(json_top_string "$manifest" schema)"
  if [ -n "$schema" ] && [ "$schema" != "1" ]; then
    die "release manifest uses format $schema, which this installer doesn't understand — re-run: curl -fsSL $BASE_URL/install.sh | sh"
  fi

  [ -n "${ALKERA_VERSION:-}" ] || version="$(json_top_string "$manifest" version)"
  [ -n "$version" ] || die "could not resolve a version from $manifest_url"

  rel_url="$(json_target_field "$manifest" "$target" url)"
  expected_sha="$(json_target_field "$manifest" "$target" sha256)"
  [ -n "$rel_url" ] || die "no $target build in release $version"
  [ -n "$expected_sha" ] || die "manifest has no checksum for $target"

  info "installing alkera $version ($target)"
  # The manifest pins the artifact size; announce it so a multi-minute download
  # doesn't look like a hang. The awk fallback parser can't read unquoted JSON
  # numbers, so tolerate a non-numeric value by omitting the size.
  size="$(json_target_field "$manifest" "$target" size)"
  case "$size" in
    '' | *[!0-9]*) info "downloading alkera $version" ;;
    *) info "downloading alkera $version ($((size / 1048576)) MB)" ;;
  esac
  # ALKERA_ARTIFACT lets a caller keep the release tarball somewhere of its own
  # (the CI action caches it there, so a re-run skips the download). It is a
  # location, not a claim: a file already sitting there is reused, and then it
  # faces the same manifest checksum below as a fresh download does. A stale or
  # half-written cache (an interrupted download leaves a partial file) fails that
  # checksum and is re-fetched, not fatal -- so it can never wedge later runs.
  tarball="$workdir/alkera.tar.gz"
  if [ -n "${ALKERA_ARTIFACT:-}" ]; then
    tarball="$ALKERA_ARTIFACT"
    mkdir -p "$(dirname "$tarball")"
  fi
  reused=0
  if [ -s "$tarball" ]; then
    info "reusing the release artifact at $tarball"
    reused=1
  else
    download_progress "$(resolve_url "$rel_url")" "$tarball" || die "download failed"
  fi

  stage_for_verification
  info "verifying checksum"
  actual_sha="$(sha256_of "$verified")"
  if [ "$actual_sha" != "$expected_sha" ] && [ "$reused" -eq 1 ]; then
    # A REUSED cache that mismatches is stale or partial: discard it, fetch fresh to
    # the same path, and re-verify. A FRESH download that mismatches falls straight
    # through to the die below -- self-heal is not a checksum bypass.
    info "cached artifact failed checksum; re-downloading"
    rm -f "$tarball"
    download_progress "$(resolve_url "$rel_url")" "$tarball" || die "download failed"
    stage_for_verification
    actual_sha="$(sha256_of "$verified")"
  fi
  [ "$actual_sha" = "$expected_sha" ] || die "checksum mismatch (expected $expected_sha, got $actual_sha)"

  info "extracting"
  extract="$workdir/extract"
  mkdir -p "$extract"
  tar -xzf "$verified" -C "$extract" || die "extract failed"
  [ -x "$extract/alkera.dist/alkera" ] || die "malformed artifact: alkera.dist/alkera missing"

  if [ -e "$INSTALL_DIR/alkera" ] && [ -d "$INSTALL_DIR/alkera" ] && [ ! -L "$INSTALL_DIR/alkera" ]; then
    die "$INSTALL_DIR/alkera exists and is a directory; remove it and re-run"
  fi

  # Stage on the same filesystem as the install dir so the final move is an
  # atomic rename, then prove the binary runs BEFORE touching the live install.
  mkdir -p "$INSTALL_DIR"
  stage_dir="$INSTALL_DIR/.stage-$$"
  rm -rf "$stage_dir"
  mkdir -p "$stage_dir"
  mv "$extract/alkera.dist" "$stage_dir/alkera.dist"
  if [ -f "$extract/VERSION" ]; then
    mv "$extract/VERSION" "$stage_dir/VERSION"
  else
    printf '%s\n' "$version" >"$stage_dir/VERSION"
  fi
  chmod +x "$stage_dir/alkera.dist/alkera" 2>/dev/null || true
  info "checking the binary runs"
  smoke_run "$stage_dir/alkera.dist/alkera" \
    || die "downloaded binary failed to run (or hung) on this system; nothing was changed"

  # Reinstalling a version that's already on disk must never delete it in
  # place (the stable symlink would dangle if we died mid-way) — land in a
  # fresh sibling dir instead and let GC reap the old one after the repoint.
  version_dir="$INSTALL_DIR/$version"
  [ -e "$version_dir" ] && version_dir="$INSTALL_DIR/$version-r$$"
  mv "$stage_dir" "$version_dir"
  stage_dir=""

  prev_link="$(readlink "$INSTALL_DIR/alkera" 2>/dev/null || true)"

  tmp_link="$INSTALL_DIR/.alkera.$$"
  rm -f "$tmp_link"
  ln -s "$version_dir/alkera.dist/alkera" "$tmp_link"
  mv -f "$tmp_link" "$INSTALL_DIR/alkera"

  # Keep the new install plus the one the symlink pointed at before (a
  # still-running alkera may be loading files from it lazily); drop the rest.
  # Only dirs older than THIS install may be deleted: a concurrent installer's
  # fresh dir is always newer, so two racing installs can't destroy each
  # other's work (both finish functional; the last symlink write wins).
  prev_dir=""
  case "$prev_link" in
    */alkera.dist/alkera) prev_dir="${prev_link%/alkera.dist/alkera}" ;;
  esac
  for dir in "$INSTALL_DIR"/*/; do
    d="${dir%/}"
    [ -d "$d/alkera.dist" ] || continue
    [ "$d" = "$version_dir" ] && continue
    [ -n "$prev_dir" ] && [ "$d" = "$prev_dir" ] && continue
    [ "$d" -ot "$age_ref" ] || continue
    rm -rf "$d" || true
  done

  write_marker "$version"
  if [ -z "${ALKERA_NO_MODIFY_PATH:-}" ]; then
    on_path || add_to_path
  fi

  printf '\n'
  info "installed alkera $version -> $INSTALL_DIR/alkera"
  on_path || info "open a new terminal (or run: export PATH=\"$INSTALL_DIR:\$PATH\")"
  info "next: alkera login"
}

do_reinstall() {
  info "reinstalling (removing the current install first)"
  do_uninstall
  do_install
}

show_help() {
  cat <<EOF
Alkera CLI installer

  curl -fsSL ${BASE_URL}/install.sh | sh
  curl -fsSL ${BASE_URL}/install.sh | sh -s -- --version 0.0.1
  curl -fsSL ${BASE_URL}/install.sh | sh -s -- --reinstall
  curl -fsSL ${BASE_URL}/install.sh | sh -s -- --uninstall

Flags:
  --version <v>             install a specific version (default: latest)
  --reinstall               remove the current install, then install fresh
  --uninstall               remove the install (keeps ~/.alkera auth/preferences)
  -h, --help                show this help

Environment:
  ALKERA_VERSION            install a specific version (--version overrides this)
  ALKERA_INSTALL_DIR        install location (default: ~/.alkera/bin)
  ALKERA_ARTIFACT           keep the release tarball here (reused if it already
                            matches the manifest's checksum; downloaded if not)
  ALKERA_RELEASE_BASE_URL   release URL (default: https://releases.alkera.ai)
  ALKERA_CHANNEL            release channel (default: stable)
  ALKERA_NO_MODIFY_PATH     set to skip editing the shell profile PATH
EOF
}

# A real arg loop (not a single-$1 case) so flags like --version take a value
# and can be combined, e.g. `sh -s -- --version 0.0.1`.
action="install"
arg_version=""
while [ $# -gt 0 ]; do
  case "$1" in
    --uninstall) action="uninstall" ;;
    --reinstall) action="reinstall" ;;
    --version)
      shift
      [ $# -gt 0 ] || die "--version requires a value (e.g. --version 0.0.1)"
      arg_version="$1"
      ;;
    --version=*) arg_version="${1#--version=}" ;;
    -h | --help) action="help" ;;
    *) die "unknown argument: $1 (try --help)" ;;
  esac
  shift
done

# The flag wins over the ALKERA_VERSION env var; both feed the same pinned path.
if [ -n "$arg_version" ]; then
  ALKERA_VERSION="$arg_version"
fi

case "$action" in
  uninstall) do_uninstall ;;
  reinstall) do_reinstall ;;
  help) show_help ;;
  install) do_install ;;
esac
