import {
  mdiContentSave,
  mdiExitToApp,
  mdiImageMultiple,
  mdiMenu,
  mdiPlus,
  mdiSwapHorizontal,
  mdiText,
  mdiTooltipText,
  mdiViewCarousel,
} from '@mdi/js';
import axios from 'axios';
import Head from 'next/head';
import NextImage from 'next/image';
import { useRouter } from 'next/router';
import React, {
  ReactElement,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import BaseButton from '../components/BaseButton';
import BaseIcon from '../components/BaseIcon';
import { baseURLApi, getPageTitle } from '../config';
import LayoutAuthenticated from '../layouts/Authenticated';
import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator';
import { useReversePlayback } from '../hooks/useReversePlayback';
import { logger } from '../lib/logger';

type TourPage = {
  id: string;
  name?: string;
  slug?: string;
  sort_order?: number;
  environment?: string;
  source_key?: string;
  requires_auth?: boolean;
  ui_schema_json?: string;
  background_image_url?: string;
  background_video_url?: string;
  background_audio_url?: string;
  background_loop?: boolean;
};

type ProjectAsset = {
  id: string;
  name?: string;
  asset_type?: 'image' | 'video' | 'audio' | 'file';
  type?:
    | 'icon'
    | 'background_image'
    | 'audio'
    | 'video'
    | 'transition'
    | 'logo'
    | 'favicon'
    | 'document'
    | 'general';
  cdn_url?: string | null;
  storage_key?: string | null;
};

type AssetOption = {
  value: string;
  label: string;
};

type CanvasElementType =
  | 'navigation_next'
  | 'navigation_prev'
  | 'gallery'
  | 'carousel'
  | 'tooltip'
  | 'description'
  | 'video_player'
  | 'audio_player';

type NavigationElementType = Extract<
  CanvasElementType,
  'navigation_next' | 'navigation_prev'
>;

type NavigationButtonKind = 'forward' | 'back';

type CanvasElement = {
  id: string;
  type: CanvasElementType;
  label: string;
  xPercent: number;
  yPercent: number;
  width?: string;
  height?: string;
  minWidth?: string;
  maxWidth?: string;
  minHeight?: string;
  maxHeight?: string;
  margin?: string;
  padding?: string;
  gap?: string;
  fontSize?: string;
  lineHeight?: string;
  fontWeight?: string;
  border?: string;
  borderRadius?: string;
  opacity?: string;
  boxShadow?: string;
  display?: string;
  position?: string;
  justifyContent?: string;
  alignItems?: string;
  textAlign?: string;
  zIndex?: string;
  appearDelaySec?: number;
  appearDurationSec?: number | null;
  iconUrl?: string;
  galleryCards?: GalleryCard[];
  carouselSlides?: CarouselSlide[];
  carouselPrevIconUrl?: string;
  carouselNextIconUrl?: string;
  tooltipTitle?: string;
  tooltipText?: string;
  descriptionTitle?: string;
  descriptionText?: string;
  descriptionTitleFontSize?: string;
  descriptionTextFontSize?: string;
  descriptionTitleFontFamily?: string;
  descriptionTextFontFamily?: string;
  descriptionTitleColor?: string;
  descriptionTextColor?: string;
  descriptionBackgroundColor?: string;
  navLabel?: string;
  navType?: NavigationButtonKind;
  navDisabled?: boolean;
  targetPageId?: string;
  transitionVideoUrl?: string;
  transitionReverseMode?: 'auto_reverse' | 'separate_video';
  reverseVideoUrl?: string;
  transitionDurationSec?: number;
  mediaUrl?: string;
  mediaAutoplay?: boolean;
  mediaLoop?: boolean;
  mediaMuted?: boolean;
  backgroundImageUrl?: string;
  videoUrl?: string;
  audioUrl?: string;
};

type GalleryCard = {
  id: string;
  imageUrl: string;
  title: string;
  description: string;
};

type CarouselSlide = {
  id: string;
  imageUrl: string;
  caption: string;
};

type ConstructorSchema = {
  elements?: CanvasElement[];
};

type UiElementDefault = {
  id: string;
  element_type?: string;
  is_active?: boolean;
  default_settings_json?: Partial<CanvasElement> | string | null;
};

type DragElementState = {
  id: string;
  pointerOffsetX: number;
  pointerOffsetY: number;
};

type TransitionPreviewState = {
  videoUrl: string;
  reverseMode: 'none' | 'reverse' | 'separate';
  reverseVideoUrl?: string;
  durationSec?: number;
  title: string;
};

type EditorMenuItem =
  | 'none'
  | 'background_image'
  | 'background_video'
  | 'background_audio'
  | 'create_transition';

type ConstructorPageProps = {
  mode?: 'constructor' | 'element_edit';
};

type ConstructorInteractionMode = 'edit' | 'interact';

const parseJsonObject = <T,>(value?: unknown, fallback?: T): T => {
  if (!value) return (fallback || ({} as T)) as T;

  try {
    if (typeof value === 'string') {
      const parsed = JSON.parse(value);
      return (parsed || fallback || {}) as T;
    }

    if (typeof value === 'object') {
      return value as T;
    }

    return (fallback || ({} as T)) as T;
  } catch (error) {
    logger.error(
      'Failed to parse constructor JSON:',
      error instanceof Error ? error : { error },
    );
    return (fallback || ({} as T)) as T;
  }
};

const clamp = (value: number, min: number, max: number) =>
  Math.min(Math.max(value, min), max);

const normalizeAppearDelaySec = (value: unknown) => {
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed < 0) return 0;
  return Number(parsed);
};

const normalizeAppearDurationSec = (value: unknown) => {
  if (value === null || value === undefined || value === '') return null;
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed <= 0) return null;
  return Number(parsed);
};

const getTrimmedCssValue = (value: unknown) => {
  if (value === null || value === undefined) return '';
  return String(value).trim();
};

const createLocalId = () => {
  if (typeof window !== 'undefined' && window.crypto?.randomUUID) {
    return window.crypto.randomUUID();
  }

  return `constructor_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
};

const getAssetLabel = (asset: ProjectAsset) => {
  const baseName = asset.name?.trim() || 'Untitled asset';
  const source = String(asset.storage_key || asset.cdn_url || '').trim();
  return `${baseName}${source ? ` · ${source}` : ''}`;
};

const getAssetSourceValue = (asset: ProjectAsset) =>
  String(asset.storage_key || asset.cdn_url || '').trim();

const formatDurationNote = (durationSec?: number | string | null) => {
  const parsed = Number(durationSec);
  if (!Number.isFinite(parsed) || parsed <= 0) return 'Duration: unknown';

  const totalSeconds = Math.round(parsed);
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;

  if (minutes <= 0) return `Duration: ${seconds}s`;
  return `Duration: ${minutes}:${String(seconds).padStart(2, '0')}`;
};

const resolveAssetPlaybackUrl = (value?: string) => {
  const normalized = String(value || '').trim();
  if (!normalized) return '';

  if (normalized.startsWith('data:') || normalized.startsWith('blob:'))
    return normalized;

  if (normalized.startsWith('/api/file/download')) return normalized;

  if (normalized.startsWith('/file/download'))
    return `${baseURLApi}${normalized}`;

  if (normalized.startsWith('http://') || normalized.startsWith('https://'))
    return normalized;

  const normalizedPrivateUrl = normalized.replace(/^\/+/, '');
  return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(normalizedPrivateUrl)}`;
};

const readMediaDuration = (
  playbackUrl: string,
  mediaType: 'video' | 'audio',
): Promise<number | null> =>
  new Promise((resolve) => {
    const mediaElement =
      mediaType === 'video'
        ? document.createElement('video')
        : document.createElement('audio');

    let timeoutId: ReturnType<typeof setTimeout> | null = null;

    const cleanup = () => {
      mediaElement.removeEventListener('loadedmetadata', onLoadedMetadata);
      mediaElement.removeEventListener('error', onError);
      mediaElement.removeEventListener('abort', onError);
      if (timeoutId) clearTimeout(timeoutId);
      mediaElement.pause();
      mediaElement.removeAttribute('src');
      mediaElement.load();
    };

    const onLoadedMetadata = () => {
      const duration = Number(mediaElement.duration);
      cleanup();
      if (Number.isFinite(duration) && duration > 0) {
        resolve(duration);
        return;
      }
      resolve(null);
    };

    const onError = () => {
      cleanup();
      resolve(null);
    };

    timeoutId = setTimeout(() => {
      cleanup();
      resolve(null);
    }, 12000);

    mediaElement.preload = 'metadata';
    mediaElement.crossOrigin = 'anonymous';
    mediaElement.addEventListener('loadedmetadata', onLoadedMetadata);
    mediaElement.addEventListener('error', onError);
    mediaElement.addEventListener('abort', onError);
    mediaElement.src = playbackUrl;
    mediaElement.load();
  });

const resolveDurationWithFallback = async (
  source: string,
  mediaType: 'video' | 'audio',
) => {
  const playbackUrl = resolveAssetPlaybackUrl(source);
  if (!playbackUrl) return null;

  const directDuration = await readMediaDuration(playbackUrl, mediaType);
  if (Number.isFinite(directDuration) && Number(directDuration) > 0) {
    return Number(directDuration);
  }

  try {
    const requestUrl =
      playbackUrl.startsWith('http://') || playbackUrl.startsWith('https://')
        ? playbackUrl
        : playbackUrl.replace(/^\/api(?=\/)/, '');

    const token =
      typeof window !== 'undefined' ? localStorage.getItem('token') || '' : '';
    const response = await axios.get(requestUrl, {
      responseType: 'blob',
      headers: token ? { Authorization: `Bearer ${token}` } : undefined,
    });

    const blobUrl = URL.createObjectURL(response.data);
    try {
      const blobDuration = await readMediaDuration(blobUrl, mediaType);
      if (Number.isFinite(blobDuration) && Number(blobDuration) > 0) {
        return Number(blobDuration);
      }
      return null;
    } finally {
      URL.revokeObjectURL(blobUrl);
    }
  } catch (error) {
    logger.error(
      'Failed to fetch media for duration probing:',
      error instanceof Error ? error : { error },
    );
    return null;
  }
};

const isBackgroundImageAsset = (asset: ProjectAsset) => {
  if (asset.type) return asset.type === 'background_image';
  const normalizedName = String(asset.name || '').toLowerCase();
  if (!normalizedName) return false;
  const hasBackgroundKeyword = /\bbackground\b|\bbg\b|backdrop|wallpaper/.test(
    normalizedName,
  );
  const hasExcludedKeyword = /\bicon\b|\blogo\b/.test(normalizedName);
  return hasBackgroundKeyword && !hasExcludedKeyword;
};

const addFallbackAssetOption = (
  options: AssetOption[],
  value?: string,
  fallbackLabel?: string,
): AssetOption[] => {
  const normalizedValue = String(value || '').trim();
  if (!normalizedValue) return options;
  if (options.some((option) => option.value === normalizedValue))
    return options;
  return [
    ...options,
    {
      value: normalizedValue,
      label: fallbackLabel || `Custom URL · ${normalizedValue}`,
    },
  ];
};

const labelByType: Record<CanvasElementType, string> = {
  navigation_next: 'Navigation: Forward',
  navigation_prev: 'Navigation: Back',
  gallery: 'Gallery',
  carousel: 'Carousel',
  tooltip: 'Tooltip',
  description: 'Description',
  video_player: 'Video Player',
  audio_player: 'Audio Player',
};

const canvasElementTypes: CanvasElementType[] = [
  'navigation_next',
  'navigation_prev',
  'gallery',
  'carousel',
  'tooltip',
  'description',
  'video_player',
  'audio_player',
];

const isCanvasElementType = (value: string): value is CanvasElementType =>
  canvasElementTypes.includes(value as CanvasElementType);

const isNavigationElementType = (
  type: CanvasElementType,
): type is NavigationElementType =>
  type === 'navigation_next' || type === 'navigation_prev';

const getNavigationButtonLabel = (type: NavigationElementType) =>
  type === 'navigation_next' ? 'Forward' : 'Back';

const getNavigationButtonKind = (
  type: NavigationElementType,
): NavigationButtonKind => (type === 'navigation_prev' ? 'back' : 'forward');

const getNavigationTypeFromKind = (
  kind: NavigationButtonKind,
): NavigationElementType =>
  kind === 'back' ? 'navigation_prev' : 'navigation_next';

const createDefaultElement = (
  type: CanvasElementType,
  index: number,
): CanvasElement => {
  const base: CanvasElement = {
    id: createLocalId(),
    type,
    label: labelByType[type],
    xPercent: clamp(12 + index * 4, 5, 80),
    yPercent: clamp(16 + index * 6, 8, 85),
    appearDelaySec: 0,
    appearDurationSec: null,
  };

  if (type === 'gallery') {
    return {
      ...base,
      galleryCards: [
        { id: createLocalId(), imageUrl: '', title: 'Card 1', description: '' },
      ],
    };
  }

  if (type === 'carousel') {
    return {
      ...base,
      carouselSlides: [
        { id: createLocalId(), imageUrl: '', caption: 'Slide 1' },
      ],
      carouselPrevIconUrl: '',
      carouselNextIconUrl: '',
    };
  }

  if (type === 'tooltip') {
    return {
      ...base,
      iconUrl: '',
      tooltipTitle: 'Tooltip title',
      tooltipText: 'Tooltip text',
    };
  }

  if (type === 'description') {
    return {
      ...base,
      iconUrl: '',
      descriptionTitle: 'TITLE',
      descriptionText: '',
      descriptionTitleFontSize: '48px',
      descriptionTextFontSize: '36px',
      descriptionTitleFontFamily: 'inherit',
      descriptionTextFontFamily: 'inherit',
      descriptionTitleColor: '#000000',
      descriptionTextColor: '#4B5563',
      descriptionBackgroundColor: 'transparent',
    };
  }

  if (type === 'navigation_next' || type === 'navigation_prev') {
    return {
      ...base,
      navLabel: getNavigationButtonLabel(type),
      navType: getNavigationButtonKind(type),
      navDisabled: false,
      iconUrl: '',
      transitionReverseMode: 'auto_reverse',
    };
  }

  if (type === 'video_player' || type === 'audio_player') {
    return {
      ...base,
      mediaUrl: '',
      mediaAutoplay: true,
      mediaLoop: true,
      mediaMuted: type === 'video_player',
    };
  }

  return base;
};

const mergeElementWithDefaults = (
  element: CanvasElement,
  defaults?: Partial<CanvasElement>,
  options?: { preferElementValues?: boolean },
): CanvasElement => {
  if (!defaults) return element;

  const preferElementValues = Boolean(options?.preferElementValues);
  const base = preferElementValues ? defaults : element;
  const override = preferElementValues ? element : defaults;
  const merged: CanvasElement = {
    ...base,
    ...override,
    id: element.id,
    type: element.type,
    label: element.label || defaults.label || element.type,
    xPercent: element.xPercent ?? defaults.xPercent ?? 50,
    yPercent: element.yPercent ?? defaults.yPercent ?? 50,
  };

  merged.xPercent = clamp(Number(merged.xPercent ?? element.xPercent), 0, 100);
  merged.yPercent = clamp(Number(merged.yPercent ?? element.yPercent), 0, 100);
  merged.appearDelaySec = normalizeAppearDelaySec(merged.appearDelaySec);
  merged.appearDurationSec = normalizeAppearDurationSec(
    merged.appearDurationSec,
  );

  if (merged.type === 'gallery') {
    const cards = preferElementValues
      ? Array.isArray(element.galleryCards)
        ? element.galleryCards
        : defaults.galleryCards || []
      : Array.isArray(defaults.galleryCards)
        ? defaults.galleryCards
        : element.galleryCards || [];
    merged.galleryCards = cards.map((card, cardIndex) => ({
      id: String(card?.id || createLocalId()),
      imageUrl: String(card?.imageUrl || ''),
      title: String(card?.title || `Card ${cardIndex + 1}`),
      description: String(card?.description || ''),
    }));
  }

  if (merged.type === 'carousel') {
    const slides = preferElementValues
      ? Array.isArray(element.carouselSlides)
        ? element.carouselSlides
        : defaults.carouselSlides || []
      : Array.isArray(defaults.carouselSlides)
        ? defaults.carouselSlides
        : element.carouselSlides || [];
    merged.carouselSlides = slides.map((slide, slideIndex) => ({
      id: String(slide?.id || createLocalId()),
      imageUrl: String(slide?.imageUrl || ''),
      caption: String(slide?.caption || `Slide ${slideIndex + 1}`),
    }));
  }

  return merged;
};

