#!/usr/bin/env bash
#
# Kubaros installer
#   curl -fsSL https://install.kubaros.io | sudo bash
#
# Installs the Kubaros appliance (Docker Compose stack of pre-built container
# images) inside a dedicated management VM. It NEVER touches the Proxmox host and
# does not modify unrelated system configuration. Idempotent: re-running updates
# the stack in place.
#
# The config files (docker-compose.yml, Caddyfile, kubaros-update.sh) are served
# statically at https://install.kubaros.io. The application itself ships as
# pre-built container images on the GitLab container registry
# (git.host-on.dev/philip/kubaros).
#
set -euo pipefail

KUBAROS_DIR="${KUBAROS_DIR:-/opt/kubaros}"
KUBAROS_VERSION="${KUBAROS_VERSION:-latest}"

# Where the static config files live. Override for testing if needed.
INSTALL_BASE="${KUBAROS_INSTALL_BASE:-https://install.kubaros.io}"
# Container registry project that hosts the images (the GitLab container
# registry lives on its own subdomain). Override with KUBAROS_IMAGE_PREFIX.
IMAGE_PREFIX="${KUBAROS_IMAGE_PREFIX:-registry.host-on.dev/philip/kubaros}"
# GitLab main domain (issues the registry JWTs + drives the self-updater).
GITLAB_URL="${KUBAROS_GITLAB_URL:-https://git.host-on.dev}"
# GitLab repo (namespace/project) whose Releases drive the in-app self-updater.
RELEASES_REPO="${KUBAROS_RELEASES_REPO:-philip/kubaros}"
# Optional GitLab token (scope read_api) — only needed while the release repo is
# private. Registry pulls use KUBAROS_REGISTRY_* below.
RELEASES_TOKEN="${KUBAROS_RELEASES_TOKEN:-${KUBAROS_GITHUB_TOKEN:-}}"
# Release channel: stable (default, all users) | beta (opt-in pre-releases) |
# edge (your internal test VM only — newest commit on every push).
UPDATE_CHANNEL="${KUBAROS_UPDATE_CHANNEL:-stable}"
# Registry login — required ONLY to pull PRIVATE images (e.g. an internal
# test/edge build) and to list available tags. Pair KUBAROS_REGISTRY_USER with
# KUBAROS_REGISTRY_TOKEN (a GitLab PAT with read_registry). Leave empty for
# public images. (The legacy KUBAROS_GITHUB_* names keep working.)
REGISTRY_USER="${KUBAROS_REGISTRY_USER:-${KUBAROS_GITHUB_USER:-}}"
REGISTRY_TOKEN="${KUBAROS_REGISTRY_TOKEN:-${KUBAROS_GITHUB_TOKEN:-}}"
# Provisioning mode: keep "true" (dry-run / simulated) until real Proxmox/Talos
# is wired & verified on this appliance. Set KUBAROS_DRY_RUN=false for live infra.
DRY_RUN="${KUBAROS_DRY_RUN:-true}"
# Public domain for a real, browser-trusted Let's Encrypt certificate (e.g.
# kubaros.provider.example). When set, the appliance binds the STANDARD ports
# 80+443 and Caddy obtains an ACME cert automatically. Requires: a DNS A/AAAA
# record pointing at this host, and ports 80+443 reachable from the internet.
# Leave empty to access by IP with a self-signed cert on ports 8080/8443.
DOMAIN="${KUBAROS_DOMAIN:-}"
# Email used for the Let's Encrypt account (only when KUBAROS_DOMAIN is set).
ACME_EMAIL="${KUBAROS_ACME_EMAIL:-}"
# The Kubaros WEBSITE (license server) this appliance activates its license
# against. The provider enters their KBRS- key in the console; the appliance
# validates it at <server>/api/portal/license/validate. Defaults to kubaros.io.
LICENSE_SERVER="${KUBAROS_LICENSE_SERVER:-https://kubaros.io}"
# Phone-home telemetry to the license server (license status, update checks,
# install statistics). Only infrastructure metadata, no personal data. DSGVO
# opt-out: set KUBAROS_TELEMETRY=off.
TELEMETRY="${KUBAROS_TELEMETRY:-on}"
# UDP port the appliance WireGuard HUB listens on. Each Proxmox site's edge VM
# DIALS the appliance here (hub-and-spoke), so this UDP port must be reachable
# from the internet on the appliance host (open it in any upstream firewall).
WG_PORT="${KUBAROS_WG_PORT:-51820}"
# Seconds the edge VM (spoke) keeps the tunnel alive toward the appliance. Low so
# the ISP's stateful-UDP window stays open for appliance->edge (inbound UDP to a
# routed public IP is often only allowed briefly after the edge sends out).
WG_KEEPALIVE="${KUBAROS_WG_KEEPALIVE:-5}"

