import { initI18n, onLangChange, getLang, setLang, t, tDyn, Lang } from "./i18n";
import { initSidebar } from "./sidebar";

// Unified settings page. One init function for the whole page; each tab has
// its own idempotent loader (load*Tab) that runs on first activation and is
// safe to call again on subsequent switches.

interface Office {
  key: string;
  label_de: string;
  label_en: string;
}

interface Me {
  id: string;
  email: string;
  display_name: string;
  office: string;
  job_title: string | null;
  global_role: string;
  lang: Lang;
  email_preferences: Record<string, unknown>;
  reminder_morning_time: string;
  reminder_evening_time: string;
  reminder_timezone: string;
  reminder_warning_offset_days: number;
  // Optional override of the DRINGEND/overdue escalation channel — when
  // set, replaces the global_admins fallback for this user's deadlines.
  // Server returns either a UUID string or omits the key (omitempty).
  escalation_contact_id?: string;
}

interface CalDAVConfig {
  configured: boolean;
  url?: string;
  username?: string;
  calendar_path?: string;
  enabled?: boolean;
  last_sync_at?: string;
  last_sync_error?: string;
  updated_at?: string;
}

interface SyncLogEntry {
  id: string;
  occurred_at: string;
  direction: string;
  items_pushed: number;
  items_pulled: number;
  error?: string;
  duration_ms?: number;
}

type TabName = "profil" | "benachrichtigungen" | "caldav" | "export";
const TABS: TabName[] = ["profil", "benachrichtigungen", "caldav", "export"];
const DEFAULT_TAB: TabName = "profil";

let me: Me | null = null;
let offices: Office[] = [];
let caldavConfig: CalDAVConfig | null = null;
const loadedTabs = new Set<TabName>();

function esc(s: string): string {
  const d = document.createElement("div");
  d.textContent = s;
  return d.innerHTML;
}

function fmtDateTime(iso?: string): string {
  if (!iso) return t("caldav.never");
  try {
    const d = new Date(iso);
    return d.toLocaleString(getLang() === "de" ? "de-DE" : "en-GB", {
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
      hour: "2-digit",
      minute: "2-digit",
    });
  } catch {
    return iso;
  }
}

// --- Tab switching ----------------------------------------------------------

function parseTab(): TabName {
  const params = new URLSearchParams(window.location.search);
  const raw = params.get("tab");
  return (TABS as string[]).includes(raw ?? "") ? (raw as TabName) : DEFAULT_TAB;
}

function showTab(tab: TabName, pushHistory: boolean) {
  for (const name of TABS) {
    const panel = document.getElementById(`tab-${name}`);
    if (panel) panel.style.display = name === tab ? "" : "none";
  }
  document.querySelectorAll<HTMLElement>("#einstellungen-tabs .entity-tab").forEach((el) => {
    const name = el.getAttribute("data-tab");
    el.classList.toggle("active", name === tab);
  });

  if (pushHistory) {
    const url = new URL(window.location.href);
    if (tab === DEFAULT_TAB) {
      url.searchParams.delete("tab");
    } else {
      url.searchParams.set("tab", tab);
    }
    window.history.replaceState({}, "", url.toString());
  }

  if (!loadedTabs.has(tab)) {
    loadedTabs.add(tab);
    if (tab === "profil") void loadProfilTab();
    else if (tab === "benachrichtigungen") void loadPrefsTab();
    else if (tab === "caldav") void loadCalDAVTab();
    else if (tab === "export") void loadExportTab();
  }
}

function wireTabLinks() {
  document.querySelectorAll<HTMLAnchorElement>("#einstellungen-tabs .entity-tab").forEach((a) => {
    a.addEventListener("click", (e) => {
      e.preventDefault();
      const tab = a.getAttribute("data-tab") as TabName | null;
      if (tab) showTab(tab, true);
    });
  });
}

// --- Profil tab -------------------------------------------------------------

async function loadProfilTab() {
  const loading = document.getElementById("profil-loading")!;
  const form = document.getElementById("profil-form") as HTMLFormElement;

  await Promise.all([fetchMe(), fetchOffices()]);
  renderOfficeOptions();
  fillProfilForm();
  void renderMyPartnerUnits();

  loading.style.display = "none";
  form.style.display = "";
}

async function fetchMe(): Promise<void> {
  if (me) return;
  const resp = await fetch("/api/me");
  if (resp.status === 404) {
    // Shouldn't happen — /einstellungen is behind the onboarding gate — but
    // if it does, push the user to onboarding so they don't sit on an empty
    // form.
    window.location.href = "/onboarding";
    return;
  }
  if (!resp.ok) return;
  me = await resp.json();
}

async function fetchOffices(): Promise<void> {
  if (offices.length > 0) return;
  try {
    const resp = await fetch("/api/offices");
    if (!resp.ok) return;
    offices = await resp.json();
  } catch {
    offices = [];
  }
}

function renderOfficeOptions() {
  const select = document.getElementById("profil-office") as HTMLSelectElement | null;
  if (!select) return;
  const isEN = getLang() === "en";
  const previous = select.value || me?.office || "";
  select.innerHTML = offices
    .map((o) => {
      const label = isEN ? o.label_en : o.label_de;
      return `<option value="${esc(o.key)}">${esc(label)}</option>`;
    })
    .join("");
  if (previous && offices.some((o) => o.key === previous)) {
    select.value = previous;
  }
}

