#!/bin/sh
# rinkata installer — one-line `curl | bash` (or `| sh`).
#
# Drops the `rinkata` and `rinkata-mcp` standalone binaries into
# `~/.rinkata/bin/`, adds that directory to your PATH via your shell rc,
# and verifies the install with `rinkata --version`. The legacy `plumb` /
# `plumb-mcp` command names are also installed as back-compat aliases during
# the rename window (GOAL-PLUMB-42).
#
# Re-running upgrades to the latest published version and re-verifies;
# the rc line is appended at most once.
#
# POSIX-compatible. Refuses to run as root. No telemetry. No background
# daemon. Uninstall: `rm -rf ~/.rinkata` plus removing the `# rinkata-install:`
# line from your shell rc.

set -eu

# When the install script is served with `?version=vX.Y.Z`, an
# override line is prepended above this block; default-empty here
# means the precedence chain below falls through to $RINKATA_VERSION
# ($PLUMB_VERSION as a legacy alias) env or the published `latest` pointer.
RINKATA_VERSION_OVERRIDE="${RINKATA_VERSION_OVERRIDE:-${PLUMB_VERSION_OVERRIDE:-}}"

LOG_PREFIX="[rinkata-install]"
# The releases bucket and asset name are renamed by Spec 8/9; until then the
# published artifact is still `plumb-hub-releases` / `plumb-<target>.tar.gz`.
# RINKATA_RELEASE_BUCKET_URL is a local test hook. The bucket cannot set it.
RELEASE_BUCKET_URL="${RINKATA_RELEASE_BUCKET_URL:-https://storage.googleapis.com/plumb-hub-releases}"
# Trust anchor. Must stay byte-identical to keys/plumb-skills-signing.pub.pem.
# Served with this script from rinkata.dev, not from the release bucket.
RELEASE_PUBKEY='-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAIi2jkAB34S0ESqaibZe0baA8UhSFMFeGpSeujbIdCYE=
-----END PUBLIC KEY-----'
INSTALL_DIR="$HOME/.rinkata/bin"

log() { printf '%s %s\n' "$LOG_PREFIX" "$*"; }
err() { printf '%s ERROR: %s\n' "$LOG_PREFIX" "$*" >&2; }
die() { err "$1"; exit "${2:-1}"; }

# Decode a base64 signature. macOS base64 wants -D; GNU base64 wants -d.
b64decode() {
  if printf '' | base64 -d >/dev/null 2>&1; then
    base64 -d
  else
    base64 -D
  fi
}

