/* ==========================================================================
   PoE Manager — shared UI behaviour (sidebar, theme, modals, toasts)
   ========================================================================== */

(function () {
  "use strict";

  /* ---------------- Theme ---------------- */

  const THEME_KEY = "poe-theme";

  function applyTheme(theme) {
    document.documentElement.setAttribute("data-theme", theme);
    document.querySelectorAll("[data-theme-icon]").forEach((el) => {
      el.innerHTML = theme === "light" ? ICONS.moon : ICONS.sun;
    });
    // Favicon + Sidebar-Logo folgen demselben Theme wie der Rest der App
    // (auch bei manuellem Umschalten, nicht nur bei System-Präferenz) --
    // "dunkel"/"hell" bezeichnen hier die für den jeweiligen Modus gedachte
    // Logo-Variante (helle Farben für Dark Mode, dunkle für Light Mode).
    const suffix = theme === "light" ? "light" : "dark";
    const favicon = document.getElementById("app-favicon");
    if (favicon) favicon.href = "/static/images/icon-" + suffix + ".svg";
    const sidebarLogo = document.getElementById("sidebar-logo");
    if (sidebarLogo) sidebarLogo.src = "/static/images/logo-" + suffix + ".svg";
    const loginBgLogo = document.getElementById("login-bg-logo");
    if (loginBgLogo) loginBgLogo.src = "/static/images/logo-" + suffix + "-subline.svg";
  }

  function initTheme() {
    const saved = localStorage.getItem(THEME_KEY) ||
      (window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
    applyTheme(saved);
  }

  function toggleTheme() {
    const current = document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark";
    const next = current === "light" ? "dark" : "light";
    localStorage.setItem(THEME_KEY, next);
    applyTheme(next);
  }

  const ICONS = {
    sun: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>',
    moon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1111.2 3a7 7 0 009.8 9.8z"/></svg>',
  };

  /* ---------------- Sidebar (mobile) ---------------- */

  function initSidebar() {
    const sidebar = document.querySelector(".sidebar");
    const backdrop = document.querySelector(".sidebar-backdrop");
    const main = document.querySelector(".main");
    const toggles = document.querySelectorAll("[data-sidebar-toggle]");
    if (!sidebar) return;

    const DESKTOP_BREAKPOINT = 900;
    const COLLAPSE_KEY = "poe-sidebar-collapsed";

    // Mobile: temporäres Überlagern per Hamburger + Backdrop.
    function open() {
      sidebar.classList.add("open");
      backdrop && backdrop.classList.add("open");
    }
    function close() {
      sidebar.classList.remove("open");
      backdrop && backdrop.classList.remove("open");
    }

    // Desktop: dauerhaftes Ein-/Ausklappen, über Neuladen hinweg gemerkt.
    function setCollapsed(collapsed) {
      sidebar.classList.toggle("collapsed", collapsed);
      main && main.classList.toggle("sidebar-collapsed", collapsed);
      localStorage.setItem(COLLAPSE_KEY, collapsed ? "1" : "0");
    }

    if (localStorage.getItem(COLLAPSE_KEY) === "1") setCollapsed(true);

    toggles.forEach((btn) => btn.addEventListener("click", () => {
      if (window.innerWidth <= DESKTOP_BREAKPOINT) {
        sidebar.classList.contains("open") ? close() : open();
      } else {
        setCollapsed(!sidebar.classList.contains("collapsed"));
      }
    }));
    backdrop && backdrop.addEventListener("click", close);
    document.querySelectorAll(".nav-item").forEach((a) => a.addEventListener("click", close));
  }

  /* ---------------- Modals ---------------- */

  function openModal(id) {
    const el = document.getElementById(id);
    if (!el) return;
    el.classList.add("open");
    document.body.style.overflow = "hidden";
  }

  function closeModal(el) {
    const overlay = el.closest ? el.closest(".modal-overlay") : el;
    if (!overlay) return;
    overlay.classList.remove("open");
    document.body.style.overflow = "";
  }

  function initModals() {
    document.querySelectorAll("[data-open-modal]").forEach((trigger) => {
      trigger.addEventListener("click", () => openModal(trigger.getAttribute("data-open-modal")));
    });
    document.querySelectorAll("[data-close-modal]").forEach((btn) => {
      btn.addEventListener("click", () => closeModal(btn));
    });
    document.querySelectorAll(".modal-overlay").forEach((overlay) => {
      overlay.addEventListener("click", (e) => {
        if (e.target === overlay) closeModal(overlay);
      });
    });
    document.addEventListener("keydown", (e) => {
      if (e.key === "Escape") {
        document.querySelectorAll(".modal-overlay.open").forEach((o) => closeModal(o));
      }
    });
  }

  /* ---------------- Confirm dialog (replaces window.confirm) ---------------- */

  function ensureConfirmModal() {
    if (document.getElementById("confirm-modal")) return;
    const html = `
      <div class="modal-overlay" id="confirm-modal">
        <div class="modal" style="max-width:380px;">
          <div class="modal-header">
            <h3 id="confirm-title">Bist du sicher?</h3>
            <button type="button" class="modal-close" data-close-modal>&times;</button>
          </div>
          <div class="modal-body">
            <p id="confirm-message" class="text-dim"></p>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
            <button type="button" class="btn btn-danger" id="confirm-ok" style="background:var(--danger);color:#fff;">Bestätigen</button>
          </div>
        </div>
      </div>`;
    document.body.insertAdjacentHTML("beforeend", html);
    document.querySelectorAll("#confirm-modal [data-close-modal]").forEach((btn) => {
      btn.addEventListener("click", () => closeModal(btn));
    });
    document.getElementById("confirm-modal").addEventListener("click", (e) => {
      if (e.target.id === "confirm-modal") closeModal(e.target);
    });
  }

  window.confirmAction = function (message, onConfirm, title) {
    ensureConfirmModal();
    document.getElementById("confirm-title").innerText = title || "Bist du sicher?";
    document.getElementById("confirm-message").innerText = message;
    const okBtn = document.getElementById("confirm-ok");
    const freshBtn = okBtn.cloneNode(true);
    okBtn.parentNode.replaceChild(freshBtn, okBtn);
    freshBtn.addEventListener("click", () => {
      closeModal(freshBtn);
      onConfirm();
    });
    openModal("confirm-modal");
  };

  /* ---------------- Ja/Nein-Dialog für ungespeicherte Änderungen ---------------- */
  /* Eigener Dialog statt confirmAction (dort ist "Abbrechen" = beim Bleiben,
     hier gibt es bewusst nur die zwei angefragten Optionen: speichern oder
     verwerfen — beide verlassen die Seite, nur "Abbrechen"/Escape/Backdrop
     bricht das Verlassen selbst ab und lässt die Seite unverändert offen. */
  function ensureSaveDiscardModal() {
    if (document.getElementById("save-discard-modal")) return;
    const html = `
      <div class="modal-overlay" id="save-discard-modal">
        <div class="modal" style="max-width:420px;">
          <div class="modal-header">
            <h3>Ungespeicherte Änderungen</h3>
            <button type="button" class="modal-close" data-close-modal>&times;</button>
          </div>
          <div class="modal-body">
            <p class="text-dim" id="save-discard-message"></p>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" id="save-discard-no">Nein, verwerfen</button>
            <button type="button" class="btn btn-primary" id="save-discard-yes">Ja, speichern</button>
          </div>
        </div>
      </div>`;
    document.body.insertAdjacentHTML("beforeend", html);
    document.querySelectorAll("#save-discard-modal [data-close-modal]").forEach((btn) => {
      btn.addEventListener("click", () => closeModal(btn));
    });
    document.getElementById("save-discard-modal").addEventListener("click", (e) => {
      if (e.target.id === "save-discard-modal") closeModal(e.target);
    });
  }

  window.confirmSaveDiscard = function (message, onSave, onDiscard) {
    ensureSaveDiscardModal();
    document.getElementById("save-discard-message").innerText = message;
    const yesBtn = document.getElementById("save-discard-yes");
    const noBtn = document.getElementById("save-discard-no");
    const freshYes = yesBtn.cloneNode(true);
    yesBtn.parentNode.replaceChild(freshYes, yesBtn);
    const freshNo = noBtn.cloneNode(true);
    noBtn.parentNode.replaceChild(freshNo, noBtn);
    freshYes.addEventListener("click", () => { closeModal(freshYes); onSave(); });
    freshNo.addEventListener("click", () => { closeModal(freshNo); onDiscard(); });
    openModal("save-discard-modal");
  };

  /* ---------------- RAW-Log-Popup (Live-Log & Verlauf) ---------------- */
  /* Gleiches Modal-Grundgerüst wie der Ja/Nein-Dialog für ungespeicherte
     Änderungen (modal-overlay/modal-header/modal-body/modal-footer), nur
     breiter/höher und mit lazy nachgeladenem Inhalt -- der komplette
     Log-Inhalt wird bewusst erst beim tatsächlichen Öffnen per fetch
     nachgeladen, nicht schon beim Seitenaufbau, damit genau das Problem
     (unnötig große Ladezeiten) nicht durch das Popup selbst zurückkommt. */
  function ensureRawLogModal() {
    if (document.getElementById("raw-log-modal")) return;
    const html = `
      <div class="modal-overlay" id="raw-log-modal">
        <div class="modal" style="max-width:min(1100px, 92vw); height:85vh;">
          <div class="modal-header">
            <h3 id="raw-log-modal-title">Komplettes Log (RAW)</h3>
            <button type="button" class="modal-close" data-close-modal>&times;</button>
          </div>
          <div class="modal-body" style="padding:0; display:flex; flex-direction:column; flex:1; min-height:0;">
            <pre id="raw-log-modal-content" class="raw-log-content"></pre>
          </div>
          <div class="modal-footer">
            <span class="text-faint mono" id="raw-log-modal-meta" style="margin-right:auto; font-size:11.5px;"></span>
            <button type="button" class="btn btn-secondary" data-close-modal>Schließen</button>
          </div>
        </div>
      </div>`;
    document.body.insertAdjacentHTML("beforeend", html);
    document.querySelectorAll("#raw-log-modal [data-close-modal]").forEach((btn) => {
      btn.addEventListener("click", () => closeModal(btn));
    });
    document.getElementById("raw-log-modal").addEventListener("click", (e) => {
      if (e.target.id === "raw-log-modal") closeModal(e.target);
    });
  }

  function renderRawLogLines(container, text) {
    // Eine Zeile = ein <div>, Nummer kommt per CSS-Counter (::before) --
    // so bleibt der eigentliche Zeileninhalt reiner textContent (kein XSS-
    // Risiko) und die Nummerierung muss nirgends von Hand mitgezählt werden.
    const lines = text.split("\n");
    if (lines.length && lines[lines.length - 1] === "") lines.pop(); // trailing \n erzeugt sonst eine Phantomzeile
    container.textContent = "";
    const frag = document.createDocumentFragment();
    lines.forEach((line) => {
      const row = document.createElement("div");
      row.className = "raw-log-line";
      row.textContent = line;
      frag.appendChild(row);
    });
    container.appendChild(frag);
  }

  window.openRawLogModal = function (url, title, withLineNumbers) {
    if (!url) return;
    ensureRawLogModal();
    document.getElementById("raw-log-modal-title").innerText = title || "Komplettes Log (RAW)";
    const content = document.getElementById("raw-log-modal-content");
    const meta = document.getElementById("raw-log-modal-meta");
    content.classList.toggle("with-line-numbers", !!withLineNumbers);
    content.textContent = "Lade …";
    meta.textContent = "";
    openModal("raw-log-modal");
    fetch(url)
      .then((r) => {
        const logName = r.headers.get("X-Log-Name");
        if (logName) meta.textContent = logName;
        return r.text();
      })
      .then((text) => {
        if (withLineNumbers) renderRawLogLines(content, text);
        else content.textContent = text;
      })
      .catch(() => { content.textContent = "Fehler beim Laden des Logs."; });
  };

  /* ---------------- Hinweis-Icons (Hover-Tooltip + Klick-Modal) ---------------- */
  /* Zentraler Mechanismus für ALLE "i"-Icons (siehe _hint_icon.html) app-weit
     -- ein einziges delegiertes Set von Listenern statt pro Icon eigener
     Handler, damit auch nachträglich per JS eingefügte Icons (z.B. in
     dynamisch nachgeladenen Tabellenzeilen) ohne weiteres Zutun funktionieren.
     Hover zeigt den Text als Tooltip neben dem Mauszeiger -- aber NUR auf
     Geräten, die tatsächlich sinnvoll hovern können (matchMedia-Check bei
     JEDEM mouseenter neu ausgewertet, nicht einmalig beim Laden gecacht,
     damit z.B. ein Convertible/Tablet beim Umschalten Maus/Touch korrekt
     reagiert). Auf reinen Touch-Geräten bleibt so nur Tap -> Modal übrig,
     da dort ohnehin kein echtes mouseenter/mousemove-Hover stattfindet. */
  function ensureHintTooltip() {
    if (document.getElementById("hint-tooltip")) return;
    const el = document.createElement("div");
    el.id = "hint-tooltip";
    document.body.appendChild(el);
  }

  function ensureHintModal() {
    if (document.getElementById("hint-modal")) return;
    const html = `
      <div class="modal-overlay" id="hint-modal">
        <div class="modal" style="max-width:440px;">
          <div class="modal-header">
            <h3 id="hint-modal-title">Hinweis</h3>
            <button type="button" class="modal-close" data-close-modal>&times;</button>
          </div>
          <div class="modal-body">
            <p id="hint-modal-body" class="text-dim" style="margin:0; line-height:1.6;"></p>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-close-modal>Schließen</button>
          </div>
        </div>
      </div>`;
    document.body.insertAdjacentHTML("beforeend", html);
    document.querySelectorAll("#hint-modal [data-close-modal]").forEach((btn) => {
      btn.addEventListener("click", () => closeModal(btn));
    });
    document.getElementById("hint-modal").addEventListener("click", (e) => {
      if (e.target.id === "hint-modal") closeModal(e.target);
    });
  }

  function canHover() {
    return window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches;
  }

  function initHintIcons() {
    if (!document.querySelector(".hint-icon")) return;
    ensureHintTooltip();
    const tooltip = document.getElementById("hint-tooltip");

    function positionTooltip(e) {
      const offset = 14;
      const rect = tooltip.getBoundingClientRect();
      let x = e.clientX + offset;
      let y = e.clientY + offset;
      if (x + rect.width > window.innerWidth - 8) x = e.clientX - rect.width - offset;
      if (y + rect.height > window.innerHeight - 8) y = e.clientY - rect.height - offset;
      tooltip.style.left = Math.max(8, x) + "px";
      tooltip.style.top = Math.max(8, y) + "px";
    }

    // mouseenter/mouseleave bubbeln nicht -- Delegation via Capture-Phase
    // auf document funktioniert dafür trotzdem zuverlässig.
    document.addEventListener("mouseenter", function (e) {
      const icon = e.target.closest && e.target.closest(".hint-icon");
      if (!icon || !canHover()) return;
      tooltip.textContent = icon.dataset.hint || "";
      tooltip.style.display = "block";
      positionTooltip(e);
    }, true);

    document.addEventListener("mouseleave", function (e) {
      const icon = e.target.closest && e.target.closest(".hint-icon");
      if (!icon) return;
      tooltip.style.display = "none";
    }, true);

    document.addEventListener("mousemove", function (e) {
      if (tooltip.style.display === "block") positionTooltip(e);
    });

    document.addEventListener("click", function (e) {
      const icon = e.target.closest && e.target.closest(".hint-icon");
      if (!icon) return;
      e.preventDefault();
      tooltip.style.display = "none";
      ensureHintModal();
      document.getElementById("hint-modal-title").textContent = icon.dataset.hintTitle || "Hinweis";
      document.getElementById("hint-modal-body").textContent = icon.dataset.hint || "";
      openModal("hint-modal");
    });
  }

  /* ---------------- Warnung bei ungespeicherten Änderungen ---------------- */
  /* Erkennt generisch auf JEDER Seite, ob ein Formular mit echten
     Eingabefeldern (nicht nur versteckten Aktions-Feldern wie bei Löschen/
     Toggle-Buttons) seit dem Laden der Seite verändert wurde, und fragt vor
     dem Verlassen (Klick auf einen Link, z.B. in der Navigation) per Ja/Nein,
     ob zuerst gespeichert werden soll — statt Änderungen stillschweigend zu
     verwerfen. "Ja" sendet das Formular ganz normal ab (echter POST, kein
     AJAX-Nachbau nötig) und merkt sich das eigentliche Ziel in
     sessionStorage, um nach dem Speichern automatisch dorthin
     weiterzuleiten, statt dass der Link ein zweites Mal angeklickt werden
     muss. Für Browser-eigene Navigation (Tab schließen, Reload, Adresszeile)
     gibt es zusätzlich beforeunload — dort erlaubt der Browser aus
     Sicherheitsgründen aber nur eine generische Warnung, kein eigenes
     Ja/Nein/Speichern-Dialogfeld. */
  const PENDING_NAV_KEY = "poe-pending-nav-after-save";

  function serializeForm(form) {
    return new URLSearchParams(new FormData(form)).toString();
  }

  function initUnsavedChangesGuard() {
    const pendingNav = sessionStorage.getItem(PENDING_NAV_KEY);
    if (pendingNav) {
      sessionStorage.removeItem(PENDING_NAV_KEY);
      window.location.href = pendingNav;
      return;
    }

    const trackable = [];
    document.querySelectorAll("form[method='post' i]").forEach((form) => {
      if (form.hasAttribute("data-no-unsaved-guard")) return;
      const fields = form.querySelectorAll("input:not([type=hidden]):not([type=submit]):not([type=button]), select, textarea");
      if (!fields.length) return; // reine Aktions-Formulare (Löschen/Toggle) ohne Eingabefeld
      trackable.push({ form, initial: serializeForm(form) });
    });
    if (!trackable.length) return;

    // Verhindert die native Browser-Warnung (beforeunload), wenn die Seite
    // ohnehin schon bewusst verlassen wird — entweder weil eines der
    // beobachteten Formulare ganz normal abgeschickt wurde (eigener
    // "Speichern"/"Anlegen"-Button auf der Seite) oder weil unser eigener
    // Ja/Nein-Dialog die Navigation ausgelöst hat. Ohne das würde beim
    // normalen Speichern zusätzlich zum eigentlichen Erfolg immer noch die
    // generische Browser-Meldung aufpoppen — die soll ausschließlich dann
    // erscheinen, wenn tatsächlich UNGEFRAGT navigiert wird (Tab schließen,
    // Reload, Adresszeile), nicht bei einem bewussten Speichern.
    let navigatingAway = false;
    trackable.forEach(({ form }) => {
      form.addEventListener("submit", () => { navigatingAway = true; });
    });

    function dirtyForm() {
      return trackable.find(({ form, initial }) => serializeForm(form) !== initial) || null;
    }

    document.addEventListener("click", (e) => {
      const link = e.target.closest("a[href]");
      if (!link || link.target === "_blank" || link.href.startsWith("javascript:") || link.hasAttribute("data-no-unsaved-guard")) return;
      const dirty = dirtyForm();
      if (!dirty) return;
      e.preventDefault();
      const href = link.href;
      window.confirmSaveDiscard(
        "Es gibt ungespeicherte Änderungen auf dieser Seite. Vor dem Verlassen speichern?",
        () => {
          navigatingAway = true;
          sessionStorage.setItem(PENDING_NAV_KEY, href);
          dirty.form.requestSubmit ? dirty.form.requestSubmit() : dirty.form.submit();
        },
        () => {
          navigatingAway = true;
          window.location.href = href;
        }
      );
    });

    window.addEventListener("beforeunload", (e) => {
      if (!navigatingAway && dirtyForm()) {
        e.preventDefault();
        e.returnValue = "";
      }
    });
  }

  /* Intercept forms/buttons marked with data-confirm="message" */
  function initConfirmables() {
    document.querySelectorAll("form[data-confirm]").forEach((form) => {
      form.addEventListener("submit", function (e) {
        if (form.dataset.confirmed === "1") return;
        e.preventDefault();
        // Welcher Button hat den Submit ausgelöst? Muss beim erneuten
        // requestSubmit() explizit mitgegeben werden — sonst geht bei
        // Formularen, die ihre ID über name/value des Submit-Buttons
        // transportieren (statt über ein <input type="hidden">), dieses
        // Feld beim Neu-Absenden stillschweigend verloren (requestSubmit()
        // ohne Argument zählt als "kein Button aktiviert").
        const submitter = e.submitter;
        window.confirmAction(form.getAttribute("data-confirm"), () => {
          form.dataset.confirmed = "1";
          form.requestSubmit ? form.requestSubmit(submitter) : form.submit();
        }, form.getAttribute("data-confirm-title"));
      });
    });
  }

  /* ---------------- Toasts ---------------- */

  function ensureToastStack() {
    let stack = document.querySelector(".toast-stack");
    if (!stack) {
      stack = document.createElement("div");
      stack.className = "toast-stack";
      document.body.appendChild(stack);
    }
    return stack;
  }

  window.showToast = function (message, type) {
    const stack = ensureToastStack();
    const toast = document.createElement("div");
    toast.className = "toast " + (type || "info");
    toast.innerHTML = `<span>${message}</span><span class="toast-close">&times;</span>`;
    stack.appendChild(toast);
    const remove = () => {
      toast.classList.add("hide");
      setTimeout(() => toast.remove(), 180);
    };
    toast.querySelector(".toast-close").addEventListener("click", remove);
    setTimeout(remove, 5000);
  };

  /* Zeigt ein per Dropdown ausgewähltes, bis dahin verstecktes DHCP-Options-
     Feld (siehe settings_dhcp.html, "+ Option hinzufügen"-Auswahl bei
     Reservierungen) — so sind nur tatsächlich genutzte Options-Felder
     sichtbar, statt jede einzeln immer als leeres Eingabefeld anzuzeigen.
     Verschiebt das Feld dafür direkt unter das Dropdown (in den Container
     mit der Klasse "poe-added-options", der im Markup direkt danach steht)
     statt es nur an seiner ursprünglichen Stelle einzublenden — bei bis zu
     ~60 möglichen Optionen läge diese sonst weit unterhalb des sichtbaren
     Modal-Ausschnitts und es wirkte so, als wäre gar nichts passiert. Jede
     weitere Auswahl hängt ihr Feld unten an, sodass alle bereits
     hinzugefügten Options sichtbar aufgereiht bleiben. */
  window.poeShowOptionField = function (select) {
    const id = select.value;
    if (id) {
      const field = document.getElementById(id);
      const container = select.nextElementSibling;
      if (field && container && container.classList.contains("poe-added-options")) {
        container.appendChild(field);
        field.style.display = "";
        const input = field.querySelector("input");
        if (input) input.focus();
      }
      select.value = "";
    }
  };

  function initFlashedMessages() {
    const data = document.getElementById("flashed-data");
    if (!data) return;
    try {
      const messages = JSON.parse(data.textContent);
      messages.forEach(([category, msg]) => {
        const type = category === "danger" ? "danger" : category === "success" ? "success" : "info";
        window.showToast(msg, type);
      });
    } catch (e) { /* noop */ }
  }

  /* Nav-Gruppen mit Untermenü (z.B. "Logs") auf-/zuklappen — der manuell
     aufgeklappte Zustand bleibt über Seitenwechsel hinweg erhalten
     (localStorage), zusätzlich zur automatischen Aufklappung der Gruppe
     der aktuell aktiven Seite (server-seitig via "expanded"-Klasse). */
  const NAV_EXPANDED_KEY = "poe-nav-expanded";

  function getExpandedNavGroups() {
    try {
      return new Set(JSON.parse(localStorage.getItem(NAV_EXPANDED_KEY) || "[]"));
    } catch (e) {
      return new Set();
    }
  }

  function saveExpandedNavGroups(set) {
    localStorage.setItem(NAV_EXPANDED_KEY, JSON.stringify(Array.from(set)));
  }

  function initNavGroups() {
    const expanded = getExpandedNavGroups();
    document.querySelectorAll("[data-nav-group]").forEach((group) => {
      const key = group.dataset.navGroupKey;
      if (key && expanded.has(key)) group.classList.add("expanded");
    });
    document.querySelectorAll("[data-nav-group-toggle]").forEach((btn) => {
      btn.addEventListener("click", () => {
        const group = btn.closest("[data-nav-group]");
        const isExpanded = group.classList.toggle("expanded");
        const key = group.dataset.navGroupKey;
        if (!key) return;
        const set = getExpandedNavGroups();
        if (isExpanded) set.add(key); else set.delete(key);
        saveExpandedNavGroups(set);
      });
    });
  }

  /* ---------------- Globaler "Nächste Prüfung"-Timer (Topbar, jede Seite) ---------------- */

  function initCheckTimer() {
    const pill = document.getElementById("global-timer-pill");
    const timerEl = document.getElementById("global-timer");
    if (!pill || !timerEl) return;

    // lastRunMs/intervalMs werden bei JEDEM Tick frisch aus dem DOM gelesen
    // (nicht einmalig gecacht) — so kann z.B. das Dashboard nach einem
    // Live-Update (siehe dashboard-tiles.js) oder ein manueller "Jetzt
    // prüfen"-Klick das data-last-run-ms-Attribut aktualisieren und der
    // global laufende Countdown übernimmt das automatisch.
    function update() {
      const lastRunMs = parseInt(pill.dataset.lastRunMs, 10);
      const intervalMs = parseInt(pill.dataset.intervalMs, 10);
      if (!intervalMs) { timerEl.textContent = "--"; return; }
      let remainingMs = intervalMs;
      if (!isNaN(lastRunMs) && lastRunMs > 0) {
        const elapsed = Date.now() - lastRunMs;
        remainingMs = intervalMs - (((elapsed % intervalMs) + intervalMs) % intervalMs);
      }
      timerEl.textContent = Math.max(0, Math.ceil(remainingMs / 1000));
    }
    update();
    setInterval(update, 1000);
  }

  /* "Jetzt prüfen"-Button neben dem globalen Timer (nur Admins): startet
     tesm-check.service neu (frisches Logfile + sofortiger Durchlauf) und
     meldet das per Custom Event, damit Dashboard/Live-Log sich selbst
     aktualisieren können, ohne dass app.js die Seiteninhalte kennen muss. */
  function initRunCheckNow() {
    const btn = document.getElementById("run-check-now");
    if (!btn) return;
    btn.addEventListener("click", () => {
      btn.disabled = true;
      btn.classList.add("spinning");
      fetch("/check/run-now", { method: "POST" })
        .then((r) => r.json())
        .then((data) => {
          if (!data.success) {
            window.showToast(data.msg || "Fehler beim Starten der Prüfung.", "danger");
            btn.disabled = false;
            btn.classList.remove("spinning");
            return;
          }
          window.showToast(data.msg || "Prüfung gestartet.", "success");
          document.dispatchEvent(new CustomEvent("poe:check-triggered"));
          // Topbar-Countdown auf JEDER Seite aktualisieren, nicht nur auf
          // dem Dashboard — vorher lief er auf allen anderen Seiten mit dem
          // alten Zeitpunkt weiter, obwohl im Hintergrund längst neu geprüft
          // wurde (die Prüfung lief trotzdem korrekt, nur sichtbar tat sich
          // nichts). Der Server liefert den frischen Zeitpunkt direkt in
          // dieser Antwort mit (siehe run_check_now) statt eines zweiten
          // Requests dafür.
          const pill = document.getElementById("global-timer-pill");
          if (pill && data.last_run_epoch_ms) {
            pill.dataset.lastRunMs = data.last_run_epoch_ms;
          }
          btn.disabled = false;
          btn.classList.remove("spinning");
        })
        .catch(() => {
          window.showToast("Fehler beim Starten der Prüfung.", "danger");
          btn.disabled = false;
          btn.classList.remove("spinning");
        });
    });
  }

  /* ---------------- Sortierbare Tabellen ---------------- */
  /*
   * Klick auf <th data-sort-key="..."> sortiert die Tabelle. Zwei Modi:
   * - Normale Tabellen: sortiert <tr>-Zeilen innerhalb des einzigen <tbody>
   *   anhand von data-sort-<key> auf der jeweiligen <tr>.
   * - Akkordeon-Tabellen (mehrere <tbody>, z.B. Gruppen mit Detail-Zeile):
   *   sortiert ganze <tbody>-Blöcke anhand von data-sort-<key> auf dem
   *   jeweiligen <tbody>, damit Haupt- und Detail-Zeile zusammenbleiben.
   *   <tbody data-sort-pinned> (z.B. die virtuelle "Admin"-Zeile) bleibt
   *   dabei immer an ihrer Position.
   */
  function compareSortValues(a, b, asc) {
    a = (a === null || a === undefined) ? "" : String(a);
    b = (b === null || b === undefined) ? "" : String(b);
    const na = parseFloat(a), nb = parseFloat(b);
    const bothNumeric = a !== "" && b !== "" && !isNaN(na) && !isNaN(nb);
    const cmp = bothNumeric ? (na - nb) : a.toLowerCase().localeCompare(b.toLowerCase(), "de");
    return asc ? cmp : -cmp;
  }

  function sortTable(table, th) {
    const key = th.dataset.sortKey;
    const asc = th.dataset.sortDir !== "asc";
    table.querySelectorAll("thead th[data-sort-key]").forEach((h) => {
      delete h.dataset.sortDir;
      h.classList.remove("sort-asc", "sort-desc");
    });
    th.dataset.sortDir = asc ? "asc" : "desc";
    th.classList.add(asc ? "sort-asc" : "sort-desc");

    if (table.tBodies.length > 1) {
      const movable = Array.prototype.filter.call(table.tBodies, (tb) => !tb.hasAttribute("data-sort-pinned"));
      movable.sort((a, b) => compareSortValues(
        a.getAttribute("data-sort-" + key), b.getAttribute("data-sort-" + key), asc
      ));
      movable.forEach((tb) => table.appendChild(tb));
    } else {
      const tbody = table.tBodies[0];
      const rows = Array.prototype.filter.call(tbody.rows, (r) => !r.classList.contains("empty-row"));
      rows.sort((a, b) => compareSortValues(
        a.getAttribute("data-sort-" + key), b.getAttribute("data-sort-" + key), asc
      ));
      rows.forEach((r) => tbody.appendChild(r));
    }
  }

  function initSortableTables() {
    document.querySelectorAll("table[data-sortable] thead th[data-sort-key]").forEach((th) => {
      th.classList.add("sortable-col");
      th.addEventListener("click", () => sortTable(th.closest("table"), th));
    });
  }

  /* ---------------- Init ---------------- */

  document.addEventListener("DOMContentLoaded", function () {
    initTheme();
    initSidebar();
    initModals();
    initConfirmables();
    initFlashedMessages();
    initNavGroups();
    initCheckTimer();
    initRunCheckNow();
    initLeaseCountdowns();
    initUnsavedChangesGuard();
    initSortableTables();
    initHintIcons();
    document.querySelectorAll("[data-theme-toggle]").forEach((btn) => btn.addEventListener("click", toggleTheme));
  });

  /* ---------------- DHCP-Lease-Countdown (settings_dhcp.html) ---------------- */
  /* Ergänzt die absolute (UTC-)Ablaufzeit einer Lease um einen live tickenden
     Countdown — bei den kurzen Standard-Lease-Zeiten (600s) wirkt eine reine
     Absolutzeit sonst schnell "schon abgelaufen", sobald etwas Zeit vergangen
     ist, bis man die (nicht selbst aktualisierende) Seite tatsächlich
     anschaut. Gleiches Tick-Prinzip wie initCheckTimer oben. */
  function formatLeaseCountdown(remainingMs) {
    const abs = Math.abs(Math.round(remainingMs / 1000));
    const m = Math.floor(abs / 60);
    const s = abs % 60;
    const duration = m > 0 ? `${m}m ${s}s` : `${s}s`;
    return remainingMs >= 0 ? `läuft ab in ${duration}` : `abgelaufen vor ${duration}`;
  }

  function initLeaseCountdowns() {
    const els = document.querySelectorAll(".lease-countdown");
    if (!els.length) return;
    function tick() {
      const now = Date.now();
      els.forEach((el) => {
        const expireMs = parseInt(el.dataset.expireMs, 10);
        if (isNaN(expireMs)) return;
        el.textContent = formatLeaseCountdown(expireMs - now);
        el.classList.toggle("text-danger", expireMs < now);
      });
    }
    tick();
    setInterval(tick, 1000);
  }

  window.PoeUI = { openModal, closeModal };
})();
