import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from "react";
import { useSearchParams } from "react-router-dom";
import {
  Bell,
  CalendarCheck,
  Check,
  Clock,
  Copy,
  Link2Off,
  Pencil,
  Plus,
  RefreshCw,
  Save,
  Send,
  XCircle
} from "lucide-react";
import {
  Button,
  Card,
  ConfirmDialog,
  DataGrid,
  DataGridRowActions,
  DateTimeField,
  DismissibleAlert,
  FormField,
  MetricCard,
  IconButton,
  PageTitle,
  PasswordField,
  PeoplePicker,
  formatDateTime,
  SelectionList,
  SelectionListItem,
  TableActionGroup,
  ToggleSwitch,
  StatusBadge,
  hasScope,
  i18nMessage,
  isApiError,
  usePlatformLanguage,
  usePlatformUiCapability,
  useUnsavedChanges,
  useUnsavedDraftGuard,
  type ApiSettings,
  type AuthInfo,
  type CalendarPickerUiCapability,
  type DataGridColumn,
  type FormatDateTimeOptions,
  type PeoplePickerItem,
  type PeoplePickerSearch
} from "@govoplan/core-webui";
import {
  closeSchedulingRequest,
  createSchedulingCalendarEvent,
  createSchedulingHolds,
  createSchedulingNotifications,
  createSchedulingRequest,
  decideSchedulingRequest,
  evaluateSchedulingFreeBusy,
  getSchedulingAvailabilityResponse,
  issueSchedulingParticipantInvitation,
  listSchedulingNotifications,
  listSchedulingRequests,
  openSchedulingRequest,
  revokeSchedulingParticipantInvitation,
  searchSchedulingPeople,
  schedulingSummary,
  submitSchedulingAvailability,
  updateSchedulingRequest,
  type SchedulingCandidateSlot,
  type SchedulingAvailabilityValue,
  type SchedulingInvitationActionResponse,
  type SchedulingNotification,
  type SchedulingParticipant,
  type SchedulingPollOptionResult,
  type SchedulingRequest,
  type SchedulingSummaryResponse
} from "../../api/scheduling";
import {
  applySchedulingAvailabilityChoice,
  groupSchedulingRequests,
  schedulingParticipantForActor,
  participantDraftFromResponse,
  participantDraftsFromPicker,
  participantPayload,
  schedulingInvitationActionBlocks,
  schedulingPublicInvitationUrl,
  schedulingRelevantTimestamp,
  schedulingRequestIsOwned,
  schedulingSortPhase,
  type SchedulingActor,
  type SchedulingInvitationActionBlock,
  type SchedulingParticipantDraft,
  type SchedulingRequestGroups
} from "./schedulingViewModel";

type EditorMode = "create" | "edit" | null;
type SlotDraft = {
  draftId: string;
  sourceId?: string;
  revision?: string;
  label: string;
  description?: string | null;
  start_at: string;
  end_at: string;
  timezone: string;
  metadata?: Record<string, unknown>;
};
type ParticipantDraft = SchedulingParticipantDraft;
type InvitationRevokeTarget = {
  requestId: string;
  participant: SchedulingParticipant;
};

const I18N = {
  actions: "i18n:govoplan-core.actions.c3cd636a",
  add: "i18n:govoplan-core.add.61cc55aa",
  addRequest: "i18n:govoplan-scheduling.add_scheduling_request.3c71be15",
  addParticipant: "i18n:govoplan-scheduling.add_participant.6cff957d",
  addSlot: "i18n:govoplan-scheduling.add_slot.9fcff3ed",
  answered: "i18n:govoplan-scheduling.answered.e0aafffa",
  available: "i18n:govoplan-scheduling.available.7c62a142",
  awaitingResponse: "i18n:govoplan-scheduling.awaiting_response.ee646ea9",
  basicInformation: "i18n:govoplan-scheduling.basic_information.d8bc7383",
  busy: "i18n:govoplan-scheduling.busy.592a9d16",
  calendar: "i18n:govoplan-scheduling.calendar.adab5090",
  calendarCoordination: "i18n:govoplan-scheduling.calendar_coordination.291ab00a",
  configuredCalendar: "i18n:govoplan-scheduling.configured_calendar.e2e8ebd5",
  calendarDescription: "i18n:govoplan-scheduling.use_a_calendar_for_availability_checks_tentative_holds_a.20ccc1fa",
  finalEventLabel: "i18n:govoplan-scheduling.create_calendar_event.0b87cfcf",
  calendarIntegration: "i18n:govoplan-scheduling.calendar_integration.181ad18b",
  calendarUnavailable: "i18n:govoplan-scheduling.calendar_integration_requires_the_calendar_module_plus_c.f892cb1e",
  candidateAvailability: "i18n:govoplan-scheduling.candidate_availability.9541c4b5",
  candidateSlots: "i18n:govoplan-scheduling.candidate_slots.c414946b",
  cancellationNoticeExpired: "i18n:govoplan-scheduling.the_cancellation_notice_has_expired_a_new_link_cannot_be_issued.9c6ccc7c",
  checkFreeBusy: "i18n:govoplan-scheduling.check_free_busy.e9700e00",
  chooseAvailability: "i18n:govoplan-scheduling.choose_availability.ac95b8f6",
  clipboardUnavailable: "i18n:govoplan-scheduling.the_invitation_link_could_not_be_copied_check_browser_clipboard_permissions_and_try_again.a8b17cbc",
  closePoll: "i18n:govoplan-scheduling.close_poll.a6a18916",
  closed: "i18n:govoplan-scheduling.closed.88d86b77",
  copyInvitationLink: "i18n:govoplan-scheduling.copy_a_fresh_invitation_link_for_value0.e3799c79",
  description: "i18n:govoplan-scheduling.description.55f8ebc8",
  determined: "i18n:govoplan-scheduling.determined.9f23293d",
  decideUnavailable: "i18n:govoplan-scheduling.a_slot_can_be_selected_after_the_request_is_closed.f91ec02d",
  discard: "i18n:govoplan-scheduling.discard.36fff63c",
  discardConfirm: "i18n:govoplan-scheduling.discard_this_unsaved_scheduling_request.4a956be2",
  edit: "i18n:govoplan-scheduling.edit_scheduling_request.7e749c19",
  invitedIdentityLocked: "i18n:govoplan-scheduling.invited_participant_identity_is_locked_remove_and_add_the_participant_to_change_it.34e1201a",
  end: "i18n:govoplan-scheduling.end.a2bb9d34",
  generalSettings: "i18n:govoplan-scheduling.participation_settings.8dc6f62c",
  free: "i18n:govoplan-scheduling.free.75f52718",
  holds: "i18n:govoplan-scheduling.create_tentative_holds.51c4744e",
  invitationDeliveryUnavailable: "i18n:govoplan-scheduling.automatic_invitation_delivery_is_unavailable_copy_the_link_instead.4e39d0b3",
  invitationDeliveryFailed: "i18n:govoplan-scheduling.invitation_delivery_failed_the_link_was_created_but_was_not_delivered.8db0c306",
  invitationDeliveryRequested: "i18n:govoplan-scheduling.invitation_delivery_requested.1aaa78ba",
  invitationChanged: "i18n:govoplan-scheduling.this_invitation_changed_the_request_was_reloaded_try_again.c7095533",
  invitedRequests: "i18n:govoplan-scheduling.scheduling_requests_for_me.1d521aba",
  invitationLinkCopied: "i18n:govoplan-scheduling.invitation_link_copied.332973ec",
  invitationLinkRevoked: "i18n:govoplan-scheduling.invitation_link_revoked.c7dd20d4",
  loading: "i18n:govoplan-scheduling.loading_scheduling_requests.f42be95d",
  location: "i18n:govoplan-scheduling.location.d219c681",
  allowComments: "i18n:govoplan-scheduling.allow_comments.d63202a6",
  allowCommentsHelp: "i18n:govoplan-scheduling.let_participants_add_a_comment_to_their_response.9dce8d17",
  allowMaybe: "i18n:govoplan-scheduling.provide_a_maybe_option.e39da57a",
  allowMaybeHelp: "i18n:govoplan-scheduling.add_maybe_between_yes_and_no_for_each_candidate_slot.74dc9db6",
  anonymousEmailRequired: "i18n:govoplan-scheduling.require_an_email_address_from_guests.c2289a58",
  anonymousEmailRequiredHelp: "i18n:govoplan-scheduling.people_responding_without_an_account_must_provide_an_email.19fd3dc8",
  anonymousPassword: "i18n:govoplan-scheduling.guest_password.94545e82",
  anonymousPasswordHelp: "i18n:govoplan-scheduling.use_at_least_8_characters_the_password_is_never_displayed.035708a2",
  anonymousPasswordProtection: "i18n:govoplan-scheduling.password_protect_guest_access.13d7f08b",
  anonymousPasswordProtectionHelp: "i18n:govoplan-scheduling.people_who_are_not_signed_in_must_enter_this_password.82bcc4ce",
  comment: "i18n:govoplan-scheduling.comment.d03495b1",
  maxParticipants: "i18n:govoplan-scheduling.limit_participants_per_option.1e9aa51d",
  maxParticipantsCount: "i18n:govoplan-scheduling.maximum_participants_per_option.5abdfb27",
  maxParticipantsHelp: "i18n:govoplan-scheduling.stop_accepting_yes_responses_for_an_option_when_its_limit.79af21db",
  managedRequests: "i18n:govoplan-scheduling.managed_scheduling_requests.7a45972f",
  maybe: "i18n:govoplan-scheduling.maybe.56dd8d0b",
  myRequests: "i18n:govoplan-scheduling.my_scheduling_requests.d28ef235",
  name: "i18n:govoplan-scheduling.name.709a2322",
  newRequest: "i18n:govoplan-scheduling.new_scheduling_request.2080f675",
  noActiveInvitation: "i18n:govoplan-scheduling.no_active_invitation_link_to_revoke.4ad0f0cc",
  noDeliveryTarget: "i18n:govoplan-scheduling.this_participant_has_no_deliverable_email_address_or_account.dbe14180",
  noNotifications: "i18n:govoplan-scheduling.no_notifications_have_been_created.c8d43ca3",
  notificationsUnavailable: "i18n:govoplan-scheduling.notifications_could_not_be_loaded.f0e1b2c3",
  noParticipants: "i18n:govoplan-scheduling.no_participants.73a89101",
  noRequest: "i18n:govoplan-scheduling.no_scheduling_request_selected.ac940664",
  noRequestsInGroup: "i18n:govoplan-scheduling.no_requests_in_this_group.a63c1b40",
  notifications: "i18n:govoplan-scheduling.notifications.753a22b2",
  notifyOnAnswers: "i18n:govoplan-scheduling.notify_me_about_each_answer.505749d6",
  notifyOnAnswersHelp: "i18n:govoplan-scheduling.create_an_organizer_notification_whenever_a_response_is.253ddca8",
  open: "i18n:govoplan-scheduling.open.cf9b7706",
  openPoll: "i18n:govoplan-scheduling.open_poll.2beac9a7",
  participant: "i18n:govoplan-scheduling.participant.554f4235",
  participantEmail: "i18n:govoplan-scheduling.participant_email.2cadfd9e",
  participantRosterHidden: "i18n:govoplan-scheduling.participant_names_and_statuses_are_hidden_aggregate_counts_r.d811a69a",
  participantPrivacy: "i18n:govoplan-scheduling.participant_privacy.108c470f",
  participantRosterVisibility: "i18n:govoplan-scheduling.share_participant_names_and_response_statuses.df0bf9e0",
  participantRosterVisibilityHelp: "i18n:govoplan-scheduling.when_enabled_participants_can_see_other_participants_names.3ac78361",
  participants: "i18n:govoplan-scheduling.participants.cd56e083",
  participantPickerHelp: "i18n:govoplan-scheduling.search_visible_accounts_and_contacts_or_add_an_external_perso.877f6b44",
  allowExternalParticipants: "i18n:govoplan-scheduling.allow_external_participants.a9efcb52",
  allowExternalParticipantsHelp: "i18n:govoplan-scheduling.when_enabled_people_outside_the_configured_accounts_and.78829735",
  participation: "i18n:govoplan-scheduling.participation.9ad70cc4",
  participationRate: "i18n:govoplan-scheduling.participation_rate.46bc5504",
  responses: "i18n:govoplan-scheduling.responses.3427e3ab",
  awaiting: "i18n:govoplan-scheduling.awaiting.42aa82e0",
  capacity: "i18n:govoplan-scheduling.capacity.d3c375f8",
  past: "i18n:govoplan-scheduling.past.405c12fb",
  refresh: "i18n:govoplan-scheduling.refresh_requests.0a3ed7a1",
  reloadInvitation: "i18n:govoplan-scheduling.reload_the_request_before_changing_this_invitation.9e685df4",
  reminder: "i18n:govoplan-scheduling.send_reminder.cf5eb3bf",
  revokeInvitation: "i18n:govoplan-scheduling.revoke_the_invitation_link_for_value0.15a9c9fa",
  revokeInvitationConfirm: "i18n:govoplan-scheduling.revoke_the_current_invitation_link_for_value0_it_will_stop_working_immediately.3cdc5817",
  revokeInvitationLabel: "i18n:govoplan-scheduling.revoke_invitation_link.87bf89cf",
  revokeLink: "i18n:govoplan-scheduling.revoke_link.da371ee1",
  requestFailed: "i18n:govoplan-scheduling.request_failed.9fcda32c",
  publicPolicyUnavailable: "i18n:govoplan-scheduling.guest_links_are_not_issued_while_the_configured_participation.0794ebf0",
  policyLocked: "i18n:govoplan-scheduling.participation_controls_are_locked_after_invitation_links_are_issued.66f5a740",
  requests: "i18n:govoplan-scheduling.scheduling_requests.b3c12f4d",
  requiresAvailabilityRead: "i18n:govoplan-scheduling.requires_calendar_availability_read_access.b48ed91b",
  requiresEventWrite: "i18n:govoplan-scheduling.requires_calendar_event_write_access.887b0763",
  responseRecorded: "i18n:govoplan-scheduling.your_response_has_been_recorded.b855088d",
  responseReplace: "i18n:govoplan-scheduling.the_response_replaces_your_previous_availability_choices.74c16d53",
  resultsUnavailable: "i18n:govoplan-scheduling.response_results_are_not_available_for_this_view.1e82db18",
  invitationHelp: "i18n:govoplan-scheduling.you_can_respond_here_or_use_the_invitation_link_you_rece.1a25fd53",
  save: "i18n:govoplan-scheduling.save.efc007a3",
  saving: "i18n:govoplan-scheduling.saving.56a2285c",
  selectCalendar: "i18n:govoplan-scheduling.select_a_calendar.f6af95bb",
  selectResponse: "i18n:govoplan-scheduling.choose_availability_for_at_least_one_candidate_slot.28d2111f",
  unsavedResponse: "i18n:govoplan-scheduling.save_or_discard_your_unsent_availability_changes_before_leaving.97e10df1",
  singleChoice: "i18n:govoplan-scheduling.participants_can_choose_only_one_option.4311f51c",
  singleChoiceHelp: "i18n:govoplan-scheduling.each_participant_can_answer_yes_to_at_most_one_candidate.5313a465",
  sendInvitation: "i18n:govoplan-scheduling.send_a_fresh_invitation_to_value0.fd8d9dea",
  sendResponse: "i18n:govoplan-scheduling.submit_response.a5f0c053",
  start: "i18n:govoplan-scheduling.start.952f3754",
  statusLabel: "i18n:govoplan-scheduling.status.bae7d5be",
  title: "i18n:govoplan-scheduling.title.768e0c1c",
  titleRequired: "i18n:govoplan-scheduling.scheduling_request_title_is_required.a27338be",
  unchecked: "i18n:govoplan-scheduling.unchecked.1b927dec",
  unavailable: "i18n:govoplan-scheduling.unavailable.2c9c1f79",
  updateResponse: "i18n:govoplan-scheduling.update_response.346233cf",
  updateResponseHelp: "i18n:govoplan-scheduling.responses_may_be_updated_while_this_request_remains_open.4faecbbe",
  whatActionsDo: "i18n:govoplan-scheduling.what_do_these_actions_do.9a9aee0e"
} as const;

