import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
  type ReactNode,
} from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "wouter";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
  ArrowLeft,
  ArrowRight,
  CalendarClock,
  FileDown,
  Type,
  Languages,
  ListChecks,
  ClipboardList,
  CheckSquare,
  ListTodo,
  Bell,
  ScrollText,
  FileText,
  Settings as SettingsIcon,
  Plus,
  Pencil,
  Trash2,
  Check,
  X,
  ChevronUp,
  ChevronDown,
  Copy,
  Eye,
  EyeOff,
  RotateCcw,
  Palette,
  Sliders,
  Combine,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogFooter,
} from "@/components/ui/dialog";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { Checkbox } from "@/components/ui/checkbox";
import { useToast } from "@/hooks/use-toast";
import {
  DndContext,
  PointerSensor,
  KeyboardSensor,
  TouchSensor,
  useSensor,
  useSensors,
  closestCenter,
  type DragEndEvent,
} from "@dnd-kit/core";
import {
  SortableContext,
  horizontalListSortingStrategy,
  verticalListSortingStrategy,
  useSortable,
  sortableKeyboardCoordinates,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { GripVertical } from "lucide-react";
import { EditableCell } from "@/components/editable-cell";
import { safeHtml } from "@/lib/safe-html";
import { formatTime as i18nFormatTime } from "@/lib/i18n-format";

type Attendee = {
  id?: number;
  meetingId?: number;
  name: string;
  title: string | null;
  attendanceType: "internal" | "virtual" | "external";
  sortOrder: number;
};

type Meeting = {
  id: number;
  dailyNumber: number;
  titleAr: string;
  titleEn: string;
  meetingDate: string;
  startTime: string | null;
  endTime: string | null;
  location: string | null;
  meetingUrl: string | null;
  platform: "none" | "webex" | "teams" | "zoom" | "other";
  status: string;
  isHighlighted: number;
  notes: string | null;
  attendees: Attendee[];
  // Optional cell-merge overlay. When all three are non-null the
  // schedule renders one merged cell spanning
  // [mergeStartColumn..mergeEndColumn] showing mergeText (sanitized
  // HTML) instead of the original column values. Originals are
  // preserved on the row so clearing the merge restores them.
  mergeStartColumn?: ColumnId | null;
  mergeEndColumn?: ColumnId | null;
  mergeText?: string | null;
};

type DayResponse = { date: string; meetings: Meeting[] };

type MeRoles = {
  userId: number;
  roles: string[];
  canRead: boolean;
  canMutate: boolean;
  canApprove: boolean;
  canSubmitRequest: boolean;
  canViewAudit: boolean;
  canViewTasks: boolean;
  canViewAllTasks: boolean;
};

type RequestRow = {
  id: number;
  meetingId: number | null;
  requestedBy: number | null;
  requestType: string;
  requestDetails: unknown;
  status: "new" | "approved" | "rejected" | "withdrawn";
  reviewedBy: number | null;
  reviewDecision: string | null;
  reviewNotes: string | null;
  createdAt: string;
  updatedAt: string;
  requester: {
    username: string;
    displayNameAr: string | null;
    displayNameEn: string | null;
  } | null;
};

type TaskRow = {
  id: number;
  requestId: number | null;
  meetingId: number | null;
  assignedTo: number | null;
  taskType: string;
  status: "pending" | "in_progress" | "completed" | "cancelled";
  dueAt: string | null;
  completedAt: string | null;
  notes: string | null;
  createdAt: string;
  updatedAt: string;
  assignee: {
    username: string;
    displayNameAr: string | null;
    displayNameEn: string | null;
  } | null;
};

type AuditEntry = {
  id: number;
  action: string;
  entityType: string;
  entityId: number | null;
  oldValue: unknown;
  newValue: unknown;
  performedBy: number | null;
  performedAt: string;
  actor: {
    username: string;
    displayNameAr: string | null;
    displayNameEn: string | null;
  } | null;
};

type NotificationRow = {
  id: number;
  meetingId: number | null;
  userId: number | null;
  notificationType: string;
  scheduledAt: string | null;
  sentAt: string | null;
  status: string;
  createdAt: string;
  user: {
    username: string;
    displayNameAr: string | null;
    displayNameEn: string | null;
  } | null;
};

type FontPrefs = {
  fontFamily: string;
  fontSize: number;
  fontWeight: "regular" | "bold";
  alignment: "start" | "center";
};

type FontSettingsResponse = {
  global: ({ scope: "global"; userId: null } & FontPrefs) | null;
  user: ({ scope: "user"; userId: number } & FontPrefs) | null;
};

type PdfArchive = {
  id: number;
  archiveDate: string;
  filePath: string;
  version: number;
  // Bytes recorded by the server-side generator. Null on legacy rows
  // that were created before we wrote the PDF to object storage.
  byteSize: number | null;
  generatedBy: number | null;
  generatedAt: string;
  generator: {
    username: string;
    displayNameAr: string | null;
    displayNameEn: string | null;
  } | null;
};

function formatBytesForDisplay(bytes: number): string {
  if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
  const units = ["B", "KB", "MB"];
  let value = bytes;
  let unitIdx = 0;
  while (value >= 1024 && unitIdx < units.length - 1) {
    value /= 1024;
    unitIdx += 1;
  }
  return `${value < 10 && unitIdx > 0 ? value.toFixed(1) : Math.round(value)} ${units[unitIdx]}`;
}

const SECTIONS = [
  { key: "schedule", icon: CalendarClock },
  { key: "manage", icon: ListChecks },
  { key: "requests", icon: ClipboardList },
  { key: "approvals", icon: CheckSquare },
  { key: "tasks", icon: ListTodo },
  { key: "notifications", icon: Bell },
  { key: "audit", icon: ScrollText },
  { key: "pdf", icon: FileText },
  { key: "fontSettings", icon: SettingsIcon },
] as const;

type SectionKey = (typeof SECTIONS)[number]["key"];

type MeCapabilities = {
  userId: number;
  roles: string[];
  canRead: boolean;
  canMutate: boolean;
  canApprove: boolean;
  canSubmitRequest: boolean;
  canViewAudit: boolean;
  canViewTasks: boolean;
  canViewAllTasks: boolean;
};

function isSectionVisible(
  key: SectionKey,
  me: MeCapabilities | null | undefined,
): boolean {
  if (!me) return key === "schedule"; // Pre-load: only show the read-only schedule.
  switch (key) {
    case "schedule":
    case "pdf":
    case "fontSettings":
      return me.canRead;
    case "manage":
      return me.canMutate;
    case "requests":
      return me.canSubmitRequest || me.canApprove;
    case "approvals":
      return me.canApprove;
    case "tasks":
      // CEO and viewer roles never see it.
      return me.canViewTasks;
    case "notifications":
      return me.canRead;
    case "audit":
      return me.canViewAudit;
    default:
      return false;
  }
}

const DEFAULT_FONT: FontPrefs = {
  fontFamily: "system",
  fontSize: 14,
  fontWeight: "regular",
  alignment: "start",
};

type ColumnId = "number" | "meeting" | "attendees" | "time";

type ColumnSetting = {
  id: ColumnId;
  visible: boolean;
  width: number;
};

const DEFAULT_COLUMNS: ColumnSetting[] = [
  { id: "number", visible: true, width: 56 },
  { id: "meeting", visible: true, width: 320 },
  { id: "attendees", visible: true, width: 600 },
  // Wide enough to fit "HH:MM – HH:MM" on one line, plus the two
  // <input type="time"> controls when the cell enters edit mode.
  { id: "time", visible: true, width: 200 },
];

const MIN_COL_WIDTH = 56;
const MAX_COL_WIDTH = 1200;

const ROW_COLOR_OPTIONS: { key: string; bg: string; label: string }[] = [
  { key: "default", bg: "", label: "default" },
  { key: "red", bg: "#fee2e2", label: "red" },
  { key: "amber", bg: "#fef3c7", label: "amber" },
  { key: "green", bg: "#dcfce7", label: "green" },
  { key: "blue", bg: "#dbeafe", label: "blue" },
  { key: "violet", bg: "#ede9fe", label: "violet" },
  { key: "gray", bg: "#f3f4f6", label: "gray" },
];

const COLS_STORAGE_KEY = "em-schedule-cols-v1";
const ROW_COLORS_STORAGE_KEY = "em-schedule-row-colors-v1";
const HIGHLIGHT_STORAGE_KEY = "em-current-meeting-highlight-v1";

type HighlightPrefs = { enabled: boolean; color: string };
const DEFAULT_HIGHLIGHT_PREFS: HighlightPrefs = {
  enabled: true,
  color: "#16a34a",
};

const HIGHLIGHT_COLOR_SWATCHES = [
  "#16a34a",
  "#2563eb",
  "#dc2626",
  "#ea580c",
  "#7c3aed",
  "#0B1E3F",
];

function parseTimeToMinutes(t: string | null): number | null {
  if (!t) return null;
  const m = /^(\d{2}):(\d{2})/.exec(t);
  if (!m) return null;
  const h = Number(m[1]);
  const mm = Number(m[2]);
  if (!Number.isFinite(h) || !Number.isFinite(mm)) return null;
  return h * 60 + mm;
}

/**
 * Returns the id of the meeting that is currently in progress on `dateIso`,
 * or null. A meeting is "current" when the wall clock falls within
 * [startTime, endTime) and the displayed date matches today.
 */
function computeCurrentMeetingId(
  meetings: Meeting[],
  dateIso: string,
  nowMs: number,
): number | null {
  const now = new Date(nowMs);
  const todayIso = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
  if (todayIso !== dateIso) return null;
  const minutesNow = now.getHours() * 60 + now.getMinutes();
  for (const m of meetings) {
    const s = parseTimeToMinutes(m.startTime);
    const e = parseTimeToMinutes(m.endTime);
    if (s === null || e === null) continue;
    if (s <= minutesNow && minutesNow < e) return m.id;
  }
  return null;
}

function formatTime(t: string | null, lang: "ar" | "en"): string {
  if (!t) return "";
  // DB stores HH:mm:ss (24h). Build a Date anchored to a fixed day so
  // Intl can format the time-of-day in the user's language with AM/PM
  // (English) or ص/م (Arabic). Latin digits are enforced via the shared
  // formatTime helper. Falls back to the raw HH:mm slice if parsing
  // somehow fails (defensive — shouldn't happen for our DB shape).
  const hhmm = t.slice(0, 5);
  const parsed = new Date(`1970-01-01T${hhmm}:00`);
  if (Number.isNaN(parsed.getTime())) return hhmm;
  return i18nFormatTime(parsed, lang, {
    hour: "numeric",
    minute: "2-digit",
    hour12: true,
  });
}

function todayIso(): string {
  return new Date().toISOString().slice(0, 10);
}

function fontWeightToCss(w: FontPrefs["fontWeight"]): number {
  return { regular: 400, bold: 700 }[w];
}

function fontFamilyToCss(name: string): string {
  if (name === "system") return "";
  if (/[\s-]/.test(name)) return `"${name}", system-ui, sans-serif`;
  return `${name}, system-ui, sans-serif`;
}

function buildFontStyle(prefs: FontPrefs): CSSProperties {
  return {
    fontFamily: fontFamilyToCss(prefs.fontFamily) || undefined,
    fontSize: prefs.fontSize,
    fontWeight: fontWeightToCss(prefs.fontWeight),
    textAlign: prefs.alignment,
  };
}

function tWithFallback(
  t: (key: string) => string,
  key: string,
  fallback: string,
): string {
  const value = t(key);
  return value === key ? fallback : value;
}

async function apiJson<T>(
  url: string,
  init?: Omit<RequestInit, "body"> & { body?: unknown },
): Promise<T> {
  const { body: rawBody, headers: rawHeaders, ...rest } = init ?? {};
  const opts: RequestInit = {
    credentials: "include",
    ...rest,
    headers: { "Content-Type": "application/json", ...(rawHeaders ?? {}) },
  };
  if (rawBody !== undefined) {
    opts.body =
      typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
  }
  const res = await fetch(url, opts);
  if (res.status === 204) return undefined as T;
  const data = (await res.json().catch(() => ({}))) as T & { error?: string };
  if (!res.ok) {
    throw new Error((data as { error?: string }).error || `HTTP ${res.status}`);
  }
  return data;
}

function readJsonFromStorage<T>(key: string, fallback: T): T {
  if (typeof window === "undefined") return fallback;
  try {
    const raw = window.localStorage.getItem(key);
    if (!raw) return fallback;
    return JSON.parse(raw) as T;
  } catch {
    return fallback;
  }
}

function writeJsonToStorage(key: string, value: unknown): void {
  if (typeof window === "undefined") return;
  try {
    window.localStorage.setItem(key, JSON.stringify(value));
  } catch {
    /* ignore */
  }
}

function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(() => {
    if (typeof window === "undefined") return false;
    return window.matchMedia(query).matches;
  });
  useEffect(() => {
    if (typeof window === "undefined") return;
    const mql = window.matchMedia(query);
    const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
    setMatches(mql.matches);
    mql.addEventListener("change", handler);
    return () => mql.removeEventListener("change", handler);
  }, [query]);
  return matches;
}

function normalizeColumns(value: unknown): ColumnSetting[] {
  if (!Array.isArray(value)) return DEFAULT_COLUMNS;
  const seen = new Set<ColumnId>();
  const out: ColumnSetting[] = [];
  for (const entry of value) {
    if (!entry || typeof entry !== "object") continue;
    const e = entry as Partial<ColumnSetting>;
    if (typeof e.id !== "string") continue;
    if (!DEFAULT_COLUMNS.some((d) => d.id === e.id)) continue;
    if (seen.has(e.id as ColumnId)) continue;
    seen.add(e.id as ColumnId);
    const fallback = DEFAULT_COLUMNS.find((d) => d.id === e.id)!;
    const width =
      typeof e.width === "number" && Number.isFinite(e.width)
        ? Math.min(MAX_COL_WIDTH, Math.max(MIN_COL_WIDTH, e.width))
        : fallback.width;
    out.push({
      id: e.id as ColumnId,
      visible: typeof e.visible === "boolean" ? e.visible : true,
      width,
    });
  }
  // Append any defaults that were missing (e.g. new columns added later).
  for (const def of DEFAULT_COLUMNS) {
    if (!seen.has(def.id)) out.push({ ...def });
  }
  return out;
}

