import React, {
  useCallback,
  useEffect,
  useLayoutEffect,
  useRef,
  useState,
} from "react";
import { useTranslation } from "react-i18next";
import { Box, Center, Text, ActionIcon, Button, Stack } from "@mantine/core";
import CloseIcon from "@mui/icons-material/Close";
import LockIcon from "@mui/icons-material/Lock";

import { useFileState, useFileActions } from "@app/contexts/FileContext";
import { useFileWithUrl } from "@app/hooks/useFileWithUrl";
import { useViewer } from "@app/contexts/ViewerContext";
import { LocalEmbedPDF } from "@app/components/viewer/LocalEmbedPDF";
import { PdfViewerToolbar } from "@app/components/viewer/PdfViewerToolbar";
import { ThumbnailSidebar } from "@app/components/viewer/ThumbnailSidebar";
import { BookmarkSidebar } from "@app/components/viewer/BookmarkSidebar";
import { AttachmentSidebar } from "@app/components/viewer/AttachmentSidebar";
import { LayerSidebar } from "@app/components/viewer/LayerSidebar";
import {
  useNavigationGuard,
  useNavigationState,
} from "@app/contexts/NavigationContext";
import { useSignature } from "@app/contexts/SignatureContext";
import { useRedaction } from "@app/contexts/RedactionContext";
import type { RedactionPendingTrackerAPI } from "@app/components/viewer/RedactionPendingTracker";
import type {
  SignaturePreview,
  SignatureOverlayAPI,
} from "@app/components/viewer/viewerTypes";
import { createStirlingFilesAndStubs } from "@app/services/fileStubHelpers";
import { isStirlingFile, getFormFillFileId } from "@app/types/fileContext";
import { useViewerWorkbenchBarButtons } from "@app/components/viewer/useViewerWorkbenchBarButtons";
import { StampPlacementOverlay } from "@app/components/viewer/StampPlacementOverlay";
import {
  RulerOverlay,
  type PageMeasureScales,
  type PageScaleInfo,
  type ViewportScale,
} from "@app/components/viewer/RulerOverlay";
import type { PDFDict, PDFNumber } from "@cantoo/pdf-lib";
import { useWheelZoom } from "@app/hooks/useWheelZoom";
import { useFormFill } from "@app/tools/formFill/FormFillContext";
import { FormSaveBar } from "@app/tools/formFill/FormSaveBar";
import { useViewerKeyCommand } from "@app/hooks/useViewerKeyCommand";
import { alert } from "@app/components/toast";

// ─── Measure dictionary extraction ────────────────────────────────────────────

async function extractPageMeasureScales(
  file: Blob,
): Promise<PageMeasureScales | null> {
  try {
    const {
      PDFDocument,
      PDFDict,
      PDFName,
      PDFArray,
      PDFNumber,
      PDFString,
      PDFHexString,
    } = await import("@cantoo/pdf-lib");
    const pdfDoc = await PDFDocument.load(await file.arrayBuffer(), {
      ignoreEncryption: true,
    });

    // Parse a Measure dict into a MeasureScale, or return null if malformed.
    const parseScale = (measureObj: unknown) => {
      if (!(measureObj instanceof PDFDict)) return null;
      // @cantoo/pdf-lib ships without individual .d.ts files so instanceof can't narrow `unknown`
      const m = measureObj as PDFDict;
      const rObj = m.lookup(PDFName.of("R"));
      const ratioLabel =
        rObj instanceof PDFString || rObj instanceof PDFHexString
          ? rObj.decodeText()
          : "";
      // D = distance array, X = x-axis fallback
      let fmtArray = m.lookup(PDFName.of("D"));
      if (!(fmtArray instanceof PDFArray)) fmtArray = m.lookup(PDFName.of("X"));
      if (!(fmtArray instanceof PDFArray)) return null;
      const firstFmt = fmtArray.lookup(0);
      if (!(firstFmt instanceof PDFDict)) return null;
      const cObj = firstFmt.lookup(PDFName.of("C"));
      const uObj = firstFmt.lookup(PDFName.of("U"));
      if (!(cObj instanceof PDFNumber) || cObj.asNumber() <= 0) return null;
      const unit =
        uObj instanceof PDFString || uObj instanceof PDFHexString
          ? uObj.decodeText()
          : "units";
      return { factor: cObj.asNumber(), unit, ratioLabel };
    };

    const result: PageMeasureScales = new Map();

    for (let i = 0; i < pdfDoc.getPageCount(); i++) {
      const page = pdfDoc.getPage(i);
      const pageHeight = page.getHeight();
      const pageNode = page.node as unknown as PDFDict;
      const viewports: ViewportScale[] = [];

      // Spec-conformant: /VP array — each viewport can have its own scale and BBox
      const vpObj = pageNode.lookup(PDFName.of("VP"));
      if (vpObj instanceof PDFArray) {
        for (let j = 0; j < vpObj.size(); j++) {
          const vpEntry = vpObj.lookup(j);
          if (!(vpEntry instanceof PDFDict)) continue;
          const scale = parseScale(vpEntry.lookup(PDFName.of("Measure")));
          if (!scale) continue;
          let bbox: ViewportScale["bbox"] = null;
          const bboxObj = vpEntry.lookup(PDFName.of("BBox"));
          if (bboxObj instanceof PDFArray && bboxObj.size() >= 4) {
            bbox = [
              (bboxObj.lookup(0) as PDFNumber).asNumber(),
              (bboxObj.lookup(1) as PDFNumber).asNumber(),
              (bboxObj.lookup(2) as PDFNumber).asNumber(),
              (bboxObj.lookup(3) as PDFNumber).asNumber(),
            ];
          }
          viewports.push({ bbox, scale });
        }
      }

      // Fallback: /Measure directly on page (non-conforming but seen in the wild)
      if (viewports.length === 0) {
        const scale = parseScale(pageNode.lookup(PDFName.of("Measure")));
        if (scale) viewports.push({ bbox: null, scale });
      }

      if (viewports.length > 0)
        result.set(i, { viewports, pageHeight } satisfies PageScaleInfo);
    }

    return result.size > 0 ? result : null;
  } catch {
    return null;
  }
}

