#!/bin/sh
# aiot-edge one-line installer v0.1.1
# Release artifact: release/v0.1.1/install.sh
# POSIX sh (not bash). Idempotent. Signature-verified (minisign + SHA256).
#
# Usage: sh install.sh --enroll-token=<TOKEN> [--gateway-uri=<URI>]
#                      [--install-prefix=<DIR>] [--dry-run]
#                      [--unsafe-skip-sig-verify]
#
# SECURITY: --enroll-token value is accepted via flag for convenience but
# prefer the AIOT_ENROLL_TOKEN environment variable to prevent leakage via
# process argument lists:
#   export AIOT_ENROLL_TOKEN=<TOKEN>
#   sh install.sh [--gateway-uri=<URI>]
#
# Self-verification instructions (recommended before running):
#   curl -O https://aethermesh.app/install.sh
#   curl -O https://aethermesh.app/install.sh.minisig
#   minisign -Vm install.sh -P RWS7TatrmwpCgr+chZpBn7gLyBwoYvQqG7rodsXrOjiehSGJcBFnRtV4
#   sh install.sh --enroll-token=YOUR_TOKEN
set -eu
IFS=$(printf ' \t\n')

# --- Defaults ---
GATEWAY_URI="https://gateway.aethermesh.app"
INSTALL_PREFIX="/usr/local"
ENROLL_TOKEN="${AIOT_ENROLL_TOKEN:-}"
DRY_RUN=0
UNSAFE_SKIP_SIG_VERIFY=0

MINISIGN_PUBKEY="RWS7TatrmwpCgr+chZpBn7gLyBwoYvQqG7rodsXrOjiehSGJcBFnRtV4"

AIOT_CONF_DIR="/etc/aiot-edge"
SCRIPT_START=$(date +%s)

# --- Helpers ---
phase() { printf '\n[%s] %s\n' "$1" "$2"; }
ok()    { printf '  ok   %s\n' "$1"; }
warn()  { printf '  WARN %s\n' "$1" >&2; }
fail()  {
    _fp=$1; _fr=$2; _fm=$3; _fh=$4
    printf '[FAIL phase=%s reason=%s] %s\nHint: %s\n' \
        "$_fp" "$_fr" "$_fm" "$_fh" >&2
    exit 1
}

# --- Argument parsing ---
for _arg in "$@"; do
    case "$_arg" in
        --enroll-token=*)        ENROLL_TOKEN="${_arg#--enroll-token=}" ;;
        --gateway-uri=*)         GATEWAY_URI="${_arg#--gateway-uri=}" ;;
        --install-prefix=*)      INSTALL_PREFIX="${_arg#--install-prefix=}" ;;
        --dry-run)               DRY_RUN=1 ;;
        --unsafe-skip-sig-verify) UNSAFE_SKIP_SIG_VERIFY=1 ;;
        *)
            fail 0 invalid_arg \
                "Unknown argument: ${_arg}" \
                "Valid flags: --enroll-token=<T> --gateway-uri=<U> --install-prefix=<P> --dry-run --unsafe-skip-sig-verify"
            ;;
    esac
done

# Prefer AIOT_ENROLL_TOKEN env var; --enroll-token flag is accepted as fallback
# but prints a security advisory to stderr to discourage routine use.
if [ -z "$ENROLL_TOKEN" ]; then
    fail 0 missing_arg \
        "--enroll-token or AIOT_ENROLL_TOKEN env var is required" \
        "Prefer: export AIOT_ENROLL_TOKEN=<TOKEN> && sh install.sh  (avoids /proc/\$PID/cmdline leakage)"
fi

# If token arrived via CLI flag (not env var), warn about cmdline exposure.
# We can detect this by checking whether AIOT_ENROLL_TOKEN was already set.
if [ -z "${AIOT_ENROLL_TOKEN:-}" ]; then
    warn "SECURITY: enroll token passed via CLI arg — visible in /proc/\$PID/cmdline"
    warn "          Prefer: export AIOT_ENROLL_TOKEN=<TOKEN> && sh install.sh"
