#!/usr/bin/env bash
DW_THEME_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$DW_THEME_DIR/Theme.sh" ]]; then source "$DW_THEME_DIR/Theme.sh"
elif [[ -f "$DW_THEME_DIR/../share/dw-aufraeumer/Theme.sh" ]]; then source "$DW_THEME_DIR/../share/dw-aufraeumer/Theme.sh"
else C_RESET=""; C_BOLD=""; C_INFO=""; C_GOLD=""; C_OK=""; C_WARN=""; C_DANGER=""; C_MUTED=""; fi
# DW Aufräumer für Linux 1.1.1
# Zeigt im Probelauf, wie viel Platz aufräumbare Posten belegen, und führt nur ausdrücklich
# gewählte Posten aus. Root nur je Einzelschritt über sudo. Keine Netzwerkverbindung, keine Telemetrie.
# MIT-Lizenz, (c) 2026 Michael Bergfeld, DoMeZos-Ware
#
# Umgebungsvariablen (nur für Tests und Sonderfälle):
#   DW_PKGMGR=apt|dnf|pacman|zypper   Paketverwaltung erzwingen (wie --paketmanager)
#   DW_SUDO=BEFEHL                Programm statt sudo für Root-Schritte
#   DW_APT_CACHE_DIR, DW_DNF_CACHE_DIR, DW_PACMAN_CACHE_DIR, DW_SNAP_DIR   andere Cache-Ordner
#   XDG_CACHE_HOME, XDG_STATE_HOME                                         wie üblich
# shellcheck disable=SC2329
set -u
export LC_ALL=C.UTF-8 2>/dev/null || export LC_ALL=C
VERSION="1.1.1"
US=$'\x1f'
ITEMS=(paketcache verwaist kernel journal snap flatpak thumbnails)

declare -A TITLE SIZE STAT DETAIL UNDO CMDS
EXEC=0; YES=0; SELECTION=""; JMAX=""; PM=""

usage() {
cat <<'EOF'
DW Aufräumer 1.1.1 - Linux aufräumen: erst Probelauf, dann gezielt ausführen

Aufruf:
  dw-aufraeumer [Optionen]

Ohne Optionen und mit interaktivem Terminal erscheint eine Checkliste zur Postenauswahl.

Optionen:
  --probelauf            nur anzeigen, was aufgeräumt werden könnte (Standard)
  --ausfuehren           aufräumen: zeigt die Befehle und fragt je Posten nach
  --ja                   mit --ausfuehren: keine Rückfragen (Befehle werden trotzdem angezeigt)
  --posten LISTE         nur diese Posten, kommagetrennt (siehe --liste); Standard: alle bzw. Checkliste
  --journal-kuerzen GRÖSSE   Systemprotokolle auf maximale Größe beschränken (z. B. 200M)
                         (nur mit --ausfuehren wirksam; Alias: --journal-vakuum)
  --paketmanager NAME    apt, dnf, pacman oder zypper festlegen (Standard: automatisch erkennen)
  --liste                Posten mit Beschreibung anzeigen
  --version              Version anzeigen
  -h, --hilfe, --help    diese Hilfe

Beispiele:
  dw-aufraeumer                                  interaktive Auswahl, dann Probelauf
  dw-aufraeumer --posten paketcache,kernel       Probelauf nur für diese Posten
  dw-aufraeumer --ausfuehren --posten paketcache,thumbnails
  dw-aufraeumer --ausfuehren --posten journal --journal-kuerzen 200M

Sicherheit:
  - Der Probelauf liest nur und braucht kein Root.
  - Beim Ausführen werden alle Befehle vorher angezeigt und jeder Posten einzeln bestätigt.
  - Root-Rechte (sudo) werden nur für den jeweiligen Einzelschritt genutzt.
  - Der laufende und der neueste Kernel bleiben immer erhalten.
  - Systemprotokolle werden nur mit --journal-kuerzen gekürzt.
  - Bei aktiver Paketsperre (laufende Paketverwaltung) wird abgebrochen.
  - Benutzerdateien und Browserdaten werden nie angefasst.
  - Protokoll jeder Ausführung: ~/.local/state/dw-aufraeumer/

Beendigungscodes: 0 in Ordnung, 1 Schritt fehlgeschlagen, 2 falscher Aufruf, 3 Paketsperre aktiv
EOF
}

have() { command -v "$1" >/dev/null 2>&1; }
err() { printf '%s%s%s\n' "$C_DANGER" "$*" "$C_RESET" >&2; }

list_items() {
  local i d
  printf '%s%sPosten:%s\n' "$C_INFO" "$C_BOLD" "$C_RESET"
  for i in "${ITEMS[@]}"; do
    case "$i" in
      paketcache) d="heruntergeladene Paketdateien (apt, dnf, pacman, zypper)";;
      verwaist)   d="nicht mehr benötigte Pakete (ohne Kernel-Pakete; zypper: nur Anzeige)";;
      kernel)     d="⚠ alte Kernel (der laufende und der neueste je Variante bleiben)";;
      journal)    d="⚠ Größe der Systemprotokolle; Kürzen nur mit --journal-kuerzen GRÖSSE";;
      snap)       d="deaktivierte Snap-Revisionen";;
      flatpak)    d="nicht mehr benötigte Flatpak-Laufzeitumgebungen";;
      thumbnails) d="Vorschaubilder-Cache im Benutzerordner";;
      *)          d="$(item_desc "$i")";;
    esac
    printf '  %s%s%-12s%s  %s%s%s\n' "$C_INFO" "$C_BOLD" "$i" "$C_RESET" "$C_MUTED" "$d" "$C_RESET"
  done
}

