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

export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"

SYNCWERK_USER="${SYNCWERK_USER:-syncwerk}"
CONFIG_DIR="${CONFIG_DIR:-/etc/syncwerk}"
CCNET_CONF_DIR="${CCNET_CONF_DIR:-${CONFIG_DIR}}"
SYNCWERK_CONF_DIR="${SYNCWERK_CONF_DIR:-${CONFIG_DIR}}"
SYNCWERK_CENTRAL_CONF_DIR="${SYNCWERK_CENTRAL_CONF_DIR:-${CONFIG_DIR}}"
RESTAPI_DIR="${RESTAPI_DIR:-/usr/share/python/syncwerk/restapi}"
RESTAPI_PYTHON="${RESTAPI_PYTHON:-/usr/bin/python3}"
RESTAPI_MANAGE="${RESTAPI_MANAGE:-${RESTAPI_DIR}/manage.py}"
RESTAPI_LOG_DIR="${RESTAPI_LOG_DIR:-/var/log/syncwerk}"
OBJECT_STORAGE_PATH="${OBJECT_STORAGE_PATH:-/var/lib/syncwerk}"
RUN_DIR="${RUN_DIR:-/run/syncwerk}"
SYNCWERK_SHARE_DIR="${SYNCWERK_SHARE_DIR:-/usr/share/syncwerk}"
SYNCWERK_PYTHON_ROOT="${SYNCWERK_PYTHON_ROOT:-/usr/share/python/syncwerk}"
LIBEVENT_ROOT="${LIBEVENT_ROOT:-/usr/lib/syncwerk/libevent}"
DJANGO_SETTINGS_MODULE="${DJANGO_SETTINGS_MODULE:-restapi.settings}"
SYNCWERK_RESTAPI_PYTHONPATH="${SYNCWERK_RESTAPI_PYTHONPATH:-${RESTAPI_DIR}:${SYNCWERK_PYTHON_ROOT}}"
PYTHONNOUSERSITE="${PYTHONNOUSERSITE:-1}"
TIMESTAMP="${TIMESTAMP:-$(date +"%Y-%m-%d_%H-%M-%S")}"
HOSTNAME="${SYNCWERK_HOSTNAME:-$(hostname -f 2>/dev/null || hostname)}"

CCNET_DB="${CCNET_DB:-syncwerk-ccnet}"
SERVER_DB="${SERVER_DB:-syncwerk-server}"
RESTAPI_DB="${RESTAPI_DB:-syncwerk-restapi}"
DB_USER="${DB_USER:-syncwerk}"
DB_HOST="${DB_HOST:-127.0.0.1}"
DB_PORT="${DB_PORT:-3306}"

SYNCWERK_SETUP_BACKUP_DATABASES="${SYNCWERK_SETUP_BACKUP_DATABASES:-1}"
SYNCWERK_SETUP_CONFIGURE_NGINX="${SYNCWERK_SETUP_CONFIGURE_NGINX:-1}"
SYNCWERK_SETUP_RESTART_NGINX="${SYNCWERK_SETUP_RESTART_NGINX:-0}"
SYNCWERK_SETUP_ENABLE_SERVICE="${SYNCWERK_SETUP_ENABLE_SERVICE:-0}"
SYNCWERK_SETUP_START_SERVICES="${SYNCWERK_SETUP_START_SERVICES:-0}"
SYNCWERK_SETUP_STOP_SERVICES="${SYNCWERK_SETUP_STOP_SERVICES:-0}"
SYNCWERK_SETUP_CREATE_ADMIN="${SYNCWERK_SETUP_CREATE_ADMIN:-0}"
SYNCWERK_SETUP_CREATE_SUPER_ADMIN="${SYNCWERK_SETUP_CREATE_SUPER_ADMIN:-$SYNCWERK_SETUP_CREATE_ADMIN}"
SYNCWERK_SETUP_MIGRATION_MODE="${SYNCWERK_SETUP_MIGRATION_MODE:-strict}"
SYNCWERK_SETUP_FORCE_MIGRATIONS="${SYNCWERK_SETUP_FORCE_MIGRATIONS:-0}"
SYNCWERK_SETUP_CHOWN_OBJECT_STORAGE="${SYNCWERK_SETUP_CHOWN_OBJECT_STORAGE:-0}"
SYNCWERK_SETUP_RUN_MANAGE_CHECK="${SYNCWERK_SETUP_RUN_MANAGE_CHECK:-1}"
SYNCWERK_SETUP_COLLECTSTATIC="${SYNCWERK_SETUP_COLLECTSTATIC:-1}"
SYNCWERK_SETUP_COMPILEMESSAGES="${SYNCWERK_SETUP_COMPILEMESSAGES:-1}"

export CONFIG_DIR CCNET_CONF_DIR SYNCWERK_CONF_DIR SYNCWERK_CENTRAL_CONF_DIR
export RESTAPI_DIR RESTAPI_LOG_DIR OBJECT_STORAGE_PATH LIBEVENT_ROOT DJANGO_SETTINGS_MODULE
export PYTHONNOUSERSITE

log() { printf '[syncwerk-admin-setup] %s\n' "$*" >&2; }
fail() { printf '[syncwerk-admin-setup] ERROR: %s\n' "$*" >&2; exit 1; }

on_error() {
    local ec="$1" line_no="$2" cmd="$3"
    printf '[syncwerk-admin-setup] ERROR at line %s: %s exited with %s\n' "$line_no" "$cmd" "$ec" >&2
    exit "$ec"
}
trap 'on_error "$?" "$LINENO" "$BASH_COMMAND"' ERR

bool_true() {
    case "${1:-}" in
        1|yes|true|on|y|Y|YES|TRUE|ON) return 0 ;;
        *) return 1 ;;
    esac
}

