import {
  createContext,
  useContext,
  useReducer,
  useCallback,
  useMemo,
  useRef,
  type ReactNode,
} from 'react';
import type {
  RoomFull,
  Wall,
  WallOpening,
  ElectricalItem,
  FurnitureItem,
  Point,
  Annotation,
} from '@house-plan-maker/shared';
import type { EditorState, EditorAction, EditorToolType, LayerVisibility, PastePayload } from '../types';
import { DEFAULT_ZOOM, DEFAULT_GRID_SIZE } from '../types';
import { wallsFromShape } from '../utils/wallUtils';
import { generateLocalId } from '../utils/geometry';

// ── Reducer ──

function assignWallDirection(w: { startX: number; startY: number; endX: number; endY: number }, roomWidth: number, roomHeight: number): Wall['direction'] {
  const dx = w.endX - w.startX;
  const dy = w.endY - w.startY;
  const isHorizontal = Math.abs(dx) > Math.abs(dy);
  if (isHorizontal) {
    return w.startY < roomHeight / 2 ? 'NORTH' : 'SOUTH';
  }
  return w.startX < roomWidth / 2 ? 'WEST' : 'EAST';
}

function generateWallsFromShape(room: RoomFull): readonly Wall[] {
  return wallsFromShape(room.shape).map((w) => ({
    ...w,
    id: generateLocalId(),
    roomId: room.id,
    direction: assignWallDirection(w, room.width ?? 0, room.height ?? 0),
  }));
}

function wallsMatchShape(existingWalls: readonly Wall[], shape: readonly Point[]): boolean {
  const shapeWalls = wallsFromShape(shape);
  if (existingWalls.length !== shapeWalls.length) return false;
  return shapeWalls.every((sw, i) => {
    const ew = existingWalls[i];
    return ew &&
      Math.abs(ew.startX - sw.startX) < 0.001 &&
      Math.abs(ew.startY - sw.startY) < 0.001 &&
      Math.abs(ew.endX - sw.endX) < 0.001 &&
      Math.abs(ew.endY - sw.endY) < 0.001;
  });
}

function createInitialState(room: RoomFull): EditorState {
  // Auto-generate walls if none exist or if they don't match the room shape
  const existingMatch = room.walls.length > 0 && wallsMatchShape(room.walls, room.shape);
  const walls = existingMatch ? room.walls : generateWallsFromShape(room);
  return {
    room,
    walls,
    openings: existingMatch ? room.openings : [],
    electricalItems: room.electricalItems,
    furnitureItems: room.furnitureItems,
    selectedIds: new Set(),
    activeTool: 'select',
    zoom: DEFAULT_ZOOM,
    panOffset: { x: 0, y: 0 },
    gridSize: DEFAULT_GRID_SIZE,
    gridVisible: true,
    snapEnabled: true,
    snapGranularity: DEFAULT_GRID_SIZE,
    layerVisibility: { walls: true, electrical: true, furniture: true, measurements: true, annotations: true, stretchCeiling: true },
    selectedElectricalIndex: null,
    selectedFurnitureIndex: null,
    annotations: room.annotations ?? [],
    furnitureProjectionIds: new Set(),
    globalFurnitureOpacity: 1,
  };
}

