#!/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-bootzeit-doktor/Theme.sh" ]]; then source "$DW_THEME_DIR/../share/dw-bootzeit-doktor/Theme.sh"
else C_RESET=""; C_BOLD=""; C_INFO=""; C_GOLD=""; C_OK=""; C_WARN=""; C_DANGER=""; C_MUTED=""; fi
# DW Bootzeit-Doktor fuer Linux 1.1.1
# Wertet systemd-analyze (time, blame, critical-chain), fehlgeschlagene Dienste und Fehlermeldungen des
# aktuellen Starts aus und erklaert sie in Klartext. Nur lesende Befehle, nur Vorschlaege, kein Netzwerk,
# keine Telemetrie, kein Root noetig.
# MIT-Lizenz, (c) 2026 Michael Bergfeld, DoMeZos-Ware
set -u
export LC_ALL=C.UTF-8 2>/dev/null || export LC_ALL=C
VERSION="1.1.1"
COUNT=10; FULL=0; HTMLOUT=""; TESTDIR=""
OUT=""; FORMAT="text"

usage() {
cat <<'EOF'
DW Bootzeit-Doktor 1.1.1 - Warum startet mein Linux langsam? Der Systemstart in Klartext

Aufruf:
  dw-bootzeit-doktor [Optionen]

Optionen:
  --html DATEI          Auswertung als HTML-Datei speichern (statt Text auf dem Bildschirm)
  --anzahl N            so viele langsamste Dienste zeigen (1 bis 50, Standard 10)
  --voll                Rechner- und Benutzernamen NICHT maskieren
  --testdaten ORDNER    gespeicherte Ausgaben auswerten (Dateien time.txt, blame.txt, critical-chain.txt,
                        failed.txt, journal-err.txt; optional hostname.txt, user.txt)
  --version             Version anzeigen
  -h, --hilfe, --help   diese Hilfe

Was das Programm tut:
  Es ruft nur lesende Befehle auf (systemd-analyze, systemctl --failed, journalctl -p err -b), erklärt die
  Ergebnisse in verständlichem Deutsch und zeigt sichere Vorschläge als Befehl zum Kopieren, jeweils mit
  Erklärung und Rückweg. Es führt nie etwas davon selbst aus, braucht kein Root und nutzt kein Netzwerk.
  Rechner- und Benutzername werden in der Ausgabe maskiert (außer mit --voll).

Beispiele:
  dw-bootzeit-doktor
  dw-bootzeit-doktor --anzahl 20
  dw-bootzeit-doktor --html ~/bootzeit.html

Beendigungscodes: 0 in Ordnung, 1 Fehler (z. B. Datei nicht schreibbar), 2 falscher Aufruf, 3 systemd nicht aktiv
EOF
}

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