export default function ExecutiveMeetingsPage() {
  const { t, i18n } = useTranslation();
  const [, setLocation] = useLocation();
  const isRtl = i18n.language === "ar";
  const lang: "ar" | "en" = isRtl ? "ar" : "en";
  const BackIcon = isRtl ? ArrowRight : ArrowLeft;

  const [section, setSection] = useState<SectionKey>("schedule");
  const [date, setDate] = useState<string>(todayIso());

  const { data, isLoading } = useQuery<DayResponse>({
    queryKey: ["/api/executive-meetings", date],
    queryFn: async () => {
      const res = await fetch(`/api/executive-meetings?date=${date}`, {
        credentials: "include",
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    },
  });

  const { data: me } = useQuery<MeRoles>({
    queryKey: ["/api/executive-meetings/me"],
    queryFn: () => apiJson<MeRoles>("/api/executive-meetings/me"),
  });

  const { data: fontResp } = useQuery<FontSettingsResponse>({
    queryKey: ["/api/executive-meetings/font-settings"],
    queryFn: () => apiJson<FontSettingsResponse>("/api/executive-meetings/font-settings"),
  });

  const effectiveFont: FontPrefs = useMemo(() => {
    const u = fontResp?.user;
    const g = fontResp?.global;
    const src = u ?? g;
    if (!src) return DEFAULT_FONT;
    return {
      fontFamily: src.fontFamily,
      fontSize: src.fontSize,
      fontWeight: src.fontWeight,
      alignment: src.alignment,
    };
  }, [fontResp]);

  const meetings = data?.meetings ?? [];

  return (
    <div
      className="min-h-screen bg-[#f4f6fb] text-foreground"
      dir={isRtl ? "rtl" : "ltr"}
    >
      <header className="bg-white border-b border-gray-200 shadow-sm print:hidden">
        <div className="max-w-screen-2xl mx-auto px-4 sm:px-6 py-3 flex items-center gap-3 sm:gap-4 flex-wrap">
          <Button
            variant="ghost"
            size="icon"
            onClick={() => setLocation("/")}
            aria-label={t("common.back")}
            className="text-foreground"
          >
            <BackIcon className="w-5 h-5" />
          </Button>

          <div className="flex items-center gap-2 sm:gap-3 min-w-0">
            <div className="w-10 h-10 sm:w-11 sm:h-11 rounded-md bg-[#0B1E3F] flex items-center justify-center text-white shrink-0">
              <CalendarClock className="w-6 h-6" />
            </div>
            <div className="min-w-0 leading-tight">
              <div className="text-sm sm:text-base font-semibold text-[#0B1E3F] truncate">
                {t("executiveMeetings.titleAr")}
              </div>
              <div className="text-[11px] sm:text-xs text-muted-foreground truncate">
                {t("executiveMeetings.titleEn")}
              </div>
            </div>
          </div>

          <div className="flex-1" />

          <div className="flex items-center gap-2">
            <Button
              variant="outline"
              size="sm"
              className="gap-1.5"
              onClick={() => setSection("pdf")}
              data-testid="em-export-shortcut"
            >
              <FileDown className="w-4 h-4" />
              <span className="hidden sm:inline">{t("executiveMeetings.exportPdf")}</span>
            </Button>
            <Button
              variant="outline"
              size="sm"
              className="gap-1.5"
              onClick={() => setSection("fontSettings")}
            >
              <Type className="w-4 h-4" />
              <span className="hidden sm:inline">{t("executiveMeetings.fontSettings")}</span>
            </Button>
            <Button
              variant="outline"
              size="sm"
              className="gap-1.5"
              onClick={() => i18n.changeLanguage(isRtl ? "en" : "ar")}
            >
              <Languages className="w-4 h-4" />
              <span className="text-xs">{isRtl ? "EN" : "ع"}</span>
            </Button>
          </div>
        </div>

        <nav className="border-t border-gray-100 bg-white overflow-x-auto">
          <div className="max-w-screen-2xl mx-auto px-2 sm:px-4 flex gap-1 sm:gap-2 min-w-max">
            {SECTIONS.filter(({ key }) => isSectionVisible(key, me)).map(
              ({ key, icon: Icon }) => {
                const active = section === key;
                return (
                  <button
                    key={key}
                    type="button"
                    onClick={() => setSection(key)}
                    className={
                      "flex items-center gap-1.5 px-3 sm:px-4 py-2.5 text-xs sm:text-sm whitespace-nowrap border-b-2 transition-colors " +
                      (active
                        ? "border-[#0B1E3F] text-[#0B1E3F] font-semibold"
                        : "border-transparent text-muted-foreground hover:text-foreground")
                    }
                    data-testid={`em-nav-${key}`}
                  >
                    <Icon className="w-4 h-4" />
                    {t(`executiveMeetings.nav.${key}`)}
                  </button>
                );
              },
            )}
          </div>
        </nav>
      </header>

      <main className="max-w-screen-2xl mx-auto px-3 sm:px-6 py-4 sm:py-6">
        {section === "schedule" && (
          <ScheduleSection
            meetings={meetings}
            date={date}
            onDateChange={setDate}
            isLoading={isLoading}
            isRtl={isRtl}
            t={t}
            font={effectiveFont}
            canMutate={!!me?.canMutate}
          />
        )}
        {section === "manage" && me && (
          <ManageSection date={date} onDateChange={setDate} canMutate={me.canMutate} isRtl={isRtl} t={t} />
        )}
        {section === "requests" && me && (
          <RequestsSection me={me} isRtl={isRtl} t={t} />
        )}
        {section === "approvals" && me && (
          <ApprovalsSection canApprove={me.canApprove} isRtl={isRtl} t={t} />
        )}
        {section === "tasks" && me && (
          <TasksSection
            canMutate={me.canMutate}
            currentUserId={me.userId}
            canViewAllTasks={me.canViewAllTasks}
            isRtl={isRtl}
            t={t}
          />
        )}
        {section === "notifications" && me && (
          <NotificationsSection
            canView={me.canRead}
            date={date}
            isRtl={isRtl}
            t={t}
          />
        )}
        {section === "audit" && me && (
          <AuditSection canView={me.canViewAudit} isRtl={isRtl} t={t} />
        )}
        {section === "pdf" && (
          <PdfSection date={date} onDateChange={setDate} lang={lang} t={t} />
        )}
        {section === "fontSettings" && me && (
          <FontSettingsSection
            font={effectiveFont}
            canEditGlobal={me.canApprove}
            t={t}
          />
        )}
      </main>
    </div>
  );
}

// ============ SCHEDULE ============

function ScheduleSection({
  meetings,
  date,
  onDateChange,
  isLoading,
  isRtl,
  t,
  font,
  canMutate,
}: {
  meetings: Meeting[];
  date: string;
  onDateChange: (d: string) => void;
  isLoading: boolean;
  isRtl: boolean;
  t: (k: string) => string;
  font: FontPrefs;
  canMutate: boolean;
}) {
  const qc = useQueryClient();
  const { toast } = useToast();
  const tableStyle = buildFontStyle(font);
  const [columns, setColumns] = useState<ColumnSetting[]>(() =>
    normalizeColumns(readJsonFromStorage(COLS_STORAGE_KEY, DEFAULT_COLUMNS)),
  );
  const [rowColors, setRowColors] = useState<Record<number, string>>(() =>
    readJsonFromStorage<Record<number, string>>(ROW_COLORS_STORAGE_KEY, {}),
  );
  const [highlightPrefs, setHighlightPrefs] = useState<HighlightPrefs>(() =>
    readJsonFromStorage<HighlightPrefs>(
      HIGHLIGHT_STORAGE_KEY,
      DEFAULT_HIGHLIGHT_PREFS,
    ),
  );

  useEffect(() => {
    writeJsonToStorage(HIGHLIGHT_STORAGE_KEY, highlightPrefs);
  }, [highlightPrefs]);

  const [nowTick, setNowTick] = useState(() => Date.now());
  useEffect(() => {
    let intervalId: ReturnType<typeof setInterval> | null = null;
    const msToNextMinute = 60_000 - (Date.now() % 60_000);
    const timeoutId = setTimeout(() => {
      setNowTick(Date.now());
      intervalId = setInterval(() => setNowTick(Date.now()), 60_000);
    }, msToNextMinute);
    return () => {
      clearTimeout(timeoutId);
      if (intervalId) clearInterval(intervalId);
    };
  }, []);

  const currentMeetingId = useMemo(
    () => computeCurrentMeetingId(meetings, date, nowTick),
    [meetings, date, nowTick],
  );

  // Optimistic local override so we can render the new row order before the
  // server round-trip finishes. Cleared on each new meetings prop.
  const [optimisticOrder, setOptimisticOrder] = useState<number[] | null>(null);
  const [reordering, setReordering] = useState(false);
  const reorderingRef = useRef(false);
  useEffect(() => {
    setOptimisticOrder(null);
  }, [meetings, date]);

  const orderedMeetings = useMemo(() => {
    if (!optimisticOrder) return meetings;
    const byId = new Map(meetings.map((m) => [m.id, m]));
    const out: Meeting[] = [];
    for (const id of optimisticOrder) {
      const m = byId.get(id);
      if (m) out.push(m);
    }
    // Append any meetings not in the optimistic order (defensive).
    for (const m of meetings) {
      if (!optimisticOrder.includes(m.id)) out.push(m);
    }
    return out;
  }, [meetings, optimisticOrder]);

  const refreshDay = useCallback(() => {
    void qc.invalidateQueries({
      queryKey: ["/api/executive-meetings", date],
    });
  }, [qc, date]);

  const saveTitle = useCallback(
    async (meeting: Meeting, html: string) => {
      const body = isRtl ? { titleAr: html } : { titleEn: html };
      try {
        await apiJson(`/api/executive-meetings/${meeting.id}`, {
          method: "PATCH",
          body,
        });
        refreshDay();
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        toast({
          title: t("common.error"),
          description: msg,
          variant: "destructive",
        });
        throw err;
      }
    },
    [isRtl, refreshDay, toast, t],
  );

  const saveAttendeeName = useCallback(
    async (meeting: Meeting, attendeeIdx: number, html: string) => {
      // Detect "truly empty" content: Tiptap empty paragraphs, &nbsp;, or
      // pure whitespace. When the attendee's name is cleared we treat the
      // edit as a delete-row gesture and remove the attendee instead of
      // saving an empty record.
      const isEmpty =
        html
          .replace(/<[^>]*>/g, "")
          .replace(/&nbsp;/g, " ")
          .replace(/\u00a0/g, " ")
          .trim() === "";
      const next = isEmpty
        ? meeting.attendees
            .filter((_, i) => i !== attendeeIdx)
            // Re-pack sortOrder so the server stores a contiguous run; the
            // PUT endpoint takes the array as the new source of truth and
            // reassigns ids on insert.
            .map((a, i) => ({ ...a, sortOrder: i }))
        : meeting.attendees.map((a, i) =>
            i === attendeeIdx ? { ...a, name: html } : a,
          );
      try {
        await apiJson(`/api/executive-meetings/${meeting.id}/attendees`, {
          method: "PUT",
          body: {
            attendees: next.map((a) => ({
              name: a.name,
              title: a.title,
              attendanceType: a.attendanceType,
              sortOrder: a.sortOrder,
            })),
          },
        });
        refreshDay();
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        toast({
          title: t("common.error"),
          description: msg,
          variant: "destructive",
        });
        throw err;
      }
    },
    [refreshDay, toast, t],
  );

  // -------------------------------------------------------------------
  // "+ Add attendee" — optimistic ghost row
  //
  // Clicking the inline "+" button in the attendees cell does NOT hit
  // the server immediately (the API requires a non-empty name). Instead
  // we render a synthetic in-edit EditableCell next to the existing
  // attendees. When the user saves a non-empty name we PUT the full
  // array with the new attendee appended; if they leave the cell empty
  // (blur or Escape) we just discard the pending state.
  //
  // We allow at most one pending attendee at a time across the whole
  // page — the AttendeesCell hides "+" buttons elsewhere while a ghost
  // row is open so the user can't accidentally orphan their typing.
  type PendingAttendee = {
    meetingId: number;
    attendanceType: Attendee["attendanceType"];
    // Bumped each time the user clicks "+" so React remounts the ghost
    // EditableCell (giving it a fresh editor and focus).
    key: number;
  };
  const [pendingAttendee, setPendingAttendee] =
    useState<PendingAttendee | null>(null);

  const startAddAttendee = useCallback(
    (meetingId: number, attendanceType: Attendee["attendanceType"]) => {
      // Only one ghost row may be open at a time across the whole page.
      // We refuse a new start while another is pending — the user must
      // first commit (blur with text) or cancel (Escape / blur empty).
      // The "+" buttons are already hidden in this state, but a stale
      // click that beats the re-render is still possible.
      setPendingAttendee((prev) =>
        prev ? prev : { meetingId, attendanceType, key: Date.now() },
      );
    },
    [],
  );

  const cancelAddAttendee = useCallback(() => {
    setPendingAttendee(null);
  }, []);

  const commitAddAttendee = useCallback(
    async (
      meeting: Meeting,
      attendanceType: Attendee["attendanceType"],
      html: string,
    ) => {
      // Always clear pending — whether we end up persisting or not.
      setPendingAttendee(null);
      const isEmpty =
        html
          .replace(/<[^>]*>/g, "")
          .replace(/&nbsp;/g, " ")
          .replace(/\u00a0/g, " ")
          .trim() === "";
      if (isEmpty) return;
      const maxSort = meeting.attendees.reduce(
        (m, a) => Math.max(m, a.sortOrder ?? 0),
        -1,
      );
      const next = [
        ...meeting.attendees.map((a) => ({
          name: a.name,
          title: a.title,
          attendanceType: a.attendanceType,
          sortOrder: a.sortOrder,
        })),
        {
          name: html,
          title: null as string | null,
          attendanceType,
          sortOrder: maxSort + 1,
        },
      ];
      try {
        await apiJson(`/api/executive-meetings/${meeting.id}/attendees`, {
          method: "PUT",
          body: { attendees: next },
        });
        refreshDay();
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        toast({
          title: t("common.error"),
          description: msg,
          variant: "destructive",
        });
      }
    },
    [refreshDay, toast, t],
  );

  const saveTimes = useCallback(
    async (
      meeting: Meeting,
      startTime: string | null,
      endTime: string | null,
    ) => {
      try {
        await apiJson(`/api/executive-meetings/${meeting.id}`, {
          method: "PATCH",
          body: { startTime, endTime },
        });
        refreshDay();
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        toast({
          title: t("common.error"),
          description: msg,
          variant: "destructive",
        });
        throw err;
      }
    },
    [refreshDay, toast, t],
  );

  // PATCH the cell-merge overlay on a meeting. `merge === null` clears
  // the overlay and restores the original cells; otherwise it sets a
  // new range + free text that the row will display in place of the
  // spanned cells.
  const saveMerge = useCallback(
    async (
      meeting: Meeting,
      merge:
        | { mergeStartColumn: ColumnId; mergeEndColumn: ColumnId; mergeText: string }
        | null,
    ) => {
      try {
        await apiJson(`/api/executive-meetings/${meeting.id}`, {
          method: "PATCH",
          body: { merge },
        });
        refreshDay();
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        toast({
          title: t("common.error"),
          description: msg,
          variant: "destructive",
        });
        throw err;
      }
    },
    [refreshDay, toast, t],
  );

  // Inline "delete entire meeting" action exposed on each schedule row.
  // Uses the same DELETE endpoint as the Manage section, but lives on
  // the schedule itself so editors can remove a row without context-
  // switching. Confirmation surfaces the meeting title so users know
  // exactly what will be removed (titles can contain rich-text HTML so
  // we strip tags before showing the prompt).
  const [deletingMeetingId, setDeletingMeetingId] = useState<number | null>(
    null,
  );
  const deleteMeeting = useCallback(
    async (meeting: Meeting) => {
      if (deletingMeetingId != null) return;
      // Pick the localized display title with the same fallback the row
      // uses, then strip HTML so the native confirm dialog shows plain
      // text (titles are Tiptap rich text in the DB).
      const rawTitle = isRtl
        ? meeting.titleAr
        : meeting.titleEn || meeting.titleAr;
      const plainTitle = rawTitle
        .replace(/<[^>]*>/g, "")
        .replace(/&nbsp;/g, " ")
        .replace(/\u00a0/g, " ")
        .trim();
      const message = t("executiveMeetings.schedule.deleteRowConfirm").replace(
        "{{title}}",
        plainTitle,
      );
      if (!window.confirm(message)) return;
      setDeletingMeetingId(meeting.id);
      try {
        await apiJson(`/api/executive-meetings/${meeting.id}`, {
          method: "DELETE",
        });
        toast({ title: t("executiveMeetings.schedule.deleted") });
        refreshDay();
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        toast({
          title: t("common.error"),
          description: msg,
          variant: "destructive",
        });
      } finally {
        setDeletingMeetingId(null);
      }
    },
    [deletingMeetingId, isRtl, refreshDay, t, toast],
  );

  const [creatingRow, setCreatingRow] = useState(false);
  // When the user clicks "Add row" we capture the new meeting id here so
  // that, once the refreshed list contains it, we can scroll the row into
  // view and auto-open the title cell in inline-edit mode.
  const [autoEditTitleId, setAutoEditTitleId] = useState<number | null>(null);
  const createRow = useCallback(async () => {
    if (creatingRow) return;
    setCreatingRow(true);
    try {
      const created = (await apiJson(`/api/executive-meetings`, {
        method: "POST",
        body: {
          titleAr: t("executiveMeetings.schedule.newMeetingDefaultAr"),
          titleEn: t("executiveMeetings.schedule.newMeetingDefaultEn"),
          meetingDate: date,
        },
      })) as Meeting | undefined;
      if (created?.id) setAutoEditTitleId(created.id);
      refreshDay();
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({
        title: t("common.error"),
        description: msg,
        variant: "destructive",
      });
    } finally {
      setCreatingRow(false);
    }
  }, [creatingRow, date, refreshDay, toast, t]);

  // Once the auto-edit target meeting is in the list, scroll it into view.
  // The MeetingRow itself receives `startInEditMode` so its EditableCell
  // opens immediately and clears `autoEditTitleId` via `onStartedEditing`.
  // We also schedule a fallback clear so the state never goes stale if the
  // title cell never mounts (e.g. column hidden, row filtered out).
  useEffect(() => {
    if (autoEditTitleId == null) return;
    if (!meetings.some((m) => m.id === autoEditTitleId)) return;
    const rafId = window.requestAnimationFrame(() => {
      const row = document.querySelector(
        `[data-testid="em-row-${autoEditTitleId}"]`,
      );
      if (row && "scrollIntoView" in row) {
        (row as HTMLElement).scrollIntoView({
          block: "center",
          behavior: "smooth",
        });
      }
    });
    const fallback = window.setTimeout(() => {
      setAutoEditTitleId(null);
    }, 1500);
    return () => {
      window.cancelAnimationFrame(rafId);
      window.clearTimeout(fallback);
    };
  }, [autoEditTitleId, meetings]);
  const clearAutoEditTitleId = useCallback(() => {
    setAutoEditTitleId(null);
  }, []);

  const reorderRows = useCallback(
    async (fromId: number, toId: number) => {
      if (fromId === toId) return;
      if (reorderingRef.current) return;
      const ids = meetings.map((m) => m.id);
      const fromIdx = ids.indexOf(fromId);
      const toIdx = ids.indexOf(toId);
      if (fromIdx < 0 || toIdx < 0) return;
      const newOrder = ids.slice();
      newOrder.splice(fromIdx, 1);
      newOrder.splice(toIdx, 0, fromId);
      setOptimisticOrder(newOrder);
      reorderingRef.current = true;
      setReordering(true);
      try {
        await apiJson(`/api/executive-meetings/reorder`, {
          method: "POST",
          body: { meetingDate: date, orderedIds: newOrder },
        });
        refreshDay();
      } catch (err) {
        setOptimisticOrder(null);
        const msg = err instanceof Error ? err.message : String(err);
        toast({
          title: t("common.error"),
          description: msg,
          variant: "destructive",
        });
      } finally {
        reorderingRef.current = false;
        setReordering(false);
      }
    },
    [meetings, date, refreshDay, toast, t],
  );

  useEffect(() => {
    writeJsonToStorage(COLS_STORAGE_KEY, columns);
  }, [columns]);

  useEffect(() => {
    writeJsonToStorage(ROW_COLORS_STORAGE_KEY, rowColors);
  }, [rowColors]);

  const visibleColumns = columns.filter((c) => c.visible);

  const setColumnWidth = useCallback((id: ColumnId, width: number) => {
    setColumns((prev) =>
      prev.map((c) =>
        c.id === id
          ? {
              ...c,
              width: Math.min(MAX_COL_WIDTH, Math.max(MIN_COL_WIDTH, width)),
            }
          : c,
      ),
    );
  }, []);

  const setRowColor = useCallback((meetingId: number, colorKey: string) => {
    setRowColors((prev) => {
      const next = { ...prev };
      if (colorKey === "default") {
        delete next[meetingId];
      } else {
        next[meetingId] = colorKey;
      }
      return next;
    });
  }, []);

  // Responsive breakpoints:
  //   < md (<768px)   → stacked card layout (phones)
  //   md..<xl         → fluid table, browser shrinks columns to fit (tablets)
  //   ≥ xl (≥1280px)  → fixed-width table with resize handles (desktop)
  //   print           → fixed-width table at all viewports (preserves the
  //                      historical print layout from any screen size).
  // Whether the configured pixel widths are *enforced* is controlled purely
  // by CSS: `table-auto xl:table-fixed print:table-fixed`. We always emit the
  // colgroup and per-cell widths so they're available whenever the table is
  // in fixed-layout mode (xl screens AND any print). In `table-auto` mode,
  // the browser treats those widths as hints and shrinks columns to fit.
  // Resize handles only appear on large screens AND fine pointers (mouse).
  // Touch devices — including iPad Pro at >=1280 CSS px — get no handles
  // since drag-resizing with a finger fights touch scrolling.
  const isXl = useMediaQuery("(min-width: 1280px)");
  const isFinePointer = useMediaQuery("(pointer: fine)");
  const showResizeHandles = isXl && isFinePointer;

  // dnd-kit: sortable column headers + sortable rows.
  //
  // - PointerSensor for desktop mouse: a 6px activation distance keeps a
  //   normal click on cell content from being mistaken for a drag (so the
  //   inline editors on title/attendee/time cells still open on click).
  // - TouchSensor for iPad and other touch devices: PointerSensor alone
  //   does not work on touch because the browser claims the gesture as a
  //   vertical scroll before the finger has moved 6px, so the drag never
  //   activates. A long-press activation (delay+tolerance) lets the user
  //   press-and-hold the row's grip handle to start dragging while still
  //   leaving normal scrolling intact everywhere else on the row.
  // - KeyboardSensor for accessibility / keyboard reordering.
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
    useSensor(TouchSensor, {
      activationConstraint: { delay: 200, tolerance: 8 },
    }),
    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
  );

  const onColumnDragEnd = useCallback(
    (e: DragEndEvent) => {
      const { active, over } = e;
      if (!over || active.id === over.id) return;
      setColumns((prev) => {
        const ids = prev.map((c) => c.id);
        const fromIdx = ids.indexOf(active.id as ColumnId);
        const toIdx = ids.indexOf(over.id as ColumnId);
        if (fromIdx < 0 || toIdx < 0) return prev;
        const next = prev.slice();
        const [moved] = next.splice(fromIdx, 1);
        if (!moved) return prev;
        next.splice(toIdx, 0, moved);
        return next;
      });
    },
    [],
  );

  const onRowDragEnd = useCallback(
    (e: DragEndEvent) => {
      const { active, over } = e;
      if (!over || active.id === over.id) return;
      void reorderRows(Number(active.id), Number(over.id));
    },
    [reorderRows],
  );

  return (
    <div className="space-y-4 print:space-y-2">
      <div className="flex items-center justify-between gap-3 flex-wrap print:hidden">
        <h2
          className="text-xl sm:text-2xl font-bold text-[#0B1E3F]"
          data-testid="em-schedule-heading"
        >
          {t("executiveMeetings.scheduleHeading")}
        </h2>
        <div className="flex items-center gap-2 flex-wrap">
          <ColumnsCustomizer
            columns={columns}
            onChange={setColumns}
            t={t}
            highlightPrefs={highlightPrefs}
            onHighlightPrefsChange={setHighlightPrefs}
          />
          <label className="flex items-center gap-2 text-sm">
            <span className="text-muted-foreground">{t("executiveMeetings.date")}</span>
            <input
              type="date"
              value={date}
              onChange={(e) => onDateChange(e.target.value)}
              className="border border-gray-300 rounded px-2 py-1 text-sm bg-white"
            />
          </label>
        </div>
      </div>

      <div className="hidden print:block text-center mb-2">
        <div className="font-bold text-lg">{t("executiveMeetings.titleAr")}</div>
        <div className="text-sm">{date}</div>
      </div>

      <div
        className="bg-white border-2 border-[#0B1E3F] rounded-md overflow-x-auto shadow-sm print:shadow-none print:border-black print:overflow-hidden"
        id="executive-schedule-printable"
      >
        <table
          className="border-collapse text-sm md:text-[15px] w-full table-auto xl:table-fixed print:table-fixed"
          dir={isRtl ? "rtl" : "ltr"}
          style={tableStyle}
        >
          {/* Always emit colgroup + th widths so they're available for print
              and for ≥xl screens. CSS classes on the <table> decide whether
              they're enforced (table-fixed) or treated as hints (table-auto). */}
          <colgroup>
            {visibleColumns.map((c) => (
              <col key={c.id} style={{ width: c.width }} />
            ))}
          </colgroup>
          <DndContext
            sensors={sensors}
            collisionDetection={closestCenter}
            onDragEnd={onColumnDragEnd}
          >
            <thead>
              <tr className="bg-[#0B1E3F] text-white">
                <SortableContext
                  items={visibleColumns.map((c) => c.id)}
                  strategy={horizontalListSortingStrategy}
                >
                  {visibleColumns.map((c, idx) => (
                    <SortableHeader
                      key={c.id}
                      col={c}
                      isLast={idx === visibleColumns.length - 1}
                      isRtl={isRtl}
                      t={t}
                      showResizeHandle={showResizeHandles}
                      onResize={(delta) => setColumnWidth(c.id, c.width + delta)}
                    />
                  ))}
                </SortableContext>
              </tr>
            </thead>
          </DndContext>
          <DndContext
            sensors={sensors}
            collisionDetection={closestCenter}
            onDragEnd={onRowDragEnd}
          >
            <tbody>
              {isLoading && (
                <tr>
                  <td
                    colSpan={visibleColumns.length}
                    className="py-10 text-center text-muted-foreground"
                  >
                    {t("common.loading")}
                  </td>
                </tr>
              )}
              {!isLoading && orderedMeetings.length === 0 && (
                <tr>
                  <td
                    colSpan={visibleColumns.length}
                    className="py-10 text-center text-muted-foreground"
                  >
                    {t("executiveMeetings.noMeetings")}
                  </td>
                </tr>
              )}
              <SortableContext
                items={orderedMeetings.map((m) => m.id)}
                strategy={verticalListSortingStrategy}
              >
                {orderedMeetings.map((m) => (
                  <MeetingRow
                    key={m.id}
                    meeting={m}
                    isRtl={isRtl}
                    t={t}
                    visibleColumns={visibleColumns}
                    rowColorKey={rowColors[m.id] ?? "default"}
                    onPickRowColor={(key) => setRowColor(m.id, key)}
                    canMutate={canMutate}
                    onSaveTitle={(html) => saveTitle(m, html)}
                    onSaveAttendeeName={(idx, html) =>
                      saveAttendeeName(m, idx, html)
                    }
                    onSaveTimes={(start, end) => saveTimes(m, start, end)}
                    onSaveMerge={(merge) => saveMerge(m, merge)}
                    onDeleteMeeting={() => deleteMeeting(m)}
                    // Disable every row's delete button while any delete
                    // is in flight, since deleteMeeting short-circuits
                    // concurrent calls — keeps the UI honest about which
                    // clicks will do anything.
                    isDeleting={deletingMeetingId !== null}
                    isCurrent={
                      highlightPrefs.enabled && currentMeetingId === m.id
                    }
                    highlightColor={highlightPrefs.color}
                    reordering={reordering}
                    autoEditTitle={autoEditTitleId === m.id}
                    onAutoEditTitleConsumed={clearAutoEditTitleId}
                    pendingAttendee={pendingAttendee}
                    onStartAddAttendee={startAddAttendee}
                    onCommitAddAttendee={(type, html) =>
                      commitAddAttendee(m, type, html)
                    }
                    onCancelAddAttendee={cancelAddAttendee}
                  />
                ))}
              </SortableContext>
              {canMutate && !isLoading && (
                <tr
                  className="print:hidden"
                  data-testid="em-add-row-tr"
                >
                  <td
                    colSpan={visibleColumns.length}
                    className="border border-gray-300 p-0"
                  >
                    <button
                      type="button"
                      onClick={createRow}
                      disabled={creatingRow}
                      className="w-full flex items-center justify-center gap-2 py-2.5 text-sm text-[#0B1E3F] hover:bg-blue-50/60 focus:bg-blue-50/60 focus:outline-none transition-colors disabled:opacity-60"
                      data-testid="em-add-row-button"
                    >
                      <Plus className="w-4 h-4" />
                      <span>
                        {creatingRow
                          ? t("common.loading")
                          : t("executiveMeetings.schedule.addRow")}
                      </span>
                    </button>
                  </td>
                </tr>
              )}
            </tbody>
          </DndContext>
        </table>
      </div>
    </div>
  );
}

function ResizeHandle({
  isRtl,
  onResize,
}: {
  isRtl: boolean;
  onResize: (delta: number) => void;
}) {
  const startXRef = useRef<number | null>(null);

  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
    e.preventDefault();
    e.stopPropagation();
    (e.target as HTMLDivElement).setPointerCapture(e.pointerId);
    startXRef.current = e.clientX;
  };

  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
    if (startXRef.current === null) return;
    const raw = e.clientX - startXRef.current;
    // In RTL the trailing edge is on the LEFT, so dragging left makes the
    // column wider. Negate the delta in that case.
    const delta = isRtl ? -raw : raw;
    if (Math.abs(delta) >= 1) {
      startXRef.current = e.clientX;
      onResize(delta);
    }
  };

  const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
    startXRef.current = null;
    try {
      (e.target as HTMLDivElement).releasePointerCapture(e.pointerId);
    } catch {
      /* ignore */
    }
  };

  // Place handle on the trailing (logical end) edge so it works the same in
  // both LTR and RTL.
  const sideClass = isRtl ? "left-0" : "right-0";

  return (
    <div
      role="separator"
      aria-orientation="vertical"
      className={`absolute top-0 ${sideClass} h-full w-1.5 cursor-col-resize hover:bg-white/40 active:bg-white/60`}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={onPointerUp}
      onPointerCancel={onPointerUp}
    />
  );
}

