import {
  mdiCloudUpload,
  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 { getPageTitle } from '../config';
import LayoutAuthenticated from '../layouts/Authenticated';
import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator';
import { extractPageLinksAndElements } from '../lib/extractPageLinks';
import { usePageSwitch } from '../hooks/usePageSwitch';
import { useTransitionPlayback } from '../hooks/useTransitionPlayback';
import { logger } from '../lib/logger';
import { resolveAssetPlaybackUrl } from '../lib/assetUrl';
import { parseJsonObject } from '../lib/parseJson';
import { buildElementStyle, ELEMENT_STYLE_PROPS } from '../lib/elementStyles';
import type { PreloadPageLink, PreloadElement } from '../types/preload';
import type {
  CanvasElementType,
  CanvasElement,
  ConstructorSchema,
  ConstructorAsset as ProjectAsset,
  AssetOption,
  GalleryCard,
  CarouselSlide,
  NavigationButtonKind,
  NormalizedElementDefault,
} from '../types/constructor';
import {
  normalizeElementDefault,
  buildElementDefaultsMap,
  isCanvasElementType,
} from '../types/constructor';

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 NavigationElementType = Extract<
  CanvasElementType,
  'navigation_next' | 'navigation_prev'
>;

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 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 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',
  spot: 'Hotspot',
  description: 'Description',
  tooltip: 'Tooltip',
  gallery: 'Gallery',
  carousel: 'Carousel',
  logo: 'Logo',
  video_player: 'Video Player',
  audio_player: 'Audio Player',
  popup: 'Popup',
};

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,
  };

  // For style properties, use defaults if element has empty/null/undefined value
  // This ensures DB defaults are applied when element has no explicit value
  const elementRecord = element as unknown as Record<string, unknown>;
  const defaultsRecord = defaults as unknown as Record<string, unknown>;
  const mergedRecord = merged as unknown as Record<string, unknown>;
  ELEMENT_STYLE_PROPS.forEach((prop) => {
    const elementValue = elementRecord[prop];
    const defaultValue = defaultsRecord[prop];
    const elementIsEmpty =
      elementValue === '' ||
      elementValue === undefined ||
      elementValue === null;
    const defaultHasValue =
      defaultValue !== undefined &&
      defaultValue !== null &&
      defaultValue !== '';
    if (elementIsEmpty && defaultHasValue) {
      mergedRecord[prop] = defaultValue;
    }
  });

  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 =>
  buildElementStyle(element);

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 '/project-element-defaults/project-element-defaults-list';
    return `/project-element-defaults/project-element-defaults-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<PreloadPageLink[]>([]);
  const [allPagesPreloadElements, setAllPagesPreloadElements] = useState<
    PreloadElement[]
  >([]);
  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 [isSavingToStage, setIsSavingToStage] = 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 lastInitializedPageIdRef = useRef<string | null>(null);
  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
  // Uses allPagesPreloadElements (extracted in loadData) for proper neighbor preloading
  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: allPagesPreloadElements, // Use elements from ALL pages for proper neighbor preloading
    currentPageId: activePageId,
    enabled: !isLoading && !!activePageId,
    // maxNeighborDepth defaults to 1 - only preload immediate neighbors
  });

  // Page switch hook for smooth background transitions (uses blob URLs from preload cache)
  const pageSwitch = usePageSwitch({
    preloadCache: preloadOrchestrator
      ? {
          getReadyBlobUrl: preloadOrchestrator.getReadyBlobUrl,
          getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,
          preloadedUrls: preloadOrchestrator.preloadedUrls,
        }
      : undefined,
  });

  // Helper to switch pages without flash
  // Uses usePageSwitch hook to resolve blob URLs from preload cache
  // Also updates storage path state for editing/saving purposes
  const switchToPage = useCallback(
    async (page: TourPage | null) => {
      // Mark this page as initialized to prevent redundant effect calls
      if (page) {
        lastInitializedPageIdRef.current = page.id;
      }

      // Update storage path state (for editing and saving)
      setBackgroundImageUrl(page?.background_image_url || '');
      setBackgroundVideoUrl(page?.background_video_url || '');
      setBackgroundAudioUrl(page?.background_audio_url || '');

      // Use hook to resolve and set blob URLs for display
      await pageSwitch.switchToPage(
        page
          ? {
              id: page.id,
              background_image_url: page.background_image_url,
              background_video_url: page.background_video_url,
              background_audio_url: page.background_audio_url,
            }
          : null,
        () => {
          if (page) {
            setActivePageId(page.id);
          }
        },
      );
    },
    [pageSwitch],
  );

  const { isBuffering: isReverseBuffering } = useTransitionPlayback({
    videoRef: transitionVideoRef,
    transition: transitionPreview
      ? {
          videoUrl: resolveAssetPlaybackUrl(transitionPreview.videoUrl),
          reverseMode: transitionPreview.reverseMode,
          reverseVideoUrl: transitionPreview.reverseVideoUrl
            ? resolveAssetPlaybackUrl(transitionPreview.reverseVideoUrl)
            : undefined,
          durationSec: transitionPreview.durationSec,
          targetPageId: pendingNavigationPageId || undefined,
          displayName: transitionPreview.title,
        }
      : null,
    onComplete: async (targetPageId) => {
      const video = transitionVideoRef.current;
      if (targetPageId) {
        const targetPage = pages.find((p) => p.id === targetPageId) || null;
        // Use switchToPage which resolves blob URLs via usePageSwitch
        await switchToPage(targetPage);
        setSelectedElementId('');
        setSelectedMenuItem('none');
        setErrorMessage('');
        requestAnimationFrame(() => {
          requestAnimationFrame(() => {
            video?.removeAttribute('src');
            video?.load();
            setTransitionPreview(null);
            setPendingNavigationPageId('');
          });
        });
      } else {
        video?.removeAttribute('src');
        video?.load();
        setTransitionPreview(null);
        setPendingNavigationPageId('');
      }
    },
    timeouts: {
      playbackStartMs: 3000,
      hardTimeoutMs: 45000,
    },
    features: {
      useBlobUrl: true,
      preDecodeImages: false, // We handle image loading via usePageSwitch
    },
    preload: {
      preloadedUrls: preloadOrchestrator.preloadedUrls,
      getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,
    },
  });

  // Clear previous background overlay when new background is ready (direct navigation)
  useEffect(() => {
    if (
      pageSwitch.isSwitching &&
      pageSwitch.isNewBgReady &&
      pageSwitch.previousBgImageUrl
    ) {
      pageSwitch.clearPreviousBackground();
    }
  }, [
    pageSwitch.isSwitching,
    pageSwitch.isNewBgReady,
    pageSwitch.previousBgImageUrl,
    pageSwitch.clearPreviousBackground,
  ]);

  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 pageNameBySlug = useMemo(() => {
    const acc: Record<string, string> = {};
    pages.forEach((page, index) => {
      acc[String(page.slug)] = 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,
          assetsResponse,
          uiElementsResponse,
        ] = await Promise.all([
          axios.get(`/projects/${projectId}`),
          axios.get(
            `/tour_pages?limit=500&sort=asc&field=sort_order&project=${projectId}&environment=dev`,
          ),
          axios.get(
            `/assets?limit=500&page=0&sort=desc&field=createdAt&project=${projectId}`,
          ),
          axios.get(
            `/project-element-defaults?projectId=${projectId}&limit=200&page=0&sort=asc&field=sort_order`,
          ),
        ]);

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

        // Extract page links and preload elements using shared utility
        const { pageLinks: syntheticPageLinks, preloadElements: allPreloadElements } =
          extractPageLinksAndElements(pageRows);

        setPageLinks(syntheticPageLinks);
        setAllPagesPreloadElements(allPreloadElements);
        setAssets(assetRows);

        // Process project element defaults using shared utilities
        const uiElementRows = Array.isArray(uiElementsResponse?.data?.rows)
          ? uiElementsResponse.data.rows
          : [];
        const normalizedDefaults = uiElementRows
          .map((row: Record<string, unknown>) => normalizeElementDefault(row))
          .filter((d): d is NormalizedElementDefault => d !== null);
        const defaultsByType = buildElementDefaultsMap(normalizedDefaults);
        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
        ? '/project-element-defaults/project-element-defaults-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,
              // Support both targetPageSlug (new) and targetPageId (legacy)
              targetPageSlug:
                typeof item.targetPageSlug === 'string' ? item.targetPageSlug : '',
              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 '';
    });
    // Set storage paths for editing/saving
    setBackgroundImageUrl(activePage.background_image_url || '');
    setBackgroundVideoUrl(activePage.background_video_url || '');
    setBackgroundAudioUrl(activePage.background_audio_url || '');
    // Resolve blob URLs via hook for display (handles initial load and route changes)
    // Only call if this page wasn't already initialized via switchToPage function
    if (lastInitializedPageIdRef.current !== activePage.id) {
      lastInitializedPageIdRef.current = activePage.id;
      pageSwitch.switchToPage({
        id: activePage.id,
        background_image_url: activePage.background_image_url,
        background_video_url: activePage.background_video_url,
        background_audio_url: activePage.background_audio_url,
      });
    }
  }, [activePage, elementIdFromRoute, uiElementDefaultsByType, pageSwitch.switchToPage]);

  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,
      };

      // Transitions are now stored directly in navigation elements as transitionVideoUrl
      setSuccessMessage('Transition video can be set directly on navigation elements.');
      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,
  ]);

  /**
   * Save all dev content to stage environment.
   * This copies all pages, elements, transitions, and audio from dev to stage.
   */
  const saveToStage = useCallback(async () => {
    if (!projectId) {
      setErrorMessage('Project ID is required to save to stage.');
      return;
    }

    // First save current changes
    await saveConstructor();

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

      await axios.post('/publish/save-to-stage', { projectId });

      setSuccessMessage(
        'Successfully saved dev content to stage environment. All pages, elements, and transitions have been copied.',
      );
    } catch (error: any) {
      const message =
        error?.response?.data?.message ||
        error?.message ||
        'Failed to save to stage.';
      logger.error(
        'Failed to save to stage:',
        error instanceof Error ? error : { error },
      );
      setErrorMessage(message);
    } finally {
      setIsSavingToStage(false);
    }
  }, [projectId, saveConstructor]);

  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 || isReverseBuffering) {
          return;
        }
        if (element.navDisabled) {
          return;
        }
        const direction =
          element.navType === 'back' || element.type === 'navigation_prev'
            ? 'back'
            : 'forward';
        // Support both targetPageSlug (new) and targetPageId (legacy)
        const targetPageSlug = String(element.targetPageSlug || '').trim();
        const legacyTargetPageId = String(element.targetPageId || '').trim();

        // Resolve slug to page ID, or use legacy targetPageId
        const targetPage = targetPageSlug
          ? pages.find((p) => p.slug === targetPageSlug)
          : legacyTargetPageId
            ? pages.find((p) => p.id === legacyTargetPageId)
            : null;
        const targetPageId = targetPage?.id || '';

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

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

        if (!hasPlayableTransition) {
          setPendingNavigationPageId('');
          setTransitionPreview(null);
          // Use switchToPage which resolves blob URLs via usePageSwitch (reduces flash)
          switchToPage(targetPage).then(() => {
            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) {
        // Use img tag with flexible sizing - auto for dimensions not provided
        const imgStyle: React.CSSProperties = {
          width: element.width || 'auto',
          height: element.height || 'auto',
          objectFit: 'contain',
        };
        // eslint-disable-next-line @next/next/no-img-element
        return (
          <img
            src={resolveAssetPlaybackUrl(element.iconUrl)}
            alt='Navigation icon'
            style={imgStyle}
            draggable={false}
          />
        );
      }

      // Support both targetPageSlug (new) and targetPageId (legacy)
      const targetPageName = element.targetPageSlug
        ? pageNameBySlug[element.targetPageSlug]
        : 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 || '48px',
              fontFamily: element.descriptionTitleFontFamily || 'inherit',
              color: element.descriptionTitleColor || '#000000',
            }}
          >
            {element.descriptionTitle || 'TITLE'}
          </p>
          {element.descriptionText && (
            <p
              className='line-clamp-4'
              style={{
                fontSize: element.descriptionTextFontSize || '36px',
                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 = {};
  // Prefer hook's blob URLs (instant display) but fall back to resolved URLs for manual changes
  const backgroundImageSrc =
    pageSwitch.currentBgImageUrl || resolveAssetPlaybackUrl(backgroundImageUrl);
  const backgroundVideoSrc =
    pageSwitch.currentBgVideoUrl || resolveAssetPlaybackUrl(backgroundVideoUrl);
  const backgroundAudioSrc =
    pageSwitch.currentBgAudioUrl || 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';
  }

  return (
    <>
      <Head>
        <title>
          {getPageTitle(isElementEditMode ? 'Edit Element' : 'Constructor')}
        </title>
      </Head>
      <div className='relative w-screen h-screen bg-black 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) => {
                    const page = pages.find((p) => p.id === event.target.value);
                    if (page) switchToPage(page);
                  }}
                >
                  {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-black overflow-hidden'
          style={canvasBackgroundStyle}
        >
          {/* Background image - CSS backgroundImage on canvas provides instant display,
              NextImage enhances with optimized loading. bg-black prevents white flash. */}
          {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}
                onLoad={() => pageSwitch.markBackgroundReady()}
                onError={() => pageSwitch.markBackgroundReady()}
              />
            </div>
          ) : null}

          {/* Previous background overlay - shows during page switch until new bg is ready */}
          {pageSwitch.previousBgImageUrl &&
            pageSwitch.isSwitching &&
            !pageSwitch.isNewBgReady && (
              <div
                className='absolute inset-0 pointer-events-none z-10'
                style={{
                  backgroundImage: `url("${pageSwitch.previousBgImageUrl}")`,
                  backgroundSize: 'cover',
                  backgroundPosition: 'center',
                }}
              />
            )}

          {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 ||
                  isReverseBuffering);

              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.targetPageSlug || ''}
                          onChange={(event) =>
                            updateSelectedElement({
                              targetPageSlug: event.target.value,
                              // Clear legacy targetPageId when using slug
                              targetPageId: '',
                            })
                          }
                        >
                          <option value=''>Not selected</option>
                          {pages
                            .filter((page) => page.id !== activePageId)
                            .map((page, index) => (
                              <option key={page.id} value={page.slug}>
                                {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-green-700'
                  onClick={saveToStage}
                  disabled={isSavingToStage || isSaving}
                  title='Copy all dev content to stage environment'
                >
                  <BaseIcon path={mdiCloudUpload} size={16} />
                  <span>
                    {isSavingToStage ? 'Publishing...' : 'Save to Stage'}
                  </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: isReverseBuffering ? 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;
