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

ORIGINAL_ARGS=("$@")

APT_BASE_URL="https://deb.syncwerk.de/syncwerk-server"
INSTALL_PAGE_URL="https://deb.syncwerk.de/install/"
KEY_URL="https://deb.syncwerk.de/keys/syncwerk-archive-key.asc"
EXPECTED_KEY_FINGERPRINT="A2CFF83F2E299853AE57BB0502697A88981677E8"
KEYRING="/etc/apt/keyrings/syncwerk-archive-keyring.gpg"
SOURCE_LIST="/etc/apt/sources.list.d/syncwerk-server.list"
LOG_FILE="/var/log/syncwerk/bootstrap-installer.log"

CHANNEL=""
SUITE="trixie"
YES=0
DRY_RUN=0
NO_START=0
PACKAGE_NAME="syncwerk-server-admin"
CONFIGURE_NGINX=1
RELOAD_NGINX=1
SERVER_NAMES=""
LETSENCRYPT=0
LETSENCRYPT_DOMAIN=""
LETSENCRYPT_EMAIL=""
LETSENCRYPT_STAGING=0
LETSENCRYPT_DRY_RUN=0
CREATE_ADMIN=1

log() {
  printf '[%s] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"
}

fail() {
  printf '[%s] ERROR: %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*" >&2
  exit 1
}

usage() {
  cat <<'USAGE'
Syncwerk Server bootstrap installer for Debian 13 Trixie.

Usage:
  syncwerk-server.sh --channel stable|testing|unstable [options]

Options:
  --channel stable|testing|unstable
      Select the Syncwerk repository channel.

  --suite trixie
      Debian suite. Only trixie is supported by this installer.

  --yes
      Continue non-interactively when an existing Syncwerk source/package is detected.

  --dry-run
      Print the actions without changing the system.

  --no-start
      Install packages but do not start syncwerk-server.service.

  --create-admin
      Create an initial admin account if /etc/syncwerk/admin.txt does not exist.
      Default unless --no-start is used.

  --no-create-admin
      Do not create an initial admin account.

  --server-name <name>
      nginx server_name value for Syncwerk. If omitted, setup uses hostname -f
      plus detected IPv4 addresses. Use quotes for multiple names.

  --skip-nginx
      Do not install or configure nginx. Not recommended for a normal server.

  --no-nginx-reload
      Render and test nginx configuration but do not reload/restart nginx.

  --letsencrypt
      Request a Let's Encrypt certificate after nginx setup. Requires --domain.

  --domain <fqdn>
      Public DNS name for nginx and Let's Encrypt HTTP-01 validation.

  --email <address>
      Let's Encrypt registration and expiry-notification address.

  --letsencrypt-staging
      Use the Let's Encrypt staging CA.

  --letsencrypt-dry-run
      Validate the Let's Encrypt renewal flow without replacing the certificate.

  --package <name>
      Install a different package. Default: syncwerk-server-admin.

  --help
      Show this help text.

Quick start:
  curl -fsSL https://deb.syncwerk.de/install/syncwerk-server.sh | bash -s -- --channel stable

Verified installation:
  curl -fsSL https://deb.syncwerk.de/install/syncwerk-server.sh -o syncwerk-server-install.sh
  curl -fsSL https://deb.syncwerk.de/install/syncwerk-server.sh.sha256 -o syncwerk-server-install.sh.sha256
  sha256sum -c syncwerk-server-install.sh.sha256
  less syncwerk-server-install.sh
  bash syncwerk-server-install.sh --channel stable
USAGE
}

run() {
  if [[ "$DRY_RUN" == "1" ]]; then
    printf 'DRY-RUN:'
    printf ' %q' "$@"
    printf '\n'
    return 0
  fi

  "$@"
}

confirm_if_needed() {
  local reason="$1"

  [[ "$DRY_RUN" == "1" ]] && return 0
  [[ "$YES" == "1" ]] && return 0

  if [[ ! -t 0 ]]; then
    fail "${reason}; rerun with --yes to continue non-interactively"
  fi

  printf '%s Continue? [y/N] ' "$reason" >&2
  local answer
  read -r answer
  case "$answer" in
    y|Y|yes|YES)
      return 0
      ;;
    *)
      fail "aborted by user"
      ;;
  esac
}

require_command() {
  local cmd="$1"
  command -v "$cmd" >/dev/null 2>&1 || fail "required command not found: ${cmd}"
}

have_command() {
  command -v "$1" >/dev/null 2>&1
}