c_blue="\033[1;34m"; c_green="\033[1;32m"; c_yellow="\033[1;33m"; c_red="\033[1;31m"; c_reset="\033[0m"
info()  { echo -e "${c_blue}==>${c_reset} $*"; }
ok()    { echo -e "${c_green}✓${c_reset} $*"; }
warn()  { echo -e "${c_yellow}!${c_reset} $*"; }
err()   { echo -e "${c_red}✗${c_reset} $*" >&2; }

require_root() {
  if [[ "${EUID}" -ne 0 ]]; then
    err "Please run as root (sudo)."; exit 1
  fi
}

check_os() {
  if [[ ! -f /etc/os-release ]]; then err "Unsupported OS (no /etc/os-release)."; exit 1; fi
  . /etc/os-release
  case "${ID}" in
    debian|ubuntu) ok "Detected ${PRETTY_NAME}" ;;
    *) warn "Untested OS '${ID}'. Debian/Ubuntu is recommended. Continuing." ;;
  esac
}

ensure_prereqs() {
  # Base tools the installer itself needs (the host python3 is NOT required —
  # keys are generated with openssl). Auto-installs the few missing packages.
  local missing=()
  for c in curl openssl; do command -v "$c" >/dev/null 2>&1 || missing+=("$c"); done
  if [[ ${#missing[@]} -eq 0 ]]; then return; fi
  info "Installing prerequisites: ${missing[*]}..."
  if command -v apt-get >/dev/null 2>&1; then
    apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl openssl ca-certificates
  elif command -v dnf >/dev/null 2>&1; then
    dnf install -y curl openssl ca-certificates
  elif command -v yum >/dev/null 2>&1; then
    yum install -y curl openssl ca-certificates
  else
    err "Could not auto-install ${missing[*]} (no apt/dnf/yum). Install them and re-run."; exit 1
  fi
  ok "Prerequisites installed."
}

ensure_docker() {
  if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
    ok "Docker & Compose present."
    return
  fi
  # Auto-install: a piped 'curl | bash' install can't read interactive input,
  # so we install Docker automatically (override with KUBAROS_SKIP_DOCKER=1).
  if [[ "${KUBAROS_SKIP_DOCKER:-0}" == "1" ]]; then
    err "Docker / Compose not found and KUBAROS_SKIP_DOCKER=1. Install Docker and re-run."; exit 1
  fi
  info "Docker / Compose not found — installing automatically via get.docker.com..."
  curl -fsSL https://get.docker.com | sh || warn "get.docker.com script failed; trying distribution packages..."

  # Fallback for distros not yet covered by the convenience script (e.g. a very
  # fresh Debian 13 / trixie): install Docker + the Compose v2 plugin from apt.
  if ! (command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1); then
    if command -v apt-get >/dev/null 2>&1; then
      info "Installing Docker from distribution packages (apt)..."
      apt-get update -qq || true
      DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker.io docker-compose-v2 \
        || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker.io docker-compose-plugin \
        || true
    elif command -v dnf >/dev/null 2>&1; then
      dnf install -y docker docker-compose-plugin || true
    fi
  fi

  systemctl enable --now docker >/dev/null 2>&1 || true
  if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
    ok "Docker installed."
  else
    err "Docker installation failed. Install Docker Engine + the Compose plugin manually and re-run."; exit 1
  fi
}

gen_secret() { openssl rand -hex 32; }
gen_fernet() {
  # A Fernet key is url-safe base64 of 32 random bytes. Generate it with openssl
  # so it is always valid even when the host python3 lacks the 'cryptography'
  # module (otherwise the backend crashes: "Fernet key must be 32 url-safe
  # base64-encoded bytes").
  openssl rand -base64 32 | tr '+/' '-_'
}

setup_env() {
  mkdir -p "${KUBAROS_DIR}/state"
  cd "${KUBAROS_DIR}"
  local host_ip; host_ip="$(hostname -I 2>/dev/null | awk '{print $1}')"

  # Compute network/TLS settings based on whether a public domain is provided.
  local site_addr base_url http_port https_port
  if [[ -n "${DOMAIN}" ]]; then
    site_addr="${DOMAIN}"; base_url="https://${DOMAIN}"
    http_port="80"; https_port="443"
  else
    site_addr=":443"; [[ -n "${host_ip}" ]] && site_addr="https://${host_ip}"
    base_url="https://${host_ip}:8443"
    http_port="8080"; https_port="8443"
  fi

  if [[ -f .env ]]; then
    ok "Existing configuration kept (${KUBAROS_DIR}/.env)."
    # Add newer keys on upgrade (never touches existing secrets/passwords).
    grep -q '^KUBAROS_INSTALL_BASE=' .env || echo "KUBAROS_INSTALL_BASE=\"${INSTALL_BASE}\"" >> .env
    grep -q '^KUBAROS_SITE_ADDRESS=' .env || echo "KUBAROS_SITE_ADDRESS=\"${site_addr}\"" >> .env
    grep -q '^KUBAROS_ACME_EMAIL='  .env || echo "KUBAROS_ACME_EMAIL=\"${ACME_EMAIL}\"" >> .env
    grep -q '^KUBAROS_DOMAIN='      .env || echo "KUBAROS_DOMAIN=\"${DOMAIN}\"" >> .env
    grep -q '^KUBAROS_LICENSE_SERVER=' .env || echo "KUBAROS_LICENSE_SERVER=\"${LICENSE_SERVER}\"" >> .env
    grep -q '^KUBAROS_TELEMETRY='      .env || echo "KUBAROS_TELEMETRY=\"${TELEMETRY}\"" >> .env
    grep -q '^KUBAROS_WG_PORT='        .env || echo "KUBAROS_WG_PORT=\"${WG_PORT}\"" >> .env
    grep -q '^KUBAROS_WG_KEEPALIVE='   .env || echo "KUBAROS_WG_KEEPALIVE=\"${WG_KEEPALIVE}\"" >> .env
    grep -q '^KUBAROS_REDIS_URL='       .env || echo "KUBAROS_REDIS_URL=\"${KUBAROS_REDIS_URL:-}\"" >> .env
    # If a domain is explicitly provided on re-run, switch the relevant settings
    # (lets the operator move from IP access to a trusted domain by re-running).
    if [[ -n "${DOMAIN}" ]]; then
      set_env_kv KUBAROS_DOMAIN "${DOMAIN}"
      set_env_kv KUBAROS_ACME_EMAIL "${ACME_EMAIL}"
      set_env_kv KUBAROS_SITE_ADDRESS "${site_addr}"
      set_env_kv KUBAROS_BASE_URL "${base_url}"
      set_env_kv KUBAROS_HTTP_PORT "${http_port}"
      set_env_kv KUBAROS_HTTPS_PORT "${https_port}"
      info "Configured domain ${DOMAIN} (Let's Encrypt on ports 80/443)."
    fi
    return
  fi
  info "Generating secure configuration..."
  cat > .env <<EOF
KUBAROS_BASE_URL="${base_url}"
KUBAROS_DOMAIN="${DOMAIN}"
KUBAROS_ACME_EMAIL="${ACME_EMAIL}"
KUBAROS_SITE_ADDRESS="${site_addr}"
KUBAROS_INSTALL_BASE="${INSTALL_BASE}"
KUBAROS_LICENSE_SERVER="${LICENSE_SERVER}"
KUBAROS_TELEMETRY="${TELEMETRY}"
KUBAROS_WG_PORT="${WG_PORT}"
KUBAROS_WG_KEEPALIVE="${WG_KEEPALIVE}"
KUBAROS_VERSION="${KUBAROS_VERSION}"
KUBAROS_UPDATE_CHANNEL="${UPDATE_CHANNEL}"
KUBAROS_HTTP_PORT="${http_port}"
KUBAROS_HTTPS_PORT="${https_port}"
KUBAROS_IMAGE_PREFIX="${IMAGE_PREFIX}"
KUBAROS_GITLAB_URL="${GITLAB_URL}"
KUBAROS_RELEASES_REPO="${RELEASES_REPO}"
KUBAROS_RELEASES_TOKEN="${RELEASES_TOKEN}"
KUBAROS_REGISTRY_USER="${REGISTRY_USER}"
KUBAROS_REGISTRY_TOKEN="${REGISTRY_TOKEN}"
DB_NAME="kubaros"
JWT_SECRET="$(gen_secret)"
KUBAROS_ENCRYPTION_KEY="$(gen_fernet)"
LICENSE_SIGNING_KEY="$(gen_secret)"
KUBAROS_DRY_RUN="${DRY_RUN}"
# Distributed provisioning worker (opt-in): set to redis://redis:6379/0 and
# enable the `worker` service in docker-compose.yml. Empty = in-process jobs.
KUBAROS_REDIS_URL="${KUBAROS_REDIS_URL:-}"
EOF
  chmod 600 .env
  ok "Configuration written to ${KUBAROS_DIR}/.env"
}

# Set or replace KEY="VALUE" in the .env (in $PWD).
set_env_kv() {
  local key="$1" val="$2"
  if grep -q "^${key}=" .env 2>/dev/null; then
    # Use a safe delimiter; values here contain no '|'.
    sed -i "s|^${key}=.*|${key}=\"${val}\"|" .env
  else
    echo "${key}=\"${val}\"" >> .env
  fi
}

# Generate /opt/kubaros/Caddyfile from .env. Domain => Let's Encrypt; otherwise
# Caddy's internal self-signed CA. Generated locally so the TLS mode is always
# syntactically correct (this is NOT fetched from the install source).
write_caddyfile() {
  cd "${KUBAROS_DIR}"
  local domain email site_addr
  domain="$(grep '^KUBAROS_DOMAIN='       .env 2>/dev/null | cut -d'"' -f2)"
  email="$(grep '^KUBAROS_ACME_EMAIL='    .env 2>/dev/null | cut -d'"' -f2)"
  site_addr="$(grep '^KUBAROS_SITE_ADDRESS=' .env 2>/dev/null | cut -d'"' -f2)"
  [[ -z "${site_addr}" ]] && site_addr=":443"

  if [[ -n "${domain}" ]]; then
    info "Writing Caddyfile for ${domain} (Let's Encrypt)..."
    { printf '{\n'
      [[ -n "${email}" ]] && printf '\temail %s\n' "${email}"
      printf '}\n\n%s {\n\tencode gzip\n\n\thandle /api/* {\n\t\treverse_proxy backend:8001\n\t}\n\n\thandle {\n\t\treverse_proxy frontend:3000\n\t}\n}\n' "${domain}"
    } > Caddyfile
  else
    info "Writing Caddyfile for ${site_addr} (self-signed internal CA)..."
    printf '{\n\tlocal_certs\n\tauto_https disable_redirects\n}\n\n%s {\n\tencode gzip\n\n\thandle /api/* {\n\t\treverse_proxy backend:8001\n\t}\n\n\thandle {\n\t\treverse_proxy frontend:3000\n\t}\n}\n' "${site_addr}" > Caddyfile
  fi
  ok "Caddyfile written."
}

install_cli() {
  # Install a global 'kubaros' helper so the operator can run
  # `kubaros update|restart|logs|status|...` from anywhere.
  info "Installing the 'kubaros' command..."
  cat > /usr/local/bin/kubaros <<'CLI'
#!/usr/bin/env bash
# Kubaros appliance control CLI.
set -euo pipefail
KUBAROS_DIR="${KUBAROS_DIR:-/opt/kubaros}"
cd "${KUBAROS_DIR}" 2>/dev/null || { echo "Kubaros is not installed at ${KUBAROS_DIR}." >&2; exit 1; }

base="$(grep '^KUBAROS_INSTALL_BASE=' .env 2>/dev/null | cut -d'"' -f2)"
base="${base:-https://install.kubaros.io}"

# Echo the latest STABLE semver tag published for the backend image in the
# container registry, or empty if it can't be determined. This is the
# authoritative "what can I actually pull" source — it never reports a tag that
# doesn't exist. Supports ghcr.io (legacy prefix) and Docker registry v2
# endpoints with a /jwt/auth service (e.g. GitLab).
_registry_latest() {
  local prefix repo host auth_host tok auth=""
  prefix="$(grep '^KUBAROS_IMAGE_PREFIX=' .env 2>/dev/null | cut -d'"' -f2)"
  prefix="${prefix:-registry.host-on.dev/philip/kubaros}"
  prefix="${prefix#https://}"; prefix="${prefix%/}"
  host="${prefix%%/*}"; repo="${prefix#*/}"
  # JWT-Auth: GitLab issues registry tokens on its main domain; generic v2
  # registries on their own host.
  auth_host="$(grep '^KUBAROS_GITLAB_URL=' .env 2>/dev/null | cut -d'"' -f2)"
  auth_host="${auth_host:-https://git.host-on.dev}"; auth_host="${auth_host#https://}"; auth_host="${auth_host%/}"
  case "${auth_host}" in "${host}") auth_host="${host}";; esac
  if [[ "${host}" == "ghcr.io" ]]; then
    repo="phigi87/${repo#phigi87/}"
    repo="${repo%/kubaros-backend}/kubaros-backend"
    auth_host="ghcr.io"
    local ruser rtoken
    ruser="$(grep '^KUBAROS_GITHUB_USER=' .env 2>/dev/null | cut -d'"' -f2)"
    rtoken="$(grep '^KUBAROS_GITHUB_TOKEN=' .env 2>/dev/null | cut -d'"' -f2)"
    [[ -n "${ruser}" && -n "${rtoken}" ]] && auth="-u ${ruser}:${rtoken}"
    tok="$(curl -fsSL ${auth} "https://ghcr.io/token?scope=repository:${repo}:pull" 2>/dev/null \
          | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
  else
    repo="${repo}/kubaros-backend"
    local ruser rtoken
    ruser="$(grep '^KUBAROS_REGISTRY_USER=' .env 2>/dev/null | cut -d'"' -f2)"
    rtoken="$(grep '^KUBAROS_REGISTRY_TOKEN=' .env 2>/dev/null | cut -d'"' -f2)"
    [[ -z "${rtoken}" ]] && rtoken="$(grep '^KUBAROS_GITHUB_TOKEN=' .env 2>/dev/null | cut -d'"' -f2)"
    [[ -n "${ruser}" && -n "${rtoken}" ]] && auth="-u ${ruser}:${rtoken}"
    tok="$(curl -fsSL ${auth} "https://${auth_host}/jwt/auth?service=container_registry&scope=repository:${repo}:pull" 2>/dev/null \
          | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
    # Fallback: try the registry host itself as the auth endpoint.
    if [[ -z "${tok}" && "${auth_host}" != "${host}" ]]; then
      tok="$(curl -fsSL ${auth} "https://${host}/jwt/auth?service=container_registry&scope=repository:${repo}:pull" 2>/dev/null \
            | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
    fi
  fi
  [[ -z "${tok}" ]] && return 0
  curl -fsSL -H "Authorization: Bearer ${tok}" "https://${host}/v2/${repo}/tags/list" 2>/dev/null \
    | tr ',][' '\n\n\n' \
    | sed -n 's/.*"\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)".*/\1/p' \
    | sort -V | tail -1
}

cmd="${1:-help}"; shift || true

# Ensure host WireGuard prerequisites before any `up` (kernel module + /dev/net/tun
# for the userspace fallback) so the backend container's tunnel works and the
# /dev/net/tun device mapping never blocks startup.
_ensure_wg() {
  modprobe wireguard 2>/dev/null || true
  modprobe tun 2>/dev/null || true
  if [ ! -e /dev/net/tun ]; then
    mkdir -p /dev/net 2>/dev/null || true
    mknod /dev/net/tun c 10 200 2>/dev/null || true
    chmod 600 /dev/net/tun 2>/dev/null || true
  fi
}

case "${cmd}" in
  check)
    cur="$(grep '^KUBAROS_VERSION=' .env 2>/dev/null | cut -d'"' -f2)"; cur="${cur:-latest}"
    latest="$(_registry_latest)"
    echo "Installed version : ${cur}"
    echo "Latest available  : ${latest:-unknown (could not reach the container registry)}"
    if [[ -n "${latest}" && "${cur}" != "${latest}" ]]; then
      echo; echo "→ An update is available. Apply it with:  kubaros update"
    elif [[ -n "${latest}" ]]; then
      echo; echo "✓ You are on the latest version."
    fi
    ;;
  update|upgrade)
    # `kubaros update`            -> update to the latest published version
    # `kubaros update --check`    -> only report (no changes); alias of `kubaros check`
    # `kubaros update X.Y.Z`      -> pin a specific version
    # `kubaros update latest`     -> follow the rolling 'latest' tag
    check_only=false; target=""
    for a in "$@"; do
      case "${a}" in
        --check|-c) check_only=true ;;
        --*) ;;  # ignore unknown flags
        *) target="${a}" ;;
      esac
    done
    cur="$(grep '^KUBAROS_VERSION=' .env 2>/dev/null | cut -d'"' -f2)"; cur="${cur:-latest}"
    latest="$(_registry_latest)"
    if ${check_only}; then
      echo "Installed version : ${cur}"
      echo "Latest available  : ${latest:-unknown (could not reach the container registry)}"
      [[ -n "${latest}" && "${cur}" != "${latest}" ]] && { echo; echo "→ Run 'kubaros update' to install ${latest}."; }
      exit 0
    fi
    # Refresh stack files (compose + host updater) ONLY when the installer-format
    # version changed (deploy/VERSION), verified against sha256sums.txt. The
    # Caddyfile is generated by the installer (TLS depends on your domain/IP) and
    # is intentionally left as-is — re-run the installer to change domain/TLS.
    remote="$(curl -fsSL "${base}/VERSION" 2>/dev/null | tr -d '[:space:]' || true)"
    cur="$(grep '^KUBAROS_INSTALLER_VERSION=' .env 2>/dev/null | cut -d'"' -f2 || true)"
    if [[ -z "${remote}" || "${remote}" != "${cur}" ]]; then
      curl -fsSL "${base}/sha256sums.txt" -o sha256sums.txt 2>/dev/null || rm -f sha256sums.txt
      refresh_ok=1
      for f in docker-compose.yml kubaros-update.sh; do
        if curl -fsSL "${base}/${f}" -o "${f}.new" 2>/dev/null; then
          want="$(grep -E " ${f}$" sha256sums.txt 2>/dev/null | awk '{print $1}' | head -1 || true)"
          got="$(sha256sum "${f}.new" 2>/dev/null | awk '{print $1}' || true)"
          if [[ -n "${want}" && "${got}" != "${want}" ]]; then
            echo "warn: checksum mismatch for ${f}; keeping the current file" >&2
            rm -f "${f}.new"; refresh_ok=0; continue
          fi
          mv "${f}.new" "${f}"
        else
          rm -f "${f}.new"; echo "warn: could not refresh ${f} from ${base} (keeping current)" >&2
          refresh_ok=0
        fi
      done
      chmod +x kubaros-update.sh 2>/dev/null || true
      if [[ "${refresh_ok}" == "1" && -n "${remote}" ]]; then
        if grep -q '^KUBAROS_INSTALLER_VERSION=' .env; then
          sed -i "s|^KUBAROS_INSTALLER_VERSION=.*|KUBAROS_INSTALLER_VERSION=\"${remote}\"|" .env
        else
          echo "KUBAROS_INSTALLER_VERSION=\"${remote}\"" >> .env
        fi
      fi
    else
      echo "Installer files up to date (version ${remote})."
    fi
    newver="${target:-${latest:-latest}}"
    prev="${cur}"
    if grep -q '^KUBAROS_VERSION=' .env; then sed -i "s|^KUBAROS_VERSION=.*|KUBAROS_VERSION=\"${newver}\"|" .env;
    else echo "KUBAROS_VERSION=\"${newver}\"" >> .env; fi
    echo "==> Updating to ${newver}..."
    _ensure_wg
    if docker compose pull; then
      docker compose up -d
      echo "✓ Kubaros updated to ${newver}."
    else
      echo "✗ Could not pull images for '${newver}' (the tag may not exist in the registry)." >&2
      if [[ "${newver}" != "latest" && -n "${latest}" && "${newver}" != "${latest}" ]]; then
        echo "→ Retrying with the latest published version (${latest})..." >&2
        sed -i "s|^KUBAROS_VERSION=.*|KUBAROS_VERSION=\"${latest}\"|" .env
        if docker compose pull && docker compose up -d; then echo "✓ Kubaros updated to ${latest}."; exit 0; fi
      fi
      # Revert the pin so the stack keeps running on the previous version.
      sed -i "s|^KUBAROS_VERSION=.*|KUBAROS_VERSION=\"${prev}\"|" .env
      docker compose up -d || true
      echo "✗ Update aborted; still on ${prev}. Run 'kubaros check' to see available versions." >&2
      exit 1
    fi
    ;;
  restart) docker compose restart "$@" ;;
  start|up) _ensure_wg; docker compose up -d ;;
  stop|down) docker compose down ;;
  status|ps) docker compose ps ;;
  logs) docker compose logs -f "$@" ;;
  pull) docker compose pull ;;
  config) "${EDITOR:-nano}" "${KUBAROS_DIR}/.env" ;;
  url) grep '^KUBAROS_BASE_URL=' .env | cut -d'"' -f2 ;;
  version) grep '^KUBAROS_VERSION=' .env | cut -d'"' -f2 ;;
  help|--help|-h|*)
    cat <<USAGE
