#!/bin/sh
set -eu
umask 077

fail() { printf '\nError: %s\n' "$*" >&2; exit 1; }
command -v docker >/dev/null 2>&1 || fail 'Install Docker and Docker Compose first: https://rebalio.app/help/self-hosting'
command -v curl >/dev/null 2>&1 || fail 'curl is required.'
docker info >/dev/null 2>&1 || fail 'Docker is not running or your user cannot access it.'
docker compose version >/dev/null 2>&1 || fail 'Docker Compose is required.'

install_dir=$(pwd -P)
project_name=${COMPOSE_PROJECT_NAME:-$(basename "$install_dir" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-' | sed 's/^[^a-z0-9]*//')}
validate_cookie() {
  case "$1" in ''|*[!A-Za-z0-9_-]*|__Host-*|__Secure-*) fail 'Set REBALIO_SESSION_COOKIE_NAME to a unique name using letters, digits, underscores or hyphens (no __Host- or __Secure- prefix).';; esac
  [ "${#1}" -le 128 ] || fail 'REBALIO_SESSION_COOKIE_NAME must be at most 128 characters.'
}
check_project_owner() {
  owners=$(docker ps -a --filter "label=com.docker.compose.project=$project_name" --format '{{.Label "com.docker.compose.project.working_dir"}}') || fail 'Cannot inspect Compose project ownership.'
  if [ -n "$owners" ]; then
    printf '%s\n' "$owners" | while IFS= read -r owner; do
      [ "$owner" = "$install_dir" ] || fail "Compose project $project_name belongs to $owner. Choose a distinct COMPOSE_PROJECT_NAME; do not rename an existing data volume."
    done || exit 1
  fi
}
install_port=${REBALIO_PORT:-8088}
case "$install_port" in *[!0-9]*|'') fail 'REBALIO_PORT must be a port number.';; esac
[ "$install_port" -ge 1 ] && [ "$install_port" -le 65535 ] || fail 'REBALIO_PORT must be between 1 and 65535.'
image='ghcr.io/digitalcentral/rebalio:latest'
# A second installer must not race key creation or startup.
mkdir .install-lock 2>/dev/null || fail "Another installer is running. If it stopped unexpectedly, remove $install_dir/.install-lock and retry."
cleanup() { rm -f .env.install-tmp compose.yaml.install-tmp .compose.install-tmp; rmdir .install-lock; }
trap cleanup EXIT
trap 'exit 1' HUP INT TERM

if [ ! -f .env ]; then
  case "$project_name" in ''|[!a-z0-9]*|*[!a-z0-9_-]*) fail 'COMPOSE_PROJECT_NAME must start with a lowercase letter or digit and contain only lowercase letters, digits, underscores or hyphens.';; esac
  validate_cookie "${REBALIO_SESSION_COOKIE_NAME:-}"
  check_project_owner
  [ ! -e compose.yaml ] || fail 'Existing Compose configuration has no .env. Restore your encryption key from backup; do not generate a replacement.'
  if docker volume inspect "${project_name}_rebalio-data" >/dev/null 2>&1; then
    fail 'Existing Rebalio data has no .env. Restore your encryption key from backup.'
  fi