# Verify a raw Ed25519 signature (base64 .sig from scripts/sign-bundle.ts)
# before anything is unpacked. OpenSSL 3 can do this. macOS LibreSSL cannot
# load an Ed25519 SPKI key, so fall back to the inlined verifier. The
# verifier is part of this script — it is not downloaded from the bucket.
verify_release_signature() {
  archive=$1
  sig_b64=$2
  printf '%s\n' "$RELEASE_PUBKEY" > "$SCRATCH/release.pub.pem"
  b64decode < "$sig_b64" > "$SCRATCH/release.sig" || return 1
  # OpenSSL 3 verifies Ed25519 with `pkeyutl -rawin`. OpenSSL 1.1.1 can load
  # the key but rejects -rawin, so a failed pkeyutl must fall through to
  # python3 instead of failing the install.
  if openssl pkeyutl -verify -pubin -inkey "$SCRATCH/release.pub.pem" \
      -rawin -in "$archive" -sigfile "$SCRATCH/release.sig" >/dev/null 2>&1; then
    return 0
  fi
  if ! command -v python3 >/dev/null 2>&1; then
    err "OpenSSL cannot verify Ed25519 and python3 is not installed."
    return 1
  fi
  cat > "$SCRATCH/ed25519_verify.py" <<'ED25519_PY'
"""Ed25519 verify for install.sh (RFC 8032). Stdlib only.

Node's `crypto.sign(null, data, ed25519Key)` emits a raw 64-byte signature.
macOS ships LibreSSL, which cannot load an Ed25519 SPKI key, so install.sh
cannot rely on `openssl pkeyutl` there. This module is the fallback, and it
is inlined into install.sh so `curl | sh` does not fetch a second file.
"""

import hashlib

P = 2**255 - 19
L = 2**252 + 27742317777372353535851937790883648493
D = (-121665 * pow(121666, P - 2, P)) % P
I = pow(2, (P - 1) // 4, P)
BY = (4 * pow(5, P - 2, P)) % P


def _sha512(data: bytes) -> bytes:
    return hashlib.sha512(data).digest()


def _inv(x: int) -> int:
    return pow(x, P - 2, P)


def _xrecover(y: int) -> int:
    xx = (y * y - 1) * _inv(D * y * y + 1) % P
    x = pow(xx, (P + 3) // 8, P)
    if (x * x - xx) % P != 0:
        x = (x * I) % P
    if (x * x - xx) % P != 0:
        raise ValueError("not on curve")
    if x % 2 != 0:
        x = P - x
    return x


def _isoncurve(point):
    x, y = point
    return (-x * x + y * y - 1 - D * x * x * y * y) % P == 0


BX = _xrecover(BY)
B = (BX, BY)


def _edwards_add(p, q):
    x1, y1 = p
    x2, y2 = q
    x3 = (x1 * y2 + x2 * y1) * _inv(1 + D * x1 * x2 * y1 * y2)
    y3 = (y1 * y2 + x1 * x2) * _inv(1 - D * x1 * x2 * y1 * y2)
    return (x3 % P, y3 % P)


def _scalarmult(point, e):
    result = (0, 1)
    addend = point
    while e:
        if e & 1:
            result = _edwards_add(result, addend)
        addend = _edwards_add(addend, addend)
        e >>= 1
    return result


def _decodepoint(s):
    if len(s) != 32:
        raise ValueError("bad point length")
    y = int.from_bytes(s, "little")
    sign = (y >> 255) & 1
    y &= (1 << 255) - 1
    x = _xrecover(y)
    if (x & 1) != sign:
        x = P - x
    point = (x, y)
    if not _isoncurve(point):
        raise ValueError("point off curve")
    return point


def _decodeint(s: bytes) -> int:
    return int.from_bytes(s, "little")


def verify(public: bytes, signature: bytes, message: bytes) -> bool:
    """Return True when `signature` is a raw 64-byte Ed25519 sig over `message`."""
    try:
        if len(public) != 32 or len(signature) != 64:
            return False
        R = _decodepoint(signature[:32])
        A = _decodepoint(public)
        S = _decodeint(signature[32:])
        if S >= L:
            return False
        digest = _sha512(signature[:32] + public + message)
        k = int.from_bytes(digest, "little") % L
        return _scalarmult(B, S) == _edwards_add(R, _scalarmult(A, k))
    except Exception:
        return False


def raw_public_from_spki_pem(pem: str) -> bytes:
    """Last 32 bytes of an Ed25519 SPKI PEM (the raw public key)."""
    import base64

    body = "".join(
        line.strip()
        for line in pem.splitlines()
        if line.strip() and not line.startswith("-----")
    )
    der = base64.b64decode(body)
    # SPKI: 12-byte AlgorithmIdentifier prefix, then the 32-byte key.
    if len(der) < 32:
        raise ValueError("SPKI too short")
    return der[-32:]


def main(argv):
    import sys

    if len(argv) != 4:
        sys.stderr.write("usage: ed25519_verify.py <spki.pem> <sig.raw> <file>\n")
        return 2
    pem = open(argv[1], encoding="utf8").read()
    signature = open(argv[2], "rb").read()
    message = open(argv[3], "rb").read()
    if verify(raw_public_from_spki_pem(pem), signature, message):
        return 0
    sys.stderr.write("signature does not verify\n")
    return 1


if __name__ == "__main__":
    import sys

    raise SystemExit(main(sys.argv))
ED25519_PY
  python3 "$SCRATCH/ed25519_verify.py" \
    "$SCRATCH/release.pub.pem" "$SCRATCH/release.sig" "$archive"
}

# `curl | sudo bash` is the classic supply-chain anti-pattern;
# user-scope install eliminates that whole class of risk.
if [ "$(id -u)" -eq 0 ]; then
  die "Refusing to install as root. Re-run as your regular user."
fi

# `uname -m` reports `arm64` on macOS and `aarch64` on Linux for the
# same hardware; normalize to the asset names the release publishes.
OS_RAW=$(uname -s)
ARCH_RAW=$(uname -m)
case "$OS_RAW" in
  Darwin) OS="darwin" ;;
  Linux)  OS="linux" ;;
  *) die "Unsupported OS '$OS_RAW'. macOS and Linux are supported." ;;
