#!/usr/bin/env bash
# agentina installer — fetch the latest signed tarball, verify, install.
#
# Production usage (single-line install via curl):
#
#   curl -fsSL https://install.agentina.io | sudo AGENTINA_TOKEN=act_... bash
#
# Or, for an existing artifact:
#
#   sudo bash install.sh \
#     --archive /tmp/agentina-0.0.1-linux-x64.tar.gz \
#     --token act_AAAAAAAAAAAAAAAAAAAAAAAA \
#     --cp https://cp.agentina.digcub-lab.com
#
# What it does:
#   1. Detect OS/arch
#   2. Download the latest stable manifest from dl.agentina.io
#      (or use --archive when run locally)
#   3. Verify cosign signature against keys/agentina-internal-release.pub
#   4. Create unprivileged 'agentina' user
#   5. Extract to /opt/agentina/<version>/, symlink current → version
#   6. Install systemd unit (or launchd plist on macOS)
#   7. If AGENTINA_TOKEN is set, run `agentina activate` immediately
#   8. enable + start the service
#
# Refuses to overwrite an existing install (use --upgrade for that).
# All steps are idempotent and reversible: a failed install leaves
# the previous version untouched.

# Re-exec with bash if running under a different shell (e.g., sh/dash).
if [ -z "${BASH_VERSION:-}" ]; then
  if command -v bash >/dev/null 2>&1; then
    exec bash "$0" "$@"
  else
    echo "ERROR: bash is required. Install it with: apt install -y bash" >&2
    exit 1
  fi
fi
set -euo pipefail

# ── Defaults ────────────────────────────────────────────────────
INSTALL_ROOT="${AGENTINA_INSTALL_ROOT:-/opt/agentina}"
STATE_DIR="${AGENTINA_STATE_DIR:-/var/lib/agentina}"
LOG_DIR="${AGENTINA_LOG_DIR:-/var/log/agentina}"
DEFAULT_CHANNEL="${AGENTINA_CHANNEL:-stable}"
CDN_BASE="${AGENTINA_CDN_BASE:-https://dl.agentina.digcub-lab.com/v1/dist}"
SYSTEM_USER="agentina"
SYSTEM_GROUP="agentina"

# ── Args ────────────────────────────────────────────────────────
ARCHIVE=""
TOKEN="${AGENTINA_TOKEN:-}"
CP_URL="${AGENTINA_CONTROL_PLANE_URL:-https://cp.agentina.digcub-lab.com}"
KEYS_PATH=""
UPGRADE=false
DRY_RUN=false
VERSION=""
ALLOW_DEV_BUILDS=false
ONBOARD=false
COSIGN_PUBKEY=""
ALLOW_UNSIGNED=false
INSECURE_TLOG=false
VERSION_PINNED=false

while [[ $# -gt 0 ]]; do
  case "$1" in
    --archive)         ARCHIVE="$2"; shift 2 ;;
    --token)           TOKEN="$2"; shift 2 ;;
    --cp)              CP_URL="$2"; shift 2 ;;
    --keys)            KEYS_PATH="$2"; shift 2 ;;
    --version)         VERSION="$2"; VERSION_PINNED=true; shift 2 ;;
    --channel)         DEFAULT_CHANNEL="$2"; shift 2 ;;
    --install-root)    INSTALL_ROOT="$2"; shift 2 ;;
    --state-dir)       STATE_DIR="$2"; shift 2 ;;
    --upgrade)         UPGRADE=true; shift ;;
    --dry-run)         DRY_RUN=true; shift ;;
    --allow-dev-builds) ALLOW_DEV_BUILDS=true; shift ;;
    --cosign-pubkey)   COSIGN_PUBKEY="$2"; shift 2 ;;
    --allow-unsigned)  ALLOW_UNSIGNED=true; shift ;;
    --insecure-ignore-tlog) INSECURE_TLOG=true; shift ;;
    --onboard)         ONBOARD=true; shift ;;
    --help|-h)         cat <<'USAGE'
agentina install — bootstrap or upgrade the Agentina agent.

Required (one of):
  --archive <path>          local tarball (skips download)
  (otherwise downloads <CDN>/<channel>/manifest.json + binary)

Optional:
  --token <act_...>         activate immediately (or AGENTINA_TOKEN env)
  --cp <url>                Control Plane URL (default https://cp.agentina.digcub-lab.com)
  --keys <path>             trusted-keys JSON for activate (or AGENTINA_TRUSTED_KEYS_PATH)
  --version <v>             pin a specific version instead of channel-latest
  --channel <ch>            stable | beta | enterprise-lts (default stable)
  --install-root <path>     default /opt/agentina
  --state-dir <path>        default /var/lib/agentina
  --upgrade                 allow overwriting existing install
  --dry-run                 print actions but don't execute
  --cosign-pubkey <path>    Agentina release public key. REQUIRED: every download is verified against it
                            (or AGENTINA_COSIGN_PUBKEY; a key pinned by an earlier install is reused)
  --allow-unsigned          development / staging only: install even when a download cannot be verified
                            (never accepts an INVALID signature)
  --insecure-ignore-tlog    verify without a transparency-log entry (throw-away dev keys / no Rekor access only)
  --allow-dev-builds        accept signing.status = "dev" / "unsigned" (after the signature verified)
                            (development / staging only — refused otherwise)
  --onboard                 after install, launch the onboarding wizard
                            (browser-based; activate + connectivity + service)
  --help                    this message
USAGE
      exit 0 ;;
    *)
      echo "unknown arg: $1" >&2; exit 2 ;;
  esac
