import { useEffect, useMemo, useRef, useState, type DragEvent as ReactDragEvent, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent } from "react";
import { ArrowUp, ChevronRight, Copy, Download, File, Folder, Home, KeyRound, Link2, MoveRight, Plus, RefreshCw, Search, Trash2, UploadCloud } from "lucide-react";
import {
  Button,
  ConfirmDialog,
  DismissibleAlert,
  FieldLabel,
  FileDropZone,
  FormField,
  LoadingIndicator,
  ToggleSwitch,
  hasScope,
  type ApiSettings,
  type AuthInfo, i18nMessage } from
"@govoplan/core-webui";
import {
  bulkDeleteFiles,
  bulkRenameFiles,
  browseFileConnectorProfile,
  createFileConnectorSpace,
  createFolder,
  deleteFolder,
  downloadFile,
  downloadFilesAsZip,
  fetchResourceAccessExplanation,
  listFilesDelta,
  listFileConnectorProfiles,
  listFileSpaces,
  listManagedFileSnapshot,
  resolveFilePatterns,
  syncFileConnectorFile,
  transferFiles,
  uploadFiles,
  virtualFolderResourceId,
  type ConflictResolution,
  type ConflictStrategy,
  type FileConnectorBrowseItem,
  type FileConnectorProfile,
  type FileDeltaResponse,
  type FileFolder,
  type FileSpace,
  type ManagedFile,
  type RenameResponse,
  type ResourceAccessExplanationResponse } from
"../../api/files";
import { EMPTY_FILES, EMPTY_FOLDERS, EMPTY_SPACES, INTERNAL_DRAG_TYPE } from "./constants";
import { FileConflictDialog, FileContextMenu, FileDialog, FolderTree, RenamePreviewList, TransferFolderSelector } from "./components/FileManagerComponents";
import type {
  ContextMenuState,
  DialogKind,
  EntrySelectionKey,
  ExplorerEntry,
  FolderEntry,
  FileActionTarget,
  FileConflictItem,
  RenameMode,
  SelectionSets,
  SortColumn,
  SortDirection,
  TransferMode } from
"./types";
import {
  buildExplorerEntries,
  buildFolderEntryFromSources,
  buildFolderTree,
  candidateRenamedPath,
  entrySelectionKey,
  fileIdsForSelection,
  folderAncestorPaths,
  folderBreadcrumbs,
  folderContentLabel,
  formatBytes,
  formatDate,
  isFileInFolder,
  joinFolder,
  lastPathSegment,
  normalizeFilePath,
  normalizeFolder,
  parseDragState,
  parentFolderPath,
  sortExplorerEntries,
  treeNodeKey } from
"./utils/fileManagerUtils";
import { useFileSelection } from "./hooks/useFileSelection";
import { useFileTreeState } from "./hooks/useFileTreeState";
import { useFileDialogs } from "./hooks/useFileDialogs";
import { useFileDragDropState } from "./hooks/useFileDragDropState";

type UploadPhase = "idle" | "uploading" | "unpacking" | "finalizing";
type FileAccessExplanationTarget = {
  resourceType: "file" | "folder";
  resourceId: string;
  label: string;
  action: string;
};

const DEFAULT_FILE_LIST_ROW_HEIGHT = 58;
const FILE_LIST_OVERSCAN_ROWS = 8;

