#!/usr/bin/env bash
#
# Installs or updates Arquimeding on a Linux host.
#
# It is safe to run twice: the data directory, the license and the UI password
# survive an update. The desktop calls it over the SSH connection it already
# opens for remote instances; a person can also run it by hand:
#
#   curl -fsSL https://arquimeding.com/updates/files/install-remote.sh \
#     | sudo bash -s -- --license ARQ1...
#
# The last line of output is machine readable:
#   ARQUIMEDING_INSTALL_OK {"version":"2.4.0",...}
#
set -euo pipefail

BASE_URL="${ARQUIMEDING_RELEASE_BASE_URL:-https://arquimeding.com/updates/files}"
UPDATE_URL="${ARQUIMEDING_UPDATE_API_URL:-}"
PORT=3100
HOST=127.0.0.1
DATA_DIR=/var/lib/arquimeding
PREFIX=/usr/local/lib/arquimeding
LICENSE_KEY="${ARQUIMEDING_LICENSE_KEY:-}"
UI_PASSWORD=""
OPENCODE_BIN=""
OPENCODE_VERSION=""
SERVICE=arquimeding

usage() {
  cat <<'EOF'
Usage: install-remote.sh [options]

  --license KEY      License key to install (ARQ1...). Kept if omitted on update.
  --password VALUE   UI password. Generated when omitted.
  --port N           Port to listen on (default: 3100).
  --host ADDR        Bind address (default: 127.0.0.1).
  --data-dir PATH    Data directory (default: /var/lib/arquimeding).
  --base-url URL     Where the tarballs and latest.json live.
  --update-url URL   Update check endpoint written to the service environment.
  --opencode-binary PATH  Use an OpenCode CLI already on this host.
  --opencode-version V    OpenCode CLI version to install (default: match the app).
  --uninstall        Remove the service and the installed package. Keeps data.
  -h, --help         Show this help.
EOF
}

while [ $# -gt 0 ]; do
  case "$1" in
    --license) LICENSE_KEY="${2:?missing value}"; shift 2 ;;
    --password) UI_PASSWORD="${2:?missing value}"; shift 2 ;;
    --port) PORT="${2:?missing value}"; shift 2 ;;
    --host) HOST="${2:?missing value}"; shift 2 ;;
    --data-dir) DATA_DIR="${2:?missing value}"; shift 2 ;;
    --base-url) BASE_URL="${2:?missing value}"; shift 2 ;;
    --update-url) UPDATE_URL="${2:?missing value}"; shift 2 ;;
    --opencode-binary) OPENCODE_BIN="${2:?missing value}"; shift 2 ;;
    --opencode-version) OPENCODE_VERSION="${2:?missing value}"; shift 2 ;;
    --uninstall) UNINSTALL=1; shift ;;
    -h|--help) usage; exit 0 ;;
    *) echo "unknown option: $1" >&2; usage >&2; exit 2 ;;
  esac
done

if [ "$(id -u)" -ne 0 ]; then
  echo "install-remote.sh must run as root (use sudo)" >&2
  exit 1
fi

if ! command -v systemctl >/dev/null 2>&1; then
  echo "systemd is required; this host has no systemctl" >&2
  exit 1
fi

if [ "${UNINSTALL:-}" = 1 ]; then
  systemctl disable --now "$SERVICE" 2>/dev/null || true
  rm -f "/etc/systemd/system/$SERVICE.service"
  systemctl daemon-reload || true
  rm -rf "$PREFIX"
  echo "ARQUIMEDING_UNINSTALL_OK"
  exit 0
fi

BASE_URL="${BASE_URL%/}"
if [ -z "$UPDATE_URL" ]; then
  UPDATE_URL="${BASE_URL%/updates/files}/v1/update/check"
fi

need() {
  command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2; exit 1; }
}

install_system_packages() {
  if command -v apt-get >/dev/null 2>&1; then
    DEBIAN_FRONTEND=noninteractive apt-get update -qq
    DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl ca-certificates git xz-utils >/dev/null
  elif command -v dnf >/dev/null 2>&1; then
    dnf install -y -q curl ca-certificates git xz >/dev/null
  elif command -v yum >/dev/null 2>&1; then
    yum install -y -q curl ca-certificates git xz >/dev/null
  fi
}

if ! command -v curl >/dev/null 2>&1 \
  || ! command -v tar >/dev/null 2>&1 \
  || ! command -v git >/dev/null 2>&1 \
  || ! command -v xz >/dev/null 2>&1; then
  install_system_packages