done

# ── Preconditions ──────────────────────────────────────────────
if [[ "$EUID" -ne 0 ]] && ! $DRY_RUN; then
  echo "ERROR: must run as root (or use sudo). Try --dry-run to inspect first." >&2
  exit 2
fi

OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
case "$OS" in
  linux|darwin) ;;
  *) echo "ERROR: unsupported OS '$OS' — only linux and darwin supported"; exit 2 ;;
esac

ARCH_RAW="$(uname -m)"
case "$ARCH_RAW" in
  x86_64|amd64) ARCH="x64" ;;
  aarch64|arm64) ARCH="arm64" ;;
  *) echo "ERROR: unsupported arch '$ARCH_RAW'"; exit 2 ;;
esac

run() {
  if $DRY_RUN; then
    echo "  DRY: $*"
  else
    eval "$@"
  fi
}

# The key the agent CLI's own upgrade reads (<install-root>/keys/agentina-internal-release.pub) is where it is pinned;
# the Studio installer's pin (/etc/agentina/release.pub) is honoured too.
PINNED_RELEASE_KEY="$INSTALL_ROOT/keys/agentina-internal-release.pub"
AGENT_RELEASE_KEY="/etc/agentina/release.pub"
# >>> verify-artifact (unit-tested by scripts/test/install-verify.test.mjs — keep this block self-contained)
# Release-artifact verification (F-C7) — cryptographic and FAIL CLOSED.
#
# What a customer server downloads here becomes code that runs (the bundles) or the licence trust root
# (trusted-keys.json). A hash fetched from the same host as the artifact proves nothing, so the check is a
# cosign signature verified against a RELEASE PUBLIC KEY THAT DOES NOT COME FROM THAT HOST:
#   --cosign-pubkey FILE | AGENTINA_COSIGN_PUBKEY   the key for this run (also pinned for later runs)
#   /etc/agentina/release.pub                       pinned by an earlier install (used by the root updater)
#   /opt/agentina/keys/agentina-internal-release.pub  the key the agent CLI installer provisioned
# Nothing is accepted that cannot be verified: no key, no cosign, no published signature or an invalid one
# all REFUSE. The single opt-out is explicit — --allow-unsigned / AGENTINA_ALLOW_UNSIGNED=1 — and it only
# covers the ABSENCE of a verifier or a signature, never an INVALID signature. The transparency-log entry is
# required unless the operator passes --insecure-ignore-tlog (throw-away dev keys / no Rekor access); the
# manifest's own claims are never consulted for any of this.
_truthy() { case "${1:-}" in 1|true|yes|TRUE|YES) return 0 ;; *) return 1 ;; esac; }
# Position-independent: a script may set these (or parse flags into them) before OR after this block; it may also
# pre-set PINNED_RELEASE_KEY / AGENT_RELEASE_KEY to its own locations. The block text is identical in both installers.
COSIGN_PUBKEY="${COSIGN_PUBKEY:-${AGENTINA_COSIGN_PUBKEY:-}}"
PINNED_RELEASE_KEY="${AGENTINA_PINNED_KEY_PATH:-${PINNED_RELEASE_KEY:-/etc/agentina/release.pub}}"
AGENT_RELEASE_KEY="${AGENTINA_AGENT_KEY_PATH:-${AGENT_RELEASE_KEY:-/opt/agentina/keys/agentina-internal-release.pub}}"
ALLOW_UNSIGNED="${ALLOW_UNSIGNED:-false}";  if _truthy "${AGENTINA_ALLOW_UNSIGNED:-}"; then ALLOW_UNSIGNED=true; fi
INSECURE_TLOG="${INSECURE_TLOG:-false}";    if _truthy "${AGENTINA_COSIGN_INSECURE_IGNORE_TLOG:-}"; then INSECURE_TLOG=true; fi

_no_key_help() {
  echo "ERROR: no release public key found — cannot verify what would be installed." >&2
  echo "  Obtain the Agentina release public key from the vendor over a channel you trust (not from the download host)," >&2
  echo "  check its fingerprint, then re-run with:  --cosign-pubkey /path/to/agentina-internal-release.pub" >&2
  echo "  (it is pinned to $PINNED_RELEASE_KEY so later updates verify automatically)." >&2
  echo "  Development/staging only, knowingly unverified:  --allow-unsigned" >&2
}