fi

# [1/10] Detect architecture + OS
phase "1/10" "Detecting architecture and OS"

_arch=$(uname -m)
_kname=$(uname -s)
_kver=$(uname -r | cut -c1-100)

case "${_kname}/${_arch}" in
    Linux/aarch64)
        ok "Platform: aarch64-linux (musl binary compatible)"
        ;;
    *)
        fail 1 unsupported_arch \
            "Only aarch64-linux supported in v0.1.1 (detected ${_kname}/${_arch})" \
            "Use an aarch64 device: Raspberry Pi 4/5, Jetson Nano, etc."
        ;;
esac

if [ -d /lib/aarch64-linux-gnu ] && ! [ -e /lib/ld-musl-aarch64.so.1 ]; then
    warn "glibc-only system detected; musl static binary is untested — proceed with caution"
fi

OS_KERNEL_STR=$(printf '%s %s' "$_kname" "$_kver" | cut -c1-128)
CPU_ARCH="aarch64"
ok "arch=${CPU_ARCH}  kernel=${OS_KERNEL_STR}"

# [2/10] Detect prerequisites
phase "2/10" "Checking prerequisites"

HAS_MINISIGN=0

command -v curl > /dev/null 2>&1 || \
    fail 2 missing_prereq "curl not found" "apt install curl  /  apk add curl"

command -v openssl > /dev/null 2>&1 || \
    fail 2 missing_prereq "openssl not found" "apt install openssl  /  apk add openssl"

if command -v sha256sum > /dev/null 2>&1; then
    ok "sha256sum: found"
elif command -v shasum > /dev/null 2>&1; then
    ok "shasum: found"
else
    fail 2 missing_prereq "sha256sum / shasum not found" "apt install coreutils"
fi

if command -v minisign > /dev/null 2>&1; then
    HAS_MINISIGN=1
    ok "minisign: found"
else
    # FAIL-CLOSED: minisign is required for release signature verification.
    # Without it we cannot prove the binary has not been tampered with.
    if [ "$UNSAFE_SKIP_SIG_VERIFY" != "1" ]; then
        fail 2 missing_prereq \
            "minisign not found — release signature verification is required" \
            "Install minisign and re-run:
  Debian/Ubuntu : apt install minisign
  Alpine        : apk add minisign
  macOS (brew)  : brew install minisign
  Manual        : https://jedisct1.github.io/minisign/
  To bypass (NOT RECOMMENDED): re-run with --unsafe-skip-sig-verify"
    else
        warn "##############################################################"
        warn "# WARNING: --unsafe-skip-sig-verify is set.                  #"
        warn "# Release signature verification is DISABLED.                #"
        warn "# The binary authenticity CANNOT be guaranteed.              #"
        warn "# Do NOT use this in production environments.                #"
        warn "##############################################################"
        warn "minisign not found — signature check bypassed by operator request"
    fi
fi

ok "Prerequisites satisfied"

# --- Dry-run exit (phases 1-2 done; fetch SHA256 only; no mutations) ---
if [ "$DRY_RUN" = "1" ]; then
    printf '\n[DRY-RUN] Would install  : %s/bin/aiot-edge\n' "$INSTALL_PREFIX"
    printf '[DRY-RUN] Would enroll at: %s/v1/devices/enroll\n' "$GATEWAY_URI"
    printf '[DRY-RUN] Would write env: %s/runtime.env\n' "$AIOT_CONF_DIR"
    printf '[DRY-RUN] Fetching SHA256 manifest (read-only)...\n'
    _sha_url="${GATEWAY_URI}/releases/aiot-edge-aarch64-musl.sha256"
    if curl -fsSL --max-time 15 -o /tmp/aiot-edge-dryrun.sha256 "$_sha_url" 2>/dev/null; then
        printf '[DRY-RUN] SHA256 manifest: '
        cat /tmp/aiot-edge-dryrun.sha256
        printf '\n'
    else
        printf '[DRY-RUN] SHA256 manifest not yet published at %s\n' "$_sha_url"
        printf '[DRY-RUN] (Expected; binary releases pending R2 upload)\n'
    fi
    _elapsed=$(( $(date +%s) - SCRIPT_START ))
    printf '\n[DRY-RUN COMPLETE] phases 1-2 verified in %ss. No system changes made.\n' "$_elapsed"
    exit 0