function ColumnsCustomizer({
  columns,
  onChange,
  t,
  highlightPrefs,
  onHighlightPrefsChange,
}: {
  columns: ColumnSetting[];
  onChange: (next: ColumnSetting[]) => void;
  t: (k: string) => string;
  highlightPrefs: HighlightPrefs;
  onHighlightPrefsChange: (next: HighlightPrefs) => void;
}) {
  const toggle = (id: ColumnId, value: boolean) => {
    // Always keep at least one column visible.
    const visibleCount = columns.filter((c) => c.visible).length;
    if (!value && visibleCount <= 1) return;
    onChange(columns.map((c) => (c.id === id ? { ...c, visible: value } : c)));
  };

  const reset = () => onChange(DEFAULT_COLUMNS.map((c) => ({ ...c })));

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          size="sm"
          className="gap-1.5"
          data-testid="em-customize-columns-trigger"
        >
          <Sliders className="w-4 h-4" />
          <span className="hidden sm:inline">{t("executiveMeetings.customize.title")}</span>
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-72 p-3" align="end">
        <div className="text-sm font-semibold text-[#0B1E3F] mb-2">
          {t("executiveMeetings.customize.title")}
        </div>
        <p className="text-[11px] text-muted-foreground mb-3 leading-relaxed">
          {t("executiveMeetings.customize.hint")}
        </p>
        <ul className="space-y-1.5">
          {columns.map((c) => (
            <li
              key={c.id}
              className="flex items-center gap-2 rounded border border-gray-100 bg-gray-50 px-2 py-1.5"
              data-testid={`em-customize-row-${c.id}`}
            >
              <Checkbox
                id={`em-col-${c.id}`}
                checked={c.visible}
                onCheckedChange={(v) => toggle(c.id, !!v)}
                data-testid={`em-customize-toggle-${c.id}`}
              />
              <label
                htmlFor={`em-col-${c.id}`}
                className="flex-1 text-sm text-[#0B1E3F] cursor-pointer truncate"
              >
                {t(`executiveMeetings.col.${c.id}`)}
              </label>
              {!c.visible ? (
                <EyeOff className="w-3.5 h-3.5 text-muted-foreground" />
              ) : (
                <Eye className="w-3.5 h-3.5 text-[#0B1E3F]" />
              )}
            </li>
          ))}
        </ul>
        <div className="mt-3 flex items-center justify-between">
          <Button
            variant="ghost"
            size="sm"
            className="gap-1.5 text-xs"
            onClick={reset}
            data-testid="em-customize-reset"
          >
            <RotateCcw className="w-3.5 h-3.5" />
            {t("executiveMeetings.customize.reset")}
          </Button>
          <span className="text-[11px] text-muted-foreground">
            {t("executiveMeetings.customize.dragHint")}
          </span>
        </div>
        <div className="mt-4 pt-3 border-t border-gray-200">
          <div className="flex items-center justify-between mb-2">
            <label
              htmlFor="em-highlight-toggle"
              className="text-sm font-semibold text-[#0B1E3F] cursor-pointer"
            >
              {t("executiveMeetings.highlight.title")}
            </label>
            <Switch
              id="em-highlight-toggle"
              checked={highlightPrefs.enabled}
              onCheckedChange={(v) =>
                onHighlightPrefsChange({ ...highlightPrefs, enabled: !!v })
              }
              data-testid="em-highlight-toggle"
            />
          </div>
          <p className="text-[11px] text-muted-foreground mb-2 leading-relaxed">
            {t("executiveMeetings.highlight.hint")}
          </p>
          <div className="flex items-center gap-1.5 flex-wrap">
            {HIGHLIGHT_COLOR_SWATCHES.map((c) => {
              const selected = highlightPrefs.color === c;
              return (
                <button
                  key={c}
                  type="button"
                  onClick={() =>
                    onHighlightPrefsChange({ ...highlightPrefs, color: c })
                  }
                  className={
                    "w-6 h-6 rounded-full border-2 transition-transform " +
                    (selected
                      ? "border-[#0B1E3F] scale-110"
                      : "border-gray-300 hover:scale-110")
                  }
                  style={{ backgroundColor: c }}
                  aria-label={`highlight color ${c}`}
                  data-testid={`em-highlight-color-${c}`}
                  disabled={!highlightPrefs.enabled}
                />
              );
            })}
            <button
              type="button"
              onClick={() =>
                onHighlightPrefsChange({ ...DEFAULT_HIGHLIGHT_PREFS })
              }
              className="ml-1 text-[11px] text-[#0B1E3F] underline hover:no-underline disabled:opacity-50"
              data-testid="em-highlight-reset"
            >
              {t("executiveMeetings.highlight.reset")}
            </button>
          </div>
        </div>
      </PopoverContent>
    </Popover>
  );
}

/**
 * Sortable column header for the schedule table. The whole <th> is the
 * drag handle so it stays grabbable in any RTL/LTR mode and on touch. The
 * resize handle on the trailing edge stops drag propagation so resizing
 * still works.
 */
function SortableHeader({
  col,
  isLast,
  isRtl,
  t,
  showResizeHandle,
  onResize,
}: {
  col: ColumnSetting;
  isLast: boolean;
  isRtl: boolean;
  t: (k: string) => string;
  showResizeHandle: boolean;
  onResize: (delta: number) => void;
}) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: col.id });
  const align =
    col.id === "number" || col.id === "time" ? "text-center" : "text-start";
  const style: CSSProperties = {
    width: col.width,
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.5 : 1,
    cursor: "grab",
  };
  return (
    <th
      ref={setNodeRef}
      className={`relative border border-[#0B1E3F] px-2 py-2 ${align} font-semibold select-none`}
      style={style}
      data-testid={`em-col-header-${col.id}`}
      {...attributes}
      {...listeners}
    >
      {t(`executiveMeetings.col.${col.id}`)}
      {/* Resize handles only render on large screens AND fine (mouse)
          pointers — see comment on showResizeHandles. */}
      {!isLast && showResizeHandle && (
        <ResizeHandle isRtl={isRtl} onResize={onResize} />
      )}
    </th>
  );
}

function RowColorPickerSwatches({
  current,
  onPick,
  t,
}: {
  current: string;
  onPick: (key: string) => void;
  t: (k: string) => string;
}) {
  return (
    <>
      <div className="text-[11px] text-muted-foreground mb-2">
        {t("executiveMeetings.rowColor.label")}
      </div>
      <div className="flex items-center gap-1.5">
        {ROW_COLOR_OPTIONS.map((opt) => {
          const selected = current === opt.key;
          const isDefault = opt.key === "default";
          return (
            <button
              key={opt.key}
              type="button"
              onClick={() => onPick(opt.key)}
              className={
                "w-6 h-6 rounded-full border-2 transition-transform " +
                (selected
                  ? "border-[#0B1E3F] scale-110"
                  : "border-gray-300 hover:scale-110")
              }
              style={{
                background: isDefault
                  ? "repeating-linear-gradient(45deg,#fff,#fff 4px,#e5e7eb 4px,#e5e7eb 8px)"
                  : opt.bg,
              }}
              aria-label={opt.label}
              data-testid={`em-row-color-${opt.key}`}
            />
          );
        })}
      </div>
    </>
  );
}

function RowColorPicker({
  current,
  onPick,
  t,
}: {
  current: string;
  onPick: (key: string) => void;
  t: (k: string) => string;
}) {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <button
          type="button"
          className="absolute top-1 inline-end-1 p-1 rounded text-muted-foreground hover:text-[#0B1E3F] hover:bg-white/70 opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity"
          aria-label={t("executiveMeetings.rowColor.label")}
          data-testid="em-row-color-trigger"
        >
          <Palette className="w-3.5 h-3.5" />
        </button>
      </PopoverTrigger>
      <PopoverContent className="w-auto p-2" align="end">
        <RowColorPickerSwatches current={current} onPick={onPick} t={t} />
      </PopoverContent>
    </Popover>
  );
}

// Reusable row-level "delete entire meeting" trigger. Rendered as an
// absolutely-positioned overlay inside whichever cell is the row's
// current anchor (first visible cell on unmerged rows, the merged
// cell on merged rows) so it stays available regardless of which
// columns the user has hidden via the column customizer. The host
// <td> must carry `relative group` for the positioning + hover
// reveal to work, which every renderCell branch already does.
function DeleteRowButton({
  meetingId,
  isDeleting,
  onDelete,
  label,
}: {
  meetingId: number;
  isDeleting: boolean;
  onDelete: () => void;
  label: string;
}) {
  return (
    <button
      type="button"
      // Pinned to the cell's leading-top corner. The color picker (on
      // # cell only) sits at top-end, the merge trigger sits at
      // bottom-end, so the three row-level overlays don't collide.
      // Hover-reveal on desktop, ~40% on touch (same pattern as the
      // merge trigger and grip handle), focusable for keyboard users,
      // hidden in print. Stops propagation so the click never bubbles
      // into row drag or inline-edit affordances.
      onClick={(e) => {
        e.stopPropagation();
        onDelete();
      }}
      onPointerDown={(e) => e.stopPropagation()}
      disabled={isDeleting}
      className="absolute top-1 inline-start-1 p-1 rounded text-destructive/80 hover:text-destructive hover:bg-white/80 opacity-0 group-hover:opacity-100 focus:opacity-100 [@media(hover:none)_and_(pointer:coarse)]:opacity-40 [@media(hover:none)_and_(pointer:coarse)]:p-1.5 transition-opacity print:hidden disabled:opacity-50 disabled:cursor-not-allowed"
      aria-label={label}
      title={label}
      data-testid={`em-delete-row-${meetingId}`}
    >
      <Trash2 className="w-3.5 h-3.5 [@media(hover:none)_and_(pointer:coarse)]:w-4 [@media(hover:none)_and_(pointer:coarse)]:h-4" />
    </button>
  );
}