export default function FilesPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
  const canDownload = hasScope(auth, "files:download");
  const canUpload = hasScope(auth, "files:upload");
  const canOrganize = hasScope(auth, "files:organize");
  const canDelete = hasScope(auth, "files:delete");
  const [spaces, setSpaces] = useState<FileSpace[]>(EMPTY_SPACES);
  const [activeSpaceId, setActiveSpaceId] = useState("");
  const activeSpace = spaces.find((space) => space.id === activeSpaceId) ?? spaces[0] ?? null;
  const activeSpaceIsConnector = activeSpace?.space_type === "connector";
  const [filesBySpace, setFilesBySpace] = useState<Record<string, ManagedFile[]>>({});
  const [foldersBySpace, setFoldersBySpace] = useState<Record<string, FileFolder[]>>({});
  const [fileDeltaWatermarksBySpace, setFileDeltaWatermarksBySpace] = useState<Record<string, string>>({});
  const files = activeSpace ? filesBySpace[activeSpace.id] ?? EMPTY_FILES : EMPTY_FILES;
  const folders = activeSpace ? foldersBySpace[activeSpace.id] ?? EMPTY_FOLDERS : EMPTY_FOLDERS;

  const [currentFolder, setCurrentFolder] = useState("");
  const [searchPattern, setSearchPattern] = useState("");
  const [searchCaseSensitive, setSearchCaseSensitive] = useState(false);
  const [searchActive, setSearchActive] = useState(false);
  const [searchResults, setSearchResults] = useState<ManagedFile[] | null>(null);
  const [sortColumn, setSortColumn] = useState<SortColumn>("name");
  const [sortDirection, setSortDirection] = useState<SortDirection>("asc");
  const [unmatchedCount, setUnmatchedCount] = useState<number | null>(null);
  const [unpackZip, setUnpackZip] = useState(false);
  const [busy, setBusy] = useState(false);
  const [uploadActive, setUploadActive] = useState(false);
  const [uploadPhase, setUploadPhase] = useState<UploadPhase>("idle");
  const [uploadProgress, setUploadProgress] = useState<number | null>(null);
  const [connectorProfiles, setConnectorProfiles] = useState<FileConnectorProfile[]>([]);
  const [connectorProfileId, setConnectorProfileId] = useState("");
  const [connectorLibraryId, setConnectorLibraryId] = useState<string | null>(null);
  const [connectorPath, setConnectorPath] = useState("");
  const [connectorItems, setConnectorItems] = useState<FileConnectorBrowseItem[]>([]);
  const [connectorSelectedItem, setConnectorSelectedItem] = useState<FileConnectorBrowseItem | null>(null);
  const [connectorLoading, setConnectorLoading] = useState(false);
  const [connectorError, setConnectorError] = useState("");
  const [connectorSpaceLabel, setConnectorSpaceLabel] = useState("");
  const [connectorSpaceOwnerSpaceId, setConnectorSpaceOwnerSpaceId] = useState("");
  const [connectorSpaceItemsBySpace, setConnectorSpaceItemsBySpace] = useState<Record<string, FileConnectorBrowseItem[]>>({});
  const [connectorSpaceLibraryBySpace, setConnectorSpaceLibraryBySpace] = useState<Record<string, string | null>>({});
  const [connectorSpaceSelectedItem, setConnectorSpaceSelectedItem] = useState<FileConnectorBrowseItem | null>(null);
  const [connectorSpaceLoading, setConnectorSpaceLoading] = useState(false);
  const [connectorSpaceError, setConnectorSpaceError] = useState("");
  const [message, setMessage] = useState("");
  const [error, setError] = useState("");
  const [accessExplanationTarget, setAccessExplanationTarget] = useState<FileAccessExplanationTarget | null>(null);
  const [resourceAccessExplanation, setResourceAccessExplanation] = useState<ResourceAccessExplanationResponse | null>(null);
  const [resourceAccessLoading, setResourceAccessLoading] = useState(false);
  const { setDragActive, internalDrag, setInternalDrag, dropTargetKey, setDropTargetKey, clearDropState: clearDragDropState } = useFileDragDropState();
  const {
    dialog,
    setDialog,
    dialogTarget,
    setDialogTarget,
    transferMode,
    setTransferMode,
    transferDialogState,
    setTransferDialogState,
    contextMenu,
    setContextMenu,
    conflictDialog,
    setConflictDialog,
    deleteDialog,
    setDeleteDialog,
    openDialog: openRawDialog,
    closeDialog: closeRawDialog
  } = useFileDialogs();
  const [newFolderName, setNewFolderName] = useState("");
  const [newFolderError, setNewFolderError] = useState("");
  const [renamePreviewVisibleCount, setRenamePreviewVisibleCount] = useState(20);
  const [singleRenameName, setSingleRenameName] = useState("");
  const [renameMode, setRenameMode] = useState<RenameMode>("prefix");
  const [renameFind, setRenameFind] = useState("");
  const [renameReplacement, setRenameReplacement] = useState("");
  const [renamePrefix, setRenamePrefix] = useState("");
  const [renameSuffix, setRenameSuffix] = useState("");
  const [renameRecursive, setRenameRecursive] = useState(false);
  const [renamePreview, setRenamePreview] = useState<RenameResponse | null>(null);
  const visibleFiles = searchActive && searchResults ? searchResults : files;
  const explorerEntries = useMemo(() => {
    const entries = buildExplorerEntries(visibleFiles, folders, currentFolder, searchActive);
    return sortExplorerEntries(entries, sortColumn, sortDirection);
  }, [visibleFiles, folders, currentFolder, searchActive, sortColumn, sortDirection]);
  const fileListViewportRef = useRef<HTMLDivElement | null>(null);
  const fileListMeasureRowRef = useRef<HTMLDivElement | null>(null);
  const managedSpaceLoadsInFlightRef = useRef<Set<string>>(new Set());
  const [fileListViewportHeight, setFileListViewportHeight] = useState(0);
  const [fileListScrollTop, setFileListScrollTop] = useState(0);
  const [fileListRowHeight, setFileListRowHeight] = useState(DEFAULT_FILE_LIST_ROW_HEIGHT);
  const virtualExplorerRows = useMemo(() => {
    const rowHeight = Math.max(1, fileListRowHeight);
    const entryScrollTop = Math.max(0, fileListScrollTop - rowHeight);
    const viewportHeight = Math.max(fileListViewportHeight, rowHeight * FILE_LIST_OVERSCAN_ROWS);
    const startIndex = Math.max(0, Math.floor(entryScrollTop / rowHeight) - FILE_LIST_OVERSCAN_ROWS);
    const endIndex = Math.min(
      explorerEntries.length,
      Math.ceil((entryScrollTop + viewportHeight) / rowHeight) + FILE_LIST_OVERSCAN_ROWS
    );

    return {
      startIndex,
      topSpacerHeight: startIndex * rowHeight,
      bottomSpacerHeight: Math.max(0, (explorerEntries.length - endIndex) * rowHeight),
      entries: explorerEntries.slice(startIndex, endIndex)
    };
  }, [explorerEntries, fileListRowHeight, fileListScrollTop, fileListViewportHeight]);
  const visibleEntryKeys = useMemo(() => explorerEntries.map(entrySelectionKey), [explorerEntries]);
  const {
    selectedFileIds,
    setSelectedFileIds,
    selectedFolderPaths,
    setSelectedFolderPaths,
    selectedEntryCount,
    hasSelection,
    selectedSummary,
    currentSelectionKeys,
    setSelectionAnchor,
    applySelectionKeys,
    clearSelection,
    currentSelectionSets,
    setsForEntry,
    isEntrySelected,
    preventTextSelectionOnShift,
    handleEntrySelection
  } = useFileSelection(visibleEntryKeys, busy);
  const { expandedTreeNodes, setExpandedTreeNodes, cancelTreeDragExpand, scheduleTreeDragExpand, toggleTreeFolder } = useFileTreeState({
    activeSpaceId,
    currentFolder,
    onOpenFolder: openFolder
  });
  const selectedFiles = useMemo(() => files.filter((file) => selectedFileIds.has(file.id)), [files, selectedFileIds]);
  const selectedDownloadFileIds = useMemo(() => fileIdsForSelection(files, selectedFileIds, selectedFolderPaths), [files, selectedFileIds, selectedFolderPaths]);
  const accessExplainableTarget = useMemo(
    () => accessTargetForSelection(selectedFileIds, selectedFolderPaths, activeSpaceId),
    [activeSpaceId, filesBySpace, foldersBySpace, selectedFileIds, selectedFolderPaths]
  );
  const canExplainResourceAccess =
  hasScope(auth, "admin:users:read") ||
  hasScope(auth, "admin:roles:read") ||
  hasScope(auth, "access:membership:read") ||
  hasScope(auth, "access:role:read");
  const folderCrumbs = useMemo(() => folderBreadcrumbs(currentFolder), [currentFolder]);
  const activeDialogTarget = dialogTarget ?? (activeSpace ? { spaceId: activeSpace.id, folderPath: currentFolder } : null);
  const activeDialogSpace = activeDialogTarget ? spaces.find((space) => space.id === activeDialogTarget.spaceId) ?? null : null;
  const downloadLabel = selectedDownloadFileIds.length > 1 ? "i18n:govoplan-files.download_zip.7bd0e4b0" : "i18n:govoplan-files.download.a479c9c3";
  const activeConnectorProfile = connectorProfiles.find((profile) => profile.id === connectorProfileId) ?? null;
  const activeConnectorSpaceItems = activeSpaceIsConnector && activeSpace ? connectorSpaceItemsBySpace[activeSpace.id] ?? [] : [];

  useEffect(() => {
    const viewport = fileListViewportRef.current;
    if (!viewport) return undefined;

    const updateViewportHeight = () => {
      setFileListViewportHeight(viewport.clientHeight);
      setFileListScrollTop(viewport.scrollTop);
    };

    updateViewportHeight();
    if (typeof ResizeObserver === "undefined") return undefined;

    const observer = new ResizeObserver(updateViewportHeight);
    observer.observe(viewport);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    const row = fileListMeasureRowRef.current;
    if (!row) return undefined;

    const updateRowHeight = () => {
      const measuredHeight = row.getBoundingClientRect().height;
      if (measuredHeight > 0) setFileListRowHeight(Math.round(measuredHeight));
    };

    updateRowHeight();
    if (typeof ResizeObserver === "undefined") return undefined;

    const observer = new ResizeObserver(updateRowHeight);
    observer.observe(row);
    return () => observer.disconnect();
  }, [activeSpaceId, currentFolder]);

  useEffect(() => {
    const viewport = fileListViewportRef.current;
    if (!viewport) return;
    viewport.scrollTop = 0;
    setFileListScrollTop(0);
  }, [activeSpaceId, currentFolder, searchActive, searchResults, sortColumn, sortDirection]);

  async function loadSpaces() {
    setBusy(true);
    setError("");
    try {
      const response = await listFileSpaces(settings);
      setSpaces(response.spaces);
      const nextActiveSpace = response.spaces.find((space) => space.id === activeSpaceId) ?? response.spaces[0] ?? null;
      setActiveSpaceId(nextActiveSpace?.id || "");
      if (nextActiveSpace && !isConnectorSpace(nextActiveSpace)) await loadSpaceContents(nextActiveSpace, { silent: true });
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy(false);
    }
  }

  function applyManagedSpaceDelta(spaceId: string, response: FileDeltaResponse) {
    if (response.full) {
      setFilesBySpace((current) => ({ ...current, [spaceId]: response.files }));
      setFoldersBySpace((current) => ({ ...current, [spaceId]: response.folders }));
    } else {
      const deletedFileIds = new Set(response.deleted.filter((item) => !item.resource_type || item.resource_type === "file").map((item) => item.id));
      const deletedFolderIds = new Set(response.deleted.filter((item) => item.resource_type === "folder").map((item) => item.id));
      setFilesBySpace((current) => {
        const byId = new Map((current[spaceId] ?? EMPTY_FILES).map((file) => [file.id, file]));
        deletedFileIds.forEach((id) => byId.delete(id));
        response.files.forEach((file) => byId.set(file.id, file));
        return { ...current, [spaceId]: Array.from(byId.values()) };
      });
      setFoldersBySpace((current) => {
        const byId = new Map((current[spaceId] ?? EMPTY_FOLDERS).map((folder) => [folder.id, folder]));
        deletedFolderIds.forEach((id) => byId.delete(id));
        response.folders.forEach((folder) => byId.set(folder.id, folder));
        return { ...current, [spaceId]: Array.from(byId.values()) };
      });
    }
    if (response.watermark) {
      setFileDeltaWatermarksBySpace((current) => ({ ...current, [spaceId]: response.watermark || "" }));
    }
  }

  async function loadSpaceContents(space = activeSpace, options: {silent?: boolean;} = {}) {
    if (!space) return;
    if (isConnectorSpace(space)) {
      await loadConnectorSpaceContents(space, { silent: options.silent, folderPath: "" });
      return;
    }
    if (managedSpaceLoadsInFlightRef.current.has(space.id)) return;
    managedSpaceLoadsInFlightRef.current.add(space.id);
    if (!options.silent) {
      setBusy(true);
      setError("");
      setMessage("");
    }
    try {
      let since = fileDeltaWatermarksBySpace[space.id];
      let response: FileDeltaResponse | null = null;
      if (!since) {
        const snapshot = await listManagedFileSnapshot(settings, {
          owner_type: space.owner_type,
          owner_id: space.owner_id
        });
        setFilesBySpace((current) => ({ ...current, [space.id]: snapshot.files }));
        setFoldersBySpace((current) => ({ ...current, [space.id]: snapshot.folders }));
        if (snapshot.watermark) {
          setFileDeltaWatermarksBySpace((current) => ({ ...current, [space.id]: snapshot.watermark || "" }));
        }
      } else {
        do {
          response = await listFilesDelta(settings, {
            owner_type: space.owner_type,
            owner_id: space.owner_id,
            since: since || undefined
          });
          applyManagedSpaceDelta(space.id, response);
          since = response.watermark || "";
        } while (response.has_more && response.watermark);
      }
      setSelectedFileIds(new Set());
      setSelectedFolderPaths(new Set());
      setUnmatchedCount(null);
      setRenamePreview(null);
      if (!options.silent) {
        setSearchActive(false);
        setSearchPattern("");
        setSearchResults(null);
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      managedSpaceLoadsInFlightRef.current.delete(space.id);
      if (!options.silent) setBusy(false);
    }
  }

  useEffect(() => {
    void loadSpaces();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);

  useEffect(() => {
    if (!contextMenu) return undefined;
    const close = () => setContextMenu(null);
    const closeOnEscape = (event: KeyboardEvent) => {
      if (event.key === "Escape") setContextMenu(null);
    };
    window.addEventListener("click", close);
    window.addEventListener("resize", close);
    window.addEventListener("scroll", close, true);
    window.addEventListener("keydown", closeOnEscape);
    return () => {
      window.removeEventListener("click", close);
      window.removeEventListener("resize", close);
      window.removeEventListener("scroll", close, true);
      window.removeEventListener("keydown", closeOnEscape);
    };
  }, [contextMenu]);

  useEffect(() => {
    setCurrentFolder("");
    setSearchPattern("");
    setSearchActive(false);
    setSearchResults(null);
    setSelectedFileIds(new Set());
    setSelectedFolderPaths(new Set());
    setConnectorSpaceSelectedItem(null);
    setConnectorSpaceError("");
    const nextSpace = spaces.find((space) => space.id === activeSpaceId);
    if (nextSpace && isConnectorSpace(nextSpace)) {
      void loadConnectorSpaceContents(nextSpace, { folderPath: "", silent: true });
    } else if (nextSpace && filesBySpace[nextSpace.id] === undefined && foldersBySpace[nextSpace.id] === undefined) {
      void loadSpaceContents(nextSpace, { silent: true });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [activeSpaceId, spaces]);



  function resetTransientState(options: {keepSearch?: boolean;} = {}) {
    clearSelection();
    if (!options.keepSearch) {
      setSearchActive(false);
      setSearchPattern("");
      setSearchResults(null);
    }
    setUnmatchedCount(null);
    setRenamePreview(null);
  }

  function currentActionTarget(): FileActionTarget | null {
    return activeDialogTarget;
  }

  function openDialog(kind: DialogKind, target: FileActionTarget | null = null) {
    if ((kind === "upload" || kind === "connector-sync") && !canUpload) return;
    if (kind === "connector-space" && !canOrganize) return;
    if (kind && ["create-folder", "rename", "single-rename", "transfer"].includes(kind) && !canOrganize) return;
    if (target && isConnectorSpace(findSpace(target.spaceId)) && kind !== "connector-sync") return;
    if (kind === "create-folder") {
      setNewFolderName("");
      setNewFolderError("");
    }
    openRawDialog(kind, target);
  }

  function closeDialog() {
    closeRawDialog();
    setNewFolderError("");
    setUploadActive(false);
    setUploadPhase("idle");
    setUploadProgress(null);
    setConnectorError("");
    setConnectorSelectedItem(null);
    setConnectorSpaceLabel("");
  }

  function updateActiveDialogFolder(spaceId: string, folderPath: string) {
    setDialogTarget({ spaceId, folderPath: normalizeFolder(folderPath) });
  }

  function findSpace(spaceId: string): FileSpace | null {
    return spaces.find((space) => space.id === spaceId) ?? null;
  }

  function isConnectorSpace(space: FileSpace | null | undefined): boolean {
    return space?.space_type === "connector";
  }

  function connectorSpaceLibrary(space: FileSpace): string | null {
    return connectorSpaceLibraryBySpace[space.id] ?? space.library_id ?? null;
  }

  function connectorSpaceRootPath(space: FileSpace): string {
    return normalizeFolder(space.remote_path || "");
  }

  function connectorSpaceBrowsePath(space: FileSpace, folderPath: string): string {
    return normalizeFolder(joinFolder(connectorSpaceRootPath(space), folderPath));
  }

  function connectorSpaceRelativePath(space: FileSpace, path: string): string {
    const root = connectorSpaceRootPath(space);
    const normalized = normalizeFolder(path);
    if (!root) return normalized;
    if (normalized === root) return "";
    if (normalized.startsWith(`${root}/`)) return normalized.slice(root.length + 1);
    return normalized;
  }

  function connectorSpaceLibraryForSync(space: FileSpace, item: FileConnectorBrowseItem): string {
    return connectorSpaceLibrary(space) || connectorMetadataString(item, "library_id") || connectorMetadataString(item, "share") || space.connector_profile_id || "default";
  }

  async function loadConnectorSpaceContents(
  space: FileSpace,
  options: {folderPath?: string;libraryId?: string | null;silent?: boolean;} = {})
  {
    if (!space.connector_profile_id) {
      setConnectorSpaceError("i18n:govoplan-files.connector_profile_is_not_configured_for_this_spa.669f57b5");
      return;
    }
    const relativeFolder = normalizeFolder(options.folderPath ?? currentFolder);
    const libraryId = options.libraryId !== undefined ? options.libraryId : connectorSpaceLibrary(space);
    if (!options.silent) {
      setBusy(true);
      setError("");
      setMessage("");
    }
    setConnectorSpaceLoading(true);
    setConnectorSpaceError("");
    try {
      const response = await browseFileConnectorProfile(settings, space.connector_profile_id, {
        path: connectorSpaceBrowsePath(space, relativeFolder),
        library_id: libraryId || undefined
      });
      setConnectorSpaceItemsBySpace((current) => ({ ...current, [space.id]: response.items }));
      setConnectorSpaceLibraryBySpace((current) => ({ ...current, [space.id]: response.library_id ?? libraryId ?? null }));
      setConnectorSpaceSelectedItem(null);
      setCurrentFolder(relativeFolder);
    } catch (err) {
      const detail = err instanceof Error ? err.message : String(err);
      setConnectorSpaceError(detail);
      setConnectorSpaceItemsBySpace((current) => ({ ...current, [space.id]: [] }));
    } finally {
      setConnectorSpaceLoading(false);
      if (!options.silent) setBusy(false);
    }
  }

  function openConnectorSpaceItem(item: FileConnectorBrowseItem) {
    if (!activeSpace || !isConnectorSpace(activeSpace)) return;
    if (item.kind === "file") {
      setConnectorSpaceSelectedItem(item);
      return;
    }
    if (item.kind === "library") {
      void loadConnectorSpaceContents(activeSpace, { folderPath: "", libraryId: item.external_id || item.path });
      return;
    }
    void loadConnectorSpaceContents(activeSpace, {
      folderPath: connectorSpaceRelativePath(activeSpace, item.path),
      libraryId: connectorSpaceLibrary(activeSpace)
    });
  }

  function browseConnectorSpaceParent() {
    if (!activeSpace || !isConnectorSpace(activeSpace)) return;
    if (currentFolder) {
      void loadConnectorSpaceContents(activeSpace, {
        folderPath: parentFolderPath(currentFolder),
        libraryId: connectorSpaceLibrary(activeSpace)
      });
      return;
    }
    if (connectorSpaceLibrary(activeSpace) && connectorSpaceLibrary(activeSpace) !== (activeSpace.library_id ?? null)) {
      void loadConnectorSpaceContents(activeSpace, { folderPath: "", libraryId: activeSpace.library_id ?? null });
    }
  }

  async function syncConnectorSpaceSelection(itemOverride?: FileConnectorBrowseItem) {
    const space = activeSpace;
    const selected = itemOverride ?? connectorSpaceSelectedItem;
    if (!canUpload || !space || !isConnectorSpace(space) || !space.connector_profile_id || !selected || selected.kind !== "file") return;
    const relativePath = connectorSpaceRelativePath(space, selected.path);
    const targetFolder = parentFolderPath(relativePath);
    setBusy(true);
    setError("");
    setMessage("");
    setConnectorSpaceError("");
    try {
      const response = await syncFileConnectorFile(settings, space.connector_profile_id, {
        library_id: connectorSpaceLibraryForSync(space, selected),
        path: selected.path,
        owner_type: space.owner_type,
        owner_id: space.owner_id,
        target_folder: targetFolder,
        conflict_strategy: "rename",
        source_revision: connectorRevision(selected),
        metadata: {
          ...selected.metadata,
          connector_space_id: space.connector_space_id,
          browse_name: selected.name,
          browse_path: selected.path,
          browse_modified_at: selected.modified_at,
          browse_etag: selected.etag,
          synced_from_space: true
        }
      });
      const actionLabel = response.action === "created" ?
      "i18n:govoplan-files.synced_new_file.d5e8243d" :
      response.action === "updated" ?
      "i18n:govoplan-files.synced_new_version.83784264" :
      "i18n:govoplan-files.connector_source_already_matched.7eaf5be1";
      setMessage(i18nMessage("i18n:govoplan-files.value_value.36bc3a40", { value0: actionLabel, value1: response.file.display_path }));
      setConnectorSpaceSelectedItem(null);
      const ownerSpace = spaces.find((item) => item.space_type !== "connector" && item.owner_type === space.owner_type && item.owner_id === space.owner_id);
      if (ownerSpace) await loadSpaceContents(ownerSpace, { silent: ownerSpace.id !== activeSpaceId });
    } catch (err) {
      const detail = err instanceof Error ? err.message : String(err);
      setError(detail);
      setConnectorSpaceError(detail);
    } finally {
      setBusy(false);
    }
  }



  async function handleFilesUpload(fileList: FileList | File[], options: {conflictStrategy?: ConflictStrategy;conflictResolutions?: ConflictResolution[];bypassConflictDialog?: boolean;target?: FileActionTarget;} = {}) {
    if (!canUpload) return;
    const target = options.target ?? currentActionTarget();
    const targetSpace = target ? findSpace(target.spaceId) : null;
    if (!target || !targetSpace || isConnectorSpace(targetSpace)) return;
    const selected = Array.from(fileList);
    if (selected.length === 0) return;
    if (!options.bypassConflictDialog && !unpackZip) {
      const conflicts = uploadConflicts(selected, target);
      if (conflicts.length > 0) {
        closeDialog();
        setConflictDialog({
          operation: "upload",
          title: i18nMessage("i18n:govoplan-files.value_upload_conflict_s.3a04f117", { value0: conflicts.length }),
          message: "i18n:govoplan-files.files_with_the_same_visible_name_already_exist_i.1bb487b0",
          items: conflicts,
          pendingUpload: { files: selected, target },
          review: false
        });
        return;
      }
    }
    const uploadsZipArchive = unpackZip && selected.some((file) => file.name.toLowerCase().endsWith(".zip"));
    setBusy(true);
    setUploadActive(true);
    setUploadPhase("uploading");
    setUploadProgress(0);
    setError("");
    setMessage(i18nMessage("i18n:govoplan-files.uploading_value_file_s.715ba963", { value0: selected.length }));
    try {
      const response = await uploadFiles(settings, selected, {
        owner_type: targetSpace.owner_type,
        owner_id: targetSpace.owner_id,
        path: target.folderPath,
        unpack_zip: unpackZip,
        conflict_strategy: options.conflictStrategy ?? "reject",
        conflict_resolutions: options.conflictResolutions,
        onProgress: ({ percentage }) => {
          setUploadProgress(percentage);
          if (percentage !== null && percentage >= 100) {
            setUploadPhase(uploadsZipArchive ? "unpacking" : "finalizing");
            setMessage(uploadsZipArchive ? "i18n:govoplan-files.unpacking_zip_upload.35019691" : "i18n:govoplan-files.finalizing_upload.bcce936d");
          }
        }
      });
      setUploadPhase("finalizing");
      setUploadProgress(100);
      setMessage(i18nMessage("i18n:govoplan-files.uploaded_value_file_s_into_value.d0fa052b", { value0: response.files.length, value1: target.folderPath || "i18n:govoplan-files.root.e96857c5" }));
      closeDialog();
      setConflictDialog(null);
      resetTransientState();
      await loadSpaceContents(targetSpace, { silent: targetSpace.id !== activeSpaceId });
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
      setMessage("");
    } finally {
      setBusy(false);
      setUploadActive(false);
      setUploadPhase("idle");
      setUploadProgress(null);
    }
  }

  async function uploadExternalFilesToTarget(fileList: FileList | File[], target: FileActionTarget) {
    const previousTarget = dialogTarget;
    setDialogTarget(target);
    try {
      await handleFilesUpload(fileList);
    } finally {
      setDialogTarget(previousTarget);
    }
  }

  async function openConnectorSyncDialog(target: FileActionTarget | null = toolbarTarget()) {
    if (!canUpload || !target) return;
    openDialog("connector-sync", target);
    if (connectorProfiles.length === 0) {
      await loadConnectorProfilesForDialog();
      return;
    }
    const profileId = connectorProfileId || connectorProfiles[0]?.id || "";
    if (profileId && connectorItems.length === 0) await browseConnector(profileId, "", null);
  }

  async function openConnectorSpaceDialog() {
    if (!canOrganize) return;
    const defaultOwnerSpace = activeSpace && !activeSpaceIsConnector ?
    activeSpace :
    spaces.find((space) => !isConnectorSpace(space)) ?? null;
    setConnectorSpaceOwnerSpaceId(defaultOwnerSpace?.id || "");
    setConnectorSpaceLabel("");
    openDialog("connector-space", null);
    if (connectorProfiles.length === 0) {
      await loadConnectorProfilesForDialog();
      return;
    }
    const profileId = connectorProfileId || connectorProfiles[0]?.id || "";
    if (profileId && connectorItems.length === 0) await browseConnector(profileId, "", null);
  }

  async function loadConnectorProfilesForDialog(preferredProfileId = connectorProfileId) {
    setConnectorLoading(true);
    setConnectorError("");
    try {
      const response = await listFileConnectorProfiles(settings);
      const enabledProfiles = response.profiles.filter((profile) => profile.enabled);
      setConnectorProfiles(enabledProfiles);
      const nextProfileId = enabledProfiles.some((profile) => profile.id === preferredProfileId) ?
      preferredProfileId :
      enabledProfiles[0]?.id || "";
      setConnectorProfileId(nextProfileId);
      setConnectorLibraryId(null);
      setConnectorPath("");
      setConnectorItems([]);
      setConnectorSelectedItem(null);
      if (nextProfileId) {
        await browseConnector(nextProfileId, "", null);
      } else {
        setConnectorError("i18n:govoplan-files.no_connector_profiles_are_available.9be3be28");
      }
    } catch (err) {
      setConnectorError(err instanceof Error ? err.message : String(err));
    } finally {
      setConnectorLoading(false);
    }
  }

  async function browseConnector(profileId: string, path = "", libraryId: string | null = connectorLibraryId) {
    if (!profileId) {
      setConnectorPath("");
      setConnectorLibraryId(null);
      setConnectorItems([]);
      setConnectorSelectedItem(null);
      return;
    }
    setConnectorLoading(true);
    setConnectorError("");
    try {
      const response = await browseFileConnectorProfile(settings, profileId, {
        path: normalizeFolder(path),
        library_id: libraryId || undefined
      });
      setConnectorPath(response.path || "");
      setConnectorLibraryId(response.library_id ?? libraryId ?? null);
      setConnectorItems(response.items);
      setConnectorSelectedItem(null);
    } catch (err) {
      setConnectorError(err instanceof Error ? err.message : String(err));
      setConnectorItems([]);
      setConnectorSelectedItem(null);
    } finally {
      setConnectorLoading(false);
    }
  }

  function handleConnectorProfileChange(profileId: string) {
    setConnectorProfileId(profileId);
    setConnectorLibraryId(null);
    setConnectorPath("");
    setConnectorItems([]);
    setConnectorSelectedItem(null);
    if (profileId) void browseConnector(profileId, "", null);
  }

  function openConnectorItem(item: FileConnectorBrowseItem) {
    if (!connectorProfileId) return;
    if (item.kind === "file") {
      setConnectorSelectedItem(item);
      return;
    }
    if (item.kind === "library") {
      void browseConnector(connectorProfileId, "", item.external_id || item.path);
      return;
    }
    void browseConnector(connectorProfileId, item.path, connectorLibraryId);
  }

  function browseConnectorParent() {
    if (!connectorProfileId) return;
    if (connectorPath) {
      void browseConnector(connectorProfileId, parentFolderPath(connectorPath), connectorLibraryId);
      return;
    }
    if (connectorLibraryId) void browseConnector(connectorProfileId, "", null);
  }

  function connectorSpaceSuggestedLabel(): string {
    const location = connectorPath ? lastPathSegment(connectorPath) : connectorLibraryId || "";
    return [activeConnectorProfile?.label, location].filter(Boolean).join(" / ") || "i18n:govoplan-files.linked_file_space.1a614c7d";
  }

  async function createConnectorSpaceFromDialog() {
    const ownerSpace = findSpace(connectorSpaceOwnerSpaceId) ?? spaces.find((space) => !isConnectorSpace(space)) ?? null;
    if (!canOrganize || !ownerSpace || isConnectorSpace(ownerSpace) || !connectorProfileId) {
      setConnectorError("i18n:govoplan-files.choose_a_connector_profile_and_owner_space.4c386b00");
      return;
    }
    const label = connectorSpaceLabel.trim() || connectorSpaceSuggestedLabel();
    const remotePath = normalizeFolder(connectorPath);
    setBusy(true);
    setError("");
    setMessage("");
    setConnectorError("");
    try {
      const created = await createFileConnectorSpace(settings, {
        owner_type: ownerSpace.owner_type,
        owner_id: ownerSpace.owner_id,
        label,
        connector_profile_id: connectorProfileId,
        library_id: connectorLibraryId || undefined,
        remote_path: remotePath,
        sync_mode: "manual",
        metadata: {
          created_from_ui: true,
          source_label: activeConnectorProfile?.label,
          source_path: remotePath,
          source_library_id: connectorLibraryId
        }
      });
      setMessage(i18nMessage("i18n:govoplan-files.added_connector_space_value.a24725b8", { value0: created.label }));
      closeDialog();
      resetTransientState();
      await loadSpaces();
      setActiveSpaceId(`connector:${created.id}`);
      setCurrentFolder("");
    } catch (err) {
      const detail = err instanceof Error ? err.message : String(err);
      setError(detail);
      setConnectorError(detail);
    } finally {
      setBusy(false);
    }
  }

  function connectorMetadataString(item: FileConnectorBrowseItem, key: string): string | null {
    const value = item.metadata[key];
    if (typeof value === "string" && value.trim()) return value;
    if (typeof value === "number" || typeof value === "boolean") return String(value);
    return null;
  }

  function connectorLibraryForSync(item: FileConnectorBrowseItem): string {
    return connectorLibraryId || connectorMetadataString(item, "library_id") || connectorMetadataString(item, "share") || activeConnectorProfile?.id || "default";
  }

  function connectorRevision(item: FileConnectorBrowseItem): string | null {
    return item.etag || item.modified_at || connectorMetadataString(item, "revision");
  }

  async function syncConnectorSelection() {
    const target = currentActionTarget();
    const targetSpace = target ? findSpace(target.spaceId) : null;
    const selected = connectorSelectedItem;
    if (!canUpload || !target || !targetSpace || !connectorProfileId || !selected || selected.kind !== "file") {
      setConnectorError("i18n:govoplan-files.choose_a_connector_file_and_destination_folder.3c6f3ffb");
      return;
    }
    setBusy(true);
    setError("");
    setMessage("");
    setConnectorError("");
    try {
      const response = await syncFileConnectorFile(settings, connectorProfileId, {
        library_id: connectorLibraryForSync(selected),
        path: selected.path,
        owner_type: targetSpace.owner_type,
        owner_id: targetSpace.owner_id,
        target_folder: target.folderPath,
        conflict_strategy: "rename",
        source_revision: connectorRevision(selected),
        metadata: {
          ...selected.metadata,
          browse_name: selected.name,
          browse_path: selected.path,
          browse_modified_at: selected.modified_at,
          browse_etag: selected.etag,
          synced_from_ui: true
        }
      });
      const actionLabel = response.action === "created" ?
      "i18n:govoplan-files.synced_new_file.d5e8243d" :
      response.action === "updated" ?
      "i18n:govoplan-files.synced_new_version.83784264" :
      "i18n:govoplan-files.connector_source_already_matched.7eaf5be1";
      setMessage(i18nMessage("i18n:govoplan-files.value_value.36bc3a40", { value0: actionLabel, value1: response.file.display_path }));
      closeDialog();
      resetTransientState();
      await loadSpaceContents(targetSpace, { silent: targetSpace.id !== activeSpaceId });
    } catch (err) {
      const detail = err instanceof Error ? err.message : String(err);
      setError(detail);
      setConnectorError(detail);
    } finally {
      setBusy(false);
    }
  }

  async function handleCreateFolder() {
    const target = currentActionTarget();
    const targetSpace = target ? findSpace(target.spaceId) : null;
    const trimmedName = newFolderName.trim();
    if (!target || !targetSpace || isConnectorSpace(targetSpace) || !trimmedName) return;
    const folderPath = joinFolder(target.folderPath, trimmedName);
    if (folderExistsInTarget(target, folderPath)) {
      setNewFolderError(`Folder already exists: ${folderPath}.`);
      return;
    }
    if (pathExistsInTarget(target, folderPath, { includeFolders: false })) {
      setNewFolderError(`A file already exists at ${folderPath}.`);
      return;
    }
    setBusy(true);
    setError("");
    setNewFolderError("");
    setMessage("");
    try {
      await createFolder(settings, {
        owner_type: targetSpace.owner_type,
        owner_id: targetSpace.owner_id,
        path: folderPath
      });
      setMessage(i18nMessage("i18n:govoplan-files.created_folder_value.6c3002f9", { value0: folderPath }));
      closeDialog();
      setNewFolderName("");
      resetTransientState();
      await loadSpaceContents(targetSpace, { silent: targetSpace.id !== activeSpaceId });
      setExpandedTreeNodes((current) => {
        const next = new Set(current);
        folderAncestorPaths(target.folderPath, { includeSelf: true }).forEach((path) => next.add(treeNodeKey(target.spaceId, path)));
        return next;
      });
    } catch (err) {
      setNewFolderError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy(false);
    }
  }

  async function runPatternSearch(patternOverride = searchPattern, caseSensitiveOverride = searchCaseSensitive) {
    const trimmedPattern = patternOverride.trim();
    if (!activeSpace || activeSpaceIsConnector || !trimmedPattern) {
      await clearSearch();
      return;
    }
    const caseSensitive = caseSensitiveOverride;
    setBusy(true);
    setError("");
    setMessage("");
    try {
      const response = await resolveFilePatterns(settings, {
        patterns: [trimmedPattern],
        owner_type: activeSpace.owner_type,
        owner_id: activeSpace.owner_id,
        path_prefix: currentFolder,
        include_unmatched: true,
        case_sensitive: caseSensitive
      });
      setSearchResults(response.patterns.flatMap((pattern) => pattern.matches));
      setSearchActive(true);
      setSearchPattern(trimmedPattern);
      setSearchCaseSensitive(caseSensitive);
      setUnmatchedCount(response.unmatched.length);
      setSelectedFileIds(new Set());
      setSelectedFolderPaths(new Set());
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy(false);
    }
  }

  async function clearSearch() {
    setSearchPattern("");
    setSearchActive(false);
    setSearchResults(null);
    setUnmatchedCount(null);
    setSelectedFileIds(new Set());
    setSelectedFolderPaths(new Set());
  }

  function deleteSelected(
  fileIds: Set<string> = selectedFileIds,
  folderPaths: Set<string> = selectedFolderPaths,
  space: FileSpace | null = activeSpace)
  {
    const fileCount = fileIds.size;
    const folderCount = folderPaths.size;
    if (!space || isConnectorSpace(space) || fileCount === 0 && folderCount === 0) return;
    const folderText = folderCount ? `${folderCount} folder(s) and their contents` : "";
    const fileText = fileCount ? `${fileCount} file(s)` : "";
    const what = [folderText, fileText].filter(Boolean).join(" plus ");
    setDeleteDialog({
      fileIds: new Set(fileIds),
      folderPaths: new Set(folderPaths),
      spaceId: space.id,
      title: "i18n:govoplan-files.delete_selected_item_s.4b6a0023",
      message: i18nMessage("i18n:govoplan-files.delete_value_audit_relevant_blobs_remain_retaine.290af88f", { value0: what })
    });
  }

  async function performConfirmedDelete() {
    const pending = deleteDialog;
    if (!pending) return;
    const space = findSpace(pending.spaceId);
    if (!space) {
      setDeleteDialog(null);
      return;
    }
    setBusy(true);
    setError("");
    setMessage("");
    try {
      if (pending.fileIds.size > 0) {
        await bulkDeleteFiles(settings, Array.from(pending.fileIds));
      }
      if (pending.folderPaths.size > 0) {
        for (const folderPath of pending.folderPaths) {
          await deleteFolder(settings, {
            owner_type: space.owner_type,
            owner_id: space.owner_id,
            path: folderPath,
            recursive: true
          });
        }
      }
      setDeleteDialog(null);
      resetTransientState();
      await loadSpaceContents(space, { silent: space.id !== activeSpaceId });
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy(false);
    }
  }


  function downloadLabelForSets(sets: SelectionSets, spaceId = activeSpaceId): string {
    const count = fileIdsForSelection(filesBySpace[spaceId] ?? EMPTY_FILES, sets.fileIds, sets.folderPaths).length;
    return count > 1 ? "i18n:govoplan-files.download_zip.7bd0e4b0" : "i18n:govoplan-files.download.a479c9c3";
  }

  async function downloadSelection(
  fileIds: Set<string> = selectedFileIds,
  folderPaths: Set<string> = selectedFolderPaths,
  sourceFiles: ManagedFile[] = files)
  {
    if (!canDownload) return;
    const idsToDownload = fileIdsForSelection(sourceFiles, fileIds, folderPaths);
    if (idsToDownload.length === 0) return;
    setBusy(true);
    setError("");
    try {
      if (idsToDownload.length === 1) {
        const file = sourceFiles.find((item) => item.id === idsToDownload[0]);
        if (file) await downloadFile(settings, file);
      } else {
        await downloadFilesAsZip(settings, idsToDownload, "multi-seal-mail-files.zip");
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy(false);
    }
  }

  function openTransferDialog(mode: TransferMode, sourceSpaceId = activeSpaceId, sets: SelectionSets = currentSelectionSets(), defaultTarget?: FileActionTarget) {
    if (!canOrganize) return;
    if (!sourceSpaceId || sets.fileIds.size === 0 && sets.folderPaths.size === 0) return;
    setTransferMode(mode);
    setTransferDialogState({
      fileIds: new Set(sets.fileIds),
      folderPaths: new Set(sets.folderPaths),
      sourceSpaceId,
      targetSpaceId: defaultTarget?.spaceId ?? sourceSpaceId,
      targetFolder: defaultTarget?.folderPath ?? currentFolder
    });
    setDialog("transfer");
  }

  function invalidDropReason(sourceSpaceId: string, folderPaths: Iterable<string>, target: FileActionTarget): string | null {
    if (sourceSpaceId !== target.spaceId) return null;
    const targetFolder = normalizeFolder(target.folderPath);
    for (const folderPathRaw of folderPaths) {
      const folderPath = normalizeFolder(folderPathRaw);
      if (!folderPath) continue;
      if (targetFolder === folderPath || targetFolder.startsWith(`${folderPath}/`)) {
        return "i18n:govoplan-files.cannot_move_or_copy_a_folder_into_itself_or_one_.4adbd5ea";
      }
    }
    return null;
  }

  async function runTransfer(mode: TransferMode, sourceSpaceId: string, sets: SelectionSets, target: FileActionTarget, options: {conflictStrategy?: ConflictStrategy;conflictResolutions?: ConflictResolution[];bypassConflictDialog?: boolean;} = {}) {
    const sourceSpace = findSpace(sourceSpaceId);
    const targetSpace = findSpace(target.spaceId);
    if (!sourceSpace || !targetSpace || isConnectorSpace(sourceSpace) || isConnectorSpace(targetSpace)) return;
    const reason = invalidDropReason(sourceSpaceId, sets.folderPaths, target);
    if (reason) {
      setError(reason);
      return;
    }
    if (sets.fileIds.size === 0 && sets.folderPaths.size === 0) return;
    if (mode === "move" && transferWouldBeNoop(sourceSpaceId, sets, target)) {
      setInternalDrag(null);
      setDropTargetKey("");
      return;
    }
    if (!options.bypassConflictDialog) {
      const conflicts = transferConflicts(mode, sourceSpaceId, sets, target);
      if (conflicts.length > 0) {
        closeDialog();
        setConflictDialog({
          operation: "transfer",
          title: i18nMessage("i18n:govoplan-files.value_conflict_s.b3872a48", { value0: conflicts.length }),
          message: i18nMessage("i18n:govoplan-files.value_this_selection_would_create_duplicate_visi.26ae34d4", { value0: mode === "copy" ? "i18n:govoplan-files.copying.43b7de98" : "i18n:govoplan-files.moving.65cd4dd8" }),
          items: conflicts,
          pendingTransfer: { mode, sourceSpaceId, sets: { fileIds: new Set(sets.fileIds), folderPaths: new Set(sets.folderPaths) }, target },
          review: false
        });
        setInternalDrag(null);
        setDropTargetKey("");
        return;
      }
    }
    setBusy(true);
    setError("");
    setMessage("");
    try {
      const response = await transferFiles(settings, {
        operation: mode,
        file_ids: Array.from(sets.fileIds),
        folder_paths: Array.from(sets.folderPaths),
        source_owner_type: sourceSpace.owner_type,
        source_owner_id: sourceSpace.owner_id,
        target_owner_type: targetSpace.owner_type,
        target_owner_id: targetSpace.owner_id,
        target_folder: target.folderPath,
        conflict_strategy: options.conflictStrategy ?? "reject",
        conflict_resolutions: options.conflictResolutions
      });
      setMessage(i18nMessage("i18n:govoplan-files.value_value_file_s_and_value_folder_s.c160e725", { value0: mode === "copy" ? "i18n:govoplan-files.copied.8e3df45a" : "i18n:govoplan-files.moved.0fc28db0", value1: response.files, value2: response.folders }));
      setConflictDialog(null);
      closeDialog();
      resetTransientState();
      const reloadIds = new Set([sourceSpace.id, targetSpace.id]);
      await Promise.all(Array.from(reloadIds).map((spaceId) => {
        const space = findSpace(spaceId);
        return space ? loadSpaceContents(space, { silent: space.id !== activeSpaceId }) : Promise.resolve();
      }));
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy(false);
      setInternalDrag(null);
      setDropTargetKey("");
    }
  }

  async function handleTransferDialogSubmit() {
    if (!transferDialogState) return;
    const state = transferDialogState;
    const mode = transferMode;
    closeDialog();
    await runTransfer(mode, state.sourceSpaceId, state, {
      spaceId: state.targetSpaceId,
      folderPath: state.targetFolder
    });
  }

  async function runRenamePreview(dryRun: boolean) {
    if (!activeSpace || activeSpaceIsConnector || selectedEntryCount === 0) return;
    setBusy(true);
    setError("");
    try {
      const response = await bulkRenameFiles(settings, {
        file_ids: Array.from(selectedFileIds),
        folder_paths: Array.from(selectedFolderPaths),
        owner_type: activeSpace.owner_type,
        owner_id: activeSpace.owner_id,
        mode: renameMode,
        find: renameFind,
        replacement: renameReplacement,
        prefix: renamePrefix,
        suffix: renameSuffix,
        recursive: renameRecursive,
        dry_run: dryRun
      });
      setRenamePreview(response);
      setRenamePreviewVisibleCount(20);
      if (dryRun) {
        setMessage("");
      } else {
        setMessage(i18nMessage("i18n:govoplan-files.renamed_value_item_s.3c4a40fe", { value0: response.items.length }));
      }
      if (!dryRun) {
        closeDialog();
        resetTransientState();
        await loadSpaceContents(activeSpace);
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy(false);
    }
  }

  function openRenameDialog() {
    if (!canOrganize || activeSpaceIsConnector || selectedEntryCount === 0) return;
    setRenamePreview(null);
    if (selectedEntryCount === 1) {
      const selectedFile = selectedFiles[0];
      const selectedFolderPath = Array.from(selectedFolderPaths)[0];
      setSingleRenameName(selectedFile ? selectedFile.filename : lastPathSegment(selectedFolderPath));
      openDialog("single-rename");
      return;
    }
    openDialog("rename");
  }

  async function runSingleRename() {
    if (!canOrganize || !activeSpace || activeSpaceIsConnector || selectedEntryCount !== 1 || !singleRenameName.trim()) return;
    setBusy(true);
    setError("");
    try {
      const response = await bulkRenameFiles(settings, {
        file_ids: Array.from(selectedFileIds),
        folder_paths: Array.from(selectedFolderPaths),
        owner_type: activeSpace.owner_type,
        owner_id: activeSpace.owner_id,
        mode: "direct",
        new_name: singleRenameName,
        dry_run: false
      });
      setMessage(i18nMessage("i18n:govoplan-files.renamed_value_item_s.3c4a40fe", { value0: response.items.length }));
      closeDialog();
      resetTransientState();
      await loadSpaceContents(activeSpace);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy(false);
    }
  }

  function openFolder(spaceId: string, path: string) {
    const space = findSpace(spaceId);
    if (spaceId !== activeSpaceId) setActiveSpaceId(spaceId);
    resetTransientState();
    if (space && isConnectorSpace(space)) {
      void loadConnectorSpaceContents(space, { folderPath: normalizeFolder(path) });
      return;
    }
    setCurrentFolder(normalizeFolder(path));
  }


  function handleEntryKeyDown(entry: ExplorerEntry, event: ReactKeyboardEvent<HTMLElement>) {
    if (event.key === " " || event.key === "Spacebar") {
      event.preventDefault();
      handleEntrySelection(entry, event as unknown as ReactMouseEvent<HTMLElement>);
    }
    if (event.key === "Enter") {
      event.preventDefault();
      if (entry.kind === "folder") activeSpace && openFolder(activeSpace.id, entry.path);else
      if (canDownload) void downloadFile(settings, entry.file);
    }
  }

  function dragSetsForEntry(entry: ExplorerEntry): SelectionSets {
    const key = entrySelectionKey(entry);
    return currentSelectionKeys().has(key) ? currentSelectionSets() : setsForEntry(entry);
  }

  function handleInternalDragStart(entry: ExplorerEntry, event: ReactDragEvent<HTMLElement>) {
    if (!canOrganize || !activeSpace || activeSpaceIsConnector) {event.preventDefault();return;}
    const sets = dragSetsForEntry(entry);
    if (!currentSelectionKeys().has(entrySelectionKey(entry))) {
      applySelectionKeys(new Set<EntrySelectionKey>([entrySelectionKey(entry)]));
      setSelectionAnchor(entrySelectionKey(entry));
    }
    const state = { sourceSpaceId: activeSpace.id, fileIds: Array.from(sets.fileIds), folderPaths: Array.from(sets.folderPaths) };
    setInternalDrag(state);
    event.dataTransfer.effectAllowed = "i18n:govoplan-files.copymove.d0fa5904";
    event.dataTransfer.setData(INTERNAL_DRAG_TYPE, JSON.stringify(state));
    event.dataTransfer.setData("text/plain", `${state.fileIds.length + state.folderPaths.length} item(s)`);
  }

  function handleTreeFolderDragStart(spaceId: string, path: string, event: ReactDragEvent<HTMLElement>) {
    if (!canOrganize || isConnectorSpace(findSpace(spaceId))) {event.preventDefault();return;}
    const folderPath = normalizeFolder(path);
    if (!folderPath) return;
    const state = { sourceSpaceId: spaceId, fileIds: [], folderPaths: [folderPath] };
    setInternalDrag(state);
    event.dataTransfer.effectAllowed = "i18n:govoplan-files.copymove.d0fa5904";
    event.dataTransfer.setData(INTERNAL_DRAG_TYPE, JSON.stringify(state));
    event.dataTransfer.setData("text/plain", folderPath);
  }

  function isInternalDrag(event: ReactDragEvent<HTMLElement>): boolean {
    return event.dataTransfer.types.includes(INTERNAL_DRAG_TYPE) || Boolean(internalDrag);
  }

  function dropTargetId(target: FileActionTarget): string {
    return `${target.spaceId}:${normalizeFolder(target.folderPath)}`;
  }

  function handleDropTargetDragOver(event: ReactDragEvent<HTMLElement>, target: FileActionTarget) {
    event.preventDefault();
    event.stopPropagation();
    if (isConnectorSpace(findSpace(target.spaceId))) {
      event.dataTransfer.dropEffect = "none";
      setDropTargetKey("");
      return;
    }
    if (isInternalDrag(event)) {
      if (!canOrganize) {event.dataTransfer.dropEffect = "none";setDropTargetKey("");return;}
      const state = internalDrag;
      const mode = event.ctrlKey ? "copy" : "move";
      const sets = state ? { fileIds: new Set(state.fileIds), folderPaths: new Set(state.folderPaths) } : null;
      const reason = state ? invalidDropReason(state.sourceSpaceId, state.folderPaths, target) : null;
      const noop = state && sets && mode === "move" ? transferWouldBeNoop(state.sourceSpaceId, sets, target) : false;
      event.dataTransfer.dropEffect = reason || noop ? "none" : mode;
      setDropTargetKey(reason || noop ? "" : dropTargetId(target));
      return;
    }
    event.dataTransfer.dropEffect = canUpload ? "copy" : "none";
    setDropTargetKey(canUpload ? dropTargetId(target) : "");
  }

  async function handleDropOnTarget(event: ReactDragEvent<HTMLElement>, target: FileActionTarget) {
    event.preventDefault();
    event.stopPropagation();
    setDragActive(false);
    setDropTargetKey("");
    if (isConnectorSpace(findSpace(target.spaceId))) return;
    if (isInternalDrag(event)) {
      if (!canOrganize) return;
      const state = internalDrag ?? parseDragState(event.dataTransfer.getData(INTERNAL_DRAG_TYPE));
      if (!state) return;
      const reason = invalidDropReason(state.sourceSpaceId, state.folderPaths, target);
      if (reason) {
        setError(reason);
        return;
      }
      await runTransfer(event.ctrlKey ? "copy" : "move", state.sourceSpaceId, { fileIds: new Set(state.fileIds), folderPaths: new Set(state.folderPaths) }, target);
      return;
    }
    if (canUpload && event.dataTransfer.files.length > 0) {
      await uploadExternalFilesToTarget(event.dataTransfer.files, target);
    }
  }

  function clearDropState() {
    clearDragDropState();
    cancelTreeDragExpand();
  }

  function spaceForContext(menu: ContextMenuState | null): FileSpace | null {
    const spaceId = menu?.spaceId ?? activeSpaceId;
    return findSpace(spaceId) ?? activeSpace;
  }

  function filesForContext(menu: ContextMenuState | null): ManagedFile[] {
    const spaceId = menu?.spaceId ?? activeSpaceId;
    return filesBySpace[spaceId] ?? files;
  }

  function folderEntryForPath(spaceId: string, path: string): FolderEntry {
    const sourceFiles = filesBySpace[spaceId] ?? EMPTY_FILES;
    const sourceFolders = foldersBySpace[spaceId] ?? EMPTY_FOLDERS;
    return buildFolderEntryFromSources(sourceFiles, sourceFolders, path);
  }

  function selectedSetsForContext(menu: ContextMenuState | null): SelectionSets {
    if (!menu) return { fileIds: new Set(selectedFileIds), folderPaths: new Set(selectedFolderPaths) };
    if (menu.source === "tree") {
      if (menu.entry?.kind === "folder") return { fileIds: new Set(), folderPaths: new Set([menu.entry.path]) };
      if (menu.folderPath !== undefined) {
        const sourceFiles = filesForContext(menu);
        const fileIds = new Set(sourceFiles.filter((file) => isFileInFolder(file, menu.folderPath ?? "")).map((file) => file.id));
        return { fileIds, folderPaths: new Set() };
      }
    }
    if (menu.target === "empty" || !menu.entry) {
      return { fileIds: new Set(selectedFileIds), folderPaths: new Set(selectedFolderPaths) };
    }
    const key = entrySelectionKey(menu.entry);
    if (currentSelectionKeys().has(key)) {
      return { fileIds: new Set(selectedFileIds), folderPaths: new Set(selectedFolderPaths) };
    }
    return setsForEntry(menu.entry);
  }

  function openContextMenu(event: ReactMouseEvent<HTMLElement>, target: "empty" | "folder" | "file", entry?: ExplorerEntry) {
    event.preventDefault();
    event.stopPropagation();
    if (entry) {
      const key = entrySelectionKey(entry);
      const currentKeys = currentSelectionKeys();
      if (!currentKeys.has(key)) {
        applySelectionKeys(new Set<EntrySelectionKey>([key]));
        setSelectionAnchor(key);
      }
    }
    setContextMenu({ x: event.clientX, y: event.clientY, target, entry, source: "list", spaceId: activeSpaceId, folderPath: entry?.kind === "folder" ? entry.path : currentFolder });
  }

  function openTreeContextMenu(event: ReactMouseEvent<HTMLElement>, spaceId: string, path: string) {
    event.preventDefault();
    event.stopPropagation();
    const normalizedPath = normalizeFolder(path);
    const entry = normalizedPath ? folderEntryForPath(spaceId, normalizedPath) : undefined;
    setContextMenu({
      x: event.clientX,
      y: event.clientY,
      target: entry ? "folder" : "empty",
      entry,
      source: "tree",
      spaceId,
      folderPath: normalizedPath
    });
  }

  function openTreeEmptyContextMenu(event: ReactMouseEvent<HTMLElement>) {
    event.preventDefault();
    const target = toolbarTarget();
    if (!target) return;
    setContextMenu({
      x: event.clientX,
      y: event.clientY,
      target: "empty",
      source: "tree",
      spaceId: target.spaceId,
      folderPath: target.folderPath
    });
  }

  function contextActionTarget(menu: ContextMenuState | null): FileActionTarget | null {
    const space = spaceForContext(menu);
    if (!space) return null;
    const folderPath = menu?.entry?.kind === "folder" ? menu.entry.path : menu?.folderPath ?? currentFolder;
    return { spaceId: space.id, folderPath: normalizeFolder(folderPath) };
  }

  function openUploadDialogForContext(menu: ContextMenuState | null) {
    const target = contextActionTarget(menu);
    setContextMenu(null);
    openDialog("upload", target);
  }

  function openCreateFolderDialogForContext(menu: ContextMenuState | null) {
    const target = contextActionTarget(menu);
    setContextMenu(null);
    openDialog("create-folder", target);
  }

  async function downloadContextSelection(menu: ContextMenuState | null) {
    const { fileIds, folderPaths } = selectedSetsForContext(menu);
    const sourceFiles = filesForContext(menu);
    setContextMenu(null);
    await downloadSelection(fileIds, folderPaths, sourceFiles);
  }

  async function deleteContextSelection(menu: ContextMenuState | null) {
    const { fileIds, folderPaths } = selectedSetsForContext(menu);
    const space = spaceForContext(menu);
    setContextMenu(null);
    await deleteSelected(fileIds, folderPaths, space);
  }

  function accessTargetForSelection(fileIds: Set<string>, folderPaths: Set<string>, spaceId: string): FileAccessExplanationTarget | null {
    if (fileIds.size === 1 && folderPaths.size === 0) {
      const fileId = Array.from(fileIds)[0];
      const file = filesInSpace(spaceId).find((item) => item.id === fileId);
      return file ? {
        resourceType: "file",
        resourceId: file.id,
        label: file.display_path || file.filename,
        action: "files:file:read"
      } : null;
    }
    if (fileIds.size === 0 && folderPaths.size === 1) {
      const folderPath = normalizeFolder(Array.from(folderPaths)[0]);
      const folder = foldersInSpace(spaceId).find((item) => normalizeFolder(item.path) === folderPath);
      if (folder) {
        return {
          resourceType: "folder",
          resourceId: folder.id,
          label: folder.path,
          action: "files:file:read"
        };
      }
      const space = findSpace(spaceId);
      const tenantId = (auth.active_tenant ?? auth.tenant)?.id;
      if (!space || !tenantId || !folderPath) return null;
      return {
        resourceType: "folder",
        resourceId: virtualFolderResourceId({ tenantId, ownerType: space.owner_type, ownerId: space.owner_id, path: folderPath }),
        label: folderPath,
        action: "files:file:read"
      };
    }
    return null;
  }

  function accessTargetForContext(menu: ContextMenuState | null): FileAccessExplanationTarget | null {
    const { fileIds, folderPaths } = selectedSetsForContext(menu);
    return accessTargetForSelection(fileIds, folderPaths, menu?.spaceId ?? activeSpaceId);
  }

  function openAccessExplanationForContext(menu: ContextMenuState | null) {
    const target = accessTargetForContext(menu);
    setContextMenu(null);
    if (target) void openAccessExplanation(target);
  }

  async function openAccessExplanation(target: FileAccessExplanationTarget): Promise<void> {
    if (!auth.user?.id) return;
    setAccessExplanationTarget(target);
    setResourceAccessExplanation(null);
    setResourceAccessLoading(true);
    setError("");
    try {
      setResourceAccessExplanation(await fetchResourceAccessExplanation(settings, {
        userId: auth.user.id,
        resourceType: target.resourceType,
        resourceId: target.resourceId,
        action: target.action,
        tenantId: (auth.active_tenant ?? auth.tenant)?.id
      }));
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
      setAccessExplanationTarget(null);
    } finally {
      setResourceAccessLoading(false);
    }
  }

  function openTransferDialogForContext(menu: ContextMenuState | null, mode: TransferMode) {
    const sets = selectedSetsForContext(menu);
    const space = spaceForContext(menu);
    const target = contextActionTarget(menu) ?? (activeSpace ? { spaceId: activeSpace.id, folderPath: currentFolder } : null);
    setContextMenu(null);
    if (space) openTransferDialog(mode, space.id, sets, target ?? undefined);
  }

  function toolbarTarget(): FileActionTarget | null {
    return activeSpace ? { spaceId: activeSpace.id, folderPath: currentFolder } : null;
  }

  function parentFolder(path: string): string {
    const parts = normalizeFilePath(path).split("/").filter(Boolean);
    return normalizeFolder(parts.slice(0, -1).join("/"));
  }

  function filesInSpace(spaceId: string): ManagedFile[] {
    return filesBySpace[spaceId] ?? EMPTY_FILES;
  }

  function foldersInSpace(spaceId: string): FileFolder[] {
    return foldersBySpace[spaceId] ?? EMPTY_FOLDERS;
  }

  function pathExistsInTarget(target: FileActionTarget, path: string, options: {excludeFileId?: string;includeFolders?: boolean;} = {}): boolean {
    const normalized = normalizeFilePath(path);
    const targetFiles = filesInSpace(target.spaceId);
    if (targetFiles.some((file) => file.id !== options.excludeFileId && normalizeFilePath(file.display_path) === normalized)) return true;
    if (options.includeFolders && folderExistsInTarget(target, normalized)) return true;
    return false;
  }

  function folderExistsInTarget(target: FileActionTarget, path: string): boolean {
    const normalized = normalizeFolder(path);
    return foldersInSpace(target.spaceId).some((folder) => normalizeFolder(folder.path) === normalized);
  }

  function uploadConflicts(uploadedFiles: File[], target: FileActionTarget): FileConflictItem[] {
    const conflicts: FileConflictItem[] = [];
    const reserved = new Set<string>();
    uploadedFiles.forEach((file, index) => {
      const targetPath = normalizeFilePath(joinFolder(target.folderPath, file.name));
      if (pathExistsInTarget(target, targetPath, { includeFolders: true }) || reserved.has(targetPath)) {
        conflicts.push({
          id: `upload:${index}:${targetPath}`,
          kind: "file",
          label: file.name,
          targetPath,
          action: "rename",
          newPath: nextClientAvailablePath(target, targetPath, reserved)
        });
      }
      reserved.add(targetPath);
    });
    return conflicts;
  }

  function nextClientAvailablePath(target: FileActionTarget, desiredPath: string, reserved = new Set<string>()): string {
    const desired = normalizeFilePath(desiredPath);
    if (!reserved.has(desired) && !pathExistsInTarget(target, desired, { includeFolders: true })) return desired;
    for (let counter = 1; counter < 10000; counter += 1) {
      const candidate = candidateRenamedPath(desired, counter);
      if (!reserved.has(candidate) && !pathExistsInTarget(target, candidate, { includeFolders: true })) return candidate;
    }
    return desired;
  }

  function transferWouldBeNoop(sourceSpaceId: string, sets: SelectionSets, target: FileActionTarget): boolean {
    if (sourceSpaceId !== target.spaceId) return false;
    const targetFolder = normalizeFolder(target.folderPath);
    const sourceFiles = filesInSpace(sourceSpaceId);
    const selectedItems = sets.fileIds.size + sets.folderPaths.size;
    if (selectedItems === 0) return false;
    for (const fileId of sets.fileIds) {
      const file = sourceFiles.find((item) => item.id === fileId);
      if (!file || parentFolder(file.display_path) !== targetFolder) return false;
    }
    for (const folderPath of sets.folderPaths) {
      if (parentFolder(folderPath) !== targetFolder) return false;
    }
    return true;
  }

  function transferConflicts(mode: TransferMode, sourceSpaceId: string, sets: SelectionSets, target: FileActionTarget): FileConflictItem[] {
    const conflicts: FileConflictItem[] = [];
    const sourceFiles = filesInSpace(sourceSpaceId);
    const reserved = new Set<string>();
    const maybeAddConflict = (id: string, kind: "file" | "folder", label: string, targetPath: string, excludeFileId?: string) => {
      const normalized = kind === "folder" ? normalizeFolder(targetPath) : normalizeFilePath(targetPath);
      const exists = kind === "folder" ?
      foldersInSpace(target.spaceId).some((folder) => normalizeFolder(folder.path) === normalized) :
      pathExistsInTarget(target, normalized, { excludeFileId: mode === "move" && sourceSpaceId === target.spaceId ? excludeFileId : undefined });
      if (exists || reserved.has(normalized)) {
        conflicts.push({
          id,
          kind,
          label,
          targetPath: normalized,
          action: "rename",
          newPath: kind === "folder" ? nextClientAvailableFolderPath(target, normalized, reserved) : nextClientAvailablePath(target, normalized, reserved)
        });
      }
      reserved.add(normalized);
    };

    for (const fileId of sets.fileIds) {
      const file = sourceFiles.find((item) => item.id === fileId);
      if (!file) continue;
      const targetPath = normalizeFilePath(joinFolder(target.folderPath, file.filename));
      if (mode === "move" && sourceSpaceId === target.spaceId && normalizeFilePath(file.display_path) === targetPath) continue;
      maybeAddConflict(`file:${file.id}`, "file", file.filename, targetPath, file.id);
    }

    for (const folderPathRaw of sets.folderPaths) {
      const folderPath = normalizeFolder(folderPathRaw);
      const folderName = lastPathSegment(folderPath);
      const targetFolderPath = normalizeFolder(joinFolder(target.folderPath, folderName));
      if (mode === "move" && sourceSpaceId === target.spaceId && folderPath === targetFolderPath) continue;
      maybeAddConflict(`folder:${folderPath}`, "folder", folderName, targetFolderPath);
      for (const file of sourceFiles.filter((item) => isFileInFolder(item, folderPath))) {
        const relative = normalizeFilePath(file.display_path).slice(folderPath.length + 1);
        const targetPath = normalizeFilePath(joinFolder(targetFolderPath, relative));
        if (mode === "move" && sourceSpaceId === target.spaceId && normalizeFilePath(file.display_path) === targetPath) continue;
        maybeAddConflict(`folder-file:${folderPath}:${file.id}`, "file", relative, targetPath, file.id);
      }
    }
    return conflicts;
  }

  function nextClientAvailableFolderPath(target: FileActionTarget, desiredPath: string, reserved = new Set<string>()): string {
    const desired = normalizeFolder(desiredPath);
    if (!reserved.has(desired) && !foldersInSpace(target.spaceId).some((folder) => normalizeFolder(folder.path) === desired)) return desired;
    for (let counter = 1; counter < 10000; counter += 1) {
      const candidate = candidateRenamedPath(desired, counter);
      if (!reserved.has(candidate) && !foldersInSpace(target.spaceId).some((folder) => normalizeFolder(folder.path) === candidate)) return candidate;
    }
    return desired;
  }

  function updateConflictItem(id: string, patch: Partial<FileConflictItem>) {
    setConflictDialog((current) => current ? { ...current, items: current.items.map((item) => item.id === id ? { ...item, ...patch } : item) } : current);
  }

  async function applyConflictDecision(strategy: ConflictStrategy | "review") {
    const currentConflict = conflictDialog;
    if (!currentConflict) return;
    if (strategy === "review" && !currentConflict.review) {
      setConflictDialog({ ...currentConflict, review: true });
      return;
    }

    const reviewed = currentConflict.review || strategy === "review";
    const conflictStrategy: ConflictStrategy = reviewed ? "reject" : strategy as ConflictStrategy;
    const conflictResolutions: ConflictResolution[] | undefined = reviewed ?
    currentConflict.items.map((item) => ({
      target_path: item.targetPath,
      action: item.action,
      new_path: item.action === "rename" ? item.newPath : undefined
    })) :
    undefined;

    setConflictDialog(null);

    if (currentConflict.operation === "upload" && currentConflict.pendingUpload) {
      const pending = currentConflict.pendingUpload;
      let filesToUpload = pending.files;
      let uploadResolutions = conflictResolutions;
      if (reviewed) {
        const skippedPaths = new Set(currentConflict.items.filter((item) => item.action === "skip").map((item) => item.targetPath));
        filesToUpload = pending.files.filter((file) => !skippedPaths.has(normalizeFilePath(joinFolder(pending.target.folderPath, file.name))));
        uploadResolutions = conflictResolutions?.filter((item) => item.action !== "skip");
      }
      if (filesToUpload.length === 0) {
        closeDialog();
        setMessage("i18n:govoplan-files.no_files_uploaded_all_conflicts_were_skipped.1166f3b5");
        return;
      }
      await handleFilesUpload(filesToUpload, {
        target: pending.target,
        conflictStrategy,
        conflictResolutions: uploadResolutions,
        bypassConflictDialog: true
      });
      return;
    }
    if (currentConflict.operation === "transfer" && currentConflict.pendingTransfer) {
      const pending = currentConflict.pendingTransfer;
      await runTransfer(pending.mode, pending.sourceSpaceId, pending.sets, pending.target, {
        conflictStrategy,
        conflictResolutions,
        bypassConflictDialog: true
      });
    }
  }

  function toggleSort(column: SortColumn) {
    if (sortColumn === column) {
      setSortDirection((current) => current === "asc" ? "desc" : "asc");
      return;
    }
    setSortColumn(column);
    setSortDirection("asc");
  }

  function sortLabel(column: SortColumn, label: string): string {
    if (sortColumn !== column) return label;
    return `${label} ${sortDirection === "asc" ? "↑" : "↓"}`;
  }

  const noticeTone = message.startsWith("i18n:govoplan-files.no_files_uploaded_all_conflicts_were_skipped") ? "warning" :
  message.startsWith("i18n:govoplan-files.uploading_value_file_s") ||
  message === "i18n:govoplan-files.unpacking_zip_upload.35019691" ||
  message === "i18n:govoplan-files.finalizing_upload.bcce936d" ? "info" : "success";

  const toolbar =
  <div className="file-manager-toolbar" aria-label="i18n:govoplan-files.file_actions.9e1b94c5">
      <Button variant="primary" onClick={() => openDialog("upload", toolbarTarget())} disabled={busy || !activeSpace || activeSpaceIsConnector || !canUpload}><UploadCloud size={16} aria-hidden="true" /> i18n:govoplan-files.upload.8bdf057f</Button>
      <Button
      onClick={() => activeSpaceIsConnector ? void syncConnectorSpaceSelection() : void openConnectorSyncDialog(toolbarTarget())}
      disabled={busy || !activeSpace || !canUpload || activeSpaceIsConnector && connectorSpaceSelectedItem?.kind !== "file"}>
      
        <RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-files.sync.905f6309
      </Button>
      <Button onClick={() => void openConnectorSpaceDialog()} disabled={busy || !canOrganize}>
        <Link2 size={16} aria-hidden="true" /> i18n:govoplan-files.add_space.e4d674d4
      </Button>
      <Button onClick={() => void downloadSelection()} disabled={busy || activeSpaceIsConnector || !canDownload || selectedDownloadFileIds.length === 0}><Download size={16} aria-hidden="true" /> {downloadLabel}</Button>
      <Button onClick={() => openDialog("create-folder", toolbarTarget())} disabled={busy || !activeSpace || activeSpaceIsConnector || !canOrganize}><Plus size={16} aria-hidden="true" /> i18n:govoplan-files.create_folder.97bafaba</Button>
      <Button onClick={() => openTransferDialog("move")} disabled={busy || activeSpaceIsConnector || !canOrganize || !hasSelection}><MoveRight size={16} aria-hidden="true" /> i18n:govoplan-files.move.76cdb950</Button>
      <Button onClick={() => openTransferDialog("copy")} disabled={busy || activeSpaceIsConnector || !canOrganize || !hasSelection}><Copy size={16} aria-hidden="true" /> i18n:govoplan-files.copy.af74f7c5</Button>
      {hasSelection && <Button onClick={openRenameDialog} disabled={busy || activeSpaceIsConnector || !canOrganize}>{selectedEntryCount === 1 ? "i18n:govoplan-files.rename.d3f4cb89" : "i18n:govoplan-files.bulk_rename.7dcaa624"}</Button>}
      <Button onClick={() => accessExplainableTarget && void openAccessExplanation(accessExplainableTarget)} disabled={busy || activeSpaceIsConnector || !canExplainResourceAccess || !accessExplainableTarget}><KeyRound size={16} aria-hidden="true" /> i18n:govoplan-files.explain_access.4d5fac37</Button>
      <Button variant="danger" onClick={() => void deleteSelected()} disabled={busy || activeSpaceIsConnector || !canDelete || !hasSelection}><Trash2 size={16} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</Button>
      {activeSpaceIsConnector &&
    <Button onClick={() => activeSpace && void loadConnectorSpaceContents(activeSpace)} disabled={busy || connectorSpaceLoading || !activeSpace}>
          <RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-files.refresh.56e3badc
        </Button>
    }
    </div>;


  const uploadBusyLabel = uploadPhase === "unpacking" ? "i18n:govoplan-files.unpacking_zip_archive.698095f4" : "i18n:govoplan-files.uploading_files.6536791d";
  const uploadProgressLabel = uploadPhase === "unpacking" ?
  "i18n:govoplan-files.extracting_files_on_the_server.845a3c1a" :
  uploadProgress !== null && uploadProgress >= 100 ?
  "i18n:govoplan-files.finalizing_upload.bcce936d" :
  undefined;
  const visibleUploadProgress = uploadPhase === "unpacking" ? null : uploadProgress;
  const connectorLocationLabel = activeConnectorProfile ?
  [activeConnectorProfile.label, connectorLibraryId, connectorPath || (connectorLibraryId ? "i18n:govoplan-files.root.e96857c5" : "")].filter(Boolean).join(" / ") :
  "i18n:govoplan-files.connector.ba358306";
  const connectorParentDisabled = !connectorProfileId || connectorLoading || busy || !connectorPath && !connectorLibraryId;
  const activeConnectorSpaceLibrary = activeSpace && activeSpaceIsConnector ? connectorSpaceLibrary(activeSpace) : null;
  const connectorSpaceLocationLabel = activeSpace && activeSpaceIsConnector ?
  [activeSpace.label, activeConnectorSpaceLibrary, currentFolder || (activeConnectorSpaceLibrary ? "i18n:govoplan-files.root.e96857c5" : "")].filter(Boolean).join(" / ") :
  "";
  const connectorSpaceParentDisabled = !activeSpace || !activeSpaceIsConnector || connectorSpaceLoading || busy || !currentFolder && !activeConnectorSpaceLibrary;

  function renderExplorerEntry(entry: ExplorerEntry, rowIndex: number) {
    const entryDropTarget = entry.kind === "folder" && activeSpace ? { spaceId: activeSpace.id, folderPath: entry.path } : null;
    const isDropTarget = entryDropTarget ? dropTargetKey === dropTargetId(entryDropTarget) : false;

    return entry.kind === "folder" ?
    <div
      key={entry.id}
      className={`file-list-row folder-row ${isEntrySelected(entry) ? "is-selected" : ""} ${isDropTarget ? "is-drop-target" : ""}`}
      role="row"
      aria-rowindex={rowIndex}
      tabIndex={0}
      draggable
      onDragStart={(event) => handleInternalDragStart(entry, event)}
      onDragEnd={() => {setInternalDrag(null);clearDropState();}}
      onDragOver={(event) => entryDropTarget && handleDropTargetDragOver(event, entryDropTarget)}
      onDragLeave={clearDropState}
      onDrop={(event) => entryDropTarget && void handleDropOnTarget(event, entryDropTarget)}
      onMouseDown={preventTextSelectionOnShift}
      onClick={(event) => handleEntrySelection(entry, event)}
      onDoubleClick={() => {if (!busy && activeSpace) openFolder(activeSpace.id, entry.path);}}
      onContextMenu={(event) => openContextMenu(event, "folder", entry)}
      onKeyDown={(event) => handleEntryKeyDown(entry, event)}>
      
        <div className="file-list-name-cell">
          <div className="file-list-name">
            <Folder className="file-row-icon" size={20} aria-hidden="true" />
            <span><strong>{entry.name}</strong><small>{folderContentLabel(entry)}</small></span>
          </div>
        </div>
        <span>{formatBytes(entry.totalSize)}</span>
        <span className="file-row-tail"><span>{formatDate(entry.updatedAt)}</span></span>
      </div> :

    <div
      key={entry.id}
      className={`file-list-row file-row ${isEntrySelected(entry) ? "is-selected" : ""}`}
      role="row"
      aria-rowindex={rowIndex}
      tabIndex={0}
      draggable
      onDragStart={(event) => handleInternalDragStart(entry, event)}
      onDragEnd={() => {setInternalDrag(null);clearDropState();}}
      onMouseDown={preventTextSelectionOnShift}
      onClick={(event) => handleEntrySelection(entry, event)}
      onDoubleClick={() => {if (!busy) void downloadFile(settings, entry.file);}}
      onContextMenu={(event) => openContextMenu(event, "file", entry)}
      onKeyDown={(event) => handleEntryKeyDown(entry, event)}>
      
        <div className="file-list-name-cell">
          <div className="file-list-name">
            <File className="file-row-icon" size={20} aria-hidden="true" />
            <span>
              <strong>{searchActive ? entry.file.display_path : entry.file.filename}</strong>
              <small
              className={`file-linkage-status ${activeCampaignShareCount(entry.file) > 0 ? "is-linked" : ""}`}
              title={campaignShareTitle(entry.file)}>
              
                {activeCampaignShareCount(entry.file) > 0 && <Link2 size={12} aria-hidden="true" />}
                {campaignShareLabel(entry.file)}
              </small>
            </span>
          </div>
        </div>
        <span>{formatBytes(entry.file.size_bytes)}</span>
        <span className="file-row-tail"><span>{formatDate(entry.file.updated_at)}</span></span>
      </div>;

  }

  function renderConnectorSpaceContent() {
    if (!activeSpace || !activeSpaceIsConnector) return null;
    return (
      <div className="connector-space-panel">
        <div className="connector-browser connector-space-browser">
          <div className="connector-browser-toolbar">
            <Button onClick={browseConnectorSpaceParent} disabled={connectorSpaceParentDisabled}><ArrowUp size={15} aria-hidden="true" /> i18n:govoplan-files.up.2038bdec</Button>
            <span className="connector-browser-path" title={connectorSpaceLocationLabel}>{connectorSpaceLocationLabel}</span>
            <Button onClick={() => void loadConnectorSpaceContents(activeSpace)} disabled={busy || connectorSpaceLoading}><RefreshCw size={15} aria-hidden="true" /> i18n:govoplan-files.refresh.56e3badc</Button>
          </div>
          {connectorSpaceError && <p className="field-error connector-browser-error">{connectorSpaceError}</p>}
          <div className="connector-browser-list" role="list" aria-label="i18n:govoplan-files.connector_space_files.dbb0ab24">
            {connectorSpaceLoading &&
            <div className="connector-browser-empty"><LoadingIndicator label="i18n:govoplan-files.loading_connector_space.c176e6b8" /> <span>{i18nMessage("i18n:govoplan-files.loading_connector_space.356d83c6")}</span></div>
            }
            {!connectorSpaceLoading && activeConnectorSpaceItems.length === 0 &&
            <div className="connector-browser-empty">i18n:govoplan-files.no_connector_items.d634e023</div>
            }
            {!connectorSpaceLoading && activeConnectorSpaceItems.map((item) => {
              const isSelected = connectorSpaceSelectedItem?.kind === "file" && item.kind === "file" && connectorSpaceSelectedItem.path === item.path;
              const itemSize = item.size_bytes === null || item.size_bytes === undefined ? "" : formatBytes(item.size_bytes);
              const itemModified = item.modified_at ? formatDate(item.modified_at) : "";
              const meta = item.kind === "file" ?
              [itemSize, itemModified].filter(Boolean).join(" · ") :
              item.kind === "library" ?
              "i18n:govoplan-files.library.b8100f5b" :
              "i18n:govoplan-files.folder.30baa249";
              return (
                <button
                  type="button"
                  key={`${item.kind}:${item.external_id || item.path}`}
                  className={`connector-browser-row ${isSelected ? "is-selected" : ""}`}
                  onClick={() => openConnectorSpaceItem(item)}
                  onDoubleClick={() => {
                    if (item.kind === "file") void syncConnectorSpaceSelection(item);
                  }}
                  disabled={busy}>
                  
                  {item.kind === "file" ? <File size={18} aria-hidden="true" /> : <Folder size={18} aria-hidden="true" />}
                  <span>
                    <strong>{item.name}</strong>
                    <small>{meta}</small>
                  </span>
                </button>);

            })}
          </div>
        </div>
        <div className="connector-sync-selection">
          <strong>{connectorSpaceSelectedItem?.kind === "file" ? connectorSpaceSelectedItem.name : "i18n:govoplan-files.no_file_selected.f76f1c1c"}</strong>
          <small>
            {connectorSpaceSelectedItem?.kind === "file" ? i18nMessage("i18n:govoplan-files.value_value.598aab59", { value0:
              connectorSpaceSelectedItem.path, value1: parentFolderPath(connectorSpaceRelativePath(activeSpace, connectorSpaceSelectedItem.path)) || "i18n:govoplan-files.root.e96857c5" }) :
            "i18n:govoplan-files.select_a_remote_file_to_sync_it_into_this_space_.8e58a5ae"}
          </small>
        </div>
      </div>);

  }

  return (
    <div className="workspace-data-page module-entry-page file-manager-page file-manager-fullscreen">
      {error &&
      <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>
      }
      {message && !error &&
      <DismissibleAlert tone={noticeTone} resetKey={message} floating>{message}</DismissibleAlert>
      }

      <div className={`file-manager-shell ${busy ? "is-loading" : ""}`}>
        <aside className="file-tree-panel" aria-label="i18n:govoplan-files.file_spaces_and_folders.443d2056">
          <div className="file-tree-heading">i18n:govoplan-files.spaces.9b584b52</div>
          <div className="file-tree-list" onContextMenu={openTreeEmptyContextMenu}>
            {spaces.length === 0 && <p className="muted small-text">i18n:govoplan-files.no_file_spaces_available.a81fa3d0</p>}
            {spaces.map((space) => {
              const isActiveRoot = activeSpaceId === space.id && currentFolder === "";
              const spaceIsConnector = isConnectorSpace(space);
              const nodes = spaceIsConnector ? [] : buildFolderTree(filesBySpace[space.id] ?? EMPTY_FILES, foldersBySpace[space.id] ?? EMPTY_FOLDERS);
              const rootTarget = { spaceId: space.id, folderPath: "" };
              const rootDropActive = dropTargetKey === dropTargetId(rootTarget);
              return (
                <div className="file-tree-space" key={space.id}>
                  <button
                    type="button"
                    className={`file-tree-node file-tree-root ${isActiveRoot ? "is-active" : ""} ${rootDropActive && !spaceIsConnector ? "is-drop-target" : ""}`}
                    onClick={() => openFolder(space.id, "")}
                    onContextMenu={(event) => {if (!spaceIsConnector) openTreeContextMenu(event, space.id, "");}}
                    onDragOver={(event) => {if (!spaceIsConnector) handleDropTargetDragOver(event, rootTarget);}}
                    onDragLeave={clearDropState}
                    onDrop={(event) => {if (!spaceIsConnector) void handleDropOnTarget(event, rootTarget);}}
                    disabled={busy}>
                    
                    {spaceIsConnector ? <Link2 size={15} aria-hidden="true" /> : <Home size={15} aria-hidden="true" />}
                    <span>{space.label}</span>
                  </button>
                  {!spaceIsConnector &&
                  <FolderTree
                    nodes={nodes}
                    activeSpaceId={activeSpaceId}
                    spaceId={space.id}
                    currentFolder={currentFolder}
                    dropTargetKey={dropTargetKey}
                    onOpen={openFolder}
                    expandedKeys={expandedTreeNodes}
                    onToggle={toggleTreeFolder}
                    onContextMenu={openTreeContextMenu}
                    onDragOverTarget={handleDropTargetDragOver}
                    onDropOnTarget={handleDropOnTarget}
                    onClearDropState={clearDropState}
                    onRequestDragExpand={scheduleTreeDragExpand}
                    onDragStartFolder={handleTreeFolderDragStart}
                    onDragEndFolder={() => {setInternalDrag(null);clearDropState();}}
                    disabled={busy} />

                  }
                </div>);

            })}
          </div>
        </aside>

        <section className="file-list-panel" aria-label="i18n:govoplan-files.current_folder_contents.f9a24fa8">
          <div className="file-list-sticky">
            {toolbar}
            <nav className="file-breadcrumbs" aria-label="i18n:govoplan-files.current_folder.5aeab2f0">
              <button type="button" className="file-breadcrumb" onClick={() => activeSpace && openFolder(activeSpace.id, "")} disabled={busy || !activeSpace}>
                <Home size={15} aria-hidden="true" /> {activeSpace?.label || "i18n:govoplan-files.files.6ce6c512"}
              </button>
              {folderCrumbs.map((crumb) =>
              <span className="file-breadcrumb-segment" key={crumb.path}>
                  <ChevronRight size={14} aria-hidden="true" />
                  <button type="button" className="file-breadcrumb" onClick={() => activeSpace && openFolder(activeSpace.id, crumb.path)} disabled={busy}>{crumb.name}</button>
                </span>
              )}
            </nav>
            <FileSearchRow
              value={searchPattern}
              caseSensitive={searchCaseSensitive}
              busy={busy}
              searchActive={searchActive}
              disabled={!activeSpace || activeSpaceIsConnector}
              onSearch={(patternValue, caseSensitiveValue) => void runPatternSearch(patternValue, caseSensitiveValue)}
              onClear={() => void clearSearch()} />
            
            <div className="file-list-meta">
              <span>{activeSpaceIsConnector ? connectorSpaceSelectedItem?.kind === "file" ? connectorSpaceSelectedItem.name : "i18n:govoplan-files.remote_connector_space.d8956863" : selectedSummary}</span>
              <span>
                {activeSpaceIsConnector ? i18nMessage("i18n:govoplan-files.value_folder_s_value_file_s.76b92c8c", { value0:
                  activeConnectorSpaceItems.filter((item) => item.kind === "folder" || item.kind === "library").length, value1: activeConnectorSpaceItems.filter((item) => item.kind === "file").length }) : i18nMessage("i18n:govoplan-files.value_folder_s_value_file_s.76b92c8c", { value0:
                  explorerEntries.filter((entry) => entry.kind === "folder").length, value1: explorerEntries.filter((entry) => entry.kind === "file").length })}
              </span>
              {activeSpaceIsConnector &&
              <span>{[activeSpace?.provider?.toUpperCase() || "i18n:govoplan-files.connector.ba358306", activeSpace?.read_only ? "i18n:govoplan-files.read_only.9b19a5a2" : "i18n:govoplan-files.writable.dd35487a", `${activeSpace?.sync_mode || "manual"} sync`].join(" · ")}</span>
              }
              {busy && <span>i18n:govoplan-files.working.13b7bfca</span>}
              {!activeSpaceIsConnector && unmatchedCount !== null && <span>{unmatchedCount} i18n:govoplan-files.visible_file_s_were_not_matched.dc497e90</span>}
              {!activeSpaceIsConnector && selectedFiles.some((file) => file.audit_relevant) && <span>i18n:govoplan-files.audit_relevant_files_stay_retained_when_deleted_.2b7b4543</span>}
            </div>
            {!activeSpaceIsConnector &&
            <div className="file-list-table-head" role="row">
                <button type="button" onClick={() => toggleSort("name")}>{sortLabel("name", "i18n:govoplan-files.name.709a2322")}</button>
                <button type="button" onClick={() => toggleSort("size")}>{sortLabel("size", "i18n:govoplan-files.size.b7152342")}</button>
                <button type="button" onClick={() => toggleSort("modified")}>{sortLabel("modified", "i18n:govoplan-files.modified.19a532c8")}</button>
              </div>
            }
          </div>

          {activeSpaceIsConnector ?
          renderConnectorSpaceContent() :

          <div
            className="file-list-drop-target"
            ref={fileListViewportRef}
            onScroll={(event) => setFileListScrollTop(event.currentTarget.scrollTop)}
            onContextMenu={(event) => openContextMenu(event, "empty")}
            onClick={(event) => {
              if (event.target === event.currentTarget) applySelectionKeys(new Set<EntrySelectionKey>());
            }}>
            
              <div className="file-list-table" role="table" aria-label="i18n:govoplan-files.managed_files.4dd6ad11" aria-rowcount={explorerEntries.length + 1}>
                {(() => {
                const normalizedCurrentFolder = normalizeFolder(currentFolder);
                const canOpenParent = Boolean(normalizedCurrentFolder && activeSpace);
                const targetParentFolder = parentFolderPath(normalizedCurrentFolder);
                return (
                  <div
                    ref={fileListMeasureRowRef}
                    className={`file-list-row folder-row file-parent-row ${canOpenParent ? "" : "is-disabled"}`}
                    role="row"
                    aria-rowindex={1}
                    tabIndex={canOpenParent ? 0 : -1}
                    aria-disabled={!canOpenParent}
                    aria-label={canOpenParent ? i18nMessage("i18n:govoplan-files.open_parent_folder_value.8030a7c7", { value0: targetParentFolder || activeSpace?.label || "root" }) : "i18n:govoplan-files.already_at_the_root_folder.1238f110"}
                    onMouseDown={preventTextSelectionOnShift}
                    onClick={() => {if (canOpenParent) applySelectionKeys(new Set<EntrySelectionKey>());}}
                    onDoubleClick={() => {
                      if (canOpenParent && activeSpace && !busy) openFolder(activeSpace.id, targetParentFolder);
                    }}
                    onKeyDown={(event) => {
                      if (event.key === "Enter" && canOpenParent && activeSpace && !busy) {
                        event.preventDefault();
                        openFolder(activeSpace.id, targetParentFolder);
                      }
                    }}>
                    
                      <div className="file-list-name-cell">
                        <div className="file-list-name">
                          <Folder className="file-row-icon" size={20} aria-hidden="true" />
                          <span><strong>..</strong><small>i18n:govoplan-files.parent_folder.d8ed4c2a</small></span>
                        </div>
                      </div>
                      <span aria-hidden="true">-</span>
                      <span className="file-row-tail" aria-hidden="true"><span>-</span></span>
                    </div>);

              })()}
                {explorerEntries.length === 0 &&
              <div className="file-list-empty">i18n:govoplan-files.this_folder_is_empty_use_upload_or_create_folder.5977d784</div>
              }
                {virtualExplorerRows.topSpacerHeight > 0 &&
              <div className="file-list-virtual-spacer" style={{ height: virtualExplorerRows.topSpacerHeight }} aria-hidden="true" />
              }
                {virtualExplorerRows.entries.map((entry, index) => renderExplorerEntry(entry, virtualExplorerRows.startIndex + index + 2))}
                {virtualExplorerRows.bottomSpacerHeight > 0 &&
              <div className="file-list-virtual-spacer" style={{ height: virtualExplorerRows.bottomSpacerHeight }} aria-hidden="true" />
              }
              </div>
            </div>
          }
        </section>
        {busy &&
        <div className="file-manager-loading-overlay" aria-live="polite" aria-busy="true">
            <div className="loading-frame-panel"><LoadingIndicator label="i18n:govoplan-files.loading_files.3b142382" /> <span>{i18nMessage("i18n:govoplan-files.loading_files.bfd27a7e")}</span></div>
          </div>
        }
      </div>

      {conflictDialog &&
      <FileConflictDialog
        state={conflictDialog}
        busy={busy}
        onClose={() => setConflictDialog(null)}
        onCancel={() => setConflictDialog(null)}
        onOverwrite={() => void applyConflictDecision("overwrite")}
        onRename={() => void applyConflictDecision("rename")}
        onReview={() => void applyConflictDecision("review")}
        onApplyReview={() => void applyConflictDecision("review")}
        onUpdateItem={updateConflictItem} />

      }

      <ConfirmDialog
        open={Boolean(deleteDialog)}
        title={deleteDialog?.title || "i18n:govoplan-files.delete_selected_item_s.4b6a0023"}
        message={deleteDialog?.message || "i18n:govoplan-files.delete_selected_item_s.4b6a0023"}
        confirmLabel="i18n:govoplan-files.delete.f6fdbe48"
        cancelLabel="i18n:govoplan-files.cancel.77dfd213"
        tone="danger"
        busy={busy}
        onConfirm={() => void performConfirmedDelete()}
        onCancel={() => setDeleteDialog(null)} />
      

      {contextMenu &&
      <FileContextMenu
        menu={contextMenu}
        hasSelection={selectedSetsForContext(contextMenu).fileIds.size > 0 || selectedSetsForContext(contextMenu).folderPaths.size > 0}
        canCreateFolder={canOrganize}
        canUpload={canUpload}
        canDownload={canDownload}
        canOrganize={canOrganize}
        canDelete={canDelete}
        canExplainAccess={canExplainResourceAccess && Boolean(accessTargetForContext(contextMenu))}
        downloadLabel={downloadLabelForSets(selectedSetsForContext(contextMenu), contextMenu.spaceId ?? activeSpaceId)}
        onCreateFolder={() => openCreateFolderDialogForContext(contextMenu)}
        onUpload={() => openUploadDialogForContext(contextMenu)}
        onDownload={() => void downloadContextSelection(contextMenu)}
        onMove={() => openTransferDialogForContext(contextMenu, "move")}
        onCopy={() => openTransferDialogForContext(contextMenu, "copy")}
        onExplainAccess={() => openAccessExplanationForContext(contextMenu)}
        onDelete={() => void deleteContextSelection(contextMenu)} />

      }

      {accessExplanationTarget &&
      <FileDialog title="i18n:govoplan-files.access_explanation.75ee7f62" onClose={() => {if (!resourceAccessLoading) {setAccessExplanationTarget(null);setResourceAccessExplanation(null);}}}>
          <ResourceAccessExplanationContent
          loading={resourceAccessLoading}
          explanation={resourceAccessExplanation}
          fallbackResourceLabel={accessExplanationTarget.label} />
          <div className="button-row compact-actions align-end">
            <Button onClick={() => {setAccessExplanationTarget(null);setResourceAccessExplanation(null);}} disabled={resourceAccessLoading}>i18n:govoplan-files.close.bbfa773e</Button>
          </div>
        </FileDialog>
      }

      {dialog === "upload" &&
      <FileDialog title={i18nMessage("i18n:govoplan-files.upload_to_value_value.b83a34b4", { value0: activeDialogSpace?.label || "i18n:govoplan-files.files.6ce6c512", value1: activeDialogTarget?.folderPath || "i18n:govoplan-files.root.e96857c5" })} onClose={closeDialog}>
          <FileDropZone
          disabled={busy || !activeDialogSpace || !canUpload}
          busy={uploadActive}
          progress={visibleUploadProgress}
          busyLabel={uploadBusyLabel}
          progressLabel={uploadProgressLabel}
          note={`Files are uploaded into ${activeDialogTarget?.folderPath || "i18n:govoplan-files.root.e96857c5"}.`}
          onFiles={(files) => handleFilesUpload(files)} />
        
          <ToggleSwitch label="i18n:govoplan-files.unpack_zip_uploads.256fead4" checked={unpackZip} onChange={setUnpackZip} disabled={busy} />
          <div className="field-block">
            <FieldLabel className="form-label" help="i18n:govoplan-files.choose_the_destination_folder_before_selecting_o.a4922d75">i18n:govoplan-files.destination_folder.f8ccb63b</FieldLabel>
            <TransferFolderSelector
            space={activeDialogSpace}
            nodes={activeDialogTarget ? buildFolderTree(filesBySpace[activeDialogTarget.spaceId] ?? EMPTY_FILES, foldersBySpace[activeDialogTarget.spaceId] ?? EMPTY_FOLDERS) : []}
            selectedFolder={activeDialogTarget?.folderPath || ""}
            disabled={busy || !activeDialogSpace}
            onSelect={(folderPath) => activeDialogTarget && updateActiveDialogFolder(activeDialogTarget.spaceId, folderPath)} />
          
          </div>
          <div className="button-row compact-actions align-end">
            <Button onClick={closeDialog} disabled={busy}>i18n:govoplan-files.cancel.77dfd213</Button>
          </div>
        </FileDialog>
      }

      {dialog === "connector-sync" &&
      <FileDialog title={i18nMessage("i18n:govoplan-files.sync_to_value_value.29c362ea", { value0: activeDialogSpace?.label || "i18n:govoplan-files.files.6ce6c512", value1: activeDialogTarget?.folderPath || "i18n:govoplan-files.root.e96857c5" })} onClose={closeDialog}>
          <div className="connector-sync-grid">
            <FormField label="i18n:govoplan-files.connector_profile.9e9cd317">
              <select value={connectorProfileId} onChange={(event) => handleConnectorProfileChange(event.target.value)} disabled={busy || connectorLoading}>
                {connectorProfiles.length === 0 && <option value="">i18n:govoplan-files.no_connector_profiles.03c730ee</option>}
                {connectorProfiles.map((profile) =>
              <option key={profile.id} value={profile.id}>{profile.label}</option>
              )}
              </select>
            </FormField>
            <div className="field-block">
              <FieldLabel className="form-label">i18n:govoplan-files.destination_folder.f8ccb63b</FieldLabel>
              <TransferFolderSelector
              space={activeDialogSpace}
              nodes={activeDialogTarget ? buildFolderTree(filesBySpace[activeDialogTarget.spaceId] ?? EMPTY_FILES, foldersBySpace[activeDialogTarget.spaceId] ?? EMPTY_FOLDERS) : []}
              selectedFolder={activeDialogTarget?.folderPath || ""}
              disabled={busy || !activeDialogSpace}
              onSelect={(folderPath) => activeDialogTarget && updateActiveDialogFolder(activeDialogTarget.spaceId, folderPath)} />
            
            </div>
          </div>

          <div className="connector-browser">
            <div className="connector-browser-toolbar">
              <Button onClick={browseConnectorParent} disabled={connectorParentDisabled}><ArrowUp size={15} aria-hidden="true" /> i18n:govoplan-files.up.2038bdec</Button>
              <span className="connector-browser-path" title={connectorLocationLabel}>{connectorLocationLabel}</span>
              <Button onClick={() => connectorProfileId && void browseConnector(connectorProfileId, connectorPath, connectorLibraryId)} disabled={busy || connectorLoading || !connectorProfileId}><RefreshCw size={15} aria-hidden="true" /> i18n:govoplan-files.refresh.56e3badc</Button>
            </div>
            {connectorError && <p className="field-error connector-browser-error">{connectorError}</p>}
            <div className="connector-browser-list" role="list" aria-label="i18n:govoplan-files.connector_files.8f351f5d">
              {connectorLoading &&
            <div className="connector-browser-empty"><LoadingIndicator label="i18n:govoplan-files.loading_connector_files.e5b0871d" /> <span>{i18nMessage("i18n:govoplan-files.loading_connector_files.02c876e0")}</span></div>
            }
              {!connectorLoading && connectorItems.length === 0 &&
            <div className="connector-browser-empty">i18n:govoplan-files.no_connector_items.d634e023</div>
            }
              {!connectorLoading && connectorItems.map((item) => {
              const isSelected = connectorSelectedItem?.kind === "file" && item.kind === "file" && connectorSelectedItem.path === item.path;
              const itemSize = item.size_bytes === null || item.size_bytes === undefined ? "" : formatBytes(item.size_bytes);
              const itemModified = item.modified_at ? formatDate(item.modified_at) : "";
              const meta = item.kind === "file" ?
              [itemSize, itemModified].filter(Boolean).join(" · ") :
              item.kind === "library" ?
              "i18n:govoplan-files.library.b8100f5b" :
              "i18n:govoplan-files.folder.30baa249";
              return (
                <button
                  type="button"
                  key={`${item.kind}:${item.external_id || item.path}`}
                  className={`connector-browser-row ${isSelected ? "is-selected" : ""}`}
                  onClick={() => openConnectorItem(item)}
                  disabled={busy}>
                  
                    {item.kind === "file" ? <File size={18} aria-hidden="true" /> : <Folder size={18} aria-hidden="true" />}
                    <span>
                      <strong>{item.name}</strong>
                      <small>{meta}</small>
                    </span>
                  </button>);

            })}
            </div>
          </div>

          <div className="connector-sync-selection">
            <strong>{connectorSelectedItem?.kind === "file" ? connectorSelectedItem.name : "i18n:govoplan-files.no_file_selected.f76f1c1c"}</strong>
            <small>
              {connectorSelectedItem?.kind === "file" ? i18nMessage("i18n:govoplan-files.value_value.598aab59", { value0:
              connectorSelectedItem.path, value1: activeDialogTarget?.folderPath || "i18n:govoplan-files.root.e96857c5" }) :
            "i18n:govoplan-files.select_a_remote_file_to_sync_into_the_destinatio.80477a47"}
            </small>
          </div>
          <div className="button-row compact-actions align-end">
            <Button onClick={closeDialog} disabled={busy}>i18n:govoplan-files.cancel.77dfd213</Button>
            <Button variant="primary" onClick={() => void syncConnectorSelection()} disabled={busy || connectorLoading || !connectorSelectedItem || connectorSelectedItem.kind !== "file" || !activeDialogSpace}>i18n:govoplan-files.sync.905f6309</Button>
          </div>
        </FileDialog>
      }

      {dialog === "connector-space" &&
      <FileDialog title="i18n:govoplan-files.add_connector_space.aa6bdbd6" onClose={closeDialog}>
          <div className="connector-sync-grid">
            <FormField label="i18n:govoplan-files.connector_profile.9e9cd317">
              <select value={connectorProfileId} onChange={(event) => handleConnectorProfileChange(event.target.value)} disabled={busy || connectorLoading}>
                {connectorProfiles.length === 0 && <option value="">i18n:govoplan-files.no_connector_profiles.03c730ee</option>}
                {connectorProfiles.map((profile) =>
              <option key={profile.id} value={profile.id}>{profile.label}</option>
              )}
              </select>
            </FormField>
            <FormField label="i18n:govoplan-files.owner_space.364c4b62">
              <select value={connectorSpaceOwnerSpaceId} onChange={(event) => setConnectorSpaceOwnerSpaceId(event.target.value)} disabled={busy}>
                {spaces.filter((space) => !isConnectorSpace(space)).map((space) =>
              <option key={space.id} value={space.id}>{space.label}</option>
              )}
              </select>
            </FormField>
            <FormField label="i18n:govoplan-files.space_label.ec892726">
              <input value={connectorSpaceLabel} onChange={(event) => setConnectorSpaceLabel(event.target.value)} disabled={busy} placeholder={connectorSpaceSuggestedLabel()} />
            </FormField>
          </div>

          <div className="connector-browser">
            <div className="connector-browser-toolbar">
              <Button onClick={browseConnectorParent} disabled={connectorParentDisabled}><ArrowUp size={15} aria-hidden="true" /> i18n:govoplan-files.up.2038bdec</Button>
              <span className="connector-browser-path" title={connectorLocationLabel}>{connectorLocationLabel}</span>
              <Button onClick={() => connectorProfileId && void browseConnector(connectorProfileId, connectorPath, connectorLibraryId)} disabled={busy || connectorLoading || !connectorProfileId}><RefreshCw size={15} aria-hidden="true" /> i18n:govoplan-files.refresh.56e3badc</Button>
            </div>
            {connectorError && <p className="field-error connector-browser-error">{connectorError}</p>}
            <div className="connector-browser-list" role="list" aria-label="i18n:govoplan-files.connector_folders.960cbb03">
              {connectorLoading &&
            <div className="connector-browser-empty"><LoadingIndicator label="i18n:govoplan-files.loading_connector_folders.426de3b6" /> <span>{i18nMessage("i18n:govoplan-files.loading_connector_folders.dde26f3e")}</span></div>
            }
              {!connectorLoading && connectorItems.length === 0 &&
            <div className="connector-browser-empty">i18n:govoplan-files.no_connector_items.d634e023</div>
            }
              {!connectorLoading && connectorItems.map((item) => {
              const itemSize = item.size_bytes === null || item.size_bytes === undefined ? "" : formatBytes(item.size_bytes);
              const itemModified = item.modified_at ? formatDate(item.modified_at) : "";
              const meta = item.kind === "file" ?
              [itemSize, itemModified].filter(Boolean).join(" · ") :
              item.kind === "library" ?
              "i18n:govoplan-files.library.b8100f5b" :
              "i18n:govoplan-files.folder.30baa249";
              return (
                <button
                  type="button"
                  key={`${item.kind}:${item.external_id || item.path}`}
                  className="connector-browser-row"
                  onClick={() => openConnectorItem(item)}
                  disabled={busy}>
                  
                    {item.kind === "file" ? <File size={18} aria-hidden="true" /> : <Folder size={18} aria-hidden="true" />}
                    <span>
                      <strong>{item.name}</strong>
                      <small>{meta}</small>
                    </span>
                  </button>);

            })}
            </div>
          </div>

          <div className="connector-sync-selection">
            <strong>{connectorSpaceLabel.trim() || connectorSpaceSuggestedLabel()}</strong>
            <small>{[activeConnectorProfile?.label, connectorLibraryId, connectorPath || (connectorLibraryId ? "i18n:govoplan-files.root.e96857c5" : "")].filter(Boolean).join(" / ") || "i18n:govoplan-files.connector_root.9027235c"}</small>
          </div>
          <div className="button-row compact-actions align-end">
            <Button onClick={closeDialog} disabled={busy}>i18n:govoplan-files.cancel.77dfd213</Button>
            <Button variant="primary" onClick={() => void createConnectorSpaceFromDialog()} disabled={busy || connectorLoading || !connectorProfileId || !connectorSpaceOwnerSpaceId}>i18n:govoplan-files.create_space.b50606b4</Button>
          </div>
        </FileDialog>
      }

      {dialog === "create-folder" &&
      <FileDialog title="i18n:govoplan-files.create_folder.e59f63fa" onClose={closeDialog}>
          <FormField label="i18n:govoplan-files.folder_name.b2ce023b" help="i18n:govoplan-files.create_a_folder_below_the_selected_destination.9041ee76">
            <input autoFocus value={newFolderName} placeholder="i18n:govoplan-files.e_g_invoices.77a73bd1" onChange={(event) => {setNewFolderName(event.target.value);setNewFolderError("");}} onKeyDown={(event) => {if (event.key === "Enter") void handleCreateFolder();}} />
            <small>i18n:govoplan-files.created_inside.3426a815 {activeDialogTarget?.folderPath || "i18n:govoplan-files.root.e96857c5"}.</small>
            {newFolderError && <small className="field-error">{newFolderError}</small>}
          </FormField>
          <div className="field-block">
            <FieldLabel className="form-label" help="i18n:govoplan-files.choose_the_parent_folder_for_the_new_subfolder.2fcb6580">i18n:govoplan-files.parent_folder.d8ed4c2a</FieldLabel>
            <TransferFolderSelector
            space={activeDialogSpace}
            nodes={activeDialogTarget ? buildFolderTree(filesBySpace[activeDialogTarget.spaceId] ?? EMPTY_FILES, foldersBySpace[activeDialogTarget.spaceId] ?? EMPTY_FOLDERS) : []}
            selectedFolder={activeDialogTarget?.folderPath || ""}
            disabled={busy || !activeDialogSpace}
            onSelect={(folderPath) => activeDialogTarget && updateActiveDialogFolder(activeDialogTarget.spaceId, folderPath)} />
          
          </div>
          <div className="button-row compact-actions align-end">
            <Button onClick={closeDialog} disabled={busy}>i18n:govoplan-files.cancel.77dfd213</Button>
            <Button variant="primary" onClick={() => void handleCreateFolder()} disabled={busy || !newFolderName.trim()}>i18n:govoplan-files.create_folder.97bafaba</Button>
          </div>
        </FileDialog>
      }

      {dialog === "transfer" && transferDialogState &&
      <FileDialog title={`${transferMode === "copy" ? "i18n:govoplan-files.copy.af74f7c5" : "i18n:govoplan-files.move.76cdb950"} selection`} onClose={closeDialog}>
          <p className="muted">i18n:govoplan-files.choose_a_destination_space_and_folder_folders_ar.45158643</p>
          <div className="form-grid two-column-form-grid">
            <FormField label="i18n:govoplan-files.destination_space.92b63970" help="i18n:govoplan-files.select_the_space_that_will_receive_the_selected_.0a8bea8f">
              <select value={transferDialogState.targetSpaceId} onChange={(event) => setTransferDialogState((current) => current ? { ...current, targetSpaceId: event.target.value, targetFolder: "" } : current)}>
                {spaces.filter((space) => !isConnectorSpace(space)).map((space) => <option key={space.id} value={space.id}>{space.label}</option>)}
              </select>
            </FormField>
            <div className="field-block">
              <FieldLabel className="form-label" help="i18n:govoplan-files.choose_the_target_folder_folders_are_transferred.f7ab8e9e">i18n:govoplan-files.destination_folder.f8ccb63b</FieldLabel>
              <TransferFolderSelector
              space={findSpace(transferDialogState.targetSpaceId)}
              nodes={buildFolderTree(filesBySpace[transferDialogState.targetSpaceId] ?? EMPTY_FILES, foldersBySpace[transferDialogState.targetSpaceId] ?? EMPTY_FOLDERS)}
              selectedFolder={transferDialogState.targetFolder}
              disabled={busy}
              onSelect={(folderPath) => setTransferDialogState((current) => current ? { ...current, targetFolder: folderPath } : current)} />
            
            </div>
          </div>
          <div className="button-row compact-actions align-end">
            <Button onClick={closeDialog} disabled={busy}>i18n:govoplan-files.cancel.77dfd213</Button>
            <Button variant="primary" onClick={() => void handleTransferDialogSubmit()} disabled={busy}>{transferMode === "copy" ? "i18n:govoplan-files.copy.af74f7c5" : "i18n:govoplan-files.move.76cdb950"}</Button>
          </div>
        </FileDialog>
      }

      {dialog === "single-rename" &&
      <FileDialog title="i18n:govoplan-files.rename_item.3d21d6ca" onClose={closeDialog}>
          <FormField label="i18n:govoplan-files.new_name.9e627c7b" help="i18n:govoplan-files.only_the_visible_name_changes_immutable_blobs_st.01557e71">
            <input autoFocus value={singleRenameName} onChange={(event) => setSingleRenameName(event.target.value)} onKeyDown={(event) => {if (event.key === "Enter") void runSingleRename();}} />
          </FormField>
          <div className="button-row compact-actions align-end">
            <Button onClick={closeDialog} disabled={busy}>i18n:govoplan-files.cancel.77dfd213</Button>
            <Button variant="primary" onClick={() => void runSingleRename()} disabled={busy || !singleRenameName.trim()}>i18n:govoplan-files.rename.d3f4cb89</Button>
          </div>
        </FileDialog>
      }

      {dialog === "rename" &&
      <FileDialog title="i18n:govoplan-files.bulk_rename_selected_items.5e79d694" onClose={closeDialog}>
          <p className="muted">i18n:govoplan-files.bulk_rename_changes_managed_display_paths_only_i.8cf34a6b</p>
          <div className="form-grid two-column-form-grid">
            <FormField label="i18n:govoplan-files.mode.a7b93d21" help="i18n:govoplan-files.choose_how_the_selected_names_should_be_changed.0231854f">
              <select value={renameMode} onChange={(event) => setRenameMode(event.target.value as RenameMode)}>
                <option value="prefix">i18n:govoplan-files.add_prefix.672452bc</option>
                <option value="suffix">i18n:govoplan-files.add_suffix_before_extension.784381fe</option>
                <option value="replace">i18n:govoplan-files.find_and_replace.2c6b404b</option>
              </select>
            </FormField>
            {renameMode === "prefix" && <FormField label="i18n:govoplan-files.prefix.90eceb01"><input value={renamePrefix} onChange={(event) => setRenamePrefix(event.target.value)} /></FormField>}
            {renameMode === "suffix" && <FormField label="i18n:govoplan-files.suffix.885db975"><input value={renameSuffix} onChange={(event) => setRenameSuffix(event.target.value)} /></FormField>}
            {renameMode === "replace" && <><FormField label="i18n:govoplan-files.find.df251b06"><input value={renameFind} onChange={(event) => setRenameFind(event.target.value)} /></FormField><FormField label="i18n:govoplan-files.replacement.c385e347"><input value={renameReplacement} onChange={(event) => setRenameReplacement(event.target.value)} /></FormField></>}
          </div>
          {selectedFolderPaths.size > 0 &&
        <ToggleSwitch label="i18n:govoplan-files.apply_recursively_to_folder_contents.c7076984" checked={renameRecursive} onChange={setRenameRecursive} disabled={busy} />
        }
          <div className="button-row compact-actions">
            <Button onClick={() => void runRenamePreview(true)} disabled={busy || selectedEntryCount === 0}>i18n:govoplan-files.preview_rename.bb890b24</Button>
            <Button variant="primary" onClick={() => void runRenamePreview(false)} disabled={busy || selectedEntryCount === 0 || !renamePreview}>i18n:govoplan-files.apply_preview.8692d5eb</Button>
          </div>
          {renamePreview &&
        <RenamePreviewList
          response={renamePreview}
          selectedFileIds={selectedFileIds}
          selectedFolderPaths={selectedFolderPaths}
          recursive={renameRecursive}
          visibleCount={renamePreviewVisibleCount}
          onShowMore={() => setRenamePreviewVisibleCount((current) => current + 20)}
          onShowFewer={() => setRenamePreviewVisibleCount(20)} />

        }
        </FileDialog>
      }
    </div>);

}

function ResourceAccessExplanationContent({
  loading,
  explanation,
  fallbackResourceLabel
}: {loading: boolean;explanation: ResourceAccessExplanationResponse | null;fallbackResourceLabel: string;}) {
  if (loading) return <p className="muted small-note">i18n:govoplan-files.loading_access_explanation.04a7c934</p>;
  if (!explanation) return null;
  const userLabel = explanation.user.display_name || explanation.user.email || explanation.user.id;
  const resourceLabel = explanation.provenance.find((item) => item.kind === "resource")?.label || fallbackResourceLabel || explanation.resource_id;
  return (
    <>
      <div className="form-grid compact responsive-form-grid">
        <div><span className="form-label">i18n:govoplan-files.user.9f8a2389</span><p>{userLabel}</p></div>
        <div><span className="form-label">i18n:govoplan-files.resource.d1c626a9</span><p>{resourceLabel}</p></div>
        <div><span className="form-label">i18n:govoplan-files.action.97c89a4d</span><p><code>{explanation.action}</code></p></div>
        <div><span className="form-label">i18n:govoplan-files.evidence.8487d192</span><p>{explanation.provenance.length}</p></div>
      </div>
      {explanation.provenance.length === 0 ?
      <p className="muted small-note">i18n:govoplan-files.no_access_evidence_was_returned.84a21e4e</p> :
      <div className="admin-assignment-grid">
          {explanation.provenance.map((item, index) =>
        <div key={`${item.kind}:${item.id ?? index}`}>
              <strong>{provenanceKindLabel(item.kind)}</strong>
              <div className="muted small-note">
                {item.source || "i18n:govoplan-files.no_source.6dcf9723"}
                {item.id && <> · <code>{item.id}</code></>}
              </div>
              {item.label && <p>{item.label}</p>}
              {Object.keys(item.details ?? {}).length > 0 && <p className="muted small-note">{formatProvenanceDetails(item.details ?? {})}</p>}
            </div>
        )}
        </div>
      }
    </>);
}

function provenanceKindLabel(kind: string): string {
  switch (kind) {
    case "resource":
      return "i18n:govoplan-files.resource.d1c626a9";
    case "owner":
      return "i18n:govoplan-files.owner.89ff3122";
    case "share":
      return "i18n:govoplan-files.share.09ca55ca";
    case "policy":
      return "i18n:govoplan-files.policy.0b779a05";
    case "role":
      return "i18n:govoplan-files.role.b5b4a5a2";
    case "right":
      return "i18n:govoplan-files.permission.2f81a22d";
    default:
      return kind;
  }
}

function formatProvenanceDetails(details: Record<string, unknown>): string {
  return Object.entries(details).map(([key, value]) => `${key}: ${formatProvenanceValue(value)}`).join("; ");
}

function formatProvenanceValue(value: unknown): string {
  if (value === null || value === undefined) return "i18n:govoplan-files.none.6eef6648";
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
  return JSON.stringify(value);
}

function FileSearchRow({
  value,
  caseSensitive,
  busy,
  searchActive,
  disabled,
  onSearch,
  onClear








}: {value: string;caseSensitive: boolean;busy: boolean;searchActive: boolean;disabled: boolean;onSearch: (value: string, caseSensitive: boolean) => void;onClear: () => void;}) {
  const [draft, setDraft] = useState(value);
  const [caseSensitiveDraft, setCaseSensitiveDraft] = useState(caseSensitive);

  useEffect(() => {
    setDraft(value);
  }, [value]);

  useEffect(() => {
    setCaseSensitiveDraft(caseSensitive);
  }, [caseSensitive]);

  function clear() {
    setDraft("");
    onClear();
  }

  return (
    <div className="file-search-row">
      <Search size={17} aria-hidden="true" />
      <input
        value={draft}
        placeholder="i18n:govoplan-files.search_this_folder_pdf_pdf_or_2026_pdf.74dec36a"
        onChange={(event) => setDraft(event.target.value)}
        onKeyDown={(event) => {if (event.key === "Enter") onSearch(draft, caseSensitiveDraft);}} />
      
      <ToggleSwitch label="i18n:govoplan-files.case_sensitive.c73af7bd" checked={caseSensitiveDraft} onChange={setCaseSensitiveDraft} disabled={busy || disabled} />
      <Button onClick={() => onSearch(draft, caseSensitiveDraft)} disabled={busy || disabled}>i18n:govoplan-files.search.bce06414</Button>
      {searchActive && <Button onClick={clear} disabled={busy}>i18n:govoplan-files.clear.719ea396</Button>}
    </div>);

}

function activeCampaignShares(file: ManagedFile) {
  return (file.shares ?? []).filter((share) => share.target_type === "campaign" && !share.revoked_at);
}

function activeCampaignShareCount(file: ManagedFile): number {
  return activeCampaignShares(file).length;
}

function campaignShareLabel(file: ManagedFile): string {
  const count = activeCampaignShareCount(file);
  if (count === 0) return "i18n:govoplan-files.no_campaign_links.4203c2be";
  if (count === 1) return "i18n:govoplan-files.linked_to_1_campaign.5349694f";
  return i18nMessage("i18n:govoplan-files.linked_to_value_campaigns.1ad7be94", { value0: count });
}

function campaignShareTitle(file: ManagedFile): string | undefined {
  const shares = activeCampaignShares(file);
  if (shares.length === 0) return undefined;
  return shares.map((share) => share.target_id).join(", ");
}
