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

# A4W Auftrag4Works - Linux/Plesk Update-Skript
# Version: 2026-06-12-1
#
# Nutzung:
#   sudo /bin/bash ./update-a4w-linux.sh --yes "/pfad/A4W-Auftrag4Works-Linux-Server-0.1.1.30.zip"
#   sudo /bin/bash ./update-a4w-linux.sh              # sucht neuestes Linux-ZIP im a4w-data/Updates-Ordner
#   sudo /bin/bash ./update-a4w-linux.sh --status     # zeigt Diagnose zum aktuellen Stand
#   sudo /bin/bash ./a4w-maintenance.sh               # Wartungsmenue fuer Backup, Restore, Passwort-Reset

SERVICE_NAME_DEFAULT="a4w"
APP_DIR_DEFAULT="/var/www/vhosts/auftrag4works.de/demo.auftrag4works.de"
BACKUP_ROOT_DEFAULT="/var/backups/a4w"
ROOT_UPDATE_HELPER="/usr/local/sbin/a4w-update"
DLL_NAME="A4W Auftrag4Works.Web.dll"
FALLBACK_DLL_NAME="AuftragoBusiness.dll"
SCRIPT_VERSION="2026-06-13-1"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_PATH="$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}")"
STAMP="$(date +%Y%m%d-%H%M%S)"
LOG_FILE=""
TMP_DIR=""
ASSUME_YES=0
ALLOW_SYSTEMD_RUN=1

log() { echo -e "\033[1;32m[OK]\033[0m $*" | tee -a "${LOG_FILE:-/dev/null}"; }
info() { echo -e "\033[1;34m[INFO]\033[0m $*" | tee -a "${LOG_FILE:-/dev/null}"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*" | tee -a "${LOG_FILE:-/dev/null}"; }
err() { echo -e "\033[1;31m[FEHLER]\033[0m $*" | tee -a "${LOG_FILE:-/dev/null}" >&2; }

usage() {
  cat <<EOF
A4W Auftrag4Works Linux-Updater $SCRIPT_VERSION

Nutzung:
  sudo /bin/bash $0 [--yes] [Update-ZIP]
  sudo /bin/bash $0 --status [Installationsverzeichnis]
  sudo $0 --help

Ohne Update-ZIP sucht das Skript im Datenordner automatisch nach dem neuesten
A4W-Auftrag4Works-Linux-Server-*.zip. Windows-Update-Payloads werden nur als
Fallback erkannt und mit Warnung behandelt.

Optionen:
  --yes, -y              Defaults ohne Rueckfrage verwenden
  --direct               Update direkt in diesem Prozess ausfuehren
  --no-systemd-run       Alias fuer --direct
EOF
}

need_root() {
  if [[ "${EUID}" -ne 0 ]]; then
    err "Bitte als root oder mit sudo ausfuehren."
    exit 1
  fi
}

ask() {
  local prompt="$1"
  local default="$2"
  local value
  if [[ "$ASSUME_YES" -eq 1 || ! -t 0 ]]; then
    echo "$default"
    return
  fi
  read -r -p "$prompt [$default]: " value
  echo "${value:-$default}"
}

parse_global_args() {
  POSITIONAL_ARGS=()
  local arg
  for arg in "$@"; do
    case "$arg" in
      --yes|-y|--non-interactive)
        ASSUME_YES=1
        ;;
      --direct|--no-systemd-run)
        ALLOW_SYSTEMD_RUN=0
        ;;
      *)
        POSITIONAL_ARGS+=("$arg")
        ;;
    esac
  done
}

