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

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

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

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

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

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

type NavigationButtonKind = 'forward' | 'back';

type CanvasElement = {
  id: string;
  type: CanvasElementType;
  label: string;
  xPercent: number;
  yPercent: number;
  appearDelaySec?: number;
  appearDurationSec?: number | null;
  iconUrl?: string;
  galleryCards?: GalleryCard[];
  carouselSlides?: CarouselSlide[];
  carouselPrevIconUrl?: string;
  carouselNextIconUrl?: string;
  tooltipTitle?: string;
  tooltipText?: string;
  descriptionTitle?: string;
  descriptionText?: string;
  navLabel?: string;
  navType?: NavigationButtonKind;
  targetPageId?: string;
  transitionVideoUrl?: string;
  transitionReverseMode?: 'auto_reverse' | 'separate_video';
  reverseVideoUrl?: string;
  transitionDurationSec?: number;
  mediaUrl?: string;
  mediaAutoplay?: boolean;
  mediaLoop?: boolean;
  mediaMuted?: boolean;
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  try {
    const parsed = new URL(normalized, 'http://localhost');
    const privateUrl = parsed.searchParams.get('privateUrl');
    return String(privateUrl || '').trim();
  } catch (error) {
    console.error('Failed to parse download URL:', error);
    return '';
  }
};

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

  try {
    const parsed = new URL(normalized);
    const hostname = String(parsed.hostname || '').toLowerCase();
    if (
      !hostname.includes('amazonaws.com') &&
      !hostname.includes('cloudfront.net')
    )
      return '';
    const decodedPath = decodeURIComponent(
      String(parsed.pathname || '').replace(/^\/+/, ''),
    );
    if (!decodedPath) return '';

    const pathParts = decodedPath.split('/').filter(Boolean);
    if (pathParts.length <= 1) return decodedPath;

    const firstPart = pathParts[0];
    const isLikelyStoragePrefix = /^[a-f0-9]{24,64}$/i.test(firstPart);
    if (isLikelyStoragePrefix) {
      return pathParts.slice(1).join('/');
    }

    return decodedPath;
  } catch (error) {
    console.error('Failed to parse S3 asset URL:', error);
    return '';
  }
};

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

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

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

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

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

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

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

  if (normalized.startsWith('http://') || normalized.startsWith('https://')) {
    const downloadPrivateUrl = extractPrivateUrlFromDownloadPath(normalized);
    if (downloadPrivateUrl) {
      return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(downloadPrivateUrl)}`;
    }

    const s3ObjectKey = extractS3ObjectKey(normalized);
    if (s3ObjectKey) {
      return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(s3ObjectKey)}`;
    }

    return normalized;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  if (type === 'description') {
    return {
      ...base,
      iconUrl: '',
      descriptionTitle: 'Description title',
      descriptionText: 'Description text',
    };
  }

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

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

  return base;
};

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

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

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

  return element.label;
};

