#!/usr/bin/env bash
# Rizoma WebPanel — installer / updater.
# Usage:
#   curl -fsSL https://repo.rizomarl.com/webpanel/install.sh | sudo bash
#   curl -fsSL https://repo.rizomarl.com/webpanel/install.sh | sudo bash -s -- --version 0.2.0
#   curl -fsSL https://repo.rizomarl.com/webpanel/install.sh | sudo bash -s -- --check
#   curl -fsSL https://repo.rizomarl.com/webpanel/install.sh | sudo bash -s -- --channel beta
set -euo pipefail

REPO="${RIZOMA_WEBPANEL_REPO:-https://repo.rizomarl.com/webpanel}"
CHANNEL="stable"
PIN_VERSION=""
CHECK_ONLY=0
SKIP_DOCKER=0
FORCE=0
VERBOSE=0
ADMIN_USER="${RIZOMA_WEBPANEL_ADMIN_USER:-admin}"
ADMIN_PASS="${RIZOMA_WEBPANEL_ADMIN_PASS:-}"
BIN_DIR="${WEBPANEL_BIN_DIR:-/opt/rizoma/webpanel/bin}"
ENV_FILE="${WEBPANEL_ENV_FILE:-/opt/rizoma/webpanel/data/webpanel.env}"
SYSTEMD_DIR="${WEBPANEL_SYSTEMD_DIR:-/etc/systemd/system}"
# Loopback mesh-agent status endpoint (contract site-gateway.mesh-peer-status.v1).
MESH_STATUS_URL="${RIZOMA_MESH_STATUS_URL:-http://127.0.0.1:8081/v1/local/mesh-peer/status}"

while [ $# -gt 0 ]; do
  case "$1" in
    --channel) CHANNEL="$2"; shift 2 ;;
    --version) PIN_VERSION="$2"; shift 2 ;;
    --check) CHECK_ONLY=1; shift ;;
    --skip-docker) SKIP_DOCKER=1; shift ;;
    --force) FORCE=1; shift ;;
    --verbose) VERBOSE=1; shift ;;
    --admin-user) ADMIN_USER="$2"; shift 2 ;;
    --admin-pass) ADMIN_PASS="$2"; shift 2 ;;
    *) echo "unknown arg: $1 (want --channel|--version|--check|--skip-docker|--force|--verbose|--admin-user|--admin-pass)" >&2; exit 1 ;;
  esac
done

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

arch="$(uname -m)"
case "$arch" in
  x86_64) arch=amd64 ;;
  aarch64|arm64) arch=arm64 ;;
  *) echo "unsupported arch: $arch" >&2; exit 1 ;;
esac

remote_version() { curl -fsSL "$REPO/version.json" | python3 -c 'import json,sys;print(json.load(sys.stdin)["version"])'; }
local_version() {
  [ -x "$BIN_DIR/webpanel" ] && "$BIN_DIR/webpanel" --version 2>/dev/null | awk '{print $2}' || echo "none"
}

RV="$(remote_version)"
LV="$(local_version)"
# Preflight details stay silent unless --verbose (no channel/arch/version
# or endpoint disclosure on shared terminals/logs).
[ "$VERBOSE" = 1 ] && echo "channel=$CHANNEL arch=$arch local=$LV remote=$RV"

if [ "$CHECK_ONLY" = 1 ]; then
  [ "$LV" = "$RV" ] && echo "up to date" || echo "update available: $LV -> $RV"
  exit 0
fi

# --- Mesh enrollment gate -------------------------------------------------
# WebPanel is mesh-bound: refuse to install on hosts without an enrolled
# Rizoma Mesh agent (no agent => no mesh IP => panel unreachable by design).
[ "$VERBOSE" = 1 ] && echo "==> mesh enrollment check ($MESH_STATUS_URL)"
mesh_json="$(curl -fsSL --max-time 5 "$MESH_STATUS_URL" 2>/dev/null || true)"
# Prints "ip|peer|enrolled|in_range" or nothing on any failure.
mesh_info="$(printf '%s' "$mesh_json" | python3 -c '
import json,sys
try:
    d = json.load(sys.stdin)
    ip = (d.get("mesh_ipv4") or "").strip()
    o = ip.split(".")
    in_range = len(o) == 4 and o[0] == "100" and o[1].isdigit() and 64 <= int(o[1]) <= 127 and all(p.isdigit() and 0 <= int(p) <= 255 for p in o[2:])
    print("%s|%s|%s|%s" % (ip, (d.get("peer_id") or "").strip(), "yes" if d.get("enrolled") else "no", "yes" if in_range else "no"))
