// t-paliad-088: Event Types — shared client module.
//
// Four surfaces share this module:
//   1. EventTypePicker — multi-tag chip cluster on /deadlines/new and the
//      /deadlines/{id} edit form. Lets the user pick 0..N event types via
//      search-as-you-type, "Alle anzeigen" (browse-all), or "+ Neuer Typ".
//   2. EventTypeMultiSelectFilter — listbox-panel filter on /deadlines and
//      /agenda. Multi-select with search + "Alle" + "Ohne Typ" specials.
//   3. AddEventTypeModal — opened from inside the picker via a
//      "+ Neuen Typ hinzufügen…" affordance. Any authenticated user may
//      publish firm-wide types (per m's Q6); admins moderate via archive.
//   4. BrowseAllEventTypesModal (t-paliad-107) — opened from the picker via
//      "Alle anzeigen". Lists every type grouped by category with sticky
//      search and multi-select checkboxes pre-populated from the picker.
//
// Backend contract: see internal/handlers/event_types.go and
// internal/services/event_type_service.go.

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

export interface EventType {
  id: string;
  slug: string;
  label_de: string;
  label_en: string;
  category: string;
  jurisdiction?: string | null;
  description: string;
  trigger_event_id?: number | null;
  created_by?: string | null;
  is_firm_wide: boolean;
  archived_at?: string | null;
  created_at: string;
  updated_at: string;
}

export const CATEGORY_ORDER = [
  "submission",
  "decision",
  "order",
  "service",
  "fee",
  "hearing",
  "other",
] as const;

export type Category = (typeof CATEGORY_ORDER)[number];

export function eventTypeLabel(et: EventType): string {
  const lang = getLang();
  const primary = lang === "en" ? et.label_en : et.label_de;
  return primary?.trim() || et.label_en || et.label_de || et.slug;
}

export function categoryLabel(category: string): string {
  return tDyn(`event_types.cat.${category}`) || category;
}

let cache: EventType[] | null = null;
let cachePromise: Promise<EventType[]> | null = null;

export async function fetchEventTypes(force = false): Promise<EventType[]> {
  if (!force && cache) return cache;
  if (!force && cachePromise) return cachePromise;
  cachePromise = (async () => {
    const resp = await fetch("/api/event-types");
    if (!resp.ok) {
      cachePromise = null;
      return [];
    }
    cache = (await resp.json()) as EventType[];
    cachePromise = null;
    return cache;
  })();
  return cachePromise;
}

export function invalidateEventTypeCache() {
  cache = null;
  cachePromise = null;
}

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

function groupByCategory(types: EventType[]): Map<string, EventType[]> {
  const m = new Map<string, EventType[]>();
  for (const cat of CATEGORY_ORDER) m.set(cat, []);
  for (const et of types) {
    const list = m.get(et.category) ?? [];
    list.push(et);
    m.set(et.category, list);
  }
  // Sort each bucket by label.
  for (const [k, v] of m) {
    v.sort((a, b) => eventTypeLabel(a).localeCompare(eventTypeLabel(b)));
    m.set(k, v);
  }
  return m;
}

// ============================================================================
// Picker (multi-tag chip cluster) — used on deadline create/edit
// ============================================================================

interface PickerOptions {
  initialIDs?: string[];
  onChange?: (ids: string[]) => void;
  currentUserAdmin?: boolean;
}

export interface PickerHandle {
  getIDs(): string[];
  setIDs(ids: string[]): void;
  refresh(): Promise<void>;
}