esac
case "$ARCH_RAW" in
  arm64|aarch64) ARCH="arm64" ;;
  x86_64)        ARCH="x64" ;;
  *) die "Unsupported architecture '$ARCH_RAW'. arm64 and x64 are supported." ;;
esac
TARGET="$OS-$ARCH"
log "detected platform: $TARGET"

# Set the trap BEFORE the first allocation so a SIGINT during download
# doesn't leak a tmpdir. Created before version resolution because the
# signed latest manifest is verified with the same scratch dir.
SCRATCH=$(mktemp -d "${TMPDIR:-/tmp}/rinkata-install.XXXXXX")
cleanup() { rm -rf "$SCRATCH"; }
trap cleanup EXIT INT TERM

# Version precedence: URL-served override, then $RINKATA_VERSION (or the
# legacy $PLUMB_VERSION alias) env, then the signed latest manifest. The
# unsigned `latest` object is not a trust anchor. The matched version must
# pass the strict vX.Y.Z gate before we construct any download URL.
MANIFEST_SHA=""
RINKATA_VERSION="${RINKATA_VERSION:-${PLUMB_VERSION:-}}"
if [ -n "$RINKATA_VERSION_OVERRIDE" ]; then
  VERSION="$RINKATA_VERSION_OVERRIDE"
  log "version pinned via ?version= → $VERSION"
elif [ -n "$RINKATA_VERSION" ]; then
  VERSION="$RINKATA_VERSION"
  log "version pinned via RINKATA_VERSION → $VERSION"
else
  log "resolving latest version..."
  curl -fsSL "$RELEASE_BUCKET_URL/latest.manifest" -o "$SCRATCH/latest.manifest" \
    || die "Failed to download latest.manifest."
  curl -fsSL "$RELEASE_BUCKET_URL/latest.manifest.sig" -o "$SCRATCH/latest.manifest.sig" \
    || die "Failed to download latest.manifest.sig."
  verify_release_signature "$SCRATCH/latest.manifest" "$SCRATCH/latest.manifest.sig" \
    || die "Signature verification failed for latest.manifest."
  VERSION=$(awk '$1 == "version" { print $2 }' "$SCRATCH/latest.manifest")
  MANIFEST_SHA=$(awk -v name="plumb-$TARGET.tar.gz" '$1 == name { print $2 }' "$SCRATCH/latest.manifest")
  [ -n "$VERSION" ] || die "Signed manifest has no version."
  [ -n "$MANIFEST_SHA" ] || die "Signed manifest has no digest for plumb-$TARGET.tar.gz."
  log "latest is $VERSION"
fi

# A malformed tag here is almost always a typo in $RINKATA_VERSION; fail
# loudly rather than constructing a download URL that 404s halfway.
if ! printf '%s' "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
  die "Version '$VERSION' doesn't match expected vX.Y.Z."
fi

BASE_URL="$RELEASE_BUCKET_URL/$VERSION"
ARCHIVE="plumb-$TARGET.tar.gz"
CHECKSUM="$ARCHIVE.sha256"

log "downloading $VERSION ($TARGET)..."
curl -fsSL "$BASE_URL/$ARCHIVE"  -o "$SCRATCH/$ARCHIVE"  || die "Failed to download $ARCHIVE from $BASE_URL"
curl -fsSL "$BASE_URL/$CHECKSUM" -o "$SCRATCH/$CHECKSUM" || die "Failed to download $CHECKSUM from $BASE_URL"

