"use client";

import Link from "next/link";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Canvas, PencilBrush, type FabricObject, type IText } from "fabric";
import { toast } from "sonner";
import type { FileDto, ListFilesResponseDto } from "@use2pdf/shared-types";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { apiFetch, ApiRequestError, fetchFileBytes } from "@/lib/api-client";
import { exportPagePng, exportPdf } from "@/lib/editor/export";
import { loadPdfDocument, readPageInfos, renderPageToDataUrl, type PDFDocumentProxy } from "@/lib/editor/pdf-loader";
import { EXTRA_FABRIC_PROPS, displaySize, totalRotation, type PageState } from "@/lib/editor/types";
import { EditorToolbar } from "./editor-toolbar";
import { FormatBar } from "./format-bar";
import { InsertPanel, type InsertApi } from "./insert-panel";
import { PropertiesPanel } from "./properties-panel";
import { ThumbnailBar } from "./thumbnail-bar";

const BG_SCALE = 1.5;
const THUMB_SCALE = 0.12;
const UNDO_LIMIT = 50;
const AUTOSAVE_INTERVAL_MS = 4000;

type Status = "loading" | "ready" | "error";

interface UndoStack {
  undo: string[];
  redo: string[];
}

function autosaveKey(fileId: string) {
  return `u2p-editor:${fileId}`;
}

