import { initI18n, onLangChange, t, tDyn, getLang, translateEvent } from "./i18n";
import { initSidebar } from "./sidebar";
import { renderAgendaTimeline, type AgendaItem } from "./agenda-render";

interface DashboardUser {
  id: string;
  email: string;
  display_name: string;
  office: string;
  role: string;
}

interface DeadlineSummary {
  overdue: number;
  today: number;
  this_week: number;
  next_week: number;
  later: number;
}

interface MatterSummary {
  active: number;
  archived: number;
  total: number;
}

interface UpcomingDeadline {
  id: string;
  title: string;
  due_date: string;
  project_id: string;
  project_title: string;
  project_reference: string;
  urgency: "overdue" | "today" | "urgent" | "soon";
}

interface UpcomingAppointment {
  id: string;
  title: string;
  start_at: string;
  end_at: string | null;
  type: string | null;
  project_id: string | null;
  project_title: string | null;
  project_reference: string | null;
}

interface ActivityEntry {
  timestamp: string;
  actor_email: string | null;
  actor_name: string | null;
  project_id: string;
  project_title: string;
  project_reference: string;
  action: string | null;
  details: string;
  description: string | null;
  metadata: Record<string, unknown> | null;
}

interface DashboardData {
  user: DashboardUser | null;
  deadline_summary: DeadlineSummary;
  matter_summary: MatterSummary;
  upcoming_deadlines: UpcomingDeadline[];
  upcoming_appointments: UpcomingAppointment[];
  recent_activity: ActivityEntry[];
  inbox_summary?: InboxSummary;
}

interface InboxEntry {
  id: string;
  entity_type: string;
  entity_title?: string | null;
  project_id: string;
  project_title: string;
  requested_at: string;
  requester_id: string;
  requester_name: string;
}

interface InboxSummary {
  pending_count: number;
  top: InboxEntry[];
}

// DashboardLayoutSpec mirrors the Go shape in
// internal/services/dashboard_layout_spec.go. The client treats the spec
// as advice: unknown widget keys are dropped silently (server is the
// source of truth for the catalog).
interface DashboardWidgetRef {
  key: string;
  visible: boolean;
  settings?: { count?: number; horizon_days?: number };
}
interface DashboardLayoutSpec {
  v: number;
  widgets: DashboardWidgetRef[];
}

declare global {
  interface Window {
    __PALIAD_DASHBOARD__?: DashboardData | null;
    __PALIAD_DASHBOARD_LAYOUT__?: DashboardLayoutSpec | null;
    __PALIAD_DASHBOARD_CATALOG__?: unknown;
  }
}

let currentLayout: DashboardLayoutSpec | null = null;

// settingsFor returns the (possibly-empty) settings blob for a given
// widget key in the active layout. Falls back to an empty object so
// renderers can read `.count ?? defaultN` without null checks.
function settingsFor(key: string): { count?: number; horizon_days?: number } {
  if (!currentLayout) return {};
  for (const w of currentLayout.widgets) {
    if (w.key === key) return w.settings ?? {};
  }
  return {};
}

const POLL_INTERVAL_MS = 60_000;
// 30-day look-ahead matches the agenda.tsx default chip and the server's
// default `to=today+30d` window — keeps the inline agenda visually
// consistent with /agenda when users follow the "full agenda" link.
const AGENDA_LOOKAHEAD_DAYS = 30;
const COLLAPSE_KEY_PREFIX = "paliad:dashboard:collapse:";
let data: DashboardData | null = null;
let agendaItems: AgendaItem[] | null = null;

async function loadDashboard(): Promise<void> {
  const unavailable = document.getElementById("dashboard-unavailable")!;
  try {
    const resp = await fetch("/api/dashboard");
    if (resp.status === 503) {
      unavailable.style.display = "block";
      return;
    }
    if (!resp.ok) {
      unavailable.style.display = "block";
      return;
    }
    data = await resp.json();
    render();
  } catch {
    unavailable.style.display = "block";
  }
}