const getElementButtonTitle = (element: CanvasElement) => {
  if (element.type === 'gallery') {
    return `${element.label} (${element.galleryCards?.length || 0})`;
  }

  if (element.type === 'carousel') {
    return `${element.label} (${element.carouselSlides?.length || 0})`;
  }

  if (element.type === 'tooltip') return element.tooltipTitle ?? '';
  if (element.type === 'description') return element.descriptionTitle ?? '';
  if (element.type === 'navigation_next' || element.type === 'navigation_prev')
    return (
      element.navLabel?.trim() ||
      getNavigationButtonLabel(element.type as NavigationElementType)
    );
  if (
    (element.type === 'video_player' || element.type === 'audio_player') &&
    element.mediaUrl
  ) {
    return `${element.label} · configured`;
  }

  return element.label;
};

const buildCanvasElementStyle = (
  element: CanvasElement,
): React.CSSProperties => {
  const style: React.CSSProperties = {};
  const width = getTrimmedCssValue(element.width);
  if (width) style.width = width;
  const height = getTrimmedCssValue(element.height);
  if (height) style.height = height;
  const minWidth = getTrimmedCssValue(element.minWidth);
  if (minWidth) style.minWidth = minWidth;
  const maxWidth = getTrimmedCssValue(element.maxWidth);
  if (maxWidth) style.maxWidth = maxWidth;
  const minHeight = getTrimmedCssValue(element.minHeight);
  if (minHeight) style.minHeight = minHeight;
  const maxHeight = getTrimmedCssValue(element.maxHeight);
  if (maxHeight) style.maxHeight = maxHeight;
  const margin = getTrimmedCssValue(element.margin);
  if (margin) style.margin = margin;
  const padding = getTrimmedCssValue(element.padding);
  if (padding) style.padding = padding;
  const gap = getTrimmedCssValue(element.gap);
  if (gap) style.gap = gap;
  const fontSize = getTrimmedCssValue(element.fontSize);
  if (fontSize) style.fontSize = fontSize;
  const lineHeight = getTrimmedCssValue(element.lineHeight);
  if (lineHeight) style.lineHeight = lineHeight;
  const fontWeight = getTrimmedCssValue(element.fontWeight);
  if (fontWeight)
    style.fontWeight = fontWeight as React.CSSProperties['fontWeight'];
  const border = getTrimmedCssValue(element.border);
  if (border) style.border = border;
  const borderRadius = getTrimmedCssValue(element.borderRadius);
  if (borderRadius) style.borderRadius = borderRadius;
  const opacity = getTrimmedCssValue(element.opacity);
  if (opacity) {
    const parsed = Number(opacity);
    if (Number.isFinite(parsed)) style.opacity = parsed;
  }
  const boxShadow = getTrimmedCssValue(element.boxShadow);
  if (boxShadow) style.boxShadow = boxShadow;
  const display = getTrimmedCssValue(element.display);
  if (display) style.display = display;
  const position = getTrimmedCssValue(element.position);
  if (position) style.position = position as React.CSSProperties['position'];
  const justifyContent = getTrimmedCssValue(element.justifyContent);
  if (justifyContent)
    style.justifyContent =
      justifyContent as React.CSSProperties['justifyContent'];
  const alignItems = getTrimmedCssValue(element.alignItems);
  if (alignItems)
    style.alignItems = alignItems as React.CSSProperties['alignItems'];
  const textAlign = getTrimmedCssValue(element.textAlign);
  if (textAlign)
    style.textAlign = textAlign as React.CSSProperties['textAlign'];
  const zIndex = getTrimmedCssValue(element.zIndex);
  if (zIndex) {
    const parsed = Number(zIndex);
    if (Number.isFinite(parsed)) style.zIndex = parsed;
  }
  return style;
};