function fillProfilForm() {
  if (!me) return;
  (document.getElementById("profil-email") as HTMLInputElement).value = me.email;
  (document.getElementById("profil-display-name") as HTMLInputElement).value = me.display_name;
  (document.getElementById("profil-office") as HTMLSelectElement).value = me.office;
  (document.getElementById("profil-role") as HTMLInputElement).value = me.job_title ?? "";
  (document.getElementById("profil-lang") as HTMLSelectElement).value = me.lang || "de";
}

async function saveProfil(ev: Event) {
  ev.preventDefault();
  const msg = document.getElementById("profil-msg")!;
  msg.textContent = "";
  msg.className = "form-msg";

  const displayName = (document.getElementById("profil-display-name") as HTMLInputElement).value.trim();
  const office = (document.getElementById("profil-office") as HTMLSelectElement).value;
  const jobTitle = (document.getElementById("profil-role") as HTMLInputElement).value.trim();
  const lang = (document.getElementById("profil-lang") as HTMLSelectElement).value as Lang;

  if (!displayName) {
    msg.textContent = t("einstellungen.profil.error.display_name");
    msg.className = "form-msg form-msg-error";
    return;
  }
  if (!office) {
    msg.textContent = t("einstellungen.profil.error.office");
    msg.className = "form-msg form-msg-error";
    return;
  }
  if (!jobTitle) {
    msg.textContent = t("einstellungen.profil.error.job_title");
    msg.className = "form-msg form-msg-error";
    return;
  }

  const payload = {
    display_name: displayName,
    office,
    job_title: jobTitle,
    lang,
  };

  const submitBtn = (ev.target as HTMLFormElement).querySelector<HTMLButtonElement>("button[type=submit]")!;
  submitBtn.disabled = true;
  try {
    const resp = await fetch("/api/me", {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    if (resp.ok) {
      me = await resp.json();
      msg.textContent = t("einstellungen.saved");
      msg.className = "form-msg form-msg-success";
      // Keep the client-side lang in sync with the newly persisted value so
      // the UI (and other tabs) immediately reflect the choice.
      if (me && me.lang && me.lang !== getLang()) {
        setLang(me.lang);
      }
    } else {
      const data = await resp.json().catch(() => ({}) as { error?: string });
      msg.textContent = data.error || t("einstellungen.error.generic");
      msg.className = "form-msg form-msg-error";
    }
  } catch {
    msg.textContent = t("einstellungen.error.generic");
    msg.className = "form-msg form-msg-error";
  } finally {
    submitBtn.disabled = false;
  }
}

// --- Benachrichtigungen tab -------------------------------------------------

function readPrefBool(key: string, fallback: boolean): boolean {
  if (!me?.email_preferences) return fallback;
  const v = me.email_preferences[key];
  return typeof v === "boolean" ? v : fallback;
}

async function loadPrefsTab() {
  await Promise.all([fetchMe(), loadUserOptions()]);
  fillPrefsForm();
  fillEscalationContactOptions();
}

function fillEscalationContactOptions() {
  const select = document.getElementById("prefs-escalation-contact") as HTMLSelectElement | null;
  if (!select || !me) return;
  // Filter out self — pointing your escalation at yourself is silly and
  // the server enforces it via a CHECK constraint. Sort by display_name
  // for a stable order (then email as tiebreaker for users without a
  // display name).
  const candidates = userOptions
    .filter((u) => u.id !== me!.id)
    .slice()
    .sort((a, b) => {
      const an = (a.display_name || a.email).toLowerCase();
      const bn = (b.display_name || b.email).toLowerCase();
      return an.localeCompare(bn);
    });
  const defaultLabel = t("einstellungen.prefs.escalation.default_option");
  const opts: string[] = [`<option value="">${esc(defaultLabel)}</option>`];
  for (const u of candidates) {
    const label = u.display_name ? `${u.display_name} (${u.email})` : u.email;
    opts.push(`<option value="${esc(u.id)}">${esc(label)}</option>`);
  }
  select.innerHTML = opts.join("");
  select.value = me.escalation_contact_id ?? "";
}

function fillPrefsForm() {
  const master = document.getElementById("prefs-reminders-master") as HTMLInputElement;
  const overdue = document.getElementById("prefs-reminders-overdue") as HTMLInputElement;
  const dueToday = document.getElementById("prefs-reminders-due-today") as HTMLInputElement;
  const dueWarning = document.getElementById("prefs-reminders-due-warning") as HTMLInputElement;

  master.checked = readPrefBool("deadline_reminders", true);
  overdue.checked = readPrefBool("deadline_reminders.overdue", true);
  dueToday.checked = readPrefBool("deadline_reminders.due_today", true);
  dueWarning.checked = readPrefBool("deadline_reminders.due_warning", true);

  // The model returns "HH:MM:SS" but <input type="time"> wants "HH:MM".
  (document.getElementById("prefs-reminder-morning") as HTMLInputElement).value =
    (me?.reminder_morning_time ?? "09:00:00").slice(0, 5);
  (document.getElementById("prefs-reminder-evening") as HTMLInputElement).value =
    (me?.reminder_evening_time ?? "16:00:00").slice(0, 5);
  (document.getElementById("prefs-reminder-timezone") as HTMLInputElement).value =
    me?.reminder_timezone ?? "Europe/Berlin";
  (document.getElementById("prefs-warning-offset") as HTMLInputElement).value =
    String(me?.reminder_warning_offset_days ?? 7);

  updatePrefsSubGroup(master.checked);
  master.addEventListener("change", () => updatePrefsSubGroup(master.checked));
}

function updatePrefsSubGroup(masterOn: boolean) {
  const sub = document.getElementById("prefs-reminders-sub")!;
  sub.querySelectorAll<HTMLInputElement>('input[type="checkbox"]').forEach((el) => {
    el.disabled = !masterOn;
  });
  sub.style.opacity = masterOn ? "1" : "0.5";
}

async function savePrefs(ev: Event) {
  ev.preventDefault();
  const msg = document.getElementById("prefs-msg")!;
  msg.textContent = "";
  msg.className = "form-msg";

  // Start from the existing prefs so we don't erase keys we don't know about
  // (future additions, other subsystems).
  const next: Record<string, unknown> = { ...(me?.email_preferences ?? {}) };
  next["deadline_reminders"] = (document.getElementById("prefs-reminders-master") as HTMLInputElement).checked;
  next["deadline_reminders.overdue"] = (document.getElementById("prefs-reminders-overdue") as HTMLInputElement).checked;
  next["deadline_reminders.due_today"] = (document.getElementById("prefs-reminders-due-today") as HTMLInputElement).checked;
  next["deadline_reminders.due_warning"] = (document.getElementById("prefs-reminders-due-warning") as HTMLInputElement).checked;
  // Retire legacy keys so the next save's PATCH body doesn't keep them on
  // the row indefinitely.
  delete next["deadline_reminders.tomorrow"];
  delete next["deadline_reminders.due_today_evening"];
  delete next["deadline_reminders.weekly"];

  const morning = (document.getElementById("prefs-reminder-morning") as HTMLInputElement).value;
  const evening = (document.getElementById("prefs-reminder-evening") as HTMLInputElement).value;
  const timezone = (document.getElementById("prefs-reminder-timezone") as HTMLInputElement).value.trim();
  const warningOffset = parseInt(
    (document.getElementById("prefs-warning-offset") as HTMLInputElement).value,
    10,
  );
  // "" = clear back to the global_admins fallback; UUID = explicit
  // escalation contact. The server's UpdateProfileInput uses *string with
  // empty-string-as-clear semantics for nullable references.
  const escalationContact = (document.getElementById("prefs-escalation-contact") as HTMLSelectElement).value;

  if (!morning || !evening) {
    msg.textContent = t("einstellungen.prefs.times.error.required");
    msg.className = "form-msg form-msg-error";
    return;
  }
  if (!Number.isFinite(warningOffset) || warningOffset < 1 || warningOffset > 30) {
    msg.textContent = t("einstellungen.prefs.warning_offset.error");
    msg.className = "form-msg form-msg-error";
    return;
  }

  const submitBtn = (ev.target as HTMLFormElement).querySelector<HTMLButtonElement>("button[type=submit]")!;
  submitBtn.disabled = true;
  try {
    const resp = await fetch("/api/me", {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        email_preferences: next,
        reminder_morning_time: morning,
        reminder_evening_time: evening,
        reminder_timezone: timezone || "Europe/Berlin",
        reminder_warning_offset_days: warningOffset,
        escalation_contact_id: escalationContact,
      }),
    });
    if (resp.ok) {
      me = await resp.json();
      msg.textContent = t("einstellungen.saved");
      msg.className = "form-msg form-msg-success";
    } else {
      const data = await resp.json().catch(() => ({}) as { error?: string });
      msg.textContent = data.error || t("einstellungen.error.generic");
      msg.className = "form-msg form-msg-error";
    }
  } catch {
    msg.textContent = t("einstellungen.error.generic");
    msg.className = "form-msg form-msg-error";
  } finally {
    submitBtn.disabled = false;
  }
}

