import { useEffect, useRef, useState } from 'react';
import {
    IconArrowBackUp, IconArrowForwardUp, IconChevronDown, IconEye, IconEyeOff,
    IconLock, IconLockOpen, IconDeviceFloppy, IconDownload, IconCheck,
    IconBoxMultiple, IconBoxMultipleFilled, IconFrame, IconPlayerPlay, IconArrowLeft,
    IconLine,
} from '@tabler/icons-react';
import {
    DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem,
    DropdownMenuSeparator, DropdownMenuShortcut,
} from '@/components/ui/dropdown-menu';
import { usePrintEditorStore } from '../state/usePrintEditorStore';
import {
    ELEMENT_TYPES, getGroupAncestry, getGroupElements, getFrames,
} from '../schema/documentSchema';
import { filesToImagePayloads } from '../utils/imageFiles';
import { canEditStyle, SHAPE_BORDER_KEYS, COLOR_ADJUST_DEFAULTS } from './propertyControls';
import ColorPalettesPanel from './sections/ColorPalettesPanel';
import UserContentPanel, { isUserEditable } from './sections/UserContentPanel';
import { T, IconBtn, SectionLockContext } from '../ui/figma';
import { elementIcon } from '../ui/elementIcons';
import {
    PositionSection, LayoutSection, AppearanceSection, TextStrokeSection, BorderSection,
    EffectsSection, TypographySection, ContentSection, FillSection, SolidColorSection,
    TextFillSection,
    TextPathSection,
    LinkedContentSection,
} from './sections/sections';
import { isTextOnPath } from '../utils/textPathLayout';
import { elementColorSpans, hasColorSpans } from '../utils/textRuns';
import { couldBeLegacyBangla, legacyBanglaToUnicode, legacyBanglaFamily } from '../utils/legacyBangla';

/** Text styles that only mean something inside a line box, not along a curve. */
const BLOCK_ONLY_TEXT_STYLES = ['textAlign', 'verticalAlign', 'lineHeight', 'whiteSpace'];

const TABS = [
    { id: 'design', label: 'Design' },
    { id: 'colors', label: 'Colour palettes' },
];
/**
 * Admin-only third tab: it is not a panel of its own but a switch that flips the
 * whole editor into the mode the person filling the template in gets — left panel
 * gone, artwork locked, this panel showing the fill-in list. Leaving it is just
 * picking Design or Colour palettes again.
 */
const POV_TAB = { id: 'userpov', label: "User's POV" };
import {
    ShapeSection, ImageSection, QrSection, BarcodeSection, TableSection,
    FrameSection, FramesSection, ThumbnailSection, ExportSection,
} from './sections/elementSections';
import GroupSection from './sections/GroupSection';
import UserAccessSection from './sections/UserAccessSection';
import { isSectionLocked, toggleSectionLock } from '../schema/sectionLocks';

const IS_MAC = typeof navigator !== 'undefined'
    && /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent || '');
const MOD = IS_MAC ? '⌘' : 'Ctrl';

/* ------------------------------------------------------------------ *
 *  Zoom dropdown — zoom controls + view options (Figma "100% ▾")
 * ------------------------------------------------------------------ */

function viewportRect() {
    return document.querySelector('[data-workspace="1"]')?.getBoundingClientRect() || null;
}