fi
need curl
need tar

version_at_least() {
  # version_at_least <found> <required>
  [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" = "$2" ]
}

NODE_BIN=""
NPM_CMD=()
if command -v node >/dev/null 2>&1; then
  FOUND_NODE="$(node -v | sed 's/^v//')"
  if version_at_least "$FOUND_NODE" "24.14.0"; then
    NODE_BIN="$(command -v node)"
    if command -v npm >/dev/null 2>&1 && npm --version >/dev/null 2>&1; then
      NPM_CMD=( npm )
    fi
  fi
fi

if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
  echo "Installing a private Node.js runtime under $PREFIX/node" >&2
  case "$(uname -m)" in
    x86_64|amd64) NODE_ARCH=x64 ;;
    aarch64|arm64) NODE_ARCH=arm64 ;;
    *) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;;
  esac
  NODE_VERSION=24.14.0
  NODE_DIR="$PREFIX/node"
  NODE_TARBALL="node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz"
  mkdir -p "$NODE_DIR"
  TMP_NODE="$(mktemp -d)"
  trap 'rm -rf "$TMP_NODE"' EXIT
  curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/${NODE_TARBALL}" -o "$TMP_NODE/$NODE_TARBALL"
  if curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" -o "$TMP_NODE/SHASUMS256.txt"; then
    ( cd "$TMP_NODE" && grep " $NODE_TARBALL\$" SHASUMS256.txt | sha256sum -c - >/dev/null )
  else
    echo "warning: could not verify the Node.js checksum" >&2
  fi
  tar -xJf "$TMP_NODE/$NODE_TARBALL" -C "$NODE_DIR" --strip-components=1
  NODE_BIN="$NODE_DIR/bin/node"
  NPM_CMD=( "$NODE_BIN" "$NODE_DIR/lib/node_modules/npm/bin/npm-cli.js" )
fi