fi

# [3/10] Generate device Ed25519 keypair
phase "3/10" "Generating device Ed25519 keypair"

mkdir -p "$AIOT_CONF_DIR"
# P2-3: Restrict conf dir to root-only (0700); private keys live here.
chmod 0700 "$AIOT_CONF_DIR"

if [ -f "${AIOT_CONF_DIR}/device.key" ]; then
    ok "Keypair exists — reusing ${AIOT_CONF_DIR}/device.key (idempotent)"
else
    openssl genpkey -algorithm ed25519 \
        -out "${AIOT_CONF_DIR}/device.key" 2>/dev/null || \
        fail 3 keygen_failed \
            "openssl genpkey -algorithm ed25519 failed" \
            "Ensure openssl >= 1.1.1: openssl version"
    chmod 0600 "${AIOT_CONF_DIR}/device.key"
    ok "Keypair generated: ${AIOT_CONF_DIR}/device.key (0600)"
fi

openssl pkey -in "${AIOT_CONF_DIR}/device.key" -pubout \
    -out "${AIOT_CONF_DIR}/device.pub" 2>/dev/null || \
    fail 3 pubkey_extract_failed \
        "Failed to extract public key from device.key" \
        "Inspect: openssl pkey -in ${AIOT_CONF_DIR}/device.key -text -noout"

ok "Public key: ${AIOT_CONF_DIR}/device.pub"

# [4/10] Download binary
phase "4/10" "Downloading aiot-edge binary"

_bin_url="${GATEWAY_URI}/releases/aiot-edge-aarch64-musl"
_bin_new="${INSTALL_PREFIX}/bin/aiot-edge.new"

mkdir -p "${INSTALL_PREFIX}/bin"

curl -fsSL --max-time 120 -o "$_bin_new" "$_bin_url" || \
    fail 4 download_failed \
        "Binary download failed from ${_bin_url}" \
        "Check network connectivity and GATEWAY_URI; try: curl -I ${_bin_url}"

ok "Downloaded: ${_bin_new}"

# [5/10] Verify SHA256
phase "5/10" "Verifying SHA256 checksum"

_sha_url="${GATEWAY_URI}/releases/aiot-edge-aarch64-musl.sha256"

curl -fsSL --max-time 15 -o /tmp/aiot-edge-install.sha256 "$_sha_url" || \
    fail 5 sha256_fetch_failed \
        "Cannot fetch SHA256 manifest from ${_sha_url}" \
        "Releases may not be published yet; contact support"

_expected=$(awk '{print $1; exit}' /tmp/aiot-edge-install.sha256)

if command -v sha256sum > /dev/null 2>&1; then
    _actual=$(sha256sum "$_bin_new" | awk '{print $1}')
else
    _actual=$(shasum -a 256 "$_bin_new" | awk '{print $1}')
fi

[ "$_actual" = "$_expected" ] || \
    fail 5 sha256_mismatch \
        "Checksum mismatch (expected=${_expected} got=${_actual})" \
        "Re-run installer to retry download; persistent = file corrupt or tampered"

ok "SHA256 verified: ${_actual}"

# [6/10] Verify minisign signature
phase "6/10" "Verifying minisign signature"

