import { initI18n, onLangChange, t, tDyn, getLang } from "./i18n";
import { initSidebar } from "./sidebar";
import {
  attachEventTypeMultiSelectFilter,
  fetchEventTypes,
  eventTypeLabel,
  type EventType,
  type FilterHandle,
} from "./event-types";
import { projectIndent } from "./project-indent";
import { mountCalendar, type CalendarHandle, type CalendarItem } from "./calendar/mount-calendar";
import { formatRuleLabelHTML, formatCustomRuleLabelHTML } from "./rule-label";

// Two-eyes glyph 👀 inside .approval-pill--icon. m's 2026-05-08 follow-
// up: "two eyes instead of the one." Emoji rather than SVG keeps the
// pill markup trivially short and inherits the user's emoji font.
const APPROVAL_PILL_GLYPH = "👀";

// Sparkle glyph ✨ inside .approval-pill--agent (t-paliad-161). Renders
// next to (not in place of) 👀 when the pending row originated from a
// Paliadin chat suggestion. The two glyphs are orthogonal: 👀 = "needs
// approval", ✨ = "Paliadin drafted this". Both can coexist; either can
// appear alone in future autopilot states.
const AGENT_PILL_GLYPH = "✨";

// EventsPage shared client (t-paliad-110). Drives /deadlines and
// /appointments off the same shell — the route handler injects
// `window.__PALIAD_EVENTS__ = { defaultType: "deadline" | "appointment" }`
// on first paint and we read it here to set the initial chip + filter
// visibility. The chip toggle on the page lets the user widen to
// "Beides" without a route change; the URL gets `?type=` for state.

declare global {
  interface Window {
    __PALIAD_EVENTS__?: {
      defaultType?: EventTypeChoice;
    };
  }
}

type EventTypeChoice = "deadline" | "appointment" | "all";
type EventView = "cards" | "list" | "calendar";

interface EventListItem {
  type: "deadline" | "appointment";
  id: string;
  title: string;
  description?: string;
  event_date: string;
  project_id?: string;
  project_reference?: string;
  project_title?: string;
  project_type?: string;

  // Approval workflow (t-paliad-138). "pending" → render the warning pill.
  approval_status?: "approved" | "pending" | "legacy";
  // t-paliad-161: when approval_status='pending', tells us whether the row
  // was drafted by a user or by Paliadin (✨ glyph). NULL when not pending.
  requester_kind?: "user" | "agent";

  // deadline-only
  due_date?: string;
  status?: string;
  completed_at?: string;
  source?: string;
  rule_id?: string;
  rule_code?: string;
  rule_name?: string;
  rule_name_en?: string;
  // t-paliad-258 — free-text rule label when the deadline was created
  // via the Custom rule path. Mutually exclusive with rule_id.
  custom_rule_text?: string;
  event_type_ids?: string[];

  // appointment-only
  start_at?: string;
  end_at?: string;
  location?: string;
  appointment_type?: string;
}

interface EventSummary {
  deadlines?: {
    overdue: number;
    today: number;
    this_week: number;
    next_week: number;
    later: number;
    completed: number;
    total: number;
  };
  appointments?: {
    today: number;
    this_week: number;
    next_week: number;
    later: number;
    total: number;
  };
}

interface Project {
  id: string;
  reference?: string | null;
  title: string;
  path: string;
}

interface Me {
  id: string;
  job_title: string | null;
  global_role: string;
}

const PERSONAL = "__personal__";

// Status options per Type. Deadlines carry the full set; appointments
// only get the date-bucket subset (no pending/overdue/completed — those
// are deadline-only concepts). type=all reuses the deadline set so users
// keep all deadline-side filters available in Beides mode.
type StatusOption = { value: string; key: string };

const STATUS_OPTIONS_DEADLINE: StatusOption[] = [
  { value: "all", key: "deadlines.filter.all" },
  { value: "pending", key: "deadlines.filter.pending" },
  { value: "overdue", key: "deadlines.filter.overdue" },
  { value: "today", key: "deadlines.filter.today" },
  { value: "this_week", key: "deadlines.filter.thisweek" },
  { value: "next_week", key: "deadlines.filter.nextweek" },
  { value: "later", key: "deadlines.filter.later" },
  { value: "completed", key: "deadlines.filter.completed" },
];

// Appointment status options — m/paliad#54: the legacy 'upcoming' /
// "Ab heute" option was a UI lie (backend never narrowed past events for
// appointments) and is removed. 'today' is the sane default — matches the
// dashboard tile. 'all' stays as the explicit opt-in for past events.
const STATUS_OPTIONS_APPOINTMENT: StatusOption[] = [
  { value: "today", key: "deadlines.filter.today" },
  { value: "this_week", key: "deadlines.filter.thisweek" },
  { value: "next_week", key: "deadlines.filter.nextweek" },
  { value: "later", key: "deadlines.filter.later" },
  { value: "all", key: "events.filter.status.all" },
];

