#!/usr/bin/env bash
#
# Beezifi UFW Tool - installer
#
# Downloads the agent from your Beezifi Security instance and installs it.
# Run as root on a systemd host (the normal case) and it is installed as the
# "beezifi-ufw-tool" systemd service: dedicated unprivileged user, sudo limited
# to the ufw binary, restart on failure, start on boot. Re-running upgrades in place.
#
# Usage:
#   sudo ./install.sh --host https://your-beezifi-host --agent-secret bza_... [options]
#
# Options:
#   --host <url>           Base URL of your Beezifi Security instance (required)
#   --agent-secret <s>     Agent secret shown when you added this host on the
#                          Firewall page (bza_...). Prompted for if omitted.
#   --dir <path>           Install directory (default: /opt/beezifi-ufw-tool as root,
#                          otherwise $HOME/beezifi-ufw-tool)
#   --port <n>             Port the agent listens on (default 4001)
#   --bind <addr>          Listen address (default 127.0.0.1). Anything else needs
#                          --ssl-cert and --ssl-key: the agent refuses plaintext on the network.
#   --ssl-cert <path>      TLS certificate chain (e.g. /etc/letsencrypt/live/<host>/fullchain.pem)
#   --ssl-key <path>       TLS private key  (e.g. /etc/letsencrypt/live/<host>/privkey.pem)
#   --allow-from <ips>     Comma-separated platform IPs: adds a ufw rule allowing
#                          the agent port only from them
#   --allow-rules          Also let the platform add "allow" rules (default: deny-only)
#   --pm2                  Run under pm2 instead of systemd (non-root installs)
#   --no-run               Install and configure only; don't start anything
#   --api-key <key>        Legacy: raw platform API key instead of an agent secret
#
set -euo pipefail

HOST=""
INSTALL_DIR=""
API_KEY=""
AGENT_SECRET=""
PORT=4001
BIND_HOST="127.0.0.1"
SSL_CERT=""
SSL_KEY=""
ALLOW_FROM=""
ALLOW_RULES=0
RUN_AFTER_INSTALL=1
USE_PM2=0
PM2_APP_NAME="beezifi-ufw-tool"
SERVICE_NAME="beezifi-ufw-tool"
SERVICE_USER="beezifi-agent"
TARBALL_NAME="beezifi-ufw-tool.tar.gz"

log()  { printf '\033[1;34m[ufw-tool]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[ufw-tool]\033[0m %s\n' "$*" >&2; }
die()  { printf '\033[1;31m[ufw-tool]\033[0m %s\n' "$*" >&2; exit 1; }

# --- Parse args ---------------------------------------------------------------
while [ $# -gt 0 ]; do
  case "$1" in
    --host)         HOST="$2"; shift 2 ;;
    --dir)          INSTALL_DIR="$2"; shift 2 ;;
    --agent-secret) AGENT_SECRET="$2"; shift 2 ;;
    --api-key)      API_KEY="$2"; shift 2 ;;
    --port)         PORT="$2"; shift 2 ;;
    --bind)         BIND_HOST="$2"; shift 2 ;;
    --ssl-cert)     SSL_CERT="$2"; shift 2 ;;
    --ssl-key)      SSL_KEY="$2"; shift 2 ;;
    --allow-from)   ALLOW_FROM="$2"; shift 2 ;;
    --allow-rules)  ALLOW_RULES=1; shift ;;
    --pm2)          USE_PM2=1; shift ;;
    --no-run)       RUN_AFTER_INSTALL=0; shift ;;
    -h|--help)      sed -n '2,29p' "$0"; exit 0 ;;
    *) die "Unknown argument: $1 (use --help for usage)" ;;
  esac
done

[ -n "$HOST" ] || die "Missing required --host <url>, e.g. --host https://security.example.com"
HOST="${HOST%/}"
{ [[ "$PORT" =~ ^[0-9]+$ ]] && [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ]; } || die "--port must be 1-65535"

