// Sidebar client-side behavior
// Hover expand with delay, pin toggle, mobile hamburger

import { initGlobalSearch } from "./search";
import { getChangelogSeen } from "./changelog-seen";
import { cycleTheme, getThemePref, onThemeChange, type ThemePref } from "./theme";
import { t } from "./i18n";
const PIN_KEY = "paliad-sidebar-pinned";
const LEGACY_PIN_KEY = "patholo-sidebar-pinned";
const WIDTH_KEY = "paliad-sidebar-width";
const SIDEBAR_WIDTH_MIN = 180;
const SIDEBAR_WIDTH_MAX = 480;
const SIDEBAR_WIDTH_DEFAULT = 240;
// Per-tab scroll position of the .sidebar-nav scroll container. Persisted
// on every scroll event, restored on initSidebar() so a full-page nav
// click doesn't bounce the user back to the top of a long sidebar
// (Werkzeuge + projects + user views can easily overflow). sessionStorage
// scopes it to the tab — opening a sidebar link in a new tab (Cmd-click)
// starts that tab fresh at the top, which matches user expectation.
const SCROLL_KEY = "paliad.sidebar.scroll";

// toggleMobileSidebar opens or closes the slide-out drawer. Exposed so the
// BottomNav menu slot can call it without duplicating the open/close
// machinery (overlay, body-scroll lock, etc.).
export function toggleMobileSidebar(): void {
  const sidebar = document.querySelector<HTMLElement>(".sidebar");
  const overlay = document.querySelector<HTMLElement>(".sidebar-overlay");
  if (!sidebar) return;
  const isOpen = sidebar.classList.contains("mobile-open");
  if (isOpen) {
    sidebar.classList.remove("mobile-open");
    overlay?.classList.remove("visible");
    document.body.classList.remove("no-scroll");
  } else {
    sidebar.classList.add("mobile-open");
    overlay?.classList.add("visible");
    document.body.classList.add("no-scroll");
  }
}

// readStoredWidth returns the user's persisted sidebar width in px, falling
// back to SIDEBAR_WIDTH_DEFAULT when missing, malformed, or outside the
// supported clamp range. Stored values are written by initSidebarResize on
// dragend; the read happens on every page load (initial CSS-var apply).
function readStoredWidth(): number {
  const raw = localStorage.getItem(WIDTH_KEY);
  if (raw === null) return SIDEBAR_WIDTH_DEFAULT;
  const n = parseInt(raw, 10);
  if (!Number.isFinite(n) || n < SIDEBAR_WIDTH_MIN || n > SIDEBAR_WIDTH_MAX) {
    return SIDEBAR_WIDTH_DEFAULT;
  }
  return n;
}

function applySidebarWidth(px: number): void {
  document.documentElement.style.setProperty("--sidebar-width", `${px}px`);
}

// readStoredScroll returns the persisted scrollTop or 0 when missing /
// malformed. Bounds are checked at apply time against the actual
// scrollHeight, so a stale value pointing past the current scroll range
// is harmless (the browser clamps assignments to [0, max]).
function readStoredScroll(): number {
  const raw = sessionStorage.getItem(SCROLL_KEY);
  if (raw === null) return 0;
  const n = parseInt(raw, 10);
  if (!Number.isFinite(n) || n < 0) return 0;
  return n;
}

function applySidebarScroll(nav: HTMLElement, px: number): void {
  if (px <= 0) return;
  nav.scrollTop = px;
}

// migrateLegacyPinKey copies the pre-rebrand pin state into the new key on
// first load and removes the stale entry. Drop this fallback once the rename
// grace period is over.
function migrateLegacyPinKey(): void {
  const legacy = localStorage.getItem(LEGACY_PIN_KEY);
  if (legacy === null) return;
  if (localStorage.getItem(PIN_KEY) === null) {
    localStorage.setItem(PIN_KEY, legacy);
  }
  localStorage.removeItem(LEGACY_PIN_KEY);
}