const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
  const router = useRouter();
  const canvasRef = useRef<HTMLDivElement>(null);
  const elementEditorRef = useRef<HTMLDivElement>(null);
  const [isAuthReady, setIsAuthReady] = useState(false);
  const isElementEditMode = mode === 'element_edit';

  const projectId = useMemo(() => {
    const value = router.query.projectId;
    if (Array.isArray(value)) return value[0] || '';
    return String(value || '');
  }, [router.query.projectId]);
  const pageElementsListHref = useMemo(() => {
    if (!projectId) return '/page_elements/page_elements-list';
    return `/page_elements/page_elements-list?projectId=${encodeURIComponent(projectId)}`;
  }, [projectId]);

  const pageIdFromRoute = useMemo(() => {
    const value = router.query.pageId;
    if (Array.isArray(value)) return value[0] || '';
    return String(value || '');
  }, [router.query.pageId]);
  const elementIdFromRoute = useMemo(() => {
    const value = router.query.elementId;
    if (Array.isArray(value)) return value[0] || '';
    return String(value || '');
  }, [router.query.elementId]);

  const [pages, setPages] = useState<TourPage[]>([]);
  const [pageLinks, setPageLinks] = useState<
    {
      id: string;
      from_pageId?: string;
      to_pageId?: string;
      is_active?: boolean;
      transition?: { id: string; video_url?: string };
    }[]
  >([]);
  const [assets, setAssets] = useState<ProjectAsset[]>([]);
  const [uiElementDefaultsByType, setUiElementDefaultsByType] = useState<
    Partial<Record<CanvasElementType, Partial<CanvasElement>>>
  >({});
  const [activePageId, setActivePageId] = useState('');
  const [projectName, setProjectName] = useState('');

  const [elements, setElements] = useState<CanvasElement[]>([]);
  const [backgroundImageUrl, setBackgroundImageUrl] = useState('');
  const [backgroundVideoUrl, setBackgroundVideoUrl] = useState('');
  const [backgroundAudioUrl, setBackgroundAudioUrl] = useState('');
  const [selectedElementId, setSelectedElementId] = useState('');
  const [selectedMenuItem, setSelectedMenuItem] =
    useState<EditorMenuItem>('none');
  const [transitionPreview, setTransitionPreview] =
    useState<TransitionPreviewState | null>(null);
  const [_pendingNavigationPageId, setPendingNavigationPageId] = useState('');

  const [isLoading, setIsLoading] = useState(true);
  const [isSaving, setIsSaving] = useState(false);
  const [isCreatingPage, setIsCreatingPage] = useState(false);
  const [isCreatingTransition, setIsCreatingTransition] = useState(false);
  const [newTransitionName, setNewTransitionName] = useState('');
  const [newTransitionVideoUrl, setNewTransitionVideoUrl] = useState('');
  const [newTransitionSupportsReverse, setNewTransitionSupportsReverse] =
    useState(true);
  const [errorMessage, setErrorMessage] = useState('');
  const [successMessage, setSuccessMessage] = useState('');
  const [resolvedDurationBySource, setResolvedDurationBySource] = useState<
    Record<string, number | null>
  >({});
  const [canvasElapsedSec, setCanvasElapsedSec] = useState(0);
  const [preloadedIconUrlMap, setPreloadedIconUrlMap] = useState<
    Record<string, boolean>
  >({});
  const [constructorInteractionMode, setConstructorInteractionMode] =
    useState<ConstructorInteractionMode>('edit');
  const [constructorControlsPosition, setConstructorControlsPosition] =
    useState({ x: 20, y: 20 });

  const [menuPosition, setMenuPosition] = useState({ x: 0, y: 110 });
  const [isMenuOpen, setIsMenuOpen] = useState(false);
  const [editorPosition, setEditorPosition] = useState({ x: 0, y: 72 });
  const [isEditorCollapsed, setIsEditorCollapsed] = useState(false);

  const constructorControlsDragRef = useRef<{
    pointerOffsetX: number;
    pointerOffsetY: number;
  } | null>(null);
  const menuDragRef = useRef<{
    pointerOffsetX: number;
    pointerOffsetY: number;
  } | null>(null);
  const editorDragRef = useRef<{
    pointerOffsetX: number;
    pointerOffsetY: number;
  } | null>(null);
  const elementDragRef = useRef<DragElementState | null>(null);
  const transitionVideoRef = useRef<HTMLVideoElement | null>(null);
  const lastLoadedTransitionUrl = useRef<string | null>(null);
  const lastLoadedBlobUrl = useRef<string | null>(null);
  const reverseAnimationFrame = useRef<number | null>(null);
  const [isBufferingForReverse, setIsBufferingForReverse] = useState(false);
  const didSetInitialCanvasFocus = useRef(false);
  const durationProbeInFlightRef = useRef<Set<string>>(new Set());
  const pagePlaybackStartedAtRef = useRef<number>(Date.now());
  const preloadedIconUrlsRef = useRef<Set<string>>(new Set());

  const activePage = useMemo(
    () => pages.find((item) => item.id === activePageId) || null,
    [activePageId, pages],
  );

  // Preload orchestrator for better DX when previewing pages
  // Preloads neighbor page assets and transition videos
  const preloadOrchestrator = usePreloadOrchestrator({
    pages: pages.map((p) => ({
      id: p.id,
      background_image_url: p.background_image_url,
      background_video_url: p.background_video_url,
    })),
    pageLinks,
    elements: elements.map((e) => ({
      id: e.id,
      pageId: activePageId,
      element_type: e.type,
      content_json: JSON.stringify({
        iconUrl: e.iconUrl,
        backgroundImageUrl: e.backgroundImageUrl,
        videoUrl: e.videoUrl,
        audioUrl: e.audioUrl,
        transitionVideoUrl: e.transitionVideoUrl,
      }),
    })),
    currentPageId: activePageId,
    enabled: !isLoading && !!activePageId,
    maxNeighborDepth: 1, // Reduced depth for constructor to minimize network load
  });

  const isConstructorEditMode = constructorInteractionMode === 'edit';
  const allowedNavigationTypes = useMemo<NavigationElementType[]>(() => {
    return ['navigation_next', 'navigation_prev'];
  }, []);
  const pageNameById = useMemo(() => {
    const acc: Record<string, string> = {};
    pages.forEach((page, index) => {
      acc[String(page.id)] = page.name || `Page ${index + 1}`;
    });
    return acc;
  }, [pages]);
  const selectedElement = useMemo(
    () => elements.find((element) => element.id === selectedElementId) || null,
    [elements, selectedElementId],
  );
  const iconPreloadTargets = useMemo(() => {
    const preloadableTypes: CanvasElementType[] = [
      'navigation_next',
      'navigation_prev',
      'tooltip',
      'description',
    ];
    const urls = elements
      .filter(
        (element) =>
          preloadableTypes.includes(element.type) && Boolean(element.iconUrl),
      )
      .map((element) => resolveAssetPlaybackUrl(element.iconUrl))
      .filter(Boolean);

    return Array.from(new Set(urls));
  }, [elements]);
  const normalizeNavigationElementType = useCallback(
    (
      element: CanvasElement,
      nextType: NavigationElementType,
    ): CanvasElement => {
      if (!isNavigationElementType(element.type)) return element;

      const nextButtonLabel = getNavigationButtonLabel(nextType);
      const hasDefaultLabel =
        element.label === labelByType.navigation_next ||
        element.label === labelByType.navigation_prev;
      const hasDefaultNavLabel =
        !element.navLabel ||
        element.navLabel === getNavigationButtonLabel('navigation_next') ||
        element.navLabel === getNavigationButtonLabel('navigation_prev');

      return {
        ...element,
        type: nextType,
        navType: getNavigationButtonKind(nextType),
        label: hasDefaultLabel ? labelByType[nextType] : element.label,
        navLabel: hasDefaultNavLabel ? nextButtonLabel : element.navLabel,
      };
    },
    [],
  );
  const imageAssetOptions = useMemo(
    () =>
      assets
        .filter(
          (asset) => asset.asset_type === 'image' && getAssetSourceValue(asset),
        )
        .map((asset) => ({
          value: getAssetSourceValue(asset),
          label: getAssetLabel(asset),
        })),
    [assets],
  );
  const backgroundImageAssetOptions = useMemo(
    () =>
      assets
        .filter(
          (asset) =>
            asset.asset_type === 'image' &&
            getAssetSourceValue(asset) &&
            isBackgroundImageAsset(asset),
        )
        .map((asset) => ({
          value: getAssetSourceValue(asset),
          label: getAssetLabel(asset),
        })),
    [assets],
  );
  const videoAssetOptions = useMemo(
    () =>
      assets
        .filter(
          (asset) => asset.asset_type === 'video' && getAssetSourceValue(asset),
        )
        .map((asset) => ({
          value: getAssetSourceValue(asset),
          label: getAssetLabel(asset),
        })),
    [assets],
  );
  const audioAssetOptions = useMemo(
    () =>
      assets
        .filter(
          (asset) => asset.asset_type === 'audio' && getAssetSourceValue(asset),
        )
        .map((asset) => ({
          value: getAssetSourceValue(asset),
          label: getAssetLabel(asset),
        })),
    [assets],
  );
  const transitionVideoAssetOptions = useMemo(() => {
    const typedAssets = assets
      .filter(
        (asset) =>
          asset.type === 'transition' &&
          asset.asset_type === 'video' &&
          getAssetSourceValue(asset),
      )
      .map((asset) => ({
        value: getAssetSourceValue(asset),
        label: getAssetLabel(asset),
      }));

    if (typedAssets.length > 0) return typedAssets;

    const taggedAssets = assets
      .filter(
        (asset) =>
          asset.asset_type === 'video' &&
          getAssetSourceValue(asset) &&
          /\[TRANSITION\]/i.test(String(asset.name || '')),
      )
      .map((asset) => ({
        value: getAssetSourceValue(asset),
        label: getAssetLabel(asset),
      }));

    if (taggedAssets.length > 0) return taggedAssets;

    return videoAssetOptions;
  }, [assets, videoAssetOptions]);
  const iconAssetOptions = useMemo(
    () =>
      assets
        .filter(
          (asset) =>
            asset.type === 'icon' &&
            asset.asset_type === 'image' &&
            getAssetSourceValue(asset),
        )
        .map((asset) => ({
          value: getAssetSourceValue(asset),
          label: getAssetLabel(asset),
        })),
    [assets],
  );
  const getKnownDurationForSource = useCallback(
    (source?: string) => {
      const normalizedSource = String(source || '').trim();
      if (!normalizedSource) return null;

      const resolvedDuration = resolvedDurationBySource[normalizedSource];
      if (Number.isFinite(resolvedDuration) && Number(resolvedDuration) > 0) {
        return Number(resolvedDuration);
      }

      return null;
    },
    [resolvedDurationBySource],
  );

  const durationProbeTargets = useMemo<
    Array<{ source: string; mediaType: 'video' | 'audio' }>
  >(() => {
    const targets: Array<{ source: string; mediaType: 'video' | 'audio' }> = [];

    if (backgroundVideoUrl) {
      targets.push({ source: backgroundVideoUrl, mediaType: 'video' });
    }

    if (backgroundAudioUrl) {
      targets.push({ source: backgroundAudioUrl, mediaType: 'audio' });
    }

    if (
      selectedElement &&
      (selectedElement.type === 'video_player' ||
        selectedElement.type === 'audio_player') &&
      selectedElement.mediaUrl
    ) {
      targets.push({
        source: selectedElement.mediaUrl,
        mediaType: selectedElement.type === 'video_player' ? 'video' : 'audio',
      });
    }

    if (newTransitionVideoUrl) {
      targets.push({ source: newTransitionVideoUrl, mediaType: 'video' });
    }

    elements.forEach((element) => {
      if (!isNavigationElementType(element.type)) return;
      if (element.transitionVideoUrl) {
        targets.push({
          source: element.transitionVideoUrl,
          mediaType: 'video',
        });
      }
      if (element.reverseVideoUrl) {
        targets.push({ source: element.reverseVideoUrl, mediaType: 'video' });
      }
    });

    return targets;
  }, [
    backgroundAudioUrl,
    backgroundVideoUrl,
    elements,
    newTransitionVideoUrl,
    selectedElement,
  ]);

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

    durationProbeTargets.forEach(({ source, mediaType }) => {
      const normalizedSource = String(source || '').trim();
      if (!normalizedSource) return;

      if (getKnownDurationForSource(normalizedSource)) return;

      const probeKey = `${mediaType}:${normalizedSource}`;
      if (durationProbeInFlightRef.current.has(probeKey)) return;
      durationProbeInFlightRef.current.add(probeKey);

      resolveDurationWithFallback(normalizedSource, mediaType)
        .then((duration) => {
          if (isCancelled) return;
          setResolvedDurationBySource((prev) => ({
            ...prev,
            [normalizedSource]:
              Number.isFinite(duration) && Number(duration) > 0
                ? Number(duration)
                : null,
          }));
        })
        .catch((error) => {
          logger.error(
            'Failed to resolve media duration:',
            error instanceof Error ? error : { error },
          );
          if (isCancelled) return;
          setResolvedDurationBySource((prev) => ({
            ...prev,
            [normalizedSource]: null,
          }));
        })
        .finally(() => {
          durationProbeInFlightRef.current.delete(probeKey);
        });
    });

    return () => {
      isCancelled = true;
    };
  }, [durationProbeTargets, getKnownDurationForSource]);

  const backgroundVideoDurationNote = useMemo(
    () => formatDurationNote(getKnownDurationForSource(backgroundVideoUrl)),
    [backgroundVideoUrl, getKnownDurationForSource],
  );
  const backgroundAudioDurationNote = useMemo(
    () => formatDurationNote(getKnownDurationForSource(backgroundAudioUrl)),
    [backgroundAudioUrl, getKnownDurationForSource],
  );
  const selectedMediaDurationNote = useMemo(() => {
    if (
      !selectedElement ||
      (selectedElement.type !== 'video_player' &&
        selectedElement.type !== 'audio_player')
    ) {
      return 'Duration: unknown';
    }

    return formatDurationNote(
      getKnownDurationForSource(selectedElement.mediaUrl || ''),
    );
  }, [getKnownDurationForSource, selectedElement]);
  const newTransitionDurationNote = useMemo(
    () => formatDurationNote(getKnownDurationForSource(newTransitionVideoUrl)),
    [getKnownDurationForSource, newTransitionVideoUrl],
  );
  const selectedTransitionDurationNote = useMemo(() => {
    if (!selectedElement || !isNavigationElementType(selectedElement.type)) {
      return 'Duration: unknown';
    }

    return formatDurationNote(
      getKnownDurationForSource(selectedElement.transitionVideoUrl || ''),
    );
  }, [getKnownDurationForSource, selectedElement]);

  useEffect(() => {
    if (newTransitionVideoUrl) return;
    if (!transitionVideoAssetOptions.length) return;
    setNewTransitionVideoUrl(transitionVideoAssetOptions[0].value);
  }, [newTransitionVideoUrl, transitionVideoAssetOptions]);

  useEffect(() => {
    setElements((prev) => {
      let hasChanges = false;
      const next = prev.map((element) => {
        if (!isNavigationElementType(element.type)) return element;

        const resolvedDuration = getKnownDurationForSource(
          element.transitionVideoUrl || '',
        );
        const nextDuration =
          Number.isFinite(resolvedDuration) && Number(resolvedDuration) > 0
            ? Number(resolvedDuration)
            : undefined;
        if (element.transitionDurationSec === nextDuration) return element;

        hasChanges = true;
        return {
          ...element,
          transitionDurationSec: nextDuration,
        };
      });

      return hasChanges ? next : prev;
    });
  }, [getKnownDurationForSource]);

  const loadData = useCallback(
    async (preservePageId?: string) => {
      if (!projectId || !router.isReady || !isAuthReady) return;

      try {
        setIsLoading(true);
        setErrorMessage('');
        setSuccessMessage('');

        const [
          projectResponse,
          pagesResponse,
          pageLinksResponse,
          assetsResponse,
          uiElementsResponse,
        ] = await Promise.all([
          axios.get(`/projects/${projectId}`),
          axios.get(
            `/tour_pages?limit=500&sort=asc&field=sort_order&project=${projectId}`,
          ),
          axios.get(`/page_links?limit=500&project=${projectId}`),
          axios.get(
            `/assets?limit=500&page=0&sort=desc&field=createdAt&project=${projectId}`,
          ),
          axios.get(
            '/ui-elements?limit=200&page=0&sort=asc&field=sort_order&is_active=true',
          ),
        ]);

        const pageRows: TourPage[] = Array.isArray(pagesResponse?.data?.rows)
          ? pagesResponse.data.rows
          : [];
        const pageLinkRows = Array.isArray(pageLinksResponse?.data?.rows)
          ? pageLinksResponse.data.rows
          : [];
        const assetRows: ProjectAsset[] = Array.isArray(
          assetsResponse?.data?.rows,
        )
          ? assetsResponse.data.rows
          : [];
        setProjectName(projectResponse?.data?.name || '');
        setPages(pageRows);

        // Build synthetic page_links from navigation elements in all pages' ui_schema_json
        const syntheticPageLinks: typeof pageLinkRows = [];
        pageRows.forEach((page) => {
          try {
            const uiSchema = typeof page.ui_schema_json === 'string'
              ? JSON.parse(page.ui_schema_json)
              : page.ui_schema_json;
            const pageElements = Array.isArray(uiSchema?.elements) ? uiSchema.elements : [];
            pageElements.forEach((el: any) => {
              if (el.targetPageId && typeof el.targetPageId === 'string') {
                syntheticPageLinks.push({
                  id: `synthetic-${page.id}-${el.id}`,
                  from_pageId: page.id,
                  to_pageId: el.targetPageId,
                  is_active: true,
                  transition: el.transitionVideoUrl ? {
                    id: `transition-${el.id}`,
                    video_url: el.transitionVideoUrl,
                  } : undefined,
                });
              }
            });
          } catch {
            // Skip pages with invalid ui_schema_json
          }
        });

        // Combine API page_links with synthetic ones from navigation elements
        setPageLinks([...pageLinkRows, ...syntheticPageLinks]);
        setAssets(assetRows);

        const uiElementRows: UiElementDefault[] = Array.isArray(
          uiElementsResponse?.data?.rows,
        )
          ? uiElementsResponse.data.rows
          : [];
        const defaultsByType: Partial<
          Record<CanvasElementType, Partial<CanvasElement>>
        > = {};
        uiElementRows.forEach((row) => {
          const elementType = String(row.element_type || '').trim();
          if (!isCanvasElementType(elementType)) return;
          const rawDefaults = parseJsonObject<Partial<CanvasElement>>(
            row.default_settings_json,
            {},
          );
          defaultsByType[elementType] = rawDefaults;
        });
        setUiElementDefaultsByType(defaultsByType);

        // Preserve current page if specified and it still exists, otherwise use route or first page
        const preservedPageExists =
          preservePageId && pageRows.some((p: any) => p.id === preservePageId);
        const defaultPageId = preservedPageExists
          ? preservePageId
          : pageIdFromRoute || pageRows[0]?.id || '';
        setActivePageId(defaultPageId);
        setIsMenuOpen(false);
      } catch (error: any) {
        if (error?.response?.status === 401) {
          const message = 'Your session has expired. Please sign in again.';
          logger.error(
            'Unauthorized constructor request:',
            error instanceof Error ? error : { error },
          );
          setErrorMessage(message);
          setPages([]);
          setAssets([]);
          router.replace('/login');
          return;
        }

        const message =
          error?.response?.data?.message ||
          error?.message ||
          'Failed to load constructor data.';
        logger.error(
          'Failed to load constructor data:',
          error instanceof Error ? error : { error },
        );
        setErrorMessage(message);
        setPages([]);
        setAssets([]);
        setUiElementDefaultsByType({});
      } finally {
        setIsLoading(false);
      }
    },
    [isAuthReady, pageIdFromRoute, projectId, router],
  );

  useEffect(() => {
    if (!router.isReady || typeof window === 'undefined') return;

    const token =
      sessionStorage.getItem('token') || localStorage.getItem('token');
    if (!token) {
      setIsAuthReady(false);
      setErrorMessage('Please sign in to continue.');
      router.replace('/login');
      return;
    }

    setIsAuthReady(true);
  }, [router]);

  useEffect(() => {
    if (!router.isReady) return;
    if (projectId) return;
    router.replace(
      isElementEditMode
        ? '/page_elements/page_elements-list'
        : '/projects/projects-list',
    );
  }, [isElementEditMode, projectId, router]);

  useEffect(() => {
    loadData();
  }, [loadData]);

  useEffect(() => {
    if (typeof window === 'undefined') return;

    setConstructorControlsPosition((prev) => {
      if (prev.x > 0) return prev;
      return {
        x: 20,
        y: 20,
      };
    });

    setMenuPosition((prev) => {
      if (prev.x > 0) return prev;
      return {
        x: Math.max(window.innerWidth - 280, 20),
        y: prev.y,
      };
    });

    setEditorPosition((prev) => {
      if (prev.x > 0) return prev;
      return {
        x: Math.max(window.innerWidth - 400, 20),
        y: prev.y,
      };
    });
  }, []);

  useEffect(() => {
    if (!router.isReady || !isAuthReady || isLoading) return;
    if (didSetInitialCanvasFocus.current) return;
    if (!canvasRef.current) return;

    didSetInitialCanvasFocus.current = true;
    requestAnimationFrame(() => {
      canvasRef.current?.focus({ preventScroll: true });
    });
  }, [isAuthReady, isLoading, router.isReady]);

  useEffect(() => {
    if (typeof window === 'undefined') return;
    if (isLoading || !activePageId) {
      setCanvasElapsedSec(0);
      return;
    }

    pagePlaybackStartedAtRef.current = Date.now();
    setCanvasElapsedSec(0);

    const intervalId = window.setInterval(() => {
      const elapsed = (Date.now() - pagePlaybackStartedAtRef.current) / 1000;
      setCanvasElapsedSec(elapsed > 0 ? elapsed : 0);
    }, 100);

    return () => window.clearInterval(intervalId);
  }, [activePageId, isLoading]);

  useEffect(() => {
    if (typeof window === 'undefined') return;

    const targetSet = new Set(iconPreloadTargets);
    const nextPreloaded = new Set<string>();
    preloadedIconUrlsRef.current.forEach((url) => {
      if (targetSet.has(url)) nextPreloaded.add(url);
    });
    preloadedIconUrlsRef.current = nextPreloaded;
    setPreloadedIconUrlMap(() => {
      const nextMap: Record<string, boolean> = {};
      nextPreloaded.forEach((url) => {
        nextMap[url] = true;
      });
      return nextMap;
    });

    if (!iconPreloadTargets.length) return;

    let isCancelled = false;
    const preloadImages: HTMLImageElement[] = [];

    iconPreloadTargets.forEach((url) => {
      if (preloadedIconUrlsRef.current.has(url)) return;

      const image = new Image();
      const markReady = () => {
        if (isCancelled) return;
        preloadedIconUrlsRef.current.add(url);
        setPreloadedIconUrlMap((prev) => {
          if (prev[url]) return prev;
          return {
            ...prev,
            [url]: true,
          };
        });
      };

      image.onload = markReady;
      image.onerror = () => {
        logger.error('Failed to preload icon asset:', { url });
        markReady();
      };
      image.src = url;
      preloadImages.push(image);
    });

    return () => {
      isCancelled = true;
      preloadImages.forEach((image) => {
        image.onload = null;
        image.onerror = null;
      });
    };
  }, [iconPreloadTargets]);

  useEffect(() => {
    if (!activePage) {
      setElements([]);
      setSelectedElementId('');
      setBackgroundImageUrl('');
      setBackgroundVideoUrl('');
      setBackgroundAudioUrl('');
      return;
    }

    const schema = parseJsonObject<ConstructorSchema>(
      activePage.ui_schema_json,
      {},
    );
    const normalizedElements = Array.isArray(schema.elements)
      ? schema.elements
          .filter(
            (item) =>
              item && item.type && labelByType[item.type as CanvasElementType],
          )
          .map((item) => {
            const elementType = item.type as CanvasElementType;
            const normalizedElement: CanvasElement = {
              ...item,
              id: String(item.id || createLocalId()),
              label:
                typeof item.label === 'string' && item.label.trim()
                  ? item.label
                  : labelByType[elementType],
              xPercent: clamp(Number(item.xPercent || 0), 0, 100),
              yPercent: clamp(Number(item.yPercent || 0), 0, 100),
              appearDelaySec: normalizeAppearDelaySec(item.appearDelaySec),
              appearDurationSec: normalizeAppearDurationSec(
                item.appearDurationSec,
              ),
              galleryCards: Array.isArray(item.galleryCards)
                ? item.galleryCards.map((card: any, index: number) => ({
                    id: String(card?.id || createLocalId()),
                    imageUrl: String(card?.imageUrl || ''),
                    title: String(card?.title || `Card ${index + 1}`),
                    description: String(card?.description || ''),
                  }))
                : undefined,
              carouselSlides: Array.isArray(item.carouselSlides)
                ? item.carouselSlides.map((slide: any, index: number) => ({
                    id: String(slide?.id || createLocalId()),
                    imageUrl: String(slide?.imageUrl || ''),
                    caption: String(slide?.caption || `Slide ${index + 1}`),
                  }))
                : undefined,
              iconUrl: typeof item.iconUrl === 'string' ? item.iconUrl : '',
              carouselPrevIconUrl:
                typeof item.carouselPrevIconUrl === 'string'
                  ? item.carouselPrevIconUrl
                  : '',
              carouselNextIconUrl:
                typeof item.carouselNextIconUrl === 'string'
                  ? item.carouselNextIconUrl
                  : '',
              tooltipTitle:
                typeof item.tooltipTitle === 'string' ? item.tooltipTitle : '',
              tooltipText:
                typeof item.tooltipText === 'string' ? item.tooltipText : '',
              descriptionTitle:
                typeof item.descriptionTitle === 'string'
                  ? item.descriptionTitle
                  : '',
              descriptionText:
                typeof item.descriptionText === 'string'
                  ? item.descriptionText
                  : '',
              navLabel: typeof item.navLabel === 'string' ? item.navLabel : '',
              navType:
                item.navType === 'back' || item.navType === 'forward'
                  ? item.navType
                  : isNavigationElementType(elementType)
                    ? getNavigationButtonKind(
                        elementType as NavigationElementType,
                      )
                    : undefined,
              targetPageId:
                typeof item.targetPageId === 'string' ? item.targetPageId : '',
              transitionVideoUrl:
                typeof item.transitionVideoUrl === 'string'
                  ? item.transitionVideoUrl
                  : '',
              transitionReverseMode:
                item.transitionReverseMode === 'separate_video'
                  ? 'separate_video'
                  : ('auto_reverse' as const),
              reverseVideoUrl:
                typeof item.reverseVideoUrl === 'string'
                  ? item.reverseVideoUrl
                  : '',
              transitionDurationSec: item.transitionDurationSec
                ? Number(item.transitionDurationSec)
                : undefined,
              mediaUrl: typeof item.mediaUrl === 'string' ? item.mediaUrl : '',
              mediaAutoplay:
                typeof item.mediaAutoplay === 'boolean'
                  ? item.mediaAutoplay
                  : true,
              mediaLoop:
                typeof item.mediaLoop === 'boolean' ? item.mediaLoop : true,
              mediaMuted:
                typeof item.mediaMuted === 'boolean'
                  ? item.mediaMuted
                  : item.type === 'video_player',
            };

            return mergeElementWithDefaults(
              normalizedElement,
              uiElementDefaultsByType[elementType],
              { preferElementValues: true },
            );
          })
      : [];

    setElements(normalizedElements);
    setSelectedMenuItem('none');
    setSelectedElementId((current) => {
      if (!normalizedElements.length) return '';
      if (
        elementIdFromRoute &&
        normalizedElements.some((element) => element.id === elementIdFromRoute)
      ) {
        return elementIdFromRoute;
      }
      if (normalizedElements.some((element) => element.id === current))
        return current;
      return '';
    });
    setBackgroundImageUrl(activePage.background_image_url || '');
    setBackgroundVideoUrl(activePage.background_video_url || '');
    setBackgroundAudioUrl(activePage.background_audio_url || '');
  }, [activePage, elementIdFromRoute, uiElementDefaultsByType]);

  useEffect(() => {
    if (allowedNavigationTypes.length !== 1) return;
    const forcedType = allowedNavigationTypes[0];

    setElements((prev) => {
      let hasChanges = false;
      const nextElements = prev.map((element) => {
        if (
          !isNavigationElementType(element.type) ||
          element.type === forcedType
        )
          return element;
        hasChanges = true;
        return normalizeNavigationElementType(element, forcedType);
      });
      return hasChanges ? nextElements : prev;
    });
  }, [allowedNavigationTypes, normalizeNavigationElementType]);

  useEffect(() => {
    const onPointerMove = (event: MouseEvent) => {
      if (constructorControlsDragRef.current) {
        const maxX = Math.max(window.innerWidth - 460, 0);
        const maxY = Math.max(window.innerHeight - 64, 0);
        const nextX = clamp(
          event.clientX - constructorControlsDragRef.current.pointerOffsetX,
          0,
          maxX,
        );
        const nextY = clamp(
          event.clientY - constructorControlsDragRef.current.pointerOffsetY,
          0,
          maxY,
        );
        setConstructorControlsPosition({ x: nextX, y: nextY });
        return;
      }

      if (menuDragRef.current) {
        const nextX = clamp(
          event.clientX - menuDragRef.current.pointerOffsetX,
          0,
          window.innerWidth - 240,
        );
        const nextY = clamp(
          event.clientY - menuDragRef.current.pointerOffsetY,
          0,
          window.innerHeight - 60,
        );
        setMenuPosition({ x: nextX, y: nextY });
        return;
      }

      if (editorDragRef.current) {
        const editorWidth = isEditorCollapsed ? 260 : 380;
        const nextX = clamp(
          event.clientX - editorDragRef.current.pointerOffsetX,
          0,
          window.innerWidth - editorWidth,
        );
        const nextY = clamp(
          event.clientY - editorDragRef.current.pointerOffsetY,
          0,
          window.innerHeight - 60,
        );
        setEditorPosition({ x: nextX, y: nextY });
        return;
      }

      if (!elementDragRef.current || !canvasRef.current) return;

      const rect = canvasRef.current.getBoundingClientRect();
      const rawX =
        event.clientX - rect.left - elementDragRef.current.pointerOffsetX;
      const rawY =
        event.clientY - rect.top - elementDragRef.current.pointerOffsetY;
      const nextXPercent = clamp((rawX / rect.width) * 100, 0, 100);
      const nextYPercent = clamp((rawY / rect.height) * 100, 0, 100);

      setElements((prev) =>
        prev.map((item) =>
          item.id === elementDragRef.current?.id
            ? { ...item, xPercent: nextXPercent, yPercent: nextYPercent }
            : item,
        ),
      );
    };

    const onPointerUp = () => {
      constructorControlsDragRef.current = null;
      menuDragRef.current = null;
      editorDragRef.current = null;
      elementDragRef.current = null;
    };

    window.addEventListener('mousemove', onPointerMove);
    window.addEventListener('mouseup', onPointerUp);

    return () => {
      window.removeEventListener('mousemove', onPointerMove);
      window.removeEventListener('mouseup', onPointerUp);
    };
  }, [isEditorCollapsed]);

  useEffect(() => {
    if (isConstructorEditMode) return;
    elementDragRef.current = null;
    setSelectedElementId('');
    setSelectedMenuItem('none');
  }, [isConstructorEditMode]);

  useEffect(() => {
    if (!isConstructorEditMode) return;
    if (!selectedElementId && selectedMenuItem === 'none') return;

    const onOutsideMouseDown = (event: MouseEvent) => {
      const target = event.target as HTMLElement | null;
      if (!target) return;
      if (elementEditorRef.current?.contains(target)) return;
      const clickedElementId = target
        .closest('[data-constructor-element-id]')
        ?.getAttribute('data-constructor-element-id');
      if (
        selectedElementId &&
        clickedElementId &&
        clickedElementId === selectedElementId
      )
        return;
      setSelectedElementId('');
      setSelectedMenuItem('none');
    };

    window.addEventListener('mousedown', onOutsideMouseDown);
    return () => window.removeEventListener('mousedown', onOutsideMouseDown);
  }, [isConstructorEditMode, selectedElementId, selectedMenuItem]);

  const selectElementForEdit = (elementId: string) => {
    setSelectedElementId(elementId);
    setSelectedMenuItem('none');
  };

  const selectMenuItemForEdit = (item: EditorMenuItem) => {
    setSelectedElementId('');
    setSelectedMenuItem(item);
  };

  const addElement = (type: CanvasElementType) => {
    const nextElementType: CanvasElementType = isNavigationElementType(type)
      ? allowedNavigationTypes.includes(type)
        ? type
        : allowedNavigationTypes[0]
      : type;
    const baseElement = createDefaultElement(nextElementType, elements.length);
    const nextElement = mergeElementWithDefaults(
      baseElement,
      uiElementDefaultsByType[nextElementType],
    );
    setElements((prev) => [...prev, nextElement]);
    selectElementForEdit(nextElement.id);
    setSuccessMessage('Element added. Drag it to set position.');
    setErrorMessage('');
  };

  const createPage = useCallback(async () => {
    if (!projectId) {
      setErrorMessage('Project is required.');
      return;
    }

    const maxSortOrder = Math.max(
      0,
      ...pages.map((item) => Number(item.sort_order || 0)),
    );
    const nextPageNumber = pages.length + 1;

    const payload = {
      project: projectId,
      environment: activePage?.environment || 'dev',
      source_key: '',
      name: `Page ${nextPageNumber}`,
      slug: `page-${nextPageNumber}-${Date.now().toString().slice(-4)}`,
      sort_order: maxSortOrder + 1,
      background_image_url: '',
      background_video_url: '',
      background_audio_url: '',
      background_loop: false,
      requires_auth: false,
      ui_schema_json: JSON.stringify({ elements: [] }),
    };

    try {
      setIsCreatingPage(true);
      setErrorMessage('');
      setSuccessMessage('');
      const response = await axios.post('/tour_pages', { data: payload });
      const createdPage = response?.data;

      await loadData();

      if (createdPage?.id) {
        setActivePageId(createdPage.id);
      }

      setIsMenuOpen(true);
      setSuccessMessage(
        'New page created. You can now configure it in constructor.',
      );
    } catch (error: any) {
      const message =
        error?.response?.data?.message ||
        error?.message ||
        'Failed to create page.';
      logger.error(
        'Failed to create page from constructor:',
        error instanceof Error ? error : { error },
      );
      setErrorMessage(message);
    } finally {
      setIsCreatingPage(false);
    }
  }, [activePage?.environment, loadData, pages, projectId]);

  const createTransition = useCallback(async () => {
    if (!projectId) {
      setErrorMessage('Project is required.');
      return;
    }

    const sanitizedVideoUrl = String(newTransitionVideoUrl || '').trim();
    if (!sanitizedVideoUrl) {
      setErrorMessage('Select a transition video asset first.');
      return;
    }

    const sanitizedName =
      String(newTransitionName || '').trim() ||
      `Transition ${Date.now().toString().slice(-4)}`;
    const resolvedDurationSec = getKnownDurationForSource(sanitizedVideoUrl);
    if (!resolvedDurationSec) {
      setErrorMessage(
        'Could not resolve transition video duration yet. Please wait a moment and try again.',
      );
      return;
    }

    try {
      setIsCreatingTransition(true);
      setErrorMessage('');
      setSuccessMessage('');

      const payload = {
        project: projectId,
        environment: activePage?.environment || 'dev',
        source_key: '',
        name: sanitizedName,
        slug: `transition-${createLocalId()}`,
        video_url: sanitizedVideoUrl,
        audio_url: '',
        supports_reverse: Boolean(newTransitionSupportsReverse),
        duration_sec: resolvedDurationSec,
      };

      await axios.post('/transitions', { data: payload });
      setSuccessMessage('Transition created.');
      setNewTransitionName('');
    } catch (error: any) {
      const message =
        error?.response?.data?.message ||
        error?.message ||
        'Failed to create transition.';
      logger.error(
        'Failed to create transition from constructor:',
        error instanceof Error ? error : { error },
      );
      setErrorMessage(message);
    } finally {
      setIsCreatingTransition(false);
    }
  }, [
    activePage?.environment,
    getKnownDurationForSource,
    newTransitionName,
    newTransitionSupportsReverse,
    newTransitionVideoUrl,
    projectId,
  ]);

  const saveConstructor = useCallback(async () => {
    if (!activePageId) {
      setErrorMessage('Select a page before saving.');
      return;
    }

    try {
      setIsSaving(true);
      setErrorMessage('');
      setSuccessMessage('');

      const existingSchema = parseJsonObject<Record<string, any>>(
        activePage?.ui_schema_json,
        {},
      );
      const schemaToSave = {
        ...existingSchema,
        elements,
      };

      await axios.put(`/tour_pages/${activePageId}`, {
        id: activePageId,
        data: {
          environment: activePage?.environment,
          source_key: activePage?.source_key,
          name: activePage?.name,
          slug: activePage?.slug,
          sort_order: activePage?.sort_order,
          requires_auth: activePage?.requires_auth,
          ui_schema_json: schemaToSave,
          background_image_url: backgroundImageUrl,
          background_video_url: backgroundVideoUrl,
          background_audio_url: backgroundAudioUrl,
          background_loop: Boolean(backgroundAudioUrl),
        },
      });

      setSuccessMessage(
        'Constructor settings saved. Element positions are stored in percentages.',
      );
      await loadData(activePageId);
    } catch (error: any) {
      const message =
        error?.response?.data?.message ||
        error?.message ||
        'Failed to save constructor changes.';
      logger.error(
        'Failed to save constructor changes:',
        error instanceof Error ? error : { error },
      );
      setErrorMessage(message);
    } finally {
      setIsSaving(false);
    }
  }, [
    activePage?.environment,
    activePage?.name,
    activePage?.requires_auth,
    activePage?.slug,
    activePage?.sort_order,
    activePage?.source_key,
    activePage?.ui_schema_json,
    activePageId,
    backgroundAudioUrl,
    backgroundImageUrl,
    backgroundVideoUrl,
    elements,
    loadData,
  ]);

  const onElementMouseDown = (event: React.MouseEvent, elementId: string) => {
    if (!isConstructorEditMode) return;
    event.preventDefault();
    if (!canvasRef.current) return;

    const currentElement = elements.find((item) => item.id === elementId);
    if (!currentElement) return;
    selectElementForEdit(elementId);

    const rect = canvasRef.current.getBoundingClientRect();
    const elementLeftPx = (currentElement.xPercent / 100) * rect.width;
    const elementTopPx = (currentElement.yPercent / 100) * rect.height;

    elementDragRef.current = {
      id: elementId,
      pointerOffsetX: event.clientX - rect.left - elementLeftPx,
      pointerOffsetY: event.clientY - rect.top - elementTopPx,
    };
  };

  const preventImageDragStart = (event: React.DragEvent<HTMLImageElement>) => {
    event.preventDefault();
  };

  const updateSelectedElement = (patch: Partial<CanvasElement>) => {
    if (!selectedElementId) return;
    setElements((prev) =>
      prev.map((item) =>
        item.id === selectedElementId ? { ...item, ...patch } : item,
      ),
    );
  };

  const removeSelectedElement = () => {
    if (!selectedElementId) return;

    let nextSelectedId = '';
    setElements((prev) => {
      const filtered = prev.filter((item) => item.id !== selectedElementId);
      nextSelectedId = filtered[0]?.id || '';
      return filtered;
    });
    if (nextSelectedId) {
      selectElementForEdit(nextSelectedId);
    } else {
      setSelectedElementId('');
      setSelectedMenuItem('none');
    }
    setSuccessMessage('Element removed.');
  };

  const updateGalleryCard = (cardId: string, patch: Partial<GalleryCard>) => {
    if (!selectedElement || selectedElement.type !== 'gallery') return;
    const nextCards = (selectedElement.galleryCards || []).map((card) =>
      card.id === cardId ? { ...card, ...patch } : card,
    );
    updateSelectedElement({ galleryCards: nextCards });
  };

  const addGalleryCard = () => {
    if (!selectedElement || selectedElement.type !== 'gallery') return;
    const nextCards = [
      ...(selectedElement.galleryCards || []),
      {
        id: createLocalId(),
        imageUrl: '',
        title: `Card ${(selectedElement.galleryCards || []).length + 1}`,
        description: '',
      },
    ];
    updateSelectedElement({ galleryCards: nextCards });
  };

  const removeGalleryCard = (cardId: string) => {
    if (!selectedElement || selectedElement.type !== 'gallery') return;
    const nextCards = (selectedElement.galleryCards || []).filter(
      (card) => card.id !== cardId,
    );
    updateSelectedElement({ galleryCards: nextCards });
  };

  const updateCarouselSlide = (
    slideId: string,
    patch: Partial<CarouselSlide>,
  ) => {
    if (!selectedElement || selectedElement.type !== 'carousel') return;
    const nextSlides = (selectedElement.carouselSlides || []).map((slide) =>
      slide.id === slideId ? { ...slide, ...patch } : slide,
    );
    updateSelectedElement({ carouselSlides: nextSlides });
  };

  const addCarouselSlide = () => {
    if (!selectedElement || selectedElement.type !== 'carousel') return;
    const nextSlides = [
      ...(selectedElement.carouselSlides || []),
      {
        id: createLocalId(),
        imageUrl: '',
        caption: `Slide ${(selectedElement.carouselSlides || []).length + 1}`,
      },
    ];
    updateSelectedElement({ carouselSlides: nextSlides });
  };

  const removeCarouselSlide = (slideId: string) => {
    if (!selectedElement || selectedElement.type !== 'carousel') return;
    const nextSlides = (selectedElement.carouselSlides || []).filter(
      (slide) => slide.id !== slideId,
    );
    updateSelectedElement({ carouselSlides: nextSlides });
  };

  const openTransitionPreviewForElement = (
    element: CanvasElement,
    direction: 'forward' | 'back',
  ) => {
    if (!isNavigationElementType(element.type)) return;

    if (!element.transitionVideoUrl) {
      setErrorMessage(
        'Select transition video asset to preview transition playback.',
      );
      return;
    }

    if (
      direction === 'back' &&
      element.transitionReverseMode === 'separate_video' &&
      !element.reverseVideoUrl
    ) {
      setErrorMessage(
        'Select back-transition asset or switch reverse mode to Auto Reverse.',
      );
      return;
    }

    setTransitionPreview({
      videoUrl: element.transitionVideoUrl,
      reverseMode:
        direction === 'forward'
          ? 'none'
          : element.transitionReverseMode === 'separate_video'
            ? 'separate'
            : 'reverse',
      reverseVideoUrl: element.reverseVideoUrl,
      durationSec: element.transitionDurationSec,
      title: `${element.navLabel || element.label} · ${direction}`,
    });
  };

  const openTransitionPreview = (direction: 'forward' | 'back') => {
    if (
      !selectedElement ||
      (selectedElement.type !== 'navigation_next' &&
        selectedElement.type !== 'navigation_prev')
    ) {
      return;
    }

    openTransitionPreviewForElement(selectedElement, direction);
  };

  const onCanvasElementClick = (element: CanvasElement) => {
    if (!isConstructorEditMode) {
      if (isNavigationElementType(element.type)) {
        // Disable navigation while transition is playing or buffering
        if (transitionPreview || isBufferingForReverse) {
          return;
        }
        if (element.navDisabled) {
          return;
        }
        const direction =
          element.navType === 'back' || element.type === 'navigation_prev'
            ? 'back'
            : 'forward';
        const configuredTargetId = String(element.targetPageId || '').trim();
        const fallbackTargetId = (() => {
          const currentPageIndex = pages.findIndex(
            (page) => page.id === activePageId,
          );
          if (currentPageIndex < 0) return '';

          const nextPageIndex =
            direction === 'back' ? currentPageIndex - 1 : currentPageIndex + 1;
          const nextPage = pages[nextPageIndex];
          return nextPage ? String(nextPage.id || '').trim() : '';
        })();
        const targetPageId = configuredTargetId || fallbackTargetId;

        if (!targetPageId) {
          setErrorMessage(
            'No target page available for this navigation button.',
          );
          return;
        }

        const hasPlayableTransition =
          Boolean(element.transitionVideoUrl) &&
          !(
            direction === 'back' &&
            element.transitionReverseMode === 'separate_video' &&
            !element.reverseVideoUrl
          );

        if (!hasPlayableTransition) {
          setPendingNavigationPageId('');
          setTransitionPreview(null);
          setActivePageId(targetPageId);
          setSelectedElementId('');
          setSelectedMenuItem('none');
          setErrorMessage('');
          return;
        }

        setPendingNavigationPageId(targetPageId);
        openTransitionPreviewForElement(element, direction);
      }
      return;
    }

    selectElementForEdit(element.id);
  };

  const onMenuDragStart = (event: React.MouseEvent) => {
    const targetRect = (
      event.currentTarget as HTMLElement
    ).getBoundingClientRect();
    menuDragRef.current = {
      pointerOffsetX: event.clientX - targetRect.left,
      pointerOffsetY: event.clientY - targetRect.top,
    };
  };

  const onConstructorControlsDragStart = (event: React.MouseEvent) => {
    const targetRect = (
      event.currentTarget as HTMLElement
    ).getBoundingClientRect();
    constructorControlsDragRef.current = {
      pointerOffsetX: event.clientX - targetRect.left,
      pointerOffsetY: event.clientY - targetRect.top,
    };
  };

  const onElementEditorDragStart = (event: React.MouseEvent) => {
    const target = event.target as HTMLElement;
    if (target.closest('button')) return;

    const targetRect = (
      event.currentTarget as HTMLElement
    ).getBoundingClientRect();
    editorDragRef.current = {
      pointerOffsetX: event.clientX - targetRect.left,
      pointerOffsetY: event.clientY - targetRect.top,
    };
  };

  const renderCanvasElementContent = (element: CanvasElement) => {
    if (
      element.type === 'navigation_next' ||
      element.type === 'navigation_prev'
    ) {
      const fallbackNavLabel = getNavigationButtonLabel(element.type);
      const navigationLabel = element.navLabel?.trim() || fallbackNavLabel;
      if (element.iconUrl) {
        return (
          <div className='relative min-w-[60px] min-h-[60px] w-full h-full'>
            <NextImage
              src={resolveAssetPlaybackUrl(element.iconUrl)}
              alt='Navigation icon'
              fill
              sizes='100vw'
              className='object-contain'
              draggable={false}
            />
          </div>
        );
      }

      const targetPageName = element.targetPageId
        ? pageNameById[element.targetPageId]
        : '';
      return (
        <div className='flex flex-col items-start gap-1'>
          <div className='flex items-center gap-2'>
            <span>{navigationLabel}</span>
          </div>
          {targetPageName ? (
            <span className='text-[10px] text-gray-500'>
              To: {targetPageName}
            </span>
          ) : null}
        </div>
      );
    }

    if (element.type === 'tooltip') {
      if (element.iconUrl) {
        return (
          <div className='relative h-auto w-auto max-h-[220px] max-w-[220px]'>
            <NextImage
              src={resolveAssetPlaybackUrl(element.iconUrl)}
              alt='Tooltip icon'
              fill
              sizes='100vw'
              className='object-contain'
              draggable={false}
            />
          </div>
        );
      }

      return (
        <div className='max-w-[200px] text-left'>
          <p className='text-[11px] font-bold'>{element.tooltipTitle}</p>
          <p className='text-[10px] text-gray-600 line-clamp-3'>
            {element.tooltipText || 'Tooltip text'}
          </p>
        </div>
      );
    }

    if (element.type === 'description') {
      if (element.iconUrl) {
        return (
          <div className='relative h-auto w-auto max-h-[220px] max-w-[220px]'>
            <NextImage
              src={resolveAssetPlaybackUrl(element.iconUrl)}
              alt='Description icon'
              fill
              sizes='100vw'
              className='object-contain'
              draggable={false}
            />
          </div>
        );
      }

      const bgColor = element.descriptionBackgroundColor || 'transparent';
      return (
        <div
          className='w-full text-left p-2 rounded'
          style={{ backgroundColor: bgColor }}
        >
          <p
            className='font-bold'
            style={{
              fontSize: element.descriptionTitleFontSize
                ? `calc(${element.descriptionTitleFontSize} * 0.25)`
                : '12px',
              fontFamily: element.descriptionTitleFontFamily || 'inherit',
              color: element.descriptionTitleColor || '#000000',
            }}
          >
            {element.descriptionTitle || 'TITLE'}
          </p>
          {element.descriptionText && (
            <p
              className='line-clamp-4'
              style={{
                fontSize: element.descriptionTextFontSize
                  ? `calc(${element.descriptionTextFontSize} * 0.25)`
                  : '9px',
                fontFamily: element.descriptionTextFontFamily || 'inherit',
                color: element.descriptionTextColor || '#4B5563',
              }}
            >
              {element.descriptionText}
            </p>
          )}
        </div>
      );
    }

    if (element.type === 'gallery') {
      const cards = element.galleryCards || [];
      return (
        <div className='w-[220px]'>
          <p className='mb-1 text-left text-[10px] font-semibold text-gray-600'>
            Gallery ({cards.length})
          </p>
          <div className='grid grid-cols-3 gap-1'>
            {cards.slice(0, 6).map((card) => (
              <div
                key={card.id}
                className='h-12 overflow-hidden rounded bg-gray-100'
              >
                {card.imageUrl ? (
                  <div className='relative h-full w-full'>
                    <NextImage
                      src={resolveAssetPlaybackUrl(card.imageUrl)}
                      alt={card.title || 'Gallery card'}
                      fill
                      sizes='100vw'
                      className='object-cover'
                      draggable={false}
                    />
                  </div>
                ) : (
                  <div className='flex h-full items-center justify-center text-[9px] text-gray-400'>
                    No image
                  </div>
                )}
              </div>
            ))}
          </div>
        </div>
      );
    }

    if (element.type === 'carousel') {
      const firstSlide = (element.carouselSlides || [])[0];
      return (
        <div className='w-[220px] text-left'>
          <p className='mb-1 text-[10px] font-semibold text-gray-600'>
            Carousel ({element.carouselSlides?.length || 0})
          </p>
          <div className='h-20 overflow-hidden rounded bg-gray-100'>
            {firstSlide?.imageUrl ? (
              <div className='relative h-full w-full'>
                <NextImage
                  src={resolveAssetPlaybackUrl(firstSlide.imageUrl)}
                  alt={firstSlide.caption || 'Carousel slide'}
                  fill
                  sizes='100vw'
                  className='object-cover'
                  draggable={false}
                />
              </div>
            ) : (
              <div className='flex h-full items-center justify-center text-[10px] text-gray-400'>
                No slide image
              </div>
            )}
          </div>
          <p className='mt-1 text-[10px] text-gray-600 line-clamp-1'>
            {firstSlide?.caption || 'No caption'}
          </p>
          {(element.carouselPrevIconUrl || element.carouselNextIconUrl) && (
            <div className='mt-1 flex items-center justify-between text-[9px] text-gray-500'>
              <span className='flex items-center gap-1'>
                {element.carouselPrevIconUrl ? (
                  <div className='relative h-3 w-3'>
                    <NextImage
                      src={resolveAssetPlaybackUrl(element.carouselPrevIconUrl)}
                      alt='Previous icon'
                      fill
                      sizes='100vw'
                      className='object-contain'
                      draggable={false}
                    />
                  </div>
                ) : null}
                Prev
              </span>
              <span className='flex items-center gap-1'>
                Next
                {element.carouselNextIconUrl ? (
                  <div className='relative h-3 w-3'>
                    <NextImage
                      src={resolveAssetPlaybackUrl(element.carouselNextIconUrl)}
                      alt='Next icon'
                      fill
                      sizes='100vw'
                      className='object-contain'
                      draggable={false}
                    />
                  </div>
                ) : null}
              </span>
            </div>
          )}
        </div>
      );
    }

    if (element.type === 'video_player') {
      return (
        <div className='w-[220px] text-left'>
          <p className='mb-1 text-[10px] font-semibold text-gray-600'>
            Video player
          </p>
          <video
            key={`${element.id}_${element.mediaUrl || ''}_${String(Boolean(element.mediaAutoplay))}_${String(Boolean(element.mediaLoop))}_${String(Boolean(element.mediaMuted))}`}
            className='h-24 w-full rounded bg-black object-cover'
            src={resolveAssetPlaybackUrl(element.mediaUrl)}
            controls
            autoPlay={Boolean(element.mediaAutoplay)}
            loop={Boolean(element.mediaLoop)}
            muted={Boolean(element.mediaMuted)}
            playsInline
          />
        </div>
      );
    }

    if (element.type === 'audio_player') {
      return (
        <div className='w-[240px] text-left'>
          <p className='mb-1 text-[10px] font-semibold text-gray-600'>
            Audio player
          </p>
          <audio
            key={`${element.id}_${element.mediaUrl || ''}_${String(Boolean(element.mediaAutoplay))}_${String(Boolean(element.mediaLoop))}`}
            className='w-full'
            src={resolveAssetPlaybackUrl(element.mediaUrl)}
            controls
            autoPlay={Boolean(element.mediaAutoplay)}
            loop={Boolean(element.mediaLoop)}
          />
        </div>
      );
    }

    return getElementButtonTitle(element);
  };

  const isElementVisibleOnCanvas = (element: CanvasElement) => {
    const delay = Number(element.appearDelaySec || 0);
    if (canvasElapsedSec < delay) return false;

    if (
      element.appearDurationSec === null ||
      element.appearDurationSec === undefined
    ) {
      return true;
    }

    const duration = Number(element.appearDurationSec);
    if (!Number.isFinite(duration) || duration <= 0) return true;

    return canvasElapsedSec <= delay + duration;
  };

  const isElementReadyForCanvasRender = (element: CanvasElement) => {
    const isPreloadableIconElement =
      (element.type === 'navigation_next' ||
        element.type === 'navigation_prev' ||
        element.type === 'tooltip' ||
        element.type === 'description') &&
      Boolean(element.iconUrl);

    if (!isPreloadableIconElement) return true;

    const playbackUrl = resolveAssetPlaybackUrl(element.iconUrl);
    if (!playbackUrl) return true;

    return Boolean(preloadedIconUrlMap[playbackUrl]);
  };

  const canvasBackgroundStyle: React.CSSProperties = {};
  const backgroundImageSrc = resolveAssetPlaybackUrl(backgroundImageUrl);
  const backgroundVideoSrc = resolveAssetPlaybackUrl(backgroundVideoUrl);
  const backgroundAudioSrc = resolveAssetPlaybackUrl(backgroundAudioUrl);
  const backgroundImageSelectOptions = addFallbackAssetOption(
    backgroundImageAssetOptions,
    backgroundImageUrl,
    `Current image · ${backgroundImageUrl}`,
  );
  const backgroundVideoSelectOptions = addFallbackAssetOption(
    videoAssetOptions,
    backgroundVideoUrl,
    `Current video · ${backgroundVideoUrl}`,
  );
  const backgroundAudioSelectOptions = addFallbackAssetOption(
    audioAssetOptions,
    backgroundAudioUrl,
    `Current audio · ${backgroundAudioUrl}`,
  );
  const hasEditorSelection =
    isConstructorEditMode &&
    (Boolean(selectedElement) || selectedMenuItem !== 'none');
  const editorTitle =
    selectedMenuItem === 'background_image'
      ? 'Background image'
      : selectedMenuItem === 'background_video'
        ? 'Background video'
        : selectedMenuItem === 'background_audio'
          ? 'Background audio'
          : selectedMenuItem === 'create_transition'
            ? 'Create transition'
            : selectedElement?.label || 'Element editor';

  if (backgroundImageSrc) {
    canvasBackgroundStyle.backgroundImage = `url("${backgroundImageSrc}")`;
    canvasBackgroundStyle.backgroundSize = 'cover';
    canvasBackgroundStyle.backgroundPosition = 'center';
  }

  useEffect(() => {
    const video = transitionVideoRef.current;
    if (!transitionPreview || !video) return;

    let startWatchdogTimer: ReturnType<typeof setTimeout> | null = null;
    let finishTimer: ReturnType<typeof setTimeout> | null = null;
    let hardTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
    let didFinish = false;
    let didStartPlayback = false;

    const sourceCandidateRaw =
      transitionPreview.reverseMode === 'separate'
        ? transitionPreview.reverseVideoUrl || ''
        : transitionPreview.videoUrl;
    const sourceUrl = resolveAssetPlaybackUrl(sourceCandidateRaw);

    const cleanupReverseFrame = () => {
      if (reverseAnimationFrame.current !== null) {
        cancelAnimationFrame(reverseAnimationFrame.current);
        reverseAnimationFrame.current = null;
      }
    };

    const clearTimers = () => {
      if (startWatchdogTimer) clearTimeout(startWatchdogTimer);
      if (finishTimer) clearTimeout(finishTimer);
      if (hardTimeoutTimer) clearTimeout(hardTimeoutTimer);
      startWatchdogTimer = null;
      finishTimer = null;
      hardTimeoutTimer = null;
    };

    const cleanupPreviewBlobUrl = (force = false) => {
      // Only cleanup if forced (when switching to different source)
      if (!force) return;
      if (!lastLoadedBlobUrl.current) return;
      URL.revokeObjectURL(lastLoadedBlobUrl.current);
      lastLoadedBlobUrl.current = null;
    };

    const shouldLoadTransitionViaBlob = (candidateUrl: string) => {
      try {
        const parsedUrl = new URL(candidateUrl, window.location.origin);
        const isSameOrigin = parsedUrl.origin === window.location.origin;
        if (!isSameOrigin) return false;
        return (
          parsedUrl.pathname === '/api/file/download' ||
          parsedUrl.pathname === '/file/download'
        );
      } catch (error) {
        logger.error('Transition preview URL parsing failed:', {
          candidateUrl,
          error: error instanceof Error ? error : { error },
        });
        return false;
      }
    };

    const buildBlobRequestUrl = (candidateUrl: string) => {
      if (candidateUrl.startsWith('/api/')) {
        return candidateUrl.replace(/^\/api(?=\/)/, '');
      }
      return candidateUrl;
    };

    const resolvePlayableTransitionSource = async () => {
      // Reuse existing blob URL if source matches
      if (lastLoadedBlobUrl.current && lastLoadedTransitionUrl.current === sourceUrl) {
        logger.info('Reusing cached blob URL');
        return lastLoadedBlobUrl.current;
      }

      // Cleanup old blob URL if switching to different source
      if (lastLoadedBlobUrl.current && lastLoadedTransitionUrl.current !== sourceUrl) {
        cleanupPreviewBlobUrl(true);
      }

      // For reverse mode, ALWAYS use blob URL (required for seeking to work)
      // For forward mode, only use blob for same-origin /api/file/download URLs
      const needsBlobUrl = transitionPreview.reverseMode === 'reverse' || shouldLoadTransitionViaBlob(sourceUrl);

      if (!needsBlobUrl) {
        return sourceUrl;
      }

      // Try to get from preload cache first (using same URL format as preload)
      try {
        const cachedBlobUrl = await preloadOrchestrator.getCachedBlobUrl(sourceUrl);
        if (cachedBlobUrl) {
          logger.info('Using preloaded blob URL from cache', { reverseMode: transitionPreview.reverseMode });
          lastLoadedBlobUrl.current = cachedBlobUrl;
          return cachedBlobUrl;
        }
      } catch (cacheError) {
        logger.warn('Cache lookup failed, falling back to fetch', { cacheError });
      }

      // Fall back to axios fetch if not in cache
      logger.info('Fetching video as blob for seeking support', { reverseMode: transitionPreview.reverseMode });
      const token =
        typeof window !== 'undefined'
          ? localStorage.getItem('token') || ''
          : '';
      const requestUrl = buildBlobRequestUrl(sourceUrl);
      const response = await axios.get(requestUrl, {
        responseType: 'blob',
        headers: token ? { Authorization: `Bearer ${token}` } : undefined,
      });

      lastLoadedBlobUrl.current = URL.createObjectURL(response.data);
      logger.info('Created blob URL for video', { blobUrl: lastLoadedBlobUrl.current.substring(0, 50) });
      return lastLoadedBlobUrl.current;
    };

    const finishPreview = (reason: string) => {
      if (didFinish) return;
      didFinish = true;
      clearTimers();
      cleanupReverseFrame();
      setIsBufferingForReverse(false);
      video.pause();
      video.removeAttribute('src');
      video.load();
      // Don't cleanup blob URL here - might be reused for reverse
      setTransitionPreview(null);
      setPendingNavigationPageId((pendingPageId) => {
        const nextPageId = String(pendingPageId || '').trim();
        if (nextPageId) {
          setActivePageId(nextPageId);
          setSelectedElementId('');
          setSelectedMenuItem('none');
          setErrorMessage('');
        }
        return '';
      });
      logger.info('Transition preview finished', {
        reason,
        src: video.currentSrc || sourceUrl || '',
      });
    };

    const configuredDurationSec = Number(transitionPreview.durationSec);
    const getMediaErrorDetails = () => {
      if (!video.error) return null;
      const mediaError = video.error as MediaError & { message?: string };
      return {
        code: mediaError.code,
        message: mediaError.message || '',
      };
    };

    const logTransitionIssue = (reason: string, error?: unknown) => {
      logger.error('Transition preview issue:', {
        reason,
        src: video.currentSrc || sourceUrl || '',
        readyState: video.readyState,
        networkState: video.networkState,
        duration: video.duration,
        configuredDurationSec: transitionPreview.durationSec,
        reverseMode: transitionPreview.reverseMode,
        mediaError: getMediaErrorDetails(),
        error: error instanceof Error ? error : { error },
      });
    };

    const scheduleFinishByDuration = (durationSec: number) => {
      if (!Number.isFinite(durationSec) || durationSec <= 0 || finishTimer) {
        return;
      }
      finishTimer = setTimeout(
        () => {
          finishPreview('duration-timer');
        },
        durationSec * 1000 + 200,
      );
    };

    const runReversePreview = () => {
      cleanupReverseFrame();

      const duration =
        Number.isFinite(video.duration) && video.duration > 0
          ? video.duration
          : Number.isFinite(configuredDurationSec) && configuredDurationSec > 0
            ? configuredDurationSec
            : 0.7;

      const getBufferedEnd = () =>
        video.buffered.length > 0
          ? video.buffered.end(video.buffered.length - 1)
          : 0;

      const startFrameStepping = (targetTime: number) => {
        // Get the maximum buffered position - use buffered instead of seekable
        // because seekable is empty when server doesn't support HTTP Range requests
        // but we can still seek within buffered regions
        const getMaxBuffered = (): number => {
          if (video.buffered.length === 0) return 0;
          return video.buffered.end(video.buffered.length - 1);
        };

        const performSeekAndStep = () => {
          const maxBuffered = getMaxBuffered();
          // Clamp target to what's actually buffered
          const safeTarget = Math.min(targetTime, maxBuffered);

          logger.info('Preparing seek for reverse', {
            requestedTarget: targetTime,
            maxBuffered,
            safeTarget,
            currentTime: video.currentTime,
            readyState: video.readyState,
            bufferedRanges: video.buffered.length,
            paused: video.paused,
          });

          // If nothing is buffered yet, we can't reverse
          if (safeTarget < 0.1) {
            logger.info('No buffered range available, aborting reverse');
            finishPreview('reverse-no-buffered');
            return;
          }

          const beginFrameStepping = () => {
            // CRITICAL: Ensure video is paused before starting frame-stepping
            video.pause();

            logger.info('Starting frame-stepping', {
              targetTime: safeTarget,
              actualCurrentTime: video.currentTime,
              paused: video.paused,
            });

            const stepSize = 1 / 30; // 30fps equivalent
            let lastFrameTime = performance.now();
            let stepCount = 0;

            const step = (currentFrameTime: number) => {
              if (didFinish) {
                reverseAnimationFrame.current = null;
                return;
              }

              // Ensure video stays paused (browser might try to auto-play)
              if (!video.paused) {
                video.pause();
              }

              const elapsed = currentFrameTime - lastFrameTime;
              if (elapsed < 33) { // ~30fps throttle
                reverseAnimationFrame.current = requestAnimationFrame(step);
                return;
              }
              lastFrameTime = currentFrameTime;
              stepCount++;

              const currentTime = video.currentTime;
              const newTime = currentTime - stepSize;

              // Log every 30 steps (~1 second) to track progress
              if (stepCount % 30 === 0) {
                logger.info('Frame-stepping progress', {
                  stepCount,
                  currentTime,
                  newTime,
                });
              }

              // Log first 5 steps in detail to debug
              if (stepCount <= 5) {
                logger.info('Frame step detail', {
                  stepCount,
                  beforeSet: currentTime,
                  settingTo: newTime,
                  paused: video.paused,
                });
              }

              if (newTime <= 0) {
                reverseAnimationFrame.current = null;
                video.currentTime = 0;
                logger.info('Frame-stepping complete', { totalSteps: stepCount, finalCurrentTime: video.currentTime });
                finishPreview('reverse-complete');
              } else {
                video.currentTime = newTime;
                // Verify the seek worked
                if (stepCount <= 5) {
                  logger.info('After set', { actualCurrentTime: video.currentTime, expected: newTime });
                }
                reverseAnimationFrame.current = requestAnimationFrame(step);
              }
            };

            reverseAnimationFrame.current = requestAnimationFrame(step);
          };

          const onSeeked = () => {
            video.removeEventListener('seeked', onSeeked);
            if (didFinish) return;

            logger.info('Seeked event fired', {
              currentTime: video.currentTime,
              targetWas: safeTarget,
            });

            // Verify seek worked (allow some tolerance)
            if (video.currentTime < 0.1) {
              logger.info('Seek resulted in position 0, retrying once');
              // One retry - set listener again before seeking
              const onRetrySeek = () => {
                video.removeEventListener('seeked', onRetrySeek);
                if (didFinish) return;
                if (video.currentTime >= 0.1) {
                  beginFrameStepping();
                } else {
                  logger.info('Seek retry failed, aborting');
                  finishPreview('reverse-seek-failed');
                }
              };
              video.addEventListener('seeked', onRetrySeek);
              video.currentTime = safeTarget;
              return;
            }

            beginFrameStepping();
          };

          // Add listener BEFORE setting currentTime
          video.addEventListener('seeked', onSeeked);
          logger.info('Setting currentTime for reverse seek', {
            from: video.currentTime,
            to: safeTarget,
            paused: video.paused,
            readyState: video.readyState,
          });
          video.currentTime = safeTarget;
        };

        // If video is playing, wait for it to pause before seeking
        if (!video.paused) {
          const onPause = () => {
            video.removeEventListener('pause', onPause);
            if (didFinish) return;
            // Small delay to let video element stabilize
            requestAnimationFrame(() => {
              if (didFinish) return;
              performSeekAndStep();
            });
          };
          video.addEventListener('pause', onPause);
          video.pause();
        } else {
          performSeekAndStep();
        }
      };

      const bufferedEnd = getBufferedEnd();

      logger.info('Starting reverse playback', {
        duration,
        videoDuration: video.duration,
        readyState: video.readyState,
        bufferedEnd,
      });

      // If already fully buffered, start immediately
      if (bufferedEnd >= duration - 0.1) {
        logger.info('Video fully buffered, starting reverse immediately');
        setIsBufferingForReverse(false);
        startFrameStepping(duration);
        return;
      }

      // Need to buffer more - hide video and use progress event to track
      logger.info('Buffering video for reverse playback...');
      setIsBufferingForReverse(true);

      let bufferTimeoutId: ReturnType<typeof setTimeout> | null = null;
      let progressCheckCount = 0;
      const maxProgressChecks = 100; // ~5 seconds at 20 checks/sec

      // Will be updated after onEndedDuringBuffer is defined
      let cleanupBufferingFn: (() => void) | null = null;
      const cleanupBuffering = () => {
        if (cleanupBufferingFn) cleanupBufferingFn();
      };

      const onProgress = () => {
        if (didFinish) {
          cleanupBuffering();
          return;
        }

        progressCheckCount++;
        const currentBufferedEnd = getBufferedEnd();

        logger.info('Buffer progress', {
          bufferedEnd: currentBufferedEnd,
          duration,
          progressCheckCount,
        });

        // Check if fully buffered
        if (currentBufferedEnd >= duration - 0.1) {
          cleanupBuffering();
          setIsBufferingForReverse(false);
          logger.info('Video fully buffered via progress event');
          startFrameStepping(duration);
          return;
        }

        // Safety check - don't wait forever
        if (progressCheckCount >= maxProgressChecks) {
          cleanupBuffering();
          setIsBufferingForReverse(false);
          logger.info('Buffer timeout, starting from available buffer', {
            bufferedEnd: currentBufferedEnd,
          });
          if (currentBufferedEnd >= 0.5) {
            startFrameStepping(currentBufferedEnd);
          } else {
            finishPreview('reverse-buffer-timeout');
          }
        }
      };

      const onCanPlayThrough = () => {
        if (didFinish) {
          cleanupBuffering();
          return;
        }

        const currentBufferedEnd = getBufferedEnd();

        // Start reverse from whatever is buffered when canplaythrough fires
        // Don't wait for full duration - video might end before full buffer
        if (currentBufferedEnd >= 0.5) {
          cleanupBuffering();
          setIsBufferingForReverse(false);
          const targetTime = Math.min(currentBufferedEnd, duration);
          logger.info('Starting reverse from canplaythrough', {
            bufferedEnd: currentBufferedEnd,
            duration,
            targetTime,
          });
          startFrameStepping(targetTime);
        }
      };

      // Handle video ending during buffering (start reverse from end)
      const onEndedDuringBuffer = () => {
        if (didFinish) {
          cleanupBuffering();
          return;
        }
        cleanupBuffering();
        setIsBufferingForReverse(false);
        const currentBufferedEnd = getBufferedEnd();
        logger.info('Video ended during buffering, starting reverse', {
          bufferedEnd: currentBufferedEnd,
          duration,
        });
        // Video ended, so we can reverse from the end
        if (currentBufferedEnd >= 0.5) {
          startFrameStepping(currentBufferedEnd);
        } else {
          startFrameStepping(duration);
        }
      };

      // Now define the actual cleanup function
      cleanupBufferingFn = () => {
        video.removeEventListener('progress', onProgress);
        video.removeEventListener('canplaythrough', onCanPlayThrough);
        video.removeEventListener('ended', onEndedDuringBuffer);
        if (bufferTimeoutId) {
          clearTimeout(bufferTimeoutId);
          bufferTimeoutId = null;
        }
      };

      video.addEventListener('progress', onProgress);
      video.addEventListener('canplaythrough', onCanPlayThrough);
      video.addEventListener('ended', onEndedDuringBuffer);

      // Start playback to trigger buffering (hidden/muted)
      video.muted = true;
      video.currentTime = 0;
      video.play().catch(() => {
        // If play fails, just wait for progress events
      });

      // Fallback timeout - don't wait forever
      bufferTimeoutId = setTimeout(() => {
        if (didFinish) return;
        cleanupBuffering();
        setIsBufferingForReverse(false);
        const currentBufferedEnd = getBufferedEnd();
        logger.info('Buffer fallback timeout', { bufferedEnd: currentBufferedEnd });
        if (currentBufferedEnd >= 0.5) {
          startFrameStepping(currentBufferedEnd);
        } else {
          finishPreview('reverse-buffer-timeout');
        }
      }, 8000); // 8 second max wait
    };

    const attemptPlay = () => {
      video.play().catch((playError) => {
        // Don't log as error for reverse mode - autoplay might fail but that's ok
        if (transitionPreview.reverseMode !== 'reverse') {
          logTransitionIssue('play-failed', playError);
        }
      });
    };

    const loadSourceCandidate = async () => {
      logger.info('loadSourceCandidate called', {
        reverseMode: transitionPreview.reverseMode,
        sourceUrl,
      });

      didStartPlayback = false;

      if (startWatchdogTimer) {
        clearTimeout(startWatchdogTimer);
      }

      try {
        const playableSourceUrl = await resolvePlayableTransitionSource();
        logger.info('playableSourceUrl resolved:', { playableSourceUrl });
        if (didFinish) return;

        video.pause();
        cleanupReverseFrame();

        // For reverse mode, reuse already-loaded video if same URL (preserves buffer)
        const isSameSource = lastLoadedTransitionUrl.current === playableSourceUrl;

        if (transitionPreview.reverseMode === 'reverse' && isSameSource && video.readyState >= 2) {
          logger.info('Reusing buffered video for reverse', {
            readyState: video.readyState,
            duration: video.duration,
            bufferedEnd: video.buffered.length > 0 ? video.buffered.end(video.buffered.length - 1) : 0,
          });
          // Video is already loaded, start reverse immediately
          runReversePreview();
          return;
        }

        video.src = playableSourceUrl;
        video.currentTime = 0;
        video.load();
        lastLoadedTransitionUrl.current = playableSourceUrl;

        logger.info('Calling attemptPlay');
        // For reverse mode, also start playback to ensure data loads
        // runReversePreview will take over control
        attemptPlay();

        startWatchdogTimer = setTimeout(() => {
          if (didStartPlayback || didFinish) return;
          logTransitionIssue('playback-start-slow');
          // For reverse, trigger manually if canplaythrough hasn't fired
          if (transitionPreview.reverseMode === 'reverse') {
            didStartPlayback = true;
            runReversePreview();
          } else {
            attemptPlay();
          }
        }, 3000); // Reduced timeout for faster feedback
      } catch (error) {
        logTransitionIssue('source-prepare-failed', error);
        finishPreview('source-prepare-failed');
      }
    };

    if (!sourceUrl) {
      logTransitionIssue('missing-source');
      finishPreview('missing-source');
      return () => {
        cleanupReverseFrame();
      };
    }

    const onLoadedMetadata = () => {
      if (didFinish) return;
      // For forward playback, start immediately
      if (transitionPreview.reverseMode !== 'reverse') {
        video.currentTime = 0;
        attemptPlay();
      }
    };

    const onCanPlayThrough = () => {
      if (didFinish) return;
      logger.info('canplaythrough fired', {
        reverseMode: transitionPreview.reverseMode,
        didStartPlayback,
      });
      // For reverse playback, wait for more data to be buffered
      // so seeking works reliably
      if (transitionPreview.reverseMode === 'reverse' && !didStartPlayback) {
        didStartPlayback = true;
        if (startWatchdogTimer) {
          clearTimeout(startWatchdogTimer);
          startWatchdogTimer = null;
        }
        video.pause(); // Stop forward playback
        runReversePreview();
      }
    };

    const onCanPlay = () => {
      if (didFinish) return;
      // Don't auto-play during reverse mode - we control playback manually
      if (transitionPreview.reverseMode === 'reverse' && didStartPlayback) {
        return;
      }
      attemptPlay();
    };

    const onPlaying = () => {
      logger.info('onPlaying fired', {
        reverseMode: transitionPreview.reverseMode,
        didStartPlayback,
        didFinish,
      });

      if (didFinish) return;

      // For reverse mode, pause and start reverse playback
      if (transitionPreview.reverseMode === 'reverse' && !didStartPlayback) {
        logger.info('Triggering reverse from onPlaying');
        didStartPlayback = true;
        if (startWatchdogTimer) {
          clearTimeout(startWatchdogTimer);
          startWatchdogTimer = null;
        }
        video.pause();
        runReversePreview();
        return;
      }

      didStartPlayback = true;
      if (startWatchdogTimer) {
        clearTimeout(startWatchdogTimer);
        startWatchdogTimer = null;
      }

      if (transitionPreview.reverseMode !== 'reverse') {
        const mediaDurationSec = Number(video.duration);
        const durationSec =
          Number.isFinite(configuredDurationSec) && configuredDurationSec > 0
            ? configuredDurationSec
            : Number.isFinite(mediaDurationSec) && mediaDurationSec > 0
              ? mediaDurationSec
              : NaN;
        if (Number.isFinite(durationSec) && durationSec > 0) {
          scheduleFinishByDuration(durationSec);
        }
      }
    };

    const onEnded = () => finishPreview('ended');

    const onPlaybackError = (eventName: string, error?: unknown) => {
      if (didFinish) return;
      logTransitionIssue(eventName, error);
      finishPreview(eventName);
    };

    const onError = () => onPlaybackError('video-error');
    const onAbort = () => onPlaybackError('video-abort');
    const onStalled = () => {
      if (didFinish) return;
      logTransitionIssue('video-stalled');
    };

    video.addEventListener('loadedmetadata', onLoadedMetadata);
    video.addEventListener('canplaythrough', onCanPlayThrough);
    video.addEventListener('canplay', onCanPlay);
    video.addEventListener('playing', onPlaying);
    video.addEventListener('ended', onEnded);
    video.addEventListener('error', onError);
    video.addEventListener('abort', onAbort);
    video.addEventListener('stalled', onStalled);

    hardTimeoutTimer = setTimeout(() => {
      if (didFinish) return;
      logTransitionIssue('hard-timeout');
      finishPreview('hard-timeout');
    }, 45000);

    void loadSourceCandidate();

    return () => {
      video.removeEventListener('loadedmetadata', onLoadedMetadata);
      video.removeEventListener('canplaythrough', onCanPlayThrough);
      video.removeEventListener('canplay', onCanPlay);
      video.removeEventListener('playing', onPlaying);
      video.removeEventListener('ended', onEnded);
      video.removeEventListener('error', onError);
      video.removeEventListener('abort', onAbort);
      video.removeEventListener('stalled', onStalled);
      clearTimers();
      cleanupReverseFrame();
      cleanupPreviewBlobUrl(true); // Force cleanup on effect cleanup
      if (!didFinish) {
        video.pause();
        video.removeAttribute('src');
        video.load();
      }
    };
  }, [transitionPreview]);

  useEffect(() => {
    return () => {
      if (reverseAnimationFrame.current !== null) {
        clearInterval(reverseAnimationFrame.current);
      }
    };
  }, []);

  return (
    <>
      <Head>
        <title>
          {getPageTitle(isElementEditMode ? 'Edit Element' : 'Constructor')}
        </title>
      </Head>
      <div className='relative w-screen h-screen bg-white overflow-hidden'>
        <div className='absolute top-4 left-4 z-30 flex max-w-[80vw] flex-col gap-2'>
          <p className='text-xs font-semibold text-gray-700'>
            {projectName || 'Loading project...'}
          </p>
          {errorMessage ? (
            <p className='rounded bg-red-50 px-2 py-1 text-xs text-red-600'>
              {errorMessage}
            </p>
          ) : null}
          {successMessage ? (
            <p className='rounded bg-green-50 px-2 py-1 text-xs text-green-700'>
              {successMessage}
            </p>
          ) : null}

          {pages.length > 0 && isElementEditMode && (
            <div className='flex items-center gap-2'>
              <BaseButton
                color='lightDark'
                label='Back to Elements'
                icon={mdiExitToApp}
                href={pageElementsListHref}
              />
              <BaseButton
                color='info'
                label={isSaving ? 'Saving...' : 'Save'}
                icon={mdiContentSave}
                onClick={saveConstructor}
                disabled={isSaving}
              />
            </div>
          )}
        </div>

        {pages.length > 0 && !isElementEditMode && (
          <div
            className='fixed z-40 w-[min(92vw,460px)] rounded-lg border border-gray-200 bg-white shadow-xl'
            style={{
              left: constructorControlsPosition.x,
              top: constructorControlsPosition.y,
            }}
          >
            <div
              className='flex cursor-move items-center justify-between rounded-t-lg border-b border-gray-200 bg-gray-50 px-3 py-2'
              onMouseDown={onConstructorControlsDragStart}
            >
              <span className='text-xs font-bold uppercase'>
                Constructor Controls
              </span>
            </div>
            <div className='space-y-2 p-3'>
              <div className='flex flex-wrap items-center gap-2'>
                <select
                  className='rounded border border-gray-300 bg-white px-3 py-2 text-sm'
                  value={activePageId}
                  onChange={(event) => setActivePageId(event.target.value)}
                >
                  {pages.map((page, index) => (
                    <option key={page.id} value={page.id}>
                      {page.name || `Page ${index + 1}`}
                    </option>
                  ))}
                </select>
                <BaseButton
                  color='lightDark'
                  label='Exit to Assets'
                  icon={mdiExitToApp}
                  href={
                    projectId
                      ? `/projects/${projectId}`
                      : '/projects/projects-list'
                  }
                />
              </div>
              <div className='flex flex-wrap items-center gap-2'>
                <div className='inline-flex overflow-hidden rounded border border-gray-300 bg-white text-xs font-semibold'>
                  <button
                    type='button'
                    className={`px-3 py-1.5 ${
                      isConstructorEditMode
                        ? 'bg-blue-600 text-white'
                        : 'text-gray-700 hover:bg-gray-50'
                    }`}
                    onClick={() => setConstructorInteractionMode('edit')}
                  >
                    Edit mode
                  </button>
                  <button
                    type='button'
                    className={`border-l border-gray-300 px-3 py-1.5 ${
                      !isConstructorEditMode
                        ? 'bg-blue-600 text-white'
                        : 'text-gray-700 hover:bg-gray-50'
                    }`}
                    onClick={() => setConstructorInteractionMode('interact')}
                  >
                    Interact mode
                  </button>
                </div>
                <span className='text-[11px] text-gray-600'>
                  {isConstructorEditMode
                    ? 'Drag & configure elements.'
                    : 'Click and interact with rendered elements.'}
                </span>
              </div>
            </div>
          </div>
        )}

        <div
          ref={canvasRef}
          tabIndex={-1}
          className='absolute inset-0 bg-white overflow-hidden'
          style={canvasBackgroundStyle}
        >
          {backgroundImageSrc ? (
            <div className='absolute inset-0 h-full w-full pointer-events-none select-none'>
              <NextImage
                key={`bg_image_${backgroundImageSrc}`}
                src={backgroundImageSrc}
                alt='Background'
                fill
                sizes='100vw'
                className='object-cover'
                draggable={false}
              />
            </div>
          ) : null}

          {backgroundVideoSrc ? (
            <video
              key={`bg_video_${backgroundVideoSrc}`}
              className='absolute inset-0 w-full h-full object-cover'
              src={backgroundVideoSrc}
              autoPlay
              loop
              muted
              playsInline
            />
          ) : null}

          {backgroundAudioSrc ? (
            <audio
              key={`bg_audio_${backgroundAudioSrc}`}
              src={backgroundAudioSrc}
              autoPlay
              loop
              hidden
            />
          ) : null}

          {isLoading ? (
            <div className='absolute inset-0 flex items-center justify-center'>
              <p className='text-sm text-gray-500'>Loading constructor...</p>
            </div>
          ) : pages.length === 0 ? (
            <div className='absolute inset-0 flex items-center justify-center'>
              <BaseButton
                color='info'
                label={isCreatingPage ? 'Creating...' : 'Create First Page'}
                icon={mdiPlus}
                onClick={createPage}
                disabled={isCreatingPage}
              />
            </div>
          ) : (
            elements.map((element) => {
              const shouldRender =
                selectedElementId === element.id ||
                (isElementVisibleOnCanvas(element) &&
                  isElementReadyForCanvasRender(element));
              if (!shouldRender) return null;

              const hasIconDrivenSize =
                Boolean(element.iconUrl) &&
                (element.type === 'tooltip' ||
                  element.type === 'description' ||
                  element.type === 'navigation_next' ||
                  element.type === 'navigation_prev');
              const isNavigationIconElement =
                Boolean(element.iconUrl) &&
                (element.type === 'navigation_next' ||
                  element.type === 'navigation_prev');
              const hasTransparentBackground =
                (element.type === 'description' &&
                  !element.iconUrl &&
                  (!element.descriptionBackgroundColor ||
                    element.descriptionBackgroundColor === 'transparent')) ||
                // Navigation buttons with icons should be transparent (icon is visible)
                ((element.type === 'navigation_next' ||
                  element.type === 'navigation_prev') &&
                  Boolean(element.iconUrl)) ||
                element.type === 'tooltip' ||
                element.type === 'gallery' ||
                element.type === 'carousel';
              const isNavDisabled =
                isNavigationElementType(element.type) &&
                (element.navDisabled || transitionPreview || isBufferingForReverse);

              return (
                <button
                  key={element.id}
                  type='button'
                  data-constructor-element-id={element.id}
                  className={`absolute rounded text-xs font-semibold text-left ${
                    hasTransparentBackground ? '' : 'border shadow'
                  } ${
                    hasIconDrivenSize
                      ? 'overflow-hidden p-0 leading-none'
                      : 'px-3 py-2'
                  } ${
                    isNavigationIconElement
                      ? 'flex items-center justify-center'
                      : ''
                  } ${
                    isConstructorEditMode
                      ? 'cursor-move'
                      : isNavDisabled
                        ? 'cursor-not-allowed opacity-50'
                        : 'cursor-pointer'
                  } ${
                    selectedElementId === element.id
                      ? 'border-blue-500 bg-blue-50 border shadow'
                      : hasTransparentBackground
                        ? 'bg-transparent'
                        : 'border-blue-200 bg-white/95'
                  }`}
                  style={{
                    ...buildCanvasElementStyle(element),
                    left: `${element.xPercent}%`,
                    top: `${element.yPercent}%`,
                    transform: 'translate(-50%, -50%)',
                  }}
                  onMouseDown={
                    isConstructorEditMode
                      ? (event) => onElementMouseDown(event, element.id)
                      : undefined
                  }
                  onClick={() => onCanvasElementClick(element)}
                >
                  {renderCanvasElementContent(element)}
                </button>
              );
            })
          )}
        </div>

        {pages.length > 0 && hasEditorSelection && (
          <div
            ref={elementEditorRef}
            className={`fixed z-40 ${isEditorCollapsed ? 'w-[260px]' : 'w-[380px]'} max-h-[calc(100vh-2rem)] overflow-auto rounded-lg border border-gray-200 bg-white/95 p-3 shadow-xl`}
            style={{ left: editorPosition.x, top: editorPosition.y }}
          >
            <div
              className='mb-3 flex items-center justify-between gap-2 cursor-move'
              onMouseDown={onElementEditorDragStart}
            >
              <p className='text-xs font-bold uppercase tracking-wide text-gray-700'>
                {editorTitle}
              </p>
              <div className='flex items-center gap-2'>
                <button
                  type='button'
                  className='text-xs text-gray-700 hover:underline'
                  onClick={() => setIsEditorCollapsed((prev) => !prev)}
                >
                  {isEditorCollapsed ? 'Expand' : 'Collapse'}
                </button>
                {selectedElement && (
                  <button
                    type='button'
                    className='text-xs text-red-600 hover:underline'
                    onClick={removeSelectedElement}
                  >
                    Remove element
                  </button>
                )}
              </div>
            </div>

            {!isEditorCollapsed && (
              <>
                {selectedMenuItem === 'background_image' && (
                  <div>
                    <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                      Background image
                    </label>
                    <select
                      className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                      value={backgroundImageUrl}
                      onChange={(event) => {
                        const nextValue = event.target.value;
                        setBackgroundImageUrl(nextValue);
                        if (nextValue) setBackgroundVideoUrl('');
                      }}
                    >
                      <option value=''>None</option>
                      {backgroundImageSelectOptions.map((option) => (
                        <option key={option.value} value={option.value}>
                          {option.label}
                        </option>
                      ))}
                    </select>
                  </div>
                )}

                {selectedMenuItem === 'background_video' && (
                  <div>
                    <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                      Background video
                    </label>
                    <select
                      className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                      value={backgroundVideoUrl}
                      onChange={(event) => {
                        const nextValue = event.target.value;
                        setBackgroundVideoUrl(nextValue);
                        if (nextValue) setBackgroundImageUrl('');
                      }}
                    >
                      <option value=''>None</option>
                      {backgroundVideoSelectOptions.map((option) => (
                        <option key={option.value} value={option.value}>
                          {option.label}
                        </option>
                      ))}
                    </select>
                    <p className='mt-1 text-[11px] text-gray-500'>
                      {backgroundVideoDurationNote}
                    </p>
                  </div>
                )}

                {selectedMenuItem === 'background_audio' && (
                  <div>
                    <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                      Background audio (loop)
                    </label>
                    <select
                      className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                      value={backgroundAudioUrl}
                      onChange={(event) =>
                        setBackgroundAudioUrl(event.target.value)
                      }
                    >
                      <option value=''>None</option>
                      {backgroundAudioSelectOptions.map((option) => (
                        <option key={option.value} value={option.value}>
                          {option.label}
                        </option>
                      ))}
                    </select>
                    <p className='mt-1 text-[11px] text-gray-500'>
                      {backgroundAudioDurationNote}
                    </p>
                  </div>
                )}

                {selectedMenuItem === 'create_transition' && (
                  <div className='rounded border border-gray-200 p-2 space-y-2'>
                    <p className='text-[11px] font-semibold text-gray-600'>
                      Create next page transition
                    </p>
                    <input
                      className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                      placeholder='Name'
                      value={newTransitionName}
                      onChange={(event) =>
                        setNewTransitionName(event.target.value)
                      }
                    />
                    <select
                      className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                      value={newTransitionVideoUrl}
                      onChange={(event) =>
                        setNewTransitionVideoUrl(event.target.value)
                      }
                    >
                      <option value=''>Transition video asset</option>
                      {transitionVideoAssetOptions.map((option) => (
                        <option key={option.value} value={option.value}>
                          {option.label}
                        </option>
                      ))}
                    </select>
                    <p className='text-[11px] text-gray-500'>
                      Transition duration is automatic from video metadata.{' '}
                      {newTransitionDurationNote}
                    </p>
                    <label className='flex items-center gap-2 text-[11px] text-gray-700'>
                      <input
                        type='checkbox'
                        checked={newTransitionSupportsReverse}
                        onChange={(event) =>
                          setNewTransitionSupportsReverse(event.target.checked)
                        }
                      />
                      Supports reverse playback
                    </label>
                    <button
                      type='button'
                      className='menu-action-btn'
                      onClick={createTransition}
                      disabled={isCreatingTransition}
                    >
                      <BaseIcon path={mdiSwapHorizontal} size={16} />
                      <span>
                        {isCreatingTransition
                          ? 'Creating Transition...'
                          : 'Create Transition'}
                      </span>
                    </button>
                  </div>
                )}

                {selectedElement && (
                  <div className='mb-2 space-y-2'>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Label
                      </label>
                      <input
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        value={selectedElement.label}
                        onChange={(event) =>
                          updateSelectedElement({ label: event.target.value })
                        }
                      />
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Appear delay (sec)
                      </label>
                      <input
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        type='number'
                        min='0'
                        step='0.1'
                        value={selectedElement.appearDelaySec ?? 0}
                        onChange={(event) =>
                          updateSelectedElement({
                            appearDelaySec: normalizeAppearDelaySec(
                              event.target.value,
                            ),
                          })
                        }
                      />
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Appear duration (sec)
                      </label>
                      <input
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        type='number'
                        min='0.1'
                        step='0.1'
                        placeholder='Unlimited'
                        value={selectedElement.appearDurationSec ?? ''}
                        onChange={(event) =>
                          updateSelectedElement({
                            appearDurationSec: normalizeAppearDurationSec(
                              event.target.value,
                            ),
                          })
                        }
                      />
                      <p className='mt-1 text-[11px] text-gray-500'>
                        Leave empty for unlimited.
                      </p>
                    </div>
                  </div>
                )}

                {selectedElement &&
                  (selectedElement.type === 'navigation_next' ||
                    selectedElement.type === 'navigation_prev') && (
                    <div className='space-y-2'>
                      <div>
                        <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                          Type
                        </label>
                        <select
                          className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                          value={
                            selectedElement.navType === 'back' ||
                            selectedElement.navType === 'forward'
                              ? selectedElement.navType
                              : getNavigationButtonKind(selectedElement.type)
                          }
                          onChange={(event) => {
                            const requestedKind: NavigationButtonKind =
                              event.target.value === 'back'
                                ? 'back'
                                : 'forward';
                            const requestedType =
                              getNavigationTypeFromKind(requestedKind);
                            const nextType = allowedNavigationTypes.includes(
                              requestedType,
                            )
                              ? requestedType
                              : allowedNavigationTypes[0];
                            updateSelectedElement(
                              normalizeNavigationElementType(
                                selectedElement,
                                nextType,
                              ),
                            );
                          }}
                        >
                          <option
                            value='forward'
                            disabled={
                              !allowedNavigationTypes.includes(
                                'navigation_next',
                              )
                            }
                          >
                            Forward
                          </option>
                          <option
                            value='back'
                            disabled={
                              !allowedNavigationTypes.includes(
                                'navigation_prev',
                              )
                            }
                          >
                            Back
                          </option>
                        </select>
                      </div>
                      <div>
                        <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                          Button text
                        </label>
                        <input
                          className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                          value={selectedElement.navLabel || ''}
                          onChange={(event) =>
                            updateSelectedElement({
                              navLabel: event.target.value,
                            })
                          }
                        />
                      </div>
                      <div>
                        <label className='flex items-center gap-2 text-[11px] font-semibold text-gray-600'>
                          <input
                            type='checkbox'
                            checked={selectedElement.navDisabled || false}
                            onChange={(event) =>
                              updateSelectedElement({
                                navDisabled: event.target.checked,
                              })
                            }
                          />
                          Disabled
                        </label>
                      </div>
                      <div>
                        <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                          Icon
                        </label>
                        <select
                          className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                          value={selectedElement.iconUrl || ''}
                          onChange={(event) =>
                            updateSelectedElement({
                              iconUrl: event.target.value,
                            })
                          }
                        >
                          <option value=''>Not selected</option>
                          {addFallbackAssetOption(
                            iconAssetOptions,
                            selectedElement.iconUrl,
                            `Current icon · ${selectedElement.iconUrl || ''}`,
                          ).map((option) => (
                            <option key={option.value} value={option.value}>
                              {option.label}
                            </option>
                          ))}
                        </select>
                        <p className='mt-1 text-[11px] text-gray-500'>
                          {selectedMediaDurationNote}
                        </p>
                      </div>
                      <div>
                        <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                          Target page
                        </label>
                        <select
                          className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                          value={selectedElement.targetPageId || ''}
                          onChange={(event) =>
                            updateSelectedElement({
                              targetPageId: event.target.value,
                            })
                          }
                        >
                          <option value=''>Not selected</option>
                          {pages
                            .filter((page) => page.id !== activePageId)
                            .map((page, index) => (
                              <option key={page.id} value={page.id}>
                                {page.name || `Page ${index + 1}`}
                              </option>
                            ))}
                        </select>
                      </div>
                      <div>
                        <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                          Transition video asset
                        </label>
                        <select
                          className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                          value={selectedElement.transitionVideoUrl || ''}
                          onChange={(event) => {
                            const nextVideoUrl = event.target.value;
                            const resolvedDuration =
                              getKnownDurationForSource(nextVideoUrl);
                            updateSelectedElement({
                              transitionVideoUrl: nextVideoUrl,
                              transitionDurationSec:
                                resolvedDuration || undefined,
                            });
                          }}
                        >
                          <option value=''>Not selected</option>
                          {addFallbackAssetOption(
                            transitionVideoAssetOptions,
                            selectedElement.transitionVideoUrl,
                            `Current video · ${selectedElement.transitionVideoUrl || ''}`,
                          ).map((option) => (
                            <option key={option.value} value={option.value}>
                              {option.label}
                            </option>
                          ))}
                        </select>
                        <p className='mt-1 text-[11px] text-gray-500'>
                          {selectedTransitionDurationNote}
                        </p>
                      </div>
                      <div>
                        <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                          Back transition mode
                        </label>
                        <select
                          className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                          value={
                            selectedElement.transitionReverseMode ||
                            'auto_reverse'
                          }
                          onChange={(event) =>
                            updateSelectedElement({
                              transitionReverseMode:
                                event.target.value === 'separate_video'
                                  ? 'separate_video'
                                  : 'auto_reverse',
                            })
                          }
                        >
                          <option value='auto_reverse'>
                            Auto reverse transition video
                          </option>
                          <option value='separate_video'>
                            Use separate back-transition video
                          </option>
                        </select>
                      </div>
                      {selectedElement.transitionReverseMode ===
                        'separate_video' && (
                        <div>
                          <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                            Back transition video asset
                          </label>
                          <select
                            className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                            value={selectedElement.reverseVideoUrl || ''}
                            onChange={(event) =>
                              updateSelectedElement({
                                reverseVideoUrl: event.target.value,
                              })
                            }
                          >
                            <option value=''>Not selected</option>
                            {addFallbackAssetOption(
                              transitionVideoAssetOptions,
                              selectedElement.reverseVideoUrl,
                              `Current back video · ${selectedElement.reverseVideoUrl || ''}`,
                            ).map((option) => (
                              <option key={option.value} value={option.value}>
                                {option.label}
                              </option>
                            ))}
                          </select>
                        </div>
                      )}
                      <p className='text-[11px] text-gray-500'>
                        Transition duration is set automatically from the
                        selected video. {selectedTransitionDurationNote}
                      </p>
                      <div className='flex gap-2 pt-1'>
                        <BaseButton
                          small
                          color='lightDark'
                          label='Preview Forward'
                          onClick={() => openTransitionPreview('forward')}
                        />
                        <BaseButton
                          small
                          color='lightDark'
                          label='Preview Back'
                          onClick={() => openTransitionPreview('back')}
                        />
                      </div>
                    </div>
                  )}

                {selectedElement && selectedElement.type === 'tooltip' && (
                  <div className='space-y-2'>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Icon
                      </label>
                      <select
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        value={selectedElement.iconUrl || ''}
                        onChange={(event) =>
                          updateSelectedElement({ iconUrl: event.target.value })
                        }
                      >
                        <option value=''>Not selected</option>
                        {addFallbackAssetOption(
                          iconAssetOptions,
                          selectedElement.iconUrl,
                          `Current icon · ${selectedElement.iconUrl || ''}`,
                        ).map((option) => (
                          <option key={option.value} value={option.value}>
                            {option.label}
                          </option>
                        ))}
                      </select>
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Tooltip title
                      </label>
                      <input
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        value={selectedElement.tooltipTitle || ''}
                        onChange={(event) =>
                          updateSelectedElement({
                            tooltipTitle: event.target.value,
                          })
                        }
                      />
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Tooltip text
                      </label>
                      <textarea
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        rows={4}
                        value={selectedElement.tooltipText || ''}
                        onChange={(event) =>
                          updateSelectedElement({
                            tooltipText: event.target.value,
                          })
                        }
                      />
                    </div>
                  </div>
                )}

                {selectedElement && selectedElement.type === 'description' && (
                  <div className='space-y-2'>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Icon
                      </label>
                      <select
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        value={selectedElement.iconUrl || ''}
                        onChange={(event) =>
                          updateSelectedElement({ iconUrl: event.target.value })
                        }
                      >
                        <option value=''>Not selected</option>
                        {addFallbackAssetOption(
                          iconAssetOptions,
                          selectedElement.iconUrl,
                          `Current icon · ${selectedElement.iconUrl || ''}`,
                        ).map((option) => (
                          <option key={option.value} value={option.value}>
                            {option.label}
                          </option>
                        ))}
                      </select>
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Description title
                      </label>
                      <input
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        value={selectedElement.descriptionTitle || ''}
                        onChange={(event) =>
                          updateSelectedElement({
                            descriptionTitle: event.target.value,
                          })
                        }
                      />
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Description text
                      </label>
                      <textarea
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        rows={5}
                        value={selectedElement.descriptionText || ''}
                        onChange={(event) =>
                          updateSelectedElement({
                            descriptionText: event.target.value,
                          })
                        }
                      />
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Title font size
                      </label>
                      <input
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        value={
                          selectedElement.descriptionTitleFontSize || '48px'
                        }
                        onChange={(event) =>
                          updateSelectedElement({
                            descriptionTitleFontSize: event.target.value,
                          })
                        }
                        placeholder='e.g. 48px'
                      />
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Text font size
                      </label>
                      <input
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        value={
                          selectedElement.descriptionTextFontSize || '36px'
                        }
                        onChange={(event) =>
                          updateSelectedElement({
                            descriptionTextFontSize: event.target.value,
                          })
                        }
                        placeholder='e.g. 36px'
                      />
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Title font family
                      </label>
                      <input
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        value={
                          selectedElement.descriptionTitleFontFamily ||
                          'inherit'
                        }
                        onChange={(event) =>
                          updateSelectedElement({
                            descriptionTitleFontFamily: event.target.value,
                          })
                        }
                        placeholder='e.g. Arial, Helvetica, sans-serif'
                      />
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Text font family
                      </label>
                      <input
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        value={
                          selectedElement.descriptionTextFontFamily || 'inherit'
                        }
                        onChange={(event) =>
                          updateSelectedElement({
                            descriptionTextFontFamily: event.target.value,
                          })
                        }
                        placeholder='e.g. Arial, Helvetica, sans-serif'
                      />
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Title color
                      </label>
                      <input
                        type='color'
                        className='w-full h-8 rounded border border-gray-300 px-1 py-1'
                        value={
                          selectedElement.descriptionTitleColor || '#000000'
                        }
                        onChange={(event) =>
                          updateSelectedElement({
                            descriptionTitleColor: event.target.value,
                          })
                        }
                      />
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Text color
                      </label>
                      <input
                        type='color'
                        className='w-full h-8 rounded border border-gray-300 px-1 py-1'
                        value={
                          selectedElement.descriptionTextColor || '#4B5563'
                        }
                        onChange={(event) =>
                          updateSelectedElement({
                            descriptionTextColor: event.target.value,
                          })
                        }
                      />
                    </div>
                    <div>
                      <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                        Background color
                      </label>
                      <input
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        value={
                          selectedElement.descriptionBackgroundColor ||
                          'transparent'
                        }
                        onChange={(event) =>
                          updateSelectedElement({
                            descriptionBackgroundColor: event.target.value,
                          })
                        }
                        placeholder='e.g. transparent, #ffffff, rgba(0,0,0,0.5)'
                      />
                    </div>
                  </div>
                )}

                {selectedElement &&
                  (selectedElement.type === 'video_player' ||
                    selectedElement.type === 'audio_player') && (
                    <div className='space-y-2'>
                      <div>
                        <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
                          {selectedElement.type === 'video_player'
                            ? 'Video asset'
                            : 'Audio asset'}
                        </label>
                        <select
                          className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                          value={selectedElement.mediaUrl || ''}
                          onChange={(event) =>
                            updateSelectedElement({
                              mediaUrl: event.target.value,
                            })
                          }
                        >
                          <option value=''>Not selected</option>
                          {addFallbackAssetOption(
                            selectedElement.type === 'video_player'
                              ? videoAssetOptions
                              : audioAssetOptions,
                            selectedElement.mediaUrl,
                            `Current media · ${selectedElement.mediaUrl || ''}`,
                          ).map((option) => (
                            <option key={option.value} value={option.value}>
                              {option.label}
                            </option>
                          ))}
                        </select>
                      </div>
                      <label className='flex items-center gap-2 text-[11px] text-gray-700'>
                        <input
                          type='checkbox'
                          checked={Boolean(selectedElement.mediaAutoplay)}
                          onChange={(event) =>
                            updateSelectedElement({
                              mediaAutoplay: event.target.checked,
                            })
                          }
                        />
                        Autoplay
                      </label>
                      <label className='flex items-center gap-2 text-[11px] text-gray-700'>
                        <input
                          type='checkbox'
                          checked={Boolean(selectedElement.mediaLoop)}
                          onChange={(event) =>
                            updateSelectedElement({
                              mediaLoop: event.target.checked,
                            })
                          }
                        />
                        Loop
                      </label>
                      {selectedElement.type === 'video_player' && (
                        <label className='flex items-center gap-2 text-[11px] text-gray-700'>
                          <input
                            type='checkbox'
                            checked={Boolean(selectedElement.mediaMuted)}
                            onChange={(event) =>
                              updateSelectedElement({
                                mediaMuted: event.target.checked,
                              })
                            }
                          />
                          Muted
                        </label>
                      )}
                    </div>
                  )}

                {selectedElement && selectedElement.type === 'gallery' && (
                  <div className='space-y-2'>
                    <div className='flex items-center justify-between'>
                      <p className='text-[11px] font-semibold text-gray-600'>
                        Gallery cards
                      </p>
                      <button
                        type='button'
                        className='text-xs text-blue-700 hover:underline'
                        onClick={addGalleryCard}
                      >
                        + Add card
                      </button>
                    </div>
                    {(selectedElement.galleryCards || []).map((card, index) => (
                      <div
                        key={card.id}
                        className='rounded border border-gray-200 p-2 space-y-2'
                      >
                        <div className='flex items-center justify-between'>
                          <p className='text-[11px] font-semibold text-gray-700'>
                            Card {index + 1}
                          </p>
                          <button
                            type='button'
                            className='text-xs text-red-600 hover:underline'
                            onClick={() => removeGalleryCard(card.id)}
                          >
                            Remove
                          </button>
                        </div>
                        <select
                          className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                          value={card.imageUrl}
                          onChange={(event) =>
                            updateGalleryCard(card.id, {
                              imageUrl: event.target.value,
                            })
                          }
                        >
                          <option value=''>Image asset</option>
                          {addFallbackAssetOption(
                            imageAssetOptions,
                            card.imageUrl,
                            `Current image · ${card.imageUrl}`,
                          ).map((option) => (
                            <option key={option.value} value={option.value}>
                              {option.label}
                            </option>
                          ))}
                        </select>
                        <input
                          className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                          placeholder='Title'
                          value={card.title}
                          onChange={(event) =>
                            updateGalleryCard(card.id, {
                              title: event.target.value,
                            })
                          }
                        />
                        <textarea
                          className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                          placeholder='Description'
                          rows={3}
                          value={card.description}
                          onChange={(event) =>
                            updateGalleryCard(card.id, {
                              description: event.target.value,
                            })
                          }
                        />
                      </div>
                    ))}
                  </div>
                )}

                {selectedElement && selectedElement.type === 'carousel' && (
                  <div className='space-y-2'>
                    <div className='rounded border border-gray-200 p-2 space-y-2'>
                      <p className='text-[11px] font-semibold text-gray-700'>
                        Navigation icons
                      </p>
                      <select
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        value={selectedElement.carouselPrevIconUrl || ''}
                        onChange={(event) =>
                          updateSelectedElement({
                            carouselPrevIconUrl: event.target.value,
                          })
                        }
                      >
                        <option value=''>Previous icon</option>
                        {addFallbackAssetOption(
                          iconAssetOptions,
                          selectedElement.carouselPrevIconUrl,
                          `Current prev icon · ${selectedElement.carouselPrevIconUrl || ''}`,
                        ).map((option) => (
                          <option key={option.value} value={option.value}>
                            {option.label}
                          </option>
                        ))}
                      </select>
                      <select
                        className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                        value={selectedElement.carouselNextIconUrl || ''}
                        onChange={(event) =>
                          updateSelectedElement({
                            carouselNextIconUrl: event.target.value,
                          })
                        }
                      >
                        <option value=''>Next icon</option>
                        {addFallbackAssetOption(
                          iconAssetOptions,
                          selectedElement.carouselNextIconUrl,
                          `Current next icon · ${selectedElement.carouselNextIconUrl || ''}`,
                        ).map((option) => (
                          <option key={option.value} value={option.value}>
                            {option.label}
                          </option>
                        ))}
                      </select>
                    </div>
                    <div className='flex items-center justify-between'>
                      <p className='text-[11px] font-semibold text-gray-600'>
                        Carousel slides
                      </p>
                      <button
                        type='button'
                        className='text-xs text-blue-700 hover:underline'
                        onClick={addCarouselSlide}
                      >
                        + Add slide
                      </button>
                    </div>
                    {(selectedElement.carouselSlides || []).map(
                      (slide, index) => (
                        <div
                          key={slide.id}
                          className='rounded border border-gray-200 p-2 space-y-2'
                        >
                          <div className='flex items-center justify-between'>
                            <p className='text-[11px] font-semibold text-gray-700'>
                              Slide {index + 1}
                            </p>
                            <button
                              type='button'
                              className='text-xs text-red-600 hover:underline'
                              onClick={() => removeCarouselSlide(slide.id)}
                            >
                              Remove
                            </button>
                          </div>
                          <select
                            className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                            value={slide.imageUrl}
                            onChange={(event) =>
                              updateCarouselSlide(slide.id, {
                                imageUrl: event.target.value,
                              })
                            }
                          >
                            <option value=''>Image asset</option>
                            {addFallbackAssetOption(
                              imageAssetOptions,
                              slide.imageUrl,
                              `Current image · ${slide.imageUrl}`,
                            ).map((option) => (
                              <option key={option.value} value={option.value}>
                                {option.label}
                              </option>
                            ))}
                          </select>
                          <input
                            className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
                            placeholder='Caption'
                            value={slide.caption}
                            onChange={(event) =>
                              updateCarouselSlide(slide.id, {
                                caption: event.target.value,
                              })
                            }
                          />
                        </div>
                      ),
                    )}
                  </div>
                )}
              </>
            )}
          </div>
        )}

        {pages.length > 0 && !isElementEditMode && (
          <div
            className='fixed z-40 w-60 border border-gray-200 rounded-lg bg-white shadow-xl'
            style={{ left: menuPosition.x, top: menuPosition.y }}
          >
            <div
              className='flex items-center justify-between px-3 py-2 border-b border-gray-200 cursor-move bg-gray-50 rounded-t-lg'
              onMouseDown={onMenuDragStart}
            >
              <span className='text-xs font-bold uppercase'>
                Constructor Menu
              </span>
              <button
                type='button'
                onClick={() => setIsMenuOpen((prev) => !prev)}
              >
                <BaseIcon path={mdiMenu} size={18} />
              </button>
            </div>

            {isMenuOpen && (
              <div className='p-2 space-y-1'>
                <button
                  type='button'
                  className='menu-action-btn'
                  onClick={() => selectMenuItemForEdit('background_image')}
                >
                  <BaseIcon path={mdiImageMultiple} size={16} />
                  <span>Background Image</span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn'
                  onClick={() => selectMenuItemForEdit('background_video')}
                >
                  <BaseIcon path={mdiViewCarousel} size={16} />
                  <span>Background Video</span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn'
                  onClick={() => selectMenuItemForEdit('background_audio')}
                >
                  <BaseIcon path={mdiTooltipText} size={16} />
                  <span>Background Audio</span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn'
                  onClick={() => addElement(allowedNavigationTypes[0])}
                >
                  <BaseIcon path={mdiSwapHorizontal} size={16} />
                  <span>Add Navigation Button</span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn'
                  onClick={() => selectMenuItemForEdit('create_transition')}
                >
                  <BaseIcon path={mdiSwapHorizontal} size={16} />
                  <span>Add Transition</span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn'
                  onClick={() => addElement('gallery')}
                >
                  <BaseIcon path={mdiImageMultiple} size={16} />
                  <span>Add Gallery</span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn'
                  onClick={() => addElement('carousel')}
                >
                  <BaseIcon path={mdiViewCarousel} size={16} />
                  <span>Add Carousel</span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn'
                  onClick={() => addElement('tooltip')}
                >
                  <BaseIcon path={mdiTooltipText} size={16} />
                  <span>Add Tooltip</span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn'
                  onClick={() => addElement('description')}
                >
                  <BaseIcon path={mdiText} size={16} />
                  <span>Add Description</span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn'
                  onClick={() => addElement('video_player')}
                >
                  <BaseIcon path={mdiViewCarousel} size={16} />
                  <span>Add Video Player</span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn'
                  onClick={() => addElement('audio_player')}
                >
                  <BaseIcon path={mdiTooltipText} size={16} />
                  <span>Add Audio Player</span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn'
                  onClick={createPage}
                  disabled={isCreatingPage}
                >
                  <BaseIcon path={mdiPlus} size={16} />
                  <span>
                    {isCreatingPage ? 'Creating Page...' : 'Create New Page'}
                  </span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn !text-blue-700'
                  onClick={saveConstructor}
                  disabled={isSaving}
                >
                  <BaseIcon path={mdiContentSave} size={16} />
                  <span>{isSaving ? 'Saving...' : 'Save'}</span>
                </button>
                <button
                  type='button'
                  className='menu-action-btn !text-red-700'
                  onClick={() =>
                    router.push(
                      projectId
                        ? `/projects/${projectId}`
                        : '/projects/projects-list',
                    )
                  }
                >
                  <BaseIcon path={mdiExitToApp} size={16} />
                  <span>Exit</span>
                </button>
              </div>
            )}
          </div>
        )}
      </div>

      {transitionPreview && (
        <div className='fixed inset-0 z-50 overflow-hidden pointer-events-none'>
          <video
            ref={transitionVideoRef}
            className='absolute inset-0 h-full w-full object-cover transition-opacity duration-300 ease-linear'
            style={{ opacity: isBufferingForReverse ? 0 : 1 }}
            muted
            playsInline
            preload='auto'
            disablePictureInPicture
          />
        </div>
      )}

      <style jsx>{`
        .menu-action-btn {
          width: 100%;
          display: flex;
          align-items: center;
          gap: 8px;
          font-size: 12px;
          padding: 6px 8px;
          border-radius: 6px;
          color: #1f2937;
          text-align: left;
        }

        .menu-action-btn:hover {
          background: #f3f4f6;
        }
      `}</style>
    </>
  );
};

ConstructorPage.getLayout = function getLayout(page: ReactElement) {
  return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};

export default ConstructorPage;