function render(): void {
  if (!data) return;
  renderGreeting(data.user);
  renderSummary(data.deadline_summary);
  renderMatters(data.matter_summary);
  renderDeadlines(data.upcoming_deadlines);
  renderAppointments(data.upcoming_appointments);
  renderAgenda();
  renderActivity(data.recent_activity);
  renderInbox(data.inbox_summary ?? { pending_count: 0, top: [] });
  toggleOnboardingHint(data.user);
  // Apply the saved layout AFTER renderers so the per-widget settings
  // applied above (count truncation, horizon filtering) are stable
  // before we toggle visibility + reorder. Failing to find the layout
  // is non-fatal — the factory default markup order takes over.
  applyLayout();
}

function renderGreeting(user: DashboardUser | null): void {
  const nameEl = document.getElementById("dashboard-greeting-name")!;
  const chip = document.getElementById("dashboard-office-chip")!;
  const dateEl = document.getElementById("dashboard-date")!;

  if (user) {
    nameEl.textContent = user.display_name ? `, ${user.display_name}` : "";
    const officeLabel = tDyn(`office.${user.office}`) || user.office;
    chip.textContent = officeLabel;
    chip.className = `dashboard-office-chip office-chip office-${user.office}`;
    chip.style.display = "inline-block";
  } else {
    nameEl.textContent = "";
    chip.style.display = "none";
  }

  const now = new Date();
  dateEl.textContent = now.toLocaleDateString(
    getLang() === "de" ? "de-DE" : "en-GB",
    { weekday: "long", year: "numeric", month: "long", day: "numeric" },
  );
}

function renderSummary(s: DeadlineSummary): void {
  setCount("dashboard-count-overdue", s.overdue);
  setCount("dashboard-count-today", s.today);
  setCount("dashboard-count-this-week", s.this_week);
  setCount("dashboard-count-next-week", s.next_week);
  setCount("dashboard-count-later", s.later);

  // Überfällig is an emergency category — hide the card entirely on a clean
  // slate (the .dashboard-summary-grid uses auto-fit so the row re-flows to
  // 4 cards) and trip the alarm styling when there's anything overdue. See
  // t-paliad-105 / t-paliad-106 / t-paliad-110.
  const overdueCard = document.getElementById("dashboard-card-overdue")!;
  overdueCard.classList.toggle("dashboard-card-overdue-hidden", s.overdue === 0);
  overdueCard.classList.toggle("dashboard-card-alarm", s.overdue > 0);
}

function renderMatters(s: MatterSummary): void {
  setCount("dashboard-matter-active", s.active);
  setCount("dashboard-matter-archived", s.archived);
  setCount("dashboard-matter-total", s.total);
}

function renderDeadlines(items: UpcomingDeadline[]): void {
  const list = document.getElementById("dashboard-deadlines-list")!;
  const empty = document.getElementById("dashboard-deadlines-empty")!;

  // Per-widget settings: truncate by count + filter by horizon. Backend
  // returns 40 rows / 60d; the widget settings narrow it. Defaults match
  // the catalog (10 rows, 30 days).
  const s = settingsFor("upcoming-deadlines");
  items = filterByHorizonDays(items, s.horizon_days ?? 30, (d) => d.due_date);
  items = items.slice(0, s.count ?? 10);

  if (!items.length) {
    list.innerHTML = "";
    list.style.display = "none";
    empty.style.display = "block";
    return;
  }
  list.style.display = "";
  empty.style.display = "none";
  list.innerHTML = items.map((d) => {
    const urgencyClass = `dashboard-urgency-${d.urgency}`;
    const urgencyLabel = tDyn(`dashboard.urgency.${d.urgency}`);
    return `<li class="dashboard-list-item">
      <a href="/projects/${esc(d.project_id)}/deadlines" class="dashboard-list-link">
        <div class="dashboard-list-main">
          <span class="dashboard-list-title">${esc(d.title)}</span>
          <span class="dashboard-list-ref" title="${escAttr(`${d.project_reference} · ${d.project_title}`)}">${esc(d.project_reference)} &middot; ${esc(d.project_title)}</span>
        </div>
        <div class="dashboard-list-meta">
          <span class="dashboard-urgency-badge ${urgencyClass}" title="${escAttr(urgencyLabel)}">${esc(formatRelative(d.due_date))}</span>
        </div>
      </a>
    </li>`;
  }).join("");
}

