#!/bin/sh
# oppi installer.
#
#   curl -fsSL <url>/install.sh | sh
#
# Installs into ~/.local by default:
#   ~/.local/bin/oppi
#   ~/.local/share/oppi/...
# so the binary finds its data via <exe>/../share/oppi/... (no env vars needed).
#
# Environment overrides:
#   PREFIX=/usr/local        install elsewhere (e.g. a system-wide install)
#   OPPI_RELEASE_BASE=<url>   download base (defaults to the public oppi-lang site)
#   OPPI_TARBALL=<path>       install from a local tarball instead of downloading
#                             (checksum verified only if <path>.sha256 exists —
#                             a local file has no separate trust channel)
#   OPPI_NO_VERIFY=1          skip checksum verification (downloads verify by
#                             default and FAIL otherwise — this is the explicit
#                             opt-out, e.g. for a host without any sha256 tool)
set -eu

PREFIX="${PREFIX:-$HOME/.local}"
RELEASE_BASE="${OPPI_RELEASE_BASE:-https://oppi-lang.eu}"
TARBALL="oppi-linux-x86_64.tar.gz"

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

# --- platform check (Linux x86_64 only for now) -----------------------------
os="$(uname -s)"
arch="$(uname -m)"
[ "$os" = "Linux" ] || err "this installer supports Linux only for now (got $os); build from source instead."
[ "$arch" = "x86_64" ] || err "this installer supports x86_64 only for now (got $arch); build from source instead."

fetch() { # fetch URL OUTFILE
  # --netrc-optional: if the download host needs auth (e.g. the site is still
  # basic-auth gated for pre-launch testing), curl picks up credentials from
  # ~/.netrc; with no matching entry it fetches anonymously (public launch). wget
  # reads ~/.netrc on its own. So the installer works both gated and public.
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL --netrc-optional "$1" -o "$2"
  elif command -v wget >/dev/null 2>&1; then
    wget -qO "$2" "$1"
  else
    err "need curl or wget to download oppi"
  fi
}

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

# --- obtain the tarball (+ checksum) ----------------------------------------
if [ -n "${OPPI_TARBALL:-}" ]; then
  say "Using local tarball: $OPPI_TARBALL"
  cp "$OPPI_TARBALL" "$tmp/$TARBALL"
  if [ -f "$OPPI_TARBALL.sha256" ]; then cp "$OPPI_TARBALL.sha256" "$tmp/$TARBALL.sha256"; fi
else
  say "Downloading oppi…"
  fetch "$RELEASE_BASE/$TARBALL" "$tmp/$TARBALL"
  if [ "${OPPI_NO_VERIFY:-0}" != "1" ]; then
    fetch "$RELEASE_BASE/$TARBALL.sha256" "$tmp/$TARBALL.sha256" \
      || err "could not fetch checksum $TARBALL.sha256 — refusing to install unverified (set OPPI_NO_VERIFY=1 to override)"
  fi
fi

# --- verify checksum ---------------------------------------------------------
# Verification is the DEFAULT for downloads and any failure is fatal; the only
# soft path is a local OPPI_TARBALL without a .sha256 next to it (same-origin
# file: a checksum would add no trust) or the explicit OPPI_NO_VERIFY=1 opt-out.
if [ "${OPPI_NO_VERIFY:-0}" = "1" ]; then
  say "warning: OPPI_NO_VERIFY=1 — checksum verification SKIPPED"
elif [ -f "$tmp/$TARBALL.sha256" ]; then
  expected="$(awk '{ print $1 }' "$tmp/$TARBALL.sha256")"
  if command -v sha256sum >/dev/null 2>&1; then
    actual="$(sha256sum "$tmp/$TARBALL" | awk '{ print $1 }')"
  elif command -v shasum >/dev/null 2>&1; then
    actual="$(shasum -a 256 "$tmp/$TARBALL" | awk '{ print $1 }')"
  else
    err "no sha256 tool found (need sha256sum or shasum) — refusing to install unverified (set OPPI_NO_VERIFY=1 to override)"
  fi
  [ "$expected" = "$actual" ] \
    || err "checksum mismatch (expected $expected, got $actual)"
  say "Checksum OK."
fi

# --- verify the SIGNATURE ----------------------------------------------------
# POURQUOI, ET POURQUOI LA SOMME NE SUFFIT PAS (2026-09-04). La somme sha256 est
# servie par LE MÊME hôte que l'archive : qui prend la main sur le serveur
# remplace les deux, et la vérification concorde toujours. Elle protège d'un
# téléchargement abîmé, pas d'une archive SUBSTITUÉE. Or ce script exécute ce
# qu'il télécharge, sur la machine de qui l'installe.
#
# La clé publique est ÉPINGLÉE ICI : elle voyage avec l'installeur au lieu
# d'être re-téléchargée depuis l'hôte qu'elle sert à contrôler — sinon le
# contrôle serait circulaire. La clé privée ne quitte jamais le poste qui
# publie ; elle n'est ni sur le serveur, ni dans le dépôt.
#
# LIMITE, dite franchement : si l'attaquant remplace AUSSI ce script sur le
# serveur, il en retire ce bloc. La signature protège donc qui garde une copie
# d'install.sh, qui vérifie l'archive à la main, et contre la substitution de
# l'archive SEULE. C'est strictement mieux qu'une somme co-hébergée ; ce n'est
# pas une chaîne de confiance complète.
OPPI_PUBKEY='-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAbY7AhFgcFNlwMP4DurSs38p9g8XbFhIs16aq+mlgSvM=
-----END PUBLIC KEY-----'

