import { useState, useCallback, useEffect, useRef, lazy, Suspense } from 'react';
import { useBlocker } from 'react-router';
import { useTranslation } from 'react-i18next';
import type Konva from 'konva';
import { useEditor } from './context/EditorContext';
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
import { boundingBox } from './utils/geometry';
import { normalizeAngleDegrees } from './utils/angle';
import { EditorCanvas } from './EditorCanvas';
import { EditorToolbar } from './EditorToolbar';
import { PropertiesPanel } from './PropertiesPanel';
import { ElectricalPalette } from './panels/ElectricalPalette';
import { FurniturePalette } from './panels/FurniturePalette';
import { CableLengthStatus } from './panels/CableLengthStatus';
import { ProjectionPanel } from './projection/ProjectionPanel';
import { ExportDialog } from './export/ExportDialog';
import { importRoomFromJson } from './export/roomFormat';

const Room3DView = lazy(() =>
  import('./three/Room3DView').then((m) => ({ default: m.Room3DView })),
);
import {
  getRoomFull,
  bulkUpdateWalls,
  batchSyncOpenings,
  batchSyncElectrical,
  batchSyncFurniture,
  batchSyncAnnotations,
  updateRoom,
} from '../../api/client';
import type {
  CreateWallOpeningDto,
  UpdateWallOpeningDto,
  CreateElectricalItemDto,
  UpdateElectricalItemDto,
  CreateFurnitureItemDto,
  UpdateFurnitureItemDto,
  CreateAnnotationDto,
  UpdateAnnotationDto,
} from '@house-plan-maker/shared';
import styles from './room-editor-layout.module.css';

const AUTO_SAVE_DELAY_MS = 5000;

interface RoomEditorLayoutProps {
  readonly roomId: string;
}