function renderAppointments(items: UpcomingAppointment[]): void {
  const list = document.getElementById("dashboard-appointments-list")!;
  const empty = document.getElementById("dashboard-appointments-empty")!;

  const s = settingsFor("upcoming-appointments");
  items = filterByHorizonDays(items, s.horizon_days ?? 30, (a) => a.start_at);
  items = items.slice(0, s.count ?? 10);

  if (!items.length) {
    list.innerHTML = "";
    list.style.display = "none";
    empty.style.display = "block";
    return;
  }
  list.style.display = "";
  empty.style.display = "none";
  list.innerHTML = items.map((a) => {
    const dot = a.type
      ? `<span class="dashboard-termin-dot dashboard-termin-${esc(a.type)}" aria-hidden="true"></span>`
      : `<span class="dashboard-termin-dot" aria-hidden="true"></span>`;
    const href = a.project_id ? `/projects/${esc(a.project_id)}/appointments` : "#";
    const tag = a.project_id ? "a" : "div";
    const projectLine = a.project_reference && a.project_title
      ? `<span class="dashboard-list-ref" title="${escAttr(`${a.project_reference} · ${a.project_title}`)}">${esc(a.project_reference)} &middot; ${esc(a.project_title)}</span>`
      : "";
    return `<li class="dashboard-list-item">
      <${tag} href="${href}" class="dashboard-list-link">
        <div class="dashboard-list-main">
          <span class="dashboard-list-title">${dot}${esc(a.title)}</span>
          ${projectLine}
        </div>
        <div class="dashboard-list-meta">
          <span class="dashboard-appt-time">${esc(formatDateTime(a.start_at))}</span>
        </div>
      </${tag}>
    </li>`;
  }).join("");
}

function renderActivity(items: ActivityEntry[]): void {
  const list = document.getElementById("dashboard-activity-list")!;
  const empty = document.getElementById("dashboard-activity-empty")!;

  const s = settingsFor("recent-activity");
  items = items.slice(0, s.count ?? 10);

  if (!items.length) {
    list.innerHTML = "";
    list.style.display = "none";
    empty.style.display = "block";
    return;
  }
  list.style.display = "";
  empty.style.display = "none";
  list.innerHTML = items.map((e) => {
    const actor = e.actor_name || e.actor_email || t("dashboard.activity.system");
    const shortKey = e.action ? `dashboard.action.short.${e.action}` : "";
    const translated = shortKey ? tDyn(shortKey) : "";
    const hasI18n = translated !== "" && translated !== shortKey;
    const shortAction = hasI18n
      ? translated
      : (e.action || t("dashboard.activity.event"));
    // Localize the muted detail line so it speaks DE in DE and EN in EN —
    // historical rows carry English nouns inside DE narrative ("Deadline „ok"
    // geändert", "Note zu deadline hinzugefügt"); translateEvent parses both
    // legacy and new (value-only) shapes.
    const stored = e.description ?? (hasI18n ? "" : e.details);
    const { description: detail } = translateEvent(e.action, "", stored);
    // For checklist_* events with a known instance_id, deep-link the project
    // ref straight to the instance — saves a click vs. landing on the project.
    // Falls back to /projects/{id} for any other event or when the instance
    // ID is missing (older rows pre-metadata, or checklist_deleted).
    const ref = activityHref(e);
    return `<li class="dashboard-activity-item">
      <span class="dashboard-activity-time">${esc(formatDateTime(e.timestamp))}</span>
      <div class="dashboard-activity-body">
        <p class="dashboard-activity-summary"><strong>${esc(actor)}</strong> ${esc(shortAction)}</p>
        <p class="dashboard-activity-detail">
          <a href="${escAttr(ref)}" class="dashboard-activity-project">${esc(e.project_reference)}</a>${detail ? ` <span>${esc(detail)}</span>` : ""}
        </p>
      </div>
    </li>`;
  }).join("");
  // Row-level click handler: clicking anywhere on the row navigates to the
  // same target as the inner .dashboard-activity-project link. Inner <a>/
  // <button> clicks bubble through unchanged (Cmd-click → new tab still
  // works) and text remains selectable — same pattern as .entity-table rows
  // (t-098/099) and the project Verlauf cards (t-paliad-103).
  list.querySelectorAll<HTMLLIElement>(".dashboard-activity-item").forEach((row) => {
    const link = row.querySelector<HTMLAnchorElement>(".dashboard-activity-project");
    if (!link) return;
    row.addEventListener("click", (e) => {
      const target = e.target as HTMLElement;
      if (target.closest("a") || target.closest("button")) return;
      window.location.href = link.href;
    });
  });
}