function statusOptionsFor(type: EventTypeChoice): StatusOption[] {
  if (type === "appointment") return STATUS_OPTIONS_APPOINTMENT;
  return STATUS_OPTIONS_DEADLINE;
}

function defaultStatusFor(type: EventTypeChoice): string {
  return type === "appointment" ? "today" : "pending";
}

let currentType: EventTypeChoice = "deadline";
let currentView: EventView = "cards";
let statusFilter = "pending";
let projectFilter = "";
let appointmentTypeFilter = "";
let allItems: EventListItem[] = [];
let allProjects: Project[] = [];
let me: Me | null = null;
let eventTypeFilter: FilterHandle | null = null;
let eventTypeByID: Map<string, EventType> = new Map();
let loadedOK = false;
// Calendar handle is created lazily when /events first switches into the
// Kalender view (t-paliad-224). The handle owns its own month/week/day
// state + ?cal_view / ?cal_date URL contract via mountCalendar.
let calendar: CalendarHandle | null = null;

function urlParams(): URLSearchParams {
  return new URLSearchParams(window.location.search);
}

function defaultType(): EventTypeChoice {
  const injected = window.__PALIAD_EVENTS__?.defaultType;
  if (injected === "deadline" || injected === "appointment" || injected === "all") {
    return injected;
  }
  return "all";
}

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

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

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

function fmtDateTime(iso: string): string {
  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;
  }
}

function fmtTime(iso: string): string {
  try {
    const d = new Date(iso);
    return d.toLocaleTimeString(getLang() === "de" ? "de-DE" : "en-GB", {
      hour: "2-digit",
      minute: "2-digit",
    });
  } catch {
    return "";
  }
}

// formatDateCell yields the per-row date label. Deadlines render as
// "31.08.2026" (date-only). Appointments render as "15.09.2026 09:00",
// extended with "–11:00" when end_at is on the same day, or
// "→ 16.09.2026 11:00" when end_at spills into a later day.
function formatDateCell(item: EventListItem): string {
  if (item.type === "deadline") {
    return fmtDateOnly(item.due_date ?? item.event_date);
  }
  if (!item.start_at) return "";
  const start = new Date(item.start_at);
  const startLabel = fmtDateTime(item.start_at);
  if (!item.end_at) return startLabel;
  const end = new Date(item.end_at);
  const sameDay =
    start.getFullYear() === end.getFullYear() &&
    start.getMonth() === end.getMonth() &&
    start.getDate() === end.getDate();
  if (sameDay) {
    return `${startLabel}–${fmtTime(item.end_at)}`;
  }
  return `${startLabel} → ${fmtDateTime(item.end_at)}`;
}

function urgencyClass(item: EventListItem): string {
  if (item.type === "deadline") {
    if (item.status === "completed") return "frist-urgency-done";
  }
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const d = new Date(item.event_date);
  const diffDays = Math.floor((d.getTime() - today.getTime()) / 86400000);
  if (diffDays < 0) return "frist-urgency-overdue";
  if (diffDays <= 7) return "frist-urgency-soon";
  return "frist-urgency-later";
}

function ruleDisplay(item: EventListItem): string {
  if (item.type !== "deadline") return "";
  // t-paliad-258 addendum — canonical display contract: Name primary,
  // Citation muted secondary ("Notice of Appeal · UPC.RoP.220.1").
  // Custom rules render the lawyer's free text + a "Custom" badge.
  // Legacy rule-code-only saves (Fristenrechner, no rule_id) still
  // show the bare citation as last-resort fallback.
  const hasName = (item.rule_name && item.rule_name.trim()) ||
    (item.rule_name_en && item.rule_name_en.trim());
  if (hasName || (item.rule_code && item.rule_code.trim())) {
    return formatRuleLabelHTML(
      {
        name: item.rule_name || "",
        name_en: item.rule_name_en,
        rule_code: item.rule_code,
      },
      esc,
    );
  }
  if (item.custom_rule_text && item.custom_rule_text.trim()) {
    return formatCustomRuleLabelHTML(item.custom_rule_text, esc);
  }
  return "&mdash;";
}

function eventTypeDisplay(item: EventListItem): string {
  if (item.type !== "deadline") return "";
  const ids = item.event_type_ids ?? [];
  if (ids.length === 0) return "&mdash;";
  const labels: string[] = [];
  for (const id of ids) {
    const et = eventTypeByID.get(id);
    if (et) labels.push(eventTypeLabel(et));
  }
  if (labels.length === 0) return "&mdash;";
  return labels.map((l) => `<span class="entity-event-type-pill">${esc(l)}</span>`).join(" ");
}

function rowTypeChip(item: EventListItem): string {
  const label = item.type === "deadline" ? t("events.row.type.deadline") : t("events.row.type.appointment");
  const cls = item.type === "deadline" ? "events-row-type-frist" : "events-row-type-termin";
  return `<span class="events-row-type-chip ${cls}">${esc(label)}</span>`;
}