maybe_spawn_transient_update() {
  if [[ "$ALLOW_SYSTEMD_RUN" -ne 1 || "${A4W_UPDATE_TRANSIENT_CHILD:-0}" == "1" ]]; then
    return 1
  fi
  if [[ -t 0 ]]; then
    return 1
  fi
  if ! command -v systemd-run >/dev/null 2>&1; then
    err "systemd-run nicht gefunden. Web-gestartetes Update wird nicht direkt ausgefuehrt, damit der Dienst nicht im gestoppten Zustand haengen bleibt. Bitte per SSH mit --direct ausfuehren."
    exit 1
  fi

  local unit="a4w-update-${STAMP}"
  local child_args=(--direct --yes)
  child_args+=("${POSITIONAL_ARGS[@]}")

  info "Starte Update als eigenen systemd-Job ${unit}.service, damit es beim Stoppen von ${SERVICE_NAME_DEFAULT} nicht beendet wird."
  if systemd-run \
    --unit="$unit" \
    --description="A4W Auftrag4Works Update" \
    --collect \
    --setenv=A4W_UPDATE_TRANSIENT_CHILD=1 \
    /bin/bash "$SCRIPT_PATH" "${child_args[@]}"; then
    echo "Update-Job gestartet: ${unit}.service"
    echo "Status: journalctl -u ${unit}.service -n 120 --no-pager"
    return 0
  fi

  err "systemd-run konnte den Update-Job nicht starten. Web-gestartetes Update wurde vor dem Dienststopp abgebrochen. Bitte per SSH mit --direct ausfuehren."
  exit 1
}

quote() {
  printf "'%s'" "${1//\'/\'\\\'\'}"
}