function editorReducer(state: EditorState, action: EditorAction): EditorState {
  switch (action.type) {
    case 'SET_ROOM': {
      const room = action.room;
      const existingMatch = room.walls.length > 0 && wallsMatchShape(room.walls, room.shape);
      const walls = existingMatch ? room.walls : generateWallsFromShape(room);
      return {
        ...state,
        room,
        walls,
        openings: existingMatch ? room.openings : [],
        electricalItems: room.electricalItems,
        furnitureItems: room.furnitureItems,
        annotations: room.annotations ?? state.annotations,
      };
    }
    case 'UPDATE_ROOM_PROPS':
      return { ...state, room: { ...state.room, ...action.props } };
    case 'SET_WALLS':
      return { ...state, walls: action.walls };
    case 'UPDATE_WALL':
      return {
        ...state,
        walls: state.walls.map((w) =>
          w.id === action.wall.id ? action.wall : w,
        ),
      };
    case 'ADD_OPENING':
      return { ...state, openings: [...state.openings, action.opening] };
    case 'UPDATE_OPENING':
      return {
        ...state,
        openings: state.openings.map((o) =>
          o.id === action.opening.id ? action.opening : o,
        ),
      };
    case 'REMOVE_OPENING':
      return {
        ...state,
        openings: state.openings.filter((o) => o.id !== action.id),
        selectedIds: removeFromSet(state.selectedIds, action.id),
      };
    case 'ADD_ELECTRICAL':
      return {
        ...state,
        electricalItems: [...state.electricalItems, action.item],
      };
    case 'UPDATE_ELECTRICAL':
      return {
        ...state,
        electricalItems: state.electricalItems.map((i) =>
          i.id === action.item.id ? action.item : i,
        ),
      };
    case 'REMOVE_ELECTRICAL':
      return {
        ...state,
        electricalItems: state.electricalItems.filter((i) => i.id !== action.id),
        selectedIds: removeFromSet(state.selectedIds, action.id),
      };
    case 'ADD_FURNITURE':
      return {
        ...state,
        furnitureItems: [...state.furnitureItems, action.item],
      };
    case 'UPDATE_FURNITURE':
      return {
        ...state,
        furnitureItems: state.furnitureItems.map((i) =>
          i.id === action.item.id ? action.item : i,
        ),
      };
    case 'REMOVE_FURNITURE':
      return {
        ...state,
        furnitureItems: state.furnitureItems.filter((i) => i.id !== action.id),
        selectedIds: removeFromSet(state.selectedIds, action.id),
      };
    case 'SET_SELECTED':
      return { ...state, selectedIds: action.ids };
    case 'ADD_TO_SELECTION':
      return {
        ...state,
        selectedIds: addToSet(state.selectedIds, action.id),
      };
    case 'REMOVE_FROM_SELECTION':
      return {
        ...state,
        selectedIds: removeFromSet(state.selectedIds, action.id),
      };
    case 'CLEAR_SELECTION':
      return { ...state, selectedIds: new Set() };
    case 'SET_TOOL':
      return { ...state, activeTool: action.tool, selectedIds: new Set() };
    case 'SET_ZOOM':
      return { ...state, zoom: action.zoom };
    case 'SET_PAN_OFFSET':
      return { ...state, panOffset: action.offset };
    case 'SET_VIEW':
      return { ...state, zoom: action.zoom, panOffset: action.offset };
    case 'SET_GRID_SIZE':
      return { ...state, gridSize: action.gridSize };
    case 'TOGGLE_GRID':
      return { ...state, gridVisible: !state.gridVisible };
    case 'TOGGLE_SNAP':
      return { ...state, snapEnabled: !state.snapEnabled };
    case 'SET_SNAP_GRANULARITY':
      return { ...state, snapGranularity: action.granularity };
    case 'TOGGLE_LAYER':
      return {
        ...state,
        layerVisibility: {
          ...state.layerVisibility,
          [action.layer]: !state.layerVisibility[action.layer],
        },
      };
    case 'SET_ELECTRICAL_INDEX':
      return { ...state, selectedElectricalIndex: action.index };
    case 'SET_FURNITURE_INDEX':
      return { ...state, selectedFurnitureIndex: action.index };
    case 'DELETE_SELECTED': {
      return {
        ...state,
        openings: state.openings.filter((o) => !state.selectedIds.has(o.id)),
        electricalItems: state.electricalItems.filter((e) => !state.selectedIds.has(e.id)),
        furnitureItems: state.furnitureItems.filter((f) => !state.selectedIds.has(f.id)),
        annotations: state.annotations.filter((a) => !state.selectedIds.has(a.id)),
        selectedIds: new Set(),
      };
    }
    case 'SELECT_ALL': {
      const allIds = new Set<string>();
      for (const o of state.openings) allIds.add(o.id);
      for (const e of state.electricalItems) allIds.add(e.id);
      for (const f of state.furnitureItems) allIds.add(f.id);
      for (const a of state.annotations) allIds.add(a.id);
      return { ...state, selectedIds: allIds };
    }
    // ── Clipboard ──
    case 'COPY_SELECTED':
      // No state change — clipboard is handled via ref in the provider
      return state;
    case 'PASTE_CLIPBOARD': {
      const { items } = action;
      return {
        ...state,
        openings: [...state.openings, ...items.openings],
        electricalItems: [...state.electricalItems, ...items.electricalItems],
        furnitureItems: [...state.furnitureItems, ...items.furnitureItems],
        annotations: [...state.annotations, ...items.annotations],
        selectedIds: items.newSelectedIds,
      };
    }
    // ── Alignment ──
    case 'ALIGN_SELECTED': {
      if (state.selectedIds.size < 2) return state;
      return applyAlignment(state, action.alignment);
    }
    // ── Annotations ──
    case 'ADD_ANNOTATION':
      return { ...state, annotations: [...state.annotations, action.annotation] };
    case 'UPDATE_ANNOTATION':
      return {
        ...state,
        annotations: state.annotations.map((a) =>
          a.id === action.annotation.id ? action.annotation : a,
        ),
      };
    case 'REMOVE_ANNOTATION':
      return {
        ...state,
        annotations: state.annotations.filter((a) => a.id !== action.id),
        selectedIds: removeFromSet(state.selectedIds, action.id),
      };
    case 'TOGGLE_FURNITURE_PROJECTION': {
      const next = new Set(state.furnitureProjectionIds);
      if (next.has(action.id)) next.delete(action.id);
      else next.add(action.id);
      return { ...state, furnitureProjectionIds: next };
    }
    case 'SET_GLOBAL_FURNITURE_OPACITY': {
      const clamped = Math.min(1, Math.max(0, action.opacity));
      return { ...state, globalFurnitureOpacity: clamped };
    }
    // ── Import ──
    case 'SYNC_SAVE': {
      // Build set of all new IDs so we can prune any selection that did not survive
      const newIds = new Set<string>();
      for (const w of action.walls) newIds.add(w.id);
      for (const o of action.openings) newIds.add(o.id);
      for (const e of action.electricalItems) newIds.add(e.id);
      for (const f of action.furnitureItems) newIds.add(f.id);
      // Remap selected IDs through the id map (so freshly created server items
      // stay selected). Fall back to the original id when no mapping is given.
      const idMap = action.idMap;
      const remappedSelection = new Set<string>();
      for (const id of state.selectedIds) {
        const next = idMap?.get(id) ?? id;
        if (newIds.has(next)) remappedSelection.add(next);
      }
      // Use server annotations when provided; otherwise just remap attached ids
      // for the existing client-only annotation list.
      let remappedAnnotations = action.annotations
        ? [...action.annotations]
        : state.annotations;
      if (idMap) {
        remappedAnnotations = remappedAnnotations.map((a) =>
          a.attachedToId && idMap.has(a.attachedToId)
            ? { ...a, attachedToId: idMap.get(a.attachedToId)! }
            : a,
        );
      }
      return {
        ...state,
        walls: action.walls,
        openings: action.openings,
        electricalItems: action.electricalItems,
        furnitureItems: action.furnitureItems,
        selectedIds: remappedSelection,
        annotations: remappedAnnotations,
      };
    }
    case 'IMPORT_ROOM':
      return {
        ...state,
        room: {
          ...state.room,
          name: action.room.name,
          shape: action.room.shape,
          wallHeight: action.room.wallHeight,
          plinthHeight: action.room.plinthHeight,
          plinthThickness: action.room.plinthThickness,
        },
        walls: action.walls,
        openings: action.openings,
        electricalItems: action.electricalItems,
        furnitureItems: action.furnitureItems,
        annotations: [],
        selectedIds: new Set(),
      };
    default:
      return state;
  }
}