// Resolve an activity row to the most-specific deep-link target. Mirrors the
// rules in projects-detail.ts:eventDetailHref so the activity feed and the
// project Verlauf agree on where each event family points. Falls back to the
// owning project page when no metadata is wired (older rows or _deleted/
// deadlines_imported events). Wired families: checklist_*, deadline_*,
// appointment_*, note_created — see t-paliad-097/102.
function activityHref(e: ActivityEntry): string {
  const action = e.action ?? "";
  const meta = (e.metadata ?? null) as Record<string, unknown> | null;
  if (meta) {
    if (action.startsWith("checklist_") && action !== "checklist_deleted") {
      const id = meta["checklist_instance_id"];
      if (typeof id === "string" && id) return `/checklists/instances/${id}`;
    }
    if (
      action.startsWith("deadline_") &&
      action !== "deadline_deleted" &&
      action !== "deadlines_imported"
    ) {
      const id = meta["deadline_id"];
      if (typeof id === "string" && id) return `/deadlines/${id}`;
    }
    if (action.startsWith("appointment_") && action !== "appointment_deleted") {
      const id = meta["appointment_id"];
      if (typeof id === "string" && id) return `/appointments/${id}`;
    }
    if (action === "note_created") {
      const apptID = meta["appointment_id"];
      if (typeof apptID === "string" && apptID) return `/appointments/${apptID}`;
      const deadlineID = meta["deadline_id"];
      if (typeof deadlineID === "string" && deadlineID) return `/deadlines/${deadlineID}`;
    }
  }
  return `/projects/${e.project_id}`;
}

// Render the inline Agenda section. Items are fetched once on mount via
// loadAgenda(); subsequent re-renders (lang change, dashboard poll) reuse
// the cached array. The dashboard inline agenda is read-only — no chip
// filters, default 30-day window — see CollapsibleSection in
// dashboard.tsx for the surrounding shell.
function renderAgenda(): void {
  const timeline = document.getElementById("dashboard-agenda-timeline");
  const empty = document.getElementById("dashboard-agenda-empty");
  if (!timeline || !empty) return;
  if (agendaItems === null) {
    // Items haven't landed yet — keep the timeline blank but hide empty
    // hint so we don't flash "nothing due" before the fetch resolves.
    timeline.innerHTML = "";
    timeline.style.display = "none";
    empty.style.display = "none";
    return;
  }
  if (!agendaItems.length) {
    timeline.innerHTML = "";
    timeline.style.display = "none";
    empty.style.display = "block";
    return;
  }
  empty.style.display = "none";
  timeline.style.display = "";
  timeline.innerHTML = renderAgendaTimeline(agendaItems);
}

async function loadAgenda(): Promise<void> {
  const s = settingsFor("inline-agenda");
  const horizon = s.horizon_days ?? AGENDA_LOOKAHEAD_DAYS;
  const from = toAgendaDate(startOfToday());
  const to = toAgendaDate(addDays(startOfToday(), horizon - 1));
  try {
    const resp = await fetch(`/api/agenda?from=${from}&to=${to}&types=deadlines,appointments`);
    if (!resp.ok) {
      // Fail silently — the rest of the dashboard still loads. The
      // inline agenda is best-effort: a 503 (DB-less knowledge-platform
      // deploy) or 401 (session timed out, should be caught by the
      // page-level redirect) just leaves the section empty.
      agendaItems = [];
      renderAgenda();
      return;
    }
    agendaItems = (await resp.json()) as AgendaItem[];
    renderAgenda();
  } catch {
    agendaItems = [];
    renderAgenda();
  }
}

