#!/usr/bin/env bash
set -Eeuo pipefail

readonly REPO_NAME="tresen"
readonly PACKAGE_REPO_URL="${TRESEN_PKG_REPO:-https://packages.ryife.io}"
readonly PACKAGE_KEY_URL="${TRESEN_PKG_KEY_URL:-${PACKAGE_REPO_URL%/}/tresen.gpg}"
readonly PACKAGE_KEY_FINGERPRINT="${TRESEN_PKG_KEY_FINGERPRINT:-7A46313AB5D7D89031D0E308454141CD8551E1BB}"
readonly DEFAULT_TRESEN_ROOT="/var/lib/tresen"
readonly DOCS_URL="https://docs.ryife.io/tresen/"
readonly NODESOURCE_KEY_URL="https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key"

site_address="${TRESEN_SITE_ADDRESS:-}"
webserver="${TRESEN_WEBSERVER:-}"
tls_mode="${TRESEN_TLS_MODE:-}"
tresen_root="${TRESEN_ROOT:-$DEFAULT_TRESEN_ROOT}"
db_name="${TRESEN_DB_NAME:-tresen}"
db_user="${TRESEN_DB_USER:-tresen}"
admin_username="${TRESEN_ADMIN_USERNAME:-admin}"
db_password="${TRESEN_DB_PASSWORD:-}"
jwt_secret="${TRESEN_JWT_SECRET:-}"
admin_password="${TRESEN_ADMIN_PASSWORD:-}"
admin_password_generated=false
package_key_file=''
public_origin=''
db_name_explicit=false
db_user_explicit=false
admin_username_explicit=false
[[ -n "${TRESEN_DB_NAME+x}" ]] && db_name_explicit=true
[[ -n "${TRESEN_DB_USER+x}" ]] && db_user_explicit=true
[[ -n "${TRESEN_ADMIN_USERNAME+x}" ]] && admin_username_explicit=true

shopt -s nullglob

cleanup() {
    [[ -z "$package_key_file" ]] || rm -f -- "$package_key_file"
}

trap cleanup EXIT

fail() {
    printf 'Fehler: %s\n' "$1" >&2
    exit 1
}

info() {
    printf '\n==> %s\n' "$1"
}

warn() {
    printf 'Warnung: %s\n' "$1" >&2
}

validate_url() {
    local label="$1"
    local value="$2"
    [[ "$value" == https://* ]] || fail "${label} muss HTTPS verwenden."
    [[ "$value" != *' '* && "$value" != *$'\t'* ]] || fail "${label} enthaelt ungueltige Zeichen."
    [[ "$value" != *'#'* && "$value" != *'['* && "$value" != *']'* && "$value" != *'{'* && "$value" != *'}'* ]] || \
        fail "${label} enthaelt ungueltige Zeichen."
    [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || fail "${label} darf keine Zeilenumbrueche enthalten."
}

prompt_required() {
    local variable="$1"
    local prompt="$2"
    local current value=''
    current="${!variable:-}"
    if [[ -n "$current" ]]; then
        return
    fi
    while [[ -z "$value" ]]; do
        read -r -p "${prompt}: " value || fail "Eingabe abgebrochen."
    done
    printf -v "$variable" '%s' "$value"
}

prompt_default() {
    local variable="$1"
    local prompt="$2"
    local default="$3"
    local current value
    current="${!variable:-}"
    if [[ -n "$current" ]]; then
        return
    fi
    read -r -p "${prompt} [${default}]: " value || fail "Eingabe abgebrochen."
    printf -v "$variable" '%s' "${value:-$default}"
}

prompt_webserver() {
    local value="${webserver,,}"
    if [[ -z "$value" ]]; then
        printf '\nWebserver:\n'
        printf '  1) Manuell nach %s\n' "$DOCS_URL"
        printf '  2) Caddy automatisch konfigurieren\n'
        printf '  3) Nginx automatisch konfigurieren\n'
        read -r -p 'Auswahl [1]: ' value || fail "Eingabe abgebrochen."
        value="${value:-1}"
    fi
    case "$value" in
        1|manual|manuell) webserver="manual" ;;
        2|caddy) webserver="caddy" ;;
        3|nginx) webserver="nginx" ;;
        *) fail "Ungueltige Webserver-Auswahl: ${value}" ;;
    esac
}