export function initSidebar() {
  migrateLegacyPinKey();
  // Apply the persisted width before any sidebar paint so the layout is
  // stable from frame 1. Safe to call before the .sidebar element exists —
  // the CSS var lives on document.documentElement.
  applySidebarWidth(readStoredWidth());
  initInviteModal();
  initGlobalSearch();
  initChangelogBadge();
  initInboxBadge();
  initAdminGroup();
  initPaliadinLinks();
  initProjectContextChartLink();
  initUserViewsGroup();
  initThemeToggle();
  const sidebar = document.querySelector<HTMLElement>(".sidebar");
  if (!sidebar) return;
  initSidebarResize(sidebar);
  initSidebarScrollRestore(sidebar);

  const pinBtn = sidebar.querySelector<HTMLButtonElement>(".sidebar-pin");
  const hamburger = document.querySelector<HTMLButtonElement>(".sidebar-hamburger");
  const overlay = document.querySelector<HTMLElement>(".sidebar-overlay");

  let hoverTimer: ReturnType<typeof setTimeout> | null = null;

  function isMobile(): boolean {
    return window.innerWidth < 1024;
  }

  // Restore pin state on desktop. We mirror the `sidebar-pinned` class on
  // both <body> and <html>: the runtime path historically set it on body,
  // but the pre-paint FOUC script in PWAHead.tsx can only reach <html>
  // (body doesn't exist yet at that point). Keeping both in sync from
  // every site that toggles pin state means the CSS selectors
  // `.has-sidebar.sidebar-pinned` (body-rooted) and
  // `:root.sidebar-pinned .has-sidebar` (html-rooted) never disagree.
  if (localStorage.getItem(PIN_KEY) === "true" && !isMobile()) {
    sidebar.classList.add("pinned");
    document.body.classList.add("sidebar-pinned");
    document.documentElement.classList.add("sidebar-pinned");
  } else {
    // Pre-paint script may have added it (e.g. last visit was wide enough
    // and pinned) but current viewport is mobile — clear so the CSS rule
    // doesn't fight the responsive @media block.
    document.documentElement.classList.remove("sidebar-pinned");
  }

  // Desktop: hover expand with 150ms delay
  sidebar.addEventListener("mouseenter", () => {
    if (isMobile() || sidebar.classList.contains("pinned")) return;
    hoverTimer = setTimeout(() => {
      sidebar.classList.add("expanded");
    }, 150);
  });

  sidebar.addEventListener("mouseleave", () => {
    if (hoverTimer) {
      clearTimeout(hoverTimer);
      hoverTimer = null;
    }
    // Mid-resize drags briefly pull the cursor off the sidebar — keep
    // the sidebar expanded so the user can finish the drag. The resizing
    // class is cleared on dragend; the next mouseleave will collapse.
    if (sidebar.classList.contains("resizing")) return;
    sidebar.classList.remove("expanded");
  });

  // Pin toggle
  if (pinBtn) {
    pinBtn.addEventListener("click", (e) => {
      e.preventDefault();
      const wasPinned = sidebar.classList.contains("pinned");
      if (wasPinned) {
        sidebar.classList.remove("pinned");
        document.body.classList.remove("sidebar-pinned");
        document.documentElement.classList.remove("sidebar-pinned");
        localStorage.setItem(PIN_KEY, "false");
      } else {
        sidebar.classList.add("pinned");
        sidebar.classList.remove("expanded");
        document.body.classList.add("sidebar-pinned");
        document.documentElement.classList.add("sidebar-pinned");
        localStorage.setItem(PIN_KEY, "true");
      }
    });
  }

  // Mobile: hamburger toggle
  function closeMobile() {
    sidebar.classList.remove("mobile-open");
    overlay?.classList.remove("visible");
    document.body.classList.remove("no-scroll");
  }

  if (hamburger) {
    hamburger.addEventListener("click", () => {
      toggleMobileSidebar();
    });
  }

  if (overlay) {
    overlay.addEventListener("click", closeMobile);
  }

  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape" && sidebar.classList.contains("mobile-open")) {
      closeMobile();
    }
  });

  // Close mobile sidebar on nav click
  sidebar.querySelectorAll<HTMLAnchorElement>("a[href]").forEach((link) => {
    link.addEventListener("click", () => {
      if (isMobile()) closeMobile();
    });
  });

  // Handle resize: clean up states when crossing breakpoint
  let wasMobile = isMobile();
  window.addEventListener("resize", () => {
    const nowMobile = isMobile();
    if (wasMobile === nowMobile) return;
    wasMobile = nowMobile;

    if (nowMobile) {
      sidebar.classList.remove("expanded", "pinned");
      document.body.classList.remove("sidebar-pinned");
      document.documentElement.classList.remove("sidebar-pinned");
    } else {
      closeMobile();
      if (localStorage.getItem(PIN_KEY) === "true") {
        sidebar.classList.add("pinned");
        document.body.classList.add("sidebar-pinned");
        document.documentElement.classList.add("sidebar-pinned");
      }
    }
  });
}