function MeetingRow({
  meeting,
  isRtl,
  t,
  visibleColumns,
  rowColorKey,
  onPickRowColor,
  canMutate,
  onSaveTitle,
  onSaveAttendeeName,
  onSaveTimes,
  onSaveMerge,
  onDeleteMeeting,
  isDeleting,
  isCurrent,
  highlightColor,
  reordering,
  autoEditTitle = false,
  onAutoEditTitleConsumed,
  pendingAttendee,
  onStartAddAttendee,
  onCommitAddAttendee,
  onCancelAddAttendee,
}: {
  meeting: Meeting;
  isRtl: boolean;
  t: (k: string) => string;
  visibleColumns: ColumnSetting[];
  rowColorKey: string;
  onPickRowColor: (key: string) => void;
  canMutate: boolean;
  onSaveTitle: (html: string) => Promise<void>;
  onSaveAttendeeName: (idx: number, html: string) => Promise<void>;
  onSaveTimes: (
    startTime: string | null,
    endTime: string | null,
  ) => Promise<void>;
  onSaveMerge: (
    merge:
      | { mergeStartColumn: ColumnId; mergeEndColumn: ColumnId; mergeText: string }
      | null,
  ) => Promise<void>;
  onDeleteMeeting: () => void;
  isDeleting: boolean;
  isCurrent: boolean;
  highlightColor: string;
  reordering: boolean;
  autoEditTitle?: boolean;
  onAutoEditTitleConsumed?: () => void;
  pendingAttendee?: {
    meetingId: number;
    attendanceType: Attendee["attendanceType"];
    key: number;
  } | null;
  onStartAddAttendee?: (
    meetingId: number,
    attendanceType: Attendee["attendanceType"],
  ) => void;
  onCommitAddAttendee?: (
    attendanceType: Attendee["attendanceType"],
    html: string,
  ) => Promise<void>;
  onCancelAddAttendee?: () => void;
}) {
  const title = isRtl ? meeting.titleAr : meeting.titleEn || meeting.titleAr;
  const isCancelled = meeting.status === "cancelled";
  const highlight = meeting.isHighlighted === 1;

  const rowColor = ROW_COLOR_OPTIONS.find((o) => o.key === rowColorKey);
  const rowBg = rowColor?.bg || "";

  // Make the row a sortable item. `attributes` get spread onto the <tr> for
  // a11y; `listeners` go on the dedicated grip handle in the # cell so a
  // normal click on the cell content (to enter inline-edit mode) is still
  // possible.
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: meeting.id, disabled: !canMutate });

  const numberCellCls =
    "border border-gray-300 px-2 py-2 text-center align-middle font-semibold " +
    (highlight || isCancelled
      ? "bg-red-600 text-white"
      : "bg-white text-[#0B1E3F]");

  // Always provide width + overflow:hidden inline. In `table-auto` mode the
  // browser treats width as a hint and shrinks cells to fit; in `table-fixed`
  // mode (xl screens AND any print) it enforces them. overflow:hidden + the
  // existing break-words on inner divs keeps wrapping behavior correct.
  const cellStyle = (col: ColumnSetting): CSSProperties => ({
    width: col.width,
    overflow: "hidden",
  });

  // Compute the cell-merge overlay for this row. We resolve the stored
  // start/end column ids against the *canonical* schedule column order
  // (not against `visibleColumns`) so that a merge whose start or end
  // column is currently hidden still renders across whichever columns
  // in the range remain visible. This guarantees a row that has a
  // merge in the DB never silently falls back to "unmerged" just
  // because the user toggled off one of the boundary columns.
  const canonStart = canonicalMergeIndex(meeting.mergeStartColumn);
  const canonEnd = canonicalMergeIndex(meeting.mergeEndColumn);
  const hasStoredMerge =
    canonStart >= 0 && canonEnd >= canonStart && typeof meeting.mergeText === "string";
  const visibleMergeIndices: number[] = [];
  if (hasStoredMerge) {
    visibleColumns.forEach((c, idx) => {
      const ci = canonicalMergeIndex(c.id);
      if (ci >= canonStart && ci <= canonEnd) visibleMergeIndices.push(idx);
    });
  }
  // Headers are themselves drag-sortable, so users can reorder the
  // schedule columns. If a previously-set merge ends up spanning
  // *non-contiguous* visible indices after a reorder, rendering one
  // colSpan cell at the first index would silently swallow the wrong
  // columns. In that case we degrade to "unmerged" rendering — the DB
  // merge state stays intact, and unmerging or restoring canonical
  // order brings the merged cell back.
  const visibleMergeContiguous =
    visibleMergeIndices.length === 0 ||
    visibleMergeIndices[visibleMergeIndices.length - 1] -
      visibleMergeIndices[0] +
      1 ===
      visibleMergeIndices.length;
  const isMerged =
    hasStoredMerge && visibleMergeIndices.length > 0 && visibleMergeContiguous;
  const mergeFirstIdx = isMerged ? visibleMergeIndices[0] : -1;
  const mergeSkipSet = isMerged ? new Set(visibleMergeIndices.slice(1)) : null;
  // First visible cell that's NOT part of a merge. The merge trigger
  // for an unmerged row is anchored here so editors can still open the
  // merge menu even when the # column is hidden. We look for the
  // first index in visibleColumns whose canonical id exists (which is
  // every schedule column), so this collapses to `0` in practice but
  // would skip non-mergeable columns if they're added later.
  const firstUnmergedIdx = isMerged ? -1 : visibleColumns.length > 0 ? 0 : -1;

  // Light tint for the current meeting on the editable cells (meeting,
  // attendees, time). The number cell keeps its solid white background
  // and original text colour so it stays legible — only the rest of the
  // row gets a soft wash of highlightColor. We keep the original text
  // colour everywhere so the row remains readable.
  const tintBg =
    isCurrent && !(highlight || isCancelled)
      ? hexToRgba(highlightColor, 0.13)
      : null;
  const tintedCellStyle = (col: ColumnSetting): CSSProperties =>
    tintBg ? { ...cellStyle(col), backgroundColor: tintBg } : cellStyle(col);

  const renderCell = (col: ColumnSetting, idx: number): ReactNode => {
    // Anchor the merge-menu trigger to whichever cell is the first
    // visible one on an unmerged row. Using "first visible cell"
    // (instead of always the # cell) keeps the trigger reachable when
    // editors hide the number column. The trigger is rendered as an
    // absolutely-positioned overlay inside the cell, so every cell type
    // needs `relative group` on its <td> for the overlay to find it.
    const showMergeTrigger = canMutate && !isMerged && idx === firstUnmergedIdx;
    const mergeOverlay = showMergeTrigger ? (
      <MergeMenu
        meetingId={meeting.id}
        // Offer Unmerge whenever the row has a stored merge in the DB,
        // even if we're in degraded (non-contiguous) rendering — that's
        // the only way the user can clear the stored state.
        hasStoredMerge={hasStoredMerge}
        existingMergeText={meeting.mergeText ?? null}
        t={t}
        onChange={onSaveMerge}
      />
    ) : null;
    // Delete trigger uses the same "first visible cell" anchoring as the
    // merge trigger so the action stays available even when editors hide
    // the # column via the column customizer. Only one button per row.
    // The merged-cell branch (in renderCells below) renders its own copy.
    const showDeleteTrigger = canMutate && !isMerged && idx === firstUnmergedIdx;
    const deleteOverlay = showDeleteTrigger ? (
      <DeleteRowButton
        meetingId={meeting.id}
        isDeleting={isDeleting}
        onDelete={onDeleteMeeting}
        label={t("executiveMeetings.schedule.deleteRow")}
      />
    ) : null;
    switch (col.id) {
      case "number":
        return (
          <td
            key="number"
            className={`relative group ${numberCellCls}`}
            style={
              tintBg
                ? {
                    ...cellStyle(col),
                    backgroundColor: highlightColor,
                    color: "white",
                  }
                : cellStyle(col)
            }
          >
            <div className="flex items-center justify-center gap-1">
              {canMutate && (
                <button
                  type="button"
                  // The grip handle is hover-only on devices that have a
                  // real hover (desktop mouse), but always visible at ~40%
                  // opacity on touch devices that have no hover state — so
                  // iPad users can actually find what to press to reorder
                  // a row. The tap target is also enlarged on touch with a
                  // small padding so the long-press lands reliably.
                  // touch-action:none stays on the handle itself so the
                  // browser hands touch events to dnd-kit instead of
                  // claiming them as a vertical scroll; the rest of the
                  // row keeps default touch-action so page scrolling works
                  // normally outside of the handle.
                  className="opacity-0 group-hover:opacity-70 hover:opacity-100 [@media(hover:none)_and_(pointer:coarse)]:opacity-40 [@media(hover:none)_and_(pointer:coarse)]:group-hover:opacity-40 [@media(hover:none)_and_(pointer:coarse)]:p-1.5 cursor-grab active:cursor-grabbing touch-none print:hidden"
                  aria-label={t("executiveMeetings.dragRow")}
                  data-testid={`em-row-grip-${meeting.id}`}
                  {...listeners}
                  {...attributes}
                >
                  <GripVertical className="w-3.5 h-3.5 [@media(hover:none)_and_(pointer:coarse)]:w-4 [@media(hover:none)_and_(pointer:coarse)]:h-4" />
                </button>
              )}
              <span>{meeting.dailyNumber}</span>
            </div>
            <RowColorPicker current={rowColorKey} onPick={onPickRowColor} t={t} />
            {deleteOverlay}
            {mergeOverlay}
          </td>
        );
      case "meeting":
        return (
          <td
            key="meeting"
            className="border border-gray-300 px-3 py-3 align-middle text-[#0B1E3F] relative group"
            style={tintedCellStyle(col)}
          >
            <EditableCell
              value={title}
              onSave={onSaveTitle}
              ariaLabel={t("executiveMeetings.editMeetingTitle")}
              dir={isRtl ? "rtl" : "ltr"}
              className="font-medium leading-snug break-words"
              disabled={!canMutate}
              testId={`em-edit-title-${meeting.id}`}
              startInEditMode={autoEditTitle}
              onStartedEditing={onAutoEditTitleConsumed}
            />
            {meeting.location && (
              <div className="text-[11px] text-muted-foreground mt-0.5 break-words">
                {meeting.location}
              </div>
            )}
            {deleteOverlay}
            {mergeOverlay}
          </td>
        );
      case "attendees":
        return (
          <td
            key="attendees"
            className="border border-gray-300 px-3 py-3 align-middle relative group"
            style={tintedCellStyle(col)}
          >
            <AttendeesCell
              meeting={meeting}
              t={t}
              canMutate={canMutate}
              onSaveAttendeeName={onSaveAttendeeName}
              pendingAttendee={pendingAttendee ?? null}
              onStartAddAttendee={onStartAddAttendee}
              onCommitAddAttendee={onCommitAddAttendee}
              onCancelAddAttendee={onCancelAddAttendee}
            />
            {deleteOverlay}
            {mergeOverlay}
          </td>
        );
      case "time":
        return (
          <td
            key="time"
            className="border border-gray-300 px-3 py-3 align-middle text-center relative group"
            style={tintedCellStyle(col)}
          >
            <TimeRangeCell
              meeting={meeting}
              t={t}
              lang={isRtl ? "ar" : "en"}
              canMutate={canMutate}
              onSaveTimes={onSaveTimes}
            />
            {deleteOverlay}
            {mergeOverlay}
          </td>
        );
    }
  };

  // When a merge is active we render a single <td colSpan=N> with an
  // EditableCell for `mergeText`, and skip the cells the merge covers
  // after the first. The merged cell hosts its own MergeMenu so users
  // can always unmerge or re-merge — even when the # column is hidden.
  // Originals stay in the DB so unmerge restores them.
  const renderCells = (): ReactNode[] => {
    if (!isMerged) {
      return visibleColumns.map((c, idx) => renderCell(c, idx));
    }
    const cells: ReactNode[] = [];
    const span = visibleMergeIndices.length;
    visibleColumns.forEach((col, idx) => {
      if (mergeSkipSet?.has(idx)) return;
      if (idx === mergeFirstIdx) {
        // Sum widths of the spanned visible columns so layout in
        // table-fixed mode reserves the right horizontal space.
        const totalWidth = visibleMergeIndices.reduce(
          (sum, i) => sum + (visibleColumns[i].width || 0),
          0,
        );
        cells.push(
          <td
            key="merge"
            colSpan={span}
            className="border border-gray-300 px-3 py-3 align-middle text-[#0B1E3F] relative group"
            style={{
              width: totalWidth,
              overflow: "hidden",
              ...(tintBg ? { backgroundColor: tintBg } : null),
            }}
            data-testid={`em-merge-cell-${meeting.id}`}
          >
            <EditableCell
              value={meeting.mergeText ?? ""}
              onSave={async (html) => {
                await onSaveMerge({
                  mergeStartColumn: meeting.mergeStartColumn as ColumnId,
                  mergeEndColumn: meeting.mergeEndColumn as ColumnId,
                  mergeText: html,
                });
              }}
              ariaLabel={t("executiveMeetings.merge.editLabel")}
              dir={isRtl ? "rtl" : "ltr"}
              className="font-medium leading-snug break-words"
              disabled={!canMutate}
              placeholder={t("executiveMeetings.merge.placeholder")}
              testId={`em-merge-edit-${meeting.id}`}
            />
            {canMutate && (
              <DeleteRowButton
                meetingId={meeting.id}
                isDeleting={isDeleting}
                onDelete={onDeleteMeeting}
                label={t("executiveMeetings.schedule.deleteRow")}
              />
            )}
            {canMutate && (
              <MergeMenu
                meetingId={meeting.id}
                isMerged
                hasStoredMerge
                existingMergeText={meeting.mergeText ?? null}
                t={t}
                onChange={onSaveMerge}
              />
            )}
          </td>,
        );
      } else {
        cells.push(renderCell(col, idx));
      }
    });
    return cells;
  };

  // Combine row background with highlight tint. When the row is the current
  // meeting we apply a soft tint (color at low opacity) plus a coloured ring
  // via box-shadow (rings on <tr> don't render across cells; box-shadow does).
  const baseBg = rowBg || "white";
  const trStyle: CSSProperties = {
    backgroundColor: baseBg,
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.5 : reordering ? 0.7 : 1,
    boxShadow: isCurrent ? `inset 0 0 0 2px ${highlightColor}` : undefined,
  };

  return (
    <tr
      ref={setNodeRef}
      className="group"
      style={trStyle}
      data-testid={`em-row-${meeting.id}`}
      data-current-meeting={isCurrent ? "true" : undefined}
    >
      {renderCells()}
    </tr>
  );
}

// Canonical schedule column order for cell-merge ranges. MUST stay in
// sync with the API's MERGE_COLUMNS array in
// artifacts/api-server/src/routes/executive-meetings.ts. We use this
// (instead of `visibleColumns`) when computing whether a stored merge
// overlaps the currently visible columns, so that hiding one boundary
// of the range still lets the merged cell render across whichever
// columns inside the range remain visible.
const CANONICAL_MERGE_ORDER: ColumnId[] = [
  "number",
  "meeting",
  "attendees",
  "time",
];
function canonicalMergeIndex(id: ColumnId | string | null | undefined): number {
  if (id == null) return -1;
  return CANONICAL_MERGE_ORDER.indexOf(id as ColumnId);
}

// Convert a #RRGGBB or #RGB hex color into an rgba() string with the
// given alpha. Used to derive the soft tint applied to the current
// meeting row from the user's highlight color preference. Falls back
// to the input string unchanged if it isn't a recognised hex.
function hexToRgba(hex: string, alpha: number): string {
  const m3 = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(hex);
  const m6 = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
  let r = 0;
  let g = 0;
  let b = 0;
  if (m6) {
    r = parseInt(m6[1], 16);
    g = parseInt(m6[2], 16);
    b = parseInt(m6[3], 16);
  } else if (m3) {
    r = parseInt(m3[1] + m3[1], 16);
    g = parseInt(m3[2] + m3[2], 16);
    b = parseInt(m3[3] + m3[3], 16);
  } else {
    return hex;
  }
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}

// Small popover-driven control attached to a row (in the # cell when
// not merged, or to the merged cell itself) that lets editors pick a
// merge range or clear an existing one. Each option corresponds to a
// fixed [start..end] column range.
function MergeMenu({
  meetingId,
  // True iff the row is rendered as a merged cell *right now*. Used
  // for testid wiring only.
  isMerged = false,
  // True iff the row has a stored merge in the DB (regardless of
  // whether it's currently renderable as merged). Drives the
  // "Unmerge" option so users can always clear stored merges, even
  // when the visible columns aren't contiguous.
  hasStoredMerge,
  // Existing merge text from the DB, preserved when the user picks a
  // new merge range so we never silently overwrite their content.
  existingMergeText,
  t,
  onChange,
}: {
  meetingId: number;
  isMerged?: boolean;
  hasStoredMerge: boolean;
  existingMergeText: string | null;
  t: (k: string) => string;
  onChange: (
    merge:
      | { mergeStartColumn: ColumnId; mergeEndColumn: ColumnId; mergeText: string }
      | null,
  ) => Promise<void>;
}) {
  // Re-merging keeps the existing text so users don't accidentally lose
  // what they had typed. New merges (no stored text) start empty.
  const apply = async (start: ColumnId, end: ColumnId) => {
    await onChange({
      mergeStartColumn: start,
      mergeEndColumn: end,
      mergeText: existingMergeText ?? "",
    });
  };
  return (
    <Popover>
      <PopoverTrigger asChild>
        <button
          type="button"
          className="absolute bottom-1 inline-end-1 p-1 rounded text-muted-foreground hover:text-[#0B1E3F] hover:bg-white/70 opacity-0 group-hover:opacity-100 focus:opacity-100 [@media(hover:none)_and_(pointer:coarse)]:opacity-40 [@media(hover:none)_and_(pointer:coarse)]:p-1.5 transition-opacity print:hidden"
          aria-label={t("executiveMeetings.merge.label")}
          data-testid={`em-merge-trigger-${meetingId}`}
        >
          <Combine className="w-3.5 h-3.5" />
        </button>
      </PopoverTrigger>
      <PopoverContent className="w-56 p-1" align="end">
        <button
          type="button"
          className="w-full text-start px-2 py-1.5 text-sm rounded hover:bg-accent"
          data-testid={`em-merge-opt-meeting-attendees-${meetingId}`}
          onClick={() => apply("meeting", "attendees")}
        >
          {t("executiveMeetings.merge.meetingAttendees")}
        </button>
        <button
          type="button"
          className="w-full text-start px-2 py-1.5 text-sm rounded hover:bg-accent"
          data-testid={`em-merge-opt-meeting-time-${meetingId}`}
          onClick={() => apply("meeting", "time")}
        >
          {t("executiveMeetings.merge.meetingTime")}
        </button>
        <button
          type="button"
          className="w-full text-start px-2 py-1.5 text-sm rounded hover:bg-accent"
          data-testid={`em-merge-opt-all-${meetingId}`}
          onClick={() => apply("number", "time")}
        >
          {t("executiveMeetings.merge.all")}
        </button>
        {hasStoredMerge && (
          <button
            type="button"
            className="w-full text-start px-2 py-1.5 text-sm rounded hover:bg-accent text-destructive"
            data-testid={`em-merge-opt-unmerge-${meetingId}`}
            onClick={() => onChange(null)}
          >
            {t("executiveMeetings.merge.unmerge")}
          </button>
        )}
      </PopoverContent>
    </Popover>
  );
}

function TimeRangeCell({
  meeting,
  t,
  lang,
  canMutate,
  onSaveTimes,
}: {
  meeting: Meeting;
  t: (k: string) => string;
  lang: "ar" | "en";
  canMutate: boolean;
  onSaveTimes: (
    startTime: string | null,
    endTime: string | null,
  ) => Promise<void>;
}) {
  const { toast } = useToast();
  const startSaved = meeting.startTime ? meeting.startTime.slice(0, 5) : "";
  const endSaved = meeting.endTime ? meeting.endTime.slice(0, 5) : "";

  const [editing, setEditing] = useState(false);
  const [start, setStart] = useState(startSaved);
  const [end, setEnd] = useState(endSaved);
  const wrapperRef = useRef<HTMLDivElement | null>(null);
  const blurTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const startInputRef = useRef<HTMLInputElement | null>(null);

  // If the saved times change while we're not editing (e.g. another tab
  // updated them, or the user picked a different date), sync local state.
  useEffect(() => {
    if (!editing) {
      setStart(startSaved);
      setEnd(endSaved);
    }
  }, [editing, startSaved, endSaved]);

  useEffect(() => {
    if (editing) {
      startInputRef.current?.focus();
    }
  }, [editing]);

  const enterEdit = useCallback(() => {
    if (!canMutate) return;
    setEditing(true);
  }, [canMutate]);

  const cancel = useCallback(() => {
    setStart(startSaved);
    setEnd(endSaved);
    setEditing(false);
  }, [startSaved, endSaved]);

  const save = useCallback(async () => {
    const newStart = start || null;
    const newEnd = end || null;
    if (newStart === (startSaved || null) && newEnd === (endSaved || null)) {
      setEditing(false);
      return;
    }
    if (newStart && newEnd && newStart > newEnd) {
      // Surface the validation error and keep the user in edit mode so they
      // can correct it without losing the values they just typed.
      toast({
        title: t("executiveMeetings.schedule.timeOrderError"),
        variant: "destructive",
      });
      startInputRef.current?.focus();
      return;
    }
    setEditing(false);
    try {
      await onSaveTimes(newStart, newEnd);
    } catch {
      // The parent `saveTimes` handler is responsible for surfacing the
      // error toast; we only need to roll back the local draft state so
      // the cell shows the previously-saved values.
      setStart(startSaved);
      setEnd(endSaved);
    }
  }, [start, end, startSaved, endSaved, onSaveTimes]);

  const onFocusOut = (e: React.FocusEvent<HTMLDivElement>) => {
    if (!editing) return;
    const next = e.relatedTarget as Node | null;
    if (next && wrapperRef.current?.contains(next)) return;
    if (blurTimerRef.current) clearTimeout(blurTimerRef.current);
    blurTimerRef.current = setTimeout(() => {
      void save();
    }, 150);
  };

  const onFocusIn = () => {
    if (blurTimerRef.current) {
      clearTimeout(blurTimerRef.current);
      blurTimerRef.current = null;
    }
  };

  if (!editing) {
    const hasAny = !!(meeting.startTime || meeting.endTime);
    const display = hasAny
      ? `${formatTime(meeting.startTime, lang)} – ${formatTime(meeting.endTime, lang)}`
      : "—";
    return (
      <div
        role={canMutate ? "button" : undefined}
        tabIndex={canMutate ? 0 : -1}
        aria-label={t("executiveMeetings.col.time")}
        className={
          "font-mono text-[#0B1E3F] whitespace-nowrap leading-tight " +
          (canMutate
            ? "cursor-text hover:bg-blue-50/50 focus:bg-blue-50/50 focus:outline-none rounded transition-colors px-1"
            : "")
        }
        onClick={enterEdit}
        onKeyDown={(e) => {
          if (e.key === "Enter" || e.key === " ") {
            e.preventDefault();
            enterEdit();
          }
        }}
        data-testid={`em-time-${meeting.id}`}
      >
        {display}
      </div>
    );
  }

  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Escape") {
      e.preventDefault();
      cancel();
    } else if (e.key === "Enter") {
      e.preventDefault();
      void save();
    }
  };

  return (
    <div
      ref={wrapperRef}
      onFocus={onFocusIn}
      onBlur={onFocusOut}
      className="flex items-center justify-center gap-1 font-mono whitespace-nowrap"
      data-testid={`em-time-${meeting.id}`}
      data-editing="true"
    >
      <input
        ref={startInputRef}
        type="time"
        value={start}
        onChange={(e) => setStart(e.target.value)}
        onKeyDown={onKeyDown}
        aria-label={t("executiveMeetings.schedule.timeStart")}
        className="w-[5rem] border border-blue-400 rounded px-1 py-0.5 text-xs bg-white"
        data-testid={`em-time-start-${meeting.id}`}
      />
      <span className="text-muted-foreground">–</span>
      <input
        type="time"
        value={end}
        onChange={(e) => setEnd(e.target.value)}
        onKeyDown={onKeyDown}
        aria-label={t("executiveMeetings.schedule.timeEnd")}
        className="w-[5rem] border border-blue-400 rounded px-1 py-0.5 text-xs bg-white"
        data-testid={`em-time-end-${meeting.id}`}
      />
      <button
        type="button"
        onMouseDown={(e) => e.preventDefault()}
        onClick={() => void save()}
        aria-label={t("common.save")}
        className="ms-0.5 p-0.5 rounded text-green-700 hover:bg-green-50 focus:bg-green-50 focus:outline-none"
        data-testid={`em-time-save-${meeting.id}`}
      >
        <Check className="w-3.5 h-3.5" />
      </button>
      <button
        type="button"
        onMouseDown={(e) => e.preventDefault()}
        onClick={cancel}
        aria-label={t("common.cancel")}
        className="p-0.5 rounded text-red-700 hover:bg-red-50 focus:bg-red-50 focus:outline-none"
        data-testid={`em-time-cancel-${meeting.id}`}
      >
        <X className="w-3.5 h-3.5" />
      </button>
    </div>
  );
}

