import React, { useCallback, useEffect, useState } from 'react';
import Layout from './components/Layout';
import FileLoader from './components/FileLoader';
import ReorderPanel from './components/ReorderPanel';
import ActionsPanel from './components/ActionsPanel';
import PagePreviewModal from './components/PagePreviewModal';
import WorkspacePanel from './components/WorkspacePanel';
import ActionDialog, {
  type ActionDialogAction,
} from './components/ActionDialog';
import HelpDialog from './components/HelpDialog';
import { PDFDocument } from 'pdf-lib';
import type {
  StoredWorkspace,
  WorkspaceSummary,
} from './workspace/workspaceTypes';
import {
  createInitialPageRefs,
  createPageRefId,
  createPdfId,
  createWorkspaceId,
  defaultWorkspaceNameFromPdfName,
  normalizeRotation,
  useWorkspaceState,
} from './workspace/useWorkspaceState';
import {
  deleteWorkspaceFromIndexedDb,
  listWorkspaces,
  loadWorkspaceFromIndexedDb,
  saveWorkspaceToIndexedDb,
} from './workspace/workspaceDb';
import type { PageRef, PdfFile } from './pdf/pdfTypes';
import {
  loadPdfFromFile,
  mergePdfFiles,
  splitIntoSinglePages,
  exportPages,
} from './pdf/pdfService';
import { usePdfThumbnails } from './pdf/usePdfThumbnails';
import { usePdfGeneratedOutputs } from './hooks/usePdfGeneratedOutputs';
import {
  createSplitResultsZip,
  createSplitZipFilename,
} from './pdf/pdfZipService';
import {
  createSelectionPdfName,
  createSelectionWorkspaceName,
  getSelectedPagesInVisualOrder,
} from './workspace/workspaceSelection';

function isEditableKeyboardTarget(target: EventTarget | null): boolean {
  if (!(target instanceof HTMLElement)) return false;

  const tagName = target.tagName.toLowerCase();
  return (
    target.isContentEditable ||
    tagName === 'input' ||
    tagName === 'textarea' ||
    tagName === 'select'
  );
}