# ---------------------------------------------------------------- Ausgabe (Text oder HTML)
# Ersatztexte in Anfuehrungszeichen: in Bash >= 5.2 steht ein unquotiertes & im Ersatz fuer den Treffer
esc() { local s="$1"; s="${s//&/"&amp;"}"; s="${s//</"&lt;"}"; s="${s//>/"&gt;"}"; s="${s//\"/"&quot;"}"; printf '%s' "$s"; }
emit() { OUT+="$1"$'\n'; }
doc_begin() {
  if [ "$FORMAT" = html ]; then
    emit '<!DOCTYPE html>'
    emit '<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">'
    emit '<meta http-equiv="Content-Security-Policy" content="default-src '"'none'"'; style-src '"'unsafe-inline'"'; img-src data:">'
    emit "<title>Bootzeit-Doktor: Auswertung</title>"
    emit '<style>:root{--bg:#050d1f;--surface:#091428;--border:#12294f;--cyan:#00d4ff;--gold:#f0c040;--text:#f0f4ff;--muted:#8899bb;--danger:#ff5577;--warn:#f0c040;--ok:#00ffaa;--font-head:Syne,"Segoe UI",sans-serif;--font-mono:"JetBrains Mono",Consolas,monospace}body{background:var(--bg);color:var(--text);font-family:var(--font-mono);max-width:70rem;margin:0 auto;padding:1.5rem}h1,h2,th{color:var(--cyan);font-family:var(--font-head)}h1{font-size:2rem;font-weight:800}h2{border-bottom:1px solid var(--border)}.total{color:var(--gold);font-weight:800;font-size:1.25rem}table{width:100%;border-collapse:collapse}tr:nth-child(even){background:var(--surface)}th,td{padding:.7rem;text-align:left;border-bottom:1px solid var(--border)}pre,code{background:var(--surface);color:var(--cyan);padding:.4rem;border-radius:8px;overflow:auto}.log{color:var(--muted)}.badge{border-radius:99px;padding:.2rem .6rem;font-size:.8rem;font-weight:600}.badge-safe{background:var(--ok);color:#00131a}.badge-warn{background:var(--gold);color:#00131a}.badge-crit{background:var(--danger);color:#050d1f}.badge-unk{background:var(--muted);color:#050d1f}.timeline-bar{display:flex;min-height:2.2rem;border-radius:10px;overflow:hidden;margin:1rem 0}.timeline-bar>div{padding:.5rem;display:flex;align-items:center;justify-content:center;color:#050d1f;font-weight:700;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.timeline-ok{background:var(--ok)}.timeline-warn{background:var(--gold)}.timeline-crit{background:var(--danger);color:#f0f4ff}.dur-badge{display:inline-block;margin-left:.4rem;padding:.05rem .4rem;border-radius:999px;background:rgba(5,13,31,.35);font-size:.75rem}.note{color:var(--muted)}</style>'
    emit '</head><body><main>'
  fi
}
doc_end() { [ "$FORMAT" = html ] && emit '</main></body></html>'; return 0; }
h1() { if [ "$FORMAT" = html ]; then emit "<h1>$(esc "$1")</h1>"; else emit "${C_INFO}${C_BOLD}$1${C_RESET}"; emit "${C_INFO}${1//?/=}${C_RESET}"; fi; }
h2() {
  if [ "$FORMAT" = html ]; then emit "<h2>$(esc "$1")</h2>"
  elif [[ "$1" == Fehlgeschlagene* ]]; then emit ""; emit "${C_DANGER}${C_BOLD}✗ $1${C_RESET}"
  else emit ""; emit "${C_INFO}${C_BOLD}== $1 ==${C_RESET}"; fi
}
para() { if [ "$FORMAT" = html ]; then emit "<p>$(esc "$1")</p>"; else emit "$1"; fi; }
note() { if [ "$FORMAT" = html ]; then emit "<p class=\"note\">$(esc "$1")</p>"; else emit "${C_MUTED}$1${C_RESET}"; fi; }
bullet() { if [ "$FORMAT" = html ]; then emit "<ul><li>$(esc "$1")</li></ul>"; else emit "  ${C_INFO}-${C_RESET} $1"; fi; }
code() { # Befehl zum Kopieren
  if [ "$FORMAT" = html ]; then emit "<pre>\$ $(esc "$1")</pre>"
  else emit "${C_INFO}  \$ $1${C_RESET}"; fi
}
logline() { if [ "$FORMAT" = html ]; then emit "<pre class=\"log\">$(esc "$1")</pre>"; else emit "${C_MUTED}  $1${C_RESET}"; fi; }
tbl_begin() { # Spaltenbreiten (nur Text) in TW1..TW3; Kopfzeile folgt per trow
  if [ "$FORMAT" = html ]; then emit '<table>'; fi
}
tbl_end() { if [ "$FORMAT" = html ]; then emit '</table>'; fi; }
badge_html() {
  case "$1" in
    wichtig) printf '<span class="badge badge-crit">Systemkritisch</span>';;
    bedingt) printf '<span class="badge badge-warn">Prüfen</span>';;
    optional|unkritisch) printf '<span class="badge badge-safe">Gefahrlos abschaltbar</span>';;
    unbekannt) printf '<span class="badge badge-unk">Prüfen</span>';;
    *) printf '%s' "$(esc "$1")";;
  esac
}
trow() { # trow Kopf(0/1) A B C D
  local head="$1" a="$2" b="$3" c="$4" d="${5:-}"
  if [ "$FORMAT" = html ]; then
    local t=td; [ "$head" = 1 ] && t=th
    local cc; cc="$(esc "$c")"
    if [ "$head" != 1 ]; then
      case "$c" in wichtig|bedingt|optional|unkritisch|unbekannt) cc="$(badge_html "$c")";; esac
    fi
    emit "<tr><$t>$(esc "$a")</$t><$t>$(esc "$b")</$t><$t>$cc</$t><$t>$(esc "$d")</$t></tr>"
  else
    local line; line="$(printf '%-9s %-40s %-10s %s' "$a" "$b" "$c" "$d")"
    if [ "$head" = 1 ]; then emit "${C_INFO}${C_BOLD}${line%"${line##*[![:space:]]}"}${C_RESET}"
    else emit "${line%"${line##*[![:space:]]}"}"; fi
  fi
}

# ---------------------------------------------------------------- Zeitwerte
# Dauer wie "1min 3.412s", "929ms", "18us", "2.292s" in Sekunden (awk-Funktion, in Parsern eingebunden)
AWK_DUR='
function dur2s(str,   t, tot, num, unit, ok) {
  tot = 0; ok = 0
  while (match(str, /[0-9]+(\.[0-9]+)?(us|ms|min|h|d|s)/)) {
    t = substr(str, RSTART, RLENGTH); str = substr(str, RSTART + RLENGTH)
    match(t, /[0-9]+(\.[0-9]+)?/); num = substr(t, RSTART, RLENGTH) + 0; unit = substr(t, RSTART + RLENGTH)
    if (unit == "us") num /= 1000000; else if (unit == "ms") num /= 1000; else if (unit == "min") num *= 60; else if (unit == "h") num *= 3600; else if (unit == "d") num *= 86400
    tot += num; ok = 1
  }
  return ok ? tot : -1
}'

fmt_s() { # Sekunden -> lesbar
  awk -v s="$1" 'BEGIN{ if (s+0 >= 60) { m=int(s/60); printf "%d min %02d s", m, int(s-m*60+0.5) } else if (s+0 >= 10) { x=sprintf("%.1f",s); sub(/\./,",",x); printf "%s s", x } else if (s+0 >= 0.1) { x=sprintf("%.2f",s); sub(/\./,",",x); printf "%s s", x } else { printf "%d ms", s*1000+0.5 } }'
}

# ---------------------------------------------------------------- Erklaerungstabelle
# explain EINHEIT -> E_KNOWN, E_DESC, E_LVL (wichtig|bedingt|optional|unkritisch), E_COND, E_KIND (disable|touch)
E_KNOWN=0; E_DESC=""; E_LVL=""; E_COND=""; E_KIND="disable"
setE() { E_KNOWN=1; E_DESC="$1"; E_LVL="$2"; E_COND="${3:-}"; E_KIND="${4:-disable}"; }
explain() {
  local u="$1"
  E_KNOWN=0; E_DESC=""; E_LVL=""; E_COND=""; E_KIND="disable"
  case "$u" in
    NetworkManager-wait-online.service|systemd-networkd-wait-online.service)
      setE "Wartet beim Start, bis eine Netzwerkverbindung steht. Dienste, die das Netz brauchen, starten erst danach, der Start kann sich dadurch verzögern." optional "Nur abschalten, wenn beim Start nichts zwingend Netz braucht, zum Beispiel keine Netzlaufwerke (NFS/SMB) in /etc/fstab oder Server-Dienste, die das Netz beim Start voraussetzen. Das WLAN verbindet sich weiterhin von selbst.";;
    NetworkManager.service) setE "Verwaltet WLAN, LAN und andere Netzwerkverbindungen." wichtig;;
    systemd-networkd.service) setE "Richtet das Netzwerk ein (häufig auf Servern)." wichtig;;
    wpa_supplicant.service) setE "Baut verschlüsselte WLAN-Verbindungen auf." wichtig;;
    systemd-resolved.service) setE "Übersetzt Internetadressen in Zahlen (Namensauflösung)." wichtig;;
    systemd-timesyncd.service|chrony.service|chronyd.service) setE "Hält die Uhrzeit über das Netz richtig." unkritisch;;
    ModemManager.service) setE "Verwaltet Mobilfunk-Modems und UMTS-/LTE-Sticks." optional "Nur abschalten, wenn Sie kein Mobilfunkmodem oder keinen Mobilfunk-Stick am Rechner nutzen.";;
    bluetooth.service) setE "Ermöglicht Bluetooth (Maus, Tastatur, Kopfhörer)." optional "Nur abschalten, wenn Sie kein Bluetooth nutzen.";;
    cups.service|cups.socket|cups.path) setE "Druckdienst: Drucker einrichten und drucken." bedingt "Ohne diesen Dienst gibt es keine Druckfunktion.";;
    cups-browsed.service) setE "Sucht Netzwerkdrucker im Heimnetz automatisch." optional "Nur abschalten, wenn Sie keine Netzwerkdrucker automatisch finden lassen wollen. Bereits eingerichtete Drucker bleiben nutzbar.";;
    avahi-daemon.service|avahi-daemon.socket) setE "Findet Geräte im Heimnetz (Drucker, Chromecast, Dateifreigaben) automatisch." bedingt "Wird von einigen Programmen zum Finden von Geräten benötigt.";;
    snapd.service|snapd.socket|snapd.seeded.service|snapd.apparmor.service|snapd.autoimport.service|snapd.core-fixup.service|snapd.recovery-chooser-trigger.service)
      setE "Verwaltet Snap-Pakete. Beim ersten Start nach Updates wird die Snap-Datenbank geprüft, das kann einige Sekunden dauern." bedingt "Unter Ubuntu sind einige Programme (zum Beispiel Firefox) Snap-Pakete. Nur abschalten, wenn keine Snap-Programme genutzt werden.";;
    snap-*.mount|snap.*.service|snap.*.scope) setE "Eingebundenes oder gestartetes Snap-Paket." unkritisch;;
    apt-daily.service|apt-daily-upgrade.service|apt-daily.timer|apt-daily-upgrade.timer)
      setE "Prüft automatisch auf Paketaktualisierungen (Debian/Ubuntu)." unkritisch;;
    unattended-upgrades.service) setE "Installiert Sicherheitsaktualisierungen automatisch." bedingt "Schützt das System; nicht abschalten, wenn Sie Updates nicht selbst einspielen.";;
    packagekit.service) setE "Hintergrunddienst für Softwareverwaltung und Aktualisierungen mit grafischen Programmen." bedingt "Wird von Software-Center und Aktualisierungsverwaltung genutzt.";;
    man-db.service|e2scrub_reap.service|fstrim.service|logrotate.service|motd-news.service|systemd-tmpfiles-clean.service|plocate-updatedb.service|mlocate.service)
      setE "Routineaufgabe im Hintergrund (Aufräumen, Index erneuern oder Ähnliches)." unkritisch;;
    plymouth-quit-wait.service|plymouth-quit.service|plymouth-start.service|plymouth-read-write.service)
      setE "Zeigt oder beendet den grafischen Startbildschirm. Wartet auf das Ende des Starts, dauert also oft genau so lange wie die übrigen Dienste." unkritisch;;
    cloud-init.service|cloud-init-local.service|cloud-config.service|cloud-final.service|cloud-init-main.service|cloud-init-network.service)
      setE "Richtet Cloud-Server beim Start automatisch ein (Netzwerk, Benutzer, Schlüssel). Auf einem normalen Heim-PC ohne Cloud-Anbindung überflüssig." optional "Nur abschalten, wenn der Rechner kein Cloud- oder virtueller Server ist. Auf Cloud-Servern nicht abschalten." touch;;
    whoopsie.service|apport.service|kerneloops.service|ubuntu-report.service)
      setE "Sendet oder sammelt Fehlerberichte zu Programmabstürzen." optional "Nur abschalten, wenn Sie keine Absturzberichte sammeln wollen.";;
    speech-dispatcher.service) setE "Sprachausgabe (Vorlesen) für Barrierefreiheit." bedingt "Nicht abschalten, wenn Sie Vorlesefunktionen nutzen.";;
    fwupd.service|fwupd-refresh.service) setE "Firmware-Aktualisierungen für Geräte (BIOS/UEFI, SSD, Dockingstation)." bedingt "Ohne diesen Dienst erscheinen keine Firmware-Updates.";;
    smartd.service|smartmontools.service) setE "Überwacht die Gesundheit von Festplatten und SSDs (S.M.A.R.T.)." bedingt "Warnt vor drohenden Plattendefekten; besser aktiv lassen.";;
    udisks2.service) setE "Hängt USB-Sticks und Laufwerke im Desktop ein." wichtig;;
    accounts-daemon.service) setE "Verwaltet Benutzerkonten für die grafische Anmeldung." wichtig;;
    colord.service) setE "Verwaltet Farbprofile für Bildschirme und Drucker." bedingt;;
    geoclue.service) setE "Ortungsdienst für Programme, die den Standort brauchen." bedingt;;
    switcheroo-control.service) setE "Wechselt bei Notebooks mit zwei Grafikchips zwischen diesen." bedingt;;
    power-profiles-daemon.service|thermald.service|irqbalance.service|tlp.service|upower.service)
      setE "Steuert Energiesparen, Temperatur oder Lastverteilung." bedingt;;
    lightdm.service|gdm.service|gdm3.service|sddm.service|display-manager.service)
      setE "Grafische Anmeldung (Anmeldebildschirm)." wichtig;;
    ssh.service|sshd.service|ssh.socket) setE "Fernzugriff per SSH auf diesen Rechner." bedingt "Nur abschalten, wenn Sie nie aus der Ferne auf diesen Rechner zugreifen.";;
    docker.service|docker.socket|containerd.service) setE "Container-Laufzeit (Docker)." bedingt "Nur abschalten, wenn Sie keine Docker-Container nutzen.";;
    libvirtd.service|virtlogd.service|virtqemud.service) setE "Verwaltung virtueller Maschinen (KVM/QEMU)." bedingt "Nur abschalten, wenn Sie keine virtuellen Maschinen mit libvirt nutzen.";;
    ufw.service|firewalld.service|nftables.service) setE "Firewall: schützt vor unerwünschten Netzwerkzugriffen." wichtig;;
    apparmor.service) setE "Sicherheitsprofile, die Programme einschränken." wichtig;;
    rsyslog.service|systemd-journald.service|systemd-journal-flush.service) setE "Schreibt Systemprotokolle." wichtig;;
    systemd-udevd.service|systemd-udev-trigger.service|systemd-udev-settle.service)
      case "$u" in
        systemd-udev-settle.service) setE "Wartet, bis alle Geräte erkannt sind. Kann den Start deutlich verzögern, wird aber von manchen Speicher-Einrichtungen (LVM, ZFS, RAID) noch gebraucht." bedingt "Nur nach Prüfung abschalten; bei LVM, ZFS oder RAID nicht.";;
        *) setE "Erkennt Hardware und legt Gerätedateien an." wichtig;;
      esac;;
    systemd-logind.service) setE "Verwaltet Anmeldungen und Sitzungen." wichtig;;
    systemd-random-seed.service|systemd-sysctl.service|systemd-modules-load.service|systemd-remount-fs.service|systemd-tmpfiles-setup.service|systemd-tmpfiles-setup-dev.service|systemd-update-utmp.service|systemd-binfmt.service|systemd-rfkill.service|systemd-backlight@*.service|systemd-vconsole-setup.service|keyboard-setup.service|console-setup.service|kmod-static-nodes.service)
      setE "Grundlegende Systemvorbereitung beim Start." wichtig;;
    lvm2-monitor.service|lvm2-pvscan@*.service|dm-event.service) setE "Überwacht und aktiviert logische Datenträger (LVM)." bedingt "Wird bei LVM-Einrichtung gebraucht.";;
    user@*.service|user-runtime-dir@*.service) setE "Startet die Benutzersitzung (Hintergrunddienste Ihres Kontos)." wichtig;;
    systemd-fsck@*.service|systemd-fsck-root.service) setE "Prüft ein Dateisystem vor dem Einhängen. Dauert nach unsauberem Herunterfahren oder alle paar Starts länger." unkritisch;;
    gpu-manager.service) setE "Wählt beim Start den passenden Grafiktreiber (Ubuntu)." unkritisch;;
    wsl-pro.service|landscape-client.service) setE "Zusatzdienst für Ubuntu Pro bzw. Landscape-Verwaltung." bedingt "Nur relevant, wenn Sie Ubuntu Pro oder Landscape nutzen.";;
    -.mount|*.mount) setE "Einhängepunkt: Ein Laufwerk oder Ordner wird ins Dateisystem eingebunden." unkritisch;;
    dev-*.device|sys-*.device) setE "Gerät: Hardware wurde erkannt. Eine lange Zeit bedeutet meist, dass auf ein Gerät oder Laufwerk gewartet wird." unkritisch;;
    *.target) setE "Sammelpunkt (Ziel) im Startablauf, kein einzelnes Programm." unkritisch;;
    *.slice|*.scope|*.socket|*.timer|*.path) setE "Verwaltungseinheit von systemd (Gruppe, Auslöser oder Zeitplan)." unkritisch;;
  esac
}

