import { t, tDyn, getLang } from "./i18n";

// /projects Cards view (t-paliad-149 PR 2).
//
// Renders one card per project with configurable facts (title row, type
// chip, status, clientmatter, parent path, deadline counts, next 3 events,
// last 3 Verlauf entries, team chips). Layout is per-user, named, and
// drag-rearrangeable in edit mode (see editMode flag below).
//
// Data flow:
//   1. orchestrator (client/projects.ts) calls renderCardsView(...)
//   2. we fetch the active layout (GET /api/user-card-layouts → default first)
//   3. we fetch the projects tree with the orchestrator's chip/search params
//   4. we fetch /api/projects/cards-preview for per-project event rollups
//   5. flatten tree to cards (leaf-ish only by default; "Alle Ebenen" toggle)
//   6. render the grid; lazy-fill preview slots via IntersectionObserver
//      when the project list is huge (cap currently 200 — paliad fits today)
//
// Edit mode toggles in-place: each card grows drag handles + visibility
// toggles + count steppers; the toolbar grows save/discard/rename/delete.

import type { ProjectTreeNode } from "./project-tree";

// ----------------------------------------------------------------------
// Types — mirror internal/services/layout_spec.go
// ----------------------------------------------------------------------

export type FactKey =
  | "title-row"
  | "type-chip"
  | "status-chip"
  | "client-matter"
  | "parent-path"
  | "deadline-counts"
  | "next-events"
  | "recent-verlauf"
  | "team-chips"
  | "reference"
  | "last-activity-at";

const ALL_FACT_KEYS: FactKey[] = [
  "title-row",
  "type-chip",
  "status-chip",
  "client-matter",
  "parent-path",
  "deadline-counts",
  "next-events",
  "recent-verlauf",
  "team-chips",
  "reference",
  "last-activity-at",
];

interface LayoutFact {
  key: FactKey;
  visible: boolean;
  count?: number;
}

interface LayoutSpec {
  facts: LayoutFact[];
  density: "compact" | "roomy";
  grid_columns: "auto" | "2" | "3" | "4";
  show_all_levels: boolean;
}

interface UserCardLayout {
  id: string;
  user_id: string;
  name: string;
  is_default: boolean;
  layout: LayoutSpec;
  created_at: string;
  updated_at: string;
}

interface CardEventPreview {
  kind: "deadline" | "appointment" | "project_event";
  id: string;
  title: string;
  event_date: string;
  status?: string | null;
  actor_name?: string | null;
  route: string;
}

interface ProjectCardPreview {
  project_id: string;
  next_events: CardEventPreview[];
  recent_verlauf: CardEventPreview[];
  team_initials: string[];
  team_count: number;
  last_activity_at?: string | null;
}

// ----------------------------------------------------------------------
// Module state
// ----------------------------------------------------------------------

let layouts: UserCardLayout[] = [];
let activeLayoutId: string | null = null;
let editMode = false;
let editDraft: LayoutSpec | null = null;
let treeCache: ProjectTreeNode[] = [];
let previewCache: Map<string, ProjectCardPreview> = new Map();

// ----------------------------------------------------------------------
// Public entry — called by the orchestrator when view-mode = "cards"
// ----------------------------------------------------------------------

export interface CardsViewOpts {
  treeParams: URLSearchParams;
}