# Kurzbeschreibungen für Checkliste / nummerierte Auswahl (barrierefrei, Deutsch)
item_desc() {
  case "$1" in
    paketcache)  printf '%s' "Veraltete Installationsdateien";;
    verwaist)    printf '%s' "Nicht mehr benötigte Pakete";;
    kernel)      printf '%s' "⚠ Alte Kernel entfernen";;
    journal)     printf '%s' "⚠ Alte Systemprotokolle kürzen";;
    snap)        printf '%s' "Deaktivierte Snap-Revisionen";;
    flatpak)     printf '%s' "Ungenutzte Flatpak-Laufzeiten";;
    thumbnails)  printf '%s' "Bild-Vorschau-Cache leeren";;
    *)           printf '%s' "$1";;
  esac
}

# Nummerierte Auswahlliste (Fallback ohne whiptail / bei Dialogfehler)
numbered_select_items() {
  local i n answer num
  local -a picked=()
  printf '%s\n' "Posten auswählen – nummerierte Liste:"
  printf '%s\n' "Nummern durch Leerzeichen getrennt eingeben, 'a' für alle, 'q' zum Abbrechen."
  n=0
  for i in "${ITEMS[@]}"; do
    n=$((n + 1))
    printf '  %d) %-12s  %s\n' "$n" "$i" "$(item_desc "$i")"
  done
  printf 'Auswahl: '
  read -r answer || answer=q
  case "$answer" in
    q|Q|quit|Quit|abbrechen|Abbrechen) err "Auswahl abgebrochen."; return 1;;
    a|A|alle|Alle|"") picked=("${ITEMS[@]}");;
    *)
      for num in $answer; do
        [[ "$num" =~ ^[0-9]+$ ]] || { err "Ungültige Eingabe: $num"; return 2; }
        if [ "$num" -lt 1 ] || [ "$num" -gt "${#ITEMS[@]}" ]; then
          err "Nummer außerhalb des Bereichs: $num"
          return 2
        fi
        picked+=("${ITEMS[$((num - 1))]}")
      done;;
  esac
  if [ "${#picked[@]}" -eq 0 ]; then
    err "Keine Posten ausgewählt."
    return 1
  fi
  SELECTION="$(IFS=,; printf '%s' "${picked[*]}")"
  return 0
}

# Interaktive Postenauswahl (whiptail-Checkliste oder nummerierte Liste)
interactive_select_items() {
  local result i whiptail_rc=0
  local -a picked=()
  if have whiptail && [ -t 1 ]; then
    local -a wargs=()
    for i in "${ITEMS[@]}"; do
      wargs+=("$i" "$(item_desc "$i")" "ON")
    done
    # Dialog auf stderr/tty, Auswahl auf stdout zurückholen
    result="$(whiptail --title "DW Aufräumer" --checklist \
      "Wählen Sie die zu bereinigenden Posten (Leertaste zum Umschalten, Enter zum Bestätigen):" \
      18 70 7 "${wargs[@]}" 3>&1 1>&2 2>&3)" || whiptail_rc=$?
    if [ "$whiptail_rc" -eq 0 ]; then
      for i in "${ITEMS[@]}"; do
        case " $result " in
          *"\"$i\""*|*" $i "*) picked+=("$i");;
        esac
      done
      if [ "${#picked[@]}" -eq 0 ]; then
        err "Keine Posten ausgewählt."
        return 1
      fi
      SELECTION="$(IFS=,; printf '%s' "${picked[*]}")"
      return 0
    fi
    # Abbruch (1) bzw. Escape (255) beenden; andere Fehler → nummerierte Liste
    if [ "$whiptail_rc" -eq 1 ] || [ "$whiptail_rc" -eq 255 ]; then
      err "Auswahl abgebrochen."
      return 1
    fi
    printf '%s\n' "Hinweis: Checklisten-Dialog nicht nutzbar – wechsle zur nummerierten Liste." >&2
  fi
  numbered_select_items
}

# ---------------------------------------------------------------- Hilfsfunktionen
human() {
  local b="$1"
  if   [ "$b" -ge 1073741824 ]; then printf '%d,%d GB' $((b/1073741824)) $(( (b*10/1073741824)%10 ))
  elif [ "$b" -ge 1048576 ];    then printf '%d,%d MB' $((b/1048576)) $(( (b*10/1048576)%10 ))
  elif [ "$b" -ge 1024 ];       then printf '%d KB' $((b/1024))
  else printf '%d Byte' "$b"; fi
}

# Größe mit Einheit (B, K, M, G, T; Basis 1024) in Byte
tobytes() {
  awk -v v="$1" 'BEGIN{ u="B"; n=v; if (match(v,/[BKMGT]$/)) { u=substr(v,RSTART,1); n=substr(v,1,RSTART-1) }
    m=1; if(u=="K")m=1024; else if(u=="M")m=1048576; else if(u=="G")m=1073741824; else if(u=="T")m=1099511627776;
    printf "%d", n*m }'
}

# Summe der Dateigrößen (Muster) in Ordnern
sum_files() {
  local pat="$1" depth="$2"; shift 2
  local d out=0 s
  for d in "$@"; do
    [ -d "$d" ] || continue
    s="$(find "$d" -maxdepth "$depth" -type f -name "$pat" -printf '%s\n' 2>/dev/null | awk '{s+=$1} END{printf "%d", s}')"
    out=$((out + ${s:-0}))
  done
  printf '%d' "$out"
}

# Befehl vormerken: addcmd POSTEN R|U Wort...  (R = Root nötig)
addcmd() {
  local item="$1" mode="$2" rec w
  shift 2
  rec="$mode"
  for w in "$@"; do rec+="$US$w"; done
  CMDS[$item]+="$rec"$'\n'
}

detect_pm() {
  if [ -n "${DW_PKGMGR:-}" ]; then
    case "$DW_PKGMGR" in apt|dnf|pacman|zypper) PM="$DW_PKGMGR";; *) err "Paketverwaltung muss apt, dnf, pacman oder zypper sein."; exit 2;; esac
    return
  fi
  if have apt-get && have dpkg-query; then PM=apt
  elif have dnf && have rpm; then PM=dnf
  elif have pacman; then PM=pacman
  elif have zypper && have rpm; then PM=zypper
  else PM=""; fi
}

