import { mdiContentSave, mdiExitToApp, mdiPlus } from '@mdi/js';
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, {
  ReactElement,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { flushSync } from 'react-dom';
import BaseButton from '../components/BaseButton';
import CanvasBackground from '../components/Constructor/CanvasBackground';
import CanvasLoadingSpinner from '../components/CanvasLoadingSpinner';
import TransitionBlackOverlay from '../components/TransitionBlackOverlay';
import ConstructorToolbar from '../components/Constructor/ConstructorToolbar';
import TransitionPreviewOverlay from '../components/Constructor/TransitionPreviewOverlay';
import CanvasElementComponent from '../components/Constructor/CanvasElement';
import GalleryCarouselOverlay from '../components/UiElements/GalleryCarouselOverlay';
import ElementEditorPanel from '../components/Constructor/ElementEditorPanel';
import { BackdropPortalProvider } from '../components/BackdropPortal';
import { getPageTitle } from '../config';
import LayoutAuthenticated from '../layouts/Authenticated';
import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator';
import { usePageNavigationState } from '../hooks/usePageNavigationState';
import { usePageNavigation } from '../hooks/usePageNavigation';
import { useTransitionPlayback } from '../hooks/useTransitionPlayback';
import { useTransitionSettings } from '../hooks/useTransitionSettings';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { fetch as fetchGlobalTransitionDefaults } from '../stores/global_transition_defaults/globalTransitionDefaultsSlice';
import {
  fetchByProjectAndEnv as fetchProjectTransitionSettings,
  selectByProjectAndEnv as selectProjectTransitionSettings,
} from '../stores/project_transition_settings/projectTransitionSettingsSlice';
import type { ElementTransitionSettings } from '../types/transition';
import {
  entityToProjectSettings,
  extractElementTransitionSettings,
} from '../types/transition';
import { logger } from '../lib/logger';
import { isSafari } from '../lib/browserUtils';
import { resolveAssetPlaybackUrl } from '../lib/assetUrl';
import { downloadManager } from '../lib/offline/DownloadManager';
import { parseJsonObject } from '../lib/parseJson';
import {
  resolveNavigationTarget,
  hasPlayableTransition,
  getNavigationDirection,
  isBackNavigation,
} from '../lib/navigationHelpers';
import {
  mergeElementWithDefaults,
  createLocalId,
  normalizeAppearDelaySec,
  normalizeAppearDurationSec,
  ELEMENT_TYPE_LABELS,
  getNavigationButtonKind,
  isNavigationElementType,
  isTooltipElementType,
  isDescriptionElementType,
  isMediaElementType,
  isVideoPlayerElementType,
  clamp,
} from '../lib/elementDefaults';
import type {
  CanvasElementType,
  CanvasElement,
  ConstructorSchema,
  EditorMenuItem,
  GalleryCard,
  GalleryInfoSpan,
  CarouselSlide,
} from '../types/constructor';
import type { TourPage } from '../types/entities';

// Constructor-specific hooks
import {
  useCanvasElapsedTime,
  isElementVisibleAtTime,
} from '../hooks/useCanvasElapsedTime';
import {
  useMediaDurationProbe,
  buildDurationProbeTargets,
} from '../hooks/useMediaDurationProbe';
import { useIconPreload } from '../hooks/useIconPreload';
import { useOutsideClick } from '../hooks/useOutsideClick';
import { useDraggable } from '../hooks/useDraggable';
import { useCanvasElementDrag } from '../hooks/useCanvasElementDrag';
import { useTransitionPreview } from '../hooks/useTransitionPreview';
import { useConstructorPageActions } from '../hooks/useConstructorPageActions';
import { useConstructorElements } from '../hooks/useConstructorElements';
import { usePageBackground } from '../hooks/usePageBackground';
import { useConstructorData } from '../hooks/useConstructorData';
import { useAssetOptions } from '../hooks/useAssetOptions';
import { usePublishStatus } from '../hooks/usePublishStatus';
import {
  ConstructorProvider,
  type ConstructorContextValue,
  type NavigationElementType,
} from '../context/ConstructorContext';
import { useCanvasScale } from '../hooks/useCanvasScale';
import { useVideoSoundControl } from '../hooks/useVideoSoundControl';
import { useNetworkAware } from '../hooks/useNetworkAware';

// TourPage type is imported from '../types/entities'
// NavigationElementType is imported from '../context/ConstructorContext'

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

type ConstructorInteractionMode = 'edit' | 'interact';

// Use ELEMENT_TYPE_LABELS from elementDefaults for label lookup
const labelByType = ELEMENT_TYPE_LABELS;

const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
  const router = useRouter();
  const dispatch = useAppDispatch();
  const globalTransitionDefaults = useAppSelector(
    (state) => state.global_transition_defaults.data,
  );
  const canvasRef = useRef<HTMLDivElement>(null);
  const elementEditorRef = useRef<HTMLDivElement>(null);
  const toolbarRef = 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]);

  // Project transition settings from environment-aware store (constructor always uses 'dev')
  const projectTransitionSettingsEntity = useAppSelector((state) =>
    projectId
      ? selectProjectTransitionSettings(state, projectId, 'dev')
      : undefined,
  );
  const projectTransitionSettings = useMemo(
    () => entityToProjectSettings(projectTransitionSettingsEntity),
    [projectTransitionSettingsEntity],
  );

  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]);

  // Use React Query for data fetching (replaces manual loadData)
  const {
    project,
    pages,
    pageLinks,
    allPagesPreloadElements,
    assets,
    uiElementDefaultsByType,
    projectName,
    isLoading: isDataLoading,
    isError: isDataError,
    error: dataError,
    refetch: refetchData,
  } = useConstructorData({
    projectId,
    isAuthReady,
  });

  // Canvas scale for responsive UI elements and letterbox mode
  const { cssVars: canvasCssVars, letterboxStyles } = useCanvasScale({
    designWidth: project?.design_width,
    designHeight: project?.design_height,
  });

  // Page navigation with history tracking via shared hook
  const {
    currentPageId: activePageId,
    pageHistory,
    applyPageSelection,
    getNavigationContext,
    setCurrentPageId: setActivePageId,
  } = usePageNavigation({
    pages,
    trackHistory: true,
  });

  // Consolidated page background state (replaces 8 separate useState hooks)
  const {
    background: pageBackground,
    setBackground: setPageBackground,
    updateFromPage: updateBackgroundFromPage,
    setImageUrl: setBackgroundImageUrl,
    setVideoUrl: setBackgroundVideoUrl,
    setAudioUrl: setBackgroundAudioUrl,
    setVideoSettings: setBackgroundVideoSettings,
    // Legacy compatibility values for components that expect flat props
    backgroundImageUrl,
    backgroundVideoUrl,
    backgroundAudioUrl,
    backgroundVideoAutoplay,
    backgroundVideoLoop,
    backgroundVideoMuted,
    backgroundVideoStartTime,
    backgroundVideoEndTime,
  } = usePageBackground();

  // Sound control hook for iOS autoplay compatibility
  // Videos start muted (for iOS autoplay), can be controlled via page settings
  const soundControl = useVideoSoundControl({
    pageHasSound: backgroundVideoMuted === false, // Show button when page allows sound
    hasBackgroundVideo: Boolean(backgroundVideoUrl),
    videoUrl: backgroundVideoUrl, // Track video changes for page navigation reset
  });

  // Network-aware transitions: skip video on slow networks, use CSS fade instead
  const { shouldUseVideoTransitions, networkInfo } = useNetworkAware();

  // Fetch global transition defaults on mount
  useEffect(() => {
    dispatch(fetchGlobalTransitionDefaults());
  }, [dispatch]);

  // Fetch project transition settings for dev environment
  useEffect(() => {
    if (projectId) {
      dispatch(
        fetchProjectTransitionSettings({ projectId, environment: 'dev' }),
      );
    }
  }, [dispatch, projectId]);

  const [selectedMenuItem, setSelectedMenuItem] =
    useState<EditorMenuItem>('none');
  // Transition preview state managed by useTransitionPreview hook (below)

  // Combined loading state: initial auth check + data loading
  const [isInitializing, setIsInitializing] = useState(true);
  const isLoading = isInitializing || isDataLoading;
  // isSaving, isSavingToStage, isCreatingPage are managed by useConstructorPageActions hook
  const [newTransitionName, setNewTransitionName] = useState('');
  const [newTransitionVideoUrl, setNewTransitionVideoUrl] = useState('');
  const [newTransitionSupportsReverse, setNewTransitionSupportsReverse] =
    useState(true);
  const [errorMessage, setErrorMessage] = useState('');
  const [successMessage, setSuccessMessage] = useState('');

  // Auto-dismiss toast messages after 5 seconds
  useEffect(() => {
    if (!errorMessage) return;
    const timer = setTimeout(() => setErrorMessage(''), 5000);
    return () => clearTimeout(timer);
  }, [errorMessage]);

  useEffect(() => {
    if (!successMessage) return;
    const timer = setTimeout(() => setSuccessMessage(''), 5000);
    return () => clearTimeout(timer);
  }, [successMessage]);

  const [constructorInteractionMode, setConstructorInteractionMode] =
    useState<ConstructorInteractionMode>('edit');
  // Note: isMenuOpen kept for context backward compatibility, not used by ConstructorToolbar
  const [isMenuOpen, setIsMenuOpen] = useState(true);
  const [isEditorCollapsed, setIsEditorCollapsed] = useState(false);
  const [elementEditorTab, setElementEditorTab] = useState<
    'general' | 'css' | 'effects'
  >('general');
  const [activeGalleryCarousel, setActiveGalleryCarousel] = useState<{
    elementId: string;
    initialIndex: number;
  } | null>(null);
  // Current element transition settings (for CSS transitions when no video)
  const [
    currentElementTransitionSettings,
    setCurrentElementTransitionSettings,
  ] = useState<ElementTransitionSettings | null>(null);

  const isConstructorEditMode = constructorInteractionMode === 'edit';
  const allowedNavigationTypes = useMemo<NavigationElementType[]>(() => {
    return ['navigation_next', 'navigation_prev'];
  }, []);

  // Element CRUD operations via useConstructorElements hook
  const {
    elements,
    setElements,
    selectedElementId,
    selectedElement,
    selectElement,
    clearSelection,
    addElement,
    updateSelectedElement,
    removeSelectedElement,
    galleryCards,
    galleryInfoSpans,
    carouselSlides,
    updateElementPosition,
    normalizeNavigationType,
  } = useConstructorElements({
    initialElements: [],
    elementDefaultsByType: uiElementDefaultsByType,
    allowedNavigationTypes,
    initialSelectedElementId: elementIdFromRoute,
    onElementSelected: useCallback(() => {
      setSelectedMenuItem('none');
    }, []),
    onSelectionCleared: useCallback(() => {
      setSelectedMenuItem('none');
    }, []),
    onElementAdded: useCallback(() => {
      setSuccessMessage('Element added. Drag it to set position.');
      setErrorMessage('');
    }, []),
    onElementRemoved: useCallback(() => {
      setSuccessMessage('Element removed.');
    }, []),
  });

  // Check if any element has full-width carousel mode enabled
  const hasFullWidthCarousel = useMemo(
    () => elements.some((el) => el.carouselFullWidth === true),
    [elements],
  );

  // Look up current element for gallery carousel (so it receives updates from element editor)
  const activeGalleryCarouselElement = useMemo(() => {
    if (!activeGalleryCarousel) return null;
    return (
      elements.find((el) => el.id === activeGalleryCarousel.elementId) || null
    );
  }, [activeGalleryCarousel, elements]);

  // Draggable panels using useDraggable hook
  const { position: toolbarPosition, onDragStart: onToolbarDragStart } =
    useDraggable({
      initialPosition: { x: 20, y: 20 },
      elementWidth: 200, // Use collapsed width for bounds - expanded can go off-screen
      elementHeight: 56,
    });

  const { position: editorPosition, onDragStart: onElementEditorDragStart } =
    useDraggable({
      initialPosition: { x: 9999, y: 0 }, // Top-right corner (x will be clamped)
      elementWidth: isEditorCollapsed ? 220 : 300,
      elementHeight: 60,
    });

  const transitionVideoRef = useRef<HTMLVideoElement | null>(null);
  const lastInitializedPageIdRef = useRef<string | null>(null);
  const didSetInitialCanvasFocus = useRef(false);
  const selectedElementIdRef = useRef<string>('');
  selectedElementIdRef.current = selectedElementId;

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

  // Last project-level save: most recent updatedAt across all pages
  const lastProjectSaveAt = useMemo(() => {
    if (!pages.length) return null;
    return pages.reduce(
      (latest, page) => {
        if (!page.updatedAt) return latest;
        if (!latest) return page.updatedAt;
        return new Date(page.updatedAt) > new Date(latest)
          ? page.updatedAt
          : latest;
      },
      null as string | null,
    );
  }, [pages]);

  // Transition preview state management
  const {
    preview: transitionPreview,
    pendingPageId: pendingNavigationPageId,
    openPreview: openTransitionPreviewForElement,
    openPreviewWithTarget,
    closePreview: closeTransitionPreview,
  } = useTransitionPreview({
    isNavigationElementType,
    onError: setErrorMessage,
  });

  // Canvas elapsed time for element visibility timing
  const { elapsedSec: canvasElapsedSec } = useCanvasElapsedTime({
    pageId: activePageId,
    enabled: !isLoading,
  });

  // Element dragging with percentage positioning
  const { onElementDragStart, cancelDrag: cancelElementDrag } =
    useCanvasElementDrag({
      canvasRef,
      onPositionChange: updateElementPosition,
      enabled: constructorInteractionMode === 'edit',
    });

  // Preload orchestrator for better DX when previewing pages
  // Preloads current page + transition videos only
  // STREAM-FIRST: Constructor always uses online mode
  // Transition videos stream on-demand, then cache for replay
  const preloadOrchestrator = usePreloadOrchestrator({
    pages: pages.map((p) => ({
      id: p.id,
      background_image_url: p.background_image_url,
      background_video_url: p.background_video_url,
      background_audio_url: p.background_audio_url,
    })),
    pageLinks,
    elements: allPagesPreloadElements,
    currentPageId: activePageId,
    pageHistory,
    enabled: !isLoading && !!activePageId,
  });

  // Resolve transition settings using cascade: element → project → global
  const transitionSettings = useTransitionSettings({
    globalDefaults: globalTransitionDefaults,
    projectSettings: projectTransitionSettings ?? null,
    elementSettings: currentElementTransitionSettings,
  });

  // Unified page navigation state machine (replaces 6+ separate hooks)
  // Uses useReducer for atomic state transitions, preventing race conditions
  const navState = usePageNavigationState({
    preloadCache: preloadOrchestrator
      ? {
          getReadyBlobUrl: preloadOrchestrator.getReadyBlobUrl,
          getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,
          preloadedUrls: preloadOrchestrator.preloadedUrls,
        }
      : undefined,
    transitionSettings,
  });

  // Destructure for convenience (matches previous hook interfaces)
  // showElements/showSpinner are derived from the unified state machine phase:
  // - showElements: true when phase is 'idle' or 'fading_in'
  // - showSpinner: true when phase is 'preparing', 'loading_bg', or 'transition_done'
  const {
    currentImageUrl: navCurrentBgImageUrl,
    currentVideoUrl: navCurrentBgVideoUrl,
    currentAudioUrl: navCurrentBgAudioUrl,
    previousImageUrl: navPreviousBgImageUrl,
    previousVideoUrl: navPreviousBgVideoUrl,
    isSwitching: navIsSwitching,
    isNewBgReady: navIsNewBgReady,
    pendingTransitionComplete,
    isFadingIn,
    showElements: navShowElements,
    showSpinner: navShowSpinner,
    showTransitionVideo,
    transitionStyle,
    lastKnownBgUrl,
    onBackgroundReady: navOnBackgroundReady,
    onVideoBufferStateChange,
    onTransitionEnded,
    navigateToPage: navNavigateToPage,
    resetToIdle: navResetToIdle,
    startTransition,
  } = navState;

  // Reset navigation state when starting a new transition
  const resetFadeIn = useCallback(() => {
    navResetToIdle();
  }, [navResetToIdle]);

  // Helper to switch pages without flash
  // Uses unified navigation state machine for blob URL resolution
  // isBack parameter indicates this is a back navigation (pops history instead of pushing)
  const switchToPage = useCallback(
    async (page: TourPage | null, isBack = false) => {
      // Mark this page as initialized to prevent redundant effect calls
      if (page) {
        lastInitializedPageIdRef.current = page.id;
      }

      // Use unified navigation state machine for atomic state transitions
      await navNavigateToPage(
        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,
        {
          hasTransition: false, // No video transition for direct navigation
          isBack,
          onSwitched: () => {
            if (page) {
              // Use applyPageSelection for proper history management (pops on back)
              applyPageSelection(page.id, isBack);
            }
          },
        },
      );
    },
    [navNavigateToPage, applyPageSelection],
  );

  const { isBuffering: isTransitionBuffering, isVideoReady: isTransitionVideoReady } = useTransitionPlayback({
    videoRef: transitionVideoRef,
    transition: transitionPreview
      ? {
          videoUrl: resolveAssetPlaybackUrl(transitionPreview.videoUrl),
          storageKey: transitionPreview.storageKey,
          reverseMode: transitionPreview.reverseMode,
          reverseVideoUrl: transitionPreview.reverseVideoUrl
            ? resolveAssetPlaybackUrl(transitionPreview.reverseVideoUrl)
            : undefined,
          reverseStorageKey: transitionPreview.reverseStorageKey, // Raw path for cache lookup
          durationSec: transitionPreview.durationSec,
          targetPageId: pendingNavigationPageId || undefined,
          displayName: transitionPreview.title,
          isBack: transitionPreview.isBack, // Pass through for history management
        }
      : null,
    onComplete: async (targetPageId, isBack) => {
      // Resume background downloads now that transition is complete
      downloadManager.resumeAll();
      const video = transitionVideoRef.current;
      if (targetPageId) {
        const targetPage = pages.find((p) => p.id === targetPageId) || null;
        // Signal that transition video has ended
        // State machine transitions to 'transition_done', waiting for background
        onTransitionEnded();
        // DON'T close preview here - it stays visible until background is ready
        // The useEffect watching showTransitionVideo will close it
        // Navigate to target page - state machine handles ready state
        await switchToPage(targetPage, isBack ?? false);
        clearSelection();
        setSelectedMenuItem('none');
        setErrorMessage('');
      } else {
        video?.removeAttribute('src');
        video?.load();
        closeTransitionPreview();
        navResetToIdle();
      }
    },
    timeouts: {
      playbackStartMs: 3000,
      hardTimeoutMs: 45000,
    },
    features: {
      useBlobUrl: true,
      preDecodeImages: false, // We handle image loading via navigation state machine
    },
    preload: {
      preloadedUrls: preloadOrchestrator.preloadedUrls,
      getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,
      getReadyBlobUrl: preloadOrchestrator.getReadyBlobUrl,
      getReadyBlob: preloadOrchestrator.getReadyBlob,
    },
  });

  // Sync transition video buffering state with navigation state machine
  // This enables unified showSpinner logic in the state machine
  useEffect(() => {
    const isBuffering = Boolean(transitionPreview) && isTransitionBuffering;
    onVideoBufferStateChange(isBuffering);
  }, [transitionPreview, isTransitionBuffering, onVideoBufferStateChange]);

  // Clean up transition preview when state machine says video overlay should be hidden
  // showTransitionVideo is true during 'transitioning', 'transition_done', and 'fading_in' phases
  // During 'fading_in', the overlay fades out (isFadingOut=true), then removed when phase goes to 'idle'
  useEffect(() => {
    if (transitionPreview && !showTransitionVideo) {
      closeTransitionPreview();
    }
  }, [transitionPreview, showTransitionVideo, closeTransitionPreview]);

  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]);

  // Icon preloading for smooth rendering
  const { preloadedUrlMap: preloadedIconUrlMap } = useIconPreload({
    iconUrls: iconPreloadTargets,
    enabled: !isLoading,
  });

  // Use the useAssetOptions hook to derive memoized asset options
  const assetOptions = useAssetOptions({ assets });
  // Media duration probing with caching
  const durationProbeTargets = useMemo(
    () =>
      buildDurationProbeTargets({
        backgroundVideoUrl,
        backgroundAudioUrl,
        selectedElement,
        newTransitionVideoUrl,
        elements,
        isMediaElementType,
        isVideoPlayerElementType,
        isNavigationElementType,
      }),
    [
      backgroundAudioUrl,
      backgroundVideoUrl,
      elements,
      newTransitionVideoUrl,
      selectedElement,
    ],
  );

  const { getDuration, getDurationNote, durationBySource } =
    useMediaDurationProbe({
      targets: durationProbeTargets,
    });

  const backgroundVideoDurationNote = getDurationNote(backgroundVideoUrl);
  const backgroundAudioDurationNote = getDurationNote(backgroundAudioUrl);
  const selectedMediaDurationNote = useMemo(() => {
    if (!selectedElement || !isMediaElementType(selectedElement.type)) {
      return 'Duration: unknown';
    }
    return getDurationNote(selectedElement.mediaUrl || '');
  }, [getDurationNote, selectedElement]);

  const newTransitionDurationNote = getDurationNote(newTransitionVideoUrl);

  const selectedTransitionDurationNote = useMemo(() => {
    if (!selectedElement || !isNavigationElementType(selectedElement.type)) {
      return 'Duration: unknown';
    }
    return getDurationNote(selectedElement.transitionVideoUrl || '');
  }, [getDurationNote, selectedElement]);

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

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

        const resolvedDuration = getDuration(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;
    });
  }, [getDuration]);

  // Handle initial page selection when pages are loaded
  const prevPagesLengthRef = useRef(0);
  useEffect(() => {
    // Only set initial page when pages first load (not on every change)
    if (pages.length > 0 && prevPagesLengthRef.current === 0) {
      const defaultPageId = pageIdFromRoute || pages[0]?.id || '';
      // Use applyPageSelection to set initial page and history
      applyPageSelection(defaultPageId, false);
      setIsMenuOpen(false);
      setIsInitializing(false);
    }
    prevPagesLengthRef.current = pages.length;
  }, [pages, pageIdFromRoute, applyPageSelection]);

  // Handle empty project case - mark initialized when data loads with no pages
  // This allows the "Create First Page" button to show
  useEffect(() => {
    if (!isDataLoading && pages.length === 0) {
      setIsInitializing(false);
    }
  }, [isDataLoading, pages.length]);

  // Handle query errors
  useEffect(() => {
    if (isDataError && dataError) {
      const message = dataError.message || 'Failed to load constructor data.';
      logger.error('Failed to load constructor data:', dataError);
      setErrorMessage(message);
    }
  }, [isDataError, dataError]);

  // Refetch wrapper that preserves current page
  const handleReload = useCallback(async () => {
    const currentPageId = activePageId;
    await refetchData();
    // After refetch, restore the active page if it still exists
    // This is handled by the pages effect above
    if (currentPageId && pages.some((p) => p.id === currentPageId)) {
      setActivePageId(currentPageId);
    }
  }, [activePageId, pages, refetchData]);

  // Page actions (save, create page, save to stage)
  const {
    isSaving,
    isSavingToStage,
    isCreatingPage,
    isCreatingTransition,
    saveConstructor,
    saveToStage,
    createPage,
    createTransition,
  } = useConstructorPageActions({
    projectId,
    project,
    pages,
    activePage,
    activePageId,
    elements,
    pageBackground,
    onReload: handleReload,
    onSetActivePageId: setActivePageId,
    onSetMenuOpen: setIsMenuOpen,
    onError: setErrorMessage,
    onSuccess: setSuccessMessage,
  });

  // Publish status for timestamp display
  const { lastSavedToStage, refresh: refreshPublishStatus } = usePublishStatus({
    projectId,
  });

  // Wrap saveToStage to refresh publish status after save
  const handleSaveToStage = useCallback(async () => {
    await saveToStage();
    await refreshPublishStatus();
  }, [saveToStage, refreshPublishStatus]);

  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]);

  // React Query handles data fetching automatically based on projectId and isAuthReady

  // Panel initial positions are handled by useDraggable hooks

  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 (!activePage) {
      setElements([]);
      clearSelection();
      updateBackgroundFromPage(null);
      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: Partial<GalleryCard>) => ({
                    id: String(card?.id || createLocalId()),
                    imageUrl: String(card?.imageUrl ?? ''),
                    title: String(card?.title ?? ''),
                    description: String(card?.description ?? ''),
                  }))
                : undefined,
              galleryHeaderImageUrl:
                typeof item.galleryHeaderImageUrl === 'string'
                  ? item.galleryHeaderImageUrl
                  : undefined,
              galleryTitle:
                typeof item.galleryTitle === 'string'
                  ? item.galleryTitle
                  : undefined,
              galleryInfoSpans: Array.isArray(item.galleryInfoSpans)
                ? item.galleryInfoSpans.map(
                    (span: Partial<GalleryInfoSpan>) => ({
                      id: String(span?.id || createLocalId()),
                      text: String(span?.text ?? ''),
                      iconUrl: span?.iconUrl ? String(span.iconUrl) : undefined,
                    }),
                  )
                : undefined,
              galleryColumns:
                typeof item.galleryColumns === 'number'
                  ? item.galleryColumns
                  : undefined,
              carouselSlides: Array.isArray(item.carouselSlides)
                ? item.carouselSlides.map((slide: Partial<CarouselSlide>) => ({
                    id: String(slide?.id || createLocalId()),
                    imageUrl: String(slide?.imageUrl ?? ''),
                    caption: String(slide?.caption ?? ''),
                  }))
                : undefined,
              iconUrl: typeof item.iconUrl === 'string' ? item.iconUrl : '',
              carouselPrevIconUrl:
                typeof item.carouselPrevIconUrl === 'string'
                  ? item.carouselPrevIconUrl
                  : '',
              carouselNextIconUrl:
                typeof item.carouselNextIconUrl === 'string'
                  ? item.carouselNextIconUrl
                  : '',
              // Carousel button positions
              carouselPrevX:
                typeof item.carouselPrevX === 'number'
                  ? item.carouselPrevX
                  : undefined,
              carouselPrevY:
                typeof item.carouselPrevY === 'number'
                  ? item.carouselPrevY
                  : undefined,
              carouselNextX:
                typeof item.carouselNextX === 'number'
                  ? item.carouselNextX
                  : undefined,
              carouselNextY:
                typeof item.carouselNextY === 'number'
                  ? item.carouselNextY
                  : undefined,
              // Carousel button dimensions
              carouselPrevWidth:
                typeof item.carouselPrevWidth === 'string'
                  ? item.carouselPrevWidth
                  : undefined,
              carouselPrevHeight:
                typeof item.carouselPrevHeight === 'string'
                  ? item.carouselPrevHeight
                  : undefined,
              carouselNextWidth:
                typeof item.carouselNextWidth === 'string'
                  ? item.carouselNextWidth
                  : undefined,
              carouselNextHeight:
                typeof item.carouselNextHeight === 'string'
                  ? item.carouselNextHeight
                  : undefined,
              // Gallery Carousel Settings
              galleryCarouselPrevIconUrl:
                typeof item.galleryCarouselPrevIconUrl === 'string'
                  ? item.galleryCarouselPrevIconUrl
                  : '',
              galleryCarouselNextIconUrl:
                typeof item.galleryCarouselNextIconUrl === 'string'
                  ? item.galleryCarouselNextIconUrl
                  : '',
              galleryCarouselBackIconUrl:
                typeof item.galleryCarouselBackIconUrl === 'string'
                  ? item.galleryCarouselBackIconUrl
                  : '',
              galleryCarouselBackLabel:
                typeof item.galleryCarouselBackLabel === 'string'
                  ? item.galleryCarouselBackLabel
                  : '',
              galleryCarouselPrevX:
                typeof item.galleryCarouselPrevX === 'number'
                  ? item.galleryCarouselPrevX
                  : undefined,
              galleryCarouselPrevY:
                typeof item.galleryCarouselPrevY === 'number'
                  ? item.galleryCarouselPrevY
                  : undefined,
              galleryCarouselNextX:
                typeof item.galleryCarouselNextX === 'number'
                  ? item.galleryCarouselNextX
                  : undefined,
              galleryCarouselNextY:
                typeof item.galleryCarouselNextY === 'number'
                  ? item.galleryCarouselNextY
                  : undefined,
              galleryCarouselBackX:
                typeof item.galleryCarouselBackX === 'number'
                  ? item.galleryCarouselBackX
                  : undefined,
              galleryCarouselBackY:
                typeof item.galleryCarouselBackY === 'number'
                  ? item.galleryCarouselBackY
                  : undefined,
              galleryCarouselPrevWidth:
                typeof item.galleryCarouselPrevWidth === 'string'
                  ? item.galleryCarouselPrevWidth
                  : undefined,
              galleryCarouselPrevHeight:
                typeof item.galleryCarouselPrevHeight === 'string'
                  ? item.galleryCarouselPrevHeight
                  : undefined,
              galleryCarouselNextWidth:
                typeof item.galleryCarouselNextWidth === 'string'
                  ? item.galleryCarouselNextWidth
                  : undefined,
              galleryCarouselNextHeight:
                typeof item.galleryCarouselNextHeight === 'string'
                  ? item.galleryCarouselNextHeight
                  : undefined,
              galleryCarouselBackWidth:
                typeof item.galleryCarouselBackWidth === 'string'
                  ? item.galleryCarouselBackWidth
                  : undefined,
              galleryCarouselBackHeight:
                typeof item.galleryCarouselBackHeight === 'string'
                  ? item.galleryCarouselBackHeight
                  : undefined,
              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
                  : isVideoPlayerElementType(item.type),
            };

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

    setElements(normalizedElements);
    setSelectedMenuItem('none');

    const currentSelectedId = selectedElementIdRef.current;
    if (!normalizedElements.length) {
      clearSelection();
    } else if (
      elementIdFromRoute &&
      normalizedElements.some((element) => element.id === elementIdFromRoute)
    ) {
      selectElement(elementIdFromRoute);
    } else if (
      currentSelectedId &&
      !normalizedElements.some((element) => element.id === currentSelectedId)
    ) {
      // Current selection no longer valid
      clearSelection();
    }
    // If current selection is still valid, do nothing (keep current)
    // Update consolidated background state (replaces 8 separate setters)
    updateBackgroundFromPage(activePage);
  }, [
    activePage,
    elementIdFromRoute,
    uiElementDefaultsByType,
    clearSelection,
    selectElement,
    setElements,
    updateBackgroundFromPage,
  ]);

  // Separate effect for initial background loading (matches RuntimePresentation pattern)
  // This effect ONLY handles initial page load when backgrounds are empty.
  // switchToPage handles all subsequent navigation by calling navNavigateToPage directly.
  // Keeping this separate prevents race conditions where state updates trigger
  // this effect before activePageId has been updated via applyPageSelection.
  useEffect(() => {
    if (!activePage || lastInitializedPageIdRef.current === activePage.id)
      return;

    // Only initialize when backgrounds are EMPTY (initial load)
    if (!navCurrentBgImageUrl && !navCurrentBgVideoUrl) {
      lastInitializedPageIdRef.current = activePage.id;
      navNavigateToPage({
        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,
    navCurrentBgImageUrl,
    navCurrentBgVideoUrl,
    navNavigateToPage,
  ]);

  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 normalizeNavigationType(element, forcedType);
      });
      return hasChanges ? nextElements : prev;
    });
  }, [allowedNavigationTypes, normalizeNavigationType, setElements]);

  // Element drag is now handled by useCanvasElementDrag hook

  useEffect(() => {
    if (isConstructorEditMode) return;
    cancelElementDrag();
    clearSelection();
    setSelectedMenuItem('none');
  }, [isConstructorEditMode, cancelElementDrag, clearSelection]);

  // Outside click detection to clear element/menu selection
  // Ignore clicks on menu to allow menu item selection
  useOutsideClick({
    containerRef: elementEditorRef,
    ignoreRefs: [toolbarRef],
    ignoreDataAttribute: 'data-constructor-element-id',
    selectedValue: selectedElementId,
    onOutsideClick: useCallback(() => {
      clearSelection();
      setSelectedMenuItem('none');
    }, [clearSelection]),
    enabled:
      isConstructorEditMode &&
      (!!selectedElementId || selectedMenuItem !== 'none'),
  });

  // Thin wrappers for hook functions (handle additional state like selectedMenuItem)
  const selectElementForEdit = useCallback(
    (elementId: string) => {
      selectElement(elementId);
      // Note: setSelectedMenuItem('none') is handled by onElementSelected callback
    },
    [selectElement],
  );

  const selectMenuItemForEdit = useCallback(
    (item: EditorMenuItem) => {
      clearSelection();
      setSelectedMenuItem(item);
    },
    [clearSelection],
  );

  // createPage, saveConstructor, saveToStage are now provided by useConstructorPageActions hook

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

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

    // Select the element for editing
    selectElementForEdit(elementId);

    // Start drag with current position
    onElementDragStart(
      event,
      elementId,
      currentElement.xPercent,
      currentElement.yPercent,
    );
  };

  // openTransitionPreviewForElement is now provided by useTransitionPreview hook

  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 || isTransitionBuffering) {
          return;
        }
        if (element.navDisabled) {
          return;
        }

        // Cancel any pending fade to prevent stale fade state across navigations
        resetFadeIn();

        // Use shared navigation helpers
        const direction = getNavigationDirection(element);

        // Get navigation context from hook for history-based back navigation
        const navContext = getNavigationContext();

        // Pass history context for history-based back navigation
        const navTarget = resolveNavigationTarget(element, pages, navContext);

        if (!navTarget) {
          // Back buttons always use history, forward buttons use target page
          if (isBackNavigation(element)) {
            if (!navContext.previousPageId) {
              setErrorMessage(
                'No previous page in history. Navigate to another page first.',
              );
            } else {
              setErrorMessage(
                'Previous page not found. It may have been deleted.',
              );
            }
          } else {
            setErrorMessage(
              'No target page configured for this navigation button.',
            );
          }
          return;
        }

        // For back navigation, use transition from navTarget (the forward element that brought us here)
        // For forward navigation, use the element's own transition settings
        const transitionSource = isBackNavigation(element)
          ? {
              type: element.type,
              transitionVideoUrl: navTarget.transitionVideoUrl,
              transitionReverseMode: navTarget.transitionReverseMode,
              reverseVideoUrl: navTarget.reverseVideoUrl,
            }
          : element;

        // Extract element transition settings for CSS-based transitions
        // For back navigation, use navTarget's settings (the forward element that brought us here)
        // For forward navigation, use the clicked element's settings
        const elementTransitionSource = isBackNavigation(element)
          ? navTarget
          : element;
        const elementSettings = extractElementTransitionSettings(
          elementTransitionSource,
        );
        // Use flushSync to ensure state is updated synchronously before transition starts
        // Without this, React's async state batching causes the transition to use OLD settings
        flushSync(() => {
          setCurrentElementTransitionSettings(elementSettings);
        });

        // Note: Background ready state is reset atomically by navigateToPage/switchToPage

        // Check if transition can be played using shared helper
        const canPlayTransition = hasPlayableTransition(transitionSource, direction);

        // Check if video is already cached (use video even on slow network if cached)
        const transitionUrl = transitionSource.transitionVideoUrl;
        const isTransitionCached =
          transitionUrl && preloadOrchestrator.getReadyBlobUrl(transitionUrl);

        // Use video if: has playable transition AND (cached OR good network)
        const useVideoTransition =
          canPlayTransition && (isTransitionCached || shouldUseVideoTransitions);

        if (!useVideoTransition) {
          closeTransitionPreview();

          // Log when skipping video due to slow network
          if (canPlayTransition && !shouldUseVideoTransitions && transitionUrl) {
            logger.info(
              '[NAVIGATION] Skipping video transition due to slow network, downloading in background',
              {
                effectiveType: networkInfo.effectiveType,
                downlink: networkInfo.downlink,
                rtt: networkInfo.rtt,
              },
            );

            // Start background download of transition video for future use (low priority)
            downloadManager.addJob({
              assetId: `transition-bg-${transitionUrl}`,
              projectId: 'transition-preload',
              url: resolveAssetPlaybackUrl(transitionUrl),
              filename: transitionUrl.split('/').pop() || 'transition.mp4',
              variantType: 'original',
              assetType: 'video',
              priority: 10, // Low priority - background preload
              storageKey: transitionUrl,
            });
          }

          // Use switchToPage which resolves blob URLs via navigation state machine (reduces flash)
          // Pass isBack flag for proper history management
          switchToPage(navTarget.page, navTarget.isBack).then(() => {
            clearSelection();
            setSelectedMenuItem('none');
            setErrorMessage('');
          });
          return;
        }

        // Signal navigation state machine that video transition is starting
        // This sets phase to 'transitioning' so spinner shows during buffering
        logger.info('[TRANSITION-START] 🚀 Starting transition', {
          targetPageId: navTarget.pageId,
          direction,
          isBack: navTarget.isBack,
          transitionVideoUrl: transitionSource.transitionVideoUrl?.slice(-60),
          reverseVideoUrl: transitionSource.reverseVideoUrl?.slice(-60),
        });
        // Pause background downloads to give transition video exclusive bandwidth
        downloadManager.pauseAll();
        startTransition(navTarget.pageId, navTarget.isBack);
        openPreviewWithTarget(transitionSource, direction, navTarget.pageId);
      }
      return;
    }

    selectElementForEdit(element.id);
  };

  // Handler for gallery card clicks
  const handleGalleryCardClick = useCallback(
    (element: CanvasElement, cardIndex: number) => {
      if (element.galleryCards && element.galleryCards.length > 0) {
        setActiveGalleryCarousel({
          elementId: element.id,
          initialIndex: cardIndex,
        });
      }
    },
    [],
  );

  // Handler for gallery carousel button position changes (constructor only)
  const handleGalleryCarouselButtonPositionChange = useCallback(
    (button: 'prev' | 'next' | 'back', x: number, y: number) => {
      if (!activeGalleryCarousel) return;

      const positionPatch =
        button === 'prev'
          ? { galleryCarouselPrevX: x, galleryCarouselPrevY: y }
          : button === 'next'
            ? { galleryCarouselNextX: x, galleryCarouselNextY: y }
            : { galleryCarouselBackX: x, galleryCarouselBackY: y };

      // Update the element by explicit ID (not by selection)
      // because the gallery element may not be selected when the carousel is open
      setElements((prev) =>
        prev.map((el) =>
          el.id === activeGalleryCarousel.elementId
            ? { ...el, ...positionPatch }
            : el,
        ),
      );
      // No need to update activeGalleryCarousel - it stores only elementId
      // and the element lookup is done via activeGalleryCarouselElement useMemo
    },
    [activeGalleryCarousel, setElements],
  );

  // Handler for carousel element button position changes (constructor only)
  const handleCarouselButtonPositionChange = useCallback(
    (elementId: string, button: 'prev' | 'next', x: number, y: number) => {
      const positionPatch =
        button === 'prev'
          ? { carouselPrevX: x, carouselPrevY: y }
          : { carouselNextX: x, carouselNextY: y };

      setElements((prev) =>
        prev.map((el) =>
          el.id === elementId ? { ...el, ...positionPatch } : el,
        ),
      );
    },
    [setElements],
  );

  const isElementVisibleOnCanvas = (element: CanvasElement) =>
    isElementVisibleAtTime(
      canvasElapsedSec,
      element.appearDelaySec,
      element.appearDurationSec,
    );

  // Check if a single element's icon is ready (used for individual element visibility)
  const isElementIconReady = useCallback(
    (element: CanvasElement) => {
      const isPreloadableIconElement =
        (isNavigationElementType(element.type) ||
          isTooltipElementType(element.type) ||
          isDescriptionElementType(element.type)) &&
        Boolean(element.iconUrl);

      if (!isPreloadableIconElement) return true;

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

      return Boolean(preloadedIconUrlMap[playbackUrl]);
    },
    [preloadedIconUrlMap],
  );

  // Check if ALL element icons are ready - used to show all elements together
  // This prevents staggered element appearance where some elements show before others
  const areAllElementIconsReady = useMemo(() => {
    return elements.every((element) => isElementIconReady(element));
  }, [elements, isElementIconReady]);

  // URL resolver that uses preloaded blob URLs when available
  // Depends on readyUrlsVersion to re-render when blob URLs become ready after preload
  const resolveUrlWithBlob = useCallback(
    (url: string | undefined): string => {
      if (!url) return '';

      // Try to get blob URL from preload orchestrator (instant display)
      // Check storage key first (most reliable), then resolved URL
      const blobUrl =
        preloadOrchestrator.getReadyBlobUrl(url) ||
        preloadOrchestrator.getReadyBlobUrl(resolveAssetPlaybackUrl(url));
      if (blobUrl) return blobUrl;

      // Fall back to standard resolution
      return resolveAssetPlaybackUrl(url);
    },
    [preloadOrchestrator, preloadOrchestrator.readyUrlsVersion],
  );

  const canvasBackgroundStyle: React.CSSProperties = {};

  // Unified background URL resolution
  // Edit mode: use local storage paths (for immediate editing feedback)
  // Interact mode: use nav state values (blob URLs from preload cache)
  const backgroundImageSrc = useMemo(() => {
    if (isConstructorEditMode && backgroundImageUrl) {
      return resolveUrlWithBlob(backgroundImageUrl);
    }
    return navCurrentBgImageUrl;
  }, [
    isConstructorEditMode,
    backgroundImageUrl,
    navCurrentBgImageUrl,
    resolveUrlWithBlob,
  ]);

  const backgroundVideoSrc = useMemo(() => {
    if (isConstructorEditMode && backgroundVideoUrl) {
      return resolveUrlWithBlob(backgroundVideoUrl);
    }
    return navCurrentBgVideoUrl;
  }, [
    isConstructorEditMode,
    backgroundVideoUrl,
    navCurrentBgVideoUrl,
    resolveUrlWithBlob,
  ]);

  const backgroundAudioSrc = useMemo(() => {
    if (isConstructorEditMode && backgroundAudioUrl) {
      return resolveUrlWithBlob(backgroundAudioUrl);
    }
    return navCurrentBgAudioUrl;
  }, [
    isConstructorEditMode,
    backgroundAudioUrl,
    navCurrentBgAudioUrl,
    resolveUrlWithBlob,
  ]);

  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';

  // Background image is rendered by CanvasBackground component (same as runtime)
  // No CSS background-image needed on canvas div

  // Duration notes for UI display
  const durationNotes = useMemo(
    () => ({
      backgroundVideo: backgroundVideoDurationNote,
      backgroundAudio: backgroundAudioDurationNote,
      selectedMedia: selectedMediaDurationNote,
      selectedTransition: selectedTransitionDurationNote,
      newTransition: newTransitionDurationNote,
    }),
    [
      backgroundVideoDurationNote,
      backgroundAudioDurationNote,
      selectedMediaDurationNote,
      selectedTransitionDurationNote,
      newTransitionDurationNote,
    ],
  );

  // Transition creation state for context
  const transitionCreationState = useMemo(
    () => ({
      name: newTransitionName,
      videoUrl: newTransitionVideoUrl,
      supportsReverse: newTransitionSupportsReverse,
      isCreating: isCreatingTransition,
      setName: setNewTransitionName,
      setVideoUrl: setNewTransitionVideoUrl,
      setSupportsReverse: setNewTransitionSupportsReverse,
      create: () =>
        createTransition({
          name: newTransitionName,
          videoUrl: newTransitionVideoUrl,
          supportsReverse: newTransitionSupportsReverse,
          durationSec: getDuration(newTransitionVideoUrl),
        }),
    }),
    [
      newTransitionName,
      newTransitionVideoUrl,
      newTransitionSupportsReverse,
      isCreatingTransition,
      createTransition,
      getDuration,
    ],
  );

  // Build context value for ConstructorProvider
  // This allows child components to access constructor state without prop drilling
  const constructorContextValue: ConstructorContextValue = useMemo(
    () => ({
      // Project state
      projectId,

      // Page state
      pages,
      activePageId,
      activePage,
      setActivePageId,

      // Background state
      pageBackground,
      setPageBackground,
      updateBackgroundFromPage,

      // Background convenience setters
      setBackgroundImageUrl,
      setBackgroundVideoUrl,
      setBackgroundAudioUrl,
      setBackgroundVideoSettings,

      // Element state
      elements,
      setElements,
      selectedElementId,
      selectedElement,
      selectElement,
      clearSelection,
      updateElement: (id: string, patch: Partial<CanvasElement>) => {
        setElements((prev) =>
          prev.map((el) => (el.id === id ? { ...el, ...patch } : el)),
        );
      },
      removeElement: (id: string) => {
        setElements((prev) => prev.filter((el) => el.id !== id));
        if (selectedElementId === id) {
          clearSelection();
        }
      },
      updateSelectedElement,
      removeSelectedElement,

      // Menu state
      selectedMenuItem,
      setSelectedMenuItem,
      isMenuOpen,
      setIsMenuOpen,

      // Editor state
      elementEditorTab,
      setElementEditorTab,

      // Assets
      assets,
      isLoadingAssets: isDataLoading,

      // Asset options (derived from assets)
      assetOptions,

      // Gallery/Carousel operations
      galleryCards,
      galleryInfoSpans,
      carouselSlides,

      // Duration resolver
      getDuration,

      // Duration notes
      durationNotes,

      // Transition preview
      onPreviewTransition: openTransitionPreview,

      // Transition creation
      transitionCreation: transitionCreationState,

      // Navigation settings
      allowedNavigationTypes,
      normalizeNavigationType,

      // Actions
      save: saveConstructor,
      isSaving,
    }),
    [
      projectId,
      pages,
      activePageId,
      activePage,
      pageBackground,
      setPageBackground,
      updateBackgroundFromPage,
      setBackgroundImageUrl,
      setBackgroundVideoUrl,
      setBackgroundAudioUrl,
      setBackgroundVideoSettings,
      elements,
      setElements,
      selectedElementId,
      selectedElement,
      selectElement,
      clearSelection,
      updateSelectedElement,
      removeSelectedElement,
      selectedMenuItem,
      isMenuOpen,
      elementEditorTab,
      assets,
      isDataLoading,
      assetOptions,
      galleryCards,
      galleryInfoSpans,
      carouselSlides,
      getDuration,
      durationNotes,
      openTransitionPreview,
      transitionCreationState,
      allowedNavigationTypes,
      normalizeNavigationType,
      saveConstructor,
      isSaving,
    ],
  );

  return (
    <ConstructorProvider value={constructorContextValue}>
      <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-[1000] 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 && (
          <ConstructorToolbar
            ref={toolbarRef}
            position={toolbarPosition}
            onDragStart={onToolbarDragStart}
            pages={pages}
            activePageId={activePageId}
            onPageChange={(pageId) => {
              const page = pages.find((p) => p.id === pageId);
              if (page) switchToPage(page);
            }}
            interactionMode={constructorInteractionMode}
            onModeChange={setConstructorInteractionMode}
            onSelectMenuItem={selectMenuItemForEdit}
            allowedNavigationTypes={allowedNavigationTypes}
            onAddElement={addElement}
            onCreatePage={createPage}
            isCreatingPage={isCreatingPage}
            onSave={saveConstructor}
            onSaveToStage={handleSaveToStage}
            isSaving={isSaving}
            isSavingToStage={isSavingToStage}
            lastSavedAt={lastProjectSaveAt}
            lastSavedToStageAt={lastSavedToStage}
            projectId={projectId}
            onExit={() =>
              router.push(
                projectId
                  ? `/projects/${projectId}`
                  : '/projects/projects-list',
              )
            }
          />
        )}

        {/* Canvas container: z-[46] creates stacking context above carousel (z-10 bg, z-45 controls) portaled to body */}
        <div
          ref={canvasRef}
          tabIndex={-1}
          className={`relative z-[46] overflow-clip ${hasFullWidthCarousel ? 'bg-transparent' : 'bg-black'}`}
          style={{
            ...canvasCssVars,
            ...letterboxStyles,
            ...canvasBackgroundStyle,
          }}
        >
          <BackdropPortalProvider>
            {/* Safari Black Flash Prevention (video transitions only):
                Persistent snapshot layer shown ONLY during video transitions.
                z-[1] keeps it behind backgrounds (z-5) but above the black container. */}
            {lastKnownBgUrl &&
              isSafari() &&
              (transitionPreview || pendingTransitionComplete) && (
                <div
                  className='absolute inset-0 z-[1] pointer-events-none'
                  style={{
                    backgroundImage: `url("${lastKnownBgUrl}")`,
                    backgroundSize: 'contain',
                    backgroundPosition: 'center',
                    backgroundRepeat: 'no-repeat',
                  }}
                />
              )}

            {/* Background wrapper - z-5 keeps it BELOW carousel slide (z-10).
                Previous background overlay shows during loading.
                Black overlay for fade effect is rendered separately at z-[100]. */}
            <div className='absolute inset-0 z-5'>
              <CanvasBackground
                backgroundImageUrl={backgroundImageSrc}
                backgroundVideoUrl={backgroundVideoSrc}
                backgroundAudioUrl={backgroundAudioSrc}
                previousBgImageUrl={navPreviousBgImageUrl}
                previousBgVideoUrl={navPreviousBgVideoUrl}
                isSwitching={navIsSwitching}
                isNewBgReady={navIsNewBgReady}
                onBackgroundReady={navOnBackgroundReady}
                onVideoBufferStateChange={onVideoBufferStateChange}
                videoAutoplay={backgroundVideoAutoplay}
                videoLoop={backgroundVideoLoop}
                videoMuted={soundControl.isMuted}
                videoStartTime={backgroundVideoStartTime}
                videoEndTime={backgroundVideoEndTime}
                videoStoragePath={
                  backgroundVideoUrl || activePage?.background_video_url
                }
                pauseVideo={
                  Boolean(transitionPreview) ||
                  pendingTransitionComplete ||
                  navIsSwitching
                }
              />
            </div>

            {/* Page loading spinner - from unified navigation state machine.
                navShowSpinner is true when:
                - Phase is 'preparing', 'loading_bg', 'transition_done', OR
                - Video transition is active but buffering
                Additionally for constructor: show while element icons are loading.
                Only show in Interact mode - Edit mode doesn't need loading indicators.
                Skip when video transition overlay is active - it has its own spinner. */}
            {!isConstructorEditMode &&
              !transitionPreview &&
              (navShowSpinner ||
                (navShowElements && !areAllElementIconsReady)) && (
                <CanvasLoadingSpinner isVisible={true} zIndex={100} />
              )}

            {/* Elements container - z-[46] keeps it ABOVE carousel slide (z-10) AND carousel controls (z-45).
                UI controls (z-50) remain on top.
                No fade animation - elements switch instantly behind the black overlay.
                Shows when phase is 'idle' or 'fading_in' (navShowElements) AND all element icons are preloaded.
                This ensures all elements appear together (no staggered appearance).
                Exception: Edit mode always shows elements for editing (no icon preload wait). */}
            {(isConstructorEditMode ||
              (navShowElements && areAllElementIconsReady)) && (
              <div className='absolute inset-0 z-[46]'>
                {!isLoading && 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) => {
                    // Icon preloading is now handled at container level (areAllElementIconsReady)
                    // Here we only check visibility based on appear delay/duration timing
                    const shouldRender =
                      selectedElementId === element.id ||
                      isElementVisibleOnCanvas(element);
                    if (!shouldRender) return null;

                    return (
                      <CanvasElementComponent
                        key={element.id}
                        element={element}
                        isSelected={selectedElementId === element.id}
                        isEditMode={isConstructorEditMode}
                        onClick={() => onCanvasElementClick(element)}
                        onMouseDown={(event) =>
                          onElementMouseDown(event, element.id)
                        }
                        resolveUrl={resolveUrlWithBlob}
                        onGalleryCardClick={(cardIndex) =>
                          handleGalleryCardClick(element, cardIndex)
                        }
                        onCarouselButtonPositionChange={(button, x, y) =>
                          handleCarouselButtonPositionChange(
                            element.id,
                            button,
                            x,
                            y,
                          )
                        }
                        letterboxStyles={letterboxStyles}
                        pageTransitionSettings={transitionSettings}
                        preloadCache={{
                          getReadyBlob: preloadOrchestrator.getReadyBlob,
                          getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,
                        }}
                      />
                    );
                  })
                )}
              </div>
            )}

            {/* Overlay for fade-through-color transition - z-[100] is ABOVE elements (z-[46]).
                This covers the elements during page transition to hide the instant switch.
                Only rendered for 'fade' type. */}
            <TransitionBlackOverlay
              isFadingIn={isFadingIn}
              transitionType={transitionSettings.type}
              transitionStyle={transitionStyle}
              overlayColor={transitionSettings.overlayColor}
            />
          </BackdropPortalProvider>
        </div>

        {/* ElementEditorPanel now uses ConstructorContext for all state */}
        {pages.length > 0 && hasEditorSelection && (
          <ElementEditorPanel
            elementEditorRef={elementEditorRef}
            position={editorPosition}
            isCollapsed={isEditorCollapsed}
            onToggleCollapse={() => setIsEditorCollapsed((prev) => !prev)}
            onDragStart={onElementEditorDragStart}
            title={editorTitle}
          />
        )}
      </div>

      <TransitionPreviewOverlay
        videoKey={transitionPreview?.videoUrl}
        videoRef={transitionVideoRef}
        isActive={Boolean(transitionPreview) && showTransitionVideo}
        isBuffering={isTransitionBuffering}
        isVideoReady={isTransitionVideoReady}
        showSpinner={true}
        letterboxStyles={letterboxStyles}
        isFadingOut={isFadingIn}
        fadeOutDuration={transitionSettings.durationMs}
      />

      {/* Gallery Carousel Overlay */}
      {activeGalleryCarousel && activeGalleryCarouselElement && (
        <GalleryCarouselOverlay
          cards={activeGalleryCarouselElement.galleryCards || []}
          initialIndex={activeGalleryCarousel.initialIndex}
          onClose={() => setActiveGalleryCarousel(null)}
          resolveUrl={resolveUrlWithBlob}
          prevIconUrl={activeGalleryCarouselElement.galleryCarouselPrevIconUrl}
          nextIconUrl={activeGalleryCarouselElement.galleryCarouselNextIconUrl}
          backIconUrl={activeGalleryCarouselElement.galleryCarouselBackIconUrl}
          backLabel={
            activeGalleryCarouselElement.galleryCarouselBackLabel || 'BACK'
          }
          prevX={activeGalleryCarouselElement.galleryCarouselPrevX}
          prevY={activeGalleryCarouselElement.galleryCarouselPrevY}
          nextX={activeGalleryCarouselElement.galleryCarouselNextX}
          nextY={activeGalleryCarouselElement.galleryCarouselNextY}
          backX={activeGalleryCarouselElement.galleryCarouselBackX}
          backY={activeGalleryCarouselElement.galleryCarouselBackY}
          prevWidth={activeGalleryCarouselElement.galleryCarouselPrevWidth}
          prevHeight={activeGalleryCarouselElement.galleryCarouselPrevHeight}
          nextWidth={activeGalleryCarouselElement.galleryCarouselNextWidth}
          nextHeight={activeGalleryCarouselElement.galleryCarouselNextHeight}
          backWidth={activeGalleryCarouselElement.galleryCarouselBackWidth}
          backHeight={activeGalleryCarouselElement.galleryCarouselBackHeight}
          letterboxStyles={letterboxStyles}
          isEditMode={isConstructorEditMode}
          onButtonPositionChange={handleGalleryCarouselButtonPositionChange}
          pageTransitionSettings={transitionSettings}
          galleryElement={activeGalleryCarouselElement}
        />
      )}

      <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>
    </ConstructorProvider>
  );
};

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

export default ConstructorPage;