# Pick the key (explicit > pinned > agent-provisioned). Refuses when there is none, unless --allow-unsigned.
resolve_release_key() {
  local k
  if [[ -z "$COSIGN_PUBKEY" ]]; then
    for k in "$PINNED_RELEASE_KEY" "$AGENT_RELEASE_KEY"; do
      if [[ -f "$k" ]]; then COSIGN_PUBKEY="$k"; break; fi
    done
  fi
  if [[ -z "$COSIGN_PUBKEY" ]]; then
    if $ALLOW_UNSIGNED; then
      echo "  ⚠ no release public key — artifacts will NOT be signature-verified (--allow-unsigned)"; return 0
    fi
    _no_key_help; return 1
  fi
  if [[ ! -f "$COSIGN_PUBKEY" ]]; then
    echo "ERROR: release public key not found: $COSIGN_PUBKEY" >&2
    if $ALLOW_UNSIGNED; then return 0; fi
    return 1
  fi
  return 0
}

require_cosign() {
  if command -v cosign >/dev/null 2>&1; then return 0; fi
  if $ALLOW_UNSIGNED; then echo "  ⚠ cosign is not installed — artifacts will NOT be signature-verified (--allow-unsigned)"; return 0; fi
  echo "ERROR: cosign is required to verify what would be installed, and is not installed." >&2
  echo "  Install it (https://docs.sigstore.dev/cosign/installation/), then re-run. Knowingly unverified: --allow-unsigned" >&2
  return 6
}

# verify_artifact <file> <signature-url> <label>  — returns 0 only if verified (or explicitly allowed to be absent).
verify_artifact() {
  local file="$1" sigsrc="$2" label="${3:-artifact}" sig
  if ! command -v cosign >/dev/null 2>&1; then
    if $ALLOW_UNSIGNED; then echo "  ⚠ $label NOT signature-verified (cosign not installed; --allow-unsigned)"; return 0; fi
    echo "ERROR: cosign is required to verify $label and is not installed — refusing." >&2
    echo "  Install it (https://docs.sigstore.dev/cosign/installation/), or knowingly skip: --allow-unsigned" >&2
    return 6
  fi
  if [[ -z "$COSIGN_PUBKEY" || ! -f "$COSIGN_PUBKEY" ]]; then
    if $ALLOW_UNSIGNED; then echo "  ⚠ $label NOT signature-verified (no release public key; --allow-unsigned)"; return 0; fi
    _no_key_help; return 6
  fi
  sig="$(mktemp)" || return 6
  if ! curl -fsSL "$sigsrc" -o "$sig" 2>/dev/null || [[ ! -s "$sig" ]]; then
    rm -f "$sig"
    if $ALLOW_UNSIGNED; then echo "  ⚠ $label NOT signature-verified (no signature published; --allow-unsigned)"; return 0; fi
    echo "ERROR: no signature published for $label ($sigsrc) — refusing to install it." >&2
    echo "  Knowingly unverified (development/staging only): --allow-unsigned" >&2
    return 6
  fi
  local flags=(--key "$COSIGN_PUBKEY" --signature "$sig")
  if $INSECURE_TLOG; then flags+=(--insecure-ignore-tlog); fi
  if cosign verify-blob "${flags[@]}" "$file" >/dev/null 2>&1; then
    rm -f "$sig"; echo "  ✓ $label signature verified"; return 0
  fi
  rm -f "$sig"
  echo "ERROR: $label signature INVALID (tampered download, wrong signer, or no transparency-log entry) — refusing to install it." >&2
  return 6
}

# verify_sha256 <file> <expected-hex> <label> — binds a download to the hash in the (already verified) manifest.
verify_sha256() {
  local file="$1" want="$2" label="${3:-artifact}" got
  if [[ ! "$want" =~ ^[a-f0-9]{64}$ ]]; then
    echo "ERROR: the signed manifest carries no valid sha256 for $label — refusing." >&2; return 6
  fi
  got="$(sha256sum "$file" | awk '{print $1}')"
  if [[ "$got" != "$want" ]]; then
    echo "ERROR: $label sha256 mismatch (manifest $want, download $got) — refusing." >&2; return 6
  fi
  echo "  ✓ $label matches the signed manifest's sha256"
}

# A version string becomes part of a URL and a path: keep it to one safe component ("..", "../x", "a b", ";rm" are not versions).
require_safe_version() {
  local v="${1:-}" what="${2:-version}"
  if [[ "$v" =~ ^[0-9A-Za-z][0-9A-Za-z._+-]{0,63}$ && "$v" != "." && "$v" != ".." ]]; then return 0; fi
  echo "ERROR: unsafe $what: '$v' (expected something like 1.2.3)" >&2; return 6
}