parse_args() {
  while [[ "$#" -gt 0 ]]; do
    case "$1" in
      --channel)
        [[ "$#" -ge 2 ]] || fail "--channel requires an argument"
        CHANNEL="$2"
        shift 2
        ;;
      --suite)
        [[ "$#" -ge 2 ]] || fail "--suite requires an argument"
        SUITE="$2"
        shift 2
        ;;
      --yes)
        YES=1
        shift
        ;;
      --dry-run)
        DRY_RUN=1
        shift
        ;;
      --no-start)
        NO_START=1
        shift
        ;;
      --create-admin)
        CREATE_ADMIN=1
        shift
        ;;
      --no-create-admin)
        CREATE_ADMIN=0
        shift
        ;;
      --server-name)
        [[ "$#" -ge 2 ]] || fail "--server-name requires an argument"
        SERVER_NAMES="$2"
        shift 2
        ;;
      --skip-nginx)
        CONFIGURE_NGINX=0
        shift
        ;;
      --no-nginx-reload)
        RELOAD_NGINX=0
        shift
        ;;
      --letsencrypt)
        LETSENCRYPT=1
        shift
        ;;
      --domain)
        [[ "$#" -ge 2 ]] || fail "--domain requires an argument"
        LETSENCRYPT_DOMAIN="$2"
        [[ -z "$SERVER_NAMES" ]] && SERVER_NAMES="$2"
        shift 2
        ;;
      --email)
        [[ "$#" -ge 2 ]] || fail "--email requires an argument"
        LETSENCRYPT_EMAIL="$2"
        shift 2
        ;;
      --letsencrypt-staging)
        LETSENCRYPT_STAGING=1
        shift
        ;;
      --letsencrypt-dry-run)
        LETSENCRYPT_DRY_RUN=1
        shift
        ;;
      --package)
        [[ "$#" -ge 2 ]] || fail "--package requires an argument"
        PACKAGE_NAME="$2"
        shift 2
        ;;
      --help|-h)
        usage
        exit 0
        ;;
      *)
        fail "unknown argument: $1"
        ;;
    esac
  done
}

distribution_for_channel() {
  case "$CHANNEL" in
    unstable)
      printf '%s-unstable\n' "$SUITE"
      ;;
    testing)
      printf '%s-testing\n' "$SUITE"
      ;;
    stable)
      printf '%s-stable\n' "$SUITE"
      ;;
    "")
      fail "--channel is required; use --channel stable, testing or unstable"
      ;;
    *)
      fail "invalid channel '${CHANNEL}'; expected stable, testing or unstable"
      ;;
  esac
}

setup_logging() {
  if [[ "$DRY_RUN" == "1" ]]; then
    log "dry-run active; not writing ${LOG_FILE}"
    return 0
  fi

  install -d -m 0755 /var/log/syncwerk
  touch "$LOG_FILE"
  chmod 0640 "$LOG_FILE"
  exec > >(tee -a "$LOG_FILE") 2>&1
}

maybe_reexec_with_sudo() {
  if [[ "$EUID" -eq 0 ]]; then
    return 0
  fi

  if [[ "$DRY_RUN" == "1" ]]; then
    log "not running as root; continuing because --dry-run was used"
    return 0
  fi

  local script_path="${BASH_SOURCE[0]}"
  if have_command sudo && [[ -r "$script_path" && "$script_path" != "bash" ]]; then
    exec sudo -E bash "$script_path" "${ORIGINAL_ARGS[@]}"
  fi

  fail "run as root, use sudo with a downloaded script, or pipe into sudo bash"
}

validate_selection() {
  [[ "$SUITE" == "trixie" ]] || fail "unsupported suite '${SUITE}'; only trixie is supported"
  distribution_for_channel >/dev/null

  if [[ "$LETSENCRYPT" == "1" ]]; then
    [[ "$CONFIGURE_NGINX" == "1" ]] || fail "--letsencrypt requires nginx setup; remove --skip-nginx"
    [[ -n "$LETSENCRYPT_DOMAIN" ]] || fail "--letsencrypt requires --domain <fqdn>"
    case "$LETSENCRYPT_DOMAIN" in
      *:*) fail "Let's Encrypt domain must be a DNS name, not an IPv6 address" ;;
    esac
    if [[ "$LETSENCRYPT_DOMAIN" =~ ^[0-9]+(\.[0-9]+){3}$ ]]; then
      fail "Let's Encrypt domain must be a DNS name, not an IPv4 address"
    fi
  fi
}