// initSidebarResize wires the right-edge drag handle: mousedown/touchstart
// captures the starting cursor position and current width, mousemove/touchmove
// applies the delta as --sidebar-width (clamped), mouseup/touchend persists
// the final value to localStorage. Double-click on the handle resets to the
// default width. The .resizing class on the sidebar suppresses both the
// width transition (avoid jitter while dragging) and the hover-collapse
// (the cursor leaves the sidebar mid-drag).
function initSidebarResize(sidebar: HTMLElement): void {
  const handle = sidebar.querySelector<HTMLElement>(".sidebar-resize-handle");
  if (!handle) return;

  let dragging = false;
  let startX = 0;
  let startWidth = 0;

  function applyDelta(clientX: number): void {
    const delta = clientX - startX;
    let next = startWidth + delta;
    if (next < SIDEBAR_WIDTH_MIN) next = SIDEBAR_WIDTH_MIN;
    if (next > SIDEBAR_WIDTH_MAX) next = SIDEBAR_WIDTH_MAX;
    applySidebarWidth(next);
  }

  function readCurrentWidth(): number {
    const raw = getComputedStyle(document.documentElement)
      .getPropertyValue("--sidebar-width")
      .trim();
    const n = parseInt(raw, 10);
    return Number.isFinite(n) ? n : SIDEBAR_WIDTH_DEFAULT;
  }

  function endDrag(): void {
    if (!dragging) return;
    dragging = false;
    sidebar.classList.remove("resizing");
    document.body.classList.remove("sidebar-resizing");
    document.removeEventListener("mousemove", onMouseMove);
    document.removeEventListener("mouseup", endDrag);
    document.removeEventListener("touchmove", onTouchMove);
    document.removeEventListener("touchend", endDrag);
    document.removeEventListener("touchcancel", endDrag);
    localStorage.setItem(WIDTH_KEY, String(readCurrentWidth()));
  }

  function onMouseMove(e: MouseEvent): void {
    applyDelta(e.clientX);
  }

  function onTouchMove(e: TouchEvent): void {
    if (e.touches.length === 0) return;
    applyDelta(e.touches[0].clientX);
    // preventDefault stops the browser from interpreting the drag as a
    // page scroll. touch-action: none on the handle covers most browsers,
    // but Safari still needs the explicit call.
    if (e.cancelable) e.preventDefault();
  }

  function startDrag(clientX: number): void {
    dragging = true;
    startX = clientX;
    // Use the rendered sidebar width — robust against any future state
    // (pinned vs hover-expanded) where the CSS var might not be the
    // single source of truth.
    startWidth = sidebar.getBoundingClientRect().width;
    sidebar.classList.add("resizing");
    document.body.classList.add("sidebar-resizing");
  }

  handle.addEventListener("mousedown", (e) => {
    // Only primary button. Right/middle clicks would otherwise stick the
    // sidebar in resizing state until a real mouseup arrives.
    if (e.button !== 0) return;
    e.preventDefault();
    startDrag(e.clientX);
    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", endDrag);
  });

  handle.addEventListener("touchstart", (e) => {
    if (e.touches.length === 0) return;
    startDrag(e.touches[0].clientX);
    document.addEventListener("touchmove", onTouchMove, { passive: false });
    document.addEventListener("touchend", endDrag);
    document.addEventListener("touchcancel", endDrag);
  }, { passive: true });

  handle.addEventListener("dblclick", (e) => {
    e.preventDefault();
    applySidebarWidth(SIDEBAR_WIDTH_DEFAULT);
    localStorage.setItem(WIDTH_KEY, String(SIDEBAR_WIDTH_DEFAULT));
  });
}