except Exception:
    pass
' 2>/dev/null || true)"
mesh_ip="${mesh_info%%|*}"; rest="${mesh_info#*|}"
mesh_peer="${rest%%|*}"; rest="${rest#*|}"
mesh_enrolled="${rest%%|*}"; mesh_range="${rest##*|}"
if [ "$mesh_enrolled" != "yes" ] || [ "$mesh_range" != "yes" ] || [ -z "$mesh_ip" ]; then
  cat >&2 <<'EOF'
ERROR: Rizoma Mesh not detected on this host.

Rizoma WebPanel is mesh-bound and requires an enrolled Rizoma Mesh agent:
no agent means no mesh IP (100.64.0.0/10), and the panel would be
unreachable by design. Installation refused.

Enroll this host in RIZOMA MESH first, then re-run this installer.
See documentation at: https://rizomarl.com/docs
EOF
  exit 1
fi
[ "$VERBOSE" = 1 ] && echo "mesh detected: peer=${mesh_peer:-unknown} ip=$mesh_ip"

# --- Docker prerequisite ---------------------------------------------------
# All deploy paths (projects, stacks, databases) end in containers: without a
# Docker daemon the panel is a UI shell. Default-on; --skip-docker opts out
# for hosts where Docker is managed externally.
ensure_docker() {
  if docker info >/dev/null 2>&1; then
    echo "docker detected: $(docker version --format '{{.Server.Version}}' 2>/dev/null || echo unknown)"
    return 0
  fi
  if [ "$SKIP_DOCKER" = 1 ]; then
    cat >&2 <<'EOF'
WARNING: Docker daemon not reachable and --skip-docker was given.
Projects, stacks and databases will fail to deploy until Docker is
installed and /var/run/docker.sock is readable by the webpanel user.
EOF
    return 0
  fi
  if [ -f /etc/os-release ]; then . /etc/os-release; fi
  case "${ID:-}" in
    debian|ubuntu) ;;
    *)
      cat >&2 <<EOF
ERROR: Docker daemon not reachable and automatic install supports
Debian/Ubuntu only (detected: ${ID:-unknown}).
Install Docker manually, ensure 'docker info' works, then re-run
this installer (or pass --skip-docker to proceed without Docker).
EOF
      return 1
      ;;
  esac
  echo "==> installing docker-ce (required runtime)"
  apt-get update
  apt-get install -y ca-certificates curl gnupg
  install -m 0755 -d /etc/apt/keyrings
  curl -fsSL "https://download.docker.com/linux/${ID}/gpg" -o /etc/apt/keyrings/docker.asc
  chmod a+r /etc/apt/keyrings/docker.asc
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" \
    > /etc/apt/sources.list.d/docker.list
  apt-get update
  apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
  systemctl enable --now docker
  docker info >/dev/null 2>&1 || {
    echo "ERROR: Docker installed but daemon not reachable" >&2
    return 1
  }
  echo "docker installed: $(docker version --format '{{.Server.Version}}')"
}
ensure_docker

# The panel runs as the webpanel user and talks to the daemon socket.
if getent group docker >/dev/null 2>&1; then
  if ! id -nG webpanel 2>/dev/null | tr ' ' '\n' | grep -qx docker; then
    echo "==> adding webpanel user to docker group"
    usermod -aG docker webpanel
  fi
  if ! sudo -u webpanel docker info >/dev/null 2>&1; then
    echo "WARNING: webpanel user cannot reach the Docker daemon yet (socket: /var/run/docker.sock). Deployments will fail until fixed." >&2
  fi
elif ! docker info >/dev/null 2>&1; then
  echo "WARNING: Docker daemon not reachable at all. Deployments will fail until fixed." >&2
fi

TARGET="${PIN_VERSION:-$RV}"
[ -n "$TARGET" ] || { echo "cannot resolve target version" >&2; exit 1; }
# Version match alone is not enough: a previous run may have died mid-install
# (binary present, but no unit/service/credentials). Only short-circuit when
# the service is actually installed and active — otherwise complete the job.
if [ "$LV" = "$TARGET" ] && [ "$FORCE" != 1 ]; then
  if [ -f "$SYSTEMD_DIR/rizoma-webpanel.service" ] && systemctl is-active --quiet rizoma-webpanel 2>/dev/null; then
    echo "already at $TARGET (service active)"
    exit 0
  fi
  echo "version $TARGET present but installation incomplete — completing"