export default function SchedulingPage({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
  const { translateText } = usePlatformLanguage();
  const { requestDiscard, requestNavigation } = useUnsavedChanges();
  const [searchParams, setSearchParams] = useSearchParams();
  const requestedId = searchParams.get("request_id") ?? "";
  const calendarPickerCapability = usePlatformUiCapability<CalendarPickerUiCapability>("calendar.picker");
  const CalendarPicker = calendarPickerCapability?.CalendarPicker;
  const [requests, setRequests] = useState<SchedulingRequest[]>([]);
  const [selectedId, setSelectedId] = useState("");
  const [editorMode, setEditorMode] = useState<EditorMode>(null);
  const [editingRequestId, setEditingRequestId] = useState("");
  const [summary, setSummary] = useState<SchedulingSummaryResponse | null>(null);
  const [notifications, setNotifications] = useState<SchedulingNotification[]>([]);
  const [loading, setLoading] = useState(true);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState("");
  const [success, setSuccess] = useState("");
  const [draftDirty, setDraftDirty] = useState(false);
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [location, setLocation] = useState("");
  const [calendarId, setCalendarId] = useState("");
  const [calendarEnabled, setCalendarEnabled] = useState(false);
  const [slots, setSlots] = useState<SlotDraft[]>(() => initialSlots(translateText));
  const [participants, setParticipants] = useState<ParticipantDraft[]>([]);
  const [allowExternalParticipants, setAllowExternalParticipants] = useState(true);
  const [participantRosterVisible, setParticipantRosterVisible] = useState(false);
  const [notifyOnAnswers, setNotifyOnAnswers] = useState(true);
  const [singleChoice, setSingleChoice] = useState(false);
  const [participantLimitEnabled, setParticipantLimitEnabled] = useState(false);
  const [maxParticipantsPerOption, setMaxParticipantsPerOption] = useState(1);
  const [allowMaybe, setAllowMaybe] = useState(true);
  const [allowComments, setAllowComments] = useState(false);
  const [participantEmailRequired, setParticipantEmailRequired] = useState(false);
  const [anonymousPasswordProtectionEnabled, setAnonymousPasswordProtectionEnabled] = useState(false);
  const [anonymousPassword, setAnonymousPassword] = useState("");
  const [availability, setAvailability] = useState<Record<string, SchedulingAvailabilityValue | "">>({});
  const [savedAvailability, setSavedAvailability] = useState<Record<string, SchedulingAvailabilityValue | "">>({});
  const [availabilityComment, setAvailabilityComment] = useState("");
  const [savedAvailabilityComment, setSavedAvailabilityComment] = useState("");
  const [availabilityLoading, setAvailabilityLoading] = useState(false);
  const [summaryUnavailable, setSummaryUnavailable] = useState(false);
  const [notificationsUnavailable, setNotificationsUnavailable] = useState(false);
  const [detailsLoading, setDetailsLoading] = useState(false);
  const [revokeInvitationTarget, setRevokeInvitationTarget] = useState<InvitationRevokeTarget | null>(null);
  const [invitationActionClock, setInvitationActionClock] = useState(() => new Date());
  const detailLoadSequence = useRef(0);

  const canWrite = hasScope(auth, "scheduling:schedule:write");
  const canAdminister = hasScope(auth, "scheduling:schedule:admin");
  const canCreateOrWrite = canWrite || canAdminister;
  const canRespond = hasScope(auth, "scheduling:availability:write");
  const canReadCalendars = hasScope(auth, "calendar:calendar:read");
  const canReadAvailability = hasScope(auth, "calendar:availability:read");
  const canWriteCalendarEvent = hasScope(auth, "calendar:event:write");
  const calendarIntegrationAvailable = Boolean(calendarPickerCapability) && canReadCalendars && canReadAvailability && canWriteCalendarEvent;
  const actor = useMemo<SchedulingActor>(() => ({
    accountId: auth.principal?.account_id || auth.user.account_id,
    userId: auth.user.id,
    membershipId: auth.principal?.membership_id,
    identityId: auth.principal?.identity_id,
    email: auth.principal?.email || auth.user.email
  }), [auth.principal, auth.user.account_id, auth.user.email, auth.user.id]);
  const groups = useMemo(() => groupSchedulingRequests(requests, actor), [actor, requests]);
  const selected = useMemo(
    () => requests.find((item) => item.id === selectedId) ?? firstRequest(groups, canAdminister),
    [canAdminister, groups, requests, selectedId]
  );
  const selectedParticipant = useMemo(
    () => selected ? schedulingParticipantForActor(selected, actor) : null,
    [actor, selected]
  );
  const canManageSelected = Boolean(
    selected && (canAdminister || (canWrite && schedulingRequestIsOwned(selected, actor)))
  );
  const canEditSelected = Boolean(
    selected && canManageSelected && !["decided", "handed_off", "cancelled", "archived"].includes(selected.status)
  );
  const selectedPhase = selected ? schedulingSortPhase(selected, actor) : null;
  const showPlanningCalendarActions = Boolean(
    selected &&
    selected.calendar_integration_enabled &&
    canManageSelected &&
    selectedPhase !== "past" &&
    ["draft", "collecting", "closed"].includes(selected.status) &&
    !selected.selected_slot_id
  );
  const showFinalCalendarAction = Boolean(
    selected &&
    selected.calendar_integration_enabled &&
    canManageSelected &&
    selectedPhase !== "past" &&
    selected.status === "decided" &&
    selected.selected_slot_id &&
    !selected.calendar_event_id &&
    selected.create_calendar_event_on_decision
  );
  const optionResultById = useMemo(() => {
    const map = new Map<string, SchedulingSummaryResponse["poll_summary"]["option_results"][number]>();
    for (const result of summary?.poll_summary.option_results ?? []) {
      map.set(result.option_id, result);
      map.set(result.option_key, result);
    }
    return map;
  }, [summary]);
  const responseDirty = useMemo(
    () => availabilityComment !== savedAvailabilityComment || !availabilityValuesEqual(availability, savedAvailability),
    [availability, availabilityComment, savedAvailability, savedAvailabilityComment]
  );
  const editorOriginal = editorMode === "edit"
    ? requests.find((request) => request.id === editingRequestId) ?? null
    : null;
  const participationPolicyLocked = Boolean(
    editorOriginal?.participants.some((participant) => participant.poll_invitation_id)
  );
  const participantSearch = useCallback<PeoplePickerSearch>(
    (query, options) => searchSchedulingPeople(settings, query, options.limit, options.signal),
    [settings]
  );

  useEffect(() => {
    void loadRequests();
  }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);

  useEffect(() => {
    if (!requestedId || requestedId === selectedId) return;
    if (requests.some((request) => request.id === requestedId)) setSelectedId(requestedId);
  }, [requestedId, requests, selectedId]);

  useEffect(() => {
    if (!selected?.id || editorMode) {
      detailLoadSequence.current += 1;
      setSummary(null);
      setNotifications([]);
      setSummaryUnavailable(false);
      setNotificationsUnavailable(false);
      setDetailsLoading(false);
      return;
    }
    setAvailability({});
    setSavedAvailability({});
    setAvailabilityComment("");
    setSavedAvailabilityComment("");
    setSuccess("");
    void loadDetails(selected.id);
  }, [editorMode, selected?.id]);

  useEffect(() => {
    setInvitationActionClock(new Date());
    if (selected?.status !== "cancelled" || !selected.cancellation_notice_until) return undefined;
    const expiresAt = Date.parse(selected.cancellation_notice_until);
    if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return undefined;
    let timer: number | undefined;
    const scheduleExpiryRefresh = () => {
      const remaining = expiresAt - Date.now();
      if (remaining <= 0) {
        setInvitationActionClock(new Date());
        return;
      }
      timer = window.setTimeout(
        scheduleExpiryRefresh,
        Math.min(remaining + 25, 2_147_483_647)
      );
    };
    scheduleExpiryRefresh();
    return () => {
      if (timer !== undefined) window.clearTimeout(timer);
    };
  }, [selected?.cancellation_notice_until, selected?.status]);

  useEffect(() => {
    if (!selected?.id || !selectedParticipant || !canRespond || editorMode || selected.status !== "collecting") {
      setAvailabilityLoading(false);
      return undefined;
    }
    let cancelled = false;
    setAvailabilityLoading(true);
    void getSchedulingAvailabilityResponse(settings, selected.id)
      .then((response) => {
        if (cancelled) return;
        const answers = Object.fromEntries(response.answers.map((answer) => [answer.slot_id, answer.value]));
        const comment = response.comment ?? "";
        setAvailability(answers);
        setSavedAvailability(answers);
        setAvailabilityComment(comment);
        setSavedAvailabilityComment(comment);
      })
      .catch((err) => {
        if (!cancelled) setError(errorMessage(err, translateText(I18N.requestFailed)));
      })
      .finally(() => {
        if (!cancelled) setAvailabilityLoading(false);
      });
    return () => {
      cancelled = true;
    };
  }, [
    canRespond,
    editorMode,
    selected?.id,
    selected?.status,
    selectedParticipant?.id,
    settings.accessToken,
    settings.apiBaseUrl,
    settings.apiKey,
    translateText
  ]);

  useUnsavedDraftGuard({
    dirty: Boolean(editorMode && draftDirty),
    message: I18N.discardConfirm,
    onSave: persistDraft,
    onDiscard: resetDraft,
    enabled: Boolean(editorMode)
  });

  useUnsavedDraftGuard({
    dirty: responseDirty,
    message: I18N.unsavedResponse,
    onSave: persistAvailability,
    onDiscard: resetResponseDraft,
    enabled: Boolean(!editorMode && selectedParticipant && selected?.status === "collecting")
  });

  async function loadRequests(preferredId?: string) {
    setLoading(true);
    setError("");
    try {
      const response = await listSchedulingRequests(settings);
      setRequests(response.requests);
      setSelectedId((current) => {
        if (preferredId && response.requests.some((item) => item.id === preferredId)) return preferredId;
        if (response.requests.some((item) => item.id === current)) return current;
        const requested = response.requests.find((item) => item.id === requestedId);
        if (requested) return requested.id;
        const nextGroups = groupSchedulingRequests(response.requests, actor);
        return firstRequest(nextGroups, canAdminister)?.id ?? "";
      });
    } catch (err) {
      setError(errorMessage(err, translateText(I18N.requestFailed)));
    } finally {
      setLoading(false);
    }
  }

  async function loadDetails(requestId: string) {
    const sequence = detailLoadSequence.current + 1;
    detailLoadSequence.current = sequence;
    setDetailsLoading(true);
    const [summaryResult, notificationResult] = await Promise.allSettled([
      schedulingSummary(settings, requestId),
      listSchedulingNotifications(settings, requestId)
    ]);
    if (detailLoadSequence.current !== sequence) return;
    if (summaryResult.status === "fulfilled") {
      setSummary(summaryResult.value);
      setSummaryUnavailable(false);
    } else {
      setSummary(null);
      setSummaryUnavailable(true);
    }
    if (notificationResult.status === "fulfilled") {
      setNotifications(notificationResult.value.notifications);
      setNotificationsUnavailable(false);
    } else {
      setNotifications([]);
      setNotificationsUnavailable(true);
    }
    setDetailsLoading(false);
  }

  function beginCreate() {
    requestNavigation(startCreate);
  }

  function startCreate() {
    resetDraft();
    setError("");
    setSuccess("");
    setEditingRequestId("");
    setEditorMode("create");
  }

  function beginEdit(request: SchedulingRequest) {
    requestNavigation(() => {
      setTitle(request.title);
      setDescription(request.description ?? "");
      setLocation(request.location ?? "");
      setCalendarId(request.calendar_id ?? "");
      setCalendarEnabled(Boolean(request.calendar_integration_enabled));
      setSlots(request.slots.map((slot) => ({
        draftId: nextDraftId("slot"),
        sourceId: slot.id,
        revision: slot.revision,
        label: slot.label,
        description: slot.description,
        start_at: localValue(new Date(slot.start_at)),
        end_at: localValue(new Date(slot.end_at)),
        timezone: slot.timezone,
        metadata: slot.metadata
      })));
      setParticipants(request.participants.map((participant) => (
        participantDraftFromResponse(participant, nextDraftId("participant"))
      )));
      setAllowExternalParticipants(request.allow_external_participants);
      setParticipantRosterVisible(request.participant_visibility === "names_and_statuses");
      setNotifyOnAnswers(request.notify_on_answers);
      setSingleChoice(request.single_choice);
      setParticipantLimitEnabled(request.max_participants_per_option !== null);
      setMaxParticipantsPerOption(request.max_participants_per_option ?? 1);
      setAllowMaybe(request.allow_maybe);
      setAllowComments(request.allow_comments);
      setParticipantEmailRequired(request.participant_email_required);
      setAnonymousPasswordProtectionEnabled(request.anonymous_password_protection_enabled);
      setAnonymousPassword("");
      setError("");
      setSuccess("");
      setEditingRequestId(request.id);
      setDraftDirty(false);
      setEditorMode("edit");
    });
  }

  function discardEditor() {
    if (saving) return;
    requestDiscard(exitEditor);
  }

  function exitEditor() {
    resetDraft();
    setEditingRequestId("");
    setEditorMode(null);
  }

  async function persistDraft(): Promise<boolean> {
    if (!canCreateOrWrite || !editorMode) return false;
    setError("");
    if (!title.trim()) {
      setError(I18N.titleRequired);
      return false;
    }
    if (calendarEnabled && !calendarId) {
      setError("i18n:govoplan-scheduling.select_a_calendar_or_turn_off_calendar_integration.cc3652a9");
      return false;
    }
    if (slots.some((slot) => !slot.start_at || !slot.end_at || new Date(slot.end_at) <= new Date(slot.start_at))) {
      setError("i18n:govoplan-scheduling.every_candidate_slot_must_end_after_it_starts.47836010");
      return false;
    }
    const original = editorMode === "edit"
      ? requests.find((request) => request.id === editingRequestId)
      : null;
    if (
      anonymousPasswordProtectionEnabled &&
      anonymousPassword.length < 8 &&
      !(original?.anonymous_password_protection_enabled && anonymousPassword.length === 0)
    ) {
      setError(I18N.anonymousPasswordHelp);
      return false;
    }
    setSaving(true);
    try {
      const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
      const commonPayload = {
        title: title.trim(),
        description: description.trim() || null,
        location: location.trim() || null,
        allow_external_participants: allowExternalParticipants,
        participant_visibility: participantRosterVisible ? "names_and_statuses" : "aggregates_only",
        notify_on_answers: notifyOnAnswers,
        single_choice: singleChoice,
        max_participants_per_option: participantLimitEnabled ? maxParticipantsPerOption : null,
        allow_maybe: allowMaybe,
        allow_comments: allowComments,
        participant_email_required: participantEmailRequired,
        anonymous_password_protection_enabled: anonymousPasswordProtectionEnabled,
        ...(anonymousPassword ? { anonymous_password: anonymousPassword } : {}),
        calendar: {
          enabled: calendarEnabled,
          calendar_id: calendarEnabled ? calendarId : null,
          freebusy_enabled: calendarEnabled,
          tentative_holds_enabled: calendarEnabled,
          create_event_on_decision: calendarEnabled
        }
      } as const;
      let request: SchedulingRequest;
      if (editorMode === "create") {
        request = await createSchedulingRequest(settings, {
          ...commonPayload,
          timezone,
          status: "draft",
          slots: slots.map((slot) => ({
            label: slot.label.trim() || null,
            description: slot.description ?? null,
            start_at: isoFromLocal(slot.start_at),
            end_at: isoFromLocal(slot.end_at),
            timezone: slot.timezone || timezone,
            location: location.trim() || null,
            metadata: slot.metadata ?? {}
          })),
          participants: participants.map(participantPayload)
        });
        setRequests((items) => [request, ...items]);
      } else {
        if (!original) {
          setError(I18N.requestFailed);
          return false;
        }
        request = await updateSchedulingRequest(settings, original.id, {
          ...commonPayload,
          slots: slots.map((slot) => ({
            ...(slot.sourceId ? { id: slot.sourceId, revision: slot.revision } : {}),
            label: slot.label.trim(),
            description: slot.description ?? null,
            start_at: isoFromLocal(slot.start_at),
            end_at: isoFromLocal(slot.end_at),
            timezone: slot.timezone || timezone,
            location: location.trim() || null,
            metadata: slot.metadata ?? {}
          })),
          participants: participants.map(participantPayload)
        });
        replaceRequest(request);
      }
      setSelectedId(request.id);
      setSelectedRequestSearchParam(request.id);
      setDraftDirty(false);
      setEditingRequestId("");
      setEditorMode(null);
      await loadRequests(request.id);
      return true;
    } catch (err) {
      setError(errorMessage(err, translateText(I18N.requestFailed)));
      return false;
    } finally {
      setSaving(false);
    }
  }

  async function runAction(action: () => Promise<{ request?: SchedulingRequest } | unknown>) {
    if (!selected || !canManageSelected) return;
    setSaving(true);
    setError("");
    setSuccess("");
    try {
      const response = await action();
      const updated = isRequestEnvelope(response) ? response.request : null;
      if (updated) replaceRequest(updated);
      await loadRequests(selected.id);
      await loadDetails(selected.id);
    } catch (err) {
      setError(errorMessage(err, translateText(I18N.requestFailed)));
    } finally {
      setSaving(false);
    }
  }

  async function runParticipantInvitationAction(
    requestId: string,
    participant: SchedulingParticipant,
    action: "copy" | "send" | "revoke"
  ): Promise<boolean> {
    const request = requests.find((item) => item.id === requestId);
    if (!request || !(canAdminister || (canWrite && schedulingRequestIsOwned(request, actor)))) return false;
    if (!participant.revision) {
      setError(I18N.reloadInvitation);
      return false;
    }
    let succeeded = false;
    let failureMessage = "";
    let completionMessage = "";
    setSaving(true);
    setError("");
    setSuccess("");
    try {
      let copied = true;
      let result: SchedulingInvitationActionResponse;
      if (action === "copy") {
        const copyResult = await copySchedulingInvitationLink(
          issueSchedulingParticipantInvitation(
            settings,
            requestId,
            participant.id,
            participant.revision,
            action
          )
        );
        result = copyResult.response;
        copied = copyResult.copied;
      } else if (action === "send") {
        result = await issueSchedulingParticipantInvitation(
          settings,
          requestId,
          participant.id,
          participant.revision,
          action
        );
      } else {
        result = await revokeSchedulingParticipantInvitation(
          settings,
          requestId,
          participant.id,
          participant.revision
        );
      }
      if (action === "copy") {
        if (!copied) {
          throw new Error(translateText(I18N.clipboardUnavailable));
        }
        completionMessage = I18N.invitationLinkCopied;
      } else if (action === "send") {
        if (["failed", "skipped"].includes(result.status)) {
          throw new Error(translateText(I18N.invitationDeliveryFailed));
        }
        completionMessage = I18N.invitationDeliveryRequested;
      } else {
        completionMessage = I18N.invitationLinkRevoked;
      }
      succeeded = true;
    } catch (err) {
      if (isApiError(err, 409)) {
        failureMessage = I18N.invitationChanged;
      } else {
        failureMessage = errorMessage(err, translateText(I18N.requestFailed));
      }
    } finally {
      await loadRequests(requestId);
      await loadDetails(requestId);
      setSaving(false);
      if (failureMessage) setError(failureMessage);
      if (completionMessage) setSuccess(completionMessage);
    }
    return succeeded;
  }

  async function confirmRevokeInvitation() {
    if (!revokeInvitationTarget) return;
    const target = revokeInvitationTarget;
    setRevokeInvitationTarget(null);
    await runParticipantInvitationAction(target.requestId, target.participant, "revoke");
  }

  async function sendAvailability(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    await persistAvailability();
  }

  async function persistAvailability(): Promise<boolean> {
    if (!selected || !selectedParticipant || !canRespond || availabilityLoading) return false;
    const answers = selected.slots
      .map((slot) => ({
        slot_id: slot.id,
        value: availability[slot.id],
        option_revision: slot.revision
      }))
      .filter((answer): answer is {
        slot_id: string;
        value: SchedulingAvailabilityValue;
        option_revision: string;
      } => Boolean(answer.value));
    if (!answers.length) {
      setError(I18N.selectResponse);
      return false;
    }
    setSaving(true);
    setError("");
    setSuccess("");
    try {
      const response = await submitSchedulingAvailability(settings, selected.id, {
        answers,
        ...(selected.allow_comments ? { comment: availabilityComment.trim() || null } : {})
      });
      replaceRequest(response.request);
      const persistedAvailability = Object.fromEntries(answers.map((answer) => [answer.slot_id, answer.value]));
      const persistedComment = selected.allow_comments ? availabilityComment.trim() : "";
      setAvailability(persistedAvailability);
      setSavedAvailability(persistedAvailability);
      setAvailabilityComment(persistedComment);
      setSavedAvailabilityComment(persistedComment);
      setSuccess(I18N.responseRecorded);
      await loadRequests(selected.id);
      return true;
    } catch (err) {
      setError(errorMessage(err, translateText(I18N.requestFailed)));
      return false;
    } finally {
      setSaving(false);
    }
  }

  return (
    <main className="scheduling-page">
      <section className="scheduling-workspace">
        <div className="scheduling-workspace-layout">
          <aside className="scheduling-request-sidebar">
            <Card
              title={I18N.requests}
              actions={(
                <div className="scheduling-sidebar-actions">
                  <IconButton
                    label={I18N.refresh}
                    icon={<RefreshCw aria-hidden="true" size={16} />}
                    onClick={() => requestNavigation(() => void loadRequests(selected?.id))}
                    disabled={loading || saving} />
                  {canCreateOrWrite ? (
                    <Button type="button" variant="primary" onClick={beginCreate} disabled={saving}>
                      <Plus aria-hidden="true" size={16} /> {I18N.add}
                    </Button>
                  ) : null}
                </div>
              )}>
              {loading ? <p className="scheduling-note" role="status">{I18N.loading}</p> : null}
              <RequestGroup
                title={I18N.myRequests}
                requests={groups.owned}
                selectedId={editorMode ? undefined : selected?.id}
                actor={actor}
                disabled={saving}
                onSelect={selectRequest} />
              <RequestGroup
                title={I18N.invitedRequests}
                requests={groups.invited}
                selectedId={editorMode ? undefined : selected?.id}
                actor={actor}
                disabled={saving}
                onSelect={selectRequest} />
              {canAdminister && groups.other.length ? (
                <RequestGroup
                  title={I18N.managedRequests}
                  requests={groups.other}
                  selectedId={editorMode ? undefined : selected?.id}
                  actor={actor}
                  disabled={saving}
                  onSelect={selectRequest} />
              ) : null}
            </Card>
          </aside>

          <section className="scheduling-main-panel">
            {error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
            {success ? <DismissibleAlert tone="success" resetKey={success}>{success}</DismissibleAlert> : null}

            {editorMode ? (
              <div className="scheduling-editor-surface">
                <header className="scheduling-page-header">
                  <div className="scheduling-page-title">
                    <CalendarCheck aria-hidden="true" size={20} />
                    <div>
                      <PageTitle>{editorMode === "create" ? I18N.newRequest : I18N.edit}</PageTitle>
                      <p>{editorMode === "create" ? I18N.addRequest : title}</p>
                    </div>
                  </div>
                  <div className="scheduling-page-actions">
                    <Button type="button" onClick={discardEditor} disabled={saving}>{I18N.discard}</Button>
                    <Button type="submit" form="scheduling-editor-form" variant="primary" disabled={saving || !canCreateOrWrite}>
                      <Save aria-hidden="true" size={16} /> {saving ? I18N.saving : I18N.save}
                    </Button>
                  </div>
                </header>

                <form
                  id="scheduling-editor-form"
                  className="scheduling-editor-scroll"
                  onSubmit={(event) => {
                    event.preventDefault();
                    void persistDraft();
                  }}>
                  <Card title={I18N.basicInformation}>
                    <div className="form-grid">
                      <FormField label={I18N.title}>
                        <input required maxLength={500} value={title} onChange={(event) => changeDraft(setTitle, event.target.value)} />
                      </FormField>
                      <FormField label={I18N.description}>
                        <textarea rows={4} value={description} onChange={(event) => changeDraft(setDescription, event.target.value)} />
                      </FormField>
                      <FormField label={I18N.location}>
                        <input maxLength={500} value={location} onChange={(event) => changeDraft(setLocation, event.target.value)} />
                      </FormField>
                    </div>
                  </Card>

                  <Card title={I18N.calendarIntegration}>
                    <ToggleSwitch
                      label={I18N.calendar}
                      help={I18N.calendarDescription}
                      checked={calendarEnabled}
                      disabled={!calendarIntegrationAvailable}
                      onChange={(checked) => {
                        setCalendarEnabled(checked);
                        if (!checked) setCalendarId("");
                        setDraftDirty(true);
                      }} />
                    {!calendarIntegrationAvailable ? <p className="scheduling-capability-note">{I18N.calendarUnavailable}</p> : null}
                    {calendarEnabled && CalendarPicker ? (
                      <CalendarPicker
                        settings={settings}
                        auth={auth}
                        value={calendarId}
                        onChange={(value) => {
                          setCalendarId(value);
                          setDraftDirty(true);
                        }}
                        label={I18N.calendar}
                        emptyLabel={I18N.selectCalendar}
                        required />
                    ) : null}
                  </Card>

                  <EditableSlots
                    slots={slots}
                    disabled={!canCreateOrWrite}
                    reorderable
                    onAddBelow={(index) => {
                      setSlots((items) => editorMode === "edit"
                        ? [...items, newSlot(items.length + 1, translateText)]
                        : insertAfter(items, index, newSlot(items.length + 1, translateText)));
                      setDraftDirty(true);
                    }}
                    onChange={changeSlotDraft}
                    onMove={(from, to) => {
                      setSlots((items) => moveItem(items, from, to));
                      setDraftDirty(true);
                    }}
                    onRemove={(index) => {
                      setSlots((items) => items.filter((_item, itemIndex) => itemIndex !== index));
                      setDraftDirty(true);
                    }} />

                  <EditableParticipants
                    participants={participants}
                    disabled={!canCreateOrWrite}
                    allowExternalParticipants={allowExternalParticipants}
                    search={participantSearch}
                    onAllowExternalParticipantsChange={(value) => {
                      setAllowExternalParticipants(value);
                      setDraftDirty(true);
                    }}
                    onChange={(value) => {
                      setParticipants((current) => participantDraftsFromPicker(
                        value,
                        current,
                        () => nextDraftId("participant")
                      ));
                      setDraftDirty(true);
                    }} />

                  <SchedulingPrivacySettings
                    disabled={!canCreateOrWrite}
                    participantRosterVisible={participantRosterVisible}
                    onChange={(value) => {
                      setParticipantRosterVisible(value);
                      setDraftDirty(true);
                    }} />

                  <SchedulingSettings
                    disabled={!canCreateOrWrite}
                    policyLocked={participationPolicyLocked}
                    notifyOnAnswers={notifyOnAnswers}
                    singleChoice={singleChoice}
                    participantLimitEnabled={participantLimitEnabled}
                    maxParticipantsPerOption={maxParticipantsPerOption}
                    allowMaybe={allowMaybe}
                    allowComments={allowComments}
                    participantEmailRequired={participantEmailRequired}
                    anonymousPasswordProtectionEnabled={anonymousPasswordProtectionEnabled}
                    anonymousPassword={anonymousPassword}
                    passwordRequired={anonymousPasswordProtectionEnabled && !(requests.find((request) => request.id === editingRequestId)?.anonymous_password_protection_enabled)}
                    onChange={(field, value) => {
                      setDraftDirty(true);
                      if (field === "notifyOnAnswers") setNotifyOnAnswers(Boolean(value));
                      if (field === "singleChoice") setSingleChoice(Boolean(value));
                      if (field === "participantLimitEnabled") setParticipantLimitEnabled(Boolean(value));
                      if (field === "maxParticipantsPerOption") setMaxParticipantsPerOption(Number(value));
                      if (field === "allowMaybe") setAllowMaybe(Boolean(value));
                      if (field === "allowComments") setAllowComments(Boolean(value));
                      if (field === "participantEmailRequired") setParticipantEmailRequired(Boolean(value));
                      if (field === "anonymousPasswordProtectionEnabled") {
                        setAnonymousPasswordProtectionEnabled(Boolean(value));
                        if (!value) setAnonymousPassword("");
                      }
                      if (field === "anonymousPassword") setAnonymousPassword(String(value));
                    }} />

                </form>
              </div>
            ) : selected ? (
              <div className="scheduling-detail">
                <Card
                  title={selected.title}
                  actions={(
                    <div className="scheduling-actions">
                      {canEditSelected ? (
                        <Button type="button" onClick={() => beginEdit(selected)} disabled={saving}>
                          <Pencil aria-hidden="true" size={16} /> {I18N.edit}
                        </Button>
                      ) : null}
                      {canManageSelected ? (
                        <LifecycleActions
                          request={selected}
                          saving={saving}
                          onOpen={() => void runAction(() => openSchedulingRequest(settings, selected.id))}
                          onClose={() => requestNavigation(() => void runAction(() => closeSchedulingRequest(settings, selected.id)))}
                          onReminder={() => void runAction(() => createSchedulingNotifications(settings, selected.id, "reminder"))} />
                      ) : null}
                    </div>
                  )}>
                  <div className="scheduling-status-row">
                    <StatusBadge status={selected.status} label={requestStatusLabel(selected, actor)} />
                  </div>
                  {selected.description ? <p>{selected.description}</p> : null}
                  {selected.location ? <p><strong>{I18N.location}:</strong> {selected.location}</p> : null}
                </Card>

                <ParticipationStats
                  request={selected}
                  responseCount={summary?.poll_summary.response_count
                    ?? selected.participant_aggregate.status_counts.responded
                    ?? null}
                  loading={detailsLoading} />

                {summaryUnavailable ? (
                  <DismissibleAlert tone="info" dismissible={false} compact>
                    {I18N.resultsUnavailable}
                  </DismissibleAlert>
                ) : null}

                {selected.public_participation_policy_enforcement_available === false ? (
                  <DismissibleAlert tone="warning" dismissible={false} compact>
                    {selected.public_participation_policy_enforcement_reason || I18N.publicPolicyUnavailable}
                  </DismissibleAlert>
                ) : null}

                {selectedParticipant && selected.status === "collecting" ? (
                  <Card
                    title={I18N.candidateAvailability}
                    actions={(
                      <Button
                        variant="primary"
                        type="submit"
                        form="scheduling-response-form"
                        disabled={saving || availabilityLoading || !canRespond || (selectedParticipant.status === "responded" && !selected.allow_participant_updates)}>
                        <Send aria-hidden="true" size={16} />
                        {selectedParticipant.status === "responded" ? I18N.updateResponse : I18N.sendResponse}
                      </Button>
                    )}>
                    <form
                      id="scheduling-response-form"
                      className="scheduling-response-card"
                      aria-busy={availabilityLoading}
                      onSubmit={(event) => void sendAvailability(event)}>
                      <p>{selectedParticipant.status === "responded"
                        ? (selected.allow_participant_updates ? I18N.responseReplace : I18N.responseRecorded)
                        : I18N.invitationHelp}</p>
                      {selectedParticipant.status === "responded" && selected.allow_participant_updates ? (
                        <p className="scheduling-capability-note">{I18N.updateResponseHelp}</p>
                      ) : null}
                      <div className="scheduling-response-grid">
                        {selected.slots.map((slot) => (
                          <label key={slot.id}>
                            <span><strong>{slot.label}</strong><small>{formatRange(slot.start_at, slot.end_at)}</small></span>
                            <select
                              value={availability[slot.id] ?? ""}
                              disabled={availabilityLoading || (selectedParticipant.status === "responded" && !selected.allow_participant_updates)}
                              onChange={(event) => changeAvailability(selected, slot.id, event.target.value as SchedulingAvailabilityValue | "")}>
                              <option value="">{I18N.chooseAvailability}</option>
                              <option value="available">{I18N.available}</option>
                              {selected.allow_maybe ? <option value="maybe">{I18N.maybe}</option> : null}
                              <option value="unavailable">{I18N.unavailable}</option>
                            </select>
                          </label>
                        ))}
                      </div>
                      {selected.allow_comments ? (
                        <FormField label={I18N.comment} help={I18N.allowCommentsHelp}>
                          <textarea
                            rows={3}
                            maxLength={4000}
                            value={availabilityComment}
                            disabled={availabilityLoading || (selectedParticipant.status === "responded" && !selected.allow_participant_updates)}
                            onChange={(event) => setAvailabilityComment(event.target.value)} />
                        </FormField>
                      ) : null}
                    </form>
                  </Card>
                ) : null}

                <Card title={I18N.candidateSlots}>
                  <CandidateSlotsGrid
                    slots={selected.slots}
                    optionResultById={optionResultById}
                    showDecisionAction={canManageSelected}
                    decisionEnabled={selected.status === "closed"}
                    resultsAvailable={!summaryUnavailable && !detailsLoading}
                    saving={saving}
                    allowMaybe={selected.allow_maybe}
                    maxParticipantsPerOption={selected.max_participants_per_option}
                    onDecide={(slot) => void runAction(() => decideSchedulingRequest(settings, selected.id, {
                      slot_id: slot.id,
                      handoff_to_calendar: selected.create_calendar_event_on_decision
                    }))} />
                </Card>

                {selected.calendar_integration_enabled && canManageSelected && (showPlanningCalendarActions || showFinalCalendarAction || selected.calendar_event_id) ? (
                  <Card
                    title={I18N.calendarCoordination}
                    actions={(
                      <div className="scheduling-card-actions">
                        {showPlanningCalendarActions ? (
                          <Button
                            type="button"
                            disabled={saving || !canReadAvailability}
                            disabledReason={!canReadAvailability ? I18N.requiresAvailabilityRead : undefined}
                            onClick={() => void runAction(() => evaluateSchedulingFreeBusy(settings, selected.id))}>
                            <RefreshCw aria-hidden="true" size={16} /> {I18N.checkFreeBusy}
                          </Button>
                        ) : null}
                        {showPlanningCalendarActions && selected.calendar_hold_enabled ? (
                          <Button
                            type="button"
                            disabled={saving || !canWriteCalendarEvent}
                            disabledReason={!canWriteCalendarEvent ? I18N.requiresEventWrite : undefined}
                            onClick={() => void runAction(() => createSchedulingHolds(settings, selected.id))}>
                            <Clock aria-hidden="true" size={16} /> {I18N.holds}
                          </Button>
                        ) : null}
                        {showFinalCalendarAction ? (
                          <Button
                            type="button"
                            disabled={saving || !canWriteCalendarEvent}
                            disabledReason={!canWriteCalendarEvent ? I18N.requiresEventWrite : undefined}
                            onClick={() => void runAction(() => createSchedulingCalendarEvent(settings, selected.id))}>
                            <CalendarCheck aria-hidden="true" size={16} /> {I18N.finalEventLabel}
                          </Button>
                        ) : null}
                      </div>
                    )}>
                    {CalendarPicker && selected.calendar_id ? (
                      <CalendarPicker
                        className="scheduling-selected-calendar"
                        settings={settings}
                        auth={auth}
                        value={selected.calendar_id}
                        onChange={() => undefined}
                        label={I18N.configuredCalendar}
                        disabled />
                    ) : <p>{I18N.configuredCalendar}</p>}
                  </Card>
                ) : null}

                <div className="scheduling-columns">
                  <Card title={I18N.participants}>
                    <div className="scheduling-status-row">
                      <span>{i18nMessage("i18n:govoplan-scheduling.value_participants.e776b092", { value0: selected.participant_aggregate.total })}</span>
                      {Object.entries(selected.participant_aggregate.status_counts)
                        .filter(([, count]) => count > 0)
                        .map(([status, count]) => (
                          <span key={status}><strong>{participantStatusLabel(status)}</strong> {count}</span>
                        ))}
                    </div>
                    {selected.effective_participant_visibility === "names_and_statuses" ? (
                      <ParticipantsGrid
                        request={selected}
                        participants={selected.participants}
                        canManage={canManageSelected}
                        saving={saving}
                        now={invitationActionClock}
                        onCopy={(participant) => void runParticipantInvitationAction(selected.id, participant, "copy")}
                        onSend={(participant) => void runParticipantInvitationAction(selected.id, participant, "send")}
                        onRevoke={(participant) => setRevokeInvitationTarget({
                          requestId: selected.id,
                          participant
                        })} />
                    ) : <p className="scheduling-capability-note">{I18N.participantRosterHidden}</p>}
                  </Card>
                  <Card title={I18N.notifications}>
                    {notificationsUnavailable ? (
                      <p className="scheduling-note">{I18N.notificationsUnavailable}</p>
                    ) : notifications.length ? notifications.map((notification) => (
                      <div className="scheduling-compact-row" key={notification.id}>
                        <span>{notificationKindLabel(notification.event_kind)}</span>
                        <StatusBadge status={notification.status} label={notificationStatusLabel(notification.status)} />
                      </div>
                    )) : <p className="scheduling-note">{I18N.noNotifications}</p>}
                  </Card>
                </div>

                <Card title={I18N.whatActionsDo} collapsible collapseKey="scheduling-action-help">
                  <ul className="scheduling-action-help">
                    <li>i18n:govoplan-scheduling.open_starts_collecting_availability_responses.c8ec34e5</li>
                    <li>i18n:govoplan-scheduling.close_stops_accepting_new_availability_responses.a1d57519</li>
                    <li>i18n:govoplan-scheduling.free_busy_checks_each_candidate_slot_against_the_selecte.50f0371a</li>
                    <li>i18n:govoplan-scheduling.tentative_holds_create_one_provisional_calendar_event_pe.ff3f1884</li>
                    <li>i18n:govoplan-scheduling.the_final_event_is_created_only_for_the_selected_slot.e3621252</li>
                    <li>i18n:govoplan-scheduling.reminder_creates_a_notification_job_for_every_active_par.7ec68797</li>
                  </ul>
                </Card>
              </div>
            ) : <div className="scheduling-empty">{I18N.noRequest}</div>}
          </section>
        </div>
      </section>
      <ConfirmDialog
        open={Boolean(revokeInvitationTarget)}
        title={I18N.revokeInvitationLabel}
        message={i18nMessage(I18N.revokeInvitationConfirm, {
          value0: participantDisplayLabel(revokeInvitationTarget?.participant ?? null)
        })}
        confirmLabel={I18N.revokeLink}
        tone="danger"
        busy={saving}
        onCancel={() => setRevokeInvitationTarget(null)}
        onConfirm={() => void confirmRevokeInvitation()} />
    </main>
  );

  function changeDraft(setter: (value: string) => void, value: string) {
    setter(value);
    setDraftDirty(true);
  }

  function changeSlotDraft(index: number, field: "label" | "start_at" | "end_at", value: string) {
    setSlots((items) => items.map((item, itemIndex) => {
      if (itemIndex !== index) return item;
      if (field === "start_at" && value && (!item.end_at || new Date(item.end_at) <= new Date(value))) {
        return { ...item, start_at: value, end_at: addLocalMinutes(value, 60) };
      }
      return { ...item, [field]: value };
    }));
    setDraftDirty(true);
  }

  function changeAvailability(
    request: SchedulingRequest,
    slotId: string,
    value: SchedulingAvailabilityValue | ""
  ) {
    setAvailability((current) => applySchedulingAvailabilityChoice(
      request.slots.map((slot) => slot.id),
      current,
      slotId,
      value,
      request.single_choice
    ));
  }

  function selectRequest(requestId: string) {
    if (!editorMode && requestId === selected?.id) return;
    requestNavigation(() => {
      exitEditor();
      setSelectedId(requestId);
      setSelectedRequestSearchParam(requestId);
      setError("");
      setSuccess("");
    });
  }

  function setSelectedRequestSearchParam(requestId: string) {
    const next = new URLSearchParams(searchParams);
    next.set("request_id", requestId);
    setSearchParams(next, { replace: true });
  }

  function resetResponseDraft() {
    setAvailability({ ...savedAvailability });
    setAvailabilityComment(savedAvailabilityComment);
  }

  function replaceRequest(updated: SchedulingRequest) {
    setRequests((items) => items.map((item) => item.id === updated.id ? updated : item));
  }

  function resetDraft() {
    setTitle("");
    setDescription("");
    setLocation("");
    setCalendarId("");
    setCalendarEnabled(false);
    setSlots(initialSlots(translateText));
    setParticipants([]);
    setAllowExternalParticipants(true);
    setParticipantRosterVisible(false);
    setNotifyOnAnswers(true);
    setSingleChoice(false);
    setParticipantLimitEnabled(false);
    setMaxParticipantsPerOption(1);
    setAllowMaybe(true);
    setAllowComments(false);
    setParticipantEmailRequired(false);
    setAnonymousPasswordProtectionEnabled(false);
    setAnonymousPassword("");
    setDraftDirty(false);
  }
}

function RequestGroup({
  title,
  requests,
  selectedId,
  actor,
  disabled,
  onSelect
}: {
  title: string;
  requests: SchedulingRequest[];
  selectedId?: string;
  actor: SchedulingActor;
  disabled: boolean;
  onSelect: (requestId: string) => void;
}) {
  return (
    <section className="scheduling-request-group" aria-label={title}>
      <h2>{title}</h2>
      <div className="scheduling-list-group">
        {requests.length ? (
          <SelectionList label={title} className="scheduling-request-list">
            {requests.map((request) => (
              <SelectionListItem
                key={request.id}
                selected={selectedId === request.id}
                className="scheduling-list-item"
                disabled={disabled}
                onClick={() => onSelect(request.id)}>
                <span><strong>{request.title}</strong><small>{formatRelevantDate(request)}</small></span>
                <StatusBadge status={request.status} label={requestStatusLabel(request, actor)} />
              </SelectionListItem>
            ))}
          </SelectionList>
        ) : <p className="scheduling-list-empty">{I18N.noRequestsInGroup}</p>}
      </div>
    </section>
  );
}

type SchedulingSettingField =
  | "notifyOnAnswers"
  | "singleChoice"
  | "participantLimitEnabled"
  | "maxParticipantsPerOption"
  | "allowMaybe"
  | "allowComments"
  | "participantEmailRequired"
  | "anonymousPasswordProtectionEnabled"
  | "anonymousPassword";

function SchedulingPrivacySettings({
  disabled,
  participantRosterVisible,
  onChange
}: {
  disabled: boolean;
  participantRosterVisible: boolean;
  onChange: (value: boolean) => void;
}) {
  return (
    <Card title={I18N.participantPrivacy}>
      <ToggleSwitch
        label={I18N.participantRosterVisibility}
        help={I18N.participantRosterVisibilityHelp}
        checked={participantRosterVisible}
        disabled={disabled}
        onChange={onChange} />
    </Card>
  );
}

function SchedulingSettings({
  disabled,
  policyLocked,
  notifyOnAnswers,
  singleChoice,
  participantLimitEnabled,
  maxParticipantsPerOption,
  allowMaybe,
  allowComments,
  participantEmailRequired,
  anonymousPasswordProtectionEnabled,
  anonymousPassword,
  passwordRequired,
  onChange
}: {
  disabled: boolean;
  policyLocked: boolean;
  notifyOnAnswers: boolean;
  singleChoice: boolean;
  participantLimitEnabled: boolean;
  maxParticipantsPerOption: number;
  allowMaybe: boolean;
  allowComments: boolean;
  participantEmailRequired: boolean;
  anonymousPasswordProtectionEnabled: boolean;
  anonymousPassword: string;
  passwordRequired: boolean;
  onChange: (field: SchedulingSettingField, value: boolean | number | string) => void;
}) {
  return (
    <Card title={I18N.generalSettings}>
      <div className="settings-grid">
        <ToggleSwitch
          label={I18N.notifyOnAnswers}
          help={I18N.notifyOnAnswersHelp}
          checked={notifyOnAnswers}
          disabled={disabled}
          onChange={(value) => onChange("notifyOnAnswers", value)} />
        <ToggleSwitch
          label={I18N.singleChoice}
          help={I18N.singleChoiceHelp}
          checked={singleChoice}
          disabled={disabled || policyLocked}
          onChange={(value) => onChange("singleChoice", value)} />
        <ToggleSwitch
          label={I18N.allowMaybe}
          help={I18N.allowMaybeHelp}
          checked={allowMaybe}
          disabled={disabled || policyLocked}
          onChange={(value) => onChange("allowMaybe", value)} />
        <ToggleSwitch
          label={I18N.allowComments}
          help={I18N.allowCommentsHelp}
          checked={allowComments}
          disabled={disabled || policyLocked}
          onChange={(value) => onChange("allowComments", value)} />
        <ToggleSwitch
          label={I18N.anonymousEmailRequired}
          help={I18N.anonymousEmailRequiredHelp}
          checked={participantEmailRequired}
          disabled={disabled || policyLocked}
          onChange={(value) => onChange("participantEmailRequired", value)} />
        <div className="scheduling-setting-with-field">
          <ToggleSwitch
            label={I18N.maxParticipants}
            help={I18N.maxParticipantsHelp}
            checked={participantLimitEnabled}
            disabled={disabled || policyLocked}
            onChange={(value) => onChange("participantLimitEnabled", value)} />
          <FormField label={I18N.maxParticipantsCount} help={I18N.maxParticipantsHelp}>
            <input
              type="number"
              min={1}
              step={1}
              required={participantLimitEnabled}
              disabled={disabled || policyLocked || !participantLimitEnabled}
              value={maxParticipantsPerOption}
              onChange={(event) => onChange("maxParticipantsPerOption", Math.max(1, event.target.valueAsNumber || 1))} />
          </FormField>
        </div>
        <div className="scheduling-setting-with-field">
          <ToggleSwitch
            label={I18N.anonymousPasswordProtection}
            help={I18N.anonymousPasswordProtectionHelp}
            checked={anonymousPasswordProtectionEnabled}
            disabled={disabled || policyLocked}
            onChange={(value) => onChange("anonymousPasswordProtectionEnabled", value)} />
          <FormField label={I18N.anonymousPassword} help={I18N.anonymousPasswordHelp}>
            <PasswordField
              autoComplete="new-password"
              minLength={8}
              maxLength={1024}
              required={passwordRequired}
              disabled={disabled || policyLocked || !anonymousPasswordProtectionEnabled}
              value={anonymousPassword}
              saved={anonymousPasswordProtectionEnabled && !passwordRequired}
              onValueChange={(value) => onChange("anonymousPassword", value)} />
          </FormField>
        </div>
        </div>
      {policyLocked ? (
        <DismissibleAlert tone="info" dismissible={false} compact>
          {I18N.policyLocked}
        </DismissibleAlert>
      ) : null}
    </Card>
  );
}

function ParticipationStats({
  request,
  responseCount,
  loading
}: {
  request: SchedulingRequest;
  responseCount: number | null;
  loading: boolean;
}) {
  const total = request.participant_aggregate.total;
  const knownResponseCount = responseCount ?? 0;
  const awaiting = responseCount === null ? null : Math.max(0, total - knownResponseCount);
  const rate = responseCount === null ? null : total ? Math.round((knownResponseCount / total) * 100) : 0;
  return (
    <Card title={I18N.participation}>
      <div className="metric-grid inside">
        <MetricCard label={I18N.participants} value={total} />
        <MetricCard label={I18N.responses} value={loading ? "…" : responseCount ?? "—"} tone="good" />
        <MetricCard label={I18N.awaiting} value={loading ? "…" : awaiting ?? "—"} tone={awaiting ? "warning" : "neutral"} />
        <MetricCard label={I18N.participationRate} value={loading ? "…" : rate === null ? "—" : `${rate}%`} tone="info" />
      </div>
    </Card>
  );
}

function LifecycleActions({
  request,
  saving,
  onOpen,
  onClose,
  onReminder
}: {
  request: SchedulingRequest;
  saving: boolean;
  onOpen: () => void;
  onClose: () => void;
  onReminder: () => void;
}) {
  if (request.status === "draft") {
    return <Button type="button" variant="primary" disabled={saving} onClick={onOpen}><Send aria-hidden="true" size={16} /> {I18N.openPoll}</Button>;
  }
  if (request.status === "collecting") {
    return (
      <div className="scheduling-actions">
        <Button type="button" disabled={saving} onClick={onReminder}><Bell aria-hidden="true" size={16} /> {I18N.reminder}</Button>
        <Button type="button" variant="primary" disabled={saving} onClick={onClose}><XCircle aria-hidden="true" size={16} /> {I18N.closePoll}</Button>
      </div>
    );
  }
  return null;
}

function EditableSlots({
  slots,
  disabled,
  reorderable,
  onAddBelow,
  onChange,
  onMove,
  onRemove
}: {
  slots: SlotDraft[];
  disabled: boolean;
  reorderable: boolean;
  onAddBelow: (index: number) => void;
  onChange: (index: number, field: "label" | "start_at" | "end_at", value: string) => void;
  onMove: (from: number, to: number) => void;
  onRemove: (index: number) => void;
}) {
  const columns: DataGridColumn<SlotDraft>[] = [
    {
      id: "title",
      header: I18N.title,
      minWidth: 180,
      resizable: true,
      value: (slot) => slot.label,
      render: (slot, index) => (
        <input
          required
          maxLength={500}
          disabled={disabled}
          aria-label={I18N.title}
          value={slot.label}
          onChange={(event) => onChange(index, "label", event.target.value)} />
      )
    },
    {
      id: "start",
      header: I18N.start,
      minWidth: 280,
      resizable: true,
      value: (slot) => slot.start_at,
      render: (slot, index) => (
        <DateTimeField
          required
          disabled={disabled}
          aria-label={I18N.start}
          value={slot.start_at}
          onChange={(value) => onChange(index, "start_at", value)} />
      )
    },
    {
      id: "end",
      header: I18N.end,
      minWidth: 280,
      resizable: true,
      value: (slot) => slot.end_at,
      render: (slot, index) => (
        <DateTimeField
          required
          disabled={disabled}
          aria-label={I18N.end}
          value={slot.end_at}
          min={addLocalMinutes(slot.start_at, 1)}
          onChange={(value) => onChange(index, "end_at", value)} />
      )
    },
    {
      id: "actions",
      header: I18N.actions,
      width: 184,
      sticky: "end",
      render: (_slot, index) => (
        <DataGridRowActions
          disabled={disabled}
          reorderable={reorderable}
          onAddBelow={() => onAddBelow(index)}
          onMoveUp={reorderable && index > 0 ? () => onMove(index, index - 1) : undefined}
          onMoveDown={reorderable && index < slots.length - 1 ? () => onMove(index, index + 1) : undefined}
          onRemove={() => onRemove(index)}
          removeDisabled={slots.length === 1}
          addLabel={I18N.addSlot}
          removeLabel={i18nMessage("i18n:govoplan-scheduling.remove_slot_value.3adc7576", { value0: index + 1 })} />
      )
    }
  ];

  return (
    <Card title={I18N.candidateSlots}>
      <DataGrid
        id="scheduling-create-candidate-slots-grid"
        rows={slots}
        columns={columns}
        getRowKey={(slot) => slot.draftId}
        initialFit="container" />
    </Card>
  );
}

function EditableParticipants({
  participants,
  disabled,
  allowExternalParticipants,
  search,
  onAllowExternalParticipantsChange,
  onChange
}: {
  participants: ParticipantDraft[];
  disabled: boolean;
  allowExternalParticipants: boolean;
  search: PeoplePickerSearch;
  onAllowExternalParticipantsChange: (value: boolean) => void;
  onChange: (participants: PeoplePickerItem[]) => void;
}) {
  return (
    <Card title={I18N.participants}>
      {participants.some((participant) => participant.identityLocked) ? (
        <p className="scheduling-capability-note">{I18N.invitedIdentityLocked}</p>
      ) : null}
      <ToggleSwitch
        label={I18N.allowExternalParticipants}
        help={I18N.allowExternalParticipantsHelp}
        checked={allowExternalParticipants}
        disabled={disabled}
        onChange={onAllowExternalParticipantsChange} />
      <PeoplePicker
        id="scheduling-participant-picker"
        label={I18N.addParticipant}
        help={I18N.participantPickerHelp}
        selectedLabel={I18N.participants}
        value={participants}
        search={search}
        disabled={disabled}
        allowManualExternal={allowExternalParticipants}
        manualEmailRequired
        onChange={onChange} />
    </Card>
  );
}

function CandidateSlotsGrid({
  slots,
  optionResultById,
  showDecisionAction,
  decisionEnabled,
  resultsAvailable,
  saving,
  allowMaybe,
  maxParticipantsPerOption,
  onDecide
}: {
  slots: SchedulingCandidateSlot[];
  optionResultById: Map<string, SchedulingPollOptionResult>;
  showDecisionAction: boolean;
  decisionEnabled: boolean;
  resultsAvailable: boolean;
  saving: boolean;
  allowMaybe: boolean;
  maxParticipantsPerOption: number | null;
  onDecide: (slot: SchedulingCandidateSlot) => void;
}) {
  const columns: DataGridColumn<SchedulingCandidateSlot>[] = [
    {
      id: "title",
      header: I18N.title,
      minWidth: 190,
      resizable: true,
      value: (slot) => slot.label,
      render: (slot) => (
        <span className="scheduling-slot-title">
          <strong>{slot.label}</strong>
          {slot.freebusy_status ? <StatusBadge status={slot.freebusy_status} label={freeBusyLabel(slot.freebusy_status)} /> : null}
        </span>
      )
    },
    { id: "start", header: I18N.start, minWidth: 160, resizable: true, value: (slot) => slot.start_at, render: (slot) => formatDateTime(slot.start_at, SHORT_DATE_TIME_OPTIONS) },
    { id: "end", header: I18N.end, minWidth: 160, resizable: true, value: (slot) => slot.end_at, render: (slot) => formatDateTime(slot.end_at, SHORT_DATE_TIME_OPTIONS) },
    {
      id: "available",
      header: I18N.available,
      width: 100,
      align: "right",
      value: (slot) => resultsAvailable
        ? optionResultById.get(slot.poll_option_id ?? `slot-${slot.position + 1}`)?.values.available ?? 0
        : "—"
    },
    ...(allowMaybe ? [{
      id: "maybe",
      header: I18N.maybe,
      width: 90,
      align: "right",
      value: (slot) => resultsAvailable
        ? optionResultById.get(slot.poll_option_id ?? `slot-${slot.position + 1}`)?.values.maybe ?? 0
        : "—"
    } satisfies DataGridColumn<SchedulingCandidateSlot>] : []),
    ...(maxParticipantsPerOption !== null ? [{
      id: "capacity",
      header: I18N.capacity,
      width: 100,
      align: "right",
      value: () => maxParticipantsPerOption
    } satisfies DataGridColumn<SchedulingCandidateSlot>] : []),
    {
      id: "unavailable",
      header: I18N.unavailable,
      width: 110,
      align: "right",
      value: (slot) => resultsAvailable
        ? optionResultById.get(slot.poll_option_id ?? `slot-${slot.position + 1}`)?.values.unavailable ?? 0
        : "—"
    },
    ...(showDecisionAction ? [{
      id: "actions",
      header: I18N.actions,
      width: 72,
      sticky: "end",
      render: (slot) => <TableActionGroup actions={[{
        id: "decide",
        label: i18nMessage("i18n:govoplan-scheduling.decide_on_value.196409cd", { value0: slot.label }),
        icon: <Check aria-hidden="true" size={16} />,
        disabled: saving || !decisionEnabled,
        disabledReason: !decisionEnabled ? I18N.decideUnavailable : undefined,
        onClick: () => onDecide(slot)
      }]} />
    } satisfies DataGridColumn<SchedulingCandidateSlot>] : [])
  ];

  return (
    <DataGrid
      id="scheduling-candidate-slots-grid"
      rows={slots}
      columns={columns}
      getRowKey={(slot) => slot.id}
      initialFit="container" />
  );
}

function ParticipantsGrid({
  request,
  participants,
  canManage,
  saving,
  now,
  onCopy,
  onSend,
  onRevoke
}: {
  request: SchedulingRequest;
  participants: SchedulingParticipant[];
  canManage: boolean;
  saving: boolean;
  now: Date;
  onCopy: (participant: SchedulingParticipant) => void;
  onSend: (participant: SchedulingParticipant) => void;
  onRevoke: (participant: SchedulingParticipant) => void;
}) {
  const columns: DataGridColumn<SchedulingParticipant>[] = [
    { id: "name", header: I18N.name, minWidth: 180, resizable: true, value: (participant) => participant.display_name ?? "" },
    { id: "email", header: I18N.participantEmail, minWidth: 220, resizable: true, value: (participant) => participant.email ?? "" },
    {
      id: "status",
      header: I18N.statusLabel,
      width: 140,
      value: (participant) => participantStatusLabel(participant.status),
      render: (participant) => <StatusBadge status={participant.status} label={participantStatusLabel(participant.status)} />
    },
    ...(canManage ? [{
      id: "actions",
      header: I18N.actions,
      width: 132,
      sticky: "end",
      align: "right",
      render: (participant) => {
        const label = participantDisplayLabel(participant);
        const blocks = schedulingInvitationActionBlocks(request, participant, now);
        const copyDisabledReason = invitationActionDisabledReason(blocks.copy, request);
        const deliveryDisabledReason = invitationActionDisabledReason(blocks.send, request);
        const revokeDisabledReason = invitationActionDisabledReason(blocks.revoke, request);
        return (
          <TableActionGroup
            minimumSlots={3}
            actions={[
              {
                id: "copy-invitation",
                label: i18nMessage(I18N.copyInvitationLink, { value0: label }),
                icon: <Copy aria-hidden="true" size={16} />,
                disabled: saving || Boolean(copyDisabledReason),
                disabledReason: copyDisabledReason,
                onClick: () => onCopy(participant)
              },
              {
                id: "send-invitation",
                label: i18nMessage(I18N.sendInvitation, { value0: label }),
                icon: <Send aria-hidden="true" size={16} />,
                disabled: saving || Boolean(deliveryDisabledReason),
                disabledReason: deliveryDisabledReason,
                onClick: () => onSend(participant)
              },
              {
                id: "revoke-invitation",
                label: i18nMessage(I18N.revokeInvitation, { value0: label }),
                icon: <Link2Off aria-hidden="true" size={16} />,
                variant: "danger",
                disabled: saving || Boolean(revokeDisabledReason),
                disabledReason: revokeDisabledReason,
                onClick: () => onRevoke(participant)
              }
            ]} />
        );
      }
    } satisfies DataGridColumn<SchedulingParticipant>] : [])
  ];

  return (
    <DataGrid
      id="scheduling-participants-grid"
      rows={participants}
      columns={columns}
      getRowKey={(participant) => participant.id}
      emptyText={I18N.noParticipants}
      initialFit="container" />
  );
}

function invitationActionDisabledReason(
  block: SchedulingInvitationActionBlock | null,
  request: SchedulingRequest
): string | undefined {
  if (block === "participation_policy_unavailable") {
    return request.public_participation_policy_enforcement_reason || I18N.publicPolicyUnavailable;
  }
  if (block === "cancellation_notice_expired") return I18N.cancellationNoticeExpired;
  if (block === "delivery_unavailable") return I18N.invitationDeliveryUnavailable;
  if (block === "no_delivery_target") return I18N.noDeliveryTarget;
  if (block === "no_active_invitation") return I18N.noActiveInvitation;
  if (block === "participant_revision_unavailable") return I18N.reloadInvitation;
  return undefined;
}

function participantDisplayLabel(participant: SchedulingParticipant | null): string {
  return participant?.display_name?.trim() || participant?.email?.trim() || I18N.participant;
}

async function copySchedulingInvitationLink(
  responsePromise: Promise<SchedulingInvitationActionResponse>
): Promise<{ response: SchedulingInvitationActionResponse; copied: boolean }> {
  if (
    typeof navigator !== "undefined" &&
    navigator.clipboard?.write &&
    typeof ClipboardItem !== "undefined"
  ) {
    try {
      const content = responsePromise.then((response) => {
        const value = response.action_url
          ? schedulingPublicInvitationUrl(response.action_url, window.location.origin)
          : null;
        return new Blob([value ?? ""], { type: "text/plain" });
      }).catch(() => new Blob([], { type: "text/plain" }));
      await navigator.clipboard.write([new ClipboardItem({ "text/plain": content })]);
      const response = await responsePromise;
      const copied = Boolean(
        response.action_url &&
        schedulingPublicInvitationUrl(response.action_url, window.location.origin)
      );
      return { response, copied };
    } catch {
      // The request result is reused below; no second invitation is issued.
    }
  }

  const response = await responsePromise;
  const value = response.action_url
    ? schedulingPublicInvitationUrl(response.action_url, window.location.origin)
    : null;
  if (!value) return { response, copied: false };
  try {
    if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
      await navigator.clipboard.writeText(value);
      return { response, copied: true };
    }
  } catch {
    // The caller reports the bounded clipboard failure without exposing the URL.
  }
  return { response, copied: false };
}

function firstRequest(groups: SchedulingRequestGroups, includeManaged: boolean): SchedulingRequest | null {
  return groups.owned[0] ?? groups.invited[0] ?? (includeManaged ? groups.other[0] : null) ?? null;
}

function requestStatusLabel(request: SchedulingRequest, actor: SchedulingActor): string {
  const phase = schedulingSortPhase(request, actor);
  if (phase === "past") return I18N.past;
  if (phase === "determined") return I18N.determined;
  if (phase === "closed") return I18N.closed;
  if (phase === "answered") return I18N.answered;
  if (!schedulingRequestIsOwned(request, actor) && schedulingParticipantForActor(request, actor)) return I18N.awaitingResponse;
  return request.status === "draft" ? "i18n:govoplan-scheduling.draft.23d33e22" : I18N.open;
}

function freeBusyLabel(status: string): string {
  if (status === "free") return I18N.free;
  if (status === "busy") return I18N.busy;
  if (status === "error") return "i18n:govoplan-scheduling.error.7f2f6a15";
  return I18N.unchecked;
}

function participantStatusLabel(status: string): string {
  return ({
    invited: "i18n:govoplan-scheduling.invited.53469df1",
    responded: "i18n:govoplan-scheduling.responded.4f218211",
    declined: "i18n:govoplan-scheduling.declined.ff59b80f",
    removed: "i18n:govoplan-scheduling.removed.b5e77c5c",
    draft: "i18n:govoplan-scheduling.draft.23d33e22"
  } as Record<string, string>)[status] ?? status;
}

function notificationKindLabel(kind: string): string {
  return ({
    invitation: "i18n:govoplan-scheduling.invitation.6306ef74",
    reminder: "i18n:govoplan-scheduling.reminder.b87a1929",
    decision: "i18n:govoplan-scheduling.decision.7f59a1f1",
    cancellation: "i18n:govoplan-scheduling.cancellation.319aaae4",
    participant_removed: "i18n:govoplan-scheduling.participant_removed.0cf4ec4c",
    participant_replaced: "i18n:govoplan-scheduling.participant_replaced.2623752d"
  } as Record<string, string>)[kind] ?? kind;
}

function notificationStatusLabel(status: string): string {
  return ({
    pending: "i18n:govoplan-scheduling.pending.96f608c1",
    queued: "i18n:govoplan-scheduling.queued.6a599877",
    sending: "i18n:govoplan-scheduling.sending.ceafde86",
    sent: "i18n:govoplan-scheduling.sent.35f49dcf",
    failed: "i18n:govoplan-scheduling.failed.09fef5d8",
    skipped: "i18n:govoplan-scheduling.skipped.5a000ad7"
  } as Record<string, string>)[status] ?? status;
}

function isRequestEnvelope(value: unknown): value is { request: SchedulingRequest } {
  return Boolean(value && typeof value === "object" && "request" in value);
}

function initialSlots(translateText: (value: string) => string): SlotDraft[] {
  return [newSlot(1, translateText)];
}

function newSlot(position: number, translateText: (value: string) => string): SlotDraft {
  const start = new Date(Date.now() + 24 * 60 * 60 * 1000);
  start.setMinutes(0, 0, 0);
  const end = new Date(start.getTime() + 60 * 60 * 1000);
  return {
    draftId: nextDraftId("slot"),
    label: translateText(i18nMessage("i18n:govoplan-scheduling.option_value.3f643dc0", { value0: position })),
    start_at: localValue(start),
    end_at: localValue(end),
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
    metadata: {}
  };
}

let draftIdSequence = 0;

function nextDraftId(prefix: string): string {
  draftIdSequence += 1;
  return `${prefix}-${draftIdSequence}`;
}

function insertAfter<T>(items: T[], index: number, item: T): T[] {
  const insertionIndex = Math.min(items.length, Math.max(0, index + 1));
  return [...items.slice(0, insertionIndex), item, ...items.slice(insertionIndex)];
}

function moveItem<T>(items: T[], from: number, to: number): T[] {
  if (from === to || from < 0 || to < 0 || from >= items.length || to >= items.length) return items;
  const next = [...items];
  const [item] = next.splice(from, 1);
  next.splice(to, 0, item);
  return next;
}

function localValue(date: Date): string {
  const pad = (value: number) => value.toString().padStart(2, "0");
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}

function addLocalMinutes(value: string, minutes: number): string {
  if (!value) return "";
  const date = new Date(value);
  if (!Number.isFinite(date.getTime())) return value;
  return localValue(new Date(date.getTime() + minutes * 60 * 1000));
}

function isoFromLocal(value: string): string {
  return new Date(value).toISOString();
}

function availabilityValuesEqual(
  left: Record<string, SchedulingAvailabilityValue | "">,
  right: Record<string, SchedulingAvailabilityValue | "">
): boolean {
  const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
  return Array.from(keys).every((key) => (left[key] ?? "") === (right[key] ?? ""));
}

const SHORT_DATE_TIME_OPTIONS: FormatDateTimeOptions = {
  year: undefined,
  month: undefined,
  day: undefined,
  hour: undefined,
  minute: undefined,
  timeZoneName: undefined,
  dateStyle: "short",
  timeStyle: "short"
};

function formatRange(start: string, end: string): string {
  return `${formatDateTime(start, SHORT_DATE_TIME_OPTIONS)} – ${formatDateTime(end, SHORT_DATE_TIME_OPTIONS)}`;
}

function formatRelevantDate(request: SchedulingRequest): string {
  const timestamp = schedulingRelevantTimestamp(request);
  return Number.isFinite(timestamp) ? formatDateTime(new Date(timestamp).toISOString(), SHORT_DATE_TIME_OPTIONS) : "";
}

function errorMessage(error: unknown, fallback: string): string {
  return error instanceof Error && error.message ? error.message : fallback;
}