if [ "${OPPI_NO_VERIFY:-0}" = "1" ]; then
  :
elif [ -z "${OPPI_TARBALL:-}" ]; then
  if command -v openssl >/dev/null 2>&1; then
    fetch "$RELEASE_BASE/$TARBALL.sig" "$tmp/$TARBALL.sig" \
      || err "could not fetch signature $TARBALL.sig — refusing to install unverified (set OPPI_NO_VERIFY=1 to override)"
    printf '%s\n' "$OPPI_PUBKEY" > "$tmp/oppi-pub.pem"
    openssl pkeyutl -verify -rawin -pubin -inkey "$tmp/oppi-pub.pem" \
      -sigfile "$tmp/$TARBALL.sig" -in "$tmp/$TARBALL" >/dev/null 2>&1 \
      || err "SIGNATURE INVALIDE — cette archive n'a pas été publiée par la clé d'oppi. Installation refusée."
    say "Signature OK."
  else
    # openssl absent : on le DIT, on ne fait pas semblant d'avoir vérifié.
    say "warning: openssl absent — signature NON vérifiée (la somme sha256 l'a été)."
  fi
fi

# --- install ----------------------------------------------------------------
# Une mise à jour REMPLACE ce qui appartient à oppi ; elle ne se superpose pas
# (2026-10-03). Extraire l'archive PAR-DESSUS l'ancienne installation laissait
# en place les modules qu'une version retire (lib/Core/Read.oppi survivait),
# que le compilateur pouvait encore charger. Ce qui est à oppi dans $PREFIX :
#   - share/oppi/ ENTIER, remplacé d'un bloc ;
#   - les fichiers de bin/ que l'archive déclare dans share/oppi/.installed-bin
#     (écrit par scripts/package.sh) : ceux de la version précédente qu'une
#     nouvelle ne déclare plus (un binaire renommé) sont retirés.
# Rien d'autre n'est touché : $PREFIX (~/.local) est partagé avec d'autres
# logiciels. L'archive ne doit donc contenir que bin/<fichier> et share/oppi/.
say "Installing to $PREFIX…"
mkdir -p "$PREFIX/bin" "$PREFIX/share"
bad="$(tar -tzf "$tmp/$TARBALL" | grep -Ev '^(\./)?(bin/?|bin/[^/]+|share/?|share/oppi(/.*)?)$' || true)"
[ -z "$bad" ] || err "archive refused: it holds paths outside bin/ and share/oppi/: $(printf '%s' "$bad" | head -3 | tr '\n' ' ')"

# Préparé à côté de la cible (même système de fichiers), pour que le
# remplacement soit un simple renommage.
stage="$(mktemp -d "$PREFIX/share/.oppi-install.XXXXXX")"
trap 'rm -rf "$tmp" "$stage"' EXIT
tar -xzf "$tmp/$TARBALL" -C "$stage"
[ -f "$stage/bin/oppi" ] || err "install failed: bin/oppi not found in tarball"
[ -d "$stage/share/oppi" ] || err "install failed: share/oppi not found in tarball"
# liste des binaires livrés : celle de l'archive (scripts/package.sh l'écrit) ;
# une archive d'avant cette liste ne livrait que bin/oppi, on la dresse alors
# (à part : l'installation reste identique à l'archive).
if [ -f "$stage/share/oppi/.installed-bin" ]; then
  cp "$stage/share/oppi/.installed-bin" "$stage/bin-list"
else
  ( cd "$stage/bin" && ls -A ) > "$stage/bin-list"
fi
own() { case "$1" in ''|.*|*/*) return 1 ;; *) return 0 ;; esac; } # nom simple

# binaires : renommage atomique (marche aussi si un `oppi` tourne encore)
for f in $(cat "$stage/bin-list"); do
  own "$f" && [ -f "$stage/bin/$f" ] || err "archive refused: bad binary entry '$f'"
  chmod +x "$stage/bin/$f"
  mv -f "$stage/bin/$f" "$PREFIX/bin/$f"
done
# ceux de la version précédente que celle-ci ne livre plus
if [ -f "$PREFIX/share/oppi/.installed-bin" ]; then
  for f in $(cat "$PREFIX/share/oppi/.installed-bin"); do
    own "$f" || continue
    grep -qxF "$f" "$stage/bin-list" || rm -f "$PREFIX/bin/$f"
  done
fi

# share/oppi : l'ancien est écarté, le nouveau prend sa place, l'ancien part
if [ -e "$PREFIX/share/oppi" ]; then mv "$PREFIX/share/oppi" "$stage/old-share"; fi
mv "$stage/share/oppi" "$PREFIX/share/oppi"

bin="$PREFIX/bin/oppi"

# --- PATH guidance ----------------------------------------------------------
case ":$PATH:" in
  *":$PREFIX/bin:"*) ;;
  *)
    say ""
    say "NOTE: $PREFIX/bin is not on your PATH. Add it with, e.g.:"
    say "  echo 'export PATH=\"$PREFIX/bin:\$PATH\"' >> ~/.bashrc && . ~/.bashrc"
    ;;
esac

say ""
say "Installed: $("$bin" --version)"
say "Get started:  oppi --help"