export async function renderCardsView(opts: CardsViewOpts) {
  const wrap = document.getElementById("projects-cards-wrap");
  const toolbar = document.getElementById("projects-cards-toolbar");
  const grid = document.getElementById("projects-cards-grid");
  if (!wrap || !toolbar || !grid) return;
  wrap.style.display = "block";
  toolbar.style.display = "flex";

  // Step 1: layouts.
  if (layouts.length === 0) {
    await reloadLayouts();
  }
  if (layouts.length === 0) {
    grid.innerHTML = `<div class="projects-cards-empty">${escHTML(t("projects.cards.empty") || "Keine Projekte zum Anzeigen.")}</div>`;
    return;
  }
  if (!activeLayoutId) {
    const def = layouts.find((l) => l.is_default) || layouts[0];
    activeLayoutId = def.id;
  }
  populateLayoutSelect();
  attachToolbarHandlers();

  // Step 2: tree (chip/search-narrowed) + cards preview.
  const treeURL = `/api/projects/tree?${opts.treeParams.toString()}`;
  const previewURL = `/api/projects/cards-preview`;
  const [treeResp, previewResp] = await Promise.all([
    fetch(treeURL).then((r) => (r.ok ? r.json() : [])),
    fetch(previewURL).then((r) => (r.ok ? r.json() : [])),
  ]);
  treeCache = treeResp as ProjectTreeNode[];
  previewCache = new Map();
  for (const p of previewResp as ProjectCardPreview[]) {
    previewCache.set(p.project_id, p);
  }

  rerender();
}

export function teardownCardsView() {
  const wrap = document.getElementById("projects-cards-wrap");
  const toolbar = document.getElementById("projects-cards-toolbar");
  const editToolbar = document.getElementById("projects-cards-edit-toolbar");
  if (wrap) wrap.style.display = "none";
  if (toolbar) toolbar.style.display = "none";
  if (editToolbar) editToolbar.style.display = "none";
  editMode = false;
  editDraft = null;
}

// ----------------------------------------------------------------------
// Layout management — fetch + select + edit mode
// ----------------------------------------------------------------------

async function reloadLayouts(): Promise<void> {
  const resp = await fetch("/api/user-card-layouts");
  if (!resp.ok) {
    layouts = [];
    return;
  }
  layouts = (await resp.json()) as UserCardLayout[];
  // Server may not have any rows yet — auto-seed by hitting the cards
  // view's "default" path: GET /api/user-card-layouts/__seed-default__
  // Actually the seed happens server-side on GetDefault; we trigger it
  // by making a no-op preview request which doesn't pull layouts. The
  // simplest path: if empty, POST a Standard layout from the client.
  if (layouts.length === 0) {
    const seed = await fetch("/api/user-card-layouts", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name: "Standard", layout: defaultLayout(), is_default: true }),
    });
    if (seed.ok) {
      const row = (await seed.json()) as UserCardLayout;
      layouts = [row];
    }
  }
}

function defaultLayout(): LayoutSpec {
  return {
    facts: [
      { key: "title-row", visible: true },
      { key: "type-chip", visible: true },
      { key: "status-chip", visible: true },
      { key: "client-matter", visible: true },
      { key: "parent-path", visible: true },
      { key: "deadline-counts", visible: true },
      { key: "next-events", visible: true, count: 3 },
      { key: "recent-verlauf", visible: true, count: 3 },
      { key: "team-chips", visible: true },
    ],
    density: "roomy",
    grid_columns: "auto",
    show_all_levels: false,
  };
}

function getActiveLayout(): UserCardLayout | null {
  if (!activeLayoutId) return null;
  return layouts.find((l) => l.id === activeLayoutId) || null;
}

function getEffectiveSpec(): LayoutSpec {
  if (editMode && editDraft) return editDraft;
  const a = getActiveLayout();
  return a ? a.layout : defaultLayout();
}

function populateLayoutSelect() {
  const sel = document.getElementById("projects-cards-layout-select") as HTMLSelectElement | null;
  if (!sel) return;
  sel.innerHTML = "";
  for (const l of layouts) {
    const opt = document.createElement("option");
    opt.value = l.id;
    opt.textContent = l.is_default ? `${l.name} · ${t("projects.cards.layout.is_default") || "Standard"}` : l.name;
    sel.appendChild(opt);
  }
  if (activeLayoutId) sel.value = activeLayoutId;
}