if [ "$HAS_MINISIGN" = "1" ]; then
    _sig_url="${GATEWAY_URI}/releases/aiot-edge-aarch64-musl.minisig"
    curl -fsSL --max-time 15 -o /tmp/aiot-edge-install.minisig "$_sig_url" || \
        fail 6 sig_fetch_failed \
            "Cannot fetch .minisig from ${_sig_url}" \
            "Releases may not be published yet; contact support"
    printf '%s\n' "$MINISIGN_PUBKEY" > /tmp/aiot-edge-release.pub
    minisign -V -p /tmp/aiot-edge-release.pub \
        -m "$_bin_new" -x /tmp/aiot-edge-install.minisig || \
        fail 6 sig_verify_failed \
            "minisign verification FAILED — binary may be tampered" \
            "Do NOT install. Contact security@aethermesh.app immediately."
    ok "minisign signature verified"
else
    # HAS_MINISIGN=0 and UNSAFE_SKIP_SIG_VERIFY=1 (already enforced above).
    warn "Phase 6 SKIPPED — install minisign for full signature verification"
fi

# [7/10] Atomic install
phase "7/10" "Atomically installing binary"

chmod 0755 "$_bin_new"
mv "$_bin_new" "${INSTALL_PREFIX}/bin/aiot-edge"
ok "Installed: ${INSTALL_PREFIX}/bin/aiot-edge (0755)"

# [8/10] Enroll device (RFC-0007 §3.1)
phase "8/10" "Enrolling device with gateway"

# Gather hardware attestation (RFC-0007 §3 hardware_attestation schema)
if [ -f /proc/cpuinfo ]; then
    _cpu_count=$(grep -c 'processor' /proc/cpuinfo 2>/dev/null || printf '1')
else
    _cpu_count=$(nproc 2>/dev/null || printf '1')
fi
[ "${_cpu_count:-0}" -ge 1 ] || _cpu_count=1

if [ -f /proc/meminfo ]; then
    _mem_kb=$(awk '/MemTotal/{print $2; exit}' /proc/meminfo)
    _mem_bytes=$(( _mem_kb * 1024 ))
else
    _mem_bytes=1073741824
fi

# Sanitize kernel string: ASCII printable only, max 128 chars (RFC-0001 v1.1 §7)
_os_kernel_safe=$(printf '%s' "$OS_KERNEL_STR" | tr -cd ' A-Za-z0-9._-' | cut -c1-128)

# Build PEM as JSON string (escape embedded newlines as \n)
_pub_pem_json=$(awk '{printf "%s\\n", $0}' "${AIOT_CONF_DIR}/device.pub")

# Construct JSON body (RFC-0007 §3 schema; additionalProperties: false)
_body=$(printf \
    '{"enroll_token":"%s","hardware_attestation":{"cpu_arch":"%s","os_kernel":"%s","sysinfo_summary":{"cpu_count":%d,"mem_total_bytes":%d}},"public_key_pem":"%s"}' \
    "$ENROLL_TOKEN" \
    "$CPU_ARCH" \
    "$_os_kernel_safe" \
    "$_cpu_count" \
    "$_mem_bytes" \
    "$_pub_pem_json")

if ! _enroll_out=$(curl -sS --max-time 30 \
        -X POST \
        -H 'Content-Type: application/json; charset=utf-8' \
        -d "$_body" \
        "${GATEWAY_URI}/v1/devices/enroll" 2>&1); then
    # Zeroize token from memory before failing
    ENROLL_TOKEN=""
    unset ENROLL_TOKEN AIOT_ENROLL_TOKEN
    fail 8 enroll_network_error \
        "curl to /v1/devices/enroll failed: ${_enroll_out}" \
        "Check network and GATEWAY_URI; test: curl -v ${GATEWAY_URI}/healthz"
fi

# Zeroize token from shell memory now that enrollment HTTP call is complete.
# The token is not passed to the aiot-edge binary at runtime; it is used
# only once here during the enrollment HTTP POST.
ENROLL_TOKEN=""
unset ENROLL_TOKEN AIOT_ENROLL_TOKEN