// ──────────────────────────────────────────────────────────────────────────────

export interface EmbedPdfViewerProps {
  sidebarsVisible: boolean;
  setSidebarsVisible: (v: boolean) => void;
  onClose?: () => void;
  previewFile?: File | null;
  // ── Signature overlay pass-through (opt-in; all default off) ──────────────
  signaturePreviews?: SignaturePreview[];
  signaturePreviewsReadOnly?: boolean;
  signaturePlacementMode?: boolean;
  signaturePlacementData?: string;
  signaturePlacementType?: "canvas" | "image" | "text";
  onSignaturePreviewsChange?: (previews: SignaturePreview[]) => void;
  signatureOverlayApiRef?: React.RefObject<SignatureOverlayAPI | null>;
}

const EmbedPdfViewerContent = ({
  sidebarsVisible: _sidebarsVisible,
  setSidebarsVisible: _setSidebarsVisible,
  onClose,
  previewFile,
  signaturePreviews,
  signaturePreviewsReadOnly,
  signaturePlacementMode,
  signaturePlacementData,
  signaturePlacementType,
  onSignaturePreviewsChange,
  signatureOverlayApiRef,
}: EmbedPdfViewerProps) => {
  const { t } = useTranslation();
  const viewerRef = React.useRef<HTMLDivElement>(null);
  const pdfContainerRef = useRef<HTMLDivElement>(null);
  const [isViewerHovered, setIsViewerHovered] = React.useState(false);

  const {
    isThumbnailSidebarVisible,
    toggleThumbnailSidebar,
    isBookmarkSidebarVisible,
    isAttachmentSidebarVisible,
    isLayerSidebarVisible,
    setHasLayers,
    isCommentsSidebarVisible,
    isSearchInterfaceVisible,
    searchInterfaceActions,
    zoomActions,
    scrollActions,
    panActions: _panActions,
    rotationActions,
    getScrollState,
    getRotationState,
    setAnnotationMode,
    isAnnotationsVisible,
    exportActions,
    printActions,
    selectionActions,
    setApplyChanges,
    applyChanges: viewerApplyChanges,
    pdfRenderMode,
    cyclePdfRenderMode,
    setActiveFileIndex: _setActiveFileIndex,
    activeFileId,
    setActiveFileId,
  } = useViewer();

  const scrollState = getScrollState();
  const rotationState = getRotationState();

  // Track initial rotation to detect changes
  const initialRotationRef = useRef<number | null>(null);
  useEffect(() => {
    if (
      initialRotationRef.current === null &&
      rotationState.rotation !== undefined
    ) {
      initialRotationRef.current = rotationState.rotation;
    }
  }, [rotationState.rotation]);

  // Get signature and annotation contexts
  const {
    signatureApiRef,
    annotationApiRef,
    historyApiRef,
    signatureConfig,
    isPlacementMode,
  } = useSignature();

  // Track whether there are unsaved annotation changes in this viewer session.
  // This is our source of truth for navigation guards; it is set when the
  // annotation history changes, and cleared after we successfully apply changes.
  const hasAnnotationChangesRef = useRef(false);
  // EmbedPDF can emit once from the saved undo stack before the saved file remounts.
  // Ignore that stale update without suppressing future edits on the same instance.
  const savedAnnotationHistoryApiRef =
    useRef<typeof historyApiRef.current>(null);

  // Scroll position preservation system
  // We continuously track the last known good scroll position, so we always have it available
  const lastKnownScrollPageRef = useRef<number>(1);
  const pendingScrollRestoreRef = useRef<number | null>(null);
  const scrollRestoreAttemptsRef = useRef<number>(0);

  // Rotation preservation system
  // Similar to scroll preservation - track rotation across file reloads
  const pendingRotationRestoreRef = useRef<number | null>(null);
  const rotationRestoreAttemptsRef = useRef<number>(0);

  const formApplyInProgressRef = useRef(false);
  const applyChangesInFlightRef = useRef<Promise<void> | null>(null);

  // Get redaction context
  const { redactionsApplied, setRedactionsApplied } = useRedaction();

  // Ref for redaction pending tracker API
  const redactionTrackerRef = useRef<RedactionPendingTrackerAPI>(null);

  // Get current file from FileContext
  const { selectors } = useFileState();
  const { actions } = useFileActions();
  const activeFiles = selectors.getFiles();
  const activeFilesRef = useRef(activeFiles);
  activeFilesRef.current = activeFiles;
  const activeFileIds = activeFiles.map((f) => f.fileId);

  // Navigation guard for unsaved changes
  const {
    setHasUnsavedChanges,
    registerUnsavedChangesChecker,
    unregisterUnsavedChangesChecker,
    registerNavigationWarningHandlers,
    unregisterNavigationWarningHandlers,
  } = useNavigationGuard();

  const { selectedTool } = useNavigationState();

  // Form fill context
  const { fetchFields: fetchFormFields, setProviderMode } = useFormFill();

  const isInAnnotationTool =
    selectedTool === "sign" ||
    selectedTool === "addText" ||
    selectedTool === "addImage" ||
    selectedTool === "annotate";
  const isSignatureMode = isInAnnotationTool;
  const isManualRedactMode = selectedTool === "redact";

  // Enable annotations when annotation tool is selected OR when annotations are visible
  // (so users can interact with existing comment annotations in reader/viewer mode)
  const shouldEnableAnnotations =
    selectedTool === "annotate" || isSignatureMode || isAnnotationsVisible;

  // Enable redaction only when redaction tool is selected
  const shouldEnableRedaction = selectedTool === "redact";

  // FormFill tool mode — uses PDFBox backend for full-fidelity form handling
  const isFormFillToolActive = (selectedTool as string) === "formFill";

  // Form overlays are shown in BOTH modes:
  // - Normal viewer: form overlays visible (PDFium WASM, frontend-only)
  // - formFill tool: form overlays visible (PDFBox, backend)
  const shouldEnableFormFill = true;

  // Switch the provider when the tool mode changes
  useEffect(() => {
    setProviderMode(isFormFillToolActive ? "pdfbox" : "pdflib");
  }, [isFormFillToolActive, setProviderMode]);

  // Track previous annotation/redaction state to detect tool switches
  const prevEnableAnnotationsRef = useRef(shouldEnableAnnotations);
  const prevEnableRedactionRef = useRef(shouldEnableRedaction);

  // Track scroll position whenever scrollState changes from the context
  // This ensures we always have the most up-to-date position
  useEffect(() => {
    if (scrollState.currentPage > 0) {
      lastKnownScrollPageRef.current = scrollState.currentPage;
    }
  }, [scrollState.currentPage]);

  // Preserve scroll position when switching between annotation and redaction tools
  // Using useLayoutEffect to capture synchronously before DOM updates
  useLayoutEffect(() => {
    const annotationsChanged =
      prevEnableAnnotationsRef.current !== shouldEnableAnnotations;
    const redactionChanged =
      prevEnableRedactionRef.current !== shouldEnableRedaction;

    if (annotationsChanged || redactionChanged) {
      // Read scroll state directly AND use the tracked value - take whichever is valid
      const currentScrollState = getScrollState();
      const pageFromState = currentScrollState.currentPage;
      const pageFromRef = lastKnownScrollPageRef.current;

      // Use the current state if valid, otherwise fall back to tracked ref
      const pageToRestore = pageFromState > 0 ? pageFromState : pageFromRef;

      if (pageToRestore > 0) {
        pendingScrollRestoreRef.current = pageToRestore;
        scrollRestoreAttemptsRef.current = 0;
      }

      prevEnableAnnotationsRef.current = shouldEnableAnnotations;
      prevEnableRedactionRef.current = shouldEnableRedaction;
    }
  }, [shouldEnableAnnotations, shouldEnableRedaction, getScrollState]);

  // Keep annotation mode enabled when entering placement tools without overriding manual toggles
  useEffect(() => {
    if (isInAnnotationTool) {
      setAnnotationMode(true);
    }
  }, [isInAnnotationTool, setAnnotationMode]);
  const isPlacementOverlayActive = Boolean(
    isInAnnotationTool && isPlacementMode && signatureConfig,
  );

  // Determine which file to display — use activeFileId (stable) not activeFileIndex (shifts on removal)
  const currentFile = React.useMemo(() => {
    if (previewFile) {
      return previewFile;
    } else if (activeFiles.length > 0) {
      const byId = activeFileId
        ? activeFiles.find(
            (f) => isStirlingFile(f) && f.fileId === activeFileId,
          )
        : null;
      return byId || activeFiles[0];
    }
    return null;
  }, [previewFile, activeFiles, activeFileId]);

  // Stable id — avoids blob URL churn when FileContext recreates file objects each render.
  const currentFileStableId =
    currentFile && isStirlingFile(currentFile) ? currentFile.fileId : null;
  const fileWithUrl = useFileWithUrl(currentFile, currentFileStableId);

  // Determine the effective file to display
  const effectiveFile = React.useMemo(() => {
    if (previewFile) {
      // In preview mode, show the preview file
      if (previewFile.size === 0) {
        return null;
      }
      return { file: previewFile, url: null };
    } else {
      return fileWithUrl;
    }
  }, [previewFile, fileWithUrl]);

  // Check if the current file is encrypted (gate the viewer to prevent PDFium crash)
  const isCurrentFileEncrypted = React.useMemo(() => {
    if (!currentFile || !isStirlingFile(currentFile)) return false;
    const stub = selectors.getStirlingFileStub(currentFile.fileId);
    return stub?.processedFile?.isEncrypted === true;
  }, [currentFile, selectors]);

  const bookmarkCacheKey = React.useMemo(() => {
    if (currentFile && isStirlingFile(currentFile)) {
      return currentFile.fileId;
    }

    if (previewFile) {
      const uniquePreviewId = `${previewFile.name}-${previewFile.size}-${previewFile.lastModified ?? "na"}`;
      return `preview-${uniquePreviewId}`;
    }

    if (effectiveFile?.url) {
      return effectiveFile.url;
    }

    if (effectiveFile?.file instanceof File) {
      const fileObj = effectiveFile.file;
      return `file-${fileObj.name}-${fileObj.size}-${fileObj.lastModified ?? "na"}`;
    }

    return undefined;
  }, [currentFile, effectiveFile, previewFile]);

  // Generate cache keys for all active files to enable preloading
  const allBookmarkCacheKeys = React.useMemo(() => {
    if (previewFile) {
      return [bookmarkCacheKey].filter(Boolean) as string[];
    }

    return activeFiles
      .map((file) => {
        if (isStirlingFile(file)) {
          return file.fileId;
        }
        return undefined;
      })
      .filter(Boolean) as string[];
  }, [activeFiles, previewFile, bookmarkCacheKey]);

  useWheelZoom({
    ref: viewerRef,
    onZoomIn: zoomActions.zoomIn,
    onZoomOut: zoomActions.zoomOut,
  });

  const viewerKeyCommand = useViewerKeyCommand();

  // Handle keyboard shortcuts
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      const mod = event.ctrlKey || event.metaKey;

      // Ctrl+P (print) must be intercepted unconditionally
      // whenever the viewer is mounted, even before the user has hovered over it.
      // Ctrl+R (rotate) is intercepted only on desktop (Tauri), while on web it still falls through to browser refresh.
      // Without this, the browser falls through to its native "print HTML page"
      // or "reload page" behaviour.

      if (mod) {
        const target = event.target as Element;
        const isInTextInput =
          target.tagName === "INPUT" ||
          target.tagName === "TEXTAREA" ||
          (target as HTMLElement).isContentEditable;

        if (!isInTextInput) {
          const wasOverridden = viewerKeyCommand(event);
          if (!wasOverridden) {
            switch (event.key) {
              case "p":
              case "P":
                event.preventDefault();
                printActions.print();
                return;
              case "a":
              case "A":
                // Intercept unconditionally so the browser can't blanket-select the surrounding UI chrome.
                event.preventDefault();
                {
                  const totalPages = getScrollState().totalPages;
                  if (totalPages > 0) {
                    void selectionActions.selectAll(totalPages);
                  }
                }
                return;
              case "=":
              case "+":
                event.preventDefault();
                zoomActions.zoomIn();
                return;
              case "-":
              case "_":
                event.preventDefault();
                zoomActions.zoomOut();
                return;
              case "0":
                event.preventDefault();
                zoomActions.requestZoom("fit-width");
                return;
            }
          }
        }
      }

      // All remaining shortcuts require the viewer to be hovered so they
      // don't conflict with the rest of the UI when the viewer is mounted
      // but not the active focus target.
      if (!isViewerHovered) return;

      // Modifier key shortcuts (Ctrl/Cmd + key)
      if (mod) {
        switch (event.key) {
          case "f":
          case "F":
            event.preventDefault();
            if (isSearchInterfaceVisible) {
              window.dispatchEvent(new CustomEvent("refocus-search-input"));
            } else {
              searchInterfaceActions.open();
            }
            return;
          case "s":
          case "S":
            // Ctrl+S: Save/apply changes
            if (!event.shiftKey) {
              event.preventDefault();
              if (viewerApplyChanges) {
                viewerApplyChanges();
              }
            }
            return;

          case "z":
          case "Z":
            // Ctrl+Z: Undo; Ctrl+Shift+Z: Redo
            event.preventDefault();
            if (event.shiftKey) {
              historyApiRef.current?.redo?.();
            } else {
              historyApiRef.current?.undo?.();
            }
            return;
          case "y":
          case "Y":
            // Ctrl+Y: Redo
            event.preventDefault();
            historyApiRef.current?.redo?.();
            return;
        }
        return;
      }

      // Non-modifier shortcuts
      switch (event.key) {
        case "Home":
          event.preventDefault();
          scrollActions.scrollToFirstPage();
          return;
        case "End":
          event.preventDefault();
          scrollActions.scrollToLastPage();
          return;
        case "PageUp":
          event.preventDefault();
          scrollActions.scrollToPreviousPage();
          return;
        case "PageDown":
          event.preventDefault();
          scrollActions.scrollToNextPage();
          return;
        case "Escape":
          if (isSearchInterfaceVisible) {
            event.preventDefault();
            searchInterfaceActions.close();
          }
          return;
      }
    };

    document.addEventListener("keydown", handleKeyDown);
    return () => {
      document.removeEventListener("keydown", handleKeyDown);
    };
  }, [
    isViewerHovered,
    isSearchInterfaceVisible,
    zoomActions,
    searchInterfaceActions,
    scrollActions,
    printActions,
    exportActions,
    rotationActions,
    historyApiRef,
    viewerApplyChanges,
    cyclePdfRenderMode,
    viewerKeyCommand,
    selectionActions,
    getScrollState,
  ]);

  // Watch the annotation history API to detect when the document becomes "dirty".
  // We treat any change that makes the history undoable as unsaved changes until
  // the user explicitly applies them via applyChanges.
  useEffect(() => {
    const historyApi = historyApiRef.current;
    if (!historyApi || !historyApi.subscribe) {
      return;
    }

    const updateHasChanges = () => {
      if (savedAnnotationHistoryApiRef.current === historyApi) {
        savedAnnotationHistoryApiRef.current = null;
        return;
      }

      const canUndo = historyApi.canUndo?.() ?? false;
      if (!hasAnnotationChangesRef.current && canUndo) {
        hasAnnotationChangesRef.current = true;
        setHasUnsavedChanges(true);
      }
    };

    const unsubscribe = historyApi.subscribe(updateHasChanges);
    return () => {
      if (typeof unsubscribe === "function") {
        unsubscribe();
      }
    };
  }, [historyApiRef.current, setHasUnsavedChanges]);

  // Register checker for unsaved changes (annotations only for now)
  useEffect(() => {
    if (previewFile) {
      return;
    }

    const checkForChanges = () => {
      // Check for annotation history changes (using ref that's updated by useEffect)
      const hasAnnotationChanges = hasAnnotationChangesRef.current;

      // Check for pending redactions
      const hasPendingRedactions =
        (redactionTrackerRef.current?.getPendingCount() ?? 0) > 0;

      // Always consider applied redactions as unsaved until export
      const hasAppliedRedactions = redactionsApplied;

      return (
        hasAnnotationChanges || hasPendingRedactions || hasAppliedRedactions
      );
    };

    registerUnsavedChangesChecker(checkForChanges);

    return () => {
      unregisterUnsavedChangesChecker();
    };
  }, [
    historyApiRef,
    previewFile,
    registerUnsavedChangesChecker,
    unregisterUnsavedChangesChecker,
    isManualRedactMode,
    redactionsApplied,
  ]);

  // Save changes - save annotations and redactions to file (overwrites active file)
  const applyChanges = useCallback(async () => {
    if (applyChangesInFlightRef.current) {
      return applyChangesInFlightRef.current;
    }

    if (!currentFile || activeFileIds.length === 0) return;

    const saveChanges = async () => {
      console.log(
        "[Viewer] Applying changes - exporting PDF with annotations/redactions",
      );

      // Use the continuously tracked scroll position - more reliable than reading at this moment
      const pageToRestore = lastKnownScrollPageRef.current;

      // Save the current rotation to restore after reload
      const currentRotation = rotationState.rotation ?? 0;

      // Step 0: Commit any pending redactions before export
      const hadPendingRedactions =
        (redactionTrackerRef.current?.getPendingCount() ?? 0) > 0;

      // Mark redactions as applied BEFORE committing, so the button stays enabled during the save process
      // This ensures the button doesn't become disabled when pendingCount becomes 0
      if (hadPendingRedactions || redactionsApplied) {
        setRedactionsApplied(true);
      }

      if (hadPendingRedactions) {
        console.log("[Viewer] Committing pending redactions before export");
        redactionTrackerRef.current?.commitAllPending();
        // Give a small delay for the commit to process
        await new Promise((resolve) => setTimeout(resolve, 100));
      }

      // Step 1: Export PDF with annotations using EmbedPDF
      const arrayBuffer = await exportActions.saveAsCopy();
      if (!arrayBuffer) {
        throw new Error("Failed to export PDF");
      }

      // Step 2: Convert ArrayBuffer to File
      const blob = new Blob([arrayBuffer], { type: "application/pdf" });
      const filename = currentFile.name || "document.pdf";
      const file = new File([blob], filename, { type: "application/pdf" });

      // Step 3: Create StirlingFiles and stubs for version history
      // Only consume the current file, not all active files
      const currentFileId = currentFileStableId;
      if (!currentFileId) throw new Error("Current file ID not found");

      const parentStub = selectors.getStirlingFileStub(currentFileId);
      if (!parentStub) throw new Error("Parent stub not found");

      const { stirlingFiles, stubs } = await createStirlingFilesAndStubs(
        [file],
        parentStub,
        selectedTool ?? "multiTool",
      );

      // Store the page to restore after file replacement triggers re-render
      pendingScrollRestoreRef.current = pageToRestore;
      scrollRestoreAttemptsRef.current = 0;

      // Store the rotation to restore after file replacement
      pendingRotationRestoreRef.current = currentRotation;
      rotationRestoreAttemptsRef.current = 0;
      // Track the new file ID so the viewer follows it after the list reorders
      const newFileId = stubs[0]?.id;
      if (newFileId) setActiveFileId(newFileId);

      // Step 4: Consume only the current file (replace in context)
      await actions.consumeFiles([currentFileId], stirlingFiles, stubs);

      // Mark annotations as saved so navigation away from the viewer is allowed.
      savedAnnotationHistoryApiRef.current = historyApiRef.current;
      hasAnnotationChangesRef.current = false;
      setHasUnsavedChanges(false);
      setRedactionsApplied(false);
    };

    const savePromise = saveChanges()
      .catch((error) => {
        console.error("Apply changes failed:", error);
        alert({
          title: t("viewer.saveChangesErrorTitle", "Could not save changes"),
          body:
            error instanceof Error && error.message
              ? error.message
              : t(
                  "viewer.saveChangesErrorBody",
                  "The document could not be saved. Try again.",
                ),
          alertType: "error",
        });
        throw error;
      })
      .finally(() => {
        applyChangesInFlightRef.current = null;
      });

    applyChangesInFlightRef.current = savePromise;
    return savePromise;
  }, [
    currentFile,
    activeFiles,
    exportActions,
    actions,
    selectors,
    historyApiRef,
    setHasUnsavedChanges,
    setRedactionsApplied,
    rotationState.rotation,
    t,
  ]);

  // Apply form fill changes - reload the filled PDF into the viewer
  const handleFormApply = useCallback(
    async (filledBlob: Blob) => {
      if (formApplyInProgressRef.current) return;
      if (!currentFile || activeFileIds.length === 0) return;

      formApplyInProgressRef.current = true;
      try {
        console.log(
          "[Viewer] Applying form fill changes - reloading filled PDF",
        );

        // Use the continuously tracked scroll position
        const pageToRestore = lastKnownScrollPageRef.current;

        // Save the current rotation to restore after reload
        const currentRotation = rotationState.rotation ?? 0;

        // Convert Blob to File
        const filename = currentFile.name || "document.pdf";
        const file = new File([filledBlob], filename, {
          type: "application/pdf",
        });

        // Get current file info for creating the updated version
        const currentFileId = currentFileStableId;
        if (!currentFileId) throw new Error("Current file ID not found");

        const parentStub = selectors.getStirlingFileStub(currentFileId);
        if (!parentStub) throw new Error("Parent stub not found");

        // Create StirlingFiles and stubs for version history
        const { stirlingFiles, stubs } = await createStirlingFilesAndStubs(
          [file],
          parentStub,
          selectedTool ?? "multiTool",
        );

        // Store the page to restore after file replacement
        pendingScrollRestoreRef.current = pageToRestore;
        scrollRestoreAttemptsRef.current = 0;

        // Store the rotation to restore after file replacement
        pendingRotationRestoreRef.current = currentRotation;
        rotationRestoreAttemptsRef.current = 0;

        // Track the new file ID so the viewer follows it after the list reorders
        const newFileId = stubs[0]?.id;
        if (newFileId) setActiveFileId(newFileId);

        // Replace the current file in context
        await actions.consumeFiles([currentFileId], stirlingFiles, stubs);

        console.log("[Viewer] Form fill changes applied successfully");
      } catch (error) {
        console.error("[Viewer] Apply form changes failed:", error);
      } finally {
        formApplyInProgressRef.current = false;
      }
    },
    [
      currentFile,
      activeFiles,
      actions,
      selectors,
      activeFileIds.length,
      rotationState.rotation,
    ],
  );

  useEffect(() => {
    const handler = (e: Event) => {
      const blob = (e as CustomEvent).detail?.blob;
      if (blob) {
        handleFormApply(blob);
      }
    };
    window.addEventListener("formfill:apply", handler);
    return () => window.removeEventListener("formfill:apply", handler);
  }, [handleFormApply]);

  // Apply layer visibility changes - reload the modified PDF into the viewer
  const layerApplyInProgressRef = useRef(false);
  const handleLayerApply = useCallback(
    async (modifiedBlob: Blob) => {
      if (layerApplyInProgressRef.current) return;
      if (!currentFile || activeFileIds.length === 0) return;

      layerApplyInProgressRef.current = true;
      try {
        const pageToRestore = lastKnownScrollPageRef.current;
        const currentRotation = rotationState.rotation ?? 0;

        const filename = currentFile.name || "document.pdf";
        const file = new File([modifiedBlob], filename, {
          type: "application/pdf",
        });

        const currentFileId = currentFileStableId;
        if (!currentFileId) throw new Error("Current file ID not found");

        const parentStub = selectors.getStirlingFileStub(currentFileId);
        if (!parentStub) throw new Error("Parent stub not found");

        const { stirlingFiles, stubs } = await createStirlingFilesAndStubs(
          [file],
          parentStub,
          selectedTool ?? "multiTool",
        );

        pendingScrollRestoreRef.current = pageToRestore;
        scrollRestoreAttemptsRef.current = 0;
        pendingRotationRestoreRef.current = currentRotation;
        rotationRestoreAttemptsRef.current = 0;

        const newFileId = stubs[0]?.id;
        if (newFileId) setActiveFileId(newFileId);

        await actions.consumeFiles([currentFileId], stirlingFiles, stubs);
      } catch (error) {
        console.error("[Viewer] Apply layer changes failed:", error);
      } finally {
        layerApplyInProgressRef.current = false;
      }
    },
    [
      currentFile,
      activeFiles,
      actions,
      selectors,
      activeFileIds.length,
      rotationState.rotation,
    ],
  );

  // Discard pending redactions but save already-applied ones
  // This is called when user clicks "Discard & Leave" - we want to:
  // 1. NOT commit pending redaction marks (they get discarded)
  // 2. Save the PDF with already-applied redactions (if any)
  const discardAndSaveApplied = useCallback(async () => {
    // Only save if there are applied redactions to preserve
    if (!redactionsApplied || !currentFile || activeFileIds.length === 0) {
      return;
    }

    try {
      console.log(
        "[Viewer] Discarding pending marks but saving applied redactions",
      );

      // Save current view state to restore after file replacement
      const pageToRestore = lastKnownScrollPageRef.current;
      const currentRotation = rotationState.rotation ?? 0;

      // Export PDF WITHOUT committing pending marks - this saves only applied redactions
      const arrayBuffer = await exportActions.saveAsCopy();
      if (!arrayBuffer) {
        throw new Error("Failed to export PDF");
      }

      // Convert ArrayBuffer to File
      const blob = new Blob([arrayBuffer], { type: "application/pdf" });
      const filename = currentFile.name || "document.pdf";
      const file = new File([blob], filename, { type: "application/pdf" });

      // Create StirlingFiles and stubs for version history
      const currentFileId = currentFileStableId;
      if (!currentFileId) throw new Error("Current file ID not found");

      const parentStub = selectors.getStirlingFileStub(currentFileId);
      if (!parentStub) throw new Error("Parent stub not found");

      const { stirlingFiles, stubs } = await createStirlingFilesAndStubs(
        [file],
        parentStub,
        selectedTool ?? "multiTool",
      );

      // Store view state to restore after file replacement
      pendingScrollRestoreRef.current = pageToRestore;
      scrollRestoreAttemptsRef.current = 0;
      pendingRotationRestoreRef.current = currentRotation;
      rotationRestoreAttemptsRef.current = 0;

      // Consume only the current file (replace in context)
      await actions.consumeFiles([currentFileId], stirlingFiles, stubs);

      // Clear flags
      hasAnnotationChangesRef.current = false;
      setRedactionsApplied(false);

      console.log("[Viewer] Applied redactions saved, pending marks discarded");
    } catch (error) {
      console.error("Failed to save applied redactions:", error);
    }
  }, [
    redactionsApplied,
    currentFile,
    activeFiles,
    activeFileIds.length,
    exportActions,
    actions,
    selectors,
    setRedactionsApplied,
    rotationState.rotation,
  ]);

  // Register navigation warning handlers so the global modal can call our save/discard logic
  useEffect(() => {
    if (previewFile) return;

    registerNavigationWarningHandlers({
      onApplyAndContinue: async () => {
        await applyChanges();
      },
      onDiscardAndContinue: async () => {
        await discardAndSaveApplied();
        const historyApi = historyApiRef.current;
        if (historyApi?.canUndo) {
          while (historyApi.canUndo()) {
            historyApi.undo?.();
          }
        }
        hasAnnotationChangesRef.current = false;
      },
    });
    return () => unregisterNavigationWarningHandlers();
  }, [
    previewFile,
    applyChanges,
    discardAndSaveApplied,
    registerNavigationWarningHandlers,
    unregisterNavigationWarningHandlers,
  ]);

  // Restore scroll position after file replacement or tool switch
  // Uses polling with retries to ensure the scroll succeeds
  useEffect(() => {
    if (pendingScrollRestoreRef.current === null) return;

    const pageToRestore = pendingScrollRestoreRef.current;
    const maxAttempts = 10;
    const attemptInterval = 100; // ms between attempts

    const attemptScroll = () => {
      const currentState = getScrollState();
      const targetPage = Math.min(pageToRestore, currentState.totalPages);

      // Only attempt if we have valid state (totalPages > 0 means PDF is loaded)
      if (currentState.totalPages > 0 && targetPage > 0) {
        scrollActions.scrollToPage(targetPage, "instant");

        // Check if scroll succeeded after a brief delay
        setTimeout(() => {
          const afterState = getScrollState();
          if (
            afterState.currentPage === targetPage ||
            scrollRestoreAttemptsRef.current >= maxAttempts
          ) {
            // Success or max attempts reached - clear pending
            pendingScrollRestoreRef.current = null;
            scrollRestoreAttemptsRef.current = 0;
          } else {
            // Scroll might not have worked, retry
            scrollRestoreAttemptsRef.current++;
            if (scrollRestoreAttemptsRef.current < maxAttempts) {
              setTimeout(attemptScroll, attemptInterval);
            } else {
              // Give up after max attempts
              pendingScrollRestoreRef.current = null;
              scrollRestoreAttemptsRef.current = 0;
            }
          }
        }, 50);
      } else if (scrollRestoreAttemptsRef.current < maxAttempts) {
        // PDF not ready yet, retry
        scrollRestoreAttemptsRef.current++;
        setTimeout(attemptScroll, attemptInterval);
      } else {
        // Give up after max attempts
        pendingScrollRestoreRef.current = null;
        scrollRestoreAttemptsRef.current = 0;
      }
    };

    // Start attempting after initial delay
    const timer = setTimeout(attemptScroll, 150);
    return () => clearTimeout(timer);
  }, [scrollState.totalPages, scrollActions, getScrollState]);

  // Restore rotation after file replacement or tool switch
  // Uses polling with retries to ensure the rotation succeeds
  useEffect(() => {
    if (pendingRotationRestoreRef.current === null) return;

    const rotationToRestore = pendingRotationRestoreRef.current;
    const maxAttempts = 10;
    const attemptInterval = 100; // ms between attempts

    const attemptRotation = () => {
      const currentState = getScrollState();

      // Only attempt if PDF is loaded (totalPages > 0)
      if (currentState.totalPages > 0) {
        rotationActions.setRotation(rotationToRestore);

        // Check if rotation succeeded after a brief delay
        setTimeout(() => {
          const currentRotation = rotationActions.getRotation();
          if (
            currentRotation === rotationToRestore ||
            rotationRestoreAttemptsRef.current >= maxAttempts
          ) {
            // Success or max attempts reached - clear pending
            pendingRotationRestoreRef.current = null;
            rotationRestoreAttemptsRef.current = 0;
          } else {
            // Rotation might not have worked, retry
            rotationRestoreAttemptsRef.current++;
            if (rotationRestoreAttemptsRef.current < maxAttempts) {
              setTimeout(attemptRotation, attemptInterval);
            } else {
              // Give up after max attempts
              pendingRotationRestoreRef.current = null;
              rotationRestoreAttemptsRef.current = 0;
            }
          }
        }, 50);
      } else if (rotationRestoreAttemptsRef.current < maxAttempts) {
        // PDF not ready yet, retry
        rotationRestoreAttemptsRef.current++;
        setTimeout(attemptRotation, attemptInterval);
      } else {
        // Give up after max attempts
        pendingRotationRestoreRef.current = null;
        rotationRestoreAttemptsRef.current = 0;
      }
    };

    // Start attempting after initial delay
    const timer = setTimeout(attemptRotation, 150);
    return () => clearTimeout(timer);
  }, [scrollState.totalPages, rotationActions, getScrollState]);

  // Register applyChanges with ViewerContext so tools can access it directly
  useEffect(() => {
    setApplyChanges(applyChanges);
    return () => {
      setApplyChanges(null);
    };
  }, [applyChanges, setApplyChanges]);

  // Ruler / measurement tool state
  const [isRulerActive, setIsRulerActive] = useState(false);
  const [pageMeasureScales, setPageMeasureScales] =
    useState<PageMeasureScales | null>(null);

  useEffect(() => {
    const file = effectiveFile?.file;
    if (!file) {
      setPageMeasureScales(null);
      return;
    }
    let cancelled = false;
    extractPageMeasureScales(file).then((scales) => {
      if (!cancelled) setPageMeasureScales(scales);
    });
    return () => {
      cancelled = true;
    };
  }, [effectiveFile]);

  // Register workbench bar buttons for the viewer
  useViewerWorkbenchBarButtons(isRulerActive, setIsRulerActive);

  // Auto-fetch form fields when a PDF is loaded in the viewer.
  // In normal viewer mode, this uses PDFium WASM (frontend-only).
  // In formFill tool mode, this uses PDFBox (backend).
  const formFillFileIdRef = useRef<string | null>(null);
  const formFillProviderRef = useRef(isFormFillToolActive);

  // Generate a unique identifier for the current file to detect file changes
  const currentFileId = React.useMemo(() => {
    return getFormFillFileId(currentFile);
  }, [currentFile]);

  useEffect(() => {
    const fileChanged = currentFileId !== formFillFileIdRef.current;
    const providerChanged =
      formFillProviderRef.current !== isFormFillToolActive;
    formFillProviderRef.current = isFormFillToolActive;

    if (fileChanged) {
      console.log(
        "[FormFill] File changed. Old:",
        formFillFileIdRef.current,
        "New:",
        currentFileId,
      );
      formFillFileIdRef.current = currentFileId;
      // NOTE: Don't call resetFormFill() here — fetchFormFields() handles
      // clearing old state internally. Calling reset() before fetch() would
      // double-increment fetchVersionRef, causing version mismatches when
      // the effect re-fires before the async fetch completes.
    }

    if (
      currentFile &&
      !isCurrentFileEncrypted &&
      (fileChanged || providerChanged)
    ) {
      console.log("[FormFill] Fetching form fields for:", currentFileId);
      fetchFormFields(currentFile, currentFileId ?? undefined);
    }
  }, [
    isFormFillToolActive,
    currentFile,
    currentFileId,
    fetchFormFields,
    isCurrentFileEncrypted,
  ]);

  const sidebarWidthRem = 15;
  const commentsSidebarWidthRem = 18;
  const totalRightMargin =
    (isThumbnailSidebarVisible ? sidebarWidthRem : 0) +
    (isBookmarkSidebarVisible ? sidebarWidthRem : 0) +
    (isAttachmentSidebarVisible ? sidebarWidthRem : 0) +
    (isLayerSidebarVisible ? sidebarWidthRem : 0) +
    (isCommentsSidebarVisible ? commentsSidebarWidthRem : 0);

  return (
    <Box
      ref={viewerRef}
      onMouseEnter={() => setIsViewerHovered(true)}
      onMouseLeave={() => setIsViewerHovered(false)}
      style={{
        position: "relative",
        height: "100%",
        display: "flex",
        flexDirection: "column",
        overflow: "hidden",
        contain: "layout style paint",
      }}
    >
      {/* Close Button - Only show in preview mode */}
      {onClose && previewFile && (
        <ActionIcon
          variant="filled"
          color="gray"
          size="lg"
          style={{
            position: "absolute",
            top: "1rem",
            right: "1rem",
            zIndex: 1000,
            borderRadius: "50%",
          }}
          onClick={onClose}
        >
          <CloseIcon />
        </ActionIcon>
      )}

      {!effectiveFile ? (
        <Center style={{ flex: 1 }}>
          <Text c="red">
            {t(
              "viewer.error.noFileProvided",
              "Error: No file provided to viewer",
            )}
          </Text>
        </Center>
      ) : isCurrentFileEncrypted ? (
        <Center style={{ flex: 1 }}>
          <Stack align="center" gap="md">
            <LockIcon style={{ fontSize: 48, opacity: 0.5 }} />
            <Text fw={500}>
              {t(
                "encryptedPdfUnlock.viewerLocked",
                "This PDF is password-protected",
              )}
            </Text>
            <Button
              variant="filled"
              onClick={() => {
                if (currentFile && isStirlingFile(currentFile)) {
                  actions.openEncryptedUnlockPrompt(currentFile.fileId);
                }
              }}
            >
              {t("encryptedPdfUnlock.viewerUnlock", "Unlock")}
            </Button>
          </Stack>
        </Center>
      ) : (
        <>
          {/* EmbedPDF Viewer */}
          <Box
            ref={pdfContainerRef}
            style={{
              position: "relative",
              flex: 1,
              overflow: "hidden",
              minHeight: 0,
              minWidth: 0,
              marginRight: `${totalRightMargin}rem`,
              transition: "margin-right 0.3s ease",
            }}
          >
            <LocalEmbedPDF
              key={currentFileId || "no-file"}
              pdfRenderMode={pdfRenderMode}
              file={effectiveFile.file}
              url={effectiveFile.url}
              fileName={
                previewFile
                  ? previewFile.name
                  : currentFile && isStirlingFile(currentFile)
                    ? currentFile.name
                    : effectiveFile?.file instanceof File
                      ? effectiveFile.file.name
                      : undefined
              }
              enableAnnotations={shouldEnableAnnotations}
              isSignMode={selectedTool === "sign"}
              showBakedAnnotations={isAnnotationsVisible}
              enableRedaction={shouldEnableRedaction}
              enableFormFill={shouldEnableFormFill}
              isManualRedactionMode={isManualRedactMode}
              signatureApiRef={signatureApiRef as React.RefObject<any>}
              annotationApiRef={annotationApiRef as React.RefObject<any>}
              historyApiRef={historyApiRef as React.RefObject<any>}
              redactionTrackerRef={
                redactionTrackerRef as React.RefObject<RedactionPendingTrackerAPI>
              }
              fileId={currentFileId}
              isCommentsSidebarVisible={isCommentsSidebarVisible}
              commentsSidebarRightOffset={`${(isThumbnailSidebarVisible ? sidebarWidthRem : 0) + (isBookmarkSidebarVisible ? sidebarWidthRem : 0) + (isAttachmentSidebarVisible ? sidebarWidthRem : 0) + (isLayerSidebarVisible ? sidebarWidthRem : 0)}rem`}
              onSignatureAdded={() => {
                // Handle signature added - for debugging, enable console logs as needed
                // Future: Handle signature completion
              }}
              signaturePreviews={signaturePreviews}
              signaturePreviewsReadOnly={signaturePreviewsReadOnly}
              signaturePlacementMode={signaturePlacementMode}
              signaturePlacementData={signaturePlacementData}
              signaturePlacementType={signaturePlacementType}
              onSignaturePreviewsChange={onSignaturePreviewsChange}
              signatureOverlayApiRef={signatureOverlayApiRef}
            />
            {/* Floating save bar for form-filled PDFs (like Chrome/Firefox PDF viewers) */}
            <FormSaveBar
              file={currentFile ?? null}
              isFormFillToolActive={isFormFillToolActive}
              onApply={handleFormApply}
            />
            <StampPlacementOverlay
              containerRef={pdfContainerRef}
              isActive={isPlacementOverlayActive}
              signatureConfig={signatureConfig}
            />
            <RulerOverlay
              containerRef={pdfContainerRef}
              isActive={isRulerActive}
              pageMeasureScales={pageMeasureScales}
            />
          </Box>
        </>
      )}

      {/* Bottom Toolbar Overlay */}
      {effectiveFile && (
        <div
          style={{
            position: "fixed",
            bottom: 0,
            left: 0,
            right: 0,
            zIndex: 50,
            display: "flex",
            justifyContent: "center",
            pointerEvents: "none",
            background: "transparent",
          }}
        >
          <div style={{ pointerEvents: "auto" }}>
            <PdfViewerToolbar
              currentPage={scrollState.currentPage}
              totalPages={scrollState.totalPages}
            />
          </div>
        </div>
      )}

      {/* Thumbnail Sidebar */}
      <ThumbnailSidebar
        visible={isThumbnailSidebarVisible}
        onToggle={toggleThumbnailSidebar}
        activeFileId={activeFileId}
      />
      <BookmarkSidebar
        visible={isBookmarkSidebarVisible}
        thumbnailVisible={isThumbnailSidebarVisible}
        documentCacheKey={bookmarkCacheKey}
        preloadCacheKeys={allBookmarkCacheKeys}
      />
      <AttachmentSidebar
        visible={isAttachmentSidebarVisible}
        thumbnailVisible={isThumbnailSidebarVisible}
        bookmarkVisible={isBookmarkSidebarVisible}
        documentCacheKey={bookmarkCacheKey}
        preloadCacheKeys={allBookmarkCacheKeys}
      />
      <LayerSidebar
        visible={isLayerSidebarVisible}
        rightOffset={
          (isThumbnailSidebarVisible ? sidebarWidthRem : 0) +
          (isBookmarkSidebarVisible ? sidebarWidthRem : 0) +
          (isAttachmentSidebarVisible ? sidebarWidthRem : 0)
        }
        file={effectiveFile?.file ?? null}
        documentCacheKey={bookmarkCacheKey}
        onApplyLayers={handleLayerApply}
        onLayersDetected={setHasLayers}
      />
    </Box>
  );
};

const EmbedPdfViewer = (props: EmbedPdfViewerProps) => {
  return <EmbedPdfViewerContent {...props} />;
};

export default EmbedPdfViewer;