function addToSet(set: ReadonlySet<string>, id: string): ReadonlySet<string> {
  const next = new Set(set);
  next.add(id);
  return next;
}

function removeFromSet(set: ReadonlySet<string>, id: string): ReadonlySet<string> {
  if (!set.has(id)) return set;
  const next = new Set(set);
  next.delete(id);
  return next;
}

// ── Alignment helpers ──

interface PositionedItem {
  readonly id: string;
  readonly x: number;
  readonly y: number;
}

function getSelectedPositionedItems(state: EditorState): readonly PositionedItem[] {
  const items: PositionedItem[] = [];
  for (const id of state.selectedIds) {
    const elec = state.electricalItems.find((e) => e.id === id);
    if (elec) { items.push({ id: elec.id, x: elec.x, y: elec.y }); continue; }
    const furn = state.furnitureItems.find((f) => f.id === id);
    if (furn) { items.push({ id: furn.id, x: furn.x, y: furn.y }); continue; }
    const ann = state.annotations.find((a) => a.id === id);
    if (ann) { items.push({ id: ann.id, x: ann.x, y: ann.y }); continue; }
  }
  return items;
}

function applyAlignment(state: EditorState, alignment: string): EditorState {
  const items = getSelectedPositionedItems(state);
  if (items.length < 2) return state;

  const offsets = new Map<string, { dx: number; dy: number }>();

  switch (alignment) {
    case 'left': {
      const minX = Math.min(...items.map((i) => i.x));
      for (const item of items) offsets.set(item.id, { dx: minX - item.x, dy: 0 });
      break;
    }
    case 'right': {
      const maxX = Math.max(...items.map((i) => i.x));
      for (const item of items) offsets.set(item.id, { dx: maxX - item.x, dy: 0 });
      break;
    }
    case 'top': {
      const minY = Math.min(...items.map((i) => i.y));
      for (const item of items) offsets.set(item.id, { dx: 0, dy: minY - item.y });
      break;
    }
    case 'bottom': {
      const maxY = Math.max(...items.map((i) => i.y));
      for (const item of items) offsets.set(item.id, { dx: 0, dy: maxY - item.y });
      break;
    }
    case 'center-h': {
      const avgX = items.reduce((s, i) => s + i.x, 0) / items.length;
      for (const item of items) offsets.set(item.id, { dx: avgX - item.x, dy: 0 });
      break;
    }
    case 'center-v': {
      const avgY = items.reduce((s, i) => s + i.y, 0) / items.length;
      for (const item of items) offsets.set(item.id, { dx: 0, dy: avgY - item.y });
      break;
    }
    case 'distribute-h': {
      const sorted = [...items].sort((a, b) => a.x - b.x);
      if (sorted.length < 3) break;
      const minX = sorted[0].x;
      const maxX = sorted[sorted.length - 1].x;
      const step = (maxX - minX) / (sorted.length - 1);
      for (let i = 0; i < sorted.length; i++) {
        offsets.set(sorted[i].id, { dx: minX + step * i - sorted[i].x, dy: 0 });
      }
      break;
    }
    case 'distribute-v': {
      const sorted = [...items].sort((a, b) => a.y - b.y);
      if (sorted.length < 3) break;
      const minY = sorted[0].y;
      const maxY = sorted[sorted.length - 1].y;
      const step = (maxY - minY) / (sorted.length - 1);
      for (let i = 0; i < sorted.length; i++) {
        offsets.set(sorted[i].id, { dx: 0, dy: minY + step * i - sorted[i].y });
      }
      break;
    }
  }

  if (offsets.size === 0) return state;

  const applyOffset = <T extends { readonly id: string; readonly x: number; readonly y: number }>(
    list: readonly T[],
  ): readonly T[] =>
    list.map((item) => {
      const o = offsets.get(item.id);
      if (!o || (o.dx === 0 && o.dy === 0)) return item;
      return { ...item, x: item.x + o.dx, y: item.y + o.dy };
    });

  return {
    ...state,
    electricalItems: applyOffset(state.electricalItems),
    furnitureItems: applyOffset(state.furnitureItems),
    annotations: applyOffset(state.annotations),
  };
}