default_app_dir_for_zip() {
  local update_zip="$1"
  if [[ "$update_zip" == */a4w-data/Updates/* ]]; then
    echo "${update_zip%%/a4w-data/Updates/*}"
    return
  fi
  echo "$APP_DIR_DEFAULT"
}

refresh_root_update_helper() {
  local app_dir="$1"
  local updated_script="$app_dir/update-a4w-linux.sh"
  if [[ "$SCRIPT_PATH" != "$ROOT_UPDATE_HELPER" || ! -f "$updated_script" ]]; then
    return
  fi

  if cp -a "$updated_script" "$ROOT_UPDATE_HELPER.next" \
    && chown root:root "$ROOT_UPDATE_HELPER.next" \
    && chmod 750 "$ROOT_UPDATE_HELPER.next" \
    && mv -f "$ROOT_UPDATE_HELPER.next" "$ROOT_UPDATE_HELPER"; then
    log "Root-Update-Helper aktualisiert: $ROOT_UPDATE_HELPER"
  else
    rm -f "$ROOT_UPDATE_HELPER.next" || true
    warn "Root-Update-Helper konnte nicht aktualisiert werden. Dienststart wird trotzdem fortgesetzt."
  fi
}

require_command() {
  local name="$1"
  local hint="$2"
  if ! command -v "$name" >/dev/null 2>&1; then
    err "Benötigtes Kommando fehlt: $name. $hint"
    exit 1
  fi
}

check_update_prerequisites() {
  require_command systemctl "Bitte systemd/systemctl auf dem Zielserver bereitstellen."
  require_command rsync "Bitte rsync installieren, z. B. apt-get install rsync."
  require_command sha256sum "Bitte coreutils installieren."
  if ! command -v python3 >/dev/null 2>&1 && ! command -v unzip >/dev/null 2>&1; then
    err "Weder python3 noch unzip gefunden. Mindestens eines davon wird zum sicheren Entpacken benötigt."
    exit 1
  fi
}

cleanup() {
  if [[ -n "${TMP_DIR:-}" && -d "$TMP_DIR" ]]; then
    rm -rf "$TMP_DIR"
  fi
}
trap cleanup EXIT

service_file_path() {
  local service_name="$1"
  if [[ -f "/etc/systemd/system/${service_name}.service" ]]; then
    echo "/etc/systemd/system/${service_name}.service"
    return
  fi
  systemctl show -p FragmentPath --value "${service_name}.service" 2>/dev/null || true
}

read_service_value() {
  local service_name="$1"
  local key="$2"
  local file
  file="$(service_file_path "$service_name")"
  if [[ -n "$file" && -f "$file" ]]; then
    grep -E "^${key}=" "$file" | tail -n 1 | cut -d= -f2- || true
  fi
}

detect_service_user_group() {
  local app_dir="$1"
  local service_name="$2"
  local user group

  user="$(read_service_value "$service_name" "User")"
  group="$(read_service_value "$service_name" "Group")"

  if [[ -z "$user" ]]; then
    user="$(stat -c '%U' "$app_dir" 2>/dev/null || echo "www-data")"
  fi
  if [[ "$user" == "root" || -z "$user" ]]; then
    user="www-data"
  fi

  if ! id "$user" >/dev/null 2>&1; then
    warn "Dienst-/Dateibenutzer '$user' existiert nicht. Fallback auf www-data."
    user="www-data"
  fi

  if [[ -z "$group" ]]; then
    group="$(id -gn "$user" 2>/dev/null || stat -c '%G' "$app_dir" 2>/dev/null || echo "www-data")"
  fi
  if ! getent group "$group" >/dev/null 2>&1; then
    warn "Gruppe '$group' existiert nicht. Verwende primaere Gruppe von $user."
    group="$(id -gn "$user")"
  fi

  echo "$user:$group"
}

find_app_dll() {
  local dir="$1"
  if [[ -f "$dir/$DLL_NAME" ]]; then
    echo "$dir/$DLL_NAME"
    return
  fi
  if [[ -f "$dir/$FALLBACK_DLL_NAME" ]]; then
    echo "$dir/$FALLBACK_DLL_NAME"
    return
  fi
  find "$dir" -maxdepth 1 -type f -name '*.dll' | sort | head -n 1
}

sha_file() {
  local file="$1"
  if [[ -f "$file" ]]; then
    sha256sum "$file" | awk '{print $1}'
  else
    echo "-"
  fi
}

file_summary() {
  local file="$1"
  if [[ -f "$file" ]]; then
    printf "%s | %s | %s" "$(basename "$file")" "$(stat -c '%s Bytes' "$file")" "$(stat -c '%y' "$file" | cut -d. -f1)"
  else
    printf "nicht gefunden"
  fi
}

version_from_name() {
  local name="$1"
  basename "$name" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 || true
}

show_status() {
  local app_dir="${1:-$APP_DIR_DEFAULT}"
  local service_name="${SERVICE_NAME_DEFAULT}"
  local dll
  dll="$(find_app_dll "$app_dir" || true)"

  echo "A4W Linux-Status"
  echo "  Skript-Version: $SCRIPT_VERSION"
  echo "  App:            $app_dir"
  echo "  Dienst:         $service_name"
  echo "  Dienstdatei:    $(service_file_path "$service_name")"
  echo "  Dienst-User:    $(read_service_value "$service_name" "User")"
  echo "  Dienst-Gruppe:  $(read_service_value "$service_name" "Group")"
  echo "  DLL:            ${dll:-nicht gefunden}"
  if [[ -n "${dll:-}" ]]; then
    echo "  DLL-Datei:       $(file_summary "$dll")"
    echo "  DLL-SHA256:      $(sha_file "$dll")"
  fi
  echo "  systemctl:"
  systemctl is-active "$service_name" 2>/dev/null || true
}

find_latest_update_zip() {
  local app_dir="$1"
  local data_dir="$app_dir/a4w-data"
  local updates_dir="$data_dir/Updates"
  local found=""

  if [[ -d "$updates_dir" ]]; then
    found="$(find "$updates_dir" -maxdepth 1 -type f -name 'A4W-Auftrag4Works-Linux-Server-*.zip' -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -n 1 | cut -d' ' -f2- || true)"
    if [[ -n "$found" ]]; then
      echo "$found"
      return
    fi
    found="$(find "$updates_dir" -maxdepth 1 -type f -name 'A4W-Auftrag4Works-update-payload-*.zip' -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -n 1 | cut -d' ' -f2- || true)"
    if [[ -n "$found" ]]; then
      warn "Es wurde nur ein generisches/Windows-artiges update-payload-ZIP gefunden. Besser ist A4W-Auftrag4Works-Linux-Server-<version>.zip."
      echo "$found"
      return
    fi
  fi

  return 1
}

extract_zip_normalized() {
  local zip="$1"
  local target="$2"

  if command -v python3 >/dev/null 2>&1; then
    python3 - "$zip" "$target" <<'PY'
import os, sys, zipfile
zip_path, target = sys.argv[1], sys.argv[2]
with zipfile.ZipFile(zip_path) as zf:
    for member in zf.infolist():
        name = member.filename.replace('\\', '/')
        name = name.lstrip('/')
        if not name or name.endswith('/'):
            os.makedirs(os.path.join(target, name), exist_ok=True)
            continue
        dest = os.path.normpath(os.path.join(target, name))
        if not dest.startswith(os.path.abspath(target)):
            raise RuntimeError(f"Unsicherer ZIP-Pfad: {member.filename}")
        os.makedirs(os.path.dirname(dest), exist_ok=True)
        with zf.open(member) as src, open(dest, 'wb') as dst:
            dst.write(src.read())
PY
  else
    warn "python3 nicht gefunden; nutze unzip. Bei Windows-Backslashes kann unzip warnen."
    unzip -q "$zip" -d "$target"
  fi
}

find_payload_root() {
  local tmp="$1"
  local dll_path=""
  dll_path="$(find "$tmp" -type f \( -name "$DLL_NAME" -o -name "$FALLBACK_DLL_NAME" \) | sort | head -n 1 || true)"
  if [[ -z "$dll_path" ]]; then
    dll_path="$(find "$tmp" -type f -name '*.dll' | sort | head -n 1 || true)"
  fi
  if [[ -z "$dll_path" ]]; then
    return 1
  fi
  dirname "$dll_path"
}

main_update() {
  need_root
  if maybe_spawn_transient_update; then
    exit 0
  fi
  check_update_prerequisites

  local update_zip="${1:-}"
  local app_dir backup_root backup_dir service_name owner_group owner group data_dir updates_dir

  if [[ -n "$update_zip" && "$update_zip" != /* ]]; then
    update_zip="$(pwd)/$update_zip"
  fi

  app_dir="$(ask "Installationsverzeichnis" "$(default_app_dir_for_zip "$update_zip")")"
  app_dir="${app_dir%/}"
  backup_root="$(ask "Backup-Verzeichnis" "$BACKUP_ROOT_DEFAULT")"
  backup_root="${backup_root%/}"
  service_name="$SERVICE_NAME_DEFAULT"
  data_dir="$app_dir/a4w-data"
  updates_dir="$data_dir/Updates"

  if [[ ! -d "$app_dir" ]]; then
    err "Installationsverzeichnis nicht gefunden: $app_dir"
    exit 1
  fi

  if [[ -z "$update_zip" ]]; then
    update_zip="$(find_latest_update_zip "$app_dir" || true)"
  fi
  if [[ -z "$update_zip" || ! -f "$update_zip" ]]; then
    err "Update-ZIP nicht gefunden. Bitte Pfad angeben oder Linux-ZIP in $updates_dir ablegen."
    exit 1
  fi

  mkdir -p "$backup_root"
  LOG_FILE="$backup_root/update-$STAMP.log"
  backup_dir="$backup_root/program-files-$STAMP"
  TMP_DIR="/tmp/a4w-update-$STAMP"
  mkdir -p "$TMP_DIR" "$backup_dir"

  owner_group="$(detect_service_user_group "$app_dir" "$service_name")"
  owner="${owner_group%%:*}"
  group="${owner_group#*:}"

  local current_dll current_sha expected_version zip_sha
  current_dll="$(find_app_dll "$app_dir" || true)"
  current_sha="$(sha_file "${current_dll:-}")"
  expected_version="$(version_from_name "$update_zip")"
  zip_sha="$(sha_file "$update_zip")"

  echo ""
  echo "Update-Konfiguration:" | tee -a "$LOG_FILE"
  echo "  Skript:    update-a4w-linux.sh $SCRIPT_VERSION" | tee -a "$LOG_FILE"
  echo "  App:       $app_dir" | tee -a "$LOG_FILE"
  echo "  Daten:     $data_dir" | tee -a "$LOG_FILE"
  echo "  ZIP:       $update_zip" | tee -a "$LOG_FILE"
  echo "  ZIP-SHA:   $zip_sha" | tee -a "$LOG_FILE"
  echo "  Version:   ${expected_version:-aus Dateiname nicht erkannt}" | tee -a "$LOG_FILE"
  echo "  Backup:    $backup_dir" | tee -a "$LOG_FILE"
  echo "  Log:       $LOG_FILE" | tee -a "$LOG_FILE"
  echo "  Dienst:    $service_name" | tee -a "$LOG_FILE"
  echo "  Rechte:    $owner:$group" | tee -a "$LOG_FILE"
  echo "  Aktuelle DLL: ${current_dll:-nicht gefunden}" | tee -a "$LOG_FILE"
  echo "  Aktuelle DLL-SHA: $current_sha" | tee -a "$LOG_FILE"
  echo "" | tee -a "$LOG_FILE"

  log "Sichere aktuelle Programmdateien ..."
  rsync -a \
    --exclude 'a4w-data' \
    --exclude 'a4w-logs' \
    --exclude 'App_Data' \
    --exclude 'data' \
    --exclude 'wwwroot/uploads' \
    "$app_dir/" "$backup_dir/" | tee -a "$LOG_FILE"

  log "Entpacke Update-ZIP mit normalisierten Pfaden ..."
  extract_zip_normalized "$update_zip" "$TMP_DIR"

  local payload_root package_dll package_sha
  payload_root="$(find_payload_root "$TMP_DIR" || true)"
  if [[ -z "$payload_root" || ! -d "$payload_root" ]]; then
    err "Im Update-ZIP wurde keine startbare DLL gefunden. Abbruch ohne Aenderung."
    find "$TMP_DIR" -maxdepth 3 -type f | head -n 80 | tee -a "$LOG_FILE" || true
    exit 1
  fi

  package_dll="$(find_app_dll "$payload_root" || true)"
  package_sha="$(sha_file "$package_dll")"

  info "Payload-Wurzel: $payload_root"
  info "Paket-DLL:      $package_dll"
  info "Paket-DLL-SHA:  $package_sha"
  info "Dateien im Paket: $(find "$payload_root" -type f | wc -l)"

  local pending_changes
  pending_changes="$(rsync -ani --delete \
    --exclude 'a4w-data' \
    --exclude 'a4w-logs' \
    --exclude 'App_Data' \
    --exclude 'data' \
    --exclude 'wwwroot/uploads' \
    --exclude 'appsettings.json' \
    --exclude 'appsettings.Production.json' \
    --exclude 'appsettings.Development.json' \
    "$payload_root/" "$app_dir/" | sed -n '1,20p' || true)"

  if [[ -z "$pending_changes" && "$package_sha" != "-" && "$current_sha" == "$package_sha" ]]; then
    log "Keine geaenderten Programmdateien erkannt. Dienst bleibt unveraendert aktiv."
    echo "  Zielversion: ${expected_version:-unbekannt}" | tee -a "$LOG_FILE"
    echo "  Log:         $LOG_FILE" | tee -a "$LOG_FILE"
    exit 0
  fi

  if find "$payload_root" -maxdepth 1 -type f \( -name 'A4W Auftrag4Works.exe' -o -name 'A4WUpdater.exe' \) | grep -q .; then
    warn "Das Paket enthaelt Windows-Dateien. Fuer Linux sollte bevorzugt A4W-Auftrag4Works-Linux-Server-<version>.zip verwendet werden."
  fi

  log "Stoppe Dienst ${service_name} ..."
  systemctl stop "${service_name}.service" || true

  log "Kopiere neue Programmdateien ..."
  rsync -a --delete --itemize-changes \
    --exclude 'a4w-data' \
    --exclude 'a4w-logs' \
    --exclude 'App_Data' \
    --exclude 'data' \
    --exclude 'wwwroot/uploads' \
    --exclude 'appsettings.json' \
    --exclude 'appsettings.Production.json' \
    --exclude 'appsettings.Development.json' \
    "$payload_root/" "$app_dir/" | tee -a "$LOG_FILE"

  log "Setze Rechte auf $owner:$group ..."
  chown -R "$owner:$group" "$app_dir"
  find "$app_dir" -maxdepth 1 -type f -name '*.sh' -exec chmod 750 {} \; || true
  find "$app_dir" -type d \( -name 'a4w-data' -o -name 'a4w-logs' \) -prune -o -type d -exec chmod 750 {} \; || true
  find "$app_dir" -type d \( -name 'a4w-data' -o -name 'a4w-logs' \) -prune -o -type f -exec chmod 640 {} \; || true
  chmod 750 "$app_dir" || true

  local new_dll new_sha
  new_dll="$(find_app_dll "$app_dir" || true)"
  new_sha="$(sha_file "${new_dll:-}")"

  echo "" | tee -a "$LOG_FILE"
  echo "Versions-/Dateipruefung:" | tee -a "$LOG_FILE"
  echo "  Erwartete Version aus ZIP: ${expected_version:-unbekannt}" | tee -a "$LOG_FILE"
  echo "  Vorher DLL-SHA:           $current_sha" | tee -a "$LOG_FILE"
  echo "  Paket  DLL-SHA:           $package_sha" | tee -a "$LOG_FILE"
  echo "  Nachher DLL-SHA:          $new_sha" | tee -a "$LOG_FILE"
  echo "  Nachher DLL-Datei:         $(file_summary "${new_dll:-}")" | tee -a "$LOG_FILE"

  if [[ "$package_sha" != "-" && "$new_sha" != "$package_sha" ]]; then
    err "Die Ziel-DLL entspricht nach dem Kopieren nicht der Paket-DLL. Rollback wird versucht."
    systemctl stop "${service_name}.service" || true
    rsync -a --delete "$backup_dir/" "$app_dir/" | tee -a "$LOG_FILE"
    chown -R "$owner:$group" "$app_dir" || true
    systemctl start "${service_name}.service" || true
    exit 1
  fi

  log "Starte Dienst ${service_name} ..."
  refresh_root_update_helper "$app_dir"
  systemctl daemon-reload
  systemctl start "${service_name}.service"
  sleep 3

  if ! systemctl --no-pager --full status "${service_name}.service" | tee -a "$LOG_FILE"; then
    err "Dienststart fehlgeschlagen. Rollback wird versucht ..."
    systemctl stop "${service_name}.service" || true
    rsync -a --delete "$backup_dir/" "$app_dir/" | tee -a "$LOG_FILE"
    chown -R "$owner:$group" "$app_dir" || true
    systemctl start "${service_name}.service" || true
    exit 1
  fi

  echo "" | tee -a "$LOG_FILE"
  log "Update abgeschlossen."
  echo "  Erwartete Version: ${expected_version:-unbekannt}" | tee -a "$LOG_FILE"
  echo "  Backup:            $backup_dir" | tee -a "$LOG_FILE"
  echo "  Log:               $LOG_FILE" | tee -a "$LOG_FILE"
  echo "" | tee -a "$LOG_FILE"
  echo "Nachpruefen:" | tee -a "$LOG_FILE"
  echo "  systemctl status $service_name --no-pager" | tee -a "$LOG_FILE"
  echo "  journalctl -u $service_name -n 100 --no-pager" | tee -a "$LOG_FILE"
  echo "  sudo /bin/bash $app_dir/a4w-maintenance.sh" | tee -a "$LOG_FILE"
  echo "  Browser: Programmupdates-Seite neu laden und installierte Version pruefen." | tee -a "$LOG_FILE"
}

parse_global_args "$@"

case "${POSITIONAL_ARGS[0]:-}" in
  --help|-h)
    usage
    exit 0
    ;;
  --status)
    show_status "${POSITIONAL_ARGS[1]:-$APP_DIR_DEFAULT}"
    exit 0
    ;;
  *)
    main_update "${POSITIONAL_ARGS[0]:-}"
    ;;
esac