function startOfToday(): Date {
  const d = new Date();
  d.setHours(0, 0, 0, 0);
  return d;
}

function addDays(d: Date, days: number): Date {
  const r = new Date(d);
  r.setDate(r.getDate() + days);
  return r;
}

function toAgendaDate(d: Date): string {
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
}

// Wire collapsible-section toggles. Each .dashboard-section carries a
// data-collapse-key and the SSR markup renders aria-expanded="true" so
// unstyled fallback shows everything; here we restore persisted state and
// attach click handlers. Persistence is per-section via localStorage —
// keys live under paliad:dashboard:collapse:<section> per the brief.
function initCollapsibleSections(): void {
  const sections = document.querySelectorAll<HTMLElement>(".dashboard-section[data-collapse-key]");
  sections.forEach((section) => {
    const key = section.dataset.collapseKey || "";
    if (!key) return;
    const stored = localStorage.getItem(COLLAPSE_KEY_PREFIX + key);
    const collapsed = stored === "true";
    applyCollapseState(section, collapsed);

    const toggle = section.querySelector<HTMLButtonElement>(".dashboard-section-toggle");
    if (!toggle) return;
    toggle.addEventListener("click", () => {
      const nowExpanded = section.getAttribute("aria-expanded") === "true";
      const nextCollapsed = nowExpanded; // expanded → collapsing
      applyCollapseState(section, nextCollapsed);
      try {
        localStorage.setItem(COLLAPSE_KEY_PREFIX + key, String(nextCollapsed));
      } catch {
        // localStorage may be full or disabled (Safari private mode);
        // collapse still works for the current page life. Silent.
      }
    });
  });
  // Re-localise the toggle aria-labels on language switch so screen
  // readers always read the current language. The visible heading text
  // is handled by the i18n applyTranslations pass already.
  syncCollapseAriaLabels();
}

function applyCollapseState(section: HTMLElement, collapsed: boolean): void {
  section.setAttribute("aria-expanded", String(!collapsed));
  const toggle = section.querySelector<HTMLButtonElement>(".dashboard-section-toggle");
  if (toggle) {
    toggle.setAttribute("aria-expanded", String(!collapsed));
    toggle.setAttribute(
      "aria-label",
      collapsed ? t("dashboard.section.expand") : t("dashboard.section.collapse"),
    );
  }
}

function syncCollapseAriaLabels(): void {
  document
    .querySelectorAll<HTMLElement>(".dashboard-section[data-collapse-key]")
    .forEach((section) => {
      const collapsed = section.getAttribute("aria-expanded") !== "true";
      applyCollapseState(section, collapsed);
    });
}

function renderInbox(s: InboxSummary): void {
  const summary = document.getElementById("dashboard-inbox-summary");
  const list = document.getElementById("dashboard-inbox-list");
  const empty = document.getElementById("dashboard-inbox-empty");
  if (!summary || !list || !empty) return;

  const settings = settingsFor("inbox-approvals");
  const cap = settings.count ?? 3;
  const top = s.top.slice(0, cap);

  if (s.pending_count === 0) {
    summary.style.display = "none";
    list.innerHTML = "";
    list.style.display = "none";
    empty.style.display = "block";
    return;
  }
  empty.style.display = "none";
  summary.style.display = "block";
  summary.textContent = getLang() === "de"
    ? `${s.pending_count} offene Freigaben warten auf dich.`
    : `${s.pending_count} open approvals are waiting for you.`;
  list.style.display = "";
  list.innerHTML = top.map((e) => {
    const entityLabel = e.entity_type === "deadline"
      ? tDyn("dashboard.inbox.entity.deadline")
      : (e.entity_type === "appointment"
        ? tDyn("dashboard.inbox.entity.appointment")
        : e.entity_type);
    const title = e.entity_title || entityLabel;
    return `<li class="dashboard-list-item">
      <a href="/inbox" class="dashboard-list-link">
        <div class="dashboard-list-main">
          <span class="dashboard-list-title">${esc(title)}</span>
          <span class="dashboard-list-ref" title="${escAttr(`${e.project_title} · ${e.requester_name}`)}">${esc(e.project_title)} &middot; ${esc(e.requester_name)}</span>
        </div>
        <div class="dashboard-list-meta">
          <span class="dashboard-appt-time">${esc(formatDateTime(e.requested_at))}</span>
        </div>
      </a>
    </li>`;
  }).join("");
}