const App: React.FC = () => {
  const [actionDialog, setActionDialog] = useState<{
    title: string;
    content: React.ReactNode;
    actions: ActionDialogAction[];
  } | null>(null);
  const [helpOpen, setHelpOpen] = useState(false);

  const [pdf, setPdf] = useState<PdfFile | null>(null);
  const [isBusy, setIsBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const [workspaces, setWorkspaces] = useState<WorkspaceSummary[]>([]);
  const [activeWorkspaceId, setActiveWorkspaceId] = useState<string | null>(
    null
  );
  const [workspaceName, setWorkspaceName] = useState('');

  const [previewPageId, setPreviewPageId] = useState<string | null>(null);

  const [pendingFile, setPendingFile] = useState<File | null>(null);
  const [showMergeOptions, setShowMergeOptions] = useState(false);
  const [mergeMode, setMergeMode] = useState<
    'overwrite' | 'append' | 'insertAt'
  >('append');
  const [mergeInsertAt, setMergeInsertAt] = useState<string>('');

  const {
    splitDownloads,
    splitZipDownload,
    subsetDownload,
    exportDownload,
    replaceSplitResults,
    replaceSubsetResult,
    replaceExportResult,
    clearAllResults: clearGeneratedOutputs,
  } = usePdfGeneratedOutputs();

  const handleWorkspaceContentChanged = useCallback(() => {
    clearGeneratedOutputs();
  }, [clearGeneratedOutputs]);

  const {
    pages,
    selectedPageIds,
    setSelectedPageIds,
    lastSelectedVisualIndex,
    setLastSelectedVisualIndex,
    workspaceDirty,
    setWorkspaceDirty,
    workspaceMessage,
    setWorkspaceMessage,
    workspaceHistory,
    redoHistory,
    getCurrentCommandState,
    createWorkspaceCommand,
    executeWorkspaceCommand,
    handleUndo,
    handleRedo,
    replaceWorkspaceState,
    resetWorkspaceState: resetWorkspaceCommandState,
  } = useWorkspaceState({ onContentChanged: handleWorkspaceContentChanged });

  const handleThumbnailError = useCallback(
    (message: string, thrown: unknown) => {
      console.error(thrown);
      setError(message);
    },
    []
  );

  const { thumbnails: reorderThumbnails, clearThumbnailCache } =
    usePdfThumbnails({
      pdf,
      pages,
      onError: handleThumbnailError,
    });

  const closeActionDialog = useCallback(() => {
    setActionDialog(null);
  }, []);

  const openActionDialog = useCallback(
    (dialog: {
      title: string;
      content: React.ReactNode;
      actions: ActionDialogAction[];
    }) => {
      setActionDialog(dialog);
    },
    []
  );

  const refreshWorkspaces = async () => {
    try {
      const summaries = await listWorkspaces();
      setWorkspaces(summaries);
    } catch (e) {
      console.error(e);
      setError('Failed to read saved workspaces from browser storage.');
    }
  };

  useEffect(() => {
    void refreshWorkspaces();
  }, []);

  const resetWorkspaceState = () => {
    setPdf(null);
    setActiveWorkspaceId(null);
    setWorkspaceName('');
    resetWorkspaceCommandState();
    clearGeneratedOutputs();
    clearThumbnailCache();
    setPreviewPageId(null);
  };

  const handleSaveWorkspace = async (): Promise<boolean> => {
    if (!pdf || pages.length === 0) return false;

    setError(null);

    const now = new Date().toISOString();
    const name =
      workspaceName.trim() || defaultWorkspaceNameFromPdfName(pdf.name);
    const workspaceId = activeWorkspaceId ?? createWorkspaceId();

    const existing = workspaces.find(
      (workspace) => workspace.id === workspaceId
    );

    const workspace: StoredWorkspace = {
      schemaVersion: 1,
      id: workspaceId,
      name,
      createdAt: existing?.createdAt ?? now,
      updatedAt: now,

      pdfId: pdf.id,
      pdfName: pdf.name,
      sourcePageCount: pdf.pageCount,

      pages,
      selectedPageIds,
      history: workspaceHistory,
      redoHistory,
    };

    setIsBusy(true);

    try {
      await saveWorkspaceToIndexedDb({
        workspace,
        pdfArrayBuffer: pdf.arrayBuffer,
      });

      setActiveWorkspaceId(workspaceId);
      setWorkspaceName(name);
      setWorkspaceDirty(false);
      setWorkspaceMessage(`Workspace "${name}" saved.`);
      await refreshWorkspaces();

      return true;
    } catch (e) {
      console.error(e);
      setError(
        'Failed to save workspace. The browser storage quota may be full.'
      );
      return false;
    } finally {
      setIsBusy(false);
    }
  };

  const performResetWorkspace = () => {
    resetWorkspaceState();
  };

  const handleResetWorkspace = () => {
    if (!pdf) return;

    if (!workspaceDirty) {
      performResetWorkspace();
      return;
    }

    openActionDialog({
      title: 'Reset workspace?',
      content: (
        <>
          <p style={{ marginTop: 0 }}>This workspace has unsaved changes.</p>
          <p style={{ marginBottom: 0 }}>
            Do you want to save it before resetting?
          </p>
        </>
      ),
      actions: [
        {
          label: 'Cancel',
          variant: 'secondary',
          onClick: closeActionDialog,
        },
        {
          label: 'Reset without saving',
          variant: 'danger',
          onClick: () => {
            closeActionDialog();
            performResetWorkspace();
          },
        },
        {
          label: 'Save and reset',
          variant: 'primary',
          autoFocus: true,
          onClick: async () => {
            closeActionDialog();
            const saved = await handleSaveWorkspace();
            if (saved) {
              performResetWorkspace();
            }
          },
        },
      ],
    });
  };

  const handleLoadWorkspace = async (workspaceId: string) => {
    setError(null);
    setIsBusy(true);

    try {
      const loaded = await loadWorkspaceFromIndexedDb(workspaceId);

      if (!loaded) {
        setError('Workspace not found.');
        await refreshWorkspaces();
        return;
      }

      clearGeneratedOutputs();

      const doc = await PDFDocument.load(loaded.pdfArrayBuffer);

      const loadedPdf: PdfFile = {
        id: loaded.workspace.pdfId,
        name: loaded.workspace.pdfName,
        pageCount: doc.getPageCount(),
        arrayBuffer: loaded.pdfArrayBuffer,
        doc,
      };

      setPdf(loadedPdf);
      replaceWorkspaceState({
        pages: loaded.workspace.pages,
        selectedPageIds: loaded.workspace.selectedPageIds ?? [],
        lastSelectedVisualIndex: null,
        history: loaded.workspace.history ?? [],
        redoHistory: loaded.workspace.redoHistory ?? [],
        dirty: false,
        message: `Workspace "${loaded.workspace.name}" loaded.`,
      });
      setPreviewPageId(null);

      clearThumbnailCache();

      setActiveWorkspaceId(loaded.workspace.id);
      setWorkspaceName(loaded.workspace.name);
    } catch (e) {
      console.error(e);
      setError('Failed to load workspace from browser storage.');
    } finally {
      setIsBusy(false);
    }
  };

  const handleDeleteWorkspace = (workspaceId: string) => {
    const workspace = workspaces.find((item) => item.id === workspaceId);
    const name = workspace?.name ?? 'this workspace';

    openActionDialog({
      title: 'Delete workspace?',
      content: (
        <>
          <p style={{ marginTop: 0 }}>
            Delete the saved workspace <strong>{name}</strong> from this
            browser?
          </p>
          <p style={{ marginBottom: 0 }}>
            The currently open in-memory document will not be closed, but the
            saved workspace entry will be removed.
          </p>
        </>
      ),
      actions: [
        {
          label: 'Cancel',
          variant: 'secondary',
          onClick: closeActionDialog,
        },
        {
          label: 'Delete workspace',
          variant: 'danger',
          autoFocus: true,
          onClick: () => {
            closeActionDialog();
            void performDeleteWorkspace(workspaceId);
          },
        },
      ],
    });
  };

  const performDeleteWorkspace = async (workspaceId: string) => {
    setError(null);

    try {
      await deleteWorkspaceFromIndexedDb(workspaceId);

      if (activeWorkspaceId === workspaceId) {
        setActiveWorkspaceId(null);
        setWorkspaceDirty(true);
        setWorkspaceMessage(
          'Saved workspace deleted. Current in-memory document remains open.'
        );
      }

      await refreshWorkspaces();
    } catch (e) {
      console.error(e);
      setError('Failed to delete workspace.');
    }
  };

  const loadFileAsNew = async (file: File) => {
    setError(null);
    resetWorkspaceState();

    setIsBusy(true);
    try {
      const loaded = await loadPdfFromFile(file);
      const initialPages = createInitialPageRefs(loaded.pageCount);

      setPdf(loaded);
      replaceWorkspaceState({
        pages: initialPages,
        selectedPageIds: [],
        lastSelectedVisualIndex: null,
        history: [],
        redoHistory: [],
        dirty: true,
        message: null,
      });

      setWorkspaceName(defaultWorkspaceNameFromPdfName(loaded.name));
    } catch (e) {
      console.error(e);
      setError('Failed to load PDF (see console).');
    } finally {
      setIsBusy(false);
    }
  };

  const handleFileLoaded = (file: File) => {
    if (!pdf || pages.length === 0) {
      void loadFileAsNew(file);
    } else {
      setPendingFile(file);
      setShowMergeOptions(true);
      setMergeMode('append');
      setMergeInsertAt(String(pages.length + 1));
    }
  };

  const handleMergeCancel = () => {
    setPendingFile(null);
    setShowMergeOptions(false);
  };

  const handleMergeConfirm = async () => {
    if (!pendingFile) return;

    if (!pdf || mergeMode === 'overwrite') {
      await loadFileAsNew(pendingFile);
      setPendingFile(null);
      setShowMergeOptions(false);
      return;
    }

    setError(null);
    setIsBusy(true);

    try {
      // 1) Materialize the current in-memory workspace (page refs + rotations)
      const currentBlob = await exportPages(pdf, pages);
      const currentArrayBuffer = await currentBlob.arrayBuffer();
      const currentDoc = await PDFDocument.load(currentArrayBuffer);
      const currentPdf: PdfFile = {
        id: pdf.id,
        name: pdf.name,
        doc: currentDoc,
        arrayBuffer: currentArrayBuffer,
        pageCount: pages.length,
      };

      // 2) Load the new PDF
      const newPdf = await loadPdfFromFile(pendingFile);

      // 3) Determine insert position (0-based)
      let insertAt = pages.length; // default: append at end
      if (mergeMode === 'insertAt') {
        const parsed = parseInt(mergeInsertAt, 10);
        if (Number.isFinite(parsed)) {
          insertAt = Math.min(Math.max(parsed - 1, 0), pages.length);
        }
      } else if (mergeMode === 'append') {
        insertAt = pages.length;
      }

      // 4) Merge
      const mergedPdf = await mergePdfFiles(currentPdf, newPdf, insertAt);
      const mergedPages = createInitialPageRefs(mergedPdf.pageCount);

      // 5) Reset state to the merged document
      setPdf(mergedPdf);
      replaceWorkspaceState({
        pages: mergedPages,
        selectedPageIds: [],
        lastSelectedVisualIndex: null,
        history: [],
        redoHistory: [],
        dirty: true,
        message: null,
      });
      clearGeneratedOutputs();
      clearThumbnailCache();
      setPreviewPageId(null);
      setWorkspaceName(defaultWorkspaceNameFromPdfName(mergedPdf.name));
      setActiveWorkspaceId(null);
    } catch (e) {
      console.error(e);
      setError('Failed to merge PDF (see console).');
    } finally {
      setIsBusy(false);
      setPendingFile(null);
      setShowMergeOptions(false);
    }
  };

  useEffect(() => {
    if (
      previewPageId != null &&
      !pages.some((page) => page.id === previewPageId)
    ) {
      setPreviewPageId(null);
    }
  }, [previewPageId, pages]);

  const hasPdf = !!pdf;

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (isEditableKeyboardTarget(e.target)) return;

      if (e.key === 'F1' || e.key === '?') {
        e.preventDefault();
        setHelpOpen(true);
      }
    };

    window.addEventListener('keydown', handleKeyDown);

    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, []);

  // === UI interactions ===
  const handleRotatePageClockwise = (pageId: string) => {
    const before = getCurrentCommandState();
    const afterPages = pages.map((page) =>
      page.id === pageId
        ? { ...page, rotation: (normalizeRotation(page.rotation) + 90) % 360 }
        : page
    );

    executeWorkspaceCommand(
      createWorkspaceCommand({
        type: 'page.rotate',
        label: 'Rotated page clockwise',
        before,
        after: {
          ...before,
          pages: afterPages,
        },
        details: {
          pageId,
          degrees: 90,
        },
      })
    );
  };

  const handleRotatePageCounterclockwise = (pageId: string) => {
    const before = getCurrentCommandState();
    const afterPages = pages.map((page) =>
      page.id === pageId
        ? { ...page, rotation: (normalizeRotation(page.rotation) + 270) % 360 }
        : page
    );

    executeWorkspaceCommand(
      createWorkspaceCommand({
        type: 'page.rotate',
        label: 'Rotated page counterclockwise',
        before,
        after: {
          ...before,
          pages: afterPages,
        },
        details: {
          pageId,
          degrees: -90,
        },
      })
    );
  };

  const handleDeletePage = (pageId: string) => {
    const page = pages.find((item) => item.id === pageId);
    const visualIndex = page ? pages.indexOf(page) : -1;
    const pageLabel =
      visualIndex >= 0 ? `page at position ${visualIndex + 1}` : 'this page';

    openActionDialog({
      title: 'Delete page?',
      content: (
        <p style={{ margin: 0 }}>
          Delete <strong>{pageLabel}</strong> from the current workspace?
        </p>
      ),
      actions: [
        {
          label: 'Cancel',
          variant: 'secondary',
          onClick: closeActionDialog,
        },
        {
          label: 'Delete page',
          variant: 'danger',
          autoFocus: true,
          onClick: () => {
            closeActionDialog();
            performDeletePage(pageId);
          },
        },
      ],
    });
  };

  const performDeletePage = (pageId: string) => {
    const before = getCurrentCommandState();

    executeWorkspaceCommand(
      createWorkspaceCommand({
        type: 'page.delete',
        label: 'Deleted page',
        before,
        after: {
          pages: pages.filter((page) => page.id !== pageId),
          selectedPageIds: selectedPageIds.filter((id) => id !== pageId),
          lastSelectedVisualIndex: null,
        },
        details: {
          pageId,
        },
      })
    );
  };

  const handleReorder = (newPages: PageRef[]) => {
    const before = getCurrentCommandState();

    executeWorkspaceCommand(
      createWorkspaceCommand({
        type: 'pages.reorder',
        label: 'Reordered pages',
        before,
        after: {
          ...before,
          pages: newPages,
        },
        details: {
          pageCount: newPages.length,
        },
      })
    );
  };

  const handleToggleSelect = (
    pageId: string,
    visualIndex: number,
    e: React.MouseEvent<HTMLButtonElement>
  ) => {
    setSelectedPageIds((prev) => {
      if (e.shiftKey && lastSelectedVisualIndex !== null && pages.length > 0) {
        const from = Math.min(lastSelectedVisualIndex, visualIndex);
        const to = Math.max(lastSelectedVisualIndex, visualIndex);
        const rangeIds = pages.slice(from, to + 1).map((page) => page.id);

        const set = new Set(prev);
        rangeIds.forEach((id) => set.add(id));
        return Array.from(set);
      }

      if (prev.includes(pageId)) {
        return prev.filter((id) => id !== pageId);
      }

      return [...prev, pageId];
    });

    setLastSelectedVisualIndex(visualIndex);
  };

  const handleSelectAll = () => {
    setSelectedPageIds(pages.map((page) => page.id));
    setLastSelectedVisualIndex(null);
  };

  const handleClearSelection = () => {
    setSelectedPageIds([]);
    setLastSelectedVisualIndex(null);
  };

  const performDeleteSelected = useCallback(
    (pageIdsToDelete: string[]) => {
      if (pageIdsToDelete.length === 0) return;

      const before = getCurrentCommandState();
      const selectedSet = new Set(pageIdsToDelete);

      executeWorkspaceCommand(
        createWorkspaceCommand({
          type: 'pages.delete',
          label:
            pageIdsToDelete.length === 1
              ? 'Deleted selected page'
              : `Deleted ${pageIdsToDelete.length} selected pages`,
          before,
          after: {
            pages: pages.filter((page) => !selectedSet.has(page.id)),
            selectedPageIds: [],
            lastSelectedVisualIndex: null,
          },
          details: {
            count: pageIdsToDelete.length,
          },
        })
      );
    },
    [
      createWorkspaceCommand,
      executeWorkspaceCommand,
      getCurrentCommandState,
      pages,
    ]
  );

  const handleDeleteSelected = useCallback(() => {
    if (selectedPageIds.length === 0) return;

    const idsToDelete = [...selectedPageIds];

    openActionDialog({
      title:
        idsToDelete.length === 1
          ? 'Delete selected page?'
          : 'Delete selected pages?',
      content: (
        <p style={{ margin: 0 }}>
          Delete{' '}
          <strong>
            {idsToDelete.length === 1
              ? '1 selected page'
              : `${idsToDelete.length} selected pages`}
          </strong>{' '}
          from the current workspace?
        </p>
      ),
      actions: [
        {
          label: 'Cancel',
          variant: 'secondary',
          onClick: closeActionDialog,
        },
        {
          label: idsToDelete.length === 1 ? 'Delete page' : 'Delete pages',
          variant: 'danger',
          autoFocus: true,
          onClick: () => {
            closeActionDialog();
            performDeleteSelected(idsToDelete);
          },
        },
      ],
    });
  }, [
    closeActionDialog,
    openActionDialog,
    performDeleteSelected,
    selectedPageIds,
  ]);

  const handleCopyPagesToSlot = (pageIds: string[], insertSlot: number) => {
    if (!pdf || pageIds.length === 0) return;

    const pageIdSet = new Set(pageIds);

    // Copy in current visual order, not in arbitrary selectedPageIds order.
    const sourcePages = pages.filter((page) => pageIdSet.has(page.id));

    if (sourcePages.length === 0) return;

    const copiedPages: PageRef[] = sourcePages.map((page) => ({
      ...page,
      id: createPageRefId(),
    }));

    const clampedSlot = Math.min(Math.max(insertSlot, 0), pages.length);

    const afterPages = [
      ...pages.slice(0, clampedSlot),
      ...copiedPages,
      ...pages.slice(clampedSlot),
    ];

    const before = getCurrentCommandState();

    executeWorkspaceCommand(
      createWorkspaceCommand({
        type: 'pages.copy',
        label:
          copiedPages.length === 1
            ? 'Copied page'
            : `Copied ${copiedPages.length} pages`,
        before,
        after: {
          pages: afterPages,
          selectedPageIds: copiedPages.map((page) => page.id),
          lastSelectedVisualIndex: null,
        },
        details: {
          count: copiedPages.length,
          insertSlot: clampedSlot,
        },
      })
    );
  };

  const handleOpenPreview = (pageId: string) => {
    setPreviewPageId(pageId);
  };

  const handleClosePreview = () => {
    setPreviewPageId(null);
  };

  const handlePreviewPrevious = () => {
    setPreviewPageId((current) => {
      if (current == null || pages.length === 0) return current;

      const visualIndex = pages.findIndex((page) => page.id === current);
      if (visualIndex <= 0) return current;

      return pages[visualIndex - 1].id;
    });
  };

  const handlePreviewNext = () => {
    setPreviewPageId((current) => {
      if (current == null || pages.length === 0) return current;

      const visualIndex = pages.findIndex((page) => page.id === current);
      if (visualIndex < 0 || visualIndex >= pages.length - 1) return current;

      return pages[visualIndex + 1].id;
    });
  };

  useEffect(() => {
    if (!hasPdf || previewPageId !== null) return;

    const handleKeyDown = (e: KeyboardEvent) => {
      if (isEditableKeyboardTarget(e.target)) return;

      const key = e.key.toLowerCase();

      if ((e.ctrlKey || e.metaKey) && key === 'z') {
        e.preventDefault();
        if (e.shiftKey) {
          handleRedo();
        } else {
          handleUndo();
        }
        return;
      }

      if ((e.ctrlKey || e.metaKey) && key === 'y') {
        e.preventDefault();
        handleRedo();
        return;
      }

      if ((e.ctrlKey || e.metaKey) && key === 'a') {
        e.preventDefault();
        setSelectedPageIds(pages.map((page) => page.id));
        setLastSelectedVisualIndex(null);
        return;
      }

      if (
        (e.key === 'Delete' || e.key === 'Backspace') &&
        selectedPageIds.length > 0
      ) {
        e.preventDefault();
        handleDeleteSelected();
        return;
      }

      if (e.key === 'Escape' && selectedPageIds.length > 0) {
        e.preventDefault();
        setSelectedPageIds([]);
        setLastSelectedVisualIndex(null);
      }
    };

    window.addEventListener('keydown', handleKeyDown);

    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [
    hasPdf,
    previewPageId,
    pages,
    selectedPageIds,
    workspaceHistory,
    redoHistory,
    handleUndo,
    handleRedo,
    handleDeleteSelected,
    setSelectedPageIds,
    setLastSelectedVisualIndex,
  ]);

  const handleSplit = async () => {
    if (!pdf) return;
    setError(null);
    setIsBusy(true);
    try {
      const result = await splitIntoSinglePages(pdf);
      const zipBlob = await createSplitResultsZip(result);
      replaceSplitResults(result, {
        blob: zipBlob,
        filename: createSplitZipFilename(pdf.name),
      });
    } catch (e) {
      console.error(e);
      setError('Error while splitting PDF (see console).');
    } finally {
      setIsBusy(false);
    }
  };

  const handleExtractSelected = async () => {
    if (!pdf || selectedPageIds.length === 0) return;
    setError(null);
    setIsBusy(true);

    try {
      const selectedPages = getSelectedPagesInVisualOrder(
        pages,
        selectedPageIds
      );

      if (selectedPages.length === 0) return;

      const blob = await exportPages(pdf, selectedPages);
      const base = pdf.name.replace(/\.pdf$/i, '');
      const filename = `${base}_selected.pdf`;
      replaceSubsetResult(blob, filename);
    } catch (e) {
      console.error(e);
      setError('Error while extracting selected pages (see console).');
    } finally {
      setIsBusy(false);
    }
  };

  const performOpenSelectionAsWorkspace = async () => {
    if (!pdf || selectedPageIds.length === 0) return;

    const selectedPages = getSelectedPagesInVisualOrder(pages, selectedPageIds);
    if (selectedPages.length === 0) return;

    setError(null);
    setIsBusy(true);

    try {
      const selectedPageCount = selectedPages.length;
      const blob = await exportPages(pdf, selectedPages);
      const arrayBuffer = await blob.arrayBuffer();
      const doc = await PDFDocument.load(arrayBuffer);
      const pdfName = createSelectionPdfName(pdf.name, selectedPageCount);
      const workspaceName = createSelectionWorkspaceName(
        pdf.name,
        selectedPageCount
      );

      const extractedPdf: PdfFile = {
        id: createPdfId(),
        name: pdfName,
        doc,
        pageCount: doc.getPageCount(),
        arrayBuffer,
      };

      setPdf(extractedPdf);
      replaceWorkspaceState({
        pages: createInitialPageRefs(extractedPdf.pageCount),
        selectedPageIds: [],
        lastSelectedVisualIndex: null,
        history: [],
        redoHistory: [],
        dirty: true,
        message: `Created a new workspace from ${selectedPageCount} selected ${
          selectedPageCount === 1 ? 'page' : 'pages'
        }.`,
      });

      setActiveWorkspaceId(null);
      setWorkspaceName(workspaceName);
      setPreviewPageId(null);
      clearGeneratedOutputs();
      clearThumbnailCache();
    } catch (e) {
      console.error(e);
      setError('Error while opening selection as a new workspace.');
    } finally {
      setIsBusy(false);
    }
  };

  const handleOpenSelectionAsWorkspace = () => {
    if (!pdf || selectedPageIds.length === 0) return;

    const selectedPages = getSelectedPagesInVisualOrder(pages, selectedPageIds);
    if (selectedPages.length === 0) return;

    if (!workspaceDirty) {
      void performOpenSelectionAsWorkspace();
      return;
    }

    openActionDialog({
      title: 'Open selection as new workspace?',
      content: (
        <>
          <p style={{ marginTop: 0 }}>
            This will replace the current in-memory workspace with a new
            workspace built from {selectedPages.length}{' '}
            {selectedPages.length === 1 ? 'selected page' : 'selected pages'}.
          </p>
          <p style={{ marginBottom: 0 }}>
            The current workspace has unsaved changes. Do you want to save it
            before opening the selection as a new workspace?
          </p>
        </>
      ),
      actions: [
        {
          label: 'Cancel',
          variant: 'secondary',
          onClick: closeActionDialog,
        },
        {
          label: 'Open without saving',
          variant: 'danger',
          onClick: () => {
            closeActionDialog();
            void performOpenSelectionAsWorkspace();
          },
        },
        {
          label: 'Save and open',
          variant: 'primary',
          autoFocus: true,
          onClick: async () => {
            closeActionDialog();
            const saved = await handleSaveWorkspace();
            if (saved) {
              await performOpenSelectionAsWorkspace();
            }
          },
        },
      ],
    });
  };

  const handleExportReordered = async () => {
    if (!pdf || pages.length === 0) return;
    setError(null);
    setIsBusy(true);

    try {
      const blob = await exportPages(pdf, pages);
      const base = pdf.name.replace(/\.pdf$/i, '');
      const filename = `${base}_reordered.pdf`;
      replaceExportResult(blob, filename);
    } catch (e) {
      console.error(e);
      setError('Error while exporting reordered PDF (see console).');
    } finally {
      setIsBusy(false);
    }
  };

  const previewVisualIndex =
    previewPageId != null
      ? pages.findIndex((page) => page.id === previewPageId)
      : -1;
  const previewPage =
    previewVisualIndex >= 0 ? pages[previewVisualIndex] : null;

  const canPreviewPrevious = previewVisualIndex > 0;
  const canPreviewNext =
    previewVisualIndex >= 0 && previewVisualIndex < pages.length - 1;

  return (
    <Layout onOpenHelp={() => setHelpOpen(true)}>
      <FileLoader pdf={pdf} onFileLoaded={handleFileLoaded} />

      <WorkspacePanel
        hasPdf={hasPdf}
        isBusy={isBusy}
        activeWorkspaceId={activeWorkspaceId}
        workspaceName={workspaceName}
        workspaceDirty={workspaceDirty}
        workspaceMessage={workspaceMessage}
        workspaces={workspaces}
        history={workspaceHistory}
        redoHistory={redoHistory}
        onWorkspaceNameChange={(value) => {
          setWorkspaceName(value);
          setWorkspaceDirty(true);
        }}
        onSaveWorkspace={handleSaveWorkspace}
        onLoadWorkspace={handleLoadWorkspace}
        onDeleteWorkspace={handleDeleteWorkspace}
        onRefreshWorkspaces={refreshWorkspaces}
        onResetWorkspace={handleResetWorkspace}
        onUndo={handleUndo}
        onRedo={handleRedo}
      />

      {showMergeOptions && pendingFile && pdf && pages.length > 0 && (
        <div
          className="card"
          style={{ border: '1px solid #bfdbfe', background: '#eff6ff' }}
        >
          <h2>Open file: merge or replace?</h2>
          <p style={{ fontSize: '0.85rem', color: '#374151' }}>
            You already have <strong>{pdf.name}</strong> with {pages.length}{' '}
            pages open. What should happen with{' '}
            <strong>{pendingFile.name}</strong>?
          </p>

          <div
            style={{
              display: 'flex',
              flexDirection: 'column',
              gap: '0.5rem',
              marginTop: '0.5rem',
              fontSize: '0.9rem',
            }}
          >
            <label
              style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}
            >
              <input
                type="radio"
                name="mergeMode"
                value="overwrite"
                checked={mergeMode === 'overwrite'}
                onChange={() => setMergeMode('overwrite')}
              />
              <span>Replace current document</span>
            </label>

            <label
              style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}
            >
              <input
                type="radio"
                name="mergeMode"
                value="append"
                checked={mergeMode === 'append'}
                onChange={() => setMergeMode('append')}
              />
              <span>Merge and append pages at the end</span>
            </label>

            <label
              style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}
            >
              <input
                type="radio"
                name="mergeMode"
                value="insertAt"
                checked={mergeMode === 'insertAt'}
                onChange={() => setMergeMode('insertAt')}
              />
              <span>
                Merge and insert starting at position{' '}
                <input
                  type="number"
                  min={1}
                  max={pages.length + 1}
                  value={mergeInsertAt}
                  onChange={(e) => setMergeInsertAt(e.target.value)}
                  style={{
                    width: '4rem',
                    padding: '0.15rem 0.3rem',
                    fontSize: '0.85rem',
                  }}
                />{' '}
                <span style={{ color: '#6b7280' }}>
                  (1 = before first page, {pages.length + 1} = after last page)
                </span>
              </span>
            </label>
          </div>

          <div className="button-row" style={{ marginTop: '0.75rem' }}>
            <button
              className="secondary"
              type="button"
              onClick={handleMergeCancel}
              disabled={isBusy}
            >
              Cancel
            </button>
            <button
              className="primary"
              type="button"
              onClick={handleMergeConfirm}
              disabled={isBusy}
            >
              {isBusy ? 'Working…' : 'Continue'}
            </button>
          </div>
        </div>
      )}

      <ReorderPanel
        pages={pages}
        thumbnails={reorderThumbnails}
        isBusy={isBusy}
        hasPdf={hasPdf}
        selectedPageIds={selectedPageIds}
        onRotateClockwise={handleRotatePageClockwise}
        onRotateCounterclockwise={handleRotatePageCounterclockwise}
        onDelete={handleDeletePage}
        onReorder={handleReorder}
        onCopyPagesToSlot={handleCopyPagesToSlot}
        onToggleSelect={handleToggleSelect}
        onSelectAll={handleSelectAll}
        onOpenPreview={handleOpenPreview}
        onClearSelection={handleClearSelection}
        onDeleteSelected={handleDeleteSelected}
      />

      <ActionsPanel
        hasPdf={hasPdf}
        isBusy={isBusy}
        selectedCount={selectedPageIds.length}
        onSplit={handleSplit}
        onExtractSelected={handleExtractSelected}
        onOpenSelectionAsWorkspace={handleOpenSelectionAsWorkspace}
        onExportReordered={handleExportReordered}
        splitDownloads={splitDownloads}
        splitZipDownload={splitZipDownload}
        subsetDownload={subsetDownload}
        exportDownload={exportDownload}
      />

      {error && (
        <div
          className="card"
          style={{ border: '1px solid #fecaca', background: '#fef2f2' }}
        >
          <strong>Error:</strong> {error}
        </div>
      )}

      <PagePreviewModal
        isOpen={previewPage !== null}
        pdf={pdf}
        pageIndex={previewPage?.sourcePageIndex ?? null}
        rotation={previewPage?.rotation ?? 0}
        visualIndex={previewVisualIndex >= 0 ? previewVisualIndex : null}
        totalPages={pages.length}
        canGoPrevious={canPreviewPrevious}
        canGoNext={canPreviewNext}
        onPrevious={handlePreviewPrevious}
        onNext={handlePreviewNext}
        onClose={handleClosePreview}
      />

      <ActionDialog
        open={actionDialog !== null}
        title={actionDialog?.title ?? ''}
        actions={actionDialog?.actions ?? []}
        onClose={closeActionDialog}
      >
        {actionDialog?.content}
      </ActionDialog>

      <HelpDialog open={helpOpen} onClose={() => setHelpOpen(false)} />
    </Layout>
  );
};

export default App;