// ── ZoomPan Context ──

interface ZoomPanContextValue {
  readonly zoom: number;
  readonly panOffset: Point;
  setZoom(zoom: number): void;
  setPanOffset(offset: Point): void;
}

const ZoomPanContext = createContext<ZoomPanContextValue | null>(null);

// ── Selection Context ──

interface SelectionContextValue {
  readonly selectedIds: ReadonlySet<string>;
  readonly activeTool: EditorToolType;
  readonly dispatch: (action: EditorAction) => void;
  setTool(tool: EditorToolType): void;
  selectElement(id: string, addToSelection?: boolean): void;
  clearSelection(): void;
  deleteSelected(): void;
  selectAll(): void;
}

const SelectionContext = createContext<SelectionContextValue | null>(null);

// ── SceneData Context ──

interface SceneDataContextValue {
  readonly room: RoomFull;
  readonly walls: readonly Wall[];
  readonly openings: readonly WallOpening[];
  readonly electricalItems: readonly ElectricalItem[];
  readonly furnitureItems: readonly FurnitureItem[];
  readonly annotations: readonly Annotation[];
  readonly furnitureProjectionIds: ReadonlySet<string>;
  readonly globalFurnitureOpacity: number;
  readonly gridSize: number;
  readonly gridVisible: boolean;
  readonly snapEnabled: boolean;
  readonly snapGranularity: number;
  readonly layerVisibility: LayerVisibility;
  readonly selectedElectricalIndex: number | null;
  readonly selectedFurnitureIndex: number | null;
  readonly dispatch: (action: EditorAction) => void;
  setWalls(walls: readonly Wall[]): void;
  updateWall(wall: Wall): void;
  addOpening(opening: WallOpening): void;
  updateOpening(opening: WallOpening): void;
  removeOpening(id: string): void;
  addElectrical(item: ElectricalItem): void;
  updateElectrical(item: ElectricalItem): void;
  removeElectrical(id: string): void;
  addFurniture(item: FurnitureItem): void;
  updateFurniture(item: FurnitureItem): void;
  removeFurniture(id: string): void;
  addAnnotation(annotation: Annotation): void;
  updateAnnotation(annotation: Annotation): void;
  removeAnnotation(id: string): void;
  toggleFurnitureProjection(id: string): void;
  copySelected(): void;
  pasteClipboard(): void;
}