// --- CalDAV tab -------------------------------------------------------------

async function loadCalDAVTab() {
  const ok = await loadCalDAVConfig();
  if (!ok) return;
  fillCalDAVForm();
  renderCalDAVStatus();
  await loadCalDAVLog();
  // Slice 2b — multi-calendar bindings. loadBindingProjects feeds the
  // project picker for scope=project; runs in parallel with the binding
  // list fetch.
  void loadBindingProjects();
  await loadBindings();
}

async function loadCalDAVConfig(): Promise<boolean> {
  try {
    const resp = await fetch("/api/caldav-config");
    if (resp.status === 501) {
      document.getElementById("caldav-disabled")!.style.display = "block";
      document.getElementById("caldav-form")!.style.display = "none";
      return false;
    }
    if (!resp.ok) return false;
    caldavConfig = await resp.json();
    return true;
  } catch {
    return false;
  }
}

function fillCalDAVForm() {
  if (!caldavConfig?.configured) return;
  (document.getElementById("caldav-url") as HTMLInputElement).value = caldavConfig.url ?? "";
  (document.getElementById("caldav-username") as HTMLInputElement).value = caldavConfig.username ?? "";
  (document.getElementById("caldav-calendar-path") as HTMLInputElement).value = caldavConfig.calendar_path ?? "";
  (document.getElementById("caldav-enabled") as HTMLInputElement).checked = caldavConfig.enabled ?? false;
  document.getElementById("caldav-delete-btn")!.style.display = "";
}