export function attachEventTypePicker(container: HTMLElement, opts: PickerOptions): PickerHandle {
  let selected = new Set<string>(opts.initialIDs ?? []);
  let allTypes: EventType[] = [];

  container.classList.add("event-type-picker");
  container.innerHTML = `
    <div class="event-type-chips" data-role="chips"></div>
    <div class="event-type-search-row">
      <input type="text" class="event-type-search" data-role="search" placeholder="${esc(t("event_types.picker.search"))}" />
      <button type="button" class="event-type-browse-btn" data-role="browse">${esc(t("event_types.picker.browse_all"))}</button>
      <button type="button" class="event-type-add-btn" data-role="add">${esc(t("event_types.picker.add"))}</button>
    </div>
    <div class="event-type-suggest" data-role="suggest" hidden></div>
  `;

  const chipsEl = container.querySelector<HTMLElement>("[data-role=chips]")!;
  const searchEl = container.querySelector<HTMLInputElement>("[data-role=search]")!;
  const suggestEl = container.querySelector<HTMLElement>("[data-role=suggest]")!;
  const browseBtn = container.querySelector<HTMLButtonElement>("[data-role=browse]")!;
  const addBtn = container.querySelector<HTMLButtonElement>("[data-role=add]")!;

  function notify() {
    opts.onChange?.(Array.from(selected));
  }

  function renderChips() {
    const byID = new Map(allTypes.map((et) => [et.id, et]));
    chipsEl.innerHTML = Array.from(selected)
      .map((id) => {
        const et = byID.get(id);
        if (!et) return "";
        return `<span class="event-type-chip" data-id="${esc(et.id)}">
          <span class="event-type-chip-label">${esc(eventTypeLabel(et))}</span>
          <button type="button" class="event-type-chip-remove" aria-label="${esc(t("event_types.picker.remove"))}">×</button>
        </span>`;
      })
      .join("");
    chipsEl.querySelectorAll<HTMLButtonElement>(".event-type-chip-remove").forEach((btn) => {
      btn.addEventListener("click", () => {
        const id = btn.parentElement?.dataset.id;
        if (id) {
          selected.delete(id);
          renderChips();
          notify();
        }
      });
    });
  }

  function renderSuggest(query: string) {
    const q = query.trim().toLowerCase();
    if (q.length < 1) {
      suggestEl.hidden = true;
      suggestEl.innerHTML = "";
      return;
    }
    const matches = allTypes
      .filter((et) => !selected.has(et.id))
      .filter((et) => {
        const lde = et.label_de.toLowerCase();
        const len = et.label_en.toLowerCase();
        return lde.includes(q) || len.includes(q);
      })
      .slice(0, 12);
    if (matches.length === 0) {
      suggestEl.hidden = false;
      suggestEl.innerHTML = `<div class="event-type-suggest-empty">${esc(t("event_types.picker.no_match"))}</div>`;
      return;
    }
    suggestEl.hidden = false;
    suggestEl.innerHTML = matches
      .map(
        (et) => `<button type="button" class="event-type-suggest-row" data-id="${esc(et.id)}">
          <span class="event-type-suggest-label">${esc(eventTypeLabel(et))}</span>
          <span class="event-type-suggest-cat">${esc(categoryLabel(et.category))}</span>
        </button>`,
      )
      .join("");
    suggestEl.querySelectorAll<HTMLButtonElement>(".event-type-suggest-row").forEach((row) => {
      row.addEventListener("click", () => {
        const id = row.dataset.id!;
        selected.add(id);
        searchEl.value = "";
        renderSuggest("");
        renderChips();
        notify();
        searchEl.focus();
      });
    });
  }

  searchEl.addEventListener("input", () => renderSuggest(searchEl.value));
  searchEl.addEventListener("focus", () => renderSuggest(searchEl.value));
  searchEl.addEventListener("blur", () => {
    // Delay so the click lands first.
    setTimeout(() => {
      suggestEl.hidden = true;
    }, 200);
  });
  browseBtn.addEventListener("click", async () => {
    if (allTypes.length === 0) {
      // Cache might still be hydrating on a slow first paint — make sure
      // the modal opens against the freshest data we have.
      allTypes = await fetchEventTypes();
    }
    const result = await openBrowseEventTypesModal({
      types: allTypes,
      initialIDs: Array.from(selected),
    });
    if (result) {
      selected = new Set(result);
      searchEl.value = "";
      renderSuggest("");
      renderChips();
      notify();
    }
  });

  addBtn.addEventListener("click", async () => {
    const created = await openAddEventTypeModal({
      prefillLabel: searchEl.value.trim(),
      isAdmin: !!opts.currentUserAdmin,
    });
    if (created) {
      await fetchEventTypes(true);
      allTypes = (await fetchEventTypes()) ?? [];
      selected.add(created.id);
      searchEl.value = "";
      renderSuggest("");
      renderChips();
      notify();
    }
  });

  const handle: PickerHandle = {
    getIDs: () => Array.from(selected),
    setIDs: (ids) => {
      selected = new Set(ids);
      renderChips();
    },
    refresh: async () => {
      invalidateEventTypeCache();
      allTypes = await fetchEventTypes(true);
      renderChips();
    },
  };

  void (async () => {
    allTypes = await fetchEventTypes();
    renderChips();
  })();

  return handle;
}