function AttendeesCell({
  meeting,
  t,
  canMutate = false,
  onSaveAttendeeName,
  pendingAttendee = null,
  onStartAddAttendee,
  onCommitAddAttendee,
  onCancelAddAttendee,
}: {
  meeting: Meeting;
  t: (k: string) => string;
  canMutate?: boolean;
  onSaveAttendeeName?: (idx: number, html: string) => Promise<void>;
  pendingAttendee?: {
    meetingId: number;
    attendanceType: Attendee["attendanceType"];
    key: number;
  } | null;
  onStartAddAttendee?: (
    meetingId: number,
    attendanceType: Attendee["attendanceType"],
  ) => void;
  onCommitAddAttendee?: (
    attendanceType: Attendee["attendanceType"],
    html: string,
  ) => Promise<void>;
  onCancelAddAttendee?: () => void;
}) {
  // Build (attendee, originalIndex) tuples so attendees can be sliced into
  // virtual/internal/external groups while still referring back to their
  // index in `meeting.attendees` (needed by the PUT save endpoint).
  const tagged = meeting.attendees.map((a, i) => ({ a, i }));
  const virtual = tagged.filter(({ a }) => a.attendanceType === "virtual");
  const internal = tagged.filter(({ a }) => a.attendanceType === "internal");
  const external = tagged.filter(({ a }) => a.attendanceType === "external");
  const hasSplit =
    virtual.length > 0 && (internal.length > 0 || external.length > 0);

  const platformLabel: Record<Meeting["platform"], string> = {
    none: "",
    webex: "Webex",
    teams: "Teams",
    zoom: "Zoom",
    other: t("executiveMeetings.virtual"),
  };

  // pendingAttendee is GLOBAL — it's the single ghost row open anywhere on
  // the page. So we must distinguish:
  //  * "is the ghost in THIS row, in THIS group?" — controls whether to
  //    render the ghost EditableCell here.
  //  * "is a ghost open ANYWHERE on the page?" — controls whether to hide
  //    the "+" buttons in this row, so the user can't orphan their typing
  //    by clicking "+" in a different row while one is already open.
  const isPendingForThisMeeting =
    !!pendingAttendee && pendingAttendee.meetingId === meeting.id;
  const isPendingFor = (type: Attendee["attendanceType"]) =>
    isPendingForThisMeeting && pendingAttendee!.attendanceType === type;
  const hasAnyPending = !!pendingAttendee;

  const startAdd = onStartAddAttendee
    ? (type: Attendee["attendanceType"]) =>
        onStartAddAttendee(meeting.id, type)
    : undefined;

  const commitAdd = onCommitAddAttendee;
  const cancelAdd = onCancelAddAttendee;

  const flowProps = {
    canMutate,
    onSaveAttendeeName,
    isPending: false,
    pendingKey: pendingAttendee?.key ?? 0,
    onStartAdd: hasAnyPending ? undefined : startAdd,
    onCommitAdd: commitAdd,
    onCancelAdd: cancelAdd,
    addLabel: t("executiveMeetings.schedule.addAttendee"),
    editAriaLabel: t("executiveMeetings.schedule.editAttendeeAria"),
    newAriaLabel: t("executiveMeetings.schedule.newAttendeeAria"),
    meetingId: meeting.id,
  };

  if (hasSplit) {
    return (
      <div className="space-y-2">
        {(virtual.length > 0 || isPendingFor("virtual")) && (
          <AttendeeGroup
            label={
              meeting.platform !== "none"
                ? `${t("executiveMeetings.virtualAttendance")} — ${platformLabel[meeting.platform]}`
                : t("executiveMeetings.virtualAttendance")
            }
            items={virtual}
            addType="virtual"
            {...flowProps}
            isPending={isPendingFor("virtual")}
          />
        )}
        {(internal.length > 0 || isPendingFor("internal")) && (
          <AttendeeGroup
            label={t("executiveMeetings.internalAttendance")}
            items={internal}
            addType="internal"
            {...flowProps}
            isPending={isPendingFor("internal")}
          />
        )}
        {(external.length > 0 || isPendingFor("external")) && (
          <AttendeeGroup
            label={t("executiveMeetings.externalAttendance")}
            items={external}
            addType="external"
            {...flowProps}
            isPending={isPendingFor("external")}
          />
        )}
        {/* Add chips for groups that don't yet exist for this meeting. */}
        {canMutate && !hasAnyPending && startAdd && (
          <div className="flex flex-wrap justify-center gap-1.5 print:hidden text-xs">
            {virtual.length === 0 && (
              <AddAttendeeChip
                onClick={() => startAdd("virtual")}
                label={t("executiveMeetings.schedule.addVirtualAttendee")}
                testId={`em-add-attendee-virtual-${meeting.id}`}
              />
            )}
            {internal.length === 0 && (
              <AddAttendeeChip
                onClick={() => startAdd("internal")}
                label={t("executiveMeetings.schedule.addInternalAttendee")}
                testId={`em-add-attendee-internal-${meeting.id}`}
              />
            )}
            {external.length === 0 && (
              <AddAttendeeChip
                onClick={() => startAdd("external")}
                label={t("executiveMeetings.schedule.addExternalAttendee")}
                testId={`em-add-attendee-external-${meeting.id}`}
              />
            )}
          </div>
        )}
      </div>
    );
  }

  // Single-group cases: meeting.platform-only virtual list, all-internal,
  // all-external, or empty. Pick a sensible default add type so the user
  // can add a first attendee without reaching for the request modal.
  const singleGroupType: Attendee["attendanceType"] =
    meeting.platform !== "none" && virtual.length > 0
      ? "virtual"
      : tagged[0]?.a.attendanceType ?? "internal";

  if (meeting.platform !== "none" && virtual.length > 0) {
    return (
      <AttendeeGroup
        label={`${t("executiveMeetings.virtualAttendance")} — ${platformLabel[meeting.platform]}`}
        items={virtual}
        addType="virtual"
        {...flowProps}
        isPending={isPendingFor("virtual")}
      />
    );
  }

  // Empty cell or single-type cell — render flow without a header.
  return (
    <AttendeeFlow
      items={tagged}
      addType={singleGroupType}
      {...flowProps}
      isPending={isPendingFor(singleGroupType)}
    />
  );
}

type AttendeeWithIndex = { a: Attendee; i: number };

function AddAttendeeChip({
  onClick,
  label,
  testId,
}: {
  onClick: () => void;
  label: string;
  testId: string;
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      data-testid={testId}
      className="inline-flex items-center rounded-full border border-dashed border-blue-400 px-2.5 py-1 min-h-[1.5rem] text-xs text-blue-700 hover:bg-blue-50 hover:border-blue-500 transition-colors"
    >
      {label}
    </button>
  );
}

type AttendeeFlowSharedProps = {
  canMutate?: boolean;
  onSaveAttendeeName?: (idx: number, html: string) => Promise<void>;
  addType: Attendee["attendanceType"];
  isPending: boolean;
  pendingKey: number;
  onStartAdd?: (type: Attendee["attendanceType"]) => void;
  onCommitAdd?: (
    type: Attendee["attendanceType"],
    html: string,
  ) => Promise<void>;
  onCancelAdd?: () => void;
  addLabel: string;
  editAriaLabel: string;
  newAriaLabel: string;
  meetingId: number;
};

function AttendeeGroup({
  label,
  items,
  ...flowProps
}: { label: string; items: AttendeeWithIndex[] } & AttendeeFlowSharedProps) {
  return (
    <div>
      {/* The group label is purely informational — make it pointer-events:none
          so a near-miss tap on the label still falls through to whatever is
          underneath (e.g. an attendee name or the cell background). */}
      <div className="text-xs font-semibold text-[#0B1E3F] mb-0.5 text-center pointer-events-none select-none">
        — {label} —
      </div>
      <AttendeeFlow items={items} {...flowProps} />
    </div>
  );
}

function AttendeeFlow({
  items,
  canMutate = false,
  onSaveAttendeeName,
  addType,
  isPending,
  pendingKey,
  onStartAdd,
  onCommitAdd,
  onCancelAdd,
  addLabel,
  editAriaLabel,
  newAriaLabel,
}: { items: AttendeeWithIndex[] } & AttendeeFlowSharedProps) {
  const editable = canMutate && !!onSaveAttendeeName;
  const showAdd = canMutate && !!onStartAdd && !isPending;

  if (items.length === 0 && !showAdd && !isPending) {
    return <span className="text-xs text-gray-500">—</span>;
  }

  return (
    <ul className="flex flex-wrap justify-center items-baseline gap-x-4 gap-y-0.5 text-[#0B1E3F] leading-snug">
      {items.map(({ a, i }, displayIdx) => (
        <li
          key={a.id ?? i}
          className={editable ? "min-w-[3rem]" : "whitespace-nowrap"}
        >
          <span className="text-gray-500 me-1">{displayIdx + 1}-</span>
          {editable ? (
            <EditableCell
              value={a.name}
              onSave={(html) => onSaveAttendeeName!(i, html)}
              ariaLabel={editAriaLabel}
              dir="auto"
              placeholder="…"
              // Always-visible dashed underline gives each attendee name a
              // discoverable click target even when names wrap onto multiple
              // lines. The vertical padding + min-height widen the tap
              // target so a near-miss tap on an iPad/touch device still
              // lands on the editor. Hidden in edit mode (the editor draws
              // its own border) and on print.
              className="inline-block align-baseline min-w-[3rem] min-h-[1.5rem] px-0.5 py-0.5 border-b border-dashed border-gray-400/70 hover:border-blue-500 data-[editing=true]:border-b-0 print:border-b-0 print:py-0 print:min-h-0"
              testId={`em-edit-attendee-${i}`}
            />
          ) : (
            <span dangerouslySetInnerHTML={{ __html: safeHtml(a.name) }} />
          )}
        </li>
      ))}
      {isPending && onCommitAdd && (
        <li className="min-w-[6rem]" data-testid={`em-add-attendee-pending-${addType}`}>
          <span className="text-gray-500 me-1">{items.length + 1}-</span>
          <EditableCell
            // pendingKey changes every time the user clicks "+" so React
            // remounts the editor with a fresh focus.
            key={`pending-${pendingKey}`}
            value=""
            onSave={(html) => onCommitAdd(addType, html)}
            ariaLabel={newAriaLabel}
            dir="auto"
            placeholder="…"
            className="inline-block align-baseline min-w-[6rem] min-h-[1.5rem] px-0.5 py-0.5 border-b border-dashed border-blue-500 data-[editing=true]:border-b-0 print:border-b-0 print:py-0 print:min-h-0"
            testId={`em-add-attendee-pending-input-${addType}`}
            startInEditMode
            // Force a save call on blur even when the user typed nothing,
            // so the parent can clear the pending ghost row.
            forceSaveOnBlur
            // Escape should also clear the ghost.
            onCancel={onCancelAdd}
          />
        </li>
      )}
      {showAdd && (
        <li className="print:hidden">
          <button
            type="button"
            onClick={() => onStartAdd!(addType)}
            data-testid={`em-add-attendee-${addType}`}
            aria-label={addLabel}
            className="text-xs text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded px-2 py-1 min-h-[1.5rem] border border-dashed border-blue-300 hover:border-blue-500 transition-colors"
          >
            {addLabel}
          </button>
        </li>
      )}
    </ul>
  );
}

// ============ MANAGE ============

type MeetingFormState = {
  id: number | null;
  titleAr: string;
  titleEn: string;
  meetingDate: string;
  dailyNumber: string;
  startTime: string;
  endTime: string;
  location: string;
  meetingUrl: string;
  platform: Meeting["platform"];
  status: string;
  isHighlighted: boolean;
  notes: string;
  attendees: Attendee[];
};

function emptyMeetingForm(date: string): MeetingFormState {
  return {
    id: null,
    titleAr: "",
    titleEn: "",
    meetingDate: date,
    dailyNumber: "",
    startTime: "",
    endTime: "",
    location: "",
    meetingUrl: "",
    platform: "none",
    status: "scheduled",
    isHighlighted: false,
    notes: "",
    attendees: [],
  };
}

