/**
 * RuntimePresentation Component
 *
 * Renders a presentation for a specific project and environment.
 * Used by /p/[projectSlug] and /p/[projectSlug]/stage routes.
 */

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 { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import CardBox from './CardBox';
import RuntimeControls from './Runtime/RuntimeControls';
import RuntimeElement from './RuntimeElement';
import TransitionPreviewOverlay from './Constructor/TransitionPreviewOverlay';
import GalleryCarouselOverlay from './UiElements/GalleryCarouselOverlay';
import InfoPanelOverlay from './UiElements/InfoPanelOverlay';
import ImageDetailPanel from './UiElements/ImageDetailPanel';
import { BackdropPortalProvider } from './BackdropPortal';
import { RotatePrompt } from './RotatePrompt';
import CanvasBackground from './Constructor/CanvasBackground';
import CanvasLoadingSpinner from './CanvasLoadingSpinner';
import TransitionBlackOverlay from './TransitionBlackOverlay';
import { useCanvasScale } from '../hooks/useCanvasScale';
import { CANVAS_CONFIG } from '../config/canvas.config';
import LayoutGuest from '../layouts/Guest';
import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator';
import { useVideoSoundControl } from '../hooks/useVideoSoundControl';
import { usePageDataLoader } from '../hooks/usePageDataLoader';
import { useProjectAssets } from '../hooks/useProjectAssets';
import { usePageNavigation } from '../hooks/usePageNavigation';
import {
  extractPageLinksOnly,
  extractElementsForPages,
} from '../lib/extractPageLinks';
import { usePageNavigationState } from '../hooks/usePageNavigationState';
import { useTransitionPlayback } from '../hooks/useTransitionPlayback';
import { useNetworkAware } from '../hooks/useNetworkAware';
import { resolveAssetPlaybackUrl } from '../lib/assetUrl';
import { presentationHasAudio } from '../lib/presentationAudio';
import { downloadManager } from '../lib/offline/DownloadManager';
import { isSafari } from '../lib/browserUtils';
import { logger } from '../lib/logger';
import { backgroundAudioController } from '../lib/backgroundAudioController';
import {
  resolveNavigationTarget,
  isTransitionBlocking,
  isBackNavigation,
  isNavigationType,
} from '../lib/navigationHelpers';
import { useTransitionSettings } from '../hooks/useTransitionSettings';
import { useAppSelector, useAppDispatch } from '../stores/hooks';
import { logoutUser } from '../stores/authSlice';
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 { TransitionPhase } from '../types/presentation';
import type {
  CanvasElement,
  GalleryCarouselMediaItem,
  InfoPanelImage,
} from '../types/constructor';
import {
  isElementFlagEnabled,
  isInfoPanelElementType,
} from '../lib/elementDefaults';
import type { ElementTransitionSettings } from '../types/transition';
import {
  entityToProjectSettings,
  extractElementTransitionSettings,
} from '../types/transition';

interface RuntimePresentationProps {
  projectSlug: string;
  environment: 'stage' | 'production';
}

export default function RuntimePresentation({
  projectSlug,
  environment,
}: RuntimePresentationProps) {
  const dispatch = useAppDispatch();
  const router = useRouter();
  const globalTransitionDefaults = useAppSelector(
    (state) => state.global_transition_defaults.data,
  );

  // Use shared hook for loading project and pages data
  // Note: We can't fetch project transition settings until we have the project ID
  const runtimeApiHeaders = useMemo(
    () => ({
      'X-Runtime-Project-Slug': projectSlug,
      'X-Runtime-Environment': environment,
    }),
    [environment, projectSlug],
  );

  const { project, pages, isLoading, error, errorStatus, initialPageId } =
    usePageDataLoader({
      projectSlug,
      environment,
      apiHeaders: runtimeApiHeaders,
    });

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

  // Fetch project transition settings when project is loaded
  useEffect(() => {
    if (project?.id) {
      dispatch(
        fetchProjectTransitionSettings({
          projectId: project.id,
          environment,
          apiHeaders: runtimeApiHeaders,
        }),
      );
    }
  }, [dispatch, project?.id, environment, runtimeApiHeaders]);

  useEffect(() => {
    if (
      environment !== 'production' ||
      (errorStatus !== 401 && errorStatus !== 403)
    ) {
      return;
    }

    if (errorStatus === 403) {
      dispatch(logoutUser());
    }

    const next = `/p/${projectSlug}`;
    router.replace(`/login?next=${encodeURIComponent(next)}`);
  }, [dispatch, environment, errorStatus, projectSlug, router]);

  // Select project transition settings from store (environment-aware)
  const projectTransitionSettingsEntity = useAppSelector((state) =>
    project?.id
      ? selectProjectTransitionSettings(state, project.id, environment)
      : undefined,
  );
  const projectTransitionSettings = useMemo(
    () => entityToProjectSettings(projectTransitionSettingsEntity),
    [projectTransitionSettingsEntity],
  );

  // Track current element's transition settings (set when navigation is triggered)
  const [
    currentElementTransitionSettings,
    setCurrentElementTransitionSettings,
  ] = useState<ElementTransitionSettings | null>(null);

  // Resolve transition settings using the cascade: Element → Project → Global
  const transitionSettings = useTransitionSettings({
    globalDefaults: globalTransitionDefaults,
    projectSettings: projectTransitionSettings,
    elementSettings: currentElementTransitionSettings,
  });

  // Resolve project assets (favicon, og_image, logo) to presigned URLs
  const { faviconUrl, ogImageUrl } = useProjectAssets(project);

  // Page navigation with history tracking via shared hook
  const {
    currentPageId: selectedPageId,
    pageHistory,
    applyPageSelection,
    getNavigationContext,
  } = usePageNavigation({
    pages,
    defaultPageId: initialPageId || undefined,
    trackHistory: true,
  });

  // Get current page for design dimensions (presentations use page dimensions, not project)
  const currentPage = pages.find((p) => p.id === selectedPageId);

  // Canvas scale for responsive UI elements and letterbox mode
  // Uses page's design dimensions (saved at constructor save time) for presentation isolation
  const {
    cssVars,
    letterboxStyles,
    isPortrait,
    showRotatePrompt,
    canvasWidth,
    canvasHeight,
  } = useCanvasScale({
    designWidth: currentPage?.design_width ?? undefined,
    designHeight: currentPage?.design_height ?? undefined,
  });

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

  const [transitionPreview, setTransitionPreview] = useState<{
    targetPageId: string;
    videoUrl: string;
    storageKey: string;
    isBack: boolean;
    reverseVideoUrl?: string;
    reverseStorageKey?: string;
  } | null>(null);
  const [isFullscreen, setIsFullscreen] = useState(false);
  const [activeGalleryCarousel, setActiveGalleryCarousel] = useState<{
    element: CanvasElement;
    initialIndex: number;
  } | null>(null);
  const [activeInfoPanel, setActiveInfoPanel] = useState<CanvasElement | null>(
    null,
  );
  const [activeDetailImage, setActiveDetailImage] =
    useState<InfoPanelImage | null>(null);
  const [activeInfoPanelGallery, setActiveInfoPanelGallery] = useState<{
    items: GalleryCarouselMediaItem[];
    initialIndex: number;
  } | null>(null);
  // Track selected image in media section (runtime-only local state)
  const [runtimeSelectedImageId, setRuntimeSelectedImageId] = useState<
    string | null
  >(null);

  const transitionVideoRef = useRef<HTMLVideoElement>(null);
  const lastInitializedPageIdRef = useRef<string | null>(null);

  // Note: Initial page selection is handled by usePageNavigation hook via defaultPageId

  // Phase 1: Extract pageLinks from ALL pages (needed for navigation graph)
  // This is lightweight - only extracts navigation structure, not asset URLs
  const pageLinks = useMemo(() => {
    const links = extractPageLinksOnly(pages);
    if (links.length > 0) {
      logger.info('[PRELOAD] Extracted page links', {
        count: links.length,
        links: links.map((link) => ({
          from: link.from_pageId?.slice(-8),
          to: link.to_pageId?.slice(-8),
          hasTransition: !!link.transition?.video_url,
        })),
      });
    }
    return links;
  }, [pages]);

  // Phase 2: Extract elements only for current + neighbor pages (progressive)
  // This avoids parsing ui_schema_json for all pages upfront
  const preloadElements = useMemo(() => {
    if (!selectedPageId || pages.length === 0) return [];

    // Build simple neighbor set from pageLinks
    const neighborIds = new Set<string>();
    neighborIds.add(selectedPageId); // Current page
    pageLinks.forEach((link) => {
      if (link.from_pageId === selectedPageId && link.to_pageId) {
        neighborIds.add(link.to_pageId); // Direct neighbors
      }
    });

    // Extract elements only for current + neighbors
    const elements = extractElementsForPages(pages, Array.from(neighborIds));
    logger.info('[PRELOAD] Extracted elements for pages', {
      currentPage: selectedPageId.slice(-8),
      pageCount: neighborIds.size,
      elementCount: elements.length,
    });
    return elements;
  }, [pages, pageLinks, selectedPageId]);

  // Initialize preload orchestrator with transformed data
  // STREAM-FIRST: Preloads current page + transition videos only
  // Online: Videos stream on-demand, cache after playback (no bandwidth competition)
  // Offline: Assets already fully downloaded via useOfflineMode.startDownload()
  const preloadOrchestrator = usePreloadOrchestrator({
    pages,
    pageLinks,
    elements: preloadElements,
    currentPageId: selectedPageId,
    pageHistory,
    enabled: !isLoading && !error,
  });

  // Selected page - moved early for easier access
  const selectedPage = useMemo(
    () => pages.find((p) => p.id === selectedPageId) || null,
    [pages, selectedPageId],
  );

  // 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,
    currentEmbedUrl: navCurrentBgEmbedUrl,
    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,
    setBackgroundDirectly: navSetBackgroundDirectly,
    resetToIdle: navResetToIdle,
    startTransition,
  } = navState;

  // Integrate useTransitionPlayback hook for smooth transitions (matches Constructor pattern)
  const {
    isBuffering,
    isVideoReady,
    phase: transitionPhase,
  } = useTransitionPlayback({
    videoRef: transitionVideoRef,
    transition: transitionPreview
      ? {
          videoUrl: transitionPreview.videoUrl,
          storageKey: transitionPreview.storageKey,
          reverseMode: transitionPreview.reverseVideoUrl ? 'separate' : 'none',
          reverseVideoUrl: transitionPreview.reverseVideoUrl,
          reverseStorageKey: transitionPreview.reverseStorageKey, // Raw path for cache lookup
          targetPageId: transitionPreview.targetPageId,
          displayName: 'Transition',
          isBack: transitionPreview.isBack,
        }
      : null,
    onComplete: async (targetPageId, isBack) => {
      // Resume background downloads now that transition is complete
      downloadManager.resumeAll();
      if (targetPageId) {
        const targetPage = pages.find((p) => p.id === targetPageId);
        // Mark this page as initialized to prevent redundant effect calls
        lastInitializedPageIdRef.current = targetPageId;
        // Signal that transition video has ended
        // State machine transitions to 'transition_done', waiting for background
        onTransitionEnded();
        // DON'T close transitionPreview here - it stays visible until background is ready
        // The useEffect below will close it when pendingTransitionComplete becomes false
        // Navigate to target page - state machine handles ready state
        await navNavigateToPage(targetPage, {
          hasTransition: false, // Already played
          isBack: isBack ?? false,
          onSwitched: () => {
            applyPageSelection(targetPageId, isBack ?? false);
          },
        });
      } else {
        // No target page - clean up and remove overlay
        const video = transitionVideoRef.current;
        video?.removeAttribute('src');
        video?.load();
        setTransitionPreview(null);
        navResetToIdle();
      }
    },
    features: {
      useBlobUrl: true,
      // Don't pre-decode images in the hook - we handle it via overlay:
      // Overlay shows last transition frame while new page background loads behind it
      preDecodeImages: false,
    },
    preload: {
      preloadedUrls: preloadOrchestrator?.preloadedUrls || new Set(),
      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 isTransitionBuffering = Boolean(transitionPreview) && isBuffering;
    onVideoBufferStateChange(isTransitionBuffering);
  }, [transitionPreview, isBuffering, 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) {
      setTransitionPreview(null);
    }
  }, [transitionPreview, showTransitionVideo]);

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

  // Handle first user interaction for background audio unlock
  // CRITICAL: Use touchEnd, NOT touchStart - iOS Safari only unlocks audio
  // when finger is LIFTED from screen (touchend), not when touched (touchstart)
  const handleCanvasInteraction = useCallback(() => {
    backgroundAudioController.notifyUserInteraction();
  }, []);

  const toggleFullscreen = useCallback(async () => {
    try {
      if (!document.fullscreenElement) {
        await document.documentElement.requestFullscreen();
        setIsFullscreen(true);
      } else {
        await document.exitFullscreen();
        setIsFullscreen(false);
      }
    } catch (err) {
      logger.error(
        'Fullscreen toggle failed:',
        err instanceof Error ? err : { error: err },
      );
    }
  }, []);

  useEffect(() => {
    const handleFullscreenChange = () => {
      setIsFullscreen(Boolean(document.fullscreenElement));
    };

    document.addEventListener('fullscreenchange', handleFullscreenChange);
    return () =>
      document.removeEventListener('fullscreenchange', handleFullscreenChange);
  }, []);

  const pageElements = useMemo(() => {
    if (!selectedPage) return [];

    try {
      const uiSchema =
        typeof selectedPage.ui_schema_json === 'string'
          ? JSON.parse(selectedPage.ui_schema_json)
          : selectedPage.ui_schema_json;

      return Array.isArray(uiSchema?.elements) ? uiSchema.elements : [];
    } catch {
      return [];
    }
  }, [selectedPage]);

  // Set initial backgrounds when page first loads (before preload cache is populated)
  // The condition ensures this only runs once on initial load when backgrounds are empty.
  // After that, navigateToPage handles all subsequent navigation explicitly.
  useEffect(() => {
    if (selectedPage && lastInitializedPageIdRef.current !== selectedPage.id) {
      // Only initialize when backgrounds are empty (initial load)
      if (
        !navCurrentBgImageUrl &&
        !navCurrentBgVideoUrl &&
        !navCurrentBgEmbedUrl
      ) {
        lastInitializedPageIdRef.current = selectedPage.id;
        navNavigateToPage(selectedPage);
      }
    }
  }, [
    selectedPage,
    navCurrentBgImageUrl,
    navCurrentBgVideoUrl,
    navCurrentBgEmbedUrl,
    navNavigateToPage,
  ]);

  // Video transition overlay removal - clears when elements should show
  // When phase becomes 'idle' or 'fading_in' (navShowElements=true),
  // the transition preview is no longer needed and can be cleared
  useEffect(() => {
    if (navShowElements && transitionPreview) {
      // Clear transition preview - overlay will be removed
      setTransitionPreview(null);
    }
  }, [navShowElements, transitionPreview]);

  const navigateToPage = useCallback(
    async (
      targetPageId: string,
      transitionVideoUrl?: string,
      isBack = false,
      reverseVideoUrl?: string,
    ) => {
      const targetPage = pages.find((p) => p.id === targetPageId);
      if (!targetPage) return;

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

      // For back navigation, verify reversed video is available
      // Without reversed video, back navigation should use CSS fade instead
      const isBackWithoutReverse = isBack && !reverseVideoUrl;

      // Use video if: has transition AND (cached OR good network) AND not back-without-reverse
      const useVideoTransition =
        transitionVideoUrl &&
        (isTransitionCached || shouldUseVideoTransitions) &&
        !isBackWithoutReverse;

      if (useVideoTransition) {
        // Reset states from previous transition/navigation
        resetFadeIn();
        // Pause background downloads to give transition video exclusive bandwidth
        downloadManager.pauseAll();
        // Signal navigation state machine that video transition is starting
        // This sets phase to 'transitioning' so spinner shows during buffering
        startTransition(targetPageId, isBack);
        // Play transition using useTransitionPlayback hook
        setTransitionPreview({
          targetPageId,
          videoUrl: resolveAssetPlaybackUrl(transitionVideoUrl),
          storageKey: transitionVideoUrl, // Raw storage path for cache lookup
          isBack,
          reverseVideoUrl: reverseVideoUrl
            ? resolveAssetPlaybackUrl(reverseVideoUrl)
            : undefined,
          reverseStorageKey: reverseVideoUrl, // Raw storage path for reverse video cache lookup
        });
      } else {
        // Direct navigation with fade-from-black effect:
        // Page switches instantly, black overlay fades out to reveal new page
        // Mark this page as initialized to prevent redundant effect calls
        lastInitializedPageIdRef.current = targetPageId;

        // Log when skipping video due to missing reversed video (back navigation)
        if (isBackWithoutReverse && transitionVideoUrl) {
          logger.info(
            '[NAVIGATION] Skipping video transition for back navigation - reversed video not ready, using CSS fade',
            {
              transitionVideoUrl: transitionVideoUrl?.slice(-60),
              isBack,
            },
          );
        }

        // Log when skipping video due to slow network
        if (
          transitionVideoUrl &&
          !shouldUseVideoTransitions &&
          !isBackWithoutReverse
        ) {
          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-${transitionVideoUrl}`,
            projectId: 'transition-preload',
            url: resolveAssetPlaybackUrl(transitionVideoUrl),
            filename: transitionVideoUrl.split('/').pop() || 'transition.mp4',
            variantType: 'original',
            assetType: 'video',
            priority: 10, // Low priority - background preload
            storageKey: transitionVideoUrl,
          });
        }

        await navNavigateToPage(targetPage, {
          hasTransition: false,
          isBack,
          onSwitched: () => {
            applyPageSelection(targetPageId, isBack);
          },
        });
      }
    },
    [
      pages,
      navNavigateToPage,
      resetFadeIn,
      applyPageSelection,
      startTransition,
      shouldUseVideoTransitions,
      networkInfo,
      preloadOrchestrator,
    ],
  );

  const handleInfoPanelNavigateToPage = useCallback(
    (targetPageSlug: string) => {
      if (
        isTransitionBlocking(transitionPhase as TransitionPhase, isBuffering)
      ) {
        return;
      }

      const targetPage = pages.find((page) => page.slug === targetPageSlug);
      if (!targetPage) return;

      setActiveInfoPanel(null);
      setActiveDetailImage(null);
      setActiveInfoPanelGallery(null);
      setRuntimeSelectedImageId(null);
      navigateToPage(targetPage.id);
    },
    [navigateToPage, pages, transitionPhase, isBuffering],
  );

  const handleInfoPanelOpenExternalUrl = useCallback((url: string) => {
    const trimmed = url.trim();
    if (!trimmed) return;
    const href = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
    window.open(href, '_blank', 'noopener,noreferrer');
  }, []);

  const handleInfoPanelUseAsBackground = useCallback(
    (item: InfoPanelImage) => {
      const mediaType =
        item.itemType === 'video'
          ? 'video'
          : item.itemType === '360'
            ? '360'
            : 'image';
      if (
        (mediaType === 'image' && !item.imageUrl) ||
        (mediaType === 'video' && !item.videoUrl) ||
        (mediaType === '360' && !item.embedUrl)
      ) {
        return;
      }

      navSetBackgroundDirectly(
        mediaType === 'image' && item.imageUrl
          ? resolveAssetPlaybackUrl(item.imageUrl)
          : '',
        mediaType === 'video' && item.videoUrl
          ? resolveAssetPlaybackUrl(item.videoUrl)
          : '',
        mediaType === '360' && item.embedUrl
          ? resolveAssetPlaybackUrl(item.embedUrl)
          : '',
        navCurrentBgAudioUrl,
      );
      setActiveDetailImage(null);
      setActiveInfoPanelGallery(null);
    },
    [navCurrentBgAudioUrl, navSetBackgroundDirectly],
  );

  const handleInfoPanelOpenGallery = useCallback(
    (items: InfoPanelImage[], initialIndex: number) => {
      const activeItemId = items[initialIndex]?.id;
      const galleryItems = items
        .map<GalleryCarouselMediaItem | null>((item) => {
          const mediaType =
            item.itemType === 'video'
              ? 'video'
              : item.itemType === '360'
                ? '360'
                : 'image';
          if (mediaType === 'image' && !item.imageUrl) return null;
          if (mediaType === 'video' && !item.videoUrl) return null;
          if (mediaType === '360' && !item.embedUrl) return null;

          return {
            id: item.id,
            imageUrl: item.imageUrl,
            videoUrl: item.videoUrl,
            embedUrl: item.embedUrl,
            caption: item.caption,
            title: item.caption,
            mediaType,
          };
        })
        .filter((item): item is GalleryCarouselMediaItem => Boolean(item));

      if (galleryItems.length === 0) return;
      setActiveDetailImage(null);
      setActiveInfoPanelGallery({
        items: galleryItems,
        initialIndex: Math.max(
          0,
          galleryItems.findIndex((item) => item.id === activeItemId),
        ),
      });
    },
    [],
  );

  // Page loading state from unified navigation state machine
  // navShowSpinner: true when phase is 'preparing', 'loading_bg', or 'transition_done'
  // navShowElements: true when phase is 'idle' or 'fading_in'
  const areTransitionsReady = preloadOrchestrator?.areTransitionsReady ?? true;

  const handleElementClick = useCallback(
    (element: CanvasElement) => {
      if (
        isNavigationType(element.type) &&
        isElementFlagEnabled(element.navDisabled)
      ) {
        return;
      }
      if (
        isInfoPanelElementType(element.type) &&
        isElementFlagEnabled(element.infoPanelDisabled)
      ) {
        return;
      }

      // Handle info panel click
      if (isInfoPanelElementType(element.type)) {
        setActiveInfoPanel(element);
        setActiveDetailImage(null);
        return;
      }

      // Block navigation while transition is actively playing or buffering
      if (
        isTransitionBlocking(transitionPhase as TransitionPhase, isBuffering)
      ) {
        return;
      }

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

      // Use shared helper to resolve navigation target with history context
      const navTarget = resolveNavigationTarget(element, pages, navContext);

      // Debug: log element navigation data
      logger.info('Element clicked', {
        elementType: element.type,
        targetPageSlug: element.targetPageSlug,
        legacyTargetPageId: element.targetPageId,
        resolvedTargetPageId: navTarget?.pageId,
        transitionVideoUrl: element.transitionVideoUrl,
        hasTransition: Boolean(element.transitionVideoUrl),
        isBack: isBackNavigation(element),
        previousPageId: navContext.previousPageId,
      });

      if (navTarget) {
        // 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);
        });

        navigateToPage(
          navTarget.pageId,
          navTarget.transitionVideoUrl,
          navTarget.isBack,
          navTarget.reverseVideoUrl,
        );
      }
    },
    [
      navigateToPage,
      pages,
      transitionPhase,
      isBuffering,
      getNavigationContext,
      setCurrentElementTransitionSettings,
    ],
  );

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

  // URL resolver that uses preloaded blob URLs when available (instant display)
  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],
  );

  // Background URLs come directly from navigation state (already resolved)
  const backgroundImageUrl = navCurrentBgImageUrl;
  const backgroundVideoUrl = navCurrentBgVideoUrl;
  const backgroundEmbedUrl = navCurrentBgEmbedUrl;
  const backgroundAudioUrl = navCurrentBgAudioUrl;

  // Background video playback settings from selected page
  const videoAutoplay = selectedPage?.background_video_autoplay ?? true;
  const videoLoop = selectedPage?.background_video_loop ?? true;
  // Note: pageVideoMuted is the page setting, but we use soundControl.isMuted for actual muted state
  // This allows iOS to autoplay (starts muted) while giving user control via sound button
  const pageVideoMuted = selectedPage?.background_video_muted ?? true;
  const videoStartTime =
    selectedPage?.background_video_start_time != null
      ? parseFloat(String(selectedPage.background_video_start_time))
      : null;
  const videoEndTime =
    selectedPage?.background_video_end_time != null
      ? parseFloat(String(selectedPage.background_video_end_time))
      : null;

  // Background audio playback settings from selected page
  const audioLoop = selectedPage?.background_audio_loop ?? true;
  const audioStartTime =
    selectedPage?.background_audio_start_time != null
      ? parseFloat(String(selectedPage.background_audio_start_time))
      : null;
  const audioEndTime =
    selectedPage?.background_audio_end_time != null
      ? parseFloat(String(selectedPage.background_audio_end_time))
      : null;
  const hasPresentationAudio = useMemo(
    () => presentationHasAudio(pages),
    [pages],
  );

  // Global sound control starts muted for browser autoplay compatibility.
  const soundControl = useVideoSoundControl({
    pageHasSound: pageVideoMuted === false,
    hasBackgroundVideo: Boolean(backgroundVideoUrl),
    hasBackgroundAudio: Boolean(backgroundAudioUrl),
    hasPresentationAudio,
  });

  // Note: useBackgroundVideoPlayback is handled internally by CanvasBackground component

  if (isLoading) {
    return (
      <>
        <Head>
          <meta name='theme-color' content='#000000' />
          <meta name='color-scheme' content='dark' />
          <style>{`html, body { background-color: #000000 !important; }`}</style>
        </Head>
        <div className='flex items-center justify-center min-h-screen bg-black'>
          <div className='text-white text-xl'>Loading presentation...</div>
        </div>
      </>
    );
  }

  if (error) {
    return (
      <>
        <Head>
          <meta name='theme-color' content='#000000' />
          <meta name='color-scheme' content='dark' />
          <style>{`html, body { background-color: #000000 !important; }`}</style>
        </Head>
        <div className='flex items-center justify-center min-h-screen bg-black'>
          <CardBox className='max-w-md'>
            <h2 className='text-xl font-bold text-red-500 mb-4'>Error</h2>
            <p className='text-gray-300'>{error}</p>
          </CardBox>
        </div>
      </>
    );
  }

  return (
    <>
      {/* Rotate prompt for portrait orientation */}
      <RotatePrompt show={showRotatePrompt && isPortrait} />

      <Head>
        <title>{project?.name || 'Presentation'}</title>
        {/* Dark theme for browser UI and background */}
        <meta name='theme-color' content='#000000' />
        <meta name='color-scheme' content='dark' />
        <meta
          name='apple-mobile-web-app-status-bar-style'
          content='black-translucent'
        />
        <style>{`
          html, body { background-color: #000000 !important; }
        `}</style>
        {faviconUrl && <link key='favicon' rel='icon' href={faviconUrl} />}
        {ogImageUrl && (
          <>
            <meta key='og:image' property='og:image' content={ogImageUrl} />
            <meta
              key='twitter:image:src'
              property='twitter:image:src'
              content={ogImageUrl}
            />
          </>
        )}
        {project?.name && (
          <>
            <meta key='og:title' property='og:title' content={project.name} />
            <meta
              key='twitter:title'
              property='twitter:title'
              content={project.name}
            />
          </>
        )}
        {project?.description && (
          <>
            <meta
              key='og:description'
              property='og:description'
              content={project.description}
            />
            <meta
              key='twitter:description'
              property='twitter:description'
              content={project.description}
            />
          </>
        )}
      </Head>

      {/* Outer container: full viewport with black background for letterbox bars */}
      {/* onClick/onTouchEnd: Notify audio controller of user interaction for autoplay unlock */}
      <div
        className='relative w-screen h-screen overflow-hidden bg-black'
        onClick={handleCanvasInteraction}
        onTouchEnd={handleCanvasInteraction}
      >
        {/* Inner canvas: maintains aspect ratio centered in viewport.
            z-[46] creates stacking context above carousel (z-10 bg, z-45 controls) portaled to body. */}
        <div
          className='relative z-[46] overflow-hidden'
          style={{
            ...cssVars,
            ...letterboxStyles,
          }}
        >
          <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',
                  }}
                />
              )}

            {/* Page background wrapper - z-5 keeps it BELOW carousel slide (z-10).
                Uses shared CanvasBackground component for single source of truth with constructor.
                Previous background overlay shows during loading.
                Black overlay for fade effect is rendered separately at z-[100]. */}
            <div
              data-testid='page-background-wrapper'
              className='absolute inset-0 z-5'
            >
              <CanvasBackground
                backgroundImageUrl={backgroundImageUrl}
                backgroundVideoUrl={backgroundVideoUrl}
                backgroundEmbedUrl={backgroundEmbedUrl}
                backgroundAudioUrl={backgroundAudioUrl}
                previousBgImageUrl={navPreviousBgImageUrl}
                previousBgVideoUrl={navPreviousBgVideoUrl}
                isSwitching={navIsSwitching}
                isNewBgReady={navIsNewBgReady}
                onBackgroundReady={navOnBackgroundReady}
                onVideoBufferStateChange={onVideoBufferStateChange}
                videoAutoplay={videoAutoplay}
                videoLoop={videoLoop}
                videoMuted={soundControl.isMuted}
                videoStartTime={videoStartTime}
                videoEndTime={videoEndTime}
                videoStoragePath={selectedPage?.background_video_url}
                pauseVideo={
                  Boolean(transitionPreview) ||
                  pendingTransitionComplete ||
                  navIsSwitching
                }
                audioLoop={audioLoop}
                audioStartTime={audioStartTime}
                audioEndTime={audioEndTime}
                audioStoragePath={selectedPage?.background_audio_url}
              />
            </div>
            {/* End page background wrapper */}

            {/* 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
                Skip when video transition overlay is active - it has its own spinner. */}
            {navShowSpinner && !transitionPreview && (
              <CanvasLoadingSpinner isVisible={true} zIndex={100} />
            )}

            {/* Page elements wrapper - 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). */}
            {navShowElements && (
              <div
                data-testid='page-elements-wrapper'
                className='pointer-events-none absolute inset-0 z-[46]'
              >
                {pageElements.map((element: CanvasElement) => (
                  <RuntimeElement
                    key={element.id}
                    element={element}
                    isDisabled={
                      (isNavigationType(element.type) &&
                        isElementFlagEnabled(element.navDisabled)) ||
                      (isInfoPanelElementType(element.type) &&
                        isElementFlagEnabled(element.infoPanelDisabled))
                    }
                    onClick={() => handleElementClick(element)}
                    resolveUrl={resolveUrlWithBlob}
                    onGalleryCardClick={(cardIndex) =>
                      handleGalleryCardClick(element, cardIndex)
                    }
                    letterboxStyles={letterboxStyles}
                    pageTransitionSettings={transitionSettings}
                    preloadCache={
                      preloadOrchestrator
                        ? {
                            getReadyBlob: preloadOrchestrator.getReadyBlob,
                            getCachedBlobUrl:
                              preloadOrchestrator.getCachedBlobUrl,
                          }
                        : undefined
                    }
                    isInfoPanelOpen={activeInfoPanel?.id === element.id}
                  />
                ))}
              </div>
            )}
            {/* End page elements wrapper */}

            {/* 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}
            />

            {/* Transition overlay - uses useTransitionPlayback hook for smooth transitions */}
            {/* Opacity is 0 during 'preparing' phase to show old page while video loads */}
            {/* Fades out during 'fading_in' phase when background is ready */}
            {/* Overlay stays visible until fade completes (phase goes to 'idle') */}
            {transitionPreview && showTransitionVideo && (
              <TransitionPreviewOverlay
                videoKey={transitionPreview.videoUrl}
                videoRef={transitionVideoRef}
                isActive={true}
                isBuffering={
                  // Show spinner during buffering:
                  // - 'idle': React render cycle before hook effect runs
                  // - 'preparing': Video loading/buffering
                  // - isBuffering: Waiting for first frame or mid-playback buffering
                  transitionPhase === 'idle' ||
                  transitionPhase === 'preparing' ||
                  isBuffering
                }
                isVideoReady={isVideoReady}
                showSpinner={true}
                letterboxStyles={letterboxStyles}
                isFadingOut={isFadingIn}
                fadeOutDuration={transitionSettings.durationMs}
              />
            )}

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

            {activeInfoPanelGallery && (
              <GalleryCarouselOverlay
                cards={activeInfoPanelGallery.items}
                initialIndex={activeInfoPanelGallery.initialIndex}
                onClose={() => setActiveInfoPanelGallery(null)}
                resolveUrl={resolveUrlWithBlob}
                prevIconUrl={activeInfoPanel?.galleryCarouselPrevIconUrl}
                nextIconUrl={activeInfoPanel?.galleryCarouselNextIconUrl}
                backIconUrl={activeInfoPanel?.galleryCarouselBackIconUrl}
                backLabel={activeInfoPanel?.galleryCarouselBackLabel || 'BACK'}
                prevX={activeInfoPanel?.galleryCarouselPrevX}
                prevY={activeInfoPanel?.galleryCarouselPrevY}
                nextX={activeInfoPanel?.galleryCarouselNextX}
                nextY={activeInfoPanel?.galleryCarouselNextY}
                backX={activeInfoPanel?.galleryCarouselBackX}
                backY={activeInfoPanel?.galleryCarouselBackY}
                prevWidth={activeInfoPanel?.galleryCarouselPrevWidth}
                prevHeight={activeInfoPanel?.galleryCarouselPrevHeight}
                nextWidth={activeInfoPanel?.galleryCarouselNextWidth}
                nextHeight={activeInfoPanel?.galleryCarouselNextHeight}
                backWidth={activeInfoPanel?.galleryCarouselBackWidth}
                backHeight={activeInfoPanel?.galleryCarouselBackHeight}
                letterboxStyles={letterboxStyles}
                isEditMode={false}
                pageTransitionSettings={transitionSettings}
                galleryElement={activeInfoPanel || undefined}
              />
            )}

            {/* Info Panel Overlay */}
            {activeInfoPanel && (
              <>
                <InfoPanelOverlay
                  element={
                    runtimeSelectedImageId
                      ? {
                          ...activeInfoPanel,
                          infoPanelSelectedImageId: runtimeSelectedImageId,
                        }
                      : activeInfoPanel
                  }
                  onClose={() => {
                    setActiveInfoPanel(null);
                    setActiveDetailImage(null);
                    setActiveInfoPanelGallery(null);
                    setRuntimeSelectedImageId(null);
                  }}
                  resolveUrl={resolveUrlWithBlob}
                  letterboxStyles={letterboxStyles}
                  cssVars={cssVars}
                  onImageClick={(image) => setActiveDetailImage(image)}
                  onOpenGallery={handleInfoPanelOpenGallery}
                  onUseAsBackground={handleInfoPanelUseAsBackground}
                  onSelectImage={(imageId) =>
                    setRuntimeSelectedImageId(imageId)
                  }
                  onNavigateToPage={handleInfoPanelNavigateToPage}
                  onOpenExternalUrl={handleInfoPanelOpenExternalUrl}
                  active360ItemId={
                    activeDetailImage?.itemType === '360'
                      ? activeDetailImage.id
                      : null
                  }
                />
                {activeDetailImage && (
                  <ImageDetailPanel
                    element={activeInfoPanel}
                    image={activeDetailImage}
                    onClose={() => setActiveDetailImage(null)}
                    resolveUrl={resolveUrlWithBlob}
                    letterboxStyles={letterboxStyles}
                    cssVars={cssVars}
                  />
                )}
              </>
            )}
          </BackdropPortalProvider>
        </div>
        {/* End inner canvas container */}

        {/* Controls: Offline toggle, Fullscreen, and Sound buttons */}
        {/* Positioned outside canvas to avoid scaling with canvas transform */}
        {!activeGalleryCarousel && !activeInfoPanelGallery && (
          <RuntimeControls
            projectId={project?.id || null}
            projectSlug={projectSlug}
            projectName={project?.name}
            pages={pages}
            isFullscreen={isFullscreen}
            toggleFullscreen={toggleFullscreen}
            canvasWidth={canvasWidth}
            canvasHeight={canvasHeight}
            showSoundButton={soundControl.showSoundButton}
            isMuted={soundControl.isMuted}
            onSoundToggle={soundControl.toggleSound}
          />
        )}

        {/* Toast notifications for offline download status */}
        <ToastContainer
          position='bottom-center'
          autoClose={5000}
          hideProgressBar={false}
          newestOnTop={false}
          closeOnClick
          rtl={false}
          pauseOnFocusLoss
          draggable
          pauseOnHover
          theme='dark'
        />
      </div>
    </>
  );
}

// Layout wrapper for standalone usage
RuntimePresentation.getLayout = function getLayout(page: ReactElement) {
  return <LayoutGuest>{page}</LayoutGuest>;
};
