import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Stage, Layer, Rect, Line, Text, Group } from 'react-konva';
import type Konva from 'konva';
import type { Wall, WallOpening, ElectricalItem, FurnitureItem, ElectricalType, Annotation } from '@house-plan-maker/shared';
import { wallLength as computeWallLength } from '../utils/wallUtils';
import {
  projectionScale,
  projectionToPixel,
  pixelToProjection,
  projectOpenings,
  projectElectricalItems,
  projectFurnitureItems,
  computePlinthSegments,
  wallDirectionLabel,
  getProjectionAxis,
} from '../utils/projectionMapping';
import { ProjectionDoor } from './ProjectionDoor';
import { ProjectionWindow } from './ProjectionWindow';
import { ProjectionElectrical } from './ProjectionElectrical';
import { ProjectionFurniture } from './ProjectionFurniture';
import { ProjectionMeasurements } from './ProjectionMeasurements';
import type { EditorToolType } from '../types';
import { useCanvasColors } from '../utils/canvasThemeColors';

interface WallProjectionViewProps {
  readonly wall: Wall;
  /** All walls in the room — lets furniture projection assign each item
   *  to its closest wall so neighbour walls don't double-project items. */
  readonly walls?: readonly Wall[];
  readonly openings: readonly WallOpening[];
  readonly electricalItems: readonly ElectricalItem[];
  readonly furnitureItems: readonly FurnitureItem[];
  readonly annotations: readonly Annotation[];
  readonly globalFurnitureOpacity?: number;
  readonly showMeasurements?: boolean;
  readonly wallHeight: number;
  readonly plinthHeight: number;
  /** Distance (m) the stretch ceiling hangs below the structural ceiling. */
  readonly stretchCeilingOffset?: number;
  readonly outletWidth?: number;
  readonly outletHeight?: number;
  readonly selectedIds: ReadonlySet<string>;
  readonly isHighlighted: boolean;
  readonly onSelectElement: (id: string) => void;
  readonly width: number;
  readonly height: number;
  readonly onStageRef?: (wallId: string, stage: Konva.Stage | null) => void;
  readonly onUpdateElectrical?: (item: ElectricalItem) => void;
  readonly onUpdateOpening?: (opening: WallOpening) => void;
  readonly onUpdateAnnotation?: (annotation: Annotation) => void;
  readonly onEditAnnotation?: (annotationId: string) => void;
  readonly onPlaceElectrical?: (wallId: string, alongWall: number, fromFloor: number) => void;
  readonly onPlaceAnnotation?: (wallId: string, alongWall: number, fromFloor: number) => void;
  readonly activeTool?: EditorToolType;
  readonly selectedElectricalType?: ElectricalType | null;
  /** Multiplier applied to hardcoded Konva font sizes. Defaults to 1 for
   *  the interactive view; the hidden export renderer passes a larger
   *  value so text stays legible in high-resolution PDF exports. */
  readonly fontScale?: number;
}

const PADDING = 40;
const PLINTH_COLOR = '#8b7355';
// WALL_FILL and WALL_STROKE now come from useCanvasColors().
const FLOOR_COLOR = '#94a3b8';
const CEILING_COLOR = '#94a3b8';

/** Minimum pixel distance to distinguish a drag from a click. */
const DRAG_THRESHOLD = 4;

type DragKind = 'electrical-elevation' | 'opening-position';

interface DragInfo {
  readonly kind: DragKind;
  readonly itemId: string;
  readonly startPixelX: number;
  readonly startPixelY: number;
  /**
   * Projection-coord offset between the cursor and the item's stored
   * anchor at drag start. Subtracted from the live cursor position so
   * the drag respects where inside the item the user grabbed instead of
   * teleporting the anchor to the cursor (which made non-middle anchors
   * jump on drag start).
   */
  readonly offsetAlongWall: number;
  readonly offsetFromFloor: number;
  readonly exceeded: boolean;
}