# ---------------------------------------------------------------- Daten holen
TMPD="$(mktemp -d)" || { err "Temporärer Ordner nicht anlegbar."; exit 1; }
trap 'rm -rf "$TMPD"' EXIT

run_ro() { # lesenden Befehl mit Zeitbegrenzung ausfuehren
  if have timeout; then timeout 60 "$@" 2>/dev/null; else "$@" 2>/dev/null; fi
}

collect() {
  if [ -n "$TESTDIR" ]; then
    local f
    for f in time blame critical-chain failed journal-err; do
      if [ -f "$TESTDIR/$f.txt" ]; then cat -- "$TESTDIR/$f.txt" >"$TMPD/$f"; else : >"$TMPD/$f"; fi
    done
    if [ -f "$TESTDIR/hostname.txt" ]; then HOSTN="$(head -n1 -- "$TESTDIR/hostname.txt")"; else HOSTN=""; fi
    if [ -f "$TESTDIR/user.txt" ]; then USERN="$(head -n1 -- "$TESTDIR/user.txt")"; else USERN=""; fi
    return 0
  fi
  if ! have systemd-analyze || [ ! -d /run/systemd/system ]; then
    err "systemd ist auf diesem System nicht aktiv (oder systemd-analyze fehlt)."
    err "Der Bootzeit-Doktor wertet systemd-Ausgaben aus und kann hier nichts untersuchen."
    err "Hinweis: Unter WSL lässt sich systemd in /etc/wsl.conf mit [boot] systemd=true aktivieren. Mit --testdaten ORDNER lassen sich gespeicherte Ausgaben auswerten."
    exit 3
  fi
  run_ro systemd-analyze time >"$TMPD/time"
  run_ro systemd-analyze blame --no-pager >"$TMPD/blame"
  run_ro systemd-analyze critical-chain --no-pager >"$TMPD/critical-chain"
  run_ro systemctl --failed --no-legend --plain --no-pager >"$TMPD/failed"
  run_ro journalctl -p err -b --no-pager -q >"$TMPD/journal-err"
  HOSTN="$(hostname 2>/dev/null || uname -n)"; USERN="$(id -un 2>/dev/null)"
}