function attachToolbarHandlers() {
  const sel = document.getElementById("projects-cards-layout-select") as HTMLSelectElement | null;
  const editBtn = document.getElementById("projects-cards-layout-edit") as HTMLButtonElement | null;
  const newBtn = document.getElementById("projects-cards-layout-new") as HTMLButtonElement | null;
  const showAll = document.getElementById("projects-cards-show-all-levels") as HTMLInputElement | null;

  // Idempotent attach guards via dataset flag.
  if (sel && !sel.dataset.bound) {
    sel.dataset.bound = "1";
    sel.addEventListener("change", () => {
      activeLayoutId = sel.value;
      const a = getActiveLayout();
      if (showAll && a) showAll.checked = a.layout.show_all_levels;
      rerender();
    });
  }
  if (editBtn && !editBtn.dataset.bound) {
    editBtn.dataset.bound = "1";
    editBtn.addEventListener("click", () => enterEditMode());
  }
  if (newBtn && !newBtn.dataset.bound) {
    newBtn.dataset.bound = "1";
    newBtn.addEventListener("click", () => createNewLayout());
  }
  if (showAll && !showAll.dataset.bound) {
    showAll.dataset.bound = "1";
    showAll.checked = getEffectiveSpec().show_all_levels;
    showAll.addEventListener("change", () => {
      // In view mode this is a save-as-active toggle. In edit mode it
      // updates the draft.
      if (editMode && editDraft) {
        editDraft.show_all_levels = showAll.checked;
        rerender();
      } else {
        const a = getActiveLayout();
        if (!a) return;
        const newSpec: LayoutSpec = { ...a.layout, show_all_levels: showAll.checked };
        void persistLayout(a.id, { layout: newSpec });
        a.layout = newSpec;
        rerender();
      }
    });
  }

  // Edit-toolbar wiring (only first time the elements exist).
  const eDensity = document.getElementById("projects-cards-edit-density") as HTMLSelectElement | null;
  const eGrid = document.getElementById("projects-cards-edit-grid") as HTMLSelectElement | null;
  const eRename = document.getElementById("projects-cards-edit-rename") as HTMLButtonElement | null;
  const eDelete = document.getElementById("projects-cards-edit-delete") as HTMLButtonElement | null;
  const eSetDefault = document.getElementById("projects-cards-edit-set-default") as HTMLButtonElement | null;
  const eDiscard = document.getElementById("projects-cards-edit-discard") as HTMLButtonElement | null;
  const eSave = document.getElementById("projects-cards-edit-save") as HTMLButtonElement | null;

  if (eDensity && !eDensity.dataset.bound) {
    eDensity.dataset.bound = "1";
    eDensity.addEventListener("change", () => {
      if (!editDraft) return;
      editDraft.density = eDensity.value as "compact" | "roomy";
      rerender();
    });
  }
  if (eGrid && !eGrid.dataset.bound) {
    eGrid.dataset.bound = "1";
    eGrid.addEventListener("change", () => {
      if (!editDraft) return;
      editDraft.grid_columns = eGrid.value as LayoutSpec["grid_columns"];
      rerender();
    });
  }
  if (eRename && !eRename.dataset.bound) {
    eRename.dataset.bound = "1";
    eRename.addEventListener("click", () => renameActiveLayout());
  }
  if (eDelete && !eDelete.dataset.bound) {
    eDelete.dataset.bound = "1";
    eDelete.addEventListener("click", () => deleteActiveLayout());
  }
  if (eSetDefault && !eSetDefault.dataset.bound) {
    eSetDefault.dataset.bound = "1";
    eSetDefault.addEventListener("click", () => setActiveAsDefault());
  }
  if (eDiscard && !eDiscard.dataset.bound) {
    eDiscard.dataset.bound = "1";
    eDiscard.addEventListener("click", () => leaveEditMode(false));
  }
  if (eSave && !eSave.dataset.bound) {
    eSave.dataset.bound = "1";
    eSave.addEventListener("click", () => leaveEditMode(true));
  }
}