IS_ROOT=0; [ "$(id -u)" = "0" ] && IS_ROOT=1
HAS_SYSTEMD=0; [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1 && HAS_SYSTEMD=1
USE_SYSTEMD=0
if [ "$USE_PM2" = "0" ] && [ "$IS_ROOT" = "1" ] && [ "$HAS_SYSTEMD" = "1" ]; then USE_SYSTEMD=1; fi

if [ -z "$INSTALL_DIR" ]; then
  if [ "$IS_ROOT" = "1" ]; then INSTALL_DIR="/opt/beezifi-ufw-tool"; else INSTALL_DIR="$HOME/beezifi-ufw-tool"; fi
fi

case "$BIND_HOST" in
  127.0.0.1|::1|localhost) ;;
  *) { [ -n "$SSL_CERT" ] && [ -n "$SSL_KEY" ]; } \
       || die "--bind $BIND_HOST needs --ssl-cert and --ssl-key: the agent refuses to listen on the network without TLS." ;;
esac
if [ -n "$SSL_CERT$SSL_KEY" ]; then
  { [ -n "$SSL_CERT" ] && [ -n "$SSL_KEY" ]; } || die "Pass both --ssl-cert and --ssl-key."
  { [ -r "$SSL_CERT" ] && [ -r "$SSL_KEY" ]; } || die "Cannot read $SSL_CERT / $SSL_KEY (run as root)."
fi
ALLOW_IPS=()
if [ -n "$ALLOW_FROM" ]; then
  [ "$IS_ROOT" = "1" ] || die "--allow-from changes ufw and needs root."
  IFS=',' read -r -a ALLOW_IPS <<< "$ALLOW_FROM"
  for ip in "${ALLOW_IPS[@]}"; do
    [[ "$ip" =~ ^[0-9a-fA-F:.]+(/[0-9]{1,3})?$ ]] || die "Invalid --allow-from address: $ip"
  done
fi

# --- Prerequisites ------------------------------------------------------------
command -v curl >/dev/null 2>&1 || die "curl is required but not found. Install it and re-run."
command -v tar  >/dev/null 2>&1 || die "tar is required but not found. Install it and re-run."

if ! command -v node >/dev/null 2>&1; then
  die "Node.js was not found. Install Node.js 22 LTS first, e.g.:
  Debian/Ubuntu: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs
  RHEL/Fedora:   curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash - && sudo yum install -y nodejs
Then re-run this installer."
fi
NODE_BIN="$(command -v node)"
NODE_MAJOR="$(node -e 'process.stdout.write(process.versions.node.split(".")[0])')"
if [ "$NODE_MAJOR" -lt 22 ]; then
  die "Node.js 22+ is required (18 and 20 are end-of-life) (found $(node -v)). Upgrade Node.js and re-run this installer."
fi

UFW_BIN="$(command -v ufw || true)"
if [ -z "$UFW_BIN" ]; then
  [ "$USE_SYSTEMD" = "1" ] && die "ufw is required: apt-get install -y ufw (Debian/Ubuntu) or dnf install -y ufw (RHEL/Fedora)."
  warn "ufw was not found on this host. The tool will install, but rule commands will fail until ufw is installed."
fi

if [ "$USE_SYSTEMD" = "1" ] && [ -n "$SSL_CERT" ]; then
  SYSTEMD_VERSION="$(systemctl --version | awk 'NR==1 {print $2}')"
  [ "${SYSTEMD_VERSION:-0}" -ge 247 ] \
    || die "systemd $SYSTEMD_VERSION is too old for LoadCredential (needs 247+). Keep --bind 127.0.0.1 behind a TLS reverse proxy instead."
fi

if [ -z "$AGENT_SECRET" ] && [ -z "$API_KEY" ]; then
  if [ -t 0 ]; then
    printf '\033[1;34m[ufw-tool]\033[0m Enter the agent secret for this host from the Firewall page (input hidden): '
    read -r -s AGENT_SECRET
    echo
  fi
  [ -n "$AGENT_SECRET" ] || die "No agent secret given. Pass --agent-secret bza_..."