// ============================================================================
// Multi-select filter (listbox panel) — used on /deadlines + /agenda
// ============================================================================

interface FilterOptions {
  initialIDs?: string[];
  initialIncludeUntyped?: boolean;
  onChange?: (ids: string[], includeUntyped: boolean) => void;
}

export interface FilterHandle {
  getIDs(): string[];
  getIncludeUntyped(): boolean;
  setSelection(ids: string[], includeUntyped: boolean): void;
  refresh(): Promise<void>;
  /** Serialise to the `?event_type=` query-param value (or "" when "Alle"). */
  toQueryValue(): string;
}

export function attachEventTypeMultiSelectFilter(
  trigger: HTMLButtonElement,
  panel: HTMLElement,
  opts: FilterOptions = {},
): FilterHandle {
  let selected = new Set<string>(opts.initialIDs ?? []);
  let includeUntyped = !!opts.initialIncludeUntyped;
  let allTypes: EventType[] = [];
  let searchQuery = "";

  trigger.classList.add("multi-trigger");
  trigger.setAttribute("aria-haspopup", "listbox");
  trigger.setAttribute("aria-expanded", "false");
  trigger.innerHTML = `
    <span class="multi-label" data-role="label"></span>
    <span class="multi-chevron" aria-hidden="true">▾</span>
  `;
  panel.classList.add("multi-panel");
  panel.hidden = true;

  function updateLabel() {
    const labelEl = trigger.querySelector<HTMLElement>("[data-role=label]")!;
    const total = selected.size + (includeUntyped ? 1 : 0);
    if (total === 0) {
      labelEl.textContent = t("event_types.filter.all");
    } else if (total === 1 && includeUntyped) {
      labelEl.textContent = t("event_types.filter.untyped");
    } else if (total === 1 && selected.size === 1) {
      const id = Array.from(selected)[0];
      const et = allTypes.find((x) => x.id === id);
      labelEl.textContent = et ? eventTypeLabel(et) : t("event_types.filter.n_selected").replace("{n}", "1");
    } else {
      labelEl.textContent = t("event_types.filter.n_selected").replace("{n}", String(total));
    }
  }

  function renderPanel() {
    const groups = groupByCategory(allTypes);
    const q = searchQuery.trim().toLowerCase();
    const matches = (et: EventType) => {
      if (!q) return true;
      return (
        et.label_de.toLowerCase().includes(q) ||
        et.label_en.toLowerCase().includes(q) ||
        et.slug.toLowerCase().includes(q)
      );
    };
    const renderGroup = (cat: string) => {
      const list = (groups.get(cat) ?? []).filter(matches);
      if (list.length === 0) return "";
      return `<div class="multi-group">
        <div class="multi-group-label">${esc(categoryLabel(cat))}</div>
        ${list
          .map(
            (et) => `<label class="multi-option">
              <input type="checkbox" data-id="${esc(et.id)}" ${selected.has(et.id) ? "checked" : ""} />
              <span>${esc(eventTypeLabel(et))}</span>
            </label>`,
          )
          .join("")}
      </div>`;
    };
    panel.innerHTML = `
      <div class="multi-search-row">
        <input type="text" class="multi-search" data-role="search" placeholder="${esc(t("event_types.filter.search"))}" value="${esc(searchQuery)}" />
      </div>
      <div class="multi-specials">
        <label class="multi-option multi-special">
          <input type="checkbox" data-role="all" ${selected.size === 0 && !includeUntyped ? "checked" : ""} />
          <span>${esc(t("event_types.filter.all"))}</span>
        </label>
        <label class="multi-option multi-special">
          <input type="checkbox" data-role="untyped" ${includeUntyped ? "checked" : ""} />
          <span>${esc(t("event_types.filter.untyped"))}</span>
        </label>
      </div>
      <div class="multi-list">
        ${CATEGORY_ORDER.map(renderGroup).join("")}
      </div>
      <div class="multi-actions">
        <button type="button" class="btn-cancel" data-role="reset">${esc(t("event_types.filter.reset"))}</button>
        <button type="button" class="btn-primary btn-cta-lime" data-role="close">${esc(t("event_types.filter.apply"))}</button>
      </div>
    `;

    const searchInput = panel.querySelector<HTMLInputElement>("[data-role=search]")!;
    searchInput.addEventListener("input", () => {
      searchQuery = searchInput.value;
      renderPanel();
      // re-focus the search after re-render
      const fresh = panel.querySelector<HTMLInputElement>("[data-role=search]")!;
      fresh.focus();
      fresh.setSelectionRange(searchInput.selectionStart ?? 0, searchInput.selectionEnd ?? 0);
    });

    const allCb = panel.querySelector<HTMLInputElement>("[data-role=all]")!;
    allCb.addEventListener("change", () => {
      if (allCb.checked) {
        selected.clear();
        includeUntyped = false;
        renderPanel();
        updateLabel();
        opts.onChange?.([], false);
      } else {
        // Re-tick "Alle" if user tried to uncheck it without ticking anything
        // else — a "no filter" state with everything off doesn't exist.
        allCb.checked = true;
      }
    });

    const untypedCb = panel.querySelector<HTMLInputElement>("[data-role=untyped]")!;
    untypedCb.addEventListener("change", () => {
      includeUntyped = untypedCb.checked;
      renderPanel();
      updateLabel();
      opts.onChange?.(Array.from(selected), includeUntyped);
    });

    panel.querySelectorAll<HTMLInputElement>(".multi-list input[type=checkbox]").forEach((cb) => {
      cb.addEventListener("change", () => {
        const id = cb.dataset.id!;
        if (cb.checked) selected.add(id);
        else selected.delete(id);
        renderPanel();
        updateLabel();
        opts.onChange?.(Array.from(selected), includeUntyped);
      });
    });

    panel.querySelector<HTMLButtonElement>("[data-role=reset]")!.addEventListener("click", () => {
      selected.clear();
      includeUntyped = false;
      searchQuery = "";
      renderPanel();
      updateLabel();
      opts.onChange?.([], false);
    });
    panel.querySelector<HTMLButtonElement>("[data-role=close]")!.addEventListener("click", () => {
      closePanel();
    });
  }

  function openPanel() {
    panel.hidden = false;
    trigger.setAttribute("aria-expanded", "true");
    renderPanel();
    setTimeout(() => {
      const search = panel.querySelector<HTMLInputElement>("[data-role=search]");
      search?.focus();
    }, 0);
  }
  function closePanel() {
    panel.hidden = true;
    trigger.setAttribute("aria-expanded", "false");
  }

  trigger.addEventListener("click", () => {
    if (panel.hidden) openPanel();
    else closePanel();
  });

  document.addEventListener("click", (e) => {
    const target = e.target as Node;
    if (panel.hidden) return;
    if (panel.contains(target) || trigger.contains(target)) return;
    closePanel();
  });
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape" && !panel.hidden) closePanel();
  });

  const handle: FilterHandle = {
    getIDs: () => Array.from(selected),
    getIncludeUntyped: () => includeUntyped,
    setSelection: (ids, untyped) => {
      selected = new Set(ids);
      includeUntyped = untyped;
      updateLabel();
      if (!panel.hidden) renderPanel();
    },
    refresh: async () => {
      invalidateEventTypeCache();
      allTypes = await fetchEventTypes(true);
      updateLabel();
      if (!panel.hidden) renderPanel();
    },
    toQueryValue: () => {
      const tokens: string[] = [];
      for (const id of selected) tokens.push(id);
      if (includeUntyped) tokens.push("none");
      return tokens.join(",");
    },
  };

  void (async () => {
    allTypes = await fetchEventTypes();
    updateLabel();
  })();

  // Trigger label and (when open) panel content come from t() — re-render
  // when the language changes so "Alle Typen" / "All types" stays in sync
  // with the active locale (t-paliad-117).
  onLangChange(() => {
    updateLabel();
    if (!panel.hidden) renderPanel();
  });

  return handle;
}

