#!/bin/sh
# Archis installer.
#
#   curl -fsSL https://archis.dev/install.sh | sh
#
# What this does, in order: ask the control plane which build matches this machine,
# download it, verify its SHA-256 against what the control plane said, and only then put
# anything on disk.
#
# WHY THE HASH COMES FROM THE API AND THE BYTES COME FROM THE BUCKET. Those are two
# different hosts, and that is the entire security argument for an unsigned install. The
# manifest arrives over TLS from a host we control; the archive arrives from object
# storage. Someone who could tamper with the storage bucket still cannot make the hash
# match, and someone who could tamper with the response cannot serve the bytes. A single
# host serving both would collapse that.
#
# It is written in POSIX sh, not bash: the smallest Linux images ship dash as /bin/sh, and
# an installer that only runs where bash exists fails on exactly the containers people most
# want to install into.
#
# Environment overrides:
#   CHI_CHANNEL      stable (default) | beta | internal
#   CHI_INSTALL_DIR  where to put the binaries
#   CHI_API_BASE     control plane origin, for testing against a staging deployment
#   CHI_FORCE        set to 1 to reinstall even when this build is already installed

set -eu

API_BASE="${CHI_API_BASE:-https://archis.dev}"
CHANNEL="${CHI_CHANNEL:-stable}"

# --- output -------------------------------------------------------------------
# Colour only when stdout is a terminal. Piped into a log, escape codes are noise.
if [ -t 1 ]; then
    BOLD=$(printf '\033[1m'); DIM=$(printf '\033[2m')
    RED=$(printf '\033[31m'); GREEN=$(printf '\033[32m'); RESET=$(printf '\033[0m')
else
    BOLD=''; DIM=''; RED=''; GREEN=''; RESET=''
fi

say()  { printf '%s\n' "$*"; }
step() { printf '%s==>%s %s\n' "$BOLD" "$RESET" "$*"; }
die()  { printf '%serror:%s %s\n' "$RED" "$RESET" "$*" >&2; exit 1; }

# --- preconditions ------------------------------------------------------------
# Checked up front so the script fails before downloading anything rather than half way
# through.
if command -v curl >/dev/null 2>&1; then
    fetch() { curl -fsSL "$1"; }
    fetch_to() { curl -fsSL -o "$2" "$1"; }
elif command -v wget >/dev/null 2>&1; then
    fetch() { wget -qO- "$1"; }
    fetch_to() { wget -qO "$2" "$1"; }
else
    die "neither curl nor wget is available"
fi

if command -v sha256sum >/dev/null 2>&1; then
    sha256_of() { sha256sum "$1" | cut -d' ' -f1; }
elif command -v shasum >/dev/null 2>&1; then
    sha256_of() { shasum -a 256 "$1" | cut -d' ' -f1; }
else
    # Refusing is the right call. Installing without verifying would mean trusting the
    # network, which is precisely what the hash is there to avoid.
    die "no sha256sum or shasum available, so the download cannot be verified"
fi

command -v tar >/dev/null 2>&1 || die "tar is required"

# --- identify this machine ----------------------------------------------------
os=$(uname -s)
case "$os" in
    Darwin) platform=darwin ;;
    Linux)  platform=linux ;;
    MINGW*|MSYS*|CYGWIN*)
        die "this script does not install on Windows. See ${API_BASE}/downloads" ;;
    *) die "unsupported operating system: $os" ;;
esac

machine=$(uname -m)
case "$machine" in
    arm64|aarch64) arch=arm64 ;;
    x86_64|amd64)  arch=x86_64 ;;
    *) die "unsupported architecture: $machine" ;;
esac

# --- choose a destination -----------------------------------------------------
# /usr/local/bin when it is writable, ~/.local/bin otherwise. Deliberately no sudo: a
# script piped from the network should not escalate, and a tool that lives in the user's
# own bin directory works just as well.
#
# Resolved here, before anything is downloaded, because the already-installed check below
# has to read the version of the binary a new install would replace.
if [ -n "${CHI_INSTALL_DIR:-}" ]; then
    dest="$CHI_INSTALL_DIR"
elif [ -w /usr/local/bin ] 2>/dev/null; then
    dest=/usr/local/bin
else
    dest="$HOME/.local/bin"
fi

step "Resolving the latest $CHANNEL build for ${platform}-${arch}"

# --- ask the control plane ----------------------------------------------------
manifest_url="${API_BASE}/v1/releases/latest?platform=${platform}&arch=${arch}&channel=${CHANNEL}"
manifest=$(fetch "$manifest_url" 2>/dev/null) || manifest=''

if [ -z "$manifest" ]; then
    die "no $CHANNEL release published for ${platform}-${arch} yet.
  Check ${API_BASE}/downloads, or try CHI_CHANNEL=beta."
fi

# Extracting fields with grep rather than jq, because jq is not installed on a clean
# machine and requiring it would be a second install step.
#
# The pattern anchors on the opening quote of the key, which is what keeps "url" from
# matching inside "notes_url" — there is an underscore before url there, not a quote.
json_string() {
    printf '%s' "$manifest" \
        | grep -o "\"$1\":\"[^\"]*\"" \
        | head -1 \
        | sed "s/^\"$1\":\"//; s/\"$//"
}

