#!/bin/sh
# Impeccable launcher. Runs the platform binary shipped next to this script:
#   <this dir>/bin/<os>-<arch>/impeccable
# Order: $IMPECCABLE_BIN, the sibling binary, ~/.impeccable/bin/impeccable,
# the version-pinned cache, then `impeccable` on PATH. Never needs Node.
# The unversioned home binary and the PATH candidate are validated with the
# engine-probe handshake first: the retired 3.x npm CLI also installed a bin
# named `impeccable`, and exec'ing it would fail every verb with
# "Unknown command". Trusted candidates (IMPECCABLE_BIN, the sibling binary,
# the version-pinned cache) are exec'd without a probe: hooks run them on
# every edit and must stay fast. The version-pinned cache and the download are
# verified against engine-checksums.txt (next to this script), never against
# anything published beside the binary; a <binary>.verified stamp records that
# a cached binary passed, so only the first run hashes it.
set -eu

# True when the candidate answers the engine handshake (prints
# "impeccable-engine <version>", exit 0). Quiet and fast (<100ms).
# IMPECCABLE_LAUNCHER_PROBE marks the child as a probe: a copy of this
# launcher reached recursively (e.g. symlinked onto PATH as `impeccable`)
# then skips its own probes and refuses to download, so probing stays cheap
# and can never loop.
probe_ok() {
  probe_out=$(IMPECCABLE_LAUNCHER_PROBE=1 "$1" engine-probe 2>/dev/null) || return 1
  case "$probe_out" in
    impeccable-engine*) return 0 ;;
  esac
  return 1
}
probing=${IMPECCABLE_LAUNCHER_PROBE:-}

# Prints the digest engine-checksums.txt records for engine version $1 and
# asset $2 (lines are "<version> <asset> <sha256>"), nothing when there is none.
# A CR is a separator too (CRLF checkouts), and a last line with no newline
# still counts.
manifest_digest() {
  [ -f "$manifest" ] || return 0
  while IFS="$(printf ' \t\r')" read -r m_version m_asset m_sum _ || [ -n "$m_version" ]; do
    case "$m_version" in ''|'#'*) continue ;; esac
    if [ "$m_version" = "$1" ] && [ "$m_asset" = "$2" ]; then
      printf '%s' "$m_sum"
      return 0
    fi
  done < "$manifest"
  return 0
}
# Sets $expected (and, on Windows on ARM, which runs x64 binaries,
# $expected_x64) from engine-checksums.txt. Only called off the stamped fast
# path: hooks run this launcher on every edit.
read_expected() {
  expected=$(manifest_digest "$version" "$asset")
  expected_x64=""
  if [ "$os" = windows ] && [ "$arch" = arm64 ]; then
    expected_x64=$(manifest_digest "$version" impeccable-windows-x64.exe)
  fi
}
# Sets $actual to the sha256 of $1, empty when no hash tool works.
hash_file() {
  actual=""
  if command -v shasum >/dev/null 2>&1; then
    if digest=$(shasum -a 256 "$1" 2>/dev/null); then actual=${digest%% *}; fi
  elif command -v sha256sum >/dev/null 2>&1; then
    if digest=$(sha256sum "$1" 2>/dev/null); then actual=${digest%% *}; fi
  fi
}

dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
manifest="$dir/engine-checksums.txt"

# What the binary needs to know about its home: the skill directory (for
# reference/*.md and command-metadata.json) and how to name itself in the
# commands it prints. Exported BEFORE any exec below, including the
# IMPECCABLE_BIN override: an engine binary reached with no IMPECCABLE_SKILL_DIR
# cannot find reference/*.md (so native platform refs never inline) or read its
# own version (so UPDATE_AVAILABLE never fires). Setting it here covers every
# candidate the launcher can exec.
: "${IMPECCABLE_SKILL_DIR:=$(CDPATH= cd -- "$dir/.." && pwd)}"
: "${IMPECCABLE_SELF:=$0}"
export IMPECCABLE_SKILL_DIR IMPECCABLE_SELF

if [ -n "${IMPECCABLE_BIN:-}" ] && [ -x "${IMPECCABLE_BIN}" ]; then
  exec "${IMPECCABLE_BIN}" "$@"
fi

case "$(uname -s 2>/dev/null || echo unknown)" in
  Darwin) os=darwin ;;
  Linux) os=linux ;;
  MINGW*|MSYS*|CYGWIN*|Windows_NT) os=windows ;;
  *) os=unknown ;;
esac
case "$(uname -m 2>/dev/null || echo unknown)" in
  arm64|aarch64) arch=arm64 ;;
  x86_64|amd64) arch=x64 ;;
  *) arch=unknown ;;
esac

bin="$dir/bin/$os-$arch/impeccable"
[ "$os" = windows ] && bin="$bin.exe"

if [ -x "$bin" ]; then
  exec "$bin" "$@"
fi
if [ -f "$bin" ]; then
  # Lost the executable bit in transit (zip extraction, some copiers).
  chmod +x "$bin" 2>/dev/null && exec "$bin" "$@"
fi
# On Windows (an MSYS/Git Bash shell) the cached names carry .exe so this
# launcher and impeccable.cmd share one cache.
exe=""
[ "$os" = windows ] && exe=".exe"
home_bin="${HOME:-/nonexistent}/.impeccable/bin/impeccable$exe"
if [ -z "$probing" ] && [ -x "$home_bin" ] && probe_ok "$home_bin"; then
  exec "$home_bin" "$@"