fi
if [ -n "$AGENT_SECRET" ] && ! [[ "$AGENT_SECRET" =~ ^bza_[0-9a-f]{64}$ ]]; then
  die "That does not look like an agent secret (expected bza_ followed by 64 hex characters)."
fi

# --- Download -----------------------------------------------------------------
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT

log "Downloading ${TARBALL_NAME} from ${HOST} ..."
curl -fsSL "${HOST}/downloads/${TARBALL_NAME}" -o "${TMP_DIR}/${TARBALL_NAME}" \
  || die "Download failed. Check the --host value and that the platform is reachable."

if [ "$USE_SYSTEMD" = "1" ] && systemctl is-active --quiet "$SERVICE_NAME"; then
  log "Stopping running ${SERVICE_NAME} for upgrade ..."
  systemctl stop "$SERVICE_NAME"
fi

# --- Extract ------------------------------------------------------------------
log "Installing to ${INSTALL_DIR} ..."
mkdir -p "$INSTALL_DIR"
tar -xzf "${TMP_DIR}/${TARBALL_NAME}" -C "$INSTALL_DIR" --strip-components=1 --no-same-owner
cd "$INSTALL_DIR"

# --- Configure ----------------------------------------------------------------
set_env() { # set_env NAME VALUE - replaces a commented or uncommented line, or appends
  if grep -qE "^#? *$1=" .env; then
    sed -i.bak -E "s#^\#? *$1=.*#$1=$2#" .env
    rm -f .env.bak
  else
    printf '%s=%s\n' "$1" "$2" >> .env
  fi
}

[ -f .env ] || cp .env.example .env
if [ -n "$AGENT_SECRET" ]; then
  set_env BEEZIFI_AGENT_SECRETS "$AGENT_SECRET"
else
  set_env BEEZIFI_SECURITY_API_KEYS "$API_KEY"
  warn "Configured a legacy platform API key. Issue an agent secret on the Firewall page and switch to --agent-secret."
fi
set_env UFW_PORT "$PORT"
set_env UFW_BIND_HOST "$BIND_HOST"
set_env UFW_ALLOWED_ACTIONS "$([ "$ALLOW_RULES" = "1" ] && echo 'allow,deny,reject' || echo 'deny,reject')"
if [ -n "$SSL_CERT" ] && [ "$USE_SYSTEMD" = "0" ]; then
  set_env SSL_CERT "$SSL_CERT"
  set_env SSL_KEY "$SSL_KEY"
fi

# --- npm install --------------------------------------------------------------
log "Installing npm dependencies ..."
npm ci --omit=dev --no-audit --no-fund >/dev/null

# --- systemd service ----------------------------------------------------------
if [ "$USE_SYSTEMD" = "1" ]; then
  if ! id "$SERVICE_USER" >/dev/null 2>&1; then
    log "Creating system user ${SERVICE_USER} ..."
    useradd --system --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin "$SERVICE_USER"
  fi

  # Code is root-owned and read-only to the service; only the secret file is group-readable.
  chown -R root:root "$INSTALL_DIR"
  chmod -R go-w "$INSTALL_DIR"
  chown "root:${SERVICE_USER}" .env
  chmod 640 .env

  # The service may run exactly one privileged command: ufw.
  SUDOERS=/etc/sudoers.d/beezifi-ufw-tool
  printf '%s ALL=(root) NOPASSWD: %s\n' "$SERVICE_USER" "$UFW_BIN" > "${SUDOERS}.tmp"
  chmod 440 "${SUDOERS}.tmp"
  visudo -cf "${SUDOERS}.tmp" >/dev/null || { rm -f "${SUDOERS}.tmp"; die "Generated sudoers entry failed validation."; }
  mv "${SUDOERS}.tmp" "$SUDOERS"

  CRED_LINES=""
  if [ -n "$SSL_CERT" ]; then
    # systemd reads the root-only key as root and hands the service a private copy.
    CRED_LINES="LoadCredential=tls-cert:${SSL_CERT}