// initSidebarScrollRestore wires the .sidebar-nav scroll container to
// sessionStorage so the user's scroll position survives a full-page
// navigation (every sidebar link click is a real reload — see m/paliad#85).
// Restore is synchronous on init so the first paint is already at the
// right offset; the passive scroll listener persists subsequent moves.
// reapplySidebarScroll() exists so callers that mutate sidebar content
// async (initUserViewsGroup appending /api/user-views into the Ansichten
// group) can nudge the scroll back to where it was after the layout shift.
function initSidebarScrollRestore(sidebar: HTMLElement): void {
  const nav = sidebar.querySelector<HTMLElement>(".sidebar-nav");
  if (!nav) return;
  applySidebarScroll(nav, readStoredScroll());
  nav.addEventListener("scroll", () => {
    sessionStorage.setItem(SCROLL_KEY, String(nav.scrollTop));
  }, { passive: true });
}

function reapplySidebarScroll(): void {
  const nav = document.querySelector<HTMLElement>(".sidebar .sidebar-nav");
  if (!nav) return;
  applySidebarScroll(nav, readStoredScroll());
}

// Changelog badge — fetches the count of entries newer than the locally
// stored "last seen" stamp and renders a dot + number on the Neuigkeiten
// link. Skipped on the changelog page itself because changelog.ts stamps
// the visit on load and we don't want a flash of badge before that runs.
function initChangelogBadge(): void {
  const badge = document.getElementById("sidebar-changelog-badge") as HTMLElement | null;
  if (!badge) return;
  if (window.location.pathname === "/changelog") return;

  const since = getChangelogSeen();
  const url = since
    ? `/api/changelog/unseen-count?since=${encodeURIComponent(since)}`
    : "/api/changelog/unseen-count";
  fetch(url, { credentials: "same-origin" })
    .then((r) => (r.ok ? r.json() : null))
    .then((data: { count?: number } | null) => {
      if (!data || typeof data.count !== "number" || data.count <= 0) return;
      badge.textContent = data.count > 9 ? "9+" : String(data.count);
      badge.style.display = "";
    })
    .catch(() => {
      // silent: the badge is optional and must never break the page.
    });
}

// Inbox badge (t-paliad-138) — count of approval requests where the
// current user is qualified to approve. Polls every 60s while the page
// is open. Silently swallows errors (badge is optional).
function initInboxBadge(): void {
  const badge = document.getElementById("sidebar-inbox-badge") as HTMLElement | null;
  if (!badge) return;

  const refresh = () => {
    fetch("/api/inbox/count", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : null))
      .then((data: { count?: number } | null) => {
        if (!data || typeof data.count !== "number" || data.count <= 0) {
          badge.style.display = "none";
          return;
        }
        badge.textContent = data.count > 9 ? "9+" : String(data.count);
        badge.style.display = "";
      })
      .catch(() => {
        /* silent */
      });
  };

  refresh();
  setInterval(refresh, 60_000);
}

// initThemeToggle wires the sun/moon button at the bottom of the sidebar
// (m/paliad#2). The pre-paint inline script in PWAHead.tsx already set
// the data-theme attribute on <html>; this function only owns the post-
// hydration UI: cycling on click, swapping the icon/label to match the
// current preference, and re-rendering when the OS-level scheme flips
// while the user is on "auto".
const THEME_ICONS: Record<ThemePref, string> = {
  auto: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 0 0 18z" fill="currentColor"/></svg>',
  light: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="M4.93 4.93l1.41 1.41"/><path d="M17.66 17.66l1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="M4.93 19.07l1.41-1.41"/><path d="M17.66 6.34l1.41-1.41"/></svg>',
  dark: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>',
};

const THEME_LABEL_KEYS: Record<ThemePref, "theme.toggle.auto" | "theme.toggle.light" | "theme.toggle.dark"> = {
  auto: "theme.toggle.auto",
  light: "theme.toggle.light",
  dark: "theme.toggle.dark",
};