fi
# Version-pinned user cache, filled by the download below or by `impeccable update`.
version=""
[ -f "$dir/VERSION" ] && version=$(tr -d '[:space:]' < "$dir/VERSION")
cache_root="${IMPECCABLE_HOME:-${HOME:-/nonexistent}/.impeccable}"
cached="$cache_root/bin/$version/impeccable$exe"
asset="impeccable-$os-$arch"
[ "$os" = windows ] && asset="$asset.exe"
if [ -n "$version" ] && [ -x "$cached" ]; then
  if [ -f "$cached.verified" ]; then
    exec "$cached" "$@"
  fi
  read_expected
  hash_file "$cached"
  if [ -n "$actual" ] && { [ "$actual" = "$expected" ] || [ "$actual" = "$expected_x64" ]; }; then
    { echo "$actual" > "$cached.verified"; } 2>/dev/null || :
    exec "$cached" "$@"
  fi
  echo "impeccable: cannot verify the cached engine $cached against $manifest; not using it" >&2
fi
if [ -z "$probing" ] && command -v impeccable >/dev/null 2>&1 && probe_ok impeccable; then
  exec impeccable "$@"
fi

# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
  echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
  echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
  echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
  elif command -v wget >/dev/null 2>&1; then
    wget -q -O "$tmp" "$1" 2>/dev/null
  else
    return 1
  fi
}
check_download() {
  download_file=${1:-$tmp}
  if [ ! -f "$download_file" ]; then
    echo "impeccable: download completed but the file was removed before execution: $url; check your antivirus quarantine or logs. Refusing to continue; do not disable protection." >&2
    exit 127
  fi
  if [ ! -s "$download_file" ]; then
    rm -f "$download_file"
    echo "impeccable: downloaded file is empty: $url; refusing the unverified download" >&2
    exit 127
  fi
}
if [ -n "$probing" ]; then
  # Inside another launcher's probe: no download, fail fast and quiet.
  exit 127
fi
if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
  base="${IMPECCABLE_DOWNLOAD_BASE:-https://github.com/pbakaus/impeccable/releases/download}"
  url="$base/engine-v$version/$asset"
  read_expected
  # Fail closed before any network traffic or cache write.
  if [ -z "$expected" ] && [ -z "$expected_x64" ]; then
    echo "impeccable: no checksum for engine $version ($asset) in $manifest; refusing the unverified download" >&2
    exit 127
  fi
  tmp="$cache_root/bin/$version/.impeccable.part.$$"
  # An interrupted download (Ctrl-C, a hook timeout) would otherwise leave the
  # PID-named staging file behind for good. The handlers must exit explicitly.
  cleanup() { rm -f "$tmp" 2>/dev/null || :; }
  trap 'cleanup; exit 129' HUP
  trap 'cleanup; exit 130' INT
  trap 'cleanup; exit 143' TERM
  if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
    echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
    setup_help
    exit 127
  fi
  # Check the actual staging file, not just directory existence: a cache from
  # an earlier run can be readable but no longer writable inside a sandbox.
  if ! (umask 077; : > "$tmp") 2>/dev/null; then
    echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
    setup_help
    exit 127
  fi
  fetched=0
  if [ -n "$expected" ] && fetch_url "$url"; then
    fetched=1
  elif [ -n "$expected_x64" ]; then
    # Windows on ARM runs x64 binaries; fall back when no arm64 asset exists.
    url="$base/engine-v$version/impeccable-windows-x64.exe"
    expected=$expected_x64
    fetch_url "$url" && fetched=1
  fi
  if [ "$fetched" = 1 ]; then
    check_download
    # No sha256 tool refuses the download too.
    hash_file "$tmp"
    check_download
    if [ -z "$actual" ]; then
      rm -f "$tmp"
      echo "impeccable: cannot verify $url (no sha256 tool, or hashing failed); refusing the unverified download" >&2
      exit 127
    fi
    if [ "$actual" != "$expected" ]; then
      rm -f "$tmp"
      echo "impeccable: checksum mismatch downloading $url" >&2
      exit 127
    fi
    check_download
    if ! chmod +x "$tmp" 2>/dev/null; then
      check_download
      rm -f "$tmp"
      echo "impeccable: could not make the verified download executable: $url" >&2
      exit 127
    fi
    check_download
    if ! mv -f "$tmp" "$cached" 2>/dev/null; then
      check_download
      rm -f "$tmp"
      echo "impeccable: could not cache the verified download: $url" >&2
      exit 127
    fi
    check_download "$cached"
    { echo "$actual" > "$cached.verified"; } 2>/dev/null || :
    exec "$cached" "$@"
  fi
  rm -f "$tmp" 2>/dev/null
  echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
  setup_help
  exit 127
fi

echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
echo "Download impeccable-$os-$arch from https://github.com/pbakaus/impeccable/releases (tag engine-v$version) into $cache_root/bin/$version/impeccable$exe (then chmod +x), or set IMPECCABLE_BIN to a preinstalled engine binary. Docs: https://impeccable.style" >&2
exit 127