# When the version came from the signed manifest, the archive bytes must
# match the digest named there. Rewriting the unsigned `latest` object
# cannot select a different archive.
if [ -n "$MANIFEST_SHA" ]; then
  if command -v shasum >/dev/null 2>&1; then
    ACTUAL_SHA=$(shasum -a 256 "$SCRATCH/$ARCHIVE" | awk '{ print $1 }')
  elif command -v sha256sum >/dev/null 2>&1; then
    ACTUAL_SHA=$(sha256sum "$SCRATCH/$ARCHIVE" | awk '{ print $1 }')
  else
    die "Neither shasum nor sha256sum is available; cannot check the signed manifest."
  fi
  [ "$ACTUAL_SHA" = "$MANIFEST_SHA" ] || die "Archive digest does not match the signed latest manifest."
fi

log "verifying checksum..."
# macOS ships `shasum`; most Linux distros ship `sha256sum`. Both
# consume the same `<hex>  <filename>` format, so we just pick
# whichever is present.
(
  cd "$SCRATCH"
  if command -v shasum >/dev/null 2>&1; then
    shasum -a 256 -c "$CHECKSUM"
  elif command -v sha256sum >/dev/null 2>&1; then
    sha256sum -c "$CHECKSUM"
  else
    err "Neither shasum nor sha256sum is available; cannot verify download."
    exit 1
  fi
) || die "Checksum verification failed for $ARCHIVE."

SIGNATURE="$ARCHIVE.sig"
log "downloading signature..."
curl -fsSL "$BASE_URL/$SIGNATURE" -o "$SCRATCH/$SIGNATURE" || die "Failed to download $SIGNATURE from $BASE_URL"
log "verifying signature..."
verify_release_signature "$SCRATCH/$ARCHIVE" "$SCRATCH/$SIGNATURE" || die "Signature verification failed for $ARCHIVE."

# The release tarball puts the binaries at the top level, so unpacking into
# $INSTALL_DIR is the whole install. Subsequent runs overwrite via tar — that's
# the upgrade path.
mkdir -p "$INSTALL_DIR"
log "installing to $INSTALL_DIR..."
tar -xzf "$SCRATCH/$ARCHIVE" -C "$INSTALL_DIR"

# During the rename window the tarball may still ship `plumb`/`plumb-mcp`; make
# sure both the new `rinkata*` names and the legacy names resolve regardless of
# which the artifact contains (Spec 9 repackages the asset under rinkata names).
( cd "$INSTALL_DIR"
  [ -f rinkata ]     || { [ -f plumb ]     && cp plumb rinkata; }
  [ -f rinkata-mcp ] || { [ -f plumb-mcp ] && cp plumb-mcp rinkata-mcp; }
  [ -f plumb ]       || { [ -f rinkata ]     && cp rinkata plumb; }
  [ -f plumb-mcp ]   || { [ -f rinkata-mcp ] && cp rinkata-mcp plumb-mcp; }
  for b in rinkata rinkata-mcp plumb plumb-mcp; do [ -f "$b" ] && chmod +x "$b"; done
) || die "Failed to normalize binary names in $INSTALL_DIR."

# Run the just-installed binary directly — don't trust the user's PATH yet
# (it won't be updated until they `source` the rc or open a new shell). A
# binary that won't run is a hard fail; bail before touching shell rc.
log "verifying install..."
if ! INSTALLED_VERSION=$("$INSTALL_DIR/rinkata" --version 2>/dev/null); then
  die "rinkata --version failed after install. The downloaded binary may not match this platform."
fi
log "rinkata $INSTALLED_VERSION ✓"

# zsh / bash / fish via $SHELL, with a fallback to first-existing of
# (~/.zshrc, ~/.bashrc, ~/.profile) when $SHELL isn't recognized.
# Idempotency is guarded by a marker comment we grep for before
# appending.
MARKER="# rinkata-install:"
RC_FILE=""
RC_LINE=""