// applyLayout walks the saved DashboardLayoutSpec and hides widgets whose
// keys are `visible: false`, then reorders the visible ones to match the
// layout's order. Widgets in the layout but missing from the DOM are
// ignored (the catalog must define the markup for them — Slice A has
// every catalog widget pre-rendered in dashboard.tsx). Widgets in the
// DOM but missing from the layout (e.g. a deploy added markup ahead of a
// migration) stay in their authored position so nothing disappears
// silently.
//
// Reordering target: the visible widgets live in two parents — the
// outer .container and the .dashboard-columns 2-up grid. We respect
// that boundary: widgets inside .dashboard-columns are reordered within
// it; widgets outside are reordered relative to each other inside
// .container. This keeps the existing 2-up behaviour for the
// deadlines+appointments pair without forcing a full container flatten.
function applyLayout(): void {
  if (!currentLayout || !Array.isArray(currentLayout.widgets)) return;

  // Discover widget elements once. data-widget-key set in dashboard.tsx.
  const allWidgets = Array.from(
    document.querySelectorAll<HTMLElement>("[data-widget-key]"),
  );
  if (!allWidgets.length) return;
  const byKey = new Map<string, HTMLElement>();
  allWidgets.forEach((el) => {
    const k = el.dataset.widgetKey;
    if (k) byKey.set(k, el);
  });

  // Hide widgets whose layout entry says visible:false. Anything not in
  // the layout at all stays untouched.
  const seenInLayout = new Set<string>();
  for (const w of currentLayout.widgets) {
    seenInLayout.add(w.key);
    const el = byKey.get(w.key);
    if (!el) continue;
    el.style.display = w.visible ? "" : "none";
  }

  // Reorder visible widgets inside each parent. We group widgets by their
  // current parent element so we don't move them out of .dashboard-columns
  // and lose the 2-up grid layout.
  const groups = new Map<HTMLElement, HTMLElement[]>();
  for (const w of currentLayout.widgets) {
    if (!w.visible) continue;
    const el = byKey.get(w.key);
    if (!el || !el.parentElement) continue;
    const arr = groups.get(el.parentElement) ?? [];
    arr.push(el);
    groups.set(el.parentElement, arr);
  }
  groups.forEach((widgets, parent) => {
    widgets.forEach((el) => parent.appendChild(el));
  });
}

// filterByHorizonDays drops items whose key date is more than `days`
// days from today. Items without a parseable date stay in (we don't
// want to silently hide rows on bad data). today is inclusive.
function filterByHorizonDays<T>(items: T[], days: number, key: (t: T) => string): T[] {
  if (!Number.isFinite(days) || days <= 0) return items;
  const cutoff = new Date();
  cutoff.setHours(0, 0, 0, 0);
  cutoff.setDate(cutoff.getDate() + days);
  return items.filter((t) => {
    const raw = key(t);
    if (!raw) return true;
    // due_date is "YYYY-MM-DD"; start_at is RFC 3339. Both parseable
    // by Date.
    const d = new Date(raw.length === 10 ? raw + "T00:00:00" : raw);
    if (isNaN(d.getTime())) return true;
    return d.getTime() <= cutoff.getTime();
  });
}