function canReopen(): boolean {
  return !!me && me.global_role === "global_admin";
}

async function loadProjects() {
  try {
    const resp = await fetch("/api/projects");
    if (resp.ok) allProjects = await resp.json();
  } catch {
    /* non-fatal */
  }
}

async function loadMe() {
  try {
    const resp = await fetch("/api/me");
    if (resp.ok) me = await resp.json();
  } catch {
    /* non-fatal */
  }
}

async function loadEventTypes() {
  try {
    const types = await fetchEventTypes();
    eventTypeByID = new Map(types.map((et) => [et.id, et]));
  } catch {
    /* non-fatal */
  }
}

async function loadSummary() {
  try {
    const params = new URLSearchParams();
    params.set("type", currentType);
    if (projectFilter === PERSONAL) {
      // "Nur persönliche" → items the caller created. The backend
      // applies this uniformly to both rails (deadlines + appointments)
      // and ignores any project_id we'd send. See t-paliad-128.
      params.set("personal_only", "true");
    } else if (projectFilter) {
      params.set("project_id", projectFilter);
    }
    const resp = await fetch(`/api/events/summary?${params.toString()}`);
    if (!resp.ok) return;
    const sum: EventSummary = await resp.json();

    // 4 universal cards: pick the deadline value when present, otherwise
    // the appointment value. In Beides mode, sum both rails.
    const today = (sum.deadlines?.today ?? 0) + (sum.appointments?.today ?? 0);
    const thisWeek = (sum.deadlines?.this_week ?? 0) + (sum.appointments?.this_week ?? 0);
    const nextWeek = (sum.deadlines?.next_week ?? 0) + (sum.appointments?.next_week ?? 0);
    const later = (sum.deadlines?.later ?? 0) + (sum.appointments?.later ?? 0);

    setCount("events-sum-today", today);
    setCount("events-sum-week", thisWeek);
    setCount("events-sum-next-week", nextWeek);
    setCount("events-sum-later", later);

    // Überfällig is deadline-only and conditional. Hide on appointment view
    // OR when the count is zero. Alarm-style when count > 0 on a view that
    // includes deadlines (mirrors t-paliad-105 / t-paliad-106 behaviour).
    const overdue = sum.deadlines?.overdue ?? 0;
    setCount("events-sum-overdue", overdue);
    applyOverdueState(overdue);
  } catch {
    /* non-fatal */
  }
}

// Überfällig is deadline-view-only per t-paliad-110 spec. It stays hidden
// in Beides mode even when overdue > 0 — the design treats "overdue" as a
// deadline-specific concept (appointments don't go overdue, they happen),
// so showing the card in mixed mode would be a category error.
function applyOverdueState(overdue: number) {
  const card = document.querySelector<HTMLElement>('.frist-summary-card[data-bucket="overdue"]');
  if (!card) return;
  const isDeadlineView = currentType === "deadline";
  const hidden = !isDeadlineView || overdue === 0;
  card.classList.toggle("frist-card-overdue-hidden", hidden);
  card.classList.toggle("frist-card-alarm", isDeadlineView && overdue > 0);
}

async function loadList() {
  const unavailable = document.getElementById("events-unavailable")!;
  try {
    const params = new URLSearchParams();
    params.set("type", currentType);
    if (statusFilter) {
      // Status carries either a deadline filter (pending/overdue/completed,
      // which only make sense when deadlines are in scope) or a date-bucket
      // (today/this_week/next_week/later) which the backend applies to
      // both rails. EventService.bucketAppointmentWindow turns bucket
      // values into a start_at window; non-bucket values (all/"") are a
      // no-op on the appointment side.
      params.set("status", statusFilter);
    }
    if (projectFilter === PERSONAL) {
      // "Nur persönliche" → items the caller created (t-paliad-128).
      // Applies uniformly to both rails server-side; project_id is
      // intentionally not sent (the two are mutually exclusive).
      params.set("personal_only", "true");
    } else if (projectFilter) {
      params.set("project_id", projectFilter);
    }
    if (currentType !== "appointment") {
      const eventTypeQuery = eventTypeFilter?.toQueryValue() ?? "";
      if (eventTypeQuery) params.set("event_type", eventTypeQuery);
    }
    if (currentType === "appointment" && appointmentTypeFilter) {
      params.set("type_filter", appointmentTypeFilter);
    }
    const resp = await fetch(`/api/events?${params.toString()}`);
    if (resp.status === 503) {
      unavailable.style.display = "block";
      hideTableAndCalendar();
      document.getElementById("events-empty")!.style.display = "none";
      return;
    }
    if (!resp.ok) {
      unavailable.style.display = "block";
      hideTableAndCalendar();
      return;
    }
    const data: EventListItem[] = await resp.json();
    allItems = data;
    loadedOK = true;
    render();
  } catch {
    unavailable.style.display = "block";
    hideTableAndCalendar();
  }
}