function ManageSection({
  date,
  onDateChange,
  canMutate,
  isRtl,
  t,
}: {
  date: string;
  onDateChange: (d: string) => void;
  canMutate: boolean;
  isRtl: boolean;
  t: (k: string) => string;
}) {
  const qc = useQueryClient();
  const { toast } = useToast();
  const [editing, setEditing] = useState<MeetingFormState | null>(null);
  const [saving, setSaving] = useState(false);
  const [deletingId, setDeletingId] = useState<number | null>(null);
  const [duplicating, setDuplicating] = useState<{
    id: number;
    targetDate: string;
  } | null>(null);
  const [duplicatingBusy, setDuplicatingBusy] = useState(false);

  const { data, isLoading } = useQuery<DayResponse>({
    queryKey: ["/api/executive-meetings", date],
    queryFn: () => apiJson<DayResponse>(`/api/executive-meetings?date=${date}`),
  });
  const meetings = data?.meetings ?? [];

  function openCreate() {
    setEditing(emptyMeetingForm(date));
  }
  function openDuplicate(m: Meeting) {
    setDuplicating({ id: m.id, targetDate: date });
  }
  async function performDuplicate() {
    if (!duplicating) return;
    setDuplicatingBusy(true);
    try {
      await apiJson(`/api/executive-meetings/${duplicating.id}/duplicate`, {
        method: "POST",
        body: { targetDate: duplicating.targetDate },
      });
      toast({ title: t("executiveMeetings.manage.duplicated") });
      const newDate = duplicating.targetDate;
      setDuplicating(null);
      if (newDate === date) {
        await qc.invalidateQueries({ queryKey: ["/api/executive-meetings", date] });
      } else {
        onDateChange(newDate);
      }
    } catch (err) {
      toast({
        title: t("executiveMeetings.manage.duplicateFailed"),
        description: err instanceof Error ? err.message : String(err),
        variant: "destructive",
      });
    } finally {
      setDuplicatingBusy(false);
    }
  }
  function openEdit(m: Meeting) {
    setEditing({
      id: m.id,
      titleAr: m.titleAr,
      titleEn: m.titleEn ?? "",
      meetingDate: m.meetingDate,
      dailyNumber: String(m.dailyNumber),
      startTime: m.startTime ? m.startTime.slice(0, 5) : "",
      endTime: m.endTime ? m.endTime.slice(0, 5) : "",
      location: m.location ?? "",
      meetingUrl: m.meetingUrl ?? "",
      platform: m.platform,
      status: m.status,
      isHighlighted: m.isHighlighted === 1,
      notes: m.notes ?? "",
      attendees: m.attendees.map((a) => ({ ...a })),
    });
  }

  async function save() {
    if (!editing) return;
    if (!editing.titleAr.trim() || !editing.titleEn.trim()) {
      toast({
        title: t("executiveMeetings.manage.errors.titleRequired"),
        variant: "destructive",
      });
      return;
    }
    setSaving(true);
    const body: Record<string, unknown> = {
      titleAr: editing.titleAr.trim(),
      titleEn: editing.titleEn.trim(),
      meetingDate: editing.meetingDate,
      startTime: editing.startTime || null,
      endTime: editing.endTime || null,
      location: editing.location.trim() || null,
      meetingUrl: editing.meetingUrl.trim() || null,
      platform: editing.platform,
      status: editing.status,
      isHighlighted: editing.isHighlighted ? 1 : 0,
      notes: editing.notes.trim() || null,
      attendees: editing.attendees.map((a, idx) => ({
        name: a.name,
        title: a.title,
        attendanceType: a.attendanceType,
        sortOrder: idx,
      })),
    };
    if (editing.dailyNumber.trim()) body.dailyNumber = Number(editing.dailyNumber);
    try {
      if (editing.id == null) {
        await apiJson(`/api/executive-meetings`, { method: "POST", body });
      } else {
        await apiJson(`/api/executive-meetings/${editing.id}`, {
          method: "PATCH",
          body,
        });
      }
      toast({ title: t("executiveMeetings.common.saved") });
      setEditing(null);
      await qc.invalidateQueries({ queryKey: ["/api/executive-meetings", date] });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({
        title: t("executiveMeetings.manage.errors.saveFailed"),
        description: msg,
        variant: "destructive",
      });
    } finally {
      setSaving(false);
    }
  }

  async function confirmDelete(id: number) {
    if (!window.confirm(t("executiveMeetings.manage.deleteConfirm"))) return;
    setDeletingId(id);
    try {
      await apiJson(`/api/executive-meetings/${id}`, { method: "DELETE" });
      await qc.invalidateQueries({ queryKey: ["/api/executive-meetings", date] });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({ title: msg, variant: "destructive" });
    } finally {
      setDeletingId(null);
    }
  }

  if (!canMutate) {
    return <NoPermission t={t} />;
  }

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between gap-3 flex-wrap">
        <h2 className="text-xl sm:text-2xl font-bold text-[#0B1E3F]">
          {t("executiveMeetings.manage.heading")}
        </h2>
        <div className="flex items-center gap-2">
          <label className="flex items-center gap-2 text-sm">
            <span className="text-muted-foreground">{t("executiveMeetings.date")}</span>
            <input
              type="date"
              value={date}
              onChange={(e) => onDateChange(e.target.value)}
              className="border border-gray-300 rounded px-2 py-1 text-sm bg-white"
            />
          </label>
          <Button onClick={openCreate} className="bg-[#0B1E3F] gap-1" data-testid="em-add-meeting">
            <Plus className="w-4 h-4" />
            {t("executiveMeetings.manage.addMeeting")}
          </Button>
        </div>
      </div>

      <div className="bg-white border border-gray-200 rounded-md overflow-hidden">
        <table className="w-full text-sm" dir={isRtl ? "rtl" : "ltr"}>
          <thead className="bg-gray-50 text-[#0B1E3F]">
            <tr>
              <th className="px-3 py-2 text-start w-12">#</th>
              <th className="px-3 py-2 text-start">{t("executiveMeetings.col.meeting")}</th>
              <th className="px-3 py-2 text-start w-28">{t("executiveMeetings.col.time")}</th>
              <th className="px-3 py-2 text-start w-24">{t("executiveMeetings.manage.field.status")}</th>
              <th className="px-3 py-2 text-end w-32">{t("executiveMeetings.common.actions")}</th>
            </tr>
          </thead>
          <tbody>
            {isLoading && (
              <tr>
                <td colSpan={5} className="py-8 text-center text-gray-500">
                  {t("common.loading")}
                </td>
              </tr>
            )}
            {!isLoading && meetings.length === 0 && (
              <tr>
                <td colSpan={5} className="py-8 text-center text-gray-500">
                  {t("executiveMeetings.manage.noMeetings")}
                </td>
              </tr>
            )}
            {meetings.map((m) => (
              <tr key={m.id} className="border-t border-gray-100">
                <td className="px-3 py-2 align-middle">{m.dailyNumber}</td>
                <td className="px-3 py-2 align-middle">
                  <div className="font-medium text-[#0B1E3F]">
                    {isRtl ? m.titleAr : m.titleEn || m.titleAr}
                  </div>
                  <div className="text-xs text-gray-500">
                    {m.attendees.length} · {m.platform}
                  </div>
                </td>
                <td className="px-3 py-2 align-middle font-mono text-xs whitespace-nowrap">
                  {formatTime(m.startTime, isRtl ? "ar" : "en")} — {formatTime(m.endTime, isRtl ? "ar" : "en")}
                </td>
                <td className="px-3 py-2 align-middle text-xs">
                  {t(`executiveMeetings.manage.status.${m.status}`)}
                </td>
                <td className="px-3 py-2 align-middle text-end">
                  <div className="inline-flex gap-1">
                    <Button
                      size="sm"
                      variant="ghost"
                      onClick={() => openEdit(m)}
                      data-testid={`em-edit-${m.id}`}
                    >
                      <Pencil className="w-4 h-4" />
                    </Button>
                    <Button
                      size="sm"
                      variant="ghost"
                      onClick={() => openDuplicate(m)}
                      title={t("executiveMeetings.manage.duplicateToDate")}
                      data-testid={`em-duplicate-${m.id}`}
                    >
                      <Copy className="w-4 h-4" />
                    </Button>
                    <Button
                      size="sm"
                      variant="ghost"
                      onClick={() => confirmDelete(m.id)}
                      disabled={deletingId === m.id}
                      data-testid={`em-delete-${m.id}`}
                    >
                      <Trash2 className="w-4 h-4 text-red-600" />
                    </Button>
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {editing && (
        <MeetingFormDialog
          state={editing}
          onChange={setEditing}
          onSave={save}
          onClose={() => setEditing(null)}
          saving={saving}
          t={t}
        />
      )}

      <Dialog
        open={duplicating !== null}
        onOpenChange={(o) => !o && setDuplicating(null)}
      >
        <DialogContent>
          <DialogHeader>
            <DialogTitle>
              {t("executiveMeetings.manage.duplicateToDate")}
            </DialogTitle>
          </DialogHeader>
          <FormRow label={t("executiveMeetings.manage.field.meetingDate")}>
            <Input
              type="date"
              value={duplicating?.targetDate ?? ""}
              onChange={(e) =>
                setDuplicating(
                  duplicating
                    ? { ...duplicating, targetDate: e.target.value }
                    : null,
                )
              }
              data-testid="em-duplicate-date"
            />
          </FormRow>
          <DialogFooter>
            <Button
              variant="ghost"
              onClick={() => setDuplicating(null)}
              disabled={duplicatingBusy}
            >
              {t("executiveMeetings.common.cancel")}
            </Button>
            <Button
              onClick={performDuplicate}
              disabled={duplicatingBusy || !duplicating?.targetDate}
              className="bg-[#0B1E3F]"
              data-testid="em-duplicate-confirm"
            >
              {t("executiveMeetings.manage.duplicate")}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </div>
  );
}

function MeetingFormDialog({
  state,
  onChange,
  onSave,
  onClose,
  saving,
  t,
}: {
  state: MeetingFormState;
  onChange: (s: MeetingFormState) => void;
  onSave: () => void;
  onClose: () => void;
  saving: boolean;
  t: (k: string) => string;
}) {
  function update<K extends keyof MeetingFormState>(k: K, v: MeetingFormState[K]) {
    onChange({ ...state, [k]: v });
  }
  function addAttendee() {
    onChange({
      ...state,
      attendees: [
        ...state.attendees,
        { name: "", title: null, attendanceType: "internal", sortOrder: state.attendees.length },
      ],
    });
  }
  function removeAttendee(i: number) {
    const arr = state.attendees.slice();
    arr.splice(i, 1);
    onChange({ ...state, attendees: arr });
  }
  function setAttendee(i: number, patch: Partial<Attendee>) {
    const arr = state.attendees.slice();
    arr[i] = { ...arr[i], ...patch } as Attendee;
    onChange({ ...state, attendees: arr });
  }
  function moveAttendee(i: number, dir: -1 | 1) {
    const j = i + dir;
    if (j < 0 || j >= state.attendees.length) return;
    const arr = state.attendees.slice();
    [arr[i], arr[j]] = [arr[j], arr[i]];
    onChange({ ...state, attendees: arr });
  }

  return (
    <Dialog open={true} onOpenChange={(o) => !o && onClose()}>
      <DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
        <DialogHeader>
          <DialogTitle>
            {state.id == null
              ? t("executiveMeetings.manage.addMeeting")
              : t("executiveMeetings.manage.editMeeting")}
          </DialogTitle>
        </DialogHeader>
        <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
          <FormRow label={t("executiveMeetings.manage.field.titleAr")}>
            <Input
              value={state.titleAr}
              onChange={(e) => update("titleAr", e.target.value)}
              data-testid="em-form-titleAr"
            />
          </FormRow>
          <FormRow label={t("executiveMeetings.manage.field.titleEn")}>
            <Input value={state.titleEn} onChange={(e) => update("titleEn", e.target.value)} />
          </FormRow>
          <FormRow label={t("executiveMeetings.manage.field.meetingDate")}>
            <Input
              type="date"
              value={state.meetingDate}
              onChange={(e) => update("meetingDate", e.target.value)}
            />
          </FormRow>
          <FormRow
            label={t("executiveMeetings.manage.field.dailyNumber")}
            hint={t("executiveMeetings.manage.field.dailyNumberHint")}
          >
            <Input
              type="number"
              min={1}
              value={state.dailyNumber}
              onChange={(e) => update("dailyNumber", e.target.value)}
            />
          </FormRow>
          <FormRow label={t("executiveMeetings.manage.field.startTime")}>
            <Input
              type="time"
              value={state.startTime}
              onChange={(e) => update("startTime", e.target.value)}
            />
          </FormRow>
          <FormRow label={t("executiveMeetings.manage.field.endTime")}>
            <Input
              type="time"
              value={state.endTime}
              onChange={(e) => update("endTime", e.target.value)}
            />
          </FormRow>
          <FormRow label={t("executiveMeetings.manage.field.location")}>
            <Input value={state.location} onChange={(e) => update("location", e.target.value)} />
          </FormRow>
          <FormRow label={t("executiveMeetings.manage.field.meetingUrl")}>
            <Input
              value={state.meetingUrl}
              onChange={(e) => update("meetingUrl", e.target.value)}
            />
          </FormRow>
          <FormRow label={t("executiveMeetings.manage.field.platform")}>
            <Select
              value={state.platform}
              onValueChange={(v) => update("platform", v as Meeting["platform"])}
            >
              <SelectTrigger>
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                {(["none", "webex", "teams", "zoom", "other"] as const).map((p) => (
                  <SelectItem key={p} value={p}>
                    {t(`executiveMeetings.manage.platform.${p}`)}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </FormRow>
          <FormRow label={t("executiveMeetings.manage.field.status")}>
            <Select value={state.status} onValueChange={(v) => update("status", v)}>
              <SelectTrigger>
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                {(["scheduled", "cancelled", "completed", "postponed"] as const).map((s) => (
                  <SelectItem key={s} value={s}>
                    {t(`executiveMeetings.manage.status.${s}`)}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </FormRow>
          <FormRow label={t("executiveMeetings.manage.field.isHighlighted")}>
            <Switch
              checked={state.isHighlighted}
              onCheckedChange={(v) => update("isHighlighted", v)}
            />
          </FormRow>
          <FormRow label={t("executiveMeetings.manage.field.notes")} full>
            <Textarea
              value={state.notes}
              onChange={(e) => update("notes", e.target.value)}
              rows={3}
            />
          </FormRow>
        </div>

        <div className="border-t border-gray-200 pt-3 mt-1">
          <div className="flex items-center justify-between mb-2">
            <h4 className="font-semibold text-sm text-[#0B1E3F]">
              {t("executiveMeetings.manage.attendees.label")} ({state.attendees.length})
            </h4>
            <Button size="sm" variant="outline" onClick={addAttendee} className="gap-1">
              <Plus className="w-4 h-4" />
              {t("executiveMeetings.manage.attendees.add")}
            </Button>
          </div>
          <div className="space-y-2">
            {state.attendees.map((a, i) => (
              <div
                key={i}
                className="flex items-center gap-2"
                data-testid={`em-attendee-row-${i}`}
              >
                <div className="flex flex-col">
                  <Button
                    size="icon"
                    variant="ghost"
                    className="h-5 w-5"
                    onClick={() => moveAttendee(i, -1)}
                    disabled={i === 0}
                    aria-label={t("executiveMeetings.manage.attendees.moveUp")}
                    data-testid={`em-attendee-up-${i}`}
                  >
                    <ChevronUp className="w-3 h-3" />
                  </Button>
                  <Button
                    size="icon"
                    variant="ghost"
                    className="h-5 w-5"
                    onClick={() => moveAttendee(i, 1)}
                    disabled={i === state.attendees.length - 1}
                    aria-label={t("executiveMeetings.manage.attendees.moveDown")}
                    data-testid={`em-attendee-down-${i}`}
                  >
                    <ChevronDown className="w-3 h-3" />
                  </Button>
                </div>
                <Input
                  className="flex-1"
                  placeholder={t("executiveMeetings.manage.attendees.name")}
                  value={a.name}
                  onChange={(e) => setAttendee(i, { name: e.target.value })}
                />
                <Input
                  className="flex-1"
                  placeholder={t("executiveMeetings.manage.attendees.title")}
                  value={a.title ?? ""}
                  onChange={(e) =>
                    setAttendee(i, { title: e.target.value || null })
                  }
                  data-testid={`em-attendee-title-${i}`}
                />
                <Select
                  value={a.attendanceType}
                  onValueChange={(v) =>
                    setAttendee(i, { attendanceType: v as Attendee["attendanceType"] })
                  }
                >
                  <SelectTrigger className="w-32">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    {(["internal", "virtual", "external"] as const).map((tp) => (
                      <SelectItem key={tp} value={tp}>
                        {t(`executiveMeetings.manage.attendanceType.${tp}`)}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
                <Button size="icon" variant="ghost" onClick={() => removeAttendee(i)}>
                  <Trash2 className="w-4 h-4 text-red-600" />
                </Button>
              </div>
            ))}
            {state.attendees.length === 0 && (
              <div className="text-xs text-gray-500">—</div>
            )}
          </div>
        </div>

        <DialogFooter>
          <Button variant="ghost" onClick={onClose} disabled={saving}>
            {t("executiveMeetings.common.cancel")}
          </Button>
          <Button onClick={onSave} disabled={saving} className="bg-[#0B1E3F]" data-testid="em-form-save">
            {saving ? t("common.loading") : t("executiveMeetings.common.save")}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

function FormRow({
  label,
  children,
  full,
  hint,
}: {
  label: string;
  children: React.ReactNode;
  full?: boolean;
  hint?: string;
}) {
  return (
    <div className={full ? "sm:col-span-2" : ""}>
      <Label className="text-xs text-gray-700">{label}</Label>
      <div className="mt-1">{children}</div>
      {hint && <p className="text-[11px] text-gray-500 mt-0.5">{hint}</p>}
    </div>
  );
}

// ============ REQUESTS ============

function RequestsSection({
  me,
  isRtl,
  t,
}: {
  me: MeRoles;
  isRtl: boolean;
  t: (k: string) => string;
}) {
  const qc = useQueryClient();
  const { toast } = useToast();
  const [filterStatus, setFilterStatus] = useState<string>("all");
  const [scope, setScope] = useState<"all" | "mine">(
    me.canApprove ? "all" : "mine",
  );
  const [open, setOpen] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [form, setForm] = useState<{
    requestType: string;
    meetingId: string;
    details: string;
    meetingDate: string;
    startTime: string;
    endTime: string;
    location: string;
    meetingUrl: string;
    platform: string;
    attendeeName: string;
    attendeeTitle: string;
    attendeeType: string;
    attendeeId: string;
  }>({
    requestType: "edit",
    meetingId: "",
    details: "",
    meetingDate: "",
    startTime: "",
    endTime: "",
    location: "",
    meetingUrl: "",
    platform: "none",
    attendeeName: "",
    attendeeTitle: "",
    attendeeType: "internal",
    attendeeId: "",
  });

  const queryKey = ["/api/executive-meetings/requests", filterStatus, scope];
  const { data, isLoading } = useQuery<{ requests: RequestRow[] }>({
    queryKey,
    queryFn: () => {
      const qs = new URLSearchParams();
      if (filterStatus !== "all") qs.set("status", filterStatus);
      if (scope === "mine") qs.set("mine", "1");
      return apiJson(`/api/executive-meetings/requests?${qs}`);
    },
  });

  function buildRequestDetails(): Record<string, unknown> {
    const note = form.details.trim();
    const out: Record<string, unknown> = {};
    if (note) out.note = note;
    switch (form.requestType) {
      case "reschedule":
        if (form.meetingDate) out.meetingDate = form.meetingDate;
        if (form.startTime) out.startTime = form.startTime;
        if (form.endTime) out.endTime = form.endTime;
        break;
      case "change_location":
        if (form.location.trim()) out.location = form.location.trim();
        if (form.meetingUrl.trim()) out.meetingUrl = form.meetingUrl.trim();
        if (form.platform) out.platform = form.platform;
        break;
      case "add_attendee":
        if (form.attendeeName.trim()) out.name = form.attendeeName.trim();
        if (form.attendeeTitle.trim()) out.title = form.attendeeTitle.trim();
        if (form.attendeeType) out.attendanceType = form.attendeeType;
        break;
      case "remove_attendee":
        if (form.attendeeId.trim()) out.attendeeId = Number(form.attendeeId);
        break;
      default:
        break;
    }
    return out;
  }

  async function submit() {
    if (!me.canSubmitRequest) return;
    setSubmitting(true);
    try {
      const body: Record<string, unknown> = {
        requestType: form.requestType,
        requestDetails: buildRequestDetails(),
      };
      if (form.meetingId.trim()) body.meetingId = Number(form.meetingId);
      await apiJson(`/api/executive-meetings/requests`, { method: "POST", body });
      toast({ title: t("executiveMeetings.requests.submitted") });
      setOpen(false);
      setForm({
        requestType: "edit",
        meetingId: "",
        details: "",
        meetingDate: "",
        startTime: "",
        endTime: "",
        location: "",
        meetingUrl: "",
        platform: "none",
        attendeeName: "",
        attendeeTitle: "",
        attendeeType: "internal",
        attendeeId: "",
      });
      await qc.invalidateQueries({ queryKey: ["/api/executive-meetings/requests"] });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({
        title: t("executiveMeetings.requests.submitFailed"),
        description: msg,
        variant: "destructive",
      });
    } finally {
      setSubmitting(false);
    }
  }

  async function withdraw(id: number) {
    try {
      await apiJson(`/api/executive-meetings/requests/${id}`, {
        method: "PATCH",
        body: { action: "withdraw" },
      });
      await qc.invalidateQueries({ queryKey: ["/api/executive-meetings/requests"] });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({ title: msg, variant: "destructive" });
    }
  }

  const requests = data?.requests ?? [];

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between gap-3 flex-wrap">
        <h2 className="text-xl sm:text-2xl font-bold text-[#0B1E3F]">
          {t("executiveMeetings.requests.heading")}
        </h2>
        <div className="flex items-center gap-2 flex-wrap">
          <Select value={scope} onValueChange={(v) => setScope(v as "all" | "mine")}>
            <SelectTrigger className="w-40 h-9">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="all">{t("executiveMeetings.requests.allRequests")}</SelectItem>
              <SelectItem value="mine">{t("executiveMeetings.requests.myRequests")}</SelectItem>
            </SelectContent>
          </Select>
          <Select value={filterStatus} onValueChange={setFilterStatus}>
            <SelectTrigger className="w-36 h-9">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="all">{t("executiveMeetings.common.all")}</SelectItem>
              {(
                [
                  "new",
                  "needs_edit",
                  "approved",
                  "rejected",
                  "withdrawn",
                  "done",
                ] as const
              ).map((s) => (
                <SelectItem key={s} value={s}>
                  {t(`executiveMeetings.requests.status.${s}`)}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          {me.canSubmitRequest && (
            <Button
              onClick={() => setOpen(true)}
              className="bg-[#0B1E3F] gap-1"
              data-testid="em-new-request"
            >
              <Plus className="w-4 h-4" />
              {t("executiveMeetings.requests.newRequest")}
            </Button>
          )}
        </div>
      </div>

      <div className="bg-white border border-gray-200 rounded-md overflow-hidden">
        <table className="w-full text-sm" dir={isRtl ? "rtl" : "ltr"}>
          <thead className="bg-gray-50 text-[#0B1E3F]">
            <tr>
              <th className="px-3 py-2 text-start w-32">{t("executiveMeetings.requests.requestedAt")}</th>
              <th className="px-3 py-2 text-start">{t("executiveMeetings.requests.requestedBy")}</th>
              <th className="px-3 py-2 text-start w-32">{t("executiveMeetings.manage.field.status")}</th>
              <th className="px-3 py-2 text-start">{t("executiveMeetings.requests.type.create")}</th>
              <th className="px-3 py-2 text-start">{t("executiveMeetings.requests.details")}</th>
              <th className="px-3 py-2 text-end w-24">{t("executiveMeetings.common.actions")}</th>
            </tr>
          </thead>
          <tbody>
            {isLoading && (
              <tr>
                <td colSpan={6} className="py-8 text-center text-gray-500">
                  {t("common.loading")}
                </td>
              </tr>
            )}
            {!isLoading && requests.length === 0 && (
              <tr>
                <td colSpan={6} className="py-8 text-center text-gray-500">
                  {t("executiveMeetings.requests.empty")}
                </td>
              </tr>
            )}
            {requests.map((r) => (
              <RequestListRow
                key={r.id}
                row={r}
                isRtl={isRtl}
                t={t}
                canWithdraw={me.canSubmitRequest}
                onWithdraw={withdraw}
              />
            ))}
          </tbody>
        </table>
      </div>

      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>{t("executiveMeetings.requests.newRequest")}</DialogTitle>
          </DialogHeader>
          <div className="space-y-3">
            <FormRow label={t("executiveMeetings.requests.type.create")}>
              <Select
                value={form.requestType}
                onValueChange={(v) => setForm({ ...form, requestType: v })}
              >
                <SelectTrigger>
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  {(
                    [
                      "create",
                      "edit",
                      "delete",
                      "reschedule",
                      "add_attendee",
                      "remove_attendee",
                      "change_location",
                      "cancel_meeting",
                      "note",
                      "highlight",
                      "unhighlight",
                      "other",
                    ] as const
                  ).map((tp) => (
                    <SelectItem key={tp} value={tp}>
                      {t(`executiveMeetings.requests.type.${tp}`)}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </FormRow>
            <FormRow label={t("executiveMeetings.requests.submitFor")}>
              <Input
                type="number"
                placeholder={t("executiveMeetings.requests.noTargetMeeting")}
                value={form.meetingId}
                onChange={(e) => setForm({ ...form, meetingId: e.target.value })}
              />
            </FormRow>
            {form.requestType === "reschedule" && (
              <>
                <FormRow label={t("executiveMeetings.manage.field.meetingDate")}>
                  <Input
                    type="date"
                    value={form.meetingDate}
                    onChange={(e) =>
                      setForm({ ...form, meetingDate: e.target.value })
                    }
                    data-testid="em-req-date"
                  />
                </FormRow>
                <div className="grid grid-cols-2 gap-3">
                  <FormRow label={t("executiveMeetings.manage.field.startTime")}>
                    <Input
                      type="time"
                      value={form.startTime}
                      onChange={(e) =>
                        setForm({ ...form, startTime: e.target.value })
                      }
                      data-testid="em-req-start"
                    />
                  </FormRow>
                  <FormRow label={t("executiveMeetings.manage.field.endTime")}>
                    <Input
                      type="time"
                      value={form.endTime}
                      onChange={(e) =>
                        setForm({ ...form, endTime: e.target.value })
                      }
                      data-testid="em-req-end"
                    />
                  </FormRow>
                </div>
              </>
            )}
            {form.requestType === "change_location" && (
              <>
                <FormRow label={t("executiveMeetings.manage.field.location")}>
                  <Input
                    value={form.location}
                    onChange={(e) =>
                      setForm({ ...form, location: e.target.value })
                    }
                  />
                </FormRow>
                <FormRow label={t("executiveMeetings.manage.field.meetingUrl")}>
                  <Input
                    value={form.meetingUrl}
                    onChange={(e) =>
                      setForm({ ...form, meetingUrl: e.target.value })
                    }
                  />
                </FormRow>
              </>
            )}
            {form.requestType === "add_attendee" && (
              <>
                <FormRow label={t("executiveMeetings.manage.attendees.name")}>
                  <Input
                    value={form.attendeeName}
                    onChange={(e) =>
                      setForm({ ...form, attendeeName: e.target.value })
                    }
                  />
                </FormRow>
                <FormRow label={t("executiveMeetings.manage.attendees.title")}>
                  <Input
                    value={form.attendeeTitle}
                    onChange={(e) =>
                      setForm({ ...form, attendeeTitle: e.target.value })
                    }
                  />
                </FormRow>
              </>
            )}
            {form.requestType === "remove_attendee" && (
              <FormRow label={t("executiveMeetings.manage.attendees.id")}>
                <Input
                  type="number"
                  value={form.attendeeId}
                  onChange={(e) =>
                    setForm({ ...form, attendeeId: e.target.value })
                  }
                />
              </FormRow>
            )}
            <FormRow label={t("executiveMeetings.requests.details")}>
              <Textarea
                rows={3}
                value={form.details}
                placeholder={t("executiveMeetings.requests.detailsPlaceholder")}
                onChange={(e) => setForm({ ...form, details: e.target.value })}
              />
            </FormRow>
          </div>
          <DialogFooter>
            <Button variant="ghost" onClick={() => setOpen(false)} disabled={submitting}>
              {t("executiveMeetings.common.cancel")}
            </Button>
            <Button
              onClick={submit}
              disabled={submitting}
              className="bg-[#0B1E3F]"
              data-testid="em-submit-request"
            >
              {t("executiveMeetings.common.submit")}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </div>
  );
}

function RequestListRow({
  row,
  isRtl,
  t,
  canWithdraw,
  onWithdraw,
}: {
  row: RequestRow;
  isRtl: boolean;
  t: (k: string) => string;
  canWithdraw: boolean;
  onWithdraw: (id: number) => void;
}) {
  const note =
    row.requestDetails && typeof row.requestDetails === "object"
      ? (row.requestDetails as { note?: string }).note ?? ""
      : "";
  const requesterName = row.requester
    ? isRtl
      ? row.requester.displayNameAr || row.requester.displayNameEn || row.requester.username
      : row.requester.displayNameEn || row.requester.displayNameAr || row.requester.username
    : "—";
  const showWithdraw = canWithdraw && row.status === "new";
  return (
    <tr className="border-t border-gray-100">
      <td className="px-3 py-2 align-middle text-xs text-gray-600">
        {new Date(row.createdAt).toLocaleString()}
      </td>
      <td className="px-3 py-2 align-middle">{requesterName}</td>
      <td className="px-3 py-2 align-middle text-xs">
        {t(`executiveMeetings.requests.status.${row.status}`)}
      </td>
      <td className="px-3 py-2 align-middle text-xs">
        {t(`executiveMeetings.requests.type.${row.requestType}`)}
        {row.meetingId ? <span className="text-gray-500"> · #{row.meetingId}</span> : null}
      </td>
      <td className="px-3 py-2 align-middle text-xs whitespace-pre-wrap break-words max-w-md">
        {note}
        {row.reviewNotes && (
          <div className="mt-1 text-gray-500 italic">
            ↳ {row.reviewNotes}
          </div>
        )}
      </td>
      <td className="px-3 py-2 align-middle text-end">
        {showWithdraw && (
          <Button size="sm" variant="ghost" onClick={() => onWithdraw(row.id)}>
            <X className="w-4 h-4" />
          </Button>
        )}
      </td>
    </tr>
  );
}

// ============ APPROVALS ============

function ApprovalsSection({
  canApprove,
  isRtl,
  t,
}: {
  canApprove: boolean;
  isRtl: boolean;
  t: (k: string) => string;
}) {
  const qc = useQueryClient();
  const { toast } = useToast();
  const [notes, setNotes] = useState<Record<number, string>>({});
  const { data, isLoading } = useQuery<{ requests: RequestRow[] }>({
    queryKey: ["/api/executive-meetings/requests", "new", "all"],
    queryFn: () => apiJson(`/api/executive-meetings/requests?status=new`),
  });
  const requests = data?.requests ?? [];

  async function review(
    id: number,
    status: "approved" | "rejected" | "needs_edit",
  ) {
    try {
      await apiJson(`/api/executive-meetings/requests/${id}`, {
        method: "PATCH",
        body: { status, reviewNotes: notes[id] ?? "" },
      });
      toast({
        title:
          status === "approved"
            ? t("executiveMeetings.approvals.approved")
            : status === "rejected"
              ? t("executiveMeetings.approvals.rejected")
              : t("executiveMeetings.approvals.needsEditDone"),
      });
      await Promise.all([
        qc.invalidateQueries({ queryKey: ["/api/executive-meetings/requests"] }),
        qc.invalidateQueries({ queryKey: ["/api/executive-meetings/tasks"] }),
        qc.invalidateQueries({
          predicate: (q) =>
            Array.isArray(q.queryKey) &&
            q.queryKey[0] === "/api/executive-meetings",
        }),
      ]);
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({
        title: t("executiveMeetings.approvals.actionFailed"),
        description: msg,
        variant: "destructive",
      });
    }
  }

  if (!canApprove) return <NoPermission t={t} />;

  return (
    <div className="space-y-4">
      <h2 className="text-xl sm:text-2xl font-bold text-[#0B1E3F]">
        {t("executiveMeetings.approvals.heading")}
      </h2>
      {isLoading && <div className="text-center text-gray-500 py-8">{t("common.loading")}</div>}
      {!isLoading && requests.length === 0 && (
        <div className="bg-white border border-gray-200 rounded-md p-10 text-center text-gray-500">
          {t("executiveMeetings.approvals.empty")}
        </div>
      )}
      {requests.map((r) => {
        const note =
          r.requestDetails && typeof r.requestDetails === "object"
            ? (r.requestDetails as { note?: string }).note ?? ""
            : "";
        const requesterName = r.requester
          ? isRtl
            ? r.requester.displayNameAr || r.requester.username
            : r.requester.displayNameEn || r.requester.username
          : "—";
        return (
          <div
            key={r.id}
            className="bg-white border border-gray-200 rounded-md p-4 space-y-3"
            data-testid={`em-approval-${r.id}`}
          >
            <div className="flex justify-between gap-3 flex-wrap">
              <div className="text-sm">
                <div className="font-semibold text-[#0B1E3F]">
                  {t(`executiveMeetings.requests.type.${r.requestType}`)}
                  {r.meetingId ? <span className="text-gray-500"> · #{r.meetingId}</span> : null}
                </div>
                <div className="text-xs text-gray-500">
                  {requesterName} · {new Date(r.createdAt).toLocaleString()}
                </div>
              </div>
            </div>
            <div className="text-sm whitespace-pre-wrap text-gray-700">{note || "—"}</div>
            <Textarea
              placeholder={t("executiveMeetings.approvals.reviewNotesPlaceholder")}
              rows={2}
              value={notes[r.id] ?? ""}
              onChange={(e) => setNotes({ ...notes, [r.id]: e.target.value })}
            />
            <div className="flex gap-2 justify-end flex-wrap">
              <Button
                variant="outline"
                onClick={() => review(r.id, "needs_edit")}
                className="gap-1"
                data-testid={`em-needs-edit-${r.id}`}
              >
                {t("executiveMeetings.approvals.needsEdit")}
              </Button>
              <Button
                variant="outline"
                onClick={() => review(r.id, "rejected")}
                className="gap-1"
                data-testid={`em-reject-${r.id}`}
              >
                <X className="w-4 h-4" />
                {t("executiveMeetings.approvals.reject")}
              </Button>
              <Button
                onClick={() => review(r.id, "approved")}
                className="bg-[#0B1E3F] gap-1"
                data-testid={`em-approve-${r.id}`}
              >
                <Check className="w-4 h-4" />
                {t("executiveMeetings.approvals.approve")}
              </Button>
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ============ TASKS ============

function TasksSection({
  canMutate,
  currentUserId,
  canViewAllTasks,
  isRtl,
  t,
}: {
  canMutate: boolean;
  currentUserId: number;
  canViewAllTasks: boolean;
  isRtl: boolean;
  t: (k: string) => string;
}) {
  const qc = useQueryClient();
  const { toast } = useToast();
  const [open, setOpen] = useState(false);
  const [form, setForm] = useState({
    taskType: "",
    meetingId: "",
    assignedTo: "",
    dueAt: "",
    notes: "",
  });
  const [filterStatus, setFilterStatus] = useState<string>("all");

  const { data, isLoading } = useQuery<{ tasks: TaskRow[] }>({
    queryKey: ["/api/executive-meetings/tasks", filterStatus],
    queryFn: () => {
      const qs = new URLSearchParams();
      if (filterStatus !== "all") qs.set("status", filterStatus);
      return apiJson(`/api/executive-meetings/tasks?${qs}`);
    },
  });
  const tasks = data?.tasks ?? [];

  async function createTask() {
    if (!form.taskType.trim()) return;
    try {
      const body: Record<string, unknown> = { taskType: form.taskType.trim() };
      if (form.meetingId) body.meetingId = Number(form.meetingId);
      if (form.assignedTo) body.assignedTo = Number(form.assignedTo);
      if (form.dueAt) body.dueAt = form.dueAt;
      if (form.notes) body.notes = form.notes;
      await apiJson(`/api/executive-meetings/tasks`, { method: "POST", body });
      toast({ title: t("executiveMeetings.tasks.saved") });
      setOpen(false);
      setForm({ taskType: "", meetingId: "", assignedTo: "", dueAt: "", notes: "" });
      await qc.invalidateQueries({ queryKey: ["/api/executive-meetings/tasks"] });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({ title: msg, variant: "destructive" });
    }
  }

  async function setStatus(id: number, status: TaskRow["status"]) {
    try {
      await apiJson(`/api/executive-meetings/tasks/${id}`, {
        method: "PATCH",
        body: { status },
      });
      await qc.invalidateQueries({ queryKey: ["/api/executive-meetings/tasks"] });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({ title: msg, variant: "destructive" });
    }
  }

  const [reassignTarget, setReassignTarget] = useState<TaskRow | null>(null);
  const [reassignForm, setReassignForm] = useState({ assignedTo: "", notes: "" });
  const [reassignBusy, setReassignBusy] = useState(false);

  function openReassign(task: TaskRow) {
    setReassignTarget(task);
    setReassignForm({
      assignedTo: task.assignedTo ? String(task.assignedTo) : "",
      notes: task.notes ?? "",
    });
  }

  async function submitReassign() {
    if (!reassignTarget) return;
    const body: Record<string, unknown> = {};
    const newAssignee = reassignForm.assignedTo.trim();
    if (newAssignee) body.assignedTo = Number(newAssignee);
    if (reassignForm.notes !== (reassignTarget.notes ?? "")) {
      body.notes = reassignForm.notes;
    }
    if (Object.keys(body).length === 0) {
      setReassignTarget(null);
      return;
    }
    setReassignBusy(true);
    try {
      await apiJson(`/api/executive-meetings/tasks/${reassignTarget.id}`, {
        method: "PATCH",
        body,
      });
      toast({ title: t("executiveMeetings.tasks.reassigned") });
      setReassignTarget(null);
      await qc.invalidateQueries({ queryKey: ["/api/executive-meetings/tasks"] });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({ title: msg, variant: "destructive" });
    } finally {
      setReassignBusy(false);
    }
  }

  async function deleteTask(id: number) {
    if (!window.confirm(t("executiveMeetings.tasks.deleteConfirm"))) return;
    try {
      await apiJson(`/api/executive-meetings/tasks/${id}`, { method: "DELETE" });
      await qc.invalidateQueries({ queryKey: ["/api/executive-meetings/tasks"] });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({ title: msg, variant: "destructive" });
    }
  }

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between gap-3 flex-wrap">
        <h2 className="text-xl sm:text-2xl font-bold text-[#0B1E3F]">
          {t("executiveMeetings.tasks.heading")}
          {!canViewAllTasks && (
            <span
              className="ms-2 inline-block rounded-full bg-gray-100 text-gray-700 text-xs px-2 py-0.5 align-middle font-normal"
              data-testid="em-tasks-mine-badge"
              title={t("executiveMeetings.tasks.myTasksOnlyHelp")}
            >
              {t("executiveMeetings.tasks.myTasksOnly")}
            </span>
          )}
        </h2>
        <div className="flex items-center gap-2">
          <Select value={filterStatus} onValueChange={setFilterStatus}>
            <SelectTrigger className="w-36 h-9">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="all">{t("executiveMeetings.common.all")}</SelectItem>
              {(["pending", "in_progress", "completed", "cancelled"] as const).map((s) => (
                <SelectItem key={s} value={s}>
                  {t(`executiveMeetings.tasks.status.${s}`)}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          {canMutate && (
            <Button
              onClick={() => setOpen(true)}
              className="bg-[#0B1E3F] gap-1"
              data-testid="em-add-task"
            >
              <Plus className="w-4 h-4" />
              {t("executiveMeetings.tasks.addTask")}
            </Button>
          )}
        </div>
      </div>

      <div className="bg-white border border-gray-200 rounded-md overflow-hidden">
        <table className="w-full text-sm" dir={isRtl ? "rtl" : "ltr"}>
          <thead className="bg-gray-50 text-[#0B1E3F]">
            <tr>
              <th className="px-3 py-2 text-start">{t("executiveMeetings.tasks.field.taskType")}</th>
              <th className="px-3 py-2 text-start w-28">{t("executiveMeetings.tasks.field.meeting")}</th>
              <th className="px-3 py-2 text-start">{t("executiveMeetings.tasks.field.assignee")}</th>
              <th className="px-3 py-2 text-start w-32">{t("executiveMeetings.tasks.field.dueDate")}</th>
              <th className="px-3 py-2 text-start w-32">{t("executiveMeetings.tasks.field.status")}</th>
              <th className="px-3 py-2 text-end w-44">{t("executiveMeetings.common.actions")}</th>
            </tr>
          </thead>
          <tbody>
            {isLoading && (
              <tr>
                <td colSpan={6} className="py-8 text-center text-gray-500">
                  {t("common.loading")}
                </td>
              </tr>
            )}
            {!isLoading && tasks.length === 0 && (
              <tr>
                <td colSpan={6} className="py-8 text-center text-gray-500">
                  {t("executiveMeetings.tasks.empty")}
                </td>
              </tr>
            )}
            {tasks.map((task) => {
              const assignee = task.assignee
                ? isRtl
                  ? task.assignee.displayNameAr || task.assignee.username
                  : task.assignee.displayNameEn || task.assignee.username
                : "—";
              return (
                <tr key={task.id} className="border-t border-gray-100">
                  <td className="px-3 py-2 align-middle">{task.taskType}</td>
                  <td className="px-3 py-2 align-middle text-xs text-gray-600">
                    {task.meetingId ? `#${task.meetingId}` : "—"}
                  </td>
                  <td className="px-3 py-2 align-middle">{assignee}</td>
                  <td className="px-3 py-2 align-middle text-xs">
                    {task.dueAt ? new Date(task.dueAt).toLocaleDateString() : "—"}
                  </td>
                  <td className="px-3 py-2 align-middle text-xs">
                    {t(`executiveMeetings.tasks.status.${task.status}`)}
                  </td>
                  <td className="px-3 py-2 align-middle text-end">
                    <div className="inline-flex gap-1">
                      {(canMutate || task.assignedTo === currentUserId) && (
                        <>
                          {task.status !== "completed" && (
                            <Button
                              size="sm"
                              variant="ghost"
                              onClick={() => setStatus(task.id, "completed")}
                              title={t("executiveMeetings.tasks.markCompleted")}
                            >
                              <Check className="w-4 h-4" />
                            </Button>
                          )}
                          {task.status === "pending" && (
                            <Button
                              size="sm"
                              variant="ghost"
                              onClick={() => setStatus(task.id, "in_progress")}
                              title={t("executiveMeetings.tasks.markInProgress")}
                            >
                              <ListTodo className="w-4 h-4" />
                            </Button>
                          )}
                        </>
                      )}
                      {canMutate && (
                        <>
                          <Button
                            size="sm"
                            variant="ghost"
                            onClick={() => openReassign(task)}
                            title={t("executiveMeetings.tasks.reassign")}
                            data-testid={`em-task-reassign-${task.id}`}
                          >
                            <Pencil className="w-4 h-4" />
                          </Button>
                          <Button
                            size="sm"
                            variant="ghost"
                            onClick={() => deleteTask(task.id)}
                          >
                            <Trash2 className="w-4 h-4 text-red-600" />
                          </Button>
                        </>
                      )}
                    </div>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>

      <Dialog
        open={!!reassignTarget}
        onOpenChange={(v) => !v && setReassignTarget(null)}
      >
        <DialogContent>
          <DialogHeader>
            <DialogTitle>{t("executiveMeetings.tasks.reassign")}</DialogTitle>
          </DialogHeader>
          <div className="space-y-3">
            <FormRow label={t("executiveMeetings.tasks.field.assignee")}>
              <Input
                type="number"
                value={reassignForm.assignedTo}
                onChange={(e) =>
                  setReassignForm({ ...reassignForm, assignedTo: e.target.value })
                }
                data-testid="em-task-reassign-assignee"
              />
            </FormRow>
            <FormRow label={t("executiveMeetings.tasks.field.notes")}>
              <Textarea
                rows={3}
                value={reassignForm.notes}
                onChange={(e) =>
                  setReassignForm({ ...reassignForm, notes: e.target.value })
                }
                data-testid="em-task-reassign-notes"
              />
            </FormRow>
          </div>
          <DialogFooter>
            <Button variant="outline" onClick={() => setReassignTarget(null)}>
              {t("common.cancel")}
            </Button>
            <Button
              onClick={submitReassign}
              disabled={reassignBusy}
              data-testid="em-task-reassign-save"
            >
              {t("common.save")}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>{t("executiveMeetings.tasks.addTask")}</DialogTitle>
          </DialogHeader>
          <div className="space-y-3">
            <FormRow label={t("executiveMeetings.tasks.field.taskType")}>
              <Input
                value={form.taskType}
                onChange={(e) => setForm({ ...form, taskType: e.target.value })}
                data-testid="em-task-type"
              />
            </FormRow>
            <div className="grid grid-cols-2 gap-3">
              <FormRow label={t("executiveMeetings.tasks.field.meeting")}>
                <Input
                  type="number"
                  value={form.meetingId}
                  onChange={(e) => setForm({ ...form, meetingId: e.target.value })}
                />
              </FormRow>
              <FormRow label={t("executiveMeetings.tasks.field.assignee")}>
                <Input
                  type="number"
                  placeholder={t("executiveMeetings.tasks.field.assigneeIdPlaceholder")}
                  value={form.assignedTo}
                  onChange={(e) => setForm({ ...form, assignedTo: e.target.value })}
                />
              </FormRow>
            </div>
            <FormRow label={t("executiveMeetings.tasks.field.dueDate")}>
              <Input
                type="date"
                value={form.dueAt}
                onChange={(e) => setForm({ ...form, dueAt: e.target.value })}
              />
            </FormRow>
            <FormRow label={t("executiveMeetings.tasks.field.notes")}>
              <Textarea
                rows={3}
                value={form.notes}
                onChange={(e) => setForm({ ...form, notes: e.target.value })}
              />
            </FormRow>
          </div>
          <DialogFooter>
            <Button variant="ghost" onClick={() => setOpen(false)}>
              {t("executiveMeetings.common.cancel")}
            </Button>
            <Button onClick={createTask} className="bg-[#0B1E3F]" data-testid="em-task-save">
              {t("executiveMeetings.common.save")}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </div>
  );
}

// ============ AUDIT ============

function AuditDiffSummary({
  oldValue,
  newValue,
  t,
}: {
  oldValue: unknown;
  newValue: unknown;
  t: (k: string) => string;
}) {
  const interesting = [
    "titleAr",
    "titleEn",
    "meetingDate",
    "dailyNumber",
    "startTime",
    "endTime",
    "location",
    "meetingUrl",
    "platform",
    "status",
    "isHighlighted",
    "notes",
    "assignedTo",
    "taskType",
    "dueAt",
    "reviewDecision",
    "reviewNotes",
  ] as const;

  const oldObj =
    oldValue && typeof oldValue === "object"
      ? (oldValue as Record<string, unknown>)
      : null;
  const newObj =
    newValue && typeof newValue === "object"
      ? (newValue as Record<string, unknown>)
      : null;

  const fmt = (v: unknown): string => {
    if (v === null || v === undefined || v === "") return "—";
    if (typeof v === "string") return v.length > 40 ? `${v.slice(0, 40)}…` : v;
    if (typeof v === "number" || typeof v === "boolean") return String(v);
    try {
      const s = JSON.stringify(v);
      return s.length > 40 ? `${s.slice(0, 40)}…` : s;
    } catch {
      return String(v);
    }
  };

  if (!oldObj && !newObj) return <span className="text-gray-400">—</span>;

  if (oldObj && newObj) {
    const diffs: { key: string; from: unknown; to: unknown }[] = [];
    for (const k of interesting) {
      const a = oldObj[k];
      const b = newObj[k];
      if (JSON.stringify(a) !== JSON.stringify(b)) {
        diffs.push({ key: k, from: a, to: b });
      }
    }
    if (diffs.length === 0) {
      return <span className="text-gray-400">—</span>;
    }
    return (
      <ul className="space-y-0.5">
        {diffs.slice(0, 6).map((d) => (
          <li key={d.key} className="leading-snug">
            <span className="font-semibold text-[#0B1E3F]">{d.key}</span>:{" "}
            <span className="text-red-700 line-through">{fmt(d.from)}</span>
            <span className="mx-1 text-gray-400">→</span>
            <span className="text-green-700">{fmt(d.to)}</span>
          </li>
        ))}
        {diffs.length > 6 && (
          <li className="text-gray-500">
            +{diffs.length - 6} {t("executiveMeetings.audit.moreChanges")}
          </li>
        )}
      </ul>
    );
  }

  const single = (newObj ?? oldObj) as Record<string, unknown>;
  const label = newObj
    ? t("executiveMeetings.audit.created")
    : t("executiveMeetings.audit.removed");
  const items = interesting
    .filter((k) => single[k] !== undefined && single[k] !== null && single[k] !== "")
    .slice(0, 6);
  return (
    <div>
      <div className="font-semibold text-[#0B1E3F] mb-0.5">{label}</div>
      <ul className="space-y-0.5">
        {items.map((k) => (
          <li key={k} className="leading-snug">
            <span className="font-semibold">{k}</span>: {fmt(single[k])}
          </li>
        ))}
      </ul>
    </div>
  );
}


function AuditSection({
  canView,
  isRtl,
  t,
}: {
  canView: boolean;
  isRtl: boolean;
  t: (k: string) => string;
}) {
  const [date, setDate] = useState("");
  const [dateTo, setDateTo] = useState("");
  const [entityType, setEntityType] = useState("all");
  const [actionFilter, setActionFilter] = useState("all");
  const [actorFilter, setActorFilter] = useState("");
  const [page, setPage] = useState(0);
  const PAGE_SIZE = 50;
  useEffect(() => {
    setPage(0);
  }, [date, dateTo, entityType, actionFilter, actorFilter]);
  const { data, isLoading } = useQuery<{
    entries: AuditEntry[];
    total: number;
    limit: number;
    offset: number;
    hasMore: boolean;
  }>({
    queryKey: [
      "/api/executive-meetings/audit-logs",
      date,
      dateTo,
      entityType,
      actionFilter,
      actorFilter,
      page,
    ],
    queryFn: () => {
      const qs = new URLSearchParams();
      if (date) qs.set("dateFrom", date);
      if (dateTo) qs.set("dateTo", dateTo);
      if (entityType !== "all") qs.set("entityType", entityType);
      if (actionFilter !== "all") qs.set("action", actionFilter);
      if (actorFilter.trim()) qs.set("actorId", actorFilter.trim());
      qs.set("limit", String(PAGE_SIZE));
      qs.set("offset", String(page * PAGE_SIZE));
      return apiJson(`/api/executive-meetings/audit-logs?${qs}`);
    },
    enabled: canView,
  });

  if (!canView) return <NoPermission t={t} />;
  const entries = data?.entries ?? [];

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between gap-3 flex-wrap">
        <h2 className="text-xl sm:text-2xl font-bold text-[#0B1E3F]">
          {t("executiveMeetings.audit.heading")}
        </h2>
        <div className="flex items-center gap-2 flex-wrap">
          <label className="text-xs text-gray-500 flex items-center gap-1">
            {t("executiveMeetings.audit.filter.from")}
            <input
              type="date"
              value={date}
              onChange={(e) => setDate(e.target.value)}
              className="border border-gray-300 rounded px-2 py-1 text-sm bg-white h-9"
              data-testid="em-audit-date-from"
            />
          </label>
          <label className="text-xs text-gray-500 flex items-center gap-1">
            {t("executiveMeetings.audit.filter.to")}
            <input
              type="date"
              value={dateTo}
              onChange={(e) => setDateTo(e.target.value)}
              className="border border-gray-300 rounded px-2 py-1 text-sm bg-white h-9"
              data-testid="em-audit-date-to"
            />
          </label>
          <Input
            type="number"
            placeholder={t("executiveMeetings.audit.filter.actorId")}
            value={actorFilter}
            onChange={(e) => setActorFilter(e.target.value)}
            className="w-32 h-9"
            data-testid="em-audit-actor"
          />
          <Select value={actionFilter} onValueChange={setActionFilter}>
            <SelectTrigger className="w-36 h-9" data-testid="em-audit-action">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="all">{t("executiveMeetings.audit.all")}</SelectItem>
              {(["create", "update", "delete", "approve", "reject"] as const).map(
                (a) => (
                  <SelectItem key={a} value={a}>
                    {tWithFallback(t, `executiveMeetings.audit.action.${a}`, a)}
                  </SelectItem>
                ),
              )}
            </SelectContent>
          </Select>
          <Select value={entityType} onValueChange={setEntityType}>
            <SelectTrigger className="w-36 h-9">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="all">{t("executiveMeetings.audit.all")}</SelectItem>
              {(["meeting", "attendee", "request", "task", "font_settings"] as const).map(
                (e) => (
                  <SelectItem key={e} value={e}>
                    {t(`executiveMeetings.audit.entity.${e}`)}
                  </SelectItem>
                ),
              )}
            </SelectContent>
          </Select>
        </div>
      </div>

      <div className="bg-white border border-gray-200 rounded-md overflow-hidden">
        <table className="w-full text-sm" dir={isRtl ? "rtl" : "ltr"}>
          <thead className="bg-gray-50 text-[#0B1E3F]">
            <tr>
              <th className="px-3 py-2 text-start w-44">{t("executiveMeetings.audit.col.when")}</th>
              <th className="px-3 py-2 text-start">{t("executiveMeetings.audit.col.actor")}</th>
              <th className="px-3 py-2 text-start w-28">{t("executiveMeetings.audit.col.action")}</th>
              <th className="px-3 py-2 text-start w-28">{t("executiveMeetings.audit.col.entity")}</th>
              <th className="px-3 py-2 text-start w-20">{t("executiveMeetings.audit.col.entityId")}</th>
              <th className="px-3 py-2 text-start">{t("executiveMeetings.audit.col.changes")}</th>
            </tr>
          </thead>
          <tbody>
            {isLoading && (
              <tr>
                <td colSpan={6} className="py-8 text-center text-gray-500">
                  {t("common.loading")}
                </td>
              </tr>
            )}
            {!isLoading && entries.length === 0 && (
              <tr>
                <td colSpan={6} className="py-8 text-center text-gray-500">
                  {t("executiveMeetings.audit.empty")}
                </td>
              </tr>
            )}
            {entries.map((e) => {
              const actor = e.actor
                ? isRtl
                  ? e.actor.displayNameAr || e.actor.username
                  : e.actor.displayNameEn || e.actor.username
                : "—";
              return (
                <tr
                  key={e.id}
                  className="border-t border-gray-100 align-top"
                  data-testid={`em-audit-row-${e.id}`}
                >
                  <td className="px-3 py-2 text-xs text-gray-600 whitespace-nowrap">
                    {new Date(e.performedAt).toLocaleString()}
                  </td>
                  <td className="px-3 py-2">{actor}</td>
                  <td className="px-3 py-2 text-xs">
                    {tWithFallback(
                      t,
                      `executiveMeetings.audit.action.${e.action}`,
                      e.action,
                    )}
                  </td>
                  <td className="px-3 py-2 text-xs">
                    {tWithFallback(
                      t,
                      `executiveMeetings.audit.entity.${e.entityType}`,
                      e.entityType,
                    )}
                  </td>
                  <td className="px-3 py-2 text-xs">{e.entityId ?? "—"}</td>
                  <td className="px-3 py-2 text-xs">
                    <AuditDiffSummary
                      oldValue={e.oldValue}
                      newValue={e.newValue}
                      t={t}
                    />
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      <div className="flex items-center justify-between text-xs text-gray-600">
        <span data-testid="em-audit-count">
          {t("executiveMeetings.audit.pageInfo")}{" "}
          {data
            ? `${(data.offset ?? 0) + (entries.length > 0 ? 1 : 0)}–${
                (data.offset ?? 0) + entries.length
              } / ${data.total ?? 0}`
            : "—"}
        </span>
        <div className="flex gap-2">
          <Button
            size="sm"
            variant="outline"
            disabled={page === 0}
            onClick={() => setPage((p) => Math.max(0, p - 1))}
            data-testid="em-audit-prev"
          >
            {t("common.previous")}
          </Button>
          <Button
            size="sm"
            variant="outline"
            disabled={!data?.hasMore}
            onClick={() => setPage((p) => p + 1)}
            data-testid="em-audit-next"
          >
            {t("common.next")}
          </Button>
        </div>
      </div>
    </div>
  );
}

// ============ NOTIFICATIONS ============

function NotificationsSection({
  canView,
  date,
  isRtl,
  t,
}: {
  canView: boolean;
  date: string;
  isRtl: boolean;
  t: (k: string) => string;
}) {
  const { data, isLoading } = useQuery<{ notifications: NotificationRow[] }>({
    queryKey: ["/api/executive-meetings/notifications", date],
    queryFn: () =>
      apiJson(
        `/api/executive-meetings/notifications${
          date ? `?date=${encodeURIComponent(date)}` : ""
        }`,
      ),
    enabled: canView,
  });
  if (!canView) return <NoPermission t={t} />;
  const rows = data?.notifications ?? [];
  return (
    <div className="space-y-4">
      <h2 className="text-xl sm:text-2xl font-bold text-[#0B1E3F]">
        {t("executiveMeetings.notificationsPage.heading")}
      </h2>
      <p className="text-sm text-gray-600 max-w-2xl">
        {t("executiveMeetings.notificationsPage.intro")}
      </p>
      <div className="bg-white border border-gray-200 rounded-md overflow-hidden">
        <table className="w-full text-sm" dir={isRtl ? "rtl" : "ltr"}>
          <thead className="bg-gray-50 text-[#0B1E3F]">
            <tr>
              <th className="px-3 py-2 text-start w-24">{t("executiveMeetings.notificationsPage.col.meeting")}</th>
              <th className="px-3 py-2 text-start">{t("executiveMeetings.notificationsPage.col.user")}</th>
              <th className="px-3 py-2 text-start w-28">{t("executiveMeetings.notificationsPage.col.type")}</th>
              <th className="px-3 py-2 text-start w-44">{t("executiveMeetings.notificationsPage.col.scheduledAt")}</th>
              <th className="px-3 py-2 text-start w-24">{t("executiveMeetings.notificationsPage.col.status")}</th>
            </tr>
          </thead>
          <tbody>
            {isLoading && (
              <tr>
                <td colSpan={5} className="py-8 text-center text-gray-500">
                  {t("common.loading")}
                </td>
              </tr>
            )}
            {!isLoading && rows.length === 0 && (
              <tr>
                <td colSpan={5} className="py-8 text-center text-gray-500">
                  {t("executiveMeetings.notificationsPage.empty")}
                </td>
              </tr>
            )}
            {rows.map((n) => {
              const user = n.user
                ? isRtl
                  ? n.user.displayNameAr || n.user.username
                  : n.user.displayNameEn || n.user.username
                : "—";
              return (
                <tr key={n.id} className="border-t border-gray-100">
                  <td className="px-3 py-2">{n.meetingId ? `#${n.meetingId}` : "—"}</td>
                  <td className="px-3 py-2">{user}</td>
                  <td className="px-3 py-2 text-xs">
                    {tWithFallback(
                      t,
                      `executiveMeetings.notificationsPage.type.${n.notificationType}`,
                      n.notificationType,
                    )}
                  </td>
                  <td className="px-3 py-2 text-xs">
                    {n.scheduledAt ? new Date(n.scheduledAt).toLocaleString() : "—"}
                  </td>
                  <td className="px-3 py-2 text-xs">
                    {tWithFallback(
                      t,
                      `executiveMeetings.notificationsPage.status.${n.status}`,
                      n.status,
                    )}
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// ============ PDF / PRINT ============

function PdfSection({
  date,
  onDateChange,
  lang,
  t,
}: {
  date: string;
  onDateChange: (d: string) => void;
  lang: "ar" | "en";
  t: (k: string) => string;
}) {
  const qc = useQueryClient();
  const { toast } = useToast();
  const [busy, setBusy] = useState(false);
  const archivesQuery = useQuery<{ archives: PdfArchive[] }>({
    queryKey: ["/api/executive-meetings/pdf-archives", date],
    queryFn: async () =>
      apiJson(`/api/executive-meetings/pdf-archives?date=${date}`),
    enabled: !!date,
  });

  // Stream a binary response from the API to the browser's download
  // stack. Throws with the server's error message on a non-2xx so the
  // toast surfaces a useful reason instead of a silent failure.
  async function downloadBinary(url: string, filename: string): Promise<void> {
    const res = await fetch(url, { credentials: "include" });
    if (!res.ok) {
      let message = `HTTP ${res.status}`;
      try {
        const j = (await res.json()) as { error?: string };
        if (j.error) message = j.error;
      } catch {
        /* response wasn't JSON — keep the HTTP status */
      }
      throw new Error(message);
    }
    const blob = await res.blob();
    const objectUrl = URL.createObjectURL(blob);
    try {
      const a = document.createElement("a");
      a.href = objectUrl;
      a.download = filename;
      a.rel = "noopener";
      document.body.appendChild(a);
      a.click();
      a.remove();
    } finally {
      // Free the blob URL after the click — Safari needs a tick before
      // it actually starts the download, so defer slightly.
      setTimeout(() => URL.revokeObjectURL(objectUrl), 4000);
    }
  }

  async function downloadPdf() {
    if (!date) return;
    setBusy(true);
    try {
      const url = `/api/executive-meetings/pdf?date=${encodeURIComponent(
        date,
      )}&lang=${encodeURIComponent(lang)}`;
      await downloadBinary(url, `executive-meetings-${date}.pdf`);
      // Refresh archives so the row created by the GET appears.
      await qc.invalidateQueries({
        queryKey: ["/api/executive-meetings/pdf-archives", date],
      });
      toast({ title: t("executiveMeetings.pdf.downloaded") });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({ title: msg, variant: "destructive" });
    } finally {
      setBusy(false);
    }
  }

  async function downloadArchive(archive: PdfArchive) {
    if (!archive.filePath?.startsWith("/objects/")) return;
    try {
      // /objects/<id> resolves to GET /api/storage/objects/<id> on the
      // server. The route is auth-gated behind the same session, so
      // credentials must be included.
      const url = `/api${archive.filePath.replace(/^\/objects/, "/storage/objects")}`;
      await downloadBinary(
        url,
        `executive-meetings-${archive.archiveDate}-v${archive.version}.pdf`,
      );
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({ title: msg, variant: "destructive" });
    }
  }

  const archives = archivesQuery.data?.archives ?? [];

  return (
    <div className="space-y-4 max-w-2xl">
      <h2 className="text-xl sm:text-2xl font-bold text-[#0B1E3F]">
        {t("executiveMeetings.pdf.heading")}
      </h2>
      <p className="text-sm text-gray-600">{t("executiveMeetings.pdf.intro")}</p>
      <div className="bg-white border border-gray-200 rounded-md p-4 space-y-3">
        <FormRow label={t("executiveMeetings.pdf.selectDate")}>
          <Input
            type="date"
            value={date}
            onChange={(e) => onDateChange(e.target.value)}
          />
        </FormRow>
        <div className="flex flex-wrap gap-2">
          <Button
            onClick={downloadPdf}
            disabled={busy || !date}
            className="bg-[#0B1E3F] gap-1"
            data-testid="em-download-pdf"
          >
            <FileText className="w-4 h-4" />
            {t("executiveMeetings.pdf.downloadPdf")}
          </Button>
        </div>
        <p className="text-xs text-gray-500">
          {t("executiveMeetings.pdf.downloadHint")}
        </p>
      </div>

      <div className="bg-white border border-gray-200 rounded-md">
        <div className="px-4 py-2 border-b border-gray-200 text-sm font-semibold text-[#0B1E3F]">
          {t("executiveMeetings.pdf.archivesHeading")}
        </div>
        {archivesQuery.isLoading ? (
          <div className="p-4 text-sm text-gray-500">{t("common.loading")}</div>
        ) : archives.length === 0 ? (
          <div className="p-4 text-sm text-gray-500">
            {t("executiveMeetings.pdf.noArchives")}
          </div>
        ) : (
          <ul className="divide-y divide-gray-100" data-testid="em-archives-list">
            {archives.map((a) => {
              const who =
                a.generator
                  ? lang === "ar"
                    ? a.generator.displayNameAr || a.generator.username
                    : a.generator.displayNameEn || a.generator.username
                  : "—";
              return (
                <li
                  key={a.id}
                  className="px-4 py-2 text-sm flex items-center justify-between gap-3"
                >
                  <div>
                    <div className="font-medium text-[#0B1E3F]">
                      v{a.version} — {a.archiveDate}
                    </div>
                    <div className="text-xs text-gray-500">
                      {new Date(a.generatedAt).toLocaleString(
                        lang === "ar" ? "ar" : "en",
                      )}{" "}
                      · {who}
                    </div>
                  </div>
                  <div className="flex items-center gap-2">
                    {a.filePath?.startsWith("/objects/") ? (
                      <Button
                        size="sm"
                        variant="outline"
                        onClick={() => downloadArchive(a)}
                        data-testid={`em-archive-download-${a.id}`}
                      >
                        {t("executiveMeetings.pdf.downloadArchive")}
                      </Button>
                    ) : (
                      <span
                        className="text-xs text-gray-400"
                        data-testid={`em-archive-legacy-${a.id}`}
                      >
                        {t("executiveMeetings.pdf.legacyArchive")}
                      </span>
                    )}
                    {typeof a.byteSize === "number" ? (
                      <span className="text-xs text-gray-400">
                        {formatBytesForDisplay(a.byteSize)}
                      </span>
                    ) : null}
                  </div>
                </li>
              );
            })}
          </ul>
        )}
      </div>
    </div>
  );
}

// ============ FONT SETTINGS ============

function FontSettingsSection({
  font,
  canEditGlobal,
  t,
}: {
  font: FontPrefs;
  canEditGlobal: boolean;
  t: (k: string) => string;
}) {
  const qc = useQueryClient();
  const { toast } = useToast();
  const [scope, setScope] = useState<"user" | "global">("user");
  const [prefs, setPrefs] = useState<FontPrefs>(font);
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    setPrefs(font);
  }, [font]);

  async function save() {
    setSaving(true);
    try {
      await apiJson(`/api/executive-meetings/font-settings`, {
        method: "PATCH",
        body: { scope, ...prefs },
      });
      toast({ title: t("executiveMeetings.fontSettingsPage.saved") });
      await qc.invalidateQueries({ queryKey: ["/api/executive-meetings/font-settings"] });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      toast({ title: msg, variant: "destructive" });
    } finally {
      setSaving(false);
    }
  }

  function reset() {
    setPrefs(DEFAULT_FONT);
  }

  return (
    <div className="space-y-4 max-w-2xl">
      <h2 className="text-xl sm:text-2xl font-bold text-[#0B1E3F]">
        {t("executiveMeetings.fontSettingsPage.heading")}
      </h2>
      <p className="text-sm text-gray-600">{t("executiveMeetings.fontSettingsPage.intro")}</p>

      <div className="bg-white border border-gray-200 rounded-md p-4 space-y-3">
        {canEditGlobal && (
          <FormRow label={t("executiveMeetings.fontSettingsPage.scope")}>
            <Select value={scope} onValueChange={(v) => setScope(v as "user" | "global")}>
              <SelectTrigger>
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="user">
                  {t("executiveMeetings.fontSettingsPage.scopeUser")}
                </SelectItem>
                <SelectItem value="global">
                  {t("executiveMeetings.fontSettingsPage.scopeGlobal")}
                </SelectItem>
              </SelectContent>
            </Select>
          </FormRow>
        )}
        <FormRow label={t("executiveMeetings.fontSettingsPage.fontFamily")}>
          <Select
            value={prefs.fontFamily}
            onValueChange={(v) => setPrefs({ ...prefs, fontFamily: v })}
          >
            <SelectTrigger>
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {[
                "system",
                "Cairo",
                "Tajawal",
                "Noto Naskh Arabic",
                "Amiri",
              ].map((f) => (
                <SelectItem key={f} value={f}>
                  {f}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </FormRow>
        <FormRow label={`${t("executiveMeetings.fontSettingsPage.fontSize")}: ${prefs.fontSize}px`}>
          <input
            type="range"
            min={12}
            max={22}
            value={prefs.fontSize}
            onChange={(e) => setPrefs({ ...prefs, fontSize: Number(e.target.value) })}
            className="w-full"
          />
        </FormRow>
        <FormRow label={t("executiveMeetings.fontSettingsPage.fontWeight")}>
          <Select
            value={prefs.fontWeight}
            onValueChange={(v) => setPrefs({ ...prefs, fontWeight: v as FontPrefs["fontWeight"] })}
          >
            <SelectTrigger>
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {(["regular", "bold"] as const).map((w) => (
                <SelectItem key={w} value={w}>
                  {t(`executiveMeetings.fontSettingsPage.weight.${w}`)}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </FormRow>
        <FormRow label={t("executiveMeetings.fontSettingsPage.alignment")}>
          <Select
            value={prefs.alignment}
            onValueChange={(v) => setPrefs({ ...prefs, alignment: v as FontPrefs["alignment"] })}
          >
            <SelectTrigger>
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {(["start", "center"] as const).map((a) => (
                <SelectItem key={a} value={a}>
                  {t(`executiveMeetings.fontSettingsPage.align.${a}`)}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </FormRow>

        <div className="border-t border-gray-200 pt-3">
          <Label className="text-xs text-gray-700">
            {t("executiveMeetings.fontSettingsPage.preview")}
          </Label>
          <div
            className="mt-2 p-4 border border-gray-200 rounded bg-[#fafbff]"
            style={buildFontStyle(prefs)}
          >
            {t("executiveMeetings.fontSettingsPage.previewText")}
          </div>
        </div>

        <div className="flex justify-end gap-2 pt-2">
          <Button variant="ghost" onClick={reset}>
            {t("executiveMeetings.fontSettingsPage.reset")}
          </Button>
          <Button
            onClick={save}
            disabled={saving}
            className="bg-[#0B1E3F]"
            data-testid="em-font-save"
          >
            {t("executiveMeetings.fontSettingsPage.save")}
          </Button>
        </div>
      </div>
    </div>
  );
}

// ============ SHARED ============

function NoPermission({ t }: { t: (k: string) => string }) {
  return (
    <div className="bg-white border border-gray-200 rounded-md p-10 sm:p-16 text-center">
      <div className="w-14 h-14 mx-auto mb-4 rounded-full bg-[#0B1E3F]/10 flex items-center justify-center text-[#0B1E3F]">
        <SettingsIcon className="w-6 h-6" />
      </div>
      <p className="text-sm text-gray-600 max-w-md mx-auto">
        {t("executiveMeetings.common.noPermission")}
      </p>
    </div>
  );
}
