import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { createRoot } from 'react-dom/client';
import axios from 'axios';
import {
  HashRouter,
  Navigate,
  Route,
  Routes,
  useNavigate,
  useParams,
} from 'react-router-dom';
import './styles.css';

const API_ROOT = (process.env.API_BASE_URL || '').replace(/\/$/, '');

const api = axios.create({
  baseURL: API_ROOT ? `${API_ROOT}/api` : '/api',
  withCredentials: true,
});

const DEFAULT_FOLDER_NAME = 'All Documents';

const hasFiles = (event) =>
  Array.from(event.dataTransfer?.types || []).includes('Files');

const IconChevronRight = ({ className }) => (
  <svg
    className={className ? `icon ${className}` : 'icon'}
    viewBox="0 0 24 24"
    role="img"
    aria-hidden="true"
    focusable="false"
  >
    <path
      d="M9 6l6 6-6 6"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  </svg>
);

const IconTrash = ({ className }) => (
  <svg
    className={className ? `icon ${className}` : 'icon'}
    viewBox="0 0 24 24"
    role="img"
    aria-hidden="true"
    focusable="false"
  >
    <path
      d="M4 7h16"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
    <path
      d="M10 11v6m4-6v6"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
    <path
      d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
    <path
      d="M9 7V4h6v3"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  </svg>
);

const IconDownload = ({ className }) => (
  <svg
    className={className ? `icon ${className}` : 'icon'}
    viewBox="0 0 24 24"
    role="img"
    aria-hidden="true"
    focusable="false"
  >
    <path
      d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
    <path
      d="M7 11l5 5 5-5"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
    <path
      d="M12 5v11"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  </svg>
);

const IconFolder = ({ className }) => (
  <svg
    className={className ? `icon ${className}` : 'icon'}
    viewBox="0 0 24 24"
    role="img"
    aria-hidden="true"
    focusable="false"
  >
    <path
      d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  </svg>
);

const createRootNode = () => ({
  id: 'root',
  name: DEFAULT_FOLDER_NAME,
  parentId: null,
  children: [],
  expanded: true,
  loaded: false,
});

const StatusBanner = ({ status }) => {
  if (!status) return null;
  return <div className={`status-banner ${status.variant}`}>{status.message}</div>;
};

const DropOverlay = ({ active, folderName }) => (
  <div className={`drop-overlay${active ? ' active' : ''}`}>
    <div className="drop-overlay__content">
      Drop files to upload to <strong>{folderName}</strong>
    </div>
  </div>
);

const LoginView = ({ onSubmit, status }) => (
  <div className="login-screen">
    <div className="login-card">
      <h1>Paperless-NEO</h1>
      <p>Authenticate to manage your documents.</p>
      <form onSubmit={onSubmit}>
        <label htmlFor="username">Username</label>
        <input
          id="username"
          name="username"
          placeholder="admin"
          autoComplete="username"
          required
        />
        <label htmlFor="password">Password</label>
        <input
          id="password"
          name="password"
          type="password"
          placeholder="••••••"
          autoComplete="current-password"
          required
        />
        <button type="submit">Sign in</button>
      </form>
      <StatusBanner status={status} />
    </div>
  </div>
);

const FolderNode = ({
  node,
  depth,
  isSelected,
  onToggle,
  onSelect,
  onDrop,
  onDragOver,
  onDragLeave,
  onDelete,
  renderChildren,
  onFolderDragStart,
  onFolderDragEnd,
  draggingFolderId,
}) => {
  const canToggle = node.id === 'root' || node.children.length > 0 || !node.loaded;
  const icon = canToggle ? <IconChevronRight className="toggle-icon" /> : null;
  const canDrag = node.id !== 'root';
  const isDragging = draggingFolderId === node.id;

  const handleToggleClick = (event) => {
    event.stopPropagation();
    if (canToggle) {
      onToggle(node.id);
    }
  };

  return (
    <li className={`folder-node${isDragging ? ' is-dragging' : ''}`}>
      <div
        className={`folder-row${isSelected ? ' active' : ''}`}
        draggable={canDrag}
        onClick={() => onSelect(node.id)}
        onDragOver={(event) => onDragOver(event, node.id)}
        onDragLeave={onDragLeave}
        onDrop={(event) => onDrop(event, node.id)}
        onDragStart={(event) => {
          if (!canDrag || !onFolderDragStart) return;
          onFolderDragStart(event, node.id);
        }}
        onDragEnd={(event) => {
          if (onFolderDragEnd) {
            onFolderDragEnd(event);
          }
        }}
      >
        <span
          className={`toggle${canToggle ? '' : ' invisible'}${node.expanded ? ' expanded' : ''}`}
          onClick={handleToggleClick}
        >
          {icon}
        </span>
        <span className="name">{node.name}</span>
        {node.id !== 'root' && (
          <button
            type="button"
            className="icon-button ghost"
            onClick={(event) => {
              event.stopPropagation();
              onDelete(node.id);
            }}
            title="Delete folder"
            aria-label={`Delete folder ${node.name}`}
          >
            <IconTrash className="icon-trash" />
          </button>
        )}
      </div>
      {node.expanded && node.children.length > 0 && (
        <ul className="folder-children">
          {renderChildren(node.children, depth + 1)}
        </ul>
      )}
    </li>
  );
};

const FilterBar = ({
  query,
  onQueryChange,
  tags,
  activeTagIds,
  onToggleTag,
  onClear,
  hasFilters,
}) => (
  <div className="filter-bar">
    <input
      type="search"
      placeholder="Search documents"
      value={query}
      onChange={(event) => onQueryChange(event.target.value)}
    />
    <div className="tag-filters">
      {tags.length ? (
        tags.map((tag) => (
          <button
            key={tag.id}
            type="button"
            className={`tag-filter${activeTagIds.includes(tag.id) ? ' active' : ''}`}
            onClick={() => onToggleTag(tag.id)}
          >
            {tag.label}
          </button>
        ))
      ) : (
        <span className="meta">No tags yet</span>
      )}
    </div>
    <div className="filter-actions">
      {hasFilters && (
        <button type="button" className="secondary" onClick={onClear}>
          Clear
        </button>
      )}
    </div>
  </div>
);