export function RoomEditorLayout({ roomId }: RoomEditorLayoutProps) {
  const { t } = useTranslation();
  const { state, dispatch } = useEditor();
  const [isSaving, setIsSaving] = useState(false);
  const [isAutoSaving, setIsAutoSaving] = useState(false);
  const [saveError, setSaveError] = useState<string | null>(null);
  type ViewMode = '2d' | '3d' | 'projections';
  const [viewMode, setViewMode] = useState<ViewMode>('2d');
  const [showExport, setShowExport] = useState(false);
  const canvasContainerRef = useRef<HTMLDivElement>(null);
  // Start as null so the initial render doesn't use a seed 800×600 size —
  // the Stage (and the auto-fit effect) only kicks in after the container
  // has been measured, avoiding the multi-frame resize flicker on open.
  const [canvasSize, setCanvasSize] = useState<{ width: number; height: number } | null>(null);

  // ── Dirty tracking ──
  const [isDirty, setIsDirty] = useState(false);
  const lastSavedRef = useRef({
    walls: state.walls,
    openings: state.openings,
    electricalItems: state.electricalItems,
    furnitureItems: state.furnitureItems,
    room: state.room,
  });

  // Mark dirty when state diverges from last saved snapshot
  useEffect(() => {
    const saved = lastSavedRef.current;
    const dirty =
      state.walls !== saved.walls ||
      state.openings !== saved.openings ||
      state.electricalItems !== saved.electricalItems ||
      state.furnitureItems !== saved.furnitureItems ||
      state.room.floorType !== saved.room.floorType ||
      state.room.wallColor !== saved.room.wallColor ||
      state.room.wallFinish !== saved.room.wallFinish ||
      state.room.wallHeight !== saved.room.wallHeight ||
      state.room.plinthHeight !== saved.room.plinthHeight ||
      state.room.plinthThickness !== saved.room.plinthThickness ||
      state.room.outletWidth !== saved.room.outletWidth ||
      state.room.outletHeight !== saved.room.outletHeight ||
      state.room.name !== saved.room.name;
    setIsDirty(dirty);
  }, [
    state.walls,
    state.openings,
    state.electricalItems,
    state.furnitureItems,
    state.room.floorType,
    state.room.wallColor,
    state.room.wallFinish,
    state.room.wallHeight,
    state.room.plinthHeight,
    state.room.plinthThickness,
    state.room.outletWidth,
    state.room.outletHeight,
    state.room.name,
    state.room,
  ]);

  // Warn on browser close / refresh
  useEffect(() => {
    if (!isDirty) return;

    const handleBeforeUnload = (e: BeforeUnloadEvent) => {
      e.preventDefault();
    };

    window.addEventListener('beforeunload', handleBeforeUnload);
    return () => window.removeEventListener('beforeunload', handleBeforeUnload);
  }, [isDirty]);

  // Block in-app navigation via react-router
  const blocker = useBlocker(isDirty);

  useEffect(() => {
    if (blocker.state === 'blocked') {
      const leave = window.confirm(
        t('editor.unsavedChanges'),
      );
      if (leave) {
        blocker.proceed();
      } else {
        blocker.reset();
      }
    }
  }, [blocker]);

  // ── Export refs ──
  const mainStageRef = useRef<Konva.Stage | null>(null);
  const projectionStageMapRef = useRef<Map<string, Konva.Stage>>(new Map());
  const threeCanvasRef = useRef<HTMLCanvasElement | null>(null);

  const handleMainStageRef = useCallback((stage: Konva.Stage | null) => {
    mainStageRef.current = stage;
  }, []);

  const handleProjectionStageRef = useCallback((wallId: string, stage: Konva.Stage | null) => {
    if (stage) {
      projectionStageMapRef.current.set(wallId, stage);
    } else {
      projectionStageMapRef.current.delete(wallId);
    }
  }, []);

  // ── Resize observer for canvas ──
  useEffect(() => {
    const container = canvasContainerRef.current;
    if (!container) return;

    const commitSize = (w: number, h: number): void => {
      const width = Math.floor(w);
      const height = Math.floor(h);
      if (width <= 0 || height <= 0) return;
      // Skip no-op updates so the auto-fit effect doesn't re-run on every
      // ResizeObserver tick that doesn't actually change the pixel size.
      setCanvasSize((prev) => {
        if (prev && prev.width === width && prev.height === height) return prev;
        return { width, height };
      });
    };

    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        commitSize(entry.contentRect.width, entry.contentRect.height);
      }
    });

    observer.observe(container);

    // Initial size
    const rect = container.getBoundingClientRect();
    commitSize(rect.width, rect.height);

    return () => observer.disconnect();
  }, []);

  // ── Auto-fit the room into the 2D canvas ──
  // Fires once the container has been measured and the room shape is
  // available, and again whenever either changes. Skips no-op reruns where
  // the canvas and room already match the last fit signature so we don't
  // flicker through multiple frames on open.
  const hasUserAdjustedViewRef = useRef(false);
  const lastFitSignatureRef = useRef<string>('');
  const lastDispatchedViewRef = useRef<{ zoom: number; panX: number; panY: number } | null>(null);

  useEffect(() => {
    if (viewMode !== '2d') return;
    if (!canvasSize) return;
    if (canvasSize.width <= 100 || canvasSize.height <= 100) return;
    if (state.room.shape.length === 0) return;

    const bbox = boundingBox(state.room.shape);
    const roomW = bbox.maxX - bbox.minX;
    const roomH = bbox.maxY - bbox.minY;
    if (roomW <= 0 || roomH <= 0) return;

    const signature = `${canvasSize.width}x${canvasSize.height}|${bbox.minX},${bbox.minY},${bbox.maxX},${bbox.maxY}`;

    // Already fit at this signature? Nothing to do.
    if (lastFitSignatureRef.current === signature) return;
    // User moved the camera → don't clobber their view until the room or
    // canvas actually changes dimensions (which gives a new signature).
    if (hasUserAdjustedViewRef.current && lastFitSignatureRef.current !== '') return;

    const padding = 80;
    const scaleX = (canvasSize.width - padding * 2) / roomW;
    const scaleY = (canvasSize.height - padding * 2) / roomH;
    const fitZoom = Math.min(scaleX, scaleY, 300);

    const centerX = (bbox.minX + bbox.maxX) / 2;
    const centerY = (bbox.minY + bbox.maxY) / 2;
    const panX = canvasSize.width / 2 - centerX * fitZoom;
    const panY = canvasSize.height / 2 - centerY * fitZoom;

    lastDispatchedViewRef.current = { zoom: fitZoom, panX, panY };
    // Single atomic reducer pass — produces one new state, not two, so the
    // ZoomPanContext can't emit an intermediate (newZoom, oldPan) frame.
    dispatch({ type: 'SET_VIEW', zoom: fitZoom, offset: { x: panX, y: panY } });
    lastFitSignatureRef.current = signature;
  }, [viewMode, canvasSize, state.room.shape, dispatch]);

  // Detect *manual* zoom/pan. Comparing against the values we just
  // dispatched prevents the auto-fit itself from flipping the flag.
  useEffect(() => {
    const last = lastDispatchedViewRef.current;
    if (!last) return;
    const EPS = 0.5;
    const cameFromAutoFit =
      Math.abs(state.zoom - last.zoom) < EPS &&
      Math.abs(state.panOffset.x - last.panX) < EPS &&
      Math.abs(state.panOffset.y - last.panY) < EPS;
    if (!cameFromAutoFit) {
      hasUserAdjustedViewRef.current = true;
    }
  }, [state.zoom, state.panOffset]);

  // ── Re-measure canvas when switching back to 2D view ──
  useEffect(() => {
    if (viewMode !== '2d') return;
    // Use requestAnimationFrame to measure after the style change is applied
    const rafId = requestAnimationFrame(() => {
      const container = canvasContainerRef.current;
      if (!container) return;
      const rect = container.getBoundingClientRect();
      if (rect.width > 0 && rect.height > 0) {
        setCanvasSize({ width: Math.floor(rect.width), height: Math.floor(rect.height) });
      }
    });
    return () => cancelAnimationFrame(rafId);
  }, [viewMode]);

  // ── Save handler (batch-optimized) ──
  const isSavingRef = useRef(false);

  const handleSave = useCallback(async () => {
    if (isSavingRef.current) return;
    isSavingRef.current = true;
    setIsSaving(true);
    setSaveError(null);

    try {
      // 0. Save room-level properties (floor, wall color, heights, name)
      await updateRoom(roomId, {
        name: state.room.name,
        floorType: state.room.floorType,
        wallColor: state.room.wallColor,
        wallFinish: state.room.wallFinish,
        wallHeight: state.room.wallHeight,
        plinthHeight: state.room.plinthHeight,
        plinthThickness: state.room.plinthThickness,
        outletWidth: state.room.outletWidth,
        outletHeight: state.room.outletHeight,
      });

      // 1. Save walls first (bulk replace) to get server-assigned wall IDs
      const wallDtos = state.walls.map((w) => ({
        startX: w.startX,
        startY: w.startY,
        endX: w.endX,
        endY: w.endY,
        thickness: w.thickness,
        direction: w.direction,
      }));
      const serverWalls = await bulkUpdateWalls(roomId, wallDtos);

      // Build a map from local wall to server wall by matching coordinates
      const wallIdMap = new Map<string, string>();
      for (const localWall of state.walls) {
        const match = serverWalls.find(
          (sw) =>
            Math.abs(sw.startX - localWall.startX) < 0.001 &&
            Math.abs(sw.startY - localWall.startY) < 0.001 &&
            Math.abs(sw.endX - localWall.endX) < 0.001 &&
            Math.abs(sw.endY - localWall.endY) < 0.001,
        );
        if (match) {
          wallIdMap.set(localWall.id, match.id);
        }
      }

      // 2. Fetch current server state once for diff computation
      const freshRoom = await getRoomFull(roomId);

      // 3. Compute openings — since bulkUpdateWalls CASCADE-deletes all openings,
      // we must re-create ALL openings with the new server wall IDs
      const openingsCreate: CreateWallOpeningDto[] = [];
      const openingsUpdate: { id: string; data: UpdateWallOpeningDto }[] = [];
      const openingsDelete: string[] = [];

      for (const opening of state.openings) {
        const serverWallId = wallIdMap.get(opening.wallId) ?? opening.wallId;
        openingsCreate.push({
          wallId: serverWallId,
          type: opening.type,
          positionAlongWall: opening.positionAlongWall,
          width: opening.width,
          height: opening.height,
          elevationFromFloor: opening.elevationFromFloor,
          openDirection: opening.openDirection,
          positionAnchor: opening.positionAnchor,
          gridCols: opening.gridCols,
          gridRows: opening.gridRows,
          slopeDepth: opening.slopeDepth,
          frameThickness: opening.frameThickness,
        });
      }
      // No updates or deletes needed — cascade already removed all server openings

      // 4. Electrical items — since wall IDs changed after bulk replace,
      // delete all existing and re-create with correct wall IDs
      const elecCreate: CreateElectricalItemDto[] = [];
      const elecUpdate: { id: string; data: UpdateElectricalItemDto }[] = [];
      const elecDelete: string[] = freshRoom.electricalItems.map((e) => e.id);

      for (const elec of state.electricalItems) {
        const serverWallId = elec.wallId ? (wallIdMap.get(elec.wallId) ?? elec.wallId) : null;
        elecCreate.push({
          type: elec.type,
          x: elec.x,
          y: elec.y,
          wallId: serverWallId,
          elevationFromFloor: elec.elevationFromFloor,
          rotation: normalizeAngleDegrees(elec.rotation ?? 0),
          count: elec.count,
          positionAnchor: elec.positionAnchor,
          label: elec.label,
          metadata: elec.metadata,
        });
      }

      // 5. Compute diffs for furniture
      const serverFurnIds = new Set(freshRoom.furnitureItems.map((f) => f.id));
      const localFurnIds = new Set(state.furnitureItems.map((f) => f.id));

      const furnCreate: CreateFurnitureItemDto[] = [];
      const furnUpdate: { id: string; data: UpdateFurnitureItemDto }[] = [];
      const furnDelete: string[] = [];

      for (const furn of state.furnitureItems) {
        if (furn.id.startsWith('local-')) {
          furnCreate.push({
            type: furn.type,
            x: furn.x,
            y: furn.y,
            width: furn.width,
            depth: furn.depth,
            height: furn.height,
            rotation: furn.rotation,
            elevationFromFloor: furn.elevationFromFloor,
            label: furn.label,
            showProjection: furn.showProjection ?? false,
            opacity: furn.opacity ?? 1,
            positionAnchor: furn.positionAnchor,
            metadata: furn.metadata ?? null,
          });
        } else if (serverFurnIds.has(furn.id)) {
          const serverFurn = freshRoom.furnitureItems.find((f) => f.id === furn.id);
          if (serverFurn) {
            const anchorChanged =
              serverFurn.positionAnchor.horizontal !== furn.positionAnchor.horizontal ||
              serverFurn.positionAnchor.vertical !== furn.positionAnchor.vertical;
            const metadataChanged =
              JSON.stringify(serverFurn.metadata ?? null) !== JSON.stringify(furn.metadata ?? null);
            const hasChanges =
              serverFurn.x !== furn.x ||
              serverFurn.y !== furn.y ||
              serverFurn.width !== furn.width ||
              serverFurn.depth !== furn.depth ||
              serverFurn.height !== furn.height ||
              serverFurn.rotation !== furn.rotation ||
              serverFurn.elevationFromFloor !== furn.elevationFromFloor ||
              serverFurn.label !== furn.label ||
              (serverFurn.showProjection ?? false) !== (furn.showProjection ?? false) ||
              (serverFurn.opacity ?? 1) !== (furn.opacity ?? 1) ||
              anchorChanged ||
              metadataChanged;
            if (hasChanges) {
              furnUpdate.push({
                id: furn.id,
                data: {
                  type: furn.type,
                  x: furn.x,
                  y: furn.y,
                  width: furn.width,
                  depth: furn.depth,
                  height: furn.height,
                  rotation: normalizeAngleDegrees(furn.rotation ?? 0),
                  elevationFromFloor: furn.elevationFromFloor,
                  label: furn.label,
                  showProjection: furn.showProjection ?? false,
                  opacity: furn.opacity ?? 1,
                  positionAnchor: furn.positionAnchor,
                  metadata: furn.metadata ?? null,
                },
              });
            }
          }
        }
      }

      for (const serverFurn of freshRoom.furnitureItems) {
        if (!localFurnIds.has(serverFurn.id)) {
          furnDelete.push(serverFurn.id);
        }
      }

      // 6. Execute the 3 element batch calls in parallel — responses contain
      //    final server state. Annotations need to wait until after this so we
      //    can remap their attachedToId through the new server-side ids.
      const [syncedOpenings, syncedElectrical, syncedFurniture] = await Promise.all([
        batchSyncOpenings(roomId, {
          create: openingsCreate,
          update: openingsUpdate,
          delete: openingsDelete,
        }),
        batchSyncElectrical(roomId, {
          create: elecCreate,
          update: elecUpdate,
          delete: elecDelete,
        }),
        batchSyncFurniture(roomId, {
          create: furnCreate,
          update: furnUpdate,
          delete: furnDelete,
        }),
      ]);

      // 7. Build an id map (old local id → new server id) so the reducer can
      // preserve the user's selection across the bulk-replace save flow.
      // The server batch endpoints return items in non-deterministic order, so
      // we match by content, then consume each match exactly once.
      const idMap = new Map<string, string>();

      for (const [localId, serverId] of wallIdMap) {
        idMap.set(localId, serverId);
      }

      const consumedOpenings = new Set<string>();
      for (const local of state.openings) {
        const localServerWallId = wallIdMap.get(local.wallId) ?? local.wallId;
        const match = syncedOpenings.find(
          (o) =>
            !consumedOpenings.has(o.id) &&
            o.wallId === localServerWallId &&
            o.type === local.type &&
            Math.abs(o.positionAlongWall - local.positionAlongWall) < 0.001 &&
            Math.abs(o.width - local.width) < 0.001,
        );
        if (match) {
          consumedOpenings.add(match.id);
          idMap.set(local.id, match.id);
        }
      }

      const consumedElectrical = new Set<string>();
      for (const local of state.electricalItems) {
        const localServerWallId = local.wallId
          ? (wallIdMap.get(local.wallId) ?? local.wallId)
          : null;
        const match = syncedElectrical.find(
          (e) =>
            !consumedElectrical.has(e.id) &&
            e.type === local.type &&
            (e.wallId ?? null) === localServerWallId &&
            Math.abs(e.x - local.x) < 0.001 &&
            Math.abs(e.y - local.y) < 0.001,
        );
        if (match) {
          consumedElectrical.add(match.id);
          idMap.set(local.id, match.id);
        }
      }

      const consumedFurniture = new Set<string>();
      for (const local of state.furnitureItems) {
        if (!local.id.startsWith('local-') && syncedFurniture.some((f) => f.id === local.id)) {
          idMap.set(local.id, local.id);
          consumedFurniture.add(local.id);
          continue;
        }
        const match = syncedFurniture.find(
          (f) =>
            !consumedFurniture.has(f.id) &&
            f.type === local.type &&
            Math.abs(f.x - local.x) < 0.001 &&
            Math.abs(f.y - local.y) < 0.001 &&
            Math.abs(f.width - local.width) < 0.001 &&
            Math.abs(f.depth - local.depth) < 0.001,
        );
        if (match) {
          consumedFurniture.add(match.id);
          idMap.set(local.id, match.id);
        }
      }

      // 7b. Now that the id map is built, save annotations with attachedToId
      //     remapped to the new server-side item ids.
      const serverAnnIds = new Set((freshRoom.annotations ?? []).map((a) => a.id));
      const localAnnIds = new Set(state.annotations.map((a) => a.id));
      const annCreate: CreateAnnotationDto[] = [];
      const annUpdate: { id: string; data: UpdateAnnotationDto }[] = [];
      const annDelete: string[] = [];

      for (const ann of state.annotations) {
        const remappedAttachedTo = ann.attachedToId
          ? (idMap.get(ann.attachedToId) ?? ann.attachedToId)
          : null;
        if (ann.id.startsWith('local-') || !serverAnnIds.has(ann.id)) {
          annCreate.push({
            x: ann.x,
            y: ann.y,
            text: ann.text,
            fontSize: ann.fontSize,
            color: ann.color,
            attachedToId: remappedAttachedTo,
            projectionOffsetX: ann.projectionOffsetX,
            projectionOffsetY: ann.projectionOffsetY,
          });
        } else {
          annUpdate.push({
            id: ann.id,
            data: {
              x: ann.x,
              y: ann.y,
              text: ann.text,
              fontSize: ann.fontSize,
              color: ann.color,
              attachedToId: remappedAttachedTo,
              projectionOffsetX: ann.projectionOffsetX,
              projectionOffsetY: ann.projectionOffsetY,
            },
          });
        }
      }
      for (const id of serverAnnIds) {
        if (!localAnnIds.has(id)) annDelete.push(id);
      }

      const syncedAnnotations = await batchSyncAnnotations(roomId, {
        create: annCreate,
        update: annUpdate,
        delete: annDelete,
      });

      // 8. Sync state with server-assigned IDs (single dispatch, no flicker)
      dispatch({
        type: 'SYNC_SAVE',
        walls: serverWalls,
        openings: syncedOpenings,
        electricalItems: syncedElectrical,
        furnitureItems: syncedFurniture,
        annotations: syncedAnnotations,
        idMap,
      });

      // Mark state as clean after successful save
      lastSavedRef.current = {
        walls: serverWalls,
        openings: syncedOpenings,
        electricalItems: syncedElectrical,
        furnitureItems: syncedFurniture,
        room: state.room,
      };
    } catch (err: unknown) {
      const message = err instanceof Error ? err.message : t('editor.error.load');
      setSaveError(message);
    } finally {
      setIsSaving(false);
      isSavingRef.current = false;
    }
  }, [roomId, state.walls, state.openings, state.electricalItems, state.furnitureItems, state.annotations, state.room, dispatch]);

  // ── Auto-save with ref-based debounce ──
  const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => {
    // Clear any existing timer when dirty state or saving state changes
    if (autoSaveTimerRef.current !== null) {
      clearTimeout(autoSaveTimerRef.current);
      autoSaveTimerRef.current = null;
    }

    // Only start the timer if dirty and not currently saving
    if (!isDirty || isSavingRef.current) return;

    autoSaveTimerRef.current = setTimeout(() => {
      autoSaveTimerRef.current = null;
      // Guard: don't auto-save if a manual save is in progress
      if (isSavingRef.current) return;
      setIsAutoSaving(true);
      handleSave().finally(() => setIsAutoSaving(false));
    }, AUTO_SAVE_DELAY_MS);

    return () => {
      if (autoSaveTimerRef.current !== null) {
        clearTimeout(autoSaveTimerRef.current);
        autoSaveTimerRef.current = null;
      }
    };
  }, [isDirty, handleSave]);

  // ── Keyboard shortcuts ──
  useKeyboardShortcuts({ onSave: handleSave });

  // ── Import handler ──
  const [importError, setImportError] = useState<string | null>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const handleImportClick = useCallback(() => {
    setImportError(null);
    fileInputRef.current?.click();
  }, []);

  const handleImportFile = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      const file = event.target.files?.[0];
      if (!file) return;

      // Reset the input so the same file can be re-selected
      event.target.value = '';

      const reader = new FileReader();
      reader.onload = () => {
        try {
          const text = reader.result as string;
          const imported = importRoomFromJson(text);

          const confirmed = window.confirm(
            `Import room "${imported.room.name}"? This will replace all current walls, openings, electrical items, and furniture.`,
          );
          if (!confirmed) return;

          dispatch({
            type: 'IMPORT_ROOM',
            room: imported.room,
            walls: imported.walls,
            openings: imported.openings,
            electricalItems: imported.electricalItems,
            furnitureItems: imported.furnitureItems,
          });
          setImportError(null);
        } catch (err: unknown) {
          const message = err instanceof Error ? err.message : 'Failed to import file';
          setImportError(message);
        }
      };
      reader.onerror = () => {
        setImportError('Failed to read the selected file.');
      };
      reader.readAsText(file);
    },
    [dispatch],
  );

  return (
    <div className={styles.layout}>
      {/* Toolbar */}
      {/* Hidden file input for JSON import */}
      <input
        ref={fileInputRef}
        type="file"
        accept=".json"
        style={{ display: 'none' }}
        onChange={handleImportFile}
      />

      <EditorToolbar
        onSave={handleSave}
        isSaving={isSaving}
        isDirty={isDirty}
        isAutoSaving={isAutoSaving}
        onExport={() => setShowExport(true)}
        onImport={handleImportClick}
      />

      {saveError && (
        <div className={styles.saveError}>
          {t('editor.saveFailed', { error: saveError })}
        </div>
      )}

      {importError && (
        <div className={styles.saveError}>
          {t('editor.importFailed', { error: importError })}
        </div>
      )}

      {/* Main area: palette + canvas + properties panel */}
      <div className={styles.main}>
        <div className={styles.canvasArea}>
          {/* Overlay palettes (float over canvas, don't affect layout) */}
          {state.activeTool === 'electrical' && (
            <ElectricalPalette
              selectedIndex={state.selectedElectricalIndex}
              onSelect={(index) =>
                dispatch({ type: 'SET_ELECTRICAL_INDEX', index })
              }
            />
          )}
          {state.activeTool === 'furniture' && (
            <FurniturePalette
              selectedIndex={state.selectedFurnitureIndex}
              onSelect={(index) =>
                dispatch({ type: 'SET_FURNITURE_INDEX', index })
              }
            />
          )}
          {/* View mode toggle: 2D / 3D / Projections */}
          <div className={styles.viewToggle}>
            <button
              className={[styles.viewToggleBtn, viewMode === '2d' ? styles.viewToggleBtnActive : ''].join(' ')}
              onClick={() => setViewMode('2d')}
            >
              {t('toolbar.view2D')}
            </button>
            <button
              className={[styles.viewToggleBtn, viewMode === '3d' ? styles.viewToggleBtnActive : ''].join(' ')}
              onClick={() => setViewMode('3d')}
            >
              {t('toolbar.view3D')}
            </button>
            <button
              className={[styles.viewToggleBtn, viewMode === 'projections' ? styles.viewToggleBtnActive : ''].join(' ')}
              onClick={() => setViewMode('projections')}
            >
              {t('toolbar.viewProjections')}
            </button>
          </div>

          {viewMode === '3d' && (
            <div
              className={styles.canvasContainer}
              ref={(el) => {
                // Grab the R3F canvas element for 3D export
                if (el) {
                  const canvas = el.querySelector('canvas');
                  threeCanvasRef.current = canvas;
                } else {
                  threeCanvasRef.current = null;
                }
              }}
            >
              <Suspense fallback={<div className={styles.loading3D}>{t('editor.loading3D')}</div>}>
                <Room3DView />
              </Suspense>
            </div>
          )}

          <div
            ref={canvasContainerRef}
            className={styles.canvasContainer}
            style={viewMode !== '2d' ? { position: 'absolute', width: 0, height: 0, overflow: 'hidden', pointerEvents: 'none' } : undefined}
          >
            {/* Only mount the Konva stage once the container has been
                measured — rendering at a seed 800×600 and then re-rendering
                at the real size causes a visible flicker on open. */}
            {canvasSize && (
              <EditorCanvas
                width={canvasSize.width}
                height={canvasSize.height}
                onStageRef={handleMainStageRef}
              />
            )}
          </div>
          {viewMode === '2d' && (
            <CableLengthStatus electricalItems={state.electricalItems} />
          )}

          {/* ProjectionPanel always mounted for export, hidden when not active */}
          <div style={viewMode !== 'projections' ? { position: 'absolute', width: '800px', height: '400px', overflow: 'hidden', pointerEvents: 'none', opacity: 0, zIndex: -1 } : { display: 'contents' }}>
            <ProjectionPanel
              fullView={viewMode === 'projections'}
              onStageRef={handleProjectionStageRef}
            />
          </div>
        </div>
        <PropertiesPanel />
      </div>

      {/* Export Dialog */}
      <ExportDialog
        open={showExport}
        onClose={() => setShowExport(false)}
        mainStageRef={mainStageRef}
        projectionStageRefs={projectionStageMapRef}
        threeCanvasRef={threeCanvasRef}
        is3DView={viewMode === '3d'}
        viewMode={viewMode}
      />
    </div>
  );
}
