"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useRouter } from "next/navigation";
import { api } from "@/lib/api";
import { useCollaboration, type RemoteEvent } from "@/hooks/useCollaboration";

// ─────────────────────────────────────────────────────────────────────────────
// Preset color palette  (30 hues, 3 brightness levels × 10 hue families)
// Each color is visually distinct to avoid confusion between collaborators.
// ─────────────────────────────────────────────────────────────────────────────

const PRESET_COLORS = [
  "#e63946", "#9d0208", "#ff6b6b",   // Red
  "#f4572a", "#a23b17", "#ff9472",   // Orange-red
  "#f7b731", "#a07800", "#ffe066",   // Yellow
  "#80b918", "#4a6c0a", "#b5e853",   // Yellow-green
  "#2dc653", "#0a7a2e", "#6ee08a",   // Green
  "#00b4d8", "#005f73", "#48cae4",   // Cyan
  "#4361ee", "#1a237e", "#7b9cff",   // Blue
  "#7209b7", "#3a0068", "#b56aff",   // Purple
  "#e040fb", "#880e62", "#f48fff",   // Magenta
  "#ff6392", "#a3003f", "#ffadc5",   // Rose
] as const;

function userIdToPresetColor(id: number): string {
  return PRESET_COLORS[id % PRESET_COLORS.length];
}

// ─────────────────────────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────────────────────────

type Tool = "select" | "pen" | "highlighter" | "text" | "eraser";
type ViewMode = "pan" | "draw";
type FitMode = "width" | "height";
type ScrollMode = "single" | "continuous";

interface Props {
  pdfId: number;
}

// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────

function hexToRgba(hex: string, alpha: number): string {
  let h = hex.replace("#", "");
  if (h.length === 3) h = h.split("").map((c) => c + c).join("");
  const r = parseInt(h.substring(0, 2), 16);
  const g = parseInt(h.substring(2, 4), 16);
  const b = parseInt(h.substring(4, 6), 16);
  return `rgba(${r},${g},${b},${alpha})`;
}

// ─────────────────────────────────────────────────────────────────────────────
// Toolbar button
// ─────────────────────────────────────────────────────────────────────────────