const DocumentsTable = ({
  currentFolderName,
  breadcrumbs,
  onRefresh,
  subfolders,
  documents,
  searchResults,
  isFilterActive,
  onFolderSelect,
  onFolderDrop,
  onFolderDragOver,
  onFolderDragLeave,
  onFolderDragStart,
  onFolderDragEnd,
  draggedFolderId,
  onFolderDelete,
  onDocumentRowClick,
  onDocumentOpen,
  selectedDocumentIds,
  focusedDocumentId,
  draggingDocumentIds = [],
  onDocumentDragStart,
  onDocumentDragEnd,
  onDownload,
  filterBar,
}) => {
  const showingSearchResults = searchResults !== null;
  const rows = showingSearchResults ? searchResults : documents;
  const selectedSet = useMemo(
    () => new Set(selectedDocumentIds),
    [selectedDocumentIds],
  );
  const draggingSet = useMemo(
    () => new Set(draggingDocumentIds || []),
    [draggingDocumentIds],
  );

  return (
    <section className="documents-panel column">
      <div className="column-header">
        <div className="column-header__titles">
          <nav className="breadcrumb" aria-label="Folder breadcrumbs">
            {breadcrumbs.map((crumb, index) => {
              const isLast = index === breadcrumbs.length - 1;
              return (
                <span key={crumb.id} className="breadcrumb-item">
                  {isLast ? (
                    <span className="breadcrumb-current">{crumb.name}</span>
                  ) : (
                    <a
                      href="#"
                      onClick={(event) => {
                        event.preventDefault();
                        onFolderSelect(crumb.id);
                      }}
                    >
                      {crumb.name}
                    </a>
                  )}
                  {!isLast && <span className="breadcrumb-separator">›</span>}
                </span>
              );
            })}
          </nav>
          {showingSearchResults && (
            <div className="column-subtitle">Search results</div>
          )}
        </div>
        <div className="header-actions">
          <button className="secondary" onClick={onRefresh}>
            Refresh
          </button>
        </div>
      </div>
      <div className="column-body">
        <div className="column-toolbar">{filterBar}</div>
        <div className="documents-scroll">
          {!showingSearchResults && !subfolders.length && rows.length === 0 ? (
            <div className="empty-state">
              Drop files anywhere or onto a folder to upload documents.
            </div>
          ) : (
            <table aria-multiselectable="true">
              <thead>
                <tr>
                  <th className="thumb-column">Preview</th>
                  <th>Name</th>
                  <th>Type</th>
                  <th>Updated</th>
                  <th className="actions-column">Actions</th>
                </tr>
              </thead>
              <tbody>
                {!showingSearchResults &&
                  subfolders.map((folder) => {
                    const canDragFolder = folder.id !== 'root';
                    const isDraggingFolder = draggedFolderId === folder.id;
                    return (
                      <tr
                        key={folder.id}
                        className={`folder${isDraggingFolder ? ' is-dragging' : ''}`}
                        onClick={() => onFolderSelect(folder.id)}
                        onDragOver={(event) => onFolderDragOver(event, folder.id)}
                        onDragLeave={onFolderDragLeave}
                        onDrop={(event) => onFolderDrop(event, folder.id)}
                        draggable={canDragFolder}
                        onDragStart={(event) => {
                          if (canDragFolder) {
                            onFolderDragStart(event, folder.id);
                          }
                        }}
                        onDragEnd={(event) => {
                          if (canDragFolder) {
                            onFolderDragEnd(event);
                          }
                        }}
                      >
                        <td className="thumb-cell">
                          <div className="thumb-placeholder icon">
                            <IconFolder className="icon-inline" />
                          </div>
                        </td>
                        <td>{folder.name}</td>
                        <td>Folder</td>
                        <td>—</td>
                        <td>—</td>
                        <td className="actions">
                          <button
                            type="button"
                            className="icon-button danger"
                            onClick={(event) => {
                              event.stopPropagation();
                              onFolderDelete(folder.id);
                            }}
                          >
                            Delete
                          </button>
                        </td>
                      </tr>
                    );
                  })}
                {rows.map((doc) => {
                  const isSelected = selectedSet.has(doc.id);
                  const rowClasses = ['document'];
                  if (isSelected) rowClasses.push('selected');
                  if (focusedDocumentId === doc.id) rowClasses.push('focused');
                  if (draggingSet.has(doc.id)) {
                    rowClasses.push('dragging');
                  }

                  return (
                    <tr
                      key={doc.id}
                      className={rowClasses.join(' ')}
                      aria-selected={isSelected}
                      onClick={(event) => onDocumentRowClick(doc.id, event)}
                      onDoubleClick={(event) => {
                        event.stopPropagation();
                        if (onDocumentOpen) {
                          onDocumentOpen(doc.id);
                        }
                      }}
                      draggable
                      onDragStart={(event) => onDocumentDragStart(event, doc.id)}
                      onDragEnd={onDocumentDragEnd}
                    >
                      <td className="thumb-cell">
                        {doc.thumbnail ? (
                          <img
                            src={doc.thumbnail.url}
                            alt={`Thumbnail for ${doc.title || doc.original_name}`}
                            className="document-thumbnail"
                          />
                        ) : (
                          <div className="thumb-placeholder">DOC</div>
                        )}
                      </td>
                      <td>
                        <div className="doc-name">
                          <span className="doc-name__title">{doc.title || doc.original_name}</span>
                          {(doc.tags || []).length > 0 && (
                            <div className="doc-name__tags">
                              {(doc.tags || []).map((tag) => (
                                <span key={tag.id} className="badge">
                                  {tag.label}
                                </span>
                              ))}
                            </div>
                          )}
                        </div>
                      </td>
                      <td>{doc.content_type || 'Document'}</td>
                      <td>{
                        doc.updated_at
                          ? new Date(doc.updated_at).toLocaleString()
                          : '—'
                      }</td>
                      <td className="actions">
                        <button
                          type="button"
                          className="with-icon"
                          onClick={(event) => {
                            event.stopPropagation();
                            onDownload(doc.id);
                          }}
                        >
                          <IconDownload className="icon-inline" />
                          <span>Download</span>
                        </button>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
        {rows.length === 0 && isFilterActive && (
          <div className="empty-state">No documents match the current filters.</div>
        )}
        {showingSearchResults && rows.length > 0 && (
          <div className="search-hint">
            Showing {rows.length} document{rows.length === 1 ? '' : 's'} in {currentFolderName} and subfolders.
          </div>
        )}
      </div>
    </section>
  );
};

const computeStackAngle = (docId, index) => {
  if (index === 0) return 0;
  let hash = 0;
  const source = docId || `stack-${index}`;
  for (let i = 0; i < source.length; i += 1) {
    hash = (hash * 31 + source.charCodeAt(i)) % 997;
  }
  const magnitude = Math.max(3, (hash % 13) + 3); // 3..15
  const sign = index % 2 === 0 ? 1 : -1;
  return magnitude * sign;
};

const MAX_PREVIEW_STACK_ITEMS = 15;

const PreviewStack = ({
  items = [],
  maxItems = MAX_PREVIEW_STACK_ITEMS,
  emptyMessage = 'Preview unavailable',
  onItemActivate,
  onOpenPreview,
  activeItemId = null,
}) => {
  if (!items.length) {
    return <span className="meta">{emptyMessage}</span>;
  }

  const limited = items.slice(0, maxItems);
  const hasMultiple = limited.length > 1;
  const preparedItems = useMemo(
    () =>
      limited.map((entry, index) => ({
        entry,
        angle: index === 0 ? 0 : computeStackAngle(entry.id, index),
        offset: 0,
      })),
    [limited],
  );

  return (
    <div className="preview-stack preview-stack--stacked">
      {preparedItems.map(({ entry, angle, offset }, index) => {
        const transform = hasMultiple
          ? `translate(-50%, -50%) rotate(${angle}deg)`
          : 'translate(-50%, -50%)';
        const isFront = index === 0;
        return (
          <div
            key={entry.id || index}
            className={`preview-stack__item orientation-${entry.orientation || 'landscape'}`}
            style={{
              zIndex: preparedItems.length - index,
              transform,
            }}
            aria-hidden={
              hasMultiple && !onItemActivate && !onOpenPreview ? 'true' : undefined
            }
          >
            <img
              src={entry.url}
              alt={entry.alt || ''}
              className="preview-stack__image"
              onClick={(event) => {
                event.stopPropagation();
                if (isFront && onOpenPreview) {
                  onOpenPreview(entry.id);
                } else if (onItemActivate) {
                  onItemActivate(entry.id);
                }
              }}
              onKeyDown={(event) => {
                if (!onItemActivate && !onOpenPreview) return;
                if (event.key === 'Enter' || event.key === ' ') {
                  event.preventDefault();
                  event.stopPropagation();
                  if (isFront && onOpenPreview) {
                    onOpenPreview(entry.id);
                  } else {
                    onItemActivate?.(entry.id);
                  }
                }
              }}
            />
          </div>
        );
      })}
    </div>
  );
};

const DetailPanel = ({
  selectedDocuments = [],
  detailMap = new Map(),
  tags = [],
  onDownload,
  onTagAdd,
  onTagRemove,
  onRegenerateThumbnails,
  previewEntry,
  onOpenPreview,
  onBulkTagAdd,
  onBulkTagRemove,
  onBulkMove,
  onBulkReanalyze,
  folderOptions = [],
  defaultMoveTarget = 'root',
  onPromoteSelection,
  activePreviewId = null,
}) => {
  const lookup = detailMap && typeof detailMap.get === 'function' ? detailMap : new Map();
  const selectedCount = selectedDocuments.length;
  const singleDoc = selectedCount === 1 ? selectedDocuments[0] : null;
  const detail = singleDoc ? lookup.get(singleDoc.id) || null : null;

  const handlePreviewActivate = useCallback(
    (docId) => {
      if (!docId) return;
      onPromoteSelection?.(docId);
    },
    [onPromoteSelection],
  );

  const makePreviewItem = useCallback((doc, detailEntry, fallbackUrl = null) => {
    if (!doc) return null;
    const url = doc.thumbnail?.url || detailEntry?.document?.thumbnail?.url || fallbackUrl;
    if (!url) return null;
    const asset = detailEntry?.assets?.find((item) => item.asset_type === 'thumbnail');
    const width = asset?.width || 0;
    const height = asset?.height || 0;
    const orientation = width > 0 && height > 0 ? (width >= height ? 'landscape' : 'portrait') : 'landscape';
    return {
      id: doc.id,
      url,
      orientation,
      alt: doc.title || doc.original_name || 'Document preview',
    };
  }, []);

  const stackDocuments = useMemo(() => {
    if (!selectedDocuments.length) return [];
    const seen = new Set();
    const ordered = [];
    for (let index = selectedDocuments.length - 1; index >= 0; index -= 1) {
      const doc = selectedDocuments[index];
      if (!doc?.id || seen.has(doc.id)) continue;
      seen.add(doc.id);
      ordered.push(doc);
      if (ordered.length >= MAX_PREVIEW_STACK_ITEMS) {
        break;
      }
    }
    return ordered;
  }, [selectedDocuments]);

  const singlePreviewItems = useMemo(() => {
    if (!singleDoc) return [];
    const item = makePreviewItem(singleDoc, detail, previewEntry?.url || null);
    return item ? [item] : [];
  }, [singleDoc, detail, previewEntry, makePreviewItem]);

  const stackPreviews = useMemo(
    () =>
      stackDocuments
        .map((doc) => makePreviewItem(doc, lookup.get(doc.id)))
        .filter(Boolean),
    [stackDocuments, lookup, makePreviewItem],
  );

  const commonTags = useMemo(() => {
    if (selectedCount < 2) return [];
    const tagSets = selectedDocuments.map((doc) => new Set((doc.tags || []).map((tag) => tag.label)));
    if (!tagSets.length) return [];
    const intersection = new Set(tagSets[0]);
    tagSets.slice(1).forEach((set) => {
      [...intersection].forEach((label) => {
        if (!set.has(label)) {
          intersection.delete(label);
        }
      });
    });
    return [...intersection];
  }, [selectedDocuments, selectedCount]);

  const stackTotalSizeBytes = useMemo(() => {
    if (!stackPreviews.length) return 0;
    return stackPreviews.reduce((sum, item) => {
      const detailEntry = lookup.get(item.id);
      const bytes = detailEntry?.current_version?.size_bytes || 0;
      return sum + (typeof bytes === 'number' ? bytes : 0);
    }, 0);
  }, [stackPreviews, lookup]);

  const renderSingle = () => {
    if (!singleDoc) {
      return <p className="meta">Select a document to view metadata, tags and actions.</p>;
    }
    if (!detail) {
      return <p className="meta">Loading details…</p>;
    }

    const displayName = singleDoc.title || singleDoc.original_name;

    return (
      <>
        <div>
          <div className="preview-pane preview-pane--stack">
            <PreviewStack
              items={singlePreviewItems}
              maxItems={1}
              emptyMessage="Preview loading…"
              onItemActivate={handlePreviewActivate}
              onOpenPreview={onOpenPreview}
              activeItemId={activePreviewId}
            />
          </div>
        </div>
        <h3 style={{ margin: 0 }}>{displayName}</h3>
        <div className="meta">
          <div>
            <strong>Uploaded:</strong>{' '}
            {singleDoc.uploaded_at ? new Date(singleDoc.uploaded_at).toLocaleString() : '—'}
          </div>
          <div>
            <strong>Size:</strong>{' '}
            {detail.current_version
              ? `${(detail.current_version.size_bytes / 1024).toFixed(1)} KB`
              : '—'}
          </div>
          <div>
            <strong>Type:</strong> {singleDoc.content_type || 'Unknown'}
          </div>
          <div>
            <strong>Issued:</strong>{' '}
            {detail.document.issued_at
              ? new Date(detail.document.issued_at).toLocaleString()
              : '—'}
          </div>
          <div>
            <strong>Original filename:</strong>{' '}
            {singleDoc.original_name}
          </div>
        </div>
        <div className="detail-actions">
          <button type="button" className="with-icon" onClick={() => onDownload(singleDoc.id)}>
            <IconDownload className="icon-inline" />
            <span>Download</span>
          </button>
          <button
            type="button"
            className="secondary"
            onClick={() => onOpenPreview(singleDoc.id)}
          >
            Open preview
          </button>
          <button
            type="button"
            className="secondary"
            onClick={() => onRegenerateThumbnails(singleDoc.id)}
          >
            Re-run analysis
          </button>
        </div>
        <div>
          <dt>Tags</dt>
          <div className="tag-list">
            {detail.document.tags?.length ? (
              detail.document.tags.map((tag) => (
                <span key={tag.id} className="tag-pill">
                  {tag.label}{' '}
                  <button type="button" onClick={() => onTagRemove(singleDoc.id, tag.id)}>
                    ×
                  </button>
                </span>
              ))
            ) : (
              <span className="meta">No tags yet.</span>
            )}
          </div>
          <form
            className="inline"
            onSubmit={(event) => {
              event.preventDefault();
              const input = event.currentTarget.elements.tag;
              const value = input.value.trim();
              if (!value) return;
              onTagAdd(singleDoc, value, input);
            }}
          >
            <input name="tag" placeholder="Add or create tag" list="tag-catalog" />
            <button type="submit">Add</button>
            <datalist id="tag-catalog">
              {tags.map((tag) => (
                <option key={tag.id} value={tag.label} />
              ))}
            </datalist>
          </form>
        </div>
        {detail.document.metadata &&
          Object.keys(detail.document.metadata).length > 0 && (
            <div>
              <dt>Metadata</dt>
              <pre>{JSON.stringify(detail.document.metadata, null, 2)}</pre>
            </div>
          )}
      </>
    );
  };

  const renderBulk = () => {
    const countLabel = `${selectedCount} document${selectedCount === 1 ? '' : 's'}`;
    const sizeLabel = stackTotalSizeBytes
      ? `${(stackTotalSizeBytes / 1024).toFixed(1)} KB`
      : '—';

    return (
      <>
        <div>
          <div className="preview-pane preview-pane--stack">
            <PreviewStack
              items={stackPreviews}
              emptyMessage="No previews available."
              onItemActivate={handlePreviewActivate}
              onOpenPreview={onOpenPreview}
              activeItemId={activePreviewId}
            />
          </div>
        </div>
        <h3 style={{ margin: 0 }}>{countLabel}</h3>
        <div className="meta">
          <div>
            <strong>Total size (stack):</strong> {sizeLabel}
          </div>
          <div>
            <strong>Common tags:</strong>{' '}
            {commonTags.length ? commonTags.join(', ') : 'None'}
          </div>
        </div>
        {commonTags.length > 0 && (
          <div className="bulk-tags">
            {commonTags.map((label) => (
              <span key={label} className="tag-pill">
                {label}
              </span>
            ))}
          </div>
        )}
        <div className="bulk-detail-actions">
          <form
            className="inline"
            onSubmit={(event) => {
              event.preventDefault();
              const input = event.currentTarget.elements.tag;
              const value = input.value.trim();
              if (!value) return;
              onBulkTagAdd?.({ label: value, input });
            }}
          >
            <input name="tag" placeholder="Add tag to selection" list="tag-catalog" />
            <button type="submit">Add tag</button>
          </form>
          <form
            className="inline"
            onSubmit={(event) => {
              event.preventDefault();
              const input = event.currentTarget.elements.tag;
              const value = input.value.trim();
              if (!value) return;
              onBulkTagRemove?.({ label: value, input });
            }}
          >
            <input name="tag" placeholder="Remove tag from selection" list="tag-catalog" />
            <button type="submit" className="secondary">
              Remove tag
            </button>
          </form>
        </div>
        <datalist id="tag-catalog">
          {tags.map((tag) => (
            <option key={tag.id} value={tag.label} />
          ))}
        </datalist>
        <div className="bulk-move">
          <label htmlFor="detail-bulk-move" className="meta">
            Move selection to folder
          </label>
          <select
            id="detail-bulk-move"
            name="target"
            defaultValue={defaultMoveTarget || 'root'}
            onChange={(event) => onBulkMove?.({ target: event.target.value })}
          >
            {folderOptions.map((option) => (
              <option key={option.id} value={option.id}>
                {option.label}
              </option>
            ))}
          </select>
        </div>
        <button
          type="button"
          className="secondary"
          onClick={() => onBulkReanalyze?.()}
        >
          Re-analyze selection
        </button>
      </>
    );
  };

  return (
    <aside className="detail-panel column">
      <div className="column-header">
        <h2>Details</h2>
      </div>
      <div className="column-body scrollable">
        {selectedCount <= 1 ? renderSingle() : renderBulk()}
      </div>
    </aside>
  );
};

const PreviewWorkspace = ({
  document,
  detail,
  previewEntry,
  onClose,
  onDownload,
  onRegenerateThumbnails,
}) => {
  if (!document) {
    return null;
  }

  const title =
    detail?.document?.title ||
    document.title ||
    detail?.document?.original_name ||
    document.original_name;
  const mime = previewEntry?.contentType || document.content_type || 'application/pdf';

  return (
    <section className="preview-workspace">
      <header className="preview-workspace__header">
        <div className="preview-workspace__meta">
          <button
            type="button"
            className="secondary"
            onClick={() => onClose(detail?.document?.folder_id ?? document.folder_id ?? 'root')}
          >
            ← Back
          </button>
          <div>
            <h2>{title}</h2>
            <span className="meta">
              {document.content_type || 'Document'}
              {detail?.current_version?.size_bytes
                ? ` · ${(detail.current_version.size_bytes / 1024 / 1024).toFixed(2)} MB`
                : ''}
            </span>
          </div>
        </div>
        <div className="preview-workspace__actions">
          <button type="button" className="with-icon" onClick={() => onDownload(document.id)}>
            <IconDownload className="icon-inline" />
            <span>Download</span>
          </button>
          <button
            type="button"
            className="secondary"
            onClick={() => onRegenerateThumbnails(document.id)}
          >
            Re-run analysis
          </button>
        </div>
      </header>
      <div className="preview-workspace__body">
        {!previewEntry?.url ? (
          <div className="preview-workspace__message">Loading preview…</div>
        ) : (
          <iframe
            src={previewEntry.url}
            title={`Preview of ${title}`}
            className="preview-workspace__object"
          />
        )}
      </div>
      {detail?.document?.metadata && Object.keys(detail.document.metadata).length > 0 && (
        <section className="preview-workspace__metadata">
          <h3>Metadata</h3>
          <pre>{JSON.stringify(detail.document.metadata, null, 2)}</pre>
        </section>
      )}
    </section>
  );
};

const Sidebar = ({
  folderNodes,
  onToggle,
  onSelect,
  onDrop,
  onDragOver,
  onDragLeave,
  onCreateFolder,
  onDeleteFolder,
  selectedFolder,
  onFolderDragStart,
  onFolderDragEnd,
  draggedFolderId,
}) => {
  const renderNodes = useCallback(
    (ids, depth) =>
      ids.map((id) => {
        const node = folderNodes.get(id);
        if (!node) return null;
        return (
          <FolderNode
            key={id}
            node={node}
            depth={depth}
            isSelected={selectedFolder === id}
            onToggle={() => onToggle(id)}
            onSelect={onSelect}
            onDrop={onDrop}
            onDragOver={onDragOver}
            onDragLeave={onDragLeave}
            onDelete={onDeleteFolder}
            renderChildren={renderNodes}
            onFolderDragStart={onFolderDragStart}
            onFolderDragEnd={onFolderDragEnd}
            draggingFolderId={draggedFolderId}
          />
        );
      }),
    [
      folderNodes,
      selectedFolder,
      onToggle,
      onSelect,
      onDrop,
      onDragOver,
      onDragLeave,
      onDeleteFolder,
      onFolderDragStart,
      onFolderDragEnd,
      draggedFolderId,
    ],
  );

  const rootNode = folderNodes.get('root');

  return (
    <aside className="sidebar column">
      <div className="column-header">
        <h2>Folders</h2>
      </div>
      <div className="column-body">
        <form
          className="new-folder-form inline"
          onSubmit={(event) => {
            event.preventDefault();
            const input = event.currentTarget.elements['folder-name'];
            const value = input.value.trim();
            if (!value) return;
            onCreateFolder(value, () => {
              input.value = '';
            });
          }}
        >
          <input name="folder-name" placeholder="New folder name" required />
          <button type="submit">Create</button>
        </form>
        <div className="folder-tree-scroller">
          <ul className="folder-tree">
            {rootNode && renderNodes([rootNode.id], 0)}
          </ul>
        </div>
      </div>
    </aside>
  );
};

function App({ routeFolderId = null, routeDocumentId = null, navigate }) {
  const [token, setToken] = useState(() => window.localStorage.getItem('paperless_token') || '');
  const [status, setStatus] = useState(null);
  const setStatusMessage = useCallback((message, variant = 'info') => {
    setStatus(message ? { message, variant } : null);
  }, []);
  const [loading, setLoading] = useState(false);
  const [folderNodes, setFolderNodes] = useState(() => {
    const rootNode = createRootNode();
    return new Map([[rootNode.id, rootNode]]);
  });
  const [folderContents, setFolderContents] = useState(() => new Map());
  const [selectedFolder, setSelectedFolder] = useState(routeFolderId || 'root');
  const [currentFolder, setCurrentFolder] = useState(null);
  const [currentSubfolders, setCurrentSubfolders] = useState([]);
  const [documents, setDocuments] = useState([]);
  const initialSelection = routeDocumentId ? [routeDocumentId] : [];
  const [selectedDocumentIds, setSelectedDocumentIds] = useState(initialSelection);
  const [selectionOrder, setSelectionOrder] = useState(initialSelection);
  const [focusedDocumentId, setFocusedDocumentId] = useState(routeDocumentId);
  const [documentDetails, setDocumentDetails] = useState(() => new Map());
  const documentDetailsRef = useRef(documentDetails);
  const tokenRef = useRef(token);
  const refreshPromiseRef = useRef(null);
  const breadcrumbFetchRef = useRef(new Set());
  const refreshAccessToken = useCallback(async () => {
    console.log('[Auth] Attempting to refresh access token…');
    try {
      const { data } = await api.post('/auth/refresh');
      if (data?.access_token) {
        setToken(data.access_token);
        console.log('[Auth] Access token refreshed');
        return data.access_token;
      }
      throw new Error('Missing access token in refresh response');
    } catch (error) {
      console.warn('[Auth] Failed to refresh access token', error);
      throw error;
    }
  }, []);
  const [searchResults, setSearchResults] = useState(null);
  const [tags, setTags] = useState([]);
  const [searchQuery, setSearchQuery] = useState('');
  const [activeTagFilters, setActiveTagFilters] = useState([]);
  const [draggedDocumentIds, setDraggedDocumentIds] = useState([]);
  const [draggedFolderId, setDraggedFolderId] = useState(null);
  const [dropOverlayState, setDropOverlayState] = useState({
    active: false,
    folderName: DEFAULT_FOLDER_NAME,
  });
  const [activePreviewId, setActivePreviewId] = useState(routeDocumentId || null);
  const [previewDocumentId, setPreviewDocumentId] = useState(null);
  const [previewDocumentLoading, setPreviewDocumentLoading] = useState(false);

  const initializedRef = useRef(false);
  const dragCounterRef = useRef(0);
  const selectionAnchorRef = useRef(routeDocumentId);
  const selectionOrderRef = useRef(initialSelection);

  const updateSelectionOrder = useCallback((nextSelection, interactedIds = []) => {
    const nextSet = new Set(nextSelection);
    const previousOrder = selectionOrderRef.current.filter((id) => nextSet.has(id));
    const interacted = (interactedIds || []).filter((id, index, array) => array.indexOf(id) === index);

    const base = previousOrder.filter((id) => !interacted.includes(id));
    const result = [...base];

    interacted.forEach((id) => {
      if (nextSet.has(id) && !result.includes(id)) {
        result.push(id);
      }
    });

    nextSelection.forEach((id) => {
      if (!result.includes(id)) {
        result.push(id);
      }
    });

    if (
      result.length !== selectionOrderRef.current.length ||
      result.some((id, index) => selectionOrderRef.current[index] !== id)
    ) {
      selectionOrderRef.current = result;
      setSelectionOrder(result);
    } else {
      selectionOrderRef.current = result;
    }
  }, []);

  useEffect(() => {
    documentDetailsRef.current = documentDetails;
  }, [documentDetails]);

  useEffect(() => {
    tokenRef.current = token;
  }, [token]);

  useEffect(() => {
    if (token) {
      api.defaults.headers.common.Authorization = `Bearer ${token}`;
      window.localStorage.setItem('paperless_token', token);
    } else {
      delete api.defaults.headers.common.Authorization;
      window.localStorage.removeItem('paperless_token');
    }
  }, [token]);

  useEffect(() => {
    const requestInterceptor = api.interceptors.request.use((config) => {
      const currentToken = tokenRef.current;
      if (currentToken) {
        config.headers = config.headers || {};
        if (!config.headers.Authorization) {
          config.headers.Authorization = `Bearer ${currentToken}`;
        }
      }
      return config;
    });

    const responseInterceptor = api.interceptors.response.use(
      (response) => response,
      async (error) => {
        const { response, config } = error;
        if (!response || !config) {
          return Promise.reject(error);
        }

        const status = response.status;
        const url = typeof config.url === 'string' ? config.url : '';
        const isAuthRoute = url.includes('/auth/login') || url.includes('/auth/refresh');

        if (status === 401 && !config._retry && !isAuthRoute) {
          console.warn('[Auth] 401 received for', url, '- attempting token refresh');

          if (!refreshPromiseRef.current) {
            refreshPromiseRef.current = (async () => {
              try {
                return await refreshAccessToken();
              } finally {
                refreshPromiseRef.current = null;
              }
            })();
          }

          try {
            const newToken = await refreshPromiseRef.current;
            if (!newToken) {
              throw new Error('No token returned from refresh');
            }
            config._retry = true;
            config.headers = config.headers || {};
            config.headers.Authorization = `Bearer ${newToken}`;
            console.log('[Auth] Retrying original request', url);
            return api(config);
          } catch (refreshError) {
            console.warn('[Auth] Refresh failed, clearing session');
            setToken('');
            setStatusMessage('Session expired. Please log in again.', 'error');
            return Promise.reject(refreshError);
          }
        }

        return Promise.reject(error);
      },
    );

    return () => {
      api.interceptors.request.eject(requestInterceptor);
      api.interceptors.response.eject(responseInterceptor);
    };
  }, [refreshAccessToken, setStatusMessage]);

  useEffect(() => {
    if (!selectedDocumentIds.length) {
      if (activePreviewId !== null) {
        setActivePreviewId(null);
      }
      return;
    }
    if (!selectedDocumentIds.includes(activePreviewId)) {
      setActivePreviewId(selectedDocumentIds[selectedDocumentIds.length - 1]);
    }
    initializedRef.current = true;
  }, [selectedDocumentIds, activePreviewId]);

  const currentFolderName = useMemo(() => {
    if (selectedFolder === 'root' || !currentFolder) return DEFAULT_FOLDER_NAME;
    return currentFolder.name;
  }, [selectedFolder, currentFolder]);

  const isFilterActive = useMemo(
    () => searchQuery.trim().length > 0 || activeTagFilters.length > 0,
    [searchQuery, activeTagFilters],
  );

  const applySelectedFolder = useCallback(
    (folderId, contents) => {
      const subfolders = contents?.subfolders ?? [];
      const docs = contents?.documents ?? [];
      const folderInfo = contents?.folder ?? null;

      setCurrentSubfolders(subfolders);
      setDocuments(docs);
      setCurrentFolder(folderInfo);

      const availableIds = new Set(docs.map((doc) => doc.id));
      let nextSelection = [];

      setSelectedDocumentIds((previous) => {
        if (initializedRef.current) {
          nextSelection = previous.filter((id) => availableIds.has(id));
          return nextSelection;
        }
        const filtered = previous.filter((id) => availableIds.has(id));
        if (filtered.length) {
          nextSelection = filtered;
          return filtered;
        }
        if (docs.length) {
          nextSelection = [docs[0].id];
          return nextSelection;
        }
        nextSelection = [];
        return [];
      });

      let nextFocus = focusedDocumentId && availableIds.has(focusedDocumentId)
        ? focusedDocumentId
        : null;
      if (!nextFocus && nextSelection.length) {
        nextFocus = nextSelection[nextSelection.length - 1];
      }

      setFocusedDocumentId(nextFocus);
      selectionAnchorRef.current = nextSelection.length
        ? nextSelection[nextSelection.length - 1]
        : null;
      selectionOrderRef.current = nextSelection;
      setSelectionOrder(nextSelection);

      return nextFocus;
    },
    [focusedDocumentId, setSelectionOrder],
  );

  const visibleDocuments = useMemo(
    () => (searchResults !== null ? searchResults : documents),
    [searchResults, documents],
  );

  const visibleDocumentIds = useMemo(
    () => visibleDocuments.map((doc) => doc.id),
    [visibleDocuments],
  );

  const applySelection = useCallback(
    (ids, { anchor, interactedIds = [] } = {}) => {
      const visibleSet = new Set(visibleDocumentIds);
      const unique = [];
      ids.forEach((id) => {
        if (visibleSet.has(id) && !unique.includes(id)) {
          unique.push(id);
        }
      });

      let resolvedAnchor = anchor;
      if (resolvedAnchor && !unique.includes(resolvedAnchor)) {
        resolvedAnchor = null;
      }

      const nextFocus =
        (focusedDocumentId && unique.includes(focusedDocumentId) && focusedDocumentId) ||
        resolvedAnchor ||
        (unique.length ? unique[unique.length - 1] : null);

      setSelectedDocumentIds(unique);
      setFocusedDocumentId(nextFocus);
      updateSelectionOrder(unique, interactedIds);

      if (resolvedAnchor) {
        selectionAnchorRef.current = resolvedAnchor;
      } else if (unique.length === 0) {
        selectionAnchorRef.current = null;
      } else if (
        !selectionAnchorRef.current ||
        !unique.includes(selectionAnchorRef.current)
      ) {
        selectionAnchorRef.current = unique[unique.length - 1];
      }

      return { selection: unique, focus: nextFocus };
    },
    [visibleDocumentIds, focusedDocumentId, updateSelectionOrder],
  );

  const promoteSelectionOrder = useCallback(
    (docId) => {
      if (!docId) return;
      if (!selectedDocumentIds.includes(docId)) return;
      updateSelectionOrder(selectedDocumentIds, [docId]);
      selectionAnchorRef.current = docId;
      setFocusedDocumentId(docId);
      setActivePreviewId(docId);
    },
    [selectedDocumentIds, updateSelectionOrder],
  );

  const visibleSelectedCount = useMemo(
    () => selectedDocumentIds.filter((id) => visibleDocumentIds.includes(id)).length,
    [selectedDocumentIds, visibleDocumentIds],
  );

  const allDocumentsSelected =
    visibleDocumentIds.length > 0 &&
    visibleSelectedCount === visibleDocumentIds.length;
  const someDocumentsSelected =
    visibleSelectedCount > 0 && !allDocumentsSelected;

  const folderOptions = useMemo(() => {
    const cache = new Map();
    const computePath = (id) => {
      if (cache.has(id)) {
        return cache.get(id);
      }
      if (!id || id === 'root') {
        cache.set('root', DEFAULT_FOLDER_NAME);
        return DEFAULT_FOLDER_NAME;
      }
      const node = folderNodes.get(id);
      if (!node) {
        return 'Folder';
      }
      const parentId = node.parentId || 'root';
      const parentPath = computePath(parentId);
      const name = node.name || 'Folder';
      const fullPath = parentId === 'root' ? name : `${parentPath}/${name}`;
      cache.set(id, fullPath);
      return fullPath;
    };

    const entries = [];
    folderNodes.forEach((node, id) => {
      if (!node) return;
      entries.push({ id, label: computePath(id) });
    });

    entries.sort((a, b) => {
      if (a.id === 'root') return -1;
      if (b.id === 'root') return 1;
      return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' });
    });

    return entries;
  }, [folderNodes]);

  const folderLabelMap = useMemo(() => {
    const map = new Map();
    folderOptions.forEach((option) => {
      map.set(option.id, option.label);
    });
    return map;
  }, [folderOptions]);

  const handleDocumentRowClick = useCallback(
    (documentId, event) => {
      const { shiftKey, metaKey, ctrlKey } = event;
      const additive = metaKey || ctrlKey;
      if (shiftKey) {
        event.preventDefault();
      }
      if (!visibleDocumentIds.includes(documentId)) {
        return;
      }

      let anchor = selectionAnchorRef.current;
      if (!anchor && shiftKey && selectedDocumentIds.length) {
        anchor = selectedDocumentIds[selectedDocumentIds.length - 1];
      }
      if (!anchor) {
        anchor = documentId;
      }
      let nextIds;
      let interactedIds = [];

      if (shiftKey && anchor) {
        const anchorIndex = visibleDocumentIds.indexOf(anchor);
        const targetIndex = visibleDocumentIds.indexOf(documentId);
        if (anchorIndex !== -1 && targetIndex !== -1) {
          const [start, end] =
            anchorIndex <= targetIndex
              ? [anchorIndex, targetIndex]
              : [targetIndex, anchorIndex];
          const range = visibleDocumentIds.slice(start, end + 1);
          nextIds = range;

          const ordered = anchorIndex <= targetIndex ? range : [...range].reverse();
          const previousSet = new Set(selectedDocumentIds);
          interactedIds = ordered.filter(
            (id) => id === documentId || !previousSet.has(id),
          );
          if (!interactedIds.includes(documentId)) {
            interactedIds.push(documentId);
          }
        } else {
          nextIds = [documentId];
          interactedIds = [documentId];
        }
      } else if (additive) {
        if (selectedDocumentIds.includes(documentId)) {
          nextIds = selectedDocumentIds.filter((id) => id !== documentId);
          interactedIds = [];
        } else {
          nextIds = [...selectedDocumentIds, documentId];
          interactedIds = [documentId];
        }
        anchor = documentId;
      } else {
        nextIds = [documentId];
        interactedIds = [documentId];
        anchor = documentId;
      }

      applySelection(nextIds, { anchor, interactedIds });
    },
    [
      visibleDocumentIds,
      selectedDocumentIds,
      applySelection,
    ],
  );

  const ensureFolderData = useCallback(
    async (folderId, { force = false } = {}) => {
      if (!force && folderContents.has(folderId)) {
        return folderContents.get(folderId);
      }

      const path = folderId === 'root' ? 'root' : folderId;
      const { data } = await api.get(`/folders/${path}/contents`);

      setFolderNodes((prev) => {
        const next = new Map(prev);
        const existingNode = next.get(folderId) || {
          id: folderId,
          name: folderId === 'root' ? DEFAULT_FOLDER_NAME : data.folder?.name || 'Folder',
          parentId: data.folder?.parent_id || 'root',
          children: [],
          expanded: folderId === 'root',
          loaded: false,
        };

        const childIds = (data.subfolders || []).map((child) => child.id);
        next.set(folderId, {
          ...existingNode,
          name: folderId === 'root' ? DEFAULT_FOLDER_NAME : data.folder?.name || existingNode.name,
          parentId: data.folder?.parent_id ?? existingNode.parentId ?? 'root',
          children: childIds,
          loaded: true,
        });

        (data.subfolders || []).forEach((child) => {
          const childNode = next.get(child.id);
          next.set(child.id, {
            id: child.id,
            name: child.name,
            parentId: child.parent_id ?? 'root',
            children: childNode?.children ?? [],
            expanded: childNode?.expanded ?? false,
            loaded: childNode?.loaded ?? false,
          });
        });

        return next;
      });

      setFolderContents((prev) => {
        const next = new Map(prev);
        next.set(folderId, data);
        return next;
      });

      return data;
    },
    [folderContents],
  );

  const isInvalidFolderDrop = useCallback(
    (sourceId, targetId) => {
      if (!sourceId) return false;
      if (!targetId || targetId === 'root') {
        return false;
      }
      if (sourceId === targetId) {
        return true;
      }

      let current = targetId;
      const visited = new Set();
      while (current && current !== 'root' && !visited.has(current)) {
        visited.add(current);
        if (current === sourceId) {
          return true;
        }
        const node = folderNodes.get(current);
        if (!node) break;
        current = node.parentId ?? 'root';
      }
      return false;
    },
    [folderNodes],
  );

  const moveFolder = useCallback(
    async (folderId, targetFolderId) => {
      const node = folderNodes.get(folderId);
      if (!node) {
        setStatusMessage('Folder metadata unavailable. Try refreshing.', 'error');
        return;
      }

      const previousParentKey = node.parentId ?? 'root';
      const targetKey = targetFolderId && targetFolderId !== 'root' ? targetFolderId : 'root';

      if (previousParentKey === targetKey) {
        return;
      }

      const parent_id = targetKey === 'root' ? null : targetKey;

      try {
        await api.patch(`/folders/${folderId}`, { parent_id });

        setFolderNodes((prev) => {
          const next = new Map(prev);
          const currentNode = next.get(folderId);
          if (!currentNode) {
            return prev;
          }

          const updatedNode = { ...currentNode, parentId: parent_id ?? null };
          next.set(folderId, updatedNode);

          const previousParent = next.get(previousParentKey);
          if (previousParent) {
            next.set(previousParentKey, {
              ...previousParent,
              children: (previousParent.children || []).filter((childId) => childId !== folderId),
            });
          }

          if (!next.has(targetKey)) {
            next.set(targetKey, {
              id: targetKey,
              name: targetKey === 'root' ? DEFAULT_FOLDER_NAME : 'Folder',
              parentId: targetKey === 'root' ? null : null,
              children: [],
              expanded: targetKey === 'root',
              loaded: false,
            });
          }

          const targetNode = next.get(targetKey);
          if (targetNode && !targetNode.children.includes(folderId)) {
            next.set(targetKey, {
              ...targetNode,
              children: [...targetNode.children, folderId],
            });
          }

          return next;
        });

        const refreshTargets = new Set([previousParentKey, targetKey]);
        for (const key of refreshTargets) {
          if (key === 'root') {
            await ensureFolderData('root', { force: true });
          } else {
            await ensureFolderData(key, { force: true });
          }
        }

        if (selectedFolder === folderId) {
          await ensureFolderData(folderId, { force: true });
          setSelectedFolder(folderId);
        }

        setStatusMessage('Folder moved.', 'success');
      } catch (error) {
        console.error(error);
        const message = error.response?.data?.error || 'Failed to move folder.';
        setStatusMessage(message, 'error');

        const refreshTargets = new Set([previousParentKey, targetKey]);
        for (const key of refreshTargets) {
          if (key === 'root') {
            await ensureFolderData('root', { force: true });
          } else {
            await ensureFolderData(key, { force: true });
          }
        }
      }
    },
    [api, folderNodes, ensureFolderData, selectedFolder, setSelectedFolder, setStatusMessage, setFolderNodes],
  );

  const handleFolderDragStart = useCallback(
    (event, folderId) => {
      if (folderId === 'root') {
        return;
      }
      event.stopPropagation();
      setDraggedFolderId(folderId);
      event.dataTransfer.effectAllowed = 'move';
      try {
        event.dataTransfer.setData('application/x-paperless-folder', folderId);
      } catch (
        // eslint-disable-next-line no-empty
        error
      ) {}
    },
    [setDraggedFolderId],
  );

  const handleFolderDragEnd = useCallback(() => {
    setDraggedFolderId(null);
  }, [setDraggedFolderId]);

  const ensureDocumentDetail = useCallback(
    async (documentId, { force = false } = {}) => {
      if (!documentId) return null;
      const cached = documentDetailsRef.current.get(documentId);
      if (!force && cached) {
        return cached;
      }

      const { data } = await api.get(`/documents/${documentId}`);

      setDocumentDetails((prev) => {
        const next = new Map(prev);
        next.set(documentId, data);
        documentDetailsRef.current = next;
        return next;
      });

      setDocuments((prev) =>
        prev.map((doc) => (doc.id === documentId ? data.document : doc)),
      );

      setSearchResults((prev) =>
        prev ? prev.map((doc) => (doc.id === documentId ? data.document : doc)) : null,
      );

      return data;
    },
    [setDocumentDetails, setDocuments, setSearchResults],
  );

  const refreshTags = useCallback(async () => {
    try {
      const { data } = await api.get('/tags');
      setTags(data || []);
    } catch (error) {
      console.error(error);
      setStatusMessage('Unable to load tags.', 'error');
    }
  }, [setStatusMessage]);

  const loadFolder = useCallback(
    async (folderId, { showLoading = true } = {}) => {
      const targetId = folderId || 'root';
      setSelectedFolder(targetId);
      if (showLoading) setLoading(true);
      try {
        const contents = await ensureFolderData(targetId, { force: true });
        const nextSelectedId = applySelectedFolder(targetId, contents);
        setSearchResults(null);
        if (nextSelectedId) {
          await ensureDocumentDetail(nextSelectedId, { force: true });
        }
      } catch (error) {
        console.error(error);
        setStatusMessage('Failed to load folder contents.', 'error');
      } finally {
        if (showLoading) setLoading(false);
      }
    },
    [ensureFolderData, applySelectedFolder, ensureDocumentDetail, setStatusMessage],
  );

  const selectFolder = useCallback(
    async (folderId, { replace = false, immediate = false } = {}) => {
      const targetId = folderId && folderId !== 'root' ? folderId : 'root';

      if (!navigate || immediate) {
        await loadFolder(targetId);
        return;
      }

      const path = targetId === 'root' ? '/folders' : `/folders/${targetId}`;
      navigate(path, { replace });
    },
    [loadFolder, navigate],
  );

  const initializeAfterLogin = useCallback(async () => {
    setLoading(true);
    try {
      await refreshTags();
      const initialFolder = routeFolderId && routeFolderId !== 'root' ? routeFolderId : 'root';
      await loadFolder(initialFolder, { showLoading: false });
    } catch (error) {
      console.error(error);
      setStatusMessage('Failed to initialize data.', 'error');
    } finally {
      setLoading(false);
    }
  }, [refreshTags, routeFolderId, loadFolder, setStatusMessage]);

  useEffect(() => {
    const targetParam = routeFolderId ?? 'root';

    if (targetParam === 'root' && routeDocumentId) {
      return;
    }

    const hasData = folderContents.has(targetParam);
    if (targetParam !== selectedFolder || !hasData) {
      loadFolder(targetParam, { showLoading: !routeDocumentId });
    }
  }, [routeFolderId, routeDocumentId, selectedFolder, loadFolder, folderContents]);

  const refreshCurrentFolder = useCallback(async () => {
    setLoading(true);
    try {
      const contents = await ensureFolderData(selectedFolder, { force: true });
      const nextSelectedId = applySelectedFolder(selectedFolder, contents);
      if (nextSelectedId) {
        await ensureDocumentDetail(nextSelectedId, { force: true });
      }
    } catch (error) {
      console.error(error);
      setStatusMessage('Failed to refresh folder.', 'error');
    } finally {
      setLoading(false);
    }
  }, [selectedFolder, ensureFolderData, applySelectedFolder, ensureDocumentDetail, setStatusMessage]);

  useEffect(() => {
    if (!token || initializedRef.current) return;
    initializedRef.current = true;
    initializeAfterLogin();
  }, [token, initializeAfterLogin]);

  const handleBulkMoveSubmit = useCallback(
    async (event) => {
      event.preventDefault();
      if (!selectedDocumentIds.length) {
        setStatusMessage('Select documents before moving.', 'error');
        return;
      }

      const form = new FormData(event.currentTarget);
      const target = form.get('target')?.toString() || 'root';
      const folderId = target === 'root' ? null : target;

      setLoading(true);
      try {
        await api.post('/documents/bulk/move', {
          document_ids: selectedDocumentIds,
          folder_id: folderId,
        });

        const count = selectedDocumentIds.length;
        const suffix = count === 1 ? '' : 's';
        const folderLabel =
          folderId === null
            ? DEFAULT_FOLDER_NAME
            : folderLabelMap.get(target) || 'target folder';
        setStatusMessage(
          `Moved ${count} document${suffix} to ${folderLabel}.`,
          'success',
        );

        applySelection([], { anchor: null });

        await refreshCurrentFolder();
        if (folderId && folderId !== selectedFolder) {
          await ensureFolderData(folderId, { force: true });
        }

        event.currentTarget.reset();
      } catch (error) {
        console.error(error);
        const message = error.response?.data?.error || 'Failed to move documents.';
        setStatusMessage(message, 'error');
      } finally {
        setLoading(false);
      }
    },
    [
      api,
      selectedDocumentIds,
      folderLabelMap,
      applySelection,
      refreshCurrentFolder,
      ensureFolderData,
      selectedFolder,
      setStatusMessage,
    ],
  );

  const parseTagInput = useCallback((value) => {
    return value
      .split(',')
      .map((item) => item.trim())
      .filter(Boolean);
  }, []);

  const bulkTagOperation = useCallback(
    async ({ labels, action }) => {
      const normalized = labels.map((label) => label.trim()).filter((label) => label.length > 0);
      if (!normalized.length) {
        return { ok: false, reason: 'no-labels' };
      }
      if (!selectedDocumentIds.length) {
        return { ok: false, reason: 'no-selection' };
      }

      let tagIds = [];

      if (action === 'remove') {
        const missing = normalized.find(
          (label) => !tags.some((tag) => tag.label.toLowerCase() === label.toLowerCase()),
        );
        if (missing) {
          return { ok: false, reason: 'tag-missing', label: missing };
        }

        tagIds = normalized.map((label) => {
          const tag = tags.find((item) => item.label.toLowerCase() === label.toLowerCase());
          return tag?.id;
        }).filter(Boolean);
      }

      setLoading(true);
      try {
        if (action === 'add') {
          const createdIds = [];
          for (const label of normalized) {
            let tag = tags.find((item) => item.label.toLowerCase() === label.toLowerCase()) || null;
            if (!tag) {
              const { data } = await api.post('/tags', { label, color: null });
              tag = data;
              await refreshTags();
            }
            createdIds.push(tag.id);
          }
          tagIds = Array.from(new Set(createdIds));
        }

        tagIds = Array.from(new Set(tagIds));

        if (!tagIds.length) {
          return { ok: false, reason: 'no-tags' };
        }

        await api.post('/documents/bulk/tags', {
          document_ids: selectedDocumentIds,
          tag_ids: tagIds,
          action,
        });

        await Promise.all(
          selectedDocumentIds.map((id) => ensureDocumentDetail(id, { force: true }).catch(() => null)),
        );

        return {
          ok: true,
          tagCount: tagIds.length,
          docsCount: selectedDocumentIds.length,
        };
      } catch (error) {
        console.error(error);
        const message =
          error.response?.data?.error ||
          (action === 'add' ? 'Failed to assign tags.' : 'Failed to remove tags.');
        setStatusMessage(message, 'error');
        return { ok: false, reason: 'request-failed' };
      } finally {
        setLoading(false);
      }
    },
    [selectedDocumentIds, tags, api, refreshTags, ensureDocumentDetail, setStatusMessage, setLoading],
  );

  const handleBulkTagAdd = useCallback(
    async (event) => {
      event.preventDefault();
      if (!selectedDocumentIds.length) {
        setStatusMessage('Select documents before assigning tags.', 'error');
        return;
      }

      const form = new FormData(event.currentTarget);
      const raw = form.get('tags')?.toString().trim() || '';
      const labels = parseTagInput(raw);
      if (!labels.length) {
        setStatusMessage('Enter at least one tag label.', 'error');
        return;
      }
      const result = await bulkTagOperation({ labels, action: 'add' });
      if (result?.ok) {
        const { tagCount, docsCount } = result;
        setStatusMessage(
          `Assigned ${tagCount} tag${tagCount === 1 ? '' : 's'} to ${docsCount} document${
            docsCount === 1 ? '' : 's'
          }.`,
          'success',
        );
        event.currentTarget.reset();
      } else if (result?.reason === 'no-labels') {
        setStatusMessage('Enter at least one tag label.', 'error');
      }
    },
    [
      selectedDocumentIds,
      setStatusMessage,
      parseTagInput,
      bulkTagOperation,
    ],
  );

  const handleBulkTagRemove = useCallback(
    async (event) => {
      event.preventDefault();
      if (!selectedDocumentIds.length) {
        setStatusMessage('Select documents before removing tags.', 'error');
        return;
      }

      const form = new FormData(event.currentTarget);
      const raw = form.get('tags')?.toString().trim() || '';
      const labels = parseTagInput(raw);
      if (!labels.length) {
        setStatusMessage('Enter at least one tag label to remove.', 'error');
        return;
      }

      const result = await bulkTagOperation({ labels, action: 'remove' });
      if (result?.ok) {
        const { docsCount } = result;
        setStatusMessage(
          `Removed tags from ${docsCount} document${docsCount === 1 ? '' : 's'}.`,
          'success',
        );
        event.currentTarget.reset();
      } else if (result?.reason === 'no-labels') {
        setStatusMessage('Enter at least one tag label to remove.', 'error');
      } else if (result?.reason === 'tag-missing') {
        setStatusMessage(`Tag “${result.label}” not found.`, 'error');
      }
    },
    [
      selectedDocumentIds,
      setStatusMessage,
      parseTagInput,
      bulkTagOperation,
    ],
  );

  const handleBulkTagAddFromDetail = useCallback(
    async ({ label, input }) => {
      const trimmed = (label || '').trim();
      if (!trimmed) {
        setStatusMessage('Enter a tag label.', 'error');
        return;
      }
      if (!selectedDocumentIds.length) {
        setStatusMessage('Select documents before assigning tags.', 'error');
        return;
      }
      const result = await bulkTagOperation({ labels: [trimmed], action: 'add' });
      if (result?.ok) {
        const { tagCount, docsCount } = result;
        setStatusMessage(
          `Assigned ${tagCount} tag${tagCount === 1 ? '' : 's'} to ${docsCount} document${
            docsCount === 1 ? '' : 's'
          }.`,
          'success',
        );
        if (input) {
          input.value = '';
        }
      }
    },
    [bulkTagOperation, selectedDocumentIds, setStatusMessage],
  );

  const handleBulkTagRemoveFromDetail = useCallback(
    async ({ label, input }) => {
      const trimmed = (label || '').trim();
      if (!trimmed) {
        setStatusMessage('Enter a tag label to remove.', 'error');
        return;
      }
      if (!selectedDocumentIds.length) {
        setStatusMessage('Select documents before removing tags.', 'error');
        return;
      }
      const result = await bulkTagOperation({ labels: [trimmed], action: 'remove' });
      if (result?.ok) {
        const { docsCount } = result;
        setStatusMessage(
          `Removed tags from ${docsCount} document${docsCount === 1 ? '' : 's'}.`,
          'success',
        );
        if (input) {
          input.value = '';
        }
      } else if (result?.reason === 'tag-missing') {
        setStatusMessage(`Tag “${result.label}” not found.`, 'error');
      }
    },
    [bulkTagOperation, selectedDocumentIds, setStatusMessage],
  );

  const handleBulkSelectionReanalyze = useCallback(async () => {
    if (!selectedDocumentIds.length) {
      setStatusMessage('Select documents before requesting re-analysis.', 'error');
      return;
    }

    setLoading(true);
    try {
      const { data } = await api.post('/documents/bulk/reanalyze', {
        document_ids: selectedDocumentIds,
        force: true,
      });
      const queued = data?.queued ?? selectedDocumentIds.length;
      setStatusMessage(
        `Queued re-analysis for ${queued} document${queued === 1 ? '' : 's'}.`,
        'success',
      );
    } catch (error) {
      console.error(error);
      const message =
        error.response?.data?.error || 'Failed to queue document re-analysis.';
      setStatusMessage(message, 'error');
    } finally {
      setLoading(false);
    }
  }, [selectedDocumentIds, api, setStatusMessage]);

  const uploadFile = useCallback(
    async (file, targetFolderId) => {
      if (!file || file.size === 0) {
        setStatusMessage('Skipped empty file.', 'error');
        return null;
      }

      const formData = new FormData();
      formData.append('file', file, file.name);
      if (targetFolderId && targetFolderId !== 'root') {
        formData.append('folder_id', targetFolderId);
      }

      try {
        const { data, status } = await api.post('/documents', formData);
        const duplicate = data?.reused || status === 200;
        setStatusMessage(
          duplicate
            ? `${file.name} already exists; reused existing document.`
            : `Uploaded ${file.name}`,
          duplicate ? 'info' : 'success',
        );
        return data;
      } catch (error) {
        console.error(error);
        const message = error.response?.data?.error || `Failed to upload ${file.name}.`;
        setStatusMessage(message, 'error');
        throw error;
      }
    },
    [setStatusMessage],
  );

  const folderPathCacheRef = useRef(new Map());
  const previewCacheRef = useRef(new Map());
  const [previewCacheTick, setPreviewCacheTick] = useState(0);

  const ensureFolderPathOnServer = useCallback(
    async (baseFolderId, segments) => {
      const trimmedSegments = segments.map((segment) => segment.trim()).filter(Boolean);
      if (trimmedSegments.length === 0) {
        return baseFolderId ?? null;
      }

      const cacheKey = `${baseFolderId ?? 'ROOT'}:${trimmedSegments.join('/')}`;
      const cache = folderPathCacheRef.current;
      if (cache.has(cacheKey)) {
        return cache.get(cacheKey);
      }

      const payload = {
        parent_id: baseFolderId && baseFolderId !== 'root' ? baseFolderId : null,
        segments: trimmedSegments,
      };

      const { data } = await api.post('/folders/path', payload);
      const folderId = data.folder.id;
      cache.set(cacheKey, folderId);
      return folderId;
    },
    [],
  );

  const ensurePreviewUrl = useCallback(
    async (documentId, { force = false } = {}) => {
      if (!documentId) return null;

      const cached = previewCacheRef.current.get(documentId);
      const now = Date.now();
      if (!force && cached && (!cached.expiresAt || cached.expiresAt > now)) {
        return cached;
      }

      try {
        const { data } = await api.get(`/documents/${documentId}/download`);
        const ttl = data.expires_in ? Math.max(data.expires_in - 60, 30) * 1000 : 5 * 60 * 1000;
        const entry = {
          url: data.url,
          contentType: data.content_type || null,
          filename: data.filename,
          expiresAt: now + ttl,
        };

        previewCacheRef.current.set(documentId, entry);
        setPreviewCacheTick((prev) => prev + 1);
        return entry;
      } catch (error) {
        console.error(error);
        setStatusMessage('Unable to fetch document preview.', 'error');
        throw error;
      }
    },
    [setStatusMessage],
  );

  const extractFilesFromDataTransfer = useCallback(async (dataTransfer) => {
    if (
      !dataTransfer ||
      !dataTransfer.items ||
      !window.isSecureContext ||
      !Array.from(dataTransfer.items).some((item) => typeof item.getAsFileSystemHandle === 'function')
    ) {
      throw new Error('File System Access API is required for uploads.');
    }

    const results = [];

    const pushFile = (file, ancestors) => {
      if (file) {
        results.push({
          file,
          segments: ancestors.filter(Boolean),
        });
      }
    };

    const walkDirectoryHandle = async (handle, ancestors) => {
      const nextAncestors = handle.name ? [...ancestors, handle.name] : [...ancestors];
      for await (const child of handle.values()) {
        if (child.kind === 'file') {
          const file = await child.getFile();
          pushFile(file, nextAncestors);
        } else if (child.kind === 'directory') {
          // eslint-disable-next-line no-await-in-loop
          await walkDirectoryHandle(child, nextAncestors);
        }
      }
    };

    for (const item of Array.from(dataTransfer.items)) {
      if (item.kind !== 'file') continue;
      const getHandle = item.getAsFileSystemHandle?.bind(item);
      if (!getHandle) {
        throw new Error('File System Access API handle missing.');
      }

      // eslint-disable-next-line no-await-in-loop
      const handle = await getHandle();
      if (!handle) continue;

      if (handle.kind === 'file') {
        const file = await handle.getFile();
        pushFile(file, []);
      } else if (handle.kind === 'directory') {
        // eslint-disable-next-line no-await-in-loop
        await walkDirectoryHandle(handle, []);
      }
    }

    if (!results.length) {
      throw new Error('No files detected in drop payload.');
    }

    return results;
  }, []);

  const handleFileDrop = useCallback(
    async (dataTransfer, targetFolderId) => {
      if (!token) {
        setStatusMessage('Please log in before uploading.', 'error');
        return;
      }

      setLoading(true);

      try {
        folderPathCacheRef.current.clear();

        let extracted;
        try {
          extracted = await extractFilesFromDataTransfer(dataTransfer);
        } catch (error) {
          console.error(error);
          setStatusMessage(error.message || 'Failed to process dropped files.', 'error');
          return;
        }

        if (!extracted.length) {
          setStatusMessage('No files to upload.', 'info');
          return;
        }
        const baseFolderId =
          targetFolderId && targetFolderId !== 'root' ? targetFolderId : null;

        for (const { file, segments } of extracted) {
          // eslint-disable-next-line no-await-in-loop
          const destinationId = segments.length
            ? await ensureFolderPathOnServer(baseFolderId, segments)
            : baseFolderId;

          const uploadTarget =
            destinationId ??
            (targetFolderId && targetFolderId !== 'root' ? targetFolderId : 'root');

          // eslint-disable-next-line no-await-in-loop
          await uploadFile(file, uploadTarget);
        }

        await refreshCurrentFolder();

        if (
          targetFolderId &&
          targetFolderId !== 'root' &&
          targetFolderId !== selectedFolder
        ) {
          await ensureFolderData(targetFolderId, { force: true });
        }
      } catch (error) {
        // errors surfaced through setStatusMessage
      } finally {
        setLoading(false);
      }
    },
    [
      token,
      extractFilesFromDataTransfer,
      ensureFolderPathOnServer,
      uploadFile,
      refreshCurrentFolder,
      selectedFolder,
      ensureFolderData,
      setStatusMessage,
    ],
  );

  const moveDocumentsToFolder = useCallback(
    async (documentIds, targetFolderId) => {
      const unique = Array.from(new Set(documentIds || [])).filter(Boolean);
      if (!unique.length) return;

      const target = targetFolderId === 'root' ? null : targetFolderId;
      setLoading(true);
      try {
        if (unique.length === 1) {
          await api.patch(`/documents/${unique[0]}/folder`, { folder_id: target });
        } else {
          await api.post('/documents/bulk/move', {
            document_ids: unique,
            folder_id: target,
          });
        }

        const count = unique.length;
        const suffix = count === 1 ? '' : 's';
        const targetLabel =
          target === null
            ? DEFAULT_FOLDER_NAME
            : folderLabelMap.get(targetFolderId) || 'target folder';
        setStatusMessage(
          `Moved ${count} document${suffix} to ${targetLabel}.`,
          'success',
        );

        setDocumentDetails((prev) => {
          const next = new Map(prev);
          unique.forEach((id) => next.delete(id));
          return next;
        });

        await refreshCurrentFolder();
        if (targetFolderId && targetFolderId !== selectedFolder) {
          await ensureFolderData(targetFolderId, { force: true });
        }
      } catch (error) {
        console.error(error);
        const message = error.response?.data?.error || 'Failed to move documents.';
        setStatusMessage(message, 'error');
      } finally {
        setLoading(false);
      }
    },
    [
      api,
      ensureFolderData,
      refreshCurrentFolder,
      selectedFolder,
      folderLabelMap,
      setStatusMessage,
    ],
  );

  const handleBulkMoveFromDetail = useCallback(
    async ({ target }) => {
      const destination = target === 'root' ? 'root' : target;
      if (!selectedDocumentIds.length) {
        setStatusMessage('Select documents before moving.', 'error');
        return;
      }
      await moveDocumentsToFolder(selectedDocumentIds, destination);
    },
    [selectedDocumentIds, moveDocumentsToFolder, setStatusMessage],
  );

const handleDownload = useCallback(
  async (documentId) => {
    try {
      setLoading(true);
      const { data } = await api.get(`/documents/${documentId}/download`);
      window.open(data.url, '_blank');
      setStatusMessage('Download link opened in a new tab.', 'success');
    } catch (error) {
      console.error(error);
      setStatusMessage('Unable to fetch download link.', 'error');
    } finally {
      setLoading(false);
    }
  },
  [setStatusMessage],
);

  const handleThumbnailRegeneration = useCallback(
    async (documentId) => {
      if (!token) {
        setStatusMessage('Log in to manage assets.', 'error');
        return;
      }
      setLoading(true);
      try {
        await api.post(`/documents/${documentId}/assets`, null, {
          params: { force: true },
        });
        setStatusMessage('Document re-analysis queued.', 'info');
        await ensureDocumentDetail(documentId, { force: true });
      } catch (error) {
        console.error(error);
        const message =
          error.response?.data?.error || 'Failed to request thumbnail generation.';
        setStatusMessage(message, 'error');
      } finally {
        setLoading(false);
      }
    },
    [token, ensureDocumentDetail, setStatusMessage],
  );

  const openDocumentPreview = useCallback(
    async (documentId, { replace = false, skipNavigate = false } = {}) => {
      if (!documentId) return;
      setPreviewDocumentId(documentId);
      setPreviewDocumentLoading(true);
      try {
        await ensureDocumentDetail(documentId, { force: false });
        await ensurePreviewUrl(documentId, { force: false });
        setActivePreviewId(documentId);
        if (!skipNavigate && navigate) {
          navigate(`/documents/${documentId}`, { replace });
        }
      } catch (error) {
        console.error(error);
        setPreviewDocumentId(null);
      } finally {
        setPreviewDocumentLoading(false);
      }
    },
    [ensureDocumentDetail, ensurePreviewUrl, navigate],
  );

  const closeDocumentPreview = useCallback(
    (folderId = null) => {
      setPreviewDocumentId(null);
      setPreviewDocumentLoading(false);

      const targetId = folderId || selectedFolder || 'root';

      if (!navigate) {
        loadFolder(targetId, { showLoading: false });
        return;
      }

      const path = targetId === 'root' ? '/folders' : `/folders/${targetId}`;
      navigate(path, { replace: false });
    },
    [navigate, selectedFolder, loadFolder],
  );

  useEffect(() => {
    if (!previewDocumentId) return;
    const handleKeyDown = (event) => {
      if (event.key === 'Escape') {
        closeDocumentPreview();
      }
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [previewDocumentId, closeDocumentPreview]);

  useEffect(() => {
    if (!routeDocumentId) {
      if (previewDocumentId) {
        closeDocumentPreview();
      }
      return;
    }

    let cancelled = false;

    const hydratePreview = async () => {
      try {
        const detail = await ensureDocumentDetail(routeDocumentId, { force: true });
        const doc = detail?.document || documentDetailsRef.current.get(routeDocumentId)?.document;
        const targetFolder = doc?.folder_id || routeFolderId || 'root';
        if (targetFolder && targetFolder !== selectedFolder) {
          await loadFolder(targetFolder, { showLoading: false });
        }
        if (!cancelled) {
          await openDocumentPreview(routeDocumentId, { replace: true, skipNavigate: true });
        }
      } catch (error) {
        if (!cancelled) {
          console.error(error);
          setStatusMessage('Failed to open document preview.', 'error');
        }
      }
    };

    hydratePreview();

    return () => {
      cancelled = true;
    };
  }, [
    routeDocumentId,
    routeFolderId,
    ensureDocumentDetail,
    selectedFolder,
    loadFolder,
    openDocumentPreview,
    previewDocumentId,
    closeDocumentPreview,
    setStatusMessage,
  ]);

  const handleTagRemove = useCallback(
    async (documentId, tagId) => {
      try {
        await api.delete(`/documents/${documentId}/tags/${tagId}`);
        setStatusMessage('Tag removed.', 'success');
        await Promise.all([
          refreshCurrentFolder(),
          refreshTags(),
          ensureDocumentDetail(documentId, { force: true }),
        ]);
      } catch (error) {
        console.error(error);
        setStatusMessage('Failed to remove tag.', 'error');
      }
    },
    [ensureDocumentDetail, refreshCurrentFolder, refreshTags, setStatusMessage],
  );

  const handleTagAdd = useCallback(
    async (document, label, input) => {
      let tag = tags.find((item) => item.label.toLowerCase() === label.toLowerCase()) || null;
      try {
        if (!tag) {
          const { data } = await api.post('/tags', { label, color: null });
          tag = data;
          await refreshTags();
        }
        await api.post(`/documents/${document.id}/tags`, { tag_ids: [tag.id] });
        setStatusMessage('Tag assigned.', 'success');
        input.value = '';
        await Promise.all([
          refreshCurrentFolder(),
          ensureDocumentDetail(document.id, { force: true }),
        ]);
      } catch (error) {
        console.error(error);
        setStatusMessage('Failed to assign tag.', 'error');
      }
    },
    [tags, refreshTags, refreshCurrentFolder, ensureDocumentDetail, setStatusMessage],
  );

  const handleFolderDelete = useCallback(
    async (folderId) => {
      if (!token) {
        setStatusMessage('Log in to manage folders.', 'error');
        return;
      }
      if (folderId === 'root') {
        setStatusMessage('The root folder cannot be removed.', 'error');
        return;
      }
      setLoading(true);
      try {
        const contents = await ensureFolderData(folderId, { force: true });
        const hasChildren = (contents.subfolders || []).length > 0;
        const hasDocs = (contents.documents || []).length > 0;
        if (hasChildren || hasDocs) {
          setStatusMessage('Folder must be empty before it can be deleted.', 'error');
          return;
        }
        await api.delete(`/folders/${folderId}`);
        setFolderNodes((prev) => {
          const next = new Map(prev);
          const node = next.get(folderId);
          next.delete(folderId);
          if (node) {
            const parentId = node.parentId || 'root';
            const parentNode = next.get(parentId);
            if (parentNode) {
              next.set(parentId, {
                ...parentNode,
                children: parentNode.children.filter((id) => id !== folderId),
              });
            }
          }
          return next;
        });
        setFolderContents((prev) => {
          const next = new Map(prev);
          next.delete(folderId);
          return next;
        });
        if (selectedFolder === folderId) {
          const node = folderNodes.get(folderId);
          const parentId = node?.parentId || 'root';
          setSelectedFolder(parentId);
          const parentContents = await ensureFolderData(parentId, { force: true });
          const nextSelectedId = applySelectedFolder(parentId, parentContents);
          if (nextSelectedId) {
            await ensureDocumentDetail(nextSelectedId, { force: true });
          }
        } else if (selectedFolder !== 'root') {
          await ensureFolderData(selectedFolder, { force: true });
        }
        setStatusMessage('Folder deleted.', 'success');
      } catch (error) {
        console.error(error);
        const message = error.response?.data?.error || 'Failed to delete folder.';
        setStatusMessage(message, 'error');
      } finally {
        setLoading(false);
      }
    },
    [token, ensureFolderData, selectedFolder, folderNodes, applySelectedFolder, ensureDocumentDetail, setStatusMessage],
  );

  const handleFolderCreate = useCallback(
    async (name, onSuccess) => {
      if (!token) {
        setStatusMessage('Log in to create folders.', 'error');
        return;
      }
      if (!name.trim()) {
        setStatusMessage('Folder name cannot be empty.', 'error');
        return;
      }
      const payload = {
        name: name.trim(),
        parent_id: selectedFolder === 'root' ? null : selectedFolder,
      };
      setLoading(true);
      try {
        const { data } = await api.post('/folders', payload);
        onSuccess();
        setStatusMessage('Folder created.', 'success');
        setFolderNodes((prev) => {
          const next = new Map(prev);
          const parentId = payload.parent_id || 'root';
          const parentNode = next.get(parentId);
          if (parentNode) {
            next.set(parentId, {
              ...parentNode,
              children: parentNode.children.concat([data.folder.id]),
              loaded: true,
            });
          }
          next.set(data.folder.id, {
            id: data.folder.id,
            name: data.folder.name,
            parentId: parentId,
            children: [],
            expanded: false,
            loaded: false,
          });
          return next;
        });
        await ensureFolderData(selectedFolder, { force: true });
      } catch (error) {
        console.error(error);
        const message = error.response?.data?.error || 'Failed to create folder.';
        setStatusMessage(message, 'error');
      } finally {
        setLoading(false);
      }
    },
    [token, selectedFolder, ensureFolderData, setStatusMessage],
  );

  useEffect(() => {
    if (!token) return undefined;

    if (!isFilterActive) {
      setSearchResults(null);
      return undefined;
    }

    let cancelled = false;
    let started = false;

    const debounce = setTimeout(async () => {
      started = true;
      setLoading(true);
      try {
        const params = {};
        const trimmedQuery = searchQuery.trim();
        if (trimmedQuery.length) {
          params.query = trimmedQuery;
        }
        if (activeTagFilters.length) {
          params.tags = activeTagFilters.join(',');
        }
        const folderIdentifier = selectedFolder === 'root' ? 'root' : selectedFolder;
        const { data } = await api.get(`/folders/${folderIdentifier}/documents`, {
          params,
        });
        if (cancelled) return;

        const results = data || [];
        setSearchResults(results);

        if (!results.length) {
          setSelectedDocumentIds([]);
          setFocusedDocumentId(null);
          selectionAnchorRef.current = null;
          return;
        }

        const resultIds = results.map((doc) => doc.id);
        let targetId = null;
        setSelectedDocumentIds((previous) => {
          const filtered = previous.filter((id) => resultIds.includes(id));
          if (filtered.length) {
            targetId = filtered[filtered.length - 1];
            return filtered;
          }
          targetId = resultIds[0];
          return [targetId];
        });

        setFocusedDocumentId((previous) => {
          if (previous && resultIds.includes(previous)) {
            targetId = previous;
            return previous;
          }
          return targetId;
        });

        selectionAnchorRef.current = targetId;

        if (!cancelled && targetId) {
          const cached = documentDetailsRef.current.get(targetId);
          await ensureDocumentDetail(targetId, { force: !cached });
        }
      } catch (error) {
        if (cancelled) return;
        console.error(error);
        setStatusMessage('Search failed. Please try again.', 'error');
        setSearchResults(null);
      } finally {
        if (!cancelled && started) {
          setLoading(false);
        }
      }
    }, 300);

    return () => {
      cancelled = true;
      clearTimeout(debounce);
      if (started) {
        setLoading(false);
      }
    };
  }, [
    token,
    isFilterActive,
    searchQuery,
    activeTagFilters,
    selectedFolder,
    ensureDocumentDetail,
    setStatusMessage,
  ]);

  useEffect(() => {
    if (!token) {
      setDropOverlayState((prev) => ({ ...prev, active: false }));
      dragCounterRef.current = 0;
      return undefined;
    }

    const handleDragEnter = (event) => {
      if (!hasFiles(event)) return;
      event.preventDefault();
      dragCounterRef.current += 1;
      setDropOverlayState({ active: true, folderName: currentFolderName });
    };

    const handleDragOver = (event) => {
      if (!hasFiles(event)) return;
      event.preventDefault();
      event.dataTransfer.dropEffect = 'copy';
    };

    const handleDragLeave = (event) => {
      if (!hasFiles(event)) return;
      dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
      if (dragCounterRef.current === 0) {
        setDropOverlayState((prev) => ({ ...prev, active: false }));
      }
    };

    const handleDrop = async (event) => {
      if (!hasFiles(event)) return;
      event.preventDefault();
      dragCounterRef.current = 0;
      setDropOverlayState((prev) => ({ ...prev, active: false }));
      await handleFileDrop(event.dataTransfer, selectedFolder);
    };

    window.addEventListener('dragenter', handleDragEnter);
    window.addEventListener('dragover', handleDragOver);
    window.addEventListener('dragleave', handleDragLeave);
    window.addEventListener('drop', handleDrop);

    return () => {
      window.removeEventListener('dragenter', handleDragEnter);
      window.removeEventListener('dragover', handleDragOver);
      window.removeEventListener('dragleave', handleDragLeave);
      window.removeEventListener('drop', handleDrop);
      dragCounterRef.current = 0;
      setDropOverlayState((prev) => ({ ...prev, active: false }));
    };
  }, [token, handleFileDrop, currentFolderName, selectedFolder]);

  const handleLogin = useCallback(
    async (event) => {
      event.preventDefault();
      const form = new FormData(event.currentTarget);
      const payload = {
        username: form.get('username')?.toString().trim(),
        password: form.get('password')?.toString() || '',
      };
      if (!payload.username || !payload.password) {
        setStatusMessage('Username and password are required.', 'error');
        return;
      }
      try {
        setLoading(true);
        const { data } = await api.post('/auth/login', payload);
        setToken(data.access_token);
        setStatusMessage('Login successful.', 'success');
      } catch (error) {
        console.error(error);
        setStatusMessage('Login failed. Check credentials.', 'error');
      } finally {
        setLoading(false);
      }
    },
    [setStatusMessage],
  );

  const handleLogout = useCallback(async () => {
    try {
      setLoading(true);
      await api.post('/auth/logout');
    } catch (error) {
      console.warn('[Auth] Failed to revoke refresh token during logout', error);
    } finally {
      setLoading(false);
      setToken('');
      setStatusMessage('Logged out.', 'info');
      setFolderNodes(new Map([[createRootNode().id, createRootNode()]]));
      setFolderContents(new Map());
      setSelectedFolder('root');
      setCurrentFolder(null);
      setCurrentSubfolders([]);
      setDocuments([]);
      setSelectedDocumentIds([]);
      setFocusedDocumentId(null);
      selectionAnchorRef.current = null;
      setDraggedDocumentIds([]);
      setDocumentDetails(new Map());
      setSearchResults(null);
      setTags([]);
      setSearchQuery('');
      setActiveTagFilters([]);
      initializedRef.current = false;
    }
  }, [setStatusMessage]);

  const handleBulkReanalyze = useCallback(async () => {
    try {
      setLoading(true);
      const { data } = await api.post('/documents/reanalyze');
      const total = data?.queued ?? 0;
      const suffix = total === 1 ? '' : 's';
      setStatusMessage(
        `Queued re-analysis for ${total} document${suffix}.`,
        'success',
      );
    } catch (error) {
      console.error(error);
      const message =
        error.response?.data?.error || 'Failed to queue document re-analysis.';
      setStatusMessage(message, 'error');
    } finally {
      setLoading(false);
    }
  }, [setStatusMessage]);


  const folderClickHandlers = {
    onToggle: async (folderId) => {
      const node = folderNodes.get(folderId);
      if (node && !node.loaded) {
        try {
          await ensureFolderData(folderId);
        } catch (error) {
          console.error(error);
          setStatusMessage('Failed to load folder.', 'error');
        }
      }
      setFolderNodes((prev) => {
        const next = new Map(prev);
        const current = next.get(folderId);
        if (!current) return prev;
        next.set(folderId, { ...current, expanded: !current.expanded });
        return next;
      });
    },
    onSelect: selectFolder,
    onDrop: async (event, folderId) => {
      event.preventDefault();
      event.stopPropagation();
      event.currentTarget.classList.remove('is-drop-target');

      let folderSourceId = draggedFolderId;
      if (!folderSourceId) {
        try {
          if (event.dataTransfer.types?.includes('application/x-paperless-folder')) {
            folderSourceId = event.dataTransfer.getData('application/x-paperless-folder');
          }
        } catch (
          // eslint-disable-next-line no-empty
          error
        ) {}
      }

      if (folderSourceId) {
        setDraggedFolderId(null);
        if (isInvalidFolderDrop(folderSourceId, folderId)) {
          setStatusMessage(
            'Cannot move a folder into itself or one of its descendants.',
            'error',
          );
          return;
        }
        await moveFolder(folderSourceId, folderId);
        return;
      }

      if (hasFiles(event)) {
        await handleFileDrop(event.dataTransfer, folderId);
        return;
      }

      let docIds = [];
      try {
        const raw = event.dataTransfer.getData('application/x-paperless-doc-list');
        if (raw) {
          const parsed = JSON.parse(raw);
          if (Array.isArray(parsed)) {
            docIds = parsed.filter(Boolean);
          }
        }
      } catch (
        // eslint-disable-next-line no-empty
        error
      ) {}

      if (!docIds.length) {
        try {
          const single = event.dataTransfer.getData('application/x-paperless-doc');
          if (single) {
            docIds = [single];
          }
        } catch (
          // eslint-disable-next-line no-empty
          error
        ) {}
      }

      if (!docIds.length && draggedDocumentIds.length) {
        docIds = draggedDocumentIds;
      }

      if (!docIds.length || folderId === selectedFolder) {
        return;
      }

      setDraggedDocumentIds([]);
      await moveDocumentsToFolder(docIds, folderId);
    },
    onDragOver: (event, folderId) => {
      const folderDragActive = Boolean(draggedFolderId);
      if (folderDragActive && isInvalidFolderDrop(draggedFolderId, folderId)) {
        return;
      }

      if (hasFiles(event)) {
        event.preventDefault();
        event.dataTransfer.dropEffect = 'copy';
        event.currentTarget.classList.add('is-drop-target');
        return;
      }

      if (draggedDocumentIds.length || folderDragActive) {
        event.preventDefault();
        event.dataTransfer.dropEffect = 'move';
        event.currentTarget.classList.add('is-drop-target');
      }
    },
    onDragLeave: (event) => {
      event.currentTarget.classList.remove('is-drop-target');
    },
  };

  const filterBar = (
    <FilterBar
      query={searchQuery}
      onQueryChange={setSearchQuery}
      tags={tags}
      activeTagIds={activeTagFilters}
      onToggleTag={(tagId) =>
        setActiveTagFilters((prev) =>
          prev.includes(tagId)
            ? prev.filter((id) => id !== tagId)
            : prev.concat([tagId]),
        )
      }
      onClear={() => {
        setSearchQuery('');
        setActiveTagFilters([]);
      }}
      hasFilters={isFilterActive}
    />
  );

  const defaultMoveTarget = useMemo(() => {
    return folderOptions.some((option) => option.id === selectedFolder)
      ? selectedFolder
      : 'root';
  }, [folderOptions, selectedFolder]);

  useEffect(() => {
    if (!focusedDocumentId) return;
    ensureDocumentDetail(focusedDocumentId, { force: false }).catch(() => {});
  }, [focusedDocumentId, ensureDocumentDetail]);

  const selectedDocument = useMemo(() => {
    if (!focusedDocumentId) {
      return null;
    }
    const list = searchResults ?? documents;
    return list.find((doc) => doc.id === focusedDocumentId) || null;
  }, [searchResults, documents, focusedDocumentId]);

  const selectedDetail = selectedDocument
    ? documentDetails.get(selectedDocument.id)
    : null;

  const documentLookup = useMemo(() => {
    const map = new Map();
    documents.forEach((doc) => {
      if (doc?.id) {
        map.set(doc.id, doc);
      }
    });
    if (Array.isArray(searchResults)) {
      searchResults.forEach((doc) => {
        if (doc?.id) {
          map.set(doc.id, doc);
        }
      });
    }
    return map;
  }, [documents, searchResults]);

  const orderedSelectedDocuments = useMemo(() => {
    const ordered = [];
    const seen = new Set();
    const pushDoc = (doc) => {
      if (doc?.id && !seen.has(doc.id)) {
        ordered.push(doc);
        seen.add(doc.id);
      }
    };

    selectionOrder.forEach((id) => {
      const doc = documentLookup.get(id) || documentDetails.get(id)?.document || null;
      pushDoc(doc);
    });

    selectedDocumentIds.forEach((id) => {
      if (seen.has(id)) return;
      const doc = documentLookup.get(id) || documentDetails.get(id)?.document || null;
      pushDoc(doc);
    });

    return ordered;
  }, [selectionOrder, documentLookup, selectedDocumentIds, documentDetails]);

  useEffect(() => {
    if (!selectedDocumentIds.length) return;
    const selectedSet = new Set(selectedDocumentIds);
    const targetIds = [];

    for (let index = selectionOrder.length - 1; index >= 0; index -= 1) {
      const id = selectionOrder[index];
      if (selectedSet.has(id) && !targetIds.includes(id)) {
        targetIds.push(id);
      }
      if (targetIds.length >= MAX_PREVIEW_STACK_ITEMS) {
        break;
      }
    }

    if (!targetIds.length) {
      for (let index = selectedDocumentIds.length - 1; index >= 0; index -= 1) {
        const id = selectedDocumentIds[index];
        if (!targetIds.includes(id)) {
          targetIds.push(id);
        }
        if (targetIds.length >= MAX_PREVIEW_STACK_ITEMS) {
          break;
        }
      }
    }

    targetIds.forEach((id) => {
      ensureDocumentDetail(id, { force: false }).catch(() => {});
    });
  }, [selectionOrder, selectedDocumentIds, ensureDocumentDetail]);

  const { breadcrumbs, missingBreadcrumbAncestors } = useMemo(() => {
    const chain = [];
    const seen = new Set();
    const pending = new Set();
    let currentId = selectedFolder || 'root';
    let guard = 0;

    while (currentId && !seen.has(currentId) && guard < 32) {
      guard += 1;
      seen.add(currentId);

      if (currentId === 'root') {
        chain.push({ id: 'root', name: DEFAULT_FOLDER_NAME });
        currentId = null;
        break;
      }

      const node = folderNodes.get(currentId);
      if (node) {
        chain.push({ id: currentId, name: node.name || 'Folder' });
        currentId = node.parentId ?? 'root';
        continue;
      }

      let fallbackName = '…';
      let parentId = null;

      if (currentFolder && currentFolder.id === currentId) {
        fallbackName = currentFolder.name;
        parentId = currentFolder.parent_id ?? 'root';
      }

      chain.push({ id: currentId, name: fallbackName });
      pending.add(currentId);
      currentId = parentId;
    }

    if (!chain.some((crumb) => crumb.id === 'root')) {
      chain.push({ id: 'root', name: DEFAULT_FOLDER_NAME });
    }

    const ordered = [];
    const seenOrdered = new Set();
    chain
      .slice()
      .reverse()
      .forEach((crumb) => {
        if (!seenOrdered.has(crumb.id)) {
          seenOrdered.add(crumb.id);
          ordered.push(crumb);
        }
      });

    return { breadcrumbs: ordered, missingBreadcrumbAncestors: Array.from(pending) };
  }, [selectedFolder, folderNodes, currentFolder]);

  useEffect(() => {
    if (!missingBreadcrumbAncestors.length) {
      return;
    }

    missingBreadcrumbAncestors.forEach((folderId) => {
      if (!folderId || folderId === 'root') {
        return;
      }
      if (breadcrumbFetchRef.current.has(folderId)) {
        return;
      }

      breadcrumbFetchRef.current.add(folderId);
      ensureFolderData(folderId, { force: false })
        .catch((error) => {
          console.warn('Failed to preload breadcrumb ancestor', folderId, error);
        })
        .finally(() => {
          breadcrumbFetchRef.current.delete(folderId);
        });
    });
  }, [missingBreadcrumbAncestors, ensureFolderData]);

  const selectedPreviewEntry = useMemo(() => {
    if (!selectedDocument) {
      return null;
    }
    return previewCacheRef.current.get(selectedDocument.id) || null;
  }, [selectedDocument, previewCacheTick]);

  const previewWorkspaceEntry = useMemo(() => {
    if (!previewDocumentId) {
      return null;
    }
    return previewCacheRef.current.get(previewDocumentId) || null;
  }, [previewDocumentId, previewCacheTick]);

  const previewWorkspaceDetail = previewDocumentId
    ? documentDetails.get(previewDocumentId)
    : null;

  const previewWorkspaceDocument = useMemo(() => {
    if (!previewDocumentId) return null;
    if (previewWorkspaceDetail?.document) return previewWorkspaceDetail.document;
    const pool = searchResults ?? documents;
    return pool.find((doc) => doc.id === previewDocumentId) || null;
  }, [previewDocumentId, previewWorkspaceDetail, searchResults, documents]);

  const previewActive = Boolean(previewDocumentId && previewWorkspaceDocument);

  return (
    <div className="app-shell">
      {token ? (
        <>
          <DropOverlay
            active={dropOverlayState.active}
            folderName={dropOverlayState.folderName}
          />
          <header className="app-bar">
            <div className="app-bar__main">
              <div className="app-bar__meta">
                <h1>Paperless-NEO</h1>
                <span className="app-bar__hint">
                  {previewActive
                    ? 'Viewing document preview. Press ← Back to return to the library.'
                    : 'Drag files here to upload.'}
                </span>
              </div>
              {status && (
                <div className="app-bar__status">
                  <StatusBanner status={status} />
                </div>
              )}
              <div className="app-bar__actions">
                <button
                  className="secondary"
                  type="button"
                  onClick={handleBulkReanalyze}
                >
                  Re-analyze All
                </button>
                <button className="secondary" onClick={handleLogout}>
                  Log out
                </button>
              </div>
            </div>
          </header>
          <main className={previewActive ? 'preview-main' : 'app-main'}>
            {previewActive && previewWorkspaceDocument ? (
              <PreviewWorkspace
                document={previewWorkspaceDocument}
                detail={previewWorkspaceDetail}
                previewEntry={previewWorkspaceEntry}
                onClose={closeDocumentPreview}
                onDownload={handleDownload}
                onRegenerateThumbnails={handleThumbnailRegeneration}
              />
            ) : (
              <>
                <Sidebar
                  folderNodes={folderNodes}
                  onToggle={folderClickHandlers.onToggle}
                  onSelect={folderClickHandlers.onSelect}
                  onDrop={folderClickHandlers.onDrop}
                  onDragOver={folderClickHandlers.onDragOver}
                  onDragLeave={folderClickHandlers.onDragLeave}
                  onCreateFolder={handleFolderCreate}
                  onDeleteFolder={handleFolderDelete}
                  selectedFolder={selectedFolder}
                  onFolderDragStart={handleFolderDragStart}
                  onFolderDragEnd={handleFolderDragEnd}
                  draggedFolderId={draggedFolderId}
                />
                <DocumentsTable
                  currentFolderName={currentFolderName}
                  breadcrumbs={breadcrumbs}
                  onRefresh={refreshCurrentFolder}
                  subfolders={currentSubfolders}
                  documents={documents}
                  searchResults={searchResults}
                  isFilterActive={isFilterActive}
                  onFolderSelect={selectFolder}
                  onFolderDrop={folderClickHandlers.onDrop}
                  onFolderDragOver={folderClickHandlers.onDragOver}
                  onFolderDragLeave={folderClickHandlers.onDragLeave}
                  onFolderDragStart={handleFolderDragStart}
                  onFolderDragEnd={handleFolderDragEnd}
                  draggedFolderId={draggedFolderId}
                  onFolderDelete={handleFolderDelete}
                  onDocumentRowClick={handleDocumentRowClick}
                  onDocumentOpen={openDocumentPreview}
                  selectedDocumentIds={selectedDocumentIds}
                  focusedDocumentId={focusedDocumentId}
                  draggingDocumentIds={draggedDocumentIds}
                  onDocumentDragStart={(event, documentId) => {
                const selection = selectedDocumentIds.includes(documentId)
                  ? selectedDocumentIds
                  : [documentId];
                if (!selectedDocumentIds.includes(documentId)) {
                  applySelection([documentId], {
                    anchor: documentId,
                    interactedIds: [documentId],
                  });
                }
                setDraggedDocumentIds(selection);
                event.dataTransfer.effectAllowed = 'move';
                try {
                  event.dataTransfer.setData(
                    'application/x-paperless-doc-list',
                    JSON.stringify(selection),
                  );
                } catch (
                  // eslint-disable-next-line no-empty
                  error
                ) {}
                event.currentTarget.classList.add('dragging');
              }}
              onDocumentDragEnd={(event) => {
                setDraggedDocumentIds([]);
                event.currentTarget.classList.remove('dragging');
              }}
              onDownload={handleDownload}
              filterBar={filterBar}
            />
            <DetailPanel
              selectedDocuments={orderedSelectedDocuments}
              detailMap={documentDetails}
              tags={tags}
              onDownload={handleDownload}
              onTagAdd={handleTagAdd}
              onTagRemove={handleTagRemove}
              onRegenerateThumbnails={handleThumbnailRegeneration}
              previewEntry={selectedPreviewEntry}
              onOpenPreview={openDocumentPreview}
              onBulkTagAdd={handleBulkTagAddFromDetail}
              onBulkTagRemove={handleBulkTagRemoveFromDetail}
              onBulkMove={handleBulkMoveFromDetail}
              onBulkReanalyze={handleBulkSelectionReanalyze}
              folderOptions={folderOptions}
              defaultMoveTarget={defaultMoveTarget}
              onPromoteSelection={promoteSelectionOrder}
              activePreviewId={activePreviewId}
            />
              </>
            )}
          </main>
        </>
      ) : (
        <LoginView onSubmit={handleLogin} status={status} />
      )}
    </div>
  );
}

const RoutedApp = () => {
  const navigate = useNavigate();
  const params = useParams();
  const folderId = params.folderId ?? null;
  const documentId = params.documentId ?? null;

  return (
    <App
      routeFolderId={folderId}
      routeDocumentId={documentId}
      navigate={navigate}
    />
  );
};

const AppRouter = () => (
  <Routes>
    <Route path="/" element={<Navigate to="/folders" replace />} />
    <Route path="/folders" element={<RoutedApp />} />
    <Route path="/folders/:folderId" element={<RoutedApp />} />
    <Route path="/documents/:documentId" element={<RoutedApp />} />
    <Route path="/folders/:folderId/documents/:documentId" element={<RoutedApp />} />
    <Route path="*" element={<Navigate to="/folders" replace />} />
  </Routes>
);

const container = document.getElementById('app');
const root = createRoot(container);
root.render(
  <HashRouter hashType="hashbang">
    <AppRouter />
  </HashRouter>,
);
