import React from 'react';
import { useTranslation } from 'react-i18next';
import {
  AlignCenter,
  AlignLeft,
  AlignRight,
  BadgeCheck,
  ChevronDown,
  Download,
  Heading,
  Link as LinkIcon,
  Loader2,
  Megaphone,
  Minus,
  Plus,
  Printer,
  QrCode,
  RotateCcw,
  Save,
  Trash2,
  Type,
  Undo2,
  Redo2,
  UploadCloud,
} from 'lucide-react';

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { cn } from '@/lib/utils';
import { ensureFontLoaded, useTenantFonts } from '../../lib/fonts';
import { preloadedBackgrounds, type BackgroundImageOption } from './invite-layout/backgrounds';

const DEFAULT_FONT_VALUE = '__default';

import type { EventQrInvite, EventQrInviteLayout } from '../../api';
import { authorizedFetch } from '../../auth/tokens';

import {
  CANVAS_HEIGHT,
  CANVAS_WIDTH,
  QrLayoutCustomization,
  LayoutElement,
  LayoutElementPayload,
  LayoutElementType,
  LayoutSerializationContext,
  buildDefaultElements,
  clamp,
  clampElement,
  normalizeElements,
  payloadToElements,
} from './invite-layout/schema';
import { DesignerCanvas } from './invite-layout/DesignerCanvas';
import {
  generatePdfBytes,
  generatePngDataUrl,
  openPdfInNewTab,
  triggerDownloadFromBlob,
  triggerDownloadFromDataUrl,
} from './invite-layout/export-utils';
import { buildDownloadFilename, normalizeEventDateSegment } from './invite-layout/fileNames';

export type { QrLayoutCustomization } from './invite-layout/schema';

const ELEMENT_BINDING_FIELD: Partial<Record<string, keyof QrLayoutCustomization>> = {
  headline: 'headline',
  subtitle: 'subtitle',
  description: 'description',
  badge: 'badge_label',
  link: 'link_label',
  cta: 'cta_label',
};

function sanitizeColor(value: string | null | undefined): string | null {
  if (!value) {
    return null;
  }

  const trimmed = value.trim();
  const hexMatch = trimmed.match(/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/);
  if (hexMatch) {
    if (trimmed.length === 4) {
      const r = trimmed[1];
      const g = trimmed[2];
      const b = trimmed[3];
      return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
    }
    return trimmed.toUpperCase();
  }

  return null;
}

function sanitizeGradientStops(stops: unknown): string[] | null {
  if (!Array.isArray(stops)) {
    return null;
  }

  const normalized = stops
    .map((stop) => (typeof stop === 'string' ? sanitizeColor(stop) : null))
    .filter((stop): stop is string => !!stop);

  return normalized.length ? normalized : null;
}

function sanitizePayload(payload: QrLayoutCustomization): QrLayoutCustomization {
  const normalized: QrLayoutCustomization = { ...payload };

  normalized.accent_color = sanitizeColor(payload.accent_color ?? null) ?? undefined;
  normalized.text_color = sanitizeColor(payload.text_color ?? null) ?? undefined;
  normalized.background_color = sanitizeColor(payload.background_color ?? null) ?? undefined;
  normalized.secondary_color = sanitizeColor(payload.secondary_color ?? null) ?? undefined;
  normalized.badge_color = sanitizeColor(payload.badge_color ?? null) ?? undefined;
  if (typeof payload.background_image === 'string') {
    const trimmed = payload.background_image.trim();
    normalized.background_image = trimmed.length ? trimmed : undefined;
  } else {
    normalized.background_image = undefined;
  }

  if (payload.background_gradient && typeof payload.background_gradient === 'object') {
    const { angle, stops } = payload.background_gradient as { angle?: number; stops?: unknown };
    const normalizedStops = sanitizeGradientStops(stops);
    normalized.background_gradient = normalizedStops
      ? { angle: typeof angle === 'number' ? angle : 180, stops: normalizedStops }
      : null;
  }

  if (Array.isArray(payload.instructions)) {
    normalized.instructions = payload.instructions
      .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
      .filter((entry) => entry.length > 0);
  }

  if (typeof payload.link_label === 'string') {
    normalized.link_label = payload.link_label.trim();
  }

  return normalized;
}

function serializeElements(elements: LayoutElement[], context: LayoutSerializationContext): LayoutElementPayload[] {
  return normalizeElements(elements).map((element) => {
    const base = clampElement(element);
    let content: string | null = base.content ?? null;

    switch (base.type) {
      case 'headline':
        content = context.form.headline ?? context.eventName;
        break;
      case 'subtitle':
        content = context.form.subtitle ?? '';
        break;
      case 'description':
        content = context.form.description ?? '';
        break;
      case 'link':
        content = context.form.link_label && context.form.link_label.trim().length > 0 ? context.form.link_label : context.inviteUrl;
        break;
      case 'badge':
        content = context.form.badge_label ?? context.badgeFallback;
        break;
      case 'logo':
        content = context.logoUrl ?? context.form.logo_url ?? null;
        break;
      case 'cta':
        content = context.form.cta_label ?? context.form.link_label ?? context.inviteUrl;
        break;
      default:
        break;
    }

    return {
      id: base.id,
      type: base.type,
      x: Math.round(base.x),
      y: Math.round(base.y),
      width: Math.round(base.width),
      height: Math.round(base.height),
      rotation: Math.round(base.rotation ?? 0),
      font_size: base.fontSize ? Math.round(base.fontSize) : undefined,
      align: base.align,
      content,
      font_family: base.fontFamily ?? null,
      letter_spacing: base.letterSpacing,
      line_height: base.lineHeight,
      fill: base.fill ?? null,
      locked: Boolean(base.locked),
    };
  });
}

type InviteLayoutCustomizerPanelProps = {
  invite: EventQrInvite | null;
  eventName: string;
  eventDate: string | null;
  backgroundImages?: BackgroundImageOption[];
  saving: boolean;
  resetting: boolean;
  onSave: (customization: QrLayoutCustomization) => Promise<void>;
  onReset: () => Promise<void>;
  initialCustomization: QrLayoutCustomization | null;
  draftCustomization?: QrLayoutCustomization | null;
  onDraftChange?: (draft: QrLayoutCustomization | null) => void;
};

const MAX_INSTRUCTIONS = 5;
const ZOOM_MIN = 0.1;
const ZOOM_MAX = 2;
const ZOOM_STEP = 0.05;