if [ ${#NPM_CMD[@]} -eq 0 ]; then
  CANDIDATE="$(dirname "$NODE_BIN")/../lib/node_modules/npm/bin/npm-cli.js"
  if [ -f "$CANDIDATE" ]; then
    NPM_CMD=( "$NODE_BIN" "$CANDIDATE" )
  fi
fi
if [ ${#NPM_CMD[@]} -eq 0 ]; then
  echo "npm was not found next to $NODE_BIN" >&2
  exit 1
fi

echo "Node: $("$NODE_BIN" -v)" >&2

LATEST_JSON="$(curl -fsSL "$BASE_URL/latest.json")"
VERSION="$(printf '%s' "$LATEST_JSON" | sed -n 's/.*"latestVersion"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
TARBALL_URL="$(printf '%s' "$LATEST_JSON" | sed -n 's/.*"downloadUrl"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
if [ -z "$TARBALL_URL" ]; then
  echo "latest.json at $BASE_URL/latest.json has no downloadUrl" >&2
  exit 1
fi

ALLOW=()
if "${NPM_CMD[@]}" install --help 2>/dev/null | grep -q -- '--allow-scripts'; then
  ALLOW+=( --allow-scripts=node-pty --allow-scripts=bun-pty --allow-scripts=sherpa-onnx-node )
fi

if systemctl list-unit-files "$SERVICE.service" >/dev/null 2>&1; then
  systemctl stop "$SERVICE" 2>/dev/null || true
fi

echo "Installing Arquimeding $VERSION from $TARBALL_URL" >&2
"${NPM_CMD[@]}" install -g --prefix "$PREFIX" --no-audit --no-fund "${ALLOW[@]}" "$TARBALL_URL"

CLI="$PREFIX/lib/node_modules/@arquimeding/web/bin/cli.js"
if [ ! -f "$CLI" ]; then
  echo "the package installed but $CLI is missing" >&2
  exit 1
fi

if ! id -u "$SERVICE" >/dev/null 2>&1; then
  useradd --system --home "$DATA_DIR" --shell /usr/sbin/nologin "$SERVICE"
fi
mkdir -p "$DATA_DIR"
chown -R "$SERVICE:$SERVICE" "$DATA_DIR"

if [ -z "$OPENCODE_BIN" ]; then
  OPENCODE_BIN="$DATA_DIR/.opencode/bin/opencode"
fi
if [ ! -x "$OPENCODE_BIN" ]; then
  if [ -z "$OPENCODE_VERSION" ]; then
    CLIENT_PKG="$PREFIX/lib/node_modules/@arquimeding/web/node_modules/@opencode/client/package.json"
    if [ -f "$CLIENT_PKG" ]; then
      OPENCODE_VERSION="$("$NODE_BIN" -p "require('$CLIENT_PKG').version" 2>/dev/null || true)"
    fi
    OPENCODE_VERSION="${OPENCODE_VERSION:-2.0.32}"
  fi
  echo "Installing the OpenCode CLI $OPENCODE_VERSION for $SERVICE" >&2
  if command -v runuser >/dev/null 2>&1; then
    runuser -u "$SERVICE" -- env HOME="$DATA_DIR" PATH="/usr/local/bin:/usr/bin:/bin" \
      bash -c "curl -fsSL https://opencode.ai/v2/install | bash -s -- --version '$OPENCODE_VERSION' --no-modify-path"
  else
    su -s /bin/bash "$SERVICE" -c "export HOME='$DATA_DIR'; export PATH=/usr/local/bin:/usr/bin:/bin; curl -fsSL https://opencode.ai/v2/install | bash -s -- --version '$OPENCODE_VERSION' --no-modify-path"
  fi
fi
if [ ! -x "$OPENCODE_BIN" ]; then
  echo "the OpenCode CLI is not available at $OPENCODE_BIN" >&2
  echo "install it or pass --opencode-binary" >&2
  exit 1
fi

if [ -n "$LICENSE_KEY" ]; then
  case "$LICENSE_KEY" in
    ARQ1.*) ;;
    *) echo "warning: the license key does not start with ARQ1." >&2 ;;
  esac
  printf '{\n  "version": 1,\n  "key": "%s",\n  "activatedAt": "%s"\n}\n' \
    "$LICENSE_KEY" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$DATA_DIR/license.json"
  chown "$SERVICE:$SERVICE" "$DATA_DIR/license.json"
  chmod 600 "$DATA_DIR/license.json"
fi

if [ -z "$UI_PASSWORD" ] && [ -f /etc/arquimeding.env ]; then
  UI_PASSWORD="$(sed -n 's/^ARQUIMEDING_UI_PASSWORD=//p' /etc/arquimeding.env | head -n1)"
fi
if [ -z "$UI_PASSWORD" ]; then
  UI_PASSWORD="$(head -c 48 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 28)"
fi
if [ -z "$UI_PASSWORD" ]; then
  echo "could not generate a UI password; pass --password" >&2
  exit 1
fi
umask 077
printf 'ARQUIMEDING_UI_PASSWORD=%s\n' "$UI_PASSWORD" > /etc/arquimeding.env
umask 022

cat > "/etc/systemd/system/$SERVICE.service" <<EOF
[Unit]
Description=Arquimeding
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=$SERVICE
Group=$SERVICE
WorkingDirectory=$DATA_DIR
Environment=HOME=$DATA_DIR
Environment=PATH=$DATA_DIR/.opencode/bin:$PREFIX/node/bin:/usr/local/bin:/usr/bin:/bin
Environment=OPENCODE_BINARY=$OPENCODE_BIN
Environment=ARQUIMEDING_DATA_DIR=$DATA_DIR
Environment=ARQUIMEDING_UPDATE_API_URL=$UPDATE_URL
EnvironmentFile=-/etc/arquimeding.env
ExecStart=$NODE_BIN $CLI serve --foreground --host $HOST --port $PORT
Restart=always
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable "$SERVICE" >/dev/null 2>&1 || true
systemctl restart "$SERVICE"

CHECK_HOST="$HOST"
[ "$CHECK_HOST" = "0.0.0.0" ] && CHECK_HOST=127.0.0.1
for _ in $(seq 1 30); do
  if curl -fsS "http://$CHECK_HOST:$PORT/health" >/dev/null 2>&1; then
    LICENSED=false
    [ -f "$DATA_DIR/license.json" ] && LICENSED=true
    printf 'ARQUIMEDING_INSTALL_OK {"version":"%s","port":%s,"host":"%s","dataDir":"%s","password":"%s","licensed":%s}\n' \
      "$VERSION" "$PORT" "$HOST" "$DATA_DIR" "$UI_PASSWORD" "$LICENSED"
    exit 0
  fi
  sleep 1
done

echo "the service did not answer /health on $HOST:$PORT" >&2
systemctl status "$SERVICE" --no-pager >&2 || true
journalctl -u "$SERVICE" -n 40 --no-pager >&2 || true
exit 1