# Check for server-side error (RFC-0007 §8 / RFC-0003 §V13.4)
case "$_enroll_out" in
    *'"error"'*)
        _err_code=$(printf '%s' "$_enroll_out" | \
            sed 's/.*"code"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | head -1)
        fail 8 "server_${_err_code}" \
            "Enrollment rejected by gateway: ${_err_code}" \
            "Verify enroll token is valid and not expired (TTL 24h per RFC-0007 §5)"
        ;;
esac

# Parse response fields (sed/awk; no jq dependency)
_node_id=$(printf '%s' "$_enroll_out" | \
    sed 's/.*"node_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | head -1)
_rt_token=$(printf '%s' "$_enroll_out" | \
    sed 's/.*"runtime_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | head -1)

[ -n "$_node_id" ] || \
    fail 8 enroll_parse_failed \
        "Could not parse node_id from enrollment response" \
        "Inspect raw response; ensure gateway implements RFC-0007 §4"

# Write runtime env (permissions 0600; secret not echoed)
printf 'AIOT_NODE_ID=%s\nAIOT_RUNTIME_TOKEN=%s\n' \
    "$_node_id" "$_rt_token" > "${AIOT_CONF_DIR}/runtime.env"
chmod 0600 "${AIOT_CONF_DIR}/runtime.env"

ok "Enrolled:    node_id=${_node_id}"
ok "Runtime env: ${AIOT_CONF_DIR}/runtime.env (0600)"

# [9/10] Install systemd unit (or print OpenRC alternative)
phase "9/10" "Installing service unit"

if command -v systemctl > /dev/null 2>&1; then
    id aiot > /dev/null 2>&1 || \
        useradd --system --no-create-home --shell /usr/sbin/nologin aiot 2>/dev/null || true
    chown aiot "${AIOT_CONF_DIR}/runtime.env" 2>/dev/null || true

    # AIOT_ENROLL_TOKEN is intentionally absent from the service unit.
    # Enrollment is a one-time operation performed by this installer; the
    # runtime token in AIOT_RUNTIME_TOKEN (EnvironmentFile) is used for
    # all subsequent authenticated requests. To re-enroll, re-run install.sh.
    cat > /etc/systemd/system/aiot-edge.service << EOF
[Unit]
Description=aiot-edge device daemon
After=network-online.target
Wants=network-online.target

[Service]
User=aiot
ExecStart=${INSTALL_PREFIX}/bin/aiot-edge
Restart=always
RestartSec=5
EnvironmentFile=${AIOT_CONF_DIR}/runtime.env

[Install]
WantedBy=multi-user.target
EOF

    systemctl daemon-reload
    systemctl enable --now aiot-edge || \
        fail 9 service_start_failed \
            "systemctl enable --now aiot-edge failed" \
            "Inspect: journalctl -u aiot-edge -n 50 --no-pager"
    ok "systemd unit installed and started"
else
    warn "systemd not detected — manual service setup required"
    printf '  OpenRC / init.d alternative:\n'
    printf '    start-stop-daemon --start \\\n'
    printf '      --exec %s/bin/aiot-edge \\\n' "$INSTALL_PREFIX"
    printf '      -- --env-file %s/runtime.env\n' "$AIOT_CONF_DIR"
fi

# [10/10] Smoke check
phase "10/10" "Smoke check"

if command -v systemctl > /dev/null 2>&1; then
    _svc_status=$(systemctl is-active aiot-edge 2>/dev/null || printf 'unknown')
    [ "$_svc_status" = "active" ] || \
        fail 10 service_not_active \
            "aiot-edge status=${_svc_status} (expected: active)" \
            "Run: journalctl -u aiot-edge -n 50 --no-pager"
    ok "aiot-edge: active"
fi

_elapsed=$(( $(date +%s) - SCRIPT_START ))
printf '\n'
printf '=============================================\n'
printf 'DONE  node_id     = %s\n' "$_node_id"
printf '      Time-to-Aha = %ss\n' "$_elapsed"
printf '=============================================\n'