prompt_tls_mode() {
    local value="${tls_mode,,}"
    local default_choice=1
    if is_ip_address "$site_address"; then
        default_choice=3
    fi
    if [[ -z "$value" ]]; then
        printf '\nTLS:\n'
        printf '  1) Oeffentliches Zertifikat automatisch beziehen (ACME)\n'
        printf '  2) Internes Caddy-Zertifikat (CA muss auf Clients vertraut werden)\n'
        printf '  3) HTTP ohne TLS\n'
        read -r -p "Auswahl [${default_choice}]: " value || fail "Eingabe abgebrochen."
        value="${value:-$default_choice}"
    fi
    case "$value" in
        1|automatic|auto|public|oeffentlich) tls_mode="automatic" ;;
        2|internal|intern) tls_mode="internal" ;;
        3|http|none|kein) tls_mode="http" ;;
        *) fail "Ungueltige TLS-Auswahl: ${value}" ;;
    esac
    if [[ "$tls_mode" == automatic ]] && is_ip_address "$site_address"; then
        fail 'Oeffentliches TLS kann nicht fuer eine IP-Adresse verwendet werden. Bitte internes TLS oder HTTP waehlen.'
    fi
}

validate_site_address() {
    [[ -n "$site_address" ]] || fail "Domain oder IP-Adresse darf nicht leer sein."
    [[ "$site_address" =~ ^[A-Za-z0-9._:-]+$ ]] || \
        fail "Domain oder IP-Adresse enthaelt ungueltige Zeichen. Bitte ohne http:// oder https:// angeben."
    [[ "$site_address" != .* && "$site_address" != -* ]] || \
        fail "Domain oder IP-Adresse darf nicht mit Punkt oder Bindestrich beginnen."
    if [[ "$site_address" == *:* ]]; then
        [[ "$site_address" =~ ^[0-9A-Fa-f:]+$ ]] || fail "IPv6-Adresse ist ungueltig."
        site_address="[${site_address}]"
    fi
}

plain_site_address() {
    local value="${1#[}"
    value="${value%]}"
    printf '%s' "$value"
}

is_ip_address() {
    local value
    value="$(plain_site_address "$1")"
    [[ "$value" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ || \
        ("$value" == *:* && "$value" =~ ^[0-9A-Fa-f:]+$) ]]
}