kubaros — appliance control
  kubaros check             Show installed vs. latest available version (no changes)
  kubaros update            Update to the latest published version + restart
  kubaros update X.Y.Z      Update to a specific version (e.g. 0.2.1)
  kubaros update latest     Follow the rolling 'latest' tag
  kubaros restart [svc]     Restart the stack (or one service)
  kubaros start | stop      Start / stop the stack
  kubaros status            Show container status
  kubaros logs [svc]        Follow logs (all or one service)
  kubaros pull              Pull current images only
  kubaros config            Edit /opt/kubaros/.env
  kubaros url | version     Show console URL / version
USAGE
    ;;
esac
CLI
  chmod +x /usr/local/bin/kubaros
  ok "Installed 'kubaros' command (try: kubaros status)."
}

# Fetch a file from the install base and verify it against deploy/sha256sums.txt
# (when present). Fail hard — a fresh install must never run unverified config.
fetch_verified() {
  local f="$1"
  curl -fsSL "${INSTALL_BASE}/${f}" -o "${f}.new" || { err "Could not fetch ${f} from ${INSTALL_BASE}."; return 1; }
  if [[ -f sha256sums.txt ]]; then
    local want got
    want="$(grep -E " ${f}$" sha256sums.txt 2>/dev/null | awk '{print $1}' | head -1 || true)"
    if [[ -n "${want}" ]]; then
      got="$(sha256sum "${f}.new" | awk '{print $1}')"
      if [[ "${got}" != "${want}" ]]; then
        err "Checksum mismatch for ${f} (expected ${want}, got ${got})."
        err "The install source may be mid-publish — please retry in a moment."
        rm -f "${f}.new"
        return 1
      fi
    fi
  fi
  mv "${f}.new" "${f}"
}