function initThemeToggle(): void {
  const btn = document.getElementById("sidebar-theme-toggle") as HTMLButtonElement | null;
  const icon = document.getElementById("sidebar-theme-icon") as HTMLElement | null;
  const label = document.getElementById("sidebar-theme-label") as HTMLElement | null;
  if (!btn || !icon || !label) return;

  function render(): void {
    const pref = getThemePref();
    if (icon) icon.innerHTML = THEME_ICONS[pref];
    if (label) {
      const key = THEME_LABEL_KEYS[pref];
      label.setAttribute("data-i18n", key);
      label.textContent = t(key);
    }
    if (btn) {
      btn.setAttribute("aria-label", t(key_for_aria(pref)));
      btn.setAttribute("title", t(key_for_aria(pref)));
    }
  }

  function key_for_aria(pref: ThemePref): "theme.toggle.cycle.auto" | "theme.toggle.cycle.light" | "theme.toggle.cycle.dark" {
    // The aria-label describes WHAT happens on click, not the current
    // state — assistive tech reads "Switch to dark theme" rather than
    // just "Auto". Cycle order: auto → light → dark → auto.
    if (pref === "auto") return "theme.toggle.cycle.auto";   // → light
    if (pref === "light") return "theme.toggle.cycle.light"; // → dark
    return "theme.toggle.cycle.dark";                        // → auto
  }

  btn.addEventListener("click", () => {
    cycleTheme();
  });

  // theme.ts notifies on user-cycle AND on OS-level prefers-color-scheme
  // changes while the user is on "auto" — a single subscription covers
  // both, so the icon/label always tracks the live preference.
  onThemeChange(render);
  render();
}

// t-paliad-144 Phase A2 — Meine Sichten group hydration. Fetches the
// caller's saved views and renders one nav item per view between the
// group label and the "+ Neue Sicht" trailing entry. Optional count
// badge per view (when show_count=true on the row). The "+ Neue Sicht"
// entry stays in the DOM unconditionally so the group has something
// to show even for first-time users.
interface UserViewLite {
  id: string;
  slug: string;
  name: string;
  icon?: string;
  show_count: boolean;
}

function initUserViewsGroup(): void {
  const items = document.getElementById("sidebar-views-items");
  if (!items) return;
  // Skip on auth-anon pages (/login, landing) — /api/user-views would 401.
  if (!document.body.classList.contains("has-sidebar")) return;

  fetch("/api/user-views", { credentials: "same-origin" })
    .then((r) => (r.ok ? r.json() : null))
    .then((views: UserViewLite[] | null) => {
      if (!views) return;
      const currentPath = window.location.pathname;
      items.innerHTML = "";
      for (const view of views) {
        items.appendChild(renderUserViewItem(view, currentPath));
      }
      // The synchronous restore in initSidebarScrollRestore() happened
      // before these views were appended, so a saved scrollTop that
      // pointed below the Ansichten group would now sit on the wrong
      // row. Re-apply once the layout has stabilised.
      reapplySidebarScroll();
      // After rendering, kick off count refresh for views that opted in.
      for (const view of views) {
        if (view.show_count) {
          void refreshUserViewCount(view);
        }
      }
    })
    .catch(() => {
      // Silent — sidebar already shows "+ Neue Sicht" even on failure.
    });
}

// fixVerfahrensablaufActive removed (t-paliad-179 Slice 1). The two
// sidebar entries now map 1:1 to distinct URLs (/tools/fristenrechner
// vs /tools/verfahrensablauf), so the SSR navItem helper picks the
// correct active class by pathname alone.

function renderUserViewItem(view: UserViewLite, currentPath: string): HTMLElement {
  const a = document.createElement("a");
  a.href = `/views/${encodeURIComponent(view.slug)}`;
  const active = currentPath === a.pathname;
  a.className = `sidebar-item sidebar-user-view-item${active ? " active" : ""}`;
  a.dataset.slug = view.slug;
  a.dataset.viewId = view.id;

  const iconWrap = document.createElement("span");
  iconWrap.className = "sidebar-icon";
  iconWrap.innerHTML = userViewIconSvg(view.icon);
  a.appendChild(iconWrap);

  const label = document.createElement("span");
  label.className = "sidebar-label";
  label.textContent = view.name;
  a.appendChild(label);

  if (view.show_count) {
    const badge = document.createElement("span");
    badge.className = "sidebar-badge sidebar-user-view-badge";
    badge.id = `sidebar-user-view-badge-${view.id}`;
    badge.style.display = "none";
    badge.setAttribute("aria-hidden", "true");
    a.appendChild(badge);
  }
  return a;
}