function enterEditMode() {
  const a = getActiveLayout();
  if (!a) return;
  editMode = true;
  editDraft = JSON.parse(JSON.stringify(a.layout)) as LayoutSpec;
  reflectEditToolbar();
  rerender();
}

async function leaveEditMode(saveChanges: boolean) {
  const a = getActiveLayout();
  if (!a) {
    editMode = false;
    editDraft = null;
    rerender();
    return;
  }
  if (saveChanges && editDraft) {
    await persistLayout(a.id, { layout: editDraft });
    a.layout = editDraft;
  }
  editMode = false;
  editDraft = null;
  reflectEditToolbar();
  rerender();
}

function reflectEditToolbar() {
  const editToolbar = document.getElementById("projects-cards-edit-toolbar");
  if (editToolbar) editToolbar.style.display = editMode ? "flex" : "none";

  if (editMode && editDraft) {
    const eDensity = document.getElementById("projects-cards-edit-density") as HTMLSelectElement | null;
    const eGrid = document.getElementById("projects-cards-edit-grid") as HTMLSelectElement | null;
    if (eDensity) eDensity.value = editDraft.density;
    if (eGrid) eGrid.value = editDraft.grid_columns;
  }
}

async function persistLayout(id: string, patch: Partial<{ name: string; layout: LayoutSpec; is_default: boolean }>) {
  const resp = await fetch(`/api/user-card-layouts/${encodeURIComponent(id)}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(patch),
  });
  if (resp.ok) {
    const row = (await resp.json()) as UserCardLayout;
    const idx = layouts.findIndex((l) => l.id === id);
    if (idx >= 0) layouts[idx] = row;
  }
}

async function createNewLayout() {
  const name = window.prompt(t("projects.cards.layout.new.prompt") || "Name der neuen Ansicht");
  if (!name || !name.trim()) return;
  const seed = JSON.parse(JSON.stringify(getEffectiveSpec())) as LayoutSpec;
  const resp = await fetch("/api/user-card-layouts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name: name.trim(), layout: seed, is_default: false }),
  });
  if (resp.status === 409) {
    window.alert("Name already exists.");
    return;
  }
  if (!resp.ok) return;
  const row = (await resp.json()) as UserCardLayout;
  layouts.push(row);
  activeLayoutId = row.id;
  populateLayoutSelect();
  enterEditMode();
}

async function renameActiveLayout() {
  const a = getActiveLayout();
  if (!a) return;
  const name = window.prompt(t("projects.cards.layout.rename") || "Umbenennen", a.name);
  if (!name || !name.trim() || name.trim() === a.name) return;
  await persistLayout(a.id, { name: name.trim() });
  populateLayoutSelect();
}

async function deleteActiveLayout() {
  const a = getActiveLayout();
  if (!a) return;
  if (a.is_default) {
    window.alert(t("projects.cards.layout.delete.default_blocked") || "Cannot delete default.");
    return;
  }
  if (!window.confirm(t("projects.cards.layout.delete.confirm") || "Delete?")) return;
  const resp = await fetch(`/api/user-card-layouts/${encodeURIComponent(a.id)}`, { method: "DELETE" });
  if (!resp.ok && resp.status !== 204) return;
  layouts = layouts.filter((l) => l.id !== a.id);
  const def = layouts.find((l) => l.is_default) || layouts[0];
  activeLayoutId = def ? def.id : null;
  editMode = false;
  editDraft = null;
  populateLayoutSelect();
  reflectEditToolbar();
  rerender();
}

async function setActiveAsDefault() {
  const a = getActiveLayout();
  if (!a) return;
  const resp = await fetch(`/api/user-card-layouts/${encodeURIComponent(a.id)}/set-default`, {
    method: "POST",
  });
  if (!resp.ok) return;
  // Refresh the list — server cleared the prior default in tx.
  await reloadLayouts();
  populateLayoutSelect();
  rerender();
}

// ----------------------------------------------------------------------
// Rendering
// ----------------------------------------------------------------------

function rerender() {
  const grid = document.getElementById("projects-cards-grid");
  if (!grid) return;
  const spec = getEffectiveSpec();

  // Apply grid columns + density.
  grid.classList.toggle("is-density-compact", spec.density === "compact");
  grid.classList.toggle("is-density-roomy", spec.density === "roomy");
  grid.classList.remove("is-grid-2", "is-grid-3", "is-grid-4");
  if (spec.grid_columns !== "auto") {
    grid.classList.add(`is-grid-${spec.grid_columns}`);
  }

  const cards = flattenTreeToCards(treeCache, spec.show_all_levels);
  if (cards.length === 0) {
    grid.innerHTML = `<div class="projects-cards-empty">${escHTML(t("projects.cards.empty") || "Keine Projekte zum Anzeigen.")}</div>`;
    return;
  }

  grid.innerHTML = cards.map((n) => renderCard(n, spec)).join("");
  attachCardHandlers(grid);
}

function flattenTreeToCards(roots: ProjectTreeNode[], showAllLevels: boolean): ProjectTreeNode[] {
  const out: ProjectTreeNode[] = [];
  const walk = (n: ProjectTreeNode) => {
    if (showAllLevels) {
      out.push(n);
    } else if (isLeafish(n)) {
      out.push(n);
    }
    for (const c of n.children) walk(c);
  };
  roots.forEach(walk);
  // Sort by last_activity_at DESC (from preview), pinned first.
  out.sort((a, b) => {
    if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
    const aT = previewCache.get(a.id)?.last_activity_at || "";
    const bT = previewCache.get(b.id)?.last_activity_at || "";
    if (aT !== bT) return bT.localeCompare(aT);
    return a.title.localeCompare(b.title);
  });
  return out;
}

function isLeafish(n: ProjectTreeNode): boolean {
  // Cases / Patents / Verfahren / Projekte. Mandanten + Litigations are
  // scaffolding when "Alle Ebenen" is off.
  return n.type === "case" || n.type === "patent" || n.type === "project";
}

function renderCard(n: ProjectTreeNode, spec: LayoutSpec): string {
  const visibleFacts = spec.facts.filter((f) => f.visible);
  const factHTML = visibleFacts.map((f) => renderFact(n, f, spec)).join("");

  const editChrome = editMode ? renderEditChromeForCard(spec) : "";

  return (
    `<article class="projects-card${editMode ? " is-edit-mode" : ""}" data-id="${escAttr(n.id)}">` +
    factHTML +
    editChrome +
    `</article>`
  );
}

function renderEditChromeForCard(spec: LayoutSpec): string {
  // Inline fact-row controls (drag handle + visibility toggle) per fact key.
  // We render this as a horizontally-laid-out list at the bottom of each
  // card so the user can rearrange order. In edit mode the rendered facts
  // above are "preview"; the ordering controls below mutate the draft.
  const rows = spec.facts.map((f, i) => {
    const label = t(`projects.cards.layout.fact.${f.key}` as never) || f.key;
    const cnt = f.count !== undefined
      ? `<input type="number" min="1" max="5" value="${f.count}" class="projects-cards-edit-count" data-key="${escAttr(f.key)}" />`
      : "";
    return (
      `<li class="projects-cards-edit-fact" draggable="true" data-key="${escAttr(f.key)}" data-index="${i}">` +
        `<span class="projects-cards-edit-handle" aria-hidden="true">⠿</span>` +
        `<input type="checkbox" class="projects-cards-edit-vis" ${f.visible ? "checked" : ""} data-key="${escAttr(f.key)}" />` +
        `<span class="projects-cards-edit-label">${escHTML(String(label))}</span>` +
        cnt +
      `</li>`
    );
  }).join("");
  return `<ul class="projects-cards-edit-facts">${rows}</ul>`;
}

function renderFact(n: ProjectTreeNode, f: LayoutFact, spec: LayoutSpec): string {
  const preview = previewCache.get(n.id);
  switch (f.key) {
    case "title-row":
      return renderTitleRow(n);
    case "type-chip":
      return `<div class="projects-card-row"><span class="entity-type-chip entity-type-${escAttr(n.type)}">${escHTML(String(tDyn(`projects.type.${n.type}`) || n.type))}</span></div>`;
    case "status-chip":
      return `<div class="projects-card-row"><span class="entity-status-chip entity-status-${escAttr(n.status)}">${escHTML(String(tDyn(`projects.filter.status.${n.status}`) || n.status))}</span></div>`;
    case "client-matter": {
      const cm = (n.client_number && n.matter_number)
        ? `${n.client_number}.${n.matter_number}`
        : (n.client_number || n.matter_number || "");
      if (!cm) return "";
      return `<div class="projects-card-row projects-card-cm">${escHTML(cm)}</div>`;
    }
    case "parent-path":
      // Parent path is omitted in v1 — building the breadcrumb requires
      // an extra fetch per card. Display the project's own .reference if
      // present as a stand-in cue for hierarchy.
      if (!n.reference) return "";
      return `<div class="projects-card-row projects-card-ref">${escHTML(n.reference)}</div>`;
    case "deadline-counts": {
      const open = n.open_deadlines_subtree ?? n.open_deadlines;
      const overdue = n.overdue_deadlines_subtree ?? n.overdue_deadlines;
      if (open === 0 && overdue === 0) return "";
      const parts: string[] = [];
      if (overdue > 0) parts.push(`<span class="projekt-tree-badge projekt-tree-badge-overdue">${overdue} ${escHTML(String(t("projects.cards.deadline_overdue") || "überfällig"))}</span>`);
      if (open > 0) parts.push(`<span class="projekt-tree-badge projekt-tree-badge-open">${open} ${escHTML(String(t("projects.cards.deadline_open") || "offen"))}</span>`);
      return `<div class="projects-card-row projects-card-counts">${parts.join(" ")}</div>`;
    }
    case "next-events": {
      const cap = clampCount(f.count);
      const evs = preview?.next_events || [];
      if (evs.length === 0) {
        return `<div class="projects-card-section"><div class="projects-card-section-title">${escHTML(String(t("projects.cards.next_events") || "Nächste Termine"))}</div><div class="projects-card-empty">${escHTML(String(t("projects.cards.no_next_events") || ""))}</div></div>`;
      }
      const rows = evs.slice(0, cap).map((e) => renderEventRow(e)).join("");
      return `<div class="projects-card-section"><div class="projects-card-section-title">${escHTML(String(t("projects.cards.next_events") || "Nächste Termine"))}</div>${rows}</div>`;
    }
    case "recent-verlauf": {
      const cap = clampCount(f.count);
      const evs = preview?.recent_verlauf || [];
      if (evs.length === 0) {
        return `<div class="projects-card-section"><div class="projects-card-section-title">${escHTML(String(t("projects.cards.recent_verlauf") || "Zuletzt"))}</div><div class="projects-card-empty">${escHTML(String(t("projects.cards.no_recent") || ""))}</div></div>`;
      }
      const rows = evs.slice(0, cap).map((e) => renderEventRow(e)).join("");
      return `<div class="projects-card-section"><div class="projects-card-section-title">${escHTML(String(t("projects.cards.recent_verlauf") || "Zuletzt"))}</div>${rows}</div>`;
    }
    case "team-chips": {
      if (!preview || preview.team_count === 0) return "";
      const initials = preview.team_initials.map((i) => `<span class="projects-card-team-initial">${escHTML(i)}</span>`).join("");
      const overflow = preview.team_count > preview.team_initials.length
        ? `<span class="projects-card-team-overflow">+${preview.team_count - preview.team_initials.length}</span>`
        : "";
      return `<div class="projects-card-row projects-card-team">${initials}${overflow}</div>`;
    }
    case "reference":
      if (!n.reference) return "";
      return `<div class="projects-card-row projects-card-ref">${escHTML(n.reference)}</div>`;
    case "last-activity-at": {
      const at = preview?.last_activity_at;
      if (!at) return "";
      return `<div class="projects-card-row projects-card-last-activity">${escHTML(fmtDate(at))}</div>`;
    }
    default:
      return "";
  }
  // unreachable
  void spec;
}

function renderTitleRow(n: ProjectTreeNode): string {
  const pinClass = n.pinned ? " is-pinned" : "";
  const pinLabel = n.pinned
    ? (t("projects.tree.unpin") || "Pin entfernen")
    : (t("projects.tree.pin") || "Anpinnen");
  return (
    `<div class="projects-card-title-row">` +
      `<span class="projects-card-icon projekt-tree-icon-${escAttr(n.type)}" aria-hidden="true">●</span>` +
      `<a class="projects-card-title" href="/projects/${escAttr(n.id)}">${escHTML(n.title)}</a>` +
      `<button type="button" class="projekt-tree-pin projects-card-pin${pinClass}" data-id="${escAttr(n.id)}" aria-label="${escAttr(String(pinLabel))}" title="${escAttr(String(pinLabel))}">${n.pinned ? "★" : "☆"}</button>` +
    `</div>`
  );
}

function renderEventRow(e: CardEventPreview): string {
  const kindLabel = t(`projects.cards.event.kind.${e.kind}` as never) || e.kind;
  const dateStr = fmtDate(e.event_date);
  const status = e.status ? ` <span class="projects-card-event-status entity-status-chip entity-status-${escAttr(e.status)}">${escHTML(String(tDyn(`projects.filter.status.${e.status}`) || e.status))}</span>` : "";
  const actor = e.actor_name ? ` <span class="projects-card-event-actor">${escHTML(e.actor_name)}</span>` : "";
  return (
    `<a class="projects-card-event-row" href="${escAttr(e.route)}" title="${escAttr(e.title)}">` +
      `<span class="projects-card-event-date">${escHTML(dateStr)}</span>` +
      `<span class="projects-card-event-kind">${escHTML(String(kindLabel))}</span>` +
      `<span class="projects-card-event-title">${escHTML(e.title)}</span>` +
      status +
      actor +
    `</a>`
  );
}

function clampCount(n: number | undefined): number {
  if (n === undefined) return 3;
  if (n < 1) return 1;
  if (n > 5) return 5;
  return Math.floor(n);
}

// ----------------------------------------------------------------------
// Card-level event handlers (pin click + edit-mode drag/check/count)
// ----------------------------------------------------------------------

function attachCardHandlers(grid: HTMLElement) {
  // Pin star (always-active, edit mode or not).
  grid.querySelectorAll<HTMLButtonElement>(".projects-card-pin").forEach((btn) => {
    btn.addEventListener("click", (e) => {
      e.preventDefault();
      e.stopPropagation();
      const id = btn.dataset.id!;
      void togglePin(id, btn);
    });
  });

  if (!editMode || !editDraft) return;

  // Edit-mode: visibility checkbox per fact.
  grid.querySelectorAll<HTMLInputElement>(".projects-cards-edit-vis").forEach((cb) => {
    cb.addEventListener("change", () => {
      if (!editDraft) return;
      const key = cb.dataset.key as FactKey;
      const f = editDraft.facts.find((x) => x.key === key);
      if (!f) return;
      f.visible = cb.checked;
      // The first VISIBLE fact must remain "title-row" — gate this.
      // Easier approach: always keep title-row visible regardless of click.
      const titleRow = editDraft.facts.find((x) => x.key === "title-row");
      if (titleRow) titleRow.visible = true;
      rerender();
    });
  });

  // Edit-mode: count steppers for next-events / recent-verlauf.
  grid.querySelectorAll<HTMLInputElement>(".projects-cards-edit-count").forEach((inp) => {
    inp.addEventListener("change", () => {
      if (!editDraft) return;
      const key = inp.dataset.key as FactKey;
      const f = editDraft.facts.find((x) => x.key === key);
      if (!f) return;
      const v = Math.max(1, Math.min(5, parseInt(inp.value, 10) || 3));
      f.count = v;
      rerender();
    });
  });

  // Edit-mode: HTML5 drag-and-drop for fact reordering. We attach handlers
  // on each .projects-cards-edit-fact <li>; dragover on <ul>; drop reorders
  // the editDraft.facts array.
  grid.querySelectorAll<HTMLLIElement>(".projects-cards-edit-fact").forEach((li) => {
    li.addEventListener("dragstart", (ev) => {
      const key = li.dataset.key!;
      ev.dataTransfer?.setData("text/plain", key);
      li.classList.add("is-dragging");
    });
    li.addEventListener("dragend", () => li.classList.remove("is-dragging"));
    li.addEventListener("dragover", (ev) => {
      ev.preventDefault();
      li.classList.add("is-drop-target");
    });
    li.addEventListener("dragleave", () => li.classList.remove("is-drop-target"));
    li.addEventListener("drop", (ev) => {
      ev.preventDefault();
      li.classList.remove("is-drop-target");
      const fromKey = ev.dataTransfer?.getData("text/plain") as FactKey | undefined;
      const toKey = li.dataset.key as FactKey;
      if (!fromKey || !toKey || fromKey === toKey) return;
      reorderFacts(fromKey, toKey);
      rerender();
    });
  });
}

function reorderFacts(fromKey: FactKey, toKey: FactKey) {
  if (!editDraft) return;
  const fromIdx = editDraft.facts.findIndex((f) => f.key === fromKey);
  const toIdx = editDraft.facts.findIndex((f) => f.key === toKey);
  if (fromIdx < 0 || toIdx < 0) return;
  const [moved] = editDraft.facts.splice(fromIdx, 1);
  editDraft.facts.splice(toIdx, 0, moved);
  // Server validator requires title-row to be the first visible fact.
  // Pull it to the top if it's now somewhere else.
  const trIdx = editDraft.facts.findIndex((f) => f.key === "title-row");
  if (trIdx > 0) {
    const [tr] = editDraft.facts.splice(trIdx, 1);
    editDraft.facts.unshift(tr);
  }
}

async function togglePin(projectID: string, btn: HTMLElement) {
  const wasPinned = btn.classList.contains("is-pinned");
  btn.classList.toggle("is-pinned", !wasPinned);
  btn.textContent = !wasPinned ? "★" : "☆";
  try {
    const resp = await fetch(`/api/projects/${encodeURIComponent(projectID)}/pin`, {
      method: wasPinned ? "DELETE" : "POST",
    });
    if (!resp.ok && resp.status !== 201 && resp.status !== 204) {
      btn.classList.toggle("is-pinned", wasPinned);
      btn.textContent = wasPinned ? "★" : "☆";
      return;
    }
    // Update tree cache in place so re-renders show the new state.
    const update = (n: ProjectTreeNode) => {
      if (n.id === projectID) n.pinned = !wasPinned;
      n.children.forEach(update);
    };
    treeCache.forEach(update);
  } catch {
    btn.classList.toggle("is-pinned", wasPinned);
    btn.textContent = wasPinned ? "★" : "☆";
  }
}

// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------

function fmtDate(iso: string): string {
  try {
    const d = new Date(iso);
    return d.toLocaleDateString(getLang() === "de" ? "de-DE" : "en-GB", {
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
    });
  } catch {
    return iso;
  }
}

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

function escAttr(s: string): string {
  return s.replace(/[&<>"']/g, (c) => {
    switch (c) {
      case "&": return "&amp;";
      case "<": return "&lt;";
      case ">": return "&gt;";
      case '"': return "&quot;";
      case "'": return "&#39;";
    }
    return c;
  });
}

// Avoid an "unused" warning from the type-only import.
const _unusedFactKeys: FactKey[] = ALL_FACT_KEYS;
void _unusedFactKeys;