# Returns the installer-format version currently published at the install base
# (empty if it cannot be determined — older install bases have no VERSION file).
remote_installer_version() {
  curl -fsSL "${INSTALL_BASE}/VERSION" 2>/dev/null | tr -d '[:space:]' || true
}

local_installer_version() {
  grep '^KUBAROS_INSTALLER_VERSION=' .env 2>/dev/null | cut -d'"' -f2 || true
}

fetch_stack() {
  cd "${KUBAROS_DIR}"
  # The config files are version-pinned (deploy/VERSION + sha256sums.txt) so a
  # host only refreshes them when the install source actually changed — this is
  # what prevents a stale docker-compose.yml from silently sticking around.
  local remote cur
  remote="$(remote_installer_version)"
  cur="$(local_installer_version)"
  if [[ -n "${remote}" && "${remote}" == "${cur}" ]]; then
    ok "Installer files already at version ${remote} — nothing to refresh."
    return
  fi
  [[ -n "${remote}" ]] && info "Installer format version ${remote}."

  # Caddyfile is generated locally (write_caddyfile) so its TLS mode matches the
  # chosen domain/IP — it is intentionally NOT fetched here.
  curl -fsSL "${INSTALL_BASE}/sha256sums.txt" -o sha256sums.txt 2>/dev/null || rm -f sha256sums.txt
  for f in docker-compose.yml kubaros-update.sh; do
    info "Fetching ${f}..."
    fetch_verified "${f}" || exit 1
  done
  chmod +x kubaros-update.sh
  if [[ -n "${remote}" ]]; then
    set_env_kv KUBAROS_INSTALLER_VERSION "${remote}"
  fi
  ok "Stack files in place."
}