function renderCalDAVStatus() {
  const card = document.getElementById("caldav-status-card")!;
  if (!caldavConfig?.configured) {
    card.style.display = "none";
    return;
  }
  card.style.display = "";
  document.getElementById("caldav-last-sync")!.textContent = fmtDateTime(caldavConfig.last_sync_at);
  const errRow = document.getElementById("caldav-status-error-row")!;
  if (caldavConfig.last_sync_error) {
    errRow.style.display = "";
    document.getElementById("caldav-last-error")!.textContent = caldavConfig.last_sync_error;
  } else {
    errRow.style.display = "none";
  }
}

async function loadCalDAVLog() {
  try {
    const resp = await fetch("/api/caldav-config/log");
    if (!resp.ok) return;
    const rows: SyncLogEntry[] = await resp.json();
    const tbody = document.getElementById("caldav-log-body")!;
    const empty = document.getElementById("caldav-log-empty")!;
    if (rows.length === 0) {
      tbody.innerHTML = "";
      empty.style.display = "block";
      return;
    }
    empty.style.display = "none";
    tbody.innerHTML = rows
      .map((r) => {
        const dur = r.duration_ms != null ? `${r.duration_ms} ms` : "\u2014";
        const err = r.error ? `<span class="caldav-status-error">${esc(r.error)}</span>` : "\u2014";
        return `<tr>
          <td>${esc(fmtDateTime(r.occurred_at))}</td>
          <td>${r.items_pushed}</td>
          <td>${r.items_pulled}</td>
          <td>${dur}</td>
          <td>${err}</td>
        </tr>`;
      })
      .join("");
  } catch {
    /* non-fatal */
  }
}

function readCalDAVForm() {
  return {
    url: (document.getElementById("caldav-url") as HTMLInputElement).value.trim(),
    username: (document.getElementById("caldav-username") as HTMLInputElement).value.trim(),
    password: (document.getElementById("caldav-password") as HTMLInputElement).value,
    calendar_path: (document.getElementById("caldav-calendar-path") as HTMLInputElement).value.trim(),
    enabled: (document.getElementById("caldav-enabled") as HTMLInputElement).checked,
  };
}