pm_locked() {
  case "$PM" in
    zypper) [ -e /run/zypp.pid ] || { have pgrep && pgrep -x 'zypper|packagekitd' >/dev/null 2>&1; };;
    apt)    have pgrep && pgrep -x 'apt|apt-get|aptitude|dpkg|unattended-upgr' >/dev/null 2>&1;;
    dnf)    have pgrep && pgrep -x 'dnf|dnf5|yum|rpm' >/dev/null 2>&1;;
    pacman) [ -e /var/lib/pacman/db.lck ] || { have pgrep && pgrep -x 'pacman|yay|paru' >/dev/null 2>&1; };;
    *) return 1;;
  esac
}

# ---------------------------------------------------------------- Posten erfassen
set_item() { # POSTEN Titel Größe Status
  TITLE[$1]="$2"; SIZE[$1]="$3"; STAT[$1]="$4"
}

scan_paketcache() {
  local s=0
  case "$PM" in
    apt)
      local dir="${DW_APT_CACHE_DIR:-/var/cache/apt/archives}"
      s="$(sum_files '*.deb' 2 "$dir")"
      set_item paketcache "Paket-Cache (apt)" "$s" ok
      if [ -n "${DW_APT_CACHE_DIR:-}" ]; then
        addcmd paketcache R apt-get -o "Dir::Cache::archives=$dir" -o Dir::Cache::pkgcache= -o Dir::Cache::srcpkgcache= clean
      else
        addcmd paketcache R apt-get clean
      fi;;
    dnf)
      if [ -n "${DW_DNF_CACHE_DIR:-}" ]; then
        s="$(sum_files '*.rpm' 6 "$DW_DNF_CACHE_DIR")"
      else
        s="$(sum_files '*.rpm' 6 /var/cache/dnf /var/cache/libdnf5)"
      fi
      set_item paketcache "Paket-Cache (dnf)" "$s" ok
      addcmd paketcache R dnf clean packages;;
    pacman)
      s="$(sum_files '*.pkg.tar*' 1 "${DW_PACMAN_CACHE_DIR:-/var/cache/pacman/pkg}")"
      set_item paketcache "Paket-Cache (pacman, bis zu; nur nicht installierte Versionen werden entfernt)" "$s" ok
      addcmd paketcache R pacman -Sc --noconfirm;;
    zypper)
      s="$(sum_files '*.rpm' 6 "${DW_ZYPPER_CACHE_DIR:-/var/cache/zypp/packages}")"
      set_item paketcache "Paket-Cache (zypper)" "$s" ok
      addcmd paketcache R zypper clean;;
    *) set_item paketcache "Paket-Cache" 0 nv; DETAIL[paketcache]="keine unterstützte Paketverwaltung gefunden (apt, dnf, pacman, zypper)";;
  esac
  UNDO[paketcache]="nicht nötig: Pakete werden bei Bedarf neu heruntergeladen"
  [ "$s" -eq 0 ] && [ "${STAT[paketcache]}" = ok ] && STAT[paketcache]=leer
  return 0
}

is_kernel_pkg() {
  case "$1" in
    linux-image-[0-9]*|linux-modules-[0-9]*|linux-modules-extra-[0-9]*|linux-headers-[0-9]*|linux-tools-[0-9]*|linux-buildinfo-[0-9]*|linux-cloud-tools-[0-9]*) return 0;;
  esac
  return 1
}

scan_verwaist() {
  local -a names=() skipped=()
  local n total=0 kb detail=""
  case "$PM" in
    apt)
      local line
      while IFS= read -r line; do
        if is_kernel_pkg "$line"; then skipped+=("$line"); else names+=("$line"); fi
      done < <(apt-get -s autoremove 2>/dev/null | awk '$1=="Remv" && $2 ~ /^[A-Za-z0-9][A-Za-z0-9+._:~-]*$/ {print $2}')
      if [ "${#names[@]}" -gt 0 ]; then
        while IFS=$'\t' read -r _ _ st kb; do
          case "$st" in ii*) total=$((total + ${kb:-0} * 1024));; esac
        done < <(dpkg-query -W -f='${Package}\t${Version}\t${db:Status-Abbrev}\t${Installed-Size}\n' "${names[@]}" 2>/dev/null)
        local -a rmargs=()
        for n in "${names[@]}"; do
          case "$n" in *+) rmargs+=("$n-");; *) rmargs+=("$n");; esac
        done
        addcmd verwaist R apt-get remove -y "${rmargs[@]}"
        UNDO[verwaist]="sudo apt-get install ${names[*]}"
      fi
      set_item verwaist "Nicht mehr benötigte Pakete (apt)" "$total" ok;;
    dnf)
      while IFS= read -r n; do [ -n "$n" ] && names+=("$n"); done < <(dnf -q repoquery --unneeded --qf '%{name}\n' 2>/dev/null | grep -E '^[A-Za-z0-9][A-Za-z0-9+._:~-]*$' | sort -u)
      if [ "${#names[@]}" -gt 0 ]; then
        total="$(rpm -q --qf '%{SIZE}\n' "${names[@]}" 2>/dev/null | awk '/^[0-9]+$/{s+=$1} END{printf "%d", s}')"
        addcmd verwaist R dnf remove -y "${names[@]}"
        UNDO[verwaist]="sudo dnf install ${names[*]}"
      fi
      set_item verwaist "Nicht mehr benötigte Pakete (dnf)" "${total:-0}" ok;;
    pacman)
      while IFS= read -r n; do [ -n "$n" ] && names+=("$n"); done < <(pacman -Qdtq 2>/dev/null | grep -E '^[A-Za-z0-9][A-Za-z0-9+._:~@-]*$')
      if [ "${#names[@]}" -gt 0 ]; then
        total="$(pacman -Qi "${names[@]}" 2>/dev/null | awk -F': *' '
          /^Installed Size/ { split($2,a," "); v=a[1]; u=a[2]; m=1;
            if(u=="KiB")m=1024; else if(u=="MiB")m=1048576; else if(u=="GiB")m=1073741824; s+=v*m }
          END{printf "%d", s}')"
        addcmd verwaist R pacman -Rns --noconfirm "${names[@]}"
        UNDO[verwaist]="sudo pacman -S ${names[*]}"
      fi
      set_item verwaist "Nicht mehr benötigte Pakete (pacman)" "${total:-0}" ok;;
    zypper)
      # nur Anzeige: Tabelle "S | Repository | Name | Version | Arch"
      while IFS= read -r n; do [ -n "$n" ] && names+=("$n"); done < <(zypper -q packages --unneeded 2>/dev/null | awk -F'|' 'NR>2 && $1 ~ /i/ {gsub(/^ +| +$/,"",$3); if ($3 ~ /^[A-Za-z0-9][A-Za-z0-9+._:~-]*$/) print $3}' | sort -u)
      if [ "${#names[@]}" -gt 0 ]; then
        total="$(rpm -q --qf '%{SIZE}\n' "${names[@]}" 2>/dev/null | awk '/^[0-9]+$/{s+=$1} END{printf "%d", s}')"
      fi
      set_item verwaist "Nicht mehr benötigte Pakete (zypper, nur Anzeige)" "${total:-0}" info
      detail+="  (zypper: nur Anzeige. Entfernen Sie diese Pakete nach eigener Prüfung mit: sudo zypper remove --clean-deps NAME)"$'\n';;
    *) set_item verwaist "Nicht mehr benötigte Pakete" 0 nv; DETAIL[verwaist]="keine unterstützte Paketverwaltung gefunden"; return 0;;
  esac
  for n in "${names[@]}"; do detail+="  - $n"$'\n'; done
  if [ "${#skipped[@]}" -gt 0 ]; then
    detail+="  (Kernel-Pakete werden unter 'kernel' behandelt und hier übersprungen: ${#skipped[@]})"$'\n'
  fi
  DETAIL[verwaist]="$detail"
  [ "${#names[@]}" -eq 0 ] && STAT[verwaist]=leer
  [ "$PM" = zypper ] && [ "${#names[@]}" -gt 0 ] && STAT[verwaist]=info
  return 0
}