fi

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
cd "$tmp"
echo "==> download webpanel-$TARGET-linux-$arch"
curl -fsSL -O "$REPO/$CHANNEL/webpanel-$TARGET-linux-$arch"
curl -fsSL -O "$REPO/$CHANNEL/SHA256SUMS" || curl -fsSL "$REPO/$CHANNEL/SHA256SUMS" -o SHA256SUMS
echo "==> verify checksum"
grep "webpanel-$TARGET-linux-$arch" SHA256SUMS | sha256sum -c -
if curl -fsSL -O "$REPO/$CHANNEL/SHA256SUMS.sig" 2>/dev/null; then
  echo "==> verify gpg signature"
  curl -fsSL "$REPO/keys/rizoma-archive-keyring.gpg" -o /tmp/rizoma-repo.gpg 2>/dev/null || true
  if [ -f /tmp/rizoma-repo.gpg ]; then
    gpg --no-default-keyring --keyring /tmp/rizoma-repo.gpg --verify SHA256SUMS.sig SHA256SUMS
  else
    echo "(no repo key published; checksum verified, signature present but unchecked)"
  fi
fi

id webpanel >/dev/null 2>&1 || useradd -r -s /usr/sbin/nologin webpanel
mkdir -p "$BIN_DIR"

if [ -x "$BIN_DIR/webpanel" ]; then
  bak="$BIN_DIR/webpanel.bak-$(date +%Y%m%d-%H%M%S)"
  echo "==> backup current binary -> $bak"
  cp -p "$BIN_DIR/webpanel" "$bak"
fi
echo "==> install $TARGET"
install -o webpanel -g webpanel -m 0755 "webpanel-$TARGET-linux-$arch" "$BIN_DIR/webpanel"

# --- Mesh auto-bind --------------------------------------------------------
# Point the panel at the detected mesh identity. Never clobbers an existing
# env file: only fills in keys the operator has not set (except BIND_IFACE
# and MESH_PEER_ID, which always follow the live enrollment).
set_env() { # key value file
  local key="$1" val="$2" file="$3"
  if grep -q "^${key}=" "$file" 2>/dev/null; then
    sed -i "s|^${key}=.*|${key}=${val}|" "$file"
  else
    printf '%s=%s\n' "$key" "$val" >> "$file"
  fi
}
ensure_env() { # key value file — only set when absent
  grep -q "^${1}=" "$3" 2>/dev/null || printf '%s=%s\n' "$1" "$2" >> "$3"
}
mkdir -p "$(dirname "$ENV_FILE")"
touch "$ENV_FILE"
chmod 0600 "$ENV_FILE"
echo "==> mesh auto-bind (peer=$mesh_peer ip=$mesh_ip)"
set_env RIZOMA_WEBPANEL_BIND_IFACE "$mesh_ip" "$ENV_FILE"
set_env RIZOMA_WEBPANEL_MESH_PEER_ID "$mesh_peer" "$ENV_FILE"
ensure_env RIZOMA_WEBPANEL_MESH_TLS_HOSTS "$mesh_ip" "$ENV_FILE"
ensure_env RIZOMA_WEBPANEL_DNS_MESH_SUFFIXES "rizoma.mesh" "$ENV_FILE"
# Production posture (matches live hosts): panel serves the management
# gateway TLS UI on 443. Existing hosts keep their configured values.
ensure_env RIZOMA_WEBPANEL_PORT "443" "$ENV_FILE"
ensure_env RIZOMA_WEBPANEL_MANAGEMENT_TLS_ENABLED "true" "$ENV_FILE"
ensure_env RIZOMA_WEBPANEL_MANAGEMENT_TLS_HOSTS "$mesh_ip" "$ENV_FILE"
ensure_env RIZOMA_WEBPANEL_MESH_TLS_AUTO "true" "$ENV_FILE"
chown webpanel:webpanel "$ENV_FILE" 2>/dev/null || true