export function InviteLayoutCustomizerPanel({
  invite,
  eventName,
  eventDate,
  saving,
  resetting,
  onSave,
  onReset,
  initialCustomization,
  draftCustomization,
  onDraftChange,
  backgroundImages = preloadedBackgrounds,
}: InviteLayoutCustomizerPanelProps): React.JSX.Element {
  const { t } = useTranslation('management');
  const { fonts: availableFonts, isLoading: fontsLoading } = useTenantFonts();

  const inviteUrl = invite?.url ?? '';
  const qrCodeDataUrl = invite?.qr_code_data_url ?? null;
  if (!qrCodeDataUrl) {
    console.warn('QR DataURL is null - using fallback in canvas');
  }
  const defaultInstructions = React.useMemo(() => {
    const value = t('tasks.customizer.defaults.instructions', { returnObjects: true }) as unknown;
    return Array.isArray(value) ? (value as string[]) : ['QR-Code scannen', 'Profil anlegen', 'Fotos teilen'];
  }, [t]);

  const [availableLayouts, setAvailableLayouts] = React.useState<EventQrInviteLayout[]>(invite?.layouts ?? []);
  const [layoutsLoading, setLayoutsLoading] = React.useState(false);
  const [layoutsError, setLayoutsError] = React.useState<string | null>(null);
  const [selectedLayoutId, setSelectedLayoutId] = React.useState<string | undefined>(
    draftCustomization?.layout_id ?? initialCustomization?.layout_id ?? invite?.layouts?.[0]?.id,
  );
  const [form, setForm] = React.useState<QrLayoutCustomization>({});
  const [instructions, setInstructions] = React.useState<string[]>([]);
  const [error, setError] = React.useState<string | null>(null);
  const formRef = React.useRef<HTMLFormElement | null>(null);
  const [downloadBusy, setDownloadBusy] = React.useState<string | null>(null);
  const [printBusy, setPrintBusy] = React.useState(false);
  const [elements, setElements] = React.useState<LayoutElement[]>([]);
  const [activeElementId, setActiveElementId] = React.useState<string | null>(null);
  const [inspectorElementId, setInspectorElementId] = React.useState<string | null>(null);
  const [showFloatingActions, setShowFloatingActions] = React.useState(false);
  const [zoomScale, setZoomScale] = React.useState(1);
  const [fitScale, setFitScale] = React.useState(1);
  const [previewMode, setPreviewMode] = React.useState<'fit' | 'full'>('fit');
  const [isCompact, setIsCompact] = React.useState(false);
  const fitScaleRef = React.useRef(1);
  const manualZoomRef = React.useRef(false);
  const actionsSentinelRef = React.useRef<HTMLDivElement | null>(null);
  const historyRef = React.useRef<LayoutElement[][]>([]);
  const historyIndexRef = React.useRef(-1);
  const restoringRef = React.useRef(false);
  const [canUndo, setCanUndo] = React.useState(false);
  const [canRedo, setCanRedo] = React.useState(false);
  const designerViewportRef = React.useRef<HTMLDivElement | null>(null);
  const canvasContainerRef = React.useRef<HTMLDivElement | null>(null);
  const draftSignatureRef = React.useRef<string | null>(null);
  const initialElementsRef = React.useRef<LayoutElement[]>([]);
  const activeCustomization = React.useMemo(
    () => draftCustomization ?? initialCustomization ?? null,
    [draftCustomization, initialCustomization],
  );
  const customizationSignature = React.useMemo(
    () => (activeCustomization ? JSON.stringify(activeCustomization) : null),
    [activeCustomization],
  );
  const appliedSignatureRef = React.useRef<string | null>(null);
  const appliedLayoutRef = React.useRef<string | null>(null);
  const appliedInviteRef = React.useRef<number | string | null>(null);

  React.useEffect(() => {
    if (!availableFonts.length || !elements.length) {
      return;
    }

    const families = Array.from(
      new Set(
        elements
          .map((element) => element.fontFamily)
          .filter((value): value is string => Boolean(value)),
      ),
    );

    families.forEach((family) => {
      const font = availableFonts.find((entry) => entry.family === family);
      if (font) {
        void ensureFontLoaded(font);
      }
    });
  }, [availableFonts, elements]);

  React.useEffect(() => {
    if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
      setIsCompact(false);
      return;
    }

    const query = window.matchMedia('(max-width: 1023px)');
    const update = (event?: MediaQueryListEvent) => {
      if (typeof event?.matches === 'boolean') {
        setIsCompact(event.matches);
        return;
      }
      setIsCompact(query.matches);
    };

    update();

    if (typeof query.addEventListener === 'function') {
      const listener = (event: MediaQueryListEvent) => update(event);
      query.addEventListener('change', listener);
      return () => query.removeEventListener('change', listener);
    }

    const legacyListener = (event: MediaQueryListEvent) => update(event);
    query.addListener(legacyListener);
    return () => query.removeListener(legacyListener);
  }, []);

  const clampZoom = React.useCallback(
    (value: number) => clamp(Number.isFinite(value) ? value : 1, ZOOM_MIN, ZOOM_MAX),
    [],
  );

  const recomputeFitScale = React.useCallback(() => {
    const viewport = designerViewportRef.current;
    if (!viewport) {
      return;
    }

    const { clientWidth, clientHeight } = viewport;
    if (!clientWidth || !clientHeight) {
      return;
    }

    const style = window.getComputedStyle(viewport);
    const paddingX = parseFloat(style.paddingLeft ?? '0') + parseFloat(style.paddingRight ?? '0');
    const paddingY = parseFloat(style.paddingTop ?? '0') + parseFloat(style.paddingBottom ?? '0');

    const availableWidth = clientWidth - paddingX;
    const availableHeight = clientHeight - paddingY;

    if (availableWidth <= 0 || availableHeight <= 0) {
      return;
    }

    const widthScale = availableWidth / CANVAS_WIDTH;
    const heightScale = availableHeight / CANVAS_HEIGHT;
    const nextRaw = Math.min(widthScale, heightScale);
    let baseScale = Number.isFinite(nextRaw) && nextRaw > 0 ? nextRaw : 1;
    const minScale = 0.3;
    baseScale = Math.max(baseScale, minScale);
    const limitedScale = Math.min(baseScale, 1);
    const clamped = clampZoom(limitedScale);

    fitScaleRef.current = clamped;
    setFitScale((prev) => (Math.abs(prev - clamped) < 0.001 ? prev : clamped));
    if (!manualZoomRef.current) {
      setZoomScale((prev) => (Math.abs(prev - clamped) < 0.001 ? prev : clamped));
    }

    console.debug('[Invites][Zoom] viewport size', {
      availableWidth,
      availableHeight,
      widthScale,
      heightScale,
      clamped,
    });
  }, [clampZoom]);

  React.useLayoutEffect(() => {
    recomputeFitScale();
  }, [recomputeFitScale]);

  React.useEffect(() => {
    const viewport = designerViewportRef.current;

    const handleResize = () => {
      recomputeFitScale();
    };

    window.addEventListener('resize', handleResize);

    let observer: ResizeObserver | null = null;
    if (viewport && typeof ResizeObserver === 'function') {
      observer = new ResizeObserver(() => recomputeFitScale());
      observer.observe(viewport);
    }

    recomputeFitScale();

    return () => {
      window.removeEventListener('resize', handleResize);
      if (observer) {
        observer.disconnect();
      }
    };
  }, [recomputeFitScale]);

  const cloneElements = React.useCallback(
    (items: LayoutElement[]): LayoutElement[] => items.map((item) => ({ ...item })),
    []
  );

  const elementsAreEqual = React.useCallback((a: LayoutElement[], b: LayoutElement[]): boolean => {
    if (a.length !== b.length) {
      return false;
    }
    for (let index = 0; index < a.length; index += 1) {
      const left = a[index];
      const right = b[index];
      if (
        left.id !== right.id ||
        left.type !== right.type ||
        left.x !== right.x ||
        left.y !== right.y ||
        left.width !== right.width ||
        left.height !== right.height ||
        (left.rotation ?? 0) !== (right.rotation ?? 0) ||
        (left.fontSize ?? null) !== (right.fontSize ?? null) ||
        (left.align ?? null) !== (right.align ?? null) ||
        (left.content ?? null) !== (right.content ?? null) ||
        (left.fontFamily ?? null) !== (right.fontFamily ?? null) ||
        (left.letterSpacing ?? null) !== (right.letterSpacing ?? null) ||
        (left.lineHeight ?? null) !== (right.lineHeight ?? null) ||
        (left.fill ?? null) !== (right.fill ?? null) ||
        Boolean(left.locked) !== Boolean(right.locked) ||
        Boolean(left.initial) !== Boolean(right.initial)
      ) {
        return false;
      }
    }
    return true;
  }, []);

  const pushHistory = React.useCallback(
    (snapshot: LayoutElement[]) => {
      const copy = cloneElements(snapshot);
      let history = historyRef.current.slice(0, historyIndexRef.current + 1);
      history.push(copy);
      if (history.length > 60) {
        history = history.slice(history.length - 60);
      }
      historyRef.current = history;
      historyIndexRef.current = history.length - 1;
      setCanUndo(historyIndexRef.current > 0);
      setCanRedo(false);
    },
    [cloneElements]
  );

  const resetHistory = React.useCallback(
    (snapshot: LayoutElement[]) => {
      const copy = cloneElements(snapshot);
      historyRef.current = [copy];
      historyIndexRef.current = 0;
      setCanUndo(false);
      setCanRedo(false);
    },
    [cloneElements]
  );

  const commitElements = React.useCallback(
    (producer: (current: LayoutElement[]) => LayoutElement[], options?: { silent?: boolean }) => {
      setElements((prev) => {
        const source = prev.length ? prev : initialElementsRef.current;
        const base = cloneElements(source.length ? source : []);
        const produced = producer(base);
        const normalized = normalizeElements(produced);
        if (elementsAreEqual(prev, normalized)) {
          return prev;
        }
        if (!options?.silent && !restoringRef.current) {
          pushHistory(normalized);
        }
        return normalized;
      });
    },
    [cloneElements, pushHistory, elementsAreEqual]
  );

  const selectElement = React.useCallback((id: string | null, options: { preserveInspector?: boolean } = {}) => {
    setActiveElementId(id);
    if (id) {
      setInspectorElementId(id);
      return;
    }

    if (!options.preserveInspector) {
      setInspectorElementId(null);
    }
  }, []);

  const handleUndo = React.useCallback(() => {
    if (historyIndexRef.current <= 0) {
      return;
    }
    restoringRef.current = true;
    historyIndexRef.current -= 1;
    const snapshot = cloneElements(historyRef.current[historyIndexRef.current] ?? []);
    setElements(snapshot);
    setCanUndo(historyIndexRef.current > 0);
    setCanRedo(historyRef.current.length > historyIndexRef.current + 1);
    restoringRef.current = false;
  }, [cloneElements]);

  const handleRedo = React.useCallback(() => {
    if (historyIndexRef.current === -1) {
      return;
    }
    if (historyIndexRef.current >= historyRef.current.length - 1) {
      return;
    }
    restoringRef.current = true;
    historyIndexRef.current += 1;
    const snapshot = cloneElements(historyRef.current[historyIndexRef.current] ?? []);
    setElements(snapshot);
    setCanUndo(historyIndexRef.current > 0);
    setCanRedo(historyRef.current.length > historyIndexRef.current + 1);
    restoringRef.current = false;
  }, [cloneElements]);

  const formStateRef = React.useRef(form);
  React.useEffect(() => {
    formStateRef.current = form;
  }, [form]);
  const activeLayout = React.useMemo(() => {
    if (!availableLayouts.length) {
      return null;
    }

    if (selectedLayoutId) {
      const match = availableLayouts.find((layout) => layout.id === selectedLayoutId);
      if (match) {
        return match;
      }
    }

    return availableLayouts[0];
  }, [availableLayouts, selectedLayoutId]);

  React.useEffect(() => {
    manualZoomRef.current = false;
    recomputeFitScale();
  }, [recomputeFitScale, activeLayout?.id, invite?.id]);

  const activeLayoutQrSize = React.useMemo(() => {
    const qrElement = elements.find((element) => element.type === 'qr');
    if (qrElement && typeof qrElement.width === 'number' && qrElement.width > 0) {
      return qrElement.width;
    }

    if (activeCustomization?.mode === 'advanced' && Array.isArray(activeCustomization.elements)) {
      const qrElement = activeCustomization.elements.find((element) => element?.type === 'qr');
      if (qrElement && typeof qrElement.width === 'number' && qrElement.width > 0) {
        return qrElement.width;
      }
    }

    return activeLayout?.preview?.qr_size_px ?? 500;
  }, [elements, activeCustomization?.mode, activeCustomization?.elements, activeLayout?.preview?.qr_size_px]);

  const effectiveScale = React.useMemo(() => {
    if (previewMode === 'full') {
      return 1.0;
    }
    return clampZoom(Number.isFinite(zoomScale) && zoomScale > 0 ? zoomScale : fitScale);
  }, [clampZoom, zoomScale, fitScale, previewMode]);
  const zoomPercent = Math.round(effectiveScale * 100);

  const handleZoomStep = React.useCallback(
    (direction: 1 | -1) => {
      manualZoomRef.current = true;
      setZoomScale((current) => clampZoom(current + direction * ZOOM_STEP));
    },
    [clampZoom]
  );

  const updateElement = React.useCallback(
    (id: string, updater: Partial<LayoutElement> | ((element: LayoutElement) => Partial<LayoutElement>), options?: { silent?: boolean }) => {
      commitElements(
        (current) =>
          current.map((element) => {
            if (element.id !== id) {
              return element;
            }
            const patch = typeof updater === 'function' ? updater(element) : updater;
            return clampElement({ ...element, ...patch });
          }),
        options
      );
    },
    [commitElements]
  );

  const handleElementFontChange = React.useCallback(
    (id: string, family: string) => {
      updateElement(id, { fontFamily: family || null });
      const font = availableFonts.find((entry) => entry.family === family);
      if (font) {
        void ensureFontLoaded(font).then(() => {
          fabricCanvasRef.current?.requestRenderAll();
        });
      }
    },
    [availableFonts, updateElement]
  );

  const handleFontOptionPreview = React.useCallback(
    (family: string) => {
      const font = availableFonts.find((entry) => entry.family === family);
      if (font) {
        void ensureFontLoaded(font);
      }
    },
    [availableFonts]
  );

  React.useEffect(() => {
    if (!invite) {
      setAvailableLayouts([]);
      setSelectedLayoutId(undefined);
      return;
    }

    const layouts = invite.layouts ?? [];
    setAvailableLayouts(layouts);
    setLayoutsError(null);
    setSelectedLayoutId((current) => {
      if (current && layouts.some((layout) => layout.id === current)) {
        return current;
      }
      if (activeCustomization?.layout_id && layouts.some((layout) => layout.id === activeCustomization.layout_id)) {
        return activeCustomization.layout_id;
      }
      return layouts[0]?.id;
    });
  }, [invite, activeCustomization?.layout_id]);

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

    async function loadLayouts(url: string) {
      try {
        setLayoutsLoading(true);
        setLayoutsError(null);
        const target = (() => {
          try {
            if (url.startsWith('http://') || url.startsWith('https://')) {
              const parsed = new URL(url);
              return parsed.pathname + parsed.search;
            }
          } catch (parseError) {
            console.warn('[Invites] Failed to parse layout URL', parseError);
          }
          return url;
        })();
        const response = await authorizedFetch(target, {
          method: 'GET',
          headers: { Accept: 'application/json' },
        });

        if (!response.ok) {
          console.error('[Invites] Layout request failed', response.status, response.statusText);
          throw new Error(`Failed with status ${response.status}`);
        }

        const json = await response.json();
        const items = Array.isArray(json?.data) ? (json.data as EventQrInviteLayout[]) : [];

        if (!cancelled) {
          setAvailableLayouts(items);
          setSelectedLayoutId((current) => {
            if (current && items.some((layout) => layout.id === current)) {
              return current;
            }
            if (activeCustomization?.layout_id && items.some((layout) => layout.id === activeCustomization.layout_id)) {
              return activeCustomization.layout_id;
            }
            return items[0]?.id;
          });
        }
      } catch (err) {
        if (!cancelled) {
          console.error('[Invites] Failed to load layouts', err);
          setLayoutsError(t('invites.customizer.loadingError', 'Layouts konnten nicht geladen werden.'));
        }
      } finally {
        if (!cancelled) {
          setLayoutsLoading(false);
        }
      }
    }

    if (!invite || availableLayouts.length > 0 || !invite.layouts_url) {
      return () => {
        cancelled = true;
      };
    }

    void loadLayouts(invite.layouts_url);

    return () => {
      cancelled = true;
    };
  }, [invite, availableLayouts.length, activeCustomization?.layout_id, t]);

  React.useEffect(() => {
    if (!availableLayouts.length) {
      return;
    }

    setSelectedLayoutId((current) => {
      if (current && availableLayouts.some((layout) => layout.id === current)) {
        return current;
      }
      if (activeCustomization?.layout_id && availableLayouts.some((layout) => layout.id === activeCustomization.layout_id)) {
        return activeCustomization.layout_id;
      }
      return availableLayouts[0].id;
    });
  }, [availableLayouts, activeCustomization?.layout_id]);

  React.useEffect(() => {
    const inviteKey = invite?.id ?? null;
    const layoutId = activeLayout?.id ?? null;
    const incomingSignature = customizationSignature;

    if (!invite || !activeLayout) {
      setForm({});
      setInstructions([]);
      commitElements(() => [], { silent: true });
      resetHistory([]);
      initialElementsRef.current = [];
      appliedSignatureRef.current = null;
      appliedLayoutRef.current = layoutId;
      appliedInviteRef.current = inviteKey;
      return;
    }

    if (
      draftCustomization &&
      incomingSignature &&
      incomingSignature === draftSignatureRef.current &&
      appliedLayoutRef.current === layoutId &&
      appliedInviteRef.current === inviteKey
    ) {
      appliedSignatureRef.current = incomingSignature;
      appliedLayoutRef.current = layoutId;
      appliedInviteRef.current = inviteKey;
      return;
    }

    const alreadyApplied =
      appliedSignatureRef.current === incomingSignature &&
      appliedLayoutRef.current === layoutId &&
      appliedInviteRef.current === inviteKey;

    if (alreadyApplied) {
      return;
    }

    const reuseCustomization = activeCustomization?.layout_id === activeLayout.id;

    const baseInstructions = reuseCustomization && Array.isArray(activeCustomization?.instructions) && activeCustomization.instructions?.length
      ? [...(activeCustomization.instructions as string[])]
      : ((activeLayout.instructions && activeLayout.instructions.length)
        ? [...activeLayout.instructions]
        : [...defaultInstructions]);

    setInstructions(baseInstructions);

    const newForm: QrLayoutCustomization = {
      layout_id: activeLayout.id,
      headline: reuseCustomization ? activeCustomization?.headline ?? eventName : eventName,
      subtitle: reuseCustomization ? activeCustomization?.subtitle ?? activeLayout.subtitle ?? '' : activeLayout.subtitle ?? '',
      description: reuseCustomization ? activeCustomization?.description ?? activeLayout.description ?? '' : activeLayout.description ?? '',
      badge_label: reuseCustomization ? activeCustomization?.badge_label ?? t('tasks.customizer.defaults.badgeLabel') : (activeLayout.badge_label ?? t('tasks.customizer.defaults.badgeLabel')),
      instructions_heading: reuseCustomization ? activeCustomization?.instructions_heading ?? t('tasks.customizer.defaults.instructionsHeading') : t('tasks.customizer.defaults.instructionsHeading'),
      link_heading: reuseCustomization ? activeCustomization?.link_heading ?? t('tasks.customizer.defaults.linkHeading') : t('tasks.customizer.defaults.linkHeading'),
      link_label: reuseCustomization ? activeCustomization?.link_label ?? inviteUrl : inviteUrl,
      cta_label: reuseCustomization ? activeCustomization?.cta_label ?? t('tasks.customizer.defaults.ctaLabel') : (activeLayout.cta_label ?? t('tasks.customizer.defaults.ctaLabel')),
      accent_color: sanitizeColor((reuseCustomization ? activeCustomization?.accent_color : activeLayout.preview?.accent) ?? null) ?? '#6366F1',
      text_color: sanitizeColor((reuseCustomization ? activeCustomization?.text_color : activeLayout.preview?.text) ?? null) ?? '#111827',
      background_color: sanitizeColor((reuseCustomization ? activeCustomization?.background_color : activeLayout.preview?.background) ?? null) ?? '#FFFFFF',
      secondary_color: reuseCustomization ? activeCustomization?.secondary_color ?? '#1F2937' : '#1F2937',
      badge_color: reuseCustomization ? activeCustomization?.badge_color ?? '#2563EB' : '#2563EB',
      background_image: reuseCustomization ? activeCustomization?.background_image ?? null : null,
      background_gradient: reuseCustomization ? activeCustomization?.background_gradient ?? activeLayout.preview?.background_gradient ?? null : activeLayout.preview?.background_gradient ?? null,
      logo_data_url: reuseCustomization ? activeCustomization?.logo_data_url ?? activeCustomization?.logo_url ?? null : null,
      mode: reuseCustomization ? activeCustomization?.mode : 'standard',
      elements: reuseCustomization ? activeCustomization?.elements : undefined,
    };
    setForm(newForm);
    setError(null);

    const isCustomizedAdvanced = newForm.mode === 'advanced' && Array.isArray(newForm.elements) && newForm.elements.length > 0;
    const fallbackQrSize = (() => {
      if (Array.isArray(newForm.elements)) {
        const qrElement = newForm.elements.find((element) => element?.type === 'qr');
        if (qrElement && typeof qrElement.width === 'number' && qrElement.width > 0) {
          return qrElement.width;
        }
      }
      if (typeof activeLayout.preview?.qr_size_px === 'number' && activeLayout.preview.qr_size_px > 0) {
        return activeLayout.preview.qr_size_px;
      }
      return 500;
    })();

    if (isCustomizedAdvanced) {
      const initialElements = normalizeElements(payloadToElements(newForm.elements));
      initialElementsRef.current = initialElements;
      commitElements(() => initialElements, { silent: true });
      resetHistory(initialElements);
    } else {
      const defaults = buildDefaultElements(activeLayout, newForm, eventName, fallbackQrSize);
      const normalizedDefaults = normalizeElements(defaults);
      initialElementsRef.current = normalizedDefaults;
      commitElements(() => normalizedDefaults, { silent: true });
      resetHistory(normalizedDefaults);
    }

    appliedSignatureRef.current = incomingSignature ?? null;
    appliedLayoutRef.current = layoutId;
    appliedInviteRef.current = inviteKey;
    selectElement(null);
  }, [
    activeLayout,
    invite,
    invite?.id,
    activeCustomization,
    draftCustomization,
    customizationSignature,
    defaultInstructions,
    eventName,
    inviteUrl,
    t,
    commitElements,
    resetHistory,
    selectElement,
  ]);

  React.useEffect(() => {
    if (typeof IntersectionObserver === 'undefined') {
      setShowFloatingActions(false);
      return;
    }

    const node = actionsSentinelRef.current;
    if (!node) {
      setShowFloatingActions(false);
      return;
    }

    const observer = new IntersectionObserver(([entry]) => {
      setShowFloatingActions(!entry.isIntersecting);
    });

    observer.observe(node);

    return () => {
      observer.disconnect();
    };
  }, [invite?.id, activeLayout?.id]);

  const nonRemovableIds = React.useMemo(
    () => new Set(['headline', 'qr']),
    []
  );

  const effectiveInstructions = instructions.filter((entry) => entry.trim().length > 0);

  const canvasElements = React.useMemo(() => {
    if (!activeLayout) {
      return [] as LayoutElement[];
    }

    const base = elements.length
      ? elements
      : buildDefaultElements(activeLayout, form, eventName, activeLayoutQrSize);

    return base.map((element) => ({
      ...element,
      initial: element.initial ?? nonRemovableIds.has(element.id),
    }));
  }, [activeLayout, elements, form, eventName, activeLayoutQrSize, nonRemovableIds]);

  React.useEffect(() => {
    if (!onDraftChange) {
      return;
    }

    if (!invite || !activeLayout) {
      if (draftSignatureRef.current !== null) {
        draftSignatureRef.current = null;
        onDraftChange(null);
      }
      return;
    }

    const serializationContext: LayoutSerializationContext = {
      form,
      eventName,
      inviteUrl,
      instructions: effectiveInstructions,
      qrSize: activeLayoutQrSize,
      badgeFallback: t('tasks.customizer.defaults.badgeLabel'),
      logoUrl: form.logo_url ?? null,
    };

    const advancedElements = elements.length
      ? elements
      : buildDefaultElements(activeLayout, form, eventName, activeLayoutQrSize);

    const draftPayload: QrLayoutCustomization = {
      ...form,
      layout_id: activeLayout.id,
      instructions: effectiveInstructions,
      mode: 'advanced',
      elements: serializeElements(advancedElements, serializationContext),
    };

    const sanitizedDraft = sanitizePayload(draftPayload);
    const signature = JSON.stringify(sanitizedDraft);
    if (signature !== draftSignatureRef.current) {
      draftSignatureRef.current = signature;
      onDraftChange(sanitizedDraft);
    }
  }, [
    onDraftChange,
    invite,
    invite?.id,
    activeLayout,
    activeLayout?.id,
    form,
    elements,
    effectiveInstructions,
    eventName,
    inviteUrl,
    activeLayoutQrSize,
    t,
  ]);

  const elementLabelFor = React.useCallback(
    (element: LayoutElement): string => {
      switch (element.type) {
        case 'headline':
          return t('invites.customizer.elements.headline', 'Überschrift');
        case 'subtitle':
          return t('invites.customizer.elements.subtitle', 'Untertitel');
        case 'description':
          return t('invites.customizer.elements.description', 'Beschreibung');
        case 'badge':
          return t('invites.customizer.elements.badge', 'Badge');
        case 'link':
          return t('invites.customizer.elements.link', 'Linkfeld');
        case 'cta':
          return t('invites.customizer.elements.cta', 'Call-to-Action');
        case 'qr':
          return t('invites.customizer.elements.qr', 'QR-Code');
        case 'logo':
          return t('invites.customizer.elements.logo', 'Logo');
        case 'text':
        default:
          if (element.content && element.content.trim().length) {
            const preview = element.content.trim();
            return preview.length > 28 ? `${preview.slice(0, 28)}…` : preview;
          }
          return t('invites.customizer.elements.text', 'Freier Textblock');
      }
    },
    [t]
  );

  const elementIconFor = React.useCallback((element: LayoutElement) => {
    switch (element.type) {
      case 'headline':
      case 'subtitle':
        return Heading;
      case 'description':
      case 'text':
        return AlignLeft;
      case 'badge':
        return BadgeCheck;
      case 'link':
        return LinkIcon;
      case 'cta':
        return Megaphone;
      case 'qr':
        return QrCode;
      case 'logo':
        return Type;
      default:
        return AlignLeft;
    }
  }, []);

  const createPresetElement = React.useCallback(
    (type: LayoutElementType, preferredId?: string): LayoutElement | null => {
      if (!activeLayout) {
        return null;
      }

      const baseId = preferredId ?? `text-${Date.now()}`;
      const currentForm = formStateRef.current ?? {};

      switch (type) {
        case 'subtitle':
          return { ...clampElement({
            id: 'subtitle',
            type: 'subtitle',
            x: 80,
            y: 240,
            width: 520,
            height: 110,
            fontSize: 32,
            content: (currentForm.subtitle as string) ?? '',
            align: 'left',
          }), initial: false };
        case 'badge':
          return { ...clampElement({
            id: 'badge',
            type: 'badge',
            x: 80,
            y: 40,
            width: 220,
            height: 70,
            align: 'center',
            fontSize: 22,
            content: (currentForm.badge_label as string) ?? t('tasks.customizer.defaults.badgeLabel'),
          }), initial: false };
        case 'link':
          return { ...clampElement({
            id: 'link',
            type: 'link',
            x: 660,
            y: 340 + activeLayoutQrSize,
            width: 360,
            height: 110,
            fontSize: 26,
            align: 'center',
            content:
              currentForm.link_label && String(currentForm.link_label).trim().length > 0 ? String(currentForm.link_label) : inviteUrl,
          }), initial: false };
        case 'cta':
          return { ...clampElement({
            id: 'cta',
            type: 'cta',
            x: 660,
            y: 340 + activeLayoutQrSize + 130,
            width: 360,
            height: 100,
            align: 'center',
            fontSize: 24,
            content: (currentForm.cta_label as string) ?? t('tasks.customizer.defaults.ctaLabel'),
          }), initial: false };
        case 'text':
          return { ...clampElement({
            id: baseId,
            type: 'text',
            x: 120,
            y: 520,
            width: 520,
            height: 140,
            fontSize: 26,
            align: 'left',
            content: t('invites.customizer.defaults.textBlock', 'Neuer Textblock – hier kannst du eigene Hinweise ergänzen.'),
          }), initial: false };
        default:
          return null;
      }
    },
    [activeLayout, activeLayoutQrSize, inviteUrl, t]
  );

  const addElementFromPreset = React.useCallback(
    (type: LayoutElementType, preferredId?: string) => {
      const preset = createPresetElement(type, preferredId);
      if (!preset) {
        return;
      }

      commitElements((current) => {
        const next = preferredId ? current.filter((item) => item.id !== preferredId) : [...current];
        next.push({ ...preset, initial: false });
        return next;
      });
      selectElement(preset.id);
    },
    [createPresetElement, commitElements, selectElement]
  );

  const removeElement = React.useCallback(
    (id: string) => {
      if (nonRemovableIds.has(id)) {
        return;
      }
      commitElements((current) => current.filter((item) => item.id !== id));
      if (activeElementId === id || inspectorElementId === id) {
        selectElement(null);
      }
    },
    [activeElementId, inspectorElementId, nonRemovableIds, commitElements, selectElement]
  );

  const updateElementAlign = React.useCallback(
    (id: string, align: 'left' | 'center' | 'right') => {
      selectElement(id, { preserveInspector: true });
      updateElement(id, { align });
    },
    [selectElement, updateElement]
  );

  const elementTypeOrder: Record<LayoutElementType, number> = React.useMemo(
    () => ({
      headline: 1,
      subtitle: 2,
      description: 3,
      text: 4,
      badge: 5,
      link: 6,
      cta: 7,
      qr: 8,
      logo: 9,
    }),
    []
  );

  const sortedElements = React.useMemo(() => {
    return [...elements].sort((a, b) => {
      const aOrder = elementTypeOrder[a.type] ?? 99;
      const bOrder = elementTypeOrder[b.type] ?? 99;
      if (aOrder === bOrder) {
        return a.id.localeCompare(b.id);
      }
      return aOrder - bOrder;
    });
  }, [elements, elementTypeOrder]);

  const additionOptions = React.useMemo(() => {
    const existingIds = new Set(elements.map((item) => item.id));
    return [
      {
        key: 'subtitle',
        type: 'subtitle' as LayoutElementType,
        label: t('invites.customizer.elements.addSubtitle', 'Untertitel einblenden'),
        icon: Heading,
        unique: true,
      },
      {
        key: 'badge',
        type: 'badge' as LayoutElementType,
        label: t('invites.customizer.elements.addBadge', 'Badge anzeigen'),
        icon: BadgeCheck,
        unique: true,
      },
      {
        key: 'link',
        type: 'link' as LayoutElementType,
        label: t('invites.customizer.elements.addLink', 'Linkfeld hinzufügen'),
        icon: LinkIcon,
        unique: true,
      },
      {
        key: 'cta',
        type: 'cta' as LayoutElementType,
        label: t('invites.customizer.elements.addCta', 'Call-to-Action einfügen'),
        icon: Megaphone,
        unique: true,
      },
      {
        key: 'text',
        type: 'text' as LayoutElementType,
        label: t('invites.customizer.elements.addText', 'Freien Textblock hinzufügen'),
        icon: Type,
        unique: false,
      },
    ].filter((option) => {
      if (!option.unique) {
        return true;
      }
      return !existingIds.has(option.key);
    });
  }, [elements, t]);

  const elementBindings = React.useMemo(
    () => ({
      headline: {
        field: 'headline' as const,
        label: t('invites.customizer.fields.headline', 'Überschrift'),
        multiline: false,
      },
      subtitle: {
        field: 'subtitle' as const,
        label: t('invites.customizer.fields.subtitle', 'Unterzeile'),
        multiline: false,
      },
      description: {
        field: 'description' as const,
        label: t('invites.customizer.fields.description', 'Beschreibung'),
        multiline: true,
      },
      badge: {
        field: 'badge_label' as const,
        label: t('invites.customizer.fields.badge', 'Badge-Label'),
        multiline: false,
      },
      link: {
        field: 'link_label' as const,
        label: t('invites.customizer.fields.linkLabel', 'Linktext'),
        multiline: false,
      },
      cta: {
        field: 'cta_label' as const,
        label: t('invites.customizer.fields.cta', 'Call-to-Action'),
        multiline: false,
      },
    }),
    [t]
  );

  const updateForm = React.useCallback(
    <T extends keyof QrLayoutCustomization>(key: T, value: QrLayoutCustomization[T]) => {
      setForm((prev) => ({ ...prev, [key]: value }));

      const bindingEntry = Object.entries(elementBindings).find(([, binding]) => binding.field === key);
      if (!bindingEntry) {
        return;
      }

      const [elementId] = bindingEntry;
      selectElement(elementId, { preserveInspector: true });
      commitElements(
        (current) =>
          current.map((el) =>
            el.id === elementId ? { ...el, content: String(value ?? '') } : el
          ),
        { silent: true },
      );
    },
    [commitElements, elementBindings, selectElement],
  );

  const updateElementContent = React.useCallback(
    (id: string, value: string) => {
      selectElement(id, { preserveInspector: true });
      commitElements((current) => current.map((item) => (item.id === id ? { ...item, content: value } : item)));

      const bindingField = ELEMENT_BINDING_FIELD[id];
      if (bindingField) {
        updateForm(bindingField, value);
      }
    },
    [commitElements, selectElement, updateForm],
  );



  const renderElementDetail = React.useCallback(
    (element: LayoutElement): React.ReactNode => {
      const binding = elementBindings[element.id as keyof typeof elementBindings];
      const blocks: React.ReactNode[] = [];

      if (binding) {
        const value = (form[binding.field] as string) ?? '';
        if (binding.multiline) {
          blocks.push(
            <div className="space-y-2" key={`${element.id}-binding`}>
              <Label>{binding.label}</Label>
              <Textarea
                value={value}
                onChange={(event) => updateForm(binding.field, event.target.value as never)}
                className="min-h-[96px]"
              />
            </div>
          );
        } else {
          blocks.push(
            <div className="space-y-2" key={`${element.id}-binding`}>
              <Label>{binding.label}</Label>
              <Textarea
                value={value}
                onChange={(event) => updateForm(binding.field, event.target.value as never)}
                className="min-h-[72px]"
              />
            </div>
          );
        }
      } else if (element.type === 'text') {
        blocks.push(
          <div className="space-y-2" key={`${element.id}-content`}>
            <Label>{t('invites.customizer.elements.customText', 'Textinhalt')}</Label>
            <Textarea
              value={element.content ?? ''}
              onChange={(event) => updateElementContent(element.id, event.target.value)}
              className="min-h-[96px]"
            />
          </div>
        );
      } else if (element.type === 'qr') {
        blocks.push(
          <p className="text-xs text-muted-foreground" key={`${element.id}-hint`}>
            {t('invites.customizer.elements.qrHint', 'Der QR-Code lässt sich im Canvas in Größe und Position verändern. Weitere Einstellungen sind nicht erforderlich.')}
          </p>
        );
      } else if (element.type === 'logo') {
        blocks.push(
          <p className="text-xs text-muted-foreground" key={`${element.id}-logo`}>
            {t('invites.customizer.elements.logoHint', 'Passe Logos im Bereich „Branding“ an. Hier kannst du das Element nur verschieben und skalieren.')}
          </p>
        );
      }

      if (element.type !== 'qr') {
        blocks.push(
          <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3" key={`${element.id}-appearance`}>
            <div className="space-y-2">
              <Label>{t('invites.customizer.elements.align', 'Ausrichtung')}</Label>
              <ToggleGroup
                type="single"
                value={element.align ?? 'left'}
                onValueChange={(value) => value && updateElementAlign(element.id, value as 'left' | 'center' | 'right')}
              >
                <ToggleGroupItem value="left" className="px-3" title={t('invites.customizer.elements.alignLeft', 'Links')} aria-label={t('invites.customizer.elements.alignLeft', 'Links')}>
                  <AlignLeft className="h-4 w-4" />
                  <span className="sr-only">{t('invites.customizer.elements.alignLeft', 'Links')}</span>
                </ToggleGroupItem>
                <ToggleGroupItem value="center" className="px-3" title={t('invites.customizer.elements.alignCenter', 'Zentriert')} aria-label={t('invites.customizer.elements.alignCenter', 'Zentriert')}>
                  <AlignCenter className="h-4 w-4" />
                  <span className="sr-only">{t('invites.customizer.elements.alignCenter', 'Zentriert')}</span>
                </ToggleGroupItem>
                <ToggleGroupItem value="right" className="px-3" title={t('invites.customizer.elements.alignRight', 'Rechts')} aria-label={t('invites.customizer.elements.alignRight', 'Rechts')}>
                  <AlignRight className="h-4 w-4" />
                  <span className="sr-only">{t('invites.customizer.elements.alignRight', 'Rechts')}</span>
                </ToggleGroupItem>
              </ToggleGroup>
            </div>
            <div className="space-y-2">
              <Label>{t('invites.customizer.elements.fontSize', 'Schriftgröße')}</Label>
              <Input
                type="number"
                min={10}
                max={120}
                value={Math.round(element.fontSize ?? 26)}
                onChange={(event) => {
                  const nextSize = Number(event.target.value) || 0;
                  updateElement(element.id, { fontSize: Math.max(10, Math.min(120, nextSize)) });
                }}
              />
            </div>
            <div className="space-y-2">
              <Label>{t('invites.customizer.elements.fontFamily', 'Schriftart')}</Label>
              <Select
                value={availableFonts.some((font) => font.family === element.fontFamily) ? element.fontFamily ?? DEFAULT_FONT_VALUE : DEFAULT_FONT_VALUE}
                onValueChange={(value) => handleElementFontChange(element.id, value === DEFAULT_FONT_VALUE ? '' : value)}
                disabled={fontsLoading}
              >
                <SelectTrigger>
                  <SelectValue placeholder={t('invites.customizer.elements.fontPlaceholder', 'Standard')} />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value={DEFAULT_FONT_VALUE}>{t('invites.customizer.elements.fontPlaceholder', 'Standard')}</SelectItem>
                  {availableFonts.map((font) => (
                    <SelectItem
                      key={font.family}
                      value={font.family}
                      onMouseEnter={() => handleFontOptionPreview(font.family)}
                      onFocus={() => handleFontOptionPreview(font.family)}
                    >
                      <span className="flex items-center justify-between gap-3">
                        <span className="truncate" style={{ fontFamily: font.family }}>
                          {font.family}
                        </span>
                        <span className="text-xs text-muted-foreground" style={{ fontFamily: font.family }}>
                          AaBb
                        </span>
                      </span>
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
              <Input
                value={element.fontFamily ?? ''}
                onChange={(event) => handleElementFontChange(element.id, event.target.value)}
                placeholder="z. B. Playfair Display"
                style={element.fontFamily ? { fontFamily: element.fontFamily } : undefined}
              />
            </div>
          </div>
        );
      }

      if (!blocks.length) {
        blocks.push(
          <p className="text-xs text-muted-foreground" key={`${element.id}-fallback`}>
            {t('invites.customizer.elements.selectHint', 'Tippe ein Element im Canvas oder in der Liste an, um Details zu bearbeiten.')}
          </p>
        );
      }

      return (
        <div className="mt-3 w-full rounded-xl border border-[var(--tenant-border-strong)] bg-[var(--tenant-layer)]/80 p-4 text-sm text-[var(--tenant-foreground-soft)]">
          <div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
            {t('invites.customizer.elements.details', 'Element-Details')}
          </div>
          <div className="mt-3 space-y-4">{blocks}</div>
        </div>
      );
    },
    [availableFonts, elementBindings, form, handleElementFontChange, t, updateElement, updateElementAlign, updateElementContent, updateForm]
  );

  const renderActionButtons = (mode: 'inline' | 'floating') => (
    <>
      <Button
        type="button"
        variant="outline"
        onClick={() => void handleResetClick()}
        disabled={resetting || saving}
        className={cn('w-full sm:w-auto', mode === 'floating' ? 'sm:w-auto' : '')}
      >
        {resetting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RotateCcw className="mr-2 h-4 w-4" />}
        {t('invites.customizer.actions.reset', 'Zurücksetzen')}
      </Button>
      <Button
        type="button"
        disabled={saving || resetting}
        onClick={() => {
          if (formRef.current) {
            if (typeof formRef.current.requestSubmit === 'function') {
              formRef.current.requestSubmit();
            } else {
              formRef.current.submit();
            }
          }
        }}
        className={cn('w-full bg-gradient-to-r from-amber-500 via-orange-500 to-rose-500 text-white sm:w-auto', mode === 'floating' ? 'sm:w-auto' : '')}
      >
        {saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
        {t('invites.customizer.actions.save', 'Layout speichern')}
      </Button>
    </>
  );

  function handleLayoutSelect(layout: EventQrInviteLayout) {
    setSelectedLayoutId(layout.id);
  }

  function handleInstructionChange(index: number, value: string) {
    setInstructions((prev) => {
      const next = [...prev];
      next[index] = value;
      return next;
    });
  }

  function handleAddInstruction() {
    setInstructions((prev) => (prev.length < MAX_INSTRUCTIONS ? [...prev, ''] : prev));
  }

  function handleRemoveInstruction(index: number) {
    setInstructions((prev) => prev.filter((_, idx) => idx !== index));
  }

  function handleLogoUpload(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0];
    if (!file) {
      return;
    }

    if (file.size > 1024 * 1024) {
      setError(t('tasks.customizer.errors.logoTooLarge', 'Das Logo darf maximal 1 MB groß sein.'));
      return;
    }

    const reader = new FileReader();
    reader.onload = () => {
      updateForm('logo_data_url', typeof reader.result === 'string' ? reader.result : null);
      setError(null);
    };
    reader.readAsDataURL(file);
  }

  function handleLogoRemove() {
    updateForm('logo_data_url', null);
  }

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!invite || !activeLayout) {
      return;
    }

    const payload: QrLayoutCustomization = {
      ...form,
      layout_id: activeLayout.id,
      instructions: effectiveInstructions,
    };

    const serializationContext: LayoutSerializationContext = {
      form,
      eventName,
      inviteUrl,
      instructions: effectiveInstructions,
      qrSize: activeLayoutQrSize,
      badgeFallback: t('tasks.customizer.defaults.badgeLabel'),
      logoUrl: form.logo_url ?? null,
    };
    payload.mode = 'advanced';
    payload.elements = serializeElements(
      elements.length ? elements : buildDefaultElements(activeLayout, form, eventName, activeLayoutQrSize),
      serializationContext
    );

    await onSave(sanitizePayload(payload));
  }

  async function handleResetClick() {
    await onReset();
  }

  async function handleDownload(format: string): Promise<void> {
    if (!invite) {
      return;
    }

    const normalizedFormat = format.toLowerCase();
    const eventDateSegment = normalizeEventDateSegment(eventDate);
    const filename = buildDownloadFilename(
      ['QR-Layout', eventName, activeLayout?.name ?? null, eventDateSegment],
      normalizedFormat,
      'einladungslayout',
    );
    setDownloadBusy(normalizedFormat);
    setError(null);

    try {
      const exportOptions = {
        elements: canvasElements,
        accentColor: form.accent_color ?? activeLayout?.preview?.accent ?? '#6366F1',
        textColor: form.text_color ?? activeLayout?.preview?.text ?? '#111827',
        secondaryColor: form.secondary_color ?? '#1F2937',
        badgeColor: form.badge_color ?? form.accent_color ?? '#2563EB',
        qrCodeDataUrl,
        logoDataUrl: form.logo_data_url ?? form.logo_url ?? null,
        backgroundColor: form.background_color ?? activeLayout?.preview?.background ?? '#FFFFFF',
        backgroundGradient: form.background_gradient ?? activeLayout?.preview?.background_gradient ?? null,
        backgroundImageUrl: form.background_image ?? null,
        readOnly: true,
        selectedId: null,
      } as const;

      if (normalizedFormat === 'png') {
        const dataUrl = await generatePngDataUrl(exportOptions);
        await triggerDownloadFromDataUrl(dataUrl, filename);
      } else if (normalizedFormat === 'pdf') {
        const pdfBytes = await generatePdfBytes(
          exportOptions,
          'a4',
          'portrait',
        );
        triggerDownloadFromBlob(new Blob([pdfBytes.buffer as ArrayBuffer], { type: 'application/pdf' }), filename);
      } else {
        throw new Error(`Unsupported format: ${normalizedFormat}`);
      }
    } catch (downloadError) {
      console.error('[Invites] Layout export failed', downloadError);
      setError(t('invites.customizer.errors.downloadFailed', 'Download fehlgeschlagen. Bitte versuche es erneut.'));
    } finally {
      setDownloadBusy(null);
    }
  }

  async function handlePrint(): Promise<void> {
    if (!invite) {
      return;
    }

    setPrintBusy(true);
    setError(null);

    try {
      const exportOptions = {
        elements: canvasElements,
        accentColor: form.accent_color ?? activeLayout?.preview?.accent ?? '#6366F1',
        textColor: form.text_color ?? activeLayout?.preview?.text ?? '#111827',
        secondaryColor: form.secondary_color ?? '#1F2937',
        badgeColor: form.badge_color ?? form.accent_color ?? '#2563EB',
        qrCodeDataUrl,
        logoDataUrl: form.logo_data_url ?? form.logo_url ?? null,
        backgroundColor: form.background_color ?? activeLayout?.preview?.background ?? '#FFFFFF',
        backgroundGradient: form.background_gradient ?? activeLayout?.preview?.background_gradient ?? null,
        backgroundImageUrl: form.background_image ?? null,
        readOnly: true,
        selectedId: null,
      } as const;

      const pdfBytes = await generatePdfBytes(
        exportOptions,
        'a4',
        'portrait',
      );

      await openPdfInNewTab(pdfBytes);
    } catch (printError) {
      console.error('[Invites] Print failed', printError);
      setError(t('invites.customizer.errors.printFailed', 'Drucken konnte nicht gestartet werden.'));
    } finally {
      setPrintBusy(false);
    }
  }

  if (!invite) {
    return (
      <CardPlaceholder
        title={t('invites.customizer.placeholderTitle', 'Kein Layout verfügbar')}
        description={t('invites.customizer.placeholderCopy', 'Erstelle eine Einladung, damit du Texte, Farben und Drucklayouts bearbeiten kannst.')}
      />
    );
  }

  if (!availableLayouts.length) {
    if (layoutsLoading) {
      return (
        <CardPlaceholder
          title={t('invites.customizer.loadingTitle', 'Layouts werden geladen')}
          description={t('invites.customizer.loadingDescription', 'Bitte warte einen Moment, wir bereiten die Drucklayouts vor.')}
        />
      );
    }

    return (
      <CardPlaceholder
        title={layoutsError ?? t('invites.customizer.placeholderTitle', 'Kein Layout verfügbar')}
        description={layoutsError ?? t('invites.customizer.placeholderCopy', 'Erstelle eine Einladung, damit du Texte, Farben und Drucklayouts bearbeiten kannst.')}
      />
    );
  }

  if (!activeLayout) {
    return (
      <CardPlaceholder
        title={t('invites.customizer.placeholderTitle', 'Kein Layout verfügbar')}
        description={t('invites.customizer.placeholderCopy', 'Erstelle eine Einladung, damit du Texte, Farben und Drucklayouts bearbeiten kannst.')}
      />
    );
  }

  const highlightedElementId = activeElementId ?? inspectorElementId;

  const renderResponsiveSection = (id: string, title: string, description: string, content: React.ReactNode) => {
    const body = <div className="space-y-4">{content}</div>;

    if (!isCompact) {
      return (
        <section key={id} className="space-y-4 rounded-2xl border border-border bg-[var(--tenant-surface)] p-5 shadow-sm transition-colors">
          <header className="space-y-1">
            <h3 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">{title}</h3>
            {description ? <p className="text-xs text-muted-foreground">{description}</p> : null}
          </header>
          {body}
        </section>
      );
    }

    return (
      <Collapsible key={id} defaultOpen className="rounded-2xl border border-border bg-[var(--tenant-surface)] p-3 shadow-sm transition-colors">
        <CollapsibleTrigger type="button" className="flex w-full items-center justify-between gap-3 text-left">
          <div className="space-y-1">
            <h3 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">{title}</h3>
            {description ? <p className="text-xs text-muted-foreground">{description}</p> : null}
          </div>
          <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform data-[state=open]:rotate-180" />
        </CollapsibleTrigger>
        <CollapsibleContent className="pt-4">{body}</CollapsibleContent>
      </Collapsible>
    );
  };

  return (
    <div className="space-y-4">
      <div className="hidden flex-wrap items-center justify-end gap-2 lg:flex">
        {renderActionButtons('inline')}
      </div>

      {error ? (
        <div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">{error}</div>
      ) : null}

      <div className="flex flex-col gap-6 xl:grid xl:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
        <form ref={formRef} onSubmit={handleSubmit} className={cn('order-2 space-y-6', 'xl:order-1')}>
          {renderResponsiveSection(
            'layouts',
            t('invites.customizer.sections.layouts', 'Layouts'),
            t('invites.customizer.sections.layoutsHint', 'Wähle eine Vorlage als Basis aus. Du kannst jederzeit wechseln.'),
            <>
              <Select
                value={activeLayout?.id ?? undefined}
                onValueChange={(value) => {
                  const layout = availableLayouts.find((item) => item.id === value);
                  if (layout) {
                    handleLayoutSelect(layout);
                  }
                }}
                disabled={!availableLayouts.length}
              >
                <SelectTrigger className="w-full">
                  <SelectValue placeholder={t('invites.customizer.layoutFallback', 'Layout')} />
                </SelectTrigger>
                <SelectContent className="max-h-60">
                  {availableLayouts.map((layout) => (
                    <SelectItem key={layout.id} value={layout.id}>
                      <span className="text-sm font-medium text-foreground">
                        {layout.name || t('invites.customizer.layoutFallback', 'Layout')}
                      </span>
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </>
          )}

          {renderResponsiveSection(
            'content',
            t('invites.customizer.sections.content', 'Texte & Branding'),
            t('invites.customizer.sections.contentHint', 'Passe Texte, Anleitungsschritte und Farben deiner Einladung an.'),
            <Tabs defaultValue="text" className="space-y-4">
              <TabsList className="grid w-full grid-cols-3 gap-1 text-xs sm:text-sm">
                <TabsTrigger value="text">{t('invites.customizer.sections.text', 'Texte')}</TabsTrigger>
                <TabsTrigger value="branding">{t('invites.customizer.sections.branding', 'Farbgebung')}</TabsTrigger>
              </TabsList>

              <TabsContent value="text" className="space-y-4">
                <div className="grid gap-4">
                  <div className="space-y-2">
                    <Label htmlFor="invite-headline">{t('invites.customizer.fields.headline', 'Überschrift')}</Label>
                    <Textarea
                      id="invite-headline"
                      value={form.headline ?? ''}
                      onChange={(event) => updateForm('headline', event.target.value)}
                      className="min-h-[68px]"
                    />
                  </div>
                  <div className="space-y-2">
                    <Label htmlFor="invite-subtitle">{t('invites.customizer.fields.subtitle', 'Unterzeile')}</Label>
                    <Textarea
                      id="invite-subtitle"
                      value={form.subtitle ?? ''}
                      onChange={(event) => updateForm('subtitle', event.target.value)}
                      className="min-h-[68px]"
                    />
                  </div>
                  <div className="space-y-2">
                    <Label htmlFor="invite-description">{t('invites.customizer.fields.description', 'Beschreibung')}</Label>
                    <Textarea
                      id="invite-description"
                      value={form.description ?? ''}
                      onChange={(event) => updateForm('description', event.target.value)}
                      className="min-h-[96px]"
                    />
                  </div>
                  <div className="grid gap-4 sm:grid-cols-2">
                    <div className="space-y-2">
                      <Label htmlFor="invite-link-heading">{t('invites.customizer.fields.linkHeading', 'Link-Überschrift')}</Label>
                      <Textarea
                        id="invite-link-heading"
                        value={form.link_heading ?? ''}
                        onChange={(event) => updateForm('link_heading', event.target.value)}
                        className="min-h-[68px]"
                      />
                    </div>
                    <div className="space-y-2">
                      <Label htmlFor="invite-link-label">{t('invites.customizer.fields.linkLabel', 'Link/Begleittext')}</Label>
                      <Textarea
                        id="invite-link-label"
                        value={form.link_label ?? ''}
                        onChange={(event) => updateForm('link_label', event.target.value)}
                        className="min-h-[68px]"
                      />
                    </div>
                  </div>
                </div>
              </TabsContent>

              <TabsContent value="branding" className="space-y-4">
                <div className="grid gap-4 sm:grid-cols-2">
                  <div className="space-y-2">
                    <Label htmlFor="invite-accent">{t('invites.customizer.fields.accentColor', 'Akzentfarbe')}</Label>
                    <Input
                      id="invite-accent"
                      type="color"
                      value={form.accent_color ?? '#6366F1'}
                      onChange={(event) => updateForm('accent_color', event.target.value)}
                      className="h-11"
                    />
                  </div>
                  <div className="space-y-2">
                    <Label htmlFor="invite-text-color">{t('invites.customizer.fields.textColor', 'Textfarbe')}</Label>
                    <Input
                      id="invite-text-color"
                      type="color"
                      value={form.text_color ?? '#111827'}
                      onChange={(event) => updateForm('text_color', event.target.value)}
                      className="h-11"
                    />
                  </div>
                  <div className="space-y-2">
                    <Label htmlFor="invite-background-color">{t('invites.customizer.fields.backgroundColor', 'Hintergrund')}</Label>
                    <Input
                      id="invite-background-color"
                      type="color"
                      value={form.background_color ?? '#FFFFFF'}
                      onChange={(event) => updateForm('background_color', event.target.value)}
                      className="h-11"
                    />
                  </div>
                </div>

                {backgroundImages.length ? (
                  <div className="space-y-2">
                    <div className="flex items-center justify-between gap-2">
                      <Label>{t('invites.customizer.fields.backgroundImage', 'Hintergrundbild')}</Label>
                      {form.background_image ? (
                        <Button
                          type="button"
                          size="sm"
                          variant="ghost"
                          onClick={() => {
                            updateForm('background_image', null as never);
                            updateForm('background_gradient', null as never);
                          }}
                        >
                          {t('invites.customizer.actions.removeBackgroundImage', 'Bild entfernen')}
                        </Button>
                      ) : null}
                    </div>
                    <p className="text-xs text-muted-foreground">
                      {t('invites.customizer.fields.backgroundImageHint', 'Wähle ein Bild. Es ersetzt den Farbverlauf und füllt den ganzen Hintergrund.')}
                    </p>
                    <div className="grid gap-3 sm:grid-cols-3">
                      {backgroundImages.map((item) => {
                        const isActive = form.background_image === item.url;
                        return (
                          <button
                            key={item.id}
                            type="button"
                            onClick={() => {
                              updateForm('background_image', item.url as never);
                              updateForm('background_gradient', null as never);
                            }}
                            className={cn(
                              'group overflow-hidden rounded-lg border text-left shadow-sm transition focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
                              isActive ? 'border-primary ring-2 ring-primary/50' : 'border-[var(--tenant-border-strong)]'
                            )}
                          >
                            <div className="aspect-[3/4] w-full overflow-hidden bg-[var(--tenant-surface-muted)]">
                              <img src={item.url} alt={item.label} className="h-full w-full object-cover transition group-hover:scale-105" />
                            </div>
                            <div className="p-2 text-xs text-muted-foreground line-clamp-1">{item.label}</div>
                          </button>
                        );
                      })}
                    </div>
                  </div>
                ) : null}

                <div className="space-y-2">
                  <Label>{t('invites.customizer.fields.logo', 'Logo')}</Label>
                  {form.logo_data_url ? (
                    <div className="flex items-center gap-4 rounded-lg border border-[var(--tenant-border-strong)] bg-[var(--tenant-surface-muted)] p-3 text-[var(--tenant-foreground-soft)]">
                      <img src={form.logo_data_url} alt="Logo" className="h-12 w-12 rounded border border-[var(--tenant-border-strong)] object-contain" />
                      <Button type="button" variant="ghost" onClick={handleLogoRemove} className="text-destructive hover:text-destructive/80">
                        {t('invites.customizer.actions.removeLogo', 'Logo entfernen')}
                      </Button>
                    </div>
                  ) : (
                    <label className="flex cursor-pointer items-center gap-3 rounded-lg border border-dashed border-[var(--tenant-border-strong)] bg-[var(--tenant-surface-muted)] px-4 py-3 text-sm text-muted-foreground hover:border-primary">
                      <UploadCloud className="h-4 w-4" />
                      <span>{t('invites.customizer.actions.uploadLogo', 'Logo hochladen (max. 1 MB)')}</span>
                      <input type="file" accept="image/*" className="hidden" onChange={handleLogoUpload} />
                    </label>
                  )}
                </div>
              </TabsContent>
            </Tabs>
          )}

          {renderResponsiveSection(
            'elements',
            t('invites.customizer.elements.title', 'Elemente & Positionierung'),
            t('invites.customizer.elements.hint', 'Wähle ein Element aus, um es zu verschieben, anzupassen oder zu entfernen.'),
            <>
              <div className="space-y-2">
                {sortedElements.map((element) => {
                  const Icon = elementIconFor(element);
                  const isSelected = element.id === highlightedElementId;
                  const isInspectorVisible = element.id === inspectorElementId;
                  const removable = !nonRemovableIds.has(element.id);
                  return (
                    <div
                      key={element.id}
                      className={cn(
                        'rounded-xl border px-3 py-2 transition-colors',
                        isSelected
                          ? 'border-primary bg-primary/10 text-primary'
                          : 'border-[var(--tenant-border-strong)] bg-[var(--tenant-layer)] text-foreground'
                      )}
                    >
                      <div className="flex items-center justify-between gap-3">
                        <button
                          type="button"
                          onClick={() => selectElement(element.id)}
                          className="flex flex-1 items-center gap-3 text-left"
                        >
                          <Icon className={cn('h-4 w-4', isSelected ? 'text-primary' : 'text-muted-foreground')} />
                          <span className="text-sm font-medium">{elementLabelFor(element)}</span>
                        </button>
                        {removable ? (
                          <Button
                            type="button"
                            variant="ghost"
                            size="icon"
                            onClick={() => removeElement(element.id)}
                            aria-label={t('invites.customizer.elements.remove', 'Element entfernen')}
                            className="text-muted-foreground hover:text-destructive"
                          >
                            <Trash2 className="h-4 w-4" />
                          </Button>
                        ) : null}
                      </div>
                      {isInspectorVisible ? renderElementDetail(element) : null}
                    </div>
                  );
                })}
              </div>

              {additionOptions.length ? (
                <div className="flex flex-wrap gap-2 pt-1">
                  {additionOptions.map((option) => {
                    const Icon = option.icon;
                    return (
                      <Button
                        key={option.key}
                        type="button"
                        variant="outline"
                        size="sm"
                        onClick={() => addElementFromPreset(option.type, option.unique ? option.key : undefined)}
                        className="flex items-center gap-2"
                      >
                        <Icon className="h-4 w-4" />
                        <span>{option.label}</span>
                      </Button>
                    );
                  })}
                </div>
              ) : null}

              <div className="rounded-xl border border-[var(--tenant-border-strong)] bg-[var(--tenant-layer)] p-4">
                <p className="text-xs text-muted-foreground">
                  {t('invites.customizer.elements.listHint', 'Wähle ein Element aus, um Einstellungen direkt unter dem Eintrag anzuzeigen.')} 
                </p>
              </div>
            </>
          )}

          <div className={cn('mt-6 flex flex-col gap-2 sm:flex-row sm:justify-end lg:hidden', showFloatingActions ? 'hidden' : 'flex')}>
            {renderActionButtons('inline')}
          </div>
          <div ref={actionsSentinelRef} className="h-1 w-full" />
        </form>
        <div className={cn('order-1 flex flex-col gap-4 rounded-2xl border border-[var(--tenant-border-strong)] bg-[var(--tenant-surface)] p-5 shadow-sm transition-colors', 'xl:order-2')}>
          <div className="flex flex-wrap items-center justify-between gap-3">
            <div className="flex flex-wrap items-center gap-3">
              <span className="text-sm font-medium text-muted-foreground">
                {t('invites.customizer.controls.zoom', 'Zoom')}
              </span>
              {!isCompact ? (
                <>
                  <input
                    type="range"
                    min={ZOOM_MIN}
                    max={ZOOM_MAX}
                    step={ZOOM_STEP}
                    value={effectiveScale}
                    onChange={(event) => {
                      manualZoomRef.current = true;
                      setZoomScale(clampZoom(Number(event.target.value)));
                    }}
                    className="h-1 w-36 overflow-hidden rounded-full"
                    disabled={previewMode === 'full'}
                    aria-label={t('invites.customizer.controls.zoom', 'Zoom')}
                  />
                  <span className="tabular-nums text-sm text-muted-foreground">{zoomPercent}%</span>
                </>
              ) : (
                <div className="flex items-center gap-2">
                  <Button
                    type="button"
                    variant="outline"
                    size="icon"
                    onClick={() => handleZoomStep(-1)}
                    aria-label={t('invites.customizer.controls.zoomOut', 'Verkleinern')}
                  >
                    <Minus className="h-4 w-4" />
                  </Button>
                  <span className="w-12 text-center text-xs font-medium tabular-nums text-muted-foreground">{zoomPercent}%</span>
                  <Button
                    type="button"
                    variant="outline"
                    size="icon"
                    onClick={() => handleZoomStep(1)}
                    aria-label={t('invites.customizer.controls.zoomIn', 'Vergrößern')}
                  >
                    <Plus className="h-4 w-4" />
                  </Button>
                </div>
              )}
              <ToggleGroup type="single" value={previewMode} onValueChange={(val) => setPreviewMode(val as 'fit' | 'full')} className="flex">
                <ToggleGroupItem value="fit" className="px-2 text-xs">
                  Fit
                </ToggleGroupItem>
                <ToggleGroupItem value="full" className="px-2 text-xs">
                  100%
                </ToggleGroupItem>
              </ToggleGroup>
              {!isCompact ? (
                <Button
                  type="button"
                  variant="ghost"
                  size="sm"
                  onClick={() => {
                    manualZoomRef.current = false;
                    const fitValue = clampZoom(fitScaleRef.current);
                    setZoomScale(fitValue);
                    setPreviewMode('fit');
                  }}
                  disabled={previewMode === 'full' || Math.abs(effectiveScale - clampZoom(fitScaleRef.current)) < 0.001}
                >
                  {t('invites.customizer.actions.zoomFit', 'Auf Bildschirm')}
                </Button>
              ) : (
                <Button
                  type="button"
                  variant="ghost"
                  size="icon"
                  onClick={() => {
                    manualZoomRef.current = false;
                    const fitValue = clampZoom(fitScaleRef.current);
                    setZoomScale(fitValue);
                    setPreviewMode('fit');
                  }}
                  aria-label={t('invites.customizer.actions.zoomFit', 'Auf Bildschirm')}
                >
                  <RotateCcw className="h-4 w-4" />
                </Button>
              )}
            </div>
            <div className="flex flex-wrap gap-2">
              <Button
                type="button"
                variant="outline"
                size="sm"
                onClick={handleUndo}
                disabled={!canUndo}
              >
                <Undo2 className="mr-1 h-4 w-4" />
                {t('invites.customizer.actions.undo', 'Rückgängig')}
              </Button>
              <Button
                type="button"
                variant="outline"
                size="sm"
                onClick={handleRedo}
                disabled={!canRedo}
              >
                <Redo2 className="mr-1 h-4 w-4" />
                {t('invites.customizer.actions.redo', 'Wiederholen')}
              </Button>
            </div>
          </div>

          <div className="flex justify-center">
            <div
              ref={designerViewportRef}
              className={cn(
                "w-full rounded-3xl border border-[var(--tenant-border-strong)] bg-[var(--tenant-layer)] p-4 overflow-auto",
                previewMode === 'full' ? "max-h-none h-[90vh]" : "max-h-[75vh]"
              )}
            >
              <div ref={canvasContainerRef} className="relative flex justify-center aspect-[1240/1754] mx-auto max-w-full">
                <DesignerCanvas
                  elements={canvasElements}
                  selectedId={highlightedElementId}
                  onSelect={selectElement}
                  onChange={updateElement}
                  background={form.background_color ?? activeLayout.preview?.background ?? '#FFFFFF'}
                  gradient={form.background_gradient ?? activeLayout.preview?.background_gradient ?? null}
                  backgroundImageUrl={form.background_image ?? null}
                  accent={form.accent_color ?? activeLayout.preview?.accent ?? '#6366F1'}
                  text={form.text_color ?? activeLayout.preview?.text ?? '#111827'}
                  secondary={form.secondary_color ?? '#1F2937'}
                  badge={form.badge_color ?? form.accent_color ?? '#2563EB'}
                  qrCodeDataUrl={qrCodeDataUrl}
                  logoDataUrl={form.logo_data_url ?? form.logo_url ?? null}
                  scale={effectiveScale}
                  layoutKey={`designer:${invite?.id ?? 'unknown'}:${activeLayout?.id ?? 'default'}`}
                />
              </div>
            </div>
          </div>

          <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
            <Button
              type="button"
              variant="outline"
              size="sm"
              onClick={() => void handlePrint()}
              disabled={printBusy || Boolean(downloadBusy)}
              className="flex w-full items-center justify-center gap-2 sm:w-auto"
            >
              {printBusy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Printer className="h-4 w-4" />}
              {t('invites.customizer.actions.print', 'Drucken')}
            </Button>
            <div className="flex flex-wrap gap-2">
              {(activeLayout.formats ?? []).map((format) => {
                const key = String(format ?? '').toLowerCase();
                const isBusy = downloadBusy === key;
                return (
                  <Button
                    key={`${activeLayout.id}-${key}`}
                    type="button"
                    variant="outline"
                    size="sm"
                    disabled={printBusy || (downloadBusy !== null && !isBusy)}
                    onClick={() => void handleDownload(key)}
                    className="flex items-center gap-2"
                  >
                    <Download className="h-4 w-4" />
                    {isBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : key.toUpperCase()}
                  </Button>
                );
              })}
            </div>
          </div>
        </div>
      </div>
      {showFloatingActions ? (
        <div className="pointer-events-auto fixed inset-x-4 bottom-6 z-40 flex flex-col gap-2 rounded-2xl border border-[var(--tenant-border-strong)] bg-[var(--tenant-layer-strong)]/95 p-4 shadow-2xl backdrop-blur-sm sm:inset-x-auto sm:right-6 sm:w-auto sm:flex-row sm:items-center sm:gap-3">
          {renderActionButtons('floating')}
        </div>
      ) : null}
    </div>
  );
}

function CardPlaceholder({ title, description }: { title: string; description: string }) {
  return (
    <div className="rounded-2xl border border-dashed border-[var(--tenant-border-strong)] bg-[var(--tenant-surface-muted)] p-10 text-center text-sm text-[var(--tenant-foreground-soft)] transition-colors">
      <h3 className="text-base font-semibold text-foreground">{title}</h3>
      <p className="mt-2 text-sm text-muted-foreground">{description}</p>
    </div>
  );
}