function hideTableAndCalendar() {
  const tableWrap = document.getElementById("events-table-wrap");
  const calWrap = document.getElementById("events-calendar-wrap");
  if (tableWrap) tableWrap.style.display = "none";
  if (calWrap) calWrap.hidden = true;
  teardownCalendar();
}

function render() {
  if (!loadedOK) return;
  if (currentView === "calendar") {
    renderCalendarView();
  } else {
    renderTable();
  }
}

function renderTable() {
  const tbody = document.getElementById("events-body")!;
  const empty = document.getElementById("events-empty")!;
  const emptyFiltered = document.getElementById("events-empty-filtered")!;
  const tableWrap = document.getElementById("events-table-wrap") as HTMLElement;
  const table = document.getElementById("events-table");

  if (allItems.length === 0) {
    tbody.innerHTML = "";
    tableWrap.style.display = "none";
    if (isFilterPristine()) {
      empty.style.display = "block";
      emptyFiltered.style.display = "none";
    } else {
      empty.style.display = "none";
      emptyFiltered.style.display = "block";
    }
    return;
  }

  tableWrap.style.display = "";
  empty.style.display = "none";
  emptyFiltered.style.display = "none";

  const showReopen = canReopen();
  tbody.innerHTML = allItems.map((item) => renderRow(item, showReopen)).join("");

  // Hide-on-uniform pattern (t-paliad-073, generalised in t-088). When all
  // visible rows agree on a column we drop the column entirely so empty
  // cells don't add noise. Each column has its own data signal so we can
  // toggle independently.
  const anyDeadline = allItems.some((x) => x.type === "deadline");
  const anyAppointment = allItems.some((x) => x.type === "appointment");
  const anyEventType = allItems.some((x) => (x.event_type_ids ?? []).length > 0);
  const anyLocation = allItems.some((x) => !!x.location);
  const anyAppointmentType = allItems.some((x) => !!x.appointment_type);
  const anyRule = allItems.some((x) => x.type === "deadline" && (x.rule_code || x.rule_name || x.rule_name_en));
  const anyStatus = anyDeadline; // appointments don't carry a status column

  table?.classList.toggle("events-table--hide-row-type", !anyDeadline || !anyAppointment);
  table?.classList.toggle("events-table--hide-rule", !anyRule);
  table?.classList.toggle("entity-table--hide-event-type", !anyEventType);
  table?.classList.toggle("events-table--hide-location", !anyLocation);
  table?.classList.toggle("events-table--hide-appointment-type", !anyAppointmentType);
  table?.classList.toggle("entity-table--hide-status", !anyStatus);

  wireRowHandlers(tbody);
}