export function EditorShell({ fileId }: { fileId: string }) {
  const [status, setStatus] = useState<Status>("loading");
  const [errorMessage, setErrorMessage] = useState("");
  const [fileName, setFileName] = useState("document.pdf");

  const bytesRef = useRef<ArrayBuffer | null>(null);
  const pdfRef = useRef<PDFDocumentProxy | null>(null);

  const [pages, setPagesState] = useState<PageState[]>([]);
  const pagesRef = useRef<PageState[]>([]);
  const setPages = useCallback((next: PageState[]) => {
    pagesRef.current = next;
    setPagesState(next);
  }, []);

  const [currentPageId, setCurrentPageId] = useState("");
  const currentIdRef = useRef("");

  const [zoom, setZoomState] = useState(1);
  const zoomRef = useRef(1);

  const [bgUrl, setBgUrl] = useState<string | null>(null);
  const bgCacheRef = useRef(new Map<string, Promise<string>>());
  const [thumbnails, setThumbnails] = useState<Record<string, string>>({});

  const [selected, setSelected] = useState<FabricObject | null>(null);
  const [selectedCount, setSelectedCount] = useState(0);
  const [panelVersion, setPanelVersion] = useState(0);

  const [dirty, setDirtyState] = useState(false);
  const dirtyRef = useRef(false);
  const setDirty = useCallback((v: boolean) => {
    dirtyRef.current = v;
    setDirtyState(v);
  }, []);

  const undoStacksRef = useRef(new Map<string, UndoStack>());
  const undoTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const [undoTick, setUndoTick] = useState(0);

  const [isDrawing, setIsDrawing] = useState(false);
  const [isSaving, setIsSaving] = useState(false);
  const [isExporting, setIsExporting] = useState(false);

  const canvasElRef = useRef<HTMLCanvasElement>(null);
  const fabricRef = useRef<Canvas | null>(null);
  const suppressEventsRef = useRef(false);
  const quotaWarnedRef = useRef(false);

  const currentPage = useMemo(() => pages.find((p) => p.id === currentPageId) ?? null, [pages, currentPageId]);

  // ---------- helpers ----------

  const getPage = useCallback((pageId: string) => pagesRef.current.find((p) => p.id === pageId) ?? null, []);

  const getBackground = useCallback((page: PageState, scale: number): Promise<string> | null => {
    if (page.source.kind !== "original") return null;
    const pdf = pdfRef.current;
    if (!pdf) return null;
    const pageIndex = page.source.pageIndex;
    const key = `${pageIndex}:${totalRotation(page)}:${scale}`;
    let promise = bgCacheRef.current.get(key);
    if (!promise) {
      promise = renderPageToDataUrl(pdf, pageIndex, totalRotation(page), scale);
      bgCacheRef.current.set(key, promise);
    }
    return promise;
  }, []);

  const serializeCanvas = useCallback((): string => {
    const canvas = fabricRef.current;
    return canvas ? JSON.stringify(canvas.toObject(EXTRA_FABRIC_PROPS)) : "{}";
  }, []);

  const persistOverlay = useCallback((): void => {
    const canvas = fabricRef.current;
    const pageId = currentIdRef.current;
    // Never persist while a page switch is loading — the canvas still holds
    // the previous page's objects and would be written to the wrong page.
    if (!canvas || !pageId || suppressEventsRef.current) return;
    const overlay = canvas.toObject(EXTRA_FABRIC_PROPS) as Record<string, unknown>;
    setPages(pagesRef.current.map((p) => (p.id === pageId ? { ...p, overlay } : p)));
  }, [setPages]);

  const ensureUndoBaseline = useCallback(
    (pageId: string) => {
      if (!undoStacksRef.current.has(pageId)) {
        undoStacksRef.current.set(pageId, { undo: [serializeCanvas()], redo: [] });
      }
      setUndoTick((t) => t + 1);
    },
    [serializeCanvas],
  );

  const pushUndoSnapshot = useCallback(() => {
    const stack = undoStacksRef.current.get(currentIdRef.current);
    if (!stack) return;
    const snapshot = serializeCanvas();
    if (stack.undo[stack.undo.length - 1] === snapshot) return;
    stack.undo.push(snapshot);
    if (stack.undo.length > UNDO_LIMIT) stack.undo.shift();
    stack.redo = [];
    setUndoTick((t) => t + 1);
  }, [serializeCanvas]);

  const scheduleUndoSnapshot = useCallback(() => {
    if (undoTimerRef.current) clearTimeout(undoTimerRef.current);
    undoTimerRef.current = setTimeout(pushUndoSnapshot, 300);
  }, [pushUndoSnapshot]);

  const loadSnapshot = useCallback(
    async (snapshot: string) => {
      const canvas = fabricRef.current;
      if (!canvas) return;
      suppressEventsRef.current = true;
      canvas.discardActiveObject();
      await canvas.loadFromJSON(JSON.parse(snapshot) as object);
      canvas.setZoom(zoomRef.current);
      canvas.requestRenderAll();
      suppressEventsRef.current = false;
      setSelected(null);
      setSelectedCount(0);
      setDirty(true);
    },
    [setDirty],
  );

  const openPage = useCallback(
    async (pageId: string, persistCurrent = true) => {
      const canvas = fabricRef.current;
      if (!canvas) return;
      if (persistCurrent && currentIdRef.current) persistOverlay();

      currentIdRef.current = pageId;
      setCurrentPageId(pageId);
      const page = getPage(pageId);
      if (!page) return;

      suppressEventsRef.current = true;
      canvas.discardActiveObject();
      setSelected(null);
      setSelectedCount(0);
      await canvas.loadFromJSON((page.overlay ?? { objects: [] }) as object);
      const disp = displaySize(page);
      canvas.setDimensions({ width: disp.width * zoomRef.current, height: disp.height * zoomRef.current });
      canvas.setZoom(zoomRef.current);
      canvas.requestRenderAll();
      suppressEventsRef.current = false;
      ensureUndoBaseline(pageId);

      setBgUrl(null);
      const bg = getBackground(page, BG_SCALE);
      if (bg) {
        bg.then((url) => {
          if (currentIdRef.current === pageId) setBgUrl(url);
        }).catch(() => toast.error("Could not render this page"));
      }
    },
    [ensureUndoBaseline, getBackground, getPage, persistOverlay],
  );

  const reloadCurrentPageFromState = useCallback(async () => {
    const page = getPage(currentIdRef.current);
    const canvas = fabricRef.current;
    if (!page || !canvas) return;
    suppressEventsRef.current = true;
    canvas.discardActiveObject();
    await canvas.loadFromJSON((page.overlay ?? { objects: [] }) as object);
    canvas.setZoom(zoomRef.current);
    canvas.requestRenderAll();
    suppressEventsRef.current = false;
    setSelected(null);
    setSelectedCount(0);
    pushUndoSnapshot();
    setDirty(true);
  }, [getPage, pushUndoSnapshot, setDirty]);

  // ---------- initial load ----------

  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const [bytes, list] = await Promise.all([
          fetchFileBytes(fileId),
          apiFetch<ListFilesResponseDto>("/files?page=1&pageSize=100").catch(() => null),
        ]);
        if (cancelled) return;
        bytesRef.current = bytes;
        const meta = list?.files.find((f) => f.id === fileId);
        if (meta) setFileName(meta.originalName);

        const pdf = await loadPdfDocument(bytes);
        if (cancelled) return;
        pdfRef.current = pdf;
        const infos = await readPageInfos(pdf);
        if (cancelled) return;

        let initialPages: PageState[] = infos.map((info, i) => ({
          id: crypto.randomUUID(),
          source: { kind: "original", pageIndex: i },
          baseWidth: info.baseWidth,
          baseHeight: info.baseHeight,
          baseRotation: info.baseRotation,
          extraRotation: 0,
          overlay: null,
        }));

        // Restore an autosaved session if one exists and still matches the file.
        try {
          const stored = localStorage.getItem(autosaveKey(fileId));
          if (stored) {
            const parsed = JSON.parse(stored) as { pages?: PageState[] };
            const restorable =
              Array.isArray(parsed.pages) &&
              parsed.pages.length > 0 &&
              parsed.pages.every(
                (p) => p.source.kind === "blank" || (p.source.kind === "original" && p.source.pageIndex < pdf.numPages),
              );
            if (restorable && parsed.pages) {
              initialPages = parsed.pages;
              toast.info("Restored your unsaved edits", {
                action: {
                  label: "Discard",
                  onClick: () => {
                    localStorage.removeItem(autosaveKey(fileId));
                    window.location.reload();
                  },
                },
              });
            }
          }
        } catch {
          // corrupted autosave — start fresh
        }

        setPages(initialPages);
        setStatus("ready");
      } catch (err) {
        if (cancelled) return;
        setErrorMessage(
          err instanceof ApiRequestError ? err.message : "Could not open this PDF. It may be corrupted or password-protected.",
        );
        setStatus("error");
      }
    })();
    return () => {
      cancelled = true;
      void pdfRef.current?.loadingTask.destroy().catch(() => undefined);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [fileId]);

  // ---------- fabric canvas lifecycle ----------

  useEffect(() => {
    if (status !== "ready" || !canvasElRef.current || fabricRef.current) return;

    const canvas = new Canvas(canvasElRef.current, {
      preserveObjectStacking: true,
      selection: true,
    });
    canvas.freeDrawingBrush = new PencilBrush(canvas);
    fabricRef.current = canvas;

    const updateSelection = () => {
      const objs = canvas.getActiveObjects();
      setSelectedCount(objs.length);
      setSelected(objs.length >= 1 ? canvas.getActiveObject() ?? null : null);
      setPanelVersion((v) => v + 1);
    };
    const onObjectChange = () => {
      if (suppressEventsRef.current) return;
      setDirty(true);
      scheduleUndoSnapshot();
      setPanelVersion((v) => v + 1);
    };

    canvas.on("selection:created", updateSelection);
    canvas.on("selection:updated", updateSelection);
    canvas.on("selection:cleared", updateSelection);
    canvas.on("object:added", onObjectChange);
    canvas.on("object:removed", onObjectChange);
    canvas.on("object:modified", onObjectChange);
    canvas.on("text:changed", onObjectChange);

    const first = pagesRef.current[0];
    if (first) void openPage(first.id, false);

    return () => {
      fabricRef.current = null;
      void canvas.dispose();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [status]);

  // ---------- thumbnails ----------

  useEffect(() => {
    let cancelled = false;
    (async () => {
      for (const page of pages) {
        if (page.source.kind !== "original" || thumbnails[page.id]) continue;
        const bg = getBackground(page, THUMB_SCALE);
        if (!bg) continue;
        try {
          const url = await bg;
          if (cancelled) return;
          setThumbnails((prev) => (prev[page.id] ? prev : { ...prev, [page.id]: url }));
        } catch {
          // thumbnail failure is non-fatal
        }
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [pages, thumbnails, getBackground]);

  // ---------- autosave ----------

  useEffect(() => {
    if (status !== "ready") return;
    const timer = setInterval(() => {
      if (!dirtyRef.current) return;
      persistOverlay();
      try {
        localStorage.setItem(autosaveKey(fileId), JSON.stringify({ pages: pagesRef.current, savedAt: Date.now() }));
        setDirty(false);
      } catch {
        if (!quotaWarnedRef.current) {
          quotaWarnedRef.current = true;
          toast.warning("This document is too large to autosave locally — download or save your work manually.");
        }
      }
    }, AUTOSAVE_INTERVAL_MS);
    return () => clearInterval(timer);
  }, [status, fileId, persistOverlay, setDirty]);

  useEffect(() => {
    function onBeforeUnload(e: BeforeUnloadEvent) {
      if (dirtyRef.current) e.preventDefault();
    }
    window.addEventListener("beforeunload", onBeforeUnload);
    return () => window.removeEventListener("beforeunload", onBeforeUnload);
  }, []);

  // ---------- actions ----------

  const undo = useCallback(async () => {
    const stack = undoStacksRef.current.get(currentIdRef.current);
    if (!stack || stack.undo.length <= 1) return;
    const current = stack.undo.pop();
    if (current) stack.redo.push(current);
    await loadSnapshot(stack.undo[stack.undo.length - 1]);
    setUndoTick((t) => t + 1);
  }, [loadSnapshot]);

  const redo = useCallback(async () => {
    const stack = undoStacksRef.current.get(currentIdRef.current);
    if (!stack || stack.redo.length === 0) return;
    const snapshot = stack.redo.pop();
    if (!snapshot) return;
    stack.undo.push(snapshot);
    await loadSnapshot(snapshot);
    setUndoTick((t) => t + 1);
  }, [loadSnapshot]);

  const deleteSelection = useCallback(() => {
    const canvas = fabricRef.current;
    if (!canvas) return;
    const objs = canvas.getActiveObjects();
    if (objs.length === 0) return;
    canvas.discardActiveObject();
    objs.forEach((o) => canvas.remove(o));
    canvas.requestRenderAll();
  }, []);

  const duplicateSelection = useCallback(async () => {
    const canvas = fabricRef.current;
    if (!canvas) return;
    const objs = canvas.getActiveObjects();
    if (objs.length === 0) return;
    canvas.discardActiveObject();
    const clones = await Promise.all(objs.map((o) => o.clone()));
    clones.forEach((clone) => {
      clone.set({ left: (clone.left ?? 0) + 14, top: (clone.top ?? 0) + 14 });
      canvas.add(clone);
    });
    if (clones.length === 1) canvas.setActiveObject(clones[0]);
    canvas.requestRenderAll();
  }, []);

  const setZoomLevel = useCallback(
    (z: number) => {
      zoomRef.current = z;
      setZoomState(z);
      const canvas = fabricRef.current;
      const page = getPage(currentIdRef.current);
      if (!canvas || !page) return;
      const disp = displaySize(page);
      canvas.setDimensions({ width: disp.width * z, height: disp.height * z });
      canvas.setZoom(z);
      canvas.requestRenderAll();
    },
    [getPage],
  );

  const addBlankPage = useCallback(() => {
    persistOverlay();
    const current = getPage(currentIdRef.current);
    const disp = current ? displaySize(current) : { width: 595.28, height: 841.89 };
    const newPage: PageState = {
      id: crypto.randomUUID(),
      source: { kind: "blank" },
      baseWidth: disp.width,
      baseHeight: disp.height,
      baseRotation: 0,
      extraRotation: 0,
      overlay: null,
    };
    const idx = pagesRef.current.findIndex((p) => p.id === currentIdRef.current);
    const next = [...pagesRef.current];
    next.splice(idx + 1, 0, newPage);
    setPages(next);
    setDirty(true);
    void openPage(newPage.id, false);
  }, [getPage, openPage, persistOverlay, setDirty, setPages]);

  const duplicateCurrentPage = useCallback(() => {
    persistOverlay();
    const page = getPage(currentIdRef.current);
    if (!page) return;
    const copy: PageState = {
      ...page,
      id: crypto.randomUUID(),
      overlay: page.overlay ? (JSON.parse(JSON.stringify(page.overlay)) as Record<string, unknown>) : null,
    };
    const idx = pagesRef.current.findIndex((p) => p.id === page.id);
    const next = [...pagesRef.current];
    next.splice(idx + 1, 0, copy);
    setPages(next);
    setDirty(true);
    toast.success("Page duplicated");
  }, [getPage, persistOverlay, setDirty, setPages]);

  const deleteCurrentPage = useCallback(() => {
    if (pagesRef.current.length <= 1) return;
    const idx = pagesRef.current.findIndex((p) => p.id === currentIdRef.current);
    const removed = pagesRef.current[idx];
    const next = pagesRef.current.filter((p) => p.id !== removed.id);
    setPages(next);
    undoStacksRef.current.delete(removed.id);
    setDirty(true);
    const neighbor = next[Math.min(idx, next.length - 1)];
    void openPage(neighbor.id, false);
  }, [openPage, setDirty, setPages]);

  const rotateCurrentPage = useCallback(() => {
    const canvas = fabricRef.current;
    const page = getPage(currentIdRef.current);
    if (!canvas || !page) return;

    const oldDisp = displaySize(page);
    // Remap live objects so they follow the content: rotating the page 90°
    // clockwise moves scene point (x, y) to (oldHeight − y, x).
    suppressEventsRef.current = true;
    canvas.discardActiveObject();
    canvas.getObjects().forEach((obj) => {
      const left = obj.left ?? 0;
      const top = obj.top ?? 0;
      obj.set({ left: oldDisp.height - top, top: left, angle: ((obj.angle ?? 0) + 90) % 360 });
      obj.setCoords();
    });
    suppressEventsRef.current = false;

    const rotated: PageState = { ...page, extraRotation: ((page.extraRotation + 90) % 360) as PageState["extraRotation"] };
    pagesRef.current = pagesRef.current.map((p) => (p.id === page.id ? rotated : p));

    const disp = displaySize(rotated);
    canvas.setDimensions({ width: disp.width * zoomRef.current, height: disp.height * zoomRef.current });
    canvas.requestRenderAll();
    persistOverlay(); // also flushes pagesRef into react state
    pushUndoSnapshot();
    setDirty(true);

    setThumbnails((prev) => {
      const next = { ...prev };
      delete next[page.id];
      return next;
    });
    setBgUrl(null);
    const bg = getBackground(rotated, BG_SCALE);
    if (bg) {
      bg.then((url) => {
        if (currentIdRef.current === rotated.id) setBgUrl(url);
      }).catch(() => undefined);
    }
  }, [getBackground, getPage, persistOverlay, pushUndoSnapshot, setDirty]);

  const reorderPages = useCallback(
    (next: PageState[]) => {
      persistOverlay();
      // persistOverlay replaced pagesRef with fresh objects; keep the new order
      // but take overlay payloads from the persisted array.
      const byId = new Map(pagesRef.current.map((p) => [p.id, p]));
      setPages(next.map((p) => byId.get(p.id) ?? p));
      setDirty(true);
    },
    [persistOverlay, setDirty, setPages],
  );

  // ---------- insert API ----------

  const insertApi: InsertApi = useMemo(
    () => ({
      addObject: (factory) => {
        const canvas = fabricRef.current;
        const page = getPage(currentIdRef.current);
        if (!canvas || !page) return;
        void (async () => {
          try {
            const obj = await factory(displaySize(page));
            canvas.add(obj);
            canvas.setActiveObject(obj);
            canvas.requestRenderAll();
          } catch {
            // factory already reported the error
          }
        })();
      },
      addToAllPages: (factory) => {
        persistOverlay();
        const total = pagesRef.current.length;
        const next = pagesRef.current.map((p, i) => {
          const obj = factory(displaySize(p), i + 1, total);
          const objJson = obj.toObject(EXTRA_FABRIC_PROPS) as Record<string, unknown>;
          const existing = Array.isArray(p.overlay?.objects) ? (p.overlay?.objects as unknown[]) : [];
          return { ...p, overlay: { ...(p.overlay ?? {}), objects: [...existing, objJson] } };
        });
        setPages(next);
        void reloadCurrentPageFromState();
      },
      isDrawing,
      setDrawing: (on, color, width) => {
        const canvas = fabricRef.current;
        if (!canvas) return;
        canvas.isDrawingMode = on;
        if (canvas.freeDrawingBrush) {
          canvas.freeDrawingBrush.color = color;
          canvas.freeDrawingBrush.width = width;
        }
        setIsDrawing(on);
      },
    }),
    [getPage, isDrawing, persistOverlay, reloadCurrentPageFromState, setPages],
  );

  // ---------- export / save ----------

  const downloadBlob = useCallback((blob: Blob, name: string) => {
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = name;
    document.body.appendChild(link);
    link.click();
    link.remove();
    URL.revokeObjectURL(url);
  }, []);

  const buildPdf = useCallback(async (): Promise<Uint8Array> => {
    persistOverlay();
    return exportPdf(bytesRef.current, pagesRef.current);
  }, [persistOverlay]);

  const editedName = fileName.replace(/\.pdf$/i, "") + " (edited).pdf";

  const handleDownload = useCallback(async () => {
    setIsExporting(true);
    try {
      const bytes = await buildPdf();
      downloadBlob(new Blob([bytes as BlobPart], { type: "application/pdf" }), editedName);
    } catch {
      toast.error("Could not export the PDF");
    } finally {
      setIsExporting(false);
    }
  }, [buildPdf, downloadBlob, editedName]);

  const handleSaveToFiles = useCallback(async () => {
    setIsSaving(true);
    try {
      const bytes = await buildPdf();
      const form = new FormData();
      form.append("files", new File([bytes as BlobPart], editedName, { type: "application/pdf" }));
      await apiFetch<{ files: FileDto[] }>("/files/upload", { method: "POST", body: form });
      setDirty(false);
      toast.success(`Saved “${editedName}” to your files`);
    } catch (err) {
      toast.error(err instanceof ApiRequestError ? err.message : "Could not save to your files");
    } finally {
      setIsSaving(false);
    }
  }, [buildPdf, editedName, setDirty]);

  const handlePrint = useCallback(async () => {
    setIsExporting(true);
    try {
      const bytes = await buildPdf();
      const url = URL.createObjectURL(new Blob([bytes as BlobPart], { type: "application/pdf" }));
      const iframe = document.createElement("iframe");
      iframe.style.display = "none";
      iframe.src = url;
      iframe.onload = () => {
        iframe.contentWindow?.print();
        setTimeout(() => {
          iframe.remove();
          URL.revokeObjectURL(url);
        }, 60_000);
      };
      document.body.appendChild(iframe);
    } catch {
      toast.error("Could not prepare the document for printing");
    } finally {
      setIsExporting(false);
    }
  }, [buildPdf]);

  const handleDownloadPagePng = useCallback(async () => {
    const page = getPage(currentIdRef.current);
    if (!page) return;
    persistOverlay();
    const fresh = getPage(currentIdRef.current);
    if (!fresh) return;
    try {
      let background: string;
      const bg = getBackground(fresh, 2);
      if (bg) {
        background = await bg;
      } else {
        const disp = displaySize(fresh);
        const c = document.createElement("canvas");
        c.width = Math.round(disp.width);
        c.height = Math.round(disp.height);
        const ctx = c.getContext("2d");
        if (ctx) {
          ctx.fillStyle = "#ffffff";
          ctx.fillRect(0, 0, c.width, c.height);
        }
        background = c.toDataURL("image/png");
      }
      const pageNo = pagesRef.current.findIndex((p) => p.id === fresh.id) + 1;
      const blob = await exportPagePng(fresh, background);
      downloadBlob(blob, `${fileName.replace(/\.pdf$/i, "")} - page ${pageNo}.png`);
    } catch {
      toast.error("Could not export this page as an image");
    }
  }, [downloadBlob, fileName, getBackground, getPage, persistOverlay]);

  // ---------- keyboard shortcuts ----------

  useEffect(() => {
    function isTypingTarget(e: KeyboardEvent): boolean {
      const t = e.target as HTMLElement | null;
      if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT" || t.isContentEditable)) {
        return true;
      }
      const active = fabricRef.current?.getActiveObject() as IText | null | undefined;
      return Boolean(active && "isEditing" in active && active.isEditing);
    }

    function onKeyDown(e: KeyboardEvent) {
      if (status !== "ready" || isTypingTarget(e)) return;
      const canvas = fabricRef.current;
      const mod = e.ctrlKey || e.metaKey;

      if (mod && e.key.toLowerCase() === "z" && !e.shiftKey) {
        e.preventDefault();
        void undo();
      } else if ((mod && e.key.toLowerCase() === "y") || (mod && e.shiftKey && e.key.toLowerCase() === "z")) {
        e.preventDefault();
        void redo();
      } else if (mod && e.key.toLowerCase() === "d") {
        e.preventDefault();
        void duplicateSelection();
      } else if (mod && e.key.toLowerCase() === "s") {
        e.preventDefault();
        void handleSaveToFiles();
      } else if (e.key === "Delete" || e.key === "Backspace") {
        e.preventDefault();
        deleteSelection();
      } else if (e.key === "Escape") {
        canvas?.discardActiveObject();
        canvas?.requestRenderAll();
      } else if (e.key.startsWith("Arrow")) {
        const objs = canvas?.getActiveObjects() ?? [];
        if (objs.length === 0) return;
        e.preventDefault();
        const step = e.shiftKey ? 10 : 1;
        const dx = e.key === "ArrowLeft" ? -step : e.key === "ArrowRight" ? step : 0;
        const dy = e.key === "ArrowUp" ? -step : e.key === "ArrowDown" ? step : 0;
        objs.forEach((o) => {
          o.set({ left: (o.left ?? 0) + dx, top: (o.top ?? 0) + dy });
          o.setCoords();
        });
        canvas?.requestRenderAll();
        setDirty(true);
        scheduleUndoSnapshot();
      }
    }

    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [status, undo, redo, duplicateSelection, deleteSelection, handleSaveToFiles, scheduleUndoSnapshot, setDirty]);

  // ---------- render ----------

  if (status === "loading") {
    return (
      <div className="flex h-screen items-center justify-center bg-slate-100 dark:bg-slate-900">
        <div className="text-center">
          <Spinner className="mx-auto h-10 w-10 text-brand-600" />
          <p className="mt-3 text-sm text-slate-500 dark:text-slate-400">Opening your document…</p>
        </div>
      </div>
    );
  }

  if (status === "error") {
    return (
      <div className="flex h-screen items-center justify-center bg-slate-100 p-4 dark:bg-slate-900">
        <div className="max-w-md text-center">
          <p className="text-lg font-semibold text-slate-900 dark:text-slate-100">Could not open this file</p>
          <p className="mt-2 text-sm text-slate-500 dark:text-slate-400">{errorMessage}</p>
          <Link href="/editor" className="mt-4 inline-block">
            <Button variant="secondary">Choose another file</Button>
          </Link>
        </div>
      </div>
    );
  }

  const stack = undoStacksRef.current.get(currentPageId);
  void undoTick; // re-render trigger for canUndo/canRedo
  const disp = currentPage ? displaySize(currentPage) : { width: 595, height: 842 };
  const pageIndex = pages.findIndex((p) => p.id === currentPageId);

  return (
    <div className="flex h-screen flex-col overflow-hidden bg-slate-200 dark:bg-slate-900">
      <EditorToolbar
        fileName={fileName}
        dirty={dirty}
        canUndo={Boolean(stack && stack.undo.length > 1)}
        canRedo={Boolean(stack && stack.redo.length > 0)}
        zoom={zoom}
        isSaving={isSaving}
        isExporting={isExporting}
        onUndo={() => void undo()}
        onRedo={() => void redo()}
        onZoom={setZoomLevel}
        onDownload={() => void handleDownload()}
        onDownloadPagePng={() => void handleDownloadPagePng()}
        onPrint={() => void handlePrint()}
        onSaveToFiles={() => void handleSaveToFiles()}
      />

      <FormatBar
        selected={selected}
        version={panelVersion}
        onChange={() => {
          fabricRef.current?.requestRenderAll();
          setDirty(true);
          scheduleUndoSnapshot();
          setPanelVersion((v) => v + 1);
        }}
      />

      <div className="flex min-h-0 flex-1">
        <InsertPanel api={insertApi} />

        <main className="min-w-0 flex-1 overflow-auto p-8">
          <div className="mx-auto w-fit">
            <div
              className="relative bg-white shadow-xl"
              style={{ width: disp.width * zoom, height: disp.height * zoom }}
            >
              {bgUrl && (
                // eslint-disable-next-line @next/next/no-img-element
                <img
                  src={bgUrl}
                  alt=""
                  draggable={false}
                  className="pointer-events-none absolute inset-0 h-full w-full select-none"
                />
              )}
              <div className="absolute inset-0">
                <canvas ref={canvasElRef} />
              </div>
            </div>
          </div>
        </main>

        <PropertiesPanel
          selected={selected}
          selectedCount={selectedCount}
          version={panelVersion}
          page={
            currentPage
              ? { pageNo: pageIndex + 1, pageCount: pages.length, width: disp.width, height: disp.height }
              : null
          }
          onChange={() => {
            fabricRef.current?.requestRenderAll();
            setDirty(true);
            scheduleUndoSnapshot();
            setPanelVersion((v) => v + 1);
          }}
          onDelete={deleteSelection}
          onDuplicate={() => void duplicateSelection()}
          onRotatePage={rotateCurrentPage}
          onDuplicatePage={duplicateCurrentPage}
          onDeletePage={deleteCurrentPage}
        />
      </div>

      <ThumbnailBar
        pages={pages}
        currentPageId={currentPageId}
        thumbnails={thumbnails}
        onSelect={(id) => void openPage(id)}
        onReorder={reorderPages}
        onAddBlankPage={addBlankPage}
      />
    </div>
  );
}