# --- Admin credentials (mesh-installer parity) -------------------------------
# Fresh hosts (no SQLite DB yet) get an admin account seeded from env before
# first start; the credentials are printed once at the end like the mesh
# installer's admin PIN. Existing hosts keep their current admin untouched.
DB_FILE="${WEBPANEL_DB_FILE:-$(grep -E '^RIZOMA_WEBPANEL_DB=' "$ENV_FILE" 2>/dev/null | cut -d= -f2- || true)}"
[ -n "$DB_FILE" ] || DB_FILE="/opt/rizoma/webpanel/data/webpanel.db"
FRESH_INSTALL=0
PRINT_CREDS=0
if [ ! -f "$DB_FILE" ]; then
  FRESH_INSTALL=1
  if ! grep -q '^RIZOMA_WEBPANEL_ADMIN_PASS=' "$ENV_FILE" 2>/dev/null; then
    if [ -z "$ADMIN_PASS" ]; then
      if [ -t 0 ]; then
        printf 'Admin username [%s]: ' "$ADMIN_USER" >&2
        read -r input_user || true
        [ -n "$input_user" ] && ADMIN_USER="$input_user"
        printf 'Admin password (min 12 chars, upper/lower/digit/symbol): ' >&2
        stty -echo 2>/dev/null || true
        read -r ADMIN_PASS || true
        stty echo 2>/dev/null || true
        printf '\n' >&2
      elif command -v openssl >/dev/null 2>&1; then
        # No TTY (typical curl|bash pipe): generate a compliant password
        # instead of refusing — 16 random alphanumerics + Aa1! suffix.
        ADMIN_PASS="$(openssl rand -base64 18 | tr -dc 'A-Za-z0-9' | head -c 16)Aa1!"
        echo "generated admin password (shown once at the end)" >&2
      else
        cat >&2 <<'EOF'
ERROR: fresh install with no admin password and no TTY to prompt.
Re-run with --admin-pass '<strong-password>' (min 12 chars,
upper/lower/digit/symbol) or set RIZOMA_WEBPANEL_ADMIN_PASS.
EOF
        exit 1
      fi
    fi
    # Mirror panel ValidatePassword: 12+ chars with upper/lower/digit/symbol.
    if [ "${#ADMIN_PASS}" -lt 12 ] || ! printf '%s' "$ADMIN_PASS" | grep -q '[A-Z]' \
      || ! printf '%s' "$ADMIN_PASS" | grep -q '[a-z]' \
      || ! printf '%s' "$ADMIN_PASS" | grep -q '[0-9]' \
      || ! printf '%s' "$ADMIN_PASS" | grep -q '[^A-Za-z0-9]'; then
      echo "ERROR: admin password must be min 12 chars with upper/lower/digit/symbol (first boot would refuse a weak password)" >&2
      exit 1
    fi
    set_env RIZOMA_WEBPANEL_ADMIN_USER "$ADMIN_USER" "$ENV_FILE"
    set_env RIZOMA_WEBPANEL_ADMIN_PASS "$ADMIN_PASS" "$ENV_FILE"
    PRINT_CREDS=1
  fi
fi

if [ ! -f "$SYSTEMD_DIR/rizoma-webpanel.service" ]; then
  echo "==> installing systemd unit"
  curl -fsSL "$REPO/rizoma-webpanel.service" -o "$SYSTEMD_DIR/rizoma-webpanel.service" 2>/dev/null || {
    echo "unit not on repo; copy packaging/systemd/rizoma-webpanel.service manually" >&2
  }
  systemctl daemon-reload
  systemctl enable rizoma-webpanel >/dev/null 2>&1 || true
fi
# The unit template ships EnvironmentFile=-/etc/rizoma/webpanel.env; point it
# at the env file this installer manages (default
# /opt/rizoma/webpanel/data/webpanel.env). Also pin RIZOMA_WEBPANEL_CONFIG_FILE
# so the panel reads the managed file instead of its /etc/rizoma default
# (which may not exist or be unreadable -> fatal "permission denied").
if [ -f "$SYSTEMD_DIR/rizoma-webpanel.service" ]; then
  unit_changed=0
  if ! grep -q "^EnvironmentFile=-${ENV_FILE}$" "$SYSTEMD_DIR/rizoma-webpanel.service" 2>/dev/null; then
    sed -i "s|^EnvironmentFile=.*|EnvironmentFile=-${ENV_FILE}|" "$SYSTEMD_DIR/rizoma-webpanel.service"
    unit_changed=1
  fi
  if grep -q "^Environment=RIZOMA_WEBPANEL_CONFIG_FILE=" "$SYSTEMD_DIR/rizoma-webpanel.service" 2>/dev/null; then
    sed -i "s|^Environment=RIZOMA_WEBPANEL_CONFIG_FILE=.*|Environment=RIZOMA_WEBPANEL_CONFIG_FILE=${ENV_FILE}|" "$SYSTEMD_DIR/rizoma-webpanel.service"
    unit_changed=1
  else
    sed -i "/^\[Service\]/a Environment=RIZOMA_WEBPANEL_CONFIG_FILE=${ENV_FILE}" "$SYSTEMD_DIR/rizoma-webpanel.service"
    unit_changed=1
  fi
  [ "$unit_changed" = 1 ] && systemctl daemon-reload