preflight_system() {
  require_command apt-get
  require_command dpkg
  require_command dpkg-query
  require_command uname

  local arch
  arch="$(dpkg --print-architecture)"
  [[ "$arch" == "amd64" ]] || fail "unsupported architecture '${arch}'; only amd64 is supported"

  [[ -r /etc/os-release ]] || fail "/etc/os-release not found"
  # shellcheck disable=SC1091
  source /etc/os-release
  [[ "${ID:-}" == "debian" ]] || fail "unsupported OS '${ID:-unknown}'; Debian is required"
  [[ "${VERSION_ID:-}" == "13" ]] || fail "unsupported Debian version '${VERSION_ID:-unknown}'; Debian 13 is required"
  [[ "${VERSION_CODENAME:-}" == "trixie" ]] || fail "unsupported Debian codename '${VERSION_CODENAME:-unknown}'; trixie is required"

  require_command systemctl
  [[ -d /run/systemd/system ]] || fail "systemd is not active; this installer requires systemd"
}

ensure_prerequisites() {
  local missing=()

  have_command curl || missing+=("curl")
  have_command gpg || missing+=("gnupg")

  if ! dpkg-query -W ca-certificates >/dev/null 2>&1; then
    missing+=("ca-certificates")
  fi
  if [[ "$CONFIGURE_NGINX" == "1" ]] && ! dpkg-query -W nginx >/dev/null 2>&1; then
    missing+=("nginx")
  fi

  if [[ "${#missing[@]}" -gt 0 ]]; then
    log "installing prerequisites: ${missing[*]}"
    run apt-get update
    run apt-get install -y "${missing[@]}"
  fi

  if ! have_command curl && ! have_command wget; then
    fail "curl or wget is required"
  fi
  require_command gpg
}

download_to() {
  local url="$1"
  local destination="$2"

  if have_command curl; then
    run curl -fsSL "$url" -o "$destination"
  else
    run wget -qO "$destination" "$url"
  fi
}

network_preflight() {
  local distribution="$1"
  local release_url="${APT_BASE_URL}/dists/${distribution}/InRelease"

  if [[ "$DRY_RUN" == "1" ]]; then
    log "would check network access to ${release_url}"
    return 0
  fi

  if have_command curl; then
    curl -fsSI "$KEY_URL" >/dev/null
    curl -fsSI "$release_url" >/dev/null
  else
    wget --spider -q "$KEY_URL"
    wget --spider -q "$release_url"
  fi
}

install_keyring() {
  if [[ "$DRY_RUN" == "1" ]]; then
    log "would install key ${KEY_URL} to ${KEYRING}"
    log "expected fingerprint ${EXPECTED_KEY_FINGERPRINT}"
    return 0
  fi

  local tmpdir
  tmpdir="$(mktemp -d /tmp/syncwerk-installer-key.XXXXXX)"
  local key_file="${tmpdir}/syncwerk-archive-key.asc"
  local fingerprint

  download_to "$KEY_URL" "$key_file"
  fingerprint="$(gpg --batch --with-colons --show-keys "$key_file" | awk -F: '$1 == "fpr" { print $10; exit }')"
  [[ "$fingerprint" == "$EXPECTED_KEY_FINGERPRINT" ]] \
    || fail "unexpected repository key fingerprint '${fingerprint}', expected '${EXPECTED_KEY_FINGERPRINT}'"

  install -d -m 0755 /etc/apt/keyrings
  gpg --batch --yes --dearmor -o "$KEYRING" "$key_file"
  chmod 0644 "$KEYRING"
  rm -f "$key_file"
  rmdir "$tmpdir"
}

source_content() {
  local distribution="$1"
  printf 'deb [arch=amd64 signed-by=%s] %s %s main non-free\n' "$KEYRING" "$APT_BASE_URL" "$distribution"
}

detect_existing_syncwerk_sources() {
  local matches
  matches="$(grep -Rns 'deb\.syncwerk\.de' /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null || true)"
  if [[ -n "$matches" ]]; then
    log "existing Syncwerk APT source references detected:"
    printf '%s\n' "$matches"
  fi
}