version=$(json_string version)
url=$(json_string url)
want_sha=$(json_string sha256)
filename=$(json_string filename)

[ -n "$version" ]  || die "the control plane response carried no version"
[ -n "$url" ]      || die "the control plane response carried no download URL"
[ -n "$want_sha" ] || die "the control plane response carried no SHA-256, so the download cannot be verified"

say "    version ${BOLD}${version}${RESET} ${DIM}(${CHANNEL})${RESET}"

# --- stop here when that build is already installed ---------------------------
# The one-line installer is the same command whether someone is installing or updating, so
# it gets re-run routinely. Downloading and rewriting a binary that is already byte-for-byte
# the published one costs bandwidth and replaces a file another process may be executing
# right now. Compare versions first and do nothing when they match; CHI_FORCE=1 is the way
# out for a corrupt binary or a switch between channels.
#
# `chi --version` prints "chi <semver>", so the last field is the version. A binary that
# will not run at all (wrong architecture, truncated) leaves `current` empty and we install
# over it, which is the desired outcome.
current=''
if [ -x "$dest/chi" ]; then
    current=$("$dest/chi" --version 2>/dev/null | awk 'NR==1 {print $NF}') || current=''
elif command -v chi >/dev/null 2>&1; then
    current=$(chi --version 2>/dev/null | awk 'NR==1 {print $NF}') || current=''
fi

if [ -n "$current" ] && [ "${current#v}" = "${version#v}" ] && [ -z "${CHI_FORCE:-}" ]; then
    say ""
    say "${GREEN}${BOLD}chi ${current}${RESET} — you already have the latest $CHANNEL version. Nothing to do."
    say "${DIM}Re-run with CHI_FORCE=1 to download and reinstall it anyway.${RESET}"
    exit 0
fi

# --- download -----------------------------------------------------------------
tmp=$(mktemp -d 2>/dev/null || mktemp -d -t chi-install)
# Trap on the signals that actually happen — a Ctrl-C mid-download should not leave a
# half-extracted tree in /tmp.
trap 'rm -rf "$tmp"' EXIT HUP INT TERM

archive="$tmp/${filename:-chi.tar.gz}"
step "Downloading $(basename "$url")"
fetch_to "$url" "$archive" || die "download failed: $url"

step "Verifying SHA-256"
got_sha=$(sha256_of "$archive")
if [ "$got_sha" != "$want_sha" ]; then
    die "checksum mismatch — NOT installing.
  expected $want_sha
  received $got_sha
  The download does not match what the control plane published. This can be a corrupted
  transfer or a tampered artifact; either way nothing was written to disk."
fi
say "    ${GREEN}ok${RESET} ${DIM}${got_sha}${RESET}"

# --- unpack -------------------------------------------------------------------
# WHY TAR'S STDERR IS FILTERED AND NOT SILENCED. The macOS builds up to 0.37.0 were
# sealed with the Apple extended attributes `cp` had stamped on the binaries, and GNU
# tar has no idea what `LIBARCHIVE.xattr.com.apple.provenance` is — so it says so, once
# per file, on every Linux install:
#
#     tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.com.apple.provenance'
#
# Nothing is wrong; tar ignores the keyword and extracts the file. But five warnings in
# the middle of an otherwise clean install read as five problems, and this is the very
# first command anybody runs. The release now clears those attributes at the source, so
# a build from 0.38.0 on has none of them — while every archive already published still
# does, and those are what today's `curl | sh` downloads.
#
# So the one harmless keyword is dropped and everything else tar has to say is still
# printed, and a failure is still a failure: a blanket `2>/dev/null` here would also
# swallow "unexpected end of file", which is precisely the message somebody debugging a
# truncated download needs.
tar_stderr="$tmp/tar.stderr"
if ! tar -xzf "$archive" -C "$tmp" 2>"$tar_stderr"; then
    cat "$tar_stderr" >&2
    die "could not extract $archive"
fi
grep -v 'Ignoring unknown extended header keyword' "$tar_stderr" >&2 || true
rm -f "$tar_stderr"

# The archive holds a single top-level directory named after the release.
bindir=$(find "$tmp" -type f -name chi -perm -u+x 2>/dev/null | head -1)
bindir=$(dirname "${bindir:-/nonexistent}")
[ -f "$bindir/chi" ] || die "the archive did not contain a 'chi' binary"

# --- install ------------------------------------------------------------------
# $dest was picked above; only now, with a verified archive in hand, is it worth creating
# the directory or complaining that we cannot write to it.
mkdir -p "$dest" || die "cannot create $dest"
[ -w "$dest" ] || die "$dest is not writable. Set CHI_INSTALL_DIR to somewhere you own."

step "Installing to $dest"
for bin in chi chi-mcp; do
    [ -f "$bindir/$bin" ] || continue
    # Install to a temporary name and rename, so an interrupted copy cannot leave a
    # truncated binary where a working one used to be. rename within a filesystem is
    # atomic; cp over a running binary is not.
    cp "$bindir/$bin" "$dest/.$bin.new" || die "could not write to $dest"
    chmod 755 "$dest/.$bin.new"
    mv -f "$dest/.$bin.new" "$dest/$bin" || die "could not install $bin"
    say "    $bin"