# ---------------------------------------------------------------- Auswertung
parse_time() { # -> TSV: teil <name> <sek> | gesamt <sek> | ziel <name> <sek> | roh <zeile>
  awk "$AWK_DUR"'
    /Startup finished in/ {
      line=$0; sub(/.*Startup finished in /,"",line); total=-1
      if (index(line," = ")>0) { tpos=index(line," = "); tot=substr(line,tpos+3); line=substr(line,1,tpos-1); total=dur2s(tot) }
      n=split(line, parts, / \+ /); sum=0
      for (i=1;i<=n;i++) { p=parts[i]; if (match(p,/\(([A-Za-z-]+)\)/)) { name=substr(p,RSTART+1,RLENGTH-2); d=dur2s(substr(p,1,RSTART-1)); if (d>=0) { printf "teil\t%s\t%.6f\n", name, d; sum+=d } } }
      if (total<0) total=sum
      printf "gesamt\t%.6f\n", total; found=1; next }
    / reached after / { t=$0; name=$1; sub(/.* reached after /,"",t); d=dur2s(t); if (d>=0) printf "ziel\t%s\t%.6f\n", name, d; next }
    END { if (!found) print "keine" }' "$TMPD/time"
}
parse_blame() { # -> TSV: sek <TAB> einheit, absteigend
  awk "$AWK_DUR"'
    NF>=2 { unit=$NF; dur=$0; sub(/[ \t]+[^ \t]+[ \t]*$/,"",dur); d=dur2s(dur)
            if (d>=0 && unit ~ /^[A-Za-z0-9:_.@\\-]+\.[a-z]+$/) printf "%.6f\t%s\n", d, unit }' "$TMPD/blame" | sort -t$'\t' -k1,1 -rn
}
parse_chain() { # -> TSV: einheit <TAB> ab <TAB> dauer
  awk "$AWK_DUR"'
    /@/ { l=$0; gsub(/^[ │├└─]+/,"",l)
          if (l !~ /^[A-Za-z0-9:_.@\\-]+\.[a-z]+ @/) next
          u=l; sub(/ .*/,"",u); at=l; sub(/^[^@]*@/,"",at); sub(/ .*/,"",at)
          pl=""; if (index(l," +")>0) { pl=l; sub(/.* \+/,"",pl); sub(/ .*/,"",pl) }
          a=dur2s(at); p=(pl=="")?0:dur2s(pl); if (a<0) a=0; if (p<0) p=0
          printf "%s\t%.6f\t%.6f\n", u, a, p }' "$TMPD/critical-chain"
}
parse_failed() { # -> Einheiten
  awk '{ l=$0; sub(/^[^A-Za-z0-9]+/,"",l); u=l; sub(/[ \t].*/,"",u); if (u ~ /^[A-Za-z0-9:_.@\\-]+\.[a-z]+$/) print u }' "$TMPD/failed"
}
parse_journal() { # -> "n <Anzahl>" und "q <Anzahl> <Quelle>" (Top 5) und "z <Zeile>" (erste 5)
  awk '
    /^-- / || /^Hint:/ || /^[ \t]*$/ { next }
    { n++; src=$5; sub(/\[.*/,"",src); sub(/:$/,"",src); if (src=="") src="(unbekannt)"; c[src]++; if (n<=5) { z[n]=$0 } }
    END { print "n\t" n+0; for (s in c) printf "q\t%d\t%s\n", c[s], s; for (i=1;i<=5 && i<=n;i++) print "z\t" z[i] }' "$TMPD/journal-err"
}

mask_val() { local v="$1"; if [ "${#v}" -gt 4 ]; then printf '****%s' "${v: -2}"; else printf '****'; fi; }

# Ersetzt Rechner- und Benutzername als ganze Woerter
mask_output() {
  local h="$1" u="$2" hm="" um=""
  [ -n "$h" ] && [ "${#h}" -ge 2 ] && [ "$h" != localhost ] && hm="$(mask_val "$h")"
  [ -n "$u" ] && [ "${#u}" -ge 2 ] && [ "$u" != root ] && um="$(mask_val "$u")"
  awk -v h="$h" -v hm="$hm" -v u="$u" -v um="$um" '
    function maskword(s, w, rep,   out, i, n, prev, post) {
      if (w == "" || rep == "") return s
      out = ""; n = length(w); prev = " "
      while ((i = index(s, w)) > 0) {
        if (i > 1) prev = substr(s, i-1, 1)
        post = substr(s, i+n, 1)
        if (prev !~ /[A-Za-z0-9_.-]/ && post !~ /[A-Za-z0-9_-]/) { out = out substr(s,1,i-1) rep; prev = substr(rep, length(rep), 1) }
        else { out = out substr(s,1,i+n-1); prev = substr(w, n, 1) }
        s = substr(s, i+n)
      }
      return out s
    }
    { l = $0; l = maskword(l, h, hm); l = maskword(l, u, um); print l }'
}

build_report() {
  local now; now="$(date '+%d.%m.%Y %H:%M')"
  local tsv; tsv="$(parse_time)"
  local total="" fw=0 ld=0 kn=0 ir=0 us=0 haveparts=0 name sec kind
  if [ "$tsv" != keine ] && [ -n "$tsv" ]; then
    while IFS=$'\t' read -r kind name sec; do
      case "$kind" in
        teil) haveparts=1; case "$name" in
                firmware) fw="$sec";; loader) ld="$sec";; kernel) kn="$sec";; initrd) ir="$sec";; userspace) us="$sec";; esac;;
        gesamt) total="$name";;
      esac
    done <<<"$tsv"
  fi
  doc_begin
  h1 "DW Bootzeit-Doktor $VERSION: Auswertung des Systemstarts"
  if [ -n "$total" ]; then
    if [ "$FORMAT" = html ]; then emit "<p class=\"total\">Gesamte Startzeit: $(esc "$(fmt_s "$total")")</p>"
    else emit "${C_GOLD}${C_BOLD}Gesamte Startzeit: $(fmt_s "$total")${C_RESET}"; fi
  fi
  note "Erstellt am $now. Nur lesende Auswertung, es wurde nichts am System verändert."
  if [ "$FULL" -eq 1 ]; then note "Achtung: Rechner- und Benutzername sind unmaskiert. Prüfen Sie den Bericht vor dem Weitergeben."; fi

  # ---- Gesamtzeit
  h2 "Zusammenfassung"
  if [ "$tsv" = keine ] || [ -z "$tsv" ]; then
    para "Die Startzeit konnte nicht ermittelt werden."
    local raw; raw="$(head -n2 "$TMPD/time" | tr '\n' ' ')"
    [ -n "$raw" ] && note "Ausgabe von systemd-analyze time: $raw"
    note "Möglich ist, dass der Start noch nicht abgeschlossen ist oder systemd-analyze hier nichts liefert."
  else
    if [ -z "$TESTDIR" ] && [ -n "${HOME:-}" ]; then
      local state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/dw-bootzeit-doktor"
      local previous="" previous_date="" compare_label="letzter Lauf"
      if [ -r "$state_dir/last-seconds" ]; then read -r previous < "$state_dir/last-seconds" || true; fi
      if [ -r "$state_dir/last-date" ]; then read -r previous_date < "$state_dir/last-date" || true; fi
      [ -n "$previous_date" ] && [ "$previous_date" = "$(date -d yesterday +%F 2>/dev/null)" ] && compare_label="gestern"
      if [[ "$previous" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
        local difference absdiff
        difference="$(awk -v old="$previous" -v new="$total" 'BEGIN{printf "%.1f", old-new}')"
        absdiff="${difference#-}"
        if awk -v d="$difference" 'BEGIN{exit !(d>0)}'; then
          para "+ ${absdiff} s schneller als $compare_label"
        elif awk -v d="$difference" 'BEGIN{exit !(d<0)}'; then
          para "${absdiff} s langsamer als $compare_label"
        else
          para "gleiche Startzeit wie $compare_label"
        fi
      fi
      if mkdir -p -m 700 -- "$state_dir" 2>/dev/null; then
        printf "%s\n" "$total" > "$state_dir/last-seconds" 2>/dev/null || true
        date +%F > "$state_dir/last-date" 2>/dev/null || true
      fi
    fi
    local osn; osn="$(awk -v t="$total" -v f="$fw" -v l="$ld" 'BEGIN{ printf "%.6f", t-f-l }')"
    local rowc=0
    tbl_begin
    trow 1 "Dauer" "Abschnitt" "" "Bedeutung"
    if [ "$haveparts" -eq 1 ]; then
      awk -v v="$fw" 'BEGIN{exit !(v>0)}' && trow 0 "$(fmt_s "$fw")" "Firmware (BIOS/UEFI)" "" "Wird vom Rechner selbst vor dem Betriebssystem erledigt. Das System kann daran nichts ändern; im BIOS/UEFI lassen sich Schnellstart und überflüssige Startgeräte prüfen."
      awk -v v="$ld" 'BEGIN{exit !(v>0)}' && trow 0 "$(fmt_s "$ld")" "Bootloader" "" "Auswahlmenü (GRUB o. ä.). Eine lange Wartezeit kommt oft von der Menü-Anzeigedauer."
      awk -v v="$kn" 'BEGIN{exit !(v>0)}' && trow 0 "$(fmt_s "$kn")" "Kernel" "" "Der Linux-Kern lädt Treiber und startet."
      awk -v v="$ir" 'BEGIN{exit !(v>0)}' && trow 0 "$(fmt_s "$ir")" "Initrd" "" "Erste Phase vor dem eigentlichen System (z. B. Laufwerksentschlüsselung)."
      awk -v v="$us" 'BEGIN{exit !(v>0)}' && trow 0 "$(fmt_s "$us")" "Benutzerbereich" "" "Hier starten die Dienste. Dieser Abschnitt lässt sich am besten beeinflussen."
      rowc=1
    fi
    [ "$rowc" -eq 0 ] && trow 0 "$(fmt_s "$total")" "Gesamt" "" ""
    tbl_end
    if [ "$FORMAT" = html ] && [ "$haveparts" -eq 1 ]; then
      local sump knp irp usp
      sump="$(awk -v k="$kn" -v i="$ir" -v u="$us" 'BEGIN{s=k+i+u; if(s<=0)s=1; printf "%.6f", s}')"
      knp="$(awk -v v="$kn" -v s="$sump" 'BEGIN{printf "%d", (v/s)*100+0.5}')"
      irp="$(awk -v v="$ir" -v s="$sump" 'BEGIN{printf "%d", (v/s)*100+0.5}')"
      usp="$(awk -v k="$knp" -v i="$irp" 'BEGIN{n=100-k-i; if(n<0)n=0; print n}')"
      emit '<div class="timeline-bar" role="img" aria-label="Zeitstrahl des Systemstarts">'
      timeline_seg() {
        local sec="$1" pct="$2" label="$3" cls
        awk -v v="$sec" 'BEGIN{exit !(v>0)}' || return 0
        cls="timeline-ok"
        awk -v s="$sec" 'BEGIN{exit !(s>15)}' && cls="timeline-crit"
        awk -v s="$sec" 'BEGIN{exit !(s>5 && s<=15)}' && cls="timeline-warn"
        emit "<div class=\"$cls\" style=\"width:${pct}%\">$(esc "$label"): $(esc "$(fmt_s "$sec")")<span class=\"dur-badge\">$(esc "$(fmt_s "$sec")")</span></div>"
      }
      timeline_seg "$kn" "$knp" "Kernel"
      timeline_seg "$ir" "$irp" "Initrd"
      timeline_seg "$us" "$usp" "Userspace"
      emit '</div>'
    fi
    local verdict
    verdict="$(awk -v v="$osn" 'BEGIN{ if (v<20) print "zügig (unter 20 s ohne Firmware und Bootloader)"; else if (v<45) print "etwas langsam (20 bis 45 s ohne Firmware und Bootloader)"; else print "langsam (über 45 s ohne Firmware und Bootloader)" }')"
    para "Einordnung (Faustregel, ohne Firmware und Bootloader): $verdict."
    local zn zs
    IFS=$'\t' read -r _ zn zs < <(awk -F'\t' '$1=="ziel"{print; exit}' <<<"$tsv")
    [ -n "${zn:-}" ] && note "Zielzustand: $zn nach $(fmt_s "$zs") im Benutzerbereich."
  fi

  # ---- Kritischer Pfad
  declare -gA CRIT=()
  local chain; chain="$(parse_chain)"
  local cu cp
  while IFS=$'\t' read -r cu _ cp; do [ -n "$cu" ] && CRIT["$cu"]="$cp"; done <<<"$chain"

  # ---- Langsamste Dienste
  h2 "Die $COUNT langsamsten Einheiten (systemd-analyze blame)"
  local blame; blame="$(parse_blame)"
  declare -ga SUG_UNIT=() SUG_SEC=()
  declare -A UNKNOWN_SLOW=()
  if [ -z "$blame" ]; then
    para "Keine auswertbare Liste erhalten."
  else
    note "Hinweis: Dienste starten parallel. Eine lange Zeit hier ist nur dann ein Bremsklotz, wenn die Einheit auf dem kritischen Pfad liegt (siehe unten)."
    [ "$FORMAT" = html ] && tbl_begin && trow 1 "Dauer" "Einheit" "Rolle" "Erklärung"
    [ "$FORMAT" != html ] && emit "${C_INFO}${C_BOLD}$(printf '  %-40s %8s  %-12s  %s' 'Dienst' 'Sekunden' 'Rolle' 'Erklärung')${C_RESET}"
    local i=0 bsec bunit role onpath desc
    while IFS=$'\t' read -r bsec bunit; do
      [ -n "$bunit" ] || continue
      i=$((i+1)); [ "$i" -gt "$COUNT" ] && break
      explain "$bunit"
      if [ "$E_KNOWN" -eq 1 ]; then role="$E_LVL"; else role="unbekannt"; fi
      onpath=""; [ -n "${CRIT[$bunit]+x}" ] && onpath=" [kritischer Pfad]"
      if [ "$E_KNOWN" -eq 1 ]; then desc="$E_DESC$onpath"; else desc="unbekannt (nicht in der mitgelieferten Erklärungstabelle)$onpath"; fi
      if [ "$FORMAT" = html ]; then trow 0 "$(fmt_s "$bsec")" "$bunit" "$role" "$desc"
      else
         local message color
         message="$(printf '  %-40s %8ss  %-12s  %s' "$bunit" "$(awk -v s="$bsec" 'BEGIN{printf "%.1f", s}')" "$role" "$desc")"
         color="$C_RESET"
         if awk -v s="$bsec" 'BEGIN{exit !(s>15)}'; then color="$C_DANGER"; elif awk -v s="$bsec" 'BEGIN{exit !(s>5)}'; then color="$C_WARN"; fi
         emit "${color}${message}${C_RESET}"
       fi
      if [ "$E_KNOWN" -eq 1 ] && [ "$E_LVL" = optional ]; then
        if awk -v s="$bsec" -v p="${CRIT[$bunit]:-0}" 'BEGIN{exit !(s>=2 || p>=1)}'; then SUG_UNIT+=("$bunit"); SUG_SEC+=("$bsec"); fi
      fi
      if [ "$E_KNOWN" -eq 0 ] && awk -v s="$bsec" 'BEGIN{exit !(s>=5)}'; then UNKNOWN_SLOW["$bunit"]="$bsec"; fi
    done <<<"$blame"
    [ "$FORMAT" = html ] && tbl_end
    note "Rolle: wichtig = Systemgrundlage, nicht anfassen. bedingt = je nach Nutzung nötig. optional = oft entbehrlich (siehe Vorschläge). unkritisch = Routine oder harmlos. unbekannt = nicht in der Tabelle."
  fi

  h2 "Kritischer Pfad (systemd-analyze critical-chain)"
  if [ -z "$chain" ]; then
    para "Keine auswertbare Kette erhalten."
  else
    para "Diese Einheiten bestimmen die Gesamtzeit des Starts, weil jede auf die vorige wartet. Nur hier bringt Beschleunigen etwas."
    tbl_begin
    trow 1 "Dauer" "Einheit" "Rolle" "Erklärung"
    local n=0
    while IFS=$'\t' read -r cu _ cp; do
      [ -n "$cu" ] || continue
      awk -v p="$cp" 'BEGIN{exit !(p>=0.5)}' || continue
      n=$((n+1)); [ "$n" -gt 12 ] && break
      explain "$cu"
      if [ "$E_KNOWN" -eq 1 ]; then trow 0 "$(fmt_s "$cp")" "$cu" "$E_LVL" "$E_DESC"; else trow 0 "$(fmt_s "$cp")" "$cu" "unbekannt" "unbekannt (nicht in der mitgelieferten Erklärungstabelle)"; fi
      if [ "$E_KNOWN" -eq 1 ] && [ "$E_LVL" = optional ]; then
        local dup=0 x; for x in "${SUG_UNIT[@]}"; do [ "$x" = "$cu" ] && dup=1; done
        awk -v p="$cp" 'BEGIN{exit !(p>=1)}' && [ "$dup" -eq 0 ] && { SUG_UNIT+=("$cu"); SUG_SEC+=("$cp"); }
      fi
    done <<<"$chain"
    tbl_end
    [ "$n" -eq 0 ] && note "Auf dem kritischen Pfad liegt keine Einheit mit mehr als 0,5 s Dauer."
  fi

  # ---- fehlgeschlagene Dienste
  h2 "Fehlgeschlagene Dienste (systemctl --failed)"
  local failed; failed="$(parse_failed)"
  declare -ga FAILED_UNITS=()
  if [ -z "$failed" ]; then
    para "Keine fehlgeschlagenen Dienste. Das ist gut."
  else
    local fu
    while IFS= read -r fu; do
      [ -n "$fu" ] || continue
      FAILED_UNITS+=("$fu")
      explain "$fu"
      if [ "$E_KNOWN" -eq 1 ]; then bullet "$fu ($E_LVL): $E_DESC Der Dienst ist beim Start gescheitert."
      else bullet "$fu (unbekannt, nicht in der mitgelieferten Erklärungstabelle): Der Dienst ist beim Start gescheitert."; fi
    done <<<"$failed"
    note "Ein fehlgeschlagener Dienst verlängert den Start oft, weil auf ihn gewartet wird, bis eine Frist abläuft. Ursache siehe Vorschläge unten."
  fi

  # ---- Journal
  h2 "Fehlermeldungen dieses Starts (journalctl -p err -b)"
  local jt jn=0 jk jv1 jv2
  jt="$(parse_journal)"
  declare -a JQ=() JZ=()
  while IFS=$'\t' read -r jk jv1 jv2; do
    case "$jk" in n) jn="$jv1";; q) JQ+=("$jv1"$'\t'"$jv2");; z) JZ+=("$jv1");; esac
  done <<<"$jt"
  if [ "${jn:-0}" -eq 0 ]; then
    para "Keine Fehlermeldungen (Stufe err) seit dem Start gefunden, oder das Journal ist für Ihr Konto nicht lesbar."
    note "Hinweis: Ohne Mitgliedschaft in den Gruppen adm oder systemd-journal sieht ein normaler Benutzer nur eigene Meldungen."
  else
    para "$jn Fehlermeldung(en) seit dem Start. Häufigste Quellen:"
    local q
    while IFS= read -r q; do [ -n "$q" ] && bullet "${q%%$'\t'*}x von ${q#*$'\t'}"; done < <(printf '%s\n' "${JQ[@]}" | sort -t$'\t' -k1,1 -rn | head -5)
    para "Beispiele (erste Meldungen):"
    local z; for z in "${JZ[@]}"; do logline "$z"; done
    note "Nicht jede Fehlermeldung ist ein Problem. Wiederholte Meldungen derselben Quelle sind meist der Hinweis auf eine Ursache."
  fi

  # ---- Vorschlaege
  h2 "Vorschläge (nur zum Kopieren, nichts wird automatisch ausgeführt)"
  local nsug=0 sidx
  if [ "${#SUG_UNIT[@]}" -gt 0 ]; then
    for sidx in "${!SUG_UNIT[@]}"; do
      local su="${SUG_UNIT[$sidx]}" ss="${SUG_SEC[$sidx]}"
      explain "$su"
      nsug=$((nsug+1))
      para "$nsug) $su ($(fmt_s "$ss")) abschalten"
      note "Warum: $E_DESC"
      note "Wann sinnvoll: $E_COND"
      if [ "$E_KIND" = touch ]; then
        note "Befehl zum Abschalten:"; code "sudo touch /etc/cloud/cloud-init.disabled"
        note "Rückweg:"; code "sudo rm /etc/cloud/cloud-init.disabled"
      else
        note "Befehl zum Abschalten (wirkt ab dem nächsten Start):"; code "sudo systemctl disable $su"
        note "Rückweg:"; code "sudo systemctl enable $su"
      fi
    done
  fi
  if [ "${#FAILED_UNITS[@]}" -gt 0 ]; then
    local fu2
    for fu2 in "${FAILED_UNITS[@]}"; do
      nsug=$((nsug+1))
      para "$nsug) Ursache für den Fehler von $fu2 herausfinden"
      note "Warum: Zeigt Status und die letzten Meldungen des Dienstes, ändert nichts."
      code "systemctl status $fu2"
      code "journalctl -u $fu2 -b --no-pager"
      note "Optional, wenn der Fehler bekannt und behoben oder bedeutungslos ist: nur die Fehlermarkierung löschen (repariert nichts):"
      code "sudo systemctl reset-failed $fu2"
    done
  fi
  local ukey
  for ukey in "${!UNKNOWN_SLOW[@]}"; do
    nsug=$((nsug+1))
    para "$nsug) $ukey ($(fmt_s "${UNKNOWN_SLOW[$ukey]}")) ist unbekannt und langsam"
    note "Warum: Die Einheit steht nicht in der mitgelieferten Erklärungstabelle. Es wird deshalb nichts zum Abschalten vorgeschlagen. So finden Sie heraus, wozu sie dient:"
    code "systemctl status $ukey"
    code "systemctl cat $ukey"
  done
  if [ "$nsug" -eq 0 ]; then para "Es gibt nichts Auffälliges, das sich sicher verbessern ließe."; fi

  h2 "Grenzen"
  bullet "Die Erklärungstabelle umfasst nur häufige Dienste. Unbekanntes ist als \"unbekannt\" markiert; das heißt nicht, dass es unwichtig ist."
  bullet "Vorschläge sind allgemeine Hinweise und keine Garantie für einen schnelleren Start. Prüfen Sie jeden Befehl, bevor Sie ihn ausführen."
  bullet "Abgeschaltete Dienste lassen sich mit dem jeweils genannten Rückweg wieder einschalten."
  if [ "$FULL" -eq 0 ]; then
    if [ "$FORMAT" = html ]; then note "Rechner- und Benutzername wurden maskiert."
    else emit "${C_MUTED}Rechner- und Benutzername wurden maskiert${C_RESET}"; fi
  fi
  doc_end
}