# fish uses `set -gx`; the POSIX shells use `export`. `\$HOME` keeps
# the literal `$HOME` in the rc file, so the line expands at rc-eval
# time and stays valid across users / hostnames.
RC_EXPORT_LINE="export PATH=\"\$HOME/.rinkata/bin:\$PATH\""
RC_FISH_LINE="set -gx PATH \$HOME/.rinkata/bin \$PATH"

case "${SHELL:-}" in
  */zsh)  RC_FILE="$HOME/.zshrc";  RC_LINE="$RC_EXPORT_LINE" ;;
  */bash) RC_FILE="$HOME/.bashrc"; RC_LINE="$RC_EXPORT_LINE" ;;
  */fish) RC_FILE="$HOME/.config/fish/config.fish"; RC_LINE="$RC_FISH_LINE" ;;
esac

if [ -z "$RC_FILE" ]; then
  for candidate in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.profile"; do
    if [ -f "$candidate" ]; then
      RC_FILE="$candidate"
      RC_LINE="$RC_EXPORT_LINE"
      break
    fi
  done
fi

if [ -z "$RC_FILE" ]; then
  log "no shell rc detected. To put rinkata on your PATH, add this to your shell config:"
  log "  export PATH=\"\$HOME/.rinkata/bin:\$PATH\""
elif [ -f "$RC_FILE" ] && grep -qF "$MARKER" "$RC_FILE"; then
  log "shell rc already updated: $RC_FILE (skipped)"
else
  # `>>` won't create intermediate directories; mkdir -p handles the
  # fish-on-a-fresh-box case where ~/.config/fish doesn't exist yet.
  mkdir -p "$(dirname "$RC_FILE")"
  TODAY=$(date +%Y-%m-%d)
  {
    printf '\n'
    printf '%s added %s\n' "$MARKER" "$TODAY"
    printf '%s\n' "$RC_LINE"
  } >> "$RC_FILE"
  log "added rinkata to PATH in: $RC_FILE"
fi

# Harness detection — check which AI agent tools are already installed
# so we can give tailored next-step advice. This is advisory only;
# `rinkata install` will also auto-detect at runtime.
DETECTED_HARNESSES=""
if [ -f "$HOME/.claude.json" ] || [ -d "$HOME/.claude" ]; then
  DETECTED_HARNESSES="$DETECTED_HARNESSES claude-code"
fi
if [ -f "$HOME/.codex/config.toml" ]; then
  DETECTED_HARNESSES="$DETECTED_HARNESSES codex"
fi
if [ -d "$HOME/.gemini/antigravity-cli" ]; then
  DETECTED_HARNESSES="$DETECTED_HARNESSES antigravity"
fi
if [ -f "$HOME/.openclaw/openclaw.json" ]; then
  DETECTED_HARNESSES="$DETECTED_HARNESSES openclaw"
fi
if [ -f "$HOME/.grok/config.toml" ] || [ -d "$HOME/.grok/skills" ]; then
  DETECTED_HARNESSES="$DETECTED_HARNESSES grok"
fi
if [ -f "$HOME/.hermes/config.yaml" ] || [ -d "$HOME/.hermes/skills" ]; then
  DETECTED_HARNESSES="$DETECTED_HARNESSES hermes"
fi
if [ -d "$HOME/Library/Application Support/Claude" ]; then
  DETECTED_HARNESSES="$DETECTED_HARNESSES claude-desktop"
fi
DETECTED_HARNESSES=$(printf '%s' "$DETECTED_HARNESSES" | sed 's/^ *//')

printf '\n'
log "rinkata is installed at $INSTALL_DIR/rinkata"
log ""
if [ -n "$DETECTED_HARNESSES" ]; then
  log "Detected agent harnesses: $DETECTED_HARNESSES"
  log ""
  log "Open a new shell (or source your rc) to pick up the PATH update,"
  log "then cd into a repo and run:"
  log ""
  log "    rinkata install"
  log ""
  log "to wire it into your detected harness(es) automatically."
else
  log "Open a new shell (or source your rc) to pick up the PATH update,"
  log "then cd into a repo and run:"
  log ""
  log "    rinkata install"
  log ""
  log "to wire it into Claude Code / Codex / Antigravity / OpenClaw / Hub."
fi