async function refreshUserViewCount(view: UserViewLite): Promise<void> {
  try {
    const r = await fetch(`/api/views/${encodeURIComponent(view.slug)}/run`, {
      method: "POST",
      credentials: "same-origin",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({}),
    });
    if (!r.ok) return;
    const data = (await r.json()) as { rows: unknown[] };
    const badge = document.getElementById(`sidebar-user-view-badge-${view.id}`);
    if (!badge) return;
    if (data.rows.length > 0) {
      badge.textContent = String(data.rows.length);
      badge.style.display = "";
    } else {
      badge.style.display = "none";
    }
  } catch (_e) {
    /* noop */
  }
}

// userViewIconSvg picks an SVG from a small fixed registry. Falls back
// to the folder icon for unknown / missing keys. Inline SVGs are used
// elsewhere in the sidebar (Sidebar.tsx); we duplicate a minimal subset
// here rather than re-exporting because client TS doesn't import from
// JSX-emitting modules.
function userViewIconSvg(icon?: string): string {
  switch (icon) {
    case "clock":
      return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
    case "calendar":
      return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>';
    case "bell":
      return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>';
    case "users":
      return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>';
    case "building":
      return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"/><path d="M5 21V5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v16"/><path d="M16 9h3a2 2 0 0 1 2 2v10"/></svg>';
    case "folder":
    default:
      return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
  }
}

// PALIADIN_OWNER_EMAIL must match services.PaliadinOwnerEmail (Go side).
// PoC scope — see docs/design-paliadin-2026-05-07.md §0.5.
const PALIADIN_OWNER_EMAIL = "matthias.siebels@hoganlovells.com";

// initPaliadinLinks reveals the Paliadin sidebar entries (under Übersicht
// + Admin) when /api/me confirms the caller is the Paliadin owner. Same
// fail-closed display:none pattern as initAdminGroup. Non-owners never
// see the entries; the routes themselves return 404 if they navigate
// to /paliadin or /admin/paliadin manually anyway.
function initPaliadinLinks(): void {
  const top = document.getElementById("sidebar-paliadin-link") as HTMLElement | null;
  const admin = document.getElementById("sidebar-admin-paliadin-link") as HTMLElement | null;
  if (!top && !admin) return;
  fetch("/api/me", { credentials: "same-origin" })
    .then((r) => (r.ok ? r.json() : null))
    .then((me: { email?: string } | null) => {
      if (me && me.email && me.email.toLowerCase() === PALIADIN_OWNER_EMAIL) {
        if (top) top.style.display = "";
        if (admin) admin.style.display = "";
      }
    })
    .catch(() => {
      // silent: failing closed is the safe default.
    });
}

// initProjectContextChartLink (t-paliad-177 Slice 3) reveals an "Als Chart
// anzeigen" entry in the sidebar when the user is browsing a project
// detail page. Hidden everywhere else, hidden on the chart page itself
// (the chart is the destination, not the source).
//
// Self-contained on URL parsing — no per-page handshake needed. Pages
// don't have to know about the sidebar slot; this function walks the
// pathname and renders the link if it matches.
//
// Layout intent: chip sits directly under the "Übersicht" group so it's
// visible on every project sub-tab (Verlauf / Team / Parteien / …).
function initProjectContextChartLink(): void {
  const link = document.getElementById("sidebar-project-chart-link") as HTMLAnchorElement | null;
  if (!link) return;
  const match = /^\/projects\/([0-9a-fA-F-]{36})(\/.*)?$/.exec(window.location.pathname);
  if (!match) return;
  const id = match[1];
  const rest = match[2] || "";
  // Hide on the chart page itself — a reciprocal "Zurück zum Verlauf"
  // affordance lives on the chart page header (separate slice).
  if (rest === "/chart" || rest === "/chart/") return;
  link.href = `/projects/${encodeURIComponent(id)}/chart`;
  link.style.display = "";
}