function renderRow(item: EventListItem, showReopen: boolean): string {
  const urgency = urgencyClass(item);
  const dateLabel = formatDateCell(item);
  const ruleLabel = item.type === "deadline" ? ruleDisplay(item) : "";
  const eventTypeCell = item.type === "deadline" ? eventTypeDisplay(item) : "";

  const projectCell = item.project_id
    ? `<a class="entity-ref-link" href="/projects/${esc(item.project_id)}">${esc(item.project_reference ?? "")}</a>`
        + `<span class="frist-project-title" title="${esc(item.project_title ?? "")}">${esc(item.project_title ?? "")}</span>`
    : `<span class="termin-personal-tag">${esc(t("appointments.personal"))}</span>`;

  let checkCell = "";
  let titleClass = "";
  let statusCell = "";
  if (item.type === "deadline") {
    const isDone = item.status === "completed";
    titleClass = isDone ? "frist-title-done" : "";
    const statusLabel = tDyn(`deadlines.status.${item.status ?? "pending"}`) || (item.status ?? "");
    statusCell = `<span class="entity-status-chip entity-status-${esc(item.status ?? "")}">${esc(statusLabel)}</span>`;
    const reopenLabel = esc(t("deadlines.action.reopen"));
    if (isDone) {
      checkCell = showReopen
        ? `<button type="button" class="frist-reopen-btn" aria-label="${reopenLabel}" title="${reopenLabel}">↻</button>`
        : `<input type="checkbox" class="frist-complete-cb" checked disabled aria-label="${esc(t("deadlines.complete.action"))}" />`;
    } else {
      checkCell = `<input type="checkbox" class="frist-complete-cb" aria-label="${esc(t("deadlines.complete.action"))}" />`;
    }
  } else {
    const typeClass = item.appointment_type ? `termin-type-${item.appointment_type}` : "";
    checkCell = `<span class="termin-dot ${typeClass}"></span>`;
  }

  const locationCell = item.location ? esc(item.location) : "&mdash;";
  const appointmentTypeCell = item.appointment_type
    ? `<span class="termin-type-chip termin-type-${esc(item.appointment_type)}">${esc(tDyn(`appointments.type.${item.appointment_type}`) || item.appointment_type)}</span>`
    : "&mdash;";

  // Approval pending pill (t-paliad-138 / m's 2026-05-08 cosmetic ask).
  // Soft-tint the row + drop an eye-icon pill next to the title; hover
  // reveals the lifecycle label. Inbox surface shows the full detail.
  //
  // t-paliad-161 ✨: when the pending row came from a Paliadin
  // suggestion (requester_kind='agent'), drop a second pill next to 👀.
  // Two glyphs read together as "needs approval, drafted by Paliadin".
  const pendingClass = item.approval_status === "pending" ? " entity-row--pending-update" : "";
  const pendingLabel = item.approval_status === "pending" ? t("approvals.pending_update.label") : "";
  const pendingPill = item.approval_status === "pending"
    ? `<span class="approval-pill approval-pill--icon" title="${esc(pendingLabel)}" aria-label="${esc(pendingLabel)}">${APPROVAL_PILL_GLYPH}</span>`
    : "";
  const agentLabel = t("approvals.agent.label");
  const agentPill = item.approval_status === "pending" && item.requester_kind === "agent"
    ? `<span class="approval-pill approval-pill--agent" title="${esc(agentLabel)}" aria-label="${esc(agentLabel)}">${AGENT_PILL_GLYPH}</span>`
    : "";

  return `<tr class="frist-row events-row events-row-${item.type}${pendingClass}" data-id="${esc(item.id)}" data-type="${item.type}">
    <td class="frist-col-check">${checkCell}</td>
    <td class="events-col-row-type">${rowTypeChip(item)}</td>
    <td class="frist-col-due ${urgency}"><span class="frist-due-dot"></span>${esc(dateLabel)}</td>
    <td class="frist-col-title ${titleClass}">${esc(item.title)}${pendingPill ? " " + pendingPill : ""}${agentPill ? " " + agentPill : ""}</td>
    <td class="frist-col-project">${projectCell}</td>
    <td class="frist-col-rule events-col-rule">${ruleLabel || "&mdash;"}</td>
    <td class="entity-col-event-type">${eventTypeCell || "&mdash;"}</td>
    <td class="events-col-location">${locationCell}</td>
    <td class="events-col-appointment-type">${appointmentTypeCell}</td>
    <td class="entity-col-status">${statusCell}</td>
  </tr>`;
}

// toCalendarItem adapts an EventListItem to the canonical CalendarItem
// shape consumed by mountCalendar (t-paliad-224). Bucketing date matches
// the pre-refactor behaviour: deadlines bucket on due_date (fallback to
// event_date); appointments bucket on start_at (fallback to event_date).
function toCalendarItem(item: EventListItem): CalendarItem {
  let bucketDate: string;
  if (item.type === "deadline") {
    bucketDate = item.due_date ?? item.event_date;
  } else if (item.start_at) {
    bucketDate = item.start_at;
  } else {
    bucketDate = item.event_date;
  }
  return {
    kind: item.type,
    id: item.id,
    title: item.title,
    event_date: bucketDate,
    project_id: item.project_id,
    project_title: item.project_title,
    project_reference: item.project_reference,
  };
}

function renderCalendarView() {
  const host = document.getElementById("events-calendar-wrap");
  if (!host) return;
  const tableEmpty = document.getElementById("events-empty")!;
  const tableEmptyFiltered = document.getElementById("events-empty-filtered")!;
  tableEmpty.style.display = "none";
  tableEmptyFiltered.style.display = "none";
  (host as HTMLElement).hidden = false;

  const items = allItems.map(toCalendarItem);
  if (calendar) {
    calendar.update(items);
    return;
  }
  // urlState=true: the Kalender tab persists its month/week/day + anchor
  // in ?cal_view + ?cal_date so a refresh / shared link lands on the same
  // calendar state (per t-paliad-224 §11 Q3 head decision).
  calendar = mountCalendar(host as HTMLElement, items, {
    urlState: true,
    defaultView: "month",
  });
}

function teardownCalendar() {
  if (!calendar) return;
  calendar.destroy();
  calendar = null;
}

function applyView() {
  const tableWrap = document.getElementById("events-table-wrap") as HTMLElement;
  const calWrap = document.getElementById("events-calendar-wrap") as HTMLElement;
  const summary = document.getElementById("events-summary") as HTMLElement;

  // Active state on selector buttons.
  document.querySelectorAll<HTMLButtonElement>("[data-event-view]").forEach((b) => {
    b.classList.toggle("events-view-btn-active", b.dataset.eventView === currentView);
  });

  // Body class so CSS can hook in if needed downstream.
  document.body.classList.toggle("events-view-cards", currentView === "cards");
  document.body.classList.toggle("events-view-list", currentView === "list");
  document.body.classList.toggle("events-view-calendar", currentView === "calendar");

  // Cards view = the original layout (5-card summary + table).
  // List view = no summary cards, table only — gives more vertical space
  //   and matches users' mental model of a flat list.
  // Calendar view = mountCalendar() canon (month/week/day); cards + table
  //   both hidden. The handle is torn down when the user leaves Kalender
  //   so its URL state isn't reapplied to other shapes.
  summary.style.display = currentView === "cards" ? "" : "none";
  tableWrap.style.display = currentView === "calendar" ? "none" : "";
  calWrap.hidden = currentView !== "calendar";

  if (currentView === "calendar") {
    if (loadedOK) renderCalendarView();
  } else {
    teardownCalendar();
  }
}