function ZoomMenu({ onToggleFullscreen }) {
    const zoom = usePrintEditorStore((s) => s.zoom);
    const snapEnabled = usePrintEditorStore((s) => s.snapEnabled);

    const zoomBy = (factor) => {
        const rect = viewportRect();
        const store = usePrintEditorStore.getState();
        if (!rect) { store.setZoom(store.zoom * factor); return; }
        store.zoomAt(store.zoom * factor, rect.width / 2, rect.height / 2);
    };
    const fit = () => {
        const rect = viewportRect();
        if (rect) usePrintEditorStore.getState().fitToView(rect.width, rect.height, { padding: 72 });
    };
    const to100 = () => {
        const rect = viewportRect();
        const store = usePrintEditorStore.getState();
        store.setZoom(1);
        if (rect) {
            // Centred on the frame being worked on, not on the scene origin.
            const f = store.activeFrame();
            store.setPan(
                (rect.width - (f?.width || 1050)) / 2 - (f?.x || 0),
                (rect.height - (f?.height || 600)) / 2 - (f?.y || 0),
            );
        }
    };

    return (
        <DropdownMenu>
            <DropdownMenuTrigger asChild>
                <button
                    type="button"
                    className={`flex items-center gap-0.5 h-6 px-1.5 rounded-[5px] text-[11px] tabular-nums ${T.textSoft} ${T.hoverBg}`}
                    title="Zoom and view options"
                >
                    {Math.round(zoom * 100)}%
                    <IconChevronDown size={11} />
                </button>
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end" sideOffset={6} className="w-56 rounded-[13px] border-[#e6e6e6] dark:border-[#444444] dark:bg-[#2c2c2c] py-1">
                <ZoomItem label="Zoom in" shortcut={`${MOD} +`} onClick={() => zoomBy(1.2)} />
                <ZoomItem label="Zoom out" shortcut={`${MOD} −`} onClick={() => zoomBy(1 / 1.2)} />
                <ZoomItem label="Zoom to fit" shortcut={`${MOD} 0`} onClick={fit} />
                <ZoomItem label="Zoom to 100%" shortcut={`${MOD} 1`} onClick={to100} />
                <DropdownMenuSeparator className="bg-[#e6e6e6] dark:bg-[#444444]" />
                <ZoomItem
                    label="Snap to geometry"
                    checked={snapEnabled}
                    onClick={() => usePrintEditorStore.getState().toggleSnap()}
                />
                <DropdownMenuSeparator className="bg-[#e6e6e6] dark:bg-[#444444]" />
                <ZoomItem label="Fullscreen" onClick={onToggleFullscreen} />
            </DropdownMenuContent>
        </DropdownMenu>
    );
}

function ZoomItem({ label, shortcut, onClick, checked }) {
    return (
        <DropdownMenuItem
            onClick={onClick}
            className={`text-[11px] px-2.5 py-1.5 mx-1 rounded-[5px] cursor-pointer ${T.text}`}
        >
            <span className="w-4 shrink-0">
                {checked && <IconCheck size={12} className="text-[#0d99ff]" />}
            </span>
            {label}
            {shortcut && <DropdownMenuShortcut className="text-[10px]">{shortcut}</DropdownMenuShortcut>}
        </DropdownMenuItem>
    );
}

/* ------------------------------------------------------------------ *
 *  Right panel
 * ------------------------------------------------------------------ */

export default function RightPanel({
    mode,
    povPreview = false,
    onTogglePov,
    onSave,
    onExport,
    onExportFull,
    onPresent,
    autosave = false,
    onToggleAutosave,
    saving = false,
    saveLabel,
    onToggleFullscreen,
    thumbnailPreview = null,
    thumbnailSource = null,
    thumbnailCrop = null,
    onThumbnailUpload,
    onThumbnailDelete,
    onThumbnailUseDesign,
    onThumbnailCrop,
    thumbnailCropping = false,
}) {
    const {
        document: doc,
        selectedIds,
        selectedFrameId,
        selectFrame,
        setFrame,
        placeFrame,
        applyPalette,
        clearPalette,
        replaceColor,
        resetColorPresets,
        addFrame,
        duplicateFrame,
        deleteFrame,
        renameFrame,
        enteredGroupId,
        croppingId,
        startCrop,
        stopCrop,
        recrop,
        clearCrop,
        updateElement,
        updateElements,
        renameGroup,
        ungroupSelected,
        alignSelected,
        undo,
        redo,
        past,
        future,
        textSelection,
        setTextRangeFill,
        select,
        linkContent,
        unlinkContent,
        renameContentGroup,
        setContentDetached,
    } = usePrintEditorStore();

    const el = doc.elements.find((e) => e.id === selectedIds[0]);
    const styles = el?.styles || {};

    /**
     * Characters selected inside THIS element, if any — set while its text is being
     * edited on canvas. Clamped to the text as it stands, since the selection was
     * reported against the text a keystroke ago.
     */
    const textRange = (() => {
        if (!el || !textSelection || textSelection.id !== el.id) return null;
        const len = String(el.content ?? '').length;
        const start = Math.max(0, Math.min(len, textSelection.start));
        const end = Math.max(0, Math.min(len, textSelection.end));
        return end > start ? { start, end } : null;
    })();
    // What the selection is painted in now — the first span it touches, so the
    // picker opens on the fill the user can see rather than on the element's.
    const textRangePaint = textRange
        ? (elementColorSpans(el, el.content).find(
            (s) => s.end > textRange.start && s.start < textRange.end,
        ) || null)
        : null;
    const frames = getFrames(doc);
    const selectedFrame = frames.find((f) => f.id === selectedFrameId) || null;
    const [renaming, setRenaming] = useState(false);
    const [tab, setTab] = useState('design');

    /* ---- Tabs (+ the admin's "User's POV" switch) ---------------------- *
     *
     * `tab` only ever holds a real panel. The POV tab is a mode switch, so which
     * tab LOOKS active comes from the preview flag the shell owns, and the body
     * keeps rendering the design panel underneath — which, in user mode, is the
     * fill-in list.
     */
    const tabs = onTogglePov ? [...TABS, POV_TAB] : TABS;
    const activeTab = povPreview ? POV_TAB.id : tab;
    const bodyTab = povPreview ? 'design' : tab;
    const pickTab = (id) => {
        if (id === POV_TAB.id) { onTogglePov?.(true); setTab('design'); return; }
        onTogglePov?.(false);
        setTab(id);
    };

    /**
     * User mode opens on the FILL-IN list rather than on a property inspector —
     * see sections/UserContentPanel. The per-card ⚙ switches to the full panel
     * for that one element, and the back arrow in the header returns. Admin is
     * untouched: it has always been the inspector.
     */
    const [userAdvanced, setUserAdvanced] = useState(false);
    // Entering or leaving the POV preview starts on the fill-in list — that is what
    // the user meets first, and it is where the admin left off.
    useEffect(() => { setUserAdvanced(false); }, [povPreview]);
    /**
     * Picking something ON CANVAS is the same request as pressing the card's ⚙ —
     * "show me this one" — so it opens that element's full panel straight away.
     * Picking a CARD must not, or the list (and the field being typed into) would
     * vanish under the inspector on the first keystroke's selection, so those
     * selections announce themselves here first.
     */
    const fromPanelRef = useRef(false);
    const selectFromPanel = (ids) => {
        fromPanelRef.current = true;
        select(ids);
    };
    /**
     * Which card the fill-in list should scroll to — set by CANVAS picks only.
     * Scrolling on a panel pick would yank the card out from under the pointer
     * that just pressed it, and the button being pressed would never see its click.
     */
    const [focusId, setFocusId] = useState(null);
    // Nothing selected means nothing to be advanced ABOUT — clicking empty canvas
    // drops back to the list rather than leaving an empty inspector behind.
    useEffect(() => {
        const fromPanel = fromPanelRef.current;
        fromPanelRef.current = false;
        if (!selectedIds.length) { setUserAdvanced(false); setFocusId(null); return; }
        if (fromPanel) return;
        setFocusId(selectedIds[0]);
        if (mode !== 'user') return;
        // Artwork the admin locked has no settings this user may change — keep the
        // fill-in list up rather than opening an inspector full of dead controls.
        const picked = doc.elements.find((e) => e.id === selectedIds[0]);
        if (selectedIds.length === 1 && isUserEditable(picked)) setUserAdvanced(true);
        // `selectedIds` is a fresh array on every select() call, so this fires once
        // per PICK — not on the document edits that follow one.
    }, [selectedIds, mode]);
    const simpleView = mode === 'user' && !userAdvanced;

    /**
     * The panel body keeps its scroll position across renders, so a panel opened
     * for a NEW element would start halfway down — wherever the previous one was
     * read to. Every switch of what the inspector is about starts at the top.
     * The fill-in list is excluded: it scrolls the selected CARD into view itself
     * (see UserContentPanel), which is the same job done more precisely.
     */
    const bodyRef = useRef(null);
    useEffect(() => {
        if (simpleView) return;
        bodyRef.current?.scrollTo({ top: 0 });
    }, [simpleView, tab, el?.id]);

    /** Content edits straight from the list — same permission rules as the canvas. */
    const changeUserElement = (id, patch) => {
        const target = doc.elements.find((e) => e.id === id);
        if (!isUserEditable(target)) return;
        updateElement(id, patch);
    };

    const replaceUserImage = async (id, fileList) => {
        const target = doc.elements.find((e) => e.id === id);
        if (!isUserEditable(target) || target.type !== ELEMENT_TYPES.IMAGE) return;
        const files = Array.from(fileList || []);
        if (!files.length) return;
        try {
            const [payload] = await filesToImagePayloads(files, { maxSide: 320 });
            if (!payload?.src) return;
            // A crop frames the picture it was cut from, so a replacement with
            // other proportions must not inherit it.
            updateElement(id, {
                src: payload.src,
                name: payload.name || target.name,
                crop: null,
            });
        } catch (err) {
            console.error('[print-editor] Photo replace failed', err);
        }
    };

    const openUserAdvanced = (id) => {
        selectFromPanel([id]);
        setUserAdvanced(true);
    };

    /* ---- Colour palettes: scope = the selected frame, else every frame ---- */

    const scopeFrames = selectedFrame ? [selectedFrame] : frames;
    const scopeIds = new Set(scopeFrames.map((f) => f.id));
    const scopeElements = doc.elements.filter((e) => scopeIds.has(e.frameId));
    const scopeLabel = selectedFrame
        ? selectedFrame.name
        : `All ${frames.length} frame${frames.length === 1 ? '' : 's'}`;

    // A palette RECOLOURS the artwork in scope (see utils/paletteRecolor) rather than
    // just repainting the frame, which is what makes the picked colour the colour you
    // actually get.
    const applyScopePalette = (palette) => applyPalette([...scopeIds], palette);
    const clearScopePalette = () => clearPalette([...scopeIds]);

    // One global colour of the design, swapped wherever it is painted in scope —
    // the Selection colour presets list (see utils/colorUsage). `rowId` is the
    // global's identity, not its value, so two globals holding the same colour
    // stay two globals.
    const replaceScopeColor = (rowId, to, token) => replaceColor([...scopeIds], rowId, to, token);
    const resetScopeColors = () => resetColorPresets([...scopeIds]);

    const applyPaletteTone = (stylePatch) => {
        if (!scopeElements.length) return;
        const patches = {};
        scopeElements.forEach((e) => { patches[e.id] = { styles: stylePatch }; });
        updateElements(patches);
    };

    const resetPaletteTone = () => applyPaletteTone({
        hueRotate: COLOR_ADJUST_DEFAULTS.hueRotate,
        saturate: COLOR_ADJUST_DEFAULTS.saturate,
        brightness: COLOR_ADJUST_DEFAULTS.brightness,
    });

    /** Selection is exactly one whole (non-entered) group → group view */
    const groupSel = (() => {
        if (mode !== 'admin' || selectedIds.length < 2) return null;
        const selEls = doc.elements.filter((e) => selectedIds.includes(e.id));
        if (selEls.length < 2) return null;
        // Outermost-first, so a whole nested sub-layer shows its own group panel.
        const chain = getGroupAncestry(doc.groups || [], selEls[0].groupId);
        const selSet = new Set(selectedIds);
        for (const g of chain) {
            if (g.id === enteredGroupId) break;
            const members = getGroupElements(doc.elements, doc.groups || [], g.id);
            if (members.length === selEls.length && members.every((m) => selSet.has(m.id))) {
                return { gid: g.id, group: g, members };
            }
        }
        return null;
    })();

    /** Every layer this panel reads from and writes to. */
    const selEls = doc.elements.filter((e) => selectedIds.includes(e.id));

    /**
     * Show/hide for the canvas gradient line, sat beside the Angle field. Only the
     * MAIN fill gets one — that is the only gradient the canvas draws a gizmo for
     * — and only an admin sees it, since only an admin gets the gizmo at all.
     *
     * The choice is SAVED on the layer (`gradientLineHidden`), not held for the
     * session: an admin who hides the line on one busy block wants it to stay
     * hidden there tomorrow, while every other block keeps its line.
     */
    const lineHidden = selectedFrame
        ? !!selectedFrame.gradientLineHidden
        : !!styles.gradientLineHidden;

    const gradientLineToggle = mode === 'admin' ? (
        <IconBtn
            icon={IconLine}
            label={lineHidden
                ? 'Show the gradient line on canvas — drag its ends to aim the gradient'
                : 'Hide the gradient line on canvas'}
            active={!lineHidden}
            onClick={() => (selectedFrame
                ? setFrame(selectedFrame.id, { gradientLineHidden: !lineHidden })
                : setStyle('gradientLineHidden', !lineHidden))}
        />
    ) : null;

    /**
     * Several layers of DIFFERENT kinds selected at once. Their per-kind blocks
     * (typography for text, cropping for a picture) mean nothing across the set,
     * so the panel falls back to what every layer has: paint, corners, effects.
     * Same-kind selections keep their full panel — the writes fan out anyway.
     */
    const mixedSelection = !!el && selEls.length > 1 && selEls.some((e) => e.type !== el.type);

    const gate = (key) => canEditStyle(el, key, mode);

    /**
     * One BLOCK of the panel, wrapped in what it needs to know about its own lock.
     *
     * For a USER a locked section is simply not rendered — `gate` above is the
     * finer, per-property question inside a section that is open. For an ADMIN it
     * always renders, and carries the padlock in its header that sets the lock
     * (PanelSection picks it up from the context).
     */
    const section = (key, node) => {
        if (isSectionLocked(el, key, mode)) return null;
        if (mode !== 'admin' || !el) return node;
        return (
            <SectionLockContext.Provider
                value={{
                    locked: (el.lockedSections || []).includes(key),
                    onToggle: () => updateElement(el.id, { lockedSections: toggleSectionLock(el, key) }),
                }}
            >
                {node}
            </SectionLockContext.Provider>
        );
    };

    /**
     * Every layer this panel writes to. The controls show the FIRST selection's
     * values (Figma does the same), but an edit lands on all of them — otherwise
     * restyling six layers means opening six panels. Each target still refuses
     * what its own locks forbid, and a shape's fill is spelled differently from
     * everything else's background, so the key is translated per target.
     */
    const aliasKey = (target, key) => {
        const isShape = target.type === ELEMENT_TYPES.SHAPE;
        if (isShape) return { backgroundType: 'fillType', backgroundColor: 'fill' }[key] || key;
        return { fillType: 'backgroundType', fill: 'backgroundColor' }[key] || key;
    };

    const writeStyles = (patch) => {
        const patches = {};
        selEls.forEach((t) => {
            const next = {};
            Object.entries(patch).forEach(([key, value]) => {
                const k = aliasKey(t, key);
                if (canEditStyle(t, k, mode)) next[k] = value;
            });
            if (Object.keys(next).length) patches[t.id] = { styles: next };
        });
        if (Object.keys(patches).length) updateElements(patches);
    };

    const setStyle = (key, value) => writeStyles({ [key]: value });

    const setStyles = (patch) => writeStyles(patch);

    /**
     * Properties that identify ONE layer rather than describe it — its words, its
     * picture, its crop, its name. Those stay on the layer the panel is showing,
     * however many are selected; everything else (position, size, opacity…) fans
     * out like a style.
     */
    const OWN_PROPS = [
        'content', 'contentKey', 'src', 'crop', 'name', 'colorSpans', 'shape',
        // A table's grid is its content: retyping the cells of the selected table
        // must not overwrite every other table in the selection. Linked tables are
        // the deliberate exception, and the store handles that one.
        'cells', 'rows', 'cols',
    ];

    const setProp = (patch) => {
        if (!el) return;
        const targets = Object.keys(patch).some((k) => OWN_PROPS.includes(k)) ? [el] : selEls;
        // X and Y are scene coordinates, so handing every layer the same number
        // would pile a cross-frame selection onto one artboard. Fan them out as a
        // position WITHIN each layer's own frame instead — the same spot on every
        // card, which is what a selection spanning frames is asking for.
        const frameOf = (t) => frames.find((f) => f.id === t.frameId) || frames[0] || { x: 0, y: 0 };
        const base = frameOf(el);
        const patches = {};
        targets.forEach((t) => {
            if (mode === 'user' && t.editableByUser === false) return;
            let next = patch;
            if (t.id !== el.id && (patch.x != null || patch.y != null)) {
                const f = frameOf(t);
                next = { ...patch };
                if (patch.x != null) next.x = f.x + (patch.x - base.x);
                if (patch.y != null) next.y = f.y + (patch.y - base.y);
            }
            patches[t.id] = next;
        });
        if (Object.keys(patches).length) updateElements(patches);
    };

    const onImageUpload = async (fileList) => {
        const files = Array.from(fileList || []);
        if (!files.length) return;
        try {
            const payloads = await filesToImagePayloads(files, { maxSide: 320 });
            if (!payloads.length) return;
            const store = usePrintEditorStore.getState();
            if (el?.type === ELEMENT_TYPES.IMAGE) {
                const [first, ...rest] = payloads;
                // A crop frames the picture it was cut from — a replacement with other
                // proportions would be framed by a rect that means nothing for it.
                setProp({
                    src: first.src,
                    name: first.name || el.name,
                    width: first.width,
                    height: first.height,
                    crop: null,
                });
                if (rest.length) {
                    store.addElements(rest.map((p) => ({
                        type: ELEMENT_TYPES.IMAGE, src: p.src, name: p.name, width: p.width, height: p.height,
                    })));
                }
                return;
            }
            store.addElements(payloads.map((p) => ({
                type: ELEMENT_TYPES.IMAGE, src: p.src, name: p.name, width: p.width, height: p.height,
            })));
        } catch (err) {
            console.error('[print-editor] Image upload failed', err);
        }
    };

    const isText = el?.type === ELEMENT_TYPES.TEXT || el?.type === ELEMENT_TYPES.PLACEHOLDER;

    /**
     * The paint every kind of layer understands, shown for a mixed selection (and
     * under a group's position block). A shape spells its paint `fillType`/`fill`;
     * the control speaks `backgroundType`/`backgroundColor`, so a shape shown here
     * is read through those names — `writeStyles` translates them back per layer.
     */
    const fillView = el?.type === ELEMENT_TYPES.SHAPE
        ? { ...styles, backgroundType: styles.fillType, backgroundColor: styles.fill }
        : styles;

    const commonStyleSections = el ? (
        <>
            {section('appearance', (
                <AppearanceSection
                    el={el}
                    styles={styles}
                    setProp={setProp}
                    setStyle={setStyle}
                    setStyles={setStyles}
                    mode={mode}
                    gate={gate}
                />
            ))}
            {section('fill', (
                <FillSection title="Fill" styles={fillView} setStyle={setStyle} gate={gate} angleAction={gradientLineToggle} />
            ))}
            {section('border', (
                <BorderSection styles={styles} setStyle={setStyle} gate={gate} />
            ))}
            {section('effects', (
                <EffectsSection
                    el={el}
                    styles={styles}
                    setStyle={setStyle}
                    setStyles={setStyles}
                    gate={gate}
                    isText={isText}
                />
            ))}
        </>
    ) : null;

    /**
     * Repair a Bijoy (ANSI) text layer in place — for templates imported before the
     * importer learned to transliterate, whose content is Latin bytes that only read
     * as Bangla in SutonnyMJ. Offered only when the text would actually convert.
     */
    const convertBangla = () => {
        if (!el) return;
        const next = legacyBanglaToUnicode(el.content || '');
        if (next === el.content) return;
        updateElement(el.id, {
            content: next,
            styles: { fontFamily: legacyBanglaFamily(styles.fontFamily) },
        });
    };
    const onPath = isTextOnPath(el);
    const Icon = elementIcon(el);
    /**
     * The top action row stays the ADMIN's during the POV preview: the template is
     * still theirs to save, and trapping half-finished work behind "leave the
     * preview first" would be a worse lie than the row looking one button different
     * from the real user editor.
     */
    const headerMode = povPreview ? 'admin' : mode;
    const showExport = headerMode !== 'user' || !onSave;
    // In admin, export acts on the current selection when there is one.
    const exportLabel = (headerMode === 'admin' && selectedIds.length > 0)
        ? 'Export selection (PNG + PDF)'
        : (frames.length > 1
            ? `Export ${frames.length} frames (PNG each + PDF)`
            : 'Export PNG + PDF');

    return (
        <div className="flex flex-col h-full w-full min-h-0">
            {/* Top actions — undo / redo / export / save */}
            <div className={`h-12 shrink-0 flex items-center gap-1 px-3 border-b ${T.border}`}>
                <IconBtn icon={IconArrowBackUp} label={`Undo (${MOD}+Z)`} disabled={!past.length} onClick={undo} className="h-7 w-7" />
                <IconBtn icon={IconArrowForwardUp} label={`Redo (${MOD}+Shift+Z)`} disabled={!future.length} onClick={redo} className="h-7 w-7" />
                {/* Zoom sits with the view controls, not in the tab strip: a third
                    tab pushed the tabs onto two lines and squeezed it into a corner. */}
                <ZoomMenu onToggleFullscreen={onToggleFullscreen} />
                <div className="flex-1" />
                {onPresent && (
                    <IconBtn
                        icon={IconPlayerPlay}
                        label="Preview — full screen, canvas only (Esc to leave)"
                        onClick={onPresent}
                        className="h-7 w-7"
                    />
                )}
                {showExport && (
                    <IconBtn
                        icon={IconDownload}
                        label="Export template (PNG + PDF)"
                        disabled={saving && !!onExportFull}
                        onClick={onExportFull}
                        className="h-7 w-7"
                    />
                )}
                {onSave && (
                    <button
                        type="button"
                        disabled={saving}
                        onClick={onSave}
                        className="h-7 px-3 flex items-center gap-1.5 rounded-[6px] bg-[#0d99ff] hover:bg-[#0b87e0] text-white text-[11px] font-semibold disabled:opacity-50 transition-colors"
                    >
                        {headerMode === 'user' ? <IconDownload size={13} /> : <IconDeviceFloppy size={13} />}
                        {saveLabel || (saving ? 'Saving…' : 'Save')}
                    </button>
                )}
            </div>

            {/* Autosave — saves automatically a moment after any change */}
            {onToggleAutosave && (
                <div className={`h-9 shrink-0 flex items-center justify-between pl-4 pr-3 border-b ${T.border}`}>
                    <span className={`text-[11px] font-medium ${T.text}`}>Autosave</span>
                    <button
                        type="button"
                        role="switch"
                        aria-checked={autosave}
                        title={autosave ? 'Autosave on — saves automatically after changes' : 'Autosave off'}
                        onClick={() => onToggleAutosave(!autosave)}
                        className={`relative h-[18px] w-[30px] shrink-0 rounded-full transition-colors
                            ${autosave ? 'bg-[#0d99ff]' : 'bg-black/20 dark:bg-white/25'}`}
                    >
                        <span
                            className={`absolute top-[2px] h-[14px] w-[14px] rounded-full bg-white shadow-sm transition-all
                                ${autosave ? 'left-[14px]' : 'left-[2px]'}`}
                        />
                    </button>
                </div>
            )}

            {/* Tabs get the whole row now that zoom has moved up to the action bar,
                so three of them still sit on one line. */}
            <div className={`h-10 shrink-0 flex items-center px-3 border-b ${T.border}`}>
                <div className="flex items-center gap-1">
                    {tabs.map((tabDef) => {
                        const active = activeTab === tabDef.id;
                        return (
                            <button
                                key={tabDef.id}
                                type="button"
                                title={tabDef.id === POV_TAB.id
                                    ? 'See the template as the user filling it in does — pick Design to come back'
                                    : undefined}
                                onClick={() => pickTab(tabDef.id)}
                                className={`h-10 px-2 text-[11px] font-semibold border-b-2 -mb-px transition-colors
                                    ${active
                                        ? 'border-[#0d99ff] text-[#0d99ff]'
                                        : `border-transparent ${T.textSoft} hover:${T.text}`}`}
                            >
                                {tabDef.label}
                            </button>
                        );
                    })}
                </div>
            </div>

            {/* The editor looks like a different product in the preview — say why,
                and say how to get out, rather than leaving the admin to guess. */}
            {povPreview && (
                <div className={`shrink-0 flex items-start gap-2 px-3 py-2 border-b ${T.border} bg-[#0d99ff]/10`}>
                    <IconEye size={13} className="mt-[1px] shrink-0 text-[#0d99ff]" stroke={1.75} />
                    <p className={`text-[10px] leading-[1.45] ${T.textSoft}`}>
                        Viewing as the user: layers and artwork tools are gone, and only the
                        elements you left unlocked can be changed.
                        <button
                            type="button"
                            onClick={() => pickTab('design')}
                            className="ml-1 font-semibold text-[#0d99ff] hover:underline"
                        >
                            Back to editing
                        </button>
                    </p>
                </div>
            )}

            {/* Selection header */}
            <div className={`h-10 shrink-0 flex items-center gap-1.5 pl-4 pr-2 border-b ${T.border}`}>
                {groupSel ? (
                    <>
                        <IconBoxMultiple size={13} className="shrink-0 text-[#8638e5] dark:text-[#c79bff]" stroke={1.75} />
                        {renaming ? (
                            <input
                                autoFocus
                                defaultValue={groupSel.group.name}
                                className={`flex-1 min-w-0 h-6 px-1 text-[11px] font-medium rounded-[4px] outline-none ring-1 ring-[#0d99ff] bg-transparent ${T.text}`}
                                onFocus={(e) => e.target.select()}
                                onBlur={(e) => { renameGroup(groupSel.gid, e.target.value); setRenaming(false); }}
                                onKeyDown={(e) => {
                                    if (e.key === 'Enter') e.currentTarget.blur();
                                    if (e.key === 'Escape') setRenaming(false);
                                    e.stopPropagation();
                                }}
                            />
                        ) : (
                            <button
                                type="button"
                                className={`flex-1 min-w-0 text-left text-[11px] font-medium truncate ${T.text}`}
                                title="Double-click to rename"
                                onDoubleClick={() => setRenaming(true)}
                            >
                                {groupSel.group.name}
                                <span className={T.textSoft}> · {groupSel.members.length}</span>
                            </button>
                        )}
                        <IconBtn
                            icon={IconBoxMultipleFilled}
                            label="Ungroup (⌘⇧G)"
                            onClick={ungroupSelected}
                        />
                        <IconBtn
                            icon={groupSel.members.some((m) => m.visible !== false) ? IconEye : IconEyeOff}
                            label={groupSel.members.some((m) => m.visible !== false) ? 'Hide group' : 'Show group'}
                            active={!groupSel.members.some((m) => m.visible !== false)}
                            onClick={() => {
                                const visible = !groupSel.members.some((m) => m.visible !== false);
                                const patches = {};
                                groupSel.members.forEach((m) => { patches[m.id] = { visible }; });
                                updateElements(patches);
                            }}
                        />
                        <IconBtn
                            icon={groupSel.members.every((m) => m.locked) ? IconLock : IconLockOpen}
                            label={groupSel.members.every((m) => m.locked)
                                ? 'Unlock group for the user'
                                : 'Lock group for the user (you can still edit it here)'}
                            active={groupSel.members.every((m) => m.locked)}
                            onClick={() => {
                                const locked = !groupSel.members.every((m) => m.locked);
                                const patches = {};
                                groupSel.members.forEach((m) => { patches[m.id] = { locked }; });
                                updateElements(patches);
                            }}
                        />
                    </>
                ) : el ? (
                    <>
                        {/* The way back to the fill-in list — user mode only, and only
                            while its full settings are open. */}
                        {mode === 'user' && userAdvanced && (
                            <IconBtn
                                icon={IconArrowLeft}
                                label="Back to the list"
                                onClick={() => setUserAdvanced(false)}
                                className="h-6 w-6 -ml-1"
                            />
                        )}
                        <Icon size={13} className={`${T.textSoft} shrink-0`} stroke={1.75} />
                        {mode === 'admin' && renaming ? (
                            <input
                                autoFocus
                                defaultValue={el.name || el.type}
                                className={`flex-1 min-w-0 h-6 px-1 text-[11px] font-medium rounded-[4px] outline-none ring-1 ring-[#0d99ff] bg-transparent ${T.text}`}
                                onFocus={(e) => e.target.select()}
                                onBlur={(e) => { setProp({ name: e.target.value || el.type }); setRenaming(false); }}
                                onKeyDown={(e) => {
                                    if (e.key === 'Enter') e.currentTarget.blur();
                                    if (e.key === 'Escape') setRenaming(false);
                                    e.stopPropagation();
                                }}
                            />
                        ) : (
                            <button
                                type="button"
                                className={`flex-1 min-w-0 text-left text-[11px] font-medium truncate ${T.text}`}
                                title={mode === 'admin' ? 'Double-click to rename' : undefined}
                                onDoubleClick={() => mode === 'admin' && setRenaming(true)}
                            >
                                {el.name || el.type}
                                {selectedIds.length > 1 && (
                                    <span className={`${T.textSoft}`}> +{selectedIds.length - 1}</span>
                                )}
                            </button>
                        )}
                        <IconBtn
                            icon={el.visible !== false ? IconEye : IconEyeOff}
                            label={el.visible !== false ? 'Hide' : 'Show'}
                            active={el.visible === false}
                            onClick={() => setProp({ visible: el.visible === false })}
                        />
                        {/* The USER lock — what the person filling the template in may
                            not touch. The admin's own lock (`adminLocked`) is the one
                            in the layers panel; the two are independent. */}
                        {mode === 'admin' && (
                            <IconBtn
                                icon={el.locked ? IconLock : IconLockOpen}
                                label={el.locked
                                    ? 'Unlock for the user'
                                    : 'Lock for the user (you can still edit it here)'}
                                active={!!el.locked}
                                onClick={() => updateElement(el.id, { locked: !el.locked })}
                            />
                        )}
                    </>
                ) : selectedFrame ? (
                    <>
                        <IconFrame size={13} className={`${T.textSoft} shrink-0`} stroke={1.75} />
                        {renaming ? (
                            <input
                                autoFocus
                                defaultValue={selectedFrame.name}
                                className={`flex-1 min-w-0 h-6 px-1 text-[11px] font-medium rounded-[4px] outline-none ring-1 ring-[#0d99ff] bg-transparent ${T.text}`}
                                onFocus={(e) => e.target.select()}
                                onBlur={(e) => { renameFrame(selectedFrame.id, e.target.value); setRenaming(false); }}
                                onKeyDown={(e) => {
                                    if (e.key === 'Enter') e.currentTarget.blur();
                                    if (e.key === 'Escape') setRenaming(false);
                                    e.stopPropagation();
                                }}
                            />
                        ) : (
                            <button
                                type="button"
                                className={`flex-1 min-w-0 text-left text-[11px] font-medium truncate ${T.text}`}
                                title="Double-click to rename"
                                onDoubleClick={() => setRenaming(true)}
                            >
                                {selectedFrame.name}
                            </button>
                        )}
                    </>
                ) : (
                    <span className={`text-[11px] font-medium ${T.text}`}>
                        {frames.length > 1 ? `${frames.length} frames` : 'Frame'}
                    </span>
                )}
            </div>

            {/* Sections */}
            <div ref={bodyRef} className="flex-1 min-h-0 overflow-y-auto fig-scroll">
                {bodyTab === 'colors' && (
                    <ColorPalettesPanel
                        frames={frames}
                        scopeFrames={scopeFrames}
                        scopeLabel={scopeLabel}
                        elements={scopeElements}
                        onApplyPalette={applyScopePalette}
                        onClearPalette={clearScopePalette}
                        onReplaceColor={replaceScopeColor}
                        onResetColors={resetScopeColors}
                        onAdjust={applyPaletteTone}
                        onResetAdjust={resetPaletteTone}
                    />
                )}

                {bodyTab === 'design' && !simpleView && groupSel && (
                    <>
                        <GroupSection
                            members={groupSel.members}
                            canvas={doc.canvas}
                            updateElements={updateElements}
                        />
                        {/* Paint the whole group in one go — the group block above
                            only moves and scales it. */}
                        {mode === 'admin' && commonStyleSections}
                        {showExport && <ExportSection onExport={onExport} disabled={saving && !!onExport} label={exportLabel} />}
                    </>
                )}

                {/* User mode opens on the fill-in list, whatever is selected. */}
                {bodyTab === 'design' && simpleView && (
                    <>
                        <UserContentPanel
                            frames={frames}
                            elements={doc.elements}
                            scrollRef={bodyRef}
                            selectedId={el?.id || null}
                            focusId={focusId}
                            onSelect={(id) => selectFromPanel([id])}
                            onAdvanced={openUserAdvanced}
                            onChangeElement={changeUserElement}
                            onReplaceImage={replaceUserImage}
                            onDetach={setContentDetached}
                        />
                        {showExport && <ExportSection onExport={onExport} disabled={saving && !!onExport} label={exportLabel} />}
                    </>
                )}

                {bodyTab === 'design' && !simpleView && !groupSel && !el && (
                    <>
                        {mode === 'admin' ? (
                            <>
                                {selectedFrame ? (
                                    <FrameSection
                                        frame={selectedFrame}
                                        setFrame={(patch) => setFrame(selectedFrame.id, patch)}
                                        // X / Y move the frame WITH its contents, so the
                                        // design doesn't get left behind.
                                        placeFrame={(pos) => placeFrame(selectedFrame.id, pos)}
                                        angleAction={gradientLineToggle}
                                        onDuplicate={() => duplicateFrame(selectedFrame.id)}
                                        onDelete={() => deleteFrame(selectedFrame.id)}
                                    />
                                ) : null}
                                <FramesSection
                                    frames={frames}
                                    selectedFrameId={selectedFrame?.id || null}
                                    onSelect={selectFrame}
                                    onAdd={() => addFrame()}
                                    onRemove={deleteFrame}
                                />
                                <ThumbnailSection
                                    thumbnailPreview={thumbnailPreview}
                                    thumbnailSource={thumbnailSource}
                                    thumbnailCrop={thumbnailCrop}
                                    onThumbnailUpload={onThumbnailUpload}
                                    onThumbnailDelete={onThumbnailDelete}
                                    onThumbnailUseDesign={onThumbnailUseDesign}
                                    onThumbnailCrop={onThumbnailCrop}
                                    cropping={thumbnailCropping}
                                    // Selected frame wins, else frame 1 — the button says which.
                                    frame={selectedFrame || frames[0] || null}
                                />
                            </>
                        ) : (
                            <div className={`px-4 py-3 text-[11px] leading-relaxed ${T.textSoft}`}>
                                Select an editable element on the canvas to change its content or styles.
                            </div>
                        )}
                        {showExport && <ExportSection onExport={onExport} disabled={saving && !!onExport} label={exportLabel} />}
                    </>
                )}

                {/* Mixed selection — text next to a shape next to a picture. Only
                    what they all have in common, applied to every one of them. */}
                {bodyTab === 'design' && !simpleView && !groupSel && el && mixedSelection && (
                    <>
                        <div className={`px-4 py-2.5 border-b ${T.border} text-[11px] ${T.textSoft}`}>
                            {selEls.length} layers selected — changes apply to all of them.
                        </div>
                        {section('position', (
                            <PositionSection
                                el={el}
                                canvas={frames.find((f) => f.id === el.frameId) || frames[0]}
                                setProp={setProp}
                                alignSelected={alignSelected}
                                mode={mode}
                            />
                        ))}
                        {commonStyleSections}
                        {showExport && <ExportSection onExport={onExport} disabled={saving && !!onExport} label={exportLabel} />}
                    </>
                )}

                {bodyTab === 'design' && !simpleView && !groupSel && el && !mixedSelection && (
                    <>
                        {section('position', (
                            <PositionSection
                                el={el}
                                // X/Y and the align row read against the element's own frame.
                                canvas={frames.find((f) => f.id === el.frameId) || frames[0]}
                                setProp={setProp}
                                alignSelected={alignSelected}
                                mode={mode}
                            />
                        ))}
                        {section('layout', (
                            <LayoutSection
                                el={el}
                                styles={styles}
                                setProp={setProp}
                                setStyle={setStyle}
                                setStyles={setStyles}
                                mode={mode}
                                // Padding pushes text around inside a line box; a run on a
                                // curve has none, so the control would do nothing.
                                gate={(k) => (isText || el.type === ELEMENT_TYPES.IMAGE)
                                    && !(onPath && k === 'padding')
                                    && gate(k)}
                            />
                        ))}
                        {section('appearance', (
                            <AppearanceSection
                                el={el}
                                styles={styles}
                                setProp={setProp}
                                setStyle={setStyle}
                                setStyles={setStyles}
                                mode={mode}
                                gate={gate}
                                radiusMax={el.type === ELEMENT_TYPES.QRCODE ? 80 : el.type === ELEMENT_TYPES.BARCODE || el.type === ELEMENT_TYPES.TABLE ? 40 : 400}
                            />
                        ))}

                        {isText && (
                            <>
                                {/* Admin-only (it edits the placeholder KEY), so it
                                    carries no user lock — the 'content' lock is
                                    about the words, and gates them at the store. */}
                                <ContentSection el={el} setProp={setProp} mode={mode} />
                                {/* Admin-only, like the Placeholder block above: it
                                    wires layers together, so it carries no user lock. */}
                                <LinkedContentSection
                                    el={el}
                                    selectedIds={selectedIds}
                                    elements={doc.elements}
                                    mode={mode}
                                    onLink={linkContent}
                                    onUnlink={unlinkContent}
                                    onRename={renameContentGroup}
                                />
                                {section('textPath', (
                                    <TextPathSection el={el} setProp={setProp} mode={mode} />
                                ))}
                                {section('typography', (
                                    <TypographySection
                                        styles={styles}
                                        setStyle={setStyle}
                                        setStyles={setStyles}
                                        onConvertBangla={couldBeLegacyBangla(el.content) ? convertBangla : null}
                                        // Whether the box follows its text in both axes or keeps
                                        // the column width it was drawn at. Lives on the element,
                                        // not the style sheet — it is about the box, not the type.
                                        autoSize={onPath ? null : (el.textAutoSize || 'width')}
                                        onAutoSizeChange={(v) => updateElement(el.id, { textAutoSize: v })}
                                        // A run on a curve has no line box: alignment, wrapping and
                                        // line height decide nothing, so they don't show.
                                        gate={(k) => !(onPath && BLOCK_ONLY_TEXT_STYLES.includes(k)) && gate(k)}
                                    />
                                ))}
                                {section('textFill', (
                                    <TextFillSection
                                        styles={styles}
                                        setStyle={setStyle}
                                        gate={gate}
                                        range={textRange}
                                        rangePaint={textRangePaint}
                                        onRangeFill={(paint) => setTextRangeFill(
                                            el.id, textRange.start, textRange.end, paint,
                                        )}
                                        hasSpans={hasColorSpans(el)}
                                        onClearSpans={() => updateElement(el.id, { colorSpans: [] })}
                                    />
                                ))}
                                {section('fill', (
                                    <FillSection
                                        title="Background"
                                        styles={styles}
                                        setStyle={setStyle}
                                        gate={gate}
                                        angleAction={gradientLineToggle}
                                    />
                                ))}
                                {section('border', (
                                    <BorderSection styles={styles} setStyle={setStyle} gate={gate} />
                                ))}
                                {section('stroke', (
                                    <TextStrokeSection styles={styles} setStyles={setStyles} gate={gate} />
                                ))}
                            </>
                        )}

                        {el.type === ELEMENT_TYPES.IMAGE && (
                            <>
                                {/* Admin-only — it wires layers together, so it
                                    carries no user lock; the 'image' lock is about
                                    the picture, and gates it at the store. */}
                                <LinkedContentSection
                                    el={el}
                                    selectedIds={selectedIds}
                                    elements={doc.elements}
                                    mode={mode}
                                    onLink={linkContent}
                                    onUnlink={unlinkContent}
                                    onRename={renameContentGroup}
                                />
                                {section('image', (
                                    <ImageSection
                                        el={el}
                                        styles={styles}
                                        setProp={setProp}
                                        setStyle={setStyle}
                                        mode={mode}
                                        onImageUpload={onImageUpload}
                                        cropping={croppingId === el.id}
                                        onCropToggle={() => (croppingId === el.id ? stopCrop() : startCrop(el.id))}
                                        onCropFit={(fitMode) => recrop(el.id, fitMode)}
                                        onCropClear={() => clearCrop(el.id)}
                                    />
                                ))}
                                {section('fill', (
                                    <FillSection title="Background" styles={styles} setStyle={setStyle} gate={gate} angleAction={gradientLineToggle} />
                                ))}
                                {section('border', (
                                    <BorderSection styles={styles} setStyle={setStyle} gate={gate} />
                                ))}
                            </>
                        )}

                        {el.type === ELEMENT_TYPES.SHAPE && (
                            <>
                                {section('fill', (
                                    <FillSection
                                        title="Fill"
                                        styles={styles}
                                        setStyle={setStyle}
                                        gate={() => true}
                                        typeKey="fillType"
                                        solidKey="fill"
                                        solidDefault="#e4e4e7"
                                        angleAction={gradientLineToggle}
                                    />
                                ))}
                                {section('border', (
                                    <BorderSection
                                        styles={styles}
                                        setStyle={setStyle}
                                        gate={() => true}
                                        keys={SHAPE_BORDER_KEYS}
                                        styleOptions={[
                                            { value: 'solid', label: 'Solid' },
                                            { value: 'dashed', label: 'Dashed' },
                                            { value: 'dotted', label: 'Dotted' },
                                        ]}
                                    />
                                ))}
                            </>
                        )}

                        {el.type === ELEMENT_TYPES.QRCODE && (
                            <>
                                {/* Admin-only — it wires layers together, so it
                                    carries no user lock; the 'qr' lock is about the
                                    code itself, and gates it at the store. */}
                                <LinkedContentSection
                                    el={el}
                                    selectedIds={selectedIds}
                                    elements={doc.elements}
                                    mode={mode}
                                    onLink={linkContent}
                                    onUnlink={unlinkContent}
                                    onRename={renameContentGroup}
                                />
                                {section('qr', (
                                    <QrSection el={el} styles={styles} setProp={setProp} setStyle={setStyle} mode={mode} />
                                ))}
                                {section('border', (
                                    <BorderSection
                                        styles={styles}
                                        setStyle={setStyle}
                                        gate={gate}
                                        showStyle={false}
                                        maxWidth={20}
                                    />
                                ))}
                            </>
                        )}

                        {el.type === ELEMENT_TYPES.BARCODE && (
                            <>
                                <LinkedContentSection
                                    el={el}
                                    selectedIds={selectedIds}
                                    elements={doc.elements}
                                    mode={mode}
                                    onLink={linkContent}
                                    onUnlink={unlinkContent}
                                    onRename={renameContentGroup}
                                />
                                {section('barcode', (
                                    <BarcodeSection el={el} styles={styles} setProp={setProp} setStyle={setStyle} mode={mode} />
                                ))}
                                {section('border', (
                                    <BorderSection
                                        styles={styles}
                                        setStyle={setStyle}
                                        gate={gate}
                                        showStyle={false}
                                        maxWidth={20}
                                    />
                                ))}
                            </>
                        )}

                        {el.type === ELEMENT_TYPES.TABLE && (
                            <>
                                <LinkedContentSection
                                    el={el}
                                    selectedIds={selectedIds}
                                    elements={doc.elements}
                                    mode={mode}
                                    onLink={linkContent}
                                    onUnlink={unlinkContent}
                                    onRename={renameContentGroup}
                                />
                                {section('table', (
                                    <TableSection el={el} styles={styles} setProp={setProp} setStyle={setStyle} mode={mode} />
                                ))}
                                {section('typography', (
                                    <TypographySection
                                        styles={styles}
                                        setStyle={setStyle}
                                        setStyles={setStyles}
                                        gate={(k) => ['fontFamily', 'fontSize', 'fontWeight', 'textAlign', 'verticalAlign'].includes(k) && gate(k)}
                                    />
                                ))}
                                {gate('color') && section('textFill', (
                                    <SolidColorSection
                                        title="Text color"
                                        value={styles.color || '#18181b'}
                                        onChange={(v) => setStyle('color', v)}
                                    />
                                ))}
                            </>
                        )}

                        {section('effects', (
                            <EffectsSection
                                el={el}
                                styles={styles}
                                setStyle={setStyle}
                                setStyles={setStyles}
                                gate={gate}
                                isText={isText}
                            />
                        ))}
                        {/* Shape swapping is a rare, destructive-feeling act — it sits
                            below the paint and effects an admin actually tunes. */}
                        {el.type === ELEMENT_TYPES.SHAPE && section('shape', (
                            <ShapeSection el={el} setProp={setProp} mode={mode} />
                        ))}
                        {/* Last, because it is about the panel above rather than
                            about the artwork — the admin sets it once and moves on. */}
                        <UserAccessSection el={el} updateElement={updateElement} mode={mode} />
                        {showExport && <ExportSection onExport={onExport} disabled={saving && !!onExport} label={exportLabel} />}
                    </>
                )}
            </div>
        </div>
    );
}