install_host_updater() {
  # Lets the in-app self-updater apply releases without giving the application
  # container privileged Docker access. A systemd path unit watches the request
  # file the backend writes, then runs kubaros-update.sh on the host.
  if ! command -v systemctl >/dev/null 2>&1; then
    warn "systemd not found — host updater not installed (CLI updates still work)."
    return
  fi
  info "Installing host self-updater (systemd path unit)..."
  cat > /etc/systemd/system/kubaros-update.service <<EOF
[Unit]
Description=Kubaros host updater
After=docker.service

[Service]
Type=oneshot
Environment=KUBAROS_DIR=${KUBAROS_DIR}
ExecStart=${KUBAROS_DIR}/kubaros-update.sh
EOF
  cat > /etc/systemd/system/kubaros-update.path <<EOF
[Unit]
Description=Watch for Kubaros update requests

[Path]
PathChanged=${KUBAROS_DIR}/state/update-request
Unit=kubaros-update.service

[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
  systemctl enable --now kubaros-update.path >/dev/null 2>&1 || warn "Could not enable kubaros-update.path."
  ok "Host self-updater installed."
}

registry_login() {
  # Authenticate to the container registry — required ONLY for PRIVATE images
  # (e.g. an internal test/edge build). No-op for public images. Reads the
  # token from the generated .env so re-runs and the host updater stay
  # consistent.
  cd "${KUBAROS_DIR}"
  local user token host
  host="$(grep '^KUBAROS_IMAGE_PREFIX=' .env 2>/dev/null | cut -d'"' -f2 | cut -d/ -f1)"
  host="${host:-git.host-on.dev}"
  user="$(grep '^KUBAROS_REGISTRY_USER=' .env 2>/dev/null | cut -d'"' -f2)"
  token="$(grep '^KUBAROS_REGISTRY_TOKEN=' .env 2>/dev/null | cut -d'"' -f2)"
  if [[ -z "${token}" ]]; then
    user="$(grep '^KUBAROS_GITHUB_USER=' .env 2>/dev/null | cut -d'"' -f2)"
    token="$(grep '^KUBAROS_GITHUB_TOKEN=' .env 2>/dev/null | cut -d'"' -f2)"
    host="ghcr.io"
  fi
  if [[ -n "${token}" && -n "${user}" ]]; then
    info "Logging in to ${host} for private images (user: ${user})..."
    if echo "${token}" | docker login "${host}" -u "${user}" --password-stdin >/dev/null 2>&1; then
      ok "Authenticated to ${host}."
    else
      warn "Registry login failed — public images still pull, private ones will not."
    fi
  fi
}

ensure_wireguard_host() {
  # The Remote-Connector WireGuard tunnel (appliance -> tenant nodes) needs either
  # the host kernel WireGuard module (preferred, used with NET_ADMIN) OR
  # /dev/net/tun for the bundled userspace wireguard-go fallback. Ensure both are
  # present so the backend container (which maps /dev/net/tun) always starts.
  modprobe wireguard 2>/dev/null || true
  modprobe tun 2>/dev/null || true
  if [ ! -e /dev/net/tun ]; then
    mkdir -p /dev/net 2>/dev/null || true
    mknod /dev/net/tun c 10 200 2>/dev/null || true
    chmod 600 /dev/net/tun 2>/dev/null || true
  fi
}

start_stack() {
  cd "${KUBAROS_DIR}"
  ensure_wireguard_host
  info "Pulling images and starting the stack..."
  docker compose pull
  docker compose up -d
  ok "Kubaros is starting."
}

print_summary() {
  cd "${KUBAROS_DIR}"
  local url; url="$(grep KUBAROS_BASE_URL .env | cut -d'"' -f2)"
  echo
  ok "Kubaros (${KUBAROS_VERSION}) installed."
  echo -e "   Console:  ${c_green}${url}${c_reset}"
  echo -e "   Open the URL and complete the setup wizard: create your admin account,"
  echo -e "   connect & validate your Proxmox, and register your public IPs."
  echo -e "   Images:   ${IMAGE_PREFIX}/kubaros-{backend,frontend}:${KUBAROS_VERSION}"
  echo -e "   Data dir: ${KUBAROS_DIR}"
  echo -e "   Manage:   ${c_green}kubaros status${c_reset} · ${c_green}kubaros update${c_reset} · ${c_green}kubaros logs${c_reset} · ${c_green}kubaros restart${c_reset}"
  echo -e "   Update:   re-run this installer any time, or run ${c_green}kubaros update${c_reset}."
  echo
  echo -e "   ${c_yellow}Remote Connector (WireGuard):${c_reset} if a Proxmox site runs in remote-connector"
  echo -e "   mode, allow inbound ${c_green}UDP ${WG_PORT}${c_reset} to this host in any upstream firewall —"
  echo -e "   each site's edge VM dials the appliance here (hub-and-spoke)."
  echo
}

main() {
  echo -e "${c_blue}Kubaros installer${c_reset} (${KUBAROS_VERSION})"
  require_root
  check_os
  ensure_prereqs
  ensure_docker
  setup_env
  fetch_stack
  write_caddyfile
  install_cli
  install_host_updater
  registry_login
  start_stack
  print_summary
}

main "$@"