function ToolBtn({
  id,
  current,
  onClick,
  title,
  children,
}: {
  id: Tool;
  current: Tool;
  onClick: (t: Tool) => void;
  title: string;
  children: React.ReactNode;
}) {
  return (
    <button
      onClick={() => onClick(id)}
      title={title}
      className={`p-1.5 rounded-lg transition ${
        current === id
          ? "bg-blue-600 text-white shadow"
          : "text-gray-500 hover:bg-gray-100 hover:text-gray-800"
      }`}
    >
      <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        {children}
      </svg>
    </button>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Main component
// ─────────────────────────────────────────────────────────────────────────────

export default function WorkbookViewer({ pdfId }: Props) {
  const router = useRouter();

  // DOM refs
  const scrollContainerRef = useRef<HTMLDivElement>(null);
  const pdfCanvasRef = useRef<HTMLCanvasElement>(null);
  const fabricElRef = useRef<HTMLCanvasElement>(null);

  // Library instances (dynamic imports to avoid SSR)
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const fabricRef = useRef<any>(null);
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const fabricNSRef = useRef<any>(null); // fabric namespace object
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const pdfDocRef = useRef<any>(null);

  // Per-page annotation cache (local, not yet flushed to server)
  const localAnnotations = useRef<Record<number, object>>({});
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const addedObjects = useRef<any[]>([]);
  const skipObjectTracking = useRef(false);

  // Mutable refs kept in sync with state (used inside closures/callbacks)
  const currentToolRef = useRef<Tool>("pen");
  const penColorRef = useRef<string>("#e63946");
  const strokeWidthRef = useRef<number>(3);

  // UI state
  const [currentPage, setCurrentPage] = useState(1);
  const [totalPages, setTotalPages] = useState(0);
  const [mode, setViewMode] = useState<ViewMode>("pan");
  const [tool, setTool] = useState<Tool>("pen");
  const [penColor, setPenColor] = useState("#e63946");
  const [strokeWidth, setStrokeWidth] = useState(3);
  const [saving, setSaving] = useState(false);
  const [saveNotice, setSaveNotice] = useState(false);
  const [isReady, setIsReady] = useState(false);
  const [pdfTitle, setPdfTitle] = useState("PDF");
  const [loadError, setLoadError] = useState("");
  const [rendering, setRendering] = useState(false);
  const [showThumbnails, setShowThumbnails] = useState(true);
  const [thumbnails, setThumbnails] = useState<string[]>([]);
  const [fitMode, setFitMode] = useState<FitMode>("width");
  const [scrollMode, setScrollMode] = useState<ScrollMode>("single");

  // Collaboration
  const [collabToken, setCollabToken] = useState<string | null>(null);
  const currentPageRef = useRef(1);  // mutable copy for collab callbacks
  const skipRemoteRef = useRef(false); // prevent echo-back when applying remote events
  const currentUserIdRef = useRef<number | null>(null); // populated from api.me(); used to identify own vs remote objects
  const [userRole, setUserRole] = useState<"admin" | "teacher" | "student">("student");
  const userRoleRef = useRef<"admin" | "teacher" | "student">("student");
  // Buffer for remote events that arrive while renderPageWithAnnotations is in progress
  const renderingRef = useRef(false);
  const pendingRemoteEvents = useRef<RemoteEvent[]>([]);
  // True once api.me() has resolved and pen color has been set from user id
  const [userColorReady, setUserColorReady] = useState(false);

  // Color picker
  const [colorPickerOpen, setColorPickerOpen] = useState(false);
  const [colorPickerPos, setColorPickerPos] = useState<{ top: number; left: number } | null>(null);
  const colorBtnRef = useRef<HTMLButtonElement>(null);
  const colorPickerRef = useRef<HTMLDivElement>(null);

  // Ref for auto-scrolling thumbnail panel
  const thumbRefs = useRef<(HTMLButtonElement | null)[]>([]);
  const fitModeRef = useRef<FitMode>("width");
  const scrollModeRef = useRef<ScrollMode>("single");
  // Per-page canvas refs for continuous mode
  const pageCanvasRefs = useRef<(HTMLCanvasElement | null)[]>([]);
  const pageFabricRefs = useRef<any[]>([]);
  // Active PDF.js render tasks — cancel before re-rendering
  const renderTasksRef = useRef<any[]>([]);
  // Generation counter — incremented on each renderAllPages call to abort stale runs
  const renderAllPagesGenRef = useRef(0);

  // Collab: stable ref to send functions (set after hook initializes)
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const collabSendRef = useRef<{ objectAdd: any; objectRemove: any; clear: any; colorSync: (c: string) => void } | null>(null);

  // Temp-cache sync: debounce unsaved canvas state to Redis (1.5 s after last stroke)
  const tempSyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const syncTempCanvasRef = useRef<(page: number) => void>(() => {});

  // Keep currentPageRef in sync
  useEffect(() => { currentPageRef.current = currentPage; }, [currentPage]);

  // Keep mutable refs in sync
  useEffect(() => { currentToolRef.current = tool; }, [tool]);
  useEffect(() => { penColorRef.current = penColor; }, [penColor]);
  useEffect(() => { strokeWidthRef.current = strokeWidth; }, [strokeWidth]);
  useEffect(() => { fitModeRef.current = fitMode; }, [fitMode]);
  useEffect(() => { scrollModeRef.current = scrollMode; }, [scrollMode]);

  // Keep syncTempCanvasRef current so closures inside Fabric events always see latest pdfId
  useEffect(() => {
    syncTempCanvasRef.current = (page: number) => {
      const fc = fabricRef.current;
      if (!fc) return;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const fullData: any = fc.toJSON(["_owner_id"]);
      const uid = currentUserIdRef.current;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const ownObjects = ((fullData.objects as any[]) ?? []).filter(
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        (o: any) => o._owner_id == null || o._owner_id === uid
      );
      const canvasData = { ...fullData, objects: ownObjects };
      if (tempSyncTimerRef.current) clearTimeout(tempSyncTimerRef.current);
      tempSyncTimerRef.current = setTimeout(() => {
        api.upsertTempAnnotation(pdfId, page, { canvas_data: canvasData }).catch(() => {});
      }, 1500);
    };
  }, [pdfId]);

  // Auto-scroll thumbnail sidebar to keep current page visible
  useEffect(() => {
    if (showThumbnails) {
      thumbRefs.current[currentPage - 1]?.scrollIntoView({ block: "nearest", behavior: "smooth" });
    }
  }, [currentPage, showThumbnails]);

  // Fetch JWT token for WebSocket auth (cookie not sent on WS upgrade)
  useEffect(() => {
    fetch("/api/auth/token", { credentials: "include" })
      .then(r => r.ok ? r.json() : null)
      .then((d: { access_token?: string } | null) => { if (d?.access_token) setCollabToken(d.access_token); })
      .catch(() => {});
  }, []);

  // Fetch current user id — used to distinguish own from remote objects when merging /all
  useEffect(() => {
    api.me().then((u) => {
      currentUserIdRef.current = u.id;
      setUserRole(u.role);
      userRoleRef.current = u.role;
      // Assign deterministic color from preset palette based on user id
      const assigned = userIdToPresetColor(u.id);
      setPenColor(assigned);
      penColorRef.current = assigned;
      setUserColorReady(true);  // unblocks the color-sync broadcast
    }).catch(() => {});
  }, []);

  // Close color picker when clicking outside
  useEffect(() => {
    if (!colorPickerOpen) return;
    const handler = (e: MouseEvent) => {
      if (
        colorPickerRef.current && !colorPickerRef.current.contains(e.target as Node) &&
        colorBtnRef.current && !colorBtnRef.current.contains(e.target as Node)
      ) {
        setColorPickerOpen(false);
        setColorPickerPos(null);
      }
    };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [colorPickerOpen]);

  // Handle incoming remote annotation events
  const onRemoteEvent = useCallback((event: RemoteEvent) => {
    const fabric = fabricNSRef.current;
    if (!fabric) return;

    // Resolve the right fabric canvas for the event's page
    const getCanvas = (page: number) => {
      if (scrollModeRef.current === "continuous") {
        return pageFabricRefs.current[page - 1] ?? null;
      }
      return currentPageRef.current === page ? fabricRef.current : null;
    };

    if (event.type === "clear") {
      const fc = getCanvas(event.page ?? currentPageRef.current);
      if (!fc) return;
      skipRemoteRef.current = true;
      skipObjectTracking.current = true;
      fc.clear(); fc.renderAll();
      skipRemoteRef.current = false;
      skipObjectTracking.current = false;
      return;
    }

    if (event.type === "object_remove") {
      const fc = getCanvas(event.page ?? currentPageRef.current);
      if (!fc) return;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const collab_id = (event.payload as any)?.obj_id;
      if (!collab_id) return;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const target = fc.getObjects().find((o: any) => o.collab_id === collab_id);
      if (target) { skipRemoteRef.current = true; fc.remove(target); fc.renderAll(); skipRemoteRef.current = false; }
      return;
    }

    if (event.type === "object_add") {
      // If a page render is in progress, the canvas is about to be cleared + reloaded.
      // Buffer this event and replay it after renderPageWithAnnotations finishes.
      if (renderingRef.current && scrollModeRef.current === "single") {
        pendingRemoteEvents.current.push(event);
        return;
      }
      const page = event.page ?? currentPageRef.current;
      const fc = getCanvas(page);
      if (!fc) return;
      const objJson = event.payload as Record<string, unknown>;
      skipRemoteRef.current = true;
      skipObjectTracking.current = true;
      fabric.util.enlivenObjects([objJson], (objects: any[]) => {
        const canInteract = userRoleRef.current !== "student";
        objects.forEach((obj: any) => {
          obj.collab_id = objJson.collab_id;
          obj._isRemote = true;  // don't save other users' live strokes under our account
          obj.selectable = canInteract;
          obj.evented = canInteract;
          fc.add(obj);
        });
        fc.renderAll();
        skipRemoteRef.current = false;
        skipObjectTracking.current = false;
      });
      return;
    }
  }, []);

  // When a new peer joins the room, broadcast all our own (non-remote) canvas
  // objects so they receive our pre-existing annotations immediately.
  const handlePeerJoined = useCallback(() => {
    const sendAdd = collabSendRef.current?.objectAdd;
    if (!sendAdd) return;

    const broadcastCanvas = (fc: any, page: number) => {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      fc.getObjects().forEach((obj: any) => {
        // Skip objects from other users — they already have their own
        if (obj._isRemote) return;
        if (!obj.collab_id) obj.collab_id = crypto.randomUUID();
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const json = (obj as any).toJSON(["collab_id"]);
        sendAdd(json, page);
      });
    };

    if (scrollModeRef.current === "continuous") {
      pageFabricRefs.current.forEach((fc, idx) => {
        if (fc) broadcastCanvas(fc, idx + 1);
      });
    } else {
      const fc = fabricRef.current;
      if (fc) broadcastCanvas(fc, currentPageRef.current);
    }
    // Also broadcast our current pen color so the new peer can disable our swatch
    collabSendRef.current?.colorSync(penColorRef.current);
  }, []);

  // Collaboration hook
  const { users: collabUsers, connected: collabConnected, peerColors, sendObjectAdd, sendObjectRemove, sendClear, sendColorSync } =
    useCollaboration({ pdfId, token: collabToken, onEvent: onRemoteEvent, onPeerJoined: handlePeerJoined });

  // Keep send functions accessible in stable refs (used in Fabric event handlers)
  useEffect(() => {
    collabSendRef.current = { objectAdd: sendObjectAdd, objectRemove: sendObjectRemove, clear: sendClear, colorSync: sendColorSync };
  }, [sendObjectAdd, sendObjectRemove, sendClear, sendColorSync]);

  // Broadcast pen color only when BOTH WS is connected AND user color has been fetched.
  // This prevents both users broadcasting the same default "#e63946" before api.me() resolves.
  // Also re-broadcasts whenever the user manually picks a new color.
  useEffect(() => {
    if (collabConnected && userColorReady) {
      sendColorSync(penColor);
    }
  }, [collabConnected, userColorReady, penColor, sendColorSync]);

  // If our color collides with a peer's color, pick the first free preset color and rebroadcast.
  useEffect(() => {
    if (!collabConnected || !userColorReady) return;
    const uid = currentUserIdRef.current;
    if (uid == null) return;

    const taken = new Set(
      Object.entries(peerColors)
        .filter(([id]) => Number(id) !== uid)
        .map(([, c]) => c.toLowerCase())
    );
    const current = penColor.toLowerCase();
    if (!taken.has(current)) return;

    const replacement = PRESET_COLORS.find(c => !taken.has(c.toLowerCase())) ?? penColor;
    if (replacement.toLowerCase() === current) return;

    setPenColor(replacement);
    penColorRef.current = replacement;
    sendColorSync(replacement);
  }, [collabConnected, userColorReady, peerColors, penColor, sendColorSync]);

  // Re-render current page when fit mode changes — moved below renderAllPages definition

  const generateThumbnails = useCallback(async () => {
    const doc = pdfDocRef.current;
    if (!doc) return;
    for (let i = 1; i <= doc.numPages; i++) {
      const page = await doc.getPage(i);
      const vp1 = page.getViewport({ scale: 1 });
      const scale = 120 / vp1.width;
      const vp = page.getViewport({ scale });
      const cvs = document.createElement("canvas");
      cvs.width = Math.floor(vp.width);
      cvs.height = Math.floor(vp.height);
      await page.render({ canvasContext: cvs.getContext("2d")!, viewport: vp }).promise;
      const dataUrl = cvs.toDataURL("image/jpeg", 0.7);
      setThumbnails(prev => {
        const next = [...prev];
        next[i - 1] = dataUrl;
        return next;
      });
    }
  }, []);

  // ─────────────────────────────────────────────────────────────────────────
  // renderPageWithAnnotations
  // ─────────────────────────────────────────────────────────────────────────

  const renderPageWithAnnotations = useCallback(
    async (pageNum: number) => {
      const fc = fabricRef.current;
      const doc = pdfDocRef.current;
      if (!fc || !doc || !pdfCanvasRef.current || !scrollContainerRef.current) return;

      setRendering(true);
      renderingRef.current = true;
      pendingRemoteEvents.current = [];
      try {
        // Cancel any in-progress render on the same canvas
        renderTasksRef.current.forEach(t => { try { t.cancel(); } catch { /* ignore */ } });
        renderTasksRef.current = [];

        // ── 1. Render PDF page ─────────────────────────────────────────────
        const page = await doc.getPage(pageNum);
        const viewport1 = page.getViewport({ scale: 1 });
        let scale: number;
        if (fitModeRef.current === "height") {
          const containerHeight = Math.max(scrollContainerRef.current.clientHeight - 48, 400);
          scale = containerHeight / viewport1.height;
        } else {
          const containerWidth = Math.max(scrollContainerRef.current.clientWidth - 32, 300);
          scale = containerWidth / viewport1.width;
        }
        const viewport = page.getViewport({ scale });

        const pdfCvs = pdfCanvasRef.current;
        pdfCvs.width = Math.floor(viewport.width);
        pdfCvs.height = Math.floor(viewport.height);

        const singleRenderTask = page.render({
          canvasContext: pdfCvs.getContext("2d")!,
          viewport,
        });
        renderTasksRef.current.push(singleRenderTask);
        await singleRenderTask.promise;
        // ── 2. Resize Fabric canvas to match
        fc.setWidth(pdfCvs.width);
        fc.setHeight(pdfCvs.height);
        if (fc.wrapperEl) {
          fc.wrapperEl.style.width = `${pdfCvs.width}px`;
          fc.wrapperEl.style.height = `${pdfCvs.height}px`;
        }

        // ── 3. Load annotation data ───────────────────────────────────────
        const cached = localAnnotations.current[pageNum];
        let annotationData: object | null = cached ?? null;

        if (!annotationData) {
          try {
            const ann = await api.getAllAnnotations(pdfId, pageNum);
            if (ann.canvas_data && Object.keys(ann.canvas_data).length > 0) {
              annotationData = ann.canvas_data;
              localAnnotations.current[pageNum] = annotationData;
            }
          } catch { /* no annotation yet */ }
        }

        // Clear and load
        skipObjectTracking.current = true;
        fc.clear();
        addedObjects.current = [];

        if (annotationData) {
          await new Promise<void>((resolve) => {
            fc.loadFromJSON(annotationData, () => {
              // Mark objects from other users as remote — prevents saving them under current user
              const uid = currentUserIdRef.current;
              if (uid !== null) {
                const canInteract = userRoleRef.current !== "student";
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                fc.getObjects().forEach((obj: any) => {
                  if (obj._owner_id != null && obj._owner_id !== uid) {
                    obj._isRemote = true;
                    obj.selectable = canInteract;
                    obj.evented = canInteract;
                  }
                });
              }
              fc.renderAll();
              skipObjectTracking.current = false;
              resolve();
            });
          });
        } else {
          fc.renderAll();
          skipObjectTracking.current = false;
        }

        // Replay any remote events that arrived while the canvas was being loaded
        renderingRef.current = false;
        const queued = pendingRemoteEvents.current.splice(0);
        const fabric = fabricNSRef.current;
        if (fabric && queued.length > 0) {
          for (const evt of queued) {
            if (evt.type !== "object_add") continue;
            if ((evt.page ?? currentPageRef.current) !== pageNum) continue;
            const objJson = evt.payload as Record<string, unknown>;
            skipRemoteRef.current = true;
            skipObjectTracking.current = true;
            fabric.util.enlivenObjects([objJson], (objects: any[]) => {
              const canInteract = userRoleRef.current !== "student";
              objects.forEach((obj: any) => {
                obj.collab_id = objJson.collab_id;
                obj._isRemote = true;
                obj.selectable = canInteract;
                obj.evented = canInteract;
                fc.add(obj);
              });
              fc.renderAll();
              skipRemoteRef.current = false;
              skipObjectTracking.current = false;
            });
          }
        }
      } finally {
        renderingRef.current = false;
        setRendering(false);
      }
    },
    [pdfId]
  );

  // ─────────────────────────────────────────────────────────────────────────
  // renderAllPages  (continuous scroll mode)
  // ─────────────────────────────────────────────────────────────────────────

  const renderAllPages = useCallback(async () => {
    const gen = ++renderAllPagesGenRef.current;
    const doc = pdfDocRef.current;
    const container = scrollContainerRef.current;
    if (!doc || !container) return;

    // Dispose old per-page fabric instances
    pageFabricRefs.current.forEach(fc => { try { fc?.dispose(); } catch { /* ignore */ } });
    pageFabricRefs.current = [];

    // Cancel any in-progress render tasks
    renderTasksRef.current.forEach(t => { try { t.cancel(); } catch { /* ignore */ } });
    renderTasksRef.current = [];

    const fabricModule = await import("fabric");
    if (renderAllPagesGenRef.current !== gen) return;
    const fabric = fabricModule.fabric;
    const containerWidth = Math.max(container.clientWidth - 32, 300);

    for (let i = 1; i <= doc.numPages; i++) {
      if (renderAllPagesGenRef.current !== gen) return;
      const pdfCvs = pageCanvasRefs.current[i - 1];
      const fabricEl = document.getElementById(`fabric-continuous-${i}`) as HTMLCanvasElement | null;
      if (!pdfCvs || !fabricEl) continue;

      const page = await doc.getPage(i);
      const vp1 = page.getViewport({ scale: 1 });
      let scale: number;
      if (fitModeRef.current === "height") {
        const containerHeight = Math.max(container.clientHeight - 48, 400);
        scale = containerHeight / vp1.height;
      } else {
        scale = containerWidth / vp1.width;
      }
      const vp = page.getViewport({ scale });
      pdfCvs.width = Math.floor(vp.width);
      pdfCvs.height = Math.floor(vp.height);
      const contRenderTask = page.render({ canvasContext: pdfCvs.getContext("2d")!, viewport: vp });
      renderTasksRef.current.push(contRenderTask);
      try {
        await contRenderTask.promise;
      } catch (e: any) {
        if (e?.name === "RenderingCancelledException") return;
        throw e;
      }
      if (renderAllPagesGenRef.current !== gen) return;

      // Create Fabric canvas overlay
      const fc = new fabric.Canvas(fabricEl, {
        isDrawingMode: false,
        selection: false,
        enableRetinaScaling: false,
      });
      fc.setWidth(pdfCvs.width);
      fc.setHeight(pdfCvs.height);
      if (fc.wrapperEl) {
        fc.wrapperEl.style.position = "absolute";
        fc.wrapperEl.style.top = "0";
        fc.wrapperEl.style.left = "0";
        fc.wrapperEl.style.pointerEvents = "none";
      }
      pageFabricRefs.current[i - 1] = fc;

      // Load saved annotations
      const cached = localAnnotations.current[i];
      let annotationData: object | null = cached ?? null;
      if (!annotationData) {
        try {
          const ann = await api.getAllAnnotations(pdfId, i);
          if (ann.canvas_data && Object.keys(ann.canvas_data).length > 0) {
            annotationData = ann.canvas_data;
            localAnnotations.current[i] = annotationData;
          }
        } catch { /* no annotation */ }
      }
      if (annotationData) {
        await new Promise<void>((resolve) => {
          fc.loadFromJSON(annotationData, () => {
            // Mark objects from other users as remote
            const uid = currentUserIdRef.current;
            if (uid !== null) {
              const canInteract = userRoleRef.current !== "student";
              // eslint-disable-next-line @typescript-eslint/no-explicit-any
              fc.getObjects().forEach((obj: any) => {
                if (obj._owner_id != null && obj._owner_id !== uid) {
                  obj._isRemote = true;
                  obj.selectable = canInteract;
                  obj.evented = canInteract;
                }
              });
            }
            fc.renderAll();
            resolve();
          });
        });
      }
    }

  }, [pdfId]);

  // Re-render when fit mode changes
  useEffect(() => {
    if (!isReady) return;
    if (scrollMode === "continuous") {
      renderAllPages();
    } else {
      renderPageWithAnnotations(currentPage);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [fitMode]);

  // ─────────────────────────────────────────────────────────────────────────
  // Initialization (mount)
  // ─────────────────────────────────────────────────────────────────────────

  useEffect(() => {
    let cancelled = false;

    async function init() {
      try {
        // Fetch PDF title
        try {
          const pdfs = await api.listPdfs();
          const found = pdfs.find((p) => p.id === pdfId);
          if (found) setPdfTitle(found.title);
        } catch { /* ignore */ }

        if (cancelled || !fabricElRef.current) return;

        // ── Init Fabric.js ────────────────────────────────────────────────
        const fabricModule = await import("fabric");
        const fabric = fabricModule.fabric;
        fabricNSRef.current = fabric;

        if (cancelled) return;

        const fc = new fabric.Canvas(fabricElRef.current, {
          isDrawingMode: false,
          selection: false,
          enableRetinaScaling: false,
        });
        fabricRef.current = fc;

        // Position Fabric wrapper absolutely over PDF canvas
        if (fc.wrapperEl) {
          fc.wrapperEl.style.position = "absolute";
          fc.wrapperEl.style.top = "0";
          fc.wrapperEl.style.left = "0";
          fc.wrapperEl.style.pointerEvents = "none"; // start in pan mode
        }

        // Single object:added handler: undo tracking + collab broadcast for text
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        fc.on("object:added", (e: any) => {
          if (skipObjectTracking.current) return;
          addedObjects.current.push(e.target);
          if (skipRemoteRef.current) return;
          const obj = e.target;
          // Text objects: broadcast final content when editing ends, not the placeholder
          if (obj.type === "i-text" || obj.type === "text") {
            obj.once("editing:exited", () => {
              if (skipRemoteRef.current) return;
              if (!obj.collab_id) obj.collab_id = crypto.randomUUID();
              // eslint-disable-next-line @typescript-eslint/no-explicit-any
              const json = (obj as any).toJSON(["collab_id"]);
              collabSendRef.current?.objectAdd(json, currentPageRef.current);
            });
          }
        });

        // Highlighter: reduce opacity of drawn path after creation
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        fc.on("path:created", (options: any) => {
          if (currentToolRef.current === "highlighter") {
            options.path.set({ opacity: 0.42 });
            fc.renderAll();
          }
          // Broadcast stroke to collaborators
          if (!skipRemoteRef.current) {
            options.path.collab_id = crypto.randomUUID();
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const obj = (options.path as any).toJSON(["collab_id"]);
            collabSendRef.current?.objectAdd(obj, currentPageRef.current);
          }
          // Sync unsaved canvas to Redis so late-joining users see this stroke
          syncTempCanvasRef.current(currentPageRef.current);
        });

        // Object removed (eraser / undo) — sync temp cache
        fc.on("object:removed", () => {
          if (skipObjectTracking.current || skipRemoteRef.current) return;
          syncTempCanvasRef.current(currentPageRef.current);
        });

        // ── Init PDF.js ───────────────────────────────────────────────────
        const pdfjs = await import("pdfjs-dist");
        pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;

        if (cancelled) return;

        const doc = await pdfjs
          .getDocument({ url: `/api/pdfs/${pdfId}/file`, withCredentials: true })
          .promise;

        if (cancelled) { doc.destroy(); return; }

        pdfDocRef.current = doc;
        setTotalPages(doc.numPages);
        setIsReady(true);

        // ── Render first page ─────────────────────────────────────────────
        await renderPageWithAnnotations(1);
        setCurrentPage(1);

        // Generate thumbnails in background (non-blocking)
        setThumbnails([]);
        generateThumbnails();

      } catch (err: unknown) {
        if (!cancelled) {
          setLoadError(
            err instanceof Error ? err.message : "Failed to load PDF."
          );
        }
      }
    }

    init();

    return () => {
      cancelled = true;
      fabricRef.current?.dispose();
      fabricRef.current = null;
      pageFabricRefs.current.forEach(fc => { try { fc?.dispose(); } catch { /* ignore */ } });
      pageFabricRefs.current = [];
      pdfDocRef.current?.destroy();
      pdfDocRef.current = null;
    };
  }, [pdfId, renderPageWithAnnotations, generateThumbnails, renderAllPages]);

  // Re-render when switching scroll modes
  useEffect(() => {
    if (!isReady) return;
    if (scrollMode === "continuous") {
      // small delay so DOM mounts the continuous page elements first
      setTimeout(() => renderAllPages(), 50);
    } else {
      // Dispose continuous fabric instances
      pageFabricRefs.current.forEach(fc => { try { fc?.dispose(); } catch { /* ignore */ } });
      pageFabricRefs.current = [];
      renderPageWithAnnotations(currentPage);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [scrollMode]);

  // Scroll-based page tracking for continuous mode
  useEffect(() => {
    if (!isReady || scrollMode !== "continuous") return;
    const container = scrollContainerRef.current;
    if (!container) return;

    const updateCurrentPage = () => {
      const containerRect = container.getBoundingClientRect();
      let bestPage = 1;
      let bestOverlap = -1;
      pageCanvasRefs.current.forEach((cvs, idx) => {
        if (!cvs || cvs.height === 0) return;
        const rect = cvs.getBoundingClientRect();
        const top = Math.max(rect.top, containerRect.top);
        const bottom = Math.min(rect.bottom, containerRect.bottom);
        const overlap = Math.max(0, bottom - top);
        if (overlap > bestOverlap) {
          bestOverlap = overlap;
          bestPage = idx + 1;
        }
      });
      if (bestOverlap > 0) setCurrentPage(bestPage);
    };

    container.addEventListener("scroll", updateCurrentPage, { passive: true });
    return () => container.removeEventListener("scroll", updateCurrentPage);
  }, [isReady, scrollMode]);

  // ─────────────────────────────────────────────────────────────────────────
  // Apply tool / mode to Fabric canvas
  // ─────────────────────────────────────────────────────────────────────────

  useEffect(() => {
    const fc = fabricRef.current;
    const fabric = fabricNSRef.current;
    if (!isReady || !fc || !fabric) return;

    // Clean up previous mouse:down handler (added for text tool)
    fc.off("mouse:down");

    if (mode === "pan") {
      fc.isDrawingMode = false;
      fc.selection = false;
      if (fc.wrapperEl) fc.wrapperEl.style.pointerEvents = "none";
      return;
    }

    // Draw mode — enable pointer events
    if (fc.wrapperEl) fc.wrapperEl.style.pointerEvents = "all";

    switch (tool) {
      case "select":
        fc.isDrawingMode = false;
        fc.selection = true;
        break;

      case "pen": {
        fc.isDrawingMode = true;
        fc.selection = false;
        const brush = new fabric.PencilBrush(fc);
        brush.color = penColor;
        brush.width = strokeWidth;
        fc.freeDrawingBrush = brush;
        break;
      }

      case "highlighter": {
        fc.isDrawingMode = true;
        fc.selection = false;
        const hBrush = new fabric.PencilBrush(fc);
        // Full-opacity color during drawing; path:created handler applies opacity:0.42
        hBrush.color = hexToRgba(penColor, 0.99);
        hBrush.width = strokeWidth * 7;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        (hBrush as any).strokeLineCap = "square";
        fc.freeDrawingBrush = hBrush;
        break;
      }

      case "text":
        fc.isDrawingMode = false;
        fc.selection = false;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        fc.on("mouse:down", (options: any) => {
          if (options.target) return; // clicked existing object → let Fabric handle it
          const pointer = fc.getPointer(options.e);
          const textObj = new fabric.IText("Text", {
            left: pointer.x,
            top: pointer.y,
            fontSize: 20,
            fill: penColorRef.current,
            fontFamily: "Arial, sans-serif",
            padding: 4,
          });
          fc.add(textObj);
          fc.setActiveObject(textObj);
          textObj.enterEditing();
          textObj.selectAll();
          fc.renderAll();
        });
        break;

      case "eraser":
        fc.isDrawingMode = false;
        fc.selection = false;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        fc.on("mouse:down", (options: any) => {
          if (!options.target) return;
          const obj = options.target as any;
          const ownerId: number | undefined = obj._owner_id;
          const uid = currentUserIdRef.current;
          const role = userRoleRef.current;
          // Students can only erase their own annotations
          if (role === "student" && (obj._isRemote || (ownerId != null && ownerId !== uid))) return;
          const collab_id = obj.collab_id as string | undefined;
          fc.remove(obj);
          addedObjects.current = addedObjects.current.filter(o => o !== obj);
          fc.renderAll();
          if (collab_id) collabSendRef.current?.objectRemove(collab_id, currentPageRef.current);
        });
        break;
    }
  }, [isReady, mode, tool, penColor, strokeWidth]);

  // Apply tool/mode to continuous-mode per-page fabric canvases too
  useEffect(() => {
    if (!isReady || scrollMode !== "continuous") return;
    const fabric = fabricNSRef.current;
    if (!fabric) return;
    pageFabricRefs.current.forEach((fc) => {
      if (!fc) return;
      fc.off("mouse:down");
      if (mode === "pan") {
        fc.isDrawingMode = false;
        fc.selection = false;
        if (fc.wrapperEl) fc.wrapperEl.style.pointerEvents = "none";
        return;
      }
      if (fc.wrapperEl) fc.wrapperEl.style.pointerEvents = "all";
      switch (tool) {
        case "select": fc.isDrawingMode = false; fc.selection = true; break;
        case "pen": {
          fc.isDrawingMode = true; fc.selection = false;
          const b = new fabric.PencilBrush(fc); b.color = penColor; b.width = strokeWidth; fc.freeDrawingBrush = b; break;
        }
        case "highlighter": {
          fc.isDrawingMode = true; fc.selection = false;
          const hb = new fabric.PencilBrush(fc); hb.color = hexToRgba(penColor, 0.99); hb.width = strokeWidth * 7;
          (hb as any).strokeLineCap = "square"; fc.freeDrawingBrush = hb; break;
        }
        case "text":
          fc.isDrawingMode = false; fc.selection = false;
          fc.on("mouse:down", (options: any) => {
            if (options.target) return;
            const pointer = fc.getPointer(options.e);
            const t = new fabric.IText("Text", { left: pointer.x, top: pointer.y, fontSize: 20, fill: penColorRef.current, fontFamily: "Arial, sans-serif", padding: 4 });
            fc.add(t); fc.setActiveObject(t); t.enterEditing(); t.selectAll(); fc.renderAll();
          }); break;
        case "eraser":
          fc.isDrawingMode = false; fc.selection = false;
          fc.on("mouse:down", (options: any) => {
            if (!options.target) return;
            const obj = options.target as any;
            const ownerId: number | undefined = obj._owner_id;
            const uid = currentUserIdRef.current;
            const role = userRoleRef.current;
            // Students can only erase their own annotations
            if (role === "student" && (obj._isRemote || (ownerId != null && ownerId !== uid))) return;
            const collab_id = obj.collab_id as string | undefined;
            fc.remove(obj); fc.renderAll();
            if (collab_id) collabSendRef.current?.objectRemove(collab_id, currentPageRef.current);
          }); break;
      }
    });
  }, [isReady, scrollMode, mode, tool, penColor, strokeWidth]);

  // ─────────────────────────────────────────────────────────────────────────
  // goToPage — in continuous mode, scroll to the page element

  const goToPage = useCallback(
    async (newPage: number) => {
      if (newPage < 1 || newPage > totalPages || rendering) return;

      if (scrollMode === "continuous") {
        // Scroll the page wrapper into view
        const el = pageCanvasRefs.current[newPage - 1]?.parentElement;
        el?.scrollIntoView({ behavior: "smooth", block: "start" });
        setCurrentPage(newPage);
        return;
      }

      const fc = fabricRef.current;
      if (!fc) return;
      // Preserve _owner_id so remote-object detection works on cache hits
      localAnnotations.current[currentPage] = fc.toJSON(["_owner_id"]);
      await renderPageWithAnnotations(newPage);
      setCurrentPage(newPage);
    },
    [currentPage, totalPages, rendering, scrollMode, renderPageWithAnnotations]
  );

  // ─────────────────────────────────────────────────────────────────────────
  // Save
  // ─────────────────────────────────────────────────────────────────────────

  const handleSave = useCallback(async () => {
    const fc = scrollMode === "continuous"
      ? pageFabricRefs.current[currentPage - 1]
      : fabricRef.current;
    if (!fc || !isReady || saving) return;

    setSaving(true);
    // Include _owner_id in serialization so we can filter remote objects
    const fullCanvasData = fc.toJSON(["_owner_id"]);
    // Only save objects that belong to the current user (no _owner_id = own; _owner_id === uid = own)
    const uid = currentUserIdRef.current;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const ownObjects = (fullCanvasData.objects as any[] ?? []).filter(
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (o: any) => o._owner_id == null || o._owner_id === uid
    );
    const canvasData = { ...fullCanvasData, objects: ownObjects };

    try {
      await api.upsertAnnotation(pdfId, currentPage, { canvas_data: canvasData });
      // Data is now in DB — cancel any pending debounce (backend deletes Redis key too)
      if (tempSyncTimerRef.current) { clearTimeout(tempSyncTimerRef.current); tempSyncTimerRef.current = null; }
      // Cache the full display state (including remote objects) so the page looks correct on revisit
      localAnnotations.current[currentPage] = fullCanvasData;
      setSaveNotice(true);
      setTimeout(() => setSaveNotice(false), 2500);
    } catch (err: unknown) {
      alert(err instanceof Error ? err.message : "Save failed.");
    } finally {
      setSaving(false);
    }
  }, [pdfId, currentPage, isReady, saving]);

  // ─────────────────────────────────────────────────────────────────────────
  // Undo / Clear
  // ─────────────────────────────────────────────────────────────────────────

  const handleUndo = useCallback(() => {
    const fc = fabricRef.current;
    if (!fc || addedObjects.current.length === 0) return;
    const last = addedObjects.current.pop();
    if (last) { fc.remove(last); fc.renderAll(); }
  }, []);

  const handleClear = useCallback(() => {
    const fc = fabricRef.current;
    if (!fc) return;

    const role = userRoleRef.current;
    const uid = currentUserIdRef.current;

    if (role === "student") {
      // Students: only remove their own objects, leave others untouched
      if (!confirm("Xoá annotation của bạn trên trang này?")) return;
      const toRemove = fc.getObjects().filter((o: any) => !o._isRemote && (o._owner_id == null || o._owner_id === uid));
      toRemove.forEach((o: any) => {
        fc.remove(o);
        // Broadcast removal so teacher canvas updates in real time
        const collab_id = o.collab_id as string | undefined;
        if (collab_id) collabSendRef.current?.objectRemove(collab_id, currentPageRef.current);
      });
      addedObjects.current = addedObjects.current.filter(o => toRemove.indexOf(o) === -1);
      fc.renderAll();
      // Sync own cleared state to Redis
      syncTempCanvasRef.current(currentPageRef.current);
    } else {
      // Teacher / Admin: clear everything
      if (!confirm("Xoá tất cả annotation trên trang này?")) return;
      fc.clear();
      addedObjects.current = [];
      fc.renderAll();
      collabSendRef.current?.clear(currentPageRef.current);
      syncTempCanvasRef.current(currentPageRef.current);
    }
  }, []);

  // ─────────────────────────────────────────────────────────────────────────
  // Render
  // ─────────────────────────────────────────────────────────────────────────

  if (loadError) {
    return (
      <div className="min-h-screen flex flex-col items-center justify-center gap-4 px-4">
        <p className="text-red-600 text-sm bg-red-50 border border-red-200 rounded-lg px-4 py-3">
          {loadError}
        </p>
        <button
          onClick={() => router.push("/dashboard")}
          className="text-blue-600 hover:underline text-sm"
        >
          ← Back to Dashboard
        </button>
      </div>
    );
  }

  return (
    <div className="h-screen flex flex-col overflow-hidden bg-gray-100">

      {/* ── Sticky Toolbar ──────────────────────────────────────────────────── */}
      <header className="bg-white border-b shadow-sm sticky top-0 z-20">
        <div className="px-3 py-2 flex items-center gap-0">

          {/* Scrollable tools strip */}
          <div className="flex items-center gap-2 overflow-x-auto flex-1 min-w-0">

            {/* Back button + title */}
            <button
              onClick={() => router.push("/dashboard")}
              title="Back"
              className="p-1 text-gray-500 hover:text-blue-600 transition rounded flex-shrink-0"
            >
              <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
              </svg>
            </button>
            <span className="text-sm font-semibold text-gray-800 truncate max-w-[100px] flex-shrink-0">
              {pdfTitle}
            </span>

            <div className="h-5 w-px bg-gray-200 flex-shrink-0" />

            {/* Thumbnail toggle */}
            <button
              onClick={() => setShowThumbnails(v => !v)}
              title="Thumbnails"
              className={`p-1.5 rounded-lg transition flex items-center gap-1 flex-shrink-0 ${
                showThumbnails ? "bg-blue-50 text-blue-600" : "text-gray-500 hover:bg-gray-100 hover:text-gray-800"
              }`}
            >
              <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
                  d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
              </svg>
            </button>

            <div className="h-5 w-px bg-gray-200 flex-shrink-0" />

            {/* Fit mode */}
            <div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5 flex-shrink-0">
              <button
                onClick={() => setFitMode("width")}
                title="Fit Width"
                className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${
                  fitMode === "width" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
                }`}
              >
                <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h8M8 12h8M4 17h16" />
                </svg>
                <span className="hidden lg:inline">Width</span>
              </button>
              <button
                onClick={() => setFitMode("height")}
                title="Fit Height"
                className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${
                  fitMode === "height" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
                }`}
              >
                <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8V4m0 0H3m4 0l-4 4M17 8V4m0 0h4m-4 0l4 4M7 16v4m0 0H3m4 0l-4-4M17 16v4m0 0h4m-4 0l4-4" />
                </svg>
                <span className="hidden lg:inline">Height</span>
              </button>
            </div>

            <div className="h-5 w-px bg-gray-200 flex-shrink-0" />

            {/* Scroll mode */}
            <div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5 flex-shrink-0">
              <button
                onClick={() => setScrollMode("single")}
                title="Single page"
                className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${
                  scrollMode === "single" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
                }`}
              >
                <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6M9 8h6M9 16h6M5 4h14a1 1 0 011 1v14a1 1 0 01-1 1H5a1 1 0 01-1-1V5a1 1 0 011-1z" />
                </svg>
                <span className="hidden lg:inline">Single</span>
              </button>
              <button
                onClick={() => setScrollMode("continuous")}
                title="Continuous scroll"
                className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${
                  scrollMode === "continuous" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
                }`}
              >
                <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
                </svg>
                <span className="hidden lg:inline">Continuous</span>
              </button>
            </div>

            <div className="h-5 w-px bg-gray-200 flex-shrink-0" />

            {/* Page navigation */}
            <div className="flex items-center gap-0.5 flex-shrink-0">
              <button
                onClick={() => goToPage(currentPage - 1)}
                disabled={currentPage <= 1 || rendering || !isReady}
                className="p-1 rounded hover:bg-gray-100 disabled:opacity-40 transition"
                title="Previous page"
              >
                <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
                </svg>
              </button>
              {isReady ? (
                <span className="flex items-center gap-1 text-xs text-gray-600 tabular-nums">
                  <input
                    type="number"
                    min={1}
                    max={totalPages}
                    value={currentPage}
                    onChange={(e) => {
                      const v = Number(e.target.value);
                      if (v >= 1 && v <= totalPages) goToPage(v);
                    }}
                    onFocus={(e) => e.target.select()}
                    className="w-10 text-center text-xs border border-gray-300 rounded px-1 py-0.5 focus:outline-none focus:border-blue-500 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
                  />
                  <span>/ {totalPages}</span>
                </span>
              ) : (
                <span className="text-xs text-gray-600 px-1">…</span>
              )}
              <button
                onClick={() => goToPage(currentPage + 1)}
                disabled={currentPage >= totalPages || rendering || !isReady}
                className="p-1 rounded hover:bg-gray-100 disabled:opacity-40 transition"
                title="Next page"
              >
                <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
                </svg>
              </button>
            </div>

            <div className="h-5 w-px bg-gray-200 flex-shrink-0" />

            {/* Pan / Draw mode toggle */}
            <div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5 flex-shrink-0">
              <button
                onClick={() => setViewMode("pan")}
                className={`text-xs px-2.5 py-1 rounded-md font-medium transition ${
                  mode === "pan" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
                }`}
              >
                Pan
              </button>
              <button
                onClick={() => setViewMode("draw")}
                className={`text-xs px-2.5 py-1 rounded-md font-medium transition ${
                  mode === "draw" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
                }`}
              >
                Draw
              </button>
            </div>

            {/* Drawing tools — inline, visible only in draw mode */}
            {mode === "draw" && (
              <>
                <div className="h-5 w-px bg-gray-200 flex-shrink-0" />

                <div className="flex items-center gap-0.5 flex-shrink-0">
                  <ToolBtn id="select" current={tool} onClick={setTool} title="Select / Move">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
                  </ToolBtn>
                  <ToolBtn id="pen" current={tool} onClick={setTool} title="Pen">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
                      d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
                  </ToolBtn>
                  <ToolBtn id="highlighter" current={tool} onClick={setTool} title="Highlighter">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
                      d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
                  </ToolBtn>
                  <ToolBtn id="text" current={tool} onClick={setTool} title="Text">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
                      d="M9 12h6m-3-3v6M7 20h10a2 2 0 002-2V6a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z" />
                  </ToolBtn>
                  <ToolBtn id="eraser" current={tool} onClick={setTool} title="Eraser">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
                      d="M19 7l-1.5 1.5M4.5 19.5l9-9m0 0L9 6l-4.5 4.5 4.5 4.5m4.5-4.5L18 6" />
                  </ToolBtn>
                </div>

                {/* Color picker button */}
                <button
                  ref={colorBtnRef}
                  onClick={() => {
                    if (colorPickerOpen) {
                      setColorPickerOpen(false);
                      setColorPickerPos(null);
                    } else {
                      const rect = colorBtnRef.current!.getBoundingClientRect();
                      setColorPickerPos({ top: rect.bottom + 6, left: rect.left });
                      setColorPickerOpen(true);
                    }
                  }}
                  title="Pen color"
                  className="w-7 h-7 rounded-full border-2 border-white shadow flex-shrink-0 hover:scale-110 transition-transform"
                  style={{ background: penColor, outline: `2px solid ${penColor}`, outlineOffset: 2 }}
                />

                <input
                  type="range"
                  min={1}
                  max={12}
                  value={strokeWidth}
                  onChange={(e) => setStrokeWidth(Number(e.target.value))}
                  className="w-20 accent-blue-600 flex-shrink-0"
                  title={`Stroke width: ${strokeWidth}`}
                />

                <div className="h-5 w-px bg-gray-200 flex-shrink-0" />

                <button
                  onClick={handleUndo}
                  title="Undo last stroke"
                  className="p-1.5 text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition flex-shrink-0"
                >
                  <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
                      d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
                  </svg>
                </button>

                <button
                  onClick={handleClear}
                  title="Clear page annotations"
                  className="p-1.5 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition flex-shrink-0"
                >
                  <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
                      d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M4 7h16M10 3h4a1 1 0 011 1v1H9V4a1 1 0 011-1z" />
                  </svg>
                </button>
              </>
            )}
          </div>

          {/* Save — fixed right, never scrolls away */}
          <div className="flex items-center gap-2 pl-3 flex-shrink-0">

            {/* Collaboration presence */}
            <div className="flex items-center gap-1 flex-shrink-0" title={collabConnected ? `${collabUsers.length} user(s) connected` : "Connecting…"}>
              <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${collabConnected ? "bg-green-500" : "bg-gray-300"}`} />
              {collabUsers.length > 0 && (
                <div className="flex -space-x-1">
                  {collabUsers.slice(0, 5).map(u => {
                    const isSelf = u.user_id === currentUserIdRef.current;
                    const canClear = !isSelf && (userRole === "admin" || userRole === "teacher");
                    return (
                      <div key={u.user_id} className="relative group">
                        <span
                          title={u.username}
                          style={{ background: u.color }}
                          className="w-5 h-5 rounded-full border-2 border-white flex items-center justify-center text-[9px] text-white font-bold uppercase select-none"
                        >
                          {u.username[0]}
                        </span>
                        {canClear && (
                          <button
                            onClick={async () => {
                              if (!confirm(`Xoá annotation của "${u.username}" trên trang ${currentPageRef.current}?`)) return;
                              try {
                                await api.clearUserAnnotation(pdfId, currentPageRef.current, u.user_id);
                                // Remove their objects from canvas
                                const removeFromCanvas = (fc: any) => {
                                  const toRemove = fc.getObjects().filter((o: any) => o._owner_id === u.user_id);
                                  toRemove.forEach((o: any) => fc.remove(o));
                                  if (toRemove.length) fc.renderAll();
                                };
                                if (scrollModeRef.current === "continuous") {
                                  pageFabricRefs.current.forEach(fc => { if (fc) removeFromCanvas(fc); });
                                } else if (fabricRef.current) {
                                  removeFromCanvas(fabricRef.current);
                                }
                                localAnnotations.current = {};
                              } catch (e: unknown) {
                                alert(e instanceof Error ? e.message : "Xoá thất bại.");
                              }
                            }}
                            title={`Xoá annotation của ${u.username} trang này`}
                            className="absolute -top-1.5 -right-1.5 w-3.5 h-3.5 rounded-full bg-red-500 text-white hidden group-hover:flex items-center justify-center shadow z-10"
                            style={{ fontSize: 8 }}
                          >
                            ✕
                          </button>
                        )}
                      </div>
                    );
                  })}
                  {collabUsers.length > 5 && (
                    <span className="w-5 h-5 rounded-full border-2 border-white bg-gray-400 flex items-center justify-center text-[9px] text-white font-bold">
                      +{collabUsers.length - 5}
                    </span>
                  )}
                </div>
              )}
            </div>

            {saveNotice && (
              <span className="text-xs text-green-600 font-medium animate-pulse">✓ Saved</span>
            )}
            <button
              onClick={handleSave}
              disabled={saving || !isReady}
              className="bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white text-xs font-semibold px-3 py-1.5 rounded-lg transition"
            >
              {saving ? "Saving…" : "Save"}
            </button>
          </div>
        </div>
      </header>

      {/* ── Body: thumbnail sidebar + viewer ─────────────────────────────────── */}
      <div className="flex flex-1 overflow-hidden">

      {/* Thumbnail sidebar */}
      {showThumbnails && (
        <aside className="w-36 bg-white border-r border-gray-200 overflow-y-auto flex-shrink-0">
          <div className="flex flex-col gap-2 py-3 px-2">
            {Array.from({ length: totalPages }).map((_, idx) => (
              <button
                key={idx}
                ref={(el) => { thumbRefs.current[idx] = el; }}
                onClick={() => goToPage(idx + 1)}
                className={`flex flex-col items-center gap-1 rounded-lg p-1 w-full transition ${
                  currentPage === idx + 1
                    ? "ring-2 ring-blue-500 bg-blue-50"
                    : "hover:bg-gray-100"
                }`}
              >
                {thumbnails[idx] ? (
                  <img
                    src={thumbnails[idx]}
                    alt={`Page ${idx + 1}`}
                    className="w-full rounded shadow-sm border border-gray-200"
                  />
                ) : (
                  <div className="w-full aspect-[3/4] bg-gray-100 rounded animate-pulse" />
                )}
                <span className="text-[10px] text-gray-500 tabular-nums font-medium">{idx + 1}</span>
              </button>
            ))}
          </div>
        </aside>
      )}

      {/* ── Viewer area ─────────────────────────────────────────────────────── */}
      <main
        ref={scrollContainerRef}
        className="flex-1 overflow-auto py-6 px-4"
        // In Draw mode: lock touch-action so all touch events go to Fabric
        style={{ touchAction: mode === "draw" ? "none" : "auto" }}
      >
        {/* Loading spinner */}
        {!isReady && !loadError && (
          <div className="flex items-center justify-center h-64">
            <svg className="animate-spin h-8 w-8 text-blue-500" viewBox="0 0 24 24" fill="none">
              <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
              <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
            </svg>
          </div>
        )}

        {/* Canvas stack — PDF layer below, Fabric wrapper above (absolutely) */}
        <div
          className="relative mx-auto"
          style={{ display: isReady && scrollMode === "single" ? "block" : "none", width: "fit-content" }}
        >
          {/* PDF.js renders here */}
          <canvas ref={pdfCanvasRef} className="block shadow-lg rounded" />
          <canvas ref={fabricElRef} />
        </div>

        {/* Continuous scroll: one wrapper per page */}
        {isReady && scrollMode === "continuous" && (
          <div className="flex flex-col gap-6 items-center">
            {Array.from({ length: totalPages }).map((_, idx) => (
              <div key={idx} className="relative" style={{ width: "fit-content" }}>
                <canvas
                  ref={(el) => { pageCanvasRefs.current[idx] = el; }}
                  data-page={idx + 1}
                  className="block shadow-lg rounded"
                />
                <canvas id={`fabric-continuous-${idx + 1}`} />
              </div>
            ))}
          </div>
        )}

      </main>
      </div>

      {/* ── Color picker portal \u2014 rendered into document.body to escape overflow:hidden ── */}
      {colorPickerOpen && colorPickerPos && typeof document !== "undefined" && createPortal(
        <div
          ref={colorPickerRef}
          style={{ position: "fixed", top: colorPickerPos.top, left: colorPickerPos.left, zIndex: 9999 }}
          className="bg-white rounded-xl shadow-2xl border border-gray-200 p-3"
        >
          <p className="text-[10px] text-gray-400 mb-2 font-medium uppercase tracking-wide">Chọn màu bút</p>
          <div className="grid grid-cols-6 gap-1.5">
            {PRESET_COLORS.map((c) => {
              const takenByPeer = Object.entries(peerColors)
                .filter(([uid]) => Number(uid) !== currentUserIdRef.current)
                .some(([, pc]) => pc.toLowerCase() === c.toLowerCase());
              const isSelected = penColor.toLowerCase() === c.toLowerCase();
              return (
                <button
                  key={c}
                  disabled={takenByPeer}
                  onClick={() => { setPenColor(c); setColorPickerOpen(false); setColorPickerPos(null); }}
                  style={{ background: c }}
                  className={[
                    "w-7 h-7 rounded-full border-2 transition-transform",
                    isSelected ? "border-white shadow-lg scale-110 ring-2 ring-offset-1 ring-gray-400" : "border-transparent hover:scale-110",
                    takenByPeer ? "opacity-25 cursor-not-allowed" : "cursor-pointer",
                  ].join(" ")}
                  title={takenByPeer ? "Màu này đã được người dùng khác chọn" : c}
                />
              );
            })}
          </div>
        </div>,
        document.body
      )}
    </div>
  );
}