function wireRowHandlers(tbody: HTMLElement) {
  tbody.querySelectorAll<HTMLTableRowElement>(".events-row").forEach((row) => {
    const id = row.dataset.id!;
    const type = row.dataset.type!;
    row.addEventListener("click", (e) => {
      const target = e.target as HTMLElement;
      if (
        target.closest(".frist-complete-cb") ||
        target.closest(".frist-reopen-btn") ||
        target.closest("a")
      ) return;
      window.location.href = type === "deadline" ? `/deadlines/${id}` : `/appointments/${id}`;
    });

    const cb = row.querySelector<HTMLInputElement>(".frist-complete-cb");
    if (cb && !cb.disabled) {
      cb.addEventListener("change", async () => {
        if (!cb.checked) return;
        const titleCell = row.querySelector<HTMLElement>(".events-title");
        const title = (titleCell?.textContent || "").trim();
        const msg = t("deadlines.complete.confirm").replace("{title}", title || "?");
        if (!window.confirm(msg)) {
          cb.checked = false;
          return;
        }
        cb.disabled = true;
        try {
          const resp = await fetch(`/api/deadlines/${id}/complete`, { method: "PATCH" });
          if (resp.ok) {
            await Promise.all([loadList(), loadSummary()]);
          } else {
            cb.checked = false;
            cb.disabled = false;
          }
        } catch {
          cb.checked = false;
          cb.disabled = false;
        }
      });
    }
    const reopenBtn = row.querySelector<HTMLButtonElement>(".frist-reopen-btn");
    if (reopenBtn) {
      reopenBtn.addEventListener("click", async () => {
        reopenBtn.disabled = true;
        try {
          const resp = await fetch(`/api/deadlines/${id}/reopen`, { method: "PATCH" });
          if (resp.ok) {
            await Promise.all([loadList(), loadSummary()]);
          } else {
            reopenBtn.disabled = false;
          }
        } catch {
          reopenBtn.disabled = false;
        }
      });
    }
  });
}

function isFilterPristine(): boolean {
  return (
    statusFilter === defaultStatusFor(currentType) &&
    !projectFilter &&
    !appointmentTypeFilter &&
    (eventTypeFilter?.toQueryValue() ?? "") === ""
  );
}

function applyTypeVisibility() {
  // Page-level chrome — heading, subtitle, actions, type chips' active state.
  const heading = document.getElementById("events-heading");
  const subtitle = document.getElementById("events-subtitle");
  const title = document.getElementById("events-title");
  const isAppointment = currentType === "appointment";
  const isDeadline = currentType === "deadline";
  const isAll = currentType === "all";

  if (heading) heading.textContent = isAppointment ? t("appointments.list.heading") : t("deadlines.list.heading");
  if (subtitle) subtitle.textContent = isAppointment ? t("appointments.list.subtitle") : t("deadlines.list.subtitle");
  if (title) title.textContent = isAppointment ? t("appointments.list.title") : t("deadlines.list.title");

  // Single "Neu …" CTA per type. View axis is the calendar/list/cards selector.
  toggleDisplay("events-action-new-deadline", isDeadline || isAll, "inline-flex");
  toggleDisplay("events-action-new-appointment", isAppointment || isAll, "inline-flex");

  // Empty-state CTAs.
  toggleDisplay("events-empty-cta-deadline", isDeadline || isAll, "inline-flex");
  toggleDisplay("events-empty-cta-appointment", isAppointment || isAll, "inline-flex");

  // Status filter applies to all types — for appointments the option set
  // narrows to date buckets only (Heute / Diese Woche / Nächste Woche /
  // Später / Alle). populateStatusFilter handles the option swap and may
  // reset statusFilter when switching from a deadline-only value.
  populateStatusFilter();
  // Event-type multi-select is deadline-only (appointments have no event_types).
  toggleFilterPair("events-filter-event-type", !isAppointment);
  // The panel is a popup the trigger owns via `panel.hidden`. Never stamp
  // `display: block` on it from the type filter — that overrides the
  // `.multi-panel[hidden]` CSS rule and leaves the panel visible on larger
  // screens. When switching to appointment view, force-close it.
  const eventTypePanel = document.getElementById("events-filter-event-type-panel") as HTMLElement | null;
  if (eventTypePanel) {
    eventTypePanel.style.display = "";
    if (isAppointment) eventTypePanel.hidden = true;
  }
  // Termin-Typ is appointment-only.
  toggleFilterPair("events-filter-appointment-type", !isDeadline);

  // "Nur persönliche" applies uniformly to deadlines + appointments now
  // (t-paliad-128) — items where the caller is the creator. Always available.

  // Update chip active state.
  document.querySelectorAll<HTMLButtonElement>('[data-event-type]').forEach((b) => {
    b.classList.toggle("agenda-chip-active", b.dataset.eventType === currentType);
  });

  // Refresh overdue card visibility on type change.
  applyOverdueState(parseInt(document.getElementById("events-sum-overdue")?.textContent ?? "0", 10));

  // Sidebar highlight (t-paliad-115). The bare /events HTML SSRs with
  // currentPath="/events" so neither Fristen nor Termine sidebar entries
  // match — we re-highlight at hydration based on the active type so the
  // user always sees the correct nav entry lit. Same logic for BottomNav,
  // which today doesn't carry these entries but stays consistent if it
  // ever does.
  applySidebarTypeHighlight();
}