fi
read_scalar() {
  sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//"
}
check_configuration() {
  # Compose is the source of truth for .env, shell overrides and customized files.
  # Read only scalar values from its normalized YAML; never source/eval .env.
  if ! docker compose config > .compose.install-tmp; then
    fail 'Compose configuration is invalid. Add REBALIO_SESSION_COOKIE_NAME to .env and forward it in the rebalio service environment as ${REBALIO_SESSION_COOKIE_NAME:?Required}. Preserve your key and project name.'
  fi
  project_name=$(sed -n 's/^name: //p' .compose.install-tmp | read_scalar)
  [ -n "$project_name" ] || fail 'Cannot resolve the Compose project name.'
  # Restrict extraction to the rebalio service (other services may define the same keys).
  service_config=$(awk '/^  rebalio:$/ {inside=1; next} inside && /^  [^ ]|^[^ ]/ {exit} inside {print}' .compose.install-tmp)
  cookie_name=$(printf '%s\n' "$service_config" | sed -n 's/^      REBALIO_SESSION_COOKIE_NAME: //p' | read_scalar)
  [ -n "$cookie_name" ] || fail 'Add REBALIO_SESSION_COOKIE_NAME to .env and forward it in the rebalio service environment as ${REBALIO_SESSION_COOKIE_NAME:?Required} before upgrading.'
  validate_cookie "$cookie_name"
  install_port=$(printf '%s\n' "$service_config" | sed -n 's/^        published: //p' | read_scalar)
  case "$install_port" in ''|*[!0-9]*) fail 'Expected one published host port for the rebalio service.';; esac
  check_project_owner
}
if [ -f compose.yaml ]; then check_configuration; fi
printf '\nPreparing Rebalio in %s\n' "$install_dir"
docker pull "$image"
if [ ! -e .env ]; then
  docker run --rm --entrypoint node "$image" -e "console.log('REBALIO_MASTER_KEY=' + require('node:crypto').randomBytes(32).toString('hex'))" > .env.install-tmp
  # Never publish an empty or malformed key if generation fails.
  grep -Eq '^REBALIO_MASTER_KEY=[0-9a-f]{64}$' .env.install-tmp || fail 'Encryption key generation failed.'
  printf 'REBALIO_PORT=%s\nCOMPOSE_PROJECT_NAME=%s\nREBALIO_SESSION_COOKIE_NAME=%s\n' "$install_port" "$project_name" "$REBALIO_SESSION_COOKIE_NAME" >> .env.install-tmp
  mv .env.install-tmp .env
else
  printf 'Preserving your existing .env and encryption key.\n'
fi
if [ ! -e compose.yaml ]; then
  curl -fSL https://rebalio.app/compose.yaml -o compose.yaml.install-tmp
  mv compose.yaml.install-tmp compose.yaml
else
  printf 'Preserving your existing Compose configuration.\n'
fi
check_configuration
if ! docker compose up -d --wait --wait-timeout 120; then
  fail "Startup did not complete. Your files are preserved. In $install_dir, run: docker compose logs --tail=100 rebalio"
fi
printf '\nRebalio is ready.\n'
docker compose exec -T rebalio node -e '
const fs = require("node:fs");
console.log("Open: " + process.env.REBALIO_ORIGIN);
if (fs.existsSync("/data/setup-token")) {
  console.log("Setup token: " + fs.readFileSync("/data/setup-token", "utf8").trim());
  console.log("Enter this single-use token in the browser, then choose a username and a password of at least 12 characters.");
} else {
  console.log("Owner already configured. Sign in with your existing local username and password.");
}'
cat <<EOF

Installed on a VPS? Choose one access method:

1. SSH tunnel (when REBALIO_ORIGIN is http://127.0.0.1:$install_port)
   Run on your own computer:

   ssh -N -L $install_port:127.0.0.1:$install_port USER@VPS_ADDRESS

   Keep that terminal open and browse to http://127.0.0.1:$install_port.

2. Tailscale HTTPS
   Choose an unused public HTTPS port for this installation (443 or 8443, for example).
   For public port 8443, run on the VPS:

   sudo tailscale serve --bg --https=8443 http://127.0.0.1:$install_port
   tailscale serve status

   Set REBALIO_ORIGIN in .env to the exact HTTPS origin printed by Tailscale,
   including :8443. Preserve all other settings, then run in this directory:

   docker compose up -d --force-recreate --wait --wait-timeout 120

   Each installation needs a different public listener port, host port, Compose
   project and cookie name. Container port 8088 stays unchanged.
   Guide: https://rebalio.app/help/self-hosting

Installation directory: $install_dir
Compose project: $project_name
EOF