usage() {
    cat <<'EOF_USAGE'
Usage: syncwerk-server-admin setup [options]
       syncwerk-server-admin-setup [options]

Options:
  --start                 Start Syncwerk services after setup.
  --no-start              Do not start Syncwerk services. Default for the Trixie port.
  --enable-service        Enable/write systemd service integration.
  --no-enable-service     Do not enable systemd service integration. Default.
  --restart-nginx         Run nginx -t and restart nginx after rendering config.
  --no-restart-nginx      Do not restart nginx. Default.
  --skip-nginx            Do not render nginx config.
  --create-super-admin    Create or reconcile the bootstrap superadmin account. Requires runtime services.
  --no-create-super-admin Do not create or reconcile the bootstrap superadmin account. Default.
  --create-admin          Compatibility alias for --create-super-admin.
  --no-create-admin       Compatibility alias for --no-create-super-admin.
  --migration-mode=MODE   strict | legacy-fake | skip. Default: strict.
  --force-migrations      Run Django migrations even if /tmp/syncwerk_restapi_sql.md5 matches.
  -h, --help              Show this help.

Important environment switches:
  SYNCWERK_SETUP_START_SERVICES=0|1
  SYNCWERK_SETUP_ENABLE_SERVICE=0|1
  SYNCWERK_SETUP_RESTART_NGINX=0|1
  SYNCWERK_SETUP_CREATE_SUPER_ADMIN=0|1
  SYNCWERK_SETUP_CREATE_ADMIN=0|1        Legacy alias for SYNCWERK_SETUP_CREATE_SUPER_ADMIN.
  SYNCWERK_SETUP_MIGRATION_MODE=strict|legacy-fake|skip
  SYNCWERK_SETUP_COLLECTSTATIC=0|1
  SYNCWERK_SETUP_COMPILEMESSAGES=0|1
  SYNCWERK_RESTAPI_PYTHONPATH=/usr/share/python/syncwerk/restapi:/usr/share/python/syncwerk
EOF_USAGE
}

parse_args() {
    if [[ "${1:-}" == "setup" ]]; then
        shift
    fi
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --start) SYNCWERK_SETUP_START_SERVICES=1 ;;
            --no-start) SYNCWERK_SETUP_START_SERVICES=0 ;;
            --enable-service) SYNCWERK_SETUP_ENABLE_SERVICE=1 ;;
            --no-enable-service) SYNCWERK_SETUP_ENABLE_SERVICE=0 ;;
            --restart-nginx) SYNCWERK_SETUP_RESTART_NGINX=1 ;;
            --no-restart-nginx) SYNCWERK_SETUP_RESTART_NGINX=0 ;;
            --skip-nginx) SYNCWERK_SETUP_CONFIGURE_NGINX=0 ;;
            --create-super-admin|--create-admin)
                SYNCWERK_SETUP_CREATE_SUPER_ADMIN=1
                SYNCWERK_SETUP_CREATE_ADMIN=1
                ;;
            --no-create-super-admin|--no-create-admin)
                SYNCWERK_SETUP_CREATE_SUPER_ADMIN=0
                SYNCWERK_SETUP_CREATE_ADMIN=0
                ;;
            --migration-mode=*) SYNCWERK_SETUP_MIGRATION_MODE="${1#*=}" ;;
            --force-migrations) SYNCWERK_SETUP_FORCE_MIGRATIONS=1 ;;
            -h|--help) usage; exit 0 ;;
            *) fail "unknown argument: $1" ;;
        esac
        shift
    done
    case "$SYNCWERK_SETUP_MIGRATION_MODE" in
        strict|legacy-fake|skip) ;;
        *) fail "invalid migration mode: ${SYNCWERK_SETUP_MIGRATION_MODE}" ;;
    esac
}

require_root() {
    [[ "$(id -u)" -eq 0 ]] || fail "setup must run as root"
}

require_cmd() {
    command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1"
}

require_file() {
    [[ -f "$1" ]] || fail "missing required file: $1"
}

syncwerk_group() {
    id -gn "$SYNCWERK_USER"
}

runtime_group() {
    if getent group www-data >/dev/null 2>&1; then
        printf '%s\n' "www-data"
    else
        syncwerk_group
    fi
}

random_alnum() {
    local length="${1:-32}"
    "$RESTAPI_PYTHON" - "$length" <<'PY'
import secrets
import string
import sys
length = int(sys.argv[1])
alphabet = string.ascii_letters + string.digits
print(''.join(secrets.choice(alphabet) for _ in range(length)))
PY
}