function applySidebarTypeHighlight() {
  const items = document.querySelectorAll<HTMLAnchorElement>(".sidebar-item");
  for (const a of items) {
    const href = a.getAttribute("href") ?? "";
    if (href === "/events?type=deadline") {
      a.classList.toggle("active", currentType === "deadline");
    } else if (href === "/events?type=appointment") {
      a.classList.toggle("active", currentType === "appointment");
    }
  }
}

function toggleDisplay(id: string, show: boolean, displayWhenShown = "block") {
  const el = document.getElementById(id);
  if (!el) return;
  el.style.display = show ? displayWhenShown : "none";
}

// toggleFilterPair shows/hides the wrapping .filter-group so the label,
// control, and (for the multi-select) the .multi-anchor collapse together
// when a filter doesn't apply for the current type.
function toggleFilterPair(controlID: string, show: boolean) {
  const ctrl = document.getElementById(controlID);
  const group = ctrl?.closest<HTMLElement>(".filter-group");
  if (group) group.style.display = show ? "" : "none";
}

function syncURLParams() {
  const url = new URL(window.location.href);
  url.searchParams.delete("type");
  url.searchParams.delete("view");
  url.searchParams.delete("status");
  url.searchParams.delete("project_id");
  url.searchParams.delete("personal_only");
  url.searchParams.delete("event_type");
  url.searchParams.delete("type_filter");
  // Default type per route comes from window.__PALIAD_EVENTS__; only
  // surface in the URL when the user opted into something else.
  if (currentType !== defaultType()) url.searchParams.set("type", currentType);
  if (currentView !== "cards") url.searchParams.set("view", currentView);
  if (statusFilter && statusFilter !== defaultStatusFor(currentType)) {
    url.searchParams.set("status", statusFilter);
  }
  if (projectFilter) url.searchParams.set("project_id", projectFilter);
  if (currentType !== "appointment") {
    const eventQuery = eventTypeFilter?.toQueryValue() ?? "";
    if (eventQuery) url.searchParams.set("event_type", eventQuery);
  }
  if (currentType === "appointment" && appointmentTypeFilter) {
    url.searchParams.set("type_filter", appointmentTypeFilter);
  }
  window.history.replaceState(null, "", url.toString());
}

// populateStatusFilter rebuilds the Status `<select>` options for the
// current type. Called from applyTypeVisibility (type chip click +
// initial paint) and onLangChange (option labels are localised). When
// the previously selected status isn't valid for the new option set
// (e.g. user had `completed` selected and switches to Termine), it
// falls back to the per-type default and updates the URL params.
function populateStatusFilter() {
  const sel = document.getElementById("events-filter-status") as HTMLSelectElement | null;
  if (!sel) return;
  const opts = statusOptionsFor(currentType);
  sel.innerHTML = opts
    .map((o) => `<option value="${esc(o.value)}">${esc(t(o.key))}</option>`)
    .join("");
  if (!opts.some((o) => o.value === statusFilter)) {
    statusFilter = defaultStatusFor(currentType);
    syncURLParams();
  }
  sel.value = statusFilter;
}

function populateProjectFilter() {
  const sel = document.getElementById("events-filter-project") as HTMLSelectElement;
  const options: string[] = [
    `<option value="">${esc(t("deadlines.filter.akte.all"))}</option>`,
    `<option value="${PERSONAL}">${esc(t("appointments.filter.akte.personal"))}</option>`,
  ];
  for (const a of allProjects) {
    const indent = projectIndent(a.path);
    options.push(
      `<option value="${esc(a.id)}">${indent}${esc(a.reference || "")} — ${esc(a.title)}</option>`,
    );
  }
  sel.innerHTML = options.join("");
  if (projectFilter) sel.value = projectFilter;
}