export function WallProjectionView({
  wall,
  walls,
  openings,
  electricalItems,
  furnitureItems,
  annotations,
  globalFurnitureOpacity = 1,
  showMeasurements = true,
  wallHeight,
  plinthHeight,
  stretchCeilingOffset = 0,
  outletWidth,
  outletHeight,
  selectedIds,
  isHighlighted,
  onSelectElement,
  width,
  height,
  onStageRef,
  onUpdateElectrical,
  onUpdateOpening,
  onUpdateAnnotation,
  onEditAnnotation,
  onPlaceElectrical,
  onPlaceAnnotation,
  activeTool,
  selectedElectricalType,
  fontScale = 1,
}: WallProjectionViewProps) {
  const { t } = useTranslation();
  const colors = useCanvasColors();
  const stageRef = useRef<Konva.Stage | null>(null);

  // Expose stage ref to parent for export
  useEffect(() => {
    onStageRef?.(wall.id, stageRef.current);
  });

  // ── Zoom/Pan state ──
  const [viewZoom, setViewZoom] = useState(1);
  const [viewPan, setViewPan] = useState({ x: 0, y: 0 });
  const isPanningRef = useRef(false);
  const panStartRef = useRef({ x: 0, y: 0 });
  const panStartOffsetRef = useRef({ x: 0, y: 0 });
  const viewPanRef = useRef(viewPan);
  viewPanRef.current = viewPan;

  const wallLen = computeWallLength(wall);
  const projectionAxis = useMemo(() => getProjectionAxis(wall), [wall]);
  const baseScale = projectionScale(wallLen, wallHeight, width, height, PADDING);
  const effectiveScale = baseScale * viewZoom;

  // fontScale is supplied by the export renderer (RoomEditorLayout's
  // hidden per-wall stages). The interactive projection view leaves it at
  // the default of 1 — we don't auto-scale by stage size because the
  // interactive stage can be large on wide monitors.

  // ── Drag state (refs for transient data, state only for visual feedback) ──
  const dragRef = useRef<DragInfo | null>(null);
  const [dragElectricalFromFloor, setDragElectricalFromFloor] = useState<{ itemId: string; fromFloor: number } | null>(null);
  const [dragElectricalAlongWall, setDragElectricalAlongWall] = useState<{ itemId: string; alongWall: number } | null>(null);
  const [dragOpeningAlongWall, setDragOpeningAlongWall] = useState<{ openingId: string; alongWall: number } | null>(null);

  // ── Measure tool state ──
  // Stored in projection coordinates: `alongWall` = meters from the
  // canonical wall start, `fromFloor` = meters from the floor. Distance
  // is the Euclidean distance between the two endpoints in those units,
  // i.e. real-world metres on the wall surface.
  interface ProjectionMeasurement {
    readonly start: { alongWall: number; fromFloor: number };
    readonly end: { alongWall: number; fromFloor: number };
  }
  const [measurement, setMeasurement] = useState<ProjectionMeasurement | null>(null);
  const isMeasuringRef = useRef(false);

  // Clear the measurement when the user switches to a different tool or
  // the wall changes — otherwise stale lines persist between unrelated
  // interactions.
  useEffect(() => {
    if (activeTool !== 'measure') {
      setMeasurement(null);
      isMeasuringRef.current = false;
    }
  }, [activeTool]);
  useEffect(() => {
    setMeasurement(null);
    isMeasuringRef.current = false;
  }, [wall.id]);

  // ── Projected data (memoized) ──
  const projectedOpenings = useMemo(
    () => projectOpenings(wall, openings),
    [wall, openings],
  );
  const projectedElectrical = useMemo(
    () => projectElectricalItems(wall, electricalItems, walls),
    [wall, electricalItems, walls],
  );
  const projectedFurniture = useMemo(
    () => projectFurnitureItems(wall, furnitureItems, 0.15, walls),
    [wall, furnitureItems, walls],
  );
  const plinthSegments = useMemo(
    () => computePlinthSegments(wall, openings, plinthHeight),
    [wall, openings, plinthHeight],
  );

  // ── Helper to get pixel position relative to stage ──
  const getStagePointer = useCallback((evt: MouseEvent): { x: number; y: number } | null => {
    const stage = stageRef.current;
    if (!stage) return null;
    const container = stage.container();
    const rect = container.getBoundingClientRect();
    return {
      x: evt.clientX - rect.left,
      y: evt.clientY - rect.top,
    };
  }, []);

  // ── Electrical drag start ──
  const handleElectricalDragStart = useCallback((itemId: string, evt: MouseEvent) => {
    if (!onUpdateElectrical) return;
    const item = electricalItems.find((i) => i.id === itemId);
    if (!item) return;
    const pointer = getStagePointer(evt);
    if (!pointer) return;
    const proj = pixelToProjection(
      pointer.x - viewPanRef.current.x,
      pointer.y - viewPanRef.current.y,
      wallHeight,
      effectiveScale,
      PADDING,
    );
    // Project the item's stored (x, y) anchor onto the canonical wall axis
    // so we can compute the cursor's offset from that anchor at drag start.
    const wLen = wallLen || 1;
    const dx = (projectionAxis.end.x - projectionAxis.start.x) / wLen;
    const dy = (projectionAxis.end.y - projectionAxis.start.y) / wLen;
    const itemAlongWall = (item.x - projectionAxis.start.x) * dx + (item.y - projectionAxis.start.y) * dy;
    const itemFromFloor = item.elevationFromFloor ?? 0;
    dragRef.current = {
      kind: 'electrical-elevation',
      itemId,
      startPixelX: evt.clientX,
      startPixelY: evt.clientY,
      offsetAlongWall: proj.alongWall - itemAlongWall,
      offsetFromFloor: proj.fromFloor - itemFromFloor,
      exceeded: false,
    };
  }, [onUpdateElectrical, electricalItems, getStagePointer, projectionAxis, wallLen, wallHeight, effectiveScale]);

  // ── Opening drag start ──
  const handleOpeningDragStart = useCallback((openingId: string, evt: MouseEvent) => {
    if (!onUpdateOpening) return;
    const opening = openings.find((o) => o.id === openingId);
    if (!opening) return;
    const pointer = getStagePointer(evt);
    if (!pointer) return;
    const proj = pixelToProjection(
      pointer.x - viewPanRef.current.x,
      pointer.y - viewPanRef.current.y,
      wallHeight,
      effectiveScale,
      PADDING,
    );
    // Opening's projected alongWall takes axis flipping into account so
    // the offset stays consistent with how the symbol is rendered.
    const projectedAlongWall = projectionAxis.flipped
      ? (wallLen - opening.positionAlongWall)
      : opening.positionAlongWall;
    dragRef.current = {
      kind: 'opening-position',
      itemId: openingId,
      startPixelX: evt.clientX,
      startPixelY: evt.clientY,
      offsetAlongWall: proj.alongWall - projectedAlongWall,
      // Openings only drag horizontally for now, but populate the field for
      // type completeness so the DragInfo invariant holds.
      offsetFromFloor: 0,
      exceeded: false,
    };
  }, [onUpdateOpening, openings, getStagePointer, projectionAxis, wallLen, wallHeight, effectiveScale]);

  // ── Zoom handler ──
  const handleWheel = useCallback((e: Konva.KonvaEventObject<WheelEvent>) => {
    e.evt.preventDefault();
    const stage = e.target.getStage();
    if (!stage) return;

    const pointerPos = stage.getPointerPosition();
    if (!pointerPos) return;

    // Content is inside a Group with x=viewPan.x, y=viewPan.y.
    // projectionToPixel gives: contentX = PADDING + worldX * effectiveScale
    // On screen: screenX = contentX + viewPan.x
    // So: worldX = (screenX - viewPan.x - PADDING) / effectiveScale

    const oldScale = effectiveScale;
    const worldX = (pointerPos.x - viewPan.x - PADDING) / oldScale;
    const worldY = (pointerPos.y - viewPan.y - PADDING) / oldScale;

    const delta = e.evt.deltaY;
    const factor = delta > 0 ? 0.9 : 1.1;
    const newZoom = Math.max(0.3, Math.min(5, viewZoom * factor));
    const newScale = baseScale * newZoom;

    // Keep world point under cursor: screenX = PADDING + worldX * newScale + newPanX
    const newPan = {
      x: pointerPos.x - PADDING - worldX * newScale,
      y: pointerPos.y - PADDING - worldY * newScale,
    };

    setViewZoom(newZoom);
    setViewPan(newPan);
  }, [effectiveScale, viewPan, viewZoom, baseScale]);

  // ── Pan handlers ──
  const handleMouseDown = useCallback((e: Konva.KonvaEventObject<MouseEvent>) => {
    // Measure tool takes precedence over pan: a left-click on the empty
    // wall (not on an interactive item) starts a new measurement rather
    // than panning. Middle-click still pans in measure mode so the user
    // can reposition the view between measurements.
    if (activeTool === 'measure' && e.evt.button === 0) {
      const targetName = (e.target as { name?: () => string })?.name?.() ?? '';
      const isOnItem = e.target !== e.target.getStage() && targetName !== 'wall-bg';
      if (!isOnItem) {
        const pointer = getStagePointer(e.evt);
        if (pointer) {
          const proj = pixelToProjection(
            pointer.x - viewPanRef.current.x,
            pointer.y - viewPanRef.current.y,
            wallHeight,
            effectiveScale,
            PADDING,
          );
          setMeasurement({
            start: { alongWall: proj.alongWall, fromFloor: proj.fromFloor },
            end: { alongWall: proj.alongWall, fromFloor: proj.fromFloor },
          });
          isMeasuringRef.current = true;
          e.evt.preventDefault();
          return;
        }
      }
    }

    // Middle mouse or left mouse on empty space = pan
    const inPlacementMode = activeTool === 'electrical' && selectedElectricalType != null;
    if (e.evt.button === 1 || (e.evt.button === 0 && !inPlacementMode)) {
      // Don't pan if clicking on an interactive element (electrical/opening/furniture)
      const targetName = (e.target as { name?: () => string })?.name?.() ?? '';
      const isOnItem = e.target !== e.target.getStage() && targetName !== 'wall-bg';
      if (e.evt.button === 0 && isOnItem) {
        // Let the click fall through to item handlers
      } else {
        isPanningRef.current = true;
        panStartRef.current = { x: e.evt.clientX, y: e.evt.clientY };
        panStartOffsetRef.current = { ...viewPan };
        e.evt.preventDefault();
        return;
      }
    }

    // Left click on wall-bg in placement mode: place electrical
    if (e.evt.button === 0 && e.target === e.currentTarget?.getStage()?.findOne('.wall-bg')) {
      // This is handled in the wall-bg rect click
    }
  }, [activeTool, selectedElectricalType, viewPan, wallHeight, effectiveScale, getStagePointer]);

  const handleMouseMove = useCallback((e: Konva.KonvaEventObject<MouseEvent>) => {
    // Handle panning
    if (isPanningRef.current) {
      const dx = e.evt.clientX - panStartRef.current.x;
      const dy = e.evt.clientY - panStartRef.current.y;
      setViewPan({
        x: panStartOffsetRef.current.x + dx,
        y: panStartOffsetRef.current.y + dy,
      });
      return;
    }

    // Update live measurement endpoint while the mouse is held down.
    if (isMeasuringRef.current) {
      const pointer = getStagePointer(e.evt);
      if (pointer) {
        const proj = pixelToProjection(
          pointer.x - viewPanRef.current.x,
          pointer.y - viewPanRef.current.y,
          wallHeight,
          effectiveScale,
          PADDING,
        );
        setMeasurement((prev) =>
          prev
            ? {
                start: prev.start,
                end: { alongWall: proj.alongWall, fromFloor: proj.fromFloor },
              }
            : prev,
        );
      }
      return;
    }

    // Handle dragging
    const drag = dragRef.current;
    if (!drag) return;

    const dx = e.evt.clientX - drag.startPixelX;
    const dy = e.evt.clientY - drag.startPixelY;
    const dist = Math.sqrt(dx * dx + dy * dy);

    if (!drag.exceeded && dist < DRAG_THRESHOLD) return;

    if (!drag.exceeded) {
      dragRef.current = { ...drag, exceeded: true };
    }

    const pointer = getStagePointer(e.evt);
    if (!pointer) return;

    const proj = pixelToProjection(pointer.x - viewPanRef.current.x, pointer.y - viewPanRef.current.y, wallHeight, effectiveScale, PADDING);

    // Subtract the grab offset so the item moves with the cursor delta
    // instead of teleporting its anchor to the cursor. This matters for
    // non-middle anchors and for grabbing near an item edge.
    const targetAlongWall = proj.alongWall - drag.offsetAlongWall;
    const targetFromFloor = proj.fromFloor - drag.offsetFromFloor;

    if (drag.kind === 'electrical-elevation') {
      if (e.evt.ctrlKey || e.evt.metaKey) {
        // Ctrl+drag: move horizontally along wall
        const clampedAlongWall = Math.max(0, Math.min(wallLen, targetAlongWall));
        setDragElectricalAlongWall({ itemId: drag.itemId, alongWall: clampedAlongWall });
        setDragElectricalFromFloor(null);
      } else {
        // Normal drag: move vertically (elevation)
        const clampedFromFloor = Math.max(0, Math.min(wallHeight, targetFromFloor));
        setDragElectricalFromFloor({ itemId: drag.itemId, fromFloor: clampedFromFloor });
        setDragElectricalAlongWall(null);
      }
    } else if (drag.kind === 'opening-position') {
      const opening = openings.find((o) => o.id === drag.itemId);
      const halfWidth = opening ? opening.width / 2 : 0;
      const clampedAlongWall = Math.max(halfWidth, Math.min(wallLen - halfWidth, targetAlongWall));
      setDragOpeningAlongWall({ openingId: drag.itemId, alongWall: clampedAlongWall });
    }
  }, [getStagePointer, wallHeight, effectiveScale, wallLen, openings]);

  const handleMouseUp = useCallback(() => {
    isPanningRef.current = false;

    // Release the measurement drag but keep the rendered line on screen
    // so the user can read the value. The next left-click on empty space
    // in measure mode will start a fresh measurement.
    if (isMeasuringRef.current) {
      isMeasuringRef.current = false;
      // Clear a zero-length measurement (single click without drag).
      setMeasurement((prev) => {
        if (!prev) return prev;
        const dAlong = prev.end.alongWall - prev.start.alongWall;
        const dUp = prev.end.fromFloor - prev.start.fromFloor;
        if (Math.hypot(dAlong, dUp) < 0.01) return null;
        return prev;
      });
    }

    const drag = dragRef.current;
    if (!drag) return;

    dragRef.current = null;

    // If threshold was not exceeded, this was a click, not a drag -- do nothing extra
    if (!drag.exceeded) {
      setDragElectricalFromFloor(null);
      setDragElectricalAlongWall(null);
      setDragOpeningAlongWall(null);
      return;
    }

    // Commit the drag
    if (drag.kind === 'electrical-elevation' && onUpdateElectrical) {
      const item = electricalItems.find((i) => i.id === drag.itemId);
      if (item) {
        if (dragElectricalAlongWall) {
          // Horizontal drag: compute new x,y from canonical alongWall position
          const { start, end } = projectionAxis;
          const wLen = wallLen || 1;
          const t = dragElectricalAlongWall.alongWall / wLen;
          onUpdateElectrical({
            ...item,
            x: start.x + (end.x - start.x) * t,
            y: start.y + (end.y - start.y) * t,
          });
        } else if (dragElectricalFromFloor) {
          onUpdateElectrical({
            ...item,
            elevationFromFloor: Math.round(dragElectricalFromFloor.fromFloor * 100) / 100,
          });
        }
      }
    } else if (drag.kind === 'opening-position' && dragOpeningAlongWall && onUpdateOpening) {
      const opening = openings.find((o) => o.id === drag.itemId);
      if (opening) {
        // Map canonical projection position back to storage (relative to wall.start)
        const storedPos = projectionAxis.flipped
          ? wallLen - dragOpeningAlongWall.alongWall
          : dragOpeningAlongWall.alongWall;
        onUpdateOpening({
          ...opening,
          positionAlongWall: Math.round(storedPos * 100) / 100,
        });
      }
    }

    setDragElectricalFromFloor(null);
    setDragElectricalAlongWall(null);
    setDragOpeningAlongWall(null);
  }, [dragElectricalFromFloor, dragElectricalAlongWall, dragOpeningAlongWall, electricalItems, openings, projectionAxis, wallLen, onUpdateElectrical, onUpdateOpening]);

  // ── Handle click on wall background for placement ──
  const handleWallBgClick = useCallback((e: Konva.KonvaEventObject<MouseEvent>) => {
    const pointer = getStagePointer(e.evt);
    if (!pointer) return;
    const proj = pixelToProjection(
      pointer.x - viewPanRef.current.x,
      pointer.y - viewPanRef.current.y,
      wallHeight,
      effectiveScale,
      PADDING,
    );

    if (activeTool === 'annotate' && onPlaceAnnotation) {
      // Annotations may sit anywhere — including just outside the wall
      // rectangle for labels — so don't clip the click to the wall span.
      onPlaceAnnotation(wall.id, proj.alongWall, proj.fromFloor);
      return;
    }

    if (activeTool !== 'electrical' || !selectedElectricalType || !onPlaceElectrical) return;

    // Only place electrical within wall bounds
    if (proj.alongWall < 0 || proj.alongWall > wallLen) return;
    if (proj.fromFloor < 0 || proj.fromFloor > wallHeight) return;

    onPlaceElectrical(wall.id, proj.alongWall, proj.fromFloor);
  }, [activeTool, selectedElectricalType, onPlaceElectrical, onPlaceAnnotation, getStagePointer, wallHeight, effectiveScale, wallLen, wall.id]);

  // ── Reset zoom when the *physical* wall changes ──
  // We key on the wall's start/end coords rather than id so a save (which
  // re-creates walls with new ids) does not wipe the user's pan/zoom.
  const wallKey = `${wall.startX},${wall.startY},${wall.endX},${wall.endY}`;
  useEffect(() => {
    setViewZoom(1);
    setViewPan({ x: 0, y: 0 });
  }, [wallKey]);

  // ── Coordinate helpers ──
  const toPixel = useCallback(
    (alongWall: number, fromFloor: number) =>
      projectionToPixel(alongWall, fromFloor, wallHeight, effectiveScale, PADDING),
    [wallHeight, effectiveScale],
  );

  // Wall rectangle corners
  const topLeft = toPixel(0, wallHeight);
  const topRight = toPixel(wallLen, wallHeight);
  const bottomLeft = toPixel(0, 0);
  const bottomRight = toPixel(wallLen, 0);

  const label = wallDirectionLabel(wall);
  const isPlacementMode = activeTool === 'electrical' && selectedElectricalType != null;

  return (
    <Stage
      ref={stageRef}
      width={width}
      height={height}
      onWheel={handleWheel}
      onMouseDown={handleMouseDown}
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}
      style={{ cursor: isPlacementMode ? 'crosshair' : 'default' }}
    >
      <Layer>
        {/* Background */}
        <Rect
          x={0}
          y={0}
          width={width}
          height={height}
          fill={isHighlighted ? colors.canvasBgHighlight : colors.canvasBg}
          listening={false}
        />

        {/* Pan group: all content shifts with viewPan */}
        <Group x={viewPan.x} y={viewPan.y}>

        {/* Grid lines */}
        {(() => {
          const gridSize = 0.1; // 10cm grid
          const gridPixels = gridSize * effectiveScale;
          if (gridPixels < 6) return null; // too dense, skip

          const lines: React.ReactNode[] = [];
          const majorEvery = 5; // every 0.5m = bold line

          // Vertical grid lines (along wall)
          const startAlongWall = Math.floor(0 / gridSize) * gridSize;
          const endAlongWall = Math.ceil(wallLen / gridSize) * gridSize;
          for (let w = startAlongWall; w <= endAlongWall; w += gridSize) {
            const px = PADDING + w * effectiveScale;
            const isMajor = Math.abs(Math.round(w / gridSize) % majorEvery) < 0.01;
            lines.push(
              <Line
                key={`gv-${w}`}
                points={[px, PADDING, px, PADDING + wallHeight * effectiveScale]}
                stroke={isMajor ? colors.gridLineMajor : colors.gridLine}
                strokeWidth={isMajor ? 0.8 : 0.4}
                listening={false}
              />,
            );
          }

          // Horizontal grid lines (height from floor)
          const startHeight = 0;
          const endHeight = Math.ceil(wallHeight / gridSize) * gridSize;
          for (let h = startHeight; h <= endHeight; h += gridSize) {
            const py = PADDING + (wallHeight - h) * effectiveScale;
            const isMajor = Math.abs(Math.round(h / gridSize) % majorEvery) < 0.01;
            lines.push(
              <Line
                key={`gh-${h}`}
                points={[PADDING, py, PADDING + wallLen * effectiveScale, py]}
                stroke={isMajor ? colors.gridLineMajor : colors.gridLine}
                strokeWidth={isMajor ? 0.8 : 0.4}
                listening={false}
              />,
            );
          }

          return <>{lines}</>;
        })()}

        {/* Ruler ticks along bottom (wall length) */}
        {(() => {
          const step = 0.5; // every 0.5m
          const ticks: React.ReactNode[] = [];
          for (let w = 0; w <= wallLen; w += step) {
            const px = PADDING + w * effectiveScale;
            const py = PADDING + wallHeight * effectiveScale;
            ticks.push(
              <Line
                key={`rt-${w}`}
                points={[px, py, px, py + 6]}
                stroke={colors.rulerText}
                strokeWidth={0.8}
                listening={false}
              />,
            );
            ticks.push(
              <Text
                key={`rl-${w}`}
                x={px - 10}
                y={py + 7}
                width={20}
                text={`${w}`}
                align="center"
                fontSize={8 * fontScale}
                fill={colors.rulerText}
              />,
            );
          }
          return <>{ticks}</>;
        })()}

        {/* Ruler ticks along left (height from floor) */}
        {(() => {
          const step = 0.5;
          const ticks: React.ReactNode[] = [];
          for (let h = 0; h <= wallHeight; h += step) {
            const py = PADDING + (wallHeight - h) * effectiveScale;
            ticks.push(
              <Line
                key={`ht-${h}`}
                points={[PADDING - 6, py, PADDING, py]}
                stroke={colors.rulerText}
                strokeWidth={0.8}
                listening={false}
              />,
            );
            ticks.push(
              <Text
                key={`hl-${h}`}
                x={PADDING - 28}
                y={py - 5}
                width={20}
                text={`${h}`}
                align="right"
                fontSize={8 * fontScale}
                fill={colors.rulerText}
              />,
            );
          }
          return <>{ticks}</>;
        })()}

        {/* Wall rectangle fill (clickable for placement) */}
        <Rect
          name="wall-bg"
          x={topLeft.x}
          y={topLeft.y}
          width={(topRight.x - topLeft.x)}
          height={(bottomLeft.y - topLeft.y)}
          fill={colors.wallFill}
          stroke={isHighlighted ? colors.selectedStroke : colors.wallStroke}
          strokeWidth={isHighlighted ? 2 : 1.5}
          onClick={handleWallBgClick}
        />

        {/* Ceiling line */}
        <Line
          points={[topLeft.x, topLeft.y, topRight.x, topRight.y]}
          stroke={CEILING_COLOR}
          strokeWidth={1}
          dash={[6, 4]}
        />

        {/* Stretch ceiling line — drawn *below* the structural ceiling at
            `wallHeight - stretchCeilingOffset`. A tinted band fills the
            dropped zone so the user can see how much vertical space is
            lost. Only rendered when the offset is non-zero. */}
        {stretchCeilingOffset > 0 && (() => {
          const stretchY = Math.max(0, wallHeight - stretchCeilingOffset);
          const stretchLeft = toPixel(0, stretchY);
          const stretchRight = toPixel(wallLen, stretchY);
          return (
            <>
              {/* Tinted fill for the dropped zone between the structural
                  ceiling and the stretch ceiling plane. */}
              <Rect
                x={topLeft.x}
                y={topLeft.y}
                width={topRight.x - topLeft.x}
                height={stretchLeft.y - topLeft.y}
                fill="#60a5fa"
                opacity={0.18}
                listening={false}
              />
              <Line
                points={[stretchLeft.x, stretchLeft.y, stretchRight.x, stretchRight.y]}
                stroke="#2563eb"
                strokeWidth={1.25}
                dash={[4, 3]}
                listening={false}
              />
              <Text
                x={stretchLeft.x + 4}
                y={stretchLeft.y - 12}
                text={`${t('properties.stretchCeilingOffset')}: ${(stretchCeilingOffset * 100).toFixed(1)}cm`}
                fontSize={9 * fontScale}
                fill="#2563eb"
                listening={false}
              />
            </>
          );
        })()}

        {/* Floor line */}
        <Line
          points={[bottomLeft.x, bottomLeft.y, bottomRight.x, bottomRight.y]}
          stroke={FLOOR_COLOR}
          strokeWidth={2}
        />

        {/* Plinth segments */}
        {plinthSegments.map((seg, i) => {
          const segTopLeft = toPixel(seg.x, seg.height);
          const pxWidth = seg.width * effectiveScale;
          const pxHeight = seg.height * effectiveScale;
          return (
            <Rect
              key={`plinth-${i}`}
              x={segTopLeft.x}
              y={segTopLeft.y}
              width={pxWidth}
              height={pxHeight}
              fill={PLINTH_COLOR}
              stroke="#6b5b3a"
              strokeWidth={0.5}
              opacity={0.8}
            />
          );
        })}

        {/* Doors */}
        {projectedOpenings
          .filter((po) => po.opening.type === 'DOOR')
          .map((po) => {
            const isDragging = dragOpeningAlongWall?.openingId === po.opening.id;
            return (
              <ProjectionDoor
                key={po.opening.id}
                projected={po}
                wallHeight={wallHeight}
                scale={effectiveScale}
                padding={PADDING}
                isSelected={selectedIds.has(po.opening.id)}
                isDragging={isDragging}
                dragAlongWall={isDragging ? dragOpeningAlongWall?.alongWall : undefined}
                onClick={() => onSelectElement(po.opening.id)}
                onDragStart={onUpdateOpening ? handleOpeningDragStart : undefined}
              />
            );
          })}

        {/* Windows */}
        {projectedOpenings
          .filter((po) => po.opening.type === 'WINDOW')
          .map((po) => {
            const isDragging = dragOpeningAlongWall?.openingId === po.opening.id;
            return (
              <ProjectionWindow
                key={po.opening.id}
                projected={po}
                wallHeight={wallHeight}
                scale={effectiveScale}
                padding={PADDING}
                isSelected={selectedIds.has(po.opening.id)}
                isDragging={isDragging}
                dragAlongWall={isDragging ? dragOpeningAlongWall?.alongWall : undefined}
                onClick={() => onSelectElement(po.opening.id)}
                onDragStart={onUpdateOpening ? handleOpeningDragStart : undefined}
              />
            );
          })}

        {/* Furniture items (rendered first so electrical overlays them).
            When global opacity is effectively zero the items are invisible,
            so don't wire click handlers — clicks should fall through to
            whatever is underneath (wall background, electrical, …). */}
        {projectedFurniture.map((pf) => {
          const furnitureInteractive = globalFurnitureOpacity >= 0.05;
          return (
            <ProjectionFurniture
              key={pf.item.id}
              projected={pf}
              wallHeight={wallHeight}
              scale={effectiveScale}
              padding={PADDING}
              isSelected={selectedIds.has(pf.item.id)}
              globalOpacity={globalFurnitureOpacity}
              onClick={furnitureInteractive ? () => onSelectElement(pf.item.id) : undefined}
            />
          );
        })}

        {/* Electrical items (on top of furniture) */}
        {projectedElectrical.map((pe) => {
          const isDraggingV = dragElectricalFromFloor?.itemId === pe.item.id;
          const isDraggingH = dragElectricalAlongWall?.itemId === pe.item.id;
          return (
            <ProjectionElectrical
              key={pe.item.id}
              projected={pe}
              wallHeight={wallHeight}
              scale={effectiveScale}
              padding={PADDING}
              isSelected={selectedIds.has(pe.item.id)}
              isDragging={isDraggingV || isDraggingH}
              dragFromFloor={isDraggingV ? dragElectricalFromFloor?.fromFloor : undefined}
              dragAlongWall={isDraggingH ? dragElectricalAlongWall?.alongWall : undefined}
              outletWidth={outletWidth}
              outletHeight={outletHeight}
              onClick={() => onSelectElement(pe.item.id)}
              onDragStart={onUpdateElectrical ? handleElectricalDragStart : undefined}
            />
          );
        })}

        {/* Measure tool overlay — a dashed line from start to end with a
            distance label. Rendered above the items but below annotations.
            Dimensions are in wall-surface metres (√((Δalong)² + (Δup)²)). */}
        {measurement && (() => {
          const startPx = projectionToPixel(
            measurement.start.alongWall,
            measurement.start.fromFloor,
            wallHeight,
            effectiveScale,
            PADDING,
          );
          const endPx = projectionToPixel(
            measurement.end.alongWall,
            measurement.end.fromFloor,
            wallHeight,
            effectiveScale,
            PADDING,
          );
          const dAlong = measurement.end.alongWall - measurement.start.alongWall;
          const dUp = measurement.end.fromFloor - measurement.start.fromFloor;
          const distM = Math.hypot(dAlong, dUp);
          const midX = (startPx.x + endPx.x) / 2;
          const midY = (startPx.y + endPx.y) / 2;
          const label = distM >= 1 ? `${distM.toFixed(2)} m` : `${(distM * 100).toFixed(1)} cm`;
          return (
            <Group listening={false}>
              <Line
                points={[startPx.x, startPx.y, endPx.x, endPx.y]}
                stroke="#e74c3c"
                strokeWidth={1.5}
                dash={[8, 4]}
              />
              {/* Endpoint dots */}
              <Rect x={startPx.x - 3} y={startPx.y - 3} width={6} height={6} fill="#e74c3c" cornerRadius={3} />
              <Rect x={endPx.x - 3} y={endPx.y - 3} width={6} height={6} fill="#e74c3c" cornerRadius={3} />
              {distM > 0.005 && (
                <Text
                  x={midX + 8}
                  y={midY - 14}
                  text={label}
                  fontSize={12 * fontScale}
                  fontFamily="sans-serif"
                  fontStyle="bold"
                  fill="#e74c3c"
                  padding={2}
                />
              )}
            </Group>
          );
        })()}

        {/* Measurements (and per-furniture projection overlay, which renders
            independently so it stays visible even when measurements are hidden) */}
        <ProjectionMeasurements
          projectedOpenings={showMeasurements ? projectedOpenings : []}
          projectedElectrical={showMeasurements ? projectedElectrical : []}
          projectedFurniture={projectedFurniture}
          wallLength={wallLen}
          wallHeight={wallHeight}
          scale={effectiveScale}
          padding={PADDING}
          outletWidth={outletWidth}
          outletHeight={outletHeight}
          showWallDimensions={showMeasurements}
          fontScale={fontScale}
        />

        {/* Attached annotations for items on this wall — interactive.
            Also includes annotations attached directly to this wall (created
            via the annotate tool inside the projection view). */}
        {annotations
          .filter((ann) => {
            if (!ann.attachedToId) return false;
            if (ann.attachedToId === wall.id) return true;
            return projectedElectrical.some((pe) => pe.item.id === ann.attachedToId) ||
                   projectedFurniture.some((pf) => pf.item.id === ann.attachedToId);
          })
          .map((ann) => {
            const elec = projectedElectrical.find((pe) => pe.item.id === ann.attachedToId);
            const furn = projectedFurniture.find((pf) => pf.item.id === ann.attachedToId);
            const isWallAnchored = ann.attachedToId === wall.id;
            let anchorAlongWall = 0;
            let anchorFromFloor = 0;
            if (isWallAnchored) {
              // Wall-anchored annotations store their projection-space
              // anchor point in (x, y) — not a delta from another item.
              anchorAlongWall = ann.x;
              anchorFromFloor = ann.y;
            } else if (elec) {
              anchorAlongWall = elec.position.alongWall;
              anchorFromFloor = elec.position.fromFloor;
            } else if (furn) {
              anchorAlongWall = furn.rect.x + furn.rect.width / 2;
              anchorFromFloor = furn.rect.y + furn.rect.height;
            }
            const anchorPx = projectionToPixel(anchorAlongWall, anchorFromFloor, wallHeight, effectiveScale, PADDING);
            // Use the dedicated projection offset (defaulting to a small offset
            // up & to the right of the anchor) so dragging in this view does
            // not corrupt the floor-plan offset stored in ann.x/ann.y.
            const projOffsetX = ann.projectionOffsetX ?? 0.3;
            const projOffsetY = ann.projectionOffsetY ?? -0.4;
            const textX = anchorPx.x + projOffsetX * effectiveScale;
            const textY = anchorPx.y + projOffsetY * effectiveScale;
            const isSelected = selectedIds.has(ann.id);
            const fontSize = (ann.fontSize ?? 10) * fontScale;
            const boxWidth = ann.text.length * (fontSize * 0.6) + 6;
            const boxHeight = fontSize + 4;
            // URL detection mirrors the floor-plan AnnotationLayer so links
            // get the same blue tint and Ctrl/Cmd-click affordance in both
            // views. Anchored to start/end so "see http://x" stays plain.
            const annText = ann.text.trim();
            const isLink = /^https?:\/\/\S+$/i.test(annText);
            const linkColor = '#2563eb';
            const textFill = isSelected
              ? '#2563eb'
              : isLink
                ? (ann.color ?? linkColor)
                : (ann.color ?? '#334155');
            return (
              <Group key={ann.id}>
                <Line
                  points={[anchorPx.x, anchorPx.y, textX + boxWidth / 2, textY + boxHeight / 2]}
                  stroke={isSelected ? '#2563eb' : '#94a3b8'}
                  strokeWidth={0.6}
                  dash={[2, 2]}
                  listening={false}
                />
                <Group
                  x={textX}
                  y={textY}
                  draggable={onUpdateAnnotation != null}
                  onClick={(e) => {
                    if (isLink && (e.evt.ctrlKey || e.evt.metaKey)) {
                      e.cancelBubble = true;
                      window.open(annText, '_blank', 'noopener,noreferrer');
                      return;
                    }
                    onSelectElement(ann.id);
                  }}
                  onDblClick={() => onEditAnnotation?.(ann.id)}
                  onDragEnd={(e) => {
                    if (!onUpdateAnnotation) return;
                    // The Group's parent is the pan group, so node.x()/y() are
                    // already in the same coordinate system as anchorPx (no
                    // need to compensate for viewPan).
                    const newOffsetX = (e.target.x() - anchorPx.x) / effectiveScale;
                    const newOffsetY = (e.target.y() - anchorPx.y) / effectiveScale;
                    onUpdateAnnotation({
                      ...ann,
                      projectionOffsetX: Math.round(newOffsetX * 1000) / 1000,
                      projectionOffsetY: Math.round(newOffsetY * 1000) / 1000,
                    });
                  }}
                >
                  <Rect
                    x={-2}
                    y={-1}
                    width={boxWidth}
                    height={boxHeight}
                    fill="rgba(255,255,255,0.92)"
                    stroke={isSelected ? '#2563eb' : 'transparent'}
                    strokeWidth={isSelected ? 1 : 0}
                    cornerRadius={2}
                  />
                  <Text
                    text={ann.text}
                    fontSize={fontSize}
                    fill={textFill}
                  />
                </Group>
              </Group>
            );
          })
        }

        {/* Wall label (inside pan group) */}
        <Text
          x={PADDING}
          y={8}
          text={`${label} (${wallLen.toFixed(2)}m)`}
          fontSize={11 * fontScale}
          fontStyle="bold"
          fill="#334155"
        />

        </Group>

        {/* Placement mode indicator (fixed on screen, outside pan group) */}
        {isPlacementMode && (
          <Text
            x={PADDING}
            y={height - 20}
            text={t('projection.clickToPlace')}
            fontSize={9}
            fill="#2563eb"
            opacity={0.7}
          />
        )}
      </Layer>
    </Stage>
  );
}