write_source_list() {
  local distribution="$1"
  local content
  content="$(source_content "$distribution")"

  if [[ -f "$SOURCE_LIST" ]]; then
    if printf '%s\n' "$content" | cmp -s - "$SOURCE_LIST"; then
      log "APT source already up to date: ${SOURCE_LIST}"
      return 0
    fi
    confirm_if_needed "existing ${SOURCE_LIST} differs from requested Syncwerk source"
    local backup="${SOURCE_LIST}.$(date -u '+%Y%m%dT%H%M%SZ').bak"
    run cp -a "$SOURCE_LIST" "$backup"
    log "backed up existing source file to ${backup}"
  fi

  if [[ "$DRY_RUN" == "1" ]]; then
    log "would write ${SOURCE_LIST}: ${content}"
    return 0
  fi

  printf '%s\n' "$content" > "$SOURCE_LIST"
  chmod 0644 "$SOURCE_LIST"
}

detect_existing_packages() {
  local packages
  packages="$(dpkg-query -W -f='${binary:Package}\t${Version}\t${Status}\n' 'syncwerk*' 2>/dev/null | awk '$3 == "install" { print }' || true)"
  if [[ -n "$packages" ]]; then
    log "installed Syncwerk packages detected:"
    printf '%s\n' "$packages"
    confirm_if_needed "Syncwerk packages are already installed; apt may upgrade or keep them"
  fi
}

install_package() {
  if [[ "$CONFIGURE_NGINX" == "1" ]]; then
    export SYNCWERK_SETUP_CONFIGURE_NGINX=1
    export SYNCWERK_SETUP_RESTART_NGINX="$RELOAD_NGINX"
    if [[ -n "$SERVER_NAMES" ]]; then
      export SYNCWERK_NGINX_SERVER_NAMES="$SERVER_NAMES"
    fi
  else
    export SYNCWERK_SETUP_CONFIGURE_NGINX=0
    export SYNCWERK_SETUP_RESTART_NGINX=0
  fi

  if [[ "$NO_START" == "1" ]]; then
    export SYNCWERK_SETUP_START_SERVICES=0
    export SYNCWERK_SETUP_ENABLE_SERVICE=0
    export SYNCWERK_SETUP_CREATE_ADMIN=0
  else
    export SYNCWERK_SETUP_START_SERVICES=1
    export SYNCWERK_SETUP_ENABLE_SERVICE=1
    export SYNCWERK_SETUP_CREATE_ADMIN="$CREATE_ADMIN"
  fi

  run apt-get update
  run apt-get install -y "$PACKAGE_NAME"
}

ensure_admin_account() {
  if [[ "$CREATE_ADMIN" != "1" ]]; then
    log "initial admin creation disabled by --no-create-admin"
    return 0
  fi
  if [[ "$NO_START" == "1" ]]; then
    log "--no-start used; initial admin creation skipped because Syncwerk runtime services are required"
    return 0
  fi
  if [[ "$DRY_RUN" == "1" ]]; then
    log "would create initial admin account if /etc/syncwerk/admin.txt does not exist"
    return 0
  fi
  if [[ -f /etc/syncwerk/admin.txt ]]; then
    log "/etc/syncwerk/admin.txt exists; not creating a new admin credential"
    return 0
  fi

  require_command syncwerk-server-admin
  local args=(setup --start --enable-service --create-admin)
  if [[ "$CONFIGURE_NGINX" != "1" ]]; then
    args+=(--skip-nginx)
  fi
  if [[ "$RELOAD_NGINX" == "1" ]]; then
    args+=(--restart-nginx)
  else
    args+=(--no-restart-nginx)
  fi

  export SYNCWERK_SETUP_BACKUP_DATABASES=0
  run syncwerk-server-admin "${args[@]}"
  [[ -f /etc/syncwerk/admin.txt ]] || fail "admin creation completed without creating /etc/syncwerk/admin.txt"
  chmod 0600 /etc/syncwerk/admin.txt
  log "initial admin credentials written to /etc/syncwerk/admin.txt"
}

handle_nginx() {
  if [[ "$CONFIGURE_NGINX" != "1" ]]; then
    log "nginx setup skipped by --skip-nginx"
    return 0
  fi

  if [[ "$DRY_RUN" == "1" ]]; then
    log "would validate /etc/nginx/conf.d/syncwerk.conf with nginx -t"
    if [[ "$RELOAD_NGINX" == "1" ]]; then
      log "would reload or restart nginx"
    fi
    return 0
  fi

  require_command nginx
  if [[ ! -f /etc/nginx/conf.d/syncwerk.conf ]]; then
    fail "/etc/nginx/conf.d/syncwerk.conf was not created by ${PACKAGE_NAME}"
  fi
  nginx -t
  if [[ "$RELOAD_NGINX" == "1" ]]; then
    systemctl reload nginx.service >/dev/null 2>&1 || systemctl restart nginx.service >/dev/null 2>&1 || service nginx reload
  else
    log "--no-nginx-reload used; nginx was tested but not reloaded"
  fi
  systemctl --no-pager --full status nginx.service || true
}