function initFilters() {
  const params = urlParams();
  currentType = (params.get("type") as EventTypeChoice) || defaultType();
  const rawView = params.get("view");
  if (rawView === "cards" || rawView === "list" || rawView === "calendar") {
    currentView = rawView;
  }
  // Default depends on currentType: appointments default to "all" (no
  // bucket filter), deadlines default to "pending". URL value wins.
  if (params.has("status")) {
    statusFilter = params.get("status")!;
  } else {
    statusFilter = defaultStatusFor(currentType);
  }
  if (params.has("project_id")) projectFilter = params.get("project_id")!;
  // ?personal_only=true is a bookmark-friendly alias for project_id=__personal__.
  // Both URLs land on the same UI state (PERSONAL sentinel in the project select).
  if (params.get("personal_only") === "true") projectFilter = PERSONAL;
  if (params.has("type_filter")) appointmentTypeFilter = params.get("type_filter")!;

  const status = document.getElementById("events-filter-status") as HTMLSelectElement;
  const project = document.getElementById("events-filter-project") as HTMLSelectElement;
  const appointmentType = document.getElementById("events-filter-appointment-type") as HTMLSelectElement;
  const eventTrigger = document.getElementById("events-filter-event-type") as HTMLButtonElement;
  const eventPanel = document.getElementById("events-filter-event-type-panel") as HTMLElement;

  status.value = statusFilter;
  appointmentType.value = appointmentTypeFilter;

  let initialEventIDs: string[] = [];
  let initialIncludeUntyped = false;
  const initialEventRaw = params.get("event_type") ?? "";
  if (initialEventRaw) {
    for (const tok of initialEventRaw.split(",")) {
      const tt = tok.trim();
      if (!tt) continue;
      if (tt === "none") initialIncludeUntyped = true;
      else initialEventIDs.push(tt);
    }
  }

  status.addEventListener("change", async () => {
    statusFilter = status.value;
    syncURLParams();
    await Promise.all([loadList(), loadSummary()]);
  });
  project.addEventListener("change", async () => {
    projectFilter = project.value;
    syncURLParams();
    await Promise.all([loadList(), loadSummary()]);
  });
  appointmentType.addEventListener("change", async () => {
    appointmentTypeFilter = appointmentType.value;
    syncURLParams();
    await loadList();
  });

  if (eventTrigger && eventPanel) {
    eventTypeFilter = attachEventTypeMultiSelectFilter(eventTrigger, eventPanel, {
      initialIDs: initialEventIDs,
      initialIncludeUntyped: initialIncludeUntyped,
      onChange: async () => {
        syncURLParams();
        await loadList();
      },
    });
  }

  document.querySelectorAll<HTMLButtonElement>('[data-event-type]').forEach((b) => {
    b.addEventListener("click", async () => {
      const next = b.dataset.eventType as EventTypeChoice;
      if (next === currentType) return;
      currentType = next;
      applyTypeVisibility();
      syncURLParams();
      await Promise.all([loadList(), loadSummary()]);
    });
  });
}

function initView() {
  // Kalender state (view + anchor) lives inside mountCalendar; no
  // events-page-level wiring needed. The view chips below switch
  // between Karten / Liste / Kalender; applyView() handles the
  // mount + teardown.
  document.querySelectorAll<HTMLButtonElement>("[data-event-view]").forEach((btn) => {
    btn.addEventListener("click", () => {
      const next = btn.dataset.eventView as EventView;
      if (next === currentView) return;
      currentView = next;
      applyView();
      syncURLParams();
    });
  });
}

function initSummaryCards() {
  document.querySelectorAll<HTMLButtonElement>(".frist-summary-card").forEach((card) => {
    card.addEventListener("click", async () => {
      const bucket = card.dataset.bucket!;
      // Map the card to the matching status filter; the events endpoint
      // handles bucket-aware appointment filtering internally so both
      // rails reflect the click consistently in Beides mode.
      statusFilter = bucket;
      const status = document.getElementById("events-filter-status") as HTMLSelectElement;
      if (status) status.value = bucket;
      syncURLParams();
      await Promise.all([loadList(), loadSummary()]);
    });
  });
}

document.addEventListener("DOMContentLoaded", async () => {
  initI18n();
  initSidebar();
  initFilters();
  initView();
  initSummaryCards();
  applyTypeVisibility();
  applyView();
  onLangChange(() => {
    // The static [data-i18n] options are retranslated by initI18n's
    // applyTranslations(), but the project select is rebuilt at runtime
    // and its "Alle Projekte" / "Nur persönliche" labels come from t() —
    // re-run the populator so they pick up the new locale.
    populateProjectFilter();
    applyTypeVisibility();
    render();
  });
  await Promise.all([loadProjects(), loadMe(), loadEventTypes()]);
  populateProjectFilter();
  await Promise.all([loadList(), loadSummary()]);
});