// ============================================================================
// Add modal
// ============================================================================

interface AddModalOptions {
  prefillLabel?: string;
  isAdmin: boolean;
}

export function openAddEventTypeModal(opts: AddModalOptions): Promise<EventType | null> {
  return new Promise<EventType | null>((resolve) => {
    const overlay = document.createElement("div");
    overlay.className = "modal-overlay event-type-modal-overlay";
    overlay.innerHTML = `
      <div class="modal event-type-add-modal" role="dialog" aria-modal="true" aria-labelledby="event-type-add-title">
        <h2 id="event-type-add-title">${esc(t("event_types.add.title"))}</h2>
        <div class="event-type-suggest-warn" data-role="warn" hidden></div>
        <div class="form-field">
          <label for="event-type-add-label-de">${esc(t("event_types.add.label_de"))}</label>
          <input type="text" id="event-type-add-label-de" autofocus value="${esc(opts.prefillLabel || "")}" />
        </div>
        <div class="form-field">
          <label for="event-type-add-label-en">${esc(t("event_types.add.label_en"))}</label>
          <input type="text" id="event-type-add-label-en" />
        </div>
        <div class="form-field-row">
          <div class="form-field">
            <label for="event-type-add-category">${esc(t("event_types.add.category"))}</label>
            <select id="event-type-add-category">
              ${CATEGORY_ORDER.map((c) => `<option value="${c}">${esc(categoryLabel(c))}</option>`).join("")}
            </select>
          </div>
          <div class="form-field">
            <label for="event-type-add-jurisdiction">${esc(t("event_types.add.jurisdiction"))}</label>
            <select id="event-type-add-jurisdiction">
              <option value="">${esc(t("event_types.add.jurisdiction.none"))}</option>
              <option value="UPC">UPC</option>
              <option value="EPO">EPA</option>
              <option value="DPMA">DPMA</option>
              <option value="DE">DE</option>
              <option value="any">${esc(t("event_types.add.jurisdiction.any"))}</option>
            </select>
          </div>
        </div>
        <div class="form-field event-type-add-firm-wide" ${opts.isAdmin ? "" : "hidden"}>
          <label class="checkbox-label">
            <input type="checkbox" id="event-type-add-firm-wide" />
            <span>${esc(t("event_types.add.firm_wide"))}</span>
          </label>
          <p class="form-hint">${esc(t("event_types.add.firm_wide.hint"))}</p>
        </div>
        <p class="form-msg" id="event-type-add-msg"></p>
        <div class="form-actions">
          <button type="button" class="btn-cancel" data-role="cancel">${esc(t("common.cancel"))}</button>
          <button type="button" class="btn-primary btn-cta-lime" data-role="submit">${esc(t("event_types.add.submit"))}</button>
        </div>
      </div>
    `;
    document.body.appendChild(overlay);

    const labelDE = overlay.querySelector<HTMLInputElement>("#event-type-add-label-de")!;
    const labelEN = overlay.querySelector<HTMLInputElement>("#event-type-add-label-en")!;
    const catSel = overlay.querySelector<HTMLSelectElement>("#event-type-add-category")!;
    const jurSel = overlay.querySelector<HTMLSelectElement>("#event-type-add-jurisdiction")!;
    const firmWide = overlay.querySelector<HTMLInputElement>("#event-type-add-firm-wide")!;
    const msg = overlay.querySelector<HTMLElement>("#event-type-add-msg")!;
    const warnEl = overlay.querySelector<HTMLElement>("[data-role=warn]")!;
    const cancelBtn = overlay.querySelector<HTMLButtonElement>("[data-role=cancel]")!;
    const submitBtn = overlay.querySelector<HTMLButtonElement>("[data-role=submit]")!;

    let suggestTimer: number | null = null;
    function checkDuplicates(query: string) {
      if (suggestTimer) window.clearTimeout(suggestTimer);
      const q = query.trim();
      if (q.length < 2) {
        warnEl.hidden = true;
        warnEl.innerHTML = "";
        return;
      }
      suggestTimer = window.setTimeout(async () => {
        try {
          const resp = await fetch(`/api/event-types/suggest?q=${encodeURIComponent(q)}`);
          if (!resp.ok) return;
          const matches = (await resp.json()) as EventType[];
          if (matches.length === 0) {
            warnEl.hidden = true;
            warnEl.innerHTML = "";
            return;
          }
          warnEl.hidden = false;
          warnEl.innerHTML = `<strong>${esc(t("event_types.add.duplicate_warn"))}</strong> ` +
            matches.map((m) => `<span class="event-type-suggest-pill">${esc(eventTypeLabel(m))}</span>`).join(" ");
        } catch {
          /* non-fatal */
        }
      }, 250);
    }
    labelDE.addEventListener("input", () => checkDuplicates(labelDE.value));

    function close(value: EventType | null) {
      overlay.remove();
      resolve(value);
    }

    cancelBtn.addEventListener("click", () => close(null));
    overlay.addEventListener("click", (e) => {
      if (e.target === overlay) close(null);
    });
    document.addEventListener(
      "keydown",
      function onKey(e: KeyboardEvent) {
        if (e.key === "Escape") {
          document.removeEventListener("keydown", onKey);
          close(null);
        }
      },
    );

    submitBtn.addEventListener("click", async () => {
      const labelDEv = labelDE.value.trim();
      if (!labelDEv) {
        msg.textContent = t("event_types.add.error.required");
        msg.className = "form-msg form-msg-error";
        return;
      }
      submitBtn.disabled = true;
      try {
        const payload: Record<string, unknown> = {
          label_de: labelDEv,
          category: catSel.value,
          is_firm_wide: opts.isAdmin ? firmWide.checked : false,
        };
        if (labelEN.value.trim()) payload.label_en = labelEN.value.trim();
        if (jurSel.value) payload.jurisdiction = jurSel.value;
        const resp = await fetch("/api/event-types", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload),
        });
        if (resp.status === 409) {
          msg.textContent = t("event_types.add.error.slug_taken");
          msg.className = "form-msg form-msg-error";
          submitBtn.disabled = false;
          return;
        }
        if (!resp.ok) {
          const data = await resp.json().catch(() => ({}) as { error?: string });
          msg.textContent = data.error || t("event_types.add.error.generic");
          msg.className = "form-msg form-msg-error";
          submitBtn.disabled = false;
          return;
        }
        const created = (await resp.json()) as EventType;
        invalidateEventTypeCache();
        close(created);
      } catch {
        msg.textContent = t("event_types.add.error.generic");
        msg.className = "form-msg form-msg-error";
        submitBtn.disabled = false;
      }
    });
  });
}