const SceneDataContext = createContext<SceneDataContextValue | null>(null);

// ── Legacy combined context (for useEditor backward compat) ──

interface EditorContextValue {
  readonly state: EditorState;
  readonly dispatch: (action: EditorAction) => void;
  setTool(tool: EditorToolType): void;
  setZoom(zoom: number): void;
  setPanOffset(offset: Point): void;
  selectElement(id: string, addToSelection?: boolean): void;
  clearSelection(): void;
  deleteSelected(): void;
  selectAll(): void;
  addOpening(opening: WallOpening): void;
  updateOpening(opening: WallOpening): void;
  removeOpening(id: string): void;
  setWalls(walls: readonly Wall[]): void;
  updateWall(wall: Wall): void;
  addElectrical(item: ElectricalItem): void;
  updateElectrical(item: ElectricalItem): void;
  removeElectrical(id: string): void;
  addFurniture(item: FurnitureItem): void;
  updateFurniture(item: FurnitureItem): void;
  removeFurniture(id: string): void;
  addAnnotation(annotation: Annotation): void;
  updateAnnotation(annotation: Annotation): void;
  removeAnnotation(id: string): void;
  toggleFurnitureProjection(id: string): void;
  copySelected(): void;
  pasteClipboard(): void;
}

const EditorContext = createContext<EditorContextValue | null>(null);

// ── Provider ──