validate_tresen_root() {
    [[ "$tresen_root" == /* && "$tresen_root" != "/" ]] || \
        fail "TRESEN_ROOT muss ein absoluter Pfad sein."
    [[ "$tresen_root" != *[[:space:]]* && "$tresen_root" != *$'\n'* && "$tresen_root" != *$'\r'* ]] || \
        fail "TRESEN_ROOT darf keine Leerzeichen oder Zeilenumbrueche enthalten."
    tresen_root="${tresen_root%/}"
    command -v realpath >/dev/null 2>&1 || fail "realpath wurde nicht gefunden."
    tresen_root="$(realpath -m -- "$tresen_root")"
    [[ "$tresen_root" != "/" ]] || fail "TRESEN_ROOT darf nicht das Root-Dateisystem sein."
    [[ "$tresen_root" != "$DEFAULT_TRESEN_ROOT"/* ]] || \
        fail "TRESEN_ROOT darf nicht unter ${DEFAULT_TRESEN_ROOT} liegen."
    [[ "$DEFAULT_TRESEN_ROOT" != "$tresen_root"/* ]] || \
        fail "TRESEN_ROOT darf kein uebergeordneter Pfad von ${DEFAULT_TRESEN_ROOT} sein."
    case "$tresen_root" in
        /etc|/etc/*|/usr|/usr/*|/bin|/bin/*|/sbin|/sbin/*|/lib|/lib/*|/lib64|/lib64/*|/boot|/boot/*|/dev|/dev/*|/proc|/proc/*|/run|/run/*|/sys|/sys/*|/root|/root/*|/tmp|/tmp/*)
            fail "TRESEN_ROOT liegt in einem geschuetzten Systempfad."
            ;;
    esac
}

validate_identifier() {
    local label="$1"
    local value="$2"
    [[ "$value" =~ ^[a-z_][a-z0-9_]{0,62}$ ]] || \
        fail "${label} darf nur Kleinbuchstaben, Ziffern und Unterstriche enthalten und muss mit einem Buchstaben oder Unterstrich beginnen."
}

validate_distribution() {
    case "${ID:-}:${VERSION_ID:-}" in
        arch:*|fedora:*) ;;
        debian:12|ubuntu:24.04) ;;
        *) fail "Unterstuetzt werden Arch Linux, Fedora, Debian 12 und Ubuntu 24.04." ;;
    esac
}

generate_secret() {
    od -An -N32 -tx1 /dev/urandom | tr -d ' \n'
}

download_key() {
    local target="$1"
    curl --fail --proto '=https' --tlsv1.2 --location \
        "$PACKAGE_KEY_URL" --output "$target"
}

key_fingerprint() {
    local key_file="$1"
    local record field2 field3 field4 field5 field6 field7 field8 field9 field10 field11
    local primary_key_count=0
    local primary_fingerprint=''
    # shellcheck disable=SC2034
    while IFS=: read -r record field2 field3 field4 field5 field6 field7 field8 field9 field10 field11; do
        case "$record" in
            pub) primary_key_count=$((primary_key_count + 1)) ;;
            fpr)
                [[ -n "$primary_fingerprint" ]] || primary_fingerprint="$field10"
                ;;
        esac
    done < <(gpg --batch --show-keys --with-colons "$key_file")
    [[ "$primary_key_count" -eq 1 ]] || fail "Die Repository-Keydatei muss genau einen Primary-Key enthalten."
    printf '%s' "$primary_fingerprint"
}

verify_key() {
    local key_file="$1"
    local expected="${PACKAGE_KEY_FINGERPRINT// /}"
    local actual
    actual="$(key_fingerprint "$key_file")"
    [[ -n "$actual" && "${actual^^}" == "${expected^^}" ]] || \
        fail "Der Repository-Schluessel hat nicht den erwarteten Fingerprint."
}

configure_package_repository() {
    package_key_file="$(mktemp)"
    local key_file="$package_key_file"

    case "${ID:-}" in
        arch)
            [[ "$(uname -m)" == x86_64 ]] || fail "Dieses Repository unterstuetzt aktuell nur x86_64."
            pacman -S --needed --noconfirm curl gnupg
            download_key "$key_file"
            verify_key "$key_file"
            pacman-key --add "$key_file"
            pacman-key --lsign-key "$PACKAGE_KEY_FINGERPRINT"
            install -d -m 0755 /etc/pacman.d
            cat > /etc/pacman.d/tresen.conf <<EOF
[${REPO_NAME}]
SigLevel = Required DatabaseOptional
Server = ${PACKAGE_REPO_URL%/}/arch/\$arch
EOF
            if ! grep -qF 'Include = /etc/pacman.d/tresen.conf' /etc/pacman.conf; then
                printf '\nInclude = /etc/pacman.d/tresen.conf\n' >> /etc/pacman.conf
            fi
            ;;
        debian|ubuntu)
            [[ "$(dpkg --print-architecture)" == amd64 ]] || fail "Dieses Repository unterstuetzt aktuell nur amd64."
            apt-get update
            apt-get install -y ca-certificates curl gnupg
            download_key "$key_file"
            verify_key "$key_file"
            install -d -m 0755 /etc/apt/keyrings
            gpg --batch --dearmor --yes --output /etc/apt/keyrings/tresen.gpg "$key_file"
            chmod 0644 /etc/apt/keyrings/tresen.gpg
            local apt_source_dir
            apt_source_dir="${PACKAGE_REPO_URL%/}/$( [[ "${ID}" == ubuntu ]] && printf ubuntu || printf debian )"
            cat > /etc/apt/sources.list.d/tresen.list <<EOF
deb [signed-by=/etc/apt/keyrings/tresen.gpg] ${apt_source_dir} stable main
EOF
            ;;
        fedora)
            dnf install -y ca-certificates curl gnupg2
            download_key "$key_file"
            verify_key "$key_file"
            install -D -m 0644 "$key_file" /etc/pki/rpm-gpg/RPM-GPG-KEY-tresen
            rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-tresen
            cat > /etc/yum.repos.d/tresen.repo <<EOF
[${REPO_NAME}]
name=Tresen
baseurl=${PACKAGE_REPO_URL%/}/fedora/\$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-tresen
EOF
            ;;
        *)
            fail "Nicht unterstuetzte Distribution: ${ID:-unbekannt}"
            ;;
    esac
    rm -f "$key_file"
    package_key_file=''
}

is_directory_empty() {
    local directory="$1"
    local entries=("$directory"/* "$directory"/.[!.]* "$directory"/..?*)
    ((${#entries[@]} == 0))
}

clear_directory() {
    local directory="$1"
    local entries=("$directory"/* "$directory"/.[!.]* "$directory"/..?*)
    ((${#entries[@]} == 0)) || rm -rf -- "${entries[@]}"
}

prepare_runtime_root() {
    [[ ! -L "$DEFAULT_TRESEN_ROOT" ]] || fail "${DEFAULT_TRESEN_ROOT} ist ein nicht unterstuetzter Symlink."
    install -d -m 0755 "$tresen_root"
    install -d -m 0755 "$DEFAULT_TRESEN_ROOT"

    if [[ "$tresen_root" != "$DEFAULT_TRESEN_ROOT" ]]; then
        command -v mountpoint >/dev/null 2>&1 || fail "mountpoint wurde nicht gefunden."
        command -v findmnt >/dev/null 2>&1 || fail "findmnt wurde nicht gefunden."
        command -v mount >/dev/null 2>&1 || fail "mount wurde nicht gefunden."
        if mountpoint -q "$DEFAULT_TRESEN_ROOT"; then
            local current_source
            current_source="$(findmnt -n -o SOURCE --target "$DEFAULT_TRESEN_ROOT" || true)"
            [[ "$current_source" == "$tresen_root" ]] || \
                fail "${DEFAULT_TRESEN_ROOT} ist bereits auf einen anderen Pfad gemountet."
        else
            if ! is_directory_empty "$DEFAULT_TRESEN_ROOT"; then
                is_directory_empty "$tresen_root" || \
                    fail "Sowohl ${DEFAULT_TRESEN_ROOT} als auch ${tresen_root} enthalten Dateien; automatische Migration abgebrochen."
                cp -a "${DEFAULT_TRESEN_ROOT}/." "$tresen_root/"
                clear_directory "$DEFAULT_TRESEN_ROOT"
            fi
            mount --bind "$tresen_root" "$DEFAULT_TRESEN_ROOT"
        fi
        if ! grep -qF "${tresen_root} ${DEFAULT_TRESEN_ROOT} none bind" /etc/fstab; then
            printf '%s %s none bind 0 0\n' "$tresen_root" "$DEFAULT_TRESEN_ROOT" >> /etc/fstab
        fi
    fi

    if [[ -d "${DEFAULT_TRESEN_ROOT}/storage" && ! -e "${DEFAULT_TRESEN_ROOT}/data" ]]; then
        mv "${DEFAULT_TRESEN_ROOT}/storage" "${DEFAULT_TRESEN_ROOT}/data"
    fi
    install -d -m 0755 "${DEFAULT_TRESEN_ROOT}/bin"
    install -d -m 0700 "${DEFAULT_TRESEN_ROOT}/data"
}

install_system_packages() {
    local packages=(postgresql nodejs)
    case "$webserver" in
        caddy) packages+=(caddy) ;;
        nginx) packages+=(nginx) ;;
    esac

    case "${ID:-}" in
        arch)
            packages+=(jdk21-openjdk)
            pacman -Syu --needed --noconfirm "${packages[@]}"
            ;;
        debian|ubuntu)
            install_nodejs_repository
            if [[ "$webserver" == caddy ]]; then
                install_caddy_repository
            fi
            apt-get update
            packages+=(default-jre-headless)
            if [[ "${ID}" == debian && -n "${VERSION_CODENAME:-}" ]]; then
                install -d -m 0755 /etc/apt/sources.list.d
                printf 'deb https://deb.debian.org/debian %s-backports main\n' \
                    "$VERSION_CODENAME" > /etc/apt/sources.list.d/tresen-backports.list
                apt-get update
                DEBIAN_FRONTEND=noninteractive apt-get install -y postgresql nodejs
                DEBIAN_FRONTEND=noninteractive apt-get install -y \
                    -t "${VERSION_CODENAME}-backports" default-jre-headless
                if [[ "$webserver" != manual ]]; then
                    DEBIAN_FRONTEND=noninteractive apt-get install -y "$webserver"
                fi
            else
                DEBIAN_FRONTEND=noninteractive apt-get install -y "${packages[@]}"
            fi
            ;;
        fedora)
            dnf install -y postgresql-server postgresql-contrib nodejs java-21-openjdk-headless
            if [[ "$webserver" != manual ]]; then
                dnf install -y "$webserver"
            fi
            ;;
        *)
            fail "Nicht unterstuetzte Distribution: ${ID:-unbekannt}"
            ;;
    esac
}

install_nodejs_repository() {
    install -d -m 0755 /usr/share/keyrings
    curl --fail --proto '=https' --tlsv1.2 --location "$NODESOURCE_KEY_URL" | \
        gpg --batch --dearmor --yes --output /usr/share/keyrings/nodesource.gpg
    chmod 0644 /usr/share/keyrings/nodesource.gpg
    cat > /etc/apt/sources.list.d/nodesource.list <<'EOF'
deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main
EOF
}

install_caddy_repository() {
    install -d -m 0755 /usr/share/keyrings
    curl --fail --proto '=https' --tlsv1.2 --location \
        'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
        gpg --batch --dearmor --yes --output /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    chmod 0644 /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    cat > /etc/apt/sources.list.d/caddy-stable.list <<'EOF'
deb [signed-by=/usr/share/keyrings/caddy-stable-archive-keyring.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main
EOF
}

install_tresen_package() {
    case "${ID:-}" in
        arch) pacman -S --needed --noconfirm "$REPO_NAME" ;;
        debian|ubuntu) DEBIAN_FRONTEND=noninteractive apt-get install -y "$REPO_NAME" ;;
        fedora) dnf install -y "$REPO_NAME" ;;
        *) fail "Nicht unterstuetzte Distribution: ${ID:-unbekannt}" ;;
    esac
}

as_postgres() {
    if command -v runuser >/dev/null 2>&1; then
        runuser -u postgres -- "$@"
    else
        su -s /bin/sh postgres -c "$(printf '%q ' "$@")"
    fi
}

initialize_postgresql() {
    case "${ID:-}" in
        arch)
            if [[ ! -f /var/lib/postgres/data/PG_VERSION ]]; then
                install -d -o postgres -g postgres -m 0700 /var/lib/postgres/data
                as_postgres initdb -D /var/lib/postgres/data
            fi
            ;;
        fedora)
            if [[ ! -f /var/lib/pgsql/data/PG_VERSION ]]; then
                if command -v postgresql-setup >/dev/null 2>&1; then
                    postgresql-setup --initdb --unit postgresql || postgresql-setup --initdb
                else
                    fail "postgresql-setup wurde nicht gefunden."
                fi
            fi
            ;;
    esac

    systemctl enable --now postgresql
    for _ in {1..30}; do
        if as_postgres psql --dbname=postgres --command='SELECT 1' >/dev/null 2>&1; then
            return
        fi
        sleep 1
    done
    fail "PostgreSQL wurde nicht rechtzeitig verfuegbar."
}

configure_postgresql() {
    load_existing_runtime_settings
    validate_identifier "TRESEN_DB_NAME" "$db_name"
    validate_identifier "TRESEN_DB_USER" "$db_user"
    [[ -n "$db_password" ]] || db_password="$(generate_secret)"

    local role_exists database_exists
    [[ "$db_password" =~ ^[A-Za-z0-9._-]{16,128}$ ]] || \
        fail "TRESEN_DB_PASSWORD muss 16 bis 128 sichere Zeichen enthalten."
    role_exists="$(as_postgres psql --dbname=postgres --tuples-only --no-align \
        --command="SELECT 1 FROM pg_roles WHERE rolname = '${db_user}'")"
    if [[ "$role_exists" == *1* ]]; then
        as_postgres psql --dbname=postgres --command="ALTER ROLE \"${db_user}\" WITH LOGIN PASSWORD '${db_password}'"
    else
        as_postgres psql --dbname=postgres --command="CREATE ROLE \"${db_user}\" LOGIN PASSWORD '${db_password}'"
    fi

    database_exists="$(as_postgres psql --dbname=postgres --tuples-only --no-align \
        --command="SELECT 1 FROM pg_database WHERE datname = '${db_name}'")"
    if [[ "$database_exists" == *1* ]]; then
        as_postgres psql --dbname=postgres --command="ALTER DATABASE \"${db_name}\" OWNER TO \"${db_user}\""
    else
        as_postgres createdb --owner="$db_user" "$db_name"
    fi
}

read_env_value() {
    local file="$1"
    local wanted_key="$2"
    local key value
    while IFS='=' read -r key value; do
        if [[ "$key" == "$wanted_key" ]]; then
            printf '%s' "$value"
            return
        fi
    done < "$file"
}

load_existing_runtime_settings() {
    local env_file=/etc/tresen/backend.env
    [[ -f "$env_file" ]] || return 0
    if [[ "$db_name_explicit" == false ]]; then
        local existing_db_name
        existing_db_name="$(read_env_value "$env_file" DB_NAME)"
        [[ -z "$existing_db_name" ]] || db_name="$existing_db_name"
    fi
    if [[ "$db_user_explicit" == false ]]; then
        local existing_db_user
        existing_db_user="$(read_env_value "$env_file" DB_USER)"
        [[ -z "$existing_db_user" ]] || db_user="$existing_db_user"
    fi
    if [[ "$admin_username_explicit" == false ]]; then
        local existing_admin_username
        existing_admin_username="$(read_env_value "$env_file" ADMIN_USERNAME)"
        [[ -z "$existing_admin_username" ]] || admin_username="$existing_admin_username"
    fi
    [[ -n "$db_password" ]] || db_password="$(read_env_value "$env_file" DB_PASSWORD)"
    [[ -n "$jwt_secret" ]] || jwt_secret="$(read_env_value "$env_file" JWT_SECRET)"
    [[ -n "$admin_password" ]] || admin_password="$(read_env_value "$env_file" ADMIN_PASSWORD)"
}

configure_runtime_environment() {
    local cors_origins='' cookie_secure
    if [[ "$webserver" == caddy && "$tls_mode" != http ]]; then
        public_origin="https://${site_address}"
        cookie_secure=true
        if is_ip_address "$site_address"; then
            cors_origins="$public_origin"
        else
            cors_origins="https://${site_address},http://${site_address}"
        fi
    elif is_ip_address "$site_address"; then
        public_origin="http://${site_address}"
        cors_origins="$public_origin"
        cookie_secure=false
    else
        public_origin="http://${site_address}"
        cookie_secure=false
    fi
    [[ -n "$cors_origins" ]] || cors_origins="https://${site_address},http://${site_address}"
    # Capacitor uses these fixed origins for the embedded native WebView.
    cors_origins="http://localhost,https://localhost,capacitor://localhost,${cors_origins}"

    load_existing_runtime_settings
    [[ -n "$jwt_secret" ]] || jwt_secret="$(generate_secret)"
    [[ "$jwt_secret" =~ ^[A-Za-z0-9._-]{32,256}$ ]] || \
        fail "TRESEN_JWT_SECRET muss 32 bis 256 sichere Zeichen enthalten."
    if [[ -z "$admin_password" ]]; then
        admin_password="$(generate_secret)"
        admin_password_generated=true
    fi
    [[ "$admin_username" =~ ^[A-Za-z0-9._-]{1,64}$ ]] || \
        fail "TRESEN_ADMIN_USERNAME enthaelt ungueltige Zeichen."
    [[ "$admin_password" =~ ^[A-Za-z0-9._-]{8,256}$ ]] || \
        fail "TRESEN_ADMIN_PASSWORD muss 8 bis 256 sichere Zeichen enthalten."

    install -d -m 0755 /etc/tresen
    cat > /etc/tresen/backend.env <<EOF
DB_URL=jdbc:postgresql://127.0.0.1:5432/${db_name}
DB_NAME=${db_name}
DB_USER=${db_user}
DB_PASSWORD=${db_password}
SERVER_ADDRESS=127.0.0.1
SERVER_PORT=8080
JWT_SECRET=${jwt_secret}
COOKIE_SECURE=${cookie_secure}
COOKIE_SAME_SITE=Lax
CORS_ORIGINS=${cors_origins}
ADMIN_USERNAME=${admin_username}
ADMIN_PASSWORD=${admin_password}
STORAGE_DIR=${DEFAULT_TRESEN_ROOT}/data
APP_DEFAULT_API_PATH=/api
TRESEN_SERVER_MANAGEMENT_ENABLED=true
EOF
    chmod 0600 /etc/tresen/backend.env

    cat > /etc/tresen/app.env <<EOF
HOST=127.0.0.1
PORT=3000
NODE_ENV=production
EOF
    chmod 0644 /etc/tresen/app.env
}

configure_caddy() {
    local caddy_address="$site_address"
    if [[ "$tls_mode" == http ]]; then
        caddy_address="http://${site_address}"
    fi

    install -d -m 0755 /etc/caddy /etc/caddy/Caddyfile.d
    cat > /etc/caddy/Caddyfile.d/tresen.caddy <<EOF
${caddy_address} {
EOF
    if [[ "$tls_mode" == internal ]]; then
        printf '    tls internal\n\n' >> /etc/caddy/Caddyfile.d/tresen.caddy
    fi
    cat >> /etc/caddy/Caddyfile.d/tresen.caddy <<'EOF'
    encode zstd gzip

    handle_path /api/* {
        reverse_proxy 127.0.0.1:8080
    }

    handle /ws* {
        reverse_proxy 127.0.0.1:8080
    }

    handle /public/* {
        reverse_proxy 127.0.0.1:8080
    }

    handle {
        reverse_proxy 127.0.0.1:3000
    }
}
EOF
    if [[ ! -f /etc/caddy/Caddyfile ]]; then
        printf 'import /etc/caddy/Caddyfile.d/*\n' > /etc/caddy/Caddyfile
    elif ! grep -qF 'import /etc/caddy/Caddyfile.d/*' /etc/caddy/Caddyfile; then
        printf '\nimport /etc/caddy/Caddyfile.d/*\n' >> /etc/caddy/Caddyfile
    fi
    caddy validate --config /etc/caddy/Caddyfile
    systemctl enable --now caddy
    systemctl reload caddy || systemctl restart caddy
}

configure_nginx() {
    local config_path
    if [[ -d /etc/nginx/sites-available ]]; then
        config_path=/etc/nginx/sites-available/tresen
        install -d -m 0755 /etc/nginx/sites-enabled
    else
        config_path=/etc/nginx/conf.d/tresen.conf
        install -d -m 0755 /etc/nginx/conf.d
    fi

    if [[ -f "$config_path" ]] && ! grep -qF '# Managed by Tresen installer.' "$config_path"; then
        cp -p "$config_path" "${config_path}.bak.$(date +%Y%m%d%H%M%S)"
    fi
    cat > "$config_path" <<EOF
# Managed by Tresen installer.
server {
    listen 80;
    listen [::]:80;
    server_name ${site_address};
    client_max_body_size 6m;

    location = /api {
        proxy_pass http://127.0.0.1:8080/;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:8080/;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }

    location /ws {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }

    location /public/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host \$host;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }
}
EOF
    if [[ -d /etc/nginx/sites-enabled ]]; then
        ln -sfn "$config_path" /etc/nginx/sites-enabled/tresen
    fi
    nginx -t
    systemctl enable --now nginx
    systemctl reload nginx || systemctl restart nginx
}

configure_webserver() {
    case "$webserver" in
        manual)
            warn "Der Installer richtet kein TLS ein. Bitte die Dokumentation fuer HTTPS beachten."
            printf '\nWebserver bitte manuell nach %s konfigurieren.\n' "$DOCS_URL"
            ;;
        caddy)
            if [[ "$tls_mode" == http ]] && is_ip_address "$site_address"; then
                warn "Caddy verwendet fuer eine IP-Adresse HTTP. Fuer oeffentliche Server bitte einen Domainnamen verwenden."
            elif [[ "$tls_mode" == internal ]] && is_ip_address "$site_address"; then
                warn "Caddy verwendet fuer die IP-Adresse ein internes Zertifikat. Die Caddy-CA muss auf Clients vertraut werden."
            fi
            configure_caddy
            ;;
        nginx)
            warn "Nginx wird als HTTP-Reverse-Proxy eingerichtet. TLS muss anschliessend separat konfiguriert werden."
            configure_nginx
            ;;
    esac
}

start_tresen_services() {
    systemctl daemon-reload
    systemctl disable --now tresen-app.service 2>/dev/null || true
    systemctl disable tresen-backend.service tresen-ui.service 2>/dev/null || true
    systemctl enable --now tresen.target
}

[[ "${EUID}" -eq 0 ]] || fail "Bitte mit sudo oder als root ausfuehren."
command -v systemctl >/dev/null 2>&1 || fail "systemd/systemctl wird benoetigt."
# shellcheck disable=SC1091
source /etc/os-release
validate_distribution
validate_url TRESEN_PKG_REPO "$PACKAGE_REPO_URL"
validate_url TRESEN_PKG_KEY_URL "$PACKAGE_KEY_URL"

prompt_required site_address 'Domain oder IP-Adresse (ohne http:// oder https://)'
prompt_webserver
prompt_default tresen_root 'Tresen-Pfad' "$DEFAULT_TRESEN_ROOT"
validate_site_address
validate_tresen_root
if [[ "$webserver" == caddy ]]; then
    prompt_tls_mode
else
    tls_mode="http"
fi

info 'Paket-Repository konfigurieren'
configure_package_repository
prepare_runtime_root
install_system_packages
install_tresen_package

info 'PostgreSQL installieren und konfigurieren'
initialize_postgresql
configure_postgresql

info 'Tresen konfigurieren'
configure_runtime_environment
configure_webserver
start_tresen_services

printf '\nTresen wurde erfolgreich installiert.\n'
printf 'Adresse: %s\n' "$public_origin"
printf 'Runtime und Daten: %s/{bin,data}\n' "$tresen_root"
printf 'Services laufen als root, damit Systemupdates aus der Anwendung funktionieren.\n'
if [[ "$admin_password_generated" == true ]]; then
    printf 'Admin-Benutzer: %s\n' "$admin_username"
    printf 'Generiertes Admin-Passwort: %s\n' "$admin_password"
fi