// initAdminGroup reveals the Admin section in the sidebar when the caller's
// /api/me lookup confirms global_role='global_admin'. The markup is in the
// DOM with display:none for everyone — flipping it on after the fetch lands
// keeps non-admin pageloads cheap (no flash, no second render) and avoids a
// privilege flash for admins on cached pages.
function initAdminGroup(): void {
  const group = document.getElementById("sidebar-admin-group") as HTMLElement | null;
  if (!group) return;
  fetch("/api/me", { credentials: "same-origin" })
    .then((r) => (r.ok ? r.json() : null))
    .then((me: { global_role?: string } | null) => {
      if (me && me.global_role === "global_admin") {
        group.style.display = "";
      }
    })
    .catch(() => {
      // silent: not being able to check the permission just means we keep
      // the section hidden, which fails closed.
    });
}

// Invitation modal — opened from the sidebar "Kolleg:in einladen" button.
// Keeps the whole flow client-side: validates, POSTs to /api/invite, shows
// success or the server's error message in the same modal. Kept inside
// sidebar.ts because the Sidebar component owns the modal markup — every
// page that renders <Sidebar /> picks up the behaviour for free.
function initInviteModal(): void {
  const btn = document.getElementById("sidebar-invite-btn") as HTMLButtonElement | null;
  const modal = document.getElementById("invite-modal") as HTMLElement | null;
  const closeBtn = document.getElementById("invite-modal-close") as HTMLButtonElement | null;
  const cancelBtn = document.getElementById("invite-modal-cancel") as HTMLButtonElement | null;
  const form = document.getElementById("invite-form") as HTMLFormElement | null;
  const emailInput = document.getElementById("invite-email") as HTMLInputElement | null;
  const messageInput = document.getElementById("invite-message") as HTMLTextAreaElement | null;
  const submitBtn = document.getElementById("invite-submit") as HTMLButtonElement | null;
  const feedback = document.getElementById("invite-feedback") as HTMLElement | null;

  if (!btn || !modal || !form || !emailInput || !submitBtn || !feedback) return;

  function open(): void {
    clearFeedback();
    modal!.style.display = "flex";
    setTimeout(() => emailInput!.focus(), 30);
  }

  function close(): void {
    modal!.style.display = "none";
    form!.reset();
    clearFeedback();
  }

  function clearFeedback(): void {
    feedback!.style.display = "none";
    feedback!.textContent = "";
    feedback!.classList.remove("form-msg-success", "form-msg-error");
  }

  function setFeedback(kind: "success" | "error", text: string): void {
    feedback!.textContent = text;
    feedback!.classList.remove("form-msg-success", "form-msg-error");
    feedback!.classList.add(kind === "success" ? "form-msg-success" : "form-msg-error");
    feedback!.style.display = "block";
  }

  btn.addEventListener("click", (e) => {
    e.preventDefault();
    open();
  });
  closeBtn?.addEventListener("click", close);
  cancelBtn?.addEventListener("click", close);
  modal.addEventListener("click", (e) => {
    if (e.target === modal) close();
  });
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape" && modal.style.display !== "none") close();
  });

  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const email = emailInput.value.trim();
    const message = messageInput?.value.trim() ?? "";
    if (!email) return;

    submitBtn.disabled = true;
    clearFeedback();
    try {
      const res = await fetch("/api/invite", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, message }),
      });
      const data = await res.json().catch(() => ({}));
      if (res.ok) {
        const remaining = typeof data.remaining_today === "number" ? data.remaining_today : null;
        const baseMsg = (document.documentElement.lang === "en")
          ? `Invitation sent to ${email}.`
          : `Einladung gesendet an ${email}.`;
        const tail = remaining !== null
          ? ((document.documentElement.lang === "en")
              ? ` (${remaining} invitations remaining today.)`
              : ` (Noch ${remaining} Einladungen heute m\u00f6glich.)`)
          : "";
        setFeedback("success", baseMsg + tail);
        form.reset();
        setTimeout(close, 2500);
      } else {
        const msg = typeof data.error === "string"
          ? data.error
          : ((document.documentElement.lang === "en")
              ? "Failed to send invitation."
              : "Einladung konnte nicht gesendet werden.");
        setFeedback("error", msg);
      }
    } catch (_err) {
      setFeedback("error",
        (document.documentElement.lang === "en")
          ? "Network error — please try again."
          : "Netzwerkfehler — bitte erneut versuchen.");
    } finally {
      submitBtn.disabled = false;
    }
  });
}