function toggleOnboardingHint(user: DashboardUser | null): void {
  // Belt-and-braces: the server-side gate (gateOnboarded in handlers.go)
  // already redirects users without a paliad.users row to /onboarding before
  // the dashboard HTML is served. If the gate ever misses (e.g. DB lookup
  // errored and we fell through), push the user to /onboarding here so they
  // don't get stuck on a blank dashboard.
  if (!user) {
    window.location.href = "/onboarding";
    return;
  }
  const onboarding = document.getElementById("dashboard-onboarding")!;
  onboarding.style.display = "none";
}

function setCount(id: string, n: number): void {
  const el = document.getElementById(id);
  if (el) el.textContent = String(n);
}

function formatRelative(isoDate: string): string {
  const due = new Date(isoDate + "T00:00:00");
  if (isNaN(due.getTime())) return isoDate;
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const diffDays = Math.round((due.getTime() - today.getTime()) / 86400000);
  const lang = getLang();
  if (diffDays < 0) {
    const n = Math.abs(diffDays);
    return lang === "de"
      ? (n === 1 ? "vor 1 Tag" : `vor ${n} Tagen`)
      : (n === 1 ? "1 day ago" : `${n} days ago`);
  }
  if (diffDays === 0) return t("dashboard.when.today");
  if (diffDays === 1) return t("dashboard.when.tomorrow");
  return lang === "de" ? `in ${diffDays} Tagen` : `in ${diffDays} days`;
}

function formatDateTime(iso: string): string {
  const d = new Date(iso);
  if (isNaN(d.getTime())) return iso;
  const locale = getLang() === "de" ? "de-DE" : "en-GB";
  return d.toLocaleString(locale, {
    day: "2-digit",
    month: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
  });
}

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

function escAttr(s: string): string {
  return (s ?? "").replace(/&/g, "&amp;").replace(/"/g, "&quot;");
}

function schedulePolling(): void {
  // Refresh the payload every minute so open dashboards stay current when
  // teammates create Akten/Fristen. Uses the JSON endpoint — no page reload.
  // The inline agenda is refreshed on the same cadence to stay in sync
  // with the deadlines/appointments rails above it.
  window.setInterval(() => {
    void loadDashboard();
    void loadAgenda();
  }, POLL_INTERVAL_MS);
}

document.addEventListener("DOMContentLoaded", () => {
  initI18n();
  initSidebar();
  initCollapsibleSections();
  onLangChange(() => {
    render();
    syncCollapseAriaLabels();
  });

  // Configurable layout (t-paliad-219). The Go shell handler splices
  // the user's saved layout into __PALIAD_DASHBOARD_LAYOUT__. If it's
  // missing (knowledge-platform-only deploy, hydration failure), the
  // dashboard renders the factory order baked into dashboard.tsx; the
  // client also kicks off a best-effort fetch so a slow-hydrating user
  // still gets their saved layout on the next render pass.
  const layoutInline = window.__PALIAD_DASHBOARD_LAYOUT__;
  if (layoutInline) {
    currentLayout = layoutInline;
  } else if (layoutInline === undefined) {
    void fetch("/api/me/dashboard-layout").then(async (r) => {
      if (!r.ok) return;
      currentLayout = (await r.json()) as DashboardLayoutSpec;
      if (data) render();
    }).catch(() => { /* silent — factory order is the fallback */ });
  }

  // Inline agenda fetch is independent of the main dashboard payload.
  // Kicked off in parallel so the agenda section paints as soon as the
  // /api/agenda response lands instead of waiting on the dashboard
  // payload poll.
  void loadAgenda();

  const inlined = window.__PALIAD_DASHBOARD__;
  if (inlined !== undefined) {
    // Server-side hydration path: the handler spliced the payload directly
    // into the HTML. Render synchronously, then start polling.
    if (inlined === null) {
      document.getElementById("dashboard-unavailable")!.style.display = "block";
    } else {
      data = inlined;
      render();
    }
    schedulePolling();
    return;
  }
  // Fallback for dev or when the handler couldn't splice (no DB, etc.).
  void loadDashboard().then(schedulePolling);
});
