#!/usr/bin/env bash
# Rhinobase Next installation framework — requires an external verified application release.
# Copyright (c) 2026 Rhinobase Next project. All rights reserved.
# This script cannot install the application without its tested release bundle.
set -Eeuo pipefail
IFS=$'\n\t'

usage(){
  cat <<'HELP'
Usage:
  sudo bash install-rhinobase-next-ubuntu2404.sh \
    --bundle /path/to/rhinobase-next-release.tar.gz \
    --sha256 ACTUAL_64_CHARACTER_SHA256 \
    --env /path/to/your-private.env

This script:
  - Requires Ubuntu 24.04 amd64, Docker Engine and Docker Compose plugin.
  - Requires an independently verified Rhinobase Next application bundle.
  - Checks SHA-256 before extracting.
  - Refuses archive traversal, symlinks, links and special archive members.
  - Stops if /opt/rhinobase-next already contains files.
  - Requires a separate, private, operator-supplied .env for Compose.
  - Validates Docker Compose configuration before starting the service.

The application bundle must contain compose.yaml (or docker-compose.yml)
at the archive root. A private .env is supplied separately by --env.
This framework does not supply source code, database schema or images.
HELP
}
error(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || error 'Run with sudo/root.'
[[ $# -gt 0 ]] || { usage; exit 2; }
bundle=''; expected=''; envfile=''
while (($#)); do
  case "$1" in
    --bundle) [[ $# -ge 2 ]] || error 'Missing --bundle value'; bundle=$2; shift 2;;
    --sha256) [[ $# -ge 2 ]] || error 'Missing --sha256 value'; expected=$2; shift 2;;
    --env) [[ $# -ge 2 ]] || error 'Missing --env value'; envfile=$2; shift 2;;
    -h|--help) usage; exit 0;;
    *) error "Unrecognized argument: $1";;
  esac
done
[[ -f "$envfile" && -r "$envfile" ]] || error 'Required private .env file was not found or is unreadable.'
[[ -f "$bundle" ]] || error 'Verified application release file not found. This website does not include it.'
[[ "$expected" =~ ^[[:xdigit:]]{64}$ ]] || error 'Provide the actual published 64-character SHA-256 digest.'
[[ -r /etc/os-release ]] || error 'Cannot identify Ubuntu version.'
# shellcheck source=/etc/os-release
. /etc/os-release
[[ "${ID:-}" == ubuntu && "${VERSION_ID:-}" == 24.04 && "$(uname -m)" == x86_64 ]] || error 'Requires Ubuntu 24.04 LTS amd64.'
for bin in tar sha256sum python3 docker; do command -v "$bin" >/dev/null 2>&1 || error "Missing dependency: $bin"; done
docker compose version >/dev/null 2>&1 || error 'Docker Compose plugin is missing.'
docker info >/dev/null 2>&1 || error 'Docker daemon is not running.'
actual=$(sha256sum "$bundle" | awk '{print $1}')
[[ "${actual,,}" == "${expected,,}" ]] || error 'SHA-256 mismatch. Do not install this package.'
target='/opt/rhinobase-next'
if [[ -e "$target" ]]; then
  [[ -d "$target" ]] || error "$target already exists and is not a directory."
  [[ -z "$(find "$target" -mindepth 1 -maxdepth 1 -print -quit)" ]] || error "$target is not empty. This is not an update/migration script."
fi
# Python's tarfile parser checks *all* members before extracting any bytes.
python3 - "$bundle" <<'PY' || error 'Unsafe or malformed release archive.'
import sys,tarfile
from pathlib import PurePosixPath
with tarfile.open(sys.argv[1],mode='r:gz') as arc:
    members=arc.getmembers()
    if len(members)>100000: raise ValueError('Too many files')
    total=0
    for m in members:
        p=PurePosixPath(m.name)
        if p.is_absolute() or not p.parts or '..' in p.parts or m.name.startswith('./../'):
            raise ValueError('Unsafe path: '+m.name)
        if not (m.isfile() or m.isdir()):
            raise ValueError('Links and special files are forbidden: '+m.name)
        total+=m.size
    if total>20*1024**3: raise ValueError('Uncompressed archive larger than 20 GiB')
    names={str(PurePosixPath(m.name)).removeprefix('./') for m in members}
    if not ({'compose.yaml','docker-compose.yml'} & names):
        raise ValueError('Missing root-level compose.yaml or docker-compose.yml')
    if '.env' in names: raise ValueError('Do not package secrets in a public release archive')
PY
staging=$(mktemp -d /opt/.rhinobase-next-staging.XXXXXXXX)
cleanup(){ [[ -n "${staging:-}" && -d "$staging" ]] && rm -rf -- "$staging"; }
trap cleanup EXIT
# --no-same-owner/permissions prevents archival privilege and mode restoration.
tar --extract --gzip --file "$bundle" --directory "$staging" --no-same-owner --no-same-permissions
install -m 0600 "$envfile" "$staging/.env"
[[ -f "$staging/compose.yaml" || -f "$staging/docker-compose.yml" ]] || error 'Missing Compose file.'
cd "$staging"
docker compose config --quiet || error 'Docker Compose configuration validation failed.'
# The target is installed only after checksum, archive and compose validation succeed.
if [[ -d "$target" ]]; then rmdir "$target" || error 'Target changed during validation.'; fi
mv "$staging" "$target"
staging=''
cd "$target"
if ! docker compose up -d; then
  printf '\nApplication files remain in %s for diagnosis.\n' "$target" >&2
  error 'Docker Compose deployment failed. Review docker compose logs before trying anything else.'
fi
printf '\nRhinobase Next release started from %s\n' "$target"
printf 'Next steps: inspect containers, check firewall/TLS, verify backups, then validate with SYNTHETIC records.\n'
printf 'No assertion of HIPAA/GDPR/KVKK compliance is made by this installer.\n'