async function saveCalDAV(ev: Event) {
  ev.preventDefault();
  const msg = document.getElementById("caldav-msg")!;
  msg.textContent = "";

  const payload = readCalDAVForm();
  if (!payload.url || !payload.username) {
    msg.textContent = t("caldav.error.required");
    msg.className = "form-msg form-msg-error";
    return;
  }
  if (!caldavConfig?.configured && !payload.password) {
    msg.textContent = t("caldav.error.password_required");
    msg.className = "form-msg form-msg-error";
    return;
  }

  const submitBtn = (ev.target as HTMLFormElement).querySelector<HTMLButtonElement>("button[type=submit]")!;
  submitBtn.disabled = true;
  try {
    const resp = await fetch("/api/caldav-config", {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    if (resp.ok) {
      caldavConfig = { configured: true, ...(await resp.json()) };
      (document.getElementById("caldav-password") as HTMLInputElement).value = "";
      msg.textContent = t("caldav.saved");
      msg.className = "form-msg form-msg-ok";
      document.getElementById("caldav-delete-btn")!.style.display = "";
      await loadCalDAVConfig();
      renderCalDAVStatus();
      await loadCalDAVLog();
    } else {
      const data = await resp.json().catch(() => ({}) as { error?: string });
      msg.textContent = data.error || t("caldav.error.generic");
      msg.className = "form-msg form-msg-error";
    }
  } catch {
    msg.textContent = t("caldav.error.generic");
    msg.className = "form-msg form-msg-error";
  } finally {
    submitBtn.disabled = false;
  }
}

async function testCalDAVConnection() {
  const msg = document.getElementById("caldav-msg")!;
  msg.textContent = "";
  const payload = readCalDAVForm();
  const btn = document.getElementById("caldav-test-btn") as HTMLButtonElement;
  btn.disabled = true;
  try {
    const resp = await fetch("/api/caldav-config/test", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    const data = await resp.json().catch(() => ({}) as { ok?: boolean; error?: string });
    if (data.ok) {
      msg.textContent = t("caldav.test.ok");
      msg.className = "form-msg form-msg-ok";
    } else {
      msg.textContent = `${t("caldav.test.fail")}: ${data.error ?? "?"}`;
      msg.className = "form-msg form-msg-error";
    }
  } catch (e) {
    msg.textContent = `${t("caldav.test.fail")}: ${(e as Error).message}`;
    msg.className = "form-msg form-msg-error";
  } finally {
    btn.disabled = false;
  }
}

async function deleteCalDAVConfig() {
  const msg = document.getElementById("caldav-msg")!;
  if (!confirm(t("caldav.delete.confirm"))) return;
  msg.textContent = "";
  try {
    const resp = await fetch("/api/caldav-config", { method: "DELETE" });
    if (resp.ok || resp.status === 204) {
      caldavConfig = { configured: false };
      (document.getElementById("caldav-form") as HTMLFormElement).reset();
      document.getElementById("caldav-delete-btn")!.style.display = "none";
      renderCalDAVStatus();
      await loadCalDAVLog();
      msg.textContent = t("caldav.delete.done");
      msg.className = "form-msg form-msg-ok";
    } else {
      const data = await resp.json().catch(() => ({}) as { error?: string });
      msg.textContent = data.error || t("caldav.error.generic");
      msg.className = "form-msg form-msg-error";
    }
  } catch {
    msg.textContent = t("caldav.error.generic");
    msg.className = "form-msg form-msg-error";
  }
}

// --- CalDAV bindings (Slice 2b multi-calendar picker) ---------------------

interface UserCalendarBinding {
  id: string;
  user_id: string;
  calendar_path: string;
  display_name: string;
  scope_kind: "all_visible" | "personal_only" | "project" | "client" | "litigation" | "patent" | "case";
  scope_id?: string | null;
  include_personal: boolean;
  enabled: boolean;
  last_sync_at?: string | null;
  last_sync_error?: string | null;
}

interface DiscoveredCalendar {
  href: string;
  display_name: string;
  supported_components?: string[];
}

interface ProjectListItem {
  id: string;
  reference?: string;
  title?: string;
  type?: string;
}

let bindings: UserCalendarBinding[] = [];
let discoveredCalendars: DiscoveredCalendar[] = [];
let bindingProjects: ProjectListItem[] = [];
let editingBindingID: string | null = null;
// Slice 2c — capability cached from /api/caldav-discover. null = unprobed,
// true = MKCALENDAR supported (show "Create new calendar" radio),
// false = degrade UX (hide radio, surface bilingual notice).
let supportsMKCalendar: boolean | null = null;

async function loadBindings(): Promise<void> {
  const section = document.getElementById("caldav-bindings-section");
  if (!section) return;
  try {
    const resp = await fetch("/api/caldav-bindings");
    if (resp.status === 501) return; // CalDAV unavailable; leave hidden
    if (!resp.ok) return;
    bindings = (await resp.json()) as UserCalendarBinding[];
    section.style.display = "";
    renderBindingsList();
  } catch {
    /* non-fatal */
  }
}

function renderBindingsList(): void {
  const list = document.getElementById("caldav-bindings-list")!;
  const empty = document.getElementById("caldav-bindings-empty")!;
  if (!bindings.length) {
    list.innerHTML = "";
    empty.style.display = "block";
    return;
  }
  empty.style.display = "none";
  list.innerHTML = bindings.map(renderBindingCard).join("");
  // Wire per-card buttons.
  for (const b of bindings) {
    const card = document.getElementById(`caldav-binding-card-${b.id}`);
    if (!card) continue;
    card.querySelector(".caldav-binding-edit-btn")?.addEventListener("click", () => openBindingModal(b));
    card.querySelector(".caldav-binding-delete-btn")?.addEventListener("click", () => deleteBinding(b));
    const toggle = card.querySelector(".caldav-binding-enabled-toggle") as HTMLInputElement | null;
    toggle?.addEventListener("change", () => toggleBindingEnabled(b, toggle.checked));
  }
}

function renderBindingCard(b: UserCalendarBinding): string {
  const label = b.display_name || b.calendar_path;
  const scope = scopeLabel(b);
  const last = b.last_sync_at ? fmtDateTime(b.last_sync_at) : t("caldav.never");
  const err = b.last_sync_error ? `<span class="caldav-status-error">${esc(b.last_sync_error)}</span>` : "";
  return `<div class="caldav-binding-card" id="caldav-binding-card-${esc(b.id)}">
    <div class="caldav-binding-card-row">
      <div class="caldav-binding-card-title">
        <strong>${esc(label)}</strong>
        <span class="caldav-binding-scope-chip">${esc(scope)}</span>
      </div>
      <label class="caldav-toggle-label">
        <input type="checkbox" class="caldav-binding-enabled-toggle" ${b.enabled ? "checked" : ""} />
        <span data-i18n="caldav.bindings.card.enabled">Aktiv</span>
      </label>
    </div>
    <div class="caldav-binding-card-row caldav-binding-card-meta">
      <span class="caldav-binding-path">${esc(b.calendar_path)}</span>
      <span class="caldav-binding-last-sync">${esc(t("caldav.status.last_sync"))} ${esc(last)} ${err}</span>
    </div>
    <div class="caldav-binding-card-actions">
      <button type="button" class="btn-secondary caldav-binding-edit-btn" data-i18n="caldav.bindings.card.edit">Bearbeiten</button>
      <button type="button" class="btn-danger caldav-binding-delete-btn" data-i18n="caldav.bindings.card.remove">Entfernen</button>
    </div>
  </div>`;
}

function scopeLabel(b: UserCalendarBinding): string {
  switch (b.scope_kind) {
    case "all_visible":
      return t("caldav.bindings.scope.all_visible");
    case "personal_only":
      return t("caldav.bindings.scope.personal_only");
    case "project": {
      const p = bindingProjects.find((p) => p.id === b.scope_id);
      const name = p ? p.title || p.reference || p.id.slice(0, 8) : "?";
      return `${t("caldav.bindings.scope.project")}: ${name}`;
    }
    default:
      return b.scope_kind;
  }
}

async function loadBindingProjects(): Promise<void> {
  if (bindingProjects.length) return;
  try {
    const resp = await fetch("/api/projects");
    if (resp.ok) bindingProjects = (await resp.json()) as ProjectListItem[];
  } catch {
    /* ignore */
  }
}

async function loadDiscoveredCalendars(): Promise<void> {
  const sel = document.getElementById("caldav-binding-discover-select") as HTMLSelectElement;
  sel.innerHTML = `<option value="">${esc(t("caldav.bindings.modal.source.loading"))}</option>`;
  try {
    const resp = await fetch("/api/caldav-discover");
    if (!resp.ok) {
      sel.innerHTML = `<option value="">${esc(t("caldav.bindings.modal.source.discover_failed"))}</option>`;
      supportsMKCalendar = null;
      syncBindingSourceModeUI();
      return;
    }
    const data = (await resp.json()) as {
      calendars: DiscoveredCalendar[];
      supports_mkcalendar?: boolean | null;
    };
    discoveredCalendars = data.calendars || [];
    supportsMKCalendar = data.supports_mkcalendar ?? null;
    if (!discoveredCalendars.length) {
      sel.innerHTML = `<option value="">${esc(t("caldav.bindings.modal.source.discover_empty"))}</option>`;
    } else {
      sel.innerHTML = discoveredCalendars
        .map((c) => `<option value="${esc(c.href)}">${esc(c.display_name || c.href)}</option>`)
        .join("");
    }
    syncBindingSourceModeUI();
  } catch {
    sel.innerHTML = `<option value="">${esc(t("caldav.bindings.modal.source.discover_failed"))}</option>`;
    supportsMKCalendar = null;
    syncBindingSourceModeUI();
  }
}

// syncBindingSourceModeUI shows / hides the "Neuen Kalender erstellen"
// radio + the Google-degrade notice based on the cached
// supports_mkcalendar capability. Also flips the visible input
// (dropdown vs URL text box) to match the currently selected mode.
function syncBindingSourceModeUI(): void {
  const createRow = document.getElementById("caldav-binding-source-mode-create-row");
  const degrade = document.getElementById("caldav-binding-degrade-notice");
  if (createRow) createRow.style.display = supportsMKCalendar === true ? "" : "none";
  if (degrade) degrade.style.display = supportsMKCalendar === false ? "" : "none";

  // If supports_mkcalendar flipped to false while "create" was selected,
  // fall back to "existing" so the user isn't staring at a hidden radio.
  if (supportsMKCalendar !== true) {
    const createRadio = document.querySelector(
      'input[name="caldav-binding-source-mode"][value="create"]',
    ) as HTMLInputElement | null;
    if (createRadio?.checked) {
      const existing = document.querySelector(
        'input[name="caldav-binding-source-mode"][value="existing"]',
      ) as HTMLInputElement | null;
      if (existing) existing.checked = true;
    }
  }

  const mode = currentBindingSourceMode();
  const sel = document.getElementById("caldav-binding-discover-select") as HTMLSelectElement;
  const customInput = document.getElementById("caldav-binding-custom-path") as HTMLInputElement;
  sel.style.display = mode === "existing" ? "" : "none";
  customInput.style.display = mode === "custom" ? "" : "none";
}

function currentBindingSourceMode(): "existing" | "create" | "custom" {
  const checked = document.querySelector(
    'input[name="caldav-binding-source-mode"]:checked',
  ) as HTMLInputElement | null;
  return (checked?.value as "existing" | "create" | "custom") ?? "existing";
}

function openBindingModal(b: UserCalendarBinding | null) {
  editingBindingID = b ? b.id : null;
  const modal = document.getElementById("caldav-binding-modal")!;
  const title = document.getElementById("caldav-binding-modal-title")!;
  const submitBtn = document.getElementById("caldav-binding-submit-btn")!;
  const sourceField = document.getElementById("caldav-binding-source-field")!;
  const customInput = document.getElementById("caldav-binding-custom-path") as HTMLInputElement;
  const nameInput = document.getElementById("caldav-binding-display-name") as HTMLInputElement;
  const projectSel = document.getElementById("caldav-binding-project-select") as HTMLSelectElement;
  const msg = document.getElementById("caldav-binding-msg")!;
  msg.textContent = "";

  if (b) {
    title.textContent = t("caldav.bindings.modal.edit_title");
    submitBtn.textContent = t("caldav.bindings.modal.submit_edit");
    sourceField.style.display = "none";
    nameInput.value = b.display_name;
    const radio = document.querySelector(`input[name="caldav-binding-scope"][value="${b.scope_kind}"]`) as HTMLInputElement | null;
    if (radio) radio.checked = true;
  } else {
    title.textContent = t("caldav.bindings.modal.add_title");
    submitBtn.textContent = t("caldav.bindings.modal.submit_add");
    sourceField.style.display = "";
    // Reset the 3-way source-mode radio to "existing" (most common path).
    const existingRadio = document.querySelector(
      'input[name="caldav-binding-source-mode"][value="existing"]',
    ) as HTMLInputElement | null;
    if (existingRadio) existingRadio.checked = true;
    customInput.value = "";
    nameInput.value = "";
    const radio = document.querySelector(`input[name="caldav-binding-scope"][value="all_visible"]`) as HTMLInputElement;
    radio.checked = true;
    void loadDiscoveredCalendars();
  }

  // Project picker — populate options when project scope is picked.
  projectSel.innerHTML = bindingProjects
    .map((p) => `<option value="${esc(p.id)}">${esc((p.title || p.reference || p.id.slice(0, 8)))}</option>`)
    .join("");
  if (b && b.scope_kind === "project" && b.scope_id) {
    projectSel.value = b.scope_id;
    projectSel.disabled = false;
  }
  syncBindingScopeUI();
  syncBindingSourceModeUI();

  modal.style.display = "flex";
}

function closeBindingModal() {
  document.getElementById("caldav-binding-modal")!.style.display = "none";
  editingBindingID = null;
}

function syncBindingScopeUI(): void {
  const scope = (document.querySelector('input[name="caldav-binding-scope"]:checked') as HTMLInputElement | null)?.value;
  const projectSel = document.getElementById("caldav-binding-project-select") as HTMLSelectElement;
  projectSel.disabled = scope !== "project";
}

async function submitBindingModal(ev: Event): Promise<void> {
  ev.preventDefault();
  const msg = document.getElementById("caldav-binding-msg")!;
  msg.textContent = "";
  const customInput = document.getElementById("caldav-binding-custom-path") as HTMLInputElement;
  const sel = document.getElementById("caldav-binding-discover-select") as HTMLSelectElement;
  const nameInput = document.getElementById("caldav-binding-display-name") as HTMLInputElement;
  const projectSel = document.getElementById("caldav-binding-project-select") as HTMLSelectElement;
  const submitBtn = document.getElementById("caldav-binding-submit-btn") as HTMLButtonElement;

  const scope = (document.querySelector('input[name="caldav-binding-scope"]:checked') as HTMLInputElement | null)?.value;
  if (!scope) {
    msg.textContent = t("caldav.bindings.error.scope");
    msg.className = "form-msg form-msg-error";
    return;
  }
  if (scope === "project" && !projectSel.value) {
    msg.textContent = t("caldav.bindings.error.scope_project");
    msg.className = "form-msg form-msg-error";
    return;
  }

  submitBtn.disabled = true;
  try {
    if (editingBindingID) {
      const patchPayload: Record<string, unknown> = {
        display_name: nameInput.value.trim(),
        scope_kind: scope,
        enabled: true,
      };
      if (scope === "project") patchPayload.scope_id = projectSel.value;
      const resp = await fetch(`/api/caldav-bindings/${editingBindingID}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(patchPayload),
      });
      if (!resp.ok) {
        const err = await resp.json().catch(() => ({}) as { error?: string });
        msg.textContent = err.error || t("caldav.error.generic");
        msg.className = "form-msg form-msg-error";
        return;
      }
    } else {
      const mode = currentBindingSourceMode();
      if (mode === "create") {
        // Slice 2c MKCALENDAR path.
        const displayName = nameInput.value.trim();
        if (!displayName) {
          msg.textContent = t("caldav.bindings.error.create_name_required");
          msg.className = "form-msg form-msg-error";
          return;
        }
        const createPayload: Record<string, unknown> = {
          display_name: displayName,
          scope_kind: scope,
        };
        if (scope === "project") createPayload.scope_id = projectSel.value;
        const resp = await fetch("/api/caldav-mkcalendar", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(createPayload),
        });
        if (resp.status === 501) {
          // Race: probe flipped to false between modal-open and submit.
          // Re-sync the UI and surface a helpful message.
          supportsMKCalendar = false;
          syncBindingSourceModeUI();
          msg.textContent = t("caldav.bindings.error.create_unsupported");
          msg.className = "form-msg form-msg-error";
          return;
        }
        if (resp.status === 409) {
          msg.textContent = t("caldav.bindings.error.create_name_taken");
          msg.className = "form-msg form-msg-error";
          return;
        }
        if (!resp.ok) {
          const err = await resp.json().catch(() => ({}) as { error?: string });
          msg.textContent = err.error || t("caldav.error.generic");
          msg.className = "form-msg form-msg-error";
          return;
        }
      } else {
        // existing | custom — POST /api/caldav-bindings with the path.
        const path = mode === "custom" ? customInput.value.trim() : sel.value;
        if (!path) {
          msg.textContent = t("caldav.bindings.error.path");
          msg.className = "form-msg form-msg-error";
          return;
        }
        const postPayload: Record<string, unknown> = {
          calendar_path: path,
          display_name: nameInput.value.trim(),
          scope_kind: scope,
          enabled: true,
        };
        if (scope === "project") postPayload.scope_id = projectSel.value;
        if (!postPayload.display_name && mode === "existing") {
          const opt = sel.options[sel.selectedIndex];
          postPayload.display_name = opt ? opt.text : "";
        }
        const resp = await fetch("/api/caldav-bindings", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(postPayload),
        });
        if (!resp.ok) {
          const err = await resp.json().catch(() => ({}) as { error?: string });
          msg.textContent = err.error || t("caldav.error.generic");
          msg.className = "form-msg form-msg-error";
          return;
        }
      }
    }
    closeBindingModal();
    await loadBindings();
  } catch {
    msg.textContent = t("caldav.error.generic");
    msg.className = "form-msg form-msg-error";
  } finally {
    submitBtn.disabled = false;
  }
}

async function deleteBinding(b: UserCalendarBinding): Promise<void> {
  if (!confirm(t("caldav.bindings.delete.confirm"))) return;
  try {
    const resp = await fetch(`/api/caldav-bindings/${b.id}`, { method: "DELETE" });
    if (!resp.ok && resp.status !== 204 && resp.status !== 202) {
      alert(t("caldav.bindings.delete.failed"));
      return;
    }
    await loadBindings();
  } catch {
    alert(t("caldav.bindings.delete.failed"));
  }
}

async function toggleBindingEnabled(b: UserCalendarBinding, enabled: boolean): Promise<void> {
  try {
    const resp = await fetch(`/api/caldav-bindings/${b.id}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ enabled }),
    });
    if (resp.ok) {
      b.enabled = enabled;
    }
  } catch {
    /* non-fatal */
  }
}

// --- "Meine Partner Units" card on the profile tab -------------------------
//
// Read-only summary of the current user's structural memberships. Membership
// writes are admin-driven via /admin/partner-units.

interface PartnerUnit {
  id: string;
  name: string;
  lead_user_id?: string | null;
  office: string;
}

interface UserOption {
  id: string;
  display_name: string;
  email: string;
}

let userOptions: UserOption[] = [];

async function loadUserOptions(): Promise<void> {
  if (userOptions.length) return;
  try {
    const resp = await fetch("/api/users");
    if (resp.ok) userOptions = (await resp.json()) as UserOption[];
  } catch {
    // ignore
  }
}

async function renderMyPartnerUnits(): Promise<void> {
  const container = document.getElementById("partner-unit-my");
  if (!container || !me) return;
  try {
    const resp = await fetch("/api/partner-units?include=members");
    if (!resp.ok) return;
    type PUWithMembers = PartnerUnit & {
      lead_display_name?: string;
      members: { user_id: string; display_name: string; email: string }[];
    };
    const all = (await resp.json()) as PUWithMembers[];
    const mine = all.filter((u) => u.members.some((m) => m.user_id === me!.id));

    if (!mine.length) {
      container.innerHTML = `<p class="form-hint">${esc(t("partner_unit.none") || "Sie sind noch keiner Partner Unit zugeordnet.")}</p>`;
      return;
    }
    container.innerHTML = mine
      .map(
        (u) => `<div class="partner-unit-card">
          <h3>${esc(u.name)}</h3>
          <p class="form-hint">${esc(tDyn("office." + u.office) || u.office)}${u.lead_display_name ? ` &middot; <strong>${esc(u.lead_display_name)}</strong>` : ""}</p>
          <p class="form-hint"><strong>${u.members.length}</strong> ${esc(t("partner_unit.members_label") || "Mitglieder")}</p>
          <ul class="partner-unit-member-list">
            ${u.members
              .map((m) => `<li>${esc(m.display_name)} <span class="form-hint">(${esc(m.email)})</span></li>`)
              .join("")}
          </ul>
        </div>`,
      )
      .join("");
  } catch {
    // ignore — leave previous render
  }
}

// --- Export tab (t-paliad-214 Slice 1) -------------------------------------

// Personal data export. One button; on click hits GET /api/me/export and the
// browser handles the download via Content-Disposition. We use an anchor +
// hidden iframe pattern so any non-200 response can surface inline instead
// of silently triggering a save dialog with an error-html body.
async function loadExportTab(): Promise<void> {
  // Nothing to fetch on render; the tab is static text + button. Wired in
  // the DOMContentLoaded handler.
}

function runExport(): void {
  const msg = document.getElementById("export-msg");
  const btn = document.getElementById("export-btn") as HTMLButtonElement | null;
  if (msg) msg.textContent = "";
  if (btn) btn.disabled = true;
  // Trigger a navigation to the endpoint. The server sets
  // Content-Disposition: attachment which the browser respects.
  // We use a transient <a download> so the click goes through the
  // normal download path even on browsers that try to render text/json.
  const a = document.createElement("a");
  a.href = "/api/me/export";
  // download="" tells the browser to keep the server-provided filename
  // when one is set via Content-Disposition.
  a.download = "";
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  // Re-enable after a short timeout so users can re-trigger if needed.
  // We don't try to detect download completion — there's no portable
  // browser API for it.
  if (btn) {
    setTimeout(() => {
      btn.disabled = false;
      if (msg)
        msg.textContent =
          t("einstellungen.export.started") ||
          "Download gestartet. Falls nichts passiert, prüfen Sie Ihren Browser-Downloadordner.";
    }, 500);
  }
}

// --- Init -------------------------------------------------------------------

document.addEventListener("DOMContentLoaded", () => {
  initI18n();
  initSidebar();

  wireTabLinks();
  document.getElementById("profil-form")!.addEventListener("submit", saveProfil);
  document.getElementById("prefs-form")!.addEventListener("submit", savePrefs);
  document.getElementById("caldav-form")!.addEventListener("submit", saveCalDAV);
  document.getElementById("caldav-test-btn")!.addEventListener("click", testCalDAVConnection);
  document.getElementById("caldav-delete-btn")!.addEventListener("click", deleteCalDAVConfig);

  // CalDAV bindings (Slice 2b + 2c) — add/edit modal wiring.
  document.getElementById("caldav-bindings-add-btn")?.addEventListener("click", () => openBindingModal(null));
  document.getElementById("caldav-binding-modal-close")?.addEventListener("click", closeBindingModal);
  document.getElementById("caldav-binding-cancel-btn")?.addEventListener("click", closeBindingModal);
  document.getElementById("caldav-binding-form")?.addEventListener("submit", submitBindingModal);
  document.querySelectorAll('input[name="caldav-binding-source-mode"]').forEach((el) => {
    el.addEventListener("change", syncBindingSourceModeUI);
  });
  document.querySelectorAll('input[name="caldav-binding-scope"]').forEach((el) => {
    el.addEventListener("change", syncBindingScopeUI);
  });
  const exportBtn = document.getElementById("export-btn");
  if (exportBtn) exportBtn.addEventListener("click", runExport);

  onLangChange(() => {
    if (loadedTabs.has("profil")) renderOfficeOptions();
    if (loadedTabs.has("caldav")) {
      renderCalDAVStatus();
      void loadCalDAVLog();
    }
  });

  showTab(parseTab(), false);
});