interface EditorProviderProps {
  readonly room: RoomFull;
  readonly children: ReactNode;
}

export function EditorProvider({ room, children }: EditorProviderProps) {
  const [state, dispatch] = useReducer(editorReducer, room, createInitialState);

  // ── Stable callbacks (dispatch never changes) ──

  const setTool = useCallback(
    (tool: EditorToolType) => dispatch({ type: 'SET_TOOL', tool }),
    [],
  );

  const setZoom = useCallback(
    (zoom: number) => dispatch({ type: 'SET_ZOOM', zoom }),
    [],
  );

  const setPanOffset = useCallback(
    (offset: Point) => dispatch({ type: 'SET_PAN_OFFSET', offset }),
    [],
  );

  const selectElement = useCallback(
    (id: string, addToSelection = false) => {
      if (addToSelection) {
        dispatch({ type: 'ADD_TO_SELECTION', id });
      } else {
        dispatch({ type: 'SET_SELECTED', ids: new Set([id]) });
      }
    },
    [],
  );

  const clearSelection = useCallback(
    () => dispatch({ type: 'CLEAR_SELECTION' }),
    [],
  );

  // Task 6: dispatch-only callbacks — no closure over state arrays
  const deleteSelected = useCallback(
    () => dispatch({ type: 'DELETE_SELECTED' }),
    [],
  );

  const selectAll = useCallback(
    () => dispatch({ type: 'SELECT_ALL' }),
    [],
  );

  const addOpening = useCallback(
    (opening: WallOpening) => dispatch({ type: 'ADD_OPENING', opening }),
    [],
  );
  const updateOpening = useCallback(
    (opening: WallOpening) => dispatch({ type: 'UPDATE_OPENING', opening }),
    [],
  );
  const removeOpening = useCallback(
    (id: string) => dispatch({ type: 'REMOVE_OPENING', id }),
    [],
  );
  const setWalls = useCallback(
    (walls: readonly Wall[]) => dispatch({ type: 'SET_WALLS', walls }),
    [],
  );
  const updateWall = useCallback(
    (wall: Wall) => dispatch({ type: 'UPDATE_WALL', wall }),
    [],
  );
  const addElectrical = useCallback(
    (item: ElectricalItem) => dispatch({ type: 'ADD_ELECTRICAL', item }),
    [],
  );
  const updateElectrical = useCallback(
    (item: ElectricalItem) => dispatch({ type: 'UPDATE_ELECTRICAL', item }),
    [],
  );
  const removeElectrical = useCallback(
    (id: string) => dispatch({ type: 'REMOVE_ELECTRICAL', id }),
    [],
  );
  const addFurniture = useCallback(
    (item: FurnitureItem) => dispatch({ type: 'ADD_FURNITURE', item }),
    [],
  );
  const updateFurniture = useCallback(
    (item: FurnitureItem) => dispatch({ type: 'UPDATE_FURNITURE', item }),
    [],
  );
  const removeFurniture = useCallback(
    (id: string) => dispatch({ type: 'REMOVE_FURNITURE', id }),
    [],
  );
  const addAnnotation = useCallback(
    (annotation: Annotation) => dispatch({ type: 'ADD_ANNOTATION', annotation }),
    [],
  );
  const updateAnnotation = useCallback(
    (annotation: Annotation) => dispatch({ type: 'UPDATE_ANNOTATION', annotation }),
    [],
  );
  const removeAnnotation = useCallback(
    (id: string) => dispatch({ type: 'REMOVE_ANNOTATION', id }),
    [],
  );
  const toggleFurnitureProjection = useCallback(
    (id: string) => dispatch({ type: 'TOGGLE_FURNITURE_PROJECTION', id }),
    [],
  );

  // ── Clipboard (ref-based so copy reads current state without closures) ──
  const clipboardRef = useRef<{
    openings: readonly WallOpening[];
    electricalItems: readonly ElectricalItem[];
    furnitureItems: readonly FurnitureItem[];
    annotations: readonly Annotation[];
  }>({ openings: [], electricalItems: [], furnitureItems: [], annotations: [] });

  const stateRef = useRef(state);
  stateRef.current = state;

  const copySelected = useCallback(() => {
    const s = stateRef.current;
    const ids = s.selectedIds;
    if (ids.size === 0) return;
    clipboardRef.current = {
      openings: s.openings.filter((o) => ids.has(o.id)).map((o) => ({ ...o })),
      electricalItems: s.electricalItems.filter((e) => ids.has(e.id)).map((e) => ({ ...e })),
      furnitureItems: s.furnitureItems.filter((f) => ids.has(f.id)).map((f) => ({ ...f })),
      annotations: s.annotations.filter((a) => ids.has(a.id)).map((a) => ({ ...a })),
    };
  }, []);

  const pasteClipboard = useCallback(() => {
    const cb = clipboardRef.current;
    const hasItems =
      cb.openings.length > 0 ||
      cb.electricalItems.length > 0 ||
      cb.furnitureItems.length > 0 ||
      cb.annotations.length > 0;
    if (!hasItems) return;

    const PASTE_OFFSET = 0.2;
    const newSelectedIds = new Set<string>();

    const newOpenings = cb.openings.map((o) => {
      const newId = generateLocalId();
      newSelectedIds.add(newId);
      return { ...o, id: newId, positionAlongWall: o.positionAlongWall + PASTE_OFFSET };
    });
    const newElectrical = cb.electricalItems.map((e) => {
      const newId = generateLocalId();
      newSelectedIds.add(newId);
      return { ...e, id: newId, x: e.x + PASTE_OFFSET, y: e.y + PASTE_OFFSET };
    });
    const newFurniture = cb.furnitureItems.map((f) => {
      const newId = generateLocalId();
      newSelectedIds.add(newId);
      return { ...f, id: newId, x: f.x + PASTE_OFFSET, y: f.y + PASTE_OFFSET };
    });
    const newAnnotations = cb.annotations.map((a) => {
      const newId = generateLocalId();
      newSelectedIds.add(newId);
      return { ...a, id: newId, x: a.x + PASTE_OFFSET, y: a.y + PASTE_OFFSET };
    });

    const items: PastePayload = {
      openings: newOpenings,
      electricalItems: newElectrical,
      furnitureItems: newFurniture,
      annotations: newAnnotations,
      newSelectedIds,
    };
    dispatch({ type: 'PASTE_CLIPBOARD', items });
  }, []);

  // ── ZoomPan context value (changes only on zoom/panOffset) ──
  const zoomPanValue = useMemo<ZoomPanContextValue>(
    () => ({ zoom: state.zoom, panOffset: state.panOffset, setZoom, setPanOffset }),
    [state.zoom, state.panOffset, setZoom, setPanOffset],
  );

  // ── Selection context value (changes only on selection/tool) ──
  const selectionValue = useMemo<SelectionContextValue>(
    () => ({
      selectedIds: state.selectedIds,
      activeTool: state.activeTool,
      dispatch,
      setTool,
      selectElement,
      clearSelection,
      deleteSelected,
      selectAll,
    }),
    [state.selectedIds, state.activeTool, setTool, selectElement, clearSelection, deleteSelected, selectAll],
  );

  // ── SceneData context value (changes only on data edits) ──
  const sceneDataValue = useMemo<SceneDataContextValue>(
    () => ({
      room: state.room,
      walls: state.walls,
      openings: state.openings,
      electricalItems: state.electricalItems,
      furnitureItems: state.furnitureItems,
      annotations: state.annotations,
      furnitureProjectionIds: state.furnitureProjectionIds,
      globalFurnitureOpacity: state.globalFurnitureOpacity,
      gridSize: state.gridSize,
      gridVisible: state.gridVisible,
      snapEnabled: state.snapEnabled,
      snapGranularity: state.snapGranularity,
      layerVisibility: state.layerVisibility,
      selectedElectricalIndex: state.selectedElectricalIndex,
      selectedFurnitureIndex: state.selectedFurnitureIndex,
      dispatch,
      setWalls,
      updateWall,
      addOpening,
      updateOpening,
      removeOpening,
      addElectrical,
      updateElectrical,
      removeElectrical,
      addFurniture,
      updateFurniture,
      removeFurniture,
      addAnnotation,
      updateAnnotation,
      removeAnnotation,
      toggleFurnitureProjection,
      copySelected,
      pasteClipboard,
    }),
    [
      state.room,
      state.walls,
      state.openings,
      state.electricalItems,
      state.furnitureItems,
      state.annotations,
      state.furnitureProjectionIds,
      state.globalFurnitureOpacity,
      state.gridSize,
      state.gridVisible,
      state.snapEnabled,
      state.snapGranularity,
      state.layerVisibility,
      state.selectedElectricalIndex,
      state.selectedFurnitureIndex,
      setWalls,
      updateWall,
      addOpening,
      updateOpening,
      removeOpening,
      addElectrical,
      updateElectrical,
      removeElectrical,
      addFurniture,
      updateFurniture,
      removeFurniture,
      addAnnotation,
      updateAnnotation,
      removeAnnotation,
      toggleFurnitureProjection,
      copySelected,
      pasteClipboard,
    ],
  );

  // ── Legacy combined value ──
  const legacyValue = useMemo<EditorContextValue>(
    () => ({
      state,
      dispatch,
      setTool,
      setZoom,
      setPanOffset,
      selectElement,
      clearSelection,
      deleteSelected,
      selectAll,
      addOpening,
      updateOpening,
      removeOpening,
      setWalls,
      updateWall,
      addElectrical,
      updateElectrical,
      removeElectrical,
      addFurniture,
      updateFurniture,
      removeFurniture,
      addAnnotation,
      updateAnnotation,
      removeAnnotation,
      toggleFurnitureProjection,
      copySelected,
      pasteClipboard,
    }),
    [
      state,
      setTool,
      setZoom,
      setPanOffset,
      selectElement,
      clearSelection,
      deleteSelected,
      selectAll,
      addOpening,
      updateOpening,
      removeOpening,
      setWalls,
      updateWall,
      addElectrical,
      updateElectrical,
      removeElectrical,
      addFurniture,
      updateFurniture,
      removeFurniture,
      addAnnotation,
      updateAnnotation,
      removeAnnotation,
      copySelected,
      pasteClipboard,
    ],
  );

  return (
    <EditorContext.Provider value={legacyValue}>
      <ZoomPanContext.Provider value={zoomPanValue}>
        <SelectionContext.Provider value={selectionValue}>
          <SceneDataContext.Provider value={sceneDataValue}>
            {children}
          </SceneDataContext.Provider>
        </SelectionContext.Provider>
      </ZoomPanContext.Provider>
    </EditorContext.Provider>
  );
}

// ── Hooks ──

/** Legacy hook -- combines all contexts. Use granular hooks for optimal re-render perf. */
export function useEditor(): EditorContextValue {
  const ctx = useContext(EditorContext);
  if (!ctx) {
    throw new Error('useEditor must be used within an EditorProvider');
  }
  return ctx;
}

/** Granular hook for zoom/pan state (60fps changes). */
export function useZoomPan(): ZoomPanContextValue {
  const ctx = useContext(ZoomPanContext);
  if (!ctx) {
    throw new Error('useZoomPan must be used within an EditorProvider');
  }
  return ctx;
}

/** Granular hook for selection/tool state. */
export function useSelection(): SelectionContextValue {
  const ctx = useContext(SelectionContext);
  if (!ctx) {
    throw new Error('useSelection must be used within an EditorProvider');
  }
  return ctx;
}

/** Granular hook for scene data (walls, openings, items, grid, layers). */
export function useSceneData(): SceneDataContextValue {
  const ctx = useContext(SceneDataContext);
  if (!ctx) {
    throw new Error('useSceneData must be used within an EditorProvider');
  }
  return ctx;
}