# Pro Variante (letzter Namensteil) bleiben laufender und der neueste Kernel
pick_old_versions() { # liest "Version" je Zeile auf stdin (sortierbar), $1 = laufende Version; schreibt alte auf stdout
  local running="$1" v flavor
  local -A keep=() count=()
  local -a all=() rev=()
  mapfile -t all < <(sort -V)
  local i
  for (( i=${#all[@]}-1; i>=0; i-- )); do rev+=("${all[$i]}"); done
  for v in "${rev[@]}"; do
    flavor="${v##*-}"
    count[$flavor]=$(( ${count[$flavor]:-0} + 1 ))
    [ "${count[$flavor]}" -le 1 ] && keep[$v]=1
  done
  for v in "${all[@]}"; do
    [ "$v" = "$running" ] && continue
    [ -n "${keep[$v]:-}" ] && continue
    printf '%s\n' "$v"
  done
}

scan_kernel() {
  local running; running="$(uname -r 2>/dev/null)"
  local total=0 detail=""
  case "$PM" in
    apt)
      local -a pkgs=() old=() rmpk=()
      local pkg st kb
      while IFS=$'\t' read -r pkg _ st _; do
        case "$st" in ii*) pkgs+=("${pkg#linux-image-}");; esac
      done < <(dpkg-query -W -f='${Package}\t${Version}\t${db:Status-Abbrev}\t${Installed-Size}\n' 'linux-image-[0-9]*' 2>/dev/null)
      if [ "${#pkgs[@]}" -eq 0 ]; then
        set_item kernel "Alte Kernel (apt)" 0 leer; DETAIL[kernel]="keine Kernel-Pakete installiert (zum Beispiel WSL oder Container)"; return 0
      fi
      local found=0 p
      for p in "${pkgs[@]}"; do [ "$p" = "$running" ] && found=1; done
      if [ "$found" -eq 0 ]; then
        set_item kernel "Alte Kernel (apt)" 0 nv
        DETAIL[kernel]="der laufende Kernel ($running) gehört zu keinem installierten Kernel-Paket; aus Vorsicht wird nichts vorgeschlagen"
        return 0
      fi
      mapfile -t old < <(printf '%s\n' "${pkgs[@]}" | pick_old_versions "$running")
      local v
      for v in "${old[@]}"; do
        [ -n "$v" ] || continue
        while IFS=$'\t' read -r pkg _ st kb; do
          case "$st" in ii*)
            rmpk+=("$pkg"); total=$((total + ${kb:-0} * 1024)); detail+="  - $pkg"$'\n';;
          esac
        done < <(dpkg-query -W -f='${Package}\t${Version}\t${db:Status-Abbrev}\t${Installed-Size}\n' "linux-image-$v" "linux-modules-$v" "linux-modules-extra-$v" "linux-headers-$v" 2>/dev/null)
      done
      if [ "${#rmpk[@]}" -gt 0 ]; then
        addcmd kernel R apt-get purge -y "${rmpk[@]}"
        UNDO[kernel]="sudo apt-get install ${rmpk[*]}"
      fi
      detail+="  Bleibt erhalten: laufender Kernel $running und der neueste je Variante"$'\n'
      set_item kernel "Alte Kernel (apt)" "$total" ok
      [ "${#rmpk[@]}" -eq 0 ] && STAT[kernel]=leer;;
    dnf)
      local nm vr sz
      local -a vers=() rmpk=()
      local -A seen=()
      while IFS=$'\t' read -r nm vr sz; do
        [ -n "${sz:-}" ] || continue
        case "$nm" in kernel-core|kernel) [ -z "${seen[$vr]:-}" ] && { seen[$vr]=1; vers+=("$vr"); };; esac
      done < <(rpm -q --qf '%{NAME}\t%{VERSION}-%{RELEASE}.%{ARCH}\t%{SIZE}\n' kernel-core kernel 2>/dev/null)
      if [ "${#vers[@]}" -eq 0 ]; then
        set_item kernel "Alte Kernel (dnf)" 0 leer; DETAIL[kernel]="keine Kernel-Pakete gefunden"; return 0
      fi
      local found=0 p
      for p in "${vers[@]}"; do [ "$p" = "$running" ] && found=1; done
      if [ "$found" -eq 0 ]; then
        set_item kernel "Alte Kernel (dnf)" 0 nv
        DETAIL[kernel]="der laufende Kernel ($running) gehört zu keinem installierten Kernel-Paket; aus Vorsicht wird nichts vorgeschlagen"
        return 0
      fi
      local -a old=()
      mapfile -t old < <(printf '%s\n' "${vers[@]}" | pick_old_versions_dnf "$running")
      local v
      for v in "${old[@]}"; do
        [ -n "$v" ] || continue
        while IFS=$'\t' read -r nm vr sz; do
          [ -n "${sz:-}" ] || continue
          [ "$vr" = "$v" ] || continue
          case "$nm" in kernel|kernel-core|kernel-modules|kernel-modules-core|kernel-modules-extra|kernel-devel)
            rmpk+=("$nm-$vr"); total=$((total + sz)); detail+="  - $nm-$vr"$'\n';;
          esac
        done < <(rpm -q --qf '%{NAME}\t%{VERSION}-%{RELEASE}.%{ARCH}\t%{SIZE}\n' kernel kernel-core kernel-modules kernel-modules-core kernel-modules-extra kernel-devel 2>/dev/null)
      done
      if [ "${#rmpk[@]}" -gt 0 ]; then
        addcmd kernel R dnf remove -y "${rmpk[@]}"
        UNDO[kernel]="sudo dnf install ${rmpk[*]}"
      fi
      detail+="  Bleibt erhalten: laufender Kernel $running und der neueste"$'\n'
      set_item kernel "Alte Kernel (dnf)" "$total" ok
      [ "${#rmpk[@]}" -eq 0 ] && STAT[kernel]=leer;;
    pacman)
      set_item kernel "Alte Kernel (pacman)" 0 leer
      DETAIL[kernel]="pacman hält je Kernel-Paket nur eine Version; es gibt keine alten Kernel-Versionen zu entfernen"; return 0;;
    zypper)
      set_item kernel "Alte Kernel (zypper)" 0 leer
      DETAIL[kernel]="zypper: alte Kernel räumt 'sudo zypper purge-kernels' nach Ihrer Konfiguration (multiversion.kernels) auf; dieses Programm entfernt hier nichts"; return 0;;
    *) set_item kernel "Alte Kernel" 0 nv; DETAIL[kernel]="keine unterstützte Paketverwaltung gefunden"; return 0;;
  esac
  DETAIL[kernel]="$detail"
  return 0
}