LoadCredential=tls-key:${SSL_KEY}"
    if [ -d /etc/letsencrypt/renewal-hooks/deploy ]; then
      printf '#!/bin/sh\nsystemctl try-restart %s\n' "$SERVICE_NAME" > "/etc/letsencrypt/renewal-hooks/deploy/${SERVICE_NAME}.sh"
      chmod 755 "/etc/letsencrypt/renewal-hooks/deploy/${SERVICE_NAME}.sh"
    fi
  fi

  cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=Beezifi UFW Tool (firewall agent)
Documentation=${HOST}/docs.html
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=${SERVICE_USER}
Group=${SERVICE_USER}
WorkingDirectory=${INSTALL_DIR}
ExecStart=${NODE_BIN} ${INSTALL_DIR}/server.js
Environment=NODE_ENV=production
${CRED_LINES}
Restart=on-failure
RestartSec=5
# Hardening that still lets "sudo ufw" change the firewall (no NoNewPrivileges or
# ProtectSystem: ufw runs as a child of this service and must write /etc/ufw).
PrivateTmp=true
ProtectHome=true
ProtectControlGroups=true
LockPersonality=true

[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
  systemctl enable "$SERVICE_NAME" >/dev/null 2>&1
fi

# --- Inbound access for the platform --------------------------------------------
if [ "${#ALLOW_IPS[@]}" -gt 0 ]; then
  for ip in "${ALLOW_IPS[@]}"; do
    "$UFW_BIN" allow proto tcp from "$ip" to any port "$PORT" comment 'Beezifi agent from platform' >/dev/null
  done
  log "Inbound TCP ${PORT} allowed from: ${ALLOW_FROM} (not tagged beezifi:, so the agent can't remove it)"
fi
if [ -n "$UFW_BIN" ] && [ "$IS_ROOT" = "1" ] && "$UFW_BIN" status | grep -q 'Status: inactive'; then
  warn "ufw is inactive: rules will be stored but not enforced, and the agent can't list them by number. Enable it with 'ufw enable' (allow SSH first)."
fi

log "Install complete: ${INSTALL_DIR}"

# --- Run ----------------------------------------------------------------------
if [ "$RUN_AFTER_INSTALL" = "0" ]; then
  log "Skipping start."
  [ "$USE_SYSTEMD" = "1" ] && log "Start it with: systemctl start ${SERVICE_NAME}"
  exit 0
fi

if [ "$USE_SYSTEMD" = "1" ]; then
  systemctl restart "$SERVICE_NAME"
  sleep 2
  if systemctl is-active --quiet "$SERVICE_NAME"; then
    log "Service ${SERVICE_NAME} is running (user ${SERVICE_USER}, starts on boot)."
    if [ -z "$SSL_CERT" ]; then
      curl -fsS "http://127.0.0.1:${PORT}/health" >/dev/null && log "Health check OK."
    fi
  else
    journalctl -u "$SERVICE_NAME" -n 20 --no-pager >&2 || true
    die "Service failed to start. Logs: journalctl -u ${SERVICE_NAME}"
  fi
  cat <<EOF2

  Status:  systemctl status ${SERVICE_NAME}
  Logs:    journalctl -u ${SERVICE_NAME} -f
  Remove:  ${INSTALL_DIR}/uninstall.sh [--remove-rules]
EOF2
elif [ "$USE_PM2" = "1" ]; then
  if ! command -v pm2 >/dev/null 2>&1; then
    log "Installing pm2 globally ..."
    npm install -g pm2 || die "Failed to install pm2 (try: sudo npm install -g pm2), or omit --pm2."
  fi
  if pm2 describe "$PM2_APP_NAME" >/dev/null 2>&1; then pm2 restart "$PM2_APP_NAME"; else pm2 start server.js --name "$PM2_APP_NAME"; fi
  pm2 save
  log "Running under pm2 as ${PM2_APP_NAME}. For reboots run 'pm2 startup' once. The account needs passwordless 'sudo ufw'."
else
  warn "Not root or no systemd: running in the foreground. The account needs passwordless 'sudo ufw'."
  log "Starting (Ctrl+C to stop) ..."
  exec node server.js
fi