random_hex() {
    local length="${1:-40}"
    "$RESTAPI_PYTHON" - "$length" <<'PY'
import secrets
import sys
length = int(sys.argv[1])
if length % 2:
    raise SystemExit("random_hex length must be even")
print(secrets.token_hex(length // 2))
PY
}

random_secret_key() {
    "$RESTAPI_PYTHON" - <<'PY'
import secrets
print(secrets.token_urlsafe(50))
PY
}

export_restapi_pythonpath() {
    export PYTHONPATH="${SYNCWERK_RESTAPI_PYTHONPATH}${PYTHONPATH:+:${PYTHONPATH}}"
}

as_syncwerk() {
    export_restapi_pythonpath
    sudo -E -u "$SYNCWERK_USER" env \
        CONFIG_DIR="$CONFIG_DIR" \
        CCNET_CONF_DIR="$CCNET_CONF_DIR" \
        SYNCWERK_CONF_DIR="$SYNCWERK_CONF_DIR" \
        SYNCWERK_CENTRAL_CONF_DIR="$SYNCWERK_CENTRAL_CONF_DIR" \
        RESTAPI_DIR="$RESTAPI_DIR" \
        RESTAPI_LOG_DIR="$RESTAPI_LOG_DIR" \
        OBJECT_STORAGE_PATH="$OBJECT_STORAGE_PATH" \
        LIBEVENT_ROOT="$LIBEVENT_ROOT" \
        DJANGO_SETTINGS_MODULE="$DJANGO_SETTINGS_MODULE" \
        PYTHONNOUSERSITE="$PYTHONNOUSERSITE" \
        PYTHONPATH="$PYTHONPATH" \
        "$@"
}

run_manage() {
    as_syncwerk "$RESTAPI_PYTHON" "$RESTAPI_MANAGE" "$@"
}

run_manage_package_root() {
    export_restapi_pythonpath
    (
        cd "$RESTAPI_DIR"
        umask 022
        env \
            CONFIG_DIR="$CONFIG_DIR" \
            CCNET_CONF_DIR="$CCNET_CONF_DIR" \
            SYNCWERK_CONF_DIR="$SYNCWERK_CONF_DIR" \
            SYNCWERK_CENTRAL_CONF_DIR="$SYNCWERK_CENTRAL_CONF_DIR" \
            RESTAPI_DIR="$RESTAPI_DIR" \
            RESTAPI_LOG_DIR="$RESTAPI_LOG_DIR" \
            OBJECT_STORAGE_PATH="$OBJECT_STORAGE_PATH" \
            LIBEVENT_ROOT="$LIBEVENT_ROOT" \
            DJANGO_SETTINGS_MODULE="$DJANGO_SETTINGS_MODULE" \
            PYTHONNOUSERSITE="$PYTHONNOUSERSITE" \
            PYTHONPATH="$PYTHONPATH" \
            "$RESTAPI_PYTHON" "$RESTAPI_MANAGE" "$@"
    )
}

run_restapi_python() {
    as_syncwerk "$RESTAPI_PYTHON" "$@"
}

mysql_root() {
    mysql --default-character-set=utf8 "$@"
}

mysql_force_db() {
    local db="$1"
    shift
    mysql --default-character-set=utf8 --force "$db" "$@"
}

mysql_quote() {
    local value="$1"
    value="${value//\\/\\\\}"
    value="${value//\'/\\\'}"
    printf "'%s'" "$value"
}

database_exists() {
    local db="$1"
    local quoted_db count
    quoted_db="$(mysql_quote "$db")"
    count="$(mysql_root --batch --skip-column-names -e "SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ${quoted_db};" 2>/dev/null | awk 'NR == 1 { print $1 }')"
    [[ "$count" == "1" ]]
}

read_mysql_value() {
    local key="$1"
    awk -F= -v key="$key" '
        $1 == key {
            value=$2
            sub(/^[[:space:]]+/, "", value)
            sub(/[[:space:]]+$/, "", value)
            print value
            exit
        }' "${CONFIG_DIR}/mysql.txt"
}

write_mysql_client_file() {
    local password="$1"
    local tmp
    tmp="$(mktemp "${CONFIG_DIR}/mysql.txt.XXXXXX")"
    cat >"$tmp" <<EOF_MYSQL
[client]
user=${DB_USER}
password=${password}
EOF_MYSQL
    chmod 0600 "$tmp"
    mv "$tmp" "${CONFIG_DIR}/mysql.txt"
}

template_replace() {
    local file="$1"
    shift
    "$RESTAPI_PYTHON" - "$file" "$@" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
pairs = []
for item in sys.argv[2:]:
    key, value = item.split('=', 1)
    pairs.append((key, value))
text = path.read_text(encoding='utf-8')
new = text
for key, value in pairs:
    new = new.replace(key, value)
if new != text:
    path.write_text(new, encoding='utf-8')
PY
}

preflight() {
    require_root
    require_cmd adduser
    require_cmd awk
    require_cmd chmod
    require_cmd chown
    require_cmd cp
    require_cmd date
    require_cmd grep
    require_cmd hostname
    require_cmd install
    require_cmd mysql
    require_cmd mysqladmin
    require_cmd openssl
    require_cmd sed
    require_cmd sudo
    require_cmd "$RESTAPI_PYTHON"
    if bool_true "$SYNCWERK_SETUP_BACKUP_DATABASES"; then
        require_cmd mysqldump
        require_cmd pigz
        require_cmd sha256sum
    fi
    if bool_true "$SYNCWERK_SETUP_CONFIGURE_NGINX"; then
        require_cmd nginx
    fi
    if bool_true "$SYNCWERK_SETUP_COMPILEMESSAGES"; then
        require_cmd msgfmt
    fi
    require_file "$RESTAPI_MANAGE"
    require_file "${RESTAPI_DIR}/sql/latest-restapi.sql"
    require_file "${RESTAPI_DIR}/sql/latest-ccnet.sql"
    require_file "${RESTAPI_DIR}/sql/latest-server.sql"

    "$RESTAPI_PYTHON" - <<'PY'
import importlib.util
import sys
missing = [m for m in ('django', 'MySQLdb', 'drf_yasg', 'six', 'PIL', 'pymemcache') if importlib.util.find_spec(m) is None]
if missing:
    sys.stderr.write('missing Python module(s): %s\n' % ', '.join(missing))
    sys.stderr.write('Install via Debian/Syncwerk packages; setup does not perform Python package bootstrapping or create Python environments.\n')
    sys.exit(1)
PY
}

setup_user() {
    if id -u "$SYNCWERK_USER" >/dev/null 2>&1; then
        log "user ${SYNCWERK_USER} exists"
    else
        adduser --system --gecos "syncwerk" --home "$SYNCWERK_SHARE_DIR" "$SYNCWERK_USER"
    fi
}

prepare_runtime_dirs() {
    local group
    local run_group
    group="$(syncwerk_group)"
    run_group="$(runtime_group)"
    install -d -m 0750 "$CONFIG_DIR" "$RESTAPI_LOG_DIR"
    install -d -m 2750 -o "$SYNCWERK_USER" -g "$run_group" "$RUN_DIR"
    install -d -m 0711 "$OBJECT_STORAGE_PATH"
    install -d -m 0750 "${OBJECT_STORAGE_PATH}/template" "${OBJECT_STORAGE_PATH}/thumbnails"
    install -d -m 0751 "${OBJECT_STORAGE_PATH}/avatars"
    install -d -m 0755 "$SYNCWERK_SHARE_DIR"
    touch "${RESTAPI_LOG_DIR}/restapi.log" "${RESTAPI_LOG_DIR}/restapi-proxy.log" "${RESTAPI_LOG_DIR}/webdav.log" "${RESTAPI_LOG_DIR}/webapp.log" "${RESTAPI_LOG_DIR}/server.log"
    chown -R "$SYNCWERK_USER:$group" "$CONFIG_DIR" "$RESTAPI_LOG_DIR"
    chown "$SYNCWERK_USER:$run_group" "$RUN_DIR"
    chown "$SYNCWERK_USER:$group" "$OBJECT_STORAGE_PATH" "${OBJECT_STORAGE_PATH}/template" "${OBJECT_STORAGE_PATH}/avatars" "${OBJECT_STORAGE_PATH}/thumbnails"
    chmod 0750 "$CONFIG_DIR" "$RESTAPI_LOG_DIR"
    chmod 2750 "$RUN_DIR"
    chmod 0711 "$OBJECT_STORAGE_PATH"
    chmod 0750 "${OBJECT_STORAGE_PATH}/template" "${OBJECT_STORAGE_PATH}/thumbnails"
    chmod 0751 "${OBJECT_STORAGE_PATH}/avatars"
    chmod 0640 "${RESTAPI_LOG_DIR}"/*.log
}

stop_services_safe() {
    if ! bool_true "$SYNCWERK_SETUP_STOP_SERVICES"; then
        return 0
    fi
    log "stopping existing Syncwerk services if present"
    systemctl stop syncwerk-server.service >/dev/null 2>&1 || true
    service syncwerk-server stop >/dev/null 2>&1 || true
    if command -v syncwerk-server >/dev/null 2>&1; then
        syncwerk-server stop >/dev/null 2>&1 || true
    fi
}

start_database() {
    if mysqladmin ping --silent >/dev/null 2>&1; then
        return 0
    fi
    log "starting database service"
    systemctl start mariadb.service >/dev/null 2>&1 || systemctl start mysql.service >/dev/null 2>&1 || service mariadb start >/dev/null 2>&1 || service mysql start >/dev/null 2>&1 || true
    mysqladmin ping --silent >/dev/null 2>&1 || fail "MariaDB/MySQL is not reachable via mysqladmin ping"
}

create_or_update_databases() {
    local password
    if [[ -f "${CONFIG_DIR}/mysql.txt" ]]; then
        password="$(read_mysql_value password)"
        [[ -n "$password" ]] || fail "${CONFIG_DIR}/mysql.txt exists but password is empty"
    else
        password="$(random_alnum 28)"
    fi

    mysql_root <<EOF_SQL
CREATE DATABASE IF NOT EXISTS \`${CCNET_DB}\` CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE IF NOT EXISTS \`${SERVER_DB}\` CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE IF NOT EXISTS \`${RESTAPI_DB}\` CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${password}';
ALTER USER '${DB_USER}'@'localhost' IDENTIFIED BY '${password}';
GRANT ALL PRIVILEGES ON \`${CCNET_DB}\`.* TO '${DB_USER}'@'localhost';
GRANT ALL PRIVILEGES ON \`${SERVER_DB}\`.* TO '${DB_USER}'@'localhost';
GRANT ALL PRIVILEGES ON \`${RESTAPI_DB}\`.* TO '${DB_USER}'@'localhost';
FLUSH PRIVILEGES;
EOF_SQL
    write_mysql_client_file "$password"
    chown "$SYNCWERK_USER:$(syncwerk_group)" "${CONFIG_DIR}/mysql.txt"
    chmod 0600 "${CONFIG_DIR}/mysql.txt"
}

backup_databases_if_existing() {
    if ! bool_true "$SYNCWERK_SETUP_BACKUP_DATABASES"; then
        log "database backup disabled"
        return 0
    fi
    if database_exists "$CCNET_DB" && database_exists "$SERVER_DB" && database_exists "$RESTAPI_DB"; then
        local backup_dir backup_file
        backup_dir="${OBJECT_STORAGE_PATH}/backup"
        backup_file="${backup_dir}/${TIMESTAMP}_syncwerk-databases.sql.gz"
        install -d -m 0750 "$backup_dir"
        log "backing up existing databases to ${backup_file}"
        mysqldump --single-transaction --routines --events --databases "$CCNET_DB" "$SERVER_DB" "$RESTAPI_DB" | pigz >"$backup_file"
        sha256sum "$backup_file" >"${backup_file}.sha256.txt"
    else
        log "one or more Syncwerk databases do not exist yet; skipping database backup"
    fi
}

render_ccnet_conf() {
    local file server_id password
    file="${CONFIG_DIR}/ccnet.conf"
    require_file "$file"
    password="$(read_mysql_value password)"
    if grep -q 'SERVERID\|HOSTNAME\|DBUSER\|DBPASS\|DBNAME' "$file"; then
        server_id="$(random_hex 40)"
        template_replace "$file" \
            "SERVERID=${server_id}" \
            "HOSTNAME=${HOSTNAME}" \
            "DBUSER=${DB_USER}" \
            "DBPASS=${password}" \
            "DBNAME=${CCNET_DB}"
    else
        log "${file} contains no setup placeholders; leaving unchanged"
    fi
}

render_server_conf() {
    local file password
    file="${CONFIG_DIR}/server.conf"
    require_file "$file"
    password="$(read_mysql_value password)"
    if grep -q 'DBUSER\|DBPASS\|DBNAME' "$file"; then
        template_replace "$file" \
            "DBUSER=${DB_USER}" \
            "DBPASS=${password}" \
            "DBNAME=${SERVER_DB}"
    else
        log "${file} contains no setup placeholders; leaving unchanged"
    fi
}

render_restapi_settings() {
    local file secret password
    file="${CONFIG_DIR}/restapi_settings.py"
    require_file "$file"
    password="$(read_mysql_value password)"
    if grep -q 'SECRETKEY\|APIDBNAME\|CCNETDBNAME\|SERVERDBNAME\|DBUSER\|DBPASS\|HOSTNAME' "$file"; then
        secret="$(random_secret_key)"
        template_replace "$file" \
            "SECRETKEY=${secret}" \
            "APIDBNAME=${RESTAPI_DB}" \
            "CCNETDBNAME=${CCNET_DB}" \
            "SERVERDBNAME=${SERVER_DB}" \
            "DBUSER=${DB_USER}" \
            "DBPASS=${password}" \
            "HOSTNAME=${HOSTNAME}"
    else
        log "${file} contains no setup placeholders; leaving unchanged"
    fi
    if grep -q 'default_admin' "$file"; then
        sed -i 's/default_admin/superadmin/g' "$file"
    fi
    grep -q 'sql_mode=STRICT_TRANS_TABLES' "$file" || sed -i 's/storage_engine=INNODB/storage_engine=INNODB, sql_mode=STRICT_TRANS_TABLES/' "$file"
}

setup_my_key_peer() {
    if [[ ! -f "${CONFIG_DIR}/mykey.peer" ]]; then
        openssl genrsa -out "${CONFIG_DIR}/mykey.peer" 2048
    fi
    chown "$SYNCWERK_USER:$(syncwerk_group)" "${CONFIG_DIR}/mykey.peer"
    chmod 0600 "${CONFIG_DIR}/mykey.peer"
}

setup_gunicorn_conf() {
    if [[ -f "${CONFIG_DIR}/gunicorn.conf" ]]; then
        log "${CONFIG_DIR}/gunicorn.conf exists; leaving unchanged"
        return 0
    fi
    cat >"${CONFIG_DIR}/gunicorn.conf" <<'EOF_GUNICORN'
import multiprocessing

daemon = False
workers = multiprocessing.cpu_count()
threads = 5
timeout = 1200
pid = "/run/syncwerk/restapi.pid"
bind = "unix:/run/syncwerk/restapi.sock"
EOF_GUNICORN
    chown "$SYNCWERK_USER:$(syncwerk_group)" "${CONFIG_DIR}/gunicorn.conf"
    chmod 0640 "${CONFIG_DIR}/gunicorn.conf"
}

setup_ccnet_database_minimal() {
    mysql_root <<EOF_SQL
CREATE TABLE IF NOT EXISTS \`${CCNET_DB}\`.\`EmailUser\` (
  \`id\` int(11) NOT NULL AUTO_INCREMENT,
  \`email\` varchar(255) DEFAULT NULL,
  \`passwd\` varchar(256) DEFAULT NULL,
  \`language\` varchar(255) DEFAULT NULL,
  \`is_staff\` tinyint(1) NOT NULL,
  \`is_active\` tinyint(1) NOT NULL,
  \`ctime\` bigint(20) DEFAULT NULL,
  PRIMARY KEY (\`id\`),
  UNIQUE KEY \`email\` (\`email\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
CREATE TABLE IF NOT EXISTS \`${CCNET_DB}\`.\`LDAPUsers\` (
  \`id\` bigint(20) NOT NULL AUTO_INCREMENT,
  \`email\` varchar(255) NOT NULL,
  \`password\` varchar(255) NOT NULL,
  \`language\` varchar(255) DEFAULT NULL,
  \`is_staff\` tinyint(1) NOT NULL,
  \`is_active\` tinyint(1) NOT NULL,
  \`ctime\` bigint(20) DEFAULT NULL,
  \`extra_attrs\` text DEFAULT NULL,
  \`reference_id\` varchar(255) DEFAULT NULL,
  PRIMARY KEY (\`id\`),
  UNIQUE KEY \`email\` (\`email\`),
  UNIQUE KEY \`reference_id\` (\`reference_id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
ALTER TABLE \`${CCNET_DB}\`.\`EmailUser\` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;
ALTER TABLE \`${CCNET_DB}\`.\`LDAPUsers\` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;
EOF_SQL
    normalise_ccnet_duplicate_indexes
}

drop_duplicate_single_column_unique_indexes() {
    local db="$1"
    local table="$2"
    local column="$3"
    local quoted_db quoted_table quoted_column index_name
    quoted_db="$(mysql_quote "$db")"
    quoted_table="$(mysql_quote "$table")"
    quoted_column="$(mysql_quote "$column")"
    mysql_root --batch --skip-column-names <<EOF_SQL | while IFS=$'\t' read -r index_name; do
SELECT INDEX_NAME
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = ${quoted_db}
  AND TABLE_NAME = ${quoted_table}
  AND NON_UNIQUE = 0
GROUP BY INDEX_NAME
HAVING GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ',') = ${quoted_column}
   AND COUNT(*) = 1
ORDER BY CASE WHEN INDEX_NAME = ${quoted_column} THEN 0 ELSE 1 END, INDEX_NAME
LIMIT 18446744073709551615 OFFSET 1;
EOF_SQL
        [[ -n "$index_name" ]] || continue
        [[ "$index_name" =~ ^[A-Za-z0-9_]+$ ]] || fail "refusing to drop unsafe index name ${db}.${table}.${index_name}"
        log "dropping duplicate ${db}.${table}.${column} unique index ${index_name}"
        mysql_root -e "ALTER TABLE \`${db}\`.\`${table}\` DROP INDEX \`${index_name}\`;"
    done
}

normalise_ccnet_duplicate_indexes() {
    drop_duplicate_single_column_unique_indexes "$CCNET_DB" "EmailUser" "reference_id"
    drop_duplicate_single_column_unique_indexes "$CCNET_DB" "LDAPUsers" "reference_id"
}

normalise_nginx_http2_config() {
    local file="${1:-/etc/nginx/conf.d/syncwerk.conf}"
    local backup
    [[ -f "$file" ]] || return 0
    if ! grep -qE '^[[:space:]]*listen[[:space:]]+(\[::\]:)?443[[:space:]]+ssl[[:space:]]+http2;' "$file"; then
        return 0
    fi

    backup="${file}.${TIMESTAMP}.http2.bak"
    cp -a "$file" "$backup"
    "$RESTAPI_PYTHON" - "$file" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
lines = path.read_text(encoding='utf-8').splitlines(keepends=True)
new_lines = []
changed = False

for line in lines:
    stripped = line.strip()
    indent = line[: len(line) - len(line.lstrip())]
    newline = '\n' if line.endswith('\n') else ''
    if stripped == 'listen 443 ssl http2;':
        new_lines.append(f'{indent}listen 443 ssl;{newline}')
        changed = True
    elif stripped == 'listen [::]:443 ssl http2;':
        new_lines.append(f'{indent}listen [::]:443 ssl;{newline}')
        changed = True
    else:
        new_lines.append(line)

if changed and not any(line.strip() == 'http2 on;' for line in new_lines):
    for idx, line in enumerate(new_lines):
        if line.strip() == 'listen 443 ssl;':
            indent = line[: len(line) - len(line.lstrip())]
            newline = '\n' if line.endswith('\n') else ''
            new_lines.insert(idx + 1, f'{indent}http2 on;{newline}')
            break

if changed:
    path.write_text(''.join(new_lines), encoding='utf-8')
PY
    log "normalised deprecated nginx http2 listen directive in ${file}; backup: ${backup}"
}

setup_nginx() {
    if ! bool_true "$SYNCWERK_SETUP_CONFIGURE_NGINX"; then
        log "nginx setup disabled"
        return 0
    fi
    install -d -m 0755 /etc/nginx/conf.d /etc/nginx/ssl
    if [[ ! -f /etc/nginx/ssl/syncwerk.key ]]; then
        openssl genrsa -out /etc/nginx/ssl/syncwerk.key 4096
        chmod 0600 /etc/nginx/ssl/syncwerk.key
    fi
    if [[ ! -f /etc/nginx/ssl/syncwerk.crt ]]; then
        openssl req -new -x509 -sha256 -days 3650 \
            -key /etc/nginx/ssl/syncwerk.key \
            -out /etc/nginx/ssl/syncwerk.crt \
            -subj "/CN=${HOSTNAME}"
    fi
    if [[ ! -f /etc/nginx/conf.d/syncwerk.conf ]]; then
        cat > /etc/nginx/conf.d/syncwerk.conf <<EOF_NGINX
upstream syncwerk-server-restapi {
    server unix:/run/syncwerk/restapi.sock fail_timeout=0;
}

server {
    listen 80;
    listen [::]:80;
    server_name ${HOSTNAME};
    return 301 https://\$http_host\$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name ${HOSTNAME};

    ssl_certificate /etc/nginx/ssl/syncwerk.crt;
    ssl_certificate_key /etc/nginx/ssl/syncwerk.key;

    proxy_set_header X-Forwarded-For \$remote_addr;
    proxy_max_temp_file_size 0;

    location / {
        try_files \$uri \$uri/ /index.html;
        root /usr/share/syncwerk/webapp/;
        access_log ${RESTAPI_LOG_DIR}/webapp.log;
        error_log ${RESTAPI_LOG_DIR}/webapp.log;
    }

    location /notification/list {
        return 301 /notifications;
    }

    location /seafhttp {
        rewrite ^/seafhttp(.*)\$ \$1 break;
        proxy_pass http://127.0.0.1:8082;
        client_max_body_size 0;
        proxy_connect_timeout 36000s;
        proxy_read_timeout 36000s;
        proxy_send_timeout 36000s;
        proxy_request_buffering off;
    }

    location /api2 {
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Host \$http_host;
        proxy_redirect off;
        proxy_pass http://syncwerk-server-restapi;
        client_max_body_size 0;
        access_log ${RESTAPI_LOG_DIR}/restapi.log;
        error_log ${RESTAPI_LOG_DIR}/restapi.log;
    }

    location /api3 {
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Host \$http_host;
        proxy_redirect off;
        proxy_pass http://syncwerk-server-restapi;
        client_max_body_size 0;
        proxy_connect_timeout 120s;
        proxy_read_timeout 120s;
        proxy_send_timeout 120s;
        access_log ${RESTAPI_LOG_DIR}/restapi.log;
        error_log ${RESTAPI_LOG_DIR}/restapi.log;
    }

    location /client-login {
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Host \$http_host;
        proxy_redirect off;
        proxy_pass http://syncwerk-server-restapi;
        client_max_body_size 0;
        access_log ${RESTAPI_LOG_DIR}/restapi.log;
        error_log ${RESTAPI_LOG_DIR}/restapi.log;
    }

    location /media {
        root /usr/share/python/syncwerk/restapi/;
    }

    location /webdav {
        fastcgi_pass 127.0.0.1:8090;
        fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
        fastcgi_param PATH_INFO \$fastcgi_script_name;
        fastcgi_param SERVER_PROTOCOL \$server_protocol;
        fastcgi_param QUERY_STRING \$query_string;
        fastcgi_param REQUEST_METHOD \$request_method;
        fastcgi_param CONTENT_TYPE \$content_type;
        fastcgi_param CONTENT_LENGTH \$content_length;
        fastcgi_param SERVER_ADDR \$server_addr;
        fastcgi_param SERVER_PORT \$server_port;
        fastcgi_param SERVER_NAME \$server_name;
        fastcgi_param REMOTE_ADDR \$remote_addr;
        fastcgi_param HTTPS on;
        client_max_body_size 0;
        access_log ${RESTAPI_LOG_DIR}/webdav.log;
        error_log ${RESTAPI_LOG_DIR}/webdav.log;
    }
}
EOF_NGINX
    else
        normalise_nginx_http2_config /etc/nginx/conf.d/syncwerk.conf
        log "/etc/nginx/conf.d/syncwerk.conf exists; leaving custom content otherwise unchanged"
    fi
    if bool_true "$SYNCWERK_SETUP_RESTART_NGINX"; then
        nginx -t
        systemctl restart nginx.service >/dev/null 2>&1 || service nginx restart
    else
        log "nginx restart disabled; run 'nginx -t' manually if required"
    fi
}

legacy_schema_fixes() {
    log "applying legacy schema compatibility fixes"
    mysql_root --force <<EOF_SQL >/dev/null 2>&1 || true
RENAME TABLE \`${RESTAPI_DB}\`.\`institutions_institution\` TO \`${RESTAPI_DB}\`.\`tenants_tenant\`;
RENAME TABLE \`${RESTAPI_DB}\`.\`institutions_institutionadmin\` TO \`${RESTAPI_DB}\`.\`tenants_tenantadmin\`;
RENAME TABLE \`${RESTAPI_DB}\`.\`institutions_institutionquota\` TO \`${RESTAPI_DB}\`.\`tenants_tenantquota\`;
ALTER TABLE \`${RESTAPI_DB}\`.\`tenants_tenantadmin\` CHANGE \`institution_id\` \`tenant_id\` INT(11) NOT NULL;
ALTER TABLE \`${RESTAPI_DB}\`.\`tenants_tenantquota\` CHANGE \`institution_id\` \`tenant_id\` INT(11) NOT NULL;
ALTER TABLE \`${RESTAPI_DB}\`.\`profile_profile\` CHANGE \`institution\` \`tenant\` VARCHAR(225) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL;
UPDATE \`${RESTAPI_DB}\`.\`django_migrations\` SET \`app\` = Replace(app, 'institution', 'tenant') WHERE \`app\` like "%institution%";
UPDATE \`${RESTAPI_DB}\`.\`django_migrations\` SET \`name\` = Replace(name, 'institution', 'tenant') WHERE \`name\` like "%institution%";
UPDATE \`${RESTAPI_DB}\`.\`auth_permission\` SET \`name\` = Replace(name, 'institution', 'tenant') WHERE \`name\` like "%institution%";
UPDATE \`${RESTAPI_DB}\`.\`auth_permission\` SET \`codename\` = Replace(codename, 'institution', 'tenant') WHERE \`name\` like "%institution%";
ALTER TABLE \`${SERVER_DB}\`.\`SharedRepo\` ADD \`allow_view_history\` BOOLEAN DEFAULT True;
ALTER TABLE \`${SERVER_DB}\`.\`SharedRepo\` ADD \`allow_view_snapshot\` BOOLEAN DEFAULT False;
ALTER TABLE \`${SERVER_DB}\`.\`SharedRepo\` ADD \`allow_restore_snapshot\` BOOLEAN DEFAULT False;
ALTER TABLE \`${RESTAPI_DB}\`.\`AuditLog\` CHANGE COLUMN \`recepient\` \`recipient\` longtext;
ALTER TABLE \`${CCNET_DB}\`.\`EmailUser\` ADD \`language\` varchar(255) AFTER \`passwd\`;
ALTER TABLE \`${CCNET_DB}\`.\`LDAPUsers\` ADD \`language\` varchar(255) AFTER \`password\`;
ALTER TABLE \`${CCNET_DB}\`.\`LDAPUsers\` ADD \`ctime\` BIGINT AFTER \`is_active\`;
EOF_SQL
    mysql_root <<EOF_SQL >/dev/null 2>&1 || true
INSERT INTO \`${RESTAPI_DB}\`.\`django_site\` (\`id\`, \`domain\`, \`name\`)
VALUES (1, 'example.com', 'example.com')
ON DUPLICATE KEY UPDATE \`domain\` = VALUES(\`domain\`), \`name\` = VALUES(\`name\`);
EOF_SQL
    mysql_root <<EOF_SQL >/dev/null 2>&1 || true
INSERT INTO \`${RESTAPI_DB}\`.\`api3_tokenv2\`
SELECT * FROM \`${RESTAPI_DB}\`.\`api2_tokenv2\` AS \`tmp\`
WHERE NOT EXISTS (
    SELECT * FROM \`${RESTAPI_DB}\`.\`api3_tokenv2\`
    WHERE \`${RESTAPI_DB}\`.\`api3_tokenv2\`.user = \`tmp\`.user
      AND \`${RESTAPI_DB}\`.\`api3_tokenv2\`.device_id = \`tmp\`.device_id
);
EOF_SQL
}

schema_changed_or_forced() {
    if bool_true "$SYNCWERK_SETUP_FORCE_MIGRATIONS"; then
        return 0
    fi
    if [[ ! -f /tmp/syncwerk_restapi_sql.md5 ]]; then
        return 0
    fi
    ! md5sum --check /tmp/syncwerk_restapi_sql.md5 >/dev/null 2>&1
}

list_migration_apps() {
    run_restapi_python - <<'PY'
import django
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restapi.settings')
django.setup()
from django.apps import apps
labels = []
for app in apps.get_app_configs():
    mig_dir = os.path.join(app.path, 'migrations')
    if os.path.isdir(mig_dir):
        labels.append(app.label)
for label in sorted(set(labels)):
    print(label)
PY
}

run_django_migrations() {
    if [[ "$SYNCWERK_SETUP_MIGRATION_MODE" == "skip" ]]; then
        log "Django migrations skipped by SYNCWERK_SETUP_MIGRATION_MODE=skip"
        return 0
    fi
    if ! schema_changed_or_forced; then
        log "RESTAPI schema checksum unchanged; skipping Django migrations"
        rm -f /tmp/syncwerk_restapi_sql.md5
        return 0
    fi

    log "running Django migrations mode=${SYNCWERK_SETUP_MIGRATION_MODE}"
    if run_manage migrate --noinput --fake-initial; then
        rm -f /tmp/syncwerk_restapi_sql.md5
        return 0
    fi

    if [[ "$SYNCWERK_SETUP_MIGRATION_MODE" != "legacy-fake" ]]; then
        fail "Django migrations failed in strict mode. Use SYNCWERK_SETUP_MIGRATION_MODE=legacy-fake only for controlled recovery."
    fi

    log "strict migration failed; entering explicit legacy-fake recovery mode"
    local app
    while IFS= read -r app; do
        [[ -n "$app" ]] || continue
        log "migrating app ${app} with fake fallback"
        run_manage migrate "$app" --noinput --fake-initial || run_manage migrate "$app" --fake --noinput
    done < <(list_migration_apps)
    rm -f /tmp/syncwerk_restapi_sql.md5
}

update_database() {
    export_restapi_pythonpath
    log "importing latest RESTAPI SQL schema"
    mysql_force_db "$RESTAPI_DB" < "${RESTAPI_DIR}/sql/latest-restapi.sql"
    legacy_schema_fixes

    log "running api3 migration pre-pass"
    run_manage migrate api3 --noinput --fake-initial >/dev/null 2>&1 || log "api3 migration pre-pass failed; continuing to full migration path"
    run_django_migrations

    log "importing latest ccnet SQL schema"
    mysql_force_db "$CCNET_DB" < "${RESTAPI_DIR}/sql/latest-ccnet.sql" >/dev/null 2>&1
    normalise_ccnet_duplicate_indexes
    log "importing latest server SQL schema"
    mysql_force_db "$SERVER_DB" < "${RESTAPI_DIR}/sql/latest-server.sql" >/dev/null 2>&1
}

harden_avatar_media_permissions() {
    local group target link
    group="$(syncwerk_group)"
    target="${OBJECT_STORAGE_PATH}/avatars"
    link="${RESTAPI_DIR}/media/avatars"

    install -d -m 0711 "$OBJECT_STORAGE_PATH"
    install -d -m 0751 "$target"
    chown "$SYNCWERK_USER:$group" "$OBJECT_STORAGE_PATH" "$target"
    chmod 0711 "$OBJECT_STORAGE_PATH"

    chown -R "$SYNCWERK_USER:$group" "$target"
    find "$target" -type d -exec chmod 0751 {} +
    find "$target" -type f -exec chmod u=rw,go=r {} +
    find "$target" -type l -exec chown -h "$SYNCWERK_USER:$group" {} +

    if [[ -L "$link" ]]; then
        chown -h "$SYNCWERK_USER:$group" "$link"
    fi
}

migrate_avatars() {
    local target link
    target="${OBJECT_STORAGE_PATH}/avatars"
    link="${RESTAPI_DIR}/media/avatars"
    install -d -m 0751 "$target"
    if [[ -L "$link" ]]; then
        log "avatars symlink already exists"
    else
        if [[ -d "$link" ]]; then
            find "$link" -mindepth 1 -maxdepth 1 -exec mv -t "$target" -- {} + 2>/dev/null || true
            rm -rf "$link"
        fi
        ln -s "$target" "$link"
    fi
    harden_avatar_media_permissions
}

fix_permissions() {
    local group
    local run_group
    group="$(syncwerk_group)"
    run_group="$(runtime_group)"
    prepare_runtime_dirs
    chown -R "$SYNCWERK_USER:$group" "$CONFIG_DIR" "$RESTAPI_LOG_DIR"
    chown "$SYNCWERK_USER:$run_group" "$RUN_DIR"
    chmod 2750 "$RUN_DIR"
    if [[ -d "$SYNCWERK_PYTHON_ROOT" ]]; then
        log "leaving package-managed Python runtime ownership unchanged: ${SYNCWERK_PYTHON_ROOT}"
    fi
    if [[ -d "$SYNCWERK_SHARE_DIR" ]]; then
        log "leaving package-managed web/runtime share ownership unchanged: ${SYNCWERK_SHARE_DIR}"
    fi
    if bool_true "$SYNCWERK_SETUP_CHOWN_OBJECT_STORAGE"; then
        log "recursively fixing object-storage ownership under ${OBJECT_STORAGE_PATH}"
        chown -R "$SYNCWERK_USER:$group" "$OBJECT_STORAGE_PATH"
    fi
    harden_avatar_media_permissions
    chmod 0600 "${CONFIG_DIR}/mysql.txt"
}

update_static_files() {
    if bool_true "$SYNCWERK_SETUP_COLLECTSTATIC"; then
        log "collecting static files as root for package-managed RESTAPI runtime"
        run_manage_package_root collectstatic --no-input --verbosity 2
    else
        log "collectstatic disabled by SYNCWERK_SETUP_COLLECTSTATIC=0"
    fi

    if bool_true "$SYNCWERK_SETUP_COMPILEMESSAGES"; then
        log "compiling translations as root for package-managed RESTAPI runtime"
        run_manage_package_root compilemessages --verbosity 2 --locale en
        run_manage_package_root compilemessages --verbosity 2 --locale de
    else
        log "compilemessages disabled by SYNCWERK_SETUP_COMPILEMESSAGES=0"
    fi
}

manage_check() {
    if bool_true "$SYNCWERK_SETUP_RUN_MANAGE_CHECK"; then
        log "running manage.py check"
        run_manage check
    fi
}

legacy_generated_systemd_unit_matches() {
    local unit_file="$1"
    [[ -f "$unit_file" ]] || return 1
    python3 - "$unit_file" <<'PY'
from pathlib import Path
import sys

unit = Path(sys.argv[1])
expected = b"""[Unit]
Description=Syncwerk Server
After=network.target mariadb.service mysql.service

[Service]
Type=simple
ExecStart=/usr/bin/syncwerk-server start --foreground
ExecStop=/usr/bin/syncwerk-server stop

[Install]
WantedBy=multi-user.target
"""
raise SystemExit(0 if unit.read_bytes() == expected else 1)
PY
}

disable_legacy_generated_systemd_shadow_unit() {
    local etc_unit="/etc/systemd/system/syncwerk-server.service"
    local backup_dir backup_file

    [[ -e "$etc_unit" ]] || return 0
    if [[ ! -f "$etc_unit" ]]; then
        fail "${etc_unit} exists but is not a regular file; refusing to mask the package-managed unit"
    fi
    if ! legacy_generated_systemd_unit_matches "$etc_unit"; then
        fail "${etc_unit} exists and is not the known generated Trixie setup unit; move custom service changes into /etc/systemd/system/syncwerk-server.service.d/*.conf before enabling"
    fi

    backup_dir="${CONFIG_DIR%/}/backups/systemd"
    mkdir -p "$backup_dir"
    chmod 0700 "${CONFIG_DIR%/}/backups" "$backup_dir" 2>/dev/null || true
    backup_file="${backup_dir}/syncwerk-server.service.${TIMESTAMP}.disabled"
    mv "$etc_unit" "$backup_file"
    log "disabled legacy generated ${etc_unit}; backup=${backup_file}"
}

setup_service_integration() {
    if [[ -d /run/systemd/system ]]; then
        if [[ ! -f /usr/lib/systemd/system/syncwerk-server.service ]]; then
            fail "package-managed /usr/lib/systemd/system/syncwerk-server.service is missing"
        fi
        disable_legacy_generated_systemd_shadow_unit
        systemctl daemon-reload
        if ! bool_true "$SYNCWERK_SETUP_ENABLE_SERVICE"; then
            log "service enable/write disabled"
            return 0
        fi
        systemctl enable syncwerk-server.service
    else
        log "systemd is not active; skipping service enable"
    fi
}

start_services_optional() {
    if ! bool_true "$SYNCWERK_SETUP_START_SERVICES"; then
        log "Syncwerk service start disabled"
        return 0
    fi
    setup_service_integration
    systemctl start syncwerk-server.service >/dev/null 2>&1 || service syncwerk-server start >/dev/null 2>&1 || syncwerk-server start
}

read_admin_email_from_file() {
    local admin_file="${CONFIG_DIR}/admin.txt"
    [[ -f "$admin_file" ]] || return 1
    "$RESTAPI_PYTHON" - "$admin_file" <<'PY'
import json
import re
import sys
from pathlib import Path

path = Path(sys.argv[1])
text = path.read_text(encoding='utf-8').strip()
email = ''
if text.startswith('{'):
    data = json.loads(text)
    email = str(data.get('email') or '').strip()
else:
    for line in text.splitlines():
        match = re.match(r'^\s*(?:Mail|Email|E-Mail)\s*=\s*(.+?)\s*$', line, re.I)
        if match:
            email = match.group(1).strip()
            break
if not email:
    sys.exit(1)
print(email.lower())
PY
}

ensure_bootstrap_super_admin_role() {
    local admin_user="$1"
    [[ -n "$admin_user" ]] || fail "bootstrap superadmin email is empty"
    run_restapi_python - "$admin_user" <<'PY'
import os
import sys

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restapi.settings')

import django
django.setup()

from restapi.base.accounts import User
from restapi.constants import SUPERADMIN
from restapi.role_permissions.models import AdminRole

email = sys.argv[1].strip().lower()
try:
    user = User.objects.get(email=email)
except User.DoesNotExist:
    raise SystemExit('bootstrap superadmin user does not exist in ccnet: %s' % email)
if not getattr(user, 'is_staff', False):
    raise SystemExit('bootstrap superadmin user is not marked as staff in ccnet: %s' % email)

_, created = AdminRole.objects.update_or_create(
    email=email,
    defaults={'role': SUPERADMIN},
)
print('RESTAPI AdminRole ensured: email=%s role=%s created=%s' % (email, SUPERADMIN, created))
PY
}

create_super_admin_optional() {
    if ! bool_true "$SYNCWERK_SETUP_CREATE_SUPER_ADMIN"; then
        log "bootstrap superadmin creation disabled"
        return 0
    fi
    if [[ -f "${CONFIG_DIR}/admin.txt" ]]; then
        local existing_admin_user
        existing_admin_user="$(read_admin_email_from_file)" || fail "${CONFIG_DIR}/admin.txt exists but no admin email could be parsed"
        log "${CONFIG_DIR}/admin.txt exists; not creating a new bootstrap superadmin credential"
        ensure_bootstrap_super_admin_role "$existing_admin_user"
        return 0
    fi
    local admin_user admin_password tmp
    admin_user="admin@${HOSTNAME}"
    admin_password="$(random_alnum 28)"
    tmp="$(mktemp "${CONFIG_DIR}/admin.txt.XXXXXX")"
    cat >"$tmp" <<EOF_ADMIN_JSON
{
  "email": "${admin_user}",
  "password": "${admin_password}"
}
EOF_ADMIN_JSON
    chmod 0600 "$tmp"
    chown "$SYNCWERK_USER:$(syncwerk_group)" "$tmp"
    mv "$tmp" "${CONFIG_DIR}/admin.txt"
    run_restapi_python "${RESTAPI_DIR}/restapi/create-admin.py"
    ensure_bootstrap_super_admin_role "$admin_user"
    cat >"${CONFIG_DIR}/admin.txt" <<EOF_ADMIN_TXT
Mail = ${admin_user}
Pass = ${admin_password}
EOF_ADMIN_TXT
    chmod 0600 "${CONFIG_DIR}/admin.txt"
    chown "$SYNCWERK_USER:$(syncwerk_group)" "${CONFIG_DIR}/admin.txt"
    log "bootstrap superadmin credentials written to ${CONFIG_DIR}/admin.txt"
}

main() {
    parse_args "$@"
    log "starting dedicated Trixie setup"
    log "host=${HOSTNAME} migration_mode=${SYNCWERK_SETUP_MIGRATION_MODE} start_services=${SYNCWERK_SETUP_START_SERVICES} create_super_admin=${SYNCWERK_SETUP_CREATE_SUPER_ADMIN}"
    preflight
    setup_user
    prepare_runtime_dirs
    stop_services_safe
    start_database
    backup_databases_if_existing
    create_or_update_databases
    render_ccnet_conf
    setup_my_key_peer
    setup_ccnet_database_minimal
    render_server_conf
    setup_gunicorn_conf
    render_restapi_settings
    setup_nginx
    update_database
    migrate_avatars
    fix_permissions
    update_static_files
    manage_check
    setup_service_integration
    start_services_optional
    create_super_admin_optional
    log "setup completed"
}

main "$@"