# ---------------------------------------------------------------- Hauptprogramm
while [ $# -gt 0 ]; do
  case "$1" in
    --html) FORMAT="html"; if [ $# -gt 1 ] && [[ "$2" != --* ]]; then shift; HTMLOUT="$1"; else HTMLOUT="bootzeit-$(date +%F).html"; fi;;
    --anzahl) shift; [ $# -gt 0 ] || { err "--anzahl benötigt eine Zahl."; exit 2; }
      [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 50 ] || { err "--anzahl muss zwischen 1 und 50 liegen."; exit 2; }
      COUNT="$1";;
    --voll) FULL=1;;
    --testdaten) shift; [ $# -gt 0 ] || { err "--testdaten benötigt einen Ordner."; exit 2; }; TESTDIR="$1"
      [ -d "$TESTDIR" ] || { err "Ordner nicht gefunden: $TESTDIR"; exit 2; };;
    --version) echo "DW Bootzeit-Doktor $VERSION"; exit 0;;
    -h|--help|--hilfe) usage; exit 0;;
    *) err "Unbekannte Option: $1"; usage >&2; exit 2;;
  esac
  shift
done

if [ -n "$HTMLOUT" ] && [ -e "$HTMLOUT" ]; then
  err "Die Datei $HTMLOUT existiert bereits und wird nicht überschrieben. Bitte anderen Namen wählen oder die Datei vorher selbst entfernen."
  exit 1
fi

HOSTN=""; USERN=""
collect
build_report
if [ "$FULL" -eq 1 ]; then FINAL="$OUT"; else FINAL="$(printf '%s' "$OUT" | mask_output "$HOSTN" "$USERN")"; fi

if [ -n "$HTMLOUT" ]; then
  if ! ( set -o noclobber; printf '%s\n' "$FINAL" >"$HTMLOUT" ) 2>/dev/null; then
    err "Datei kann nicht geschrieben werden: $HTMLOUT"; exit 1
  fi
  echo "Bericht gespeichert: $HTMLOUT"
  if [ -t 0 ] && [ -t 1 ]; then
    ans=""
    read -r -p "Bericht jetzt im Browser ansehen? [J/n]: " ans || ans=n
    if [ -z "$ans" ] || [[ "$ans" =~ ^[JjYy] ]]; then
      if have xdg-open; then xdg-open "$HTMLOUT" >/dev/null 2>&1 || true
      else err "xdg-open nicht gefunden. Öffnen Sie die Datei selbst: $HTMLOUT"; fi
    fi
  fi
else
  printf '%s\n' "$FINAL"
fi
exit 0