handle_letsencrypt() {
  if [[ "$LETSENCRYPT" != "1" ]]; then
    return 0
  fi

  if [[ "$DRY_RUN" == "1" ]]; then
    log "would request Let's Encrypt certificate for ${LETSENCRYPT_DOMAIN}"
    return 0
  fi

  require_command syncwerk-server-admin

  local args=(setup-letsencrypt-certificate --domain "$LETSENCRYPT_DOMAIN")
  if [[ -n "$LETSENCRYPT_EMAIL" ]]; then
    args+=(--email "$LETSENCRYPT_EMAIL")
  else
    args+=(--no-email)
  fi
  [[ "$LETSENCRYPT_STAGING" == "1" ]] && args+=(--staging)
  [[ "$LETSENCRYPT_DRY_RUN" == "1" ]] && args+=(--dry-run)
  [[ "$RELOAD_NGINX" != "1" ]] && args+=(--no-restart-nginx)

  run syncwerk-server-admin "${args[@]}"
}

show_package_state() {
  dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\t${Status}\n' 'syncwerk*' 2>/dev/null \
    | awk '$NF == "installed" { print }' || true
}

handle_service() {
  if [[ "$DRY_RUN" == "1" ]]; then
    log "would inspect syncwerk-server.service and start it unless --no-start was used"
    return 0
  fi

  if ! systemctl list-unit-files syncwerk-server.service >/dev/null 2>&1; then
    log "syncwerk-server.service not found after package installation"
    return 0
  fi

  if [[ "$NO_START" == "1" ]]; then
    log "--no-start used; not starting syncwerk-server.service"
    systemctl --no-pager --full status syncwerk-server.service || true
    return 0
  fi

  systemctl enable syncwerk-server.service
  if ! systemctl start syncwerk-server.service; then
    systemctl --no-pager --full status syncwerk-server.service || true
    fail "syncwerk-server.service did not start successfully"
  fi
  systemctl --no-pager --full status syncwerk-server.service || true
}

print_summary() {
  local distribution="$1"
  local effective_create_admin="$CREATE_ADMIN"
  if [[ "$NO_START" == "1" ]]; then
    effective_create_admin=0
  fi

  cat <<SUMMARY

Syncwerk bootstrap summary
==========================
Channel:        ${CHANNEL}
Debian suite:   ${SUITE}
APT base URL:   ${APT_BASE_URL}
Distribution:   ${distribution}
Components:     main non-free
Source file:    ${SOURCE_LIST}
Keyring:        ${KEYRING}
Package:        ${PACKAGE_NAME}
nginx setup:    ${CONFIGURE_NGINX}
nginx reload:   ${RELOAD_NGINX}
server_name:    ${SERVER_NAMES:-auto}
Let's Encrypt:  ${LETSENCRYPT}
Create admin:   ${effective_create_admin}
Admin file:     /etc/syncwerk/admin.txt
Installer log:  ${LOG_FILE}
Install page:   ${INSTALL_PAGE_URL}

Installed packages:
SUMMARY
  show_package_state

  cat <<'SUMMARY'

Useful next checks:
  systemctl status syncwerk-server
  systemctl status nginx
  nginx -t
  dpkg -l '*syncwerk*'
  syncwerk-server version
  sudo cat /etc/syncwerk/admin.txt
  less /var/log/syncwerk/bootstrap-installer.log

Remove or change repository channel:
  Back up or remove /etc/apt/sources.list.d/syncwerk-server.list, then run apt-get update.
  To change channel later, rerun this installer with the desired published --channel.
SUMMARY
}

main() {
  parse_args "$@"
  validate_selection
  maybe_reexec_with_sudo
  setup_logging

  local distribution
  distribution="$(distribution_for_channel)"

  log "starting Syncwerk Server bootstrap installer"
  preflight_system
  ensure_prerequisites
  network_preflight "$distribution"
  detect_existing_syncwerk_sources
  detect_existing_packages
  install_keyring
  write_source_list "$distribution"
  install_package
  handle_nginx
  handle_letsencrypt
  handle_service
  ensure_admin_account
  print_summary "$distribution"
}

main "$@"