# refuse_downgrade <installed> <new> — a validly signed OLD release must not be replayed over a newer install.
refuse_downgrade() {
  local inst="${1:-}" new="${2:-}" a b c d e f
  if _truthy "${AGENTINA_ALLOW_DOWNGRADE:-}"; then return 0; fi
  [[ -n "$inst" && -n "$new" && "$inst" != "$new" ]] || return 0
  [[ "$inst" =~ ^[0-9]+(\.[0-9]+){0,2} && "$new" =~ ^[0-9]+(\.[0-9]+){0,2} ]] || return 0
  IFS=. read -r a b c _ <<< "${inst%%[-+]*}"; IFS=. read -r d e f _ <<< "${new%%[-+]*}"
  a=$((10#${a:-0})); b=$((10#${b:-0})); c=$((10#${c:-0})); d=$((10#${d:-0})); e=$((10#${e:-0})); f=$((10#${f:-0}))
  if (( d < a || (d == a && e < b) || (d == a && e == b && f < c) )); then
    echo "ERROR: refusing a downgrade from $inst to $new (set AGENTINA_ALLOW_DOWNGRADE=1 to roll back deliberately)." >&2; return 6
  fi
  return 0
}

# Remember the key for later runs (the root updater verifies with the pinned key, never with one that arrives with a download).
pin_release_key() {
  local key="$1" tmp
  [[ -f "$key" ]] || return 1
  if ! grep -q -e '-----BEGIN PUBLIC KEY-----' "$key"; then echo "ERROR: $key is not a PEM public key" >&2; return 6; fi
  if [[ "$key" -ef "$PINNED_RELEASE_KEY" ]]; then return 0; fi
  mkdir -p "$(dirname "$PINNED_RELEASE_KEY")" || return 1
  tmp="$(mktemp "$PINNED_RELEASE_KEY.XXXXXX")" || return 1
  if cat "$key" > "$tmp" && chmod 0644 "$tmp"; then
    if [[ "${EUID:-1}" -eq 0 ]]; then chown root:root "$tmp"; fi
    mv -f -- "$tmp" "$PINNED_RELEASE_KEY" && return 0
  fi
  rm -f -- "$tmp"; return 1
}
# assert_safe_tar_listing <archive.tar.gz> <label> — refuse to extract (as root!) an archive whose member list is not plain:
# only regular files and directories (no symlinks, hard links, devices, fifos), no absolute names, no ".." component, no
# control characters or backslashes (shown by tar as escapes), and a sane number of members. The archives are already
# signature-verified; this keeps a mis-built or compromised-but-signed archive from writing outside the target.
assert_safe_tar_listing() {
  local archive="$1" label="${2:-archive}" types names count
  types="$(tar --quoting-style=escape -tvzf "$archive" 2>/dev/null | awk '{ print substr($1,1,1) }')" \
    || { echo "ERROR: $label is not a readable gzip tar archive" >&2; return 1; }
  names="$(tar --quoting-style=escape -tzf "$archive" 2>/dev/null)" \
    || { echo "ERROR: $label is not a readable gzip tar archive" >&2; return 1; }
  if [ -z "$names" ]; then echo "ERROR: $label is empty" >&2; return 1; fi
  if printf '%s\n' "$types" | grep -qv '^[-d]$'; then
    echo "ERROR: $label contains a link or special file (only regular files and directories are allowed)" >&2; return 1
  fi
  if printf '%s\n' "$names" | grep -Eq '^/|(^|/)\.\.(/|$)|\\|^[A-Za-z]:'; then
    echo "ERROR: $label contains an absolute path, a '..' component or an unprintable name" >&2; return 1
  fi
  count="$(printf '%s\n' "$names" | wc -l)"
  if [ "$count" -gt 200000 ]; then echo "ERROR: $label has an implausible number of members ($count)" >&2; return 1; fi
  return 0
}
# <<< verify-artifact

echo "── agentina install ──"
echo "  os/arch:      $OS-$ARCH"
echo "  install root: $INSTALL_ROOT"
echo "  state dir:    $STATE_DIR"
echo "  channel:      $DEFAULT_CHANNEL"
echo "  upgrade?:     $UPGRADE"
echo "  dry-run?:     $DRY_RUN"
echo ""

# ── 0. prereq check ────────────────────────────────────────────
# Fail fast with actionable messages rather than confusing errors
# 5 steps in.
echo "── 0. prereqs ──"
MISSING=()
command -v node >/dev/null 2>&1 || MISSING+=("node (>=22)")
command -v tar  >/dev/null 2>&1 || MISSING+=("tar")
command -v curl >/dev/null 2>&1 || MISSING+=("curl")
if [[ "${#MISSING[@]}" -gt 0 ]]; then
  echo "ERROR: missing required tools: ${MISSING[*]}" >&2
  echo "  Install on Debian/Ubuntu: apt-get install -y nodejs tar curl" >&2
  echo "  Install on RHEL/Fedora:   dnf install -y nodejs tar curl" >&2
  echo "  Install on macOS:         brew install node curl  (tar is built in)" >&2
  exit 2
fi
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
if [[ "$NODE_MAJOR" -lt 20 ]]; then
  echo "ERROR: node $NODE_MAJOR is too old; agent requires node 20+" >&2
  exit 2
fi
echo "  node $(node --version)  tar $(tar --version 2>&1 | head -1 | awk '{print $NF}')  curl ok"
# F-C7: what is installed here runs as a service — it is verified against the release key and nothing unverifiable is
# accepted. Find the key and cosign NOW, before anything is downloaded.
require_cosign || exit 6
resolve_release_key || exit 6
if command -v cosign >/dev/null 2>&1; then
  echo "  cosign $(cosign version --json 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).gitVersion" 2>/dev/null || echo unknown) (signature verification ENABLED; release key: ${COSIGN_PUBKEY:-none})"
fi
echo ""

# ── Refuse re-install (unless --upgrade) ───────────────────────
if [[ -d "$INSTALL_ROOT/current" && "$UPGRADE" != "true" ]]; then
  echo "ERROR: $INSTALL_ROOT/current already exists. Pass --upgrade to overwrite." >&2
  exit 4
fi

# ── Acquire archive (download or local) ────────────────────────
if [[ -z "$ARCHIVE" ]]; then
  echo "── 1. download manifest ──"
  TMP="$(mktemp -d)"
  trap "rm -rf '$TMP'" EXIT
  MANIFEST_URL="$CDN_BASE/agent/$DEFAULT_CHANNEL/manifest.json"
  if ! curl -fsSL "$MANIFEST_URL" -o "$TMP/manifest.json"; then
    echo "ERROR: failed to fetch $MANIFEST_URL" >&2; exit 5
  fi
  # the manifest names the version that becomes part of a URL and a path: trust it only once its signature checks out
  verify_artifact "$TMP/manifest.json" "$MANIFEST_URL.sig" "release manifest" || exit 6
  if [[ -z "$VERSION" ]]; then
    VERSION="$(node -e "var m=JSON.parse(require('fs').readFileSync('$TMP/manifest.json'));console.log((m.current&&m.current.version)||m.version||'')")"
  fi
  require_safe_version "$VERSION" "release version" || exit 7
  ARCHIVE_URL="$CDN_BASE/agent/$DEFAULT_CHANNEL/$VERSION/agentina-${VERSION}-${OS}-${ARCH}.tar.gz"
  ARCHIVE="$TMP/$(basename "$ARCHIVE_URL")"
  ARCHIVE_SIG_URL="$ARCHIVE_URL.sig"
  echo "  fetching $ARCHIVE_URL"
  if ! curl -fsSL "$ARCHIVE_URL" -o "$ARCHIVE"; then
    echo "ERROR: failed to fetch $ARCHIVE_URL" >&2; exit 5
  fi
else
  ARCHIVE_SIG_URL="file://$ARCHIVE.sig"   # a local archive's signature lives next to it
fi

if [[ ! -f "$ARCHIVE" ]]; then
  echo "ERROR: archive not found: $ARCHIVE" >&2
  exit 5
fi

# ── 2. verify the archive's cosign signature — FAIL CLOSED (F-C7) ─────
#
# The archive is checked against the release public key BEFORE a single byte of it is extracted. Nothing about the
# archive (including the signing.status its own manifest claims) is consulted to decide HOW to verify: the
# transparency-log relaxation is the operator's choice (--insecure-ignore-tlog), never the archive's.
echo ""
echo "── 2. verify ──"
verify_artifact "$ARCHIVE" "$ARCHIVE_SIG_URL" "agent archive" || exit 6

# Verify SHA-256 of every file matches manifest.json (extracts to tmp first)
echo ""
echo "── 3. extract + verify manifest ──"
EXTRACT_DIR="$(mktemp -d)"
assert_safe_tar_listing "$ARCHIVE" "agent archive" || exit 6
tar --no-same-owner --no-same-permissions -xzf "$ARCHIVE" -C "$EXTRACT_DIR"
if [[ ! -f "$EXTRACT_DIR/manifest.json" ]]; then
  echo "ERROR: archive missing manifest.json" >&2; exit 7
fi
EXTRACTED_VERSION="$(node -e "console.log(JSON.parse(require('fs').readFileSync('$EXTRACT_DIR/manifest.json')).version)")"
if [[ -n "$VERSION" && "$VERSION" != "$EXTRACTED_VERSION" ]]; then
  echo "ERROR: manifest version $EXTRACTED_VERSION does not match requested $VERSION" >&2
  exit 7
fi
# The archive's manifest is signed content now, but a version still becomes a directory name that is removed with
# rm -rf on --upgrade: it must be ONE plain path component ("..", "../x" and quote characters are not versions).
require_safe_version "$EXTRACTED_VERSION" "archive version" || exit 7
VERSION="$EXTRACTED_VERSION"
echo "  → version $VERSION verified"
if $UPGRADE && ! $VERSION_PINNED && [[ -L "$INSTALL_ROOT/current" ]]; then
  refuse_downgrade "$(basename "$(readlink "$INSTALL_ROOT/current")")" "$VERSION" || exit 6
fi
if [[ -n "$COSIGN_PUBKEY" && "$COSIGN_PUBKEY" != "$PINNED_RELEASE_KEY" ]] && ! $DRY_RUN; then
  mkdir -p "$INSTALL_ROOT/keys" && pin_release_key "$COSIGN_PUBKEY" || true   # later `agentina upgrade` runs verify with it
fi

# Read the signing.status field that build-bundle.sh stamped onto the
# manifest. Refuse "unsigned" or "dev" builds in production unless the
# operator explicitly opted in with --allow-dev-builds.
SIGNING_STATUS="$(node -e "
const m = JSON.parse(require('fs').readFileSync('$EXTRACT_DIR/manifest.json'));
console.log(m.signing && m.signing.status || 'unsigned');
")"
echo "  → signing.status = $SIGNING_STATUS"
if [[ "$SIGNING_STATUS" != "kms" && "$ALLOW_DEV_BUILDS" != "true" ]]; then
  echo "ERROR: refusing to install a $SIGNING_STATUS build in production." >&2
  echo "  Production releases must be signed via KMS (signing.status = 'kms')." >&2
  echo "  Pass --allow-dev-builds to override (development / staging only)." >&2
  exit 8
fi

# ── 4. create system user (idempotent) ─────────────────────────
echo ""
echo "── 4. system user ──"
if [[ "$OS" == "linux" ]]; then
  if ! getent passwd "$SYSTEM_USER" >/dev/null; then
    run "useradd --system --no-create-home --shell /usr/sbin/nologin '$SYSTEM_USER'"
    echo "  → created $SYSTEM_USER"
  else
    echo "  ✓ $SYSTEM_USER already exists"
  fi
elif [[ "$OS" == "darwin" ]]; then
  if ! id "$SYSTEM_USER" >/dev/null 2>&1; then
    echo "  WARN: macOS user creation requires manual setup (use System Settings or sysadminctl)"
  fi
fi

# ── 5. install to $INSTALL_ROOT/<version>/, symlink current ────
echo ""
echo "── 5. install files ──"
TARGET="$INSTALL_ROOT/$VERSION"
run "mkdir -p '$INSTALL_ROOT' '$STATE_DIR' '$LOG_DIR'"
# State dir holds private keys + bearer + license — read by agent only.
run "chmod 0700 '$STATE_DIR'"
# Log dir: agent writes, group can read for ops debugging via `adm`,
# but world cannot. (ProtectSystem=strict in the unit means the
# service can't widen these later.)
run "chmod 0750 '$LOG_DIR'"
run "chown $SYSTEM_USER:$SYSTEM_GROUP '$STATE_DIR' '$LOG_DIR' 2>/dev/null || true"
if [[ -d "$TARGET" ]]; then
  if $UPGRADE; then
    run "rm -rf '$TARGET'"
  else
    echo "ERROR: $TARGET exists; pass --upgrade" >&2; exit 4
  fi
fi
# Layout: <version>/manifest.json + <version>/bundle/main.js
# Self-verify walks up from main.js, finds manifest.json in parent.
# The agentina shim in bundle/ resolves SCRIPT_DIR to bundle/, so
# "node $SCRIPT_DIR/main.js" works. The symlink current/ points at
# the version dir; ExecStart and CLI use current/bundle/agentina.
run "mkdir -p '$TARGET'"
run "mv '$EXTRACT_DIR/bundle' '$TARGET/bundle'"
run "cp '$EXTRACT_DIR/manifest.json' '$TARGET/manifest.json'"
run "ln -sfn '$TARGET' '$INSTALL_ROOT/current'"
# CLI convenience: /usr/local/bin/agentina → current/bundle/agentina
run "ln -sf '$INSTALL_ROOT/current/bundle/agentina' /usr/local/bin/agentina 2>/dev/null || true"
echo "  → installed to $TARGET"
echo "  → $INSTALL_ROOT/current → $TARGET"

# ── 6. install service unit ────────────────────────────────────
#
# Soft-skipped when no service manager is available (e.g. container
# without systemd). The installer is still useful for "install the
# files, I'll handle the service" workflows.
echo ""
echo "── 6. service unit ──"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SERVICE_INSTALLED=false
if [[ "$OS" == "linux" ]]; then
  if ! command -v systemctl >/dev/null 2>&1; then
    echo "  ⚠ systemctl not present — skipping service unit install"
    echo "    (you can run /opt/agentina/current/bundle/agentina run manually)"
  else
    if [[ -f "$SCRIPT_DIR/../systemd/agentina.service" ]]; then
      UNIT_SRC="$SCRIPT_DIR/../systemd/agentina.service"
    elif [[ -f "$INSTALL_ROOT/current/packaging/systemd/agentina.service" ]]; then
      UNIT_SRC="$INSTALL_ROOT/current/packaging/systemd/agentina.service"
    else
      echo "  WARN: agentina.service template not found — skip systemd install"
      UNIT_SRC=""
    fi
    if [[ -n "$UNIT_SRC" ]]; then
      run "install -m 0644 '$UNIT_SRC' /etc/systemd/system/agentina.service"
      run "systemctl daemon-reload"
      echo "  → /etc/systemd/system/agentina.service installed"
      SERVICE_INSTALLED=true
    fi
  fi
elif [[ "$OS" == "darwin" ]]; then
  if ! command -v launchctl >/dev/null 2>&1; then
    echo "  ⚠ launchctl not present — skipping plist install"
  elif [[ -f "$SCRIPT_DIR/../launchd/io.agentina.agent.plist" ]]; then
    run "install -m 0644 '$SCRIPT_DIR/../launchd/io.agentina.agent.plist' /Library/LaunchDaemons/io.agentina.agent.plist"
    echo "  → /Library/LaunchDaemons/io.agentina.agent.plist installed"
    SERVICE_INSTALLED=true
  fi
fi

# ── 6b. trusted keys ──────────────────────────────────────────
# Download the trusted public keys needed for license JWT verification.
# These ship separately from the tarball so key rotation doesn't require
# a new agent release.
KEYS_FILE="$INSTALL_ROOT/keys/trusted-keys.json"
if [[ -z "$KEYS_PATH" ]] && [[ ! -f "$KEYS_FILE" ]]; then
  echo ""
  echo "── 6b. trusted keys ──"
  run "mkdir -p '$INSTALL_ROOT/keys'"
  KEYS_URL="$CDN_BASE/keys/trusted-keys.json"
  KEYS_TMP="$(mktemp -d)"
  if curl -fsSL "$KEYS_URL" -o "$KEYS_TMP/trusted-keys.json" 2>/dev/null; then
    # The root of licence and remote-uninstall trust: verified like the archive, refused on any failure (F-C7).
    verify_artifact "$KEYS_TMP/trusted-keys.json" "$KEYS_URL.sig" "trusted keys" || { rm -rf "$KEYS_TMP"; exit 6; }
    run "install -m 0644 '$KEYS_TMP/trusted-keys.json' '$KEYS_FILE'"
    echo "  → downloaded and verified trusted keys"
  else
    echo "  ⚠ could not download trusted keys from $KEYS_URL"
  fi
  rm -rf "$KEYS_TMP"
fi
# Also copy to /etc/agentina for the interactive activate default
if [[ -f "$KEYS_FILE" ]] && [[ ! -f /etc/agentina/trusted-keys.json ]]; then
  run "mkdir -p /etc/agentina"
  run "cp '$KEYS_FILE' /etc/agentina/trusted-keys.json"
fi

# ── 6c. systemd unit (generate if not bundled) ────────────────
if ! $SERVICE_INSTALLED && [[ "$OS" == "linux" ]] && command -v systemctl >/dev/null 2>&1; then
  echo ""
  echo "── 6c. generating systemd unit ──"
  cat > /etc/systemd/system/agentina.service <<UNITEOF
[Unit]
Description=Agentina Agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=$SYSTEM_USER
ExecStart=$INSTALL_ROOT/current/bundle/agentina run --state-dir $STATE_DIR
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=agentina
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
UNITEOF
  systemctl daemon-reload
  echo "  → /etc/systemd/system/agentina.service generated"
  SERVICE_INSTALLED=true
fi

# ── 7. activate (if token provided) ────────────────────────────
ACTIVATED=false
if [[ -n "$TOKEN" ]]; then
  echo ""
  echo "── 7. activate ──"
  ACT_ARGS=("--token" "$TOKEN" "--cp" "$CP_URL" "--state-dir" "$STATE_DIR" "--no-prompt")
  if [[ -n "$KEYS_PATH" ]]; then
    ACT_ARGS+=("--keys" "$KEYS_PATH")
  elif [[ -f "$KEYS_FILE" ]]; then
    ACT_ARGS+=("--keys" "$KEYS_FILE")
  elif [[ -f /etc/agentina/trusted-keys.json ]]; then
    ACT_ARGS+=("--keys" "/etc/agentina/trusted-keys.json")
  fi
  if run "sudo -u $SYSTEM_USER '$INSTALL_ROOT/current/bundle/agentina' activate ${ACT_ARGS[*]}"; then
    ACTIVATED=true
  else
    echo "  ⚠ activation failed — see error above; service will not start cleanly until activation succeeds"
  fi
fi

# ── 8. enable + start service ──────────────────────────────────
echo ""
echo "── 8. start service ──"
if ! $SERVICE_INSTALLED; then
  echo "  (no service manager available; start manually with:"
  echo "     sudo -u $SYSTEM_USER /opt/agentina/current/bundle/agentina run --state-dir $STATE_DIR )"
elif [[ "$OS" == "linux" ]] && [[ -f /etc/systemd/system/agentina.service ]]; then
  run "systemctl enable agentina.service"
  run "systemctl restart agentina.service"
  echo "  → systemctl status agentina.service to inspect"
elif [[ "$OS" == "darwin" ]] && [[ -f /Library/LaunchDaemons/io.agentina.agent.plist ]]; then
  run "launchctl load /Library/LaunchDaemons/io.agentina.agent.plist"
  echo "  → launchctl list | grep agentina to inspect"
fi

# ── 9. post-install verification ───────────────────────────────
echo ""
echo "── 9. verify ──"
INSTALLED_VERSION=""
if [[ -x "$INSTALL_ROOT/current/bundle/agentina" ]] && ! $DRY_RUN; then
  INSTALLED_VERSION="$("$INSTALL_ROOT/current/bundle/agentina" version 2>/dev/null \
    | node -e "
      let buf=''; process.stdin.on('data',c=>buf+=c).on('end',()=>{
        try { console.log(JSON.parse(buf).version); } catch { console.log(''); }
      });" 2>/dev/null || echo "")"
  if [[ -n "$INSTALLED_VERSION" ]]; then
    echo "  ✓ agentina $INSTALLED_VERSION installed and runnable"
  else
    echo "  ⚠ agentina binary present but `version` failed — investigate"
  fi
fi

# Check whether the service actually came up (when service was installed
# AND we tried to start it AND we're not in dry-run).
SERVICE_RUNNING=false
if $SERVICE_INSTALLED && ! $DRY_RUN; then
  if [[ "$OS" == "linux" ]] && command -v systemctl >/dev/null 2>&1; then
    if systemctl is-active --quiet agentina.service 2>/dev/null; then
      SERVICE_RUNNING=true
      echo "  ✓ agentina.service is active"
    elif $ACTIVATED; then
      echo "  ⚠ agentina.service is NOT active (activation succeeded — check journalctl -u agentina -n 50)"
    fi
  fi
fi

# ── done ──
echo ""
echo "── done ──"
echo ""
echo "agentina $INSTALLED_VERSION installed at $INSTALL_ROOT/current"
echo ""
if ! $ACTIVATED; then
  echo "Next: activate the agent."
  echo ""
  echo "  Interactive:"
  echo "    sudo -u $SYSTEM_USER $INSTALL_ROOT/current/bundle/agentina activate"
  echo ""
  echo "  Or with flags:"
  echo "    sudo -u $SYSTEM_USER $INSTALL_ROOT/current/bundle/agentina activate \\"
  echo "      --token act_XXXXXXXXXXXXXXXXXXXXXXXX \\"
  echo "      --cp    $CP_URL \\"
  echo "      --keys  /path/to/trusted-keys.json"
  echo ""
  if $SERVICE_INSTALLED; then
    echo "After activation, start the service:"
    if [[ "$OS" == "linux" ]]; then
      echo "    sudo systemctl restart agentina"
      echo "    sudo systemctl status  agentina"
      echo "    sudo journalctl -u agentina -f"
    elif [[ "$OS" == "darwin" ]]; then
      echo "    sudo launchctl kickstart -k system/io.agentina.agent"
    fi
  else
    echo "Run the agent in foreground:"
    echo "    sudo -u $SYSTEM_USER $INSTALL_ROOT/current/bundle/agentina run --state-dir $STATE_DIR"
  fi
elif $SERVICE_RUNNING; then
  PORTAL_URL="${AGENTINA_MARKETING_URL:-https://agentina.digcub-lab.com}"
  echo ""
  echo "  ✔ Agent installed"
  echo "  ✔ License activated"
  echo "  ✔ Connected to cloud"
  echo "  ✔ Service running"
  echo ""
  echo "  Dashboard: $PORTAL_URL/portal"
  echo ""
  if [[ "$OS" == "linux" ]]; then
    echo "  Logs:   sudo journalctl -u agentina -f"
    echo "  Status: sudo systemctl status agentina"
  fi
elif $SERVICE_INSTALLED; then
  echo "Agent activated. Starting service..."
  systemctl restart agentina 2>/dev/null || true
  sleep 2
  if systemctl is-active --quiet agentina.service 2>/dev/null; then
    PORTAL_URL="${AGENTINA_MARKETING_URL:-https://agentina.digcub-lab.com}"
    echo ""
    echo "  ✔ Agent installed"
    echo "  ✔ License activated"
    echo "  ✔ Connected to cloud"
    echo "  ✔ Service running"
    echo ""
    echo "  Dashboard: $PORTAL_URL/portal"
  else
    echo "  ⚠ Service not yet running. Check: sudo journalctl -u agentina -n 50"
  fi
else
  echo "Agent activated. Run manually:"
  echo "  sudo -u $SYSTEM_USER $INSTALL_ROOT/current/bundle/agentina run --state-dir $STATE_DIR"
fi
echo ""
echo "Status:     sudo -u $SYSTEM_USER $INSTALL_ROOT/current/bundle/agentina status --state-dir $STATE_DIR"
echo "Quickstart: ${AGENTINA_DOCS_URL:-https://agentina.digcub-lab.com/docs/quickstart}"
echo "Support:    ${AGENTINA_DOCS_URL:-https://agentina.digcub-lab.com/portal/support}"

# ── 10. optional: launch the onboarding wizard ─────────────────
if $ONBOARD && ! $DRY_RUN && ! $ACTIVATED; then
  echo ""
  echo "── 10. launching onboarding wizard ──"
  if [[ "$EUID" -ne 0 ]]; then
    echo "  ⚠ not running as root; wizard cannot install the systemd unit"
  fi
  ONBOARD_ARGS=(--state-dir "$STATE_DIR")
  [[ -n "$CP_URL"    ]] && ONBOARD_ARGS+=(--cp "$CP_URL")
  [[ -n "$KEYS_PATH" ]] && ONBOARD_ARGS+=(--keys "$KEYS_PATH")
  exec "$INSTALL_ROOT/current/bundle/agentina" onboard "${ONBOARD_ARGS[@]}"
fi