// ============================================================================
// Browse-all modal — t-paliad-107
// ============================================================================
//
// Companion to the picker's search-as-you-type flow. Lists every available
// event type grouped by category, multi-select checkboxes, sticky search,
// pre-populated from the picker's current selection. Apply replaces; Cancel
// discards.

interface BrowseModalOptions {
  types: EventType[];
  initialIDs: string[];
}

export function openBrowseEventTypesModal(
  opts: BrowseModalOptions,
): Promise<string[] | null> {
  return new Promise<string[] | null>((resolve) => {
    let selected = new Set<string>(opts.initialIDs);
    let searchQuery = "";

    const overlay = document.createElement("div");
    overlay.className = "modal-overlay event-type-browse-overlay";
    overlay.innerHTML = `
      <div class="modal event-type-browse-modal" role="dialog" aria-modal="true" aria-labelledby="event-type-browse-title">
        <div class="event-type-browse-header">
          <h2 id="event-type-browse-title">${esc(t("event_types.browse.title"))}</h2>
          <input type="text" class="event-type-browse-search" data-role="search" placeholder="${esc(t("event_types.browse.search"))}" autocomplete="off" />
        </div>
        <div class="event-type-browse-list" data-role="list" tabindex="-1"></div>
        <div class="event-type-browse-actions">
          <span class="event-type-browse-count" data-role="count" aria-live="polite"></span>
          <button type="button" class="btn-cancel" data-role="cancel">${esc(t("event_types.browse.cancel"))}</button>
          <button type="button" class="btn-primary btn-cta-lime" data-role="apply">${esc(t("event_types.browse.apply"))}</button>
        </div>
      </div>
    `;
    document.body.appendChild(overlay);

    const modalEl = overlay.querySelector<HTMLElement>(".event-type-browse-modal")!;
    const searchEl = overlay.querySelector<HTMLInputElement>("[data-role=search]")!;
    const listEl = overlay.querySelector<HTMLElement>("[data-role=list]")!;
    const countEl = overlay.querySelector<HTMLElement>("[data-role=count]")!;
    const cancelBtn = overlay.querySelector<HTMLButtonElement>("[data-role=cancel]")!;
    const applyBtn = overlay.querySelector<HTMLButtonElement>("[data-role=apply]")!;

    const groups = groupByCategory(opts.types);

    function jurisdictionLabel(j: string | null | undefined): string {
      if (!j) return "";
      if (j === "any") return t("event_types.browse.jurisdiction.none");
      if (j === "EPO") return "EPA";
      return j;
    }

    function updateCount() {
      countEl.textContent = t("event_types.browse.selected_count").replace(
        "{n}",
        String(selected.size),
      );
    }

    function renderList() {
      const q = searchQuery.trim().toLowerCase();
      const matches = (et: EventType) => {
        if (!q) return true;
        return (
          et.label_de.toLowerCase().includes(q) ||
          et.label_en.toLowerCase().includes(q) ||
          et.slug.toLowerCase().includes(q)
        );
      };
      const sections = CATEGORY_ORDER.map((cat) => {
        const list = (groups.get(cat) ?? []).filter(matches);
        if (list.length === 0) return "";
        return `<section class="event-type-browse-group">
          <h3 class="event-type-browse-group-label">${esc(categoryLabel(cat))}</h3>
          <ul class="event-type-browse-options" role="group" aria-label="${esc(categoryLabel(cat))}">
            ${list
              .map((et) => {
                const checked = selected.has(et.id) ? "checked" : "";
                const jur = jurisdictionLabel(et.jurisdiction);
                const jurBadge = jur
                  ? `<span class="event-type-browse-jurisdiction">${esc(jur)}</span>`
                  : "";
                return `<li>
                  <label class="event-type-browse-option">
                    <input type="checkbox" data-id="${esc(et.id)}" ${checked} />
                    <span class="event-type-browse-option-label">${esc(eventTypeLabel(et))}</span>
                    ${jurBadge}
                  </label>
                </li>`;
              })
              .join("")}
          </ul>
        </section>`;
      }).join("");

      listEl.innerHTML =
        sections ||
        `<div class="event-type-browse-empty">${esc(t("event_types.browse.empty"))}</div>`;

      listEl.querySelectorAll<HTMLInputElement>("input[type=checkbox]").forEach((cb) => {
        cb.addEventListener("change", () => {
          const id = cb.dataset.id!;
          if (cb.checked) selected.add(id);
          else selected.delete(id);
          updateCount();
        });
      });
    }

    searchEl.addEventListener("input", () => {
      searchQuery = searchEl.value;
      renderList();
    });

    function close(value: string[] | null) {
      document.removeEventListener("keydown", onKey);
      overlay.remove();
      resolve(value);
    }

    function onKey(e: KeyboardEvent) {
      if (e.key === "Escape") {
        e.preventDefault();
        close(null);
        return;
      }
      if (e.key === "Tab") {
        // Lightweight focus trap: keep tabbing inside the modal.
        const focusables = modalEl.querySelectorAll<HTMLElement>(
          'input, button, [tabindex]:not([tabindex="-1"])',
        );
        const visible = Array.from(focusables).filter(
          (el) => !el.hasAttribute("disabled") && el.offsetParent !== null,
        );
        if (visible.length === 0) return;
        const first = visible[0];
        const last = visible[visible.length - 1];
        const active = document.activeElement as HTMLElement | null;
        if (e.shiftKey && active === first) {
          e.preventDefault();
          last.focus();
        } else if (!e.shiftKey && active === last) {
          e.preventDefault();
          first.focus();
        }
      }
    }
    document.addEventListener("keydown", onKey);

    cancelBtn.addEventListener("click", () => close(null));
    applyBtn.addEventListener("click", () => close(Array.from(selected)));
    overlay.addEventListener("click", (e) => {
      if (e.target === overlay) close(null);
    });

    updateCount();
    renderList();
    setTimeout(() => searchEl.focus(), 0);
  });
}