done

# --- report -------------------------------------------------------------------
#
# WHY THIS KEEPS STDERR. A binary that will not start says why, and the loader says it
# on stderr: `version GLIBC_2.39 not found`, `cannot execute binary file`, a missing
# shared object by name. Sending that to /dev/null and guessing "Wrong architecture?"
# is how #50 arrived — a correct linux-arm64 binary on a correct aarch64 machine, whose
# actual problem was that it had been built against a newer glibc than the system had.
# The guess sent the reporter looking at the one thing that was right.
#
# So: run it once, keep what it said, and let it speak.
probe="$tmp/version.err"
installed=$("$dest/chi" --version 2>"$probe") || installed=''

if [ -z "$installed" ]; then
    # A glibc mismatch is worth naming, because the fix is not "reinstall" and the
    # message the loader prints is not obviously about the OS being too old.
    if grep -q 'GLIBC_' "$probe" 2>/dev/null; then
        needs=$(sed -n 's/.*GLIBC_\([0-9][0-9.]*\).*/\1/p' "$probe" | sort -t. -k1,1n -k2,2n | tail -1)
        have=$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $NF}')
        [ -n "$have" ] || have=$(ldd --version 2>/dev/null | awk 'NR==1 {print $NF}')
        die "this build needs a newer glibc than ${platform}-${arch} has here.
  this system:  glibc ${have:-unknown}
  chi $version: needs glibc $needs
  Your architecture is fine — the C library is older than the one this binary was
  built against. Upgrading the distribution is the direct fix. If that is not an
  option, tell us at ${API_BASE}/support which distribution and version you are on:
  the floor we build against is a number we choose, and it is meant to cover you."
    fi
    die "installed, but '$dest/chi --version' did not run.
$(sed 's/^/  /' "$probe" 2>/dev/null | head -5)"
fi

say ""
say "${GREEN}${BOLD}${installed}${RESET} installed."
say ""

# --- PATH ---------------------------------------------------------------------
#
# Appended to the startup file of every shell that has one, not just the one this
# script happens to be running under: `curl | sh` runs under /bin/sh whatever the
# person actually uses, so guessing from $SHELL or $0 gets it wrong for exactly
# the people who most need it to work.
#
# Two refusals. A file that already mentions $dest is left alone, so re-running the
# installer cannot stack up duplicate lines. And a startup file that does not exist
# is not created — an empty ~/.bashrc that Archis invented would change how somebody's
# shell starts, which is more than installing a binary asked for.
add_to_path() {
    rc="$1"
    marker="# added by the Archis installer"
    [ -f "$rc" ] || return 1
    grep -q "$dest" "$rc" 2>/dev/null && return 1
    case "$rc" in
        *config.fish) printf '\n%s\nfish_add_path %s\n' "$marker" "$dest" >> "$rc" ;;
        *)            printf '\n%s\nexport PATH="%s:$PATH"\n' "$marker" "$dest" >> "$rc" ;;
    esac
    say "    $rc"
    return 0
}

case ":$PATH:" in
    *":$dest:"*) ;;
    *)
        say "${BOLD}$dest was not on your PATH.${RESET} Added it to:"
        say ""
        touched=0
        for rc in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile" \
                  "${XDG_CONFIG_HOME:-$HOME/.config}/fish/config.fish"; do
            add_to_path "$rc" && touched=1
        done
        if [ "$touched" -eq 0 ]; then
            say "    ${DIM}(nothing to edit — no shell startup file found)${RESET}"
        fi
        say ""
        say "    ${DIM}For this shell, right now:${RESET} export PATH=\"$dest:\$PATH\""
        say ""
        ;;
esac

# --- health check -------------------------------------------------------------
#
# The installed binary inspecting its own installation, which is a different
# question from `--version` running: it is what finds a half-written state
# directory or an agent Archis cannot write to. Silent unless it fails, and never
# fatal — a diagnostic that aborts an otherwise good install would be the bug it
# is meant to catch.
if ! "$dest/chi" doctor --json >/dev/null 2>&1; then
    say "${DIM}Note: 'chi doctor' reported a problem. Run it to see what.${RESET}"
    say ""
fi

say "Next:"
say ""
say "    chi auth login          ${DIM}# activate this machine — Archis Free, no card${RESET}"
say "    chi init                ${DIM}# index this repository and connect your agents${RESET}"
say "    chi doctor              ${DIM}# check the installation and say what is wrong${RESET}"
say ""
say "${DIM}Archis Free: your own repositories, two devices. Your source never leaves${RESET}"
say "${DIM}your machine. Signing in turns on anonymous usage statistics — 'chi${RESET}"
say "${DIM}telemetry preview' shows exactly what they are, 'chi telemetry off' stops${RESET}"
say "${DIM}them. Privacy disclosure: ${API_BASE}/privacy${RESET}"