# Variante für rpm-Versionsstrings (Variante = Architektur-Endung)
pick_old_versions_dnf() {
  local running="$1" v
  local -a all=()
  mapfile -t all < <(sort -V)
  local n=${#all[@]} i
  for (( i=0; i<n; i++ )); do
    v="${all[$i]}"
    [ "$v" = "$running" ] && continue
    [ "$i" -ge $((n-1)) ] && continue
    printf '%s\n' "$v"
  done
}

scan_journal() {
  if ! have journalctl; then set_item journal "Systemjournal" 0 nv; DETAIL[journal]="journalctl nicht gefunden (kein systemd)"; return 0; fi
  local out val bytes target freeable
  out="$(journalctl --disk-usage 2>/dev/null)" || out=""
  val="$(printf '%s\n' "$out" | sed -n 's/.*take up \([0-9.]*[BKMGT]\{0,1\}\) .*/\1/p' | head -n1)"
  if [ -z "$val" ]; then set_item journal "Systemjournal" 0 nv; DETAIL[journal]="Journalgröße nicht lesbar (kein systemd-Journal?)"; return 0; fi
  bytes="$(tobytes "$val")"
  UNDO[journal]="nicht möglich: entfernte Journaleinträge sind weg"
  if [ -z "$JMAX" ]; then
    set_item journal "Systemprotokolle (derzeit $(human "$bytes"))" "$bytes" info
    DETAIL[journal]="  Nur Anzeige. Zum Kürzen: --journal-kuerzen GRÖSSE angeben, z. B. --ausfuehren --posten journal --journal-kuerzen 200M"$'\n'
    return 0
  fi
  target="$(tobytes "$JMAX")"
  freeable=$((bytes - target)); [ "$freeable" -lt 0 ] && freeable=0
  set_item journal "Systemprotokolle (derzeit $(human "$bytes"), Ziel $JMAX)" "$freeable" ok
  DETAIL[journal]="  Es werden nur ältere, archivierte Protokolldateien entfernt, bis die Zielgröße erreicht ist. Entfernte Einträge sind unwiederbringlich weg."$'\n'
  if [ "$freeable" -gt 0 ]; then
    addcmd journal R journalctl "--vacuum-size=$JMAX"
  else
    STAT[journal]=leer
  fi
  return 0
}

scan_snap() {
  if ! have snap; then set_item snap "Snap-Reste" 0 nv; DETAIL[snap]="snap nicht installiert"; return 0; fi
  local dir="${DW_SNAP_DIR:-/var/lib/snapd/snaps}" total=0 detail="" name rev sz n=0
  while read -r name rev; do
    [ -n "$name" ] || continue
    sz=0; [ -f "$dir/${name}_${rev}.snap" ] && sz="$(stat -c %s -- "$dir/${name}_${rev}.snap" 2>/dev/null || echo 0)"
    total=$((total + sz)); n=$((n+1))
    detail+="  - $name (Revision $rev, $(human "$sz"))"$'\n'
    addcmd snap R snap remove "$name" "--revision=$rev"
  done < <(snap list --all 2>/dev/null | awk 'NR>1 && $NF ~ /(^|,)disabled(,|$)/ && $1 ~ /^[a-z0-9][a-z0-9-]*$/ && $3 ~ /^[a-z0-9]+$/ {print $1, $3}')
  set_item snap "Deaktivierte Snap-Revisionen" "$total" ok
  DETAIL[snap]="$detail"
  UNDO[snap]="nicht nötig: deaktivierte Revisionen sind alte Stände; aktuelle Revisionen bleiben installiert"
  [ "$n" -eq 0 ] && STAT[snap]=leer
  return 0
}

scan_flatpak() {
  if ! have flatpak; then set_item flatpak "Flatpak-Reste" 0 nv; DETAIL[flatpak]="flatpak nicht installiert"; return 0; fi
  local -A fsz=()
  local id br bytes total=0 detail="" n=0
  while read -r id br bytes; do fsz["$id/$br"]="$bytes"; done < <(flatpak list --columns=application,branch,size 2>/dev/null | awk '
    NF>=4 { u=$NF; v=$(NF-1); m=0;
      if(u=="bytes")m=1; else if(u=="kB")m=1000; else if(u=="MB")m=1000000; else if(u=="GB")m=1000000000;
      print $1, $2, int(v*m) }')
  while read -r id br; do
    [ -n "$id" ] || continue
    n=$((n+1)); bytes="${fsz["$id/$br"]:-0}"; total=$((total + bytes))
    detail+="  - $id ($br, $(human "$bytes"))"$'\n'
  done < <(flatpak uninstall --unused --assumeno 2>/dev/null </dev/null | awk '$1 ~ /^[0-9]+\.$/ { for (i=2;i<NF;i++) if ($i ~ /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+$/ && $i !~ /^[0-9.]+$/) { print $i, $(i+1); break } }')
  set_item flatpak "Nicht mehr benötigte Flatpak-Laufzeitumgebungen" "$total" ok
  DETAIL[flatpak]="$detail"
  if [ "$n" -gt 0 ]; then addcmd flatpak U flatpak uninstall --unused -y; else STAT[flatpak]=leer; fi
  UNDO[flatpak]="bei Bedarf installiert Flatpak benötigte Laufzeitumgebungen automatisch neu"
  return 0
}

scan_thumbnails() {
  local c="${XDG_CACHE_HOME:-$HOME/.cache}/thumbnails" total=0 d s
  local -a dirs=()
  for d in "$c" "$HOME/.thumbnails"; do
    case "$d" in */thumbnails|*/.thumbnails) ;; *) continue;; esac
    if [ -d "$d" ] && [ ! -L "$d" ]; then
      dirs+=("$d")
      s="$(find -P "$d" -mindepth 2 -type f -name '*.png' -printf '%s\n' 2>/dev/null | awk '{s+=$1} END{printf "%d", s}')"
      total=$((total + ${s:-0}))
      addcmd thumbnails U find -P "$d" -mindepth 2 -type f -name '*.png' -delete
    fi
  done
  set_item thumbnails "Vorschaubilder-Cache" "$total" ok
  local detail="" x shown
  for x in "${dirs[@]}"; do
    shown="$x"; case "$x" in "$HOME"/*) shown="~${x#"$HOME"}";; esac
    detail+="  - $shown"$'\n'
  done
  DETAIL[thumbnails]="$detail"
  UNDO[thumbnails]="nicht nötig: Vorschaubilder werden vom Dateimanager neu erzeugt"
  [ "$total" -eq 0 ] && { STAT[thumbnails]=leer; CMDS[thumbnails]=""; }
  return 0
}

# ---------------------------------------------------------------- Ausgabe
term_cols() {
  local c
  c="$(tput cols 2>/dev/null)" || c="${COLUMNS:-80}"
  case "$c" in ''|*[!0-9]*) c=80;; esac
  [ "$c" -lt 40 ] && c=40
  printf '%d' "$c"
}

# Farben nur auf TTY und ohne NO_COLOR (barrierefreie Rohausgabe für Umleitungen/Tests)
use_color() { [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; }

# Farbliche Abstufung: >1 GB fett Rot, 100 MB–1 GB Gelb, darunter Grün
colorize_size() {
  local bytes="$1" text="$2"
  if ! use_color; then printf '%s' "$text"; return 0; fi
  if [ "$bytes" -gt 1073741824 ]; then
    printf '%s%s%s%s' "$C_BOLD" "$C_DANGER" "$text" "$C_RESET"
  elif [ "$bytes" -ge 104857600 ]; then
    printf '%s%s%s%s' "$C_BOLD" "$C_WARN" "$text" "$C_RESET"
  else
    printf '%s%s%s%s' "$C_BOLD" "$C_OK" "$text" "$C_RESET"
  fi
}

status_text() {
  case "${STAT[$1]}" in
    leer|nv) printf '—';;
    *)    human "${SIZE[$1]}";;
  esac
}

# Sichtbare Breite auffüllen (ANSI-Codes zählen nicht mit)
pad_visible() {
  local plain="$1" width="$2" len=${#1} n
  n=$((width - len))
  while [ "$n" -gt 0 ]; do printf ' '; n=$((n - 1)); done
}

show_rec() { # Datensatz als anzeigbarer Befehl
  local rec="$1" mode w
  local -a W
  IFS="$US" read -r -a W <<<"$rec"
  mode="${W[0]}"
  [ "$mode" = R ] && { if [ "$(id -u)" -eq 0 ]; then :; else printf '%s ' "${DW_SUDO:-sudo}"; fi; }
  for w in "${W[@]:1}"; do printf '%q ' "$w"; done
}

report() {
  local item total=0 cols item_w size_w status_w desc_w plain colored title cut status status_color
  cols="$(term_cols)"
  item_w=14
  size_w=12
  status_w=13
  # Zwei Leerzeichen Einrückung + zwei Spaltenabstände
  desc_w=$((cols - 2 - item_w - 1 - size_w - 1 - status_w - 1))
  if [ "$desc_w" -lt 12 ]; then
    item_w=12
    size_w=10
    desc_w=$((cols - 2 - item_w - 1 - size_w - 1 - status_w - 1))
    [ "$desc_w" -lt 8 ] && desc_w=8
  fi

  for item in "${SEL[@]}"; do [ "${STAT[$item]}" = ok ] && total=$((total + SIZE[$item])); done
  printf '\n%s%sGesamt-Platzersparnis (geschätzt): %s%s\n\n' "$C_GOLD" "$C_BOLD" "$(human "$total")" "$C_RESET"
  printf '  %s%s%-*s%s %-*s %-*s %s\n' "$C_INFO" "$C_BOLD" "$item_w" "Posten" "$C_RESET" "$size_w" "Platz" "$status_w" "Status" "Beschreibung"
  printf '  %-*s %-*s %-*s %s\n' "$item_w" "------" "$size_w" "-----" "$status_w" "------" "------------"
  for item in "${SEL[@]}"; do
    plain="$(status_text "$item")"
    case "${STAT[$item]}" in
      leer) colored="$plain"; status="nichts zu tun"; status_color="$C_MUTED";;
      nv) colored="$plain"; status="nicht vorhanden"; status_color="$C_MUTED";;
      info) colored="$(colorize_size "${SIZE[$item]}" "$plain")"; status="nur Anzeige"; status_color="$C_WARN";;
      *) colored="$(colorize_size "${SIZE[$item]}" "$plain")"; status="prüfbar"; status_color="$C_OK";;
    esac
    title="${TITLE[$item]}"
    if [ "${#title}" -gt "$desc_w" ]; then
      cut=$((desc_w - 1))
      [ "$cut" -lt 1 ] && cut=1
      title="${title:0:$cut}…"
    fi
    if [ "$item" = kernel ] || [ "$item" = journal ]; then
      printf '  %s⚠ %s%s%-*s%s ' "$C_WARN" "$C_INFO" "$C_BOLD" "$((item_w - 2))" "$item" "$C_RESET"
    else
      printf '  %s%s%-*s%s ' "$C_INFO" "$C_BOLD" "$item_w" "$item" "$C_RESET"
    fi
    printf '%s' "$colored"
    pad_visible "$plain" "$size_w"
    printf ' %s%-*s%s %s\n' "$status_color" "$status_w" "$status" "$C_RESET" "$title"
  done
  for item in "${SEL[@]}"; do
    if [ -n "${DETAIL[$item]:-}" ]; then
      printf '\n%s%s[%s]%s\n%s%s%s\n' "$C_INFO" "$C_BOLD" "$item" "$C_RESET" "$C_MUTED" "${DETAIL[$item]%$'\n'}" "$C_RESET"
    fi
  done
  printf '\n%s%sGesamt-Platzersparnis (geschätzt): %s%s\n' "$C_GOLD" "$C_BOLD" "$(human "$total")" "$C_RESET"
}

# ---------------------------------------------------------------- Ausführen
state_dir() { printf '%s' "${XDG_STATE_HOME:-$HOME/.local/state}/dw-aufraeumer"; }

PROT=""
plog() { [ -n "$PROT" ] && printf '%s\n' "$*" >>"$PROT"; }

run_rec() {
  local rec="$1" mode
  local -a W
  IFS="$US" read -r -a W <<<"$rec"
  mode="${W[0]}"
  if [ "$mode" = R ] && [ "$(id -u)" -ne 0 ]; then
    if ! have "${DW_SUDO:-sudo}"; then err "sudo nicht gefunden; Root-Schritt kann nicht ausgeführt werden."; return 127; fi
    printf '%sRoot-Rechte werden jetzt für diesen einen Schritt angefragt.%s\n' "$C_DANGER" "$C_RESET" >&2
    "${DW_SUDO:-sudo}" "${W[@]:1}"
  else
    "${W[@]:1}"
  fi
}

execute() {
  local item rec n=0 steps=0 failed=0 okc=0 answer rc shown
  local -a todo=()
  for item in "${SEL[@]}"; do
    [ "${STAT[$item]}" = ok ] || continue
    [ -n "${CMDS[$item]:-}" ] || continue
    todo+=("$item")
  done
  if [ "${#todo[@]}" -eq 0 ]; then echo "Nichts auszuführen: für die gewählten Posten gibt es nichts zu tun."; return 0; fi
  if [ "$YES" -ne 1 ] && [ ! -t 0 ]; then
    err "Keine Bestätigung möglich (keine interaktive Eingabe). Mit --ja ohne Rückfrage ausführen."; return 1
  fi
  for item in "${todo[@]}"; do
    case "$item" in paketcache|verwaist|kernel)
      if pm_locked; then err "Abbruch: Die Paketverwaltung ist gerade aktiv (Sperre). Bitte warten und erneut versuchen."; return 3; fi;;
    esac
  done
  local dir; dir="$(state_dir)"
  ( umask 077; mkdir -p "$dir" ) 2>/dev/null && chmod 700 "$dir" 2>/dev/null
  PROT="$dir/protokoll-$(date +%Y%m%d-%H%M%S).txt"
  ( umask 077; : >"$PROT" ) 2>/dev/null || { err "Protokoll kann nicht angelegt werden: $PROT"; return 1; }
  plog "DW Aufräumer $VERSION - Protokoll"
  plog "Start: $(date '+%F %T')  Paketverwaltung: ${PM:-keine}  Benutzer-ID: $(id -u)"
  steps="${#todo[@]}"
  for item in "${todo[@]}"; do
    n=$((n+1))
    printf '\n%s%sSchritt %d von %d: %s%s (%s)\n' "$C_INFO" "$C_BOLD" "$n" "$steps" "${TITLE[$item]}" "$C_RESET" "$(human "${SIZE[$item]}")"
    echo "Befehle:"
    while IFS= read -r rec; do
      [ -n "$rec" ] || continue
      shown="$(show_rec "$rec")"
      case "$shown" in
        sudo*)
          printf '%s%s┌──────────────────────────────────────────────┐%s\n' "$C_DANGER" "$C_BOLD" "$C_RESET"
          printf '%s%s│ %s%s\n' "$C_DANGER" "$C_BOLD" "$shown" "$C_RESET"
          printf '%s%s└──────────────────────────────────────────────┘%s\n' "$C_DANGER" "$C_BOLD" "$C_RESET"
          ;;
        *) printf '  %s\n' "$shown";;
      esac
    done <<<"${CMDS[$item]}"
    if [ "$YES" -ne 1 ]; then
      printf 'Diesen Schritt ausführen? [j/N] '
      read -r answer || answer=n
      case "$answer" in j|J|ja|Ja|JA|y|Y) ;; *) echo "Übersprungen."; plog "[$item] übersprungen"; continue;; esac
    fi
    plog "[$item] geplant: $(human "${SIZE[$item]}")"
    rc=0
    while IFS= read -r rec; do
      [ -n "$rec" ] || continue
      shown="$(show_rec "$rec")"
      plog "  Befehl: ${shown//"$HOME"/\~}"
      run_rec "$rec" || { rc=$?; break; }
    done <<<"${CMDS[$item]}"
    if [ "$rc" -eq 0 ]; then
      okc=$((okc+1)); printf '%s✓ Erledigt.%s\n' "$C_OK" "$C_RESET"; plog "  Ergebnis: in Ordnung"
    else
      failed=$((failed+1)); printf '%s✗ Fehlgeschlagen (Code %s).%s\n' "$C_DANGER" "$rc" "$C_RESET" >&2; plog "  Ergebnis: Fehler, Code $rc"
    fi
    plog "  Rückgängig: ${UNDO[$item]:-nicht nötig}"
    [ -n "${DETAIL[$item]:-}" ] && plog "${DETAIL[$item]%$'\n'}"
  done
  plog "Ende: $(date '+%F %T')  in Ordnung: $okc  fehlgeschlagen: $failed"
  printf '\nFertig: %d Schritt(e) in Ordnung, %d fehlgeschlagen.\nProtokoll: %s\n' "$okc" "$failed" "$PROT"
  [ "$failed" -eq 0 ]
}

# ---------------------------------------------------------------- Hauptprogramm
NO_ARGS=0
[ $# -eq 0 ] && NO_ARGS=1

while [ $# -gt 0 ]; do
  case "$1" in
    --probelauf) EXEC=0;;
    --ausfuehren) EXEC=1;;
    --posten) shift; [ $# -gt 0 ] || { err "--posten benötigt eine Liste."; exit 2; }; SELECTION="$1";;
    --journal-kuerzen|--journal-vakuum)
      shift; [ $# -gt 0 ] || { err "--journal-kuerzen benötigt eine Größe."; exit 2; }
      [[ "$1" =~ ^[0-9]+[KMGkmg]$ ]] || { err "Ungültige Größe: $1 (Beispiel: 200M, 1G)"; exit 2; }
      JMAX="${1^^}";;
    --paketmanager) shift; [ $# -gt 0 ] || { err "--paketmanager benötigt einen Namen."; exit 2; }; DW_PKGMGR="$1";;
    --ja) YES=1;;
    --liste) list_items; exit 0;;
    --version) echo "DW Aufräumer $VERSION"; exit 0;;
    -h|--help|--hilfe) usage; exit 0;;
    *) err "Unbekannte Option: $1"; usage >&2; exit 2;;
  esac
  shift
done

# Ohne Parameter: interaktive Checkliste nur in einem echten Terminal (stdin und stdout)
if [ "$NO_ARGS" -eq 1 ] && [ -t 0 ] && [ -t 1 ]; then
  interactive_select_items || exit $?
fi

SEL=()
if [ -n "$JMAX" ] && [ "$EXEC" -eq 0 ]; then
  echo "Hinweis: --journal-kuerzen $JMAX wirkt nur mit --ausfuehren; dies ist ein Probelauf." >&2
fi
if [ -z "$SELECTION" ] || [ "$SELECTION" = alle ]; then
  SEL=("${ITEMS[@]}")
else
  IFS=',' read -r -a want <<<"$SELECTION"
  for w in "${want[@]}"; do
    ok=0; for i in "${ITEMS[@]}"; do [ "$i" = "$w" ] && ok=1; done
    if [ "$ok" -eq 0 ]; then err "Unbekannter Posten: $w (siehe --liste)"; exit 2; fi
    dup=0; for i in "${SEL[@]}"; do [ "$i" = "$w" ] && dup=1; done
    [ "$dup" -eq 0 ] && SEL+=("$w")
  done
fi

detect_pm
for it in "${SEL[@]}"; do
  TITLE[$it]="$it"; SIZE[$it]=0; STAT[$it]=ok; CMDS[$it]=""
  "scan_$it"
done

if [ "$EXEC" -eq 0 ]; then
  printf '%s%sDW Aufräumer %s - PROBELAUF%s %s(es wird nichts verändert)%s\n' "$C_INFO" "$C_BOLD" "$VERSION" "$C_RESET" "$C_MUTED" "$C_RESET"
  printf '%sPaketverwaltung: %s%s\n' "$C_MUTED" "${PM:-nicht erkannt}" "$C_RESET"
  echo
  report
  echo
  printf '%sZum Aufräumen: dw-aufraeumer --ausfuehren [--posten <Liste>]%s   (Befehle werden vorher angezeigt, Rückfrage je Posten)\n' "$C_INFO" "$C_RESET"
  exit 0
fi

printf '%s%sDW Aufräumer %s - AUSFÜHREN%s\n' "$C_INFO" "$C_BOLD" "$VERSION" "$C_RESET"
printf '%sPaketverwaltung: %s%s\n' "$C_MUTED" "${PM:-nicht erkannt}" "$C_RESET"
echo
report
execute
exit $?