fi

# Systemd hardening (ReadWritePaths etc.) requires these paths to exist
# before the unit starts, otherwise mount namespacing fails (226/NAMESPACE).
# The panel also creates its data dirs at validate time running as the
# webpanel user, so pre-create every default (or env-overridden) path owned
# by webpanel — otherwise first boot dies with "permission denied".
env_or() { # key default -> value (env file wins, then default)
  local v
  v="$(grep -E "^${1}=" "$ENV_FILE" 2>/dev/null | cut -d= -f2- || true)"
  [ -n "$v" ] && printf '%s' "$v" || printf '%s' "$2"
}
mkowned() { # dir
  mkdir -p "$1"
  chown webpanel:webpanel "$1" 2>/dev/null || true
  chmod 0750 "$1" 2>/dev/null || true
}
mkowned "$(dirname "$(env_or RIZOMA_WEBPANEL_DB /opt/rizoma/webpanel/data/webpanel.db)")"
mkowned "$(env_or RIZOMA_WEBPANEL_LOG_DIR /var/log/rizoma)"
mkowned "$(env_or RIZOMA_WEBPANEL_APP_DIR /opt/apps)"
mkowned "$(env_or RIZOMA_WEBPANEL_COMPOSE_DIR /opt/compose)"
mkowned "$(env_or RIZOMA_WEBPANEL_DOCKER_DIR /opt/docker)"
mkowned "$(env_or RIZOMA_WEBPANEL_DATA_DIR /opt/rizoma/webpanel/data)"
mkowned "$(env_or RIZOMA_WEBPANEL_CERT_DIR /opt/rizoma/webpanel/data/certs)"
mkowned "$(env_or RIZOMA_WEBPANEL_PROJECT_ROOT /opt/rizoma/projects)"
mkowned "$(env_or RIZOMA_WEBPANEL_MAIL_DIR /var/mail/rizoma)"
mkowned "$(env_or RIZOMA_WEBPANEL_FTP_HOME /home/ftp)"
for d in /opt/rizoma/webpanel /opt/rizoma/webpanel/data /var/log/rizoma /var/mail/rizoma /home/ftp; do
  mkdir -p "$d"
done

echo "==> restart"
systemctl restart rizoma-webpanel
# First boot (DB init, certs) can take a while: wait for active state.
echo "==> waiting for service"
for i in $(seq 1 30); do
  if systemctl is-active --quiet rizoma-webpanel 2>/dev/null; then
    echo "service active"
    break
  fi
  if [ "$i" = 30 ]; then
    echo "service did not become active; see journalctl -u rizoma-webpanel" >&2
    systemctl is-active rizoma-webpanel || true
    exit 1
  fi
  sleep 2
done

echo "==> health"
# Panel binds the mesh interface, not necessarily loopback: probe loopback
# first, then every local address (covers mesh-bound panels).
ok=0
# Scheme/port vary by TLS setup (plain 8080, mesh-TLS 8080, or 443):
# probe every combination on loopback and all local IPs (mesh-bound).
# First boot (migrations, cert generation, bcrypt admin seed) can take a
# while, so retry the whole sweep instead of probing once.
candidates="http://127.0.0.1:8080/api/v1/health https://127.0.0.1:8080/api/v1/health https://127.0.0.1/api/v1/health"
if command -v hostname >/dev/null 2>&1; then
  for ip in $(hostname -I 2>/dev/null); do
    candidates="$candidates http://$ip:8080/api/v1/health https://$ip:8080/api/v1/health https://$ip/api/v1/health"
  done
fi
for attempt in $(seq 1 12); do
  for url in $candidates; do
    if curl -fsk -o /dev/null --max-time 5 "$url"; then echo "healthy via $url"; ok=1; break 2; fi
  done
  [ "$attempt" = 12 ] || sleep 5
done
if [ "$PRINT_CREDS" = 1 ]; then
  cat <<EOF

=============================================
  Rizoma WebPanel installed
  URL:      https://$mesh_ip/
  Username: $ADMIN_USER
  Password: $ADMIN_PASS
  Save these credentials — the password is not shown again.
=============================================
EOF
fi
[ "$ok" = 1 ] || { echo "health check failed; see journalctl -u rizoma-webpanel" >&2; exit 1; }
"$BIN_DIR/webpanel" --version