const ConstructorPage = () => {
  const router = useRouter();
  const canvasRef = useRef<HTMLDivElement>(null);
  const elementEditorRef = useRef<HTMLDivElement>(null);
  const [isAuthReady, setIsAuthReady] = useState(false);

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

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

  const [pages, setPages] = useState<TourPage[]>([]);
  const [assets, setAssets] = useState<ProjectAsset[]>([]);
  const [activePageId, setActivePageId] = useState('');
  const [projectName, setProjectName] = useState('');

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

  const [isLoading, setIsLoading] = useState(true);
  const [isSaving, setIsSaving] = useState(false);
  const [isCreatingPage, setIsCreatingPage] = useState(false);
  const [isCreatingTransition, setIsCreatingTransition] = useState(false);
  const [newTransitionName, setNewTransitionName] = useState('');
  const [newTransitionVideoUrl, setNewTransitionVideoUrl] = useState('');
  const [newTransitionSupportsReverse, setNewTransitionSupportsReverse] =
    useState(true);
  const [newTransitionDurationSec, setNewTransitionDurationSec] = useState(0.7);
  const [errorMessage, setErrorMessage] = useState('');
  const [successMessage, setSuccessMessage] = useState('');
  const [resolvedDurationBySource, setResolvedDurationBySource] = useState<
    Record<string, number | null>
  >({});

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

  const menuDragRef = useRef<{
    pointerOffsetX: number;
    pointerOffsetY: number;
  } | null>(null);
  const editorDragRef = useRef<{
    pointerOffsetX: number;
    pointerOffsetY: number;
  } | null>(null);
  const elementDragRef = useRef<DragElementState | null>(null);
  const transitionVideoRef = useRef<HTMLVideoElement | null>(null);
  const reverseAnimationFrame = useRef<number | null>(null);
  const didSetInitialCanvasFocus = useRef(false);
  const durationProbeInFlightRef = useRef<Set<string>>(new Set());

  const activePage = useMemo(
    () => pages.find((item) => item.id === activePageId) || null,
    [activePageId, pages],
  );
  const activePageIndex = useMemo(
    () => pages.findIndex((item) => item.id === activePageId),
    [activePageId, pages],
  );
  const allowedNavigationTypes = useMemo<NavigationElementType[]>(() => {
    if (pages.length <= 1) return ['navigation_next'];
    if (activePageIndex < 0) return ['navigation_next', 'navigation_prev'];
    if (activePageIndex <= 0) return ['navigation_next'];
    if (activePageIndex >= pages.length - 1) return ['navigation_prev'];
    return ['navigation_next', 'navigation_prev'];
  }, [activePageIndex, pages.length]);
  const pageNameById = useMemo(() => {
    const acc: Record<string, string> = {};
    pages.forEach((page, index) => {
      acc[String(page.id)] = page.name || `Page ${index + 1}`;
    });
    return acc;
  }, [pages]);
  const selectedElement = useMemo(
    () => elements.find((element) => element.id === selectedElementId) || null,
    [elements, selectedElementId],
  );
  const normalizeNavigationElementType = useCallback(
    (element: CanvasElement, nextType: NavigationElementType): CanvasElement => {
      if (!isNavigationElementType(element.type)) return element;

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

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

    if (typedAssets.length > 0) return typedAssets;

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

    if (taggedAssets.length > 0) return taggedAssets;

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

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

      return null;
    },
    [resolvedDurationBySource],
  );

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

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

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

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

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

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

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

      if (getKnownDurationForSource(normalizedSource)) return;

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

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

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

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

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

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

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

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

      const [projectResponse, pagesResponse, assetsResponse] =
        await Promise.all([
          axios.get(`/projects/${projectId}`),
          axios.get(
            `/tour_pages?limit=500&sort=asc&field=sort_order&project=${projectId}`,
          ),
          axios.get(
            `/assets?limit=500&page=0&sort=desc&field=createdAt&project=${projectId}`,
          ),
        ]);

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

      const defaultPageId = pageIdFromRoute || pageRows[0]?.id || '';
      setActivePageId(defaultPageId);
      setIsMenuOpen(false);
    } catch (error: any) {
      if (error?.response?.status === 401) {
        const message = 'Your session has expired. Please sign in again.';
        console.error('Unauthorized constructor request:', error);
        setErrorMessage(message);
        setPages([]);
        setAssets([]);
        router.replace('/login');
        return;
      }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      await loadData();

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

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

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

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

    const sanitizedName =
      String(newTransitionName || '').trim() ||
      `Transition ${Date.now().toString().slice(-4)}`;
    const parsedDuration = Number(newTransitionDurationSec);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  const renderCanvasElementContent = (element: CanvasElement) => {
    if (
      element.type === 'navigation_next' ||
      element.type === 'navigation_prev'
    ) {
      const targetPageName = element.targetPageId
        ? pageNameById[element.targetPageId]
        : '';
      return (
        <div className='flex flex-col items-start gap-1'>
          <div className='flex items-center gap-2'>
            {element.iconUrl ? (
              // eslint-disable-next-line @next/next/no-img-element
              <img
                src={resolveAssetPlaybackUrl(element.iconUrl)}
                alt='Navigation icon'
                className='h-4 w-4 object-contain'
              />
            ) : null}
            <span>
              {element.navLabel ||
                (element.type === 'navigation_next' ? 'Forward' : 'Back')}
            </span>
          </div>
          {targetPageName ? (
            <span className='text-[10px] text-gray-500'>
              To: {targetPageName}
            </span>
          ) : null}
        </div>
      );
    }

    if (element.type === 'tooltip') {
      return (
        <div className='max-w-[200px] text-left'>
          {element.iconUrl ? (
            // eslint-disable-next-line @next/next/no-img-element
            <img
              src={resolveAssetPlaybackUrl(element.iconUrl)}
              alt='Tooltip icon'
              className='mb-1 h-5 w-5 object-contain'
            />
          ) : null}
          <p className='text-[11px] font-bold'>
            {element.tooltipTitle || 'Tooltip title'}
          </p>
          <p className='text-[10px] text-gray-600 line-clamp-3'>
            {element.tooltipText || 'Tooltip text'}
          </p>
        </div>
      );
    }

    if (element.type === 'description') {
      return (
        <div className='max-w-[220px] text-left'>
          {element.iconUrl ? (
            // eslint-disable-next-line @next/next/no-img-element
            <img
              src={resolveAssetPlaybackUrl(element.iconUrl)}
              alt='Description icon'
              className='mb-1 h-5 w-5 object-contain'
            />
          ) : null}
          <p className='text-[11px] font-bold'>
            {element.descriptionTitle || 'Description title'}
          </p>
          <p className='text-[10px] text-gray-600 line-clamp-4'>
            {element.descriptionText || 'Description text'}
          </p>
        </div>
      );
    }

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

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

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

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

    return getElementButtonTitle(element);
  };

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

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

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

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

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

    const finishPreview = () => {
      cleanupReverseFrame();
      setTransitionPreview(null);
    };

    const configuredDurationMs =
      (transitionPreview.durationSec && transitionPreview.durationSec > 0
        ? transitionPreview.durationSec
        : 0.7) * 1000;

    const runReversePreview = () => {
      cleanupReverseFrame();
      const duration =
        Number.isFinite(video.duration) && video.duration > 0
          ? video.duration
          : Math.max(configuredDurationMs / 1000, 0.7);
      const reverseSeconds =
        transitionPreview.durationSec && transitionPreview.durationSec > 0
          ? transitionPreview.durationSec
          : duration;
      const reverseMs = Math.max(reverseSeconds * 1000, 400);
      const reverseRate = duration / reverseSeconds;
      const startTime = performance.now();
      video.pause();
      video.currentTime = duration;

      const step = (now: number) => {
        const elapsed = now - startTime;
        const nextTime = Math.max(duration - (elapsed / 1000) * reverseRate, 0);
        video.currentTime = nextTime;

        if (elapsed >= reverseMs || nextTime <= 0.001) {
          finishPreview();
          return;
        }

        reverseAnimationFrame.current = requestAnimationFrame(step);
      };

      reverseAnimationFrame.current = requestAnimationFrame(step);
    };

    const onLoadedMetadata = () => {
      if (transitionPreview.reverseMode === 'reverse') {
        runReversePreview();
        return;
      }

      video.currentTime = 0;
      video.play().catch((playError) => {
        console.error('Transition preview playback failed:', playError);
        fallbackTimer = setTimeout(finishPreview, configuredDurationMs);
      });
    };

    const onEnded = () => finishPreview();

    video.addEventListener('loadedmetadata', onLoadedMetadata);
    video.addEventListener('ended', onEnded);
    fallbackTimer = setTimeout(finishPreview, configuredDurationMs + 500);

    return () => {
      video.removeEventListener('loadedmetadata', onLoadedMetadata);
      video.removeEventListener('ended', onEnded);
      cleanupReverseFrame();
      if (fallbackTimer) clearTimeout(fallbackTimer);
    };
  }, [transitionPreview]);

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

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

          {pages.length > 0 && (
            <div className='flex items-center gap-2'>
              <select
                className='border border-gray-300 rounded px-3 py-2 bg-white text-sm'
                value={activePageId}
                onChange={(event) => setActivePageId(event.target.value)}
              >
                {pages.map((page, index) => (
                  <option key={page.id} value={page.id}>
                    {page.name || `Page ${index + 1}`}
                  </option>
                ))}
              </select>
              <BaseButton
                color='lightDark'
                label='Exit to Assets'
                icon={mdiExitToApp}
                href={
                  projectId
                    ? `/projects/${projectId}`
                    : '/projects/projects-list'
                }
              />
            </div>
          )}
        </div>

        <div
          ref={canvasRef}
          tabIndex={-1}
          className='absolute inset-0 bg-white overflow-hidden'
          style={canvasBackgroundStyle}
        >
          {backgroundImageSrc ? (
            // eslint-disable-next-line @next/next/no-img-element
            <img
              key={`bg_image_${backgroundImageSrc}`}
              src={backgroundImageSrc}
              alt='Background'
              className='absolute inset-0 h-full w-full object-cover pointer-events-none select-none'
            />
          ) : null}

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

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

          {isLoading ? (
            <div className='absolute inset-0 flex items-center justify-center'>
              <p className='text-sm text-gray-500'>Loading constructor...</p>
            </div>
          ) : pages.length === 0 ? (
            <div className='absolute inset-0 flex items-center justify-center'>
              <BaseButton
                color='info'
                label={isCreatingPage ? 'Creating...' : 'Create First Page'}
                icon={mdiPlus}
                onClick={createPage}
                disabled={isCreatingPage}
              />
            </div>
          ) : (
            elements.map((element) => (
              <button
                key={element.id}
                type='button'
                data-constructor-element-id={element.id}
                className={`absolute border rounded px-3 py-2 text-xs font-semibold shadow cursor-move text-left ${
                  selectedElementId === element.id
                    ? 'border-blue-500 bg-blue-50'
                    : 'border-blue-200 bg-white/95'
                }`}
                style={{
                  left: `${element.xPercent}%`,
                  top: `${element.yPercent}%`,
                  transform: 'translate(-50%, -50%)',
                }}
                onMouseDown={(event) => onElementMouseDown(event, element.id)}
                onClick={() => selectElementForEdit(element.id)}
              >
                {renderCanvasElementContent(element)}
              </button>
            ))
          )}
        </div>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      {transitionPreview && (
        <div className='fixed inset-0 z-50 bg-black/95 flex items-center justify-center'>
          <video
            ref={transitionVideoRef}
            src={resolveAssetPlaybackUrl(
              transitionPreview.reverseMode === 'separate'
                ? transitionPreview.reverseVideoUrl || ''
                : transitionPreview.videoUrl,
            )}
            className='h-full w-full object-cover'
            muted
            playsInline
            autoPlay={transitionPreview.reverseMode !== 'reverse'}
            preload='auto'
          />
          <div className='absolute bottom-4 left-4 text-xs text-white/80'>
            {transitionPreview.title}
          </div>
          <button
            type='button'
            className='absolute top-4 right-4 rounded bg-white/20 px-3 py-1 text-xs text-white hover:bg-white/30'
            onClick={() => setTransitionPreview(null)}
          >
            Close
          </button>
        </div>
      )}

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

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

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

export default ConstructorPage;
